Skip to content

Commit 5b95ff0

Browse files
committed
docs(ui): add stories for Brand page
1 parent f5bb3ed commit 5b95ff0

File tree

2 files changed

+110
-18
lines changed

2 files changed

+110
-18
lines changed

.storybook/main.ts

Lines changed: 94 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
import type { StorybookConfig } from '@storybook-vue/nuxt'
2+
import { readFileSync } from 'node:fs'
3+
import { resolve } from 'node:path'
24

35
const config = {
46
stories: [
@@ -21,29 +23,98 @@ const config = {
2123
async viteFinal(newConfig) {
2224
newConfig.plugins ??= []
2325

26+
// Fix: nuxt:components:imports-alias relies on internal Nuxt state that is
27+
// cleaned up after nuxt.close() in @storybook-vue/nuxt's loadNuxtViteConfig.
28+
// When that state is gone, `import X from '#components'` is left unresolved
29+
// and Vite 8 falls through to package-subpath resolution, which fails with
30+
// "Missing '#components' specifier in 'nuxt' package".
31+
// This plugin intercepts #components first and serves a virtual module built
32+
// from the components.d.ts written during the same Nuxt boot.
33+
// Resolve the Nuxt build dir from Vite's alias map, which can be either a
34+
// plain-object (Record<string, string>) or Vite's resolved array form
35+
// (readonly Alias[] where find is string | RegExp). We must handle both
36+
// without casting to Record<string, string>, which would be unsound for the
37+
// array form.
38+
const aliases = newConfig.resolve?.alias
39+
const buildDir = (() => {
40+
if (!aliases) return undefined
41+
if (Array.isArray(aliases)) {
42+
const entry = aliases.find(a => a.find === '#build')
43+
return typeof entry?.replacement === 'string' ? entry.replacement : undefined
44+
}
45+
const value = (aliases as Record<string, unknown>)['#build']
46+
return typeof value === 'string' ? value : undefined
47+
})()
48+
newConfig.plugins.unshift({
49+
name: 'storybook-nuxt-components',
50+
enforce: 'pre',
51+
resolveId(id) {
52+
if (id === '#components') return '\0virtual:#components'
53+
return null
54+
},
55+
load(id) {
56+
if (id !== '\0virtual:#components') return
57+
if (!buildDir) return 'export {}'
58+
const dtsPath = resolve(buildDir, 'components.d.ts')
59+
// Wire the generated declaration file into Vite's file-watch graph so
60+
// that the virtual module is invalidated when Nuxt regenerates it.
61+
this.addWatchFile(dtsPath)
62+
try {
63+
const dts = readFileSync(dtsPath, 'utf-8')
64+
const lines: string[] = []
65+
// Match only the direct `typeof import("…").default` form.
66+
// Lazy/island wrappers (LazyComponent<T>, IslandComponent<T>) are
67+
// excluded intentionally — Storybook only needs the concrete type.
68+
// The format has been stable across all Nuxt 3 releases; if it ever
69+
// changes, the exports array will simply be empty and Storybook will
70+
// fall back to direct imports from `~/components`.
71+
const re = /^export const (\w+): typeof import\("([^"]+)"\)\.default$/gm
72+
let match: RegExpExecArray | null
73+
while ((match = re.exec(dts)) !== null) {
74+
const [, name, rel] = match
75+
if (!name || !rel) continue
76+
const abs = resolve(buildDir, rel)
77+
lines.push(`export { default as ${name} } from ${JSON.stringify(abs)}`)
78+
}
79+
return lines.join('\n') || 'export {}'
80+
} catch (err) {
81+
// oxlint-disable-next-line no-console -- Log and swallow errors to avoid breaking the Storybook build when components.d.ts is missing or malformed.
82+
console.warn(
83+
'[storybook-nuxt-components] Failed to build #components virtual module:',
84+
err,
85+
)
86+
return 'export {}'
87+
}
88+
},
89+
})
90+
2491
// Bridge compatibility between Storybook v10 core and v9 @storybook-vue/nuxt
2592
// v10 expects module federation globals that v9 doesn't provide
2693
newConfig.plugins.push({
2794
name: 'storybook-v10-compat',
2895
transformIndexHtml: {
2996
order: 'pre',
30-
handler(html) {
31-
const script = `
32-
<script>
33-
// Minimal shims for Storybook v10 module federation system
34-
// These will be replaced when Storybook runtime loads
35-
window.__STORYBOOK_MODULE_GLOBAL__ = { global: window };
36-
window.__STORYBOOK_MODULE_CLIENT_LOGGER__ = {
37-
deprecate: console.warn.bind(console, '[deprecated]'),
38-
once: console.log.bind(console),
39-
logger: console
40-
};
41-
window.__STORYBOOK_MODULE_CHANNELS__ = {
42-
Channel: class { on() {} off() {} emit() {} once() {} },
43-
createBrowserChannel: () => new window.__STORYBOOK_MODULE_CHANNELS__.Channel()
44-
};
45-
</script>`
46-
return html.replace(/<script>/, script + '<script>')
97+
handler() {
98+
return [
99+
{
100+
tag: 'script',
101+
injectTo: 'head-prepend' as const,
102+
children: [
103+
'// Minimal shims for Storybook v10 module federation system',
104+
'// These will be replaced when Storybook runtime loads',
105+
'window.__STORYBOOK_MODULE_GLOBAL__ = { global: window };',
106+
'window.__STORYBOOK_MODULE_CLIENT_LOGGER__ = {',
107+
" deprecate: console.warn.bind(console, '[deprecated]'),",
108+
' once: console.log.bind(console),',
109+
' logger: console',
110+
'};',
111+
'window.__STORYBOOK_MODULE_CHANNELS__ = {',
112+
' Channel: class { on() {} off() {} emit() {} once() {} },',
113+
' createBrowserChannel: () => new window.__STORYBOOK_MODULE_CHANNELS__.Channel()',
114+
'};',
115+
].join('\n'),
116+
},
117+
]
47118
},
48119
},
49120
})
@@ -73,7 +144,12 @@ const config = {
73144
const wrapped = async function (this: unknown, ...args: unknown[]) {
74145
try {
75146
return await originalFn.apply(this, args)
76-
} catch {
147+
} catch (err) {
148+
// oxlint-disable-next-line no-console -- Log and swallow errors to avoid breaking the Storybook build when vue-docgen-api encounters an unparseable component.
149+
console.warn(
150+
'[storybook:vue-docgen-plugin] Suppressed docgen error (component docs may be missing):',
151+
err,
152+
)
77153
return undefined
78154
}
79155
}

app/pages/brand.stories.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import Brand from './brand.vue'
2+
import type { Meta, StoryObj } from '@storybook-vue/nuxt'
3+
import { pageDecorator } from '../../.storybook/decorators'
4+
5+
const meta = {
6+
component: Brand,
7+
parameters: {
8+
layout: 'fullscreen',
9+
},
10+
decorators: [pageDecorator],
11+
} satisfies Meta<typeof Brand>
12+
13+
export default meta
14+
type Story = StoryObj<typeof meta>
15+
16+
export const Default: Story = {}

0 commit comments

Comments
 (0)