Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
67d5780
generate code snippets from csf file
kasperpeulen Oct 9, 2025
5efe183
Lot more cases
kasperpeulen Oct 10, 2025
446abe5
Cleanup code
kasperpeulen Oct 10, 2025
ef96eff
Inline args in JSX
kasperpeulen Oct 10, 2025
2d86537
More test examples
kasperpeulen Oct 10, 2025
3738a60
Add extra tests
kasperpeulen Oct 10, 2025
2442574
Fix type error
kasperpeulen Oct 10, 2025
18aaaf4
Template.bind expressions
kasperpeulen Oct 10, 2025
50ea146
Add componentManifestGenerator preset and implement for react
kasperpeulen Oct 15, 2025
addd229
Merge remote-tracking branch 'origin/10.1' into kasper/code-snippets
kasperpeulen Oct 15, 2025
4ed3856
Fix types
kasperpeulen Oct 15, 2025
a449847
Update code/core/src/core-server/dev-server.ts
kasperpeulen Oct 16, 2025
4ce972a
Update code/core/src/core-server/dev-server.ts
kasperpeulen Oct 16, 2025
1599fac
Update code/core/src/core-server/build-static.ts
kasperpeulen Oct 16, 2025
1bcd00b
Update code/core/src/core-server/build-static.ts
kasperpeulen Oct 16, 2025
bf9a49a
Improve dev server logic
kasperpeulen Oct 16, 2025
f2e3ddb
Merge remote-tracking branch 'origin/kasper/code-snippets' into kaspe…
kasperpeulen Oct 16, 2025
c0df22c
Add type
kasperpeulen Oct 16, 2025
25b197e
Fix lint
kasperpeulen Oct 16, 2025
18c671e
Use node logger
kasperpeulen Oct 16, 2025
11ac914
Fix
kasperpeulen Oct 16, 2025
bd91ab6
Fix typos
kasperpeulen Oct 16, 2025
1e84547
Remove redundant features fetch
kasperpeulen Oct 16, 2025
a6f6673
Apply feedback
kasperpeulen Oct 16, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 17 additions & 1 deletion code/core/src/core-server/build-static.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { cp, mkdir } from 'node:fs/promises';
import { cp, mkdir, writeFile } from 'node:fs/promises';
import { rm } from 'node:fs/promises';
import { join, relative, resolve } from 'node:path';

Expand All @@ -18,6 +18,7 @@ import { global } from '@storybook/global';
import picocolors from 'picocolors';

import { resolvePackageDir } from '../shared/utils/module';
import { type ComponentManifestGenerator } from '../types';
import { StoryIndexGenerator } from './utils/StoryIndexGenerator';
import { buildOrThrow } from './utils/build-or-throw';
import { copyAllStaticFilesRelativeToMain } from './utils/copy-all-static-files';
Expand Down Expand Up @@ -163,6 +164,21 @@ export async function buildStaticStandalone(options: BuildStaticStandaloneOption
initializedStoryIndexGenerator as Promise<StoryIndexGenerator>
)
);

if (features?.experimental_componentsManifest) {
const componentManifestGenerator: ComponentManifestGenerator = await presets.apply(
'componentManifestGenerator'
);
const indexGenerator = await initializedStoryIndexGenerator;
if (componentManifestGenerator && indexGenerator) {
const manifests = await componentManifestGenerator(indexGenerator);
await mkdir(join(options.outputDir, 'manifests'), { recursive: true });
await writeFile(
join(options.outputDir, 'manifests', 'components.json'),
JSON.stringify(manifests)
);
}
}
}

if (!core?.disableProjectJson) {
Expand Down
26 changes: 26 additions & 0 deletions code/core/src/core-server/dev-server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import polka from 'polka';
import invariant from 'tiny-invariant';

import { telemetry } from '../telemetry';
import { type ComponentManifestGenerator } from '../types';
import type { StoryIndexGenerator } from './utils/StoryIndexGenerator';
import { doTelemetry } from './utils/doTelemetry';
import { getManagerBuilder, getPreviewBuilder } from './utils/get-builders';
Expand Down Expand Up @@ -135,6 +136,31 @@ export async function storybookDevServer(options: Options) {
throw indexError;
}

const features = await options.presets.apply('features');
if (features?.experimental_componentsManifest) {
app.use('/manifests/components.json', async (req, res) => {
try {
const componentManifestGenerator: ComponentManifestGenerator = await options.presets.apply(
'componentManifestGenerator'
);
const indexGenerator = await initializedStoryIndexGenerator;
if (componentManifestGenerator && indexGenerator) {
const manifest = await componentManifestGenerator(indexGenerator);
res.setHeader('Content-Type', 'application/json');
res.end(JSON.stringify(manifest));
return;
}
res.statusCode = 400;
res.end('No component manifest generator configured.');
return;
} catch (e) {
logger.error(e instanceof Error ? e : String(e));
res.statusCode = 500;
res.end(e instanceof Error ? e.toString() : String(e));
return;
}
});
}
Comment on lines +141 to +163
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Ensure consistent JSON response format and appropriate status codes.

The endpoint has good error handling, but there are inconsistencies:

  1. Response format inconsistency: Success path returns JSON (line 150), but error paths return plain text (lines 154, 159). For a JSON API endpoint, all responses should use JSON format with appropriate Content-Type headers.

  2. Inappropriate 400 status code: Line 153 returns 400 (client error) for "No component manifest generator configured," but this is a server configuration issue, not a client request problem. Use 500 or 503 instead.

  3. Logger lacks context: Line 157's logger.error(e instanceof Error ? e : String(e)) doesn't explain what operation failed. Add a descriptive message like 'Failed to generate component manifest'.

Apply this diff to improve consistency and clarity:

-        res.statusCode = 400;
-        res.end('No component manifest generator configured.');
+        res.statusCode = 500;
+        res.setHeader('Content-Type', 'application/json');
+        res.end(JSON.stringify({ error: 'No component manifest generator configured.' }));
         return;
       } catch (e) {
-        logger.error(e instanceof Error ? e : String(e));
+        logger.error('Failed to generate component manifest:', e instanceof Error ? e : String(e));
         res.statusCode = 500;
-        res.end(e instanceof Error ? e.toString() : String(e));
+        res.setHeader('Content-Type', 'application/json');
+        res.end(JSON.stringify({ error: e instanceof Error ? e.message : String(e) }));
         return;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
app.use('/manifests/components.json', async (req, res) => {
try {
const componentManifestGenerator: ComponentManifestGenerator = await options.presets.apply(
'componentManifestGenerator'
);
const indexGenerator = await initializedStoryIndexGenerator;
if (componentManifestGenerator && indexGenerator) {
const manifest = await componentManifestGenerator(indexGenerator);
res.setHeader('Content-Type', 'application/json');
res.end(JSON.stringify(manifest));
return;
}
res.statusCode = 400;
res.end('No component manifest generator configured.');
return;
} catch (e) {
logger.error(e instanceof Error ? e : String(e));
res.statusCode = 500;
res.end(e instanceof Error ? e.toString() : String(e));
return;
}
});
}
app.use('/manifests/components.json', async (req, res) => {
try {
const componentManifestGenerator: ComponentManifestGenerator = await options.presets.apply(
'componentManifestGenerator'
);
const indexGenerator = await initializedStoryIndexGenerator;
if (componentManifestGenerator && indexGenerator) {
const manifest = await componentManifestGenerator(indexGenerator);
res.setHeader('Content-Type', 'application/json');
res.end(JSON.stringify(manifest));
return;
}
res.statusCode = 500;
res.setHeader('Content-Type', 'application/json');
res.end(JSON.stringify({ error: 'No component manifest generator configured.' }));
return;
} catch (e) {
logger.error('Failed to generate component manifest:', e instanceof Error ? e : String(e));
res.statusCode = 500;
res.setHeader('Content-Type', 'application/json');
res.end(JSON.stringify({ error: e instanceof Error ? e.message : String(e) }));
return;
}
});
🤖 Prompt for AI Agents
In code/core/src/core-server/dev-server.ts around lines 141 to 163, the endpoint
mixes JSON and plain-text responses, uses an incorrect 400 status for a
server/configuration problem, and logs errors without context; update the
handler to always return JSON with Content-Type: application/json, return a 500
(or 503) status for the missing componentManifestGenerator case (use 500 unless
you prefer 503 for temporary service unavailability), and change logger.error to
include a descriptive message like "Failed to generate component manifest" along
with the error; ensure both success and error bodies are JSON objects (e.g., {
ok: true, manifest: ... } and { ok: false, error: "message" }), and set
res.statusCode accordingly before res.end(JSON.stringify(...)).

// Now the preview has successfully started, we can count this as a 'dev' event.
doTelemetry(app, core, initializedStoryIndexGenerator, options);

Expand Down
2 changes: 1 addition & 1 deletion code/core/src/csf-tools/CsfFile.ts
Original file line number Diff line number Diff line change
Expand Up @@ -295,7 +295,7 @@ export class CsfFile {

_metaStatement: t.Statement | undefined;

_metaNode: t.Expression | undefined;
_metaNode: t.ObjectExpression | undefined;

_metaPath: NodePath<t.ExportDefaultDeclaration> | undefined;

Expand Down
Original file line number Diff line number Diff line change
@@ -1,14 +1,10 @@
// Inspired by Vitest fixture implementation:
// https://github.com/vitest-dev/vitest/blob/200a4349a2f85686bc7005dce686d9d1b48b84d2/packages/runner/src/fixture.ts
import type { PlayFunction } from 'storybook/internal/csf';
import { type Renderer } from 'storybook/internal/types';

export function mountDestructured<TRenderer extends Renderer>(
playFunction?: PlayFunction<TRenderer>
): boolean {
export function mountDestructured(playFunction?: (...args: any[]) => any): boolean {
return playFunction != null && getUsedProps(playFunction).includes('mount');
}
export function getUsedProps(fn: Function) {

export function getUsedProps(fn: (...args: any[]) => any) {
const match = fn.toString().match(/[^(]*\(([^)]*)/);

if (!match) {
Expand Down
14 changes: 14 additions & 0 deletions code/core/src/types/modules/core-common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import type { Server as NetServer } from 'net';
import type { Options as TelejsonOptions } from 'telejson';
import type { PackageJson as PackageJsonFromTypeFest } from 'type-fest';

import { type StoryIndexGenerator } from '../../core-server';
import type { Indexer, StoriesEntry } from './indexer';

/** ⚠️ This file contains internal WIP types they MUST NOT be exported outside this package for now! */
Expand Down Expand Up @@ -343,6 +344,16 @@ export type TagsOptions = Record<Tag, Partial<TagOptions>>;
* The interface for Storybook configuration used internally in presets The difference is that these
* values are the raw values, AKA, not wrapped with `PresetValue<>`
*/

export interface ComponentManifest {
id: string;
examples: { name: string; snippet: string }[];
}

export type ComponentManifestGenerator = (
storyIndexGenerator: StoryIndexGenerator
) => Promise<Record<string, ComponentManifest>>;

export interface StorybookConfigRaw {
/**
* Sets the addons you want to use with Storybook.
Expand All @@ -356,6 +367,7 @@ export interface StorybookConfigRaw {
*/
addons?: Preset[];
core?: CoreConfig;
componentManifestGenerator?: ComponentManifestGenerator;
staticDirs?: (DirectoryMapping | string)[];
logLevel?: string;
features?: {
Expand Down Expand Up @@ -453,6 +465,8 @@ export interface StorybookConfigRaw {
developmentModeForBuild?: boolean;
/** Only show input controls in Angular */
angularFilterNonInputControls?: boolean;

experimental_componentsManifest?: boolean;
};

build?: TestBuildConfig;
Expand Down
1 change: 1 addition & 0 deletions code/core/src/types/modules/indexer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ export interface BaseIndexEntry {
title: ComponentTitle;
tags?: Tag[];
importPath: Path;
componentPath?: Path;
}
export type StoryIndexEntry = BaseIndexEntry & {
type: 'story';
Expand Down
Loading