@storybook-astro/framework 1.3.0 → 1.4.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-N3WTUD2A.js → chunk-A4DQ67HA.js} +53 -2
- package/dist/chunk-A4DQ67HA.js.map +1 -0
- package/dist/{chunk-2EABPTOY.js → chunk-BV6V2Z4X.js} +2 -2
- package/dist/{chunk-AYYMNFI6.js → chunk-VZXGPM6P.js} +218 -34
- package/dist/chunk-VZXGPM6P.js.map +1 -0
- package/dist/index.d.ts +1 -1
- package/dist/middleware.js +10 -2
- package/dist/middleware.js.map +1 -1
- package/dist/node/index.d.ts +1 -1
- package/dist/preset.d.ts +1 -1
- package/dist/preset.js +39 -21
- package/dist/preset.js.map +1 -1
- package/dist/testing.js +3 -3
- package/dist/{types-BCpJLSTo.d.ts → types--SvYP5Ri.d.ts} +55 -0
- package/dist/{viteStorybookAstroMiddlewarePlugin-UB6ZLJ4B.js → viteStorybookAstroMiddlewarePlugin-246I5D3Y.js} +2 -2
- package/dist/vitest/global-setup.js +2 -2
- package/package.json +3 -2
- package/src/lib/resolve-aliased-island.test.ts +150 -0
- package/src/lib/resolve-aliased-island.ts +97 -0
- package/src/loadUserAstroConfig.ts +59 -0
- package/src/middleware.ts +15 -0
- package/src/productionRenderRuntime.ts +6 -2
- package/src/storySsrVite.ts +15 -2
- package/src/types.ts +9 -1
- package/src/vitePluginAstro.ts +10 -3
- package/src/vitePluginAstroBuildPrerender.ts +4 -1
- package/src/vitePluginAstroBuildShared.test.ts +25 -6
- package/src/vitePluginAstroBuildShared.ts +25 -12
- package/src/vitePluginAstroFonts.test.ts +153 -0
- package/src/vitePluginAstroFonts.ts +302 -0
- package/src/viteStorybookAstroMiddlewarePlugin.ts +20 -8
- package/dist/chunk-AYYMNFI6.js.map +0 -1
- package/dist/chunk-N3WTUD2A.js.map +0 -1
- package/src/vitePluginAstroFontsFallback.ts +0 -69
- /package/dist/{chunk-2EABPTOY.js.map → chunk-BV6V2Z4X.js.map} +0 -0
- /package/dist/{viteStorybookAstroMiddlewarePlugin-UB6ZLJ4B.js.map → viteStorybookAstroMiddlewarePlugin-246I5D3Y.js.map} +0 -0
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { mkdtemp, rm, writeFile } from 'node:fs/promises';
|
|
1
|
+
import { mkdir, mkdtemp, rm, writeFile } from 'node:fs/promises';
|
|
2
2
|
import { tmpdir } from 'node:os';
|
|
3
3
|
import { join } from 'node:path';
|
|
4
4
|
import { afterEach, beforeEach, describe, expect, test } from 'vitest';
|
|
@@ -22,7 +22,7 @@ describe('collectHydratedComponentPaths', () => {
|
|
|
22
22
|
await writeFile(astroFile, `---\nimport Helper from './Helper.tsx';\n---`);
|
|
23
23
|
await writeFile(namedOnlyTsx, `export const Helper = () => <div />;`);
|
|
24
24
|
|
|
25
|
-
const result = await collectHydratedComponentPaths(astroFile);
|
|
25
|
+
const result = await collectHydratedComponentPaths(astroFile, tmpDir);
|
|
26
26
|
|
|
27
27
|
expect(result).not.toContain(namedOnlyTsx.replace(/\\/g, '/'));
|
|
28
28
|
});
|
|
@@ -34,7 +34,7 @@ describe('collectHydratedComponentPaths', () => {
|
|
|
34
34
|
await writeFile(astroFile, `---\nimport Button from './Button.tsx';\n---`);
|
|
35
35
|
await writeFile(defaultTsx, `export default function Button() { return <div />; }`);
|
|
36
36
|
|
|
37
|
-
const result = await collectHydratedComponentPaths(astroFile);
|
|
37
|
+
const result = await collectHydratedComponentPaths(astroFile, tmpDir);
|
|
38
38
|
|
|
39
39
|
expect(result).toContain(defaultTsx.replace(/\\/g, '/'));
|
|
40
40
|
});
|
|
@@ -48,7 +48,7 @@ describe('collectHydratedComponentPaths', () => {
|
|
|
48
48
|
await writeFile(astroFile, `---\nimport Counter from './Counter.svelte';\n---`);
|
|
49
49
|
await writeFile(svelteFile, `<script>\n let count = 0;\n</script>\n<button>{count}</button>`);
|
|
50
50
|
|
|
51
|
-
const result = await collectHydratedComponentPaths(astroFile);
|
|
51
|
+
const result = await collectHydratedComponentPaths(astroFile, tmpDir);
|
|
52
52
|
|
|
53
53
|
expect(result).toContain(svelteFile.replace(/\\/g, '/'));
|
|
54
54
|
});
|
|
@@ -64,7 +64,7 @@ describe('collectHydratedComponentPaths', () => {
|
|
|
64
64
|
`<script setup>\nconst count = ref(0);\n</script>\n<template><button>{{ count }}</button></template>`
|
|
65
65
|
);
|
|
66
66
|
|
|
67
|
-
const result = await collectHydratedComponentPaths(astroFile);
|
|
67
|
+
const result = await collectHydratedComponentPaths(astroFile, tmpDir);
|
|
68
68
|
|
|
69
69
|
expect(result).toContain(vueFile.replace(/\\/g, '/'));
|
|
70
70
|
});
|
|
@@ -80,8 +80,27 @@ describe('collectHydratedComponentPaths', () => {
|
|
|
80
80
|
|
|
81
81
|
// Don't write missingTsx — resolveLocalImportPath will not find it,
|
|
82
82
|
// so it never reaches hasDefaultExport. Confirm the result is just empty.
|
|
83
|
-
const result = await collectHydratedComponentPaths(astroFile);
|
|
83
|
+
const result = await collectHydratedComponentPaths(astroFile, tmpDir);
|
|
84
84
|
|
|
85
85
|
expect(result).not.toContain(missingTsx.replace(/\\/g, '/'));
|
|
86
86
|
});
|
|
87
|
+
|
|
88
|
+
test('includes a tsconfig-aliased island (the static build fix)', async () => {
|
|
89
|
+
// Islands imported via path aliases never matched the staticModuleMap because
|
|
90
|
+
// readLocalImportSpecifiers filtered them out before they could become Rollup
|
|
91
|
+
// inputs. This test confirms the alias is resolved and included.
|
|
92
|
+
await mkdir(join(tmpDir, 'src', 'components'), { recursive: true });
|
|
93
|
+
const counterFile = join(tmpDir, 'src', 'components', 'Counter.tsx');
|
|
94
|
+
|
|
95
|
+
await writeFile(counterFile, `export default function Counter() { return null; }`);
|
|
96
|
+
await writeFile(join(tmpDir, 'Island.astro'), `---\nimport Counter from '@/components/Counter';\n---\n<Counter client:visible />`);
|
|
97
|
+
await writeFile(
|
|
98
|
+
join(tmpDir, 'tsconfig.json'),
|
|
99
|
+
JSON.stringify({ compilerOptions: { paths: { '@/*': ['src/*'] } } }, null, 2)
|
|
100
|
+
);
|
|
101
|
+
|
|
102
|
+
const result = await collectHydratedComponentPaths(join(tmpDir, 'Island.astro'), tmpDir);
|
|
103
|
+
|
|
104
|
+
expect(result).toContain(counterFile.replace(/\\/g, '/'));
|
|
105
|
+
});
|
|
87
106
|
});
|
|
@@ -3,6 +3,7 @@ import { access, copyFile, mkdir, readFile, readdir, stat } from 'node:fs/promis
|
|
|
3
3
|
import { dirname, resolve } from 'node:path';
|
|
4
4
|
import type { Rollup } from 'vite';
|
|
5
5
|
import type { Integration } from './integrations/index.ts';
|
|
6
|
+
import { resolveAliasedIsland } from './lib/resolve-aliased-island.ts';
|
|
6
7
|
|
|
7
8
|
/** Resolves the shared virtual module ids used by both build pipelines. */
|
|
8
9
|
export function resolveVirtualBuildModuleId(id: string) {
|
|
@@ -77,7 +78,10 @@ export async function emitHydratedComponentEntriesFromAstroFile(options: {
|
|
|
77
78
|
resolveFrom: string;
|
|
78
79
|
componentEntrypointRefs: Map<string, string>;
|
|
79
80
|
}) {
|
|
80
|
-
const hydratedComponentPaths = await collectHydratedComponentPaths(
|
|
81
|
+
const hydratedComponentPaths = await collectHydratedComponentPaths(
|
|
82
|
+
options.astroFilePath,
|
|
83
|
+
options.resolveFrom
|
|
84
|
+
);
|
|
81
85
|
|
|
82
86
|
for (const resolvedImportPath of hydratedComponentPaths) {
|
|
83
87
|
|
|
@@ -96,14 +100,18 @@ export async function emitHydratedComponentEntriesFromAstroFile(options: {
|
|
|
96
100
|
}
|
|
97
101
|
|
|
98
102
|
/** Collects the framework component files one Astro component hydrates in the browser. */
|
|
99
|
-
export async function collectHydratedComponentPaths(astroFilePath: string) {
|
|
103
|
+
export async function collectHydratedComponentPaths(astroFilePath: string, resolveFrom: string) {
|
|
100
104
|
// Only Astro components create islands, so only their framework imports
|
|
101
105
|
// need standalone client chunks in built Storybook output.
|
|
102
|
-
const
|
|
106
|
+
const allImportSpecifiers = await readAllImportSpecifiers(astroFilePath);
|
|
103
107
|
const hydratedComponentPaths: string[] = [];
|
|
104
108
|
|
|
105
|
-
for (const specifier of
|
|
106
|
-
|
|
109
|
+
for (const specifier of allImportSpecifiers) {
|
|
110
|
+
// Resolve the specifier to an on-disk path — relative imports use the
|
|
111
|
+
// importer's directory; aliased imports go through the tsconfig paths map.
|
|
112
|
+
const resolvedImportPath = specifier.startsWith('.')
|
|
113
|
+
? await resolveLocalImportPath(astroFilePath, specifier)
|
|
114
|
+
: await resolveAliasedIsland(specifier, resolveFrom);
|
|
107
115
|
|
|
108
116
|
if (!resolvedImportPath) {
|
|
109
117
|
continue;
|
|
@@ -385,22 +393,27 @@ async function copyLocalRuntimeDependencies(
|
|
|
385
393
|
}
|
|
386
394
|
}
|
|
387
395
|
|
|
388
|
-
|
|
389
|
-
|
|
396
|
+
const IMPORT_RE =
|
|
397
|
+
/(?:import|export)\s+(?:[^'"`]*?\s+from\s+)?['"`]([^'"`]+)['"`]|import\(\s*['"`]([^'"`]+)['"`]\s*\)/g;
|
|
398
|
+
|
|
399
|
+
/** Returns all import specifiers found in the file (relative, aliased, and package). */
|
|
400
|
+
async function readAllImportSpecifiers(filePath: string): Promise<string[]> {
|
|
390
401
|
if (!/\.(astro|[cm]?[jt]sx?|vue|svelte)$/.test(filePath)) {
|
|
391
402
|
return [];
|
|
392
403
|
}
|
|
393
404
|
|
|
394
405
|
const source = await readFile(filePath, 'utf-8');
|
|
395
|
-
const matches = source.matchAll(
|
|
396
|
-
/(?:import|export)\s+(?:[^'"`]*?\s+from\s+)?['"`]([^'"`]+)['"`]|import\(\s*['"`]([^'"`]+)['"`]\s*\)/g
|
|
397
|
-
);
|
|
398
406
|
|
|
399
|
-
return Array.from(
|
|
400
|
-
(specifier): specifier is string => Boolean(specifier)
|
|
407
|
+
return Array.from(source.matchAll(IMPORT_RE), (match) => match[1] ?? match[2]).filter(
|
|
408
|
+
(specifier): specifier is string => Boolean(specifier)
|
|
401
409
|
);
|
|
402
410
|
}
|
|
403
411
|
|
|
412
|
+
/** Reads local (relative) import specifiers from source files that can participate in the SSR runtime. */
|
|
413
|
+
async function readLocalImportSpecifiers(filePath: string) {
|
|
414
|
+
return (await readAllImportSpecifiers(filePath)).filter((s) => s.startsWith('.'));
|
|
415
|
+
}
|
|
416
|
+
|
|
404
417
|
/** Resolves one relative import the same way the project source tree would on disk. */
|
|
405
418
|
async function resolveLocalImportPath(importerPath: string, specifier: string) {
|
|
406
419
|
const basePath = resolve(dirname(importerPath), specifier);
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
import { describe, expect, test } from 'vitest';
|
|
2
|
+
import {
|
|
3
|
+
buildFamilyCss,
|
|
4
|
+
vitePluginAstroFonts,
|
|
5
|
+
type FontFaceData,
|
|
6
|
+
type StorybookFontFamily,
|
|
7
|
+
type StorybookFontProvider
|
|
8
|
+
} from './vitePluginAstroFonts.ts';
|
|
9
|
+
|
|
10
|
+
function makeProvider(faces: FontFaceData[]): StorybookFontProvider {
|
|
11
|
+
return {
|
|
12
|
+
name: 'test',
|
|
13
|
+
resolveFont: () => ({ fonts: faces })
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
describe('buildFamilyCss', () => {
|
|
18
|
+
test('emits @font-face block with src url and font-display: swap by default', () => {
|
|
19
|
+
const family: StorybookFontFamily = {
|
|
20
|
+
name: 'Inter',
|
|
21
|
+
cssVariable: '--font-inter',
|
|
22
|
+
provider: makeProvider([])
|
|
23
|
+
};
|
|
24
|
+
const css = buildFamilyCss(family, [
|
|
25
|
+
{ src: [{ url: 'https://example.com/inter-400.woff2', format: 'woff2' }], weight: 400, style: 'normal' }
|
|
26
|
+
]);
|
|
27
|
+
|
|
28
|
+
expect(css).toContain('@font-face');
|
|
29
|
+
expect(css).toContain('font-family: "Inter"');
|
|
30
|
+
expect(css).toContain('src: url("https://example.com/inter-400.woff2") format("woff2");');
|
|
31
|
+
expect(css).toContain('font-display: swap');
|
|
32
|
+
expect(css).toContain('font-weight: 400');
|
|
33
|
+
expect(css).toContain('font-style: normal');
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
test('emits CSS variable rule binding cssVariable to family name and fallbacks', () => {
|
|
37
|
+
const family: StorybookFontFamily = {
|
|
38
|
+
name: 'Inter',
|
|
39
|
+
cssVariable: '--font-inter',
|
|
40
|
+
provider: makeProvider([]),
|
|
41
|
+
fallbacks: ['system-ui', 'sans-serif']
|
|
42
|
+
};
|
|
43
|
+
const css = buildFamilyCss(family, []);
|
|
44
|
+
|
|
45
|
+
expect(css).toContain(':root { --font-inter: "Inter", system-ui, sans-serif; }');
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
test('handles local() source entries from local font providers', () => {
|
|
49
|
+
const family: StorybookFontFamily = {
|
|
50
|
+
name: 'CustomFont',
|
|
51
|
+
cssVariable: '--font-custom',
|
|
52
|
+
provider: makeProvider([])
|
|
53
|
+
};
|
|
54
|
+
const css = buildFamilyCss(family, [
|
|
55
|
+
{ src: [{ name: 'CustomFont Regular' }, { url: '/fonts/custom.woff2', format: 'woff2' }] }
|
|
56
|
+
]);
|
|
57
|
+
|
|
58
|
+
expect(css).toContain('local("CustomFont Regular")');
|
|
59
|
+
expect(css).toContain('url("/fonts/custom.woff2") format("woff2")');
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
test('emits variable-font weight range when provider returns a tuple', () => {
|
|
63
|
+
const family: StorybookFontFamily = {
|
|
64
|
+
name: 'Inter',
|
|
65
|
+
cssVariable: '--font-inter',
|
|
66
|
+
provider: makeProvider([])
|
|
67
|
+
};
|
|
68
|
+
const css = buildFamilyCss(family, [{ src: [{ url: '/x.woff2' }], weight: [100, 900] }]);
|
|
69
|
+
|
|
70
|
+
expect(css).toContain('font-weight: 100 900');
|
|
71
|
+
});
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
describe('vitePluginAstroFonts virtual modules', () => {
|
|
75
|
+
test('resolveId maps both virtual ids and the bare astro/assets/fonts/runtime import', async () => {
|
|
76
|
+
const plugin = vitePluginAstroFonts();
|
|
77
|
+
const resolveId = plugin.resolveId as (this: unknown, id: string) => string | undefined;
|
|
78
|
+
|
|
79
|
+
expect(resolveId.call({}, 'virtual:astro:assets/fonts/internal')).toBe(
|
|
80
|
+
'\0virtual:astro:assets/fonts/internal'
|
|
81
|
+
);
|
|
82
|
+
expect(resolveId.call({}, 'virtual:astro:assets/fonts/runtime')).toBe(
|
|
83
|
+
'\0virtual:astro:assets/fonts/runtime'
|
|
84
|
+
);
|
|
85
|
+
expect(resolveId.call({}, 'astro/assets/fonts/runtime')).toBe('\0storybook:astro-fonts-runtime');
|
|
86
|
+
expect(resolveId.call({}, 'astro/assets/fonts/runtime.js')).toBe('\0storybook:astro-fonts-runtime');
|
|
87
|
+
expect(resolveId.call({}, 'some-other-id')).toBeUndefined();
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
test('internal virtual module exports a Map populated from the resolved families', async () => {
|
|
91
|
+
const family: StorybookFontFamily = {
|
|
92
|
+
name: 'Inter',
|
|
93
|
+
cssVariable: '--font-inter',
|
|
94
|
+
provider: makeProvider([
|
|
95
|
+
{ src: [{ url: 'https://example.com/inter.woff2', format: 'woff2' }], weight: 400 }
|
|
96
|
+
])
|
|
97
|
+
};
|
|
98
|
+
const plugin = vitePluginAstroFonts({ fonts: [family] });
|
|
99
|
+
|
|
100
|
+
const buildStart = plugin.buildStart as (this: unknown) => Promise<void>;
|
|
101
|
+
const load = plugin.load as (this: unknown, id: string) => Promise<{ code: string } | undefined>;
|
|
102
|
+
|
|
103
|
+
await buildStart.call({});
|
|
104
|
+
const result = await load.call({ load: async () => null }, '\0virtual:astro:assets/fonts/internal');
|
|
105
|
+
|
|
106
|
+
expect(result?.code).toContain('componentDataByCssVariable = new Map(');
|
|
107
|
+
expect(result?.code).toContain('--font-inter');
|
|
108
|
+
expect(result?.code).toContain('@font-face');
|
|
109
|
+
expect(result?.code).toContain('fontDataByCssVariable = ');
|
|
110
|
+
expect(result?.code).toContain('https://example.com/inter.woff2');
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
test('emits empty maps when no families are configured (no-op behavior preserved)', async () => {
|
|
114
|
+
const plugin = vitePluginAstroFonts();
|
|
115
|
+
|
|
116
|
+
const buildStart = plugin.buildStart as (this: unknown) => Promise<void>;
|
|
117
|
+
const load = plugin.load as (this: unknown, id: string) => Promise<{ code: string } | undefined>;
|
|
118
|
+
|
|
119
|
+
await buildStart.call({});
|
|
120
|
+
const result = await load.call({ load: async () => null }, '\0virtual:astro:assets/fonts/internal');
|
|
121
|
+
|
|
122
|
+
expect(result?.code).toContain('componentDataByCssVariable = new Map([])');
|
|
123
|
+
expect(result?.code).toContain('fontDataByCssVariable = {}');
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
test('continues processing remaining families if one provider throws', async () => {
|
|
127
|
+
const failing: StorybookFontFamily = {
|
|
128
|
+
name: 'Broken',
|
|
129
|
+
cssVariable: '--font-broken',
|
|
130
|
+
provider: {
|
|
131
|
+
name: 'broken',
|
|
132
|
+
resolveFont: () => {
|
|
133
|
+
throw new Error('boom');
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
};
|
|
137
|
+
const working: StorybookFontFamily = {
|
|
138
|
+
name: 'Inter',
|
|
139
|
+
cssVariable: '--font-inter',
|
|
140
|
+
provider: makeProvider([{ src: [{ url: '/inter.woff2' }], weight: 400 }])
|
|
141
|
+
};
|
|
142
|
+
const plugin = vitePluginAstroFonts({ fonts: [failing, working] });
|
|
143
|
+
|
|
144
|
+
const buildStart = plugin.buildStart as (this: unknown) => Promise<void>;
|
|
145
|
+
const load = plugin.load as (this: unknown, id: string) => Promise<{ code: string } | undefined>;
|
|
146
|
+
|
|
147
|
+
await buildStart.call({});
|
|
148
|
+
const result = await load.call({ load: async () => null }, '\0virtual:astro:assets/fonts/internal');
|
|
149
|
+
|
|
150
|
+
expect(result?.code).toContain('--font-inter');
|
|
151
|
+
expect(result?.code).not.toContain('--font-broken');
|
|
152
|
+
});
|
|
153
|
+
});
|
|
@@ -0,0 +1,302 @@
|
|
|
1
|
+
import { pathToFileURL } from 'node:url';
|
|
2
|
+
import type { Plugin } from 'vite';
|
|
3
|
+
|
|
4
|
+
// We avoid a hard import of `astro/assets/fonts/types` here because consumers
|
|
5
|
+
// using older Astro versions without the new fonts API would fail to install.
|
|
6
|
+
// The provider interface we rely on is small and stable enough to type locally.
|
|
7
|
+
export interface StorybookFontProvider {
|
|
8
|
+
name: string;
|
|
9
|
+
init?: (context: { storage: FontStorage; root: URL }) => Promise<void> | void;
|
|
10
|
+
resolveFont: (options: {
|
|
11
|
+
familyName: string;
|
|
12
|
+
weights: string[];
|
|
13
|
+
styles: string[];
|
|
14
|
+
subsets: string[];
|
|
15
|
+
formats: string[];
|
|
16
|
+
}) => Promise<{ fonts: FontFaceData[] } | undefined> | { fonts: FontFaceData[] } | undefined;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface FontFaceData {
|
|
20
|
+
src: Array<{ url?: string; name?: string; format?: string; tech?: string }>;
|
|
21
|
+
weight?: string | number | [number, number];
|
|
22
|
+
style?: string;
|
|
23
|
+
display?: string;
|
|
24
|
+
unicodeRange?: string[];
|
|
25
|
+
featureSettings?: string;
|
|
26
|
+
variationSettings?: string;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface StorybookFontFamily {
|
|
30
|
+
name: string;
|
|
31
|
+
cssVariable: string;
|
|
32
|
+
provider: StorybookFontProvider;
|
|
33
|
+
weights?: Array<string | number>;
|
|
34
|
+
styles?: string[];
|
|
35
|
+
subsets?: string[];
|
|
36
|
+
formats?: string[];
|
|
37
|
+
fallbacks?: string[];
|
|
38
|
+
display?: string;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
interface FontStorage {
|
|
42
|
+
getItem: <T = unknown>(key: string, init?: () => Promise<T> | T) => Promise<T | null>;
|
|
43
|
+
setItem: (key: string, value: unknown) => Promise<void> | void;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
interface ResolvedFontData {
|
|
47
|
+
componentEntries: Array<[string, { css: string; preloads: never[] }]>;
|
|
48
|
+
fontDataByCssVariable: Record<
|
|
49
|
+
string,
|
|
50
|
+
Array<{
|
|
51
|
+
src: Array<{ url: string; format?: string; tech?: string }>;
|
|
52
|
+
weight?: string;
|
|
53
|
+
style?: string;
|
|
54
|
+
}>
|
|
55
|
+
>;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const DEFAULTS = {
|
|
59
|
+
weights: ['400'],
|
|
60
|
+
styles: ['normal', 'italic'],
|
|
61
|
+
subsets: ['latin'],
|
|
62
|
+
formats: ['woff2'],
|
|
63
|
+
fallbacks: ['sans-serif']
|
|
64
|
+
} as const;
|
|
65
|
+
|
|
66
|
+
const VIRTUAL_INTERNAL_ID = 'virtual:astro:assets/fonts/internal';
|
|
67
|
+
const VIRTUAL_RUNTIME_ID = 'virtual:astro:assets/fonts/runtime';
|
|
68
|
+
const VIRTUAL_RUNTIME_RESOLVER_ID = 'virtual:astro:assets/fonts/runtime/font-file-url-resolver';
|
|
69
|
+
const PACKAGE_RUNTIME_IDS = ['astro/assets/fonts/runtime', 'astro/assets/fonts/runtime.js'];
|
|
70
|
+
|
|
71
|
+
const RUNTIME_STUB = `
|
|
72
|
+
export const fontData = {};
|
|
73
|
+
export function createGetFontData(fontsMod) {
|
|
74
|
+
return fontsMod?.fontDataByCssVariable ?? {};
|
|
75
|
+
}
|
|
76
|
+
export const experimental_getFontFileURL = () => undefined;
|
|
77
|
+
`;
|
|
78
|
+
|
|
79
|
+
const RESOLVER_STUB = `
|
|
80
|
+
export const runtimeFontFileUrlResolver = { resolve: () => undefined };
|
|
81
|
+
`;
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Resolves Astro's font Provider API for Storybook by reading the user's
|
|
85
|
+
* configured font families, calling each provider to produce @font-face data,
|
|
86
|
+
* and emitting CSS through Astro's font virtual modules.
|
|
87
|
+
*
|
|
88
|
+
* Lightweight first cut: generates @font-face declarations and a CSS variable
|
|
89
|
+
* binding to the family name plus fallbacks. Does not handle preload links,
|
|
90
|
+
* Capsize-optimized fallback metrics, or build-time font file emission — those
|
|
91
|
+
* paths fall back to remote URLs returned by the provider directly.
|
|
92
|
+
*
|
|
93
|
+
* If no families are provided, the plugin emits no-op stubs so Astro's
|
|
94
|
+
* font virtual modules still resolve in projects that don't configure fonts.
|
|
95
|
+
*/
|
|
96
|
+
export function vitePluginAstroFonts(
|
|
97
|
+
options: {
|
|
98
|
+
fonts?: StorybookFontFamily[];
|
|
99
|
+
root?: string;
|
|
100
|
+
} = {}
|
|
101
|
+
): Plugin {
|
|
102
|
+
const families = options.fonts ?? [];
|
|
103
|
+
const rootDir = options.root ?? process.cwd();
|
|
104
|
+
const root = pathToFileURL(rootDir.endsWith('/') ? rootDir : rootDir + '/');
|
|
105
|
+
|
|
106
|
+
let resolved: ResolvedFontData | null = null;
|
|
107
|
+
let resolvePromise: Promise<ResolvedFontData> | null = null;
|
|
108
|
+
|
|
109
|
+
const ensureResolved = async () => {
|
|
110
|
+
if (!resolvePromise) {
|
|
111
|
+
resolvePromise = resolveAllFamilies(families, root);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
resolved = await resolvePromise;
|
|
115
|
+
|
|
116
|
+
return resolved;
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
return {
|
|
120
|
+
name: 'storybook-astro-fonts',
|
|
121
|
+
enforce: 'pre',
|
|
122
|
+
|
|
123
|
+
async buildStart() {
|
|
124
|
+
await ensureResolved();
|
|
125
|
+
},
|
|
126
|
+
|
|
127
|
+
resolveId(id) {
|
|
128
|
+
if (id === VIRTUAL_INTERNAL_ID) {
|
|
129
|
+
return '\0' + VIRTUAL_INTERNAL_ID;
|
|
130
|
+
}
|
|
131
|
+
if (id === VIRTUAL_RUNTIME_ID) {
|
|
132
|
+
return '\0' + VIRTUAL_RUNTIME_ID;
|
|
133
|
+
}
|
|
134
|
+
if (id === VIRTUAL_RUNTIME_RESOLVER_ID) {
|
|
135
|
+
return '\0' + VIRTUAL_RUNTIME_RESOLVER_ID;
|
|
136
|
+
}
|
|
137
|
+
if (PACKAGE_RUNTIME_IDS.includes(id)) {
|
|
138
|
+
return '\0storybook:astro-fonts-runtime';
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
return undefined;
|
|
142
|
+
},
|
|
143
|
+
|
|
144
|
+
async load(id) {
|
|
145
|
+
if (id === '\0' + VIRTUAL_INTERNAL_ID) {
|
|
146
|
+
const data = resolved ?? (await ensureResolved());
|
|
147
|
+
|
|
148
|
+
return {
|
|
149
|
+
code:
|
|
150
|
+
`export const componentDataByCssVariable = new Map(${JSON.stringify(data.componentEntries)});\n` +
|
|
151
|
+
`export const fontDataByCssVariable = ${JSON.stringify(data.fontDataByCssVariable)};\n`
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
if (id === '\0' + VIRTUAL_RUNTIME_ID || id === '\0storybook:astro-fonts-runtime') {
|
|
155
|
+
return { code: RUNTIME_STUB };
|
|
156
|
+
}
|
|
157
|
+
if (id === '\0' + VIRTUAL_RUNTIME_RESOLVER_ID) {
|
|
158
|
+
return { code: RESOLVER_STUB };
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
return undefined;
|
|
162
|
+
}
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
async function resolveAllFamilies(
|
|
167
|
+
families: StorybookFontFamily[],
|
|
168
|
+
root: URL
|
|
169
|
+
): Promise<ResolvedFontData> {
|
|
170
|
+
const componentEntries: ResolvedFontData['componentEntries'] = [];
|
|
171
|
+
const fontDataByCssVariable: ResolvedFontData['fontDataByCssVariable'] = {};
|
|
172
|
+
const storage = createMemoryStorage();
|
|
173
|
+
|
|
174
|
+
for (const family of families) {
|
|
175
|
+
try {
|
|
176
|
+
if (family.provider.init) {
|
|
177
|
+
await family.provider.init({ storage, root });
|
|
178
|
+
}
|
|
179
|
+
const result = await family.provider.resolveFont({
|
|
180
|
+
familyName: family.name,
|
|
181
|
+
weights: (family.weights ?? DEFAULTS.weights).map(String),
|
|
182
|
+
styles: family.styles ?? [...DEFAULTS.styles],
|
|
183
|
+
subsets: family.subsets ?? [...DEFAULTS.subsets],
|
|
184
|
+
formats: family.formats ?? [...DEFAULTS.formats]
|
|
185
|
+
});
|
|
186
|
+
const faces = result?.fonts ?? [];
|
|
187
|
+
|
|
188
|
+
if (faces.length === 0) {
|
|
189
|
+
continue;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
componentEntries.push([
|
|
193
|
+
family.cssVariable,
|
|
194
|
+
{ css: buildFamilyCss(family, faces), preloads: [] }
|
|
195
|
+
]);
|
|
196
|
+
fontDataByCssVariable[family.cssVariable] = faces.map(toFontData);
|
|
197
|
+
} catch (err) {
|
|
198
|
+
// Swallow per-family errors so one bad family doesn't break the rest.
|
|
199
|
+
// Errors surface as the family simply not rendering, matching Astro's
|
|
200
|
+
// behavior when a provider can't resolve a font.
|
|
201
|
+
console.warn(
|
|
202
|
+
`[storybook-astro-fonts] Failed to resolve font family "${family.name}":`,
|
|
203
|
+
err instanceof Error ? err.message : err
|
|
204
|
+
);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
return { componentEntries, fontDataByCssVariable };
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
export function buildFamilyCss(family: StorybookFontFamily, faces: FontFaceData[]): string {
|
|
212
|
+
const fallbacks = family.fallbacks ?? [...DEFAULTS.fallbacks];
|
|
213
|
+
const familyList = [JSON.stringify(family.name), ...fallbacks].join(', ');
|
|
214
|
+
const faceBlocks = faces.map((face) => buildFontFaceBlock(family, face)).join('\n');
|
|
215
|
+
const rootRule = `:root { ${family.cssVariable}: ${familyList}; }`;
|
|
216
|
+
|
|
217
|
+
return `${faceBlocks}\n${rootRule}`;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
function buildFontFaceBlock(family: StorybookFontFamily, face: FontFaceData): string {
|
|
221
|
+
const src = face.src
|
|
222
|
+
.map((source) => {
|
|
223
|
+
if (source.url) {
|
|
224
|
+
const format = source.format ? ` format(${JSON.stringify(source.format)})` : '';
|
|
225
|
+
const tech = source.tech ? ` tech(${source.tech})` : '';
|
|
226
|
+
|
|
227
|
+
return `url(${JSON.stringify(source.url)})${format}${tech}`;
|
|
228
|
+
}
|
|
229
|
+
if (source.name) {
|
|
230
|
+
return `local(${JSON.stringify(source.name)})`;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
return '';
|
|
234
|
+
})
|
|
235
|
+
.filter(Boolean)
|
|
236
|
+
.join(', ');
|
|
237
|
+
|
|
238
|
+
const descriptors: string[] = [
|
|
239
|
+
`font-family: ${JSON.stringify(family.name)};`,
|
|
240
|
+
`src: ${src};`,
|
|
241
|
+
`font-display: ${family.display ?? 'swap'};`
|
|
242
|
+
];
|
|
243
|
+
|
|
244
|
+
if (face.weight !== undefined) {
|
|
245
|
+
const weight = Array.isArray(face.weight) ? face.weight.join(' ') : String(face.weight);
|
|
246
|
+
|
|
247
|
+
descriptors.push(`font-weight: ${weight};`);
|
|
248
|
+
}
|
|
249
|
+
if (face.style) {
|
|
250
|
+
descriptors.push(`font-style: ${face.style};`);
|
|
251
|
+
}
|
|
252
|
+
if (face.unicodeRange?.length) {
|
|
253
|
+
descriptors.push(`unicode-range: ${face.unicodeRange.join(', ')};`);
|
|
254
|
+
}
|
|
255
|
+
if (face.featureSettings) {
|
|
256
|
+
descriptors.push(`font-feature-settings: ${face.featureSettings};`);
|
|
257
|
+
}
|
|
258
|
+
if (face.variationSettings) {
|
|
259
|
+
descriptors.push(`font-variation-settings: ${face.variationSettings};`);
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
return `@font-face { ${descriptors.join(' ')} }`;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
function toFontData(face: FontFaceData) {
|
|
266
|
+
return {
|
|
267
|
+
src: face.src
|
|
268
|
+
.filter((source) => source.url)
|
|
269
|
+
.map((source) => ({ url: source.url!, format: source.format, tech: source.tech })),
|
|
270
|
+
weight:
|
|
271
|
+
face.weight !== undefined
|
|
272
|
+
? Array.isArray(face.weight)
|
|
273
|
+
? face.weight.join(' ')
|
|
274
|
+
: String(face.weight)
|
|
275
|
+
: undefined,
|
|
276
|
+
style: face.style
|
|
277
|
+
};
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
function createMemoryStorage(): FontStorage {
|
|
281
|
+
const store = new Map<string, unknown>();
|
|
282
|
+
|
|
283
|
+
return {
|
|
284
|
+
async getItem(key, init) {
|
|
285
|
+
if (store.has(key)) {
|
|
286
|
+
return store.get(key) as never;
|
|
287
|
+
}
|
|
288
|
+
if (init) {
|
|
289
|
+
const value = await init();
|
|
290
|
+
|
|
291
|
+
store.set(key, value);
|
|
292
|
+
|
|
293
|
+
return value as never;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
return null;
|
|
297
|
+
},
|
|
298
|
+
async setItem(key, value) {
|
|
299
|
+
store.set(key, value);
|
|
300
|
+
}
|
|
301
|
+
};
|
|
302
|
+
}
|
|
@@ -7,13 +7,14 @@ import type { FrameworkOptions } from './types.ts';
|
|
|
7
7
|
import type { Integration } from './integrations/index.ts';
|
|
8
8
|
import { importAstroConfig } from './importAstroConfig.ts';
|
|
9
9
|
import { viteAstroContainerRenderersPlugin } from './viteAstroContainerRenderersPlugin.ts';
|
|
10
|
-
import {
|
|
10
|
+
import { vitePluginAstroFonts } from './vitePluginAstroFonts.ts';
|
|
11
11
|
import { vitePluginAstroIntegrationOptsFallback } from './vitePluginAstroIntegrationOptsFallback.ts';
|
|
12
12
|
import { vitePluginAstroVueFallback } from './vitePluginAstroVueFallback.ts';
|
|
13
13
|
import { vitePluginAstroRoutesFallback } from './vitePluginAstroRoutesFallback.ts';
|
|
14
14
|
import { vitePluginStoryModuleMocks } from './vitePluginStoryModuleMocks.ts';
|
|
15
15
|
import { ssrLoadModuleWithFsFallback } from './lib/ssr-load-module-with-fs-fallback.ts';
|
|
16
16
|
import { resolveRulesConfigFilePath } from './rules-options.ts';
|
|
17
|
+
import { loadUserAstroIntegrations } from './loadUserAstroConfig.ts';
|
|
17
18
|
|
|
18
19
|
export async function vitePluginStorybookAstroMiddleware(options: FrameworkOptions) {
|
|
19
20
|
// The internal Vite server is created lazily inside configureServer (dev-only).
|
|
@@ -25,7 +26,7 @@ export async function vitePluginStorybookAstroMiddleware(options: FrameworkOptio
|
|
|
25
26
|
const vitePlugin = {
|
|
26
27
|
name: 'storybook-astro-middleware-plugin',
|
|
27
28
|
async configureServer(server) {
|
|
28
|
-
viteServer = await createViteServer(options.integrations ?? [], resolveFrom);
|
|
29
|
+
viteServer = await createViteServer(options.integrations ?? [], resolveFrom, options.fonts);
|
|
29
30
|
const storyRulesConfigFilePath = resolveRulesConfigFilePath(options.storyRules, resolveFrom);
|
|
30
31
|
|
|
31
32
|
const filePath = fileURLToPath(new URL('./middleware', import.meta.url));
|
|
@@ -44,7 +45,8 @@ export async function vitePluginStorybookAstroMiddleware(options: FrameworkOptio
|
|
|
44
45
|
loadModule: (id: string) =>
|
|
45
46
|
ssrLoadModuleWithFsFallback(viteServer!, id, {
|
|
46
47
|
fixStacktrace: true
|
|
47
|
-
})
|
|
48
|
+
}),
|
|
49
|
+
resolveFrom
|
|
48
50
|
});
|
|
49
51
|
|
|
50
52
|
let handlerPromise = createHandler();
|
|
@@ -155,18 +157,28 @@ function createSsrServerLogger() {
|
|
|
155
157
|
return logger;
|
|
156
158
|
}
|
|
157
159
|
|
|
158
|
-
export async function createViteServer(
|
|
160
|
+
export async function createViteServer(
|
|
161
|
+
integrations: Integration[],
|
|
162
|
+
resolveFrom = process.cwd(),
|
|
163
|
+
fonts?: FrameworkOptions['fonts']
|
|
164
|
+
) {
|
|
159
165
|
const { getViteConfig, passthroughImageService } = await importAstroConfig(resolveFrom);
|
|
160
166
|
const safeIntegrations = integrations ?? [];
|
|
161
167
|
const projectAstroResolutionPlugin = createProjectAstroResolutionPlugin(resolveFrom);
|
|
162
168
|
|
|
169
|
+
const frameworkIntegrations = await Promise.all(
|
|
170
|
+
safeIntegrations.map((integration) => integration.loadIntegration(resolveFrom))
|
|
171
|
+
);
|
|
172
|
+
|
|
173
|
+
const userIntegrations = await loadUserAstroIntegrations(resolveFrom);
|
|
174
|
+
const frameworkNames = new Set(frameworkIntegrations.map(i => i.name));
|
|
175
|
+
const extraIntegrations = userIntegrations.filter(i => !frameworkNames.has(i.name));
|
|
176
|
+
|
|
163
177
|
const config = await getViteConfig(
|
|
164
178
|
{ root: resolveFrom },
|
|
165
179
|
{
|
|
166
180
|
configFile: false,
|
|
167
|
-
integrations:
|
|
168
|
-
safeIntegrations.map((integration) => integration.loadIntegration(resolveFrom))
|
|
169
|
-
),
|
|
181
|
+
integrations: [...frameworkIntegrations, ...extraIntegrations],
|
|
170
182
|
// Use the passthrough image service so nested components that use <Image>
|
|
171
183
|
// from astro:assets render as plain <img> tags without triggering image
|
|
172
184
|
// optimization (which fails in the Storybook SSR context).
|
|
@@ -181,7 +193,7 @@ export async function createViteServer(integrations: Integration[], resolveFrom
|
|
|
181
193
|
plugins: [
|
|
182
194
|
projectAstroResolutionPlugin,
|
|
183
195
|
// Fallbacks must come first to intercept before Astro's plugins
|
|
184
|
-
|
|
196
|
+
vitePluginAstroFonts({ fonts, root: resolveFrom }),
|
|
185
197
|
vitePluginAstroIntegrationOptsFallback(),
|
|
186
198
|
vitePluginAstroVueFallback(),
|
|
187
199
|
vitePluginAstroRoutesFallback(),
|