perf(coding-agent): replace legacy Babel traversal
This commit is contained in:
@@ -76,7 +76,6 @@
|
||||
"dependencies": {
|
||||
"@agentclientprotocol/sdk": "catalog:",
|
||||
"@babel/parser": "catalog:",
|
||||
"@babel/traverse": "catalog:",
|
||||
"@mozilla/readability": "catalog:",
|
||||
"@oh-my-pi/hashline": "catalog:",
|
||||
"@oh-my-pi/omp-stats": "catalog:",
|
||||
|
||||
@@ -51,7 +51,6 @@
|
||||
"dependencies": {
|
||||
"@agentclientprotocol/sdk": "catalog:",
|
||||
"@babel/parser": "catalog:",
|
||||
"@babel/traverse": "catalog:",
|
||||
"@mozilla/readability": "catalog:",
|
||||
"@oh-my-pi/hashline": "catalog:",
|
||||
"@oh-my-pi/omp-stats": "catalog:",
|
||||
|
||||
@@ -6,30 +6,11 @@ import * as path from "node:path";
|
||||
import * as url from "node:url";
|
||||
import type { ParseResult, ParserPlugin } from "@babel/parser";
|
||||
import { parse as parseBabel } from "@babel/parser";
|
||||
import * as traverseModule from "@babel/traverse";
|
||||
import { isCompiledBinary, stripWindowsExtendedLengthPathPrefix } from "@oh-my-pi/pi-utils";
|
||||
import { registerPluginCacheInvalidator } from "../../discovery/helpers";
|
||||
|
||||
const IS_COMPILED_BINARY = isCompiledBinary();
|
||||
|
||||
function isBabelTraverse(value: unknown): value is typeof traverseModule.default {
|
||||
return typeof value === "function";
|
||||
}
|
||||
|
||||
// Bun's compiled CJS interop wraps Babel traverse's default one level deeper.
|
||||
const traverseDefault: unknown = traverseModule.default;
|
||||
const nestedTraverse =
|
||||
traverseDefault !== null && typeof traverseDefault === "object" && "default" in traverseDefault
|
||||
? traverseDefault.default
|
||||
: undefined;
|
||||
const traverseCandidate = isBabelTraverse(traverseDefault) ? traverseDefault : nestedTraverse;
|
||||
if (!isBabelTraverse(traverseCandidate)) {
|
||||
throw new TypeError(
|
||||
`Invalid @babel/traverse export: expected function, got default=${typeof traverseDefault}, nested=${typeof nestedTraverse}`,
|
||||
);
|
||||
}
|
||||
const traverseAst = traverseCandidate;
|
||||
|
||||
// === Bundled host modules (issue #3423) ===
|
||||
//
|
||||
// Bun 1.3.14 stopped exposing `--compile` extras through any filesystem-style
|
||||
@@ -101,6 +82,365 @@ function parseExtensionSource(source: string, importerPath: string): ParseResult
|
||||
}
|
||||
}
|
||||
|
||||
const REQUIRE_BINDING = 1 << 0;
|
||||
const OBJECT_BINDING = 1 << 1;
|
||||
const EXPORTS_BINDING = 1 << 2;
|
||||
const MODULE_BINDING = 1 << 3;
|
||||
|
||||
interface StructuralAstNode {
|
||||
readonly type: string;
|
||||
readonly [key: string]: unknown;
|
||||
}
|
||||
|
||||
interface BindingScope {
|
||||
readonly parent: BindingScope | null;
|
||||
readonly ownsVarBindings: boolean;
|
||||
bindings: number;
|
||||
}
|
||||
|
||||
interface ScopedAstNode {
|
||||
readonly node: StructuralAstNode;
|
||||
readonly scope: BindingScope;
|
||||
readonly order: number;
|
||||
}
|
||||
|
||||
interface ScopeWalkItem {
|
||||
readonly node: StructuralAstNode;
|
||||
readonly scope: BindingScope | null;
|
||||
readonly parent: StructuralAstNode | null;
|
||||
readonly parentKey: string | null;
|
||||
}
|
||||
|
||||
function asAstNode(value: unknown): StructuralAstNode | null {
|
||||
if (!value || typeof value !== "object" || Array.isArray(value)) return null;
|
||||
const candidate = value as { readonly type?: unknown };
|
||||
return typeof candidate.type === "string" ? (candidate as StructuralAstNode) : null;
|
||||
}
|
||||
|
||||
function nodeArray(node: StructuralAstNode, key: string): readonly unknown[] | null {
|
||||
const value = node[key];
|
||||
return Array.isArray(value) ? value : null;
|
||||
}
|
||||
|
||||
function nodeArgument(node: StructuralAstNode | null, index: number): StructuralAstNode | null {
|
||||
if (!node) return null;
|
||||
const values = nodeArray(node, "arguments");
|
||||
return values ? asAstNode(values[index]) : null;
|
||||
}
|
||||
|
||||
function isIdentifier(node: StructuralAstNode | null, name: string): boolean {
|
||||
return node?.type === "Identifier" && node.name === name;
|
||||
}
|
||||
|
||||
function trackedBinding(name: unknown): number {
|
||||
switch (name) {
|
||||
case "require":
|
||||
return REQUIRE_BINDING;
|
||||
case "Object":
|
||||
return OBJECT_BINDING;
|
||||
case "exports":
|
||||
return EXPORTS_BINDING;
|
||||
case "module":
|
||||
return MODULE_BINDING;
|
||||
default:
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
function addPatternBindings(scope: BindingScope, pattern: unknown): void {
|
||||
const stack: unknown[] = [pattern];
|
||||
while (stack.length > 0) {
|
||||
const node = asAstNode(stack.pop());
|
||||
if (!node) continue;
|
||||
switch (node.type) {
|
||||
case "Identifier":
|
||||
scope.bindings |= trackedBinding(node.name);
|
||||
break;
|
||||
case "AssignmentPattern":
|
||||
stack.push(node.left);
|
||||
break;
|
||||
case "RestElement":
|
||||
stack.push(node.argument);
|
||||
break;
|
||||
case "ArrayPattern": {
|
||||
const elements = nodeArray(node, "elements");
|
||||
if (elements) stack.push(...elements);
|
||||
break;
|
||||
}
|
||||
case "ObjectPattern": {
|
||||
const properties = nodeArray(node, "properties");
|
||||
if (!properties) break;
|
||||
for (const value of properties) {
|
||||
const property = asAstNode(value);
|
||||
if (!property) continue;
|
||||
stack.push(property.type === "RestElement" ? property.argument : property.value);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case "TSParameterProperty":
|
||||
stack.push(node.parameter);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function isFunctionScopeNode(node: StructuralAstNode): boolean {
|
||||
switch (node.type) {
|
||||
case "FunctionDeclaration":
|
||||
case "FunctionExpression":
|
||||
case "ArrowFunctionExpression":
|
||||
case "ObjectMethod":
|
||||
case "ClassMethod":
|
||||
case "ClassPrivateMethod":
|
||||
case "TSDeclareFunction":
|
||||
case "TSDeclareMethod":
|
||||
case "DeclareFunction":
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function isFunctionDeclarationNode(node: StructuralAstNode): boolean {
|
||||
return node.type === "FunctionDeclaration" || node.type === "TSDeclareFunction" || node.type === "DeclareFunction";
|
||||
}
|
||||
|
||||
function isClassScopeNode(node: StructuralAstNode): boolean {
|
||||
return node.type === "ClassDeclaration" || node.type === "ClassExpression";
|
||||
}
|
||||
|
||||
function scopeKind(node: StructuralAstNode, parent: StructuralAstNode | null, parentKey: string | null): 0 | 1 | 2 {
|
||||
if (
|
||||
node.type === "Program" ||
|
||||
isFunctionScopeNode(node) ||
|
||||
node.type === "StaticBlock" ||
|
||||
node.type === "TSModuleBlock"
|
||||
) {
|
||||
return 2;
|
||||
}
|
||||
if (
|
||||
isClassScopeNode(node) ||
|
||||
node.type === "CatchClause" ||
|
||||
node.type === "ForStatement" ||
|
||||
node.type === "ForInStatement" ||
|
||||
node.type === "ForOfStatement" ||
|
||||
node.type === "SwitchStatement"
|
||||
) {
|
||||
return 1;
|
||||
}
|
||||
if (node.type === "BlockStatement" && !(parent && isFunctionScopeNode(parent) && parentKey === "body")) {
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
function nearestVarScope(scope: BindingScope): BindingScope {
|
||||
let current = scope;
|
||||
while (!current.ownsVarBindings && current.parent) current = current.parent;
|
||||
return current;
|
||||
}
|
||||
|
||||
function registerOuterDeclaration(node: StructuralAstNode, scope: BindingScope | null): void {
|
||||
if (!scope) return;
|
||||
if (
|
||||
(isFunctionDeclarationNode(node) && node.type !== "TSDeclareFunction" && node.type !== "DeclareFunction") ||
|
||||
(node.type === "ClassDeclaration" && node.declare !== true)
|
||||
) {
|
||||
addPatternBindings(scope, node.id);
|
||||
}
|
||||
}
|
||||
|
||||
function registerScopeBindings(node: StructuralAstNode, scope: BindingScope): void {
|
||||
if (isFunctionScopeNode(node)) {
|
||||
addPatternBindings(scope, node.id);
|
||||
const parameters = nodeArray(node, "params");
|
||||
if (parameters) {
|
||||
for (const parameter of parameters) addPatternBindings(scope, parameter);
|
||||
}
|
||||
}
|
||||
if (isClassScopeNode(node)) addPatternBindings(scope, node.id);
|
||||
if (node.type === "CatchClause") addPatternBindings(scope, node.param);
|
||||
|
||||
if (node.type === "ImportDeclaration") {
|
||||
const specifiers = nodeArray(node, "specifiers");
|
||||
if (specifiers) {
|
||||
for (const value of specifiers) {
|
||||
const specifier = asAstNode(value);
|
||||
if (specifier) addPatternBindings(scope, specifier.local);
|
||||
}
|
||||
}
|
||||
} else if (node.type === "TSImportEqualsDeclaration") {
|
||||
addPatternBindings(scope, node.id);
|
||||
} else if (node.type === "VariableDeclaration") {
|
||||
const target = node.kind === "var" ? nearestVarScope(scope) : scope;
|
||||
const declarations = nodeArray(node, "declarations");
|
||||
if (declarations) {
|
||||
for (const value of declarations) {
|
||||
const declaration = asAstNode(value);
|
||||
if (declaration) addPatternBindings(target, declaration.id);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function isAstMetadataKey(key: string): boolean {
|
||||
switch (key) {
|
||||
case "loc":
|
||||
case "extra":
|
||||
case "range":
|
||||
case "comments":
|
||||
case "tokens":
|
||||
case "errors":
|
||||
case "leadingComments":
|
||||
case "trailingComments":
|
||||
case "innerComments":
|
||||
case "parent":
|
||||
case "parentPath":
|
||||
case "scope":
|
||||
case "hub":
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function scopeForChild(
|
||||
node: StructuralAstNode,
|
||||
key: string,
|
||||
outerScope: BindingScope | null,
|
||||
nodeScope: BindingScope,
|
||||
): BindingScope {
|
||||
if (node.type === "SwitchStatement" && key === "discriminant") {
|
||||
return outerScope ?? nodeScope;
|
||||
}
|
||||
if (isFunctionScopeNode(node) && (key === "key" || key === "decorators")) {
|
||||
return outerScope ?? nodeScope;
|
||||
}
|
||||
if (isClassScopeNode(node) && key === "decorators") {
|
||||
return outerScope ?? nodeScope;
|
||||
}
|
||||
return nodeScope;
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds only the lexical information needed by legacy extension rewriting.
|
||||
* Scope frames are fully populated before selected nodes are returned, so
|
||||
* hoisted and TDZ bindings behave independently of textual declaration order.
|
||||
*/
|
||||
function collectScopedAstNodes(root: unknown, select: (node: StructuralAstNode) => boolean): ScopedAstNode[] {
|
||||
const rootNode = asAstNode(root);
|
||||
if (!rootNode) return [];
|
||||
|
||||
const selected: ScopedAstNode[] = [];
|
||||
const stack: ScopeWalkItem[] = [{ node: rootNode, scope: null, parent: null, parentKey: null }];
|
||||
const seen = new WeakSet<object>();
|
||||
let order = 0;
|
||||
while (stack.length > 0) {
|
||||
const item = stack.pop();
|
||||
if (!item || seen.has(item.node)) continue;
|
||||
seen.add(item.node);
|
||||
|
||||
registerOuterDeclaration(item.node, item.scope);
|
||||
const kind = scopeKind(item.node, item.parent, item.parentKey);
|
||||
const activeScope: BindingScope | null =
|
||||
kind === 0
|
||||
? item.scope
|
||||
: {
|
||||
parent: item.scope,
|
||||
ownsVarBindings: kind === 2,
|
||||
bindings: 0,
|
||||
};
|
||||
if (activeScope) {
|
||||
registerScopeBindings(item.node, activeScope);
|
||||
if (select(item.node)) selected.push({ node: item.node, scope: activeScope, order });
|
||||
}
|
||||
order++;
|
||||
|
||||
for (const key in item.node) {
|
||||
if (isAstMetadataKey(key)) continue;
|
||||
const childScope = activeScope ? scopeForChild(item.node, key, item.scope, activeScope) : null;
|
||||
const value = item.node[key];
|
||||
if (Array.isArray(value)) {
|
||||
for (const element of value) {
|
||||
const child = asAstNode(element);
|
||||
if (child) stack.push({ node: child, scope: childScope, parent: item.node, parentKey: key });
|
||||
}
|
||||
} else {
|
||||
const child = asAstNode(value);
|
||||
if (child) stack.push({ node: child, scope: childScope, parent: item.node, parentKey: key });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
selected.sort((left, right) => {
|
||||
const leftStart = typeof left.node.start === "number" ? left.node.start : Number.MAX_SAFE_INTEGER;
|
||||
const rightStart = typeof right.node.start === "number" ? right.node.start : Number.MAX_SAFE_INTEGER;
|
||||
return leftStart - rightStart || left.order - right.order;
|
||||
});
|
||||
return selected;
|
||||
}
|
||||
|
||||
function scopeHasBinding(scope: BindingScope, binding: number): boolean {
|
||||
let current: BindingScope | null = scope;
|
||||
while (current) {
|
||||
if ((current.bindings & binding) !== 0) return true;
|
||||
current = current.parent;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function isSpecifierReferenceNode(node: StructuralAstNode): boolean {
|
||||
switch (node.type) {
|
||||
case "ImportDeclaration":
|
||||
case "ExportNamedDeclaration":
|
||||
case "ExportAllDeclaration":
|
||||
case "ImportExpression":
|
||||
case "TSImportEqualsDeclaration":
|
||||
case "CallExpression":
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function isUncomputedMember(node: StructuralAstNode | null, objectName: string, propertyName: string): boolean {
|
||||
if (node?.type !== "MemberExpression" || node.computed === true) return false;
|
||||
return isIdentifier(asAstNode(node.object), objectName) && isIdentifier(asAstNode(node.property), propertyName);
|
||||
}
|
||||
|
||||
function isUnshadowedExportsTarget(node: StructuralAstNode | null, scope: BindingScope): boolean {
|
||||
return (
|
||||
(isIdentifier(node, "exports") && !scopeHasBinding(scope, EXPORTS_BINDING)) ||
|
||||
(isUncomputedMember(node, "module", "exports") && !scopeHasBinding(scope, MODULE_BINDING))
|
||||
);
|
||||
}
|
||||
|
||||
function isGlobalRequireCall(node: StructuralAstNode | null, scope: BindingScope): boolean {
|
||||
return (
|
||||
node?.type === "CallExpression" &&
|
||||
isIdentifier(asAstNode(node.callee), "require") &&
|
||||
!scopeHasBinding(scope, REQUIRE_BINDING)
|
||||
);
|
||||
}
|
||||
|
||||
function staticMemberPropertyName(node: StructuralAstNode): string | null {
|
||||
const property = asAstNode(node.property);
|
||||
if (node.computed !== true && property?.type === "Identifier" && typeof property.name === "string") {
|
||||
return property.name;
|
||||
}
|
||||
if (node.computed === true && property?.type === "StringLiteral" && typeof property.value === "string") {
|
||||
return property.value;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function staticObjectPropertyName(node: StructuralAstNode): string | null {
|
||||
if (node.computed === true) return null;
|
||||
const key = asAstNode(node.key);
|
||||
if (key?.type === "Identifier" && typeof key.name === "string") return key.name;
|
||||
return key?.type === "StringLiteral" && typeof key.value === "string" ? key.value : null;
|
||||
}
|
||||
|
||||
function collectExtensionSpecifierReferences(
|
||||
source: string,
|
||||
importerPath: string,
|
||||
@@ -108,10 +448,9 @@ function collectExtensionSpecifierReferences(
|
||||
): ExtensionSpecifierReference[] {
|
||||
const references: ExtensionSpecifierReference[] = [];
|
||||
const record = (kind: ExtensionSpecifierReference["kind"], literal: unknown): void => {
|
||||
if (!literal || typeof literal !== "object") return;
|
||||
const node = literal as { type?: string; value?: unknown; start?: number | null; end?: number | null };
|
||||
const node = asAstNode(literal);
|
||||
if (
|
||||
node.type === "StringLiteral" &&
|
||||
node?.type === "StringLiteral" &&
|
||||
typeof node.value === "string" &&
|
||||
typeof node.start === "number" &&
|
||||
typeof node.end === "number"
|
||||
@@ -119,9 +458,7 @@ function collectExtensionSpecifierReferences(
|
||||
references.push({ kind, specifier: node.value, start: node.start, end: node.end });
|
||||
}
|
||||
};
|
||||
traverseAst(ast, {
|
||||
enter(nodePath) {
|
||||
const node = nodePath.node;
|
||||
for (const { node, scope } of collectScopedAstNodes(ast, isSpecifierReferenceNode)) {
|
||||
if (
|
||||
node.type === "ImportDeclaration" ||
|
||||
node.type === "ExportNamedDeclaration" ||
|
||||
@@ -130,24 +467,20 @@ function collectExtensionSpecifierReferences(
|
||||
record("import", node.source);
|
||||
} else if (node.type === "ImportExpression") {
|
||||
record("import", node.source);
|
||||
} else if (
|
||||
node.type === "TSImportEqualsDeclaration" &&
|
||||
node.moduleReference.type === "TSExternalModuleReference"
|
||||
) {
|
||||
record("require", node.moduleReference.expression);
|
||||
} else if (node.type === "TSImportEqualsDeclaration") {
|
||||
const moduleReference = asAstNode(node.moduleReference);
|
||||
if (moduleReference?.type === "TSExternalModuleReference") {
|
||||
record("require", moduleReference.expression);
|
||||
}
|
||||
} else if (node.type === "CallExpression") {
|
||||
if (node.callee.type === "Import") {
|
||||
record("import", node.arguments[0]);
|
||||
} else if (
|
||||
node.callee.type === "Identifier" &&
|
||||
node.callee.name === "require" &&
|
||||
!nodePath.scope.hasBinding("require", true)
|
||||
) {
|
||||
record("require", node.arguments[0]);
|
||||
const callee = asAstNode(node.callee);
|
||||
if (callee?.type === "Import") {
|
||||
record("import", nodeArgument(node, 0));
|
||||
} else if (isIdentifier(callee, "require") && !scopeHasBinding(scope, REQUIRE_BINDING)) {
|
||||
record("require", nodeArgument(node, 0));
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
return references;
|
||||
}
|
||||
|
||||
@@ -1776,140 +2109,74 @@ function collectCommonJsNamedExports(source: string, modulePath: string, visited
|
||||
|
||||
const reexportSpecifiers = new Set<string>();
|
||||
const ast = parseExtensionSource(source, modulePath);
|
||||
traverseAst(ast, {
|
||||
enter(nodePath) {
|
||||
const node = nodePath.node;
|
||||
for (const { node, scope } of collectScopedAstNodes(
|
||||
ast,
|
||||
candidate => candidate.type === "CallExpression" || candidate.type === "AssignmentExpression",
|
||||
)) {
|
||||
if (node.type === "CallExpression") {
|
||||
const definePropertyCall =
|
||||
node.callee.type === "MemberExpression" &&
|
||||
!node.callee.computed &&
|
||||
node.callee.object.type === "Identifier" &&
|
||||
node.callee.object.name === "Object" &&
|
||||
node.callee.property.type === "Identifier" &&
|
||||
node.callee.property.name === "defineProperty" &&
|
||||
!nodePath.scope.hasBinding("Object", true);
|
||||
if (definePropertyCall) {
|
||||
const target = node.arguments[0];
|
||||
const property = node.arguments[1];
|
||||
const targetsExports =
|
||||
(target?.type === "Identifier" &&
|
||||
target.name === "exports" &&
|
||||
!nodePath.scope.hasBinding("exports", true)) ||
|
||||
(target?.type === "MemberExpression" &&
|
||||
!target.computed &&
|
||||
target.object.type === "Identifier" &&
|
||||
target.object.name === "module" &&
|
||||
target.property.type === "Identifier" &&
|
||||
target.property.name === "exports" &&
|
||||
!nodePath.scope.hasBinding("module", true));
|
||||
const callee = asAstNode(node.callee);
|
||||
if (isUncomputedMember(callee, "Object", "defineProperty") && !scopeHasBinding(scope, OBJECT_BINDING)) {
|
||||
const target = nodeArgument(node, 0);
|
||||
const property = nodeArgument(node, 1);
|
||||
if (
|
||||
targetsExports &&
|
||||
isUnshadowedExportsTarget(target, scope) &&
|
||||
property?.type === "StringLiteral" &&
|
||||
typeof property.value === "string" &&
|
||||
property.value !== "default" &&
|
||||
COMMONJS_NAMED_EXPORT_IDENTIFIER.test(property.value)
|
||||
) {
|
||||
names.add(property.value);
|
||||
}
|
||||
return;
|
||||
continue;
|
||||
}
|
||||
if (node.callee.type === "Identifier" && node.callee.name === "__exportStar") {
|
||||
const source = node.arguments[0];
|
||||
const target = node.arguments[1];
|
||||
const targetsExports =
|
||||
(target?.type === "Identifier" &&
|
||||
target.name === "exports" &&
|
||||
!nodePath.scope.hasBinding("exports", true)) ||
|
||||
(target?.type === "MemberExpression" &&
|
||||
!target.computed &&
|
||||
target.object.type === "Identifier" &&
|
||||
target.object.name === "module" &&
|
||||
target.property.type === "Identifier" &&
|
||||
target.property.name === "exports" &&
|
||||
!nodePath.scope.hasBinding("module", true));
|
||||
if (
|
||||
targetsExports &&
|
||||
source?.type === "CallExpression" &&
|
||||
source.callee.type === "Identifier" &&
|
||||
source.callee.name === "require" &&
|
||||
!nodePath.scope.hasBinding("require", true)
|
||||
) {
|
||||
const argument = source.arguments[0];
|
||||
if (argument?.type === "StringLiteral") {
|
||||
if (isIdentifier(callee, "__exportStar")) {
|
||||
const sourceCall = nodeArgument(node, 0);
|
||||
const target = nodeArgument(node, 1);
|
||||
if (isUnshadowedExportsTarget(target, scope) && isGlobalRequireCall(sourceCall, scope)) {
|
||||
const argument = nodeArgument(sourceCall, 0);
|
||||
if (argument?.type === "StringLiteral" && typeof argument.value === "string") {
|
||||
reexportSpecifiers.add(argument.value);
|
||||
}
|
||||
}
|
||||
return;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
if (node.type !== "AssignmentExpression" || node.operator !== "=" || node.left.type !== "MemberExpression") {
|
||||
return;
|
||||
}
|
||||
const left = node.left;
|
||||
const propertyName =
|
||||
!left.computed && left.property.type === "Identifier"
|
||||
? left.property.name
|
||||
: left.computed && left.property.type === "StringLiteral"
|
||||
? left.property.value
|
||||
: null;
|
||||
const object = left.object;
|
||||
const assignsExportsProperty =
|
||||
propertyName !== null &&
|
||||
((object.type === "Identifier" &&
|
||||
object.name === "exports" &&
|
||||
!nodePath.scope.hasBinding("exports", true)) ||
|
||||
(object.type === "MemberExpression" &&
|
||||
!object.computed &&
|
||||
object.object.type === "Identifier" &&
|
||||
object.object.name === "module" &&
|
||||
object.property.type === "Identifier" &&
|
||||
object.property.name === "exports" &&
|
||||
!nodePath.scope.hasBinding("module", true)));
|
||||
if (assignsExportsProperty) {
|
||||
if (node.type !== "AssignmentExpression" || node.operator !== "=") continue;
|
||||
const left = asAstNode(node.left);
|
||||
if (left?.type !== "MemberExpression") continue;
|
||||
|
||||
const propertyName = staticMemberPropertyName(left);
|
||||
const object = asAstNode(left.object);
|
||||
if (propertyName !== null && isUnshadowedExportsTarget(object, scope)) {
|
||||
if (propertyName !== "default" && COMMONJS_NAMED_EXPORT_IDENTIFIER.test(propertyName)) {
|
||||
names.add(propertyName);
|
||||
}
|
||||
return;
|
||||
}
|
||||
const assignsModuleExports =
|
||||
!left.computed &&
|
||||
left.object.type === "Identifier" &&
|
||||
left.object.name === "module" &&
|
||||
left.property.type === "Identifier" &&
|
||||
left.property.name === "exports" &&
|
||||
!nodePath.scope.hasBinding("module", true);
|
||||
if (!assignsModuleExports) return;
|
||||
|
||||
const right = node.right;
|
||||
if (right.type === "ObjectExpression") {
|
||||
for (const property of right.properties) {
|
||||
if ((property.type !== "ObjectProperty" && property.type !== "ObjectMethod") || property.computed) {
|
||||
continue;
|
||||
}
|
||||
const name =
|
||||
property.key.type === "Identifier"
|
||||
? property.key.name
|
||||
: property.key.type === "StringLiteral"
|
||||
? property.key.value
|
||||
: null;
|
||||
if (!isUncomputedMember(left, "module", "exports") || scopeHasBinding(scope, MODULE_BINDING)) continue;
|
||||
|
||||
const right = asAstNode(node.right);
|
||||
if (right?.type === "ObjectExpression") {
|
||||
const properties = nodeArray(right, "properties");
|
||||
if (properties) {
|
||||
for (const value of properties) {
|
||||
const property = asAstNode(value);
|
||||
if (!property || (property.type !== "ObjectProperty" && property.type !== "ObjectMethod")) continue;
|
||||
const name = staticObjectPropertyName(property);
|
||||
if (name && name !== "default" && COMMONJS_NAMED_EXPORT_IDENTIFIER.test(name)) {
|
||||
names.add(name);
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (
|
||||
right.type === "CallExpression" &&
|
||||
right.callee.type === "Identifier" &&
|
||||
right.callee.name === "require" &&
|
||||
!nodePath.scope.hasBinding("require", true)
|
||||
) {
|
||||
const argument = right.arguments[0];
|
||||
if (argument?.type === "StringLiteral") {
|
||||
continue;
|
||||
}
|
||||
if (isGlobalRequireCall(right, scope)) {
|
||||
const argument = nodeArgument(right, 0);
|
||||
if (argument?.type === "StringLiteral" && typeof argument.value === "string") {
|
||||
reexportSpecifiers.add(argument.value);
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
const nativeRequire = createRequire(modulePath);
|
||||
for (const specifier of reexportSpecifiers) {
|
||||
try {
|
||||
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
import * as fs from "node:fs";
|
||||
import "@babel/traverse";
|
||||
|
||||
const modules = Object.keys(require.cache)
|
||||
.filter(modulePath => {
|
||||
const normalizedPath = modulePath.replaceAll("\\", "/");
|
||||
return (
|
||||
normalizedPath.includes("/node_modules/@babel/traverse/") ||
|
||||
normalizedPath.includes("/node_modules/@babel/types/")
|
||||
);
|
||||
})
|
||||
.sort();
|
||||
const bytes = modules.reduce((total, modulePath) => total + fs.statSync(modulePath).size, 0);
|
||||
|
||||
process.stdout.write(JSON.stringify({ modules: modules.length, bytes, paths: modules }));
|
||||
@@ -0,0 +1,15 @@
|
||||
import * as fs from "node:fs";
|
||||
import "../../src/extensibility/plugins/legacy-pi-compat";
|
||||
|
||||
const modules = Object.keys(require.cache)
|
||||
.filter(modulePath => {
|
||||
const normalizedPath = modulePath.replaceAll("\\", "/");
|
||||
return (
|
||||
normalizedPath.includes("/node_modules/@babel/traverse/") ||
|
||||
normalizedPath.includes("/node_modules/@babel/types/")
|
||||
);
|
||||
})
|
||||
.sort();
|
||||
const bytes = modules.reduce((total, modulePath) => total + fs.statSync(modulePath).size, 0);
|
||||
|
||||
process.stdout.write(JSON.stringify({ modules: modules.length, bytes, paths: modules }));
|
||||
@@ -0,0 +1,384 @@
|
||||
import { afterAll, beforeAll, describe, expect, test } from "bun:test";
|
||||
import * as fs from "node:fs/promises";
|
||||
import * as os from "node:os";
|
||||
import * as path from "node:path";
|
||||
import * as url from "node:url";
|
||||
import {
|
||||
__rewriteLegacyExtensionSourceForTests,
|
||||
loadLegacyPiModule,
|
||||
} from "@oh-my-pi/pi-coding-agent/extensibility/plugins/legacy-pi-compat";
|
||||
import { removeWithRetries } from "@oh-my-pi/pi-utils";
|
||||
|
||||
interface RewriteCase {
|
||||
name: string;
|
||||
source: string;
|
||||
expected(importTarget: string, requireTarget: string): string;
|
||||
}
|
||||
|
||||
interface CommonJsCase {
|
||||
name: string;
|
||||
source: string;
|
||||
files?: Record<string, string>;
|
||||
expected: Record<string, unknown>;
|
||||
}
|
||||
|
||||
let rewriteRoot: string;
|
||||
let rewriteImporter: string;
|
||||
let importTarget: string;
|
||||
let requireTarget: string;
|
||||
const tempRoots: string[] = [];
|
||||
|
||||
beforeAll(async () => {
|
||||
rewriteRoot = await fs.mkdtemp(path.join(os.tmpdir(), "omp-legacy-ast-rewrite-"));
|
||||
tempRoots.push(rewriteRoot);
|
||||
const dependencyPath = path.join(rewriteRoot, "node_modules", "tracked-dep", "index.js");
|
||||
await fs.mkdir(path.dirname(dependencyPath), { recursive: true });
|
||||
await fs.writeFile(
|
||||
path.join(rewriteRoot, "node_modules", "tracked-dep", "package.json"),
|
||||
JSON.stringify({ name: "tracked-dep", version: "1.0.0", main: "index.js" }),
|
||||
"utf8",
|
||||
);
|
||||
await fs.writeFile(dependencyPath, 'module.exports = { marker: "tracked" };\n', "utf8");
|
||||
rewriteImporter = path.join(rewriteRoot, "extension.ts");
|
||||
importTarget = url.pathToFileURL(dependencyPath).href;
|
||||
requireTarget = dependencyPath.replaceAll("\\", "/");
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
for (const dir of tempRoots) await removeWithRetries(dir);
|
||||
});
|
||||
|
||||
const rewriteCases: RewriteCase[] = [
|
||||
{
|
||||
name: "import, re-export, dynamic import, and TS import-equals sources",
|
||||
source: [
|
||||
'import value from "tracked-dep";',
|
||||
'export { default as named } from "tracked-dep";',
|
||||
'export * from "tracked-dep";',
|
||||
'const lazy = import("tracked-dep");',
|
||||
'import tracked = require("tracked-dep");',
|
||||
].join("\n"),
|
||||
expected: (importPath, requirePath) =>
|
||||
[
|
||||
`import value from ${JSON.stringify(importPath)};`,
|
||||
`export { default as named } from ${JSON.stringify(importPath)};`,
|
||||
`export * from ${JSON.stringify(importPath)};`,
|
||||
`const lazy = import(${JSON.stringify(importPath)});`,
|
||||
`import tracked = require(${JSON.stringify(requirePath)});`,
|
||||
].join("\n"),
|
||||
},
|
||||
{
|
||||
name: "comments, strings, templates, regexes, and member calls are excluded",
|
||||
source: [
|
||||
"const text = 'require(\"tracked-dep\")';",
|
||||
'const template = `import("tracked-dep")`;',
|
||||
'const pattern = /require\\("tracked-dep"\\)/;',
|
||||
'// require("tracked-dep");',
|
||||
'/* import("tracked-dep"); */',
|
||||
'loader.require("tracked-dep");',
|
||||
'require.resolve("tracked-dep");',
|
||||
'(0, require)("tracked-dep");',
|
||||
].join("\n"),
|
||||
expected: () =>
|
||||
[
|
||||
"const text = 'require(\"tracked-dep\")';",
|
||||
'const template = `import("tracked-dep")`;',
|
||||
'const pattern = /require\\("tracked-dep"\\)/;',
|
||||
'// require("tracked-dep");',
|
||||
'/* import("tracked-dep"); */',
|
||||
'loader.require("tracked-dep");',
|
||||
'require.resolve("tracked-dep");',
|
||||
'(0, require)("tracked-dep");',
|
||||
].join("\n"),
|
||||
},
|
||||
{
|
||||
name: "global require is rewritten without disturbing adjacent excluded calls",
|
||||
source: ['const loaded = require("tracked-dep");', 'loader.require("tracked-dep");'].join("\n"),
|
||||
expected: (_importPath, requirePath) =>
|
||||
[`const loaded = require(${JSON.stringify(requirePath)});`, 'loader.require("tracked-dep");'].join("\n"),
|
||||
},
|
||||
{
|
||||
name: "import declarations shadow require across the program",
|
||||
source: ['import require from "node:module";', 'require("tracked-dep");'].join("\n"),
|
||||
expected: () => ['import require from "node:module";', 'require("tracked-dep");'].join("\n"),
|
||||
},
|
||||
{
|
||||
name: "TS import-equals declarations shadow require only in their namespace",
|
||||
source: [
|
||||
'namespace Nested { import require = require("node:module"); require("tracked-dep"); }',
|
||||
'require("tracked-dep");',
|
||||
].join("\n"),
|
||||
expected: (_importPath, requirePath) =>
|
||||
[
|
||||
'namespace Nested { import require = require("node:module"); require("tracked-dep"); }',
|
||||
`require(${JSON.stringify(requirePath)});`,
|
||||
].join("\n"),
|
||||
},
|
||||
{
|
||||
name: "erased ambient TS function, class, and namespace declarations do not shadow require",
|
||||
source: [
|
||||
'declare function require(id: string): unknown; require("tracked-dep");',
|
||||
'declare class require {} require("tracked-dep");',
|
||||
'declare namespace require {} require("tracked-dep");',
|
||||
].join("\n"),
|
||||
expected: (_importPath, requirePath) =>
|
||||
[
|
||||
`declare function require(id: string): unknown; require(${JSON.stringify(requirePath)});`,
|
||||
`declare class require {} require(${JSON.stringify(requirePath)});`,
|
||||
`declare namespace require {} require(${JSON.stringify(requirePath)});`,
|
||||
].join("\n"),
|
||||
},
|
||||
{
|
||||
name: "ambient TS var declarations retain Babel value bindings",
|
||||
source: 'declare var require: unknown; require("tracked-dep");',
|
||||
expected: () => 'declare var require: unknown; require("tracked-dep");',
|
||||
},
|
||||
{
|
||||
name: "ambient TS const declarations retain Babel value bindings",
|
||||
source: 'declare const require: unknown; require("tracked-dep");',
|
||||
expected: () => 'declare const require: unknown; require("tracked-dep");',
|
||||
},
|
||||
{
|
||||
name: "non-ambient TS enum and namespace names do not create Babel value bindings",
|
||||
source: [
|
||||
'function enumOuter() { { enum require { A } require("tracked-dep"); } require("tracked-dep"); }',
|
||||
'function namespaceOuter() { namespace require {} require("tracked-dep"); }',
|
||||
'namespace Outer { namespace require {} require("tracked-dep"); }',
|
||||
'require("tracked-dep");',
|
||||
].join("\n"),
|
||||
expected: (_importPath, requirePath) =>
|
||||
[
|
||||
`function enumOuter() { { enum require { A } require(${JSON.stringify(requirePath)}); } require(${JSON.stringify(requirePath)}); }`,
|
||||
`function namespaceOuter() { namespace require {} require(${JSON.stringify(requirePath)}); }`,
|
||||
`namespace Outer { namespace require {} require(${JSON.stringify(requirePath)}); }`,
|
||||
`require(${JSON.stringify(requirePath)});`,
|
||||
].join("\n"),
|
||||
},
|
||||
{
|
||||
name: "function declaration and expression names shadow require before textual declaration",
|
||||
source: [
|
||||
'function declarationScope() { require("tracked-dep"); function require() {} }',
|
||||
'const named = function require() { require("tracked-dep"); };',
|
||||
'require("tracked-dep");',
|
||||
].join("\n"),
|
||||
expected: (_importPath, requirePath) =>
|
||||
[
|
||||
'function declarationScope() { require("tracked-dep"); function require() {} }',
|
||||
'const named = function require() { require("tracked-dep"); };',
|
||||
`require(${JSON.stringify(requirePath)});`,
|
||||
].join("\n"),
|
||||
},
|
||||
{
|
||||
name: "default, rest, and destructured parameters shadow require",
|
||||
source: [
|
||||
'function defaulted(require = () => {}) { require("tracked-dep"); }',
|
||||
'function rested(...require) { require("tracked-dep"); }',
|
||||
'function destructured({ loader: require = () => {} }, [other, ...tail]) { require("tracked-dep"); }',
|
||||
].join("\n"),
|
||||
expected: () =>
|
||||
[
|
||||
'function defaulted(require = () => {}) { require("tracked-dep"); }',
|
||||
'function rested(...require) { require("tracked-dep"); }',
|
||||
'function destructured({ loader: require = () => {} }, [other, ...tail]) { require("tracked-dep"); }',
|
||||
].join("\n"),
|
||||
},
|
||||
{
|
||||
name: "var, let, const, class, and block function declarations shadow irrespective of order",
|
||||
source: [
|
||||
'function varScope() { require("tracked-dep"); var require; }',
|
||||
'function letScope() { { require("tracked-dep"); let require; } }',
|
||||
'function constScope() { { require("tracked-dep"); const require = () => {}; } }',
|
||||
'function classScope() { { require("tracked-dep"); class require {} } }',
|
||||
'function declarationScope() { { require("tracked-dep"); function require() {} } }',
|
||||
'require("tracked-dep");',
|
||||
].join("\n"),
|
||||
expected: (_importPath, requirePath) =>
|
||||
[
|
||||
'function varScope() { require("tracked-dep"); var require; }',
|
||||
'function letScope() { { require("tracked-dep"); let require; } }',
|
||||
'function constScope() { { require("tracked-dep"); const require = () => {}; } }',
|
||||
'function classScope() { { require("tracked-dep"); class require {} } }',
|
||||
'function declarationScope() { { require("tracked-dep"); function require() {} } }',
|
||||
`require(${JSON.stringify(requirePath)});`,
|
||||
].join("\n"),
|
||||
},
|
||||
{
|
||||
name: "for, switch, class, and static-block scopes do not leak",
|
||||
source: [
|
||||
'for (let require = () => false; false; ) { require("tracked-dep"); }',
|
||||
'switch (0) { case 0: require("tracked-dep"); break; case 1: const require = () => {}; }',
|
||||
'switch (require("tracked-dep")) { case 0: let require; require("tracked-dep"); }',
|
||||
'const Holder = class require { method() { require("tracked-dep"); } };',
|
||||
'const Heritage = class require extends require("tracked-dep") {};',
|
||||
'class StaticHolder { static { require("tracked-dep"); let require; } }',
|
||||
'require("tracked-dep");',
|
||||
].join("\n"),
|
||||
expected: (_importPath, requirePath) =>
|
||||
[
|
||||
'for (let require = () => false; false; ) { require("tracked-dep"); }',
|
||||
'switch (0) { case 0: require("tracked-dep"); break; case 1: const require = () => {}; }',
|
||||
`switch (require(${JSON.stringify(requirePath)})) { case 0: let require; require("tracked-dep"); }`,
|
||||
'const Holder = class require { method() { require("tracked-dep"); } };',
|
||||
'const Heritage = class require extends require("tracked-dep") {};',
|
||||
'class StaticHolder { static { require("tracked-dep"); let require; } }',
|
||||
`require(${JSON.stringify(requirePath)});`,
|
||||
].join("\n"),
|
||||
},
|
||||
{
|
||||
name: "catch, nested function, and nested block bindings stay lexical",
|
||||
source: [
|
||||
'try {} catch (require) { require("tracked-dep"); }',
|
||||
'function outer(require) { function inner() { require("tracked-dep"); } }',
|
||||
'{ const require = () => {}; { require("tracked-dep"); } }',
|
||||
'require("tracked-dep");',
|
||||
].join("\n"),
|
||||
expected: (_importPath, requirePath) =>
|
||||
[
|
||||
'try {} catch (require) { require("tracked-dep"); }',
|
||||
'function outer(require) { function inner() { require("tracked-dep"); } }',
|
||||
'{ const require = () => {}; { require("tracked-dep"); } }',
|
||||
`require(${JSON.stringify(requirePath)});`,
|
||||
].join("\n"),
|
||||
},
|
||||
];
|
||||
|
||||
async function loadCommonJsCase(testCase: CommonJsCase): Promise<{ keys: string[]; named: Record<string, unknown> }> {
|
||||
const dir = await fs.mkdtemp(path.join(os.tmpdir(), "omp-legacy-ast-exports-"));
|
||||
tempRoots.push(dir);
|
||||
const files: Record<string, string> = {
|
||||
"package.json": JSON.stringify({ name: `cjs-${testCase.name}`, version: "1.0.0", type: "module" }),
|
||||
"subject.cjs": testCase.source,
|
||||
"index.ts": [
|
||||
'import * as subject from "./subject.cjs";',
|
||||
"export const keys = Object.keys(subject).sort();",
|
||||
'export const named = Object.fromEntries(keys.filter(key => key !== "default").map(key => {',
|
||||
" const value = subject[key];",
|
||||
' return [key, typeof value === "function" ? value() : value];',
|
||||
"}));",
|
||||
].join("\n"),
|
||||
...testCase.files,
|
||||
};
|
||||
for (const [relativePath, contents] of Object.entries(files)) {
|
||||
const absolutePath = path.join(dir, relativePath);
|
||||
await fs.mkdir(path.dirname(absolutePath), { recursive: true });
|
||||
await fs.writeFile(absolutePath, contents, "utf8");
|
||||
}
|
||||
return (await loadLegacyPiModule(path.join(dir, "index.ts"))) as {
|
||||
keys: string[];
|
||||
named: Record<string, unknown>;
|
||||
};
|
||||
}
|
||||
|
||||
const commonJsCases: CommonJsCase[] = [
|
||||
{
|
||||
name: "direct-assignments",
|
||||
source: [
|
||||
'exports.alpha = "alpha";',
|
||||
'module.exports.bravo = "bravo";',
|
||||
'module.exports["charlie"] = "charlie";',
|
||||
'exports["invalid-name"] = "excluded";',
|
||||
].join("\n"),
|
||||
expected: { alpha: "alpha", bravo: "bravo", charlie: "charlie" },
|
||||
},
|
||||
{
|
||||
name: "define-property",
|
||||
source: [
|
||||
'Object.defineProperty(exports, "delta", { enumerable: true, value: "delta" });',
|
||||
'Object.defineProperty(module.exports, "echo", { enumerable: true, value: "echo" });',
|
||||
'Object.defineProperty(exports, "default", { enumerable: true, value: "excluded" });',
|
||||
].join("\n"),
|
||||
expected: { delta: "delta", echo: "echo" },
|
||||
},
|
||||
{
|
||||
name: "module-exports-object",
|
||||
source:
|
||||
'module.exports = { foxtrot: "foxtrot", golf() { return "golf"; }, "hotel": "hotel", ["computed"]: "excluded", default: "excluded", "invalid-name": "excluded" };',
|
||||
expected: { foxtrot: "foxtrot", golf: "golf", hotel: "hotel" },
|
||||
},
|
||||
{
|
||||
name: "module-exports-require",
|
||||
source: 'module.exports = require("./leaf.cjs");',
|
||||
files: { "leaf.cjs": 'exports.india = "india";\nmodule.exports.juliet = "juliet";\n' },
|
||||
expected: { india: "india", juliet: "juliet" },
|
||||
},
|
||||
{
|
||||
name: "export-star",
|
||||
source: [
|
||||
"const __exportStar = (source, target) => {",
|
||||
' for (const key of Object.keys(source)) if (key !== "default") target[key] = source[key];',
|
||||
"};",
|
||||
'__exportStar(require("./leaf.cjs"), exports);',
|
||||
].join("\n"),
|
||||
files: { "leaf.cjs": 'exports.kilo = "kilo";\n' },
|
||||
expected: { kilo: "kilo" },
|
||||
},
|
||||
{
|
||||
name: "parameter-shadows",
|
||||
source: [
|
||||
'exports.good = "good";',
|
||||
'function objectShadow(Object) { Object.defineProperty(exports, "badObject", {}); }',
|
||||
'function exportsShadow(exports) { exports.badExports = true; Object.defineProperty(exports, "badDefined", {}); }',
|
||||
"function moduleShadow(module) { module.exports.badModule = true; module.exports = { badObject: true }; }",
|
||||
'function requireShadow(require) { module.exports = require("./leaf.cjs"); __exportStar(require("./leaf.cjs"), exports); }',
|
||||
].join("\n"),
|
||||
files: { "leaf.cjs": 'exports.badLeaf = "excluded";\n' },
|
||||
expected: { good: "good" },
|
||||
},
|
||||
{
|
||||
name: "hoisted-program-exports-shadow",
|
||||
source: ['exports.badExports = "excluded";', "var exports;"].join("\n"),
|
||||
expected: {},
|
||||
},
|
||||
{
|
||||
name: "hoisted-program-module-shadow",
|
||||
source: ['module.exports.badModule = "excluded";', "var module;", 'exports.good = "good";'].join("\n"),
|
||||
expected: { good: "good" },
|
||||
},
|
||||
{
|
||||
name: "hoisted-program-require-shadow",
|
||||
source: ['module.exports = require("./leaf.cjs");', "var require;"].join("\n"),
|
||||
files: { "leaf.cjs": 'exports.badLeaf = "excluded";\n' },
|
||||
expected: {},
|
||||
},
|
||||
{
|
||||
name: "program-Object-shadow",
|
||||
source: [
|
||||
"const Object = globalThis.Object;",
|
||||
'Object.defineProperty(exports, "badObject", { enumerable: true, value: "excluded" });',
|
||||
'exports.good = "good";',
|
||||
].join("\n"),
|
||||
expected: { good: "good" },
|
||||
},
|
||||
{
|
||||
name: "block-catch-for-switch-class-static-shadows",
|
||||
source: [
|
||||
'exports.good = "good";',
|
||||
"function decoys() {",
|
||||
" { const exports = {}; exports.badBlock = true; }",
|
||||
" try {} catch (module) { module.exports.badCatch = true; }",
|
||||
" for (const module of []) { module.exports.badFor = true; }",
|
||||
' switch (0) { case 0: { const Object = globalThis.Object; Object.defineProperty(exports, "badSwitch", {}); } }',
|
||||
' const Named = class Object { method() { Object.defineProperty(exports, "badClass", {}); } };',
|
||||
' class Static { static { const Object = globalThis.Object; Object.defineProperty(exports, "badStatic", {}); } }',
|
||||
"}",
|
||||
].join("\n"),
|
||||
expected: { good: "good" },
|
||||
},
|
||||
];
|
||||
|
||||
describe("legacy Pi Babel AST behavior baseline", () => {
|
||||
test("rewrites exact source bytes with Babel binding semantics", async () => {
|
||||
for (const testCase of rewriteCases) {
|
||||
const actual = await __rewriteLegacyExtensionSourceForTests(testCase.source, rewriteImporter);
|
||||
expect(actual, testCase.name).toBe(testCase.expected(importTarget, requireTarget));
|
||||
}
|
||||
});
|
||||
|
||||
test("discovers exact CommonJS named exports with Babel binding semantics", async () => {
|
||||
for (const testCase of commonJsCases) {
|
||||
const actual = await loadCommonJsCase(testCase);
|
||||
expect(actual.keys, testCase.name).toEqual(["default", ...Object.keys(testCase.expected)].sort());
|
||||
expect(actual.named, testCase.name).toEqual(testCase.expected);
|
||||
}
|
||||
}, 30_000);
|
||||
});
|
||||
@@ -0,0 +1,42 @@
|
||||
import { describe, expect, test } from "bun:test";
|
||||
import * as path from "node:path";
|
||||
|
||||
interface CacheProbeResult {
|
||||
modules: number;
|
||||
bytes: number;
|
||||
paths: string[];
|
||||
}
|
||||
|
||||
const probePath = path.resolve(import.meta.dir, "fixtures", "legacy-pi-babel-cache-probe.ts");
|
||||
const positiveControlPath = path.resolve(import.meta.dir, "fixtures", "legacy-pi-babel-cache-positive-control.ts");
|
||||
|
||||
async function runProbe(childPath: string): Promise<CacheProbeResult> {
|
||||
const proc = Bun.spawn([process.execPath, childPath], {
|
||||
cwd: path.resolve(import.meta.dir, "../.."),
|
||||
stderr: "pipe",
|
||||
stdout: "pipe",
|
||||
});
|
||||
const [stdout, stderr, exitCode] = await Promise.all([
|
||||
new Response(proc.stdout).text(),
|
||||
new Response(proc.stderr).text(),
|
||||
proc.exited,
|
||||
]);
|
||||
expect(exitCode, stderr).toBe(0);
|
||||
return JSON.parse(stdout) as CacheProbeResult;
|
||||
}
|
||||
|
||||
describe("legacy Pi Babel startup closure", () => {
|
||||
test("detector observes a substantial traverse/types positive-control closure", async () => {
|
||||
const result = await runProbe(positiveControlPath);
|
||||
expect(result.modules, JSON.stringify(result)).toBeGreaterThanOrEqual(100);
|
||||
expect(result.bytes, JSON.stringify(result)).toBeGreaterThanOrEqual(600_000);
|
||||
}, 30_000);
|
||||
|
||||
test("static legacy compatibility import evaluates no traverse/types CommonJS modules", async () => {
|
||||
const result = await runProbe(probePath);
|
||||
expect({ modules: result.modules, bytes: result.bytes }, JSON.stringify(result)).toEqual({
|
||||
modules: 0,
|
||||
bytes: 0,
|
||||
});
|
||||
}, 30_000);
|
||||
});
|
||||
Reference in New Issue
Block a user