@storybook-astro/framework 0.1.0-beta.8 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +38 -0
- package/dist/base-IRZo3zgK.d.ts +23 -0
- package/dist/chunk-4SWPVM6R.js +96 -0
- package/dist/chunk-4SWPVM6R.js.map +1 -0
- package/dist/chunk-5EF25G5S.js +69 -0
- package/dist/chunk-5EF25G5S.js.map +1 -0
- package/dist/chunk-7GHEQUPV.js +439 -0
- package/dist/chunk-7GHEQUPV.js.map +1 -0
- package/dist/chunk-C5OH4VBR.js +492 -0
- package/dist/chunk-C5OH4VBR.js.map +1 -0
- package/dist/chunk-DNGQBPT7.js +15 -0
- package/dist/chunk-DNGQBPT7.js.map +1 -0
- package/dist/chunk-E4LB75JN.js +89 -0
- package/dist/chunk-E4LB75JN.js.map +1 -0
- package/dist/chunk-PJEDXZVN.js +240 -0
- package/dist/chunk-PJEDXZVN.js.map +1 -0
- package/dist/chunk-UK43WNEA.js +657 -0
- package/dist/chunk-UK43WNEA.js.map +1 -0
- package/dist/dist-HJOEPVRQ.js +15574 -0
- package/dist/dist-HJOEPVRQ.js.map +1 -0
- package/dist/index.d.ts +42 -0
- package/dist/index.js +13 -64
- package/dist/index.js.map +1 -1
- package/dist/integrations/index.d.ts +138 -0
- package/dist/integrations/index.js +8 -196
- package/dist/integrations/index.js.map +1 -1
- package/dist/middleware.d.ts +26 -0
- package/dist/middleware.js +179 -0
- package/dist/middleware.js.map +1 -0
- package/dist/portable-stories-BvdaQigq.d.ts +83 -0
- package/dist/preset.d.ts +14 -0
- package/dist/preset.js +5 -1
- package/dist/testing.d.ts +27 -0
- package/dist/testing.js +324 -15539
- package/dist/testing.js.map +1 -1
- package/dist/types-CHTsRtA7.d.ts +42 -0
- package/dist/viteStorybookAstroMiddlewarePlugin-NP2E52IC.js +11 -0
- package/dist/viteStorybookAstroMiddlewarePlugin-NP2E52IC.js.map +1 -0
- package/dist/vitest/index.d.ts +19 -0
- package/dist/vitest/index.js +229 -0
- package/dist/vitest/index.js.map +1 -0
- package/package.json +31 -17
- package/src/importAstroConfig.ts +11 -0
- package/src/index.ts +20 -6
- package/src/integrations/alpine.ts +5 -2
- package/src/integrations/base.ts +2 -2
- package/src/integrations/moduleResolver.ts +43 -0
- package/src/integrations/preact.ts +5 -2
- package/src/integrations/react.ts +5 -2
- package/src/integrations/solid.ts +5 -2
- package/src/integrations/svelte.ts +5 -2
- package/src/integrations/vue.ts +5 -2
- package/src/lib/sanitization.test.ts +232 -0
- package/src/lib/sanitization.ts +338 -0
- package/src/lib/ssr-load-module-with-fs-fallback.ts +29 -0
- package/src/middleware.test.ts +48 -0
- package/src/middleware.ts +204 -96
- package/src/module-mocks.ts +16 -0
- package/src/msw-helpers.ts +1 -0
- package/src/msw.ts +58 -0
- package/src/preset.ts +38 -3
- package/src/rules-options.test.ts +71 -0
- package/src/rules-options.ts +87 -0
- package/src/rules.test.ts +183 -0
- package/src/rules.ts +314 -0
- package/src/testing/astro-runtime.ts +219 -0
- package/src/testing/component-utils.ts +32 -0
- package/src/testing/index.ts +2 -0
- package/src/testing/integration-config.ts +121 -0
- package/src/testing/project-root.ts +185 -0
- package/src/testing/renderer-daemon.ts +269 -0
- package/src/testing/story-composition.ts +33 -0
- package/src/testing/types.ts +14 -0
- package/src/testing/working-directory.ts +28 -0
- package/src/testing.ts +1 -254
- package/src/types.ts +16 -4
- package/src/virtual.d.ts +2 -1
- package/src/vite/createVirtualModulePlugin.test.ts +80 -0
- package/src/vite/createVirtualModulePlugin.ts +25 -0
- package/src/viteAstroContainerRenderersPlugin.ts +60 -26
- package/src/vitePluginAstro.ts +12 -5
- package/src/vitePluginAstroBuildPrerender.ts +665 -204
- package/src/vitePluginAstroRoutesFallback.ts +37 -0
- package/src/vitePluginAstroVueFallback.ts +47 -0
- package/src/viteStorybookAstroMiddlewarePlugin.ts +88 -12
- package/src/viteStorybookRendererFallbackPlugin.ts +13 -23
- package/src/vitest/config.ts +95 -0
- package/src/vitest/global-setup.ts +16 -0
- package/src/vitest/index.ts +2 -0
- package/src/vitest/vite-plugins.ts +187 -0
- package/dist/chunk-SAOPE6SA.js +0 -557
- package/dist/chunk-SAOPE6SA.js.map +0 -1
package/src/middleware.ts
CHANGED
|
@@ -1,149 +1,257 @@
|
|
|
1
|
+
import { pathToFileURL } from 'node:url';
|
|
1
2
|
import { experimental_AstroContainer as AstroContainer } from 'astro/container';
|
|
2
3
|
import type { Integration } from './integrations/index.ts';
|
|
3
|
-
import {
|
|
4
|
+
import type { SanitizationOptions } from './lib/sanitization.ts';
|
|
5
|
+
import { resolveSanitizationOptions, sanitizeRenderPayload } from './lib/sanitization.ts';
|
|
6
|
+
import { resolveStoryModuleMock, withStoryModuleMocks } from './module-mocks.ts';
|
|
7
|
+
import { applyMswHandlers } from './msw.ts';
|
|
8
|
+
import { selectStoryRules } from './rules.ts';
|
|
9
|
+
import type { RenderStoryInput } from './types.ts';
|
|
10
|
+
import { addRenderers, resolveClientModules } from 'virtual:astro-container-renderers';
|
|
11
|
+
|
|
12
|
+
type ResolveRulesConfigModule = () => unknown | Promise<unknown>;
|
|
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
|
+
};
|
|
4
27
|
|
|
5
28
|
export type HandlerProps = {
|
|
6
29
|
component: string;
|
|
7
30
|
args?: Record<string, unknown>;
|
|
8
31
|
slots?: Record<string, unknown>;
|
|
32
|
+
story?: RenderStoryInput;
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
type HandlerFactoryOptions = {
|
|
36
|
+
mode?: 'development' | 'production';
|
|
37
|
+
sanitization?: SanitizationOptions;
|
|
38
|
+
rulesConfigFilePath?: string;
|
|
39
|
+
resolveRulesConfigModule?: ResolveRulesConfigModule;
|
|
40
|
+
loadModule?: (id: string) => Promise<{ default: unknown }>;
|
|
9
41
|
};
|
|
10
42
|
|
|
11
|
-
export async function handlerFactory(
|
|
12
|
-
const
|
|
43
|
+
export async function handlerFactory(_integrations: Integration[], options?: HandlerFactoryOptions) {
|
|
44
|
+
const mode = options?.mode ?? 'development';
|
|
13
45
|
const container = await AstroContainer.create({
|
|
14
46
|
// Somewhat hacky way to force client-side Storybook's Vite to resolve modules properly
|
|
15
|
-
resolve: async (
|
|
16
|
-
|
|
17
|
-
|
|
47
|
+
resolve: async (specifier) => {
|
|
48
|
+
const mockedModule = resolveStoryModuleMock(specifier);
|
|
49
|
+
|
|
50
|
+
if (mockedModule) {
|
|
51
|
+
return mockedModule;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if (specifier.startsWith('astro:scripts')) {
|
|
55
|
+
return `/@id/${specifier}`;
|
|
18
56
|
}
|
|
19
57
|
|
|
20
|
-
|
|
21
|
-
const resolution = integration.resolveClient(s);
|
|
58
|
+
const resolution = resolveClientModules(specifier);
|
|
22
59
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
}
|
|
60
|
+
if (resolution) {
|
|
61
|
+
return resolution;
|
|
26
62
|
}
|
|
27
63
|
|
|
28
|
-
return
|
|
64
|
+
return specifier;
|
|
29
65
|
}
|
|
30
66
|
});
|
|
31
67
|
|
|
32
68
|
addRenderers(container);
|
|
69
|
+
const sanitizationOptions = resolveSanitizationOptions(options?.sanitization);
|
|
70
|
+
const loadModule =
|
|
71
|
+
options?.loadModule ??
|
|
72
|
+
((id: string) => {
|
|
73
|
+
const normalizedId = /^[a-zA-Z]:[/\\]/.test(id) ? pathToFileURL(id).href : id;
|
|
33
74
|
|
|
34
|
-
|
|
35
|
-
const { default: Component } = await import(/* @vite-ignore */ data.component);
|
|
36
|
-
|
|
37
|
-
// Process args to convert ImageMetadata objects to usable URLs
|
|
38
|
-
const processedArgs = await processImageMetadata(data.args || {});
|
|
39
|
-
|
|
40
|
-
// Wrap the component factory to fix the createAstro calling convention mismatch.
|
|
41
|
-
// Astro compiler v2 produces: result.createAstro($$Astro, $$props, $$slots) [3 args]
|
|
42
|
-
// Astro 6 runtime expects: result.createAstro($$props, $$slots) [2 args]
|
|
43
|
-
// When v2-compiled components run against the v6 runtime, $$Astro gets captured as
|
|
44
|
-
// "props" and actual props end up as "slots". This wrapper detects the 3-arg call
|
|
45
|
-
// and strips the leading $$Astro argument.
|
|
46
|
-
const patchedComponent = patchCreateAstroCompat(Component);
|
|
47
|
-
|
|
48
|
-
const result = await container.renderToString(patchedComponent, {
|
|
49
|
-
props: processedArgs,
|
|
50
|
-
slots: data.slots ?? {}
|
|
75
|
+
return import(/* @vite-ignore */ normalizedId);
|
|
51
76
|
});
|
|
77
|
+
const componentCache = new Map<string, Promise<AstroComponentFactory>>();
|
|
78
|
+
let renderQueue = Promise.resolve<void>(undefined);
|
|
79
|
+
|
|
80
|
+
async function loadPatchedComponent(componentId: string, useCache = true) {
|
|
81
|
+
if (!useCache) {
|
|
82
|
+
const { default: component } = await loadModule(componentId);
|
|
83
|
+
|
|
84
|
+
return patchCreateAstroCompat(component);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if (!componentCache.has(componentId)) {
|
|
88
|
+
componentCache.set(componentId, (async () => {
|
|
89
|
+
const { default: component } = await loadModule(componentId);
|
|
90
|
+
|
|
91
|
+
return patchCreateAstroCompat(component);
|
|
92
|
+
})());
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const cachedComponent = componentCache.get(componentId);
|
|
96
|
+
|
|
97
|
+
if (!cachedComponent) {
|
|
98
|
+
throw new Error(`Failed to load Astro component: ${componentId}`);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
try {
|
|
102
|
+
return await cachedComponent;
|
|
103
|
+
} catch (error) {
|
|
104
|
+
// Drop failed entries so transient/module errors can recover on the next request.
|
|
105
|
+
componentCache.delete(componentId);
|
|
106
|
+
throw error;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
return async function handler(data: HandlerProps) {
|
|
111
|
+
const executeRender = async () => {
|
|
112
|
+
const rulesConfigModule = options?.resolveRulesConfigModule
|
|
113
|
+
? await options.resolveRulesConfigModule()
|
|
114
|
+
: undefined;
|
|
115
|
+
|
|
116
|
+
const selectedRules = await selectStoryRules({
|
|
117
|
+
configModule: rulesConfigModule,
|
|
118
|
+
configFilePath: options?.rulesConfigFilePath,
|
|
119
|
+
mode,
|
|
120
|
+
story: data.story
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
await applyMswHandlers(selectedRules.mswHandlers);
|
|
52
124
|
|
|
53
|
-
|
|
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
|
+
const resultPromise = renderQueue.then(executeRender, executeRender);
|
|
150
|
+
|
|
151
|
+
renderQueue = resultPromise.then(
|
|
152
|
+
() => undefined,
|
|
153
|
+
() => undefined
|
|
154
|
+
);
|
|
155
|
+
|
|
156
|
+
return resultPromise;
|
|
54
157
|
};
|
|
55
158
|
}
|
|
56
159
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
const origCreateAstro = result.createAstro;
|
|
72
|
-
|
|
73
|
-
result.createAstro = (...args: any[]) => {
|
|
74
|
-
if (args.length === 3) {
|
|
75
|
-
// Compiler v2 convention: ($$Astro, $$props, $$slots) → skip $$Astro
|
|
76
|
-
return origCreateAstro(args[1], args[2]);
|
|
160
|
+
function patchCreateAstroCompat(component: unknown): AstroComponentFactory {
|
|
161
|
+
if (typeof component !== 'function') {
|
|
162
|
+
throw new Error('Expected Astro component factory to be a function.');
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
const originalComponent = component as AstroComponentFactory;
|
|
166
|
+
const wrapped = ((result: AstroCreateResult, props: unknown, slots: unknown) => {
|
|
167
|
+
if (result && typeof result.createAstro === 'function') {
|
|
168
|
+
const originalCreateAstro = result.createAstro;
|
|
169
|
+
const runtimeExpectsAstroGlobal = originalCreateAstro.length >= 3;
|
|
170
|
+
|
|
171
|
+
result.createAstro = (...args: unknown[]) => {
|
|
172
|
+
if (args.length === 3 && !runtimeExpectsAstroGlobal) {
|
|
173
|
+
return originalCreateAstro(args[1], args[2]);
|
|
77
174
|
}
|
|
78
175
|
|
|
79
|
-
|
|
80
|
-
return origCreateAstro(...args);
|
|
176
|
+
return originalCreateAstro(...args);
|
|
81
177
|
};
|
|
82
178
|
}
|
|
83
179
|
|
|
84
|
-
return
|
|
85
|
-
};
|
|
180
|
+
return originalComponent(result, props, slots);
|
|
181
|
+
}) as AstroComponentFactory;
|
|
86
182
|
|
|
87
|
-
|
|
88
|
-
wrapped.
|
|
89
|
-
wrapped.
|
|
90
|
-
wrapped.propagation = Component.propagation;
|
|
183
|
+
wrapped.isAstroComponentFactory = originalComponent.isAstroComponentFactory;
|
|
184
|
+
wrapped.moduleId = originalComponent.moduleId;
|
|
185
|
+
wrapped.propagation = originalComponent.propagation;
|
|
91
186
|
|
|
92
187
|
return wrapped;
|
|
93
188
|
}
|
|
94
189
|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
* optimized asset references to direct file paths.
|
|
99
|
-
*/
|
|
100
|
-
async function processImageMetadata(args: Record<string, unknown>): Promise<Record<string, unknown>> {
|
|
190
|
+
async function processImageMetadata(
|
|
191
|
+
args: Record<string, unknown>
|
|
192
|
+
): Promise<Record<string, unknown>> {
|
|
101
193
|
const processed: Record<string, unknown> = {};
|
|
102
|
-
|
|
194
|
+
|
|
103
195
|
for (const [key, value] of Object.entries(args)) {
|
|
104
196
|
if (isImageMetadata(value)) {
|
|
105
|
-
// Convert ImageMetadata to a usable URL
|
|
106
197
|
processed[key] = convertImageMetadataToUrl(value);
|
|
107
|
-
|
|
108
|
-
|
|
198
|
+
|
|
199
|
+
continue;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
if (Array.isArray(value)) {
|
|
109
203
|
processed[key] = await Promise.all(
|
|
110
|
-
value.map(async (item) =>
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
204
|
+
value.map(async (item) => {
|
|
205
|
+
if (isImageMetadata(item)) {
|
|
206
|
+
return convertImageMetadataToUrl(item);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
if (isRecord(item)) {
|
|
210
|
+
return processImageMetadata(item);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
return item;
|
|
214
|
+
})
|
|
115
215
|
);
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
216
|
+
|
|
217
|
+
continue;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
if (isRecord(value)) {
|
|
221
|
+
processed[key] = await processImageMetadata(value);
|
|
222
|
+
|
|
223
|
+
continue;
|
|
121
224
|
}
|
|
225
|
+
|
|
226
|
+
processed[key] = value;
|
|
122
227
|
}
|
|
123
|
-
|
|
228
|
+
|
|
124
229
|
return processed;
|
|
125
230
|
}
|
|
126
231
|
|
|
127
|
-
|
|
128
|
-
* Type guard to check if a value is an ImageMetadata object.
|
|
129
|
-
* ImageMetadata objects typically have properties like src, width, height, format.
|
|
130
|
-
*/
|
|
131
|
-
function isImageMetadata(value: unknown): value is Record<string, any> {
|
|
232
|
+
function isImageMetadata(value: unknown): value is Record<string, unknown> {
|
|
132
233
|
return (
|
|
133
|
-
|
|
134
|
-
value
|
|
135
|
-
'src' in value &&
|
|
136
|
-
typeof (value as any).src === 'string' &&
|
|
234
|
+
isRecord(value) &&
|
|
235
|
+
typeof value.src === 'string' &&
|
|
137
236
|
('width' in value || 'height' in value || 'format' in value)
|
|
138
237
|
);
|
|
139
238
|
}
|
|
140
239
|
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
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
|
+
|
|
255
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
256
|
+
return typeof value === 'object' && value !== null;
|
|
149
257
|
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { AsyncLocalStorage } from 'node:async_hooks';
|
|
2
|
+
|
|
3
|
+
export type StoryModuleMocks = Map<string, string>;
|
|
4
|
+
|
|
5
|
+
const moduleMockStorage = new AsyncLocalStorage<StoryModuleMocks>();
|
|
6
|
+
|
|
7
|
+
export async function withStoryModuleMocks<T>(
|
|
8
|
+
moduleMocks: StoryModuleMocks,
|
|
9
|
+
callback: () => Promise<T>
|
|
10
|
+
): Promise<T> {
|
|
11
|
+
return moduleMockStorage.run(moduleMocks, callback);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function resolveStoryModuleMock(specifier: string): string | undefined {
|
|
15
|
+
return moduleMockStorage.getStore()?.get(specifier);
|
|
16
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { http, HttpResponse } from 'msw';
|
package/src/msw.ts
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import type { RequestHandler } from 'msw';
|
|
2
|
+
import { setupServer } from 'msw/node';
|
|
3
|
+
|
|
4
|
+
type MswServer = ReturnType<typeof setupServer>;
|
|
5
|
+
type MswServerListenOptions = Parameters<MswServer['listen']>[0];
|
|
6
|
+
|
|
7
|
+
type MswState = {
|
|
8
|
+
server?: MswServer;
|
|
9
|
+
pendingUpdate?: Promise<void>;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
type MswGlobalState = typeof globalThis & {
|
|
13
|
+
__storybookAstroMswState__?: MswState;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
const defaultListenOptions: MswServerListenOptions = {
|
|
17
|
+
onUnhandledRequest: 'bypass'
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
export async function applyMswHandlers(handlers: RequestHandler[]): Promise<void> {
|
|
21
|
+
const state = getMswState();
|
|
22
|
+
|
|
23
|
+
if (state.pendingUpdate) {
|
|
24
|
+
await state.pendingUpdate;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const updatePromise = syncMswHandlers(handlers, state);
|
|
28
|
+
|
|
29
|
+
state.pendingUpdate = updatePromise;
|
|
30
|
+
|
|
31
|
+
try {
|
|
32
|
+
await updatePromise;
|
|
33
|
+
} finally {
|
|
34
|
+
state.pendingUpdate = undefined;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
async function syncMswHandlers(handlers: RequestHandler[], state: MswState): Promise<void> {
|
|
39
|
+
if (!state.server) {
|
|
40
|
+
state.server = setupServer();
|
|
41
|
+
state.server.listen(defaultListenOptions);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
state.server.resetHandlers(...handlers);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function getMswState(): MswState {
|
|
48
|
+
const globalState = globalThis as MswGlobalState;
|
|
49
|
+
|
|
50
|
+
if (!globalState.__storybookAstroMswState__) {
|
|
51
|
+
globalState.__storybookAstroMswState__ = {
|
|
52
|
+
server: undefined,
|
|
53
|
+
pendingUpdate: undefined
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return globalState.__storybookAstroMswState__;
|
|
58
|
+
}
|
package/src/preset.ts
CHANGED
|
@@ -3,6 +3,8 @@ import { vitePluginStorybookAstroMiddleware } from './viteStorybookAstroMiddlewa
|
|
|
3
3
|
import { viteStorybookRendererFallbackPlugin } from './viteStorybookRendererFallbackPlugin.ts';
|
|
4
4
|
import { vitePluginAstroComponentMarker } from './vitePluginAstroComponentMarker.ts';
|
|
5
5
|
import { vitePluginAstroBuildPrerender } from './vitePluginAstroBuildPrerender.ts';
|
|
6
|
+
import { vitePluginAstroVueFallback } from './vitePluginAstroVueFallback.ts';
|
|
7
|
+
import { resolveSanitizationOptions } from './lib/sanitization.ts';
|
|
6
8
|
import { mergeWithAstroConfig } from './vitePluginAstro.ts';
|
|
7
9
|
|
|
8
10
|
export const core = {
|
|
@@ -10,7 +12,7 @@ export const core = {
|
|
|
10
12
|
renderer: '@storybook-astro/renderer'
|
|
11
13
|
};
|
|
12
14
|
|
|
13
|
-
export const viteFinal: StorybookConfigVite['viteFinal'] = async (config, { presets }) => {
|
|
15
|
+
export const viteFinal: StorybookConfigVite['viteFinal'] = async (config, { configType, presets }) => {
|
|
14
16
|
const options = await presets.apply<FrameworkOptions>('frameworkOptions');
|
|
15
17
|
const { vitePlugin: storybookAstroMiddlewarePlugin, viteConfig } =
|
|
16
18
|
await vitePluginStorybookAstroMiddleware(options);
|
|
@@ -20,6 +22,11 @@ export const viteFinal: StorybookConfigVite['viteFinal'] = async (config, { pres
|
|
|
20
22
|
}
|
|
21
23
|
|
|
22
24
|
const integrations = options.integrations ?? [];
|
|
25
|
+
const resolveFrom = options.resolveFrom ?? process.cwd();
|
|
26
|
+
const mode = configType === 'DEVELOPMENT' ? 'development' : 'production';
|
|
27
|
+
const command = configType === 'DEVELOPMENT' ? 'serve' : 'build';
|
|
28
|
+
|
|
29
|
+
resolveSanitizationOptions(options.sanitization);
|
|
23
30
|
|
|
24
31
|
config.plugins.push(
|
|
25
32
|
storybookAstroMiddlewarePlugin,
|
|
@@ -27,7 +34,8 @@ export const viteFinal: StorybookConfigVite['viteFinal'] = async (config, { pres
|
|
|
27
34
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
28
35
|
vitePluginAstroComponentMarker() as any,
|
|
29
36
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
30
|
-
vitePluginAstroBuildPrerender(
|
|
37
|
+
vitePluginAstroBuildPrerender(options) as any,
|
|
38
|
+
vitePluginAstroVueFallback(),
|
|
31
39
|
...viteConfig.plugins
|
|
32
40
|
);
|
|
33
41
|
|
|
@@ -49,7 +57,34 @@ export const viteFinal: StorybookConfigVite['viteFinal'] = async (config, { pres
|
|
|
49
57
|
aliases['react-dom'] = 'react-dom';
|
|
50
58
|
}
|
|
51
59
|
|
|
52
|
-
const finalConfig = await mergeWithAstroConfig(config, integrations);
|
|
60
|
+
const finalConfig = await mergeWithAstroConfig(config, integrations, resolveFrom, mode, command);
|
|
61
|
+
|
|
62
|
+
// Exclude @astrojs/vue from dependency optimization because it imports
|
|
63
|
+
// virtual modules that esbuild cannot resolve (virtual:@astrojs/vue/app).
|
|
64
|
+
// This must be done after mergeWithAstroConfig to avoid being overwritten.
|
|
65
|
+
if (!finalConfig.optimizeDeps) {
|
|
66
|
+
finalConfig.optimizeDeps = {};
|
|
67
|
+
}
|
|
68
|
+
if (!finalConfig.optimizeDeps.exclude) {
|
|
69
|
+
finalConfig.optimizeDeps.exclude = [];
|
|
70
|
+
}
|
|
71
|
+
if (!finalConfig.optimizeDeps.exclude.includes('@astrojs/vue')) {
|
|
72
|
+
finalConfig.optimizeDeps.exclude.push('@astrojs/vue');
|
|
73
|
+
}
|
|
74
|
+
// Mark Vue virtual modules as external so esbuild doesn't try to resolve them
|
|
75
|
+
if (!finalConfig.optimizeDeps.esbuildOptions) {
|
|
76
|
+
finalConfig.optimizeDeps.esbuildOptions = {};
|
|
77
|
+
}
|
|
78
|
+
if (!finalConfig.optimizeDeps.esbuildOptions.external) {
|
|
79
|
+
finalConfig.optimizeDeps.esbuildOptions.external = [];
|
|
80
|
+
}
|
|
81
|
+
const vueVirtualModules = ['virtual:@astrojs/vue/app', 'virtual:astro:vue-app'];
|
|
82
|
+
|
|
83
|
+
for (const mod of vueVirtualModules) {
|
|
84
|
+
if (!finalConfig.optimizeDeps.esbuildOptions.external.includes(mod)) {
|
|
85
|
+
finalConfig.optimizeDeps.esbuildOptions.external.push(mod);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
53
88
|
|
|
54
89
|
return finalConfig;
|
|
55
90
|
};
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { mkdtemp, mkdir, rm, writeFile } from 'node:fs/promises';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { tmpdir } from 'node:os';
|
|
4
|
+
import { afterEach, describe, expect, test } from 'vitest';
|
|
5
|
+
import { resolveRulesConfigFilePath } from './rules-options.ts';
|
|
6
|
+
|
|
7
|
+
const createdDirs = new Set<string>();
|
|
8
|
+
|
|
9
|
+
afterEach(async () => {
|
|
10
|
+
await Promise.all(
|
|
11
|
+
Array.from(createdDirs).map(async (directory) => {
|
|
12
|
+
await rm(directory, { recursive: true, force: true });
|
|
13
|
+
createdDirs.delete(directory);
|
|
14
|
+
})
|
|
15
|
+
);
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
async function createTempDirectory() {
|
|
19
|
+
const directory = await mkdtemp(join(tmpdir(), 'storybook-astro-story-rules-'));
|
|
20
|
+
|
|
21
|
+
createdDirs.add(directory);
|
|
22
|
+
|
|
23
|
+
return directory;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
describe('resolveRulesConfigFilePath', () => {
|
|
27
|
+
test('returns undefined when story rules are not configured', () => {
|
|
28
|
+
expect(resolveRulesConfigFilePath(undefined)).toBeUndefined();
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
test('resolves a direct file path', async () => {
|
|
32
|
+
const directory = await createTempDirectory();
|
|
33
|
+
const configPath = join(directory, 'story-rules.ts');
|
|
34
|
+
|
|
35
|
+
await writeFile(configPath, 'export default { rules: [] };', 'utf-8');
|
|
36
|
+
|
|
37
|
+
expect(resolveRulesConfigFilePath('./story-rules.ts', directory)).toBe(configPath);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
test('resolves extensionless config path', async () => {
|
|
41
|
+
const directory = await createTempDirectory();
|
|
42
|
+
const configPath = join(directory, 'story-rules.ts');
|
|
43
|
+
|
|
44
|
+
await writeFile(configPath, 'export default { rules: [] };', 'utf-8');
|
|
45
|
+
|
|
46
|
+
expect(resolveRulesConfigFilePath('./story-rules', directory)).toBe(configPath);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
test('resolves directory config to index file', async () => {
|
|
50
|
+
const directory = await createTempDirectory();
|
|
51
|
+
const configDirectory = join(directory, 'story-rules');
|
|
52
|
+
const configPath = join(configDirectory, 'index.ts');
|
|
53
|
+
|
|
54
|
+
await mkdir(configDirectory, { recursive: true });
|
|
55
|
+
await writeFile(configPath, 'export default { rules: [] };', 'utf-8');
|
|
56
|
+
|
|
57
|
+
expect(resolveRulesConfigFilePath('./story-rules', directory)).toBe(configPath);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
test('throws when the configured file does not exist', () => {
|
|
61
|
+
expect(() => resolveRulesConfigFilePath('./missing-rules', '/tmp')).toThrow(
|
|
62
|
+
'framework.options.storyRules config file was not found:'
|
|
63
|
+
);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
test('throws when config path is empty', () => {
|
|
67
|
+
expect(() => resolveRulesConfigFilePath(' ')).toThrow(
|
|
68
|
+
'framework.options.storyRules config file path cannot be empty.'
|
|
69
|
+
);
|
|
70
|
+
});
|
|
71
|
+
});
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { existsSync, statSync } from 'node:fs';
|
|
2
|
+
import { extname, resolve } from 'node:path';
|
|
3
|
+
|
|
4
|
+
export type StoryRulesOptions =
|
|
5
|
+
| string
|
|
6
|
+
| {
|
|
7
|
+
configFile: string;
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
export function resolveRulesConfigFilePath(
|
|
11
|
+
options?: StoryRulesOptions,
|
|
12
|
+
resolveFrom = process.cwd()
|
|
13
|
+
): string | undefined {
|
|
14
|
+
if (options === undefined) {
|
|
15
|
+
return undefined;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const configFile = normalizeConfigFileOption(options);
|
|
19
|
+
const resolvedConfigFilePath = resolve(resolveFrom, configFile);
|
|
20
|
+
const normalizedConfigFilePath = resolveConfigFilePath(resolvedConfigFilePath);
|
|
21
|
+
|
|
22
|
+
if (!normalizedConfigFilePath) {
|
|
23
|
+
throw new Error(
|
|
24
|
+
`framework.options.storyRules config file was not found: ${resolvedConfigFilePath}. ` +
|
|
25
|
+
'Provide an existing path in framework.options.storyRules.'
|
|
26
|
+
);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
return normalizedConfigFilePath;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function normalizeConfigFileOption(options: StoryRulesOptions): string {
|
|
33
|
+
const configFile =
|
|
34
|
+
typeof options === 'string'
|
|
35
|
+
? options
|
|
36
|
+
: typeof options === 'object' && options !== null
|
|
37
|
+
? options.configFile
|
|
38
|
+
: undefined;
|
|
39
|
+
|
|
40
|
+
if (typeof configFile !== 'string') {
|
|
41
|
+
throw new Error(
|
|
42
|
+
'framework.options.storyRules must be either a string path or an object with a string configFile.'
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const normalizedConfigFile = configFile.trim();
|
|
47
|
+
|
|
48
|
+
if (!normalizedConfigFile) {
|
|
49
|
+
throw new Error('framework.options.storyRules config file path cannot be empty.');
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return normalizedConfigFile;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function resolveConfigFilePath(filePath: string): string | undefined {
|
|
56
|
+
if (existsSync(filePath)) {
|
|
57
|
+
const fileStats = statSync(filePath);
|
|
58
|
+
|
|
59
|
+
if (fileStats.isFile()) {
|
|
60
|
+
return filePath;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if (extname(filePath)) {
|
|
65
|
+
return undefined;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const extensions = ['.ts', '.mts', '.cts', '.js', '.mjs', '.cjs'];
|
|
69
|
+
|
|
70
|
+
for (const extension of extensions) {
|
|
71
|
+
const candidateFilePath = `${filePath}${extension}`;
|
|
72
|
+
|
|
73
|
+
if (existsSync(candidateFilePath)) {
|
|
74
|
+
return candidateFilePath;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
for (const extension of extensions) {
|
|
79
|
+
const candidateFilePath = resolve(filePath, `index${extension}`);
|
|
80
|
+
|
|
81
|
+
if (existsSync(candidateFilePath)) {
|
|
82
|
+
return candidateFilePath;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return undefined;
|
|
87
|
+
}
|