Skip to content

Commit 51bc902

Browse files
docs(ui): add stories for Brand page (#2468)
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
1 parent 655ae5b commit 51bc902

File tree

2 files changed

+107
-18
lines changed

2 files changed

+107
-18
lines changed

.storybook/main.ts

Lines changed: 91 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,95 @@ 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) {
58+
throw new Error('[storybook-nuxt-components] Could not resolve the `#build` alias.')
59+
}
60+
const dtsPath = resolve(buildDir, 'components.d.ts')
61+
// Wire the generated declaration file into Vite's file-watch graph so
62+
// that the virtual module is invalidated when Nuxt regenerates it.
63+
this.addWatchFile(dtsPath)
64+
const dts = readFileSync(dtsPath, 'utf-8')
65+
const lines: string[] = []
66+
// Match only the direct `typeof import("…").default` form.
67+
// Lazy/island wrappers (LazyComponent<T>, IslandComponent<T>) are
68+
// excluded intentionally — Storybook only needs the concrete type.
69+
// The format has been stable across all Nuxt 3 releases.
70+
const re =
71+
/^export const (\w+): typeof import\("([^"]+)"\)(?:\.default|\[['"]default['"]\])\s*;?$/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).replaceAll('\\', '/')
77+
lines.push(`export { default as ${name} } from ${JSON.stringify(abs)}`)
78+
}
79+
if (lines.length === 0) {
80+
throw new Error(
81+
`[storybook-nuxt-components] No component exports were found in ${dtsPath}.`,
82+
)
83+
}
84+
return lines.join('\n')
85+
},
86+
})
87+
2488
// Bridge compatibility between Storybook v10 core and v9 @storybook-vue/nuxt
2589
// v10 expects module federation globals that v9 doesn't provide
2690
newConfig.plugins.push({
2791
name: 'storybook-v10-compat',
2892
transformIndexHtml: {
2993
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>')
94+
handler() {
95+
return [
96+
{
97+
tag: 'script',
98+
injectTo: 'head-prepend' as const,
99+
children: [
100+
'// Minimal shims for Storybook v10 module federation system',
101+
'// These will be replaced when Storybook runtime loads',
102+
'window.__STORYBOOK_MODULE_GLOBAL__ = { global: window };',
103+
'window.__STORYBOOK_MODULE_CLIENT_LOGGER__ = {',
104+
" deprecate: console.warn.bind(console, '[deprecated]'),",
105+
' once: console.log.bind(console),',
106+
' logger: console',
107+
'};',
108+
'window.__STORYBOOK_MODULE_CHANNELS__ = {',
109+
' Channel: class { on() {} off() {} emit() {} once() {} },',
110+
' createBrowserChannel: () => new window.__STORYBOOK_MODULE_CHANNELS__.Channel()',
111+
'};',
112+
].join('\n'),
113+
},
114+
]
47115
},
48116
},
49117
})
@@ -73,7 +141,12 @@ const config = {
73141
const wrapped = async function (this: unknown, ...args: unknown[]) {
74142
try {
75143
return await originalFn.apply(this, args)
76-
} catch {
144+
} catch (err) {
145+
// oxlint-disable-next-line no-console -- Log and swallow errors to avoid breaking the Storybook build when vue-docgen-api encounters an unparseable component.
146+
console.warn(
147+
'[storybook:vue-docgen-plugin] Suppressed docgen error (component docs may be missing):',
148+
err,
149+
)
77150
return undefined
78151
}
79152
}

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)