@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.
Files changed (63) hide show
  1. package/dist/{chunk-KSDXET2L.js → chunk-4HECE7IW.js} +477 -61
  2. package/dist/chunk-4HECE7IW.js.map +1 -0
  3. package/dist/{chunk-7GHEQUPV.js → chunk-POHTFYST.js} +46 -8
  4. package/dist/chunk-POHTFYST.js.map +1 -0
  5. package/dist/chunk-T7NWIO5S.js +220 -0
  6. package/dist/chunk-T7NWIO5S.js.map +1 -0
  7. package/dist/{chunk-C5OH4VBR.js → chunk-V76WSNSP.js} +124 -47
  8. package/dist/chunk-V76WSNSP.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 +11 -3
  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 +86 -16
  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/vitePluginAstroToolbarFallback.ts +38 -0
  53. package/src/viteStorybookAstroMiddlewarePlugin.ts +40 -8
  54. package/src/viteStorybookAstroRendererPlugin.ts +45 -0
  55. package/src/vitest/config.ts +45 -4
  56. package/src/vitest/global-setup.ts +45 -0
  57. package/dist/chunk-7GHEQUPV.js.map +0 -1
  58. package/dist/chunk-C5OH4VBR.js.map +0 -1
  59. package/dist/chunk-KSDXET2L.js.map +0 -1
  60. package/dist/middleware.d.ts +0 -26
  61. package/src/msw-helpers.ts +0 -1
  62. package/src/msw.ts +0 -58
  63. /package/dist/{viteStorybookAstroMiddlewarePlugin-NP2E52IC.js.map → viteStorybookAstroMiddlewarePlugin-2EFKTECT.js.map} +0 -0
@@ -0,0 +1,289 @@
1
+ import type { Dirent } from 'node:fs';
2
+ import { readdir } from 'node:fs/promises';
3
+ import { dirname, resolve } from 'node:path';
4
+ import { fileURLToPath } from 'node:url';
5
+ import { build, type Rollup } from 'vite';
6
+ import type { FrameworkOptions } from './types.ts';
7
+ import { mergeWithAstroConfig } from './vitePluginAstro.ts';
8
+ import { viteAstroContainerRenderersPlugin } from './viteAstroContainerRenderersPlugin.ts';
9
+ import { astroFilesVirtualModulePlugin } from './vite/astroFilesVirtualModulePlugin.ts';
10
+ import { storybookAstroStoryRulesConfigVirtualModulePlugin } from './vite/storybookAstroRulesConfigVirtualModulePlugin.ts';
11
+ import { storybookAstroSanitizationConfigVirtualModulePlugin } from './vite/storybookAstroSanitizationConfigVirtualModulePlugin.ts';
12
+ import { storybookAstroServerAuthConfigVirtualModulePlugin } from './vite/storybookAstroServerAuthConfigVirtualModulePlugin.ts';
13
+
14
+ const moduleRoot = resolve(dirname(fileURLToPath(import.meta.url)), '.');
15
+ // packageRoot works regardless of whether this file is running from src/ or dist/
16
+ const packageRoot = resolve(moduleRoot, '..');
17
+
18
+ export function vitePluginAstroBuildServer(options: FrameworkOptions) {
19
+ const integrations = options.integrations ?? [];
20
+ const resolveFrom = options.resolveFrom ?? process.cwd();
21
+ const storiesMap = new Map<string, Set<string>>();
22
+ const trackedSpecifiers = collectTrackedSpecifiers(integrations);
23
+ const staticEntrypointRefs = new Map<string, string>();
24
+ const componentEntrypointRefs = new Map<string, string>();
25
+ let storybookStaticOutDir = resolve(resolveFrom, 'storybook-static');
26
+
27
+ return {
28
+ name: 'storybook-astro:build-server',
29
+ apply: 'build',
30
+ enforce: 'post',
31
+
32
+ configResolved(config: { build: { outDir?: string } }) {
33
+ storybookStaticOutDir = resolve(resolveFrom, config.build.outDir ?? 'storybook-static');
34
+ },
35
+
36
+ resolveId(id: string, importer?: string) {
37
+ if (id.endsWith('.astro') && importer) {
38
+ const absoluteAstroPath = resolve(dirname(importer), id);
39
+
40
+ if (!storiesMap.has(absoluteAstroPath)) {
41
+ storiesMap.set(absoluteAstroPath, new Set());
42
+ }
43
+
44
+ storiesMap.get(absoluteAstroPath)?.add(importer);
45
+ }
46
+
47
+ if (id.startsWith('virtual:astro-static-module/')) {
48
+ return `\0${id}`;
49
+ }
50
+
51
+ if (id.startsWith('virtual:astro-component-module/')) {
52
+ return `\0${id}`;
53
+ }
54
+ },
55
+
56
+ load(id: string) {
57
+ if (id.startsWith('\0virtual:astro-static-module/')) {
58
+ const encodedSpecifier = id.replace('\0virtual:astro-static-module/', '');
59
+ const specifier = decodeURIComponent(encodedSpecifier);
60
+
61
+ if (isClientEntrypoint(specifier)) {
62
+ return [`export { default } from '${specifier}';`, `export * from '${specifier}';`].join('\n');
63
+ }
64
+
65
+ return [`import '${specifier}';`, 'export default undefined;'].join('\n');
66
+ }
67
+
68
+ if (id.startsWith('\0virtual:astro-component-module/')) {
69
+ const encodedSpecifier = id.replace('\0virtual:astro-component-module/', '');
70
+ const specifier = decodeURIComponent(encodedSpecifier);
71
+
72
+ return [`export { default } from '${specifier}';`, `export * from '${specifier}';`].join('\n');
73
+ }
74
+ },
75
+
76
+ async buildStart(this: Rollup.PluginContext) {
77
+ integrations.forEach((integration) => {
78
+ const entrypoint = integration.renderer.client?.entrypoint;
79
+
80
+ if (entrypoint) {
81
+ this.addWatchFile(entrypoint);
82
+ }
83
+ });
84
+
85
+ trackedSpecifiers.forEach((specifier) => {
86
+ const fileReferenceId = this.emitFile({
87
+ type: 'chunk',
88
+ id: toStaticVirtualId(specifier)
89
+ });
90
+
91
+ staticEntrypointRefs.set(specifier, fileReferenceId);
92
+ });
93
+
94
+ const srcRoot = resolve(resolveFrom, 'src/components');
95
+ const specifiers = await collectHydratableSourceModules(srcRoot);
96
+
97
+ specifiers.forEach((specifier) => {
98
+ const fileReferenceId = this.emitFile({
99
+ type: 'chunk',
100
+ id: toComponentVirtualId(specifier)
101
+ });
102
+
103
+ componentEntrypointRefs.set(specifier, fileReferenceId);
104
+ });
105
+ },
106
+
107
+ async writeBundle(this: Rollup.PluginContext) {
108
+ const astroComponents = Array.from(storiesMap.keys());
109
+ const staticModuleMap = buildStaticModuleMap(
110
+ this,
111
+ staticEntrypointRefs,
112
+ componentEntrypointRefs
113
+ );
114
+ const serverOutDir = resolve(dirname(storybookStaticOutDir), 'storybook-server');
115
+
116
+ await buildAstroServer({
117
+ astroComponents,
118
+ integrations,
119
+ sanitization: options.sanitization,
120
+ storyRules: options.storyRules,
121
+ server: options.server,
122
+ outDir: serverOutDir,
123
+ staticModuleMap,
124
+ resolveFrom
125
+ });
126
+ }
127
+ };
128
+ }
129
+
130
+ async function buildAstroServer(options: {
131
+ astroComponents: string[];
132
+ integrations: FrameworkOptions['integrations'];
133
+ sanitization?: FrameworkOptions['sanitization'];
134
+ storyRules?: FrameworkOptions['storyRules'];
135
+ server?: FrameworkOptions['server'];
136
+ outDir: string;
137
+ staticModuleMap: Record<string, string>;
138
+ resolveFrom: string;
139
+ }) {
140
+ const buildConfig = {
141
+ root: resolve(packageRoot, 'src/server'),
142
+ ssr: {
143
+ noExternal: /(@astrojs\/.+|react|react-dom)/
144
+ },
145
+ build: {
146
+ ssr: true,
147
+ outDir: options.outDir,
148
+ emptyOutDir: true,
149
+ sourcemap: true,
150
+ manifest: false,
151
+ rollupOptions: {
152
+ input: resolve(packageRoot, 'src/server/index.ts'),
153
+ treeshake: true
154
+ }
155
+ },
156
+ plugins: [
157
+ astroFilesVirtualModulePlugin(options.astroComponents),
158
+ storybookAstroSanitizationConfigVirtualModulePlugin(options.sanitization),
159
+ storybookAstroStoryRulesConfigVirtualModulePlugin(options.storyRules, options.resolveFrom),
160
+ storybookAstroServerAuthConfigVirtualModulePlugin(options.server),
161
+ viteAstroContainerRenderersPlugin(options.integrations, {
162
+ mode: 'production',
163
+ staticModuleMap: options.staticModuleMap
164
+ })
165
+ ]
166
+ };
167
+
168
+ const finalConfig = await mergeWithAstroConfig(
169
+ buildConfig,
170
+ options.integrations,
171
+ options.resolveFrom,
172
+ 'production',
173
+ 'build'
174
+ );
175
+
176
+ await build(finalConfig);
177
+ }
178
+
179
+ function collectTrackedSpecifiers(integrations: FrameworkOptions['integrations']) {
180
+ const specifiers = new Set<string>(['astro:scripts/page.js', 'astro:scripts/before-hydration.js']);
181
+
182
+ integrations.forEach((integration) => {
183
+ const entrypoint = integration.renderer.client?.entrypoint;
184
+
185
+ if (entrypoint) {
186
+ specifiers.add(entrypoint);
187
+ }
188
+ });
189
+
190
+ return specifiers;
191
+ }
192
+
193
+ function buildStaticModuleMap(
194
+ pluginContext: Rollup.PluginContext,
195
+ staticEntrypointRefs: Map<string, string>,
196
+ componentEntrypointRefs: Map<string, string>
197
+ ) {
198
+ const map: Record<string, string> = {};
199
+
200
+ staticEntrypointRefs.forEach((fileReferenceId, specifier) => {
201
+ const fileName = pluginContext.getFileName(fileReferenceId);
202
+
203
+ if (fileName) {
204
+ map[specifier] = toPublicPath(fileName);
205
+ }
206
+ });
207
+
208
+ componentEntrypointRefs.forEach((fileReferenceId, specifier) => {
209
+ const fileName = pluginContext.getFileName(fileReferenceId);
210
+
211
+ if (fileName) {
212
+ map[specifier] = toPublicPath(fileName);
213
+ }
214
+ });
215
+
216
+ return map;
217
+ }
218
+
219
+ function toStaticVirtualId(specifier: string) {
220
+ return `virtual:astro-static-module/${encodeURIComponent(specifier)}`;
221
+ }
222
+
223
+ function toComponentVirtualId(specifier: string) {
224
+ return `virtual:astro-component-module/${encodeURIComponent(specifier)}`;
225
+ }
226
+
227
+ function isClientEntrypoint(specifier: string) {
228
+ return specifier.startsWith('@astrojs/') && specifier.endsWith('/client.js');
229
+ }
230
+
231
+ function toPublicPath(fileName: string) {
232
+ return `./${fileName}`;
233
+ }
234
+
235
+ async function collectHydratableSourceModules(srcRoot: string): Promise<string[]> {
236
+ const modules: string[] = [];
237
+
238
+ async function walk(directory: string) {
239
+ let entries: Dirent[];
240
+
241
+ try {
242
+ entries = await readdir(directory, { withFileTypes: true });
243
+ } catch {
244
+ return;
245
+ }
246
+
247
+ await Promise.all(
248
+ entries.map(async (entry) => {
249
+ const absolutePath = resolve(directory, entry.name);
250
+
251
+ if (entry.isDirectory()) {
252
+ await walk(absolutePath);
253
+
254
+ return;
255
+ }
256
+
257
+ if (!entry.isFile()) {
258
+ return;
259
+ }
260
+
261
+ const normalizedPath = absolutePath.replace(/\\/g, '/');
262
+
263
+ if (!isHydratableSourceFile(normalizedPath)) {
264
+ return;
265
+ }
266
+
267
+ if (isNonHydratableSourceFile(normalizedPath)) {
268
+ return;
269
+ }
270
+
271
+ modules.push(normalizedPath);
272
+ })
273
+ );
274
+ }
275
+
276
+ await walk(srcRoot);
277
+
278
+ return modules;
279
+ }
280
+
281
+ function isHydratableSourceFile(input: string) {
282
+ return /\.(jsx|tsx|vue|svelte|js|ts)$/.test(input);
283
+ }
284
+
285
+ function isNonHydratableSourceFile(input: string) {
286
+ return /\.stories\.[jt]sx?$|\.stories\.vue$|\.stories\.svelte$|\.(spec|test)\.[jt]sx?$/.test(
287
+ input
288
+ );
289
+ }
@@ -0,0 +1,25 @@
1
+ import type { Plugin } from 'vite';
2
+
3
+ const OPTS_STUB = 'export default {}';
4
+
5
+ const VIRTUAL_IDS = ['astro:react:opts', 'astro:preact:opts'];
6
+
7
+ export function vitePluginAstroIntegrationOptsFallback(): Plugin {
8
+ const resolvedIds = new Map(VIRTUAL_IDS.map((id) => [id, `\0${id}`]));
9
+ const resolvedIdSet = new Set(resolvedIds.values());
10
+
11
+ return {
12
+ name: 'storybook-astro-integration-opts-fallback',
13
+ enforce: 'pre',
14
+
15
+ resolveId(id) {
16
+ return resolvedIds.get(id);
17
+ },
18
+
19
+ load(id) {
20
+ if (resolvedIdSet.has(id)) {
21
+ return { code: OPTS_STUB };
22
+ }
23
+ }
24
+ };
25
+ }
@@ -0,0 +1,38 @@
1
+ import type { Plugin } from 'vite';
2
+
3
+ const TOOLBAR_INTERNAL_STUB = `
4
+ export const loadDevToolbarApps = async () => [];
5
+ `;
6
+
7
+ /**
8
+ * Provides a fallback stub for Astro's dev toolbar virtual module.
9
+ *
10
+ * Astro's `astro/dist/runtime/client/dev-toolbar/entrypoint.js` imports
11
+ * from `astro:toolbar:internal`, a virtual module normally provided by
12
+ * Astro's own `vite-plugin-dev-toolbar` Vite plugin. In the Storybook
13
+ * context that plugin is not active, causing esbuild to fail during
14
+ * dependency optimisation when it encounters the unresolvable import.
15
+ *
16
+ * Storybook doesn't use Astro's dev toolbar, so a no-op stub is safe.
17
+ */
18
+ export function vitePluginAstroToolbarFallback(): Plugin {
19
+ const VIRTUAL_ID = 'astro:toolbar:internal';
20
+ const RESOLVED_ID = '\0' + VIRTUAL_ID;
21
+
22
+ return {
23
+ name: 'storybook-astro-toolbar-fallback',
24
+ enforce: 'pre',
25
+
26
+ resolveId(id) {
27
+ if (id === VIRTUAL_ID) {
28
+ return RESOLVED_ID;
29
+ }
30
+ },
31
+
32
+ load(id) {
33
+ if (id === RESOLVED_ID) {
34
+ return { code: TOOLBAR_INTERNAL_STUB };
35
+ }
36
+ }
37
+ };
38
+ }
@@ -1,12 +1,14 @@
1
1
  import { createRequire } from 'node:module';
2
2
  import { fileURLToPath } from 'node:url';
3
- import { createServer, type PluginOption, type ViteDevServer } from 'vite';
3
+ import type { ServerResponse } from 'node:http';
4
+ import { createServer, createLogger, type Connect, type PluginOption, type ViteDevServer } from 'vite';
4
5
  import type { RenderRequestMessage, RenderResponseMessage } from '@storybook-astro/renderer/types';
5
6
  import type { FrameworkOptions } from './types.ts';
6
7
  import type { Integration } from './integrations/index.ts';
7
8
  import { importAstroConfig } from './importAstroConfig.ts';
8
9
  import { viteAstroContainerRenderersPlugin } from './viteAstroContainerRenderersPlugin.ts';
9
10
  import { vitePluginAstroFontsFallback } from './vitePluginAstroFontsFallback.ts';
11
+ import { vitePluginAstroIntegrationOptsFallback } from './vitePluginAstroIntegrationOptsFallback.ts';
10
12
  import { vitePluginAstroVueFallback } from './vitePluginAstroVueFallback.ts';
11
13
  import { vitePluginAstroRoutesFallback } from './vitePluginAstroRoutesFallback.ts';
12
14
  import { ssrLoadModuleWithFsFallback } from './lib/ssr-load-module-with-fs-fallback.ts';
@@ -22,7 +24,7 @@ export async function vitePluginStorybookAstroMiddleware(options: FrameworkOptio
22
24
  const vitePlugin = {
23
25
  name: 'storybook-astro-middleware-plugin',
24
26
  async configureServer(server) {
25
- viteServer = await createViteServer(options.integrations, resolveFrom);
27
+ viteServer = await createViteServer(options.integrations ?? [], resolveFrom);
26
28
  const storyRulesConfigFilePath = resolveRulesConfigFilePath(options.storyRules, resolveFrom);
27
29
 
28
30
  const filePath = fileURLToPath(new URL('./middleware', import.meta.url));
@@ -31,7 +33,6 @@ export async function vitePluginStorybookAstroMiddleware(options: FrameworkOptio
31
33
  });
32
34
 
33
35
  const createHandler = () => middleware.handlerFactory(options.integrations ?? [], {
34
- mode: 'development',
35
36
  sanitization: options.sanitization,
36
37
  rulesConfigFilePath: storyRulesConfigFilePath,
37
38
  resolveRulesConfigModule: () =>
@@ -84,15 +85,15 @@ export async function vitePluginStorybookAstroMiddleware(options: FrameworkOptio
84
85
  // Create asset serving plugin (only active in dev when viteServer exists)
85
86
  const assetServingPlugin = {
86
87
  name: 'storybook-astro-assets',
87
- configureServer(server) {
88
- server.middlewares.use('/_image', (req, res, next) => {
88
+ configureServer(server: ViteDevServer) {
89
+ server.middlewares.use('/_image', (req: Connect.IncomingMessage, res: ServerResponse, next: Connect.NextFunction) => {
89
90
  if (!viteServer) {
90
91
  next();
91
92
 
92
93
  return;
93
94
  }
94
95
  // Forward the request to the Astro vite server
95
- viteServer.middlewares.handle(req, res, (err) => {
96
+ viteServer.middlewares.handle(req, res, (err?: unknown) => {
96
97
  if (err) {
97
98
  console.error('Asset serving error:', err);
98
99
  next();
@@ -122,8 +123,33 @@ export async function vitePluginStorybookAstroMiddleware(options: FrameworkOptio
122
123
  };
123
124
  }
124
125
 
126
+ /**
127
+ * Creates a Vite logger that silences known benign warnings emitted by Astro's
128
+ * Vite plugin in the SSR server context:
129
+ * - "Missing pages directory" — Storybook and test contexts have no src/pages.
130
+ * - "points to missing source files" — Sourcemap gaps in the `entities` package.
131
+ */
132
+ function createSsrServerLogger() {
133
+ const logger = createLogger();
134
+ const originalWarn = logger.warn.bind(logger);
135
+
136
+ logger.warn = (msg, options) => {
137
+ if (
138
+ msg.includes('Missing pages directory') ||
139
+ msg.includes('points to missing source files') ||
140
+ msg.includes('Failed to load source map for')
141
+ ) {
142
+ return;
143
+ }
144
+
145
+ originalWarn(msg, options);
146
+ };
147
+
148
+ return logger;
149
+ }
150
+
125
151
  export async function createViteServer(integrations: Integration[], resolveFrom = process.cwd()) {
126
- const { getViteConfig } = await importAstroConfig(resolveFrom);
152
+ const { getViteConfig, passthroughImageService } = await importAstroConfig(resolveFrom);
127
153
  const safeIntegrations = integrations ?? [];
128
154
  const projectAstroResolutionPlugin = createProjectAstroResolutionPlugin(resolveFrom);
129
155
 
@@ -133,17 +159,23 @@ export async function createViteServer(integrations: Integration[], resolveFrom
133
159
  configFile: false,
134
160
  integrations: await Promise.all(
135
161
  safeIntegrations.map((integration) => integration.loadIntegration(resolveFrom))
136
- )
162
+ ),
163
+ // Use the passthrough image service so nested components that use <Image>
164
+ // from astro:assets render as plain <img> tags without triggering image
165
+ // optimization (which fails in the Storybook SSR context).
166
+ image: { service: passthroughImageService() }
137
167
  }
138
168
  )({ mode: 'development', command: 'serve' });
139
169
 
140
170
  const viteServer = await createServer({
141
171
  configFile: false,
142
172
  ...config,
173
+ customLogger: createSsrServerLogger(),
143
174
  plugins: [
144
175
  projectAstroResolutionPlugin,
145
176
  // Fallbacks must come first to intercept before Astro's plugins
146
177
  vitePluginAstroFontsFallback(),
178
+ vitePluginAstroIntegrationOptsFallback(),
147
179
  vitePluginAstroVueFallback(),
148
180
  vitePluginAstroRoutesFallback(),
149
181
  ...(config.plugins?.filter(Boolean) ?? []),
@@ -0,0 +1,45 @@
1
+ import type { RenderMode, ServerBuildOptions } from './types.ts';
2
+ import { createVirtualModulePlugin } from './vite/createVirtualModulePlugin.ts';
3
+
4
+ const packageName = '@storybook-astro/framework';
5
+
6
+ export function viteStorybookAstroRendererPlugin(options: {
7
+ mode: 'development' | 'production';
8
+ renderMode?: RenderMode;
9
+ server?: ServerBuildOptions;
10
+ }) {
11
+ const pluginName = 'storybook-astro:renderer-module';
12
+ const virtualModuleId = 'virtual:storybook-astro-renderer';
13
+ const isProduction = options.mode === 'production';
14
+ const isStaticMode = options.renderMode === 'static';
15
+
16
+ return createVirtualModulePlugin({
17
+ pluginName,
18
+ virtualModuleId,
19
+ load() {
20
+ if (!isProduction) {
21
+ return `export * from '${packageName}/renderer/renderer-dev.ts';`;
22
+ }
23
+
24
+ if (isStaticMode) {
25
+ return `export * from '${packageName}/renderer/renderer-static.ts';`;
26
+ }
27
+
28
+ return [
29
+ `import { createServerRenderer } from '${packageName}/renderer/renderer-server.ts';`,
30
+ `const renderer = createServerRenderer(${JSON.stringify(
31
+ {
32
+ serverUrl: options.server?.serverUrl,
33
+ authToken: options.server?.authToken,
34
+ authHeader: options.server?.authHeader
35
+ },
36
+ null,
37
+ 2
38
+ )});`,
39
+ 'export const render = renderer.render;',
40
+ 'export const init = renderer.init;',
41
+ 'export const applyStyles = renderer.applyStyles;'
42
+ ].join('\n');
43
+ }
44
+ });
45
+ }
@@ -1,4 +1,6 @@
1
1
  import { defineConfig as defineVitestConfig } from 'vitest/config';
2
+ import { createLogger } from 'vite';
3
+ import { existsSync } from 'node:fs';
2
4
  import { fileURLToPath } from 'node:url';
3
5
  import type { InlineConfig, PluginOption } from 'vite';
4
6
  import type { Integration } from '../integrations/base.ts';
@@ -7,9 +9,35 @@ import { vitePluginAstroComponentMarker } from '../vitePluginAstroComponentMarke
7
9
  import { registerTestingIntegrationsForRoot } from '../testing/integration-config.ts';
8
10
  import { cjsInteropPlugin, vitestPatchForSolidJs } from './vite-plugins.ts';
9
11
 
12
+ /**
13
+ * Creates a Vite logger that suppresses known benign warnings in the test context:
14
+ * - "Missing pages directory" — Astro warns when no src/pages exists, but component
15
+ * tests don't use pages so this is always safe to ignore.
16
+ * - "points to missing source files" — Sourcemap warnings from the `entities` package
17
+ * which ships without source files. Not actionable.
18
+ */
19
+ function createTestLogger() {
20
+ const logger = createLogger();
21
+ const originalWarn = logger.warn.bind(logger);
22
+
23
+ logger.warn = (msg, options) => {
24
+ if (
25
+ msg.includes('Missing pages directory') ||
26
+ msg.includes('points to missing source files') ||
27
+ msg.includes('Failed to load source map for')
28
+ ) {
29
+ return;
30
+ }
31
+
32
+ originalWarn(msg, options);
33
+ };
34
+
35
+ return logger;
36
+ }
37
+
10
38
  // Type definition omits 'test' to allow Vitest-specific config options
11
39
  // Vite 8 type definitions conflict with Vitest config when used in monorepo
12
- // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Vitest config requires any type for test option
40
+
13
41
  export type TestingDefineConfig = Omit<InlineConfig, 'plugins' | 'test'> & {
14
42
  integrations?: Integration[];
15
43
  plugins?: PluginOption[];
@@ -51,7 +79,13 @@ export function defineConfig(options: TestingDefineConfig) {
51
79
 
52
80
  registerTestingIntegrationsForRoot(root, integrations);
53
81
 
54
- const globalSetupFilePath = fileURLToPath(new URL('./global-setup.ts', import.meta.url));
82
+ // In the workspace, import.meta.url points to src/vitest/config.ts so global-setup.ts exists.
83
+ // In a compiled tarball install, import.meta.url points to dist/vitest/config.js so we fall
84
+ // back to global-setup.js which is the tsup-compiled output.
85
+ const globalSetupTsPath = fileURLToPath(new URL('./global-setup.ts', import.meta.url));
86
+ const globalSetupFilePath = existsSync(globalSetupTsPath)
87
+ ? globalSetupTsPath
88
+ : fileURLToPath(new URL('./global-setup.js', import.meta.url));
55
89
  const testConfig = {
56
90
  ...rest.test,
57
91
  globalSetup: normalizeGlobalSetup(rest.test?.globalSetup, globalSetupFilePath)
@@ -59,7 +93,7 @@ export function defineConfig(options: TestingDefineConfig) {
59
93
 
60
94
  // Cast to any to work around Vite 8 type conflicts in monorepo environments
61
95
  // where multiple Vite versions exist in node_modules
62
- // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Type conflict with Vite 8 in monorepo
96
+
63
97
  const vitestConfig = defineVitestConfig({
64
98
  ...rest,
65
99
  root,
@@ -87,9 +121,16 @@ export function defineConfig(options: TestingDefineConfig) {
87
121
  })
88
122
  );
89
123
 
124
+ const testLogger = createTestLogger();
125
+
90
126
  return async ({ mode: viteMode, command }: { mode: string; command: 'build' | 'serve' }) => {
91
127
  const astroConfigFactory = await astroConfigFactoryPromise;
128
+ const config = await astroConfigFactory({ mode: viteMode, command });
129
+
130
+ // Inject the logger — this overrides any logger Astro may have set,
131
+ // which is intentional since we only filter benign test-context noise.
132
+ config.customLogger = testLogger;
92
133
 
93
- return astroConfigFactory({ mode: viteMode, command });
134
+ return config;
94
135
  };
95
136
  }
@@ -3,7 +3,50 @@ import {
3
3
  startTestingRendererDaemon
4
4
  } from '../testing/renderer-daemon.ts';
5
5
 
6
+ /**
7
+ * Patterns for warnings that are always benign in the test context and should
8
+ * be silenced so they don't pollute test output.
9
+ *
10
+ * - "Missing pages directory" — Astro emits this when the project root has no
11
+ * src/pages directory. Component tests never have pages.
12
+ * - "points to missing source files" — Sourcemap gaps in the `entities` package;
13
+ * a third-party packaging issue, not actionable.
14
+ */
15
+ const SUPPRESSED_WARNING_PATTERNS = [
16
+ 'Missing pages directory',
17
+ 'points to missing source files',
18
+ 'Failed to load source map for'
19
+ ];
20
+
21
+ function shouldSuppress(chunk: Buffer | string): boolean {
22
+ const msg = Buffer.isBuffer(chunk) ? chunk.toString('utf8') : String(chunk);
23
+
24
+ return SUPPRESSED_WARNING_PATTERNS.some((pattern) => msg.includes(pattern));
25
+ }
26
+
6
27
  export default async function globalSetup() {
28
+ // Intercept stderr before starting the daemon so that Astro's own logger
29
+ // (which bypasses Vite's customLogger) doesn't leak benign noise into output.
30
+ const originalWrite = process.stderr.write.bind(process.stderr);
31
+
32
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
33
+ (process.stderr as any).write = function (
34
+ chunk: Buffer | string,
35
+ encodingOrCb?: BufferEncoding | ((err?: Error | null) => void),
36
+ cb?: (err?: Error | null) => void
37
+ ): boolean {
38
+ if (shouldSuppress(chunk)) {
39
+ const done = typeof encodingOrCb === 'function' ? encodingOrCb : cb;
40
+
41
+ done?.();
42
+
43
+ return true;
44
+ }
45
+
46
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
47
+ return (originalWrite as any)(chunk, encodingOrCb, cb);
48
+ };
49
+
7
50
  const daemon = await startTestingRendererDaemon();
8
51
 
9
52
  // Workers discover the shared renderer via env instead of creating their own SSR stack.
@@ -12,5 +55,7 @@ export default async function globalSetup() {
12
55
  return async () => {
13
56
  await daemon.close();
14
57
  delete process.env[TESTING_RENDERER_DAEMON_URL_ENV];
58
+ // Restore stderr so post-teardown output is unaffected.
59
+ process.stderr.write = originalWrite;
15
60
  };
16
61
  }