@storybook-astro/framework 0.1.0-beta.9 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (92) hide show
  1. package/README.md +38 -0
  2. package/dist/base-IRZo3zgK.d.ts +23 -0
  3. package/dist/chunk-4SWPVM6R.js +96 -0
  4. package/dist/chunk-4SWPVM6R.js.map +1 -0
  5. package/dist/chunk-5EF25G5S.js +69 -0
  6. package/dist/chunk-5EF25G5S.js.map +1 -0
  7. package/dist/chunk-7GHEQUPV.js +439 -0
  8. package/dist/chunk-7GHEQUPV.js.map +1 -0
  9. package/dist/chunk-C5OH4VBR.js +492 -0
  10. package/dist/chunk-C5OH4VBR.js.map +1 -0
  11. package/dist/chunk-DNGQBPT7.js +15 -0
  12. package/dist/chunk-DNGQBPT7.js.map +1 -0
  13. package/dist/chunk-E4LB75JN.js +89 -0
  14. package/dist/chunk-E4LB75JN.js.map +1 -0
  15. package/dist/chunk-PJEDXZVN.js +240 -0
  16. package/dist/chunk-PJEDXZVN.js.map +1 -0
  17. package/dist/chunk-UK43WNEA.js +657 -0
  18. package/dist/chunk-UK43WNEA.js.map +1 -0
  19. package/dist/dist-HJOEPVRQ.js +15574 -0
  20. package/dist/dist-HJOEPVRQ.js.map +1 -0
  21. package/dist/index.d.ts +42 -0
  22. package/dist/index.js +13 -64
  23. package/dist/index.js.map +1 -1
  24. package/dist/integrations/index.d.ts +138 -0
  25. package/dist/integrations/index.js +8 -196
  26. package/dist/integrations/index.js.map +1 -1
  27. package/dist/middleware.d.ts +26 -0
  28. package/dist/middleware.js +179 -0
  29. package/dist/middleware.js.map +1 -0
  30. package/dist/portable-stories-BvdaQigq.d.ts +83 -0
  31. package/dist/preset.d.ts +14 -0
  32. package/dist/preset.js +5 -1
  33. package/dist/testing.d.ts +27 -0
  34. package/dist/testing.js +324 -15539
  35. package/dist/testing.js.map +1 -1
  36. package/dist/types-CHTsRtA7.d.ts +42 -0
  37. package/dist/viteStorybookAstroMiddlewarePlugin-NP2E52IC.js +11 -0
  38. package/dist/viteStorybookAstroMiddlewarePlugin-NP2E52IC.js.map +1 -0
  39. package/dist/vitest/index.d.ts +19 -0
  40. package/dist/vitest/index.js +229 -0
  41. package/dist/vitest/index.js.map +1 -0
  42. package/package.json +31 -17
  43. package/src/importAstroConfig.ts +11 -0
  44. package/src/index.ts +20 -6
  45. package/src/integrations/alpine.ts +5 -2
  46. package/src/integrations/base.ts +2 -2
  47. package/src/integrations/moduleResolver.ts +43 -0
  48. package/src/integrations/preact.ts +5 -2
  49. package/src/integrations/react.ts +5 -2
  50. package/src/integrations/solid.ts +5 -2
  51. package/src/integrations/svelte.ts +5 -2
  52. package/src/integrations/vue.ts +5 -2
  53. package/src/lib/sanitization.test.ts +232 -0
  54. package/src/lib/sanitization.ts +338 -0
  55. package/src/lib/ssr-load-module-with-fs-fallback.ts +29 -0
  56. package/src/middleware.test.ts +48 -0
  57. package/src/middleware.ts +204 -96
  58. package/src/module-mocks.ts +16 -0
  59. package/src/msw-helpers.ts +1 -0
  60. package/src/msw.ts +58 -0
  61. package/src/preset.ts +38 -3
  62. package/src/rules-options.test.ts +71 -0
  63. package/src/rules-options.ts +87 -0
  64. package/src/rules.test.ts +183 -0
  65. package/src/rules.ts +314 -0
  66. package/src/testing/astro-runtime.ts +219 -0
  67. package/src/testing/component-utils.ts +32 -0
  68. package/src/testing/index.ts +2 -0
  69. package/src/testing/integration-config.ts +121 -0
  70. package/src/testing/project-root.ts +185 -0
  71. package/src/testing/renderer-daemon.ts +269 -0
  72. package/src/testing/story-composition.ts +33 -0
  73. package/src/testing/types.ts +14 -0
  74. package/src/testing/working-directory.ts +28 -0
  75. package/src/testing.ts +1 -254
  76. package/src/types.ts +16 -4
  77. package/src/virtual.d.ts +2 -1
  78. package/src/vite/createVirtualModulePlugin.test.ts +80 -0
  79. package/src/vite/createVirtualModulePlugin.ts +25 -0
  80. package/src/viteAstroContainerRenderersPlugin.ts +60 -26
  81. package/src/vitePluginAstro.ts +12 -5
  82. package/src/vitePluginAstroBuildPrerender.ts +665 -204
  83. package/src/vitePluginAstroRoutesFallback.ts +37 -0
  84. package/src/vitePluginAstroVueFallback.ts +47 -0
  85. package/src/viteStorybookAstroMiddlewarePlugin.ts +88 -12
  86. package/src/viteStorybookRendererFallbackPlugin.ts +13 -23
  87. package/src/vitest/config.ts +95 -0
  88. package/src/vitest/global-setup.ts +16 -0
  89. package/src/vitest/index.ts +2 -0
  90. package/src/vitest/vite-plugins.ts +187 -0
  91. package/dist/chunk-KTGNRGDJ.js +0 -561
  92. package/dist/chunk-KTGNRGDJ.js.map +0 -1
@@ -1,253 +1,714 @@
1
- import { readFileSync } from 'node:fs';
2
- import { fileURLToPath } from 'node:url';
3
- import { basename } from 'node:path';
4
- import type { Plugin, ViteDevServer } from 'vite';
1
+ import { createRequire } from 'node:module';
2
+ import type { Dirent } from 'node:fs';
3
+ import { mkdir, readFile, readdir, writeFile } from 'node:fs/promises';
4
+ import { resolve } from 'node:path';
5
+ import { experimental_AstroContainer as AstroContainer } from 'astro/container';
6
+ import { createServer, mergeConfig, type Plugin, type Rollup } from 'vite';
7
+ import { importAstroConfig } from './importAstroConfig.ts';
5
8
  import type { Integration } from './integrations/index.ts';
6
- import type { HandlerProps } from './middleware.ts';
7
- import { createViteServer } from './viteStorybookAstroMiddlewarePlugin.ts';
8
-
9
- /**
10
- * Vite plugin that pre-renders Astro component stories at build time.
11
- *
12
- * During `storybook build`, this plugin:
13
- * 1. Creates an internal Vite SSR server with AstroContainer
14
- * 2. Detects story files that import Astro components
15
- * 3. Loads each story module via ssrLoadModule to get fully evaluated args
16
- * (including imported assets, computed values, etc.)
17
- * 4. Renders each story variant using AstroContainer
18
- * 5. Injects the pre-rendered HTML as a story parameter (`__astroPrerendered`)
19
- *
20
- * The renderer checks for this parameter in static builds and uses the
21
- * pre-rendered HTML directly instead of showing a fallback message.
22
- *
23
- * Limitations:
24
- * - Controls panel changes won't update Astro components (HTML is static)
25
- * - Build time increases with the number of Astro stories
26
- * - Stories that override the meta component are skipped
27
- */
28
- export function vitePluginAstroBuildPrerender(integrations: Integration[]): Plugin {
29
- const safeIntegrations = integrations ?? [];
30
- let viteServer: ViteDevServer | null = null;
31
- let handler: ((data: HandlerProps) => Promise<string>) | null = null;
32
-
33
- // Maps placeholder strings to Rollup emitted-file reference IDs.
34
- // Placeholders are injected into pre-rendered HTML during transform,
35
- // then resolved to final asset paths in renderChunk.
36
- const assetRefIds = new Map<string, string>();
9
+ import { ssrLoadModuleWithFsFallback } from './lib/ssr-load-module-with-fs-fallback.ts';
10
+ import { resolveSanitizationOptions, sanitizeRenderPayload } from './lib/sanitization.ts';
11
+ import { resolveStoryModuleMock, withStoryModuleMocks } from './module-mocks.ts';
12
+ import { applyMswHandlers } from './msw.ts';
13
+ import { resolveRulesConfigFilePath } from './rules-options.ts';
14
+ import { selectStoryRules } from './rules.ts';
15
+ import type { FrameworkOptions } from './types.ts';
16
+ import { vitePluginAstroFontsFallback } from './vitePluginAstroFontsFallback.ts';
17
+ import { vitePluginAstroRoutesFallback } from './vitePluginAstroRoutesFallback.ts';
18
+ import { vitePluginAstroVueFallback } from './vitePluginAstroVueFallback.ts';
19
+
20
+ const PRERENDERED_STORIES_FILE = 'astro-prerendered-stories.json';
21
+
22
+ type StoryIndex = {
23
+ entries?: Record<
24
+ string,
25
+ {
26
+ type?: string;
27
+ id?: string;
28
+ importPath?: string;
29
+ exportName?: string;
30
+ componentPath?: string;
31
+ title?: string;
32
+ name?: string;
33
+ }
34
+ >;
35
+ };
36
+
37
+ type StoryEntry = {
38
+ id: string;
39
+ importPath: string;
40
+ exportName: string;
41
+ title?: string;
42
+ name?: string;
43
+ };
44
+
45
+ type AstroCreateResult = {
46
+ createAstro?: (...args: unknown[]) => unknown;
47
+ };
48
+
49
+ type AstroComponentFactory = ((
50
+ result: AstroCreateResult,
51
+ props: unknown,
52
+ slots: unknown
53
+ ) => unknown) & {
54
+ isAstroComponentFactory?: boolean;
55
+ moduleId?: string;
56
+ propagation?: unknown;
57
+ };
58
+
59
+ export function vitePluginAstroBuildPrerender(options: FrameworkOptions): Plugin {
60
+ const integrations = options.integrations ?? [];
61
+ const resolveFrom = options.resolveFrom ?? process.cwd();
62
+ const storyRulesConfigFilePath = resolveRulesConfigFilePath(options.storyRules, resolveFrom);
63
+ const trackedSpecifiers = collectTrackedSpecifiers(integrations);
64
+ const staticEntrypointRefs = new Map<string, string>();
65
+ const componentEntrypointRefs = new Map<string, string>();
66
+ let outDir = resolve(resolveFrom, 'storybook-static');
37
67
 
38
68
  return {
39
- name: 'storybook-astro-build-prerender',
69
+ name: 'storybook-astro:build-prerender',
40
70
  apply: 'build',
41
71
  enforce: 'post',
42
72
 
43
- async buildStart() {
44
- try {
45
- viteServer = await createViteServer(safeIntegrations);
73
+ configResolved(config) {
74
+ outDir = resolve(resolveFrom, config.build.outDir ?? 'storybook-static');
75
+ },
46
76
 
47
- const filePath = fileURLToPath(new URL('./middleware', import.meta.url));
48
- const middleware = await viteServer.ssrLoadModule(filePath, {
49
- fixStacktrace: true
50
- });
77
+ resolveId(id: string) {
78
+ if (id.startsWith('virtual:astro-static-module/')) {
79
+ return `\0${id}`;
80
+ }
51
81
 
52
- handler = await middleware.handlerFactory(safeIntegrations);
53
- } catch (err) {
54
- console.warn(
55
- '[storybook-astro] Failed to create pre-render server:',
56
- err instanceof Error ? err.message : err
57
- );
82
+ if (id.startsWith('virtual:astro-component-module/')) {
83
+ return `\0${id}`;
58
84
  }
59
85
  },
60
86
 
61
- async transform(code, id) {
62
- if (!handler || !viteServer) {return null;}
87
+ load(id: string) {
88
+ if (id.startsWith('\0virtual:astro-static-module/')) {
89
+ const encodedSpecifier = id.replace('\0virtual:astro-static-module/', '');
90
+ const specifier = decodeURIComponent(encodedSpecifier);
63
91
 
64
- // Only process story files
65
- if (!/\.stories\.(jsx?|tsx?|mjs)$/.test(id)) {return null;}
92
+ if (isClientEntrypoint(specifier)) {
93
+ return [`export { default } from '${specifier}';`, `export * from '${specifier}';`].join('\n');
94
+ }
66
95
 
67
- // Parse AST to find .astro imports
68
- const ast = this.parse(code);
69
- const astroImport = findFirstAstroImport(ast);
96
+ return [`import '${specifier}';`, 'export default undefined;'].join('\n');
97
+ }
70
98
 
71
- if (!astroImport) {return null;}
99
+ if (id.startsWith('\0virtual:astro-component-module/')) {
100
+ const encodedSpecifier = id.replace('\0virtual:astro-component-module/', '');
101
+ const specifier = decodeURIComponent(encodedSpecifier);
72
102
 
73
- // Resolve the .astro import to an absolute path
74
- const resolved = await this.resolve(astroImport.source, id);
103
+ return [`export { default } from '${specifier}';`, `export * from '${specifier}';`].join('\n');
104
+ }
105
+ },
75
106
 
76
- if (!resolved) {return null;}
77
- const componentPath = resolved.id;
107
+ async buildStart(this: Rollup.PluginContext) {
108
+ integrations.forEach((integration) => {
109
+ const entrypoint = integration.renderer.client?.entrypoint;
78
110
 
79
- // Load the story module via SSR to get fully evaluated args
80
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
81
- let storyModule: Record<string, any>;
111
+ if (entrypoint) {
112
+ this.addWatchFile(entrypoint);
113
+ }
114
+ });
82
115
 
83
- try {
84
- storyModule = await viteServer.ssrLoadModule(id);
85
- } catch (err) {
86
- console.warn(
87
- `[storybook-astro] Failed to load story for pre-render: ${id}`,
88
- err instanceof Error ? err.message : err
89
- );
90
-
91
- return null;
92
- }
116
+ trackedSpecifiers.forEach((specifier) => {
117
+ const fileReferenceId = this.emitFile({
118
+ type: 'chunk',
119
+ id: toStaticVirtualId(specifier)
120
+ });
93
121
 
94
- const meta = storyModule.default || {};
122
+ staticEntrypointRefs.set(specifier, fileReferenceId);
123
+ });
95
124
 
96
- // Confirm the meta component is an Astro component
97
- if (!meta.component?.isAstroComponentFactory) {return null;}
125
+ const srcRoot = resolve(resolveFrom, 'src/components');
126
+ const specifiers = await collectHydratableSourceModules(srcRoot);
127
+
128
+ specifiers.forEach((specifier) => {
129
+ const fileReferenceId = this.emitFile({
130
+ type: 'chunk',
131
+ id: toComponentVirtualId(specifier)
132
+ });
98
133
 
99
- // Find all named exports that are story objects
100
- const storyNames = Object.keys(storyModule).filter(
101
- (k) =>
102
- k !== 'default' &&
103
- k !== '__esModule' &&
104
- typeof storyModule[k] === 'object' &&
105
- storyModule[k] !== null
134
+ componentEntrypointRefs.set(specifier, fileReferenceId);
135
+ });
136
+ },
137
+
138
+ async writeBundle(this: Rollup.PluginContext) {
139
+ const staticModuleMap = buildStaticModuleMap(
140
+ this,
141
+ staticEntrypointRefs,
142
+ componentEntrypointRefs
106
143
  );
107
144
 
108
- if (storyNames.length === 0) {return null;}
109
-
110
- // Pre-render each story
111
- const prerendered: Record<string, string> = {};
112
-
113
- for (const name of storyNames) {
114
- const story = storyModule[name];
115
-
116
- // Skip stories that override the component — the resolved path
117
- // corresponds to the meta component and may not match
118
- if (story.component && story.component !== meta.component) {continue;}
119
-
120
- // Merge meta args with story args (story args take precedence)
121
- const mergedArgs = { ...meta.args, ...story.args };
122
- const { slots = {}, ...componentArgs } = mergedArgs;
123
-
124
- try {
125
- const html = await handler({
126
- component: componentPath,
127
- args: componentArgs,
128
- slots: (slots ?? {}) as Record<string, unknown>
129
- });
130
- // Rewrite /@fs dev-server URLs to Rollup asset placeholders.
131
- // The actual files are emitted via this.emitFile and the
132
- // placeholders are resolved to final paths in renderChunk.
133
-
134
- prerendered[name] = emitAndRewriteAssetUrls(html, this, assetRefIds);
135
- } catch (err) {
136
- console.warn(
137
- `[storybook-astro] Pre-render failed for "${name}" in ${id}:`,
138
- err instanceof Error ? err.message : err
145
+ const stories = await collectAstroStories(outDir);
146
+
147
+ if (stories.length === 0) {
148
+ await writePrerenderedStoriesFile(outDir, {});
149
+
150
+ return;
151
+ }
152
+
153
+ const prerenderedStories = await prerenderStories({
154
+ stories,
155
+ integrations,
156
+ sanitization: options.sanitization,
157
+ storyRulesConfigFilePath,
158
+ staticModuleMap,
159
+ trackedSpecifiers,
160
+ resolveFrom
161
+ });
162
+
163
+ await writePrerenderedStoriesFile(outDir, prerenderedStories);
164
+ }
165
+ };
166
+ }
167
+
168
+ async function writePrerenderedStoriesFile(outDir: string, payload: Record<string, string>) {
169
+ await mkdir(outDir, { recursive: true });
170
+ await writeFile(resolve(outDir, PRERENDERED_STORIES_FILE), JSON.stringify(payload), 'utf-8');
171
+ }
172
+
173
+ async function prerenderStories(options: {
174
+ stories: StoryEntry[];
175
+ integrations: Integration[];
176
+ sanitization?: FrameworkOptions['sanitization'];
177
+ storyRulesConfigFilePath?: string;
178
+ staticModuleMap: Record<string, string>;
179
+ trackedSpecifiers: Set<string>;
180
+ resolveFrom: string;
181
+ }) {
182
+ const sanitizationOptions = resolveSanitizationOptions(options.sanitization ?? undefined);
183
+ const resolveClientModule = createClientModuleResolver(
184
+ options.integrations,
185
+ options.staticModuleMap
186
+ );
187
+ const viteServer = await createStorySsrServer(
188
+ options.integrations,
189
+ options.trackedSpecifiers,
190
+ options.resolveFrom
191
+ );
192
+ const rulesConfigModule = await loadRulesConfigModule(viteServer, options.storyRulesConfigFilePath);
193
+
194
+ try {
195
+ const container = await AstroContainer.create({
196
+ resolve: async (specifier) => {
197
+ const mockedModule = resolveStoryModuleMock(specifier);
198
+
199
+ if (mockedModule) {
200
+ return mockedModule;
201
+ }
202
+
203
+ const resolution = resolveClientModule(specifier);
204
+
205
+ if (resolution) {
206
+ return resolution;
207
+ }
208
+
209
+ return specifier;
210
+ }
211
+ });
212
+
213
+ await addContainerRenderers(container, options.integrations, resolveClientModule, viteServer);
214
+
215
+ const output: Record<string, string> = {};
216
+
217
+ for (const story of options.stories) {
218
+ const selectedRules = await selectStoryRules({
219
+ configModule: rulesConfigModule,
220
+ configFilePath: options.storyRulesConfigFilePath,
221
+ mode: 'production',
222
+ story: {
223
+ id: story.id,
224
+ title: story.title,
225
+ name: story.name
226
+ }
227
+ });
228
+
229
+ await applyMswHandlers(selectedRules.mswHandlers);
230
+
231
+ if (selectedRules.moduleMocks.size > 0) {
232
+ viteServer.moduleGraph.invalidateAll();
233
+ }
234
+
235
+ const html = await withStoryModuleMocks(selectedRules.moduleMocks, async () => {
236
+ const modulePath = resolveImportPath(story.importPath, options.resolveFrom);
237
+ const storyModule = await viteServer.ssrLoadModule(modulePath);
238
+ const meta = isRecord(storyModule.default) ? storyModule.default : {};
239
+ const storyExport = isRecord(storyModule[story.exportName]) ? storyModule[story.exportName] : {};
240
+
241
+ if (typeof meta.component !== 'function') {
242
+ throw new Error(
243
+ `Unable to prerender story "${story.id}". Missing default export component in ${story.importPath}.`
139
244
  );
140
245
  }
246
+
247
+ if (storyExport.component && storyExport.component !== meta.component) {
248
+ return undefined;
249
+ }
250
+
251
+ const mergedArgs = mergeStoryArgs(toRecord(meta.args), toRecord(storyExport.args));
252
+ const { args, slots } = separateSlots(mergedArgs);
253
+ const processedArgs = await processImageMetadata(args);
254
+ const sanitizedPayload = sanitizeRenderPayload(
255
+ {
256
+ args: processedArgs,
257
+ slots
258
+ },
259
+ sanitizationOptions
260
+ );
261
+
262
+ return container.renderToString(patchCreateAstroCompat(meta.component), {
263
+ props: sanitizedPayload.args,
264
+ slots: sanitizedPayload.slots
265
+ });
266
+ });
267
+
268
+ if (html !== undefined) {
269
+ output[story.id] = html;
141
270
  }
271
+ }
142
272
 
143
- if (Object.keys(prerendered).length === 0) {return null;}
144
-
145
- // Append code that injects pre-rendered HTML as story parameters.
146
- // This runs as module-level side effects during import, before
147
- // Storybook reads the story exports.
148
- const injections = Object.entries(prerendered).map(
149
- ([name, html]) =>
150
- `if (typeof ${name} !== 'undefined' && ${name} && typeof ${name} === 'object') {\n` +
151
- ` ${name}.parameters = Object.assign({}, ${name}.parameters, ` +
152
- `{ __astroPrerendered: ${JSON.stringify(html)} });\n` +
153
- `}`
154
- );
273
+ return output;
274
+ } finally {
275
+ await viteServer.close();
276
+ }
277
+ }
155
278
 
156
- return {
157
- code:
158
- code +
159
- '\n// Pre-rendered by storybook-astro-build-prerender\n' +
160
- injections.join('\n'),
161
- map: null
162
- };
279
+ async function createStorySsrServer(
280
+ integrations: Integration[],
281
+ trackedSpecifiers: Set<string>,
282
+ resolveFrom: string
283
+ ) {
284
+ const { getViteConfig } = await importAstroConfig(resolveFrom);
285
+ const astroConfig = await getViteConfig(
286
+ { root: resolveFrom },
287
+ {
288
+ configFile: false,
289
+ integrations: await Promise.all(
290
+ integrations.map((integration) => integration.loadIntegration(resolveFrom))
291
+ )
292
+ }
293
+ )({
294
+ mode: 'production',
295
+ command: 'serve'
296
+ });
297
+
298
+ const config = mergeConfig(astroConfig, {
299
+ appType: 'custom',
300
+ server: {
301
+ middlewareMode: true
163
302
  },
303
+ plugins: [
304
+ createProjectAstroResolutionPlugin(resolveFrom),
305
+ vitePluginAstroFontsFallback(),
306
+ vitePluginAstroVueFallback(),
307
+ vitePluginAstroRoutesFallback(),
308
+ {
309
+ name: 'storybook-astro:static-prerender-ssr-stubs',
310
+ resolveId(id: string) {
311
+ if (trackedSpecifiers.has(id)) {
312
+ return `\0storybook-astro-static-prerender-stub:${encodeURIComponent(id)}`;
313
+ }
314
+ },
315
+ load(id: string) {
316
+ if (id.startsWith('\0storybook-astro-static-prerender-stub:')) {
317
+ return 'export default undefined;';
318
+ }
319
+ }
320
+ }
321
+ ]
322
+ });
323
+
324
+ return createServer(config);
325
+ }
164
326
 
165
- renderChunk(code) {
166
- if (assetRefIds.size === 0) {return null;}
327
+ async function loadRulesConfigModule(
328
+ viteServer: Awaited<ReturnType<typeof createStorySsrServer>>,
329
+ configFilePath?: string
330
+ ) {
331
+ if (!configFilePath) {
332
+ return undefined;
333
+ }
167
334
 
168
- let result = code;
169
- let modified = false;
335
+ try {
336
+ return await ssrLoadModuleWithFsFallback(viteServer, configFilePath, {
337
+ fixStacktrace: true
338
+ });
339
+ } catch (error) {
340
+ const reason = error instanceof Error ? error.message : String(error);
170
341
 
171
- for (const [placeholder, refId] of assetRefIds) {
172
- if (!result.includes(placeholder)) {continue;}
173
- const fileName = this.getFileName(refId);
342
+ throw new Error(
343
+ `Unable to load framework.options.storyRules config module at ${configFilePath}: ${reason}`
344
+ );
345
+ }
346
+ }
174
347
 
175
- result = result.replaceAll(placeholder, fileName);
176
- modified = true;
348
+ async function addContainerRenderers(
349
+ container: Awaited<ReturnType<typeof AstroContainer.create>>,
350
+ integrations: Integration[],
351
+ resolveClientModule: (specifier: string) => string | undefined,
352
+ viteServer: Awaited<ReturnType<typeof createStorySsrServer>>
353
+ ) {
354
+ for (const integration of integrations) {
355
+ const serverRenderer = integration.renderer.server;
356
+
357
+ if (serverRenderer) {
358
+ const serverRendererModule = await viteServer.ssrLoadModule(serverRenderer.entrypoint);
359
+ const renderer = serverRendererModule.default ?? serverRendererModule;
360
+
361
+ if (integration.name === 'solid' && isRecord(renderer)) {
362
+ container.addServerRenderer({
363
+ name: serverRenderer.name,
364
+ renderer: {
365
+ ...renderer,
366
+ name: serverRenderer.name
367
+ }
368
+ });
369
+ } else {
370
+ container.addServerRenderer({
371
+ name: serverRenderer.name,
372
+ renderer
373
+ });
177
374
  }
375
+ }
178
376
 
179
- return modified ? { code: result, map: null } : null;
180
- },
377
+ const clientRenderer = integration.renderer.client;
378
+
379
+ if (clientRenderer) {
380
+ const resolvedEntrypoint =
381
+ resolveClientModule(clientRenderer.entrypoint) ?? clientRenderer.entrypoint;
382
+
383
+ container.addClientRenderer({
384
+ name: clientRenderer.name,
385
+ entrypoint: resolvedEntrypoint
386
+ });
387
+ }
388
+ }
389
+ }
390
+
391
+ function createClientModuleResolver(
392
+ integrations: Integration[],
393
+ staticModuleMap: Record<string, string>
394
+ ) {
395
+ return function resolveClientModule(specifier: string) {
396
+ if (Object.hasOwn(staticModuleMap, specifier)) {
397
+ return staticModuleMap[specifier];
398
+ }
399
+
400
+ const normalizedSpecifier = specifier.replace(/\\/g, '/').replace(/\?.*$/, '');
401
+
402
+ if (Object.hasOwn(staticModuleMap, normalizedSpecifier)) {
403
+ return staticModuleMap[normalizedSpecifier];
404
+ }
181
405
 
182
- async buildEnd() {
183
- if (viteServer) {
184
- await viteServer.close();
185
- viteServer = null;
186
- handler = null;
406
+ for (const integration of integrations) {
407
+ const resolution = integration.resolveClient(specifier);
408
+
409
+ if (resolution) {
410
+ return resolution;
187
411
  }
188
412
  }
189
413
  };
190
414
  }
191
415
 
192
- /**
193
- * Finds the first import declaration with a .astro source in the ESTree AST.
194
- */
195
- function findFirstAstroImport(
196
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
197
- ast: any
198
- ): { local: string; source: string } | null {
199
- for (const node of ast.body) {
200
- if (
201
- node.type === 'ImportDeclaration' &&
202
- typeof node.source.value === 'string' &&
203
- node.source.value.endsWith('.astro')
204
- ) {
205
- const defaultSpecifier = node.specifiers?.find(
206
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
207
- (s: any) => s.type === 'ImportDefaultSpecifier'
208
- );
416
+ async function collectAstroStories(outDir: string): Promise<StoryEntry[]> {
417
+ const indexFile = resolve(outDir, 'index.json');
418
+ const indexRaw = await readFile(indexFile, 'utf-8');
419
+ const indexJson = JSON.parse(indexRaw) as StoryIndex;
209
420
 
210
- if (defaultSpecifier) {
211
- return {
212
- local: defaultSpecifier.local.name,
213
- source: node.source.value
214
- };
421
+ return Object.values(indexJson.entries ?? {})
422
+ .filter((entry) => entry.type === 'story' && entry.componentPath?.endsWith('.astro'))
423
+ .map((entry) => {
424
+ if (!entry.id || !entry.importPath || !entry.exportName) {
425
+ throw new Error(`Encountered an invalid Storybook index entry in ${indexFile}.`);
215
426
  }
216
- }
427
+
428
+ return {
429
+ id: entry.id,
430
+ importPath: entry.importPath,
431
+ exportName: entry.exportName,
432
+ title: entry.title,
433
+ name: entry.name
434
+ };
435
+ });
436
+ }
437
+
438
+ function mergeStoryArgs(
439
+ metaArgs: Record<string, unknown> | undefined,
440
+ storyArgs: Record<string, unknown> | undefined
441
+ ) {
442
+ return {
443
+ ...(metaArgs ?? {}),
444
+ ...(storyArgs ?? {})
445
+ };
446
+ }
447
+
448
+ function separateSlots(inputArgs: Record<string, unknown>) {
449
+ const args = { ...inputArgs };
450
+ const slotsCandidate = args.slots;
451
+
452
+ delete args.slots;
453
+
454
+ if (!isRecord(slotsCandidate)) {
455
+ return {
456
+ args,
457
+ slots: {}
458
+ };
217
459
  }
218
-
219
- return null;
220
- }
221
-
222
- /**
223
- * Finds /@fs dev-server URLs in pre-rendered HTML, emits the referenced
224
- * files as Rollup assets, and replaces the URLs with placeholders that
225
- * are resolved to final paths in renderChunk.
226
- */
227
- function emitAndRewriteAssetUrls(
228
- html: string,
229
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
230
- ctx: any,
231
- refIds: Map<string, string>
232
- ): string {
233
- // Match /@fs URLs in HTML attribute values.
234
- // The URL may have a query string with HTML-encoded & (&#38;).
235
- // Note: file paths may contain spaces, so we match until the closing quote.
236
- return html.replace(/\/@fs([^"'>]+)/g, (fullMatch, rawPath: string) => {
237
- // Strip query string (may contain &#38; or &)
238
- const pathOnly = rawPath.split('?')[0];
460
+
461
+ return {
462
+ args,
463
+ slots: slotsCandidate as Record<string, string>
464
+ };
465
+ }
466
+
467
+ function resolveImportPath(importPath: string, resolveFrom: string) {
468
+ if (importPath.startsWith('./')) {
469
+ return resolve(resolveFrom, importPath.slice(2));
470
+ }
471
+
472
+ return resolve(resolveFrom, importPath);
473
+ }
474
+
475
+ function isRecord(value: unknown): value is Record<string, unknown> {
476
+ return typeof value === 'object' && value !== null;
477
+ }
478
+
479
+ function toRecord(value: unknown): Record<string, unknown> | undefined {
480
+ if (!isRecord(value)) {
481
+ return undefined;
482
+ }
483
+
484
+ return value;
485
+ }
486
+
487
+ function collectTrackedSpecifiers(integrations: Integration[]) {
488
+ const specifiers = new Set<string>(['astro:scripts/page.js', 'astro:scripts/before-hydration.js']);
489
+
490
+ integrations.forEach((integration) => {
491
+ const entrypoint = integration.renderer.client?.entrypoint;
492
+
493
+ if (entrypoint) {
494
+ specifiers.add(entrypoint);
495
+ }
496
+ });
497
+
498
+ return specifiers;
499
+ }
500
+
501
+ function buildStaticModuleMap(
502
+ pluginContext: Rollup.PluginContext,
503
+ staticEntrypointRefs: Map<string, string>,
504
+ componentEntrypointRefs: Map<string, string>
505
+ ) {
506
+ const map: Record<string, string> = {};
507
+
508
+ staticEntrypointRefs.forEach((fileReferenceId, specifier) => {
509
+ const fileName = pluginContext.getFileName(fileReferenceId);
510
+
511
+ if (fileName) {
512
+ map[specifier] = toPublicPath(fileName);
513
+ }
514
+ });
515
+
516
+ componentEntrypointRefs.forEach((fileReferenceId, specifier) => {
517
+ const fileName = pluginContext.getFileName(fileReferenceId);
518
+
519
+ if (fileName) {
520
+ map[specifier] = toPublicPath(fileName);
521
+ }
522
+ });
523
+
524
+ return map;
525
+ }
526
+
527
+ function toStaticVirtualId(specifier: string) {
528
+ return `virtual:astro-static-module/${encodeURIComponent(specifier)}`;
529
+ }
530
+
531
+ function toComponentVirtualId(specifier: string) {
532
+ return `virtual:astro-component-module/${encodeURIComponent(specifier)}`;
533
+ }
534
+
535
+ function isClientEntrypoint(specifier: string) {
536
+ return specifier.startsWith('@astrojs/') && specifier.endsWith('/client.js');
537
+ }
538
+
539
+ function toPublicPath(fileName: string) {
540
+ return `./${fileName}`;
541
+ }
542
+
543
+ async function collectHydratableSourceModules(srcRoot: string): Promise<string[]> {
544
+ const modules: string[] = [];
545
+
546
+ async function walk(directory: string) {
547
+ let entries: Dirent[];
239
548
 
240
549
  try {
241
- const source = readFileSync(pathOnly);
242
- const name = basename(pathOnly);
243
- const refId = ctx.emitFile({ type: 'asset', name, source });
244
- const placeholder = `__ASTRO_PRERENDER_ASSET_${refId}__`;
245
-
246
- refIds.set(placeholder, refId);
247
-
248
- return placeholder;
550
+ entries = await readdir(directory, { withFileTypes: true });
249
551
  } catch {
250
- return fullMatch;
552
+ return;
251
553
  }
252
- });
554
+
555
+ await Promise.all(
556
+ entries.map(async (entry) => {
557
+ const absolutePath = resolve(directory, entry.name);
558
+
559
+ if (entry.isDirectory()) {
560
+ await walk(absolutePath);
561
+
562
+ return;
563
+ }
564
+
565
+ if (!entry.isFile()) {
566
+ return;
567
+ }
568
+
569
+ const normalizedPath = absolutePath.replace(/\\/g, '/');
570
+
571
+ if (!isHydratableSourceFile(normalizedPath)) {
572
+ return;
573
+ }
574
+
575
+ if (isNonHydratableSourceFile(normalizedPath)) {
576
+ return;
577
+ }
578
+
579
+ modules.push(normalizedPath);
580
+ })
581
+ );
582
+ }
583
+
584
+ await walk(srcRoot);
585
+
586
+ return modules;
587
+ }
588
+
589
+ function isHydratableSourceFile(input: string) {
590
+ return /\.(jsx|tsx|vue|svelte|js|ts)$/.test(input);
591
+ }
592
+
593
+ function isNonHydratableSourceFile(input: string) {
594
+ return /\.stories\.[jt]sx?$|\.stories\.vue$|\.stories\.svelte$|\.(spec|test)\.[jt]sx?$/.test(
595
+ input
596
+ );
597
+ }
598
+
599
+ function patchCreateAstroCompat(component: unknown): AstroComponentFactory {
600
+ if (typeof component !== 'function') {
601
+ throw new Error('Expected Astro component factory to be a function.');
602
+ }
603
+
604
+ const originalComponent = component as AstroComponentFactory;
605
+ const wrapped = ((result: AstroCreateResult, props: unknown, slots: unknown) => {
606
+ if (result && typeof result.createAstro === 'function') {
607
+ const originalCreateAstro = result.createAstro;
608
+ const runtimeExpectsAstroGlobal = originalCreateAstro.length >= 3;
609
+
610
+ result.createAstro = (...args: unknown[]) => {
611
+ if (args.length === 3 && !runtimeExpectsAstroGlobal) {
612
+ return originalCreateAstro(args[1], args[2]);
613
+ }
614
+
615
+ return originalCreateAstro(...args);
616
+ };
617
+ }
618
+
619
+ return originalComponent(result, props, slots);
620
+ }) as AstroComponentFactory;
621
+
622
+ wrapped.isAstroComponentFactory = originalComponent.isAstroComponentFactory;
623
+ wrapped.moduleId = originalComponent.moduleId;
624
+ wrapped.propagation = originalComponent.propagation;
625
+
626
+ return wrapped;
627
+ }
628
+
629
+ async function processImageMetadata(
630
+ args: Record<string, unknown>
631
+ ): Promise<Record<string, unknown>> {
632
+ const processed: Record<string, unknown> = {};
633
+
634
+ for (const [key, value] of Object.entries(args)) {
635
+ if (isImageMetadata(value)) {
636
+ processed[key] = convertImageMetadataToUrl(value);
637
+
638
+ continue;
639
+ }
640
+
641
+ if (Array.isArray(value)) {
642
+ processed[key] = await Promise.all(
643
+ value.map(async (item) => {
644
+ if (isImageMetadata(item)) {
645
+ return convertImageMetadataToUrl(item);
646
+ }
647
+
648
+ if (isRecord(item)) {
649
+ return processImageMetadata(item);
650
+ }
651
+
652
+ return item;
653
+ })
654
+ );
655
+
656
+ continue;
657
+ }
658
+
659
+ if (isRecord(value)) {
660
+ processed[key] = await processImageMetadata(value);
661
+
662
+ continue;
663
+ }
664
+
665
+ processed[key] = value;
666
+ }
667
+
668
+ return processed;
669
+ }
670
+
671
+ function isImageMetadata(value: unknown): value is Record<string, unknown> {
672
+ return (
673
+ isRecord(value) &&
674
+ typeof value.src === 'string' &&
675
+ ('width' in value || 'height' in value || 'format' in value)
676
+ );
677
+ }
678
+
679
+ function convertImageMetadataToUrl(imageMetadata: Record<string, unknown>): string {
680
+ const src = imageMetadata.src;
681
+ const fsPath = imageMetadata.fsPath;
682
+
683
+ if (typeof src === 'string') {
684
+ return src;
685
+ }
686
+
687
+ if (typeof fsPath === 'string') {
688
+ return fsPath;
689
+ }
690
+
691
+ return String(imageMetadata);
692
+ }
693
+
694
+ function createProjectAstroResolutionPlugin(resolveFrom: string): Plugin {
695
+ const require = createRequire(import.meta.url);
696
+
697
+ return {
698
+ name: 'storybook-astro:resolve-project-astro-prerender',
699
+ enforce: 'pre',
700
+ resolveId(id: string) {
701
+ if (id !== 'astro' && !id.startsWith('astro/')) {
702
+ return null;
703
+ }
704
+
705
+ try {
706
+ return require.resolve(id, {
707
+ paths: [resolveFrom]
708
+ });
709
+ } catch {
710
+ return null;
711
+ }
712
+ }
713
+ };
253
714
  }