-
-
Notifications
You must be signed in to change notification settings - Fork 9.8k
React: Auto calculate imports, support barrel files, auto detect. components and other small tweaks #32912
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
React: Auto calculate imports, support barrel files, auto detect. components and other small tweaks #32912
Changes from 4 commits
a5f4646
17a3089
9331bf7
2b072ba
835417c
8ba3b0b
8d40e43
ce25a2f
7e9a30a
acf5925
2a05fc4
3de6fad
80b0ad2
08bdc82
8151722
50be8d7
70e8504
7ab7295
d9cc661
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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'], | ||
|
|
@@ -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> | ||
|
|
@@ -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
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Escape import content to prevent XSS. The import string Apply the ${
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>`
: ''
}
🤖 Prompt for AI Agents |
||
|
|
||
| ${okStories | ||
| .map( | ||
| (ex, k) => ` | ||
| (ex) => ` | ||
| <div class="note ok"> | ||
| <div class="row"> | ||
| <span class="ex-name">${esc(ex.name)}</span> | ||
|
|
||
| Original file line number | Diff line number | Diff line change | ||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -7,8 +7,26 @@ import { dedent } from 'ts-dedent'; | |||||||||||||
|
|
||||||||||||||
| import { componentManifestGenerator } from './generator'; | ||||||||||||||
|
|
||||||||||||||
| vi.mock('storybook/internal/common', async (importOriginal) => { | ||||||||||||||
| return { | ||||||||||||||
| ...(await importOriginal()), | ||||||||||||||
| 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
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Mock empathic/find without spy: true option. According to the coding guidelines, all package and file mocks in Vitest tests should use 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
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||
| vi.mock('tsconfig-paths', () => ({ loadConfig: () => ({ resultType: null!, message: null! }) })); | ||||||||||||||
|
|
||||||||||||||
| // Use the provided indexJson from this file | ||||||||||||||
|
|
@@ -97,6 +115,7 @@ beforeEach(() => { | |||||||||||||
| 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'; | ||||||||||||||
|
|
@@ -205,8 +224,8 @@ beforeEach(() => { | |||||||||||||
| }); | ||||||||||||||
|
|
||||||||||||||
| 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); | ||||||||||||||
|
|
||||||||||||||
|
|
@@ -217,7 +236,7 @@ test('componentManifestGenerator generates correct id, name, description and exa | |||||||||||||
| "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", | ||||||||||||||
|
|
@@ -321,7 +340,7 @@ test('componentManifestGenerator generates correct id, name, description and exa | |||||||||||||
| "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';", | ||||||||||||||
|
|
@@ -418,6 +437,7 @@ test('componentManifestGenerator generates correct id, name, description and exa | |||||||||||||
| 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'; | ||||||||||||||
|
|
@@ -441,7 +461,7 @@ async function getManifestForStory(code: string) { | |||||||||||||
| '/app' | ||||||||||||||
| ); | ||||||||||||||
|
|
||||||||||||||
| const generator = await componentManifestGenerator(); | ||||||||||||||
| const generator = await componentManifestGenerator(undefined, { configDir: '.storybook' } as any); | ||||||||||||||
| const indexJson = { | ||||||||||||||
| v: 5, | ||||||||||||||
| entries: { | ||||||||||||||
|
|
@@ -459,11 +479,11 @@ async function getManifestForStory(code: string) { | |||||||||||||
| }, | ||||||||||||||
| }; | ||||||||||||||
|
|
||||||||||||||
| 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) { | ||||||||||||||
|
|
@@ -497,7 +517,7 @@ test('fall back to index title when no component name', async () => { | |||||||||||||
| "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", | ||||||||||||||
|
|
@@ -542,7 +562,7 @@ test('component exported from other file', async () => { | |||||||||||||
| "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", | ||||||||||||||
|
|
@@ -594,7 +614,7 @@ test('unknown expressions', async () => { | |||||||||||||
| "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", | ||||||||||||||
|
|
||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -3,12 +3,14 @@ import { readFile } from 'node:fs/promises'; | |
| import { recast } from 'storybook/internal/babel'; | ||
| import { loadCsf } from 'storybook/internal/csf-tools'; | ||
| import { extractDescription } from 'storybook/internal/csf-tools'; | ||
| import { type ComponentManifestGenerator } from 'storybook/internal/types'; | ||
| import { type ComponentManifestGenerator, type PresetPropertyFn } from 'storybook/internal/types'; | ||
| import { type ComponentManifest } from 'storybook/internal/types'; | ||
|
|
||
| import * as find from 'empathic/find'; | ||
| import path from 'pathe'; | ||
|
|
||
| import { getCodeSnippet } from './generateCodeSnippet'; | ||
| import { getComponentImports } from './getComponentImports'; | ||
| import { extractJSDocInfo } from './jsdocTags'; | ||
| import { type DocObj, getMatchingDocgen, parseWithReactDocgen } from './reactDocgen'; | ||
| import { groupBy, invariant } from './utils'; | ||
|
|
@@ -17,7 +19,9 @@ interface ReactComponentManifest extends ComponentManifest { | |
| reactDocgen?: DocObj; | ||
| } | ||
|
|
||
| export const componentManifestGenerator = async () => { | ||
| export const componentManifestGenerator: PresetPropertyFn< | ||
| 'experimental_componentManifestGenerator' | ||
| > = async (config, options) => { | ||
| return (async (storyIndexGenerator) => { | ||
| const index = await storyIndexGenerator.getIndex(); | ||
|
|
||
|
|
@@ -32,7 +36,8 @@ export const componentManifestGenerator = async () => { | |
| ); | ||
| const components = await Promise.all( | ||
| singleEntryPerComponent.flatMap(async (entry): Promise<ReactComponentManifest> => { | ||
| const storyFile = await readFile(path.join(process.cwd(), entry.importPath), 'utf-8'); | ||
| const storyAbsPath = path.join(process.cwd(), entry.importPath); | ||
| const storyFile = await readFile(storyAbsPath, 'utf-8'); | ||
| const csf = loadCsf(storyFile, { makeTitle: (title) => title ?? 'No title' }).parse(); | ||
| const name = csf._meta?.component ?? entry.title.split('/').at(-1)!; | ||
| const id = entry.id.split('--')[0]; | ||
|
|
@@ -55,11 +60,23 @@ export const componentManifestGenerator = async () => { | |
| }) | ||
| .filter(Boolean); | ||
|
|
||
| const nearestPkg = find.up('package.json', { | ||
| cwd: path.dirname(storyAbsPath), | ||
| last: process.cwd(), | ||
| }); | ||
| const packageName = nearestPkg | ||
| ? JSON.parse(await readFile(nearestPkg, 'utf-8')).name | ||
| : undefined; | ||
|
||
| const fallbackImport = packageName ? `import { ${name} } from "${packageName}";` : ''; | ||
| const calculatedImports = | ||
| getComponentImports(csf, packageName).imports.join('\n').trim() ?? fallbackImport; | ||
coderabbitai[bot] marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
|
|
||
| const base = { | ||
| id, | ||
| name, | ||
| path: importPath, | ||
| stories, | ||
| import: calculatedImports, | ||
| jsDocTags: {}, | ||
| } satisfies Partial<ComponentManifest>; | ||
|
|
||
|
|
@@ -133,7 +150,6 @@ export const componentManifestGenerator = async () => { | |
| name, | ||
| description: manifestDescription?.trim(), | ||
| summary: tags.summary?.[0], | ||
| import: tags.import?.[0], | ||
| reactDocgen: docgen, | ||
| jsDocTags: tags, | ||
| stories, | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
📝 Committable suggestion
🤖 Prompt for AI Agents