@storybook-astro/framework 1.0.3 → 1.1.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/{chunk-KSDXET2L.js → chunk-4HECE7IW.js} +477 -61
- package/dist/chunk-4HECE7IW.js.map +1 -0
- package/dist/{chunk-7GHEQUPV.js → chunk-POHTFYST.js} +46 -8
- package/dist/chunk-POHTFYST.js.map +1 -0
- package/dist/chunk-T7NWIO5S.js +220 -0
- package/dist/chunk-T7NWIO5S.js.map +1 -0
- package/dist/{chunk-C5OH4VBR.js → chunk-V76WSNSP.js} +124 -47
- package/dist/chunk-V76WSNSP.js.map +1 -0
- package/dist/index.d.ts +19 -9
- package/dist/index.js +10 -3
- package/dist/index.js.map +1 -1
- package/dist/middleware.js +57 -39
- package/dist/middleware.js.map +1 -1
- package/dist/node/index.d.ts +10 -0
- package/dist/node/index.js +10 -0
- package/dist/node/index.js.map +1 -0
- package/dist/preset.d.ts +1 -1
- package/dist/preset.js +3 -3
- package/dist/testing.js +12 -64
- package/dist/testing.js.map +1 -1
- package/dist/{types-CHTsRtA7.d.ts → types-Cvor6Tyi.d.ts} +21 -5
- package/dist/{viteStorybookAstroMiddlewarePlugin-NP2E52IC.js → viteStorybookAstroMiddlewarePlugin-2EFKTECT.js} +2 -2
- package/dist/vitest/global-setup.js +42 -0
- package/dist/vitest/global-setup.js.map +1 -0
- package/dist/vitest/index.js +20 -3
- package/dist/vitest/index.js.map +1 -1
- package/package.json +11 -3
- package/src/index.ts +21 -1
- package/src/lib/sanitization.ts +104 -0
- package/src/middleware.ts +76 -44
- package/src/node/index.ts +7 -0
- package/src/preset.ts +86 -16
- package/src/renderer/renderer-dev.ts +82 -0
- package/src/renderer/renderer-server.test.ts +101 -0
- package/src/renderer/renderer-server.ts +135 -0
- package/src/renderer/renderer-static.ts +62 -0
- package/src/rules.test.ts +89 -18
- package/src/rules.ts +67 -18
- package/src/server/index.ts +111 -0
- package/src/testing/renderer-daemon.ts +10 -1
- package/src/types.ts +25 -5
- package/src/virtual.d.ts +37 -0
- package/src/vite/astroFilesVirtualModulePlugin.ts +36 -0
- package/src/vite/createVirtualModulePlugin.ts +3 -3
- package/src/vite/storybookAstroRulesConfigVirtualModulePlugin.ts +37 -0
- package/src/vite/storybookAstroSanitizationConfigVirtualModulePlugin.ts +21 -0
- package/src/vite/storybookAstroServerAuthConfigVirtualModulePlugin.test.ts +71 -0
- package/src/vite/storybookAstroServerAuthConfigVirtualModulePlugin.ts +42 -0
- package/src/vitePluginAstroBuildPrerender.ts +50 -51
- package/src/vitePluginAstroBuildServer.ts +289 -0
- package/src/vitePluginAstroIntegrationOptsFallback.ts +25 -0
- package/src/vitePluginAstroToolbarFallback.ts +38 -0
- package/src/viteStorybookAstroMiddlewarePlugin.ts +40 -8
- package/src/viteStorybookAstroRendererPlugin.ts +45 -0
- package/src/vitest/config.ts +45 -4
- package/src/vitest/global-setup.ts +45 -0
- package/dist/chunk-7GHEQUPV.js.map +0 -1
- package/dist/chunk-C5OH4VBR.js.map +0 -1
- package/dist/chunk-KSDXET2L.js.map +0 -1
- package/dist/middleware.d.ts +0 -26
- package/src/msw-helpers.ts +0 -1
- package/src/msw.ts +0 -58
- /package/dist/{viteStorybookAstroMiddlewarePlugin-NP2E52IC.js.map → viteStorybookAstroMiddlewarePlugin-2EFKTECT.js.map} +0 -0
package/src/lib/sanitization.ts
CHANGED
|
@@ -160,6 +160,20 @@ export function sanitizeRenderPayload(
|
|
|
160
160
|
};
|
|
161
161
|
}
|
|
162
162
|
|
|
163
|
+
export function serializeSanitizationOptions(options?: SanitizationOptions): string {
|
|
164
|
+
if (!options) {
|
|
165
|
+
return 'undefined';
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
assertNoFunctions(options.sanitizeHtml, 'framework.options.sanitization.sanitizeHtml');
|
|
169
|
+
|
|
170
|
+
const state = {
|
|
171
|
+
seen: new WeakSet<object>()
|
|
172
|
+
};
|
|
173
|
+
|
|
174
|
+
return serializeValue(options, 'framework.options.sanitization', state);
|
|
175
|
+
}
|
|
176
|
+
|
|
163
177
|
function mergeSanitizeHtmlOptions(userOptions?: IOptions): IOptions {
|
|
164
178
|
const merged: IOptions = {
|
|
165
179
|
...DEFAULT_SANITIZE_HTML_OPTIONS,
|
|
@@ -287,6 +301,96 @@ function matchesPathPattern(path: string, pattern: string): boolean {
|
|
|
287
301
|
return matchSegments(pathSegments, patternSegments);
|
|
288
302
|
}
|
|
289
303
|
|
|
304
|
+
function serializeValue(value: unknown, path: string, state: { seen: WeakSet<object> }): string {
|
|
305
|
+
if (value === null) {
|
|
306
|
+
return 'null';
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
if (value === undefined) {
|
|
310
|
+
return 'undefined';
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
if (typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean') {
|
|
314
|
+
return JSON.stringify(value);
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
if (value instanceof RegExp) {
|
|
318
|
+
return value.toString();
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
if (Array.isArray(value)) {
|
|
322
|
+
const serializedItems = value.map((item, index) =>
|
|
323
|
+
serializeValue(item, `${path}[${index}]`, state)
|
|
324
|
+
);
|
|
325
|
+
|
|
326
|
+
return `[${serializedItems.join(', ')}]`;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
if (isRecord(value)) {
|
|
330
|
+
if (state.seen.has(value)) {
|
|
331
|
+
throw new Error(`${path} contains a circular reference.`);
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
state.seen.add(value);
|
|
335
|
+
|
|
336
|
+
const serializedEntries = Object.entries(value)
|
|
337
|
+
.filter(([, nestedValue]) => nestedValue !== undefined)
|
|
338
|
+
.map(([key, nestedValue]) => {
|
|
339
|
+
const serializedNestedValue = serializeValue(nestedValue, `${path}.${key}`, state);
|
|
340
|
+
|
|
341
|
+
return `${JSON.stringify(key)}: ${serializedNestedValue}`;
|
|
342
|
+
});
|
|
343
|
+
|
|
344
|
+
return `{ ${serializedEntries.join(', ')} }`;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
throw new Error(
|
|
348
|
+
`${path} contains an unsupported value of type ${typeof value}. ` +
|
|
349
|
+
'Only plain objects, arrays, primitives, and regular expressions are supported.'
|
|
350
|
+
);
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
function assertNoFunctions(value: unknown, path: string): void {
|
|
354
|
+
const state = {
|
|
355
|
+
seen: new WeakSet<object>()
|
|
356
|
+
};
|
|
357
|
+
|
|
358
|
+
assertNoFunctionsRecursive(value, path, state);
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
function assertNoFunctionsRecursive(
|
|
362
|
+
value: unknown,
|
|
363
|
+
path: string,
|
|
364
|
+
state: { seen: WeakSet<object> }
|
|
365
|
+
): void {
|
|
366
|
+
if (typeof value === 'function') {
|
|
367
|
+
throw new Error(
|
|
368
|
+
`${path} cannot contain functions. ` +
|
|
369
|
+
'Function-valued sanitization hooks are not supported in framework options.'
|
|
370
|
+
);
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
if (Array.isArray(value)) {
|
|
374
|
+
value.forEach((item, index) => {
|
|
375
|
+
assertNoFunctionsRecursive(item, `${path}[${index}]`, state);
|
|
376
|
+
});
|
|
377
|
+
|
|
378
|
+
return;
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
if (isRecord(value)) {
|
|
382
|
+
if (state.seen.has(value)) {
|
|
383
|
+
return;
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
state.seen.add(value);
|
|
387
|
+
|
|
388
|
+
Object.entries(value).forEach(([key, nestedValue]) => {
|
|
389
|
+
assertNoFunctionsRecursive(nestedValue, `${path}.${key}`, state);
|
|
390
|
+
});
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
|
|
290
394
|
function matchSegments(pathSegments: string[], patternSegments: string[]): boolean {
|
|
291
395
|
if (patternSegments.length === 0) {
|
|
292
396
|
return pathSegments.length === 0;
|
package/src/middleware.ts
CHANGED
|
@@ -4,8 +4,7 @@ import type { Integration } from './integrations/index.ts';
|
|
|
4
4
|
import type { SanitizationOptions } from './lib/sanitization.ts';
|
|
5
5
|
import { resolveSanitizationOptions, sanitizeRenderPayload } from './lib/sanitization.ts';
|
|
6
6
|
import { resolveStoryModuleMock, withStoryModuleMocks } from './module-mocks.ts';
|
|
7
|
-
import {
|
|
8
|
-
import { selectStoryRules } from './rules.ts';
|
|
7
|
+
import { selectStoryRules, withStoryRuleCleanups } from './rules.ts';
|
|
9
8
|
import type { RenderStoryInput } from './types.ts';
|
|
10
9
|
import { addRenderers, resolveClientModules } from 'virtual:astro-container-renderers';
|
|
11
10
|
|
|
@@ -33,7 +32,6 @@ export type HandlerProps = {
|
|
|
33
32
|
};
|
|
34
33
|
|
|
35
34
|
type HandlerFactoryOptions = {
|
|
36
|
-
mode?: 'development' | 'production';
|
|
37
35
|
sanitization?: SanitizationOptions;
|
|
38
36
|
rulesConfigFilePath?: string;
|
|
39
37
|
resolveRulesConfigModule?: ResolveRulesConfigModule;
|
|
@@ -41,7 +39,52 @@ type HandlerFactoryOptions = {
|
|
|
41
39
|
};
|
|
42
40
|
|
|
43
41
|
export async function handlerFactory(_integrations: Integration[], options?: HandlerFactoryOptions) {
|
|
44
|
-
|
|
42
|
+
// Inject a passthrough image service before any component renders.
|
|
43
|
+
//
|
|
44
|
+
// AstroContainer has no image service configuration API, and the default
|
|
45
|
+
// getConfiguredImageService() tries to dynamically import "virtual:image-service"
|
|
46
|
+
// which fails in astro6/Vite 7's module runner. Even when it succeeds (astro5),
|
|
47
|
+
// the noop service still routes through /_image?href=... URLs that the Storybook
|
|
48
|
+
// dev server cannot serve.
|
|
49
|
+
//
|
|
50
|
+
// Pre-populating globalThis.astroAsset.imageService bypasses the dynamic import
|
|
51
|
+
// entirely. Our service returns the direct /@fs/... Vite URL from the ImageMetadata
|
|
52
|
+
// object, which Vite can serve as a static asset in the browser.
|
|
53
|
+
if (!globalThis.astroAsset) {
|
|
54
|
+
(globalThis as Record<string, unknown>).astroAsset = {};
|
|
55
|
+
}
|
|
56
|
+
(globalThis.astroAsset as Record<string, unknown>).imageService = {
|
|
57
|
+
propertiesToHash: ['src'],
|
|
58
|
+
validateOptions(options: Record<string, unknown>) {
|
|
59
|
+
return options;
|
|
60
|
+
},
|
|
61
|
+
getURL(options: { src: unknown }) {
|
|
62
|
+
const src = options.src;
|
|
63
|
+
|
|
64
|
+
if (src != null && typeof src === 'object' && 'src' in src && typeof (src as Record<string, unknown>).src === 'string') {
|
|
65
|
+
// ImageMetadata object — return the /@fs/... Vite URL directly
|
|
66
|
+
return (src as Record<string, unknown>).src as string;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return typeof src === 'string' ? src : '';
|
|
70
|
+
},
|
|
71
|
+
getHTMLAttributes(options: Record<string, unknown>) {
|
|
72
|
+
const { src, width, height, format, quality, densities, widths, formats, layout, priority, fit, position, background, ...attrs } = options;
|
|
73
|
+
const srcObj = src != null && typeof src === 'object' ? src as Record<string, unknown> : null;
|
|
74
|
+
|
|
75
|
+
return {
|
|
76
|
+
...attrs,
|
|
77
|
+
width: width ?? srcObj?.width,
|
|
78
|
+
height: height ?? srcObj?.height,
|
|
79
|
+
loading: (attrs.loading as string | undefined) ?? 'lazy',
|
|
80
|
+
decoding: (attrs.decoding as string | undefined) ?? 'async',
|
|
81
|
+
};
|
|
82
|
+
},
|
|
83
|
+
getSrcSet() {
|
|
84
|
+
return [];
|
|
85
|
+
}
|
|
86
|
+
};
|
|
87
|
+
|
|
45
88
|
const container = await AstroContainer.create({
|
|
46
89
|
// Somewhat hacky way to force client-side Storybook's Vite to resolve modules properly
|
|
47
90
|
resolve: async (specifier) => {
|
|
@@ -116,33 +159,32 @@ export async function handlerFactory(_integrations: Integration[], options?: Han
|
|
|
116
159
|
const selectedRules = await selectStoryRules({
|
|
117
160
|
configModule: rulesConfigModule,
|
|
118
161
|
configFilePath: options?.rulesConfigFilePath,
|
|
119
|
-
mode,
|
|
120
162
|
story: data.story
|
|
121
163
|
});
|
|
122
164
|
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
);
|
|
165
|
+
return withStoryRuleCleanups(selectedRules.cleanups, async () => {
|
|
166
|
+
return withStoryModuleMocks(selectedRules.moduleMocks, async () => {
|
|
167
|
+
const patchedComponent = await loadPatchedComponent(
|
|
168
|
+
data.component,
|
|
169
|
+
selectedRules.moduleMocks.size === 0
|
|
170
|
+
);
|
|
171
|
+
const processedArgs = await processImageMetadata(data.args ?? {});
|
|
172
|
+
const sanitizedPayload = sanitizeRenderPayload(
|
|
173
|
+
{
|
|
174
|
+
args: processedArgs,
|
|
175
|
+
slots: data.slots ?? {}
|
|
176
|
+
},
|
|
177
|
+
sanitizationOptions
|
|
178
|
+
);
|
|
179
|
+
|
|
180
|
+
return container.renderToString(
|
|
181
|
+
patchedComponent as Parameters<typeof container.renderToString>[0],
|
|
182
|
+
{
|
|
183
|
+
props: sanitizedPayload.args,
|
|
184
|
+
slots: sanitizedPayload.slots
|
|
185
|
+
}
|
|
186
|
+
);
|
|
187
|
+
});
|
|
146
188
|
});
|
|
147
189
|
};
|
|
148
190
|
|
|
@@ -194,7 +236,11 @@ async function processImageMetadata(
|
|
|
194
236
|
|
|
195
237
|
for (const [key, value] of Object.entries(args)) {
|
|
196
238
|
if (isImageMetadata(value)) {
|
|
197
|
-
|
|
239
|
+
// Keep ImageMetadata as a plain object — Astro's image service checks
|
|
240
|
+
// isESMImportedImage (typeof src === 'object') and skips the /@fs/ string
|
|
241
|
+
// validation that throws LocalImageUsedWrongly. Converting to a URL string
|
|
242
|
+
// causes that error when the string starts with /@fs/.
|
|
243
|
+
processed[key] = value;
|
|
198
244
|
|
|
199
245
|
continue;
|
|
200
246
|
}
|
|
@@ -203,7 +249,7 @@ async function processImageMetadata(
|
|
|
203
249
|
processed[key] = await Promise.all(
|
|
204
250
|
value.map(async (item) => {
|
|
205
251
|
if (isImageMetadata(item)) {
|
|
206
|
-
return
|
|
252
|
+
return item;
|
|
207
253
|
}
|
|
208
254
|
|
|
209
255
|
if (isRecord(item)) {
|
|
@@ -237,20 +283,6 @@ function isImageMetadata(value: unknown): value is Record<string, unknown> {
|
|
|
237
283
|
);
|
|
238
284
|
}
|
|
239
285
|
|
|
240
|
-
function convertImageMetadataToUrl(imageMetadata: Record<string, unknown>): string {
|
|
241
|
-
const src = imageMetadata.src;
|
|
242
|
-
const fsPath = imageMetadata.fsPath;
|
|
243
|
-
|
|
244
|
-
if (typeof src === 'string') {
|
|
245
|
-
return src;
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
if (typeof fsPath === 'string') {
|
|
249
|
-
return fsPath;
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
return String(imageMetadata);
|
|
253
|
-
}
|
|
254
286
|
|
|
255
287
|
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
256
288
|
return typeof value === 'object' && value !== null;
|
package/src/preset.ts
CHANGED
|
@@ -1,21 +1,30 @@
|
|
|
1
1
|
import type { StorybookConfigVite, FrameworkOptions } from './types.ts';
|
|
2
2
|
import { vitePluginStorybookAstroMiddleware } from './viteStorybookAstroMiddlewarePlugin.ts';
|
|
3
3
|
import { viteStorybookRendererFallbackPlugin } from './viteStorybookRendererFallbackPlugin.ts';
|
|
4
|
+
import { viteStorybookAstroRendererPlugin } from './viteStorybookAstroRendererPlugin.ts';
|
|
4
5
|
import { vitePluginAstroComponentMarker } from './vitePluginAstroComponentMarker.ts';
|
|
5
6
|
import { vitePluginAstroBuildPrerender } from './vitePluginAstroBuildPrerender.ts';
|
|
7
|
+
import { vitePluginAstroBuildServer } from './vitePluginAstroBuildServer.ts';
|
|
8
|
+
import { vitePluginAstroIntegrationOptsFallback } from './vitePluginAstroIntegrationOptsFallback.ts';
|
|
6
9
|
import { vitePluginAstroVueFallback } from './vitePluginAstroVueFallback.ts';
|
|
10
|
+
import { vitePluginAstroToolbarFallback } from './vitePluginAstroToolbarFallback.ts';
|
|
7
11
|
import { resolveSanitizationOptions } from './lib/sanitization.ts';
|
|
8
12
|
import { mergeWithAstroConfig } from './vitePluginAstro.ts';
|
|
9
13
|
|
|
10
14
|
export const core = {
|
|
11
15
|
builder: '@storybook/builder-vite',
|
|
12
|
-
|
|
16
|
+
// Use import.meta.resolve so Storybook receives an absolute file:// URL
|
|
17
|
+
// to the renderer preset rather than a bare package specifier. When
|
|
18
|
+
// package managers like pnpm use strict node_modules isolation, bare
|
|
19
|
+
// specifiers are resolved from the *project root*, where the renderer
|
|
20
|
+
// (a dep of this framework, not the user's project) is not hoisted.
|
|
21
|
+
// The absolute URL is resolved from *this* file's location where the
|
|
22
|
+
// renderer is always accessible as a direct dependency.
|
|
23
|
+
renderer: import.meta.resolve('@storybook-astro/renderer')
|
|
13
24
|
};
|
|
14
25
|
|
|
15
26
|
export const viteFinal: StorybookConfigVite['viteFinal'] = async (config, { configType, presets }) => {
|
|
16
27
|
const options = await presets.apply<FrameworkOptions>('frameworkOptions');
|
|
17
|
-
const { vitePlugin: storybookAstroMiddlewarePlugin, viteConfig } =
|
|
18
|
-
await vitePluginStorybookAstroMiddleware(options);
|
|
19
28
|
|
|
20
29
|
if (!config.plugins) {
|
|
21
30
|
config.plugins = [];
|
|
@@ -23,22 +32,53 @@ export const viteFinal: StorybookConfigVite['viteFinal'] = async (config, { conf
|
|
|
23
32
|
|
|
24
33
|
const integrations = options.integrations ?? [];
|
|
25
34
|
const resolveFrom = options.resolveFrom ?? process.cwd();
|
|
35
|
+
const renderMode = options.renderMode ?? 'server';
|
|
26
36
|
const mode = configType === 'DEVELOPMENT' ? 'development' : 'production';
|
|
27
37
|
const command = configType === 'DEVELOPMENT' ? 'serve' : 'build';
|
|
28
38
|
|
|
29
39
|
resolveSanitizationOptions(options.sanitization);
|
|
30
40
|
|
|
41
|
+
config.envPrefix = mergeEnvPrefixes(config.envPrefix, 'STORYBOOK_');
|
|
42
|
+
|
|
43
|
+
const { vitePlugin: storybookAstroMiddlewarePlugin, viteConfig } =
|
|
44
|
+
await vitePluginStorybookAstroMiddleware(options);
|
|
45
|
+
|
|
31
46
|
config.plugins.push(
|
|
32
|
-
storybookAstroMiddlewarePlugin,
|
|
33
47
|
viteStorybookRendererFallbackPlugin(integrations),
|
|
48
|
+
viteStorybookAstroRendererPlugin({
|
|
49
|
+
mode,
|
|
50
|
+
renderMode,
|
|
51
|
+
server: options.server
|
|
52
|
+
}),
|
|
34
53
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
35
54
|
vitePluginAstroComponentMarker() as any,
|
|
36
|
-
|
|
37
|
-
|
|
55
|
+
vitePluginAstroIntegrationOptsFallback(),
|
|
56
|
+
vitePluginAstroToolbarFallback(),
|
|
38
57
|
vitePluginAstroVueFallback(),
|
|
39
|
-
...viteConfig.plugins
|
|
40
58
|
);
|
|
41
59
|
|
|
60
|
+
if (configType === 'DEVELOPMENT') {
|
|
61
|
+
config.plugins.push(storybookAstroMiddlewarePlugin, ...viteConfig.plugins);
|
|
62
|
+
} else if (renderMode === 'static') {
|
|
63
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
64
|
+
config.plugins.push(vitePluginAstroBuildPrerender(options) as any);
|
|
65
|
+
} else {
|
|
66
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
67
|
+
config.plugins.push(vitePluginAstroBuildServer(options) as any);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if (configType !== 'DEVELOPMENT') {
|
|
71
|
+
config.build = {
|
|
72
|
+
...(config.build ?? {}),
|
|
73
|
+
manifest: true
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
config.build.rollupOptions = {
|
|
77
|
+
...(config.build.rollupOptions ?? {}),
|
|
78
|
+
preserveEntrySignatures: 'strict'
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
|
|
42
82
|
// Add React/ReactDOM aliases for storybook-solidjs compatibility
|
|
43
83
|
if (!config.resolve) {
|
|
44
84
|
config.resolve = {};
|
|
@@ -59,17 +99,18 @@ export const viteFinal: StorybookConfigVite['viteFinal'] = async (config, { conf
|
|
|
59
99
|
|
|
60
100
|
const finalConfig = await mergeWithAstroConfig(config, integrations, resolveFrom, mode, command);
|
|
61
101
|
|
|
62
|
-
// Exclude
|
|
63
|
-
// virtual modules that esbuild cannot resolve
|
|
64
|
-
// This must be done after mergeWithAstroConfig to avoid being overwritten.
|
|
102
|
+
// Exclude Astro integration packages from dependency optimization because
|
|
103
|
+
// they import virtual modules that esbuild cannot resolve.
|
|
65
104
|
if (!finalConfig.optimizeDeps) {
|
|
66
105
|
finalConfig.optimizeDeps = {};
|
|
67
106
|
}
|
|
68
107
|
if (!finalConfig.optimizeDeps.exclude) {
|
|
69
108
|
finalConfig.optimizeDeps.exclude = [];
|
|
70
109
|
}
|
|
71
|
-
|
|
72
|
-
finalConfig.optimizeDeps.exclude.
|
|
110
|
+
for (const pkg of ['@astrojs/vue', '@astrojs/react', '@astrojs/preact']) {
|
|
111
|
+
if (!finalConfig.optimizeDeps.exclude.includes(pkg)) {
|
|
112
|
+
finalConfig.optimizeDeps.exclude.push(pkg);
|
|
113
|
+
}
|
|
73
114
|
}
|
|
74
115
|
// Exclude the renderer from Vite's esbuild pre-bundler so that
|
|
75
116
|
// import.meta.hot is preserved in the preview iframe. When installed
|
|
@@ -80,20 +121,49 @@ export const viteFinal: StorybookConfigVite['viteFinal'] = async (config, { conf
|
|
|
80
121
|
if (!finalConfig.optimizeDeps.exclude.includes('@storybook-astro/renderer')) {
|
|
81
122
|
finalConfig.optimizeDeps.exclude.push('@storybook-astro/renderer');
|
|
82
123
|
}
|
|
83
|
-
// Mark
|
|
124
|
+
// Mark integration virtual modules as external so the dep bundler doesn't
|
|
125
|
+
// try to resolve them (they are Vite virtual modules with no real package).
|
|
126
|
+
// Set both esbuildOptions (Vite ≤7) and rolldownOptions (Vite 8+, Rolldown)
|
|
127
|
+
// so the correct key is populated regardless of Vite version.
|
|
128
|
+
const integrationVirtualModules = [
|
|
129
|
+
'virtual:@astrojs/vue/app',
|
|
130
|
+
'virtual:astro:vue-app',
|
|
131
|
+
'astro:react:opts',
|
|
132
|
+
'astro:preact:opts',
|
|
133
|
+
'astro:toolbar:internal'
|
|
134
|
+
];
|
|
135
|
+
|
|
136
|
+
// Vite ≤7 (esbuild-based optimizer)
|
|
84
137
|
if (!finalConfig.optimizeDeps.esbuildOptions) {
|
|
85
138
|
finalConfig.optimizeDeps.esbuildOptions = {};
|
|
86
139
|
}
|
|
87
140
|
if (!finalConfig.optimizeDeps.esbuildOptions.external) {
|
|
88
141
|
finalConfig.optimizeDeps.esbuildOptions.external = [];
|
|
89
142
|
}
|
|
90
|
-
const
|
|
91
|
-
|
|
92
|
-
for (const mod of vueVirtualModules) {
|
|
143
|
+
for (const mod of integrationVirtualModules) {
|
|
93
144
|
if (!finalConfig.optimizeDeps.esbuildOptions.external.includes(mod)) {
|
|
94
145
|
finalConfig.optimizeDeps.esbuildOptions.external.push(mod);
|
|
95
146
|
}
|
|
96
147
|
}
|
|
97
148
|
|
|
149
|
+
// Vite 8+ (Rolldown-based optimizer) — same semantics, different key
|
|
150
|
+
// Use a loose cast because rolldownOptions is absent from Vite <8 types.
|
|
151
|
+
const optimizeDepsMut = finalConfig.optimizeDeps as Record<string, unknown>;
|
|
152
|
+
const rolldownOpts = (optimizeDepsMut.rolldownOptions ?? {}) as { external?: string[] };
|
|
153
|
+
|
|
154
|
+
rolldownOpts.external = Array.from(
|
|
155
|
+
new Set([...(rolldownOpts.external ?? []), ...integrationVirtualModules])
|
|
156
|
+
);
|
|
157
|
+
optimizeDepsMut.rolldownOptions = rolldownOpts;
|
|
158
|
+
|
|
98
159
|
return finalConfig;
|
|
99
160
|
};
|
|
161
|
+
|
|
162
|
+
function mergeEnvPrefixes(
|
|
163
|
+
existing: string | string[] | undefined,
|
|
164
|
+
additionalPrefix: string
|
|
165
|
+
): string[] {
|
|
166
|
+
const prefixes = Array.isArray(existing) ? existing : existing ? [existing] : [];
|
|
167
|
+
|
|
168
|
+
return Array.from(new Set([...prefixes, additionalPrefix]));
|
|
169
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
RenderComponentInput,
|
|
3
|
+
RenderPromise,
|
|
4
|
+
RenderResponseMessage
|
|
5
|
+
} from '@storybook-astro/renderer/types';
|
|
6
|
+
|
|
7
|
+
const pendingMessages = new Map<string, RenderPromise>();
|
|
8
|
+
const ASTRO_SERVER_UNAVAILABLE_ERROR_NAME = 'AstroRenderServerUnavailableError';
|
|
9
|
+
|
|
10
|
+
export async function render(data: RenderComponentInput, timeoutMs = 5000) {
|
|
11
|
+
// eslint-disable-next-line n/no-unsupported-features/node-builtins
|
|
12
|
+
const id = crypto.randomUUID();
|
|
13
|
+
|
|
14
|
+
const promise = new Promise<RenderResponseMessage['data']>((resolve, reject) => {
|
|
15
|
+
const timeoutId = setTimeout(() => {
|
|
16
|
+
pendingMessages.delete(id);
|
|
17
|
+
|
|
18
|
+
const error = new Error(
|
|
19
|
+
`Unable to reach Astro rendering server. No render response was received within ${timeoutMs}ms.`
|
|
20
|
+
);
|
|
21
|
+
|
|
22
|
+
error.name = ASTRO_SERVER_UNAVAILABLE_ERROR_NAME;
|
|
23
|
+
|
|
24
|
+
reject(error);
|
|
25
|
+
}, timeoutMs);
|
|
26
|
+
|
|
27
|
+
pendingMessages.set(id, { resolve, reject, timeoutId });
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
import.meta.hot?.send('astro:render:request', {
|
|
31
|
+
...data,
|
|
32
|
+
id
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
return promise;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function init() {
|
|
39
|
+
import.meta.hot?.on('vite:afterUpdate', (payload) => {
|
|
40
|
+
if (payload.updates.some((update) => isAstroStyleUpdate(update.path))) {
|
|
41
|
+
applyStyles();
|
|
42
|
+
}
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
import.meta.hot?.on('astro:render:response', (data: RenderResponseMessage['data']) => {
|
|
46
|
+
if (!data.id || !pendingMessages.has(data.id)) {
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const pendingMessage = pendingMessages.get(data.id);
|
|
51
|
+
|
|
52
|
+
if (!pendingMessage) {
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
clearTimeout(pendingMessage.timeoutId);
|
|
57
|
+
pendingMessages.delete(data.id);
|
|
58
|
+
pendingMessage.resolve(data);
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function applyStyles() {
|
|
63
|
+
Array.from(document.querySelectorAll('style[data-vite-dev-id]'))
|
|
64
|
+
.filter((element) => /__vite__updateStyle/.test(element.innerHTML))
|
|
65
|
+
.forEach((element) => {
|
|
66
|
+
const script = document.createElement('script');
|
|
67
|
+
|
|
68
|
+
script.type = 'module';
|
|
69
|
+
|
|
70
|
+
const safeScriptContent = element.innerHTML
|
|
71
|
+
.replaceAll('import.meta.hot.accept(', 'import.meta.hot?.accept(')
|
|
72
|
+
.replaceAll('import.meta.hot.prune(', 'import.meta.hot?.prune(');
|
|
73
|
+
|
|
74
|
+
script.appendChild(document.createTextNode(safeScriptContent));
|
|
75
|
+
document.head.appendChild(script);
|
|
76
|
+
document.head.removeChild(script);
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function isAstroStyleUpdate(path: string): boolean {
|
|
81
|
+
return /\.astro\?astro&type=style&index=\d+&lang\.(css|scss|sass|less|stylus)$/.test(path);
|
|
82
|
+
}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { afterEach, describe, expect, test, vi } from 'vitest';
|
|
2
|
+
import { createServerRenderer } from './renderer-server.ts';
|
|
3
|
+
|
|
4
|
+
const renderPayload = {
|
|
5
|
+
component: '/src/components/Button.astro',
|
|
6
|
+
args: { label: 'Click' },
|
|
7
|
+
slots: {},
|
|
8
|
+
story: undefined
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
describe('createServerRenderer', () => {
|
|
12
|
+
afterEach(() => {
|
|
13
|
+
vi.unstubAllGlobals();
|
|
14
|
+
vi.restoreAllMocks();
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
test('sends bearer auth header with default authorization header', async () => {
|
|
18
|
+
const fetchMock = vi.fn(async () => ({
|
|
19
|
+
ok: true,
|
|
20
|
+
status: 200,
|
|
21
|
+
statusText: 'OK',
|
|
22
|
+
text: async () => '<div>rendered</div>'
|
|
23
|
+
}));
|
|
24
|
+
|
|
25
|
+
vi.stubGlobal('fetch', fetchMock);
|
|
26
|
+
vi.stubGlobal('crypto', { randomUUID: () => 'uuid-1' });
|
|
27
|
+
|
|
28
|
+
const renderer = createServerRenderer({
|
|
29
|
+
serverUrl: 'http://localhost:4000',
|
|
30
|
+
authToken: 'secret'
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
await expect(renderer.render(renderPayload, 1000)).resolves.toEqual({
|
|
34
|
+
id: 'uuid-1',
|
|
35
|
+
html: '<div>rendered</div>'
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
expect(fetchMock).toHaveBeenCalledTimes(1);
|
|
39
|
+
expect(fetchMock).toHaveBeenCalledWith(
|
|
40
|
+
'http://localhost:4000/render',
|
|
41
|
+
expect.objectContaining({
|
|
42
|
+
method: 'POST',
|
|
43
|
+
headers: {
|
|
44
|
+
'content-type': 'application/json',
|
|
45
|
+
authorization: 'Bearer secret'
|
|
46
|
+
}
|
|
47
|
+
})
|
|
48
|
+
);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
test('uses custom auth header without bearer prefix', async () => {
|
|
52
|
+
const fetchMock = vi.fn(async () => ({
|
|
53
|
+
ok: true,
|
|
54
|
+
status: 200,
|
|
55
|
+
statusText: 'OK',
|
|
56
|
+
text: async () => '<div>ok</div>'
|
|
57
|
+
}));
|
|
58
|
+
|
|
59
|
+
vi.stubGlobal('fetch', fetchMock);
|
|
60
|
+
vi.stubGlobal('crypto', { randomUUID: () => 'uuid-2' });
|
|
61
|
+
|
|
62
|
+
const renderer = createServerRenderer({
|
|
63
|
+
serverUrl: 'http://localhost:5000',
|
|
64
|
+
authToken: 'token-123',
|
|
65
|
+
authHeader: 'x-storybook-token'
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
await renderer.render(renderPayload, 1000);
|
|
69
|
+
|
|
70
|
+
expect(fetchMock).toHaveBeenCalledWith(
|
|
71
|
+
'http://localhost:5000/render',
|
|
72
|
+
expect.objectContaining({
|
|
73
|
+
headers: {
|
|
74
|
+
'content-type': 'application/json',
|
|
75
|
+
'x-storybook-token': 'token-123'
|
|
76
|
+
}
|
|
77
|
+
})
|
|
78
|
+
);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
test('throws a clear error when server rejects auth', async () => {
|
|
82
|
+
vi.stubGlobal('fetch',
|
|
83
|
+
vi.fn(async () => ({
|
|
84
|
+
ok: false,
|
|
85
|
+
status: 401,
|
|
86
|
+
statusText: 'Unauthorized',
|
|
87
|
+
text: async () => ''
|
|
88
|
+
}))
|
|
89
|
+
);
|
|
90
|
+
vi.stubGlobal('crypto', { randomUUID: () => 'uuid-3' });
|
|
91
|
+
|
|
92
|
+
const renderer = createServerRenderer({
|
|
93
|
+
serverUrl: 'http://localhost:6000',
|
|
94
|
+
authToken: 'wrong'
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
await expect(renderer.render(renderPayload, 1000)).rejects.toThrow(
|
|
98
|
+
'Astro rendering server rejected the request with 401.'
|
|
99
|
+
);
|
|
100
|
+
});
|
|
101
|
+
});
|