@storybook-astro/framework 1.1.1 → 1.2.0

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.
@@ -1,11 +1,12 @@
1
1
  import {
2
+ installPassthroughImageService,
2
3
  resolveSanitizationOptions,
3
4
  resolveStoryModuleMock,
4
5
  sanitizeRenderPayload,
5
6
  selectStoryRules,
6
7
  withStoryModuleMocks,
7
8
  withStoryRuleCleanups
8
- } from "./chunk-V76WSNSP.js";
9
+ } from "./chunk-OUEDTRBO.js";
9
10
  import "./chunk-G3PMV62Z.js";
10
11
 
11
12
  // src/middleware.ts
@@ -13,36 +14,7 @@ import { pathToFileURL } from "url";
13
14
  import { experimental_AstroContainer as AstroContainer } from "astro/container";
14
15
  import { addRenderers, resolveClientModules } from "virtual:astro-container-renderers";
15
16
  async function handlerFactory(_integrations, options) {
16
- if (!globalThis.astroAsset) {
17
- globalThis.astroAsset = {};
18
- }
19
- globalThis.astroAsset.imageService = {
20
- propertiesToHash: ["src"],
21
- validateOptions(options2) {
22
- return options2;
23
- },
24
- getURL(options2) {
25
- const src = options2.src;
26
- if (src != null && typeof src === "object" && "src" in src && typeof src.src === "string") {
27
- return src.src;
28
- }
29
- return typeof src === "string" ? src : "";
30
- },
31
- getHTMLAttributes(options2) {
32
- const { src, width, height, format, quality, densities, widths, formats, layout, priority, fit, position, background, ...attrs } = options2;
33
- const srcObj = src != null && typeof src === "object" ? src : null;
34
- return {
35
- ...attrs,
36
- width: width ?? srcObj?.width,
37
- height: height ?? srcObj?.height,
38
- loading: attrs.loading ?? "lazy",
39
- decoding: attrs.decoding ?? "async"
40
- };
41
- },
42
- getSrcSet() {
43
- return [];
44
- }
45
- };
17
+ installPassthroughImageService();
46
18
  const container = await AstroContainer.create({
47
19
  // Somewhat hacky way to force client-side Storybook's Vite to resolve modules properly
48
20
  resolve: async (specifier) => {
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/middleware.ts"],"sourcesContent":["import { pathToFileURL } from 'node:url';\nimport { experimental_AstroContainer as AstroContainer } from 'astro/container';\nimport type { Integration } from './integrations/index.ts';\nimport type { SanitizationOptions } from './lib/sanitization.ts';\nimport { resolveSanitizationOptions, sanitizeRenderPayload } from './lib/sanitization.ts';\nimport { resolveStoryModuleMock, withStoryModuleMocks } from './module-mocks.ts';\nimport { selectStoryRules, withStoryRuleCleanups } from './rules.ts';\nimport type { RenderStoryInput } from './types.ts';\nimport { addRenderers, resolveClientModules } from 'virtual:astro-container-renderers';\n\ntype ResolveRulesConfigModule = () => unknown | Promise<unknown>;\n\ntype AstroCreateResult = {\n createAstro?: (...args: unknown[]) => unknown;\n};\n\ntype AstroComponentFactory = ((\n result: AstroCreateResult,\n props: unknown,\n slots: unknown\n) => unknown) & {\n isAstroComponentFactory?: boolean;\n moduleId?: string;\n propagation?: unknown;\n};\n\nexport type HandlerProps = {\n component: string;\n args?: Record<string, unknown>;\n slots?: Record<string, unknown>;\n story?: RenderStoryInput;\n};\n\ntype HandlerFactoryOptions = {\n sanitization?: SanitizationOptions;\n rulesConfigFilePath?: string;\n resolveRulesConfigModule?: ResolveRulesConfigModule;\n loadModule?: (id: string) => Promise<{ default: unknown }>;\n};\n\nexport async function handlerFactory(_integrations: Integration[], options?: HandlerFactoryOptions) {\n // Inject a passthrough image service before any component renders.\n //\n // AstroContainer has no image service configuration API, and the default\n // getConfiguredImageService() tries to dynamically import \"virtual:image-service\"\n // which fails in astro6/Vite 7's module runner. Even when it succeeds (astro5),\n // the noop service still routes through /_image?href=... URLs that the Storybook\n // dev server cannot serve.\n //\n // Pre-populating globalThis.astroAsset.imageService bypasses the dynamic import\n // entirely. Our service returns the direct /@fs/... Vite URL from the ImageMetadata\n // object, which Vite can serve as a static asset in the browser.\n if (!globalThis.astroAsset) {\n (globalThis as Record<string, unknown>).astroAsset = {};\n }\n (globalThis.astroAsset as Record<string, unknown>).imageService = {\n propertiesToHash: ['src'],\n validateOptions(options: Record<string, unknown>) {\n return options;\n },\n getURL(options: { src: unknown }) {\n const src = options.src;\n\n if (src != null && typeof src === 'object' && 'src' in src && typeof (src as Record<string, unknown>).src === 'string') {\n // ImageMetadata object — return the /@fs/... Vite URL directly\n return (src as Record<string, unknown>).src as string;\n }\n\n return typeof src === 'string' ? src : '';\n },\n getHTMLAttributes(options: Record<string, unknown>) {\n const { src, width, height, format, quality, densities, widths, formats, layout, priority, fit, position, background, ...attrs } = options;\n const srcObj = src != null && typeof src === 'object' ? src as Record<string, unknown> : null;\n\n return {\n ...attrs,\n width: width ?? srcObj?.width,\n height: height ?? srcObj?.height,\n loading: (attrs.loading as string | undefined) ?? 'lazy',\n decoding: (attrs.decoding as string | undefined) ?? 'async',\n };\n },\n getSrcSet() {\n return [];\n }\n };\n\n const container = await AstroContainer.create({\n // Somewhat hacky way to force client-side Storybook's Vite to resolve modules properly\n resolve: async (specifier) => {\n const mockedModule = resolveStoryModuleMock(specifier);\n\n if (mockedModule) {\n return mockedModule;\n }\n\n if (specifier.startsWith('astro:scripts')) {\n return `/@id/${specifier}`;\n }\n\n const resolution = resolveClientModules(specifier);\n\n if (resolution) {\n return resolution;\n }\n\n return specifier;\n }\n });\n\n addRenderers(container);\n const sanitizationOptions = resolveSanitizationOptions(options?.sanitization);\n const loadModule =\n options?.loadModule ??\n ((id: string) => {\n const normalizedId = /^[a-zA-Z]:[/\\\\]/.test(id) ? pathToFileURL(id).href : id;\n\n return import(/* @vite-ignore */ normalizedId);\n });\n const componentCache = new Map<string, Promise<AstroComponentFactory>>();\n let renderQueue = Promise.resolve<void>(undefined);\n\n async function loadPatchedComponent(componentId: string, useCache = true) {\n if (!useCache) {\n const { default: component } = await loadModule(componentId);\n\n return patchCreateAstroCompat(component);\n }\n\n if (!componentCache.has(componentId)) {\n componentCache.set(componentId, (async () => {\n const { default: component } = await loadModule(componentId);\n\n return patchCreateAstroCompat(component);\n })());\n }\n\n const cachedComponent = componentCache.get(componentId);\n\n if (!cachedComponent) {\n throw new Error(`Failed to load Astro component: ${componentId}`);\n }\n\n try {\n return await cachedComponent;\n } catch (error) {\n // Drop failed entries so transient/module errors can recover on the next request.\n componentCache.delete(componentId);\n throw error;\n }\n }\n\n return async function handler(data: HandlerProps) {\n const executeRender = async () => {\n const rulesConfigModule = options?.resolveRulesConfigModule\n ? await options.resolveRulesConfigModule()\n : undefined;\n\n const selectedRules = await selectStoryRules({\n configModule: rulesConfigModule,\n configFilePath: options?.rulesConfigFilePath,\n story: data.story\n });\n\n return withStoryRuleCleanups(selectedRules.cleanups, async () => {\n return withStoryModuleMocks(selectedRules.moduleMocks, async () => {\n const patchedComponent = await loadPatchedComponent(\n data.component,\n selectedRules.moduleMocks.size === 0\n );\n const processedArgs = await processImageMetadata(data.args ?? {});\n const sanitizedPayload = sanitizeRenderPayload(\n {\n args: processedArgs,\n slots: data.slots ?? {}\n },\n sanitizationOptions\n );\n\n return container.renderToString(\n patchedComponent as Parameters<typeof container.renderToString>[0],\n {\n props: sanitizedPayload.args,\n slots: sanitizedPayload.slots\n }\n );\n });\n });\n };\n\n const resultPromise = renderQueue.then(executeRender, executeRender);\n\n renderQueue = resultPromise.then(\n () => undefined,\n () => undefined\n );\n\n return resultPromise;\n };\n}\n\nfunction patchCreateAstroCompat(component: unknown): AstroComponentFactory {\n if (typeof component !== 'function') {\n throw new Error('Expected Astro component factory to be a function.');\n }\n\n const originalComponent = component as AstroComponentFactory;\n const wrapped = ((result: AstroCreateResult, props: unknown, slots: unknown) => {\n if (result && typeof result.createAstro === 'function') {\n const originalCreateAstro = result.createAstro;\n const runtimeExpectsAstroGlobal = originalCreateAstro.length >= 3;\n\n result.createAstro = (...args: unknown[]) => {\n if (args.length === 3 && !runtimeExpectsAstroGlobal) {\n return originalCreateAstro(args[1], args[2]);\n }\n\n return originalCreateAstro(...args);\n };\n }\n\n return originalComponent(result, props, slots);\n }) as AstroComponentFactory;\n\n wrapped.isAstroComponentFactory = originalComponent.isAstroComponentFactory;\n wrapped.moduleId = originalComponent.moduleId;\n wrapped.propagation = originalComponent.propagation;\n\n return wrapped;\n}\n\nasync function processImageMetadata(\n args: Record<string, unknown>\n): Promise<Record<string, unknown>> {\n const processed: Record<string, unknown> = {};\n\n for (const [key, value] of Object.entries(args)) {\n if (isImageMetadata(value)) {\n // Keep ImageMetadata as a plain object — Astro's image service checks\n // isESMImportedImage (typeof src === 'object') and skips the /@fs/ string\n // validation that throws LocalImageUsedWrongly. Converting to a URL string\n // causes that error when the string starts with /@fs/.\n processed[key] = value;\n\n continue;\n }\n\n if (Array.isArray(value)) {\n processed[key] = await Promise.all(\n value.map(async (item) => {\n if (isImageMetadata(item)) {\n return item;\n }\n\n if (isRecord(item)) {\n return processImageMetadata(item);\n }\n\n return item;\n })\n );\n\n continue;\n }\n\n if (isRecord(value)) {\n processed[key] = await processImageMetadata(value);\n\n continue;\n }\n\n processed[key] = value;\n }\n\n return processed;\n}\n\nfunction isImageMetadata(value: unknown): value is Record<string, unknown> {\n return (\n isRecord(value) &&\n typeof value.src === 'string' &&\n ('width' in value || 'height' in value || 'format' in value)\n );\n}\n\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return typeof value === 'object' && value !== null;\n}\n"],"mappings":"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","names":["options"]}
1
+ {"version":3,"sources":["../src/middleware.ts"],"sourcesContent":["import { pathToFileURL } from 'node:url';\nimport { experimental_AstroContainer as AstroContainer } from 'astro/container';\nimport type { Integration } from './integrations/index.ts';\nimport { installPassthroughImageService } from './lib/passthrough-image-service.ts';\nimport type { SanitizationOptions } from './lib/sanitization.ts';\nimport { resolveSanitizationOptions, sanitizeRenderPayload } from './lib/sanitization.ts';\nimport { resolveStoryModuleMock, withStoryModuleMocks } from './module-mocks.ts';\nimport { selectStoryRules, withStoryRuleCleanups } from './rules.ts';\nimport type { RenderStoryInput } from './types.ts';\nimport { addRenderers, resolveClientModules } from 'virtual:astro-container-renderers';\n\ntype ResolveRulesConfigModule = () => unknown | Promise<unknown>;\n\ntype AstroCreateResult = {\n createAstro?: (...args: unknown[]) => unknown;\n};\n\ntype AstroComponentFactory = ((\n result: AstroCreateResult,\n props: unknown,\n slots: unknown\n) => unknown) & {\n isAstroComponentFactory?: boolean;\n moduleId?: string;\n propagation?: unknown;\n};\n\nexport type HandlerProps = {\n component: string;\n args?: Record<string, unknown>;\n slots?: Record<string, unknown>;\n story?: RenderStoryInput;\n};\n\ntype HandlerFactoryOptions = {\n sanitization?: SanitizationOptions;\n rulesConfigFilePath?: string;\n resolveRulesConfigModule?: ResolveRulesConfigModule;\n loadModule?: (id: string) => Promise<{ default: unknown }>;\n};\n\nexport async function handlerFactory(_integrations: Integration[], options?: HandlerFactoryOptions) {\n // Inject a passthrough image service before any component renders. See\n // `lib/passthrough-image-service.ts` for why this is necessary.\n installPassthroughImageService();\n\n const container = await AstroContainer.create({\n // Somewhat hacky way to force client-side Storybook's Vite to resolve modules properly\n resolve: async (specifier) => {\n const mockedModule = resolveStoryModuleMock(specifier);\n\n if (mockedModule) {\n return mockedModule;\n }\n\n if (specifier.startsWith('astro:scripts')) {\n return `/@id/${specifier}`;\n }\n\n const resolution = resolveClientModules(specifier);\n\n if (resolution) {\n return resolution;\n }\n\n return specifier;\n }\n });\n\n addRenderers(container);\n const sanitizationOptions = resolveSanitizationOptions(options?.sanitization);\n const loadModule =\n options?.loadModule ??\n ((id: string) => {\n const normalizedId = /^[a-zA-Z]:[/\\\\]/.test(id) ? pathToFileURL(id).href : id;\n\n return import(/* @vite-ignore */ normalizedId);\n });\n const componentCache = new Map<string, Promise<AstroComponentFactory>>();\n let renderQueue = Promise.resolve<void>(undefined);\n\n async function loadPatchedComponent(componentId: string, useCache = true) {\n if (!useCache) {\n const { default: component } = await loadModule(componentId);\n\n return patchCreateAstroCompat(component);\n }\n\n if (!componentCache.has(componentId)) {\n componentCache.set(componentId, (async () => {\n const { default: component } = await loadModule(componentId);\n\n return patchCreateAstroCompat(component);\n })());\n }\n\n const cachedComponent = componentCache.get(componentId);\n\n if (!cachedComponent) {\n throw new Error(`Failed to load Astro component: ${componentId}`);\n }\n\n try {\n return await cachedComponent;\n } catch (error) {\n // Drop failed entries so transient/module errors can recover on the next request.\n componentCache.delete(componentId);\n throw error;\n }\n }\n\n return async function handler(data: HandlerProps) {\n const executeRender = async () => {\n const rulesConfigModule = options?.resolveRulesConfigModule\n ? await options.resolveRulesConfigModule()\n : undefined;\n\n const selectedRules = await selectStoryRules({\n configModule: rulesConfigModule,\n configFilePath: options?.rulesConfigFilePath,\n story: data.story\n });\n\n return withStoryRuleCleanups(selectedRules.cleanups, async () => {\n return withStoryModuleMocks(selectedRules.moduleMocks, async () => {\n const patchedComponent = await loadPatchedComponent(\n data.component,\n selectedRules.moduleMocks.size === 0\n );\n const processedArgs = await processImageMetadata(data.args ?? {});\n const sanitizedPayload = sanitizeRenderPayload(\n {\n args: processedArgs,\n slots: data.slots ?? {}\n },\n sanitizationOptions\n );\n\n return container.renderToString(\n patchedComponent as Parameters<typeof container.renderToString>[0],\n {\n props: sanitizedPayload.args,\n slots: sanitizedPayload.slots\n }\n );\n });\n });\n };\n\n const resultPromise = renderQueue.then(executeRender, executeRender);\n\n renderQueue = resultPromise.then(\n () => undefined,\n () => undefined\n );\n\n return resultPromise;\n };\n}\n\nfunction patchCreateAstroCompat(component: unknown): AstroComponentFactory {\n if (typeof component !== 'function') {\n throw new Error('Expected Astro component factory to be a function.');\n }\n\n const originalComponent = component as AstroComponentFactory;\n const wrapped = ((result: AstroCreateResult, props: unknown, slots: unknown) => {\n if (result && typeof result.createAstro === 'function') {\n const originalCreateAstro = result.createAstro;\n const runtimeExpectsAstroGlobal = originalCreateAstro.length >= 3;\n\n result.createAstro = (...args: unknown[]) => {\n if (args.length === 3 && !runtimeExpectsAstroGlobal) {\n return originalCreateAstro(args[1], args[2]);\n }\n\n return originalCreateAstro(...args);\n };\n }\n\n return originalComponent(result, props, slots);\n }) as AstroComponentFactory;\n\n wrapped.isAstroComponentFactory = originalComponent.isAstroComponentFactory;\n wrapped.moduleId = originalComponent.moduleId;\n wrapped.propagation = originalComponent.propagation;\n\n return wrapped;\n}\n\nasync function processImageMetadata(\n args: Record<string, unknown>\n): Promise<Record<string, unknown>> {\n const processed: Record<string, unknown> = {};\n\n for (const [key, value] of Object.entries(args)) {\n if (isImageMetadata(value)) {\n // Keep ImageMetadata as a plain object — Astro's image service checks\n // isESMImportedImage (typeof src === 'object') and skips the /@fs/ string\n // validation that throws LocalImageUsedWrongly. Converting to a URL string\n // causes that error when the string starts with /@fs/.\n processed[key] = value;\n\n continue;\n }\n\n if (Array.isArray(value)) {\n processed[key] = await Promise.all(\n value.map(async (item) => {\n if (isImageMetadata(item)) {\n return item;\n }\n\n if (isRecord(item)) {\n return processImageMetadata(item);\n }\n\n return item;\n })\n );\n\n continue;\n }\n\n if (isRecord(value)) {\n processed[key] = await processImageMetadata(value);\n\n continue;\n }\n\n processed[key] = value;\n }\n\n return processed;\n}\n\nfunction isImageMetadata(value: unknown): value is Record<string, unknown> {\n return (\n isRecord(value) &&\n typeof value.src === 'string' &&\n ('width' in value || 'height' in value || 'format' in value)\n );\n}\n\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return typeof value === 'object' && value !== null;\n}\n"],"mappings":"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","names":[]}
@@ -1,4 +1,4 @@
1
- import { d as StorybookConfig } from '../types-Cvor6Tyi.js';
1
+ import { d as StorybookConfig } from '../types-C-jan6Px.js';
2
2
  import 'storybook/internal/types';
3
3
  import 'vite';
4
4
  import '../base-IRZo3zgK.js';
@@ -24,7 +24,7 @@ interface AstroRenderer extends WebRenderer {
24
24
  *
25
25
  * @param projectAnnotations - E.g. (import projectAnnotations from '../.storybook/preview')
26
26
  */
27
- declare function setProjectAnnotations(projectAnnotations: NamedOrDefaultProjectAnnotations<AstroRenderer> | NamedOrDefaultProjectAnnotations<AstroRenderer>[]): NormalizedProjectAnnotations<AstroRenderer>;
27
+ declare function setProjectAnnotations<R extends AstroRenderer = AstroRenderer>(projectAnnotations: NamedOrDefaultProjectAnnotations<R> | NamedOrDefaultProjectAnnotations<R>[]): NormalizedProjectAnnotations<R>;
28
28
  /**
29
29
  * Function that will receive a story along with meta (e.g. a default export from a .stories file)
30
30
  * and optionally projectAnnotations e.g. (import * as projectAnnotations from '../.storybook/preview')
@@ -51,7 +51,7 @@ declare function setProjectAnnotations(projectAnnotations: NamedOrDefaultProject
51
51
  * @param projectAnnotations - E.g. (import * as projectAnnotations from '../.storybook/preview') this can be applied automatically if you use `setProjectAnnotations` in your setup files.
52
52
  * @param exportsName - In case your story does not contain a name and you want it to have a name.
53
53
  */
54
- declare function composeStory<TArgs extends Args = Args>(story: StoryAnnotationsOrFn<AstroRenderer, TArgs>, componentAnnotations: ComponentAnnotations<AstroRenderer, TArgs>, projectAnnotations?: ProjectAnnotations<AstroRenderer>, exportsName?: string): storybook_internal_types.ComposedStoryFn<AstroRenderer, Partial<TArgs>>;
54
+ declare function composeStory<TArgs extends Args = Args, R extends AstroRenderer = AstroRenderer>(story: StoryAnnotationsOrFn<R, TArgs>, componentAnnotations: ComponentAnnotations<R, TArgs>, projectAnnotations?: ProjectAnnotations<R>, exportsName?: string): storybook_internal_types.ComposedStoryFn<AstroRenderer, Partial<TArgs>>;
55
55
  /**
56
56
  * Function that will receive a stories import (e.g. `import * as stories from './Button.stories'`)
57
57
  * and optionally a globalConfig (e.g. `import * from '../.storybook/preview`)
@@ -76,7 +76,7 @@ declare function composeStory<TArgs extends Args = Args>(story: StoryAnnotations
76
76
  * @param storiesImport - E.g. (import * as stories from './Button.stories')
77
77
  * @param projectAnnotations - E.g. (import * as globalConfig from '../.storybook/preview') this can be applied automatically if you use `setProjectAnnotations` in your setup files.
78
78
  */
79
- declare function composeStories<TModule extends Record<string, any>>(storiesImport: TModule, projectAnnotations?: ProjectAnnotations<AstroRenderer>): {
79
+ declare function composeStories<TModule extends Record<string, any>, R extends AstroRenderer = AstroRenderer>(storiesImport: TModule, projectAnnotations?: ProjectAnnotations<R>): {
80
80
  [K in keyof Omit<TModule, 'default'>]: any;
81
81
  };
82
82
 
package/dist/preset.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { e as StorybookConfigVite } from './types-Cvor6Tyi.js';
1
+ import { e as StorybookConfigVite } from './types-C-jan6Px.js';
2
2
  import 'storybook/internal/types';
3
3
  import 'vite';
4
4
  import './base-IRZo3zgK.js';
package/dist/preset.js CHANGED
@@ -1,8 +1,8 @@
1
1
  import {
2
2
  core,
3
3
  viteFinal
4
- } from "./chunk-4HECE7IW.js";
5
- import "./chunk-V76WSNSP.js";
4
+ } from "./chunk-PBISP7PA.js";
5
+ import "./chunk-OUEDTRBO.js";
6
6
  import "./chunk-POHTFYST.js";
7
7
  import "./chunk-E4LB75JN.js";
8
8
  import "./chunk-DNGQBPT7.js";
package/dist/testing.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { A as AstroRenderer, a as composeStory$1, s as setProjectAnnotations$1 } from './portable-stories-BvdaQigq.js';
1
+ import { A as AstroRenderer, a as composeStory$1, s as setProjectAnnotations$1 } from './portable-stories-DXT_GOf6.js';
2
2
  import { Store_CSFExports, ProjectAnnotations } from 'storybook/internal/types';
3
3
 
4
4
  declare function composeStories<TModule extends Store_CSFExports<AstroRenderer> & Record<string, unknown>>(storiesImport: TModule, projectAnnotations?: ProjectAnnotations<AstroRenderer>): { [K in keyof Omit<TModule, "default">]: any; };
package/dist/testing.js CHANGED
@@ -2,7 +2,7 @@ import {
2
2
  composeStories,
3
3
  composeStory,
4
4
  setProjectAnnotations
5
- } from "./chunk-5EF25G5S.js";
5
+ } from "./chunk-KXAAX3GN.js";
6
6
  import {
7
7
  renderViaTestingRendererDaemon,
8
8
  runWithWorkingDirectory
@@ -41,6 +41,15 @@ type StaticFrameworkOptions = BaseFrameworkOptions & {
41
41
  renderMode: 'static';
42
42
  storyRules?: StoryRulesOptions;
43
43
  server?: never;
44
+ /**
45
+ * Additional source directories (relative to `resolveFrom`) to scan for
46
+ * hydratable client components (JSX/TSX/Vue/Svelte). Use this when stories
47
+ * reference components that live outside the default `src/components` scan
48
+ * root — for example, workspace packages included in the `stories` globs.
49
+ *
50
+ * @example ['../../packages/components/src']
51
+ */
52
+ componentRoots?: string[];
44
53
  };
45
54
  type FrameworkOptions = ServerFrameworkOptions | StaticFrameworkOptions;
46
55
  type StorybookConfigFramework = {
@@ -16,4 +16,22 @@ declare function defineConfig(options: TestingDefineConfig): ({ mode: viteMode,
16
16
  declare function vitestPatchForSolidJs(): AstroIntegration;
17
17
  declare function cjsInteropPlugin(): Plugin;
18
18
 
19
- export { type TestingDefineConfig, cjsInteropPlugin, defineConfig, vitestPatchForSolidJs };
19
+ /**
20
+ * Vite plugin that patches Astro 6's client-side .astro file transforms for Storybook.
21
+ *
22
+ * In Astro 6, the client-side transform of .astro files produces a stub function that
23
+ * throws "Astro components cannot be used in the browser" without setting the
24
+ * `isAstroComponentFactory` marker. Storybook's renderer relies on this marker to detect
25
+ * Astro components and route them to server-side rendering via the Container API.
26
+ *
27
+ * This plugin also preserves the component's scoped CSS by importing the style sub-modules
28
+ * that the Astro Vite plugin exposes. Without this, the client-side stub would strip all
29
+ * CSS since Astro 6 no longer includes style imports in client-side .astro transforms.
30
+ *
31
+ * During builds, Astro's compile metadata cache is not populated for client-side transforms,
32
+ * so style sub-module imports would fail. Instead, raw CSS is extracted directly from the
33
+ * .astro source and inlined.
34
+ */
35
+ declare function vitePluginAstroComponentMarker(): PluginOption;
36
+
37
+ export { type TestingDefineConfig, cjsInteropPlugin, defineConfig, vitePluginAstroComponentMarker, vitestPatchForSolidJs };
@@ -241,6 +241,7 @@ function defineConfig2(options) {
241
241
  export {
242
242
  cjsInteropPlugin,
243
243
  defineConfig2 as defineConfig,
244
+ vitePluginAstroComponentMarker,
244
245
  vitestPatchForSolidJs
245
246
  };
246
247
  //# sourceMappingURL=index.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@storybook-astro/framework",
3
- "version": "1.1.1",
3
+ "version": "1.2.0",
4
4
  "description": "Community-supported Storybook framework for Astro 5 & 6 components",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -115,8 +115,8 @@
115
115
  "@storybook/svelte": "^10.0.0",
116
116
  "@storybook/vue3": "^10.0.0",
117
117
  "@vitejs/plugin-react": "^5.0.0",
118
- "@vitejs/plugin-vue": "^5.2.3",
119
- "@vitejs/plugin-vue-jsx": "^4.1.2",
118
+ "@vitejs/plugin-vue": "^5.2.3 || ^6.0.0",
119
+ "@vitejs/plugin-vue-jsx": "^4.1.2 || ^5.0.0",
120
120
  "astro": "^5.5.3 || ^6.0.0",
121
121
  "storybook": "^10.0.0",
122
122
  "storybook-solidjs": "^1.0.0-beta.7",
@@ -167,7 +167,7 @@
167
167
  }
168
168
  },
169
169
  "dependencies": {
170
- "@storybook-astro/renderer": "1.1.1",
170
+ "@storybook-astro/renderer": "1.2.0",
171
171
  "hono": "^4.11.12",
172
172
  "sanitize-html": "^2.17.0",
173
173
  "vite": "^6.4.1 || ^7.0.0 || ^8.0.0"
package/src/index.ts CHANGED
@@ -11,8 +11,18 @@ import { definePreview as definePreviewBase, type PreviewAddon, type InferTypes,
11
11
  import type { ProjectAnnotations } from 'storybook/internal/types';
12
12
  import type { AstroRenderer } from './portable-stories.ts';
13
13
 
14
- /** Preview configuration type for `.storybook/preview.ts` in Astro projects. */
15
- export type Preview = ProjectAnnotations<AstroRenderer>;
14
+ /**
15
+ * Preview configuration type for `.storybook/preview.ts` in Astro projects.
16
+ * Reflects the full type returned by `definePreview`, including addon type extensions.
17
+ * Use this to annotate your preview module when needed:
18
+ *
19
+ * ```ts
20
+ * import type { Preview } from '@storybook-astro/framework';
21
+ * const preview: Preview = { ... };
22
+ * export default preview;
23
+ * ```
24
+ */
25
+ export type Preview<Addons extends PreviewAddon<never>[] = []> = CsfPreview<AstroRenderer & InferTypes<Addons>>;
16
26
 
17
27
  // Export portable stories functionality
18
28
  export {
@@ -0,0 +1,76 @@
1
+ /**
2
+ * Install a passthrough image service on `globalThis.astroAsset.imageService`.
3
+ *
4
+ * AstroContainer has no image service configuration API, and the default
5
+ * `getConfiguredImageService()` tries to dynamically import "virtual:image-service"
6
+ * which fails in astro6/Vite 7's module runner. Even when it succeeds (astro5),
7
+ * the noop service still routes through /_image?href=... URLs that the Storybook
8
+ * dev server cannot serve.
9
+ *
10
+ * Pre-populating `globalThis.astroAsset.imageService` bypasses the dynamic import
11
+ * entirely — `getConfiguredImageService()` checks this global first and returns
12
+ * it without going through the broken virtual module. Our service returns the
13
+ * direct /@fs/... Vite URL from the ImageMetadata object, which Vite can serve
14
+ * as a static asset in the browser; in static (build-time prerender) mode the
15
+ * URL ends up being rewritten to a content-hashed Rollup asset.
16
+ *
17
+ * This must be called on the same Node process that hosts AstroContainer,
18
+ * before `container.renderToString()` is invoked.
19
+ */
20
+ export function installPassthroughImageService() {
21
+ if (!globalThis.astroAsset) {
22
+ (globalThis as Record<string, unknown>).astroAsset = {};
23
+ }
24
+
25
+ (globalThis.astroAsset as Record<string, unknown>).imageService = {
26
+ propertiesToHash: ['src'],
27
+ validateOptions(options: Record<string, unknown>) {
28
+ return options;
29
+ },
30
+ getURL(options: { src: unknown }) {
31
+ const src = options.src;
32
+
33
+ if (
34
+ src != null &&
35
+ typeof src === 'object' &&
36
+ 'src' in src &&
37
+ typeof (src as Record<string, unknown>).src === 'string'
38
+ ) {
39
+ // ImageMetadata object — return the /@fs/... Vite URL directly
40
+ return (src as Record<string, unknown>).src as string;
41
+ }
42
+
43
+ return typeof src === 'string' ? src : '';
44
+ },
45
+ getHTMLAttributes(options: Record<string, unknown>) {
46
+ const {
47
+ src,
48
+ width,
49
+ height,
50
+ format: _format,
51
+ quality: _quality,
52
+ densities: _densities,
53
+ widths: _widths,
54
+ formats: _formats,
55
+ layout: _layout,
56
+ priority: _priority,
57
+ fit: _fit,
58
+ position: _position,
59
+ background: _background,
60
+ ...attrs
61
+ } = options;
62
+ const srcObj = src != null && typeof src === 'object' ? (src as Record<string, unknown>) : null;
63
+
64
+ return {
65
+ ...attrs,
66
+ width: width ?? srcObj?.width,
67
+ height: height ?? srcObj?.height,
68
+ loading: (attrs.loading as string | undefined) ?? 'lazy',
69
+ decoding: (attrs.decoding as string | undefined) ?? 'async'
70
+ };
71
+ },
72
+ getSrcSet() {
73
+ return [];
74
+ }
75
+ };
76
+ }
package/src/middleware.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  import { pathToFileURL } from 'node:url';
2
2
  import { experimental_AstroContainer as AstroContainer } from 'astro/container';
3
3
  import type { Integration } from './integrations/index.ts';
4
+ import { installPassthroughImageService } from './lib/passthrough-image-service.ts';
4
5
  import type { SanitizationOptions } from './lib/sanitization.ts';
5
6
  import { resolveSanitizationOptions, sanitizeRenderPayload } from './lib/sanitization.ts';
6
7
  import { resolveStoryModuleMock, withStoryModuleMocks } from './module-mocks.ts';
@@ -39,51 +40,9 @@ type HandlerFactoryOptions = {
39
40
  };
40
41
 
41
42
  export async function handlerFactory(_integrations: Integration[], options?: HandlerFactoryOptions) {
42
- // Inject a passthrough image service before any component renders.
43
- //
44
- // AstroContainer has no image service configuration API, and the default
45
- // getConfiguredImageService() tries to dynamically import "virtual:image-service"
46
- // which fails in astro6/Vite 7's module runner. Even when it succeeds (astro5),
47
- // the noop service still routes through /_image?href=... URLs that the Storybook
48
- // dev server cannot serve.
49
- //
50
- // Pre-populating globalThis.astroAsset.imageService bypasses the dynamic import
51
- // entirely. Our service returns the direct /@fs/... Vite URL from the ImageMetadata
52
- // object, which Vite can serve as a static asset in the browser.
53
- if (!globalThis.astroAsset) {
54
- (globalThis as Record<string, unknown>).astroAsset = {};
55
- }
56
- (globalThis.astroAsset as Record<string, unknown>).imageService = {
57
- propertiesToHash: ['src'],
58
- validateOptions(options: Record<string, unknown>) {
59
- return options;
60
- },
61
- getURL(options: { src: unknown }) {
62
- const src = options.src;
63
-
64
- if (src != null && typeof src === 'object' && 'src' in src && typeof (src as Record<string, unknown>).src === 'string') {
65
- // ImageMetadata object — return the /@fs/... Vite URL directly
66
- return (src as Record<string, unknown>).src as string;
67
- }
68
-
69
- return typeof src === 'string' ? src : '';
70
- },
71
- getHTMLAttributes(options: Record<string, unknown>) {
72
- const { src, width, height, format, quality, densities, widths, formats, layout, priority, fit, position, background, ...attrs } = options;
73
- const srcObj = src != null && typeof src === 'object' ? src as Record<string, unknown> : null;
74
-
75
- return {
76
- ...attrs,
77
- width: width ?? srcObj?.width,
78
- height: height ?? srcObj?.height,
79
- loading: (attrs.loading as string | undefined) ?? 'lazy',
80
- decoding: (attrs.decoding as string | undefined) ?? 'async',
81
- };
82
- },
83
- getSrcSet() {
84
- return [];
85
- }
86
- };
43
+ // Inject a passthrough image service before any component renders. See
44
+ // `lib/passthrough-image-service.ts` for why this is necessary.
45
+ installPassthroughImageService();
87
46
 
88
47
  const container = await AstroContainer.create({
89
48
  // Somewhat hacky way to force client-side Storybook's Vite to resolve modules properly
@@ -0,0 +1,28 @@
1
+ /**
2
+ * Type-level tests for portable-stories utilities.
3
+ *
4
+ * These tests verify that setProjectAnnotations, composeStory, and composeStories
5
+ * accept the widened renderer type produced by definePreview when addons are used.
6
+ * Without the generic R parameter, users get "has no properties in common" errors
7
+ * when passing a definePreview result to setProjectAnnotations.
8
+ *
9
+ * See: https://github.com/storybook-astro/storybook-astro/issues/58
10
+ */
11
+ import { expectTypeOf, test } from 'vitest';
12
+ import type { NormalizedProjectAnnotations } from 'storybook/internal/types';
13
+ import { definePreview, setProjectAnnotations, type AstroRenderer } from './index.ts';
14
+
15
+ test('setProjectAnnotations accepts a module whose default export is a widened Preview type', () => {
16
+ // This is the shape of `import * as preview from '.storybook/preview'`
17
+ // when the preview uses definePreview with addons.
18
+ type PreviewModule = { default: ReturnType<typeof definePreview<[]>> };
19
+
20
+ expectTypeOf<(annotations: PreviewModule) => NormalizedProjectAnnotations<AstroRenderer>>()
21
+ .toBeCallableWith({ default: definePreview({}) });
22
+ });
23
+
24
+ test('setProjectAnnotations return type narrows correctly for base AstroRenderer', () => {
25
+ const result = setProjectAnnotations([]);
26
+
27
+ expectTypeOf(result).toMatchTypeOf<NormalizedProjectAnnotations<AstroRenderer>>();
28
+ });
@@ -89,12 +89,12 @@ const render = (args: Args, context?: any) => {
89
89
  *
90
90
  * @param projectAnnotations - E.g. (import projectAnnotations from '../.storybook/preview')
91
91
  */
92
- export function setProjectAnnotations(
92
+ export function setProjectAnnotations<R extends AstroRenderer = AstroRenderer>(
93
93
  projectAnnotations:
94
- | NamedOrDefaultProjectAnnotations<AstroRenderer>
95
- | NamedOrDefaultProjectAnnotations<AstroRenderer>[]
96
- ): NormalizedProjectAnnotations<AstroRenderer> {
97
- return originalSetProjectAnnotations<AstroRenderer>(projectAnnotations);
94
+ | NamedOrDefaultProjectAnnotations<R>
95
+ | NamedOrDefaultProjectAnnotations<R>[]
96
+ ): NormalizedProjectAnnotations<R> {
97
+ return originalSetProjectAnnotations<R>(projectAnnotations);
98
98
  }
99
99
 
100
100
  /**
@@ -123,10 +123,10 @@ export function setProjectAnnotations(
123
123
  * @param projectAnnotations - E.g. (import * as projectAnnotations from '../.storybook/preview') this can be applied automatically if you use `setProjectAnnotations` in your setup files.
124
124
  * @param exportsName - In case your story does not contain a name and you want it to have a name.
125
125
  */
126
- export function composeStory<TArgs extends Args = Args>(
127
- story: StoryAnnotationsOrFn<AstroRenderer, TArgs>,
128
- componentAnnotations: ComponentAnnotations<AstroRenderer, TArgs>,
129
- projectAnnotations?: ProjectAnnotations<AstroRenderer>,
126
+ export function composeStory<TArgs extends Args = Args, R extends AstroRenderer = AstroRenderer>(
127
+ story: StoryAnnotationsOrFn<R, TArgs>,
128
+ componentAnnotations: ComponentAnnotations<R, TArgs>,
129
+ projectAnnotations?: ProjectAnnotations<R>,
130
130
  exportsName?: string
131
131
  ) {
132
132
  // Merge project annotations with Astro renderer
@@ -139,7 +139,7 @@ export function composeStory<TArgs extends Args = Args>(
139
139
 
140
140
  return originalComposeStory<AstroRenderer, TArgs>(
141
141
  story as any,
142
- componentAnnotations,
142
+ componentAnnotations as any,
143
143
  mergedProjectAnnotations as any,
144
144
  exportsName as any
145
145
  );
@@ -169,9 +169,9 @@ export function composeStory<TArgs extends Args = Args>(
169
169
  * @param storiesImport - E.g. (import * as stories from './Button.stories')
170
170
  * @param projectAnnotations - E.g. (import * as globalConfig from '../.storybook/preview') this can be applied automatically if you use `setProjectAnnotations` in your setup files.
171
171
  */
172
- export function composeStories<TModule extends Record<string, any>>(
172
+ export function composeStories<TModule extends Record<string, any>, R extends AstroRenderer = AstroRenderer>(
173
173
  storiesImport: TModule,
174
- projectAnnotations?: ProjectAnnotations<AstroRenderer>
174
+ projectAnnotations?: ProjectAnnotations<R>
175
175
  ): { [K in keyof Omit<TModule, 'default'>]: any } {
176
176
  // Merge project annotations with Astro renderer
177
177
  const mergedProjectAnnotations: any = projectAnnotations ? {
package/src/types.ts CHANGED
@@ -37,6 +37,15 @@ type StaticFrameworkOptions = BaseFrameworkOptions & {
37
37
  renderMode: 'static';
38
38
  storyRules?: StoryRulesOptions;
39
39
  server?: never;
40
+ /**
41
+ * Additional source directories (relative to `resolveFrom`) to scan for
42
+ * hydratable client components (JSX/TSX/Vue/Svelte). Use this when stories
43
+ * reference components that live outside the default `src/components` scan
44
+ * root — for example, workspace packages included in the `stories` globs.
45
+ *
46
+ * @example ['../../packages/components/src']
47
+ */
48
+ componentRoots?: string[];
40
49
  };
41
50
 
42
51
  export type FrameworkOptions = ServerFrameworkOptions | StaticFrameworkOptions;