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

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 (87) 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-OUEDTRBO.js → chunk-B454DGX6.js} +259 -67
  8. package/dist/chunk-B454DGX6.js.map +1 -0
  9. package/dist/chunk-B5HHF6FC.js +116 -0
  10. package/dist/chunk-B5HHF6FC.js.map +1 -0
  11. package/dist/chunk-CU57AJUW.js +1402 -0
  12. package/dist/chunk-CU57AJUW.js.map +1 -0
  13. package/dist/{chunk-DNGQBPT7.js → chunk-PUTCAN6X.js} +5 -2
  14. package/dist/{chunk-DNGQBPT7.js.map → chunk-PUTCAN6X.js.map} +1 -1
  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 +205 -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/lib/revive-dates.test.ts +106 -0
  47. package/src/lib/revive-dates.ts +51 -0
  48. package/src/middleware.ts +29 -200
  49. package/src/module-mocks.ts +153 -5
  50. package/src/preset.ts +38 -8
  51. package/src/productionRenderRuntime.ts +187 -0
  52. package/src/rules.test.ts +52 -4
  53. package/src/rules.ts +54 -7
  54. package/src/server/index.ts +101 -31
  55. package/src/storyRulesRuntime.ts +34 -0
  56. package/src/storySsrVite.ts +240 -0
  57. package/src/types.ts +0 -9
  58. package/src/virtual.d.ts +17 -3
  59. package/src/vite/{astroFilesVirtualModulePlugin.ts → astroFilesPlugin.ts} +4 -4
  60. package/src/vite/sanitizeConfigPlugin.ts +18 -0
  61. package/src/vite/{storybookAstroServerAuthConfigVirtualModulePlugin.test.ts → serverAuthPlugin.test.ts} +7 -10
  62. package/src/vite/{storybookAstroServerAuthConfigVirtualModulePlugin.ts → serverAuthPlugin.ts} +6 -9
  63. package/src/vite/serverRuntimePlugin.ts +109 -0
  64. package/src/vite/{storybookAstroRulesConfigVirtualModulePlugin.ts → storyRulesPlugin.ts} +6 -7
  65. package/src/vite/{createVirtualModulePlugin.test.ts → virtualModulePlugin.test.ts} +5 -5
  66. package/src/vite/{createVirtualModulePlugin.ts → virtualModulePlugin.ts} +2 -2
  67. package/src/viteAstroContainerRenderersPlugin.ts +72 -2
  68. package/src/vitePluginAstroBuildPrerender.ts +75 -646
  69. package/src/vitePluginAstroBuildServer.ts +217 -165
  70. package/src/vitePluginAstroBuildShared.test.ts +87 -0
  71. package/src/vitePluginAstroBuildShared.ts +465 -0
  72. package/src/vitePluginStoryModuleMocks.ts +29 -0
  73. package/src/viteStorybookAstroMiddlewarePlugin.ts +8 -0
  74. package/src/viteStorybookAstroRendererPlugin.ts +13 -6
  75. package/src/viteStorybookRendererFallbackPlugin.ts +2 -2
  76. package/dist/chunk-OUEDTRBO.js.map +0 -1
  77. package/dist/chunk-PBISP7PA.js +0 -1137
  78. package/dist/chunk-PBISP7PA.js.map +0 -1
  79. package/dist/chunk-PJEDXZVN.js.map +0 -1
  80. package/dist/chunk-POHTFYST.js.map +0 -1
  81. package/dist/node/index.d.ts +0 -10
  82. package/dist/node/index.js +0 -10
  83. package/dist/node/index.js.map +0 -1
  84. package/src/vite/storybookAstroSanitizationConfigVirtualModulePlugin.ts +0 -21
  85. /package/dist/{chunk-T7NWIO5S.js.map → chunk-2EABPTOY.js.map} +0 -0
  86. /package/dist/{chunk-4SWPVM6R.js.map → chunk-WUTCMEF5.js.map} +0 -0
  87. /package/dist/{viteStorybookAstroMiddlewarePlugin-2EFKTECT.js.map → viteStorybookAstroMiddlewarePlugin-UB6ZLJ4B.js.map} +0 -0
@@ -1,15 +1,24 @@
1
- import type { Dirent } from 'node:fs';
2
- import { readdir } from 'node:fs/promises';
3
1
  import { dirname, resolve } from 'node:path';
4
2
  import { fileURLToPath } from 'node:url';
3
+ import { mkdir, writeFile } from 'node:fs/promises';
5
4
  import { build, type Rollup } from 'vite';
5
+ import { resolveRulesConfigFilePath } from './rules-options.ts';
6
6
  import type { FrameworkOptions } from './types.ts';
7
+ import {
8
+ buildStaticModuleMap,
9
+ buildSnapshotFilePath,
10
+ collectHydratedComponentPaths,
11
+ copyRuntimeSnapshot,
12
+ collectTrackedSpecifiers,
13
+ emitBuildEntrypoints,
14
+ loadVirtualBuildModule,
15
+ resolveVirtualBuildModuleId,
16
+ } from './vitePluginAstroBuildShared.ts';
7
17
  import { mergeWithAstroConfig } from './vitePluginAstro.ts';
8
18
  import { viteAstroContainerRenderersPlugin } from './viteAstroContainerRenderersPlugin.ts';
9
- import { astroFilesVirtualModulePlugin } from './vite/astroFilesVirtualModulePlugin.ts';
10
- import { storybookAstroStoryRulesConfigVirtualModulePlugin } from './vite/storybookAstroRulesConfigVirtualModulePlugin.ts';
11
- import { storybookAstroSanitizationConfigVirtualModulePlugin } from './vite/storybookAstroSanitizationConfigVirtualModulePlugin.ts';
12
- import { storybookAstroServerAuthConfigVirtualModulePlugin } from './vite/storybookAstroServerAuthConfigVirtualModulePlugin.ts';
19
+ import { sanitizeConfigPlugin } from './vite/sanitizeConfigPlugin.ts';
20
+ import { serverAuthPlugin } from './vite/serverAuthPlugin.ts';
21
+ import { serverRuntimePlugin } from './vite/serverRuntimePlugin.ts';
13
22
 
14
23
  const moduleRoot = resolve(dirname(fileURLToPath(import.meta.url)), '.');
15
24
  // packageRoot works regardless of whether this file is running from src/ or dist/
@@ -18,10 +27,8 @@ const packageRoot = resolve(moduleRoot, '..');
18
27
  export function vitePluginAstroBuildServer(options: FrameworkOptions) {
19
28
  const integrations = options.integrations ?? [];
20
29
  const resolveFrom = options.resolveFrom ?? process.cwd();
21
- const storiesMap = new Map<string, Set<string>>();
22
30
  const trackedSpecifiers = collectTrackedSpecifiers(integrations);
23
31
  const staticEntrypointRefs = new Map<string, string>();
24
- const componentEntrypointRefs = new Map<string, string>();
25
32
  let storybookStaticOutDir = resolve(resolveFrom, 'storybook-static');
26
33
 
27
34
  return {
@@ -33,108 +40,88 @@ export function vitePluginAstroBuildServer(options: FrameworkOptions) {
33
40
  storybookStaticOutDir = resolve(resolveFrom, config.build.outDir ?? 'storybook-static');
34
41
  },
35
42
 
36
- resolveId(id: string, importer?: string) {
37
- if (id.endsWith('.astro') && importer) {
38
- const absoluteAstroPath = resolve(dirname(importer), id);
39
-
40
- if (!storiesMap.has(absoluteAstroPath)) {
41
- storiesMap.set(absoluteAstroPath, new Set());
42
- }
43
-
44
- storiesMap.get(absoluteAstroPath)?.add(importer);
45
- }
46
-
47
- if (id.startsWith('virtual:astro-static-module/')) {
48
- return `\0${id}`;
49
- }
50
-
51
- if (id.startsWith('virtual:astro-component-module/')) {
52
- return `\0${id}`;
53
- }
43
+ resolveId(id: string) {
44
+ return resolveVirtualBuildModuleId(id);
54
45
  },
55
46
 
56
47
  load(id: string) {
57
- if (id.startsWith('\0virtual:astro-static-module/')) {
58
- const encodedSpecifier = id.replace('\0virtual:astro-static-module/', '');
59
- const specifier = decodeURIComponent(encodedSpecifier);
60
-
61
- if (isClientEntrypoint(specifier)) {
62
- return [`export { default } from '${specifier}';`, `export * from '${specifier}';`].join('\n');
63
- }
64
-
65
- return [`import '${specifier}';`, 'export default undefined;'].join('\n');
66
- }
67
-
68
- if (id.startsWith('\0virtual:astro-component-module/')) {
69
- const encodedSpecifier = id.replace('\0virtual:astro-component-module/', '');
70
- const specifier = decodeURIComponent(encodedSpecifier);
71
-
72
- return [`export { default } from '${specifier}';`, `export * from '${specifier}';`].join('\n');
73
- }
48
+ return loadVirtualBuildModule(id);
74
49
  },
75
50
 
76
51
  async buildStart(this: Rollup.PluginContext) {
77
- integrations.forEach((integration) => {
78
- const entrypoint = integration.renderer.client?.entrypoint;
79
-
80
- if (entrypoint) {
81
- this.addWatchFile(entrypoint);
82
- }
83
- });
84
-
85
- trackedSpecifiers.forEach((specifier) => {
86
- const fileReferenceId = this.emitFile({
87
- type: 'chunk',
88
- id: toStaticVirtualId(specifier)
89
- });
90
-
91
- staticEntrypointRefs.set(specifier, fileReferenceId);
92
- });
93
-
94
- const srcRoot = resolve(resolveFrom, 'src/components');
95
- const specifiers = await collectHydratableSourceModules(srcRoot);
96
-
97
- specifiers.forEach((specifier) => {
98
- const fileReferenceId = this.emitFile({
99
- type: 'chunk',
100
- id: toComponentVirtualId(specifier)
101
- });
102
-
103
- componentEntrypointRefs.set(specifier, fileReferenceId);
52
+ await emitBuildEntrypoints({
53
+ pluginContext: this,
54
+ integrations,
55
+ resolveFrom,
56
+ trackedSpecifiers,
57
+ staticEntrypointRefs
104
58
  });
105
59
  },
106
60
 
107
- async writeBundle(this: Rollup.PluginContext) {
108
- const astroComponents = Array.from(storiesMap.keys());
109
- const staticModuleMap = buildStaticModuleMap(
110
- this,
111
- staticEntrypointRefs,
112
- componentEntrypointRefs
113
- );
61
+ async writeBundle(
62
+ this: Rollup.PluginContext,
63
+ _outputOptions: Rollup.NormalizedOutputOptions
64
+ ) {
114
65
  const serverOutDir = resolve(dirname(storybookStaticOutDir), 'storybook-server');
66
+ const snapshotDirName = 'project';
67
+ const astroStories = await collectAstroStories(storybookStaticOutDir, resolveFrom);
68
+ const storyAstroComponentPaths = Array.from(new Set(astroStories.map((story) => story.componentPath)));
69
+ const componentPathMap = buildComponentPathMap(storyAstroComponentPaths, resolveFrom, snapshotDirName);
70
+ const storyRulesConfigFilePath = resolveRulesConfigFilePath(options.storyRules, resolveFrom);
71
+ const trackedModuleMap = buildStaticModuleMap(this, staticEntrypointRefs, new Map());
72
+ const hydratedComponentAssets = await buildHydratedComponentAssets({
73
+ componentPaths: storyAstroComponentPaths,
74
+ integrations,
75
+ resolveFrom,
76
+ outDir: storybookStaticOutDir
77
+ });
78
+ const staticModuleMap = addSnapshotModuleAliases({
79
+ ...trackedModuleMap,
80
+ ...hydratedComponentAssets.staticModuleMap
81
+ }, {
82
+ resolveFrom,
83
+ snapshotRoot: resolve(serverOutDir, snapshotDirName),
84
+ snapshotDirName
85
+ });
86
+ const staticCssMap = hydratedComponentAssets.staticCssMap;
115
87
 
116
88
  await buildAstroServer({
117
- astroComponents,
118
89
  integrations,
119
90
  sanitization: options.sanitization,
120
91
  storyRules: options.storyRules,
121
92
  server: options.server,
122
93
  outDir: serverOutDir,
94
+ snapshotDirName,
95
+ componentPathMap,
123
96
  staticModuleMap,
97
+ staticCssMap,
98
+ trackedSpecifiers: Array.from(trackedSpecifiers),
124
99
  resolveFrom
125
100
  });
101
+
102
+ await copyRuntimeSnapshot({
103
+ resolveFrom,
104
+ snapshotRoot: resolve(serverOutDir, snapshotDirName),
105
+ snapshotDirName,
106
+ astroComponents: storyAstroComponentPaths,
107
+ storyRulesConfigFilePath
108
+ });
126
109
  }
127
110
  };
128
111
  }
129
112
 
113
+ /** Builds the standalone Astro render server used by server-mode Storybook output. */
130
114
  async function buildAstroServer(options: {
131
- astroComponents: string[];
132
- integrations: FrameworkOptions['integrations'];
115
+ integrations: NonNullable<FrameworkOptions['integrations']>;
133
116
  sanitization?: FrameworkOptions['sanitization'];
134
117
  storyRules?: FrameworkOptions['storyRules'];
135
118
  server?: FrameworkOptions['server'];
136
119
  outDir: string;
120
+ snapshotDirName: string;
121
+ componentPathMap: Record<string, string>;
137
122
  staticModuleMap: Record<string, string>;
123
+ staticCssMap: Record<string, string[]>;
124
+ trackedSpecifiers: string[];
138
125
  resolveFrom: string;
139
126
  }) {
140
127
  const buildConfig = {
@@ -154,10 +141,18 @@ async function buildAstroServer(options: {
154
141
  }
155
142
  },
156
143
  plugins: [
157
- astroFilesVirtualModulePlugin(options.astroComponents),
158
- storybookAstroSanitizationConfigVirtualModulePlugin(options.sanitization),
159
- storybookAstroStoryRulesConfigVirtualModulePlugin(options.storyRules, options.resolveFrom),
160
- storybookAstroServerAuthConfigVirtualModulePlugin(options.server),
144
+ sanitizeConfigPlugin(options.sanitization),
145
+ serverAuthPlugin(options.server),
146
+ serverRuntimePlugin({
147
+ integrations: options.integrations,
148
+ storyRules: options.storyRules,
149
+ resolveFrom: options.resolveFrom,
150
+ snapshotDirName: options.snapshotDirName,
151
+ componentPathMap: options.componentPathMap,
152
+ staticModuleMap: options.staticModuleMap,
153
+ staticCssMap: options.staticCssMap,
154
+ trackedSpecifiers: options.trackedSpecifiers
155
+ }),
161
156
  viteAstroContainerRenderersPlugin(options.integrations, {
162
157
  mode: 'production',
163
158
  staticModuleMap: options.staticModuleMap
@@ -176,114 +171,171 @@ async function buildAstroServer(options: {
176
171
  await build(finalConfig);
177
172
  }
178
173
 
179
- function collectTrackedSpecifiers(integrations: FrameworkOptions['integrations']) {
180
- const specifiers = new Set<string>(['astro:scripts/page.js', 'astro:scripts/before-hydration.js']);
181
-
182
- integrations.forEach((integration) => {
183
- const entrypoint = integration.renderer.client?.entrypoint;
174
+ /** Rewrites Astro component module ids so the standalone server loads them from the snapshot tree. */
175
+ function buildComponentPathMap(
176
+ astroComponents: string[],
177
+ resolveFrom: string,
178
+ snapshotDirName: string
179
+ ) {
180
+ // The built render server loads Astro component modules from the snapshot,
181
+ // not from the original project root that existed during the build.
182
+ return Object.fromEntries(
183
+ astroComponents.map((componentPath) => [
184
+ componentPath,
185
+ buildSnapshotFilePath(resolveFrom, componentPath, snapshotDirName).replace(
186
+ new RegExp(`^${snapshotDirName}/`),
187
+ ''
188
+ )
189
+ ])
190
+ );
191
+ }
184
192
 
185
- if (entrypoint) {
186
- specifiers.add(entrypoint);
187
- }
188
- });
193
+ async function collectAstroStories(outDir: string, resolveFrom: string) {
194
+ const indexFile = resolve(outDir, 'index.json');
195
+ const indexRaw = await import('node:fs/promises').then(({ readFile }) => readFile(indexFile, 'utf-8'));
196
+ const indexJson = JSON.parse(indexRaw) as {
197
+ entries?: Record<string, { type?: string; componentPath?: string; importPath?: string; exportName?: string }>
198
+ };
189
199
 
190
- return specifiers;
200
+ return Object.values(indexJson.entries ?? [])
201
+ .filter((entry) => entry.type === 'story' && entry.componentPath?.endsWith('.astro'))
202
+ .map((entry) => ({
203
+ componentPath: entry.componentPath?.startsWith('./') || entry.componentPath?.startsWith('../')
204
+ ? resolve(resolveFrom, entry.componentPath)
205
+ : entry.componentPath
206
+ }))
207
+ .filter((entry): entry is { componentPath: string } => Boolean(entry.componentPath));
191
208
  }
192
209
 
193
- function buildStaticModuleMap(
194
- pluginContext: Rollup.PluginContext,
195
- staticEntrypointRefs: Map<string, string>,
196
- componentEntrypointRefs: Map<string, string>
197
- ) {
198
- const map: Record<string, string> = {};
210
+ async function buildHydratedComponentAssets(options: {
211
+ componentPaths: string[];
212
+ integrations: NonNullable<FrameworkOptions['integrations']>;
213
+ resolveFrom: string;
214
+ outDir: string;
215
+ }) {
216
+ const hydratedComponentPaths = Array.from(
217
+ new Set((await Promise.all(options.componentPaths.map((componentPath) => collectHydratedComponentPaths(componentPath)))).flat())
218
+ );
199
219
 
200
- staticEntrypointRefs.forEach((fileReferenceId, specifier) => {
201
- const fileName = pluginContext.getFileName(fileReferenceId);
220
+ if (hydratedComponentPaths.length === 0) {
221
+ return {
222
+ staticModuleMap: {},
223
+ staticCssMap: {}
224
+ };
225
+ }
202
226
 
203
- if (fileName) {
204
- map[specifier] = toPublicPath(fileName);
227
+ const clientEntrypoints = Array.from(
228
+ new Set(
229
+ options.integrations
230
+ .map((integration) => integration.renderer.client?.entrypoint)
231
+ .filter((entrypoint): entrypoint is string => Boolean(entrypoint))
232
+ )
233
+ );
234
+ const entryNames = Object.fromEntries(
235
+ [
236
+ ...hydratedComponentPaths.map((componentPath, index) => [`component-${index}`, componentPath]),
237
+ ...clientEntrypoints.map((entrypoint, index) => [`renderer-${index}`, entrypoint])
238
+ ]
239
+ );
240
+ const buildConfig = {
241
+ root: options.resolveFrom,
242
+ build: {
243
+ write: false,
244
+ outDir: options.outDir,
245
+ emptyOutDir: false,
246
+ manifest: false,
247
+ rollupOptions: {
248
+ input: entryNames,
249
+ preserveEntrySignatures: 'strict',
250
+ output: {
251
+ entryFileNames: '_astro/[name]-[hash].js',
252
+ chunkFileNames: '_astro/[name]-[hash].js',
253
+ assetFileNames: '_astro/[name]-[hash][extname]'
254
+ }
255
+ }
205
256
  }
206
- });
257
+ };
258
+ const finalConfig = await mergeWithAstroConfig(
259
+ buildConfig,
260
+ options.integrations,
261
+ options.resolveFrom,
262
+ 'production',
263
+ 'build'
264
+ );
265
+ const buildOutput = await build(finalConfig);
266
+ const output = Array.isArray(buildOutput) ? buildOutput.flatMap((result) => result.output) : buildOutput.output;
267
+ const staticModuleMap: Record<string, string> = {};
268
+ const staticCssMap: Record<string, string[]> = {};
207
269
 
208
- componentEntrypointRefs.forEach((fileReferenceId, specifier) => {
209
- const fileName = pluginContext.getFileName(fileReferenceId);
270
+ for (const item of output) {
271
+ await writeBuildOutputFile(options.outDir, item);
210
272
 
211
- if (fileName) {
212
- map[specifier] = toPublicPath(fileName);
273
+ if (item.type !== 'chunk' || !item.facadeModuleId) {
274
+ continue;
213
275
  }
214
- });
215
276
 
216
- return map;
217
- }
218
-
219
- function toStaticVirtualId(specifier: string) {
220
- return `virtual:astro-static-module/${encodeURIComponent(specifier)}`;
221
- }
222
-
223
- function toComponentVirtualId(specifier: string) {
224
- return `virtual:astro-component-module/${encodeURIComponent(specifier)}`;
225
- }
277
+ const normalizedFacadeId = item.facadeModuleId.replace(/\\/g, '/');
278
+ const originalInputSpecifier = entryNames[item.name];
226
279
 
227
- function isClientEntrypoint(specifier: string) {
228
- return specifier.startsWith('@astrojs/') && specifier.endsWith('/client.js');
229
- }
280
+ staticModuleMap[normalizedFacadeId] = `./${item.fileName}`;
230
281
 
231
- function toPublicPath(fileName: string) {
232
- return `./${fileName}`;
233
- }
282
+ if (originalInputSpecifier && originalInputSpecifier !== normalizedFacadeId) {
283
+ staticModuleMap[originalInputSpecifier] = `./${item.fileName}`;
284
+ }
234
285
 
235
- async function collectHydratableSourceModules(srcRoot: string): Promise<string[]> {
236
- const modules: string[] = [];
286
+ const importedCss = Array.from((item.viteMetadata?.importedCss ?? new Set<string>()).values());
237
287
 
238
- async function walk(directory: string) {
239
- let entries: Dirent[];
288
+ if (importedCss.length > 0) {
289
+ staticCssMap[normalizedFacadeId] = importedCss.map((fileName) => `./${fileName}`);
240
290
 
241
- try {
242
- entries = await readdir(directory, { withFileTypes: true });
243
- } catch {
244
- return;
291
+ if (originalInputSpecifier && originalInputSpecifier !== normalizedFacadeId) {
292
+ staticCssMap[originalInputSpecifier] = importedCss.map((fileName) => `./${fileName}`);
293
+ }
245
294
  }
295
+ }
246
296
 
247
- await Promise.all(
248
- entries.map(async (entry) => {
249
- const absolutePath = resolve(directory, entry.name);
250
-
251
- if (entry.isDirectory()) {
252
- await walk(absolutePath);
297
+ return {
298
+ staticModuleMap,
299
+ staticCssMap
300
+ };
301
+ }
253
302
 
254
- return;
255
- }
303
+ async function writeBuildOutputFile(outDir: string, item: Rollup.OutputAsset | Rollup.OutputChunk) {
304
+ const outputPath = resolve(outDir, item.fileName);
256
305
 
257
- if (!entry.isFile()) {
258
- return;
259
- }
306
+ await mkdir(dirname(outputPath), { recursive: true });
260
307
 
261
- const normalizedPath = absolutePath.replace(/\\/g, '/');
308
+ if (item.type === 'asset') {
309
+ await writeFile(outputPath, item.source);
262
310
 
263
- if (!isHydratableSourceFile(normalizedPath)) {
264
- return;
265
- }
311
+ return;
312
+ }
266
313
 
267
- if (isNonHydratableSourceFile(normalizedPath)) {
268
- return;
269
- }
314
+ await writeFile(outputPath, item.code);
315
+ }
270
316
 
271
- modules.push(normalizedPath);
272
- })
273
- );
317
+ function addSnapshotModuleAliases(
318
+ staticModuleMap: Record<string, string>,
319
+ options: {
320
+ resolveFrom: string;
321
+ snapshotRoot: string;
322
+ snapshotDirName: string;
274
323
  }
324
+ ) {
325
+ const mapWithSnapshotPaths = { ...staticModuleMap };
275
326
 
276
- await walk(srcRoot);
327
+ for (const [sourcePath, builtPath] of Object.entries(staticModuleMap)) {
328
+ if (!sourcePath.startsWith('/')) {
329
+ continue;
330
+ }
277
331
 
278
- return modules;
279
- }
332
+ const snapshotPath = resolve(
333
+ dirname(options.snapshotRoot),
334
+ buildSnapshotFilePath(options.resolveFrom, sourcePath, options.snapshotDirName)
335
+ ).replace(/\\/g, '/');
280
336
 
281
- function isHydratableSourceFile(input: string) {
282
- return /\.(jsx|tsx|vue|svelte|js|ts)$/.test(input);
283
- }
337
+ mapWithSnapshotPaths[snapshotPath] = builtPath;
338
+ }
284
339
 
285
- function isNonHydratableSourceFile(input: string) {
286
- return /\.stories\.[jt]sx?$|\.stories\.vue$|\.stories\.svelte$|\.(spec|test)\.[jt]sx?$/.test(
287
- input
288
- );
340
+ return mapWithSnapshotPaths;
289
341
  }
@@ -0,0 +1,87 @@
1
+ import { mkdtemp, rm, writeFile } from 'node:fs/promises';
2
+ import { tmpdir } from 'node:os';
3
+ import { join } from 'node:path';
4
+ import { afterEach, beforeEach, describe, expect, test } from 'vitest';
5
+ import { collectHydratedComponentPaths } from './vitePluginAstroBuildShared.ts';
6
+
7
+ describe('collectHydratedComponentPaths', () => {
8
+ let tmpDir: string;
9
+
10
+ beforeEach(async () => {
11
+ tmpDir = await mkdtemp(join(tmpdir(), 'storybook-astro-build-test-'));
12
+ });
13
+
14
+ afterEach(async () => {
15
+ await rm(tmpDir, { recursive: true, force: true });
16
+ });
17
+
18
+ test('excludes a .tsx file that has only named exports', async () => {
19
+ const astroFile = join(tmpDir, 'Island.astro');
20
+ const namedOnlyTsx = join(tmpDir, 'Helper.tsx');
21
+
22
+ await writeFile(astroFile, `---\nimport Helper from './Helper.tsx';\n---`);
23
+ await writeFile(namedOnlyTsx, `export const Helper = () => <div />;`);
24
+
25
+ const result = await collectHydratedComponentPaths(astroFile);
26
+
27
+ expect(result).not.toContain(namedOnlyTsx.replace(/\\/g, '/'));
28
+ });
29
+
30
+ test('includes a .tsx file that has a default export', async () => {
31
+ const astroFile = join(tmpDir, 'Island.astro');
32
+ const defaultTsx = join(tmpDir, 'Button.tsx');
33
+
34
+ await writeFile(astroFile, `---\nimport Button from './Button.tsx';\n---`);
35
+ await writeFile(defaultTsx, `export default function Button() { return <div />; }`);
36
+
37
+ const result = await collectHydratedComponentPaths(astroFile);
38
+
39
+ expect(result).toContain(defaultTsx.replace(/\\/g, '/'));
40
+ });
41
+
42
+ test('includes a .svelte file even though it has no literal export default in source', async () => {
43
+ // Svelte SFCs have no `export default` — the compiler generates one.
44
+ // If the check were applied to .svelte, these files would be falsely excluded.
45
+ const astroFile = join(tmpDir, 'Island.astro');
46
+ const svelteFile = join(tmpDir, 'Counter.svelte');
47
+
48
+ await writeFile(astroFile, `---\nimport Counter from './Counter.svelte';\n---`);
49
+ await writeFile(svelteFile, `<script>\n let count = 0;\n</script>\n<button>{count}</button>`);
50
+
51
+ const result = await collectHydratedComponentPaths(astroFile);
52
+
53
+ expect(result).toContain(svelteFile.replace(/\\/g, '/'));
54
+ });
55
+
56
+ test('includes a .vue <script setup> file even though it has no literal export default in source', async () => {
57
+ // Vue <script setup> components have no `export default` — the compiler generates one.
58
+ const astroFile = join(tmpDir, 'Island.astro');
59
+ const vueFile = join(tmpDir, 'Counter.vue');
60
+
61
+ await writeFile(astroFile, `---\nimport Counter from './Counter.vue';\n---`);
62
+ await writeFile(
63
+ vueFile,
64
+ `<script setup>\nconst count = ref(0);\n</script>\n<template><button>{{ count }}</button></template>`
65
+ );
66
+
67
+ const result = await collectHydratedComponentPaths(astroFile);
68
+
69
+ expect(result).toContain(vueFile.replace(/\\/g, '/'));
70
+ });
71
+
72
+ test('includes a .tsx file on read error, preserving prior behaviour', async () => {
73
+ // When the file cannot be read, hasDefaultExport returns true so the file
74
+ // is kept rather than silently dropped.
75
+ const astroFile = join(tmpDir, 'Island.astro');
76
+ const missingTsx = join(tmpDir, 'Missing.tsx');
77
+
78
+ // Point the Astro file at a component path that won't exist on disk.
79
+ await writeFile(astroFile, `---\nimport Missing from './Missing.tsx';\n---`);
80
+
81
+ // Don't write missingTsx — resolveLocalImportPath will not find it,
82
+ // so it never reaches hasDefaultExport. Confirm the result is just empty.
83
+ const result = await collectHydratedComponentPaths(astroFile);
84
+
85
+ expect(result).not.toContain(missingTsx.replace(/\\/g, '/'));
86
+ });
87
+ });