@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.
- package/dist/chunk-6RIGYMZP.js +144 -0
- package/dist/chunk-6RIGYMZP.js.map +1 -0
- package/dist/{chunk-VZXGPM6P.js → chunk-7UF6LK4Z.js} +62 -14
- package/dist/{chunk-VZXGPM6P.js.map → chunk-7UF6LK4Z.js.map} +1 -1
- package/dist/{chunk-BV6V2Z4X.js → chunk-XVVIGJV6.js} +2 -2
- package/dist/preset.js +32 -5
- package/dist/preset.js.map +1 -1
- package/dist/renderer/renderer-dev.js +2 -0
- package/dist/renderer/renderer-dev.js.map +1 -1
- package/dist/renderer/renderer-server.js +3 -1
- package/dist/renderer/renderer-server.js.map +1 -1
- package/dist/renderer/renderer-static.js +2 -0
- package/dist/renderer/renderer-static.js.map +1 -1
- package/dist/testing.js +3 -3
- package/dist/{viteStorybookAstroMiddlewarePlugin-246I5D3Y.js → viteStorybookAstroMiddlewarePlugin-LUMKF7NG.js} +2 -2
- package/dist/vitest/global-setup.js +2 -2
- package/dist/vitest/index.js +1 -1
- package/package.json +5 -2
- package/src/loadUserAstroConfig.test.ts +149 -0
- package/src/loadUserAstroConfig.ts +126 -25
- package/src/preset.ts +45 -2
- package/src/renderer/renderer-dev.ts +2 -0
- package/src/renderer/renderer-server.ts +2 -0
- package/src/renderer/renderer-static.ts +2 -0
- package/src/shim.d.ts +21 -0
- package/src/virtual.d.ts +1 -0
- package/src/vitePluginAstroComponentMarker.test.ts +231 -0
- package/src/vitePluginAstroComponentMarker.ts +173 -51
- package/src/viteStorybookAstroRendererPlugin.ts +2 -1
- package/dist/chunk-E4LB75JN.js +0 -89
- package/dist/chunk-E4LB75JN.js.map +0 -1
- /package/dist/{chunk-BV6V2Z4X.js.map → chunk-XVVIGJV6.js.map} +0 -0
- /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
|
|
41
|
-
//
|
|
42
|
-
//
|
|
43
|
-
//
|
|
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
|
-
:
|
|
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
|
-
*
|
|
66
|
-
*
|
|
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
|
|
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
|
-
|
|
74
|
-
|
|
75
|
-
|
|
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
|
|
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),
|
|
87
|
-
*
|
|
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
|
-
|
|
91
|
-
const source = readFileSync(filePath, 'utf-8');
|
|
92
|
-
const cssBlocks = extractStyleBlocks(source);
|
|
198
|
+
const cssBlocks = collectStyleBlocks(filePath, new Set());
|
|
93
199
|
|
|
94
|
-
|
|
200
|
+
if (cssBlocks.length === 0) {return '';}
|
|
95
201
|
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
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
|
-
|
|
102
|
-
(
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
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[^>]*)
|
|
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
|
-
|
|
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
|
});
|
package/dist/chunk-E4LB75JN.js
DELETED
|
@@ -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":[]}
|
|
File without changes
|