@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.
- package/dist/{base-IRZo3zgK.d.ts → base-DT67T5pi.d.ts} +1 -0
- package/dist/{chunk-T7NWIO5S.js → chunk-2EABPTOY.js} +5 -5
- package/dist/{chunk-PJEDXZVN.js → chunk-7YBE4TTI.js} +2 -1
- package/dist/chunk-7YBE4TTI.js.map +1 -0
- package/dist/{chunk-POHTFYST.js → chunk-AYYMNFI6.js} +104 -6
- package/dist/chunk-AYYMNFI6.js.map +1 -0
- package/dist/chunk-B5HHF6FC.js +116 -0
- package/dist/chunk-B5HHF6FC.js.map +1 -0
- package/dist/chunk-H3XZHW6Z.js +1402 -0
- package/dist/chunk-H3XZHW6Z.js.map +1 -0
- package/dist/{chunk-DNGQBPT7.js → chunk-PUTCAN6X.js} +5 -2
- package/dist/{chunk-DNGQBPT7.js.map → chunk-PUTCAN6X.js.map} +1 -1
- package/dist/{chunk-OUEDTRBO.js → chunk-TWAO2IQW.js} +229 -67
- package/dist/chunk-TWAO2IQW.js.map +1 -0
- package/dist/{chunk-4SWPVM6R.js → chunk-WUTCMEF5.js} +2 -2
- package/dist/index.d.ts +17 -7
- package/dist/index.js +6 -5
- package/dist/index.js.map +1 -1
- package/dist/integrations/index.d.ts +2 -1
- package/dist/integrations/index.js +1 -1
- package/dist/middleware.js +18 -131
- package/dist/middleware.js.map +1 -1
- package/dist/{types-C-jan6Px.d.ts → preset-BvgHg2of.d.ts} +8 -11
- package/dist/preset.d.ts +2 -10
- package/dist/preset.js +5 -4
- package/dist/renderer/renderer-dev.js +62 -0
- package/dist/renderer/renderer-dev.js.map +1 -0
- package/dist/renderer/renderer-server.js +92 -0
- package/dist/renderer/renderer-server.js.map +1 -0
- package/dist/renderer/renderer-static.js +54 -0
- package/dist/renderer/renderer-static.js.map +1 -0
- package/dist/testing.js +12 -11
- package/dist/testing.js.map +1 -1
- package/dist/{viteStorybookAstroMiddlewarePlugin-2EFKTECT.js → viteStorybookAstroMiddlewarePlugin-UB6ZLJ4B.js} +4 -3
- package/dist/vitest/global-setup.js +6 -5
- package/dist/vitest/global-setup.js.map +1 -1
- package/dist/vitest/index.d.ts +1 -1
- package/dist/vitest/index.js +3 -3
- package/package.json +14 -43
- package/src/astroImageService.ts +57 -0
- package/src/astroRenderHandler.ts +203 -0
- package/src/importAstroConfig.ts +1 -1
- package/src/index.ts +2 -0
- package/src/integrations/alpine.ts +1 -0
- package/src/integrations/base.ts +6 -0
- package/src/middleware.ts +29 -200
- package/src/module-mocks.ts +153 -5
- package/src/preset.ts +38 -8
- package/src/productionRenderRuntime.ts +187 -0
- package/src/rules.test.ts +52 -4
- package/src/rules.ts +54 -7
- package/src/server/index.ts +101 -31
- package/src/storyRulesRuntime.ts +34 -0
- package/src/storySsrVite.ts +240 -0
- package/src/types.ts +0 -9
- package/src/virtual.d.ts +17 -3
- package/src/vite/{astroFilesVirtualModulePlugin.ts → astroFilesPlugin.ts} +4 -4
- package/src/vite/sanitizeConfigPlugin.ts +18 -0
- package/src/vite/{storybookAstroServerAuthConfigVirtualModulePlugin.test.ts → serverAuthPlugin.test.ts} +7 -10
- package/src/vite/{storybookAstroServerAuthConfigVirtualModulePlugin.ts → serverAuthPlugin.ts} +6 -9
- package/src/vite/serverRuntimePlugin.ts +109 -0
- package/src/vite/{storybookAstroRulesConfigVirtualModulePlugin.ts → storyRulesPlugin.ts} +6 -7
- package/src/vite/{createVirtualModulePlugin.test.ts → virtualModulePlugin.test.ts} +5 -5
- package/src/vite/{createVirtualModulePlugin.ts → virtualModulePlugin.ts} +2 -2
- package/src/viteAstroContainerRenderersPlugin.ts +72 -2
- package/src/vitePluginAstroBuildPrerender.ts +75 -646
- package/src/vitePluginAstroBuildServer.ts +217 -165
- package/src/vitePluginAstroBuildShared.test.ts +87 -0
- package/src/vitePluginAstroBuildShared.ts +465 -0
- package/src/vitePluginStoryModuleMocks.ts +29 -0
- package/src/viteStorybookAstroMiddlewarePlugin.ts +8 -0
- package/src/viteStorybookAstroRendererPlugin.ts +13 -6
- package/src/viteStorybookRendererFallbackPlugin.ts +2 -2
- package/dist/chunk-OUEDTRBO.js.map +0 -1
- package/dist/chunk-PBISP7PA.js +0 -1137
- package/dist/chunk-PBISP7PA.js.map +0 -1
- package/dist/chunk-PJEDXZVN.js.map +0 -1
- package/dist/chunk-POHTFYST.js.map +0 -1
- package/dist/node/index.d.ts +0 -10
- package/dist/node/index.js +0 -10
- package/dist/node/index.js.map +0 -1
- package/src/vite/storybookAstroSanitizationConfigVirtualModulePlugin.ts +0 -21
- /package/dist/{chunk-T7NWIO5S.js.map → chunk-2EABPTOY.js.map} +0 -0
- /package/dist/{chunk-4SWPVM6R.js.map → chunk-WUTCMEF5.js.map} +0 -0
- /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
|
+
}
|
package/src/importAstroConfig.ts
CHANGED
package/src/index.ts
CHANGED
package/src/integrations/base.ts
CHANGED
|
@@ -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 {
|
|
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
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
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
|
-
|
|
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
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
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
|
}
|
package/src/module-mocks.ts
CHANGED
|
@@ -1,16 +1,164 @@
|
|
|
1
|
-
|
|
1
|
+
const STORYBOOK_ASTRO_GET_MOCK_EXPORT = '__STORYBOOK_ASTRO_GET_STORY_MODULE_MOCK_EXPORT__';
|
|
2
2
|
|
|
3
|
-
export
|
|
3
|
+
export const STORYBOOK_ASTRO_INLINE_MODULE_PREFIX = 'virtual:storybook-astro-inline-module:';
|
|
4
4
|
|
|
5
|
-
|
|
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
|
-
|
|
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
|
|
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
|
}
|