Files
HMDesign/.storybook/main.ts
T
shkim4 2e203627c5 fix(storybook): type the font path plugin as a Vite Plugin
this.warn() in the hm-ui-theme-font-path transform failed tsc -b with
TS2339 because the untyped object literal made `this` the literal itself.
Annotate it as Vite's Plugin so `this` is the plugin context. Font URL
replacement and the warning behavior are unchanged.
2026-11-09 19:35:35 +09:00

91 lines
4.9 KiB
TypeScript

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/<outside path> 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}<link rel="icon" href="data:," /><style id="hm-manager-style">${readFileSync(new URL("./manager.css", import.meta.url), "utf8")}</style>`,
// 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