@storybook-astro/framework 1.0.3 → 1.1.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 (62) hide show
  1. package/dist/{chunk-7GHEQUPV.js → chunk-POHTFYST.js} +46 -8
  2. package/dist/chunk-POHTFYST.js.map +1 -0
  3. package/dist/chunk-T7NWIO5S.js +220 -0
  4. package/dist/chunk-T7NWIO5S.js.map +1 -0
  5. package/dist/{chunk-C5OH4VBR.js → chunk-V76WSNSP.js} +124 -47
  6. package/dist/chunk-V76WSNSP.js.map +1 -0
  7. package/dist/{chunk-KSDXET2L.js → chunk-VPJDFGB5.js} +444 -60
  8. package/dist/chunk-VPJDFGB5.js.map +1 -0
  9. package/dist/index.d.ts +19 -9
  10. package/dist/index.js +10 -3
  11. package/dist/index.js.map +1 -1
  12. package/dist/middleware.js +57 -39
  13. package/dist/middleware.js.map +1 -1
  14. package/dist/node/index.d.ts +10 -0
  15. package/dist/node/index.js +10 -0
  16. package/dist/node/index.js.map +1 -0
  17. package/dist/preset.d.ts +1 -1
  18. package/dist/preset.js +3 -3
  19. package/dist/testing.js +12 -64
  20. package/dist/testing.js.map +1 -1
  21. package/dist/{types-CHTsRtA7.d.ts → types-Cvor6Tyi.d.ts} +21 -5
  22. package/dist/{viteStorybookAstroMiddlewarePlugin-NP2E52IC.js → viteStorybookAstroMiddlewarePlugin-2EFKTECT.js} +2 -2
  23. package/dist/vitest/global-setup.js +42 -0
  24. package/dist/vitest/global-setup.js.map +1 -0
  25. package/dist/vitest/index.js +20 -3
  26. package/dist/vitest/index.js.map +1 -1
  27. package/package.json +11 -3
  28. package/src/index.ts +21 -1
  29. package/src/lib/sanitization.ts +104 -0
  30. package/src/middleware.ts +76 -44
  31. package/src/node/index.ts +7 -0
  32. package/src/preset.ts +75 -15
  33. package/src/renderer/renderer-dev.ts +82 -0
  34. package/src/renderer/renderer-server.test.ts +101 -0
  35. package/src/renderer/renderer-server.ts +135 -0
  36. package/src/renderer/renderer-static.ts +62 -0
  37. package/src/rules.test.ts +89 -18
  38. package/src/rules.ts +67 -18
  39. package/src/server/index.ts +111 -0
  40. package/src/testing/renderer-daemon.ts +10 -1
  41. package/src/types.ts +25 -5
  42. package/src/virtual.d.ts +37 -0
  43. package/src/vite/astroFilesVirtualModulePlugin.ts +36 -0
  44. package/src/vite/createVirtualModulePlugin.ts +3 -3
  45. package/src/vite/storybookAstroRulesConfigVirtualModulePlugin.ts +37 -0
  46. package/src/vite/storybookAstroSanitizationConfigVirtualModulePlugin.ts +21 -0
  47. package/src/vite/storybookAstroServerAuthConfigVirtualModulePlugin.test.ts +71 -0
  48. package/src/vite/storybookAstroServerAuthConfigVirtualModulePlugin.ts +42 -0
  49. package/src/vitePluginAstroBuildPrerender.ts +50 -51
  50. package/src/vitePluginAstroBuildServer.ts +289 -0
  51. package/src/vitePluginAstroIntegrationOptsFallback.ts +25 -0
  52. package/src/viteStorybookAstroMiddlewarePlugin.ts +40 -8
  53. package/src/viteStorybookAstroRendererPlugin.ts +45 -0
  54. package/src/vitest/config.ts +45 -4
  55. package/src/vitest/global-setup.ts +45 -0
  56. package/dist/chunk-7GHEQUPV.js.map +0 -1
  57. package/dist/chunk-C5OH4VBR.js.map +0 -1
  58. package/dist/chunk-KSDXET2L.js.map +0 -1
  59. package/dist/middleware.d.ts +0 -26
  60. package/src/msw-helpers.ts +0 -1
  61. package/src/msw.ts +0 -58
  62. /package/dist/{viteStorybookAstroMiddlewarePlugin-NP2E52IC.js.map → viteStorybookAstroMiddlewarePlugin-2EFKTECT.js.map} +0 -0
@@ -0,0 +1,37 @@
1
+ import type { Plugin } from 'vite';
2
+ import type { StoryRulesOptions } from '../rules-options.ts';
3
+ import { resolveRulesConfigFilePath } from '../rules-options.ts';
4
+ import { createVirtualModulePlugin } from './createVirtualModulePlugin.ts';
5
+
6
+ export const STORYBOOK_ASTRO_STORY_RULES_CONFIG_VIRTUAL_MODULE_ID =
7
+ 'virtual:storybook-astro-story-rules-config';
8
+
9
+ export function storybookAstroStoryRulesConfigVirtualModulePlugin(
10
+ options?: StoryRulesOptions,
11
+ resolveFrom = process.cwd()
12
+ ): Plugin {
13
+ return createVirtualModulePlugin({
14
+ pluginName: 'storybook-astro:virtual-story-rules-config',
15
+ virtualModuleId: STORYBOOK_ASTRO_STORY_RULES_CONFIG_VIRTUAL_MODULE_ID,
16
+ load() {
17
+ const configFilePath = resolveRulesConfigFilePath(options, resolveFrom);
18
+
19
+ if (!configFilePath) {
20
+ return [
21
+ 'const storybookAstroStoryRulesConfig = { rules: [] };',
22
+ 'export default storybookAstroStoryRulesConfig;',
23
+ 'export const storybookAstroStoryRulesConfigFilePath = undefined;'
24
+ ].join('\n');
25
+ }
26
+
27
+ const importPath = JSON.stringify(configFilePath.replace(/\\/g, '/'));
28
+ const configPath = JSON.stringify(configFilePath.replace(/\\/g, '/'));
29
+
30
+ return [
31
+ `import * as storybookAstroStoryRulesConfigModule from ${importPath};`,
32
+ 'export default storybookAstroStoryRulesConfigModule;',
33
+ `export const storybookAstroStoryRulesConfigFilePath = ${configPath};`
34
+ ].join('\n');
35
+ }
36
+ });
37
+ }
@@ -0,0 +1,21 @@
1
+ import type { Plugin } from 'vite';
2
+ import type { SanitizationOptions } from '../lib/sanitization.ts';
3
+ import { serializeSanitizationOptions } from '../lib/sanitization.ts';
4
+ import { createVirtualModulePlugin } from './createVirtualModulePlugin.ts';
5
+
6
+ export const STORYBOOK_ASTRO_SANITIZATION_CONFIG_VIRTUAL_MODULE_ID =
7
+ 'virtual:storybook-astro-sanitization-config';
8
+
9
+ export function storybookAstroSanitizationConfigVirtualModulePlugin(
10
+ options?: SanitizationOptions
11
+ ): Plugin {
12
+ return createVirtualModulePlugin({
13
+ pluginName: 'storybook-astro:virtual-sanitization-config',
14
+ virtualModuleId: STORYBOOK_ASTRO_SANITIZATION_CONFIG_VIRTUAL_MODULE_ID,
15
+ load() {
16
+ const serializedConfig = serializeSanitizationOptions(options);
17
+
18
+ return `export default ${serializedConfig};`;
19
+ }
20
+ });
21
+ }
@@ -0,0 +1,71 @@
1
+ import type { PluginOption } from 'vite';
2
+ import { describe, expect, test } from 'vitest';
3
+ import {
4
+ STORYBOOK_ASTRO_SERVER_AUTH_CONFIG_VIRTUAL_MODULE_ID,
5
+ storybookAstroServerAuthConfigVirtualModulePlugin
6
+ } from './storybookAstroServerAuthConfigVirtualModulePlugin.ts';
7
+
8
+ function getPlugin(pluginOption: PluginOption) {
9
+ if (Array.isArray(pluginOption)) {
10
+ throw new Error('Expected a single plugin object, but got a plugin array.');
11
+ }
12
+
13
+ if (!pluginOption || typeof pluginOption !== 'object') {
14
+ throw new Error('Expected plugin option to be an object.');
15
+ }
16
+
17
+ return pluginOption;
18
+ }
19
+
20
+ function getHookHandler<T extends (...args: unknown[]) => unknown>(hook: unknown): T {
21
+ if (typeof hook === 'function') {
22
+ return hook as T;
23
+ }
24
+
25
+ if (
26
+ typeof hook === 'object' &&
27
+ hook !== null &&
28
+ 'handler' in hook &&
29
+ typeof (hook as { handler?: unknown }).handler === 'function'
30
+ ) {
31
+ return (hook as { handler: T }).handler;
32
+ }
33
+
34
+ throw new Error('Expected hook to be a function or an object with a handler function.');
35
+ }
36
+
37
+ describe('storybookAstroServerAuthConfigVirtualModulePlugin', () => {
38
+ test('normalizes auth token and header values', async () => {
39
+ const plugin = getPlugin(
40
+ storybookAstroServerAuthConfigVirtualModulePlugin({
41
+ authToken: ' test-token ',
42
+ authHeader: ' X-Storybook-Token '
43
+ })
44
+ );
45
+ const resolveId = getHookHandler<(id: string) => string | undefined>(plugin.resolveId);
46
+ const load = getHookHandler<(id: string) => Promise<string | undefined>>(plugin.load);
47
+ const resolvedId = resolveId(STORYBOOK_ASTRO_SERVER_AUTH_CONFIG_VIRTUAL_MODULE_ID);
48
+
49
+ expect(resolvedId).toBe(`\0${STORYBOOK_ASTRO_SERVER_AUTH_CONFIG_VIRTUAL_MODULE_ID}`);
50
+ await expect(load(resolvedId!)).resolves.toBe(
51
+ 'export const storybookAstroServerAuthToken = "test-token";\n' +
52
+ 'export const storybookAstroServerAuthHeader = "x-storybook-token";'
53
+ );
54
+ });
55
+
56
+ test('falls back to authorization header and undefined token', async () => {
57
+ const plugin = getPlugin(
58
+ storybookAstroServerAuthConfigVirtualModulePlugin({
59
+ authToken: ' '
60
+ })
61
+ );
62
+ const load = getHookHandler<(id: string) => Promise<string | undefined>>(plugin.load);
63
+
64
+ await expect(
65
+ load(`\0${STORYBOOK_ASTRO_SERVER_AUTH_CONFIG_VIRTUAL_MODULE_ID}`)
66
+ ).resolves.toBe(
67
+ 'export const storybookAstroServerAuthToken = undefined;\n' +
68
+ 'export const storybookAstroServerAuthHeader = "authorization";'
69
+ );
70
+ });
71
+ });
@@ -0,0 +1,42 @@
1
+ import type { Plugin } from 'vite';
2
+ import type { ServerBuildOptions } from '../types.ts';
3
+ import { createVirtualModulePlugin } from './createVirtualModulePlugin.ts';
4
+
5
+ export const STORYBOOK_ASTRO_SERVER_AUTH_CONFIG_VIRTUAL_MODULE_ID =
6
+ 'virtual:storybook-astro-server-auth-config';
7
+
8
+ export function storybookAstroServerAuthConfigVirtualModulePlugin(
9
+ options?: ServerBuildOptions
10
+ ): Plugin {
11
+ const authToken = normalizeOptionalString(options?.authToken);
12
+ const authHeader = normalizeAuthHeader(options?.authHeader);
13
+
14
+ return createVirtualModulePlugin({
15
+ pluginName: 'storybook-astro:virtual-server-auth-config',
16
+ virtualModuleId: STORYBOOK_ASTRO_SERVER_AUTH_CONFIG_VIRTUAL_MODULE_ID,
17
+ load() {
18
+ return [
19
+ `export const storybookAstroServerAuthToken = ${
20
+ authToken ? JSON.stringify(authToken) : 'undefined'
21
+ };`,
22
+ `export const storybookAstroServerAuthHeader = ${JSON.stringify(authHeader)};`
23
+ ].join('\n');
24
+ }
25
+ });
26
+ }
27
+
28
+ function normalizeOptionalString(value: string | undefined) {
29
+ if (!value) {
30
+ return undefined;
31
+ }
32
+
33
+ const normalizedValue = value.trim();
34
+
35
+ return normalizedValue || undefined;
36
+ }
37
+
38
+ function normalizeAuthHeader(value: string | undefined) {
39
+ const normalizedValue = normalizeOptionalString(value);
40
+
41
+ return (normalizedValue ?? 'authorization').toLowerCase();
42
+ }
@@ -9,11 +9,11 @@ import type { Integration } from './integrations/index.ts';
9
9
  import { ssrLoadModuleWithFsFallback } from './lib/ssr-load-module-with-fs-fallback.ts';
10
10
  import { resolveSanitizationOptions, sanitizeRenderPayload } from './lib/sanitization.ts';
11
11
  import { resolveStoryModuleMock, withStoryModuleMocks } from './module-mocks.ts';
12
- import { applyMswHandlers } from './msw.ts';
13
12
  import { resolveRulesConfigFilePath } from './rules-options.ts';
14
- import { selectStoryRules } from './rules.ts';
13
+ import { selectStoryRules, withStoryRuleCleanups } from './rules.ts';
15
14
  import type { FrameworkOptions } from './types.ts';
16
15
  import { vitePluginAstroFontsFallback } from './vitePluginAstroFontsFallback.ts';
16
+ import { vitePluginAstroIntegrationOptsFallback } from './vitePluginAstroIntegrationOptsFallback.ts';
17
17
  import { vitePluginAstroRoutesFallback } from './vitePluginAstroRoutesFallback.ts';
18
18
  import { vitePluginAstroVueFallback } from './vitePluginAstroVueFallback.ts';
19
19
 
@@ -218,7 +218,6 @@ async function prerenderStories(options: {
218
218
  const selectedRules = await selectStoryRules({
219
219
  configModule: rulesConfigModule,
220
220
  configFilePath: options.storyRulesConfigFilePath,
221
- mode: 'production',
222
221
  story: {
223
222
  id: story.id,
224
223
  title: story.title,
@@ -226,42 +225,47 @@ async function prerenderStories(options: {
226
225
  }
227
226
  });
228
227
 
229
- await applyMswHandlers(selectedRules.mswHandlers);
230
-
231
228
  if (selectedRules.moduleMocks.size > 0) {
232
229
  viteServer.moduleGraph.invalidateAll();
233
230
  }
234
231
 
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] : {};
232
+ const html = await withStoryRuleCleanups(selectedRules.cleanups, async () => {
233
+ return withStoryModuleMocks(selectedRules.moduleMocks, async () => {
234
+ const modulePath = resolveImportPath(story.importPath, options.resolveFrom);
235
+ const storyModule = await viteServer.ssrLoadModule(modulePath);
236
+ const meta = isRecord(storyModule.default) ? storyModule.default : {};
237
+ const storyExport = isRecord(storyModule[story.exportName])
238
+ ? storyModule[story.exportName]
239
+ : {};
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}.`
244
+ );
245
+ }
240
246
 
241
- if (typeof meta.component !== 'function') {
242
- throw new Error(
243
- `Unable to prerender story "${story.id}". Missing default export component in ${story.importPath}.`
244
- );
245
- }
247
+ if (storyExport.component && storyExport.component !== meta.component) {
248
+ return undefined;
249
+ }
246
250
 
247
- if (storyExport.component && storyExport.component !== meta.component) {
248
- return undefined;
249
- }
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
+ );
250
261
 
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
262
+ return container.renderToString(
263
+ patchCreateAstroCompat(meta.component) as Parameters<typeof container.renderToString>[0],
264
+ {
265
+ props: sanitizedPayload.args,
266
+ slots: sanitizedPayload.slots
267
+ }
268
+ );
265
269
  });
266
270
  });
267
271
 
@@ -281,14 +285,18 @@ async function createStorySsrServer(
281
285
  trackedSpecifiers: Set<string>,
282
286
  resolveFrom: string
283
287
  ) {
284
- const { getViteConfig } = await importAstroConfig(resolveFrom);
288
+ const { getViteConfig, passthroughImageService } = await importAstroConfig(resolveFrom);
285
289
  const astroConfig = await getViteConfig(
286
290
  { root: resolveFrom },
287
291
  {
288
292
  configFile: false,
289
293
  integrations: await Promise.all(
290
294
  integrations.map((integration) => integration.loadIntegration(resolveFrom))
291
- )
295
+ ),
296
+ // Use the passthrough image service so nested components that use <Image>
297
+ // from astro:assets render as plain <img> tags without triggering image
298
+ // optimization (which fails in the Storybook SSR context).
299
+ image: { service: passthroughImageService() }
292
300
  }
293
301
  )({
294
302
  mode: 'production',
@@ -303,6 +311,7 @@ async function createStorySsrServer(
303
311
  plugins: [
304
312
  createProjectAstroResolutionPlugin(resolveFrom),
305
313
  vitePluginAstroFontsFallback(),
314
+ vitePluginAstroIntegrationOptsFallback(),
306
315
  vitePluginAstroVueFallback(),
307
316
  vitePluginAstroRoutesFallback(),
308
317
  {
@@ -364,7 +373,7 @@ async function addContainerRenderers(
364
373
  renderer: {
365
374
  ...renderer,
366
375
  name: serverRenderer.name
367
- }
376
+ } as Parameters<typeof container.addServerRenderer>[0]['renderer']
368
377
  });
369
378
  } else {
370
379
  container.addServerRenderer({
@@ -633,7 +642,11 @@ async function processImageMetadata(
633
642
 
634
643
  for (const [key, value] of Object.entries(args)) {
635
644
  if (isImageMetadata(value)) {
636
- processed[key] = convertImageMetadataToUrl(value);
645
+ // Keep ImageMetadata as a plain object — Astro's image service checks
646
+ // isESMImportedImage (typeof src === 'object') and skips the /@fs/ string
647
+ // validation that throws LocalImageUsedWrongly. Converting to a URL string
648
+ // causes that error when the string starts with /@fs/.
649
+ processed[key] = value;
637
650
 
638
651
  continue;
639
652
  }
@@ -642,7 +655,7 @@ async function processImageMetadata(
642
655
  processed[key] = await Promise.all(
643
656
  value.map(async (item) => {
644
657
  if (isImageMetadata(item)) {
645
- return convertImageMetadataToUrl(item);
658
+ return item;
646
659
  }
647
660
 
648
661
  if (isRecord(item)) {
@@ -676,20 +689,6 @@ function isImageMetadata(value: unknown): value is Record<string, unknown> {
676
689
  );
677
690
  }
678
691
 
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
692
 
694
693
  function createProjectAstroResolutionPlugin(resolveFrom: string): Plugin {
695
694
  const require = createRequire(import.meta.url);
@@ -0,0 +1,289 @@
1
+ import type { Dirent } from 'node:fs';
2
+ import { readdir } from 'node:fs/promises';
3
+ import { dirname, resolve } from 'node:path';
4
+ import { fileURLToPath } from 'node:url';
5
+ import { build, type Rollup } from 'vite';
6
+ import type { FrameworkOptions } from './types.ts';
7
+ import { mergeWithAstroConfig } from './vitePluginAstro.ts';
8
+ 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';
13
+
14
+ const moduleRoot = resolve(dirname(fileURLToPath(import.meta.url)), '.');
15
+ // packageRoot works regardless of whether this file is running from src/ or dist/
16
+ const packageRoot = resolve(moduleRoot, '..');
17
+
18
+ export function vitePluginAstroBuildServer(options: FrameworkOptions) {
19
+ const integrations = options.integrations ?? [];
20
+ const resolveFrom = options.resolveFrom ?? process.cwd();
21
+ const storiesMap = new Map<string, Set<string>>();
22
+ const trackedSpecifiers = collectTrackedSpecifiers(integrations);
23
+ const staticEntrypointRefs = new Map<string, string>();
24
+ const componentEntrypointRefs = new Map<string, string>();
25
+ let storybookStaticOutDir = resolve(resolveFrom, 'storybook-static');
26
+
27
+ return {
28
+ name: 'storybook-astro:build-server',
29
+ apply: 'build',
30
+ enforce: 'post',
31
+
32
+ configResolved(config: { build: { outDir?: string } }) {
33
+ storybookStaticOutDir = resolve(resolveFrom, config.build.outDir ?? 'storybook-static');
34
+ },
35
+
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
+ }
54
+ },
55
+
56
+ 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
+ }
74
+ },
75
+
76
+ 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);
104
+ });
105
+ },
106
+
107
+ async writeBundle(this: Rollup.PluginContext) {
108
+ const astroComponents = Array.from(storiesMap.keys());
109
+ const staticModuleMap = buildStaticModuleMap(
110
+ this,
111
+ staticEntrypointRefs,
112
+ componentEntrypointRefs
113
+ );
114
+ const serverOutDir = resolve(dirname(storybookStaticOutDir), 'storybook-server');
115
+
116
+ await buildAstroServer({
117
+ astroComponents,
118
+ integrations,
119
+ sanitization: options.sanitization,
120
+ storyRules: options.storyRules,
121
+ server: options.server,
122
+ outDir: serverOutDir,
123
+ staticModuleMap,
124
+ resolveFrom
125
+ });
126
+ }
127
+ };
128
+ }
129
+
130
+ async function buildAstroServer(options: {
131
+ astroComponents: string[];
132
+ integrations: FrameworkOptions['integrations'];
133
+ sanitization?: FrameworkOptions['sanitization'];
134
+ storyRules?: FrameworkOptions['storyRules'];
135
+ server?: FrameworkOptions['server'];
136
+ outDir: string;
137
+ staticModuleMap: Record<string, string>;
138
+ resolveFrom: string;
139
+ }) {
140
+ const buildConfig = {
141
+ root: resolve(packageRoot, 'src/server'),
142
+ ssr: {
143
+ noExternal: /(@astrojs\/.+|react|react-dom)/
144
+ },
145
+ build: {
146
+ ssr: true,
147
+ outDir: options.outDir,
148
+ emptyOutDir: true,
149
+ sourcemap: true,
150
+ manifest: false,
151
+ rollupOptions: {
152
+ input: resolve(packageRoot, 'src/server/index.ts'),
153
+ treeshake: true
154
+ }
155
+ },
156
+ plugins: [
157
+ astroFilesVirtualModulePlugin(options.astroComponents),
158
+ storybookAstroSanitizationConfigVirtualModulePlugin(options.sanitization),
159
+ storybookAstroStoryRulesConfigVirtualModulePlugin(options.storyRules, options.resolveFrom),
160
+ storybookAstroServerAuthConfigVirtualModulePlugin(options.server),
161
+ viteAstroContainerRenderersPlugin(options.integrations, {
162
+ mode: 'production',
163
+ staticModuleMap: options.staticModuleMap
164
+ })
165
+ ]
166
+ };
167
+
168
+ const finalConfig = await mergeWithAstroConfig(
169
+ buildConfig,
170
+ options.integrations,
171
+ options.resolveFrom,
172
+ 'production',
173
+ 'build'
174
+ );
175
+
176
+ await build(finalConfig);
177
+ }
178
+
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;
184
+
185
+ if (entrypoint) {
186
+ specifiers.add(entrypoint);
187
+ }
188
+ });
189
+
190
+ return specifiers;
191
+ }
192
+
193
+ function buildStaticModuleMap(
194
+ pluginContext: Rollup.PluginContext,
195
+ staticEntrypointRefs: Map<string, string>,
196
+ componentEntrypointRefs: Map<string, string>
197
+ ) {
198
+ const map: Record<string, string> = {};
199
+
200
+ staticEntrypointRefs.forEach((fileReferenceId, specifier) => {
201
+ const fileName = pluginContext.getFileName(fileReferenceId);
202
+
203
+ if (fileName) {
204
+ map[specifier] = toPublicPath(fileName);
205
+ }
206
+ });
207
+
208
+ componentEntrypointRefs.forEach((fileReferenceId, specifier) => {
209
+ const fileName = pluginContext.getFileName(fileReferenceId);
210
+
211
+ if (fileName) {
212
+ map[specifier] = toPublicPath(fileName);
213
+ }
214
+ });
215
+
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
+ }
226
+
227
+ function isClientEntrypoint(specifier: string) {
228
+ return specifier.startsWith('@astrojs/') && specifier.endsWith('/client.js');
229
+ }
230
+
231
+ function toPublicPath(fileName: string) {
232
+ return `./${fileName}`;
233
+ }
234
+
235
+ async function collectHydratableSourceModules(srcRoot: string): Promise<string[]> {
236
+ const modules: string[] = [];
237
+
238
+ async function walk(directory: string) {
239
+ let entries: Dirent[];
240
+
241
+ try {
242
+ entries = await readdir(directory, { withFileTypes: true });
243
+ } catch {
244
+ return;
245
+ }
246
+
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);
253
+
254
+ return;
255
+ }
256
+
257
+ if (!entry.isFile()) {
258
+ return;
259
+ }
260
+
261
+ const normalizedPath = absolutePath.replace(/\\/g, '/');
262
+
263
+ if (!isHydratableSourceFile(normalizedPath)) {
264
+ return;
265
+ }
266
+
267
+ if (isNonHydratableSourceFile(normalizedPath)) {
268
+ return;
269
+ }
270
+
271
+ modules.push(normalizedPath);
272
+ })
273
+ );
274
+ }
275
+
276
+ await walk(srcRoot);
277
+
278
+ return modules;
279
+ }
280
+
281
+ function isHydratableSourceFile(input: string) {
282
+ return /\.(jsx|tsx|vue|svelte|js|ts)$/.test(input);
283
+ }
284
+
285
+ function isNonHydratableSourceFile(input: string) {
286
+ return /\.stories\.[jt]sx?$|\.stories\.vue$|\.stories\.svelte$|\.(spec|test)\.[jt]sx?$/.test(
287
+ input
288
+ );
289
+ }
@@ -0,0 +1,25 @@
1
+ import type { Plugin } from 'vite';
2
+
3
+ const OPTS_STUB = 'export default {}';
4
+
5
+ const VIRTUAL_IDS = ['astro:react:opts', 'astro:preact:opts'];
6
+
7
+ export function vitePluginAstroIntegrationOptsFallback(): Plugin {
8
+ const resolvedIds = new Map(VIRTUAL_IDS.map((id) => [id, `\0${id}`]));
9
+ const resolvedIdSet = new Set(resolvedIds.values());
10
+
11
+ return {
12
+ name: 'storybook-astro-integration-opts-fallback',
13
+ enforce: 'pre',
14
+
15
+ resolveId(id) {
16
+ return resolvedIds.get(id);
17
+ },
18
+
19
+ load(id) {
20
+ if (resolvedIdSet.has(id)) {
21
+ return { code: OPTS_STUB };
22
+ }
23
+ }
24
+ };
25
+ }