Skip to content
Merged
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
1 change: 0 additions & 1 deletion code/addons/vitest/src/postinstall.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import { existsSync } from 'node:fs';
import * as fs from 'node:fs/promises';
import { writeFile } from 'node:fs/promises';
import os from 'node:os';
Expand Down
122 changes: 7 additions & 115 deletions code/addons/vitest/src/updateVitestFile.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,10 @@
import { resolveExpression } from 'storybook/internal/babel';
import {
getConfigObjectFromMergeArg,
getEffectiveMergeConfigCall,
getTargetConfigObject,
isDefineConfigLike,
resolveExpression,
} from 'storybook/internal/babel';
import type { BabelFile, types as t } from 'storybook/internal/babel';

import { normalize } from 'pathe';
Expand Down Expand Up @@ -69,57 +75,6 @@ const mergeProperties = (
}
};

/**
* Returns true if the identifier is a local alias for `defineConfig`/`defineProject` imported from
* either `vitest/config` or `vite`.
*/
const isImportedDefineConfigLikeIdentifier = (localName: string, ast: BabelFile['ast']): boolean =>
ast.program.body.some(
(node): boolean =>
node.type === 'ImportDeclaration' &&
(node.source.value === 'vitest/config' || node.source.value === 'vite') &&
node.specifiers.some(
(specifier) =>
specifier.type === 'ImportSpecifier' &&
specifier.local.type === 'Identifier' &&
specifier.local.name === localName &&
specifier.imported.type === 'Identifier' &&
(specifier.imported.name === 'defineConfig' ||
specifier.imported.name === 'defineProject')
)
);

/** Returns true if the call expression is a defineConfig or defineProject call (including aliases). */
const isDefineConfigLike = (node: t.CallExpression, ast: BabelFile['ast']): boolean =>
node.callee.type === 'Identifier' &&
(node.callee.name === 'defineConfig' ||
node.callee.name === 'defineProject' ||
isImportedDefineConfigLikeIdentifier(node.callee.name, ast));

/**
* Resolves a mergeConfig argument to a config object expression when possible. Supports both direct
* object args and wrapped forms like `defineConfig({ ... })`.
*/
const getConfigObjectFromMergeArg = (
arg: t.Expression,
ast: BabelFile['ast']
): t.ObjectExpression | null => {
const resolved = resolveExpression(arg, ast);
if (!resolved) {
return null;
}

if (resolved.type === 'ObjectExpression') {
return resolved;
}

if (resolved.type === 'CallExpression' && resolved.arguments[0]?.type === 'ObjectExpression') {
return resolved.arguments[0] as t.ObjectExpression;
}

return null;
};

/**
* Resolves the value of a `test` ObjectProperty to an ObjectExpression. Handles both inline objects
* and shorthand identifier references, e.g.: `{ test: { ... } }` → returns the inline
Expand Down Expand Up @@ -368,69 +323,6 @@ const mergeTemplateIntoConfigObject = (
mergeProperties(properties, targetConfigObject.properties);
};

/**
* Extracts the effective mergeConfig call from a declaration, handling wrappers:
*
* - TypeScript type annotations (as X, satisfies X)
* - DefineConfig(mergeConfig(...)) outer wrapper
* - Variable references (export default config where config = mergeConfig(...))
*/
const getEffectiveMergeConfigCall = (
decl: t.Expression | t.Declaration,
ast: BabelFile['ast']
): t.CallExpression | null => {
const resolved = resolveExpression(decl, ast);
if (!resolved || resolved.type !== 'CallExpression') {
return null;
}

// Handle defineConfig(mergeConfig(...)) – arg may itself be wrapped in a TS type expression
if (isDefineConfigLike(resolved, ast) && resolved.arguments.length > 0) {
const innerArg = resolveExpression(resolved.arguments[0] as t.Expression, ast);
if (
innerArg?.type === 'CallExpression' &&
innerArg.callee.type === 'Identifier' &&
innerArg.callee.name === 'mergeConfig'
) {
return innerArg;
}
}

// Handle mergeConfig(...) directly
if (resolved.callee.type === 'Identifier' && resolved.callee.name === 'mergeConfig') {
return resolved;
}

return null;
};

/**
* Resolves the target's default export to the actual config object expression we can merge into.
* Handles: export default defineConfig({}), export default defineProject({}), export default {},
* and export default config (where config is a variable holding one of those), as well as
* TypeScript type annotations on the declaration.
*/
const getTargetConfigObject = (
target: BabelFile['ast'],
exportDefault: t.ExportDefaultDeclaration
): t.ObjectExpression | null => {
const resolved = resolveExpression(exportDefault.declaration, target);
if (!resolved) {
return null;
}
if (resolved.type === 'ObjectExpression') {
return resolved;
}
if (
resolved.type === 'CallExpression' &&
isDefineConfigLike(resolved, target) &&
resolved.arguments[0]?.type === 'ObjectExpression'
) {
return resolved.arguments[0] as t.ObjectExpression;
}
return null;
};

/**
* Merges a source Vitest configuration AST into a target configuration AST.
*
Expand Down
9 changes: 9 additions & 0 deletions code/core/src/babel/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,15 @@ import * as recast from 'recast';

export * from './babelParse';
export { unwrapTSExpression, resolveExpression } from './expression-resolver';
export {
isImportedDefineConfigLikeIdentifier,
isDefineConfigLike,
getConfigObjectFromMergeArg,
getEffectiveMergeConfigCall,
getTargetConfigObject,
canUpdateVitestConfigFile,
canUpdateVitestWorkspaceFile,
} from './vitest-config-helpers';

// @ts-expect-error (needed due to it's use of `exports.default`)
const traverse = (bt.default || bt) as typeof bt;
Expand Down
Loading
Loading