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,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+ / ^ 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 | \[ [ ' " ] d e f a u l t [ ' " ] \] ) \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 ( / < s c r i p t > / , 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 }
0 commit comments