Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 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
98f121b
Add component name and tests
kasperpeulen Oct 16, 2025
bd91ab6
Fix typos
kasperpeulen Oct 16, 2025
d3f1940
Merge branch 'kasper/code-snippets' into kasper/manifest-component-name
kasperpeulen Oct 16, 2025
b865df9
Add component description to manifest
kasperpeulen Oct 16, 2025
51cdfc7
Extract jsdoc tags
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
20 changes: 19 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,23 @@ export async function buildStaticStandalone(options: BuildStaticStandaloneOption
initializedStoryIndexGenerator as Promise<StoryIndexGenerator>
)
);

const features = await presets.apply('features');

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)
);
}
}
Comment on lines +168 to +183
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

Remove shadowing variable and use existing features.

Line 168 creates a shadow variable features that was already defined and fetched on line 101. This is redundant and can lead to confusion.

Apply this diff to use the existing features variable:

-    const features = await presets.apply('features');
-
-    if (features?.experimental_componentsManifest) {
+    if (features?.experimental_componentsManifest) {
📝 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
const features = await presets.apply('features');
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 (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)
);
}
}
🤖 Prompt for AI Agents
In code/core/src/core-server/build-static.ts around lines 168 to 183 there is a
redundant re-declaration of const features = await presets.apply('features')
that shadows the features variable already fetched earlier (around line 101);
remove this second declaration and reuse the existing features variable in the
if-check (i.e., change the code to use the previously obtained features value),
ensuring the subsequent logic that reads
features?.experimental_componentsManifest remains unchanged and that TypeScript
types still align with the original features definition.

}

if (!core?.disableProjectJson) {
Expand Down
27 changes: 27 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,32 @@ export async function storybookDevServer(options: Options) {
throw indexError;
}

app.use('/manifests/components.json', async (req, res) => {
try {
const features = await options.presets.apply('features');
if (!features?.experimental_componentsManifest) {
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 +139 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 | 🔴 Critical

Fix inverted feature flag logic.

Line 142 checks !features?.experimental_componentsManifest (NOT enabled), but the code inside this block generates and serves the manifest. This logic appears inverted - manifests should be generated when the flag IS enabled, not when it's disabled.

Apply this diff to fix the logic:

       const features = await options.presets.apply('features');
-      if (!features?.experimental_componentsManifest) {
+      if (features?.experimental_componentsManifest) {
         const componentManifestGenerator: ComponentManifestGenerator = await options.presets.apply(
           'componentManifestGenerator'
         );
📝 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 features = await options.presets.apply('features');
if (!features?.experimental_componentsManifest) {
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 features = await options.presets.apply('features');
if (features?.experimental_componentsManifest) {
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;
}
});
🤖 Prompt for AI Agents
In code/core/src/core-server/dev-server.ts around lines 139 to 163, the feature
flag check is inverted: the block that generates and serves the component
manifest runs when !features?.experimental_componentsManifest (i.e. when the
flag is false or missing). Change the condition to check for the flag being
enabled (features?.experimental_componentsManifest) so the manifest is generated
only when the experimental_componentsManifest feature is true; otherwise keep
returning the 400 "No component manifest generator configured." response. Ensure
the negation is removed and the surrounding flow/returns remain unchanged.


// 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
15 changes: 15 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,17 @@ 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;
name?: 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 +368,7 @@ export interface StorybookConfigRaw {
*/
addons?: Preset[];
core?: CoreConfig;
componentManifestGenerator?: ComponentManifestGenerator;
staticDirs?: (DirectoryMapping | string)[];
logLevel?: string;
features?: {
Expand Down Expand Up @@ -453,6 +466,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
3 changes: 3 additions & 0 deletions code/renderers/react/__mocks__/fs/promises.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
const { fs } = require('memfs');

module.exports = fs.promises;
1 change: 1 addition & 0 deletions code/renderers/react/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@
"acorn-jsx": "^5.3.1",
"acorn-walk": "^7.2.0",
"babel-plugin-react-docgen": "^4.2.1",
"comment-parser": "^1.4.1",
"es-toolkit": "^1.36.0",
"escodegen": "^2.1.0",
"expect-type": "^0.15.0",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
/**
* This is heavily based on the react-docgen `displayNameHandler`
* (https://github.com/reactjs/react-docgen/blob/26c90c0dd105bf83499a83826f2a6ff7a724620d/src/handlers/displayNameHandler.ts)
* but instead defines an `actualName` property on the generated docs that is taken first from the
* component's actual name. This addresses an issue where the name that the generated docs are
* stored under is incorrectly named with the `displayName` and not the component's actual name.
*
* This is inspired by `actualNameHandler` from
* https://github.com/storybookjs/babel-plugin-react-docgen, but is modified directly from
* displayNameHandler, using the same approach as babel-plugin-react-docgen.
*/
import type { Handler, NodePath, babelTypes as t } from 'react-docgen';
import { utils } from 'react-docgen';

const { getNameOrValue, isReactForwardRefCall } = utils;

const actualNameHandler: Handler = function actualNameHandler(documentation, componentDefinition) {
documentation.set('definedInFile', componentDefinition.hub.file.opts.filename);

if (
(componentDefinition.isClassDeclaration() || componentDefinition.isFunctionDeclaration()) &&
componentDefinition.has('id')
) {
documentation.set(
'actualName',
getNameOrValue(componentDefinition.get('id') as NodePath<t.Identifier>)
);
} else if (
componentDefinition.isArrowFunctionExpression() ||
componentDefinition.isFunctionExpression() ||
isReactForwardRefCall(componentDefinition)
) {
let currentPath: NodePath = componentDefinition;

while (currentPath.parentPath) {
if (currentPath.parentPath.isVariableDeclarator()) {
documentation.set('actualName', getNameOrValue(currentPath.parentPath.get('id')));
return;
}
if (currentPath.parentPath.isAssignmentExpression()) {
const leftPath = currentPath.parentPath.get('left');

if (leftPath.isIdentifier() || leftPath.isLiteral()) {
documentation.set('actualName', getNameOrValue(leftPath));
return;
}
}

currentPath = currentPath.parentPath;
}
// Could not find an actual name
documentation.set('actualName', '');
}
};

export default actualNameHandler;
75 changes: 75 additions & 0 deletions code/renderers/react/src/component-manifest/docgen-resolver.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import { extname } from 'node:path';

import resolve from 'resolve';

export class ReactDocgenResolveError extends Error {
// the magic string that react-docgen uses to check if a module is ignored
readonly code = 'MODULE_NOT_FOUND';

constructor(filename: string) {
super(`'${filename}' was ignored by react-docgen.`);
}
}

/* The below code was copied from:
* https://github.com/reactjs/react-docgen/blob/df2daa8b6f0af693ecc3c4dc49f2246f60552bcb/packages/react-docgen/src/importer/makeFsImporter.ts#L14-L63
* because it wasn't exported from the react-docgen package.
* watch out: when updating this code, also update the code in code/presets/react-webpack/src/loaders/docgen-resolver.ts
*/

// These extensions are sorted by priority
// resolve() will check for files in the order these extensions are sorted
export const RESOLVE_EXTENSIONS = [
'.js',
'.cts', // These were originally not in the code, I added them
'.mts', // These were originally not in the code, I added them
'.ctsx', // These were originally not in the code, I added them
'.mtsx', // These were originally not in the code, I added them
'.ts',
'.tsx',
'.mjs',
'.cjs',
'.mts',
'.cts',
'.jsx',
];
Comment on lines +22 to +35
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 | 🟡 Minor

Duplicate extension entries detected.

Lines 25-27 and 32-33 both include .cts and .mts extensions, resulting in duplicates within the RESOLVE_EXTENSIONS array. This could lead to unnecessary resolution attempts.

Apply this diff to remove the duplicates:

 export const RESOLVE_EXTENSIONS = [
   '.js',
   '.cts', // These were originally not in the code, I added them
   '.mts', // These were originally not in the code, I added them
   '.ctsx', // These were originally not in the code, I added them
   '.mtsx', // These were originally not in the code, I added them
   '.ts',
   '.tsx',
   '.mjs',
   '.cjs',
-  '.mts',
-  '.cts',
   '.jsx',
 ];
📝 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
export const RESOLVE_EXTENSIONS = [
'.js',
'.cts', // These were originally not in the code, I added them
'.mts', // These were originally not in the code, I added them
'.ctsx', // These were originally not in the code, I added them
'.mtsx', // These were originally not in the code, I added them
'.ts',
'.tsx',
'.mjs',
'.cjs',
'.mts',
'.cts',
'.jsx',
];
export const RESOLVE_EXTENSIONS = [
'.js',
'.cts', // These were originally not in the code, I added them
'.mts', // These were originally not in the code, I added them
'.ctsx', // These were originally not in the code, I added them
'.mtsx', // These were originally not in the code, I added them
'.ts',
'.tsx',
'.mjs',
'.cjs',
'.jsx',
];
🤖 Prompt for AI Agents
In code/renderers/react/src/component-manifest/docgen-resolver.ts around lines
22 to 35, the RESOLVE_EXTENSIONS array contains duplicate entries for '.cts' and
'.mts' which should be removed; update the array to include each extension only
once (remove the repeated '.cts' and '.mts' entries) while preserving the
intended ordering of unique extensions.


export function defaultLookupModule(filename: string, basedir: string): string {
const resolveOptions = {
basedir,
extensions: RESOLVE_EXTENSIONS,
// we do not need to check core modules as we cannot import them anyway
includeCoreModules: false,
};

try {
return resolve.sync(filename, resolveOptions);
} catch (error) {
const ext = extname(filename);
let newFilename: string;

// if we try to import a JavaScript file it might be that we are actually pointing to
// a TypeScript file. This can happen in ES modules as TypeScript requires to import other
// TypeScript files with .js extensions
// https://www.typescriptlang.org/docs/handbook/esm-node.html#type-in-packagejson-and-new-extensions
switch (ext) {
case '.js':
case '.mjs':
case '.cjs':
newFilename = `${filename.slice(0, -2)}ts`;
break;

case '.jsx':
newFilename = `${filename.slice(0, -3)}tsx`;
break;
default:
throw error;
}

return resolve.sync(newFilename, {
...resolveOptions,
// we already know that there is an extension at this point, so no need to check other extensions
extensions: [],
});
}
}
Loading
Loading