@storybook-astro/framework 1.0.2 → 1.1.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/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/{chunk-KSDXET2L.js → chunk-VPJDFGB5.js} +444 -60
- package/dist/chunk-VPJDFGB5.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 +13 -5
- 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 +75 -15
- 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/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
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import type { RenderComponentInput, RenderResponseMessage } from '@storybook-astro/renderer/types';
|
|
2
|
+
|
|
3
|
+
const PRERENDERED_STORIES_FILE = 'astro-prerendered-stories.json';
|
|
4
|
+
|
|
5
|
+
let prerenderedStoriesPromise: Promise<Record<string, string>> | undefined;
|
|
6
|
+
|
|
7
|
+
export async function render(data: RenderComponentInput) {
|
|
8
|
+
// eslint-disable-next-line n/no-unsupported-features/node-builtins
|
|
9
|
+
const id = crypto.randomUUID();
|
|
10
|
+
const storyId = data.story?.id;
|
|
11
|
+
|
|
12
|
+
if (!storyId) {
|
|
13
|
+
throw new Error(
|
|
14
|
+
'Astro static renderer expected a story id, but none was provided in the render payload.'
|
|
15
|
+
);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const prerenderedStories = await loadPrerenderedStories();
|
|
19
|
+
const html = prerenderedStories[storyId];
|
|
20
|
+
|
|
21
|
+
if (html === undefined) {
|
|
22
|
+
throw new Error(
|
|
23
|
+
`No prerendered HTML was found for story "${storyId}". Rebuild Storybook static output.`
|
|
24
|
+
);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
return {
|
|
28
|
+
id,
|
|
29
|
+
html
|
|
30
|
+
} satisfies RenderResponseMessage['data'];
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function init() {
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function applyStyles() {
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
async function loadPrerenderedStories() {
|
|
42
|
+
if (!prerenderedStoriesPromise) {
|
|
43
|
+
const jsonPath = resolvePrerenderedStoriesUrl();
|
|
44
|
+
|
|
45
|
+
// eslint-disable-next-line n/no-unsupported-features/node-builtins
|
|
46
|
+
prerenderedStoriesPromise = fetch(jsonPath).then(async (response) => {
|
|
47
|
+
if (!response.ok) {
|
|
48
|
+
throw new Error(
|
|
49
|
+
`Failed to load ${PRERENDERED_STORIES_FILE}. Received ${response.status} ${response.statusText}.`
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return (await response.json()) as Record<string, string>;
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return prerenderedStoriesPromise;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function resolvePrerenderedStoriesUrl() {
|
|
61
|
+
return new URL(PRERENDERED_STORIES_FILE, window.location.href).toString();
|
|
62
|
+
}
|
package/src/rules.test.ts
CHANGED
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
import { resolve } from 'node:path';
|
|
2
|
-
import type { RequestHandler } from 'msw';
|
|
3
2
|
import { describe, expect, test } from 'vitest';
|
|
4
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
defineStoryRules,
|
|
5
|
+
selectStoryRules,
|
|
6
|
+
withStoryRuleCleanups,
|
|
7
|
+
type StoryRulesConfig
|
|
8
|
+
} from './rules.ts';
|
|
5
9
|
|
|
6
10
|
function createRulesConfig(config: StoryRulesConfig) {
|
|
7
11
|
return {
|
|
@@ -13,14 +17,13 @@ describe('story rules', () => {
|
|
|
13
17
|
test('returns an empty selection when no rules are configured', async () => {
|
|
14
18
|
const selection = await selectStoryRules({
|
|
15
19
|
configModule: undefined,
|
|
16
|
-
mode: 'development',
|
|
17
20
|
story: {
|
|
18
21
|
id: 'components-card--default'
|
|
19
22
|
}
|
|
20
23
|
});
|
|
21
24
|
|
|
22
25
|
expect(selection.moduleMocks.size).toBe(0);
|
|
23
|
-
expect(selection.
|
|
26
|
+
expect(selection.cleanups).toEqual([]);
|
|
24
27
|
});
|
|
25
28
|
|
|
26
29
|
test('matches rules against story id and applies module mocks', async () => {
|
|
@@ -35,14 +38,13 @@ describe('story rules', () => {
|
|
|
35
38
|
}
|
|
36
39
|
]
|
|
37
40
|
}),
|
|
38
|
-
mode: 'development',
|
|
39
41
|
story: {
|
|
40
42
|
id: 'components-card--default'
|
|
41
43
|
}
|
|
42
44
|
});
|
|
43
45
|
|
|
44
46
|
expect(selection.moduleMocks.get('~/lib/api')).toBe('~/lib/api.mock');
|
|
45
|
-
expect(selection.
|
|
47
|
+
expect(selection.cleanups).toHaveLength(0);
|
|
46
48
|
});
|
|
47
49
|
|
|
48
50
|
test('matches rules against title and story name paths', async () => {
|
|
@@ -57,7 +59,6 @@ describe('story rules', () => {
|
|
|
57
59
|
}
|
|
58
60
|
]
|
|
59
61
|
}),
|
|
60
|
-
mode: 'development',
|
|
61
62
|
story: {
|
|
62
63
|
id: 'guides-getting-started--default-state',
|
|
63
64
|
title: 'Guides/Getting Started',
|
|
@@ -80,7 +81,6 @@ describe('story rules', () => {
|
|
|
80
81
|
}
|
|
81
82
|
]
|
|
82
83
|
}),
|
|
83
|
-
mode: 'development',
|
|
84
84
|
story: {
|
|
85
85
|
id: '/story/components-card--default'
|
|
86
86
|
}
|
|
@@ -89,28 +89,102 @@ describe('story rules', () => {
|
|
|
89
89
|
expect(selection.moduleMocks.get('~/store')).toBe('~/store.mock');
|
|
90
90
|
});
|
|
91
91
|
|
|
92
|
-
test('collects
|
|
93
|
-
const
|
|
94
|
-
const secondHandler = {} as RequestHandler;
|
|
92
|
+
test('collects cleanup functions from matching rules', async () => {
|
|
93
|
+
const cleanup = () => undefined;
|
|
95
94
|
|
|
96
95
|
const selection = await selectStoryRules({
|
|
97
96
|
configModule: createRulesConfig({
|
|
98
97
|
rules: [
|
|
99
98
|
{
|
|
100
99
|
match: '*',
|
|
101
|
-
use: (
|
|
102
|
-
|
|
100
|
+
use: () => {
|
|
101
|
+
return cleanup;
|
|
103
102
|
}
|
|
104
103
|
}
|
|
105
104
|
]
|
|
106
105
|
}),
|
|
107
|
-
mode: 'production',
|
|
108
106
|
story: {
|
|
109
107
|
id: 'components-card--default'
|
|
110
108
|
}
|
|
111
109
|
});
|
|
112
110
|
|
|
113
|
-
expect(selection.
|
|
111
|
+
expect(selection.cleanups).toEqual([cleanup]);
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
test('runs cleanups after successful execution in reverse order', async () => {
|
|
115
|
+
const sequence: string[] = [];
|
|
116
|
+
|
|
117
|
+
await withStoryRuleCleanups(
|
|
118
|
+
[
|
|
119
|
+
() => {
|
|
120
|
+
sequence.push('cleanup:first');
|
|
121
|
+
},
|
|
122
|
+
async () => {
|
|
123
|
+
sequence.push('cleanup:second');
|
|
124
|
+
}
|
|
125
|
+
],
|
|
126
|
+
async () => {
|
|
127
|
+
sequence.push('render');
|
|
128
|
+
}
|
|
129
|
+
);
|
|
130
|
+
|
|
131
|
+
expect(sequence).toEqual(['render', 'cleanup:second', 'cleanup:first']);
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
test('runs cleanups when execution throws', async () => {
|
|
135
|
+
const sequence: string[] = [];
|
|
136
|
+
|
|
137
|
+
await expect(
|
|
138
|
+
withStoryRuleCleanups(
|
|
139
|
+
[
|
|
140
|
+
() => {
|
|
141
|
+
sequence.push('cleanup');
|
|
142
|
+
}
|
|
143
|
+
],
|
|
144
|
+
async () => {
|
|
145
|
+
sequence.push('render');
|
|
146
|
+
throw new Error('render failed');
|
|
147
|
+
}
|
|
148
|
+
)
|
|
149
|
+
).rejects.toThrow('render failed');
|
|
150
|
+
|
|
151
|
+
expect(sequence).toEqual(['render', 'cleanup']);
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
test('throws when use returns a non-function value', async () => {
|
|
155
|
+
await expect(
|
|
156
|
+
selectStoryRules({
|
|
157
|
+
configModule: createRulesConfig({
|
|
158
|
+
rules: [
|
|
159
|
+
{
|
|
160
|
+
match: '*',
|
|
161
|
+
use: () => 'nope' as never
|
|
162
|
+
}
|
|
163
|
+
]
|
|
164
|
+
}),
|
|
165
|
+
story: {
|
|
166
|
+
id: 'components-card--default'
|
|
167
|
+
}
|
|
168
|
+
})
|
|
169
|
+
).rejects.toThrow('Story rule "use" must return either nothing or a cleanup function.');
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
test('aggregates cleanup failures', async () => {
|
|
173
|
+
await expect(
|
|
174
|
+
withStoryRuleCleanups(
|
|
175
|
+
[
|
|
176
|
+
() => {
|
|
177
|
+
throw new Error('first cleanup failed');
|
|
178
|
+
},
|
|
179
|
+
() => {
|
|
180
|
+
throw new Error('second cleanup failed');
|
|
181
|
+
}
|
|
182
|
+
],
|
|
183
|
+
async () => undefined
|
|
184
|
+
)
|
|
185
|
+
).rejects.toMatchObject({
|
|
186
|
+
message: 'Story rule cleanup failed.'
|
|
187
|
+
});
|
|
114
188
|
});
|
|
115
189
|
|
|
116
190
|
test('resolves relative mock replacements from config file location', async () => {
|
|
@@ -128,7 +202,6 @@ describe('story rules', () => {
|
|
|
128
202
|
]
|
|
129
203
|
}),
|
|
130
204
|
configFilePath,
|
|
131
|
-
mode: 'development',
|
|
132
205
|
story: {
|
|
133
206
|
id: 'components-card--default'
|
|
134
207
|
}
|
|
@@ -150,7 +223,6 @@ describe('story rules', () => {
|
|
|
150
223
|
}
|
|
151
224
|
]
|
|
152
225
|
}),
|
|
153
|
-
mode: 'development',
|
|
154
226
|
story: {
|
|
155
227
|
id: 'components-card--default'
|
|
156
228
|
}
|
|
@@ -171,7 +243,6 @@ describe('story rules', () => {
|
|
|
171
243
|
}
|
|
172
244
|
]
|
|
173
245
|
}),
|
|
174
|
-
mode: 'development',
|
|
175
246
|
story: {
|
|
176
247
|
id: 'components-card--default'
|
|
177
248
|
}
|
package/src/rules.ts
CHANGED
|
@@ -1,16 +1,11 @@
|
|
|
1
1
|
import { dirname, isAbsolute, resolve } from 'node:path';
|
|
2
|
-
import type { RequestHandler } from 'msw';
|
|
3
2
|
import type { RenderStoryInput } from './types.ts';
|
|
4
3
|
|
|
5
|
-
type
|
|
6
|
-
type StoryRuleUseResult = void | Promise<void>;
|
|
4
|
+
export type StoryRuleCleanup = () => void | Promise<void>;
|
|
5
|
+
type StoryRuleUseResult = void | StoryRuleCleanup | Promise<void | StoryRuleCleanup>;
|
|
7
6
|
|
|
8
7
|
export type StoryRuleUseContext = {
|
|
9
|
-
mode: StoryMode;
|
|
10
8
|
story: StoryRuleStory;
|
|
11
|
-
msw: {
|
|
12
|
-
use: (...handlers: RequestHandler[]) => void;
|
|
13
|
-
};
|
|
14
9
|
mock: (specifier: string, replacement: string) => void;
|
|
15
10
|
};
|
|
16
11
|
|
|
@@ -35,18 +30,17 @@ export type StoryRuleStory = {
|
|
|
35
30
|
export type StoryRuleSelectionInput = {
|
|
36
31
|
configModule: unknown;
|
|
37
32
|
configFilePath?: string;
|
|
38
|
-
mode: StoryMode;
|
|
39
33
|
story?: RenderStoryInput;
|
|
40
34
|
};
|
|
41
35
|
|
|
42
36
|
export type StoryRuleSelection = {
|
|
43
37
|
moduleMocks: Map<string, string>;
|
|
44
|
-
|
|
38
|
+
cleanups: StoryRuleCleanup[];
|
|
45
39
|
};
|
|
46
40
|
|
|
47
41
|
type MutableStoryRuleSelection = {
|
|
48
42
|
moduleMocks: Map<string, string>;
|
|
49
|
-
|
|
43
|
+
cleanups: StoryRuleCleanup[];
|
|
50
44
|
};
|
|
51
45
|
|
|
52
46
|
export function defineStoryRules(config: StoryRulesConfig): StoryRulesConfig {
|
|
@@ -72,14 +66,8 @@ export async function selectStoryRules(
|
|
|
72
66
|
throw new Error('Each story rule "use" entry must be a function.');
|
|
73
67
|
}
|
|
74
68
|
|
|
75
|
-
await use({
|
|
76
|
-
mode: input.mode,
|
|
69
|
+
const cleanup = await use({
|
|
77
70
|
story,
|
|
78
|
-
msw: {
|
|
79
|
-
use: (...handlers) => {
|
|
80
|
-
selection.mswHandlers.push(...handlers);
|
|
81
|
-
}
|
|
82
|
-
},
|
|
83
71
|
mock: (specifier, replacement) => {
|
|
84
72
|
const normalizedSpecifier = normalizeMockSpecifier(specifier);
|
|
85
73
|
const normalizedReplacement = normalizeMockReplacement(replacement, input.configFilePath);
|
|
@@ -87,12 +75,73 @@ export async function selectStoryRules(
|
|
|
87
75
|
selection.moduleMocks.set(normalizedSpecifier, normalizedReplacement);
|
|
88
76
|
}
|
|
89
77
|
});
|
|
78
|
+
|
|
79
|
+
if (cleanup !== undefined) {
|
|
80
|
+
if (typeof cleanup !== 'function') {
|
|
81
|
+
throw new Error('Story rule "use" must return either nothing or a cleanup function.');
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
selection.cleanups.push(cleanup);
|
|
85
|
+
}
|
|
90
86
|
}
|
|
91
87
|
}
|
|
92
88
|
|
|
93
89
|
return selection;
|
|
94
90
|
}
|
|
95
91
|
|
|
92
|
+
export async function withStoryRuleCleanups<T>(
|
|
93
|
+
cleanups: StoryRuleCleanup[],
|
|
94
|
+
callback: () => Promise<T>
|
|
95
|
+
): Promise<T> {
|
|
96
|
+
let result: T | undefined;
|
|
97
|
+
let callbackError: unknown;
|
|
98
|
+
|
|
99
|
+
try {
|
|
100
|
+
result = await callback();
|
|
101
|
+
} catch (error) {
|
|
102
|
+
callbackError = error;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
try {
|
|
106
|
+
await runStoryRuleCleanups(cleanups);
|
|
107
|
+
} catch (cleanupError) {
|
|
108
|
+
if (callbackError) {
|
|
109
|
+
throw new AggregateError(
|
|
110
|
+
[callbackError, cleanupError],
|
|
111
|
+
'Story rule execution and cleanup both failed.'
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
throw cleanupError;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
if (callbackError) {
|
|
119
|
+
throw callbackError;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
return result as T;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export async function runStoryRuleCleanups(cleanups: StoryRuleCleanup[]): Promise<void> {
|
|
126
|
+
const errors: unknown[] = [];
|
|
127
|
+
|
|
128
|
+
for (let index = cleanups.length - 1; index >= 0; index -= 1) {
|
|
129
|
+
try {
|
|
130
|
+
await cleanups[index]();
|
|
131
|
+
} catch (error) {
|
|
132
|
+
errors.push(error);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
if (errors.length === 1) {
|
|
137
|
+
throw errors[0];
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
if (errors.length > 1) {
|
|
141
|
+
throw new AggregateError(errors, 'Story rule cleanup failed.');
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
96
145
|
function normalizeRulesConfig(configModule: unknown): StoryRulesConfig {
|
|
97
146
|
const configExport = getRulesConfigExport(configModule);
|
|
98
147
|
|
|
@@ -293,7 +342,7 @@ function slugify(input: string): string {
|
|
|
293
342
|
function createEmptySelection(): MutableStoryRuleSelection {
|
|
294
343
|
return {
|
|
295
344
|
moduleMocks: new Map(),
|
|
296
|
-
|
|
345
|
+
cleanups: []
|
|
297
346
|
};
|
|
298
347
|
}
|
|
299
348
|
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import { timingSafeEqual } from 'node:crypto';
|
|
2
|
+
import { Hono } from 'hono';
|
|
3
|
+
import { cors } from 'hono/cors';
|
|
4
|
+
import type { HandlerProps } from '../middleware.ts';
|
|
5
|
+
import { handlerFactory } from '../middleware.ts';
|
|
6
|
+
import astroFiles from 'virtual:astro-files';
|
|
7
|
+
import sanitization from 'virtual:storybook-astro-sanitization-config';
|
|
8
|
+
import storyRulesConfigModule, {
|
|
9
|
+
storybookAstroStoryRulesConfigFilePath
|
|
10
|
+
} from 'virtual:storybook-astro-story-rules-config';
|
|
11
|
+
import {
|
|
12
|
+
storybookAstroServerAuthHeader,
|
|
13
|
+
storybookAstroServerAuthToken
|
|
14
|
+
} from 'virtual:storybook-astro-server-auth-config';
|
|
15
|
+
|
|
16
|
+
const app = new Hono();
|
|
17
|
+
const renderHandlerPromise = createRenderHandler();
|
|
18
|
+
|
|
19
|
+
app.use(
|
|
20
|
+
'*',
|
|
21
|
+
cors({
|
|
22
|
+
origin: '*',
|
|
23
|
+
allowMethods: ['GET', 'POST'],
|
|
24
|
+
allowHeaders: ['Content-Type', storybookAstroServerAuthHeader]
|
|
25
|
+
})
|
|
26
|
+
);
|
|
27
|
+
|
|
28
|
+
app.get('/', async (context) => context.text('OK'));
|
|
29
|
+
|
|
30
|
+
app.post('/render', async (context) => {
|
|
31
|
+
if (!isRequestAuthorized(context.req.header(storybookAstroServerAuthHeader))) {
|
|
32
|
+
return context.text('Unauthorized', 401);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const input = (await context.req.json()) as Partial<HandlerProps>;
|
|
36
|
+
const renderHandler = await renderHandlerPromise;
|
|
37
|
+
const html = await renderHandler({
|
|
38
|
+
component: input.component ?? '',
|
|
39
|
+
args: input.args ?? {},
|
|
40
|
+
slots: input.slots ?? {},
|
|
41
|
+
story: input.story
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
return context.text(html);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
export default app;
|
|
48
|
+
|
|
49
|
+
async function createRenderHandler() {
|
|
50
|
+
return handlerFactory([], {
|
|
51
|
+
sanitization: sanitization ?? undefined,
|
|
52
|
+
rulesConfigFilePath: storybookAstroStoryRulesConfigFilePath,
|
|
53
|
+
resolveRulesConfigModule: () => storyRulesConfigModule,
|
|
54
|
+
loadModule: async (componentId) => {
|
|
55
|
+
const component = astroFiles[componentId as keyof typeof astroFiles];
|
|
56
|
+
|
|
57
|
+
if (!component) {
|
|
58
|
+
throw new Error(
|
|
59
|
+
`Unable to resolve Astro component "${componentId}" in the server build output.`
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return {
|
|
64
|
+
default: component
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function isRequestAuthorized(headerValue: string | undefined) {
|
|
71
|
+
if (!storybookAstroServerAuthToken) {
|
|
72
|
+
return true;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const normalizedHeaderValue = normalizeHeaderValue(headerValue);
|
|
76
|
+
|
|
77
|
+
if (!normalizedHeaderValue) {
|
|
78
|
+
return false;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return isSecureEqual(normalizedHeaderValue, storybookAstroServerAuthToken);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function normalizeHeaderValue(value: string | undefined) {
|
|
85
|
+
if (!value) {
|
|
86
|
+
return undefined;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const trimmedValue = value.trim();
|
|
90
|
+
|
|
91
|
+
if (!trimmedValue) {
|
|
92
|
+
return undefined;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
if (storybookAstroServerAuthHeader === 'authorization' && trimmedValue.startsWith('Bearer ')) {
|
|
96
|
+
return trimmedValue.slice('Bearer '.length).trim();
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return trimmedValue;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function isSecureEqual(actual: string, expected: string) {
|
|
103
|
+
const actualBuffer = Buffer.from(actual);
|
|
104
|
+
const expectedBuffer = Buffer.from(expected);
|
|
105
|
+
|
|
106
|
+
if (actualBuffer.length !== expectedBuffer.length) {
|
|
107
|
+
return false;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
return timingSafeEqual(actualBuffer, expectedBuffer);
|
|
111
|
+
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { createServer as createHttpServer } from 'node:http';
|
|
2
2
|
import type { IncomingMessage } from 'node:http';
|
|
3
|
+
import { existsSync } from 'node:fs';
|
|
3
4
|
import { fileURLToPath } from 'node:url';
|
|
4
5
|
import type { ViteDevServer } from 'vite';
|
|
5
6
|
import { createViteServer } from '../viteStorybookAstroMiddlewarePlugin.ts';
|
|
@@ -118,7 +119,15 @@ export async function startTestingRendererDaemon(): Promise<RunningDaemon> {
|
|
|
118
119
|
renderHandlerPromises.set(resolveFrom, (async () => {
|
|
119
120
|
const integrations = resolveTestingIntegrationsForRoot(resolveFrom);
|
|
120
121
|
const viteServer = await getViteServer(resolveFrom);
|
|
121
|
-
|
|
122
|
+
// In the workspace this file is src/testing/renderer-daemon.ts so
|
|
123
|
+
// '../middleware.ts' resolves to src/middleware.ts (Vite handles .ts).
|
|
124
|
+
// When compiled by tsup, this code lands in a dist/chunk-*.js file so
|
|
125
|
+
// '../middleware.ts' would resolve to framework/middleware.ts which does
|
|
126
|
+
// not exist; fall back to './middleware.js' (sibling in dist/).
|
|
127
|
+
const middlewareSrcPath = fileURLToPath(new URL('../middleware.ts', import.meta.url));
|
|
128
|
+
const middlewareModulePath = existsSync(middlewareSrcPath)
|
|
129
|
+
? middlewareSrcPath
|
|
130
|
+
: fileURLToPath(new URL('./middleware.js', import.meta.url));
|
|
122
131
|
const middleware = await runWithWorkingDirectory(resolveFrom, () =>
|
|
123
132
|
viteServer.ssrLoadModule(middlewareModulePath, {
|
|
124
133
|
fixStacktrace: true
|
package/src/types.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { CompatibleString, Options } from 'storybook/internal/types';
|
|
1
|
+
import type { CompatibleString, Options, StorybookConfig as StorybookConfigBase } from 'storybook/internal/types';
|
|
2
2
|
import type { InlineConfig } from 'vite';
|
|
3
3
|
import type { Integration } from './integrations/index.ts';
|
|
4
4
|
import type { SanitizationOptions } from './lib/sanitization.ts';
|
|
@@ -7,6 +7,13 @@ import type { StoryRulesOptions } from './rules-options.ts';
|
|
|
7
7
|
type FrameworkName = CompatibleString<'@storybook-astro/framework'>;
|
|
8
8
|
|
|
9
9
|
export type { Integration, SanitizationOptions, StoryRulesOptions };
|
|
10
|
+
export type RenderMode = 'server' | 'static';
|
|
11
|
+
|
|
12
|
+
export type ServerBuildOptions = {
|
|
13
|
+
serverUrl?: string;
|
|
14
|
+
authToken?: string;
|
|
15
|
+
authHeader?: string;
|
|
16
|
+
};
|
|
10
17
|
|
|
11
18
|
export type RenderStoryInput = {
|
|
12
19
|
id: string;
|
|
@@ -14,13 +21,26 @@ export type RenderStoryInput = {
|
|
|
14
21
|
name?: string;
|
|
15
22
|
};
|
|
16
23
|
|
|
17
|
-
|
|
24
|
+
type BaseFrameworkOptions = {
|
|
18
25
|
integrations?: Integration[];
|
|
19
26
|
sanitization?: SanitizationOptions;
|
|
20
|
-
storyRules?: StoryRulesOptions;
|
|
21
27
|
resolveFrom?: string;
|
|
22
28
|
};
|
|
23
29
|
|
|
30
|
+
type ServerFrameworkOptions = BaseFrameworkOptions & {
|
|
31
|
+
renderMode?: 'server';
|
|
32
|
+
storyRules?: StoryRulesOptions;
|
|
33
|
+
server?: ServerBuildOptions;
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
type StaticFrameworkOptions = BaseFrameworkOptions & {
|
|
37
|
+
renderMode: 'static';
|
|
38
|
+
storyRules?: StoryRulesOptions;
|
|
39
|
+
server?: never;
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
export type FrameworkOptions = ServerFrameworkOptions | StaticFrameworkOptions;
|
|
43
|
+
|
|
24
44
|
type StorybookConfigFramework = {
|
|
25
45
|
framework: {
|
|
26
46
|
name: FrameworkName;
|
|
@@ -28,10 +48,10 @@ type StorybookConfigFramework = {
|
|
|
28
48
|
};
|
|
29
49
|
};
|
|
30
50
|
|
|
31
|
-
export type StorybookConfig = StorybookConfigFramework;
|
|
32
|
-
|
|
33
51
|
type ViteFinal = (config: InlineConfig, options: Options) => InlineConfig | Promise<InlineConfig>;
|
|
34
52
|
|
|
35
53
|
export type StorybookConfigVite = {
|
|
36
54
|
viteFinal?: ViteFinal;
|
|
37
55
|
};
|
|
56
|
+
|
|
57
|
+
export type StorybookConfig = Omit<StorybookConfigBase, 'framework'> & StorybookConfigFramework & StorybookConfigVite;
|
package/src/virtual.d.ts
CHANGED
|
@@ -5,4 +5,41 @@ declare module 'virtual:astro-container-renderers' {
|
|
|
5
5
|
export function resolveClientModules(specifier: string): string | undefined;
|
|
6
6
|
}
|
|
7
7
|
|
|
8
|
+
declare module 'virtual:astro-files' {
|
|
9
|
+
const astroFiles: Record<string, unknown>;
|
|
10
|
+
|
|
11
|
+
export default astroFiles;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
declare module 'virtual:storybook-astro-renderer' {
|
|
15
|
+
import type { RenderComponentInput, RenderResponseMessage } from '@storybook-astro/renderer/types';
|
|
16
|
+
|
|
17
|
+
export function render(
|
|
18
|
+
input: RenderComponentInput,
|
|
19
|
+
timeoutMs?: number
|
|
20
|
+
): Promise<RenderResponseMessage['data']>;
|
|
21
|
+
export function init(): void;
|
|
22
|
+
export function applyStyles(): void;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
declare module 'virtual:storybook-astro-sanitization-config' {
|
|
26
|
+
import type { SanitizationOptions } from './lib/sanitization.ts';
|
|
27
|
+
|
|
28
|
+
const sanitization: SanitizationOptions | undefined;
|
|
29
|
+
|
|
30
|
+
export default sanitization;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
declare module 'virtual:storybook-astro-story-rules-config' {
|
|
34
|
+
const configModule: unknown;
|
|
35
|
+
|
|
36
|
+
export default configModule;
|
|
37
|
+
export const storybookAstroStoryRulesConfigFilePath: string | undefined;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
declare module 'virtual:storybook-astro-server-auth-config' {
|
|
41
|
+
export const storybookAstroServerAuthToken: string | undefined;
|
|
42
|
+
export const storybookAstroServerAuthHeader: string;
|
|
43
|
+
}
|
|
44
|
+
|
|
8
45
|
declare module 'virtual:storybook-renderer-fallback' {}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type { Plugin } from 'vite';
|
|
2
|
+
import { createVirtualModulePlugin } from './createVirtualModulePlugin.ts';
|
|
3
|
+
|
|
4
|
+
type ImportRecord = {
|
|
5
|
+
id: string;
|
|
6
|
+
file: string;
|
|
7
|
+
importStatement: string;
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
export function astroFilesVirtualModulePlugin(astroComponents: string[]): Plugin {
|
|
11
|
+
return createVirtualModulePlugin({
|
|
12
|
+
pluginName: 'storybook-astro:virtual-astro-files',
|
|
13
|
+
virtualModuleId: 'virtual:astro-files',
|
|
14
|
+
load() {
|
|
15
|
+
const imports = astroComponents.reduce<ImportRecord[]>((records, file, index) => {
|
|
16
|
+
const moduleId = `_astroFile${index}`;
|
|
17
|
+
|
|
18
|
+
return [
|
|
19
|
+
...records,
|
|
20
|
+
{
|
|
21
|
+
id: moduleId,
|
|
22
|
+
file,
|
|
23
|
+
importStatement: `import ${moduleId} from '${file}';`
|
|
24
|
+
}
|
|
25
|
+
];
|
|
26
|
+
}, []);
|
|
27
|
+
|
|
28
|
+
return [
|
|
29
|
+
imports.map(({ importStatement }) => importStatement).join('\n'),
|
|
30
|
+
'export default {',
|
|
31
|
+
imports.map(({ file, id }) => `'${file}': ${id}`).join(',\n'),
|
|
32
|
+
'};'
|
|
33
|
+
].join('\n');
|
|
34
|
+
}
|
|
35
|
+
});
|
|
36
|
+
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { Plugin } from 'vite';
|
|
2
2
|
|
|
3
3
|
type CreateVirtualModulePluginOptions = {
|
|
4
4
|
pluginName: string;
|
|
@@ -6,7 +6,7 @@ type CreateVirtualModulePluginOptions = {
|
|
|
6
6
|
load: (id: string) => string | Promise<string> | undefined;
|
|
7
7
|
};
|
|
8
8
|
|
|
9
|
-
export function createVirtualModulePlugin(options: CreateVirtualModulePluginOptions):
|
|
9
|
+
export function createVirtualModulePlugin(options: CreateVirtualModulePluginOptions): Plugin {
|
|
10
10
|
const resolvedVirtualModuleId = `\0${options.virtualModuleId}`;
|
|
11
11
|
|
|
12
12
|
return {
|
|
@@ -21,5 +21,5 @@ export function createVirtualModulePlugin(options: CreateVirtualModulePluginOpti
|
|
|
21
21
|
return options.load(id);
|
|
22
22
|
}
|
|
23
23
|
}
|
|
24
|
-
} satisfies
|
|
24
|
+
} satisfies Plugin;
|
|
25
25
|
}
|