@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.
Files changed (62) hide show
  1. package/dist/{chunk-7GHEQUPV.js → chunk-POHTFYST.js} +46 -8
  2. package/dist/chunk-POHTFYST.js.map +1 -0
  3. package/dist/chunk-T7NWIO5S.js +220 -0
  4. package/dist/chunk-T7NWIO5S.js.map +1 -0
  5. package/dist/{chunk-C5OH4VBR.js → chunk-V76WSNSP.js} +124 -47
  6. package/dist/chunk-V76WSNSP.js.map +1 -0
  7. package/dist/{chunk-KSDXET2L.js → chunk-VPJDFGB5.js} +444 -60
  8. package/dist/chunk-VPJDFGB5.js.map +1 -0
  9. package/dist/index.d.ts +19 -9
  10. package/dist/index.js +10 -3
  11. package/dist/index.js.map +1 -1
  12. package/dist/middleware.js +57 -39
  13. package/dist/middleware.js.map +1 -1
  14. package/dist/node/index.d.ts +10 -0
  15. package/dist/node/index.js +10 -0
  16. package/dist/node/index.js.map +1 -0
  17. package/dist/preset.d.ts +1 -1
  18. package/dist/preset.js +3 -3
  19. package/dist/testing.js +12 -64
  20. package/dist/testing.js.map +1 -1
  21. package/dist/{types-CHTsRtA7.d.ts → types-Cvor6Tyi.d.ts} +21 -5
  22. package/dist/{viteStorybookAstroMiddlewarePlugin-NP2E52IC.js → viteStorybookAstroMiddlewarePlugin-2EFKTECT.js} +2 -2
  23. package/dist/vitest/global-setup.js +42 -0
  24. package/dist/vitest/global-setup.js.map +1 -0
  25. package/dist/vitest/index.js +20 -3
  26. package/dist/vitest/index.js.map +1 -1
  27. package/package.json +13 -5
  28. package/src/index.ts +21 -1
  29. package/src/lib/sanitization.ts +104 -0
  30. package/src/middleware.ts +76 -44
  31. package/src/node/index.ts +7 -0
  32. package/src/preset.ts +75 -15
  33. package/src/renderer/renderer-dev.ts +82 -0
  34. package/src/renderer/renderer-server.test.ts +101 -0
  35. package/src/renderer/renderer-server.ts +135 -0
  36. package/src/renderer/renderer-static.ts +62 -0
  37. package/src/rules.test.ts +89 -18
  38. package/src/rules.ts +67 -18
  39. package/src/server/index.ts +111 -0
  40. package/src/testing/renderer-daemon.ts +10 -1
  41. package/src/types.ts +25 -5
  42. package/src/virtual.d.ts +37 -0
  43. package/src/vite/astroFilesVirtualModulePlugin.ts +36 -0
  44. package/src/vite/createVirtualModulePlugin.ts +3 -3
  45. package/src/vite/storybookAstroRulesConfigVirtualModulePlugin.ts +37 -0
  46. package/src/vite/storybookAstroSanitizationConfigVirtualModulePlugin.ts +21 -0
  47. package/src/vite/storybookAstroServerAuthConfigVirtualModulePlugin.test.ts +71 -0
  48. package/src/vite/storybookAstroServerAuthConfigVirtualModulePlugin.ts +42 -0
  49. package/src/vitePluginAstroBuildPrerender.ts +50 -51
  50. package/src/vitePluginAstroBuildServer.ts +289 -0
  51. package/src/vitePluginAstroIntegrationOptsFallback.ts +25 -0
  52. package/src/viteStorybookAstroMiddlewarePlugin.ts +40 -8
  53. package/src/viteStorybookAstroRendererPlugin.ts +45 -0
  54. package/src/vitest/config.ts +45 -4
  55. package/src/vitest/global-setup.ts +45 -0
  56. package/dist/chunk-7GHEQUPV.js.map +0 -1
  57. package/dist/chunk-C5OH4VBR.js.map +0 -1
  58. package/dist/chunk-KSDXET2L.js.map +0 -1
  59. package/dist/middleware.d.ts +0 -26
  60. package/src/msw-helpers.ts +0 -1
  61. package/src/msw.ts +0 -58
  62. /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 { defineStoryRules, selectStoryRules, type StoryRulesConfig } from './rules.ts';
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.mswHandlers).toEqual([]);
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.mswHandlers).toHaveLength(0);
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 MSW handlers from matching rules', async () => {
93
- const firstHandler = {} as RequestHandler;
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: ({ msw }) => {
102
- msw.use(firstHandler, secondHandler);
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.mswHandlers).toEqual([firstHandler, secondHandler]);
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 StoryMode = 'development' | 'production';
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
- mswHandlers: RequestHandler[];
38
+ cleanups: StoryRuleCleanup[];
45
39
  };
46
40
 
47
41
  type MutableStoryRuleSelection = {
48
42
  moduleMocks: Map<string, string>;
49
- mswHandlers: RequestHandler[];
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
- mswHandlers: []
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
- const middlewareModulePath = fileURLToPath(new URL('../middleware', import.meta.url));
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
- export type FrameworkOptions = {
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 { PluginOption } from 'vite';
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): PluginOption {
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 PluginOption;
24
+ } satisfies Plugin;
25
25
  }