From 5b95ff07040944baa4988f4faa00cb410d22c6fb Mon Sep 17 00:00:00 2001 From: cylewaitforit Date: Fri, 10 Apr 2026 16:37:08 -0500 Subject: [PATCH 1/4] docs(ui): add stories for Brand page --- .storybook/main.ts | 112 +++++++++++++++++++++++++++++++------ app/pages/brand.stories.ts | 16 ++++++ 2 files changed, 110 insertions(+), 18 deletions(-) create mode 100644 app/pages/brand.stories.ts diff --git a/.storybook/main.ts b/.storybook/main.ts index 31dc739ca8..bd5ece12fd 100644 --- a/.storybook/main.ts +++ b/.storybook/main.ts @@ -1,4 +1,6 @@ import type { StorybookConfig } from '@storybook-vue/nuxt' +import { readFileSync } from 'node:fs' +import { resolve } from 'node:path' const config = { stories: [ @@ -21,29 +23,98 @@ const config = { async viteFinal(newConfig) { newConfig.plugins ??= [] + // Fix: nuxt:components:imports-alias relies on internal Nuxt state that is + // cleaned up after nuxt.close() in @storybook-vue/nuxt's loadNuxtViteConfig. + // When that state is gone, `import X from '#components'` is left unresolved + // and Vite 8 falls through to package-subpath resolution, which fails with + // "Missing '#components' specifier in 'nuxt' package". + // This plugin intercepts #components first and serves a virtual module built + // from the components.d.ts written during the same Nuxt boot. + // Resolve the Nuxt build dir from Vite's alias map, which can be either a + // plain-object (Record) or Vite's resolved array form + // (readonly Alias[] where find is string | RegExp). We must handle both + // without casting to Record, which would be unsound for the + // array form. + const aliases = newConfig.resolve?.alias + const buildDir = (() => { + if (!aliases) return undefined + if (Array.isArray(aliases)) { + const entry = aliases.find(a => a.find === '#build') + return typeof entry?.replacement === 'string' ? entry.replacement : undefined + } + const value = (aliases as Record)['#build'] + return typeof value === 'string' ? value : undefined + })() + newConfig.plugins.unshift({ + name: 'storybook-nuxt-components', + enforce: 'pre', + resolveId(id) { + if (id === '#components') return '\0virtual:#components' + return null + }, + load(id) { + if (id !== '\0virtual:#components') return + if (!buildDir) return 'export {}' + const dtsPath = resolve(buildDir, 'components.d.ts') + // Wire the generated declaration file into Vite's file-watch graph so + // that the virtual module is invalidated when Nuxt regenerates it. + this.addWatchFile(dtsPath) + try { + const dts = readFileSync(dtsPath, 'utf-8') + const lines: string[] = [] + // Match only the direct `typeof import("…").default` form. + // Lazy/island wrappers (LazyComponent, IslandComponent) are + // excluded intentionally — Storybook only needs the concrete type. + // The format has been stable across all Nuxt 3 releases; if it ever + // changes, the exports array will simply be empty and Storybook will + // fall back to direct imports from `~/components`. + const re = /^export const (\w+): typeof import\("([^"]+)"\)\.default$/gm + let match: RegExpExecArray | null + while ((match = re.exec(dts)) !== null) { + const [, name, rel] = match + if (!name || !rel) continue + const abs = resolve(buildDir, rel) + lines.push(`export { default as ${name} } from ${JSON.stringify(abs)}`) + } + return lines.join('\n') || 'export {}' + } catch (err) { + // oxlint-disable-next-line no-console -- Log and swallow errors to avoid breaking the Storybook build when components.d.ts is missing or malformed. + console.warn( + '[storybook-nuxt-components] Failed to build #components virtual module:', + err, + ) + return 'export {}' + } + }, + }) + // Bridge compatibility between Storybook v10 core and v9 @storybook-vue/nuxt // v10 expects module federation globals that v9 doesn't provide newConfig.plugins.push({ name: 'storybook-v10-compat', transformIndexHtml: { order: 'pre', - handler(html) { - const script = ` -` - return html.replace(/