Skip to content
Merged
Show file tree
Hide file tree
Changes from 22 commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
a5f4646
Add automatic component imports
kasperpeulen Oct 31, 2025
17a3089
Add automatic component imports
kasperpeulen Oct 31, 2025
9331bf7
Improve
kasperpeulen Oct 31, 2025
2b072ba
Use the package.json belonging to the story
kasperpeulen Nov 1, 2025
835417c
Get fallback components to work
kasperpeulen Nov 1, 2025
8ba3b0b
Refactor
kasperpeulen Nov 1, 2025
8d40e43
Fix type
kasperpeulen Nov 1, 2025
ce25a2f
Fix type
kasperpeulen Nov 1, 2025
7e9a30a
Fix type
kasperpeulen Nov 1, 2025
acf5925
Fix tests
kasperpeulen Nov 1, 2025
2a05fc4
Cleanup react docgen
kasperpeulen Nov 3, 2025
3de6fad
Cleanup react docgen integration
kasperpeulen Nov 3, 2025
80b0ad2
Cleanup getComponentImports
kasperpeulen Nov 3, 2025
08bdc82
Fix
kasperpeulen Nov 3, 2025
8151722
Refactor
kasperpeulen Nov 3, 2025
50be8d7
Filter by manifest tag and include story descriptions/summaries
kasperpeulen Nov 3, 2025
70e8504
Fix auto imports
kasperpeulen Nov 3, 2025
7ab7295
Fix tests
kasperpeulen Nov 3, 2025
d9cc661
Better barrel file support
kasperpeulen Nov 3, 2025
e235682
Prettier
kasperpeulen Nov 4, 2025
3f0b505
Refactor tests
kasperpeulen Nov 4, 2025
f06a97e
Fix lint
kasperpeulen Nov 4, 2025
946327d
Fix feedback
kasperpeulen Nov 4, 2025
10ef28f
Fix feedback
kasperpeulen Nov 4, 2025
8f96d8c
Fix feedback
kasperpeulen Nov 4, 2025
41d93f3
Fix
kasperpeulen Nov 4, 2025
26bbbda
Refactor
kasperpeulen Nov 4, 2025
5e59d7a
Refactor
kasperpeulen Nov 4, 2025
f4caa77
Clean up
kasperpeulen Nov 4, 2025
2780b84
Pretty
kasperpeulen Nov 4, 2025
68d91cf
Fix title
kasperpeulen Nov 4, 2025
688057b
Feedback
kasperpeulen Nov 4, 2025
7763851
Better basedir for tsconfig resolution
kasperpeulen Nov 4, 2025
8829d71
Merge remote-tracking branch 'origin/next' into kasper/manifest-auto-…
kasperpeulen Nov 4, 2025
947c37e
Address feedback
kasperpeulen Nov 4, 2025
6f87e22
Address feedback
kasperpeulen Nov 4, 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
2 changes: 1 addition & 1 deletion code/addons/vitest/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,5 @@
"types": ["vitest"],
"strict": true
},
"include": ["src/**/*", "./typings.d.ts"],
"include": ["src/**/*", "./typings.d.ts"]
}
2 changes: 1 addition & 1 deletion code/core/src/core-server/build-static.ts
Original file line number Diff line number Diff line change
Expand Up @@ -167,7 +167,7 @@ export async function buildStaticStandalone(options: BuildStaticStandaloneOption
);

if (features?.experimentalComponentsManifest) {
const componentManifestGenerator: ComponentManifestGenerator = await presets.apply(
const componentManifestGenerator = await presets.apply(
'experimental_componentManifestGenerator'
);
const indexGenerator = await initializedStoryIndexGenerator;
Expand Down
7 changes: 4 additions & 3 deletions code/core/src/core-server/dev-server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -144,7 +144,7 @@ export async function storybookDevServer(options: Options) {
if (features?.experimentalComponentsManifest) {
app.use('/manifests/components.json', async (req, res) => {
try {
const componentManifestGenerator: ComponentManifestGenerator = await options.presets.apply(
const componentManifestGenerator = await options.presets.apply(
'experimental_componentManifestGenerator'
);
const indexGenerator = await initializedStoryIndexGenerator;
Expand All @@ -169,7 +169,7 @@ export async function storybookDevServer(options: Options) {

app.get('/manifests/components.html', async (req, res) => {
try {
const componentManifestGenerator: ComponentManifestGenerator = await options.presets.apply(
const componentManifestGenerator = await options.presets.apply(
'experimental_componentManifestGenerator'
);
const indexGenerator = await initializedStoryIndexGenerator;
Expand All @@ -191,7 +191,8 @@ export async function storybookDevServer(options: Options) {
// logger?.error?.(e instanceof Error ? e : String(e));
res.statusCode = 500;
res.setHeader('Content-Type', 'text/html; charset=utf-8');
res.end(`<pre>${e instanceof Error ? e.toString() : String(e)}</pre>`);
invariant(e instanceof Error);
res.end(`<pre>${e.toString()}\n${e.stack}</pre>`);
}
});
}
Expand Down
52 changes: 48 additions & 4 deletions code/core/src/core-server/manifest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ export function renderManifestComponentsPage(manifest: ComponentsManifest) {
entries.map(([, it]) => it).filter((it) => it.error),
(manifest) => manifest.error?.name ?? 'Error'
)
);
).sort(([, a], [, b]) => b.length - a.length);

const errorGroupsHTML = errorGroups
.map(([error, grouped]) => {
Expand Down Expand Up @@ -517,15 +517,23 @@ export function renderManifestComponentsPage(manifest: ComponentsManifest) {
.card > .tg-err:checked ~ .panels .panel-err {
display: grid;
}

.card > .tg-warn:checked ~ .panels .panel-warn {
display: grid;
}

.card > .tg-stories:checked ~ .panels .panel-stories {
display: grid;
}

/* Add vertical spacing around panels only when any panel is visible */
.card > .tg-err:checked ~ .panels,
.card > .tg-warn:checked ~ .panels,
.card > .tg-stories:checked ~ .panels,
.card > .tg-props:checked ~ .panels {
margin: 10px 0;
}

/* Optional: a subtle 1px ring on the active badge, using :has() if available */
@supports selector(.card:has(.tg-err:checked)) {
.card:has(.tg-err:checked) label[for$='-err'],
Expand All @@ -536,6 +544,25 @@ export function renderManifestComponentsPage(manifest: ComponentsManifest) {
border-color: currentColor;
}
}

/* Wrap long lines in code blocks at ~120 characters */
pre, code {
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
}
pre {
white-space: pre-wrap;
overflow-wrap: anywhere;
word-break: break-word;
overflow-x: auto; /* fallback for extremely long tokens */
margin: 8px 0 0;
}
pre > code {
display: block;
white-space: inherit;
overflow-wrap: inherit;
word-break: inherit;
inline-size: min(100%, 120ch);
}
</style>
</head>
<body>
Expand Down Expand Up @@ -747,19 +774,36 @@ function renderComponentCard(key: string, c: ComponentManifest, id: string) {
<span class="ex-name">${esc(ex.name)}</span>
<span class="badge err">story error</span>
</div>
${ex?.summary ? `<div class=\"hint\">Summary: ${esc(ex.summary)}</div>` : ''}
${ex?.description ? `<div class=\"hint\">${esc(ex.description)}</div>` : ''}
${ex?.snippet ? `<pre><code>${esc(ex.snippet)}</code></pre>` : ''}
${ex?.error?.message ? `<pre><code>${esc(ex.error.message)}</code></pre>` : ''}
</div>`
)
.join('')}


${
c.import
? `<div class="note ok">
<div class="row">
<span class="ex-name">Imports</span>
</div>
<pre><code>${c.import}</code></pre>
</div>`
: ''
}

${okStories
.map(
(ex, k) => `
(ex) => `
<div class="note ok">
<div class="row">
<span class="ex-name">${esc(ex.name)}</span>
<span class="badge ok">story ok</span>
</div>
${ex?.summary ? `<div>${esc(ex.summary)}</div>` : ''}
${ex?.description ? `<div class=\"hint\">${esc(ex.description)}</div>` : ''}
${ex?.snippet ? `<pre><code>${esc(ex.snippet)}</code></pre>` : ''}
</div>`
)
Expand Down
8 changes: 7 additions & 1 deletion code/core/src/types/modules/core-common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -352,7 +352,13 @@ export interface ComponentManifest {
description?: string;
import?: string;
summary?: string;
stories: { name: string; snippet?: string; error?: { name: string; message: string } }[];
stories: {
name: string;
snippet?: string;
description?: string;
summary?: string;
error?: { name: string; message: string };
}[];
jsDocTags: Record<string, string[]>;
error?: { name: string; message: string };
}
Expand Down
1 change: 1 addition & 0 deletions code/renderers/react/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@
"acorn-walk": "^7.2.0",
"babel-plugin-react-docgen": "^4.2.1",
"comment-parser": "^1.4.1",
"empathic": "^2.0.0",
"es-toolkit": "^1.36.0",
"escodegen": "^2.1.0",
"expect-type": "^0.15.0",
Expand Down
193 changes: 193 additions & 0 deletions code/renderers/react/src/componentManifest/fixtures.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,193 @@
import { dedent } from 'ts-dedent';

export const fsMocks = {
['./package.json']: JSON.stringify({ name: 'some-package' }),
['./src/stories/Button.stories.ts']: dedent`
import type { Meta, StoryObj } from '@storybook/react';
import { fn } from 'storybook/test';
import { Button } from './Button';

const meta = {
component: Button,
args: { onClick: fn() },
} satisfies Meta<typeof Button>;
export default meta;
type Story = StoryObj<typeof meta>;

export const Primary: Story = { args: { primary: true, label: 'Button' } };
export const Secondary: Story = { args: { label: 'Button' } };
export const Large: Story = { args: { size: 'large', label: 'Button' } };
export const Small: Story = { args: { size: 'small', label: 'Button' } };`,
['./src/stories/Button.tsx']: dedent`
import React from 'react';
export interface ButtonProps {
/** Description of primary */
primary?: boolean;
backgroundColor?: string;
size?: 'small' | 'medium' | 'large';
label: string;
onClick?: () => void;
}

/**
* Primary UI component for user interaction
* @import import { Button } from '@design-system/components/Button';
*/
export const Button = ({
primary = false,
size = 'medium',
backgroundColor,
label,
...props
}: ButtonProps) => {
const mode = primary ? 'storybook-button--primary' : 'storybook-button--secondary';
return (
<button
type="button"
className={['storybook-button', \`storybook-button--\${size}\`, mode].join(' ')}
style={{ backgroundColor }}
{...props}
>
{label}
</button>
);
};`,
['./src/stories/Header.stories.ts']: dedent`
import type { Meta, StoryObj } from '@storybook/react';
import { fn } from 'storybook/test';
import Header from './Header';

/**
* Description from meta and very long.
* @summary Component summary
* @import import { Header } from '@design-system/components/Header';
*/
const meta = {
component: Header,
args: {
onLogin: fn(),
onLogout: fn(),
onCreateAccount: fn(),
}
} satisfies Meta<typeof Header>;
export default meta;
type Story = StoryObj<typeof meta>;
export const LoggedIn: Story = { args: { user: { name: 'Jane Doe' } } };
export const LoggedOut: Story = {};
`,
['./src/stories/Header.tsx']: dedent`
import { Button } from './Button';

export interface HeaderProps {
user?: User;
onLogin?: () => void;
onLogout?: () => void;
onCreateAccount?: () => void;
}

/**
* @import import { Header } from '@design-system/components/Header';
*/
export default ({ user, onLogin, onLogout, onCreateAccount }: HeaderProps) => (
<header>
<div className="storybook-header">
<div>
{user ? (
<>
<span className="welcome">
Welcome, <b>{user.name}</b>!
</span>
<Button size="small" onClick={onLogout} label="Log out" />
</>
) : (
<>
<Button size="small" onClick={onLogin} label="Log in" />
<Button primary size="small" onClick={onCreateAccount} label="Sign up" />
</>
)}
</div>
</div>
</header>
);`,
};

export const indexJson = {
v: 5,
entries: {
'example-button--primary': {
type: 'story',
subtype: 'story',
id: 'example-button--primary',
name: 'Primary',
title: 'Example/Button',
importPath: './src/stories/Button.stories.ts',
componentPath: './src/stories/Button.tsx',
tags: ['dev', 'test', 'vitest', 'autodocs'],
exportName: 'Primary',
},
'example-button--secondary': {
type: 'story',
subtype: 'story',
id: 'example-button--secondary',
name: 'Secondary',
title: 'Example/Button',
importPath: './src/stories/Button.stories.ts',
componentPath: './src/stories/Button.tsx',
tags: ['dev', 'test', 'vitest', 'autodocs'],
exportName: 'Secondary',
},
'example-button--large': {
type: 'story',
subtype: 'story',
id: 'example-button--large',
name: 'Large',
title: 'Example/Button',
importPath: './src/stories/Button.stories.ts',
componentPath: './src/stories/Button.tsx',
tags: ['dev', 'test', 'vitest', 'autodocs'],
exportName: 'Large',
},
'example-button--small': {
type: 'story',
subtype: 'story',
id: 'example-button--small',
name: 'Small',
title: 'Example/Button',
importPath: './src/stories/Button.stories.ts',
componentPath: './src/stories/Button.tsx',
tags: ['dev', 'test', 'vitest', 'autodocs'],
exportName: 'Small',
},
'example-header--docs': {
id: 'example-header--docs',
title: 'Example/Header',
name: 'Docs',
importPath: './src/stories/Header.stories.ts',
type: 'docs',
tags: ['dev', 'test', 'vitest', 'autodocs'],
storiesImports: [],
},
'example-header--logged-in': {
type: 'story',
subtype: 'story',
id: 'example-header--logged-in',
name: 'Logged In',
title: 'Example/Header',
importPath: './src/stories/Header.stories.ts',
componentPath: './src/stories/Header.tsx',
tags: ['dev', 'test', 'vitest', 'autodocs'],
exportName: 'LoggedIn',
},
'example-header--logged-out': {
type: 'story',
subtype: 'story',
id: 'example-header--logged-out',
name: 'Logged Out',
title: 'Example/Header',
importPath: './src/stories/Header.stories.ts',
componentPath: './src/stories/Header.tsx',
tags: ['dev', 'test', 'vitest', 'autodocs'],
exportName: 'LoggedOut',
},
},
};
Loading