import type { StorybookConfig } from "@storybook/react-vite" import { readFileSync, realpathSync } from "node:fs" import { fileURLToPath } from "node:url" import path from "node:path" import type { Plugin } from "vite" // Checkouts that share another checkout's node_modules (a linked git worktree whose node_modules is a junction) also // inherit that checkout's node_modules/@hm-design/* links. Those links point at the other checkout's packages/ui and its // compiled dist, so Storybook would show the other checkout's tokens (a stale palette) instead of this checkout's. // Only in that case, resolve the five @hm-design/ui entry points to this checkout's own source. Nothing is built, the // other checkout's node_modules and dist are only read, and a checkout whose links already point at its own // packages/ui keeps the unchanged dist-based resolution. const checkoutRoot = fileURLToPath(new URL("../", import.meta.url)) const localUiRoot = path.join(checkoutRoot, "packages/ui") function uiLinkedElsewhere() { try { return realpathSync(path.join(checkoutRoot, "node_modules/@hm-design/ui")) !== realpathSync(localUiRoot) } catch { return false } } const localUiAliases = [ [/^@hm-design\/ui$/, "src/index.ts"], [/^@hm-design\/ui\/theme$/, "src/theme/index.ts"], [/^@hm-design\/ui\/theme\.css$/, "src/theme.css"], [/^@hm-design\/ui\/tailwind\.css$/, "src/tailwind.css"], [/^@hm-design\/ui\/dashboard-status\.css$/, "src/patterns/dashboard-status/dashboard-status.css"], ].map(([find, file]) => ({ find: find as RegExp, replacement: path.join(localUiRoot, file as string) })) // @hm-design/ui's dist/theme.css declares the bundled font with a relative URL. When node_modules/@hm-design/ui is // symlinked to a checkout outside this Vite root, Vite rewrites that URL to /@fs/ and the dev preview // gets a 403 ("outside of Vite serving allow list"). Point only that font URL at the /fonts static directory the // Manager already uses, instead of widening server.fs.allow. theme.css is inlined through @import, so the rewrite // runs on the already-resolved CSS and is limited to the exact ".../ui/dist/fonts/NotoSansKR-variable.ttf" URL. // Dev server only: the static build emits the font as a bundled asset. const UI_DIST_FONT_URL = /[^"'()\\\s]*\/ui\/dist\/fonts\/NotoSansKR-variable\.ttf/g const FONT_STATIC_URL = "/fonts/NotoSansKR-variable.ttf" const hmUiThemeFontPath: Plugin = { name: "hm-ui-theme-font-path", apply: "serve" as const, enforce: "post" as const, transform(code: string, id: string) { const file = id.split("?")[0] if (!file.endsWith(".css") || !code.includes("NotoSansKR-variable.ttf")) return null const next = code.replace(UI_DIST_FONT_URL, FONT_STATIC_URL) if (next === code) { if (!code.includes(FONT_STATIC_URL)) { this.warn(`[hm-ui-theme-font-path] ${file}: found NotoSansKR-variable.ttf but not at ".../ui/dist/fonts/"; the URL was left unchanged and Preview may fail to load Noto Sans KR.`) } return null } return { code: next, map: null } }, } const config: StorybookConfig = { stories: ["../src/**/*.stories.@(ts|tsx)"], addons: [], // In dev, Vite serves both raw /themes JSON and JSON module imports (?import). // Storybook's static middleware would intercept the imports with application/json. staticDirs: (_, { configType }) => [ { from: "../public/fonts", to: "/fonts" }, ...(configType === "PRODUCTION" ? [{ from: "../themes", to: "/themes" }] : []), ], managerHead: (head) => `${head}`, // changeDetection off: no "new and modified stories" filter or status badges in the sidebar. features: { sidebarOnboardingChecklist: false, changeDetection: false }, // Verification Stories (the same sections as a component's representative Story) stay out of the sidebar by default. // They keep their Story IDs and stay reachable by direct URL and the sidebar tag filter. tags: { verification: { defaultFilterSelection: "exclude" }, internal: { defaultFilterSelection: "exclude" } }, // Storybook replaces vite.config's `server`, so watcher excludes go here. // _local/ (gitignored work copies, ~20k files) would otherwise make the Vite watcher stall the dev server. viteFinal: (config) => { config.server ??= {} config.server.watch = { ...config.server.watch, ignored: ["**/_local/**", "**/storybook-static/**"] } config.plugins = [...(config.plugins ?? []), hmUiThemeFontPath] if (uiLinkedElsewhere()) { config.resolve ??= {} const existing = config.resolve.alias const current = Array.isArray(existing) ? existing : Object.entries(existing ?? {}).map(([find, replacement]) => ({ find, replacement })) config.resolve.alias = [...localUiAliases, ...current] } return config }, framework: "@storybook/react-vite", } export default config