Skip to content
Closed
Show file tree
Hide file tree
Changes from 11 commits
Commits
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
4 changes: 2 additions & 2 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 Down
48 changes: 44 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;
}
Comment on lines +520 to 527
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

Remove or complete empty CSS rule blocks.

These CSS rule blocks appear to be incomplete or placeholder code. They should either be removed or completed with the intended styles.

-      
-      .card > .tg-warn:checked ~ .panels .panel-warn {
-          display: grid;
-      }
-      
-      .card > .tg-stories:checked ~ .panels .panel-stories {
-          display: grid;
-      }
📝 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
.card > .tg-warn:checked ~ .panels .panel-warn {
display: grid;
}
.card > .tg-stories:checked ~ .panels .panel-stories {
display: grid;
}
🤖 Prompt for AI Agents
In code/core/src/core-server/manifest.ts around lines 520 to 527, there are two
CSS rule blocks that look like incomplete placeholders (.card > .tg-warn:checked
~ .panels .panel-warn and .card > .tg-stories:checked ~ .panels .panel-stories);
either remove these rules entirely if they are not needed, or complete them with
the intended panel styles (for example set grid-template-columns/rows, gap,
align-items, padding, and any show/hide behavior) so they’re not empty
placeholders — choose one of those two options and update the file accordingly.


/* 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 @@ -752,9 +779,22 @@ function renderComponentCard(key: string, c: ComponentManifest, id: string) {
</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>`
: ''
}
Comment on lines +784 to +795
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

Escape import content to prevent XSS.

The import string c.import is rendered without escaping, which could lead to XSS vulnerabilities if the import content contains malicious HTML/script tags.

Apply the esc() function:

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

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In code/core/src/core-server/manifest.ts around lines 782 to 793, the template
renders c.import unescaped which can lead to XSS; replace the raw interpolation
with an escaped value (e.g., use esc(c.import) or esc(String(c.import))) when
inserting into the <pre><code> block, and ensure the esc function is
imported/available in this module; keep the conditional logic the same but wrap
the displayed content in esc(...) to prevent HTML/script injection.


${okStories
.map(
(ex, k) => `
(ex) => `
<div class="note ok">
<div class="row">
<span class="ex-name">${esc(ex.name)}</span>
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
47 changes: 37 additions & 10 deletions code/renderers/react/src/componentManifest/generator.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,33 @@

import { componentManifestGenerator } from './generator';

vi.mock('storybook/internal/common', async (importOriginal) => ({
...(await importOriginal()),
// Keep it simple: hardcode known inputs to expected outputs for this test.
resolveImport: (id: string, opts: { basedir: string }) => {
const basedir = opts?.basedir;
return basedir === '/app/src/stories' && id === './Button'
? './src/stories/Button.tsx'
: basedir === '/app/src/stories' && id === './Header'
? './src/stories/Header.tsx'
: id;
},
JsPackageManagerFactory: {
getPackageManager: () => ({
primaryPackageJson: {
packageJson: {
name: 'some-package',
},
},
}),
},
}));
vi.mock('node:fs/promises', async () => (await import('memfs')).fs.promises);
vi.mock('node:fs', async () => (await import('memfs')).fs);

vi.mock('empathic/find', async () => ({
up: (path: string) => '/app/package.json',
}));
Comment on lines +33 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

Mock empathic/find without spy: true option.

According to the coding guidelines, all package and file mocks in Vitest tests should use vi.mock() with the spy: true option.

As per coding guidelines

Apply this diff:

-vi.mock('empathic/find', async () => ({
+vi.mock('empathic/find', { spy: true }, async () => ({
   up: (path: string) => '/app/package.json',
 }));
📝 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
vi.mock('empathic/find', async () => ({
up: (path: string) => '/app/package.json',
}));
vi.mock('empathic/find', async () => ({
up: (path: string) => '/app/package.json',
}), { spy: true });
🤖 Prompt for AI Agents
In code/renderers/react/src/componentManifest/generator.test.ts around lines
27-29, the Vitest mock for 'empathic/find' is missing the required spy option;
update the vi.mock call to pass the options object with { spy: true } as the
third argument while keeping the async factory intact (i.e.,
vi.mock('empathic/find', async () => ({ up: (path: string) =>
'/app/package.json' }), { spy: true })).

vi.mock('tsconfig-paths', () => ({ loadConfig: () => ({ resultType: null!, message: null! }) }));

// Use the provided indexJson from this file
Expand Down Expand Up @@ -97,6 +122,7 @@
vi.spyOn(process, 'cwd').mockReturnValue('/app');
vol.fromJSON(
{
['./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';
Expand Down Expand Up @@ -205,19 +231,19 @@
});

test('componentManifestGenerator generates correct id, name, description and examples ', async () => {
const generator = await componentManifestGenerator();
const manifest = await generator({
const generator = await componentManifestGenerator(undefined, { configDir: '.storybook' } as any);
const manifest = await generator?.({
getIndex: async () => indexJson,
} as unknown as StoryIndexGenerator);

expect(manifest).toMatchInlineSnapshot(`

Check failure on line 239 in code/renderers/react/src/componentManifest/generator.test.ts

View workflow job for this annotation

GitHub Actions / Core Unit Tests, windows-latest

src/componentManifest/generator.test.ts > componentManifestGenerator generates correct id, name, description and examples

Error: Snapshot `componentManifestGenerator generates correct id, name, description and examples 1` mismatched - Expected + Received @@ -1,91 +1,48 @@ { "components": { "example-button": { - "description": "Primary UI component for user interaction", - "error": undefined, - "id": "example-button", - "import": "import { Button } from "some-package";", - "jsDocTags": {}, - "name": "Button", - "path": "./src/stories/Button.stories.ts", - "reactDocgen": { - "actualName": "Button", - "definedInFile": "/app/src/stories/Button.tsx", - "description": "Primary UI component for user interaction", - "displayName": "Button", - "exportName": "Button", - "methods": [], - "props": { - "backgroundColor": { - "description": "", - "required": false, - "tsType": { + "description": undefined, + "error": { + "message": "Could not read the component file located at "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 */ + export const Button = ({ + primary = false, + size = 'medium', + backgroundColor, + label, + ...props + }: ButtonProps) => { - "name": "string", - }, - }, - "label": { - "description": "", - "required": true, - "tsType": { - "name": "string", - }, - }, - "onClick": { - "description": "", - "required": false, - "tsType": { - "name": "signature", - "raw": "() => void", - "signature": { - "arguments": [], - "return": { - "name": "void", - }, - }, - "type": "function", - }, - }, - "primary": { - "defaultValue": { - "computed": false, - "value": "false", - }, - "description": "Description of primary", - "required": false, - "tsType": { - "name": "boolean", - }, - }, - "size": { - "defaultValue": { - "computed": false, - "value": "'medium'", - }, + 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> + ); + };".", + "name": "Component file could not be read", + }, - "description": "", - "required": false, - "tsType": { - "elements": [ - { - "name": "literal", - "value": "'small'", - }, - { - "name": "literal", - "value": "'medium'", - }, - { - "name": "literal", - "value": "'large'", - }, - ], - "name": "union", - "raw": "'small' | 'medium' | 'large'", - }, - }, - }, - }, + "id": "example-button", + "import": "import { Button } from "some-package";", + "jsDocTags": {}, + "name": "Button", + "path": "./src/stories/Button.stories.ts", + "reactDocgen": undefined, "stories": [ { "name": "Primary", "snippet": "const Primary = () => <Button onClick={fn()} primary label="Button"></Button>;", }, @@ -104,84 +61,56 @@
{
"components": {
"example-button": {
"description": "Primary UI component for user interaction",
"error": undefined,
"id": "example-button",
"import": undefined,
"import": "import { Button } from "some-package";",
"jsDocTags": {},
"name": "Button",
"path": "./src/stories/Button.stories.ts",
Expand Down Expand Up @@ -321,7 +347,7 @@
"description": "Description from meta and very long.",
"error": undefined,
"id": "example-header",
"import": "import { Header } from '@design-system/components/Header';",
"import": "import { Header } from "some-package";",
"jsDocTags": {
"import": [
"import { Header } from '@design-system/components/Header';",
Expand Down Expand Up @@ -418,6 +444,7 @@
async function getManifestForStory(code: string) {
vol.fromJSON(
{
['./package.json']: JSON.stringify({ name: 'some-package' }),
['./src/stories/Button.stories.ts']: code,
['./src/stories/Button.tsx']: dedent`
import React from 'react';
Expand All @@ -441,7 +468,7 @@
'/app'
);

const generator = await componentManifestGenerator();
const generator = await componentManifestGenerator(undefined, { configDir: '.storybook' } as any);
const indexJson = {
v: 5,
entries: {
Expand All @@ -459,11 +486,11 @@
},
};

const manifest = await generator({
const manifest = await generator?.({
getIndex: async () => indexJson,
} as unknown as StoryIndexGenerator);

return manifest.components['example-button'];
return manifest?.components?.['example-button'];
}

function withCSF3(body: string) {
Expand Down Expand Up @@ -492,12 +519,12 @@

export const Primary = () => <Button csf1="story" />;
`;
expect(await getManifestForStory(code)).toMatchInlineSnapshot(`

Check failure on line 522 in code/renderers/react/src/componentManifest/generator.test.ts

View workflow job for this annotation

GitHub Actions / Core Unit Tests, windows-latest

src/componentManifest/generator.test.ts > fall back to index title when no component name

Error: Snapshot `fall back to index title when no component name 1` mismatched - Expected + Received @@ -1,34 +1,33 @@ { - "description": "Primary UI component for user interaction", - "error": undefined, - "id": "example-button", - "import": "import { Button } from "some-package";", - "jsDocTags": {}, - "name": "Button", - "path": "./src/stories/Button.stories.ts", - "reactDocgen": { - "actualName": "Button", - "definedInFile": "/app/src/stories/Button.tsx", - "description": "Primary UI component for user interaction", - "displayName": "Button", - "exportName": "Button", - "methods": [], - "props": { - "primary": { - "defaultValue": { - "computed": false, - "value": "false", - }, + "description": undefined, + "error": { + "message": "Could not read the component file located at "import React from 'react'; + export interface ButtonProps { + /** Description of primary */ + primary?: boolean; + } + + /** Primary UI component for user interaction */ + export const Button = ({ + primary = false, + }: ButtonProps) => { + const mode = primary ? 'storybook-button--primary' : 'storybook-button--secondary'; + return ( + <button + type="button" + ></button> + ); + };".", + "name": "Component file could not be read", + }, - "description": "Description of primary", - "required": false, + "id": "example-button", + "import": "import { Button } from "some-package";", - "tsType": { - "name": "boolean", - }, - }, - }, - }, + "jsDocTags": {}, + "name": "Button", + "path": "./src/stories/Button.stories.ts", + "reactDocgen": undefined, "stories": [ { "name": "Primary", "snippet": "const Primary = () => <Button csf1="story" />;", }, ❯ src/componentManifest/generator.test.ts:522:43
{
"description": "Primary UI component for user interaction",
"error": undefined,
"id": "example-button",
"import": undefined,
"import": "import { Button } from "some-package";",
"jsDocTags": {},
"name": "Button",
"path": "./src/stories/Button.stories.ts",
Expand Down Expand Up @@ -537,12 +564,12 @@
const code = withCSF3(dedent`
export { Primary } from './other-file';
`);
expect(await getManifestForStory(code)).toMatchInlineSnapshot(`

Check failure on line 567 in code/renderers/react/src/componentManifest/generator.test.ts

View workflow job for this annotation

GitHub Actions / Core Unit Tests, windows-latest

src/componentManifest/generator.test.ts > component exported from other file

Error: Snapshot `component exported from other file 1` mismatched - Expected + Received @@ -1,34 +1,33 @@ { - "description": "Primary UI component for user interaction", - "error": undefined, - "id": "example-button", - "import": "import { Button } from "some-package";", - "jsDocTags": {}, - "name": "Button", - "path": "./src/stories/Button.stories.ts", - "reactDocgen": { - "actualName": "Button", - "definedInFile": "/app/src/stories/Button.tsx", - "description": "Primary UI component for user interaction", - "displayName": "Button", - "exportName": "Button", - "methods": [], - "props": { - "primary": { - "defaultValue": { - "computed": false, - "value": "false", - }, + "description": undefined, + "error": { + "message": "Could not read the component file located at "import React from 'react'; + export interface ButtonProps { + /** Description of primary */ + primary?: boolean; + } + + /** Primary UI component for user interaction */ + export const Button = ({ + primary = false, + }: ButtonProps) => { + const mode = primary ? 'storybook-button--primary' : 'storybook-button--secondary'; + return ( + <button + type="button" + ></button> + ); + };".", + "name": "Component file could not be read", + }, - "description": "Description of primary", - "required": false, + "id": "example-button", + "import": "import { Button } from "some-package";", - "tsType": { - "name": "boolean", - }, - }, - }, - }, + "jsDocTags": {}, + "name": "Button", + "path": "./src/stories/Button.stories.ts", + "reactDocgen": undefined, "stories": [ { "error": { "message": "Expected story to be a function or variable declaration 8 | export default meta; ❯ src/componentManifest/generator.test.ts:567:43
{
"description": "Primary UI component for user interaction",
"error": undefined,
"id": "example-button",
"import": undefined,
"import": "import { Button } from "some-package";",
"jsDocTags": {},
"name": "Button",
"path": "./src/stories/Button.stories.ts",
Expand Down Expand Up @@ -589,12 +616,12 @@
const code = withCSF3(dedent`
export const Primary = someWeirdExpression;
`);
expect(await getManifestForStory(code)).toMatchInlineSnapshot(`

Check failure on line 619 in code/renderers/react/src/componentManifest/generator.test.ts

View workflow job for this annotation

GitHub Actions / Core Unit Tests, windows-latest

src/componentManifest/generator.test.ts > unknown expressions

Error: Snapshot `unknown expressions 1` mismatched - Expected + Received @@ -1,34 +1,33 @@ { - "description": "Primary UI component for user interaction", - "error": undefined, - "id": "example-button", - "import": "import { Button } from "some-package";", - "jsDocTags": {}, - "name": "Button", - "path": "./src/stories/Button.stories.ts", - "reactDocgen": { - "actualName": "Button", - "definedInFile": "/app/src/stories/Button.tsx", - "description": "Primary UI component for user interaction", - "displayName": "Button", - "exportName": "Button", - "methods": [], - "props": { - "primary": { - "defaultValue": { - "computed": false, - "value": "false", - }, + "description": undefined, + "error": { + "message": "Could not read the component file located at "import React from 'react'; + export interface ButtonProps { + /** Description of primary */ + primary?: boolean; + } + + /** Primary UI component for user interaction */ + export const Button = ({ + primary = false, + }: ButtonProps) => { + const mode = primary ? 'storybook-button--primary' : 'storybook-button--secondary'; + return ( + <button + type="button" + ></button> + ); + };".", + "name": "Component file could not be read", + }, - "description": "Description of primary", - "required": false, + "id": "example-button", + "import": "import { Button } from "some-package";", - "tsType": { - "name": "boolean", - }, - }, - }, - }, + "jsDocTags": {}, + "name": "Button", + "path": "./src/stories/Button.stories.ts", + "reactDocgen": undefined, "stories": [ { "error": { "message": "Expected story to be csf factory, function or an object expression 8 | export default meta; ❯ src/componentManifest/generator.test.ts:619:43
{
"description": "Primary UI component for user interaction",
"error": undefined,
"id": "example-button",
"import": undefined,
"import": "import { Button } from "some-package";",
"jsDocTags": {},
"name": "Button",
"path": "./src/stories/Button.stories.ts",
Expand Down
Loading
Loading