import MagicString from 'magic-string';
import { walk } from 'estree-walker';
const isNodeInPatternWeakSet = new WeakSet();
function setIsNodeInPattern(node) {
return isNodeInPatternWeakSet.add(node);
}
function isNodeInPattern(node) {
return isNodeInPatternWeakSet.has(node);
}
/**
* Same logic from \@vue/compiler-core & \@vue/compiler-sfc
* Except this is using acorn AST
*/
function esmWalker(root, { onIdentifier, onImportMeta, onDynamicImport, onCallExpression }) {
const parentStack = [];
const varKindStack = [];
const scopeMap = new WeakMap();
const identifiers = [];
const setScope = (node, name) => {
let scopeIds = scopeMap.get(node);
if (scopeIds && scopeIds.has(name)) {
return;
}
if (!scopeIds) {
scopeIds = new Set();
scopeMap.set(node, scopeIds);
}
scopeIds.add(name);
};
function isInScope(name, parents) {
return parents.some((node) => {
var _scopeMap$get;
return node && ((_scopeMap$get = scopeMap.get(node)) === null || _scopeMap$get === void 0 ? void 0 : _scopeMap$get.has(name));
});
}
function handlePattern(p, parentScope) {
if (p.type === "Identifier") {
setScope(parentScope, p.name);
} else if (p.type === "RestElement") {
handlePattern(p.argument, parentScope);
} else if (p.type === "ObjectPattern") {
p.properties.forEach((property) => {
if (property.type === "RestElement") {
setScope(parentScope, property.argument.name);
} else {
handlePattern(property.value, parentScope);
}
});
} else if (p.type === "ArrayPattern") {
p.elements.forEach((element) => {
if (element) {
handlePattern(element, parentScope);
}
});
} else if (p.type === "AssignmentPattern") {
handlePattern(p.left, parentScope);
} else {
setScope(parentScope, p.name);
}
}
walk(root, {
enter(node, parent) {
if (node.type === "ImportDeclaration") {
return this.skip();
}
// track parent stack, skip for "else-if"/"else" branches as acorn nests
// the ast within "if" nodes instead of flattening them
if (parent && !(parent.type === "IfStatement" && node === parent.alternate)) {
parentStack.unshift(parent);
}
// track variable declaration kind stack used by VariableDeclarator
if (node.type === "VariableDeclaration") {
varKindStack.unshift(node.kind);
}
if (node.type === "CallExpression") {
onCallExpression === null || onCallExpression === void 0 ? void 0 : onCallExpression(node);
}
if (node.type === "MetaProperty" && node.meta.name === "import") {
onImportMeta === null || onImportMeta === void 0 ? void 0 : onImportMeta(node);
} else if (node.type === "ImportExpression") {
onDynamicImport === null || onDynamicImport === void 0 ? void 0 : onDynamicImport(node);
}
if (node.type === "Identifier") {
if (!isInScope(node.name, parentStack) && isRefIdentifier(node, parent, parentStack)) {
// record the identifier, for DFS -> BFS
identifiers.push([node, parentStack.slice(0)]);
}
} else if (isFunctionNode(node)) {
// If it is a function declaration, it could be shadowing an import
// Add its name to the scope so it won't get replaced
if (node.type === "FunctionDeclaration") {
const parentScope = findParentScope(parentStack);
if (parentScope) {
setScope(parentScope, node.id.name);
}
}
// walk function expressions and add its arguments to known identifiers
// so that we don't prefix them
node.params.forEach((p) => {
if (p.type === "ObjectPattern" || p.type === "ArrayPattern") {
handlePattern(p, node);
return;
}
walk(p.type === "AssignmentPattern" ? p.left : p, { enter(child, parent) {
// skip params default value of destructure
if ((parent === null || parent === void 0 ? void 0 : parent.type) === "AssignmentPattern" && (parent === null || parent === void 0 ? void 0 : parent.right) === child) {
return this.skip();
}
if (child.type !== "Identifier") {
return;
}
// do not record as scope variable if is a destructuring keyword
if (isStaticPropertyKey(child, parent)) {
return;
}
// do not record if this is a default value
// assignment of a destructuring variable
if ((parent === null || parent === void 0 ? void 0 : parent.type) === "TemplateLiteral" && (parent === null || parent === void 0 ? void 0 : parent.expressions.includes(child)) || (parent === null || parent === void 0 ? void 0 : parent.type) === "CallExpression" && (parent === null || parent === void 0 ? void 0 : parent.callee) === child) {
return;
}
setScope(node, child.name);
} });
});
} else if (node.type === "Property" && parent.type === "ObjectPattern") {
// mark property in destructuring pattern
setIsNodeInPattern(node);
} else if (node.type === "VariableDeclarator") {
const parentFunction = findParentScope(parentStack, varKindStack[0] === "var");
if (parentFunction) {
handlePattern(node.id, parentFunction);
}
} else if (node.type === "CatchClause" && node.param) {
handlePattern(node.param, node);
}
},
leave(node, parent) {
// untrack parent stack from above
if (parent && !(parent.type === "IfStatement" && node === parent.alternate)) {
parentStack.shift();
}
if (node.type === "VariableDeclaration") {
varKindStack.shift();
}
}
});
// emit the identifier events in BFS so the hoisted declarations
// can be captured correctly
identifiers.forEach(([node, stack]) => {
if (!isInScope(node.name, stack)) {
const parent = stack[0];
const grandparent = stack[1];
const hasBindingShortcut = isStaticProperty(parent) && parent.shorthand && (!isNodeInPattern(parent) || isInDestructuringAssignment(parent, parentStack));
const classDeclaration = parent.type === "PropertyDefinition" && (grandparent === null || grandparent === void 0 ? void 0 : grandparent.type) === "ClassBody" || parent.type === "ClassDeclaration" && node === parent.superClass;
const classExpression = parent.type === "ClassExpression" && node === parent.id;
onIdentifier === null || onIdentifier === void 0 ? void 0 : onIdentifier(node, {
hasBindingShortcut,
classDeclaration,
classExpression
}, stack);
}
});
}
function isRefIdentifier(id, parent, parentStack) {
// declaration id
if (parent.type === "CatchClause" || (parent.type === "VariableDeclarator" || parent.type === "ClassDeclaration") && parent.id === id) {
return false;
}
if (isFunctionNode(parent)) {
// function declaration/expression id
if (parent.id === id) {
return false;
}
// params list
if (parent.params.includes(id)) {
return false;
}
}
// class method name
if (parent.type === "MethodDefinition" && !parent.computed) {
return false;
}
// property key
if (isStaticPropertyKey(id, parent)) {
return false;
}
// object destructuring pattern
if (isNodeInPattern(parent) && parent.value === id) {
return false;
}
// non-assignment array destructuring pattern
if (parent.type === "ArrayPattern" && !isInDestructuringAssignment(parent, parentStack)) {
return false;
}
// member expression property
if (parent.type === "MemberExpression" && parent.property === id && !parent.computed) {
return false;
}
if (parent.type === "ExportSpecifier") {
return false;
}
// is a special keyword but parsed as identifier
if (id.name === "arguments") {
return false;
}
return true;
}
function isStaticProperty(node) {
return node && node.type === "Property" && !node.computed;
}
function isStaticPropertyKey(node, parent) {
return isStaticProperty(parent) && parent.key === node;
}
const functionNodeTypeRE = /Function(?:Expression|Declaration)$|Method$/;
function isFunctionNode(node) {
return functionNodeTypeRE.test(node.type);
}
const blockNodeTypeRE = /^BlockStatement$|^For(?:In|Of)?Statement$/;
function isBlock(node) {
return blockNodeTypeRE.test(node.type);
}
function findParentScope(parentStack, isVar = false) {
return parentStack.find(isVar ? isFunctionNode : isBlock);
}
function isInDestructuringAssignment(parent, parentStack) {
if (parent && (parent.type === "Property" || parent.type === "ArrayPattern")) {
return parentStack.some((i) => i.type === "AssignmentExpression");
}
return false;
}
function getArbitraryModuleIdentifier(node) {
return node.type === "Identifier" ? node.name : node.raw;
}
// TODO: better source map replacement
function automockModule(code, mockType, parse, options = {}) {
const globalThisAccessor = options.globalThisAccessor || "\"__vitest_mocker__\"";
const ast = parse(code);
const m = new MagicString(code);
const allSpecifiers = [];
let importIndex = 0;
for (const _node of ast.body) {
if (_node.type === "ExportAllDeclaration") {
throw new Error(`automocking files with \`export *\` is not supported in browser mode because it cannot be statically analysed`);
}
if (_node.type === "ExportNamedDeclaration") {
const node = _node;
const declaration = node.declaration;
function traversePattern(expression) {
// export const test = '1'
if (expression.type === "Identifier") {
allSpecifiers.push({ name: expression.name });
} else if (expression.type === "ArrayPattern") {
expression.elements.forEach((element) => {
if (!element) {
return;
}
traversePattern(element);
});
} else if (expression.type === "ObjectPattern") {
expression.properties.forEach((property) => {
// export const { ...rest } = {}
if (property.type === "RestElement") {
traversePattern(property);
} else if (property.type === "Property") {
traversePattern(property.value);
} else ;
});
} else if (expression.type === "RestElement") {
traversePattern(expression.argument);
} else if (expression.type === "AssignmentPattern") {
throw new Error(`AssignmentPattern is not supported. Please open a new bug report.`);
} else if (expression.type === "MemberExpression") {
throw new Error(`MemberExpression is not supported. Please open a new bug report.`);
} else ;
}
if (declaration) {
if (declaration.type === "FunctionDeclaration") {
allSpecifiers.push({ name: declaration.id.name });
} else if (declaration.type === "VariableDeclaration") {
declaration.declarations.forEach((declaration) => {
traversePattern(declaration.id);
});
} else if (declaration.type === "ClassDeclaration") {
allSpecifiers.push({ name: declaration.id.name });
} else ;
m.remove(node.start, declaration.start);
}
const specifiers = node.specifiers || [];
const source = node.source;
if (!source && specifiers.length) {
specifiers.forEach((specifier) => {
allSpecifiers.push({
alias: getArbitraryModuleIdentifier(specifier.exported),
name: getArbitraryModuleIdentifier(specifier.local)
});
});
m.remove(node.start, node.end);
} else if (source && specifiers.length) {
const importNames = [];
specifiers.forEach((specifier) => {
const importedName = `__vitest_imported_${importIndex++}__`;
importNames.push([getArbitraryModuleIdentifier(specifier.local), importedName]);
allSpecifiers.push({
name: importedName,
alias: getArbitraryModuleIdentifier(specifier.exported)
});
});
const importString = `import { ${importNames.map(([name, alias]) => `${name} as ${alias}`).join(", ")} } from '${source.value}'`;
m.overwrite(node.start, node.end, importString);
}
}
if (_node.type === "ExportDefaultDeclaration") {
const node = _node;
const declaration = node.declaration;
allSpecifiers.push({
name: "__vitest_default",
alias: "default"
});
m.overwrite(node.start, declaration.start, `const __vitest_default = `);
}
}
const moduleObject = `
const __vitest_current_es_module__ = {
__esModule: true,
${allSpecifiers.map(({ name }) => `["${name}"]: ${name},`).join("\n ")}
}
const __vitest_mocked_module__ = globalThis[${globalThisAccessor}].mockObject(__vitest_current_es_module__, "${mockType}")
`;
const assigning = allSpecifiers.map(({ name }, index) => {
return `const __vitest_mocked_${index}__ = __vitest_mocked_module__["${name}"]`;
}).join("\n");
const redeclarations = allSpecifiers.map(({ name, alias }, index) => {
return ` __vitest_mocked_${index}__ as ${alias || name},`;
}).join("\n");
const specifiersExports = `
export {
${redeclarations}
}
`;
m.append(moduleObject + assigning + specifiersExports);
return m;
}
export { automockModule as a, esmWalker as e };