Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 29 additions & 0 deletions code/core/src/csf-tools/ConfigFile.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, unknown>; export { coreVar as core };',
];

for (const source of syntaxes) {
expect(getField(['core', 'builder'], source)).toEqual('webpack5');
}
});
});

describe('module exports', () => {
Expand Down Expand Up @@ -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);
}
});
});
});
72 changes: 30 additions & 42 deletions code/core/src/csf-tools/ConfigFile.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -175,28 +168,41 @@ 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;
traverse(this._ast, {
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])) {
Expand All @@ -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;
}
Expand All @@ -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;
}
}
Expand Down Expand Up @@ -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;
}
});
Expand Down Expand Up @@ -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;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand Down