diff --git a/code/.storybook/main.ts b/code/.storybook/main.ts index d5e117afdc74..279e8a138b51 100644 --- a/code/.storybook/main.ts +++ b/code/.storybook/main.ts @@ -138,7 +138,6 @@ const config = defineMain({ developmentModeForBuild: true, experimentalTestSyntax: true, experimentalComponentsManifest: true, - experimentalCodeExamples: true, }, staticDirs: [{ from: './bench/bundle-analyzer', to: '/bundle-analyzer' }], viteFinal: async (viteConfig, { configType }) => { diff --git a/code/addons/vitest/tsconfig.json b/code/addons/vitest/tsconfig.json index 8f0586c10653..d2318b7bb29f 100644 --- a/code/addons/vitest/tsconfig.json +++ b/code/addons/vitest/tsconfig.json @@ -5,5 +5,5 @@ "types": ["vitest"], "strict": true }, - "include": ["src/**/*", "./typings.d.ts"], + "include": ["src/**/*", "./typings.d.ts"] } diff --git a/code/core/src/core-server/dev-server.ts b/code/core/src/core-server/dev-server.ts index 4c373f9f0723..cd3c5d02f1b8 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.stack}
`); } }); } diff --git a/code/core/src/core-server/manifest.ts b/code/core/src/core-server/manifest.ts index 591d1f6aa382..6e6f18b23533 100644 --- a/code/core/src/core-server/manifest.ts +++ b/code/core/src/core-server/manifest.ts @@ -1,3 +1,5 @@ +import path from 'node:path'; + import { groupBy } from 'storybook/internal/common'; import type { ComponentManifest, ComponentsManifest } from '../types'; @@ -13,7 +15,7 @@ export function renderManifestComponentsPage(manifest: ComponentsManifest) { const totals = { components: entries.length, componentsWithPropTypeError: analyses.filter((a) => a.hasPropTypeError).length, - warnings: analyses.filter((a) => a.hasWarns).length, + infos: analyses.filter((a) => a.hasWarns).length, stories: analyses.reduce((sum, a) => sum + a.totalStories, 0), storyErrors: analyses.reduce((sum, a) => sum + a.storyErrors, 0), }; @@ -24,9 +26,9 @@ export function renderManifestComponentsPage(manifest: ComponentsManifest) { totals.componentsWithPropTypeError > 0 ? `${totals.componentsWithPropTypeError}/${totals.components} prop type ${plural(totals.componentsWithPropTypeError, 'error')}` : `${totals.components} components ok`; - const compWarningsPill = - totals.warnings > 0 - ? `${totals.warnings}/${totals.components} ${plural(totals.warnings, 'warning')}` + const compInfosPill = + totals.infos > 0 + ? `${totals.infos}/${totals.components} ${plural(totals.infos, 'info', 'infos')}` : ''; const storiesPill = totals.storyErrors > 0 @@ -40,7 +42,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]) => { @@ -76,10 +78,10 @@ export function renderManifestComponentsPage(manifest: ComponentsManifest) { --muted: #9aa0a6; --fg: #e8eaed; --ok: #22c55e; - --warn: #b08900; + --info: #1e88e5; --err: #c62828; --ok-bg: #0c1a13; - --warn-bg: #1a1608; + --info-bg: #0c1624; --err-bg: #1a0e0e; --chip: #1f2330; --border: #2b2f3a; @@ -155,10 +157,10 @@ export function renderManifestComponentsPage(manifest: ComponentsManifest) { background: color-mix(in srgb, var(--ok) 18%, #000); } - .filter-pill.warn { - color: #ffd666; - border-color: color-mix(in srgb, var(--warn) 55%, var(--border)); - background: var(--warn-bg); + .filter-pill.info { + color: #b3d9ff; + border-color: color-mix(in srgb, var(--info) 55%, var(--border)); + background: var(--info-bg); } .filter-pill.err { @@ -187,7 +189,7 @@ export function renderManifestComponentsPage(manifest: ComponentsManifest) { /* Selected top pill ring via :target */ #filter-all:target ~ header .filter-pill[data-k='all'], #filter-errors:target ~ header .filter-pill[data-k='errors'], - #filter-warnings:target ~ header .filter-pill[data-k='warnings'], + #filter-infos:target ~ header .filter-pill[data-k='infos'], #filter-story-errors:target ~ header .filter-pill[data-k='story-errors'] { box-shadow: 0 0 0 var(--active-ring) currentColor; border-color: currentColor; @@ -196,7 +198,7 @@ export function renderManifestComponentsPage(manifest: ComponentsManifest) { /* Hidden targets for filtering */ #filter-all, #filter-errors, - #filter-warnings, + #filter-infos, #filter-story-errors { display: none; } @@ -292,9 +294,9 @@ export function renderManifestComponentsPage(manifest: ComponentsManifest) { border-color: color-mix(in srgb, var(--ok) 55%, var(--border)); } - .badge.warn { - color: #ffd666; - border-color: color-mix(in srgb, var(--warn) 55%, var(--border)); + .badge.info { + color: #b3d9ff; + border-color: color-mix(in srgb, var(--info) 55%, var(--border)); } .badge.err { @@ -308,7 +310,7 @@ export function renderManifestComponentsPage(manifest: ComponentsManifest) { /* 1px ring on active toggle */ .tg-err:checked + label.as-toggle, - .tg-warn:checked + label.as-toggle, + .tg-info:checked + label.as-toggle, .tg-stories:checked + label.as-toggle, .tg-props:checked + label.as-toggle { box-shadow: 0 0 0 var(--active-ring) currentColor; @@ -329,7 +331,7 @@ export function renderManifestComponentsPage(manifest: ComponentsManifest) { display: grid; } - .tg-warn:checked ~ .panels .panel-warn { + .tg-info:checked ~ .panels .panel-info { display: grid; gap: 8px; } @@ -343,7 +345,7 @@ export function renderManifestComponentsPage(manifest: ComponentsManifest) { display: grid; } - /* Colored notes for prop type error + warnings */ + /* Colored notes for prop type error + info */ .note { padding: 12px; border: 1px solid var(--border); @@ -356,10 +358,10 @@ export function renderManifestComponentsPage(manifest: ComponentsManifest) { color: #ffd1d4; } - .note.warn { - border-color: color-mix(in srgb, var(--warn) 55%, var(--border)); - background: var(--warn-bg); - color: #ffe9a6; + .note.info { + border-color: color-mix(in srgb, var(--info) 55%, var(--border)); + background: var(--info-bg); + color: #d6e8ff; } .note.ok { @@ -491,7 +493,7 @@ export function renderManifestComponentsPage(manifest: ComponentsManifest) { display: none; } - #filter-warnings:target ~ main .card:not(.has-warn) { + #filter-infos:target ~ main .card:not(.has-info) { display: none; } @@ -517,37 +519,64 @@ export function renderManifestComponentsPage(manifest: ComponentsManifest) { .card > .tg-err:checked ~ .panels .panel-err { display: grid; } - - .card > .tg-warn:checked ~ .panels .panel-warn { + + .card > .tg-info:checked ~ .panels .panel-info { 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-info: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'], - .card:has(.tg-warn:checked) label[for$='-warn'], + .card:has(.tg-info:checked) label[for$='-info'], .card:has(.tg-stories:checked) label[for$='-stories'], .card:has(.tg-props:checked) label[for$='-props'] { box-shadow: 0 0 0 1px currentColor; 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); + } - +

Components Manifest

-
${allPill}${compErrorsPill}${compWarningsPill}${storiesPill}
+
${allPill}${compErrorsPill}${compInfosPill}${storiesPill}
@@ -580,6 +609,10 @@ function analyzeComponent(c: ComponentManifest) { const hasPropTypeError = !!c.error; const warns: string[] = []; + if (!c.description?.trim()) { + warns.push('No description found. Write a jsdoc comment such as /** Component description */.'); + } + if (!c.import?.trim()) { warns.push( `Specify an @import jsdoc tag on your component or your stories meta such as @import import { ${c.name} } from 'my-design-system';` @@ -603,7 +636,7 @@ function analyzeComponent(c: ComponentManifest) { }; } -function note(title: string, bodyHTML: string, kind: 'warn' | 'err') { +function note(title: string, bodyHTML: string, kind: 'info' | 'err') { return `
${esc(title)}
@@ -627,8 +660,8 @@ function renderComponentCard(key: string, c: ComponentManifest, id: string) { ? `` : ''; - const warningsBadge = a.hasWarns - ? `` + const infosBadge = a.hasWarns + ? `` : ''; const storiesBadge = @@ -637,8 +670,8 @@ function renderComponentCard(key: string, c: ComponentManifest, id: string) { : ''; // When there is no prop type error, try to read prop types from reactDocgen if present - const hasDocgen = !a.hasPropTypeError && 'reactDocgen' in c && c.reactDocgen; - const parsedDocgen = hasDocgen ? parseReactDocgen(c.reactDocgen) : undefined; + const reactDocgen: any = !a.hasPropTypeError && 'reactDocgen' in c && c.reactDocgen; + const parsedDocgen = reactDocgen ? parseReactDocgen(reactDocgen) : undefined; const propEntries = parsedDocgen ? Object.entries(parsedDocgen.props ?? {}) : []; const propTypesBadge = !a.hasPropTypeError && propEntries.length > 0 @@ -657,8 +690,10 @@ function renderComponentCard(key: string, c: ComponentManifest, id: string) { const optional = info?.required ? '' : '?'; const defaultVal = (info?.defaultValue ?? '').trim(); const def = defaultVal ? ` = ${defaultVal}` : ''; - const doc = description ? `/** ${description} */\n` : ''; - return `${doc}${propName}${optional}: ${t}${def}`; + const doc = + ['/**', ...description.split('\n').map((line) => ` * ${line}`), ' */'].join('\n') + + '\n'; + return `${description ? doc : ''}${propName}${optional}: ${t}${def}`; }) .join('\n\n') : ''; @@ -679,7 +714,7 @@ function renderComponentCard(key: string, c: ComponentManifest, id: string) {
@@ -688,7 +723,7 @@ function renderComponentCard(key: string, c: ComponentManifest, id: string) {

${esc(c.name || key)}

${primaryBadge} - ${warningsBadge} + ${infosBadge} ${storiesBadge}
@@ -700,7 +735,7 @@ function renderComponentCard(key: string, c: ComponentManifest, id: string) { ${a.hasPropTypeError ? `` : ''} - ${a.hasWarns ? `` : ''} + ${a.hasWarns ? `` : ''} ${a.totalStories > 0 ? `` : ''} ${!a.hasPropTypeError && propEntries.length > 0 ? `` : ''} @@ -716,8 +751,8 @@ function renderComponentCard(key: string, c: ComponentManifest, id: string) { ${ a.hasWarns ? ` -
- ${a.warns.map((w) => note('Warning', esc(w), 'warn')).join('')} +
+ ${a.warns.map((w) => note('Info', esc(w), 'info')).join('')}
` : '' } @@ -730,6 +765,8 @@ function renderComponentCard(key: string, c: ComponentManifest, id: string) { Prop types ${propEntries.length} ${plural(propEntries.length, 'prop type')}
+
Component: ${reactDocgen?.definedInFile ? esc(path.relative(process.cwd(), reactDocgen.definedInFile)) : ''}${reactDocgen?.exportName ? '::' + esc(reactDocgen?.exportName) : ''}
+
Props:
${esc(propsCode)}
` @@ -747,19 +784,36 @@ function renderComponentCard(key: string, c: ComponentManifest, id: string) { ${esc(ex.name)} story error + ${ex?.summary ? `
Summary: ${esc(ex.summary)}
` : ''} + ${ex?.description ? `
${esc(ex.description)}
` : ''} ${ex?.snippet ? `
${esc(ex.snippet)}
` : ''} ${ex?.error?.message ? `
${esc(ex.error.message)}
` : ''} ` ) .join('')} + + + ${ + c.import + ? `
+
+ Imports +
+
${c.import}
+
` + : '' + } + ${okStories .map( - (ex, k) => ` + (ex) => `
${esc(ex.name)} story ok
+ ${ex?.summary ? `
${esc(ex.summary)}
` : ''} + ${ex?.description ? `
${esc(ex.description)}
` : ''} ${ex?.snippet ? `
${esc(ex.snippet)}
` : ''}
` ) diff --git a/code/core/src/node-logger/index.ts b/code/core/src/node-logger/index.ts index ad95f58e1fc0..ef6b083e01c9 100644 --- a/code/core/src/node-logger/index.ts +++ b/code/core/src/node-logger/index.ts @@ -60,17 +60,17 @@ export const logger = { npmLog.level = level; newLogger.setLogLevel(level); }, - error: (message: Error | string): void => { + error: (message: unknown): void => { let msg: string; if (message instanceof Error && message.stack) { - msg = message.stack.toString(); - } else { + msg = message.stack.toString().replace(message.toString(), colors.red(message.toString())); + } else if (typeof message === 'string') { msg = message.toString(); + } else { + msg = String(message); } - newLogger.error( - msg.replace(message.toString(), colors.red(message.toString())).replaceAll(process.cwd(), '.') - ); + newLogger.error(msg.replaceAll(process.cwd(), '.')); }, }; diff --git a/code/core/src/types/modules/core-common.ts b/code/core/src/types/modules/core-common.ts index 229e7489608e..2b7d75a0fe9a 100644 --- a/code/core/src/types/modules/core-common.ts +++ b/code/core/src/types/modules/core-common.ts @@ -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; error?: { name: string; message: string }; } diff --git a/code/renderers/react/package.json b/code/renderers/react/package.json index a5de0f1e68e2..27a63b1b52f3 100644 --- a/code/renderers/react/package.json +++ b/code/renderers/react/package.json @@ -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", diff --git a/code/renderers/react/src/componentManifest/fixtures.ts b/code/renderers/react/src/componentManifest/fixtures.ts new file mode 100644 index 000000000000..075557eb84d1 --- /dev/null +++ b/code/renderers/react/src/componentManifest/fixtures.ts @@ -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; + export default meta; + type Story = StoryObj; + + 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 ( + + ); + };`, + ['./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}! + +
+
+
+ );`, +}; + +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', + }, + }, +}; diff --git a/code/renderers/react/src/componentManifest/generator.test.ts b/code/renderers/react/src/componentManifest/generator.test.ts index fa13535eb68c..405034954ed9 100644 --- a/code/renderers/react/src/componentManifest/generator.test.ts +++ b/code/renderers/react/src/componentManifest/generator.test.ts @@ -5,208 +5,17 @@ import { type StoryIndexGenerator } from 'storybook/internal/core-server'; import { vol } from 'memfs'; import { dedent } from 'ts-dedent'; +import { fsMocks, indexJson } from './fixtures'; import { componentManifestGenerator } from './generator'; -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! }) })); - -// Use the provided indexJson from this file -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', - }, - }, -}; - beforeEach(() => { vi.spyOn(process, 'cwd').mockReturnValue('/app'); - vol.fromJSON( - { - ['./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; - export default meta; - type Story = StoryObj; - - 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 */ - export const Button = ({ - primary = false, - size = 'medium', - backgroundColor, - label, - ...props - }: ButtonProps) => { - const mode = primary ? 'storybook-button--primary' : 'storybook-button--secondary'; - return ( - - ); - };`, - ['./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' - ); - return () => vol.reset(); + vol.fromJSON(fsMocks, '/app'); }); 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,14 +26,19 @@ test('componentManifestGenerator generates correct id, name, description and exa "description": "Primary UI component for user interaction", "error": undefined, "id": "example-button", - "import": undefined, - "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": [], @@ -299,20 +113,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, @@ -321,7 +143,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';", @@ -334,8 +156,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": { @@ -395,16 +217,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", @@ -418,6 +244,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 +268,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 +286,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,13 +324,13 @@ 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", "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", @@ -524,8 +351,10 @@ test('fall back to index title when no component name', async () => { }, "stories": [ { + "description": undefined, "name": "Primary", "snippet": "const Primary = () => ; + `; + expect(getImports(code)).toMatchInlineSnapshot( + ` + { + "components": [ + { + "componentName": "Button", + "importId": "@design-system/button", + "importName": "Button", + "localImportName": "Button", + }, + { + "componentName": "ButtonGroup", + "importId": "@design-system/button-group", + "importName": "ButtonGroup", + "localImportName": "ButtonGroup", + }, + ], + "imports": [ + "import { Button } from "@design-system/button";", + "import { ButtonGroup } from "@design-system/button-group";", + ], + } + ` + ); +}); + +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": [ + { + "componentName": "Accordion.Root", + "importId": "@ds/accordion", + "importName": "Root", + "localImportName": "Accordion", + "namespace": "Accordion", + }, + ], + "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": [ + { + "componentName": "Accordion.Root", + "importId": "@ds/accordion", + "importName": "Accordion", + "localImportName": "Accordion", + }, + ], + "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 =