11import type { StorybookConfig } from '@storybook-vue/nuxt'
2+ import { readFileSync } from 'node:fs'
3+ import { resolve } from 'node:path'
24
35const 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 = / ^ e x p o r t c o n s t ( \w + ) : t y p e o f i m p o r t \( " ( [ ^ " ] + ) " \) \. d e f a u l t $ / 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 ( / < s c r i p t > / , 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 }
0 commit comments