From a5f4646e38ae127c18adbfc1cf167370f9282e72 Mon Sep 17 00:00:00 2001 From: Kasper Peulen Date: Fri, 31 Oct 2025 15:52:23 +0100 Subject: [PATCH 01/35] Add automatic component imports --- .../src/componentManifest/generator.test.ts | 213 +++++++- .../react/src/componentManifest/generator.ts | 18 +- .../getComponentImports.test.ts | 464 ++++++++++++++++++ .../componentManifest/getComponentImports.ts | 204 ++++++++ 4 files changed, 886 insertions(+), 13 deletions(-) create mode 100644 code/renderers/react/src/componentManifest/getComponentImports.test.ts create mode 100644 code/renderers/react/src/componentManifest/getComponentImports.ts diff --git a/code/renderers/react/src/componentManifest/generator.test.ts b/code/renderers/react/src/componentManifest/generator.test.ts index fa13535eb68c..4eafcc9972c4 100644 --- a/code/renderers/react/src/componentManifest/generator.test.ts +++ b/code/renderers/react/src/componentManifest/generator.test.ts @@ -7,6 +7,20 @@ 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('tsconfig-paths', () => ({ loadConfig: () => ({ resultType: null!, message: null! }) })); @@ -205,8 +219,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 +231,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 +335,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';", @@ -441,7 +455,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 +473,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 +511,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 +556,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 +608,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", @@ -636,3 +650,182 @@ test('unknown expressions', async () => { } `); }); + +test('unknown expressions', async () => { + const code = dedent` +import ActionBar from '.' +import { + BoldIcon, + CodeIcon, + ItalicIcon, + SearchIcon, + LinkIcon, + FileAddedIcon, + QuoteIcon, + ListUnorderedIcon, + ListOrderedIcon, + TasklistIcon, +} from '@primer/octicons-react' +import type {Meta, StoryObj} from '@storybook/react-vite' + +const meta: Meta = { + title: 'Experimental/Components/ActionBar', +} as Meta + +export default meta +type Story = StoryObj + +export const Playground: Story = { + render: ({'aria-labelledby': _, ...args}) => ( + + + + + + + ), +} +Playground.argTypes = { + size: { + control: { + type: 'radio', + }, + options: ['small', 'medium', 'large'], + }, + flush: { + control: { + type: 'boolean', + }, + }, + gap: { + control: {type: 'radio'}, + options: ['none', 'condensed'], + description: 'Horizontal gap scale between items', + table: {defaultValue: {summary: 'condensed'}}, + }, +} +Playground.args = { + size: 'medium', + flush: false, + gap: 'condensed', +} + +export const Default = () => ( + + + + + + + + + + + + + +) + +const BoldButton = () => + +const FormattingButtons = () => ( + <> + + + + + +) + +const AdvancedFormattingButtons = () => ( + <> + + + + + + + +) + +export const DeepChildTree = () => ( + + + + + +) + `; + expect(await getManifestForStory(code)).toMatchInlineSnapshot(` + { + "description": "Primary UI component for user interaction", + "error": undefined, + "id": "example-button", + "import": "import ActionBar 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": "Description of primary", + "required": false, + "tsType": { + "name": "boolean", + }, + }, + }, + }, + "stories": [ + { + "name": "Playground", + "snippet": "const Playground = () => + + + + + ;", + }, + { + "name": "Default", + "snippet": "const Default = () => ( + + + + + + + + + + + + + + );", + }, + { + "name": "DeepChildTree", + "snippet": "const DeepChildTree = () => ( + + + + + + );", + }, + ], + "summary": undefined, + } + `); +}); diff --git a/code/renderers/react/src/componentManifest/generator.ts b/code/renderers/react/src/componentManifest/generator.ts index 2f1a1df70f73..b033edba872e 100644 --- a/code/renderers/react/src/componentManifest/generator.ts +++ b/code/renderers/react/src/componentManifest/generator.ts @@ -1,14 +1,16 @@ import { readFile } from 'node:fs/promises'; import { recast } from 'storybook/internal/babel'; +import { JsPackageManagerFactory } from 'storybook/internal/common'; 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 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,12 @@ interface ReactComponentManifest extends ComponentManifest { reactDocgen?: DocObj; } -export const componentManifestGenerator = async () => { +export const componentManifestGenerator: PresetPropertyFn< + 'experimental_componentManifestGenerator' +> = async (config, options) => { + const packageManager = JsPackageManagerFactory.getPackageManager({ + configDir: options.configDir, + }); return (async (storyIndexGenerator) => { const index = await storyIndexGenerator.getIndex(); @@ -30,6 +37,7 @@ export const componentManifestGenerator = async () => { const singleEntryPerComponent = Object.values(groupByComponentId).flatMap((group) => group && group?.length > 0 ? [group[0]] : [] ); + const packageName = packageManager.primaryPackageJson.packageJson.name; const components = await Promise.all( singleEntryPerComponent.flatMap(async (entry): Promise => { const storyFile = await readFile(path.join(process.cwd(), entry.importPath), 'utf-8'); @@ -55,11 +63,16 @@ export const componentManifestGenerator = async () => { }) .filter(Boolean); + const fallbackImport = packageName ? `import { ${name} } from "${packageName}";` : ''; + const calculatedImports = + getComponentImports(csf, packageName).imports.join('\n').trim() ?? fallbackImport; + const base = { id, name, path: importPath, stories, + import: calculatedImports, jsDocTags: {}, } satisfies Partial; @@ -133,7 +146,6 @@ export const componentManifestGenerator = async () => { name, description: manifestDescription?.trim(), summary: tags.summary?.[0], - import: tags.import?.[0], reactDocgen: docgen, jsDocTags: tags, stories, diff --git a/code/renderers/react/src/componentManifest/getComponentImports.test.ts b/code/renderers/react/src/componentManifest/getComponentImports.test.ts new file mode 100644 index 000000000000..a82011855101 --- /dev/null +++ b/code/renderers/react/src/componentManifest/getComponentImports.test.ts @@ -0,0 +1,464 @@ +import { expect, test } from 'vitest'; + +import { loadCsf } from 'storybook/internal/csf-tools'; + +import { dedent } from 'ts-dedent'; + +import { getComponentImports } from './getComponentImports'; + +const getImports = (code: string, packageName?: string) => + getComponentImports( + loadCsf(code, { makeTitle: (t?: string) => t ?? 'title' }).parse(), + packageName + ); + +test('Get imports from multiple components', () => { + const code = dedent` + import type { Meta } from '@storybook/react'; + import { ButtonGroup } from '@design-system/button-group'; + import { Button } from '@design-system/button'; + + const meta: Meta = { + component: Button, + args: { + children: 'Click me' + } + }; + export default meta; + export const Default: Story = ; + `; + expect(getImports(code)).toMatchInlineSnapshot( + ` + { + "components": [ + "Button", + "ButtonGroup", + ], + "imports": [ + "import { ButtonGroup } from '@design-system/button-group';", + "import { Button } from '@design-system/button';", + ], + } + ` + ); +}); + +test('Namespace import with member usage', () => { + const code = dedent` + import * as Accordion from '@ds/accordion'; + + const meta = {}; + export default meta; + export const S = Hi; + `; + expect(getImports(code)).toMatchInlineSnapshot( + ` + { + "components": [ + "Accordion.Root", + ], + "imports": [ + "import * as Accordion from '@ds/accordion';", + ], + } + ` + ); +}); + +test('Named import used as namespace object', () => { + const code = dedent` + import { Accordion } from '@ds/accordion'; + + const meta = {}; + export default meta; + export const S = Hi; + `; + expect(getImports(code)).toMatchInlineSnapshot( + ` + { + "components": [ + "Accordion.Root", + ], + "imports": [ + "import { Accordion } from '@ds/accordion';", + ], + } + ` + ); +}); + +test('Default import', () => { + const code = dedent` + import Button from '@ds/button'; + + const meta = {}; + export default meta; + export const S = ; `; - expect(getImports(code)).toMatchInlineSnapshot( + expect(await getImports(code)).toMatchInlineSnapshot( ` { "components": [ @@ -36,12 +36,14 @@ test('Get imports from multiple components', () => { "importId": "@design-system/button", "importName": "Button", "localImportName": "Button", + "path": undefined, }, { "componentName": "ButtonGroup", "importId": "@design-system/button-group", "importName": "ButtonGroup", "localImportName": "ButtonGroup", + "path": undefined, }, ], "imports": [ @@ -53,7 +55,7 @@ test('Get imports from multiple components', () => { ); }); -test('Namespace import with member usage', () => { +test('Namespace import with member usage', async () => { const code = dedent` import * as Accordion from '@ds/accordion'; @@ -61,7 +63,7 @@ test('Namespace import with member usage', () => { export default meta; export const S = Hi; `; - expect(getImports(code)).toMatchInlineSnapshot( + expect(await getImports(code)).toMatchInlineSnapshot( ` { "components": [ @@ -71,6 +73,7 @@ test('Namespace import with member usage', () => { "importName": "Root", "localImportName": "Accordion", "namespace": "Accordion", + "path": undefined, }, ], "imports": [ @@ -81,7 +84,7 @@ test('Namespace import with member usage', () => { ); }); -test('Named import used as namespace object', () => { +test('Named import used as namespace object', async () => { const code = dedent` import { Accordion } from '@ds/accordion'; @@ -89,7 +92,7 @@ test('Named import used as namespace object', () => { export default meta; export const S = Hi; `; - expect(getImports(code)).toMatchInlineSnapshot( + expect(await getImports(code)).toMatchInlineSnapshot( ` { "components": [ @@ -98,6 +101,7 @@ test('Named import used as namespace object', () => { "importId": "@ds/accordion", "importName": "Accordion", "localImportName": "Accordion", + "path": undefined, }, ], "imports": [ @@ -108,7 +112,7 @@ test('Named import used as namespace object', () => { ); }); -test('Default import', () => { +test('Default import', async () => { const code = dedent` import Button from '@ds/button'; @@ -116,7 +120,7 @@ test('Default import', () => { export default meta; export const S = - ); - };`, - ['./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; - export default meta; - type Story = StoryObj; - 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; - } - - export default ({ user, onLogin, onLogout, onCreateAccount }: HeaderProps) => ( -
-
-
- {user ? ( - <> - - Welcome, {user.name}! - -
-
-
- );`, - }, - '/app' - ); + vol.fromJSON(fsMocks); return () => vol.reset(); }); @@ -243,14 +136,19 @@ test('componentManifestGenerator generates correct id, name, description and exa "description": "Primary UI component for user interaction", "error": undefined, "id": "example-button", - "import": "import { Button } from "some-package";", - "jsDocTags": {}, + "import": "import { Button } from '@design-system/components/Button';", + "jsDocTags": { + "import": [ + "import { Button } from '@design-system/components/Button';", + ], + }, "name": "Button", "path": "./src/stories/Button.stories.ts", "reactDocgen": { "actualName": "Button", - "definedInFile": "/app/src/stories/Button.tsx", - "description": "Primary UI component for user interaction", + "definedInFile": "./src/stories/Button.tsx", + "description": "Primary UI component for user interaction + @import import { Button } from '@design-system/components/Button';", "displayName": "Button", "exportName": "Button", "methods": [], @@ -347,7 +245,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 "some-package";", + "import": "import { Header } from '@design-system/components/Header';", "jsDocTags": { "import": [ "import { Header } from '@design-system/components/Header';", @@ -360,8 +258,8 @@ test('componentManifestGenerator generates correct id, name, description and exa "path": "./src/stories/Header.stories.ts", "reactDocgen": { "actualName": "", - "definedInFile": "/app/src/stories/Header.tsx", - "description": "", + "definedInFile": "./src/stories/Header.tsx", + "description": "@import import { Header } from '@design-system/components/Header';", "exportName": "default", "methods": [], "props": { @@ -530,7 +428,7 @@ test('fall back to index title when no component name', async () => { "path": "./src/stories/Button.stories.ts", "reactDocgen": { "actualName": "Button", - "definedInFile": "/app/src/stories/Button.tsx", + "definedInFile": "./src/stories/Button.tsx", "description": "Primary UI component for user interaction", "displayName": "Button", "exportName": "Button", @@ -575,7 +473,7 @@ test('component exported from other file', async () => { "path": "./src/stories/Button.stories.ts", "reactDocgen": { "actualName": "Button", - "definedInFile": "/app/src/stories/Button.tsx", + "definedInFile": "./src/stories/Button.tsx", "description": "Primary UI component for user interaction", "displayName": "Button", "exportName": "Button", @@ -627,7 +525,7 @@ test('unknown expressions', async () => { "path": "./src/stories/Button.stories.ts", "reactDocgen": { "actualName": "Button", - "definedInFile": "/app/src/stories/Button.tsx", + "definedInFile": "./src/stories/Button.tsx", "description": "Primary UI component for user interaction", "displayName": "Button", "exportName": "Button", diff --git a/code/renderers/react/src/componentManifest/generator.ts b/code/renderers/react/src/componentManifest/generator.ts index 4d00358bd92b..f6c3f9b04dc4 100644 --- a/code/renderers/react/src/componentManifest/generator.ts +++ b/code/renderers/react/src/componentManifest/generator.ts @@ -38,10 +38,14 @@ export const componentManifestGenerator: PresetPropertyFn< group && group?.length > 0 ? [group[0]] : [] ); const components = await Promise.all( - singleEntryPerComponent.flatMap(async (entry): Promise => { + singleEntryPerComponent.map(async (entry): Promise => { const absoluteImportPath = path.join(process.cwd(), entry.importPath); const storyFile = await readFile(absoluteImportPath, 'utf-8'); const csf = loadCsf(storyFile, { makeTitle: (title) => title ?? 'No title' }).parse(); + + if (csf.meta.tags?.includes('!manifest')) { + return; + } let componentName = csf._meta?.component; const title = entry.title.replace(/\s+/g, ''); @@ -87,10 +91,20 @@ export const componentManifestGenerator: PresetPropertyFn< const stories = Object.keys(csf._stories) .map((storyName) => { + const story = csf._stories[storyName]; + if (story.tags?.includes('!manifest')) { + return; + } try { + const jsdocComment = extractDescription(csf._storyStatements[storyName]); + const { tags = {}, description } = jsdocComment ? extractJSDocInfo(jsdocComment) : {}; + const finalDescription = (tags?.describe?.[0] || tags?.desc?.[0]) ?? description; + return { name: storyName, snippet: recast.print(getCodeSnippet(csf, storyName, componentName)).code, + description: finalDescription?.trim(), + summary: tags.summary?.[0], }; } catch (e) { invariant(e instanceof Error); @@ -100,7 +114,7 @@ export const componentManifestGenerator: PresetPropertyFn< }; } }) - .filter(Boolean); + .filter((it) => it != null); const base = { id, diff --git a/code/renderers/react/src/componentManifest/getComponentImports.test.ts b/code/renderers/react/src/componentManifest/getComponentImports.test.ts index c3ad6361ecd8..8f2bf0675178 100644 --- a/code/renderers/react/src/componentManifest/getComponentImports.test.ts +++ b/code/renderers/react/src/componentManifest/getComponentImports.test.ts @@ -1,15 +1,39 @@ -import { expect, test } from 'vitest'; +import { beforeEach, expect, test, vi } from 'vitest'; import { loadCsf } from 'storybook/internal/csf-tools'; +import { vol } from 'memfs'; import { dedent } from 'ts-dedent'; import { getComponentImports } from './getComponentImports'; +import { fsMocks } from './test-utils'; + +vi.mock('node:fs/promises', async () => (await import('memfs')).fs.promises); +vi.mock('node:fs', async () => (await import('memfs')).fs); +vi.mock('tsconfig-paths', () => ({ loadConfig: () => ({ resultType: null!, message: null! }) })); + +// Mock resolveImport to deterministically resolve known relative imports for these tests +vi.mock('storybook/internal/common', async (importOriginal) => ({ + ...(await importOriginal()), + resolveImport: (id: string) => { + return { + './Button': './src/stories/Button.tsx', + './Header': './src/stories/Header.tsx', + }[id]; + }, +})); + +beforeEach(() => { + vi.spyOn(process, 'cwd').mockReturnValue('/app'); + vol.fromJSON(fsMocks, '/app'); + return () => vol.reset(); +}); -const getImports = (code: string, packageName?: string) => +const getImports = (code: string, packageName?: string, storyFilePath?: string) => getComponentImports({ csf: loadCsf(code, { makeTitle: (t?: string) => t ?? 'title' }).parse(), packageName, + storyFilePath, }); test('Get imports from multiple components', async () => { @@ -454,21 +478,201 @@ test('Converts default relative import to named when packageName provided', asyn export default meta; export const S =
; `; - expect(getImports(code, 'my-package')).toMatchInlineSnapshot( - `Promise {}` + expect( + await getImports(code, 'my-package', '/app/src/stories/Header.stories.tsx') + ).toMatchInlineSnapshot( + ` + { + "components": [ + { + "componentName": "Header", + "importId": "./Header", + "importName": "default", + "importOverride": "import { Header } from '@design-system/components/Header';", + "localImportName": "Header", + "path": "./src/stories/Header.tsx", + "reactDocgen": { + "data": { + "actualName": "", + "definedInFile": "./src/stories/Header.tsx", + "description": "@import import { Header } from '@design-system/components/Header';", + "exportName": "default", + "methods": [], + "props": { + "onCreateAccount": { + "description": "", + "required": false, + "tsType": { + "name": "signature", + "raw": "() => void", + "signature": { + "arguments": [], + "return": { + "name": "void", + }, + }, + "type": "function", + }, + }, + "onLogin": { + "description": "", + "required": false, + "tsType": { + "name": "signature", + "raw": "() => void", + "signature": { + "arguments": [], + "return": { + "name": "void", + }, + }, + "type": "function", + }, + }, + "onLogout": { + "description": "", + "required": false, + "tsType": { + "name": "signature", + "raw": "() => void", + "signature": { + "arguments": [], + "return": { + "name": "void", + }, + }, + "type": "function", + }, + }, + "user": { + "description": "", + "required": false, + "tsType": { + "name": "User", + }, + }, + }, + }, + "type": "success", + }, + }, + ], + "imports": [ + "import { Header } from "my-package";", + ], + } + ` ); }); test('Converts relative import to provided packageName', async () => { const code = dedent` - import { Button } from './components/Button'; + import { Button } from './Button'; const meta = {}; export default meta; export const S = + ); + };`, + ['./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; + export default meta; + type Story = StoryObj; + 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) => ( +
+
+
+ {user ? ( + <> + + Welcome, {user.name}! + +
+
+
+ );`, +}; From 70e8504b38ad405a427e2e2548ef20f576425f62 Mon Sep 17 00:00:00 2001 From: Kasper Peulen Date: Mon, 3 Nov 2025 20:00:07 +0100 Subject: [PATCH 17/35] Fix auto imports --- code/core/src/core-server/dev-server.ts | 3 +- code/core/src/core-server/manifest.ts | 2 +- .../src/componentManifest/generator.test.ts | 20 +- .../react/src/componentManifest/generator.ts | 263 +++++++++--------- .../getComponentImports.test.ts | 139 ++++++++- .../componentManifest/getComponentImports.ts | 121 ++++++-- .../src/componentManifest/reactDocgen.ts | 59 ++-- .../react/src/componentManifest/utils.ts | 36 ++- 8 files changed, 458 insertions(+), 185 deletions(-) diff --git a/code/core/src/core-server/dev-server.ts b/code/core/src/core-server/dev-server.ts index 4c373f9f0723..594f1e886065 100644 --- a/code/core/src/core-server/dev-server.ts +++ b/code/core/src/core-server/dev-server.ts @@ -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(`
${e instanceof Error ? e.toString() : String(e)}
`); + invariant(e instanceof Error); + res.end(`
${e.toString()}\n${e.stack}
`); } }); } diff --git a/code/core/src/core-server/manifest.ts b/code/core/src/core-server/manifest.ts index b289cd01a357..6c3f136b2dea 100644 --- a/code/core/src/core-server/manifest.ts +++ b/code/core/src/core-server/manifest.ts @@ -774,7 +774,7 @@ function renderComponentCard(key: string, c: ComponentManifest, id: string) { ${esc(ex.name)} story error - ${ex?.summary ? `
${esc(ex.summary)}
` : ''} + ${ex?.summary ? `
Summary: ${esc(ex.summary)}
` : ''} ${ex?.description ? `
${esc(ex.description)}
` : ''} ${ex?.snippet ? `
${esc(ex.snippet)}
` : ''} ${ex?.error?.message ? `
${esc(ex.error.message)}
` : ''} diff --git a/code/renderers/react/src/componentManifest/generator.test.ts b/code/renderers/react/src/componentManifest/generator.test.ts index 69f28c540bde..e9fcd06c2a82 100644 --- a/code/renderers/react/src/componentManifest/generator.test.ts +++ b/code/renderers/react/src/componentManifest/generator.test.ts @@ -136,7 +136,7 @@ test('componentManifestGenerator generates correct id, name, description and exa "description": "Primary UI component for user interaction", "error": undefined, "id": "example-button", - "import": "import { Button } from '@design-system/components/Button';", + "import": "import { Button } from \"@design-system/components/Button\";", "jsDocTags": { "import": [ "import { Button } from '@design-system/components/Button';", @@ -223,20 +223,28 @@ test('componentManifestGenerator generates correct id, name, description and exa }, "stories": [ { + "description": undefined, "name": "Primary", "snippet": "const Primary = () => ;", + "summary": undefined, }, { + "description": undefined, "name": "Secondary", "snippet": "const Secondary = () => ;", + "summary": undefined, }, { + "description": undefined, "name": "Large", "snippet": "const Large = () => ;", + "summary": undefined, }, { + "description": undefined, "name": "Small", "snippet": "const Small = () => ;", + "summary": undefined, }, ], "summary": undefined, @@ -245,7 +253,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 \"@design-system/components/Header\";", "jsDocTags": { "import": [ "import { Header } from '@design-system/components/Header';", @@ -319,16 +327,20 @@ test('componentManifestGenerator generates correct id, name, description and exa }, "stories": [ { + "description": undefined, "name": "LoggedIn", "snippet": "const LoggedIn = () =>
;", + "summary": undefined, }, { + "description": undefined, "name": "LoggedOut", "snippet": "const LoggedOut = () =>
;", + "summary": undefined, }, ], "summary": "Component summary", @@ -422,7 +434,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": "import { Button } from "some-package";", + "import": "import { Button } from \"some-package\";", "jsDocTags": {}, "name": "Button", "path": "./src/stories/Button.stories.ts", @@ -449,8 +461,10 @@ test('fall back to index title when no component name', async () => { }, "stories": [ { + "description": undefined, "name": "Primary", "snippet": "const Primary = () =>
; `; - expect(await getImports(code)).toMatchInlineSnapshot( + expect(getImports(code)).toMatchInlineSnapshot( ` { "components": [ @@ -44,14 +44,12 @@ test('Get imports from multiple components', async () => { "importId": "@design-system/button", "importName": "Button", "localImportName": "Button", - "path": undefined, }, { "componentName": "ButtonGroup", "importId": "@design-system/button-group", "importName": "ButtonGroup", "localImportName": "ButtonGroup", - "path": undefined, }, ], "imports": [ @@ -63,7 +61,7 @@ test('Get imports from multiple components', async () => { ); }); -test('Namespace import with member usage', async () => { +test('Namespace import with member usage', () => { const code = dedent` import * as Accordion from '@ds/accordion'; @@ -71,7 +69,7 @@ test('Namespace import with member usage', async () => { export default meta; export const S = Hi; `; - expect(await getImports(code)).toMatchInlineSnapshot( + expect(getImports(code)).toMatchInlineSnapshot( ` { "components": [ @@ -92,7 +90,7 @@ test('Namespace import with member usage', async () => { ); }); -test('Named import used as namespace object', async () => { +test('Named import used as namespace object', () => { const code = dedent` import { Accordion } from '@ds/accordion'; @@ -100,7 +98,7 @@ test('Named import used as namespace object', async () => { export default meta; export const S = Hi; `; - expect(await getImports(code)).toMatchInlineSnapshot( + expect(getImports(code)).toMatchInlineSnapshot( ` { "components": [ @@ -120,7 +118,7 @@ test('Named import used as namespace object', async () => { ); }); -test('Default import', async () => { +test('Default import', () => { const code = dedent` import Button from '@ds/button'; @@ -128,7 +126,7 @@ test('Default import', async () => { export default meta; export const S =