@storybook-astro/framework 1.3.0 → 1.4.0-canary.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -14,6 +14,54 @@ type StoryRulesOptions = string | {
14
14
  configFile: string;
15
15
  };
16
16
 
17
+ interface StorybookFontProvider {
18
+ name: string;
19
+ init?: (context: {
20
+ storage: FontStorage;
21
+ root: URL;
22
+ }) => Promise<void> | void;
23
+ resolveFont: (options: {
24
+ familyName: string;
25
+ weights: string[];
26
+ styles: string[];
27
+ subsets: string[];
28
+ formats: string[];
29
+ }) => Promise<{
30
+ fonts: FontFaceData[];
31
+ } | undefined> | {
32
+ fonts: FontFaceData[];
33
+ } | undefined;
34
+ }
35
+ interface FontFaceData {
36
+ src: Array<{
37
+ url?: string;
38
+ name?: string;
39
+ format?: string;
40
+ tech?: string;
41
+ }>;
42
+ weight?: string | number | [number, number];
43
+ style?: string;
44
+ display?: string;
45
+ unicodeRange?: string[];
46
+ featureSettings?: string;
47
+ variationSettings?: string;
48
+ }
49
+ interface StorybookFontFamily {
50
+ name: string;
51
+ cssVariable: string;
52
+ provider: StorybookFontProvider;
53
+ weights?: Array<string | number>;
54
+ styles?: string[];
55
+ subsets?: string[];
56
+ formats?: string[];
57
+ fallbacks?: string[];
58
+ display?: string;
59
+ }
60
+ interface FontStorage {
61
+ getItem: <T = unknown>(key: string, init?: () => Promise<T> | T) => Promise<T | null>;
62
+ setItem: (key: string, value: unknown) => Promise<void> | void;
63
+ }
64
+
17
65
  type FrameworkName = CompatibleString<'@storybook-astro/framework'>;
18
66
 
19
67
  type RenderMode = 'server' | 'static';
@@ -31,6 +79,13 @@ type BaseFrameworkOptions = {
31
79
  integrations?: Integration[];
32
80
  sanitization?: SanitizationOptions;
33
81
  resolveFrom?: string;
82
+ /**
83
+ * Astro font families to resolve and inject as @font-face CSS during story
84
+ * rendering. Pass the same array you have in your `astro.config.ts` under
85
+ * `fonts:`. Currently honored in development; static/server builds fall
86
+ * back to no-op stubs.
87
+ */
88
+ fonts?: StorybookFontFamily[];
34
89
  };
35
90
  type ServerFrameworkOptions = BaseFrameworkOptions & {
36
91
  renderMode?: 'server';
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  createViteServer,
3
3
  vitePluginStorybookAstroMiddleware
4
- } from "./chunk-AYYMNFI6.js";
4
+ } from "./chunk-EU6E5SJE.js";
5
5
  import "./chunk-PUTCAN6X.js";
6
6
  import "./chunk-B5HHF6FC.js";
7
7
  import "./chunk-G3PMV62Z.js";
@@ -9,4 +9,4 @@ export {
9
9
  createViteServer,
10
10
  vitePluginStorybookAstroMiddleware
11
11
  };
12
- //# sourceMappingURL=viteStorybookAstroMiddlewarePlugin-UB6ZLJ4B.js.map
12
+ //# sourceMappingURL=viteStorybookAstroMiddlewarePlugin-RPIBVJAV.js.map
@@ -1,9 +1,9 @@
1
1
  import {
2
2
  TESTING_RENDERER_DAEMON_URL_ENV,
3
3
  startTestingRendererDaemon
4
- } from "../chunk-2EABPTOY.js";
4
+ } from "../chunk-AIEYHH2G.js";
5
5
  import "../chunk-WUTCMEF5.js";
6
- import "../chunk-AYYMNFI6.js";
6
+ import "../chunk-EU6E5SJE.js";
7
7
  import "../chunk-PUTCAN6X.js";
8
8
  import "../chunk-7YBE4TTI.js";
9
9
  import "../chunk-B5HHF6FC.js";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@storybook-astro/framework",
3
- "version": "1.3.0",
3
+ "version": "1.4.0-canary.1",
4
4
  "description": "Community-supported Storybook framework for Astro 5 & 6 components",
5
5
  "main": "./dist/index.js",
6
6
  "module": "./dist/index.js",
@@ -142,7 +142,7 @@
142
142
  }
143
143
  },
144
144
  "dependencies": {
145
- "@storybook-astro/renderer": "1.3.0",
145
+ "@storybook-astro/renderer": "1.4.0-canary.1",
146
146
  "hono": "^4.11.12",
147
147
  "sanitize-html": "^2.17.0",
148
148
  "vite": "^6.4.1 || ^7.0.0 || ^8.0.0"
@@ -0,0 +1,59 @@
1
+ import { loadConfigFromFile } from 'vite';
2
+ import { existsSync } from 'node:fs';
3
+ import { resolve } from 'node:path';
4
+ import type { AstroIntegration } from 'astro';
5
+
6
+ const CONFIG_FILENAMES = [
7
+ 'astro.config.ts',
8
+ 'astro.config.mjs',
9
+ 'astro.config.js',
10
+ 'astro.config.cjs',
11
+ ];
12
+
13
+ /**
14
+ * Loads integrations declared in the user's astro.config.* so that any Vite
15
+ * plugins they register (e.g. astro-icon's virtual:astro-icon resolver) are
16
+ * present in both the main Storybook Vite server and the internal Astro SSR
17
+ * server. Returns an empty array on any failure so the calling code can
18
+ * continue with only the framework-level integrations.
19
+ */
20
+ export async function loadUserAstroIntegrations(resolveFrom: string): Promise<AstroIntegration[]> {
21
+ const configFile = CONFIG_FILENAMES.find(name => existsSync(resolve(resolveFrom, name)));
22
+
23
+ if (!configFile) {
24
+ return [];
25
+ }
26
+
27
+ try {
28
+ const result = await loadConfigFromFile(
29
+ { command: 'serve', mode: 'development' },
30
+ configFile,
31
+ resolveFrom
32
+ );
33
+
34
+ if (!result?.config) {
35
+ return [];
36
+ }
37
+
38
+ const config = result.config as { integrations?: unknown };
39
+ const raw = config.integrations;
40
+
41
+ if (!raw) {
42
+ return [];
43
+ }
44
+
45
+ // Astro allows nested arrays from conditional spreads (e.g. ...whenX(() => mdx()))
46
+ const flat = (Array.isArray(raw) ? raw : [raw]).flat(Infinity);
47
+
48
+ return flat.filter(
49
+ (i): i is AstroIntegration => Boolean(i) && typeof i === 'object' && 'name' in i && 'hooks' in i
50
+ );
51
+ } catch (err) {
52
+ console.warn(
53
+ '[storybook-astro] Could not load astro.config to discover integrations:',
54
+ err instanceof Error ? err.message : String(err)
55
+ );
56
+
57
+ return [];
58
+ }
59
+ }
@@ -2,6 +2,7 @@ import { resolve } from 'node:path';
2
2
  import { createAstroRenderHandler, type HandlerProps } from './astroRenderHandler.ts';
3
3
  import type { Integration } from './integrations/index.ts';
4
4
  import type { SanitizationOptions } from './lib/sanitization.ts';
5
+ import type { FrameworkOptions } from './types.ts';
5
6
  import {
6
7
  createClientModuleResolver,
7
8
  createProductionAstroContainer,
@@ -34,6 +35,7 @@ type ProductionRenderRuntimeOptions = {
34
35
  trackedSpecifiers: Set<string>;
35
36
  resolveFrom: string;
36
37
  resolveComponentId?: (id: string) => string;
38
+ fonts?: FrameworkOptions['fonts'];
37
39
  };
38
40
 
39
41
  /** Creates the shared SSR runtime used by both build-time prerendering and the standalone render server. */
@@ -43,7 +45,8 @@ export async function createProductionRenderRuntime(
43
45
  const viteServer = await createStorySsrViteServer({
44
46
  integrations: options.integrations,
45
47
  trackedSpecifiers: options.trackedSpecifiers,
46
- resolveFrom: options.resolveFrom
48
+ resolveFrom: options.resolveFrom,
49
+ fonts: options.fonts
47
50
  });
48
51
 
49
52
  try {
@@ -6,7 +6,8 @@ import { importAstroConfig } from './importAstroConfig.ts';
6
6
  import type { Integration } from './integrations/index.ts';
7
7
  import { ssrLoadModuleWithFsFallback } from './lib/ssr-load-module-with-fs-fallback.ts';
8
8
  import { resolveStoryModuleMock } from './module-mocks.ts';
9
- import { vitePluginAstroFontsFallback } from './vitePluginAstroFontsFallback.ts';
9
+ import type { FrameworkOptions } from './types.ts';
10
+ import { vitePluginAstroFonts } from './vitePluginAstroFonts.ts';
10
11
  import { vitePluginAstroIntegrationOptsFallback } from './vitePluginAstroIntegrationOptsFallback.ts';
11
12
  import { vitePluginAstroRoutesFallback } from './vitePluginAstroRoutesFallback.ts';
12
13
  import { vitePluginAstroVueFallback } from './vitePluginAstroVueFallback.ts';
@@ -16,6 +17,7 @@ export async function createStorySsrViteServer(options: {
16
17
  integrations: Integration[];
17
18
  trackedSpecifiers: Set<string>;
18
19
  resolveFrom: string;
20
+ fonts?: FrameworkOptions['fonts'];
19
21
  }) {
20
22
  const { getViteConfig, passthroughImageService } = await importAstroConfig(options.resolveFrom);
21
23
  const astroConfig = await getViteConfig(
@@ -44,7 +46,7 @@ export async function createStorySsrViteServer(options: {
44
46
  },
45
47
  plugins: [
46
48
  createProjectAstroResolutionPlugin(options.resolveFrom),
47
- vitePluginAstroFontsFallback(),
49
+ vitePluginAstroFonts({ fonts: options.fonts, root: options.resolveFrom }),
48
50
  vitePluginAstroIntegrationOptsFallback(),
49
51
  vitePluginAstroVueFallback(),
50
52
  vitePluginAstroRoutesFallback(),
package/src/types.ts CHANGED
@@ -3,10 +3,11 @@ import type { InlineConfig } from 'vite';
3
3
  import type { Integration } from './integrations/index.ts';
4
4
  import type { SanitizationOptions } from './lib/sanitization.ts';
5
5
  import type { StoryRulesOptions } from './rules-options.ts';
6
+ import type { StorybookFontFamily } from './vitePluginAstroFonts.ts';
6
7
 
7
8
  type FrameworkName = CompatibleString<'@storybook-astro/framework'>;
8
9
 
9
- export type { Integration, SanitizationOptions, StoryRulesOptions };
10
+ export type { Integration, SanitizationOptions, StoryRulesOptions, StorybookFontFamily };
10
11
  export type RenderMode = 'server' | 'static';
11
12
 
12
13
  export type ServerBuildOptions = {
@@ -25,6 +26,13 @@ type BaseFrameworkOptions = {
25
26
  integrations?: Integration[];
26
27
  sanitization?: SanitizationOptions;
27
28
  resolveFrom?: string;
29
+ /**
30
+ * Astro font families to resolve and inject as @font-face CSS during story
31
+ * rendering. Pass the same array you have in your `astro.config.ts` under
32
+ * `fonts:`. Currently honored in development; static/server builds fall
33
+ * back to no-op stubs.
34
+ */
35
+ fonts?: StorybookFontFamily[];
28
36
  };
29
37
 
30
38
  type ServerFrameworkOptions = BaseFrameworkOptions & {
@@ -1,6 +1,7 @@
1
1
  import { mergeConfig, type InlineConfig } from 'vite';
2
2
  import type { Integration } from './integrations/index.ts';
3
3
  import { importAstroConfig } from './importAstroConfig.ts';
4
+ import { loadUserAstroIntegrations } from './loadUserAstroConfig.ts';
4
5
 
5
6
  const ASTRO_PLUGINS_THAT_ARE_SUPPOSEDLY_NOT_NEEDED_IN_STORYBOOK = [
6
7
  '@astro/plugin-actions',
@@ -38,13 +39,19 @@ export async function mergeWithAstroConfig(
38
39
  const { getViteConfig } = await importAstroConfig(resolveFrom);
39
40
  const safeIntegrations = integrations ?? [];
40
41
 
42
+ const frameworkIntegrations = await Promise.all(
43
+ safeIntegrations.map((integration) => integration.loadIntegration(resolveFrom))
44
+ );
45
+
46
+ const userIntegrations = await loadUserAstroIntegrations(resolveFrom);
47
+ const frameworkNames = new Set(frameworkIntegrations.map(i => i.name));
48
+ const extraIntegrations = userIntegrations.filter(i => !frameworkNames.has(i.name));
49
+
41
50
  const astroConfig = await getViteConfig(
42
51
  {},
43
52
  {
44
53
  configFile: false,
45
- integrations: await Promise.all(
46
- safeIntegrations.map((integration) => integration.loadIntegration(resolveFrom))
47
- )
54
+ integrations: [...frameworkIntegrations, ...extraIntegrations]
48
55
  }
49
56
  )({
50
57
  mode,
@@ -138,6 +138,7 @@ export function vitePluginAstroBuildPrerender(options: FrameworkOptions): Plugin
138
138
  staticCssMap,
139
139
  trackedSpecifiers,
140
140
  resolveFrom,
141
+ fonts: options.fonts,
141
142
  bundle
142
143
  });
143
144
 
@@ -162,6 +163,7 @@ async function prerenderAstroStories(options: {
162
163
  staticCssMap: StaticCssMap;
163
164
  trackedSpecifiers: Set<string>;
164
165
  resolveFrom: string;
166
+ fonts?: FrameworkOptions['fonts'];
165
167
  bundle: Rollup.OutputBundle;
166
168
  }) {
167
169
  const runtime = await createProductionRenderRuntime({
@@ -170,7 +172,8 @@ async function prerenderAstroStories(options: {
170
172
  storyRulesConfigFilePath: options.storyRulesConfigFilePath,
171
173
  staticModuleMap: options.staticModuleMap,
172
174
  trackedSpecifiers: options.trackedSpecifiers,
173
- resolveFrom: options.resolveFrom
175
+ resolveFrom: options.resolveFrom,
176
+ fonts: options.fonts
174
177
  });
175
178
  const assetPathMap = buildAssetPathMap(options.bundle);
176
179
 
@@ -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
+ });