mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-10-29 07:04:09 +08:00
Compile function export objects; name conditional export objects (#1434)
This commit is contained in:
@@ -5882,6 +5882,9 @@ export class LlEmitter {
|
||||
// for stdin. They remain JavaScript objects in boolean contexts.
|
||||
case "procStream":
|
||||
return "true";
|
||||
// A module namespace (a required builtin module object) is an object.
|
||||
case "moduleNs":
|
||||
return "true";
|
||||
case "array":
|
||||
case "record":
|
||||
case "object":
|
||||
|
||||
@@ -30,6 +30,7 @@ import {
|
||||
import {
|
||||
cjsExportMemberWriteGlobalOf,
|
||||
isCjsExportsIdentifier,
|
||||
isCjsModuleExports,
|
||||
isFreshExportsReset,
|
||||
} from "./cjs-exports-object.js";
|
||||
import { isDeadTscHelperDecl, tscInteropBindingOfDecl } from "./cjs-tsc-interop.js";
|
||||
@@ -6627,6 +6628,25 @@ export function lowerExprStatement(lowerer: Lowerer, expr: ts.Expression): IrStm
|
||||
"require() outside the module's top level (move it to the top of the file)",
|
||||
);
|
||||
}
|
||||
// Replacing the export object (or rebinding `exports`) anywhere but a
|
||||
// top-level statement makes the module's exports depend on which path
|
||||
// ran (`cond ? module.exports = a : module.exports = b`, an
|
||||
// environment-dependent branch); the static export table has one shape.
|
||||
if (
|
||||
!topLevelJs &&
|
||||
isJsSourceFile(expr.getSourceFile()) &&
|
||||
ts.isBinaryExpression(expr) &&
|
||||
expr.operatorToken.kind === ts.SyntaxKind.EqualsToken &&
|
||||
(isCjsModuleExports(lowerer, expr.left) || isCjsExportsIdentifier(lowerer, expr.left))
|
||||
) {
|
||||
lowerer.unsupported(
|
||||
"SC1090",
|
||||
expr.left,
|
||||
isCjsModuleExports(lowerer, expr.left)
|
||||
? "replacing module.exports outside a top-level statement (a conditional or environment-dependent export object)"
|
||||
: "rebinding 'exports' outside a top-level statement (a conditional or environment-dependent export object)",
|
||||
);
|
||||
}
|
||||
if (topLevelJs && ts.isExpressionStatement(stmtNode)) {
|
||||
if (isFreshExportsReset(lowerer, stmtNode))
|
||||
return { kind: "block", body: [], loc: locOf(expr) };
|
||||
|
||||
@@ -274,3 +274,21 @@ export const g = import_g.g;
|
||||
degrade: expect.stringContaining("bundled CommonJS modules") as string,
|
||||
});
|
||||
});
|
||||
|
||||
test("a chunk that only re-exports a builtin module becomes that module", () => {
|
||||
const answer = expectSameBehavior(`${keyedHelper}var require_stream = __commonJS({
|
||||
"lib/stream.js"(exports2, module2) {
|
||||
module2.exports = require("stream");
|
||||
}
|
||||
});
|
||||
var require_user = __commonJS({
|
||||
"lib/user.js"(exports2) {
|
||||
var Stream = require_stream();
|
||||
exports2.same = Stream === require("stream");
|
||||
}
|
||||
});
|
||||
console.log(typeof require_stream().PassThrough, require_user().same);
|
||||
`);
|
||||
expect(answer.chunks.map((c) => basename(c.path))).toEqual(["index.js.require_user.cjs"]);
|
||||
expect(answer.text).toContain('typeof require("stream").PassThrough');
|
||||
});
|
||||
|
||||
@@ -23,6 +23,7 @@
|
||||
* beside it). Anything else answers a degrade reason. */
|
||||
import * as ts from "./ts7/syntax.js";
|
||||
import { helperTokens } from "./helper-tokens.js";
|
||||
import { canonicalBuiltinModule } from "./builtin-modules.js";
|
||||
|
||||
export interface BundledChunkSplit {
|
||||
/** The bundle with chunk declarations blanked and calls respelled. */
|
||||
@@ -581,10 +582,11 @@ export function splitBundledChunks(
|
||||
return {
|
||||
degrade: `its bundled CommonJS module factory '${r.text}' is used other than as a direct call`,
|
||||
};
|
||||
const forwarded = forwarderSpec(chunk);
|
||||
callEdits.push({
|
||||
start: call.getStart(sf),
|
||||
end: call.getEnd(),
|
||||
text: `require(${JSON.stringify(chunk.spec)})`,
|
||||
text: `require(${JSON.stringify(forwarded ?? chunk.spec)})`,
|
||||
});
|
||||
}
|
||||
for (const name of chunks.keys()) {
|
||||
@@ -596,6 +598,11 @@ export function splitBundledChunks(
|
||||
const out: BundledChunkSplit["chunks"] = [];
|
||||
const bundleEdits: Edit[] = [...callEdits];
|
||||
for (const chunk of chunks.values()) {
|
||||
if (forwarderSpec(chunk) !== null) {
|
||||
// A chunk that only re-exports a builtin module is that module.
|
||||
bundleEdits.push({ start: chunk.stmt.getStart(sf), end: chunk.stmt.getEnd(), text: "" });
|
||||
continue;
|
||||
}
|
||||
const result = chunkSource(
|
||||
sf,
|
||||
chunk,
|
||||
@@ -656,6 +663,41 @@ export function applyEdits(source: string, from: number, to: number, edits: Edit
|
||||
return text + source.slice(at, to);
|
||||
}
|
||||
|
||||
/** The builtin module a chunk forwards, or null: a body that is exactly
|
||||
* `module.exports = require("<builtin>")` (readable-stream's
|
||||
* internal/streams/stream.js). Requiring the chunk is then requiring the
|
||||
* builtin itself (same object, and loading a builtin runs no code). */
|
||||
function forwarderSpec(chunk: Chunk): string | null {
|
||||
const moduleParam = chunk.params[1];
|
||||
if (moduleParam === undefined) return null;
|
||||
const stmts = chunk.body.statements.nodes.filter(
|
||||
(st) => !(ts.isExpressionStatement(st) && ts.isStringLiteral(st.expression)),
|
||||
);
|
||||
const only = stmts[0];
|
||||
if (stmts.length !== 1 || only === undefined || !ts.isExpressionStatement(only)) return null;
|
||||
const e = unparen(only.expression);
|
||||
if (
|
||||
!ts.isBinaryExpression(e) ||
|
||||
e.operatorToken.kind !== ts.SyntaxKind.EqualsToken ||
|
||||
!ts.isPropertyAccessExpression(e.left) ||
|
||||
!ts.isIdentifier(e.left.expression) ||
|
||||
e.left.expression.text !== moduleParam ||
|
||||
e.left.name.text !== "exports"
|
||||
)
|
||||
return null;
|
||||
const call = unparen(e.right);
|
||||
if (
|
||||
!ts.isCallExpression(call) ||
|
||||
!ts.isIdentifier(call.expression) ||
|
||||
call.expression.text !== "require" ||
|
||||
call.arguments.nodes.length !== 1 ||
|
||||
!ts.isStringLiteral(call.arguments.nodes[0]!)
|
||||
)
|
||||
return null;
|
||||
const spec = (call.arguments.nodes[0] as ts.StringLiteral).text;
|
||||
return canonicalBuiltinModule(spec) !== null ? spec : null;
|
||||
}
|
||||
|
||||
/** How code in a factory body (a chunk or a lazy ES-module initializer)
|
||||
* observes the factory's own frame, or null: its receiver, its arguments
|
||||
* object, `super`, a meta property, or (when `returns`) a top-level
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
import { spawnSync } from "node:child_process";
|
||||
import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { afterAll, expect, test } from "vitest";
|
||||
import { nodeFrontendServices } from "./services-node.js";
|
||||
|
||||
const dirs: string[] = [];
|
||||
afterAll(() => {
|
||||
for (const dir of dirs) rmSync(dir, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
function rewrite(source: string): string | null {
|
||||
return nodeFrontendServices().cjsFunctionExports(source, "/pkg/index.js");
|
||||
}
|
||||
|
||||
function run(file: string) {
|
||||
const result = spawnSync(process.execPath, [file], { encoding: "utf8" });
|
||||
return { status: result.status, stdout: result.stdout, stderr: result.stderr };
|
||||
}
|
||||
|
||||
/** The rewritten module prints what the original prints. */
|
||||
function expectSameBehavior(source: string): string {
|
||||
const result = rewrite(source);
|
||||
expect(typeof result).toBe("string");
|
||||
const dir = mkdtempSync(join(tmpdir(), "scriptc-cjs-function-exports-"));
|
||||
dirs.push(dir);
|
||||
const original = join(dir, "original.cjs");
|
||||
const rewritten = join(dir, "rewritten.cjs");
|
||||
writeFileSync(original, source);
|
||||
writeFileSync(rewritten, result as string);
|
||||
const oracle = run(original);
|
||||
expect(oracle.status).toBe(0);
|
||||
expect(run(rewritten)).toEqual(oracle);
|
||||
return result as string;
|
||||
}
|
||||
|
||||
const report = `
|
||||
console.log(typeof module.exports, module.exports === Version, Object.keys(Version).join(","));
|
||||
`;
|
||||
|
||||
test("members of a function export object read and write the function", () => {
|
||||
const rewritten = expectSameBehavior(`exports = module.exports = Version;
|
||||
exports.parse = parse;
|
||||
module.exports.major = (v) => parse(v)[0];
|
||||
function Version(v) { return { v }; }
|
||||
function parse(v) { return v.split(".").map(Number); }
|
||||
console.log(exports.major("2.1.0"), module.exports.parse("1.2").length);
|
||||
${report}`);
|
||||
expect(rewritten).toContain("Version.parse = parse");
|
||||
expect(rewritten).not.toContain("exports = module.exports");
|
||||
});
|
||||
|
||||
test.each([
|
||||
["a parameter", "function inner(Version) { return exports.parse; }"],
|
||||
["a local", "function inner() { const Version = {}; return exports.parse; }"],
|
||||
["a nested function", "function inner() { function Version() {} return exports.parse; }"],
|
||||
[
|
||||
"a catch binding",
|
||||
"function inner() { try { throw 0; } catch (Version) { return exports.parse; } }",
|
||||
],
|
||||
])("a nested binding of the function's name (%s) keeps the module as it is", (_, inner) => {
|
||||
expect(
|
||||
rewrite(`exports = module.exports = Version;
|
||||
exports.parse = parse;
|
||||
function Version() {}
|
||||
function parse() {}
|
||||
${inner}
|
||||
`),
|
||||
).toBeNull();
|
||||
});
|
||||
|
||||
test("an `exports` shorthand property keeps its key", () => {
|
||||
const rewritten = expectSameBehavior(`exports = module.exports = Version;
|
||||
exports.tag = "v";
|
||||
function Version() {}
|
||||
const holder = { exports };
|
||||
console.log(Object.keys(holder).join(","), holder.exports === Version, holder.exports.tag);
|
||||
${report}`);
|
||||
expect(rewritten).toContain("{ exports: Version }");
|
||||
});
|
||||
@@ -0,0 +1,191 @@
|
||||
/* CommonJS modules whose export object is one of their own functions:
|
||||
*
|
||||
* exports = module.exports = SemVer; // or module.exports = SemVer;
|
||||
* exports.parse = parse; // or module.exports.parse = …
|
||||
* function SemVer(version) { … }
|
||||
*
|
||||
* After that statement `module.exports` (and, after the rebinding form,
|
||||
* `exports`) IS the function, so every member read or write through them
|
||||
* is a member of the function itself. The pass respells those accesses
|
||||
* through the function's name, the expando form both resolvers model
|
||||
* (members on a function declaration), and drops the `exports =`
|
||||
* rebinding the static export protocol has no storage for.
|
||||
*
|
||||
* Exact only while nothing can observe the module before the statement
|
||||
* runs and nothing replaces the export object again: the statement is a
|
||||
* top-level statement preceded only by inert declarations, it is the
|
||||
* file's only write of `module.exports` and `exports`, the function is a
|
||||
* top-level declaration nothing reassigns and the file's only binding of
|
||||
* its name, and `exports`/`module` are the CommonJS globals (never
|
||||
* declared in the file). Other shapes answer
|
||||
* null and keep their existing treatment. */
|
||||
import * as ts from "./ts7/syntax.js";
|
||||
import {
|
||||
applyEdits,
|
||||
countDeclarations,
|
||||
declaredNames,
|
||||
isWritten,
|
||||
references,
|
||||
unparen,
|
||||
type Edit,
|
||||
} from "./npm-static-bundled-chunks.js";
|
||||
|
||||
function isModuleExports(e: ts.Expression): e is ts.PropertyAccessExpression {
|
||||
const x = unparen(e);
|
||||
return (
|
||||
ts.isPropertyAccessExpression(x) &&
|
||||
!x.questionDotToken &&
|
||||
ts.isIdentifier(x.expression) &&
|
||||
x.expression.text === "module" &&
|
||||
x.name.text === "exports"
|
||||
);
|
||||
}
|
||||
|
||||
/** True for a statement whose evaluation runs no code that could observe
|
||||
* the export object: declarations and member writes (`F.prototype.m =
|
||||
* function …`) without calls, and directives. */
|
||||
function inert(stmt: ts.Statement): boolean {
|
||||
if (ts.isFunctionDeclaration(stmt) || ts.isEmptyStatement(stmt)) return true;
|
||||
if (ts.isExpressionStatement(stmt) && ts.isStringLiteral(stmt.expression)) return true;
|
||||
if (ts.isExpressionStatement(stmt)) {
|
||||
const e = unparen(stmt.expression);
|
||||
if (
|
||||
!ts.isBinaryExpression(e) ||
|
||||
e.operatorToken.kind !== ts.SyntaxKind.EqualsToken ||
|
||||
!(ts.isPropertyAccessExpression(e.left) || ts.isElementAccessExpression(e.left))
|
||||
)
|
||||
return false;
|
||||
let root: ts.Expression = e.left;
|
||||
while (ts.isPropertyAccessExpression(root) || ts.isElementAccessExpression(root))
|
||||
root = root.expression;
|
||||
if (!ts.isIdentifier(root) || root.text === "module" || root.text === "exports") return false;
|
||||
} else if (!ts.isVariableStatement(stmt)) return false;
|
||||
let calls = false;
|
||||
ts.walkPreorder(stmt, (n) => {
|
||||
if (calls) return "stop";
|
||||
if (ts.isArrowFunction(n) || ts.isFunctionExpression(n)) return "skip";
|
||||
if (ts.isCallExpression(n) || ts.isNewExpression(n) || ts.isTaggedTemplateExpression(n))
|
||||
calls = true;
|
||||
return undefined;
|
||||
});
|
||||
return !calls;
|
||||
}
|
||||
|
||||
export function normalizeCjsFunctionExports(sf: ts.SourceFile): string | null {
|
||||
const source = sf.text;
|
||||
if (!source.includes("module.exports")) return null;
|
||||
if (!sf.fileName.endsWith(".cjs") && ts.isExternalModule(sf)) return null;
|
||||
const declared = declaredNames(sf);
|
||||
if (declared.has("exports") || declared.has("module")) return null;
|
||||
|
||||
// The whole-export statement: `[exports =] module.exports = W;`.
|
||||
let whole: { stmt: ts.ExpressionStatement; fn: string; rebinds: boolean } | null = null;
|
||||
const stmts = sf.statements.nodes;
|
||||
for (let i = 0; i < stmts.length; i++) {
|
||||
const stmt = stmts[i]!;
|
||||
if (!ts.isExpressionStatement(stmt)) continue;
|
||||
const top = unparen(stmt.expression);
|
||||
if (!ts.isBinaryExpression(top) || top.operatorToken.kind !== ts.SyntaxKind.EqualsToken)
|
||||
continue;
|
||||
let e: ts.BinaryExpression = top;
|
||||
let rebinds = false;
|
||||
if (ts.isIdentifier(top.left) && top.left.text === "exports") {
|
||||
const inner = unparen(top.right);
|
||||
if (!ts.isBinaryExpression(inner) || inner.operatorToken.kind !== ts.SyntaxKind.EqualsToken)
|
||||
continue;
|
||||
rebinds = true;
|
||||
e = inner;
|
||||
}
|
||||
if (!isModuleExports(e.left)) continue;
|
||||
const value = unparen(e.right);
|
||||
if (!ts.isIdentifier(value)) return null;
|
||||
if (whole !== null || !stmts.slice(0, i).every(inert)) return null;
|
||||
whole = { stmt, fn: value.text, rebinds };
|
||||
}
|
||||
if (whole === null) return null;
|
||||
const fnName = whole.fn;
|
||||
const fnDecls = stmts.filter(
|
||||
(st) => ts.isFunctionDeclaration(st) && st.name?.text === fnName && st.body !== undefined,
|
||||
);
|
||||
// The respelled reads name the function from wherever they sit, so no
|
||||
// other binding of its name (a parameter, a local, a nested function)
|
||||
// may exist anywhere in the file.
|
||||
if (fnDecls.length !== 1 || countDeclarations(sf, fnName) !== 1) return null;
|
||||
|
||||
const edits: Edit[] = [];
|
||||
let unsafe = false;
|
||||
const refs = references(sf);
|
||||
for (const r of refs) {
|
||||
if (r.text === fnName && isWritten(r)) unsafe = true;
|
||||
if (r.text === "exports") {
|
||||
if (isWritten(r)) {
|
||||
// Only the whole statement's own rebinding.
|
||||
const assign = r.parent;
|
||||
if (
|
||||
!whole.rebinds ||
|
||||
assign === undefined ||
|
||||
!ts.isBinaryExpression(assign) ||
|
||||
assign.parent !== whole.stmt ||
|
||||
assign.left !== r
|
||||
)
|
||||
unsafe = true;
|
||||
continue;
|
||||
}
|
||||
// Without the rebinding, `exports` keeps the replaced object.
|
||||
if (!whole.rebinds || r.getStart(sf) < whole.stmt.getEnd()) {
|
||||
unsafe = true;
|
||||
continue;
|
||||
}
|
||||
// `{ exports }` keeps its key: the shorthand spells out the value.
|
||||
const shorthand =
|
||||
r.parent !== undefined && ts.isShorthandPropertyAssignment(r.parent) && r.parent.name === r;
|
||||
edits.push({
|
||||
start: r.getStart(sf),
|
||||
end: r.getEnd(),
|
||||
text: shorthand ? `exports: ${fnName}` : fnName,
|
||||
});
|
||||
continue;
|
||||
}
|
||||
if (r.text === "module") {
|
||||
const access = r.parent;
|
||||
if (access === undefined || !isModuleExports(access as ts.Expression)) continue;
|
||||
const outer = access.parent;
|
||||
if (outer !== undefined && ts.isBinaryExpression(outer) && outer.left === access) {
|
||||
if (
|
||||
outer.operatorToken.kind >= ts.SyntaxKind.FirstAssignment &&
|
||||
outer.operatorToken.kind <= ts.SyntaxKind.LastAssignment &&
|
||||
!(
|
||||
(outer.parent === whole.stmt && !whole.rebinds) ||
|
||||
(whole.rebinds && outer.parent?.parent === whole.stmt)
|
||||
)
|
||||
)
|
||||
unsafe = true;
|
||||
continue;
|
||||
}
|
||||
// A member access through module.exports: a member of the function.
|
||||
if (
|
||||
outer !== undefined &&
|
||||
(ts.isPropertyAccessExpression(outer) || ts.isElementAccessExpression(outer)) &&
|
||||
outer.expression === access
|
||||
) {
|
||||
if (access.getStart(sf) < whole.stmt.getEnd() && !insideFunction(access)) unsafe = true;
|
||||
edits.push({ start: access.getStart(sf), end: access.getEnd(), text: fnName });
|
||||
}
|
||||
}
|
||||
}
|
||||
if (unsafe) return null;
|
||||
if (whole.rebinds) {
|
||||
// `exports = module.exports = W` → `module.exports = W`.
|
||||
const e = unparen(whole.stmt.expression) as ts.BinaryExpression;
|
||||
edits.push({ start: e.getStart(sf), end: unparen(e.right).getStart(sf), text: "" });
|
||||
}
|
||||
if (edits.length === 0) return null;
|
||||
return applyEdits(source, 0, source.length, edits);
|
||||
}
|
||||
|
||||
function insideFunction(node: ts.Node): boolean {
|
||||
for (let p = node.parent; p !== undefined && !ts.isSourceFile(p); p = p.parent) {
|
||||
if (ts.isFunctionLike(p)) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
@@ -448,7 +448,11 @@ export function npmStaticFsShadow(services: FrontendServices): NpmStaticFsShadow
|
||||
} else if (esm !== null) {
|
||||
original = esm;
|
||||
}
|
||||
const respelled = split !== null || esm !== null;
|
||||
// A module exporting one of its functions reaches its members
|
||||
// through that function.
|
||||
const fnExports = original === null ? null : services.cjsFunctionExports(original, path);
|
||||
if (fnExports !== null) original = fnExports;
|
||||
const respelled = split !== null || esm !== null || fnExports !== null;
|
||||
if (original !== null) {
|
||||
const namepaths = services.jsDocNamepaths(path, original);
|
||||
const source = namepaths ?? original;
|
||||
|
||||
@@ -2,6 +2,7 @@ import { Ts7SourceParser, type Ts7SourceKind } from "./ts7/source-parser.js";
|
||||
import { Ts7Host, type Ts7ApiFactory, type Ts7HostOptions } from "./ts7/program-host.js";
|
||||
import { lowerEsmInitsWithParser, rewriteBundlerCjsWithParser } from "./npm-static-rewrite-host.js";
|
||||
import { isBundlerCjsCandidate } from "./npm-static-rewrite-syntax.js";
|
||||
import { normalizeCjsFunctionExports } from "./npm-static-cjs-exports-alias.js";
|
||||
import {
|
||||
mayBundleChunks,
|
||||
splitBundledChunks,
|
||||
@@ -91,6 +92,13 @@ export class FrontendServices {
|
||||
if (!source.includes("__esm")) return null;
|
||||
return lowerEsmInitsWithParser(this.sourceParser(), source, path);
|
||||
}
|
||||
/** A CommonJS module whose export object is one of its functions, with
|
||||
* its member accesses respelled through that function (null: none). */
|
||||
cjsFunctionExports(source: string, path: string): string | null {
|
||||
this.ensureOpen();
|
||||
if (!source.includes("module.exports")) return null;
|
||||
return normalizeCjsFunctionExports(this.parse(path, source, "js"));
|
||||
}
|
||||
nullableClassFields(path: string, source: string): syntax.NpmStaticOverloadRewrite | null {
|
||||
return syntax.applyNpmStaticNullableClassFields(this.parse(path, source, "js"), source);
|
||||
}
|
||||
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/required-module-truthiness.cjs"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/fixtures/npm/cases/2583-cjs-function-exports/main.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/fixtures/npm/cases/2584-cjs-conditional-exports/main.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
// A required builtin module object in a boolean context is truthy, as any
|
||||
// object: readable-stream's `process.env.READABLE_STREAM === "disable" &&
|
||||
// Stream` and `Stream || exports` guards.
|
||||
"use strict";
|
||||
var Stream = require("stream");
|
||||
var os = require("os");
|
||||
console.log(Stream ? "stream" : "none", !!os, !os);
|
||||
console.log(process.env.SCRIPTC_UNSET_FLAG === "disable" && Stream ? "disabled" : "default");
|
||||
var fallback = Stream || null;
|
||||
console.log(fallback === Stream);
|
||||
@@ -0,0 +1,12 @@
|
||||
// CommonJS modules whose export object is one of their own functions:
|
||||
// semver 5/6's `exports = module.exports = Version` with members attached
|
||||
// through both `exports` and `module.exports` (and read back through
|
||||
// `exports` inside the module), and a constructor function exported whole
|
||||
// with function and value members attached afterwards.
|
||||
import Version from "gtfnexports";
|
||||
import Point from "gtfnmembers";
|
||||
|
||||
const v = Version.parse("2.4.1");
|
||||
console.log(v === null ? "null" : v.major(), String(new Version("3.0.0")));
|
||||
console.log(Version.compare("5.0.0", "3.2.0"), Version.gt("1.0.0", "2.0.0"), Version.MAX_LENGTH);
|
||||
console.log(Point.origin().sum(), new Point(2, 3).sum(), Point.dimensions);
|
||||
@@ -0,0 +1,7 @@
|
||||
// An environment-dependent export object (readable-stream 2.x's
|
||||
// READABLE_STREAM=disable switch): module.exports is replaced inside an
|
||||
// if/else, so the module's exports depend on which branch ran. scriptc
|
||||
// names the construct; Node picks the branch at load.
|
||||
import rs from "gtcondexports";
|
||||
|
||||
console.log(rs.Readable.name);
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
declare const exported: { Readable: Function; Stream: unknown; name: string };
|
||||
export = exported;
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
var Stream = require("stream");
|
||||
function Local(name) {
|
||||
this.name = name;
|
||||
}
|
||||
if (process.env.GT_STREAMS === "native" && Stream) {
|
||||
module.exports = Stream;
|
||||
exports = module.exports = Stream.Readable;
|
||||
exports.Readable = Stream.Readable;
|
||||
exports.Stream = Stream;
|
||||
} else {
|
||||
exports = module.exports = Local;
|
||||
exports.Stream = Stream || exports;
|
||||
exports.Readable = exports;
|
||||
}
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"name": "gtcondexports",
|
||||
"version": "1.0.0",
|
||||
"description": "readable-stream 2.x's environment-dependent export object: module.exports is replaced (and exports rebound) inside an if/else on an environment variable",
|
||||
"main": "index.js",
|
||||
"types": "index.d.ts"
|
||||
}
|
||||
+13
@@ -0,0 +1,13 @@
|
||||
declare class Version {
|
||||
constructor(raw: string);
|
||||
raw: string;
|
||||
major(): number;
|
||||
toString(): string;
|
||||
}
|
||||
declare namespace Version {
|
||||
const MAX_LENGTH: number;
|
||||
function parse(raw: string): Version | null;
|
||||
function compare(a: string, b: string): number;
|
||||
function gt(a: string, b: string): boolean;
|
||||
}
|
||||
export = Version;
|
||||
+25
@@ -0,0 +1,25 @@
|
||||
exports = module.exports = Version;
|
||||
|
||||
var label = process.env.GT_LABEL ? "[" + process.env.GT_LABEL + "]" : "";
|
||||
|
||||
function Version(raw) {
|
||||
this.raw = raw;
|
||||
this.parts = raw.split(".");
|
||||
}
|
||||
Version.prototype.major = function () {
|
||||
return Number(this.parts[0]);
|
||||
};
|
||||
Version.prototype.toString = function () {
|
||||
return label + this.raw;
|
||||
};
|
||||
|
||||
exports.MAX_LENGTH = 256;
|
||||
exports.parse = function parse(raw) {
|
||||
return raw.length > exports.MAX_LENGTH ? null : new Version(raw);
|
||||
};
|
||||
exports.compare = function compare(a, b) {
|
||||
return exports.parse(a).major() - exports.parse(b).major();
|
||||
};
|
||||
module.exports.gt = function gt(a, b) {
|
||||
return exports.compare(a, b) > 0;
|
||||
};
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"name": "gtfnexports",
|
||||
"version": "1.0.0",
|
||||
"description": "semver 5/6's export idiom: exports = module.exports = Version, then members attached through exports and module.exports",
|
||||
"main": "index.js",
|
||||
"types": "index.d.ts"
|
||||
}
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
declare class Point {
|
||||
constructor(x: number, y: number);
|
||||
x: number;
|
||||
y: number;
|
||||
sum(): number;
|
||||
}
|
||||
declare namespace Point {
|
||||
function origin(): Point;
|
||||
const dimensions: number;
|
||||
}
|
||||
export = Point;
|
||||
+13
@@ -0,0 +1,13 @@
|
||||
"use strict";
|
||||
function Point(x, y) {
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
}
|
||||
Point.prototype.sum = function () {
|
||||
return this.x + this.y;
|
||||
};
|
||||
module.exports = Point;
|
||||
module.exports.origin = function origin() {
|
||||
return new Point(0, 0);
|
||||
};
|
||||
module.exports.dimensions = 2;
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"name": "gtfnmembers",
|
||||
"version": "1.0.0",
|
||||
"description": "a constructor function as module.exports with function and value members attached through module.exports afterwards",
|
||||
"main": "index.js",
|
||||
"types": "index.d.ts"
|
||||
}
|
||||
@@ -19,13 +19,13 @@ export function npmCases(fixturesRoot: string): NpmCase[] {
|
||||
return [
|
||||
...globSync(join(fixturesRoot, "npm/cases/*/main.ts"))
|
||||
.sort()
|
||||
// 2465-2469, 2556-2567, 2571 and 2580-2582 are the --npm-static bundler-emitted-CJS
|
||||
// 2465-2469, 2556-2567, 2571 and 2580-2584 are the --npm-static bundler-emitted-CJS
|
||||
// cases (npm-static.test.ts drives them with the opt-in): their
|
||||
// consumers exercise the static rewrite's surface (lexer-visible
|
||||
// names the shipped .d.ts never declares, the __toESM interop
|
||||
// family), so they stay out of the flagless island lane by design.
|
||||
.filter(
|
||||
(entry) => !/\/(246[5-9]|255[6-9]|256[0-7]|2571|258[0-2])-[^/]+\/main\.ts$/.test(entry),
|
||||
(entry) => !/\/(246[5-9]|255[6-9]|256[0-7]|2571|258[0-4])-[^/]+\/main\.ts$/.test(entry),
|
||||
)
|
||||
.map((entry) => ({
|
||||
name: entry.split("/").at(-2)!,
|
||||
|
||||
@@ -76,7 +76,7 @@ async function buildStatic(entry: string, npmStatic: string[] | "auto"): Promise
|
||||
...globSync(
|
||||
join(fixturesRoot, "npm/node_modules/cryptozoo/**/*.{js,mjs,cjs,json,d.ts}"),
|
||||
).sort(),
|
||||
// the bundler-emitted-CJS mini packages (cases 2465-2469, 2556-2567, 2571, 2580-2582)
|
||||
// the bundler-emitted-CJS mini packages (cases 2465-2469, 2556-2567, 2571, 2580-2584)
|
||||
...globSync(join(fixturesRoot, "npm/node_modules/gt*/**/*.{js,cjs,json}")).sort(),
|
||||
...globSync(join(fixturesRoot, "npm/store/gt*/**/*.{js,cjs,json}")).sort(),
|
||||
];
|
||||
@@ -1514,6 +1514,32 @@ describe(`npm-static pilots${sanitize ? " (sanitized)" : ""}`, () => {
|
||||
expect(nativeRes.exitCode).toBe(nodeRes.exitCode);
|
||||
}, 180_000);
|
||||
|
||||
// 2583: a module whose export object is one of its functions (semver
|
||||
// 5/6's `exports = module.exports = Version`), under both values of the
|
||||
// environment variable the module reads.
|
||||
test("function export objects 2583-cjs-function-exports compile statically and byte-match Node", async () => {
|
||||
const entry = join(fixturesRoot, "npm/cases/2583-cjs-function-exports/main.ts");
|
||||
await expectNodeParity(entry, ["gtfnexports", "gtfnmembers"]);
|
||||
const binary = await buildStatic(entry, ["gtfnexports", "gtfnmembers"]);
|
||||
const env = { ...process.env, GT_LABEL: "x" };
|
||||
const [nodeRes, nativeRes] = await Promise.all([
|
||||
runBinary("node", [entry], env),
|
||||
runBinary(binary, [], env),
|
||||
]);
|
||||
expect(nativeRes.stdout.toString("utf8")).toBe(nodeRes.stdout.toString("utf8"));
|
||||
expect(nativeRes.stdout.toString("utf8")).toContain("[x]");
|
||||
}, 180_000);
|
||||
|
||||
// 2584: an export object replaced inside an environment-dependent branch
|
||||
// (readable-stream 2.x) is named precisely.
|
||||
test("an environment-dependent export object is refused by name", () => {
|
||||
const entry = join(fixturesRoot, "npm/cases/2584-cjs-conditional-exports/main.ts");
|
||||
const { coverage } = analyze(entry, { npmStatic: ["gtcondexports"] });
|
||||
expect(JSON.stringify(coverage)).toContain(
|
||||
"replacing module.exports outside a top-level statement (a conditional or environment-dependent export object)",
|
||||
);
|
||||
}, 120_000);
|
||||
|
||||
// 2567: an export that reads `this`, called through its namespace, sees
|
||||
// Node's module object; the lowered namespace would differ, so the
|
||||
// package stays refused.
|
||||
|
||||
Reference in New Issue
Block a user