diff --git a/CHANGELOG.prerelease.md b/CHANGELOG.prerelease.md index 222657de1c60..b8fe0a2c3bae 100644 --- a/CHANGELOG.prerelease.md +++ b/CHANGELOG.prerelease.md @@ -1,3 +1,11 @@ +## 10.1.0-alpha.4 + +- Core: Better handling for TypeScript satisfies/as syntaxes - [#32891](https://github.com/storybookjs/storybook/pull/32891), thanks @yannbf! +- Core: Fix wrong import to fix Yarn PnP support - [#32928](https://github.com/storybookjs/storybook/pull/32928), thanks @yannbf! +- ESlint: Update `@storybook/experimental-nextjs-vite` in `no-renderer-packages` rule - [#32909](https://github.com/storybookjs/storybook/pull/32909), thanks @ndelangen! +- React Native: Update withStorybook setup instructions - [#32919](https://github.com/storybookjs/storybook/pull/32919), thanks @dannyhw! +- React: Change examples to stories in manifests and show correct examples and prop types - [#32908](https://github.com/storybookjs/storybook/pull/32908), thanks @kasperpeulen! + ## 10.1.0-alpha.3 - React: Add manifests/components.html page - [#32905](https://github.com/storybookjs/storybook/pull/32905), thanks @kasperpeulen! diff --git a/code/core/src/component-testing/components/StatusBadge.tsx b/code/core/src/component-testing/components/StatusBadge.tsx index 23a3b26d13a1..7206e7385cc3 100644 --- a/code/core/src/component-testing/components/StatusBadge.tsx +++ b/code/core/src/component-testing/components/StatusBadge.tsx @@ -1,8 +1,8 @@ import React from 'react'; -import { type Color, styled, typography } from 'storybook/theming'; +import { TooltipNote, WithTooltip } from 'storybook/internal/components'; -import { TooltipNote, WithTooltip } from '../../components'; +import { type Color, styled, typography } from 'storybook/theming'; export type PlayStatus = 'rendering' | 'playing' | 'completed' | 'errored' | 'aborted'; diff --git a/code/core/src/core-server/build-static.ts b/code/core/src/core-server/build-static.ts index ef1bc814288f..c4d844ca8d8e 100644 --- a/code/core/src/core-server/build-static.ts +++ b/code/core/src/core-server/build-static.ts @@ -11,7 +11,6 @@ import { import { logger } from 'storybook/internal/node-logger'; import { getPrecedingUpgrade, telemetry } from 'storybook/internal/telemetry'; import type { BuilderOptions, CLIOptions, LoadOptions, Options } from 'storybook/internal/types'; -import { type ComponentManifestGenerator, type ComponentsManifest } from 'storybook/internal/types'; import { global } from '@storybook/global'; @@ -167,7 +166,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; diff --git a/code/core/src/core-server/dev-server.ts b/code/core/src/core-server/dev-server.ts index b5ec0b04dc90..4c373f9f0723 100644 --- a/code/core/src/core-server/dev-server.ts +++ b/code/core/src/core-server/dev-server.ts @@ -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; @@ -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; diff --git a/code/core/src/core-server/manifest.ts b/code/core/src/core-server/manifest.ts index fb7f33677298..591d1f6aa382 100644 --- a/code/core/src/core-server/manifest.ts +++ b/code/core/src/core-server/manifest.ts @@ -12,26 +12,26 @@ export function renderManifestComponentsPage(manifest: ComponentsManifest) { const analyses = entries.map(([, c]) => analyzeComponent(c)); const totals = { components: entries.length, - componentsWithError: analyses.filter((a) => a.hasComponentError).length, - componentsWithWarnings: analyses.filter((a) => a.hasWarns).length, - examples: analyses.reduce((sum, a) => sum + a.totalExamples, 0), - exampleErrors: analyses.reduce((sum, a) => sum + a.exampleErrors, 0), + componentsWithPropTypeError: analyses.filter((a) => a.hasPropTypeError).length, + warnings: analyses.filter((a) => a.hasWarns).length, + stories: analyses.reduce((sum, a) => sum + a.totalStories, 0), + storyErrors: analyses.reduce((sum, a) => sum + a.storyErrors, 0), }; // Top filters (clickable), no tags; 1px active ring lives in CSS via :target const allPill = `All`; const compErrorsPill = - totals.componentsWithError > 0 - ? `${totals.componentsWithError}/${totals.components} component ${plural(totals.componentsWithError, 'error')}` + totals.componentsWithPropTypeError > 0 + ? `${totals.componentsWithPropTypeError}/${totals.components} prop type ${plural(totals.componentsWithPropTypeError, 'error')}` : `${totals.components} components ok`; const compWarningsPill = - totals.componentsWithWarnings > 0 - ? `${totals.componentsWithWarnings}/${totals.components} component ${plural(totals.componentsWithWarnings, 'warning')}` + totals.warnings > 0 + ? `${totals.warnings}/${totals.components} ${plural(totals.warnings, 'warning')}` : ''; - const examplesPill = - totals.exampleErrors > 0 - ? `${totals.exampleErrors}/${totals.examples} example errors` - : `${totals.examples} examples ok`; + const storiesPill = + totals.storyErrors > 0 + ? `${totals.storyErrors}/${totals.stories} story errors` + : `${totals.stories} ${plural(totals.stories, 'story', 'stories')} ok`; const grid = entries.map(([key, c], idx) => renderComponentCard(key, c, `${idx}`)).join(''); @@ -188,7 +188,7 @@ export function renderManifestComponentsPage(manifest: ComponentsManifest) { #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-example-errors:target ~ header .filter-pill[data-k='example-errors'] { + #filter-story-errors:target ~ header .filter-pill[data-k='story-errors'] { box-shadow: 0 0 0 var(--active-ring) currentColor; border-color: currentColor; } @@ -197,18 +197,18 @@ export function renderManifestComponentsPage(manifest: ComponentsManifest) { #filter-all, #filter-errors, #filter-warnings, - #filter-example-errors { + #filter-story-errors { display: none; } main { - padding: 24px 0 40px; + padding: 36px 0 40px; } .grid { display: grid; grid-template-columns: 1fr; - gap: 14px; + gap: 18px; } /* one card per row */ @@ -309,7 +309,8 @@ export function renderManifestComponentsPage(manifest: ComponentsManifest) { /* 1px ring on active toggle */ .tg-err:checked + label.as-toggle, .tg-warn:checked + label.as-toggle, - .tg-ex: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; border-color: currentColor; } @@ -333,11 +334,16 @@ export function renderManifestComponentsPage(manifest: ComponentsManifest) { gap: 8px; } - .tg-ex:checked ~ .panels .panel-ex { + .tg-stories:checked ~ .panels .panel-stories { + display: grid; + gap: 8px; + } + + .tg-props:checked ~ .panels .panel-props { display: grid; } - /* Colored notes for component error + warnings */ + /* Colored notes for prop type error + warnings */ .note { padding: 12px; border: 1px solid var(--border); @@ -356,6 +362,12 @@ export function renderManifestComponentsPage(manifest: ComponentsManifest) { color: #ffe9a6; } + .note.ok { + border-color: color-mix(in srgb, var(--ok) 55%, var(--border)); + background: var(--ok-bg); + color: var(--fg); + } + .note-title { font-weight: 600; margin-bottom: 6px; @@ -365,7 +377,7 @@ export function renderManifestComponentsPage(manifest: ComponentsManifest) { white-space: normal; } - /* Example error cards */ + /* Story error cards */ .ex { padding: 10px; border: 1px solid var(--border); @@ -475,7 +487,7 @@ export function renderManifestComponentsPage(manifest: ComponentsManifest) { } /* CSS-only filtering of cards via top pills */ - #filter-errors:target ~ main .card:not(.has-error):not(.has-example-error) { + #filter-errors:target ~ main .card:not(.has-error):not(.has-story-error) { display: none; } @@ -483,7 +495,7 @@ export function renderManifestComponentsPage(manifest: ComponentsManifest) { display: none; } - #filter-example-errors:target ~ main .card:not(.has-example-error) { + #filter-story-errors:target ~ main .card:not(.has-story-error) { display: none; } @@ -510,7 +522,7 @@ export function renderManifestComponentsPage(manifest: ComponentsManifest) { display: grid; } - .card > .tg-ex:checked ~ .panels .panel-ex { + .card > .tg-stories:checked ~ .panels .panel-stories { display: grid; } @@ -518,7 +530,8 @@ export function renderManifestComponentsPage(manifest: ComponentsManifest) { @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-ex:checked) label[for$='-ex'] { + .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; } @@ -530,11 +543,11 @@ export function renderManifestComponentsPage(manifest: ComponentsManifest) { - +

Components Manifest

-
${allPill}${compErrorsPill}${compWarningsPill}${examplesPill}
+
${allPill}${compErrorsPill}${compWarningsPill}${storiesPill}
@@ -547,7 +560,7 @@ export function renderManifestComponentsPage(manifest: ComponentsManifest) { ${ errorGroups.length - ? `
${errorGroupsHTML}
` + ? `
${errorGroupsHTML}
` : '' } @@ -564,35 +577,29 @@ const esc = (s: unknown) => const plural = (n: number, one: string, many = `${one}s`) => (n === 1 ? one : many); function analyzeComponent(c: ComponentManifest) { - const hasComponentError = !!c.error; + const hasPropTypeError = !!c.error; const warns: string[] = []; - if (!c.description?.trim()) { - warns.push( - 'No description found. Write a jsdoc comment such as /** Component description */ on your component or on your stories meta.' - ); - } - 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';` ); } - const totalExamples = c.examples?.length ?? 0; - const exampleErrors = (c.examples ?? []).filter((e) => !!e?.error).length; - const exampleOk = totalExamples - exampleErrors; + const totalStories = c.stories?.length ?? 0; + const storyErrors = (c.stories ?? []).filter((e) => !!e?.error).length; + const storyOk = totalStories - storyErrors; - const hasAnyError = hasComponentError || exampleErrors > 0; // for status dot (red if any errors) + const hasAnyError = hasPropTypeError || storyErrors > 0; // for status dot (red if any errors) return { - hasComponentError, + hasPropTypeError, hasAnyError, hasWarns: warns.length > 0, warns, - totalExamples, - exampleErrors, - exampleOk, + totalStories, + storyErrors, + storyOk, }; } @@ -607,25 +614,54 @@ function note(title: string, bodyHTML: string, kind: 'warn' | 'err') { function renderComponentCard(key: string, c: ComponentManifest, id: string) { const a = analyzeComponent(c); const statusDot = a.hasAnyError ? 'dot-err' : 'dot-ok'; - const errorExamples = (c.examples ?? []).filter((ex) => !!ex?.error); + const allStories = c.stories ?? []; + const errorStories = allStories.filter((ex) => !!ex?.error); + const okStories = allStories.filter((ex) => !ex?.error); const slug = `c-${id}-${(c.id || key) .toLowerCase() .replace(/[^a-z0-9]+/g, '-') .replace(/^-+|-+$/g, '')}`; - const componentErrorBadge = a.hasComponentError - ? `` + const componentErrorBadge = a.hasPropTypeError + ? `` : ''; const warningsBadge = a.hasWarns ? `` : ''; - const examplesBadge = - a.exampleErrors > 0 - ? `` - : `${a.totalExamples} examples ok`; + const storiesBadge = + a.totalStories > 0 + ? `` + : ''; + + // 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 propEntries = parsedDocgen ? Object.entries(parsedDocgen.props ?? {}) : []; + const propTypesBadge = + !a.hasPropTypeError && propEntries.length > 0 + ? `` + : ''; + + const primaryBadge = componentErrorBadge || propTypesBadge; + + const propsCode = + propEntries.length > 0 + ? propEntries + .sort(([aName], [bName]) => aName.localeCompare(bName)) + .map(([propName, info]) => { + const description = (info?.description ?? '').trim(); + const t = (info?.type ?? 'any').trim(); + const optional = info?.required ? '' : '?'; + const defaultVal = (info?.defaultValue ?? '').trim(); + const def = defaultVal ? ` = ${defaultVal}` : ''; + const doc = description ? `/** ${description} */\n` : ''; + return `${doc}${propName}${optional}: ${t}${def}`; + }) + .join('\n\n') + : ''; const tags = c.jsDocTags && typeof c.jsDocTags === 'object' @@ -642,18 +678,18 @@ function renderComponentCard(key: string, c: ComponentManifest, id: string) { return `

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

- ${componentErrorBadge} + ${primaryBadge} ${warningsBadge} - ${examplesBadge} + ${storiesBadge}
${esc(c.id)} · ${esc(c.path)}
@@ -663,16 +699,17 @@ function renderComponentCard(key: string, c: ComponentManifest, id: string) {
- ${a.hasComponentError ? `` : ''} + ${a.hasPropTypeError ? `` : ''} ${a.hasWarns ? `` : ''} - ${a.exampleErrors > 0 ? `` : ''} + ${a.totalStories > 0 ? `` : ''} + ${!a.hasPropTypeError && propEntries.length > 0 ? `` : ''}
${ - a.hasComponentError + a.hasPropTypeError ? `
- ${note('Component error', `
${esc(c.error?.message || 'Unknown error')}
`, 'err')} + ${note('Prop type error', `
${esc(c.error?.message || 'Unknown error')}
`, 'err')}
` : '' } @@ -685,26 +722,149 @@ function renderComponentCard(key: string, c: ComponentManifest, id: string) { : '' } ${ - a.exampleErrors > 0 + !a.hasPropTypeError && propEntries.length > 0 ? ` -
- ${errorExamples +
+
+
+ Prop types + ${propEntries.length} ${plural(propEntries.length, 'prop type')} +
+
${esc(propsCode)}
+
+
` + : '' + } + ${ + a.totalStories > 0 + ? ` +
+ ${errorStories .map( (ex, j) => ` -
+
- - ${esc(ex?.name ?? `Example ${j + 1}`)} - example error + ${esc(ex.name)} + story error
${ex?.snippet ? `
${esc(ex.snippet)}
` : ''} ${ex?.error?.message ? `
${esc(ex.error.message)}
` : ''}
` ) .join('')} + ${okStories + .map( + (ex, k) => ` +
+
+ ${esc(ex.name)} + story ok +
+ ${ex?.snippet ? `
${esc(ex.snippet)}
` : ''} +
` + ) + .join('')}
` : '' }
`; } + +export type ParsedDocgen = { + props: Record< + string, + { + description?: string; + type?: string; + defaultValue?: string; + required?: boolean; + } + >; +}; + +export const parseReactDocgen = (reactDocgen: any): ParsedDocgen => { + const props: Record = (reactDocgen as any)?.props ?? {}; + return { + props: Object.fromEntries( + Object.entries(props).map(([propName, prop]) => [ + propName, + { + description: prop.description, + type: serializeTsType(prop.tsType ?? prop.type), + defaultValue: prop.defaultValue?.value, + required: prop.required, + }, + ]) + ), + }; +}; + +// Serialize a react-docgen tsType into a TypeScript-like string when raw is not available +function serializeTsType(tsType: any): string | undefined { + if (!tsType) { + return undefined; + } + // Prefer raw if provided + // Prefer raw if provided + if ('raw' in tsType && typeof tsType.raw === 'string' && tsType.raw.trim().length > 0) { + return tsType.raw; + } + + if (!tsType.name) { + return undefined; + } + + if ('elements' in tsType) { + if (tsType.name === 'union') { + const parts = (tsType.elements ?? []).map((el: any) => serializeTsType(el) ?? 'unknown'); + return parts.join(' | '); + } + if (tsType.name === 'intersection') { + const parts = (tsType.elements ?? []).map((el: any) => serializeTsType(el) ?? 'unknown'); + return parts.join(' & '); + } + if (tsType.name === 'Array') { + // Prefer raw earlier; here build fallback + const el = (tsType.elements ?? [])[0]; + const inner = serializeTsType(el) ?? 'unknown'; + return `${inner}[]`; + } + if (tsType.name === 'tuple') { + const parts = (tsType.elements ?? []).map((el: any) => serializeTsType(el) ?? 'unknown'); + return `[${parts.join(', ')}]`; + } + } + if ('value' in tsType && tsType.name === 'literal') { + return tsType.value; + } + if ('signature' in tsType && tsType.name === 'signature') { + if (tsType.type === 'function') { + const args = (tsType.signature?.arguments ?? []).map((a: any) => { + const argType = serializeTsType(a.type) ?? 'any'; + return `${a.name}: ${argType}`; + }); + const ret = serializeTsType(tsType.signature?.return) ?? 'void'; + return `(${args.join(', ')}) => ${ret}`; + } + if (tsType.type === 'object') { + const props = (tsType.signature?.properties ?? []).map((p: any) => { + const req: boolean = Boolean(p.value?.required); + const propType = serializeTsType(p.value) ?? 'any'; + return `${p.key}${req ? '' : '?'}: ${propType}`; + }); + return `{ ${props.join('; ')} }`; + } + return 'unknown'; + } + // Default case (Generic like Item) + if ('elements' in tsType) { + const inner = (tsType.elements ?? []).map((el: any) => serializeTsType(el) ?? 'unknown'); + + if (inner.length > 0) { + return `${tsType.name}<${inner.join(', ')}>`; + } + } + + return tsType.name; +} diff --git a/code/core/src/csf-tools/ConfigFile.test.ts b/code/core/src/csf-tools/ConfigFile.test.ts index 1576d7eb2b0f..f702579aeecd 100644 --- a/code/core/src/csf-tools/ConfigFile.test.ts +++ b/code/core/src/csf-tools/ConfigFile.test.ts @@ -99,6 +99,17 @@ describe('ConfigFile', () => { ) ).toEqual('webpack5'); }); + it('resolves values through various TS satisfies/as syntaxes', () => { + const syntaxes = [ + 'const coreVar = { builder: "webpack5" } as const; export const core = coreVar satisfies any;', + 'const coreVar = { builder: "webpack5" } as const; export const core = coreVar as any;', + 'const coreVar = { builder: "webpack5" } as const satisfies Record; export { coreVar as core };', + ]; + + for (const source of syntaxes) { + expect(getField(['core', 'builder'], source)).toEqual('webpack5'); + } + }); }); describe('module exports', () => { @@ -1877,5 +1888,23 @@ describe('ConfigFile', () => { expect(Object.keys(config._exportDecls)).toHaveLength(3); }); + + it('detects exports object on various TS satisfies/as export syntaxes', () => { + const syntaxes = [ + 'const config = { framework: "foo" }; export default config;', + 'const config = { framework: "foo" }; export default config satisfies StorybookConfig;', + 'const config = { framework: "foo" }; export default config as StorybookConfig;', + 'const config = { framework: "foo" }; export default config as unknown as StorybookConfig;', + 'export default { framework: "foo" };', + 'export default { framework: "foo" } satisfies StorybookConfig;', + 'export default { framework: "foo" } as StorybookConfig;', + 'export default { framework: "foo" } as unknown as StorybookConfig;', + ]; + for (const source of syntaxes) { + const config = loadConfig(source).parse(); + expect(config._exportsObject?.type).toBe('ObjectExpression'); + expect(config._exportsObject?.properties).toHaveLength(1); + } + }); }); }); diff --git a/code/core/src/csf-tools/ConfigFile.ts b/code/core/src/csf-tools/ConfigFile.ts index 70fc45d17f37..91e2e3d5c402 100644 --- a/code/core/src/csf-tools/ConfigFile.ts +++ b/code/core/src/csf-tools/ConfigFile.ts @@ -39,13 +39,6 @@ const propKey = (p: t.ObjectProperty) => { return null; }; -const unwrap = (node: t.Node | undefined | null): any => { - if (t.isTSAsExpression(node) || t.isTSSatisfiesExpression(node)) { - return unwrap(node.expression); - } - return node; -}; - const _getPath = (path: string[], node: t.Node): t.Node | undefined => { if (path.length === 0) { return node; @@ -175,15 +168,33 @@ export class ConfigFile { (exportsObject.properties as t.ObjectProperty[]).forEach((p) => { const exportName = propKey(p); if (exportName) { - let exportVal = p.value; - if (t.isIdentifier(exportVal)) { - exportVal = _findVarInitialization(exportVal.name, this._ast.program) as any; - } + const exportVal = this._resolveDeclaration(p.value as t.Node); this._exports[exportName] = exportVal as t.Expression; } }); } + /** Unwraps TS assertions/satisfies from a node, to get the underlying node. */ + _unwrap = (node: t.Node | undefined | null): any => { + if (t.isTSAsExpression(node) || t.isTSSatisfiesExpression(node)) { + return this._unwrap(node.expression); + } + return node; + }; + + /** + * Resolve a declaration node by unwrapping TS assertions/satisfies and following identifiers to + * resolve the correct node in case it's an identifier. + */ + _resolveDeclaration = (node: t.Node, parent: t.Node = this._ast.program) => { + const decl = this._unwrap(node); + if (t.isIdentifier(decl) && t.isProgram(parent)) { + const initialization = _findVarInitialization(decl.name, parent); + return initialization ? this._unwrap(initialization) : decl; + } + return decl; + }; + parse() { // eslint-disable-next-line @typescript-eslint/no-this-alias const self = this; @@ -191,12 +202,7 @@ export class ConfigFile { ExportDefaultDeclaration: { enter({ node, parent }) { self.hasDefaultExport = true; - let decl = - t.isIdentifier(node.declaration) && t.isProgram(parent) - ? _findVarInitialization(node.declaration.name, parent) - : node.declaration; - - decl = unwrap(decl); + let decl = self._resolveDeclaration(node.declaration as t.Node, parent); // csf factory if (t.isCallExpression(decl) && t.isObjectExpression(decl.arguments[0])) { @@ -223,10 +229,7 @@ export class ConfigFile { node.declaration.declarations.forEach((decl) => { if (t.isVariableDeclarator(decl) && t.isIdentifier(decl.id)) { const { name: exportName } = decl.id; - let exportVal = decl.init as t.Expression; - if (t.isIdentifier(exportVal)) { - exportVal = _findVarInitialization(exportVal.name, parent as t.Program) as any; - } + const exportVal = self._resolveDeclaration(decl.init as t.Node, parent); self._exports[exportName] = exportVal; self._exportDecls[exportName] = decl; } @@ -252,7 +255,7 @@ export class ConfigFile { const decl = _findVarDeclarator(localName, parent as t.Program) as any; // decl can be empty in case X from `import { X } from ....` because it is not handled in _findVarDeclarator if (decl) { - self._exports[exportName] = decl.init; + self._exports[exportName] = self._resolveDeclaration(decl.init, parent); self._exportDecls[exportName] = decl; } } @@ -280,24 +283,14 @@ export class ConfigFile { left.property.name === 'exports' ) { let exportObject = right; - if (t.isIdentifier(right)) { - exportObject = _findVarInitialization(right.name, parent as t.Program) as any; - } - - exportObject = unwrap(exportObject); + exportObject = self._resolveDeclaration(exportObject as t.Node, parent); if (t.isObjectExpression(exportObject)) { self._exportsObject = exportObject; (exportObject.properties as t.ObjectProperty[]).forEach((p) => { const exportName = propKey(p); if (exportName) { - let exportVal = p.value as t.Expression; - if (t.isIdentifier(exportVal)) { - exportVal = _findVarInitialization( - exportVal.name, - parent as t.Program - ) as any; - } + const exportVal = self._resolveDeclaration(p.value as t.Node, parent); self._exports[exportName] = exportVal as t.Expression; } }); @@ -564,14 +557,9 @@ export class ConfigFile { } // default export if (t.isExportDefaultDeclaration(node)) { - let decl: t.Expression | undefined | null = node.declaration as t.Expression; - if (t.isIdentifier(decl)) { - decl = _findVarInitialization(decl.name, this._ast.program); - } - - decl = unwrap(decl); - if (t.isObjectExpression(decl)) { - const properties = decl.properties as t.ObjectProperty[]; + const resolved = this._resolveDeclaration(node.declaration as t.Node); + if (t.isObjectExpression(resolved)) { + const properties = resolved.properties as t.ObjectProperty[]; removeProperty(properties, path[0]); removedRootProperty = true; } diff --git a/code/core/src/types/modules/core-common.ts b/code/core/src/types/modules/core-common.ts index 4918d619c6c4..229e7489608e 100644 --- a/code/core/src/types/modules/core-common.ts +++ b/code/core/src/types/modules/core-common.ts @@ -352,7 +352,7 @@ export interface ComponentManifest { description?: string; import?: string; summary?: string; - examples: { name: string; snippet?: string; error?: { name: string; message: string } }[]; + stories: { name: string; snippet?: string; error?: { name: string; message: string } }[]; jsDocTags: Record; error?: { name: string; message: string }; } @@ -381,7 +381,7 @@ export interface StorybookConfigRaw { */ addons?: Preset[]; core?: CoreConfig; - componentManifestGenerator?: ComponentManifestGenerator; + experimental_componentManifestGenerator?: ComponentManifestGenerator; experimental_enrichCsf?: CsfEnricher; staticDirs?: (DirectoryMapping | string)[]; logLevel?: string; diff --git a/code/lib/cli-storybook/src/codemod/helpers/config-to-csf-factory.test.ts b/code/lib/cli-storybook/src/codemod/helpers/config-to-csf-factory.test.ts index d7aeba97d34b..48aa77ccff9b 100644 --- a/code/lib/cli-storybook/src/codemod/helpers/config-to-csf-factory.test.ts +++ b/code/lib/cli-storybook/src/codemod/helpers/config-to-csf-factory.test.ts @@ -58,6 +58,36 @@ describe('main/preview codemod: general parsing functionality', () => { }); `); }); + it('should wrap defineMain call from const declared default export with different type annotations', async () => { + const typedVariants = [ + 'export default config;', + 'export default config satisfies StorybookConfig;', + 'export default config as StorybookConfig;', + 'export default config as unknown as StorybookConfig;', + ]; + + for (const variant of typedVariants) { + await expect( + transform(dedent` + const config = { + stories: ['../src/**/*.stories.@(js|jsx|ts|tsx)'], + addons: ['@storybook/addon-essentials'], + framework: '@storybook/react-vite', + }; + + ${variant} + `) + ).resolves.toMatchInlineSnapshot(` + import { defineMain } from '@storybook/react-vite/node'; + + export default defineMain({ + stories: ['../src/**/*.stories.@(js|jsx|ts|tsx)'], + addons: ['@storybook/addon-essentials'], + framework: '@storybook/react-vite', + }); + `); + } + }); it('should wrap defineMain call from const declared default export and default export mix', async () => { await expect( diff --git a/code/lib/cli-storybook/src/codemod/helpers/config-to-csf-factory.ts b/code/lib/cli-storybook/src/codemod/helpers/config-to-csf-factory.ts index c49d802979cb..42b6c1787e51 100644 --- a/code/lib/cli-storybook/src/codemod/helpers/config-to-csf-factory.ts +++ b/code/lib/cli-storybook/src/codemod/helpers/config-to-csf-factory.ts @@ -119,8 +119,11 @@ export async function configToCsfFactory( programNode.body.forEach((node) => { // Detect Syntax 1 - if (t.isExportDefaultDeclaration(node) && t.isIdentifier(node.declaration)) { - const declarationName = node.declaration.name; + const declaration = + t.isExportDefaultDeclaration(node) && config._unwrap(node.declaration as t.Node); + + if (t.isExportDefaultDeclaration(node) && t.isIdentifier(declaration)) { + const declarationName = declaration.name; declarationNodeIndex = findDeclarationNodeIndex(declarationName); diff --git a/code/lib/create-storybook/src/initiate.ts b/code/lib/create-storybook/src/initiate.ts index 3e16e26845f9..eed6de8e1b7e 100644 --- a/code/lib/create-storybook/src/initiate.ts +++ b/code/lib/create-storybook/src/initiate.ts @@ -700,7 +700,7 @@ export async function doInitiate(options: CommandOptions): Promise< 2. Wrap your metro config with the withStorybook enhancer function like this: - ${picocolors.inverse(' ' + "const withStorybook = require('@storybook/react-native/metro/withStorybook');" + ' ')} + ${picocolors.inverse(' ' + "const { withStorybook } = require('@storybook/react-native/metro/withStorybook');" + ' ')} ${picocolors.inverse(' ' + 'module.exports = withStorybook(defaultConfig);' + ' ')} For more details go to: diff --git a/code/lib/eslint-plugin/src/rules/no-renderer-packages.test.ts b/code/lib/eslint-plugin/src/rules/no-renderer-packages.test.ts index 1c2c0cd731ad..246e15494b93 100644 --- a/code/lib/eslint-plugin/src/rules/no-renderer-packages.test.ts +++ b/code/lib/eslint-plugin/src/rules/no-renderer-packages.test.ts @@ -65,7 +65,7 @@ ruleTester.run('no-renderer-packages', rule, { data: { rendererPackage: '@storybook/react', suggestions: - '@storybook/nextjs, @storybook/react-vite, @storybook/react-webpack5, @storybook/react-native-web-vite, @storybook/experimental-nextjs-vite', + '@storybook/nextjs, @storybook/react-vite, @storybook/nextjs-vite, @storybook/react-webpack5, @storybook/react-native-web-vite', }, type: AST_NODE_TYPES.ImportDeclaration, }, diff --git a/code/lib/eslint-plugin/src/rules/no-renderer-packages.ts b/code/lib/eslint-plugin/src/rules/no-renderer-packages.ts index 8bd1767a69ac..c9923de261cf 100644 --- a/code/lib/eslint-plugin/src/rules/no-renderer-packages.ts +++ b/code/lib/eslint-plugin/src/rules/no-renderer-packages.ts @@ -22,9 +22,9 @@ const rendererToFrameworks: Record = { '@storybook/react': [ '@storybook/nextjs', '@storybook/react-vite', + '@storybook/nextjs-vite', '@storybook/react-webpack5', '@storybook/react-native-web-vite', - '@storybook/experimental-nextjs-vite', ], '@storybook/server': ['@storybook/server-webpack5'], '@storybook/svelte': [ diff --git a/code/package.json b/code/package.json index 4e06b8fa0e26..9e414548879f 100644 --- a/code/package.json +++ b/code/package.json @@ -283,5 +283,6 @@ "Dependency Upgrades" ] ] - } + }, + "deferredNextVersion": "10.1.0-alpha.4" } diff --git a/code/renderers/react/src/componentManifest/generator.test.ts b/code/renderers/react/src/componentManifest/generator.test.ts index c330f3c511fd..fa13535eb68c 100644 --- a/code/renderers/react/src/componentManifest/generator.test.ts +++ b/code/renderers/react/src/componentManifest/generator.test.ts @@ -216,24 +216,6 @@ test('componentManifestGenerator generates correct id, name, description and exa "example-button": { "description": "Primary UI component for user interaction", "error": undefined, - "examples": [ - { - "name": "Primary", - "snippet": "const Primary = () => ;", - }, - { - "name": "Secondary", - "snippet": "const Secondary = () => ;", - }, - { - "name": "Large", - "snippet": "const Large = () => ;", - }, - { - "name": "Small", - "snippet": "const Small = () => ;", - }, - ], "id": "example-button", "import": undefined, "jsDocTags": {}, @@ -315,25 +297,29 @@ test('componentManifestGenerator generates correct id, name, description and exa }, }, }, - "summary": undefined, - }, - "example-header": { - "description": "Description from meta and very long.", - "error": undefined, - "examples": [ + "stories": [ { - "name": "LoggedIn", - "snippet": "const LoggedIn = () =>
;", + "name": "Primary", + "snippet": "const Primary = () => ;", }, { - "name": "LoggedOut", - "snippet": "const LoggedOut = () =>
;", + "name": "Secondary", + "snippet": "const Secondary = () => ;", + }, + { + "name": "Large", + "snippet": "const Large = () => ;", + }, + { + "name": "Small", + "snippet": "const Small = () => ;", }, ], + "summary": undefined, + }, + "example-header": { + "description": "Description from meta and very long.", + "error": undefined, "id": "example-header", "import": "import { Header } from '@design-system/components/Header';", "jsDocTags": { @@ -407,6 +393,20 @@ test('componentManifestGenerator generates correct id, name, description and exa }, }, }, + "stories": [ + { + "name": "LoggedIn", + "snippet": "const LoggedIn = () =>
;", + }, + { + "name": "LoggedOut", + "snippet": "const LoggedOut = () =>
;", + }, + ], "summary": "Component summary", }, }, @@ -496,12 +496,6 @@ test('fall back to index title when no component name', async () => { { "description": "Primary UI component for user interaction", "error": undefined, - "examples": [ - { - "name": "Primary", - "snippet": "const Primary = () =>