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/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);