mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-10-02 00:25:34 +08:00
fix(compiler): admit safe CommonJS export prologues (#482)
- Keep CommonJS packages eligible for static compilation when export prologues precede require calls. - Preserve initialization order and early-access safeguards with frontend and native regression coverage. Fixes #37 Co-authored-by: vini <91087061+vinikjkkj@users.noreply.github.com>
This commit is contained in:
@@ -11,7 +11,7 @@ import { lowerForAwaitBuiltin } from "./lower-async-iteration.js";
|
||||
import { BOOL, BYTES_U8, CAUGHT, DYN, F64, IrExpr, IrGlobal, IrJsOp, IrLocal, IrStmt, IrType, JSVAL, STRING, SrcLoc, UNDEFINED_T, VOID, arrayOf, isUnitType, shapeHasAccessorSlots, typeEquals } from "../../ir/ir.js";
|
||||
import { PoisonError, boundIdentifiersOf, dynFallbackType, dynUndefinedExpr, importCallHandleType, neverTaintedJsType, staticImportNamespaceType, stmtUsesIsland, uncheckedOverloadHandleCall } from "./lowerer.js";
|
||||
import { enforceLibBoundary } from "./lib-boundary.js";
|
||||
import { cjsExportAssignmentOf, cjsExportDiscardReason, cjsExportTargetLiteral, isCjsJsFile, isJsSourceFile, locOf, requireSpecOf } from "../program.js";
|
||||
import { cjsExportAssignmentOf, cjsExportDiscardReason, cjsExportTargetLiteral, isCjsJsFile, isEsModuleStamp, isJsSourceFile, locOf, requireSpecOf } from "../program.js";
|
||||
import { COMPOUND_ASSIGN_OPS, CompoundOp, STR_METHODS, UNSUPPORTED_STMT, isStdlibMember, sideEffectFreeOptionValue, stdlibGlobalAliasDecl, stdlibGlobalAliasNameOf, stdlibGlobalNameOf } from "./surfaces.js";
|
||||
import { isProvenanceSourceFile } from "../provenance-registry.js";
|
||||
import { ambientUndefVarRootOf, lowerImportEquals, nsUndefRead, nsWritableTarget, trapDeclRootOf } from "./lower-namespaces.js";
|
||||
@@ -4476,41 +4476,6 @@ function lowerBranchSwitch(
|
||||
);
|
||||
}
|
||||
|
||||
/** The exact `Object.defineProperty(exports|module.exports, "__esModule",
|
||||
* { value: true })` interop stamp (see the no-op lowering above). */
|
||||
function isEsModuleStamp(expr: ts.Expression): boolean {
|
||||
if (!ts.isCallExpression(expr) || expr.questionDotToken !== undefined) return false;
|
||||
const callee = expr.expression;
|
||||
if (
|
||||
!ts.isPropertyAccessExpression(callee) ||
|
||||
!ts.isIdentifier(callee.expression) ||
|
||||
callee.expression.text !== "Object" ||
|
||||
!ts.isIdentifier(callee.name) ||
|
||||
callee.name.text !== "defineProperty"
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
if (expr.arguments.length !== 3) return false;
|
||||
const [recv, nameArg, desc] = expr.arguments as unknown as [ts.Expression, ts.Expression, ts.Expression];
|
||||
const isExports =
|
||||
(ts.isIdentifier(recv) && recv.text === "exports") ||
|
||||
(ts.isPropertyAccessExpression(recv) &&
|
||||
ts.isIdentifier(recv.expression) &&
|
||||
recv.expression.text === "module" &&
|
||||
ts.isIdentifier(recv.name) &&
|
||||
recv.name.text === "exports");
|
||||
if (!isExports) return false;
|
||||
if (!ts.isStringLiteral(nameArg) || nameArg.text !== "__esModule") return false;
|
||||
if (!ts.isObjectLiteralExpression(desc) || desc.properties.length !== 1) return false;
|
||||
const p = desc.properties[0]!;
|
||||
return (
|
||||
ts.isPropertyAssignment(p) &&
|
||||
ts.isIdentifier(p.name) &&
|
||||
p.name.text === "value" &&
|
||||
p.initializer.kind === ts.SyntaxKind.TrueKeyword
|
||||
);
|
||||
}
|
||||
|
||||
/** A top-level CommonJS export statement (see cjsExportAssignmentOf):
|
||||
* `exports.f = <expr>` and each expression-valued property of
|
||||
* `module.exports = { ... }` assign their pre-registered export globals
|
||||
|
||||
@@ -0,0 +1,124 @@
|
||||
import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { expect, test } from "vitest";
|
||||
import { checkPreflight, loadProgram } from "./program.js";
|
||||
|
||||
function requireOrderDiagnostics(source: string, dependency = "exports.value = 'ready';\n") {
|
||||
const dir = mkdtempSync(join(tmpdir(), "scriptc-require-order-"));
|
||||
const entry = join(dir, "main.cjs");
|
||||
writeFileSync(entry, source);
|
||||
writeFileSync(join(dir, "dep.cjs"), dependency);
|
||||
const load = loadProgram(entry);
|
||||
try {
|
||||
return checkPreflight(load);
|
||||
} finally {
|
||||
load.dispose();
|
||||
rmSync(dir, { recursive: true, force: true });
|
||||
}
|
||||
}
|
||||
|
||||
test.for([
|
||||
"exports.read = read;",
|
||||
"module.exports.renamed = read;",
|
||||
"module.exports = read;",
|
||||
])("a hoisted function export before require is safe: %s", (publish) => {
|
||||
expect(requireOrderDiagnostics(`
|
||||
'use strict';
|
||||
Object.defineProperty(exports, '__esModule', { value: true });
|
||||
${publish}
|
||||
const dep = require('./dep.cjs');
|
||||
function read() { return helper(); }
|
||||
function helper() { return dep.value; }
|
||||
`)).toEqual([]);
|
||||
});
|
||||
|
||||
test.for([
|
||||
"exports.a = exports.b = void 0;",
|
||||
"exports.a = undefined; exports.b = 'literal'; exports.c = 1;",
|
||||
"exports.first = exports.second = read;",
|
||||
])("literal and chained export prologues do not invoke functions: %s", (prefix) => {
|
||||
expect(requireOrderDiagnostics(`
|
||||
Object.defineProperty(exports, '__esModule', { value: true });
|
||||
${prefix}
|
||||
exports.read = read;
|
||||
const dep = require('./dep.cjs');
|
||||
function read() { return dep.value; }
|
||||
`)).toEqual([]);
|
||||
});
|
||||
|
||||
test.for([
|
||||
["direct call", "read();"],
|
||||
["transitive call", "helper(); function helper() { return read(); }"],
|
||||
["callback escape", "[1].map(read);"],
|
||||
["export call", "exports.read();"],
|
||||
["export alias call", "const alias = exports; alias.read();"],
|
||||
["computed export name", "exports[read()] = read;"],
|
||||
["export initializer call", "exports.value = read();"],
|
||||
["class static initializer", "class Early { static value = read(); }"],
|
||||
["extra descriptor effect", "Object.defineProperty(exports, '__esModule', { value: true, enumerable: read() });"],
|
||||
["void initializer call", "exports.value = void read();"],
|
||||
["prototype mutation", "exports.__proto__ = read;"],
|
||||
["read-only marker", "Object.defineProperty(exports, '__esModule', { value: true }); exports.__esModule = read;"],
|
||||
["replacement export property", "module.exports = read; module.exports.name = read;"],
|
||||
["non-export assignment chain", "const box = {}; exports.alias = box.read = read;"],
|
||||
])("require still refuses an early read through %s", ([, early]) => {
|
||||
const diagnostics = requireOrderDiagnostics(`
|
||||
exports.read = read;
|
||||
${early}
|
||||
const dep = require('./dep.cjs');
|
||||
function read() { return dep.value; }
|
||||
`);
|
||||
expect(diagnostics.some((diag) => diag.code === "SC1013" && diag.message.includes("binding 'dep'")))
|
||||
.toBe(true);
|
||||
});
|
||||
|
||||
test("publishing a require binding itself still reads it before initialization", () => {
|
||||
const diagnostics = requireOrderDiagnostics(`
|
||||
exports.read = read;
|
||||
const { read } = require('./dep.cjs');
|
||||
`);
|
||||
expect(diagnostics.some((diag) => diag.code === "SC1013" && diag.message.includes("binding 'read'")))
|
||||
.toBe(true);
|
||||
});
|
||||
|
||||
test.for([
|
||||
"var Object = null; Object.defineProperty(exports, '__esModule', { value: true }); exports.read = read;",
|
||||
"var exports = null; exports.read = read;",
|
||||
"var module = null; module.exports = read;",
|
||||
])("source bindings cannot masquerade as a CommonJS prologue: %s", (prefix) => {
|
||||
const diagnostics = requireOrderDiagnostics(`
|
||||
${prefix}
|
||||
const dep = require('./dep.cjs');
|
||||
function read() { return dep.value; }
|
||||
`);
|
||||
expect(diagnostics.some((diag) => diag.code === "SC1013" && diag.message.includes("binding 'dep'")))
|
||||
.toBe(true);
|
||||
});
|
||||
|
||||
test("an earlier declarator can call an export before the require initializes", () => {
|
||||
const diagnostics = requireOrderDiagnostics(`
|
||||
exports.read = read;
|
||||
const before = read(), dep = require('./dep.cjs');
|
||||
function read() { return dep.value; }
|
||||
`);
|
||||
expect(diagnostics.some((diag) => diag.code === "SC1013" && diag.message.includes("binding 'dep'")))
|
||||
.toBe(true);
|
||||
});
|
||||
|
||||
test("a later declarator can call an export after the require initializes", () => {
|
||||
expect(requireOrderDiagnostics(`
|
||||
exports.read = read;
|
||||
const dep = require('./dep.cjs'), after = read();
|
||||
function read() { return dep.value; }
|
||||
`)).toEqual([]);
|
||||
});
|
||||
|
||||
test("function publication does not admit CommonJS cycles", () => {
|
||||
const diagnostics = requireOrderDiagnostics(`
|
||||
exports.read = read;
|
||||
const dep = require('./dep.cjs');
|
||||
function read() { return dep.value; }
|
||||
`, "const main = require('./main.cjs'); exports.value = main.read();\n");
|
||||
expect(diagnostics.some((diag) => diag.code === "SC1016")).toBe(true);
|
||||
});
|
||||
@@ -835,25 +835,124 @@ function requiresOf7(
|
||||
return out;
|
||||
}
|
||||
|
||||
/** The exact `Object.defineProperty(exports|module.exports, "__esModule",
|
||||
* { value: true })` interop stamp (shared by preflight and the no-op lowering). */
|
||||
export function isEsModuleStamp(expr: ts.Expression): boolean {
|
||||
if (!ts.isCallExpression(expr) || expr.questionDotToken !== undefined) return false;
|
||||
const callee = expr.expression;
|
||||
if (
|
||||
!ts.isPropertyAccessExpression(callee) ||
|
||||
!ts.isIdentifier(callee.expression) ||
|
||||
callee.expression.text !== "Object" ||
|
||||
!ts.isIdentifier(callee.name) ||
|
||||
callee.name.text !== "defineProperty"
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
if (expr.arguments.length !== 3) return false;
|
||||
const [recv, nameArg, desc] = expr.arguments as unknown as [ts.Expression, ts.Expression, ts.Expression];
|
||||
const isExports =
|
||||
(ts.isIdentifier(recv) && recv.text === "exports") ||
|
||||
(ts.isPropertyAccessExpression(recv) &&
|
||||
ts.isIdentifier(recv.expression) &&
|
||||
recv.expression.text === "module" &&
|
||||
ts.isIdentifier(recv.name) &&
|
||||
recv.name.text === "exports");
|
||||
if (!isExports) return false;
|
||||
if (!ts.isStringLiteral(nameArg) || nameArg.text !== "__esModule") return false;
|
||||
if (!ts.isObjectLiteralExpression(desc) || desc.properties.length !== 1) return false;
|
||||
const p = desc.properties[0]!;
|
||||
return (
|
||||
ts.isPropertyAssignment(p) &&
|
||||
ts.isIdentifier(p.name) &&
|
||||
p.name.text === "value" &&
|
||||
p.initializer.kind === ts.SyntaxKind.TrueKeyword
|
||||
);
|
||||
}
|
||||
|
||||
/** Plain CommonJS prologue writes only publish values; in particular,
|
||||
* publishing a hoisted function does not invoke its body. Later runnable
|
||||
* statements may call the export or mutate its receiver, and still need
|
||||
* the conservative TDZ scan over the whole prefix. */
|
||||
function pureCjsExport7(program: ts.Program, stmt: ts.Statement): boolean {
|
||||
const cjs = cjsExportAssignmentOf(stmt);
|
||||
if (cjs === null) return false;
|
||||
const sf = stmt.getSourceFile();
|
||||
const plainMember = (left: ts.Expression): boolean => {
|
||||
if (!ts.isPropertyAccessExpression(left) || left.questionDotToken) return false;
|
||||
// A replacement export can have setters or non-writable function
|
||||
// properties. Only the wrapper's original object gets this exception.
|
||||
if (sf.statements.some((s) => cjsExportAssignmentOf(s)?.kind === "table")) return false;
|
||||
if (left.name.text === "__proto__" || left.name.text === "__esModule") return false;
|
||||
let receiver = left.expression;
|
||||
if (isModuleExportsAccess(receiver)) {
|
||||
receiver = receiver.expression;
|
||||
return ts.isIdentifier(receiver) && !sourceBinding7(program, receiver);
|
||||
}
|
||||
return ts.isIdentifier(receiver) && receiver.text === "exports" && !sourceBinding7(program, receiver);
|
||||
};
|
||||
if (cjs.kind === "member") {
|
||||
if (!plainMember(cjs.expr.left)) return false;
|
||||
} else {
|
||||
const receiver = (cjs.expr.left as ts.PropertyAccessExpression).expression as ts.Identifier;
|
||||
if (sourceBinding7(program, receiver)) return false;
|
||||
}
|
||||
let value = cjs.expr.right;
|
||||
// tsc emits chains such as exports.A = exports.B = void 0.
|
||||
while (ts.isBinaryExpression(value) && value.operatorToken.kind === ts.SyntaxKind.EqualsToken) {
|
||||
if (!plainMember(value.left)) return false;
|
||||
value = value.right;
|
||||
}
|
||||
if (ts.isVoidExpression(value) && ts.isNumericLiteral(value.expression)) return true;
|
||||
if (ts.isStringLiteralLike(value) || ts.isNumericLiteral(value) ||
|
||||
value.kind === ts.SyntaxKind.TrueKeyword || value.kind === ts.SyntaxKind.FalseKeyword ||
|
||||
value.kind === ts.SyntaxKind.NullKeyword) return true;
|
||||
if (!ts.isIdentifier(value)) return false;
|
||||
if (value.text === "undefined" && !sourceBinding7(program, value)) return true;
|
||||
const checker = program.getTypeChecker();
|
||||
const symbol = checker.getSymbolAtLocation(value);
|
||||
return symbol !== undefined && checker.declarationsOf(symbol).some(
|
||||
(decl) => ts.isFunctionDeclaration(decl) && decl.parent === stmt.parent,
|
||||
);
|
||||
}
|
||||
|
||||
function sourceBinding7(program: ts.Program, ident: ts.Identifier): boolean {
|
||||
const checker = program.getTypeChecker();
|
||||
const symbol = checker.getSymbolAtLocation(ident);
|
||||
return symbol !== undefined && checker.declarationsOf(symbol).some(
|
||||
(decl) => !decl.getSourceFile().isDeclarationFile && !ts.isSourceFile(decl),
|
||||
);
|
||||
}
|
||||
|
||||
function purePrefixDecl7(decl: ts.VariableDeclaration): boolean {
|
||||
const init = decl.initializer;
|
||||
return init === undefined || requireSpecOf7(init) !== null ||
|
||||
ts.isStringLiteralLike(init) || ts.isNumericLiteral(init) ||
|
||||
init.kind === ts.SyntaxKind.TrueKeyword || init.kind === ts.SyntaxKind.FalseKeyword ||
|
||||
init.kind === ts.SyntaxKind.NullKeyword;
|
||||
}
|
||||
|
||||
/** True for top-level statements that cannot run user code: directives,
|
||||
* empty statements, hoisted declarations, require statements themselves,
|
||||
* and literal-initialized variables. A require preceded ONLY by these can
|
||||
* never have its bindings observed early — nothing above it executes. */
|
||||
function purePrefixStmt7(s: ts.Statement): boolean {
|
||||
* literal-initialized variables, and CommonJS function-export prologues.
|
||||
* A require preceded ONLY by these cannot observe its own bindings early;
|
||||
* require cycles are checked separately by the module graph fences. */
|
||||
function purePrefixStmt7(program: ts.Program, s: ts.Statement): boolean {
|
||||
if (ts.isEmptyStatement(s) || ts.isFunctionDeclaration(s)) return true;
|
||||
if (ts.isExpressionStatement(s) && ts.isStringLiteral(s.expression)) return true; // directive
|
||||
if (isRequireStatement7(s)) return true;
|
||||
if (isCjsJsFile7(s.getSourceFile())) {
|
||||
if (ts.isExpressionStatement(s) && isEsModuleStamp(s.expression)) {
|
||||
const call = s.expression as ts.CallExpression;
|
||||
const object = (call.expression as ts.PropertyAccessExpression).expression as ts.Identifier;
|
||||
let receiver = call.arguments[0]!;
|
||||
if (ts.isPropertyAccessExpression(receiver)) receiver = receiver.expression;
|
||||
if (!sourceBinding7(program, object) && ts.isIdentifier(receiver) && !sourceBinding7(program, receiver)) return true;
|
||||
}
|
||||
if (pureCjsExport7(program, s)) return true;
|
||||
}
|
||||
if (ts.isVariableStatement(s)) {
|
||||
return s.declarationList.declarations.every(
|
||||
(d) =>
|
||||
d.initializer === undefined ||
|
||||
requireSpecOf7(d.initializer) !== null ||
|
||||
ts.isStringLiteralLike(d.initializer) ||
|
||||
ts.isNumericLiteral(d.initializer) ||
|
||||
d.initializer.kind === ts.SyntaxKind.TrueKeyword ||
|
||||
d.initializer.kind === ts.SyntaxKind.FalseKeyword ||
|
||||
d.initializer.kind === ts.SyntaxKind.NullKeyword,
|
||||
);
|
||||
return s.declarationList.declarations.every(purePrefixDecl7);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
@@ -878,6 +977,7 @@ function requireTdzRisk7(
|
||||
sf: ts.SourceFile,
|
||||
k: number,
|
||||
decl: ts.VariableDeclaration,
|
||||
precedingDecls: readonly ts.VariableDeclaration[],
|
||||
): string | null {
|
||||
const checker = program.getTypeChecker();
|
||||
const bound: ts.Identifier[] = [];
|
||||
@@ -928,13 +1028,14 @@ function requireTdzRisk7(
|
||||
// These are the exact roots the TDZ analysis scans eagerly. Their files
|
||||
// are phase-managed already, so warm their deferred identifiers as one
|
||||
// symbol-only batch instead of paying one IPC query per occurrence.
|
||||
checker.prefetchSymbolRoots(
|
||||
stmts.slice(0, k).filter((stmt) => !ts.isFunctionDeclaration(stmt)),
|
||||
);
|
||||
for (let i = 0; i < k && hit === null; i++) {
|
||||
const s = stmts[i]!;
|
||||
if (ts.isFunctionDeclaration(s)) continue;
|
||||
scan(s);
|
||||
const roots = [
|
||||
...stmts.slice(0, k).filter((stmt) => !ts.isFunctionDeclaration(stmt)),
|
||||
...precedingDecls,
|
||||
];
|
||||
checker.prefetchSymbolRoots(roots);
|
||||
for (const root of roots) {
|
||||
if (hit !== null) break;
|
||||
scan(root);
|
||||
}
|
||||
while (hit === null && work.length > 0) {
|
||||
// A scanned reference can make a hoisted declaration's body reachable
|
||||
@@ -2588,7 +2689,7 @@ function preflight7(load: LoadResult): {
|
||||
const stmts = sf.statements;
|
||||
let firstRunnable = -1;
|
||||
stmts.forEach((s, i) => {
|
||||
if (firstRunnable < 0 && !purePrefixStmt7(s)) firstRunnable = i;
|
||||
if (firstRunnable < 0 && !purePrefixStmt7(program, s)) firstRunnable = i;
|
||||
});
|
||||
for (let k = 0; k < stmts.length; k++) {
|
||||
const stmt = stmts[k]!;
|
||||
@@ -2665,9 +2766,17 @@ function preflight7(load: LoadResult): {
|
||||
diags.push(unsupportedDiag("SC1012", loc, "require() of JSON modules"));
|
||||
continue;
|
||||
}
|
||||
// Earlier declarators run too: `const x = read(), dep =
|
||||
// require('./dep')` must not lose the guard merely because both
|
||||
// initializers share one VariableStatement.
|
||||
const precedingDecls = req.decl && ts.isVariableStatement(stmt)
|
||||
? stmt.declarationList.declarations.slice(0, stmt.declarationList.declarations.indexOf(req.decl))
|
||||
: [];
|
||||
const prefixCanRun = (firstRunnable >= 0 && firstRunnable < k) ||
|
||||
precedingDecls.some((decl) => !purePrefixDecl7(decl));
|
||||
const tdzName =
|
||||
firstRunnable >= 0 && firstRunnable < k && req.decl
|
||||
? requireTdzRisk7(program, sf, k, req.decl)
|
||||
prefixCanRun && req.decl
|
||||
? requireTdzRisk7(program, sf, k, req.decl, precedingDecls)
|
||||
: null;
|
||||
if (tdzName !== null) {
|
||||
diags.push(
|
||||
|
||||
@@ -7237,6 +7237,15 @@
|
||||
],
|
||||
"diags": []
|
||||
},
|
||||
"<repo>/tests/corpus/3067-cjs-function-export-require/main.cjs": {
|
||||
"order": [
|
||||
"<repo>/tests/corpus/3067-cjs-function-export-require/dependency.cjs",
|
||||
"<repo>/tests/corpus/3067-cjs-function-export-require/member.cjs",
|
||||
"<repo>/tests/corpus/3067-cjs-function-export-require/single.cjs",
|
||||
"<repo>/tests/corpus/3067-cjs-function-export-require/main.cjs"
|
||||
],
|
||||
"diags": []
|
||||
},
|
||||
"<repo>/tests/corpus/400-fib.ts": {
|
||||
"order": [
|
||||
"<repo>/tests/corpus/400-fib.ts"
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
console.log('dependency: initialized');
|
||||
exports.name = 'world';
|
||||
@@ -0,0 +1,9 @@
|
||||
// Publishing a hoisted function does not invoke it or read its captures.
|
||||
// The dependency still initializes at the require, once across both users.
|
||||
console.log('main: before');
|
||||
const member = require('./member.cjs');
|
||||
console.log(member.describe(), member.renamed());
|
||||
const read = require('./single.cjs');
|
||||
console.log(read());
|
||||
require('./member.cjs');
|
||||
console.log('main: after');
|
||||
@@ -0,0 +1,18 @@
|
||||
'use strict';
|
||||
Object.defineProperty(exports, '__esModule', { value: true });
|
||||
exports.describe = describe;
|
||||
module.exports.renamed = other;
|
||||
const browser = require('./dependency.cjs');
|
||||
console.log('member: initialized');
|
||||
|
||||
function describe() {
|
||||
return helper();
|
||||
}
|
||||
|
||||
function helper() {
|
||||
return 'browser:' + browser.name;
|
||||
}
|
||||
|
||||
function other() {
|
||||
return browser.name.toUpperCase();
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
'use strict';
|
||||
module.exports = read;
|
||||
const { name } = require('./dependency.cjs');
|
||||
console.log('single: initialized');
|
||||
|
||||
function read() {
|
||||
return 'single:' + name;
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
import { describe } from 'early-export';
|
||||
|
||||
console.log(describe());
|
||||
console.log(describe());
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
console.log('browser: initialized');
|
||||
exports.name = 'world';
|
||||
+1
@@ -0,0 +1 @@
|
||||
export declare function describe(): string;
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
'use strict';
|
||||
Object.defineProperty(exports, '__esModule', { value: true });
|
||||
exports.label = exports.version = void 0;
|
||||
exports.describe = describe;
|
||||
const browser = require('./browser.js');
|
||||
exports.label = browser.name;
|
||||
exports.version = '1';
|
||||
console.log('early-export: initialized');
|
||||
|
||||
function describe() {
|
||||
return 'browser:' + browser.name + ':' + exports.label + ':' + exports.version;
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"name": "early-export",
|
||||
"version": "1.0.0",
|
||||
"main": "index.js",
|
||||
"types": "index.d.ts"
|
||||
}
|
||||
@@ -122,6 +122,26 @@ describe(`npm-static pilots${sanitize ? " (sanitized)" : ""}`, () => {
|
||||
expect(nativeRes.exitCode).toBe(nodeRes.exitCode);
|
||||
}, 120_000);
|
||||
|
||||
test.for(["explicit", "auto"] as const)("hoisted CommonJS function exports stay static (%s)", async (mode) => {
|
||||
const entry = join(pilotRoot, "early-export-cli.ts");
|
||||
const npmStatic = mode === "auto" ? "auto" : ["early-export"];
|
||||
const { coverage } = analyze(entry, { npmStatic });
|
||||
expect(coverage.npmStatic).toEqual([{ package: "early-export", status: "static" }]);
|
||||
expect(coverage.preflightFailed).toBe(false);
|
||||
expect(coverage.diagnostics).toHaveLength(0);
|
||||
expect(coverage.runtimeFences ?? []).toHaveLength(0);
|
||||
expect(coverage.stats.statementsFailed).toBe(0);
|
||||
|
||||
const binary = await buildStatic(entry, npmStatic);
|
||||
const [nodeRes, nativeRes] = await Promise.all([
|
||||
runBinary("node", [entry]),
|
||||
runBinary(binary, []),
|
||||
]);
|
||||
expect(nativeRes.stdout).toEqual(nodeRes.stdout);
|
||||
expect(comparableStderr(nativeRes.stderr)).toEqual(nodeRes.stderr);
|
||||
expect(nativeRes.exitCode).toBe(nodeRes.exitCode);
|
||||
}, 120_000);
|
||||
|
||||
test("picocolors compiles fully statically and byte-matches both color branches", async () => {
|
||||
const entry = join(pilotRoot, "colors-cli.ts");
|
||||
const { coverage } = analyze(entry, { npmStatic: ["picocolors"] });
|
||||
|
||||
Reference in New Issue
Block a user