@storybook-astro/framework 1.4.0 → 1.5.0-canary.2

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 (33) hide show
  1. package/dist/chunk-6RIGYMZP.js +144 -0
  2. package/dist/chunk-6RIGYMZP.js.map +1 -0
  3. package/dist/{chunk-VZXGPM6P.js → chunk-7UF6LK4Z.js} +62 -14
  4. package/dist/{chunk-VZXGPM6P.js.map → chunk-7UF6LK4Z.js.map} +1 -1
  5. package/dist/{chunk-BV6V2Z4X.js → chunk-XVVIGJV6.js} +2 -2
  6. package/dist/preset.js +32 -5
  7. package/dist/preset.js.map +1 -1
  8. package/dist/renderer/renderer-dev.js +2 -0
  9. package/dist/renderer/renderer-dev.js.map +1 -1
  10. package/dist/renderer/renderer-server.js +3 -1
  11. package/dist/renderer/renderer-server.js.map +1 -1
  12. package/dist/renderer/renderer-static.js +2 -0
  13. package/dist/renderer/renderer-static.js.map +1 -1
  14. package/dist/testing.js +3 -3
  15. package/dist/{viteStorybookAstroMiddlewarePlugin-246I5D3Y.js → viteStorybookAstroMiddlewarePlugin-LUMKF7NG.js} +2 -2
  16. package/dist/vitest/global-setup.js +2 -2
  17. package/dist/vitest/index.js +1 -1
  18. package/package.json +5 -2
  19. package/src/loadUserAstroConfig.test.ts +149 -0
  20. package/src/loadUserAstroConfig.ts +126 -25
  21. package/src/preset.ts +45 -2
  22. package/src/renderer/renderer-dev.ts +2 -0
  23. package/src/renderer/renderer-server.ts +2 -0
  24. package/src/renderer/renderer-static.ts +2 -0
  25. package/src/shim.d.ts +21 -0
  26. package/src/virtual.d.ts +1 -0
  27. package/src/vitePluginAstroComponentMarker.test.ts +231 -0
  28. package/src/vitePluginAstroComponentMarker.ts +173 -51
  29. package/src/viteStorybookAstroRendererPlugin.ts +2 -1
  30. package/dist/chunk-E4LB75JN.js +0 -89
  31. package/dist/chunk-E4LB75JN.js.map +0 -1
  32. /package/dist/{chunk-BV6V2Z4X.js.map → chunk-XVVIGJV6.js.map} +0 -0
  33. /package/dist/{viteStorybookAstroMiddlewarePlugin-246I5D3Y.js.map → viteStorybookAstroMiddlewarePlugin-LUMKF7NG.js.map} +0 -0
@@ -0,0 +1,231 @@
1
+ import { mkdtempSync, mkdirSync, rmSync, writeFileSync } from 'node:fs';
2
+ import { tmpdir } from 'node:os';
3
+ import { join } from 'node:path';
4
+ import { afterAll, describe, expect, test } from 'vitest';
5
+ import {
6
+ extractAstroImportSpecifiers,
7
+ vitePluginAstroComponentMarker
8
+ } from './vitePluginAstroComponentMarker.ts';
9
+
10
+ const ASTRO6_CLIENT_STUB = `
11
+ export default function() {
12
+ throw new Error('Astro components cannot be used in the browser');
13
+ }
14
+ `;
15
+
16
+ const tempDir = mkdtempSync(join(tmpdir(), 'astro-marker-test-'));
17
+
18
+ afterAll(() => {
19
+ rmSync(tempDir, { recursive: true, force: true });
20
+ });
21
+
22
+ function writeAstroFile(relativePath: string, source: string): string {
23
+ const filePath = join(tempDir, relativePath);
24
+
25
+ mkdirSync(join(filePath, '..'), { recursive: true });
26
+ writeFileSync(filePath, source);
27
+
28
+ return filePath;
29
+ }
30
+
31
+ type TransformablePlugin = {
32
+ configResolved: (config: { command: string }) => void;
33
+ transform: (code: string, id: string) => { code: string } | null;
34
+ };
35
+
36
+ function createPlugin(command: 'serve' | 'build' = 'serve') {
37
+ const plugin = vitePluginAstroComponentMarker() as unknown as TransformablePlugin;
38
+
39
+ plugin.configResolved({ command });
40
+
41
+ return plugin;
42
+ }
43
+
44
+ describe('vitePluginAstroComponentMarker transform', () => {
45
+ test('ignores non-astro modules and non-stub code', () => {
46
+ const plugin = createPlugin();
47
+
48
+ expect(plugin.transform(ASTRO6_CLIENT_STUB, '/some/module.ts')).toBeNull();
49
+ expect(plugin.transform('export default {};', '/some/Component.astro')).toBeNull();
50
+ });
51
+
52
+ test('replaces the stub with a marked component factory', () => {
53
+ const filePath = writeAstroFile('Plain.astro', '<div>Hello</div>');
54
+ const plugin = createPlugin();
55
+ const result = plugin.transform(ASTRO6_CLIENT_STUB, filePath);
56
+
57
+ expect(result?.code).toContain('isAstroComponentFactory = true');
58
+ expect(result?.code).toContain(JSON.stringify(filePath));
59
+ });
60
+
61
+ test('inlines CSS for own <style> blocks in dev mode (hybrid approach)', () => {
62
+ const filePath = writeAstroFile(
63
+ 'Styled.astro',
64
+ '<div class="a">Hi</div>\n<style>.a { color: red; }</style>'
65
+ );
66
+ const plugin = createPlugin();
67
+ const result = plugin.transform(ASTRO6_CLIENT_STUB, filePath);
68
+
69
+ // Dev mode uses inline CSS instead of sub-module imports to avoid Astro cache issues
70
+ expect(result?.code).toContain('.a { color: red; }');
71
+ expect(result?.code).toContain('data-astro-dev');
72
+ });
73
+
74
+ test('unwraps :global() selectors so the CSS is valid in the browser', () => {
75
+ const filePath = writeAstroFile(
76
+ 'Global.astro',
77
+ '<div class="wrap"><slot /></div>\n<style>.wrap > :global(img) { width: 100%; }</style>'
78
+ );
79
+ const plugin = createPlugin();
80
+ const result = plugin.transform(ASTRO6_CLIENT_STUB, filePath);
81
+
82
+ expect(result?.code).toContain('.wrap > img { width: 100%; }');
83
+ expect(result?.code).not.toContain(':global(');
84
+ });
85
+
86
+ test('skips preprocessed <style lang="..."> blocks with a console warning in dev mode', () => {
87
+ const filePath = writeAstroFile(
88
+ 'Scss.astro',
89
+ '<div class="a">Hi</div>\n<style lang="scss">.a { .b { color: red; } }</style>'
90
+ );
91
+ const plugin = createPlugin();
92
+ const result = plugin.transform(ASTRO6_CLIENT_STUB, filePath);
93
+
94
+ // The raw SCSS source must not be injected as a stylesheet.
95
+ expect(result?.code).not.toContain('document.createElement');
96
+ expect(result?.code).toContain('console.warn');
97
+ expect(result?.code).toContain('scss');
98
+ });
99
+
100
+ test('dedupes injected styles so repeated module evaluation does not pile up', () => {
101
+ const filePath = writeAstroFile(
102
+ 'Dedupe.astro',
103
+ '<div class="a">Hi</div>\n<style>.a { color: red; }</style>'
104
+ );
105
+ const plugin = createPlugin();
106
+ const result = plugin.transform(ASTRO6_CLIENT_STUB, filePath);
107
+
108
+ // The injection snippet bails out if a style with the same marker already exists.
109
+ expect(result?.code).toContain("getAttribute");
110
+ expect(result?.code).toContain('data-astro-dev');
111
+ });
112
+
113
+ test('re-imports child .astro components so their scoped styles load in dev mode', () => {
114
+ const filePath = writeAstroFile(
115
+ 'Parent.astro',
116
+ [
117
+ '---',
118
+ "import Child from './Child.astro';",
119
+ "import Aliased from '@components/Other.astro';",
120
+ '---',
121
+ '<div class="parent"><Child /><Aliased /></div>',
122
+ '<style>.parent { padding: 16px; }</style>'
123
+ ].join('\n')
124
+ );
125
+ const plugin = createPlugin();
126
+ const result = plugin.transform(ASTRO6_CLIENT_STUB, filePath);
127
+
128
+ expect(result?.code).toContain(`import "./Child.astro";`);
129
+ expect(result?.code).toContain(`import "@components/Other.astro";`);
130
+ });
131
+
132
+ test('inlines CSS from the component and its children in build mode', () => {
133
+ writeAstroFile(
134
+ 'build/Child.astro',
135
+ '<div class="child">Hello</div>\n<style>.child { color: red; }</style>'
136
+ );
137
+ const parentPath = writeAstroFile(
138
+ 'build/Parent.astro',
139
+ [
140
+ '---',
141
+ "import Child from './Child.astro';",
142
+ '---',
143
+ '<div class="parent"><Child /></div>',
144
+ '<style>.parent { padding: 16px; }</style>'
145
+ ].join('\n')
146
+ );
147
+ const plugin = createPlugin('build');
148
+ const result = plugin.transform(ASTRO6_CLIENT_STUB, parentPath);
149
+
150
+ expect(result?.code).toContain('.parent { padding: 16px; }');
151
+ expect(result?.code).toContain('.child { color: red; }');
152
+ });
153
+
154
+ test('unwraps :global() selectors in build mode too', () => {
155
+ const filePath = writeAstroFile(
156
+ 'build/Global.astro',
157
+ '<div class="wrap"><slot /></div>\n<style>.wrap :global(img) { width: 100%; }</style>'
158
+ );
159
+ const plugin = createPlugin('build');
160
+ const result = plugin.transform(ASTRO6_CLIENT_STUB, filePath);
161
+
162
+ expect(result?.code).toContain('.wrap img { width: 100%; }');
163
+ expect(result?.code).not.toContain(':global(');
164
+ });
165
+
166
+ test('handles circular child imports in build mode without recursing forever', () => {
167
+ const aPath = writeAstroFile(
168
+ 'cycle/A.astro',
169
+ "---\nimport B from './B.astro';\n---\n<B />\n<style>.a { color: blue; }</style>"
170
+ );
171
+
172
+ writeAstroFile(
173
+ 'cycle/B.astro',
174
+ "---\nimport A from './A.astro';\n---\n<A />\n<style>.b { color: green; }</style>"
175
+ );
176
+
177
+ const plugin = createPlugin('build');
178
+ const result = plugin.transform(ASTRO6_CLIENT_STUB, aPath);
179
+
180
+ expect(result?.code).toContain('.a { color: blue; }');
181
+ expect(result?.code).toContain('.b { color: green; }');
182
+ });
183
+ });
184
+
185
+ describe('extractAstroImportSpecifiers', () => {
186
+ test('finds default, side-effect, and dynamic .astro imports in the frontmatter', () => {
187
+ const source = [
188
+ '---',
189
+ "import Child from './Child.astro';",
190
+ "import './SideEffect.astro';",
191
+ "const Lazy = await import('./Lazy.astro');",
192
+ "import { helper } from './utils.ts';",
193
+ '---',
194
+ '<div />'
195
+ ].join('\n');
196
+
197
+ expect(extractAstroImportSpecifiers(source)).toEqual([
198
+ './Child.astro',
199
+ './SideEffect.astro',
200
+ './Lazy.astro'
201
+ ]);
202
+ });
203
+
204
+ test('ignores commented-out imports', () => {
205
+ const source = [
206
+ '---',
207
+ "// import Old from './Old.astro';",
208
+ "/* import Older from './Older.astro'; */",
209
+ "import Current from './Current.astro';",
210
+ '---',
211
+ '<div />'
212
+ ].join('\n');
213
+
214
+ expect(extractAstroImportSpecifiers(source)).toEqual(['./Current.astro']);
215
+ });
216
+
217
+ test('returns nothing for components without frontmatter', () => {
218
+ expect(extractAstroImportSpecifiers('<div>Hello</div>')).toEqual([]);
219
+ });
220
+
221
+ test('deduplicates repeated specifiers', () => {
222
+ const source = [
223
+ '---',
224
+ "import A from './Child.astro';",
225
+ "const B = await import('./Child.astro');",
226
+ '---'
227
+ ].join('\n');
228
+
229
+ expect(extractAstroImportSpecifiers(source)).toEqual(['./Child.astro']);
230
+ });
231
+ });
@@ -1,4 +1,5 @@
1
1
  import { readFileSync } from 'node:fs';
2
+ import { dirname, resolve } from 'node:path';
2
3
  import type { PluginOption } from 'vite';
3
4
 
4
5
  /**
@@ -37,13 +38,14 @@ export function vitePluginAstroComponentMarker(): PluginOption {
37
38
 
38
39
  const moduleId = id;
39
40
 
40
- // In dev mode, import style sub-modules via Astro's Vite plugin (which has
41
- // compile metadata cached from the SSR transform).
42
- // In build mode, Astro's compile metadata cache is not populated for client-side
43
- // transforms, so sub-module imports would fail. Extract raw CSS instead.
41
+ // In Storybook's Vite 6 setup with separate client/SSR environments, Astro's
42
+ // CSS cache isn't populated for client-side transforms. CSS sub-module imports
43
+ // fail with "No Astro CSS at index N", so we inline the CSS directly.
44
+ // However, we still import child .astro components to bring them into the module
45
+ // graph so the plugin processes them (fix for issue #114).
44
46
  const styleCode = isBuild
45
47
  ? generateInlineStyles(moduleId)
46
- : generateStyleImports(moduleId);
48
+ : generateHybridStyles(moduleId);
47
49
 
48
50
  return {
49
51
  code: `
@@ -62,55 +64,178 @@ export default __astro_component;
62
64
  }
63
65
 
64
66
  /**
65
- * Reads the original .astro source file and generates import statements
66
- * for each <style> block, using the Astro Vite plugin's sub-module convention.
67
+ * Hybrid approach for dev mode: inline CSS for the current component (to avoid
68
+ * Astro's cache issues) but import child .astro components (to bring them into
69
+ * the module graph for processing). This preserves the fix for issue #114 while
70
+ * avoiding "No Astro CSS at index N" errors.
71
+ *
72
+ * Two caveats follow from inlining raw <style> source instead of routing it
73
+ * through Astro's compiler:
74
+ * - Styles are injected globally, not scoped to the component. Plain class
75
+ * selectors still match the SSR-rendered HTML, but there is no scope isolation
76
+ * between components in dev.
77
+ * - `:global(...)` wrappers are unwrapped (the browser can't parse them), and
78
+ * preprocessed blocks (`<style lang="scss">` etc.) are skipped with a warning
79
+ * since the preprocessor isn't reachable here.
67
80
  */
68
- function generateStyleImports(filePath: string): string {
81
+ function generateHybridStyles(filePath: string): string {
69
82
  try {
70
83
  const source = readFileSync(filePath, 'utf-8');
71
- const styleCount = countStyleBlocks(source);
72
84
 
73
- return Array.from({ length: styleCount }, (_, i) =>
74
- `import ${JSON.stringify(`${filePath}?astro&type=style&index=${i}&lang.css`)};`
75
- ).join('\n');
85
+ // Inline this component's own CSS (no recursion into children).
86
+ const inlinedCss = extractStyleBlocksWithLang(source).map((block, i) => {
87
+ if (block.lang && block.lang !== 'css') {
88
+ return warnUnsupportedStyleLang(filePath, block.lang);
89
+ }
90
+
91
+ return styleInjectionSnippet(
92
+ 'data-astro-dev',
93
+ `${filePath}:${i}`,
94
+ unwrapGlobalSelectors(block.css)
95
+ );
96
+ }).join('\n');
97
+
98
+ // Import child .astro components so they enter the module graph
99
+ const childImports = extractAstroImportSpecifiers(source).map(
100
+ (specifier) => `import ${JSON.stringify(specifier)};`
101
+ );
102
+
103
+ return [inlinedCss, ...childImports].filter(Boolean).join('\n');
76
104
  } catch {
77
105
  return '';
78
106
  }
79
107
  }
80
108
 
109
+ /**
110
+ * Astro's `:global(...)` wrapper marks a selector as un-scoped. Dev-mode styles
111
+ * are already injected globally, so the wrapper carries no meaning here — and the
112
+ * browser treats `:global()` as an invalid pseudo-class and drops the whole rule.
113
+ * Unwrap it to its inner selector so the rule applies.
114
+ */
115
+ function unwrapGlobalSelectors(css: string): string {
116
+ return css.replace(/:global\(\s*([^)]*?)\s*\)/g, '$1');
117
+ }
118
+
119
+ /**
120
+ * Builds a snippet that warns about a preprocessed <style> block we can't inline.
121
+ * Astro's compiler (which would turn scss/less/etc. into CSS) isn't reachable
122
+ * from this client-side transform, and shipping the raw source would break the
123
+ * browser's CSS parser, so we surface a clear console warning instead.
124
+ */
125
+ function warnUnsupportedStyleLang(filePath: string, lang: string): string {
126
+ const message =
127
+ `[storybook-astro] Skipping <style lang="${lang}"> in ${filePath}: ` +
128
+ `preprocessed styles are not supported in Storybook dev mode.`;
129
+
130
+ return `
131
+ (function() {
132
+ if (typeof console !== 'undefined') { console.warn(${JSON.stringify(message)}); }
133
+ })();`;
134
+ }
135
+
136
+ /**
137
+ * Builds a self-executing snippet that injects a CSS string into <head> as a
138
+ * <style> tag, tagged with a marker attribute. The marker also dedupes
139
+ * re-injection (e.g. when a module re-evaluates after HMR) so styles don't
140
+ * accumulate in the document.
141
+ */
142
+ function styleInjectionSnippet(markerAttr: string, markerValue: string, css: string): string {
143
+ const attr = JSON.stringify(markerAttr);
144
+ const value = JSON.stringify(markerValue);
145
+
146
+ return `
147
+ (function() {
148
+ if (typeof document === 'undefined') { return; }
149
+ const tagged = document.head.querySelectorAll('style[' + ${attr} + ']');
150
+ for (const node of tagged) {
151
+ if (node.getAttribute(${attr}) === ${value}) { return; }
152
+ }
153
+ const style = document.createElement('style');
154
+ style.setAttribute(${attr}, ${value});
155
+ style.textContent = ${JSON.stringify(css)};
156
+ document.head.appendChild(style);
157
+ })();`;
158
+ }
159
+
160
+ /**
161
+ * Extracts import specifiers ending in `.astro` from a component's frontmatter.
162
+ * Comments are stripped first so commented-out imports don't resurface as
163
+ * broken module requests in the browser.
164
+ */
165
+ export function extractAstroImportSpecifiers(source: string): string[] {
166
+ const frontmatterMatch = source.match(/^---([\s\S]*?)---/m);
167
+
168
+ if (!frontmatterMatch) {return [];}
169
+
170
+ const frontmatter = frontmatterMatch[1]
171
+ .replace(/\/\*[\s\S]*?\*\//g, '')
172
+ .replace(/\/\/[^\n]*/g, '');
173
+
174
+ // Matches static imports (`import Child from './Child.astro'`), side-effect
175
+ // imports (`import './Child.astro'`), and dynamic imports (`import('./Child.astro')`).
176
+ const importPattern = /(?:from|import)\s*\(?\s*['"]([^'"]+\.astro)['"]/g;
177
+ const specifiers = new Set<string>();
178
+ let match;
179
+
180
+ while ((match = importPattern.exec(frontmatter)) !== null) {
181
+ specifiers.add(match[1]);
182
+ }
183
+
184
+ return [...specifiers];
185
+ }
186
+
81
187
  /**
82
188
  * Reads the original .astro source file and generates a JS snippet that injects
83
- * the raw CSS from each <style> block into the document. Used during builds where
189
+ * the raw CSS from each <style> block into the document, recursing into child
190
+ * .astro components imported via relative paths. Used during builds where
84
191
  * Astro's compile metadata cache is unavailable.
85
192
  *
86
- * The CSS is unscoped (no Astro scoping transforms), which is acceptable because
87
- * Astro components show a fallback message in static builds.
193
+ * The CSS is unscoped (no Astro scoping transforms), so `:global(...)` wrappers
194
+ * are unwrapped here too without that the browser drops the whole rule (e.g.
195
+ * PageCard's `.page-card__image-wrapper :global(img)` sizing).
88
196
  */
89
197
  function generateInlineStyles(filePath: string): string {
90
- try {
91
- const source = readFileSync(filePath, 'utf-8');
92
- const cssBlocks = extractStyleBlocks(source);
198
+ const cssBlocks = collectStyleBlocks(filePath, new Set());
93
199
 
94
- if (cssBlocks.length === 0) {return '';}
200
+ if (cssBlocks.length === 0) {return '';}
95
201
 
96
- // Create a side-effect that injects styles into the document
97
- return cssBlocks.map((css, i) => {
98
- const escaped = JSON.stringify(css);
202
+ // Create a side-effect that injects styles into the document
203
+ return cssBlocks
204
+ .map(({ file, css }, i) =>
205
+ styleInjectionSnippet('data-astro-build', `${file}:${i}`, unwrapGlobalSelectors(css))
206
+ )
207
+ .join('\n');
208
+ }
99
209
 
100
-
101
- return `
102
- (function() {
103
- if (typeof document !== 'undefined') {
104
- const style = document.createElement('style');
105
- style.setAttribute('data-astro-build', ${JSON.stringify(filePath + ':' + i)});
106
- style.textContent = ${escaped};
107
- document.head.appendChild(style);
108
- }
109
- })();`;
110
- }).join('\n');
210
+ /**
211
+ * Collects <style> block contents from a component and its child .astro imports.
212
+ * Only relative specifiers are followed (aliases and packages can't be resolved
213
+ * from disk here). The visited set guards against import cycles.
214
+ */
215
+ function collectStyleBlocks(
216
+ filePath: string,
217
+ visited: Set<string>
218
+ ): Array<{ file: string; css: string }> {
219
+ if (visited.has(filePath)) {return [];}
220
+ visited.add(filePath);
221
+
222
+ let source: string;
223
+
224
+ try {
225
+ source = readFileSync(filePath, 'utf-8');
111
226
  } catch {
112
- return '';
227
+ return [];
228
+ }
229
+
230
+ const blocks = extractStyleBlocks(source).map((css) => ({ file: filePath, css }));
231
+
232
+ for (const specifier of extractAstroImportSpecifiers(source)) {
233
+ if (!specifier.startsWith('.')) {continue;}
234
+
235
+ blocks.push(...collectStyleBlocks(resolve(dirname(filePath), specifier), visited));
113
236
  }
237
+
238
+ return blocks;
114
239
  }
115
240
 
116
241
  /**
@@ -118,28 +243,25 @@ return `
118
243
  * Strips frontmatter before parsing.
119
244
  */
120
245
  function extractStyleBlocks(source: string): string[] {
246
+ return extractStyleBlocksWithLang(source).map((block) => block.css);
247
+ }
248
+
249
+ /**
250
+ * Like {@link extractStyleBlocks}, but also reports each block's `lang` attribute
251
+ * (e.g. "scss") so callers can tell preprocessed styles apart from plain CSS.
252
+ * Returns `null` for the lang when no attribute is present.
253
+ */
254
+ function extractStyleBlocksWithLang(source: string): Array<{ css: string; lang: string | null }> {
121
255
  const withoutFrontmatter = source.replace(/^---[\s\S]*?---/m, '');
122
- const blocks: string[] = [];
123
- const regex = /<style(?:\s[^>]*)?>([\s\S]*?)<\/style>/g;
256
+ const blocks: Array<{ css: string; lang: string | null }> = [];
257
+ const regex = /<style((?:\s[^>]*)?)>([\s\S]*?)<\/style>/g;
124
258
  let match;
125
259
 
126
260
  while ((match = regex.exec(withoutFrontmatter)) !== null) {
127
- blocks.push(match[1].trim());
261
+ const langMatch = match[1].match(/\blang\s*=\s*['"]?([\w-]+)/);
262
+
263
+ blocks.push({ css: match[2].trim(), lang: langMatch ? langMatch[1] : null });
128
264
  }
129
265
 
130
266
  return blocks;
131
267
  }
132
-
133
- /**
134
- * Counts the number of top-level <style> blocks in an Astro component's source.
135
- * Only counts opening tags that are NOT inside the frontmatter fence (---).
136
- */
137
- function countStyleBlocks(source: string): number {
138
- // Strip frontmatter
139
- const withoutFrontmatter = source.replace(/^---[\s\S]*?---/m, '');
140
- // Match <style> opening tags (with optional attributes)
141
- const matches = withoutFrontmatter.match(/<style(\s|>)/g);
142
-
143
-
144
- return matches ? matches.length : 0;
145
- }
@@ -41,7 +41,8 @@ export function viteStorybookAstroRendererPlugin(options: {
41
41
  )});`,
42
42
  'export const render = renderer.render;',
43
43
  'export const init = renderer.init;',
44
- 'export const applyStyles = renderer.applyStyles;'
44
+ 'export const applyStyles = renderer.applyStyles;',
45
+ 'export const isStaticMode = false;'
45
46
  ].join('\n');
46
47
  }
47
48
  });
@@ -1,89 +0,0 @@
1
- // src/vitePluginAstroComponentMarker.ts
2
- import { readFileSync } from "fs";
3
- function vitePluginAstroComponentMarker() {
4
- let isBuild = false;
5
- return {
6
- name: "storybook-astro-component-marker",
7
- enforce: "post",
8
- configResolved(config) {
9
- isBuild = config.command === "build";
10
- },
11
- transform(code, id) {
12
- if (!id.endsWith(".astro")) {
13
- return null;
14
- }
15
- if (!code.includes("Astro components cannot be used in the browser")) {
16
- return null;
17
- }
18
- const moduleId = id;
19
- const styleCode = isBuild ? generateInlineStyles(moduleId) : generateStyleImports(moduleId);
20
- return {
21
- code: `
22
- ${styleCode}
23
- const __astro_component = () => {
24
- throw new Error('Astro components are rendered server-side by Storybook.');
25
- };
26
- __astro_component.isAstroComponentFactory = true;
27
- __astro_component.moduleId = ${JSON.stringify(moduleId)};
28
- export default __astro_component;
29
- `,
30
- map: null
31
- };
32
- }
33
- };
34
- }
35
- function generateStyleImports(filePath) {
36
- try {
37
- const source = readFileSync(filePath, "utf-8");
38
- const styleCount = countStyleBlocks(source);
39
- return Array.from(
40
- { length: styleCount },
41
- (_, i) => `import ${JSON.stringify(`${filePath}?astro&type=style&index=${i}&lang.css`)};`
42
- ).join("\n");
43
- } catch {
44
- return "";
45
- }
46
- }
47
- function generateInlineStyles(filePath) {
48
- try {
49
- const source = readFileSync(filePath, "utf-8");
50
- const cssBlocks = extractStyleBlocks(source);
51
- if (cssBlocks.length === 0) {
52
- return "";
53
- }
54
- return cssBlocks.map((css, i) => {
55
- const escaped = JSON.stringify(css);
56
- return `
57
- (function() {
58
- if (typeof document !== 'undefined') {
59
- const style = document.createElement('style');
60
- style.setAttribute('data-astro-build', ${JSON.stringify(filePath + ":" + i)});
61
- style.textContent = ${escaped};
62
- document.head.appendChild(style);
63
- }
64
- })();`;
65
- }).join("\n");
66
- } catch {
67
- return "";
68
- }
69
- }
70
- function extractStyleBlocks(source) {
71
- const withoutFrontmatter = source.replace(/^---[\s\S]*?---/m, "");
72
- const blocks = [];
73
- const regex = /<style(?:\s[^>]*)?>([\s\S]*?)<\/style>/g;
74
- let match;
75
- while ((match = regex.exec(withoutFrontmatter)) !== null) {
76
- blocks.push(match[1].trim());
77
- }
78
- return blocks;
79
- }
80
- function countStyleBlocks(source) {
81
- const withoutFrontmatter = source.replace(/^---[\s\S]*?---/m, "");
82
- const matches = withoutFrontmatter.match(/<style(\s|>)/g);
83
- return matches ? matches.length : 0;
84
- }
85
-
86
- export {
87
- vitePluginAstroComponentMarker
88
- };
89
- //# sourceMappingURL=chunk-E4LB75JN.js.map
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/vitePluginAstroComponentMarker.ts"],"sourcesContent":["import { readFileSync } from 'node:fs';\nimport type { PluginOption } from 'vite';\n\n/**\n * Vite plugin that patches Astro 6's client-side .astro file transforms for Storybook.\n *\n * In Astro 6, the client-side transform of .astro files produces a stub function that\n * throws \"Astro components cannot be used in the browser\" without setting the\n * `isAstroComponentFactory` marker. Storybook's renderer relies on this marker to detect\n * Astro components and route them to server-side rendering via the Container API.\n *\n * This plugin also preserves the component's scoped CSS by importing the style sub-modules\n * that the Astro Vite plugin exposes. Without this, the client-side stub would strip all\n * CSS since Astro 6 no longer includes style imports in client-side .astro transforms.\n *\n * During builds, Astro's compile metadata cache is not populated for client-side transforms,\n * so style sub-module imports would fail. Instead, raw CSS is extracted directly from the\n * .astro source and inlined.\n */\nexport function vitePluginAstroComponentMarker(): PluginOption {\n let isBuild = false;\n\n return {\n name: 'storybook-astro-component-marker',\n enforce: 'post',\n\n configResolved(config) {\n isBuild = config.command === 'build';\n },\n\n transform(code: string, id: string) {\n // Only process main .astro modules (not sub-modules like ?astro&type=style)\n if (!id.endsWith('.astro')) {return null;}\n\n // Detect the Astro 6 client-side stub pattern\n if (!code.includes('Astro components cannot be used in the browser')) {return null;}\n\n const moduleId = id;\n\n // In dev mode, import style sub-modules via Astro's Vite plugin (which has\n // compile metadata cached from the SSR transform).\n // In build mode, Astro's compile metadata cache is not populated for client-side\n // transforms, so sub-module imports would fail. Extract raw CSS instead.\n const styleCode = isBuild\n ? generateInlineStyles(moduleId)\n : generateStyleImports(moduleId);\n\n return {\n code: `\n${styleCode}\nconst __astro_component = () => {\n throw new Error('Astro components are rendered server-side by Storybook.');\n};\n__astro_component.isAstroComponentFactory = true;\n__astro_component.moduleId = ${JSON.stringify(moduleId)};\nexport default __astro_component;\n`,\n map: null,\n };\n },\n };\n}\n\n/**\n * Reads the original .astro source file and generates import statements\n * for each <style> block, using the Astro Vite plugin's sub-module convention.\n */\nfunction generateStyleImports(filePath: string): string {\n try {\n const source = readFileSync(filePath, 'utf-8');\n const styleCount = countStyleBlocks(source);\n\n return Array.from({ length: styleCount }, (_, i) =>\n `import ${JSON.stringify(`${filePath}?astro&type=style&index=${i}&lang.css`)};`\n ).join('\\n');\n } catch {\n return '';\n }\n}\n\n/**\n * Reads the original .astro source file and generates a JS snippet that injects\n * the raw CSS from each <style> block into the document. Used during builds where\n * Astro's compile metadata cache is unavailable.\n *\n * The CSS is unscoped (no Astro scoping transforms), which is acceptable because\n * Astro components show a fallback message in static builds.\n */\nfunction generateInlineStyles(filePath: string): string {\n try {\n const source = readFileSync(filePath, 'utf-8');\n const cssBlocks = extractStyleBlocks(source);\n\n if (cssBlocks.length === 0) {return '';}\n\n // Create a side-effect that injects styles into the document\n return cssBlocks.map((css, i) => {\n const escaped = JSON.stringify(css);\n\n \nreturn `\n(function() {\n if (typeof document !== 'undefined') {\n const style = document.createElement('style');\n style.setAttribute('data-astro-build', ${JSON.stringify(filePath + ':' + i)});\n style.textContent = ${escaped};\n document.head.appendChild(style);\n }\n})();`;\n }).join('\\n');\n } catch {\n return '';\n }\n}\n\n/**\n * Extracts the content of all top-level <style> blocks from an Astro component's source.\n * Strips frontmatter before parsing.\n */\nfunction extractStyleBlocks(source: string): string[] {\n const withoutFrontmatter = source.replace(/^---[\\s\\S]*?---/m, '');\n const blocks: string[] = [];\n const regex = /<style(?:\\s[^>]*)?>([\\s\\S]*?)<\\/style>/g;\n let match;\n\n while ((match = regex.exec(withoutFrontmatter)) !== null) {\n blocks.push(match[1].trim());\n }\n\n return blocks;\n}\n\n/**\n * Counts the number of top-level <style> blocks in an Astro component's source.\n * Only counts opening tags that are NOT inside the frontmatter fence (---).\n */\nfunction countStyleBlocks(source: string): number {\n // Strip frontmatter\n const withoutFrontmatter = source.replace(/^---[\\s\\S]*?---/m, '');\n // Match <style> opening tags (with optional attributes)\n const matches = withoutFrontmatter.match(/<style(\\s|>)/g);\n\n \nreturn matches ? matches.length : 0;\n}\n"],"mappings":";AAAA,SAAS,oBAAoB;AAmBtB,SAAS,iCAA+C;AAC7D,MAAI,UAAU;AAEd,SAAO;AAAA,IACL,MAAM;AAAA,IACN,SAAS;AAAA,IAET,eAAe,QAAQ;AACrB,gBAAU,OAAO,YAAY;AAAA,IAC/B;AAAA,IAEA,UAAU,MAAc,IAAY;AAElC,UAAI,CAAC,GAAG,SAAS,QAAQ,GAAG;AAAC,eAAO;AAAA,MAAK;AAGzC,UAAI,CAAC,KAAK,SAAS,gDAAgD,GAAG;AAAC,eAAO;AAAA,MAAK;AAEnF,YAAM,WAAW;AAMjB,YAAM,YAAY,UACd,qBAAqB,QAAQ,IAC7B,qBAAqB,QAAQ;AAEjC,aAAO;AAAA,QACL,MAAM;AAAA,EACZ,SAAS;AAAA;AAAA;AAAA;AAAA;AAAA,+BAKoB,KAAK,UAAU,QAAQ,CAAC;AAAA;AAAA;AAAA,QAG/C,KAAK;AAAA,MACP;AAAA,IACF;AAAA,EACF;AACF;AAMA,SAAS,qBAAqB,UAA0B;AACtD,MAAI;AACF,UAAM,SAAS,aAAa,UAAU,OAAO;AAC7C,UAAM,aAAa,iBAAiB,MAAM;AAE1C,WAAO,MAAM;AAAA,MAAK,EAAE,QAAQ,WAAW;AAAA,MAAG,CAAC,GAAG,MAC5C,UAAU,KAAK,UAAU,GAAG,QAAQ,2BAA2B,CAAC,WAAW,CAAC;AAAA,IAC9E,EAAE,KAAK,IAAI;AAAA,EACb,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAUA,SAAS,qBAAqB,UAA0B;AACtD,MAAI;AACF,UAAM,SAAS,aAAa,UAAU,OAAO;AAC7C,UAAM,YAAY,mBAAmB,MAAM;AAE3C,QAAI,UAAU,WAAW,GAAG;AAAC,aAAO;AAAA,IAAG;AAGvC,WAAO,UAAU,IAAI,CAAC,KAAK,MAAM;AAC/B,YAAM,UAAU,KAAK,UAAU,GAAG;AAGxC,aAAO;AAAA;AAAA;AAAA;AAAA,6CAIsC,KAAK,UAAU,WAAW,MAAM,CAAC,CAAC;AAAA,0BACrD,OAAO;AAAA;AAAA;AAAA;AAAA,IAI7B,CAAC,EAAE,KAAK,IAAI;AAAA,EACd,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAMA,SAAS,mBAAmB,QAA0B;AACpD,QAAM,qBAAqB,OAAO,QAAQ,oBAAoB,EAAE;AAChE,QAAM,SAAmB,CAAC;AAC1B,QAAM,QAAQ;AACd,MAAI;AAEJ,UAAQ,QAAQ,MAAM,KAAK,kBAAkB,OAAO,MAAM;AACxD,WAAO,KAAK,MAAM,CAAC,EAAE,KAAK,CAAC;AAAA,EAC7B;AAEA,SAAO;AACT;AAMA,SAAS,iBAAiB,QAAwB;AAEhD,QAAM,qBAAqB,OAAO,QAAQ,oBAAoB,EAAE;AAEhE,QAAM,UAAU,mBAAmB,MAAM,eAAe;AAG1D,SAAO,UAAU,QAAQ,SAAS;AAClC;","names":[]}