@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
@@ -0,0 +1,203 @@
1
+ import type { experimental_AstroContainer as AstroContainer } from 'astro/container';
2
+ import type { SanitizationOptions } from './lib/sanitization.ts';
3
+ import { resolveSanitizationOptions, sanitizeRenderPayload } from './lib/sanitization.ts';
4
+ import { runWithStoryRules, type ResolveRulesConfigModule } from './storyRulesRuntime.ts';
5
+ import type { RenderStoryInput } from './types.ts';
6
+
7
+ type AstroCreateResult = {
8
+ createAstro?: (...args: unknown[]) => unknown;
9
+ };
10
+
11
+ type AstroComponentFactory = ((
12
+ result: AstroCreateResult,
13
+ props: unknown,
14
+ slots: unknown
15
+ ) => unknown) & {
16
+ isAstroComponentFactory?: boolean;
17
+ moduleId?: string;
18
+ propagation?: unknown;
19
+ };
20
+
21
+ export type HandlerProps = {
22
+ component: string;
23
+ args?: Record<string, unknown>;
24
+ slots?: Record<string, unknown>;
25
+ story?: RenderStoryInput;
26
+ };
27
+
28
+ type CreateAstroRenderHandlerOptions = {
29
+ container: Awaited<ReturnType<typeof AstroContainer.create>>;
30
+ sanitization?: SanitizationOptions;
31
+ rulesConfigFilePath?: string;
32
+ resolveRulesConfigModule?: ResolveRulesConfigModule;
33
+ loadModule: (id: string) => Promise<{ default: unknown }>;
34
+ invalidateModuleGraph?: () => void;
35
+ };
36
+
37
+ export function createAstroRenderHandler(options: CreateAstroRenderHandlerOptions) {
38
+ const sanitizationOptions = resolveSanitizationOptions(options.sanitization);
39
+ const componentCache = new Map<string, Promise<AstroComponentFactory>>();
40
+ let renderQueue = Promise.resolve<void>(undefined);
41
+
42
+ async function loadPatchedComponent(componentId: string, useCache = true) {
43
+ if (!useCache) {
44
+ const { default: component } = await options.loadModule(componentId);
45
+
46
+ return patchCreateAstroCompat(component);
47
+ }
48
+
49
+ if (!componentCache.has(componentId)) {
50
+ componentCache.set(
51
+ componentId,
52
+ (async () => {
53
+ const { default: component } = await options.loadModule(componentId);
54
+
55
+ return patchCreateAstroCompat(component);
56
+ })()
57
+ );
58
+ }
59
+
60
+ const cachedComponent = componentCache.get(componentId);
61
+
62
+ if (!cachedComponent) {
63
+ throw new Error(`Failed to load Astro component: ${componentId}`);
64
+ }
65
+
66
+ try {
67
+ return await cachedComponent;
68
+ } catch (error) {
69
+ componentCache.delete(componentId);
70
+ throw error;
71
+ }
72
+ }
73
+
74
+ return async function handler(data: HandlerProps) {
75
+ const executeRender = async () => {
76
+ return runWithStoryRules(
77
+ {
78
+ story: data.story,
79
+ rulesConfigFilePath: options.rulesConfigFilePath,
80
+ resolveRulesConfigModule: options.resolveRulesConfigModule,
81
+ invalidateModuleGraph: options.invalidateModuleGraph
82
+ },
83
+ async (selectedRules) => {
84
+ const patchedComponent = await loadPatchedComponent(
85
+ data.component,
86
+ selectedRules.moduleMocks.size === 0
87
+ );
88
+ const processedArgs = await processImageMetadata(data.args ?? {});
89
+ const sanitizedPayload = sanitizeRenderPayload(
90
+ {
91
+ args: processedArgs,
92
+ slots: data.slots ?? {}
93
+ },
94
+ sanitizationOptions
95
+ );
96
+
97
+ return options.container.renderToString(
98
+ patchedComponent as Parameters<typeof options.container.renderToString>[0],
99
+ {
100
+ props: sanitizedPayload.args,
101
+ slots: sanitizedPayload.slots
102
+ }
103
+ );
104
+ }
105
+ );
106
+ };
107
+
108
+ const resultPromise = renderQueue.then(executeRender, executeRender);
109
+
110
+ renderQueue = resultPromise.then(
111
+ () => undefined,
112
+ () => undefined
113
+ );
114
+
115
+ return resultPromise;
116
+ };
117
+ }
118
+
119
+ export function patchCreateAstroCompat(component: unknown): AstroComponentFactory {
120
+ if (typeof component !== 'function') {
121
+ throw new Error('Expected Astro component factory to be a function.');
122
+ }
123
+
124
+ const originalComponent = component as AstroComponentFactory;
125
+ const wrapped = ((result: AstroCreateResult, props: unknown, slots: unknown) => {
126
+ if (result && typeof result.createAstro === 'function') {
127
+ const originalCreateAstro = result.createAstro;
128
+ const runtimeExpectsAstroGlobal = originalCreateAstro.length >= 3;
129
+
130
+ result.createAstro = (...args: unknown[]) => {
131
+ if (args.length === 3 && !runtimeExpectsAstroGlobal) {
132
+ return originalCreateAstro(args[1], args[2]);
133
+ }
134
+
135
+ return originalCreateAstro(...args);
136
+ };
137
+ }
138
+
139
+ return originalComponent(result, props, slots);
140
+ }) as AstroComponentFactory;
141
+
142
+ wrapped.isAstroComponentFactory = originalComponent.isAstroComponentFactory;
143
+ wrapped.moduleId = originalComponent.moduleId;
144
+ wrapped.propagation = originalComponent.propagation;
145
+
146
+ return wrapped;
147
+ }
148
+
149
+ async function processImageMetadata(
150
+ args: Record<string, unknown>
151
+ ): Promise<Record<string, unknown>> {
152
+ const processed: Record<string, unknown> = {};
153
+
154
+ for (const [key, value] of Object.entries(args)) {
155
+ if (isImageMetadata(value)) {
156
+ // Keep ImageMetadata as an object so Astro's image pipeline still
157
+ // recognizes it as an imported image and skips local path validation.
158
+ processed[key] = value;
159
+
160
+ continue;
161
+ }
162
+
163
+ if (Array.isArray(value)) {
164
+ processed[key] = await Promise.all(
165
+ value.map(async (item) => {
166
+ if (isImageMetadata(item)) {
167
+ return item;
168
+ }
169
+
170
+ if (isRecord(item)) {
171
+ return processImageMetadata(item);
172
+ }
173
+
174
+ return item;
175
+ })
176
+ );
177
+
178
+ continue;
179
+ }
180
+
181
+ if (isRecord(value)) {
182
+ processed[key] = await processImageMetadata(value);
183
+
184
+ continue;
185
+ }
186
+
187
+ processed[key] = value;
188
+ }
189
+
190
+ return processed;
191
+ }
192
+
193
+ function isImageMetadata(value: unknown): value is Record<string, unknown> {
194
+ return (
195
+ isRecord(value) &&
196
+ typeof value.src === 'string' &&
197
+ ('width' in value || 'height' in value || 'format' in value)
198
+ );
199
+ }
200
+
201
+ function isRecord(value: unknown): value is Record<string, unknown> {
202
+ return typeof value === 'object' && value !== null;
203
+ }
@@ -7,5 +7,5 @@ export async function importAstroConfig(resolveFrom: string) {
7
7
  paths: [resolveFrom]
8
8
  });
9
9
 
10
- return import(pathToFileURL(astroConfigEntrypoint).href);
10
+ return import(/* @vite-ignore */ pathToFileURL(astroConfigEntrypoint).href);
11
11
  }
package/src/index.ts CHANGED
@@ -44,6 +44,8 @@ export type {
44
44
  } from './types.ts';
45
45
  export type {
46
46
  StoryRuleCleanup,
47
+ StoryRuleMock,
48
+ StoryRuleMockFactory,
47
49
  StoryRule,
48
50
  StoryRulesConfig,
49
51
  StoryRuleSelection,
@@ -5,6 +5,7 @@ export type Options = Record<string, unknown>;
5
5
 
6
6
  export class AlpineIntegration implements Integration {
7
7
  readonly name = 'alpine';
8
+ readonly factoryName = 'alpinejs';
8
9
  readonly dependencies = [
9
10
  '@astrojs/alpinejs',
10
11
  'alpinejs'
@@ -13,6 +13,12 @@ export type RendererDeclaration = {
13
13
 
14
14
  export abstract class Integration {
15
15
  abstract readonly name: string;
16
+ // Identifier used to import this integration's factory from
17
+ // `@storybook-astro/framework/integrations` when generating the server
18
+ // runtime module. Defaults to `name` for integrations whose public name
19
+ // matches their factory export. Override when they diverge (e.g. Alpine's
20
+ // `name` is "alpine" but its factory export is `alpinejs`).
21
+ readonly factoryName?: string;
16
22
  abstract readonly dependencies: string[];
17
23
  abstract readonly options: Record<string | number | symbol, unknown>;
18
24
  abstract readonly renderer: RendererDeclaration;
package/src/middleware.ts CHANGED
@@ -1,51 +1,34 @@
1
+ /// <reference path="./virtual.d.ts" />
2
+
1
3
  import { pathToFileURL } from 'node:url';
2
4
  import { experimental_AstroContainer as AstroContainer } from 'astro/container';
5
+ import { ensureAstroPassthroughImageService } from './astroImageService.ts';
6
+ import { createAstroRenderHandler, type HandlerProps } from './astroRenderHandler.ts';
3
7
  import type { Integration } from './integrations/index.ts';
4
- import { installPassthroughImageService } from './lib/passthrough-image-service.ts';
5
8
  import type { SanitizationOptions } from './lib/sanitization.ts';
6
- import { resolveSanitizationOptions, sanitizeRenderPayload } from './lib/sanitization.ts';
7
- import { resolveStoryModuleMock, withStoryModuleMocks } from './module-mocks.ts';
8
- import { selectStoryRules, withStoryRuleCleanups } from './rules.ts';
9
- import type { RenderStoryInput } from './types.ts';
9
+ import { resolveStoryModuleMock } from './module-mocks.ts';
10
10
  import { addRenderers, resolveClientModules } from 'virtual:astro-container-renderers';
11
11
 
12
12
  type ResolveRulesConfigModule = () => unknown | Promise<unknown>;
13
13
 
14
- type AstroCreateResult = {
15
- createAstro?: (...args: unknown[]) => unknown;
16
- };
17
-
18
- type AstroComponentFactory = ((
19
- result: AstroCreateResult,
20
- props: unknown,
21
- slots: unknown
22
- ) => unknown) & {
23
- isAstroComponentFactory?: boolean;
24
- moduleId?: string;
25
- propagation?: unknown;
26
- };
27
-
28
- export type HandlerProps = {
29
- component: string;
30
- args?: Record<string, unknown>;
31
- slots?: Record<string, unknown>;
32
- story?: RenderStoryInput;
33
- };
34
-
35
14
  type HandlerFactoryOptions = {
36
15
  sanitization?: SanitizationOptions;
37
16
  rulesConfigFilePath?: string;
38
17
  resolveRulesConfigModule?: ResolveRulesConfigModule;
39
18
  loadModule?: (id: string) => Promise<{ default: unknown }>;
19
+ invalidateModuleGraph?: () => void;
20
+ resolveModule?: (specifier: string) => string | undefined;
40
21
  };
41
22
 
42
- export async function handlerFactory(_integrations: Integration[], options?: HandlerFactoryOptions) {
43
- // Inject a passthrough image service before any component renders. See
44
- // `lib/passthrough-image-service.ts` for why this is necessary.
45
- installPassthroughImageService();
23
+ export type { HandlerProps };
24
+
25
+ export async function handlerFactory(
26
+ _integrations: Integration[],
27
+ options?: HandlerFactoryOptions
28
+ ) {
29
+ ensureAstroPassthroughImageService();
46
30
 
47
31
  const container = await AstroContainer.create({
48
- // Somewhat hacky way to force client-side Storybook's Vite to resolve modules properly
49
32
  resolve: async (specifier) => {
50
33
  const mockedModule = resolveStoryModuleMock(specifier);
51
34
 
@@ -53,6 +36,12 @@ export async function handlerFactory(_integrations: Integration[], options?: Han
53
36
  return mockedModule;
54
37
  }
55
38
 
39
+ const customResolution = options?.resolveModule?.(specifier);
40
+
41
+ if (customResolution) {
42
+ return customResolution;
43
+ }
44
+
56
45
  if (specifier.startsWith('astro:scripts')) {
57
46
  return `/@id/${specifier}`;
58
47
  }
@@ -68,7 +57,7 @@ export async function handlerFactory(_integrations: Integration[], options?: Han
68
57
  });
69
58
 
70
59
  addRenderers(container);
71
- const sanitizationOptions = resolveSanitizationOptions(options?.sanitization);
60
+
72
61
  const loadModule =
73
62
  options?.loadModule ??
74
63
  ((id: string) => {
@@ -76,173 +65,13 @@ export async function handlerFactory(_integrations: Integration[], options?: Han
76
65
 
77
66
  return import(/* @vite-ignore */ normalizedId);
78
67
  });
79
- const componentCache = new Map<string, Promise<AstroComponentFactory>>();
80
- let renderQueue = Promise.resolve<void>(undefined);
81
-
82
- async function loadPatchedComponent(componentId: string, useCache = true) {
83
- if (!useCache) {
84
- const { default: component } = await loadModule(componentId);
85
-
86
- return patchCreateAstroCompat(component);
87
- }
88
-
89
- if (!componentCache.has(componentId)) {
90
- componentCache.set(componentId, (async () => {
91
- const { default: component } = await loadModule(componentId);
92
-
93
- return patchCreateAstroCompat(component);
94
- })());
95
- }
96
-
97
- const cachedComponent = componentCache.get(componentId);
98
-
99
- if (!cachedComponent) {
100
- throw new Error(`Failed to load Astro component: ${componentId}`);
101
- }
102
-
103
- try {
104
- return await cachedComponent;
105
- } catch (error) {
106
- // Drop failed entries so transient/module errors can recover on the next request.
107
- componentCache.delete(componentId);
108
- throw error;
109
- }
110
- }
111
-
112
- return async function handler(data: HandlerProps) {
113
- const executeRender = async () => {
114
- const rulesConfigModule = options?.resolveRulesConfigModule
115
- ? await options.resolveRulesConfigModule()
116
- : undefined;
117
-
118
- const selectedRules = await selectStoryRules({
119
- configModule: rulesConfigModule,
120
- configFilePath: options?.rulesConfigFilePath,
121
- story: data.story
122
- });
123
-
124
- return withStoryRuleCleanups(selectedRules.cleanups, async () => {
125
- return withStoryModuleMocks(selectedRules.moduleMocks, async () => {
126
- const patchedComponent = await loadPatchedComponent(
127
- data.component,
128
- selectedRules.moduleMocks.size === 0
129
- );
130
- const processedArgs = await processImageMetadata(data.args ?? {});
131
- const sanitizedPayload = sanitizeRenderPayload(
132
- {
133
- args: processedArgs,
134
- slots: data.slots ?? {}
135
- },
136
- sanitizationOptions
137
- );
138
-
139
- return container.renderToString(
140
- patchedComponent as Parameters<typeof container.renderToString>[0],
141
- {
142
- props: sanitizedPayload.args,
143
- slots: sanitizedPayload.slots
144
- }
145
- );
146
- });
147
- });
148
- };
149
-
150
- const resultPromise = renderQueue.then(executeRender, executeRender);
151
-
152
- renderQueue = resultPromise.then(
153
- () => undefined,
154
- () => undefined
155
- );
156
-
157
- return resultPromise;
158
- };
159
- }
160
-
161
- function patchCreateAstroCompat(component: unknown): AstroComponentFactory {
162
- if (typeof component !== 'function') {
163
- throw new Error('Expected Astro component factory to be a function.');
164
- }
165
-
166
- const originalComponent = component as AstroComponentFactory;
167
- const wrapped = ((result: AstroCreateResult, props: unknown, slots: unknown) => {
168
- if (result && typeof result.createAstro === 'function') {
169
- const originalCreateAstro = result.createAstro;
170
- const runtimeExpectsAstroGlobal = originalCreateAstro.length >= 3;
171
68
 
172
- result.createAstro = (...args: unknown[]) => {
173
- if (args.length === 3 && !runtimeExpectsAstroGlobal) {
174
- return originalCreateAstro(args[1], args[2]);
175
- }
176
-
177
- return originalCreateAstro(...args);
178
- };
179
- }
180
-
181
- return originalComponent(result, props, slots);
182
- }) as AstroComponentFactory;
183
-
184
- wrapped.isAstroComponentFactory = originalComponent.isAstroComponentFactory;
185
- wrapped.moduleId = originalComponent.moduleId;
186
- wrapped.propagation = originalComponent.propagation;
187
-
188
- return wrapped;
189
- }
190
-
191
- async function processImageMetadata(
192
- args: Record<string, unknown>
193
- ): Promise<Record<string, unknown>> {
194
- const processed: Record<string, unknown> = {};
195
-
196
- for (const [key, value] of Object.entries(args)) {
197
- if (isImageMetadata(value)) {
198
- // Keep ImageMetadata as a plain object — Astro's image service checks
199
- // isESMImportedImage (typeof src === 'object') and skips the /@fs/ string
200
- // validation that throws LocalImageUsedWrongly. Converting to a URL string
201
- // causes that error when the string starts with /@fs/.
202
- processed[key] = value;
203
-
204
- continue;
205
- }
206
-
207
- if (Array.isArray(value)) {
208
- processed[key] = await Promise.all(
209
- value.map(async (item) => {
210
- if (isImageMetadata(item)) {
211
- return item;
212
- }
213
-
214
- if (isRecord(item)) {
215
- return processImageMetadata(item);
216
- }
217
-
218
- return item;
219
- })
220
- );
221
-
222
- continue;
223
- }
224
-
225
- if (isRecord(value)) {
226
- processed[key] = await processImageMetadata(value);
227
-
228
- continue;
229
- }
230
-
231
- processed[key] = value;
232
- }
233
-
234
- return processed;
235
- }
236
-
237
- function isImageMetadata(value: unknown): value is Record<string, unknown> {
238
- return (
239
- isRecord(value) &&
240
- typeof value.src === 'string' &&
241
- ('width' in value || 'height' in value || 'format' in value)
242
- );
243
- }
244
-
245
-
246
- function isRecord(value: unknown): value is Record<string, unknown> {
247
- return typeof value === 'object' && value !== null;
69
+ return createAstroRenderHandler({
70
+ container,
71
+ sanitization: options?.sanitization,
72
+ rulesConfigFilePath: options?.rulesConfigFilePath,
73
+ resolveRulesConfigModule: options?.resolveRulesConfigModule,
74
+ loadModule,
75
+ invalidateModuleGraph: options?.invalidateModuleGraph
76
+ });
248
77
  }
@@ -1,16 +1,164 @@
1
- import { AsyncLocalStorage } from 'node:async_hooks';
1
+ const STORYBOOK_ASTRO_GET_MOCK_EXPORT = '__STORYBOOK_ASTRO_GET_STORY_MODULE_MOCK_EXPORT__';
2
2
 
3
- export type StoryModuleMocks = Map<string, string>;
3
+ export const STORYBOOK_ASTRO_INLINE_MODULE_PREFIX = 'virtual:storybook-astro-inline-module:';
4
4
 
5
- const moduleMockStorage = new AsyncLocalStorage<StoryModuleMocks>();
5
+ export type StoryModuleMockEntry = {
6
+ replacement: string;
7
+ inlineModuleId?: string;
8
+ };
9
+
10
+ export type StoryModuleMockFactoryResult = Record<string, unknown>;
11
+
12
+ export type StoryModuleMocks = Map<string, StoryModuleMockEntry>;
13
+
14
+ type StoryModuleMocksGlobal = typeof globalThis & {
15
+ __STORYBOOK_ASTRO_STORY_MODULE_MOCK_STATE__?: {
16
+ activeModuleMocksStack: StoryModuleMocks[];
17
+ inlineModuleExports: Map<string, StoryModuleMockFactoryResult>;
18
+ inlineModuleSequence: number;
19
+ };
20
+ __STORYBOOK_ASTRO_GET_STORY_MODULE_MOCK_EXPORT__?: (moduleId: string, exportName: string) => unknown;
21
+ };
22
+
23
+ const moduleMocksGlobal = globalThis as StoryModuleMocksGlobal;
24
+
25
+ const moduleMockState =
26
+ moduleMocksGlobal.__STORYBOOK_ASTRO_STORY_MODULE_MOCK_STATE__ ??
27
+ (moduleMocksGlobal.__STORYBOOK_ASTRO_STORY_MODULE_MOCK_STATE__ = {
28
+ activeModuleMocksStack: [],
29
+ inlineModuleExports: new Map<string, StoryModuleMockFactoryResult>(),
30
+ inlineModuleSequence: 0
31
+ });
32
+
33
+ if (typeof moduleMocksGlobal[STORYBOOK_ASTRO_GET_MOCK_EXPORT] !== 'function') {
34
+ moduleMocksGlobal[STORYBOOK_ASTRO_GET_MOCK_EXPORT] = getStoryModuleMockExport;
35
+ }
6
36
 
7
37
  export async function withStoryModuleMocks<T>(
8
38
  moduleMocks: StoryModuleMocks,
9
39
  callback: () => Promise<T>
10
40
  ): Promise<T> {
11
- return moduleMockStorage.run(moduleMocks, callback);
41
+ moduleMockState.activeModuleMocksStack.push(moduleMocks);
42
+
43
+ try {
44
+ return await callback();
45
+ } finally {
46
+ moduleMockState.activeModuleMocksStack.pop();
47
+ cleanupInlineModuleMocks(moduleMocks);
48
+ }
12
49
  }
13
50
 
14
51
  export function resolveStoryModuleMock(specifier: string): string | undefined {
15
- return moduleMockStorage.getStore()?.get(specifier);
52
+ return getActiveModuleMocks()?.get(specifier)?.replacement;
53
+ }
54
+
55
+ export function createPathStoryModuleMock(replacement: string): StoryModuleMockEntry {
56
+ return {
57
+ replacement
58
+ };
59
+ }
60
+
61
+ export function createInlineStoryModuleMock(
62
+ exportsObject: StoryModuleMockFactoryResult
63
+ ): StoryModuleMockEntry {
64
+ const inlineModuleId = `storybook-astro-inline-module:${moduleMockState.inlineModuleSequence}`;
65
+
66
+ moduleMockState.inlineModuleSequence += 1;
67
+ moduleMockState.inlineModuleExports.set(inlineModuleId, exportsObject);
68
+
69
+ return {
70
+ replacement: `${STORYBOOK_ASTRO_INLINE_MODULE_PREFIX}${inlineModuleId}`,
71
+ inlineModuleId
72
+ };
73
+ }
74
+
75
+ export function loadStoryInlineModule(resolvedId: string): string | undefined {
76
+ const inlineModuleId = normalizeInlineModuleId(resolvedId);
77
+
78
+ if (!inlineModuleId) {
79
+ return undefined;
80
+ }
81
+
82
+ const exportsObject = moduleMockState.inlineModuleExports.get(inlineModuleId);
83
+
84
+ if (!exportsObject) {
85
+ return undefined;
86
+ }
87
+
88
+ return createInlineModuleSource(inlineModuleId, exportsObject);
89
+ }
90
+
91
+ function cleanupInlineModuleMocks(moduleMocks: StoryModuleMocks) {
92
+ for (const mockEntry of moduleMocks.values()) {
93
+ if (mockEntry.inlineModuleId) {
94
+ moduleMockState.inlineModuleExports.delete(mockEntry.inlineModuleId);
95
+ }
96
+ }
97
+ }
98
+
99
+ function getActiveModuleMocks(): StoryModuleMocks | undefined {
100
+ return moduleMockState.activeModuleMocksStack[moduleMockState.activeModuleMocksStack.length - 1];
101
+ }
102
+
103
+ function getStoryModuleMockExport(moduleId: string, exportName: string): unknown {
104
+ const exportsObject = moduleMockState.inlineModuleExports.get(moduleId);
105
+
106
+ if (!exportsObject) {
107
+ throw new Error(`Inline story module mock is unavailable: ${moduleId}`);
108
+ }
109
+
110
+ return exportsObject[exportName];
111
+ }
112
+
113
+ function createInlineModuleSource(
114
+ inlineModuleId: string,
115
+ exportsObject: StoryModuleMockFactoryResult
116
+ ): string {
117
+ const exportNames = Object.keys(exportsObject);
118
+ const sourceLines = [
119
+ `const getStoryModuleMockExport = globalThis.${STORYBOOK_ASTRO_GET_MOCK_EXPORT};`,
120
+ "if (typeof getStoryModuleMockExport !== 'function') {",
121
+ " throw new Error('Inline story module mock helper is unavailable.');",
122
+ '}',
123
+ ''
124
+ ];
125
+
126
+ if (Object.prototype.hasOwnProperty.call(exportsObject, 'default')) {
127
+ sourceLines.push(
128
+ `export default getStoryModuleMockExport(${JSON.stringify(inlineModuleId)}, 'default');`
129
+ );
130
+ }
131
+
132
+ for (const exportName of exportNames) {
133
+ if (exportName === 'default') {
134
+ continue;
135
+ }
136
+
137
+ assertValidExportName(exportName);
138
+ sourceLines.push(
139
+ `export const ${exportName} = getStoryModuleMockExport(${JSON.stringify(inlineModuleId)}, ${JSON.stringify(exportName)});`
140
+ );
141
+ }
142
+
143
+ if (sourceLines[sourceLines.length - 1] === '') {
144
+ sourceLines.push('export {};');
145
+ }
146
+
147
+ return sourceLines.join('\n');
148
+ }
149
+
150
+ function assertValidExportName(exportName: string) {
151
+ if (!/^[$A-Z_a-z][$\w]*$/u.test(exportName)) {
152
+ throw new Error(`Story module mock export name is invalid: ${exportName}`);
153
+ }
154
+ }
155
+
156
+ function normalizeInlineModuleId(resolvedId: string): string | undefined {
157
+ const normalizedId = resolvedId.startsWith('\0') ? resolvedId.slice(1) : resolvedId;
158
+
159
+ if (!normalizedId.startsWith(STORYBOOK_ASTRO_INLINE_MODULE_PREFIX)) {
160
+ return undefined;
161
+ }
162
+
163
+ return normalizedId.slice(STORYBOOK_ASTRO_INLINE_MODULE_PREFIX.length);
16
164
  }