@storybook-astro/framework 1.2.0 → 1.3.0-canary.1

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 (85) hide show
  1. package/dist/{base-IRZo3zgK.d.ts → base-DT67T5pi.d.ts} +1 -0
  2. package/dist/{chunk-T7NWIO5S.js → chunk-2EABPTOY.js} +5 -5
  3. package/dist/{chunk-PJEDXZVN.js → chunk-7YBE4TTI.js} +2 -1
  4. package/dist/chunk-7YBE4TTI.js.map +1 -0
  5. package/dist/{chunk-POHTFYST.js → chunk-AYYMNFI6.js} +104 -6
  6. package/dist/chunk-AYYMNFI6.js.map +1 -0
  7. package/dist/chunk-B5HHF6FC.js +116 -0
  8. package/dist/chunk-B5HHF6FC.js.map +1 -0
  9. package/dist/chunk-H3XZHW6Z.js +1402 -0
  10. package/dist/chunk-H3XZHW6Z.js.map +1 -0
  11. package/dist/{chunk-DNGQBPT7.js → chunk-PUTCAN6X.js} +5 -2
  12. package/dist/{chunk-DNGQBPT7.js.map → chunk-PUTCAN6X.js.map} +1 -1
  13. package/dist/{chunk-OUEDTRBO.js → chunk-TWAO2IQW.js} +229 -67
  14. package/dist/chunk-TWAO2IQW.js.map +1 -0
  15. package/dist/{chunk-4SWPVM6R.js → chunk-WUTCMEF5.js} +2 -2
  16. package/dist/index.d.ts +17 -7
  17. package/dist/index.js +6 -5
  18. package/dist/index.js.map +1 -1
  19. package/dist/integrations/index.d.ts +2 -1
  20. package/dist/integrations/index.js +1 -1
  21. package/dist/middleware.js +18 -131
  22. package/dist/middleware.js.map +1 -1
  23. package/dist/{types-C-jan6Px.d.ts → preset-BvgHg2of.d.ts} +8 -11
  24. package/dist/preset.d.ts +2 -10
  25. package/dist/preset.js +5 -4
  26. package/dist/renderer/renderer-dev.js +62 -0
  27. package/dist/renderer/renderer-dev.js.map +1 -0
  28. package/dist/renderer/renderer-server.js +92 -0
  29. package/dist/renderer/renderer-server.js.map +1 -0
  30. package/dist/renderer/renderer-static.js +54 -0
  31. package/dist/renderer/renderer-static.js.map +1 -0
  32. package/dist/testing.js +12 -11
  33. package/dist/testing.js.map +1 -1
  34. package/dist/{viteStorybookAstroMiddlewarePlugin-2EFKTECT.js → viteStorybookAstroMiddlewarePlugin-UB6ZLJ4B.js} +4 -3
  35. package/dist/vitest/global-setup.js +6 -5
  36. package/dist/vitest/global-setup.js.map +1 -1
  37. package/dist/vitest/index.d.ts +1 -1
  38. package/dist/vitest/index.js +3 -3
  39. package/package.json +14 -43
  40. package/src/astroImageService.ts +57 -0
  41. package/src/astroRenderHandler.ts +203 -0
  42. package/src/importAstroConfig.ts +1 -1
  43. package/src/index.ts +2 -0
  44. package/src/integrations/alpine.ts +1 -0
  45. package/src/integrations/base.ts +6 -0
  46. package/src/middleware.ts +29 -200
  47. package/src/module-mocks.ts +153 -5
  48. package/src/preset.ts +38 -8
  49. package/src/productionRenderRuntime.ts +187 -0
  50. package/src/rules.test.ts +52 -4
  51. package/src/rules.ts +54 -7
  52. package/src/server/index.ts +101 -31
  53. package/src/storyRulesRuntime.ts +34 -0
  54. package/src/storySsrVite.ts +240 -0
  55. package/src/types.ts +0 -9
  56. package/src/virtual.d.ts +17 -3
  57. package/src/vite/{astroFilesVirtualModulePlugin.ts → astroFilesPlugin.ts} +4 -4
  58. package/src/vite/sanitizeConfigPlugin.ts +18 -0
  59. package/src/vite/{storybookAstroServerAuthConfigVirtualModulePlugin.test.ts → serverAuthPlugin.test.ts} +7 -10
  60. package/src/vite/{storybookAstroServerAuthConfigVirtualModulePlugin.ts → serverAuthPlugin.ts} +6 -9
  61. package/src/vite/serverRuntimePlugin.ts +109 -0
  62. package/src/vite/{storybookAstroRulesConfigVirtualModulePlugin.ts → storyRulesPlugin.ts} +6 -7
  63. package/src/vite/{createVirtualModulePlugin.test.ts → virtualModulePlugin.test.ts} +5 -5
  64. package/src/vite/{createVirtualModulePlugin.ts → virtualModulePlugin.ts} +2 -2
  65. package/src/viteAstroContainerRenderersPlugin.ts +72 -2
  66. package/src/vitePluginAstroBuildPrerender.ts +75 -646
  67. package/src/vitePluginAstroBuildServer.ts +217 -165
  68. package/src/vitePluginAstroBuildShared.test.ts +87 -0
  69. package/src/vitePluginAstroBuildShared.ts +465 -0
  70. package/src/vitePluginStoryModuleMocks.ts +29 -0
  71. package/src/viteStorybookAstroMiddlewarePlugin.ts +8 -0
  72. package/src/viteStorybookAstroRendererPlugin.ts +13 -6
  73. package/src/viteStorybookRendererFallbackPlugin.ts +2 -2
  74. package/dist/chunk-OUEDTRBO.js.map +0 -1
  75. package/dist/chunk-PBISP7PA.js +0 -1137
  76. package/dist/chunk-PBISP7PA.js.map +0 -1
  77. package/dist/chunk-PJEDXZVN.js.map +0 -1
  78. package/dist/chunk-POHTFYST.js.map +0 -1
  79. package/dist/node/index.d.ts +0 -10
  80. package/dist/node/index.js +0 -10
  81. package/dist/node/index.js.map +0 -1
  82. package/src/vite/storybookAstroSanitizationConfigVirtualModulePlugin.ts +0 -21
  83. /package/dist/{chunk-T7NWIO5S.js.map → chunk-2EABPTOY.js.map} +0 -0
  84. /package/dist/{chunk-4SWPVM6R.js.map → chunk-WUTCMEF5.js.map} +0 -0
  85. /package/dist/{viteStorybookAstroMiddlewarePlugin-2EFKTECT.js.map → viteStorybookAstroMiddlewarePlugin-UB6ZLJ4B.js.map} +0 -0
@@ -1,22 +1,23 @@
1
- import { createRequire } from 'node:module';
2
- import type { Dirent } from 'node:fs';
3
- import { mkdir, readFile, readdir, writeFile } from 'node:fs/promises';
1
+ import { mkdir, readFile, writeFile } from 'node:fs/promises';
4
2
  import { resolve } from 'node:path';
5
- import type { experimental_AstroContainer as AstroContainer } from 'astro/container';
6
- import { createServer, mergeConfig, type Plugin, type Rollup } from 'vite';
7
- import { importAstroConfig } from './importAstroConfig.ts';
3
+ import type { Plugin, Rollup } from 'vite';
8
4
  import type { Integration } from './integrations/index.ts';
9
- import { installPassthroughImageService } from './lib/passthrough-image-service.ts';
10
- import { ssrLoadModuleWithFsFallback } from './lib/ssr-load-module-with-fs-fallback.ts';
11
- import { resolveSanitizationOptions, sanitizeRenderPayload } from './lib/sanitization.ts';
12
- import { resolveStoryModuleMock, withStoryModuleMocks } from './module-mocks.ts';
5
+ import {
6
+ buildStaticModuleMap,
7
+ emitHydratedComponentEntriesFromAstroFile,
8
+ collectTrackedSpecifiers,
9
+ emitBuildEntrypoints,
10
+ loadVirtualBuildModule,
11
+ resolveVirtualBuildModuleId,
12
+ stripQuery
13
+ } from './vitePluginAstroBuildShared.ts';
14
+ import {
15
+ createProductionRenderRuntime,
16
+ renderProductionStoryToHtml,
17
+ type ProductionStoryEntry
18
+ } from './productionRenderRuntime.ts';
13
19
  import { resolveRulesConfigFilePath } from './rules-options.ts';
14
- import { selectStoryRules, withStoryRuleCleanups } from './rules.ts';
15
20
  import type { FrameworkOptions } from './types.ts';
16
- import { vitePluginAstroFontsFallback } from './vitePluginAstroFontsFallback.ts';
17
- import { vitePluginAstroIntegrationOptsFallback } from './vitePluginAstroIntegrationOptsFallback.ts';
18
- import { vitePluginAstroRoutesFallback } from './vitePluginAstroRoutesFallback.ts';
19
- import { vitePluginAstroVueFallback } from './vitePluginAstroVueFallback.ts';
20
21
 
21
22
  const PRERENDERED_STORIES_FILE = 'astro-prerendered-stories.json';
22
23
 
@@ -35,28 +36,6 @@ type StoryIndex = {
35
36
  >;
36
37
  };
37
38
 
38
- type StoryEntry = {
39
- id: string;
40
- importPath: string;
41
- exportName: string;
42
- title?: string;
43
- name?: string;
44
- };
45
-
46
- type AstroCreateResult = {
47
- createAstro?: (...args: unknown[]) => unknown;
48
- };
49
-
50
- type AstroComponentFactory = ((
51
- result: AstroCreateResult,
52
- props: unknown,
53
- slots: unknown
54
- ) => unknown) & {
55
- isAstroComponentFactory?: boolean;
56
- moduleId?: string;
57
- propagation?: unknown;
58
- };
59
-
60
39
  export function vitePluginAstroBuildPrerender(options: FrameworkOptions): Plugin {
61
40
  const integrations = options.integrations ?? [];
62
41
  const resolveFrom = options.resolveFrom ?? process.cwd();
@@ -75,100 +54,56 @@ export function vitePluginAstroBuildPrerender(options: FrameworkOptions): Plugin
75
54
  outDir = resolve(resolveFrom, config.build.outDir ?? 'storybook-static');
76
55
  },
77
56
 
78
- resolveId(id: string) {
79
- if (id.startsWith('virtual:astro-static-module/')) {
80
- return `\0${id}`;
81
- }
57
+ async resolveId(this: Rollup.PluginContext, id: string, importer?: string) {
58
+ const importerPath = stripQuery(importer);
82
59
 
83
- if (id.startsWith('virtual:astro-component-module/')) {
84
- return `\0${id}`;
60
+ if (importerPath?.endsWith('.astro')) {
61
+ await emitHydratedComponentEntriesFromAstroFile({
62
+ pluginContext: this,
63
+ astroFilePath: importerPath,
64
+ resolveFrom,
65
+ componentEntrypointRefs
66
+ });
85
67
  }
68
+
69
+ return resolveVirtualBuildModuleId(id);
86
70
  },
87
71
 
88
72
  load(id: string) {
89
- if (id.startsWith('\0virtual:astro-static-module/')) {
90
- const encodedSpecifier = id.replace('\0virtual:astro-static-module/', '');
91
- const specifier = decodeURIComponent(encodedSpecifier);
92
-
93
- if (isClientEntrypoint(specifier)) {
94
- return [`export { default } from '${specifier}';`, `export * from '${specifier}';`].join('\n');
95
- }
96
-
97
- return [`import '${specifier}';`, 'export default undefined;'].join('\n');
98
- }
99
-
100
- if (id.startsWith('\0virtual:astro-component-module/')) {
101
- const withoutPrefix = id.replace('\0virtual:astro-component-module/', '');
102
- // Strip the ?component-wrapper query appended by toComponentVirtualId
103
- const encodedSpecifier = withoutPrefix.replace(/\?.*$/, '');
104
- const specifier = decodeURIComponent(encodedSpecifier);
105
-
106
- return [`export { default } from '${specifier}';`, `export * from '${specifier}';`].join('\n');
107
- }
73
+ return loadVirtualBuildModule(id);
108
74
  },
109
75
 
110
76
  async buildStart(this: Rollup.PluginContext) {
111
- integrations.forEach((integration) => {
112
- const entrypoint = integration.renderer.client?.entrypoint;
113
-
114
- if (entrypoint) {
115
- this.addWatchFile(entrypoint);
116
- }
117
- });
118
-
119
- trackedSpecifiers.forEach((specifier) => {
120
- const fileReferenceId = this.emitFile({
121
- type: 'chunk',
122
- id: toStaticVirtualId(specifier)
123
- });
124
-
125
- staticEntrypointRefs.set(specifier, fileReferenceId);
126
- });
127
-
128
- const componentRootPaths = [
129
- resolve(resolveFrom, 'src/components'),
130
- ...(options.renderMode === 'static' && options.componentRoots
131
- ? options.componentRoots.map((root) => resolve(resolveFrom, root))
132
- : [])
133
- ];
134
- const specifierArrays = await Promise.all(
135
- componentRootPaths.map((root) => collectHydratableSourceModules(root))
136
- );
137
- const specifiers = specifierArrays.flat();
138
-
139
- specifiers.forEach((specifier) => {
140
- // .svelte and .vue files must be emitted as direct chunks so their
141
- // native Vite compile plugins process them correctly. The virtual
142
- // module wrapper exposes a JS re-export stub; vite-plugin-svelte and
143
- // @vitejs/plugin-vue strip the query string before checking the
144
- // extension, so they still try to compile the stub as framework source.
145
- const chunkId = /\.(svelte|vue)$/.test(specifier)
146
- ? specifier
147
- : toComponentVirtualId(specifier);
148
-
149
- const fileReferenceId = this.emitFile({ type: 'chunk', id: chunkId });
150
-
151
- componentEntrypointRefs.set(specifier, fileReferenceId);
77
+ await emitBuildEntrypoints({
78
+ pluginContext: this,
79
+ integrations,
80
+ resolveFrom,
81
+ trackedSpecifiers,
82
+ staticEntrypointRefs
152
83
  });
153
84
  },
154
85
 
155
- async writeBundle(this: Rollup.PluginContext, _outputOptions: Rollup.NormalizedOutputOptions, bundle: Rollup.OutputBundle) {
86
+ async writeBundle(
87
+ this: Rollup.PluginContext,
88
+ _outputOptions: Rollup.NormalizedOutputOptions,
89
+ bundle: Rollup.OutputBundle
90
+ ) {
156
91
  const staticModuleMap = buildStaticModuleMap(
157
92
  this,
158
93
  staticEntrypointRefs,
159
94
  componentEntrypointRefs
160
95
  );
161
96
 
162
- const stories = await collectAstroStories(outDir);
97
+ const astroStories = await collectAstroStories(outDir);
163
98
 
164
- if (stories.length === 0) {
99
+ if (astroStories.length === 0) {
165
100
  await writePrerenderedStoriesFile(outDir, {});
166
101
 
167
102
  return;
168
103
  }
169
104
 
170
- const prerenderedStories = await prerenderStories({
171
- stories,
105
+ const prerenderedStories = await prerenderAstroStories({
106
+ astroStories,
172
107
  integrations,
173
108
  sanitization: options.sanitization,
174
109
  storyRulesConfigFilePath,
@@ -183,13 +118,15 @@ export function vitePluginAstroBuildPrerender(options: FrameworkOptions): Plugin
183
118
  };
184
119
  }
185
120
 
121
+ /** Writes the prerendered Astro story payload consumed by the static renderer. */
186
122
  async function writePrerenderedStoriesFile(outDir: string, payload: Record<string, string>) {
187
123
  await mkdir(outDir, { recursive: true });
188
124
  await writeFile(resolve(outDir, PRERENDERED_STORIES_FILE), JSON.stringify(payload), 'utf-8');
189
125
  }
190
126
 
191
- async function prerenderStories(options: {
192
- stories: StoryEntry[];
127
+ /** Renders Astro stories during the static build and stores the HTML by story id. */
128
+ async function prerenderAstroStories(options: {
129
+ astroStories: ProductionStoryEntry[];
193
130
  integrations: Integration[];
194
131
  sanitization?: FrameworkOptions['sanitization'];
195
132
  storyRulesConfigFilePath?: string;
@@ -198,114 +135,24 @@ async function prerenderStories(options: {
198
135
  resolveFrom: string;
199
136
  bundle: Rollup.OutputBundle;
200
137
  }) {
201
- const sanitizationOptions = resolveSanitizationOptions(options.sanitization ?? undefined);
202
- const resolveClientModule = createClientModuleResolver(
203
- options.integrations,
204
- options.staticModuleMap
205
- );
206
- const viteServer = await createStorySsrServer(
207
- options.integrations,
208
- options.trackedSpecifiers,
209
- options.resolveFrom
210
- );
211
- const rulesConfigModule = await loadRulesConfigModule(viteServer, options.storyRulesConfigFilePath);
138
+ const runtime = await createProductionRenderRuntime({
139
+ integrations: options.integrations,
140
+ sanitization: options.sanitization,
141
+ storyRulesConfigFilePath: options.storyRulesConfigFilePath,
142
+ staticModuleMap: options.staticModuleMap,
143
+ trackedSpecifiers: options.trackedSpecifiers,
144
+ resolveFrom: options.resolveFrom
145
+ });
212
146
  const assetPathMap = buildAssetPathMap(options.bundle);
213
147
 
214
- // Inject a passthrough image service before the container renders any
215
- // components. The `image: { service: passthroughImageService() }` config
216
- // passed to Astro above is not sufficient on Astro 6: at render time
217
- // `getConfiguredImageService()` still dynamically imports
218
- // "virtual:image-service", which fails in Vite 7's module runner with
219
- // `InvalidImageService`. Pre-populating globalThis.astroAsset.imageService
220
- // short-circuits that dynamic import. See `lib/passthrough-image-service.ts`.
221
- installPassthroughImageService();
222
-
223
148
  try {
224
- // Load AstroContainer through the SSR module graph so that internal
225
- // classes (SlotString, HTMLString) share the same module instance as the
226
- // Astro components loaded via ssrLoadModule below. Cross-module instanceof
227
- // checks fail when AstroContainer is imported statically (Node.js context)
228
- // and components are loaded via Vite SSR (separate module graph), which
229
- // causes slot HTML to be escaped character-by-character instead of being
230
- // passed through as raw HTML.
231
- const containerModule = await viteServer.ssrLoadModule('astro/container');
232
- const AstroContainerRuntime = containerModule.experimental_AstroContainer as typeof AstroContainer;
233
-
234
- const container = await AstroContainerRuntime.create({
235
- resolve: async (specifier) => {
236
- const mockedModule = resolveStoryModuleMock(specifier);
237
-
238
- if (mockedModule) {
239
- return mockedModule;
240
- }
241
-
242
- const resolution = resolveClientModule(specifier);
243
-
244
- if (resolution) {
245
- return resolution;
246
- }
247
-
248
- return specifier;
249
- }
250
- });
251
-
252
- await addContainerRenderers(container, options.integrations, resolveClientModule, viteServer);
253
-
254
149
  const output: Record<string, string> = {};
255
150
 
256
- for (const story of options.stories) {
257
- const selectedRules = await selectStoryRules({
258
- configModule: rulesConfigModule,
259
- configFilePath: options.storyRulesConfigFilePath,
260
- story: {
261
- id: story.id,
262
- title: story.title,
263
- name: story.name
264
- }
265
- });
266
-
267
- if (selectedRules.moduleMocks.size > 0) {
268
- viteServer.moduleGraph.invalidateAll();
269
- }
270
-
271
- const html = await withStoryRuleCleanups(selectedRules.cleanups, async () => {
272
- return withStoryModuleMocks(selectedRules.moduleMocks, async () => {
273
- const modulePath = resolveImportPath(story.importPath, options.resolveFrom);
274
- const storyModule = await viteServer.ssrLoadModule(modulePath);
275
- const meta = isRecord(storyModule.default) ? storyModule.default : {};
276
- const storyExport = isRecord(storyModule[story.exportName])
277
- ? storyModule[story.exportName]
278
- : {};
279
-
280
- if (typeof meta.component !== 'function') {
281
- throw new Error(
282
- `Unable to prerender story "${story.id}". Missing default export component in ${story.importPath}.`
283
- );
284
- }
285
-
286
- if (storyExport.component && storyExport.component !== meta.component) {
287
- return undefined;
288
- }
289
-
290
- const mergedArgs = mergeStoryArgs(toRecord(meta.args), toRecord(storyExport.args));
291
- const { args, slots } = separateSlots(mergedArgs);
292
- const processedArgs = await processImageMetadata(args);
293
- const sanitizedPayload = sanitizeRenderPayload(
294
- {
295
- args: processedArgs,
296
- slots
297
- },
298
- sanitizationOptions
299
- );
300
-
301
- return container.renderToString(
302
- patchCreateAstroCompat(meta.component) as Parameters<typeof container.renderToString>[0],
303
- {
304
- props: sanitizedPayload.args,
305
- slots: sanitizedPayload.slots
306
- }
307
- );
308
- });
151
+ for (const story of options.astroStories) {
152
+ const html = await renderProductionStoryToHtml({
153
+ story,
154
+ runtime,
155
+ resolveFrom: options.resolveFrom
309
156
  });
310
157
 
311
158
  if (html !== undefined) {
@@ -315,176 +162,29 @@ async function prerenderStories(options: {
315
162
 
316
163
  return output;
317
164
  } finally {
318
- await viteServer.close();
165
+ await runtime.close();
319
166
  }
320
167
  }
321
168
 
322
- async function createStorySsrServer(
323
- integrations: Integration[],
324
- trackedSpecifiers: Set<string>,
325
- resolveFrom: string
326
- ) {
327
- const { getViteConfig, passthroughImageService } = await importAstroConfig(resolveFrom);
328
- const astroConfig = await getViteConfig(
329
- { root: resolveFrom },
330
- {
331
- configFile: false,
332
- integrations: await Promise.all(
333
- integrations.map((integration) => integration.loadIntegration(resolveFrom))
334
- ),
335
- // Use the passthrough image service so nested components that use <Image>
336
- // from astro:assets render as plain <img> tags without triggering image
337
- // optimization (which fails in the Storybook SSR context).
338
- image: { service: passthroughImageService() }
339
- }
340
- )({
341
- mode: 'production',
342
- command: 'serve'
343
- });
344
-
345
- const config = mergeConfig(astroConfig, {
346
- appType: 'custom',
347
- server: {
348
- middlewareMode: true
349
- },
350
- ssr: {
351
- // Force Astro runtime modules to be loaded through Vite's SSR transform
352
- // pipeline rather than being externalized via Node.js native import().
353
- // Without this, the AstroContainer (loaded via ssrLoadModule) and the
354
- // component rendering pipeline may resolve internal classes like
355
- // SlotString/HTMLString from separate module instances, causing
356
- // instanceof checks to fail and slot HTML to be escaped.
357
- noExternal: /^astro(\/.+)?$/
358
- },
359
- plugins: [
360
- createProjectAstroResolutionPlugin(resolveFrom),
361
- vitePluginAstroFontsFallback(),
362
- vitePluginAstroIntegrationOptsFallback(),
363
- vitePluginAstroVueFallback(),
364
- vitePluginAstroRoutesFallback(),
365
- {
366
- name: 'storybook-astro:static-prerender-ssr-stubs',
367
- resolveId(id: string) {
368
- if (trackedSpecifiers.has(id)) {
369
- return `\0storybook-astro-static-prerender-stub:${encodeURIComponent(id)}`;
370
- }
371
- },
372
- load(id: string) {
373
- if (id.startsWith('\0storybook-astro-static-prerender-stub:')) {
374
- return 'export default undefined;';
375
- }
376
- }
377
- }
378
- ]
379
- });
380
-
381
- return createServer(config);
382
- }
383
-
384
- async function loadRulesConfigModule(
385
- viteServer: Awaited<ReturnType<typeof createStorySsrServer>>,
386
- configFilePath?: string
387
- ) {
388
- if (!configFilePath) {
389
- return undefined;
390
- }
391
-
392
- try {
393
- return await ssrLoadModuleWithFsFallback(viteServer, configFilePath, {
394
- fixStacktrace: true
395
- });
396
- } catch (error) {
397
- const reason = error instanceof Error ? error.message : String(error);
398
-
399
- throw new Error(
400
- `Unable to load framework.options.storyRules config module at ${configFilePath}: ${reason}`
401
- );
402
- }
403
- }
404
-
405
- async function addContainerRenderers(
406
- container: Awaited<ReturnType<typeof AstroContainer.create>>,
407
- integrations: Integration[],
408
- resolveClientModule: (specifier: string) => string | undefined,
409
- viteServer: Awaited<ReturnType<typeof createStorySsrServer>>
410
- ) {
411
- for (const integration of integrations) {
412
- const serverRenderer = integration.renderer.server;
413
-
414
- if (serverRenderer) {
415
- const serverRendererModule = await viteServer.ssrLoadModule(serverRenderer.entrypoint);
416
- const renderer = serverRendererModule.default ?? serverRendererModule;
417
-
418
- if (integration.name === 'solid' && isRecord(renderer)) {
419
- container.addServerRenderer({
420
- name: serverRenderer.name,
421
- renderer: {
422
- ...renderer,
423
- name: serverRenderer.name
424
- } as Parameters<typeof container.addServerRenderer>[0]['renderer']
425
- });
426
- } else {
427
- container.addServerRenderer({
428
- name: serverRenderer.name,
429
- renderer
430
- });
431
- }
432
- }
433
-
434
- const clientRenderer = integration.renderer.client;
435
-
436
- if (clientRenderer) {
437
- const resolvedEntrypoint =
438
- resolveClientModule(clientRenderer.entrypoint) ?? clientRenderer.entrypoint;
439
-
440
- container.addClientRenderer({
441
- name: clientRenderer.name,
442
- entrypoint: resolvedEntrypoint
443
- });
444
- }
445
- }
446
- }
447
-
448
- function createClientModuleResolver(
449
- integrations: Integration[],
450
- staticModuleMap: Record<string, string>
451
- ) {
452
- return function resolveClientModule(specifier: string) {
453
- if (Object.hasOwn(staticModuleMap, specifier)) {
454
- return staticModuleMap[specifier];
455
- }
456
-
457
- const normalizedSpecifier = specifier.replace(/\\/g, '/').replace(/\?.*$/, '');
458
-
459
- if (Object.hasOwn(staticModuleMap, normalizedSpecifier)) {
460
- return staticModuleMap[normalizedSpecifier];
461
- }
462
-
463
- for (const integration of integrations) {
464
- const resolution = integration.resolveClient(specifier);
465
-
466
- if (resolution) {
467
- return resolution;
468
- }
469
- }
470
- };
471
- }
472
-
473
- async function collectAstroStories(outDir: string): Promise<StoryEntry[]> {
169
+ /** Reads the built Storybook index and keeps only Astro stories that can be prerendered. */
170
+ async function collectAstroStories(outDir: string): Promise<ProductionStoryEntry[]> {
474
171
  const indexFile = resolve(outDir, 'index.json');
475
172
  const indexRaw = await readFile(indexFile, 'utf-8');
476
173
  const indexJson = JSON.parse(indexRaw) as StoryIndex;
477
174
 
175
+ // Static prerender only owns Astro stories. Framework-rendered stories stay
176
+ // with Storybook's normal preview pipeline and are not pre-rendered here.
478
177
  return Object.values(indexJson.entries ?? {})
479
178
  .filter((entry) => entry.type === 'story' && entry.componentPath?.endsWith('.astro'))
480
179
  .map((entry) => {
481
- if (!entry.id || !entry.importPath || !entry.exportName) {
180
+ if (!entry.id || !entry.importPath || !entry.exportName || !entry.componentPath) {
482
181
  throw new Error(`Encountered an invalid Storybook index entry in ${indexFile}.`);
483
182
  }
484
183
 
485
184
  return {
486
185
  id: entry.id,
487
186
  importPath: entry.importPath,
187
+ componentPath: entry.componentPath,
488
188
  exportName: entry.exportName,
489
189
  title: entry.title,
490
190
  name: entry.name
@@ -492,113 +192,7 @@ async function collectAstroStories(outDir: string): Promise<StoryEntry[]> {
492
192
  });
493
193
  }
494
194
 
495
- function mergeStoryArgs(
496
- metaArgs: Record<string, unknown> | undefined,
497
- storyArgs: Record<string, unknown> | undefined
498
- ) {
499
- return {
500
- ...(metaArgs ?? {}),
501
- ...(storyArgs ?? {})
502
- };
503
- }
504
-
505
- function separateSlots(inputArgs: Record<string, unknown>) {
506
- const args = { ...inputArgs };
507
- const slotsCandidate = args.slots;
508
-
509
- delete args.slots;
510
-
511
- if (!isRecord(slotsCandidate)) {
512
- return {
513
- args,
514
- slots: {}
515
- };
516
- }
517
-
518
- return {
519
- args,
520
- slots: slotsCandidate as Record<string, string>
521
- };
522
- }
523
-
524
- function resolveImportPath(importPath: string, resolveFrom: string) {
525
- if (importPath.startsWith('./')) {
526
- return resolve(resolveFrom, importPath.slice(2));
527
- }
528
-
529
- return resolve(resolveFrom, importPath);
530
- }
531
-
532
- function isRecord(value: unknown): value is Record<string, unknown> {
533
- return typeof value === 'object' && value !== null;
534
- }
535
-
536
- function toRecord(value: unknown): Record<string, unknown> | undefined {
537
- if (!isRecord(value)) {
538
- return undefined;
539
- }
540
-
541
- return value;
542
- }
543
-
544
- function collectTrackedSpecifiers(integrations: Integration[]) {
545
- const specifiers = new Set<string>(['astro:scripts/page.js', 'astro:scripts/before-hydration.js']);
546
-
547
- integrations.forEach((integration) => {
548
- const entrypoint = integration.renderer.client?.entrypoint;
549
-
550
- if (entrypoint) {
551
- specifiers.add(entrypoint);
552
- }
553
- });
554
-
555
- return specifiers;
556
- }
557
-
558
- function buildStaticModuleMap(
559
- pluginContext: Rollup.PluginContext,
560
- staticEntrypointRefs: Map<string, string>,
561
- componentEntrypointRefs: Map<string, string>
562
- ) {
563
- const map: Record<string, string> = {};
564
-
565
- staticEntrypointRefs.forEach((fileReferenceId, specifier) => {
566
- const fileName = pluginContext.getFileName(fileReferenceId);
567
-
568
- if (fileName) {
569
- map[specifier] = toPublicPath(fileName);
570
- }
571
- });
572
-
573
- componentEntrypointRefs.forEach((fileReferenceId, specifier) => {
574
- const fileName = pluginContext.getFileName(fileReferenceId);
575
-
576
- if (fileName) {
577
- map[specifier] = toPublicPath(fileName);
578
- }
579
- });
580
-
581
- return map;
582
- }
583
-
584
- function toStaticVirtualId(specifier: string) {
585
- return `virtual:astro-static-module/${encodeURIComponent(specifier)}`;
586
- }
587
-
588
- function toComponentVirtualId(specifier: string) {
589
- // Append a non-extension suffix so framework compile plugins (e.g. vite-plugin-svelte)
590
- // don't match the virtual module ID by extension and try to compile the JS re-export stub.
591
- return `virtual:astro-component-module/${encodeURIComponent(specifier)}?component-wrapper`;
592
- }
593
-
594
- function isClientEntrypoint(specifier: string) {
595
- return specifier.startsWith('@astrojs/') && specifier.endsWith('/client.js');
596
- }
597
-
598
- function toPublicPath(fileName: string) {
599
- return `./${fileName}`;
600
- }
601
-
195
+ /** Builds lookup tables that map original asset paths to emitted static asset URLs. */
602
196
  function buildAssetPathMap(bundle: Rollup.OutputBundle): Map<string, string> {
603
197
  const exactMap = new Map<string, string>();
604
198
  const stemMap = new Map<string, string>();
@@ -632,7 +226,11 @@ function buildAssetPathMap(bundle: Rollup.OutputBundle): Map<string, string> {
632
226
  return { exactMap, stemMap } as unknown as Map<string, string>;
633
227
  }
634
228
 
635
- function rewriteAssetPaths(html: string, assetPathMap: ReturnType<typeof buildAssetPathMap>): string {
229
+ /** Rewrites dev-only /@fs/ asset URLs in prerendered HTML to emitted build asset paths. */
230
+ function rewriteAssetPaths(
231
+ html: string,
232
+ assetPathMap: ReturnType<typeof buildAssetPathMap>
233
+ ): string {
636
234
  const { exactMap, stemMap } = assetPathMap as unknown as {
637
235
  exactMap: Map<string, string>;
638
236
  stemMap: Map<string, string>;
@@ -642,11 +240,8 @@ function rewriteAssetPaths(html: string, assetPathMap: ReturnType<typeof buildAs
642
240
  return html;
643
241
  }
644
242
 
645
- // Match /@fs/ URLs in HTML attribute values, stripping any query string.
646
- // Vite dev server uses /@fs//absolute/path for filesystem assets; in static
647
- // builds these are emitted as /_astro/name.hash.ext output assets.
648
- // The character class deliberately excludes only quotes (the attribute
649
- // delimiters) so that paths containing spaces are captured in full.
243
+ // Prerendering happens through a Vite SSR server, so image/style URLs can
244
+ // still point at dev-only /@fs/ paths. Rewrite them to the emitted assets.
650
245
  return html.replace(/\/@fs\/[^"']+/g, (match) => {
651
246
  const pathOnly = match.replace(/\?.*$/, '');
652
247
  const fsPath = pathOnly.slice('/@fs'.length);
@@ -672,169 +267,3 @@ function rewriteAssetPaths(html: string, assetPathMap: ReturnType<typeof buildAs
672
267
  return match;
673
268
  });
674
269
  }
675
-
676
- async function collectHydratableSourceModules(srcRoot: string): Promise<string[]> {
677
- const modules: string[] = [];
678
-
679
- async function walk(directory: string) {
680
- let entries: Dirent[];
681
-
682
- try {
683
- entries = await readdir(directory, { withFileTypes: true });
684
- } catch {
685
- return;
686
- }
687
-
688
- await Promise.all(
689
- entries.map(async (entry) => {
690
- const absolutePath = resolve(directory, entry.name);
691
-
692
- if (entry.isDirectory()) {
693
- await walk(absolutePath);
694
-
695
- return;
696
- }
697
-
698
- if (!entry.isFile()) {
699
- return;
700
- }
701
-
702
- const normalizedPath = absolutePath.replace(/\\/g, '/');
703
-
704
- if (!isHydratableSourceFile(normalizedPath)) {
705
- return;
706
- }
707
-
708
- if (isNonHydratableSourceFile(normalizedPath)) {
709
- return;
710
- }
711
-
712
- modules.push(normalizedPath);
713
- })
714
- );
715
- }
716
-
717
- await walk(srcRoot);
718
-
719
- return modules;
720
- }
721
-
722
- function isHydratableSourceFile(input: string) {
723
- // Only framework component extensions — plain .js/.ts are utilities/data
724
- // files that are not hydratable client components and must not be emitted
725
- // as entry chunks (they may lack a default export, causing a build error).
726
- return /\.(jsx|tsx|vue|svelte)$/.test(input);
727
- }
728
-
729
- function isNonHydratableSourceFile(input: string) {
730
- return /\.stories\.[jt]sx?$|\.stories\.vue$|\.stories\.svelte$|\.(spec|test)\.[jt]sx?$/.test(
731
- input
732
- );
733
- }
734
-
735
- function patchCreateAstroCompat(component: unknown): AstroComponentFactory {
736
- if (typeof component !== 'function') {
737
- throw new Error('Expected Astro component factory to be a function.');
738
- }
739
-
740
- const originalComponent = component as AstroComponentFactory;
741
- const wrapped = ((result: AstroCreateResult, props: unknown, slots: unknown) => {
742
- if (result && typeof result.createAstro === 'function') {
743
- const originalCreateAstro = result.createAstro;
744
- const runtimeExpectsAstroGlobal = originalCreateAstro.length >= 3;
745
-
746
- result.createAstro = (...args: unknown[]) => {
747
- if (args.length === 3 && !runtimeExpectsAstroGlobal) {
748
- return originalCreateAstro(args[1], args[2]);
749
- }
750
-
751
- return originalCreateAstro(...args);
752
- };
753
- }
754
-
755
- return originalComponent(result, props, slots);
756
- }) as AstroComponentFactory;
757
-
758
- wrapped.isAstroComponentFactory = originalComponent.isAstroComponentFactory;
759
- wrapped.moduleId = originalComponent.moduleId;
760
- wrapped.propagation = originalComponent.propagation;
761
-
762
- return wrapped;
763
- }
764
-
765
- async function processImageMetadata(
766
- args: Record<string, unknown>
767
- ): Promise<Record<string, unknown>> {
768
- const processed: Record<string, unknown> = {};
769
-
770
- for (const [key, value] of Object.entries(args)) {
771
- if (isImageMetadata(value)) {
772
- // Keep ImageMetadata as a plain object — Astro's image service checks
773
- // isESMImportedImage (typeof src === 'object') and skips the /@fs/ string
774
- // validation that throws LocalImageUsedWrongly. Converting to a URL string
775
- // causes that error when the string starts with /@fs/.
776
- processed[key] = value;
777
-
778
- continue;
779
- }
780
-
781
- if (Array.isArray(value)) {
782
- processed[key] = await Promise.all(
783
- value.map(async (item) => {
784
- if (isImageMetadata(item)) {
785
- return item;
786
- }
787
-
788
- if (isRecord(item)) {
789
- return processImageMetadata(item);
790
- }
791
-
792
- return item;
793
- })
794
- );
795
-
796
- continue;
797
- }
798
-
799
- if (isRecord(value)) {
800
- processed[key] = await processImageMetadata(value);
801
-
802
- continue;
803
- }
804
-
805
- processed[key] = value;
806
- }
807
-
808
- return processed;
809
- }
810
-
811
- function isImageMetadata(value: unknown): value is Record<string, unknown> {
812
- return (
813
- isRecord(value) &&
814
- typeof value.src === 'string' &&
815
- ('width' in value || 'height' in value || 'format' in value)
816
- );
817
- }
818
-
819
-
820
- function createProjectAstroResolutionPlugin(resolveFrom: string): Plugin {
821
- const require = createRequire(import.meta.url);
822
-
823
- return {
824
- name: 'storybook-astro:resolve-project-astro-prerender',
825
- enforce: 'pre',
826
- resolveId(id: string) {
827
- if (id !== 'astro' && !id.startsWith('astro/')) {
828
- return null;
829
- }
830
-
831
- try {
832
- return require.resolve(id, {
833
- paths: [resolveFrom]
834
- });
835
- } catch {
836
- return null;
837
- }
838
- }
839
- };
840
- }