@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.
Files changed (36) hide show
  1. package/dist/{chunk-N3WTUD2A.js → chunk-A4DQ67HA.js} +53 -2
  2. package/dist/chunk-A4DQ67HA.js.map +1 -0
  3. package/dist/{chunk-2EABPTOY.js → chunk-BV6V2Z4X.js} +2 -2
  4. package/dist/{chunk-AYYMNFI6.js → chunk-VZXGPM6P.js} +218 -34
  5. package/dist/chunk-VZXGPM6P.js.map +1 -0
  6. package/dist/index.d.ts +1 -1
  7. package/dist/middleware.js +10 -2
  8. package/dist/middleware.js.map +1 -1
  9. package/dist/node/index.d.ts +1 -1
  10. package/dist/preset.d.ts +1 -1
  11. package/dist/preset.js +39 -21
  12. package/dist/preset.js.map +1 -1
  13. package/dist/testing.js +3 -3
  14. package/dist/{types-BCpJLSTo.d.ts → types--SvYP5Ri.d.ts} +55 -0
  15. package/dist/{viteStorybookAstroMiddlewarePlugin-UB6ZLJ4B.js → viteStorybookAstroMiddlewarePlugin-246I5D3Y.js} +2 -2
  16. package/dist/vitest/global-setup.js +2 -2
  17. package/package.json +3 -2
  18. package/src/lib/resolve-aliased-island.test.ts +150 -0
  19. package/src/lib/resolve-aliased-island.ts +97 -0
  20. package/src/loadUserAstroConfig.ts +59 -0
  21. package/src/middleware.ts +15 -0
  22. package/src/productionRenderRuntime.ts +6 -2
  23. package/src/storySsrVite.ts +15 -2
  24. package/src/types.ts +9 -1
  25. package/src/vitePluginAstro.ts +10 -3
  26. package/src/vitePluginAstroBuildPrerender.ts +4 -1
  27. package/src/vitePluginAstroBuildShared.test.ts +25 -6
  28. package/src/vitePluginAstroBuildShared.ts +25 -12
  29. package/src/vitePluginAstroFonts.test.ts +153 -0
  30. package/src/vitePluginAstroFonts.ts +302 -0
  31. package/src/viteStorybookAstroMiddlewarePlugin.ts +20 -8
  32. package/dist/chunk-AYYMNFI6.js.map +0 -1
  33. package/dist/chunk-N3WTUD2A.js.map +0 -1
  34. package/src/vitePluginAstroFontsFallback.ts +0 -69
  35. /package/dist/{chunk-2EABPTOY.js.map → chunk-BV6V2Z4X.js.map} +0 -0
  36. /package/dist/{viteStorybookAstroMiddlewarePlugin-UB6ZLJ4B.js.map → viteStorybookAstroMiddlewarePlugin-246I5D3Y.js.map} +0 -0
@@ -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-VZXGPM6P.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-246I5D3Y.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-BV6V2Z4X.js";
5
5
  import "../chunk-WUTCMEF5.js";
6
- import "../chunk-AYYMNFI6.js";
6
+ import "../chunk-VZXGPM6P.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.2",
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,9 +142,10 @@
142
142
  }
143
143
  },
144
144
  "dependencies": {
145
- "@storybook-astro/renderer": "1.3.0",
145
+ "@storybook-astro/renderer": "1.4.0-canary.2",
146
146
  "hono": "^4.11.12",
147
147
  "sanitize-html": "^2.17.0",
148
+ "tsconfck": "^3.1.6",
148
149
  "vite": "^6.4.1 || ^7.0.0 || ^8.0.0"
149
150
  }
150
151
  }
@@ -0,0 +1,150 @@
1
+ import { mkdir, mkdtemp, rm, writeFile } from 'node:fs/promises';
2
+ import { tmpdir } from 'node:os';
3
+ import { join } from 'node:path';
4
+ import { afterEach, beforeEach, describe, expect, test } from 'vitest';
5
+ import { resolveAliasedIsland } from './resolve-aliased-island.ts';
6
+
7
+ describe('resolveAliasedIsland', () => {
8
+ let tmpDir: string;
9
+
10
+ beforeEach(async () => {
11
+ tmpDir = await mkdtemp(join(tmpdir(), 'storybook-astro-alias-test-'));
12
+ });
13
+
14
+ afterEach(async () => {
15
+ await rm(tmpDir, { recursive: true, force: true });
16
+ });
17
+
18
+ async function writeTsconfig(content: object) {
19
+ await writeFile(join(tmpDir, 'tsconfig.json'), JSON.stringify(content, null, 2));
20
+ }
21
+
22
+ test('resolves a basic @/ alias to an absolute path', async () => {
23
+ await mkdir(join(tmpDir, 'src', 'components'), { recursive: true });
24
+ const counterFile = join(tmpDir, 'src', 'components', 'Counter.tsx');
25
+
26
+ await writeFile(counterFile, `export default function Counter() {}`);
27
+ await writeTsconfig({
28
+ compilerOptions: {
29
+ paths: { '@/*': ['src/*'] }
30
+ }
31
+ });
32
+
33
+ const result = await resolveAliasedIsland('@/components/Counter', tmpDir);
34
+
35
+ expect(result).toBe(counterFile.replace(/\\/g, '/'));
36
+ });
37
+
38
+ test('resolves when baseUrl shifts the path root', async () => {
39
+ await mkdir(join(tmpDir, 'src', 'components'), { recursive: true });
40
+ const counterFile = join(tmpDir, 'src', 'components', 'Counter.tsx');
41
+
42
+ await writeFile(counterFile, `export default function Counter() {}`);
43
+ // With baseUrl: "src", paths are relative to src/
44
+ await writeTsconfig({
45
+ compilerOptions: {
46
+ baseUrl: 'src',
47
+ paths: { '@/*': ['*'] }
48
+ }
49
+ });
50
+
51
+ const result = await resolveAliasedIsland('@/components/Counter', tmpDir);
52
+
53
+ expect(result).toBe(counterFile.replace(/\\/g, '/'));
54
+ });
55
+
56
+ test('falls back to second target when first does not exist on disk', async () => {
57
+ await mkdir(join(tmpDir, 'app', 'components'), { recursive: true });
58
+ const counterFile = join(tmpDir, 'app', 'components', 'Counter.tsx');
59
+
60
+ await writeFile(counterFile, `export default function Counter() {}`);
61
+ // First target points at a non-existent dir; second target resolves.
62
+ await writeTsconfig({
63
+ compilerOptions: {
64
+ paths: { '@/*': ['missing/*', 'app/*'] }
65
+ }
66
+ });
67
+
68
+ const result = await resolveAliasedIsland('@/components/Counter', tmpDir);
69
+
70
+ expect(result).toBe(counterFile.replace(/\\/g, '/'));
71
+ });
72
+
73
+ test('resolves an alias with an explicit file extension in the specifier', async () => {
74
+ await mkdir(join(tmpDir, 'src'), { recursive: true });
75
+ const file = join(tmpDir, 'src', 'Button.tsx');
76
+
77
+ await writeFile(file, `export default function Button() {}`);
78
+ await writeTsconfig({ compilerOptions: { paths: { '@/*': ['src/*'] } } });
79
+
80
+ const result = await resolveAliasedIsland('@/Button.tsx', tmpDir);
81
+
82
+ expect(result).toBe(file.replace(/\\/g, '/'));
83
+ });
84
+
85
+ test('returns undefined when aliased file does not exist on disk', async () => {
86
+ await writeTsconfig({ compilerOptions: { paths: { '@/*': ['src/*'] } } });
87
+
88
+ const result = await resolveAliasedIsland('@/components/Missing', tmpDir);
89
+
90
+ expect(result).toBeUndefined();
91
+ });
92
+
93
+ test('returns undefined when tsconfig has no paths', async () => {
94
+ await mkdir(join(tmpDir, 'src'), { recursive: true });
95
+ await writeFile(join(tmpDir, 'src', 'Counter.tsx'), `export default function Counter() {}`);
96
+ await writeTsconfig({ compilerOptions: {} });
97
+
98
+ const result = await resolveAliasedIsland('@/Counter', tmpDir);
99
+
100
+ expect(result).toBeUndefined();
101
+ });
102
+
103
+ test('returns undefined when tsconfig.json is absent', async () => {
104
+ const result = await resolveAliasedIsland('@/Counter', tmpDir);
105
+
106
+ expect(result).toBeUndefined();
107
+ });
108
+
109
+ test('returns undefined for relative specifiers (already handled elsewhere)', async () => {
110
+ const result = await resolveAliasedIsland('./components/Counter', tmpDir);
111
+
112
+ expect(result).toBeUndefined();
113
+ });
114
+
115
+ test('returns undefined for absolute paths', async () => {
116
+ const result = await resolveAliasedIsland('/abs/path/Counter.tsx', tmpDir);
117
+
118
+ expect(result).toBeUndefined();
119
+ });
120
+
121
+ test('returns undefined for virtual: specifiers', async () => {
122
+ const result = await resolveAliasedIsland('virtual:astro-container-renderers', tmpDir);
123
+
124
+ expect(result).toBeUndefined();
125
+ });
126
+
127
+ test('returns undefined for astro: specifiers', async () => {
128
+ const result = await resolveAliasedIsland('astro:scripts/page.js', tmpDir);
129
+
130
+ expect(result).toBeUndefined();
131
+ });
132
+
133
+ test('returns undefined for /@fs/ specifiers', async () => {
134
+ const result = await resolveAliasedIsland('/@fs/abs/path/Counter.tsx', tmpDir);
135
+
136
+ expect(result).toBeUndefined();
137
+ });
138
+
139
+ test('resolves a custom alias prefix (not @/)', async () => {
140
+ await mkdir(join(tmpDir, 'src'), { recursive: true });
141
+ const file = join(tmpDir, 'src', 'Widget.vue');
142
+
143
+ await writeFile(file, `<template><div /></template>`);
144
+ await writeTsconfig({ compilerOptions: { paths: { '~/*': ['src/*'] } } });
145
+
146
+ const result = await resolveAliasedIsland('~/Widget', tmpDir);
147
+
148
+ expect(result).toBe(file.replace(/\\/g, '/'));
149
+ });
150
+ });
@@ -0,0 +1,97 @@
1
+ import { access } from 'node:fs/promises';
2
+ import { resolve } from 'node:path';
3
+ import { parse } from 'tsconfck';
4
+
5
+ // Islands embedded in `.astro` components are frequently imported through a
6
+ // tsconfig path alias (e.g. `@/components/Counter`). The raw aliased specifier
7
+ // is baked into `<astro-island component-url>` and `import()`d verbatim, so it
8
+ // must be turned into an on-disk path before it can hydrate. This helper is
9
+ // used as a last resort after the existing resolution has already had its
10
+ // chance.
11
+
12
+ const ALIAS_EXTS = ['.tsx', '.ts', '.jsx', '.js', '.vue', '.svelte', '.mts', '.mjs'];
13
+
14
+ async function isFile(candidate: string): Promise<boolean> {
15
+ try {
16
+ await access(candidate);
17
+
18
+ return true;
19
+ } catch {
20
+ return false;
21
+ }
22
+ }
23
+
24
+ /**
25
+ * Resolves a tsconfig-aliased island specifier (e.g. `@/components/Counter`)
26
+ * to an absolute on-disk file path, or `undefined` when it is not a tsconfig
27
+ * alias or no matching file exists. Already-resolvable specifiers are skipped
28
+ * so this runs only as a genuine last resort.
29
+ */
30
+ export async function resolveAliasedIsland(
31
+ specifier: string,
32
+ resolveFrom: string
33
+ ): Promise<string | undefined> {
34
+ // Skip specifiers the existing resolution can already handle.
35
+ if (
36
+ !specifier ||
37
+ specifier.startsWith('./') ||
38
+ specifier.startsWith('../') ||
39
+ specifier.startsWith('/') ||
40
+ specifier.startsWith('\0') ||
41
+ specifier.startsWith('virtual:') ||
42
+ specifier.startsWith('astro:') ||
43
+ specifier.startsWith('@astrojs/') ||
44
+ specifier.startsWith('@id/') ||
45
+ specifier.startsWith('/@fs/')
46
+ ) {
47
+ return undefined;
48
+ }
49
+
50
+ let result;
51
+
52
+ try {
53
+ result = await parse(resolve(resolveFrom, 'tsconfig.json'));
54
+ } catch {
55
+ return undefined;
56
+ }
57
+
58
+ const compilerOptions = result.tsconfig?.compilerOptions ?? {};
59
+ const paths: Record<string, string[]> = compilerOptions.paths ?? {};
60
+
61
+ if (Object.keys(paths).length === 0) {
62
+ return undefined;
63
+ }
64
+
65
+ // tsconfig paths resolve relative to baseUrl when present, otherwise the
66
+ // tsconfig directory (which tsconfck gives us as the tsconfig file's dir).
67
+ const tsconfigDir = result.tsconfigFile
68
+ ? resolve(result.tsconfigFile, '..')
69
+ : resolveFrom;
70
+ const root = compilerOptions.baseUrl
71
+ ? resolve(tsconfigDir, compilerOptions.baseUrl)
72
+ : tsconfigDir;
73
+
74
+ for (const [pattern, targets] of Object.entries(paths)) {
75
+ const prefix = pattern.replace(/\*$/, '');
76
+
77
+ if (!specifier.startsWith(prefix)) {
78
+ continue;
79
+ }
80
+
81
+ const rest = specifier.slice(prefix.length);
82
+
83
+ for (const target of targets) {
84
+ const resolvedTarget = target.replace(/\*$/, '');
85
+ const base = resolve(root, resolvedTarget, rest);
86
+ const candidates = [base, ...ALIAS_EXTS.map((ext) => `${base}${ext}`)];
87
+
88
+ for (const candidate of candidates) {
89
+ if (await isFile(candidate)) {
90
+ return candidate.replace(/\\/g, '/');
91
+ }
92
+ }
93
+ }
94
+ }
95
+
96
+ return undefined;
97
+ }
@@ -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
+ }
package/src/middleware.ts CHANGED
@@ -6,6 +6,7 @@ import { ensureAstroPassthroughImageService } from './astroImageService.ts';
6
6
  import { createAstroRenderHandler, type HandlerProps } from './astroRenderHandler.ts';
7
7
  import type { Integration } from './integrations/index.ts';
8
8
  import type { SanitizationOptions } from './lib/sanitization.ts';
9
+ import { resolveAliasedIsland } from './lib/resolve-aliased-island.ts';
9
10
  import { resolveStoryModuleMock } from './module-mocks.ts';
10
11
  import { addRenderers, resolveClientModules } from 'virtual:astro-container-renderers';
11
12
 
@@ -18,6 +19,7 @@ type HandlerFactoryOptions = {
18
19
  loadModule?: (id: string) => Promise<{ default: unknown }>;
19
20
  invalidateModuleGraph?: () => void;
20
21
  resolveModule?: (specifier: string) => string | undefined;
22
+ resolveFrom?: string;
21
23
  };
22
24
 
23
25
  export type { HandlerProps };
@@ -52,6 +54,19 @@ export async function handlerFactory(
52
54
  return resolution;
53
55
  }
54
56
 
57
+ // Last resort: an island imported via a tsconfig path alias (e.g. `@/...`)
58
+ // has its raw aliased specifier baked into the island's component-url.
59
+ // Resolve it to an on-disk file and hand back a `/@fs/` URL the dev Vite
60
+ // server can serve so the island still hydrates.
61
+ const aliasedIsland = await resolveAliasedIsland(
62
+ specifier,
63
+ options?.resolveFrom ?? process.cwd()
64
+ );
65
+
66
+ if (aliasedIsland) {
67
+ return `/@fs/${aliasedIsland}`;
68
+ }
69
+
55
70
  return specifier;
56
71
  }
57
72
  });
@@ -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 {
@@ -58,7 +61,8 @@ export async function createProductionRenderRuntime(
58
61
  const astroContainer = await createProductionAstroContainer({
59
62
  integrations: options.integrations,
60
63
  resolveClientModule,
61
- viteServer
64
+ viteServer,
65
+ resolveFrom: options.resolveFrom
62
66
  });
63
67
 
64
68
  const loadModule = async (moduleId: string) => {
@@ -4,9 +4,11 @@ import type { experimental_AstroContainer as AstroContainer } from 'astro/contai
4
4
  import { ensureAstroPassthroughImageService } from './astroImageService.ts';
5
5
  import { importAstroConfig } from './importAstroConfig.ts';
6
6
  import type { Integration } from './integrations/index.ts';
7
+ import { resolveAliasedIsland } from './lib/resolve-aliased-island.ts';
7
8
  import { ssrLoadModuleWithFsFallback } from './lib/ssr-load-module-with-fs-fallback.ts';
8
9
  import { resolveStoryModuleMock } from './module-mocks.ts';
9
- import { vitePluginAstroFontsFallback } from './vitePluginAstroFontsFallback.ts';
10
+ import type { FrameworkOptions } from './types.ts';
11
+ import { vitePluginAstroFonts } from './vitePluginAstroFonts.ts';
10
12
  import { vitePluginAstroIntegrationOptsFallback } from './vitePluginAstroIntegrationOptsFallback.ts';
11
13
  import { vitePluginAstroRoutesFallback } from './vitePluginAstroRoutesFallback.ts';
12
14
  import { vitePluginAstroVueFallback } from './vitePluginAstroVueFallback.ts';
@@ -16,6 +18,7 @@ export async function createStorySsrViteServer(options: {
16
18
  integrations: Integration[];
17
19
  trackedSpecifiers: Set<string>;
18
20
  resolveFrom: string;
21
+ fonts?: FrameworkOptions['fonts'];
19
22
  }) {
20
23
  const { getViteConfig, passthroughImageService } = await importAstroConfig(options.resolveFrom);
21
24
  const astroConfig = await getViteConfig(
@@ -44,7 +47,7 @@ export async function createStorySsrViteServer(options: {
44
47
  },
45
48
  plugins: [
46
49
  createProjectAstroResolutionPlugin(options.resolveFrom),
47
- vitePluginAstroFontsFallback(),
50
+ vitePluginAstroFonts({ fonts: options.fonts, root: options.resolveFrom }),
48
51
  vitePluginAstroIntegrationOptsFallback(),
49
52
  vitePluginAstroVueFallback(),
50
53
  vitePluginAstroRoutesFallback(),
@@ -107,6 +110,7 @@ export async function createProductionAstroContainer(options: {
107
110
  integrations: Integration[];
108
111
  resolveClientModule: (specifier: string) => string | undefined;
109
112
  viteServer: ViteDevServer;
113
+ resolveFrom: string;
110
114
  }) {
111
115
  ensureAstroPassthroughImageService();
112
116
 
@@ -136,6 +140,15 @@ export async function createProductionAstroContainer(options: {
136
140
  return resolution;
137
141
  }
138
142
 
143
+ // Last resort: an island imported via a tsconfig path alias (e.g. `@/...`)
144
+ // never matches the static map under its raw specifier. Resolve the alias
145
+ // to an on-disk path and look that up in the built module map instead.
146
+ const abs = await resolveAliasedIsland(specifier, options.resolveFrom);
147
+
148
+ if (abs) {
149
+ return options.resolveClientModule(abs) ?? specifier;
150
+ }
151
+
139
152
  return specifier;
140
153
  }
141
154
  });
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