@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
@@ -0,0 +1,57 @@
1
+ export function ensureAstroPassthroughImageService() {
2
+ if (!globalThis.astroAsset) {
3
+ (globalThis as Record<string, unknown>).astroAsset = {};
4
+ }
5
+
6
+ (globalThis.astroAsset as Record<string, unknown>).imageService = {
7
+ propertiesToHash: ['src'],
8
+ validateOptions(options: Record<string, unknown>) {
9
+ return options;
10
+ },
11
+ getURL(options: { src: unknown }) {
12
+ const src = options.src;
13
+
14
+ if (
15
+ src != null &&
16
+ typeof src === 'object' &&
17
+ 'src' in src &&
18
+ typeof (src as Record<string, unknown>).src === 'string'
19
+ ) {
20
+ return (src as Record<string, unknown>).src as string;
21
+ }
22
+
23
+ return typeof src === 'string' ? src : '';
24
+ },
25
+ getHTMLAttributes(options: Record<string, unknown>) {
26
+ const {
27
+ src,
28
+ width,
29
+ height,
30
+ format,
31
+ quality,
32
+ densities,
33
+ widths,
34
+ formats,
35
+ layout,
36
+ priority,
37
+ fit,
38
+ position,
39
+ background,
40
+ ...attrs
41
+ } = options;
42
+ const srcObject =
43
+ src != null && typeof src === 'object' ? (src as Record<string, unknown>) : null;
44
+
45
+ return {
46
+ ...attrs,
47
+ width: width ?? srcObject?.width,
48
+ height: height ?? srcObject?.height,
49
+ loading: (attrs.loading as string | undefined) ?? 'lazy',
50
+ decoding: (attrs.decoding as string | undefined) ?? 'async'
51
+ };
52
+ },
53
+ getSrcSet() {
54
+ return [];
55
+ }
56
+ };
57
+ }
@@ -0,0 +1,205 @@
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 { reviveDateStrings } from './lib/revive-dates.ts';
5
+ import { runWithStoryRules, type ResolveRulesConfigModule } from './storyRulesRuntime.ts';
6
+ import type { RenderStoryInput } from './types.ts';
7
+
8
+ type AstroCreateResult = {
9
+ createAstro?: (...args: unknown[]) => unknown;
10
+ };
11
+
12
+ type AstroComponentFactory = ((
13
+ result: AstroCreateResult,
14
+ props: unknown,
15
+ slots: unknown
16
+ ) => unknown) & {
17
+ isAstroComponentFactory?: boolean;
18
+ moduleId?: string;
19
+ propagation?: unknown;
20
+ };
21
+
22
+ export type HandlerProps = {
23
+ component: string;
24
+ args?: Record<string, unknown>;
25
+ slots?: Record<string, unknown>;
26
+ story?: RenderStoryInput;
27
+ };
28
+
29
+ type CreateAstroRenderHandlerOptions = {
30
+ container: Awaited<ReturnType<typeof AstroContainer.create>>;
31
+ sanitization?: SanitizationOptions;
32
+ rulesConfigFilePath?: string;
33
+ resolveRulesConfigModule?: ResolveRulesConfigModule;
34
+ loadModule: (id: string) => Promise<{ default: unknown }>;
35
+ invalidateModuleGraph?: () => void;
36
+ };
37
+
38
+ export function createAstroRenderHandler(options: CreateAstroRenderHandlerOptions) {
39
+ const sanitizationOptions = resolveSanitizationOptions(options.sanitization);
40
+ const componentCache = new Map<string, Promise<AstroComponentFactory>>();
41
+ let renderQueue = Promise.resolve<void>(undefined);
42
+
43
+ async function loadPatchedComponent(componentId: string, useCache = true) {
44
+ if (!useCache) {
45
+ const { default: component } = await options.loadModule(componentId);
46
+
47
+ return patchCreateAstroCompat(component);
48
+ }
49
+
50
+ if (!componentCache.has(componentId)) {
51
+ componentCache.set(
52
+ componentId,
53
+ (async () => {
54
+ const { default: component } = await options.loadModule(componentId);
55
+
56
+ return patchCreateAstroCompat(component);
57
+ })()
58
+ );
59
+ }
60
+
61
+ const cachedComponent = componentCache.get(componentId);
62
+
63
+ if (!cachedComponent) {
64
+ throw new Error(`Failed to load Astro component: ${componentId}`);
65
+ }
66
+
67
+ try {
68
+ return await cachedComponent;
69
+ } catch (error) {
70
+ componentCache.delete(componentId);
71
+ throw error;
72
+ }
73
+ }
74
+
75
+ return async function handler(data: HandlerProps) {
76
+ const executeRender = async () => {
77
+ return runWithStoryRules(
78
+ {
79
+ story: data.story,
80
+ rulesConfigFilePath: options.rulesConfigFilePath,
81
+ resolveRulesConfigModule: options.resolveRulesConfigModule,
82
+ invalidateModuleGraph: options.invalidateModuleGraph
83
+ },
84
+ async (selectedRules) => {
85
+ const patchedComponent = await loadPatchedComponent(
86
+ data.component,
87
+ selectedRules.moduleMocks.size === 0
88
+ );
89
+ const processedArgs = await processImageMetadata(data.args ?? {});
90
+ const revivedArgs = reviveDateStrings(processedArgs);
91
+ const sanitizedPayload = sanitizeRenderPayload(
92
+ {
93
+ args: revivedArgs,
94
+ slots: data.slots ?? {}
95
+ },
96
+ sanitizationOptions
97
+ );
98
+
99
+ return options.container.renderToString(
100
+ patchedComponent as Parameters<typeof options.container.renderToString>[0],
101
+ {
102
+ props: sanitizedPayload.args,
103
+ slots: sanitizedPayload.slots
104
+ }
105
+ );
106
+ }
107
+ );
108
+ };
109
+
110
+ const resultPromise = renderQueue.then(executeRender, executeRender);
111
+
112
+ renderQueue = resultPromise.then(
113
+ () => undefined,
114
+ () => undefined
115
+ );
116
+
117
+ return resultPromise;
118
+ };
119
+ }
120
+
121
+ export function patchCreateAstroCompat(component: unknown): AstroComponentFactory {
122
+ if (typeof component !== 'function') {
123
+ throw new Error('Expected Astro component factory to be a function.');
124
+ }
125
+
126
+ const originalComponent = component as AstroComponentFactory;
127
+ const wrapped = ((result: AstroCreateResult, props: unknown, slots: unknown) => {
128
+ if (result && typeof result.createAstro === 'function') {
129
+ const originalCreateAstro = result.createAstro;
130
+ const runtimeExpectsAstroGlobal = originalCreateAstro.length >= 3;
131
+
132
+ result.createAstro = (...args: unknown[]) => {
133
+ if (args.length === 3 && !runtimeExpectsAstroGlobal) {
134
+ return originalCreateAstro(args[1], args[2]);
135
+ }
136
+
137
+ return originalCreateAstro(...args);
138
+ };
139
+ }
140
+
141
+ return originalComponent(result, props, slots);
142
+ }) as AstroComponentFactory;
143
+
144
+ wrapped.isAstroComponentFactory = originalComponent.isAstroComponentFactory;
145
+ wrapped.moduleId = originalComponent.moduleId;
146
+ wrapped.propagation = originalComponent.propagation;
147
+
148
+ return wrapped;
149
+ }
150
+
151
+ async function processImageMetadata(
152
+ args: Record<string, unknown>
153
+ ): Promise<Record<string, unknown>> {
154
+ const processed: Record<string, unknown> = {};
155
+
156
+ for (const [key, value] of Object.entries(args)) {
157
+ if (isImageMetadata(value)) {
158
+ // Keep ImageMetadata as an object so Astro's image pipeline still
159
+ // recognizes it as an imported image and skips local path validation.
160
+ processed[key] = value;
161
+
162
+ continue;
163
+ }
164
+
165
+ if (Array.isArray(value)) {
166
+ processed[key] = await Promise.all(
167
+ value.map(async (item) => {
168
+ if (isImageMetadata(item)) {
169
+ return item;
170
+ }
171
+
172
+ if (isRecord(item)) {
173
+ return processImageMetadata(item);
174
+ }
175
+
176
+ return item;
177
+ })
178
+ );
179
+
180
+ continue;
181
+ }
182
+
183
+ if (isRecord(value)) {
184
+ processed[key] = await processImageMetadata(value);
185
+
186
+ continue;
187
+ }
188
+
189
+ processed[key] = value;
190
+ }
191
+
192
+ return processed;
193
+ }
194
+
195
+ function isImageMetadata(value: unknown): value is Record<string, unknown> {
196
+ return (
197
+ isRecord(value) &&
198
+ typeof value.src === 'string' &&
199
+ ('width' in value || 'height' in value || 'format' in value)
200
+ );
201
+ }
202
+
203
+ function isRecord(value: unknown): value is Record<string, unknown> {
204
+ return typeof value === 'object' && value !== null;
205
+ }
@@ -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;
@@ -0,0 +1,106 @@
1
+ import { describe, expect, test } from 'vitest';
2
+ import { reviveDateStrings } from './revive-dates.ts';
3
+
4
+ describe('reviveDateStrings', () => {
5
+ test('converts an ISO 8601 date string to a Date object', () => {
6
+ const args = { pubDate: '2025-04-12T00:00:00.000Z' };
7
+ const result = reviveDateStrings(args);
8
+
9
+ expect(result.pubDate).toBeInstanceOf(Date);
10
+ expect((result.pubDate as Date).toISOString()).toBe('2025-04-12T00:00:00.000Z');
11
+ });
12
+
13
+ test('converts dates nested inside objects', () => {
14
+ const args = {
15
+ post: {
16
+ data: {
17
+ pubDate: '2025-04-12T00:00:00.000Z'
18
+ }
19
+ }
20
+ };
21
+ const result = reviveDateStrings(args);
22
+
23
+ expect((result.post as Record<string, unknown>)).toMatchObject({
24
+ data: {
25
+ pubDate: expect.any(Date)
26
+ }
27
+ });
28
+ });
29
+
30
+ test('converts dates inside arrays', () => {
31
+ const args = {
32
+ posts: [
33
+ { pubDate: '2025-04-12T00:00:00.000Z' },
34
+ { pubDate: '2025-03-08T00:00:00.000Z' }
35
+ ]
36
+ };
37
+ const result = reviveDateStrings(args);
38
+ const posts = result.posts as Array<{ pubDate: Date }>;
39
+
40
+ expect(posts[0].pubDate).toBeInstanceOf(Date);
41
+ expect(posts[1].pubDate).toBeInstanceOf(Date);
42
+ expect(posts[0].pubDate.toISOString()).toBe('2025-04-12T00:00:00.000Z');
43
+ expect(posts[1].pubDate.toISOString()).toBe('2025-03-08T00:00:00.000Z');
44
+ });
45
+
46
+ test('leaves non-date strings untouched', () => {
47
+ const args = {
48
+ title: 'Hello World',
49
+ description: 'A post about dates',
50
+ empty: ''
51
+ };
52
+ const result = reviveDateStrings(args);
53
+
54
+ expect(result.title).toBe('Hello World');
55
+ expect(result.description).toBe('A post about dates');
56
+ expect(result.empty).toBe('');
57
+ });
58
+
59
+ test('leaves date-only strings untouched (no time component)', () => {
60
+ const args = { date: '2025-04-12' };
61
+ const result = reviveDateStrings(args);
62
+
63
+ expect(result.date).toBe('2025-04-12');
64
+ });
65
+
66
+ test('leaves partial ISO strings untouched', () => {
67
+ const args = {
68
+ noMillis: '2025-04-12T00:00:00Z',
69
+ withOffset: '2025-04-12T00:00:00.000+00:00',
70
+ dateOnly: '2025-04-12'
71
+ };
72
+ const result = reviveDateStrings(args);
73
+
74
+ expect(result.noMillis).toBe('2025-04-12T00:00:00Z');
75
+ expect(result.withOffset).toBe('2025-04-12T00:00:00.000+00:00');
76
+ expect(result.dateOnly).toBe('2025-04-12');
77
+ });
78
+
79
+ test('preserves non-string values', () => {
80
+ const args = {
81
+ count: 42,
82
+ active: true,
83
+ missing: null,
84
+ tags: ['a', 'b']
85
+ };
86
+ const result = reviveDateStrings(args);
87
+
88
+ expect(result.count).toBe(42);
89
+ expect(result.active).toBe(true);
90
+ expect(result.missing).toBe(null);
91
+ expect(result.tags).toEqual(['a', 'b']);
92
+ });
93
+
94
+ test('handles an empty args object', () => {
95
+ expect(reviveDateStrings({})).toEqual({});
96
+ });
97
+
98
+ test('round-trips a Date through JSON serialization', () => {
99
+ const original = new Date('2025-06-04T14:30:00.000Z');
100
+ const serialized = JSON.parse(JSON.stringify({ date: original })) as Record<string, unknown>;
101
+ const result = reviveDateStrings(serialized);
102
+
103
+ expect(result.date).toBeInstanceOf(Date);
104
+ expect((result.date as Date).getTime()).toBe(original.getTime());
105
+ });
106
+ });
@@ -0,0 +1,51 @@
1
+ /**
2
+ * Revives Date objects that were lost during JSON serialization.
3
+ *
4
+ * When story args travel over Vite HMR (dev) or HTTP (server mode), Date
5
+ * values are serialized by JSON.stringify into ISO 8601 strings like
6
+ * "2025-04-12T00:00:00.000Z". This function walks the args tree and
7
+ * converts those strings back into Date objects so Astro components
8
+ * receive the types they expect.
9
+ *
10
+ * Only the exact format produced by Date.toJSON() is matched
11
+ * (YYYY-MM-DDTHH:mm:ss.sssZ) to minimize false positives.
12
+ */
13
+
14
+ // Matches the exact output of Date.toJSON() / JSON.stringify(date).
15
+ const ISO_DATE_PATTERN = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/;
16
+
17
+ export function reviveDateStrings(args: Record<string, unknown>): Record<string, unknown> {
18
+ const revived: Record<string, unknown> = {};
19
+
20
+ for (const [key, value] of Object.entries(args)) {
21
+ revived[key] = reviveValue(value);
22
+ }
23
+
24
+ return revived;
25
+ }
26
+
27
+ function reviveValue(value: unknown): unknown {
28
+ if (typeof value === 'string' && ISO_DATE_PATTERN.test(value)) {
29
+ const date = new Date(value);
30
+
31
+ if (!Number.isNaN(date.getTime())) {
32
+ return date;
33
+ }
34
+
35
+ return value;
36
+ }
37
+
38
+ if (Array.isArray(value)) {
39
+ return value.map(reviveValue);
40
+ }
41
+
42
+ if (isRecord(value)) {
43
+ return reviveDateStrings(value);
44
+ }
45
+
46
+ return value;
47
+ }
48
+
49
+ function isRecord(value: unknown): value is Record<string, unknown> {
50
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
51
+ }