mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-10-29 07:04:09 +08:00
Compile TypeScript's UMD module wrapper as the module body
This commit is contained in:
@@ -0,0 +1,109 @@
|
||||
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 unwrap(source: string): string | null {
|
||||
return nodeFrontendServices().umdFactory(source, "/pkg/index.js");
|
||||
}
|
||||
|
||||
/** TypeScript's UMD wrapper around `body`. */
|
||||
function umd(body: string, params = "require, exports"): string {
|
||||
return `(function (factory) {
|
||||
if (typeof module === "object" && typeof module.exports === "object") {
|
||||
var v = factory(require, exports);
|
||||
if (v !== undefined) module.exports = v;
|
||||
}
|
||||
else if (typeof define === "function" && define.amd) {
|
||||
define(["require", "exports"], factory);
|
||||
}
|
||||
})(function (${params}) {
|
||||
${body}
|
||||
});
|
||||
`;
|
||||
}
|
||||
|
||||
const probe = `
|
||||
console.log(JSON.stringify(module.exports), Object.keys(module.exports).join(","));
|
||||
`;
|
||||
|
||||
function run(file: string): string {
|
||||
const result = spawnSync(process.execPath, [file], { encoding: "utf8" });
|
||||
expect(result.status).toBe(0);
|
||||
return result.stdout;
|
||||
}
|
||||
|
||||
/** The unwrapped module exports what the original exports. */
|
||||
function expectSameModule(source: string): string {
|
||||
const result = unwrap(source);
|
||||
expect(typeof result).toBe("string");
|
||||
const dir = mkdtempSync(join(tmpdir(), "scriptc-umd-"));
|
||||
dirs.push(dir);
|
||||
const original = join(dir, "original.cjs");
|
||||
const unwrapped = join(dir, "unwrapped.cjs");
|
||||
writeFileSync(original, source + probe);
|
||||
writeFileSync(unwrapped, result! + probe);
|
||||
const answer = run(original);
|
||||
expect(run(unwrapped)).toBe(answer);
|
||||
return result!;
|
||||
}
|
||||
|
||||
test("the factory body runs at the top level with its positions kept", () => {
|
||||
const source = umd(` "use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.twice = twice;
|
||||
const path = require("path");
|
||||
exports.sep = path.sep;
|
||||
function twice(n) {
|
||||
return n * 2;
|
||||
}`);
|
||||
const result = expectSameModule(source);
|
||||
expect(result.length).toBe(source.length);
|
||||
expect(result.split("\n").length).toBe(source.split("\n").length);
|
||||
expect(result.indexOf("exports.twice = twice;")).toBe(source.indexOf("exports.twice = twice;"));
|
||||
expect(result).not.toContain("define.amd");
|
||||
expect(result).not.toContain("factory");
|
||||
});
|
||||
|
||||
test("TypeScript's helper probes of this keep their fallback", () => {
|
||||
expectSameModule(
|
||||
umd(` "use strict";
|
||||
var __importDefault = (this && this.__importDefault) || function (mod) {
|
||||
return (mod && mod.__esModule) ? mod : { "default": mod };
|
||||
};
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
const os_1 = __importDefault(require("os"));
|
||||
exports.eol = os_1.default.EOL;`),
|
||||
);
|
||||
});
|
||||
|
||||
test("nested functions keep their own return, arguments and this", () => {
|
||||
expectSameModule(
|
||||
umd(` "use strict";
|
||||
exports.first = function () { return arguments[0]; };
|
||||
exports.self = { get() { return this; } };
|
||||
exports.arrow = () => { return 1; };`),
|
||||
);
|
||||
});
|
||||
|
||||
test("other shapes keep the original text", () => {
|
||||
// `export =` returns the module value from the factory.
|
||||
expect(unwrap(umd(` "use strict";\n return { a: 1 };`))).toBeNull();
|
||||
// The body's own `this` and `arguments` differ from the module's.
|
||||
expect(unwrap(umd(` exports.self = this;`))).toBeNull();
|
||||
expect(unwrap(umd(` exports.count = arguments.length;`))).toBeNull();
|
||||
// Other factory parameters.
|
||||
expect(unwrap(umd(` exports.a = 1;`, "require, exports, module"))).toBeNull();
|
||||
expect(unwrap(umd(` exports.a = 1;`, "req, exp"))).toBeNull();
|
||||
// Code after the wrapper.
|
||||
expect(unwrap(umd(` exports.a = 1;`) + "exports.b = 2;\n")).toBeNull();
|
||||
// Not a UMD wrapper.
|
||||
expect(unwrap(`exports.a = 1;\n`)).toBeNull();
|
||||
});
|
||||
@@ -0,0 +1,178 @@
|
||||
/* TypeScript's UMD module emit in a compiled package: the module body is a
|
||||
* factory function that the wrapper calls with the CommonJS module's own
|
||||
* `require` and `exports`.
|
||||
*
|
||||
* (function (factory) {
|
||||
* if (typeof module === "object" && typeof module.exports === "object") {
|
||||
* var v = factory(require, exports);
|
||||
* if (v !== undefined) module.exports = v;
|
||||
* }
|
||||
* else if (typeof define === "function" && define.amd) {
|
||||
* define(["require", "exports", "./dep"], factory);
|
||||
* }
|
||||
* })(function (require, exports) {
|
||||
* "use strict";
|
||||
* const dep = require("./dep");
|
||||
* exports.f = f;
|
||||
* });
|
||||
*
|
||||
* Under Node the CommonJS branch runs: the factory body runs once with the
|
||||
* module's `require` and `exports`, and a body without a `return` leaves
|
||||
* `module.exports` as it was. That is the module's own body, so the wrapper
|
||||
* is blanked out (line breaks kept, so positions stay) and the body runs at
|
||||
* the module's top level, where its requires and export assignments are the
|
||||
* module's own.
|
||||
*
|
||||
* Accepted: the wrapper is the file's only statement, its CommonJS branch
|
||||
* calls the factory as `factory(require, exports)`, the factory's
|
||||
* parameters are exactly `require` and `exports`, and the body uses no
|
||||
* `return` or `arguments` of its own. `this` in the body is the factory's
|
||||
* receiver, not the module's: it may appear only in TypeScript's helper
|
||||
* probes (`this && this.__importDefault`), which find no helper on either
|
||||
* receiver. Any other shape keeps the original text. */
|
||||
import * as ts from "./ts7/syntax.js";
|
||||
import { tokenStartOfNode } from "./ts7/node-functions.js";
|
||||
import { applyEdits } from "./npm-static-bundled-chunks.js";
|
||||
|
||||
/** True when the text may hold a UMD wrapper (a cheap probe). */
|
||||
export function mayHoldUmdWrapper(source: string): boolean {
|
||||
return source.includes("define.amd") && source.includes("(require, exports)");
|
||||
}
|
||||
|
||||
/** The module text with a UMD wrapper's factory body at the top level, or
|
||||
* null when the file is not one. */
|
||||
export function unwrapUmdFactory(sf: ts.SourceFile): string | null {
|
||||
const stmts = sf.statements.nodes;
|
||||
if (stmts.length !== 1) return null;
|
||||
const stmt = stmts[0]!;
|
||||
if (!ts.isExpressionStatement(stmt) || !ts.isCallExpression(stmt.expression)) return null;
|
||||
const call = stmt.expression;
|
||||
if (call.arguments.nodes.length !== 1) return null;
|
||||
const wrapper = unparenthesized(call.expression);
|
||||
const factory = call.arguments.nodes[0]!;
|
||||
if (!ts.isFunctionExpression(wrapper) || !ts.isFunctionExpression(factory)) return null;
|
||||
const wrapperParams = wrapper.parameters.nodes;
|
||||
if (wrapperParams.length !== 1 || !isPlainParameter(wrapperParams[0]!)) return null;
|
||||
const factoryName = (wrapperParams[0]!.name as ts.Identifier).text;
|
||||
if (!callsFactoryForCommonJs(wrapper, factoryName)) return null;
|
||||
const params = factory.parameters.nodes;
|
||||
if (
|
||||
params.length !== 2 ||
|
||||
!params.every(isPlainParameter) ||
|
||||
(params[0]!.name as ts.Identifier).text !== "require" ||
|
||||
(params[1]!.name as ts.Identifier).text !== "exports"
|
||||
)
|
||||
return null;
|
||||
const body = factory.body;
|
||||
if (body === undefined || !ownBodyKeepsModuleSemantics(body)) return null;
|
||||
const open = tokenStartOfNode(body) + 1;
|
||||
const close = body.end - 1;
|
||||
return applyEdits(sf.text, 0, sf.text.length, [
|
||||
{ start: tokenStartOfNode(stmt), end: open, text: "" },
|
||||
{ start: close, end: stmt.end, text: "" },
|
||||
]);
|
||||
}
|
||||
|
||||
function unparenthesized(e: ts.Expression): ts.Expression {
|
||||
while (ts.isParenthesizedExpression(e)) e = e.expression;
|
||||
return e;
|
||||
}
|
||||
|
||||
/** A parameter that is a bare identifier: no default, rest or pattern. */
|
||||
function isPlainParameter(p: ts.ParameterDeclaration): boolean {
|
||||
return ts.isIdentifier(p.name) && p.initializer === undefined && p.dotDotDotToken === undefined;
|
||||
}
|
||||
|
||||
/** True when the wrapper's first statement is TypeScript's CommonJS test,
|
||||
* `if (typeof module === "object" && typeof module.exports === "object")`,
|
||||
* whose branch starts by calling the factory as `factory(require, exports)`. */
|
||||
function callsFactoryForCommonJs(wrapper: ts.FunctionExpression, factoryName: string): boolean {
|
||||
const first = wrapper.body?.statements.nodes[0];
|
||||
if (first === undefined || !ts.isIfStatement(first)) return false;
|
||||
const sf = ts.getSourceFileOfNode(wrapper);
|
||||
const condition = sf.text.slice(tokenStartOfNode(first.expression), first.expression.end);
|
||||
if (
|
||||
condition.replace(/\s+/g, " ") !==
|
||||
'typeof module === "object" && typeof module.exports === "object"'
|
||||
)
|
||||
return false;
|
||||
const branch = first.thenStatement;
|
||||
const call = ts.isBlock(branch) ? branch.statements.nodes[0] : undefined;
|
||||
if (call === undefined || !ts.isVariableStatement(call)) return false;
|
||||
const declarations = call.declarationList.declarations.nodes;
|
||||
const init = declarations.length === 1 ? declarations[0]!.initializer : undefined;
|
||||
if (init === undefined || !ts.isCallExpression(init)) return false;
|
||||
const args = init.arguments.nodes;
|
||||
return (
|
||||
ts.isIdentifier(init.expression) &&
|
||||
init.expression.text === factoryName &&
|
||||
args.length === 2 &&
|
||||
ts.isIdentifier(args[0]!) &&
|
||||
args[0].text === "require" &&
|
||||
ts.isIdentifier(args[1]!) &&
|
||||
args[1].text === "exports"
|
||||
);
|
||||
}
|
||||
|
||||
/** True when the factory body behaves the same as the module's top level:
|
||||
* no `return` or `arguments` of its own, and `this` only in TypeScript's
|
||||
* helper probes. */
|
||||
function ownBodyKeepsModuleSemantics(body: ts.Block): boolean {
|
||||
let keeps = true;
|
||||
ts.walkPreorder(body, (n) => {
|
||||
if (!keeps) return "stop";
|
||||
// Nested functions have their own `return`, `arguments` and `this`;
|
||||
// arrow functions only their own `return`.
|
||||
if (
|
||||
ts.isFunctionDeclaration(n) ||
|
||||
ts.isFunctionExpression(n) ||
|
||||
ts.isMethodDeclaration(n) ||
|
||||
ts.isGetAccessorDeclaration(n) ||
|
||||
ts.isSetAccessorDeclaration(n) ||
|
||||
ts.isConstructorDeclaration(n)
|
||||
)
|
||||
return "skip";
|
||||
if (ts.isClassDeclaration(n) || ts.isClassExpression(n)) return "skip";
|
||||
if (ts.isReturnStatement(n) && !insideArrow(n, body)) keeps = false;
|
||||
else if (ts.isIdentifier(n) && n.text === "arguments") keeps = false;
|
||||
else if (n.kind === ts.SyntaxKind.ThisKeyword && !isHelperProbe(n)) keeps = false;
|
||||
return undefined;
|
||||
});
|
||||
return keeps;
|
||||
}
|
||||
|
||||
/** True when `node` sits inside an arrow function nested in `body`. */
|
||||
function insideArrow(node: ts.Node, body: ts.Block): boolean {
|
||||
for (let p = node.parent; p !== undefined && p !== body; p = p.parent)
|
||||
if (ts.isArrowFunction(p)) return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
/** `this` in TypeScript's helper probe `this && this.__name`: the left
|
||||
* operand, or the receiver of the right operand's `__` member. */
|
||||
function isHelperProbe(node: ts.Node): boolean {
|
||||
const parent = node.parent;
|
||||
if (parent === undefined) return false;
|
||||
const probe =
|
||||
ts.isBinaryExpression(parent) && parent.left === node
|
||||
? parent
|
||||
: ts.isPropertyAccessExpression(parent) &&
|
||||
parent.expression === node &&
|
||||
ts.isIdentifier(parent.name) &&
|
||||
parent.name.text.startsWith("__") &&
|
||||
parent.parent !== undefined &&
|
||||
ts.isBinaryExpression(parent.parent) &&
|
||||
parent.parent.right === parent
|
||||
? parent.parent
|
||||
: undefined;
|
||||
if (probe === undefined || probe.operatorToken.kind !== ts.SyntaxKind.AmpersandAmpersandToken)
|
||||
return false;
|
||||
const right = probe.right;
|
||||
return (
|
||||
probe.left.kind === ts.SyntaxKind.ThisKeyword &&
|
||||
ts.isPropertyAccessExpression(right) &&
|
||||
right.expression.kind === ts.SyntaxKind.ThisKeyword &&
|
||||
ts.isIdentifier(right.name) &&
|
||||
right.name.text.startsWith("__")
|
||||
);
|
||||
}
|
||||
@@ -452,6 +452,10 @@ export function npmStaticFsShadow(services: FrontendServices): NpmStaticFsShadow
|
||||
} else if (esm !== null) {
|
||||
original = esm;
|
||||
}
|
||||
// TypeScript's UMD wrapper calls its factory with the module's own
|
||||
// require and exports: the factory body is the module body.
|
||||
const umd = original === null ? null : services.umdFactory(original, path);
|
||||
if (umd !== null) original = umd;
|
||||
// Polyfill packages that ARE a Node builtin compile as the builtin.
|
||||
const aliased = original === null ? null : services.builtinAliasRequires(original, path);
|
||||
if (aliased !== null) original = aliased;
|
||||
@@ -472,6 +476,7 @@ export function npmStaticFsShadow(services: FrontendServices): NpmStaticFsShadow
|
||||
if (exportsVar !== null) original = exportsVar;
|
||||
const respelled =
|
||||
split !== null ||
|
||||
umd !== null ||
|
||||
tscHelpers !== null ||
|
||||
esm !== null ||
|
||||
aliased !== null ||
|
||||
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
mayRequireBuiltinAlias,
|
||||
rewriteBuiltinAliasRequires,
|
||||
} from "./npm-static-builtin-aliases.js";
|
||||
import { mayHoldUmdWrapper, unwrapUmdFactory } from "./npm-static-umd.js";
|
||||
import { mayHoldEnumIife, rewriteEnumIifes } from "./npm-static-enum-iife.js";
|
||||
import { mayHoldTscBuiltinImport, unwrapTscBuiltinImports } from "./npm-static-tsc-helpers.js";
|
||||
import { builtinPackageAliasFor } from "./package-shims.js";
|
||||
@@ -114,6 +115,13 @@ export class FrontendServices {
|
||||
if (!mayHoldTscBuiltinImport(source)) return null;
|
||||
return unwrapTscBuiltinImports(this.parse(path, source, "js"));
|
||||
}
|
||||
/** A package module written as TypeScript's UMD wrapper with its
|
||||
* factory body at the top level (null: not one). */
|
||||
umdFactory(source: string, path: string): string | null {
|
||||
this.ensureOpen();
|
||||
if (!mayHoldUmdWrapper(source)) return null;
|
||||
return unwrapUmdFactory(this.parse(path, source, "js"));
|
||||
}
|
||||
/** A package module with its literal-valued TypeScript enum IIFEs
|
||||
* respelled as object literals (null: none). */
|
||||
enumIifes(source: string, path: string): string | null {
|
||||
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/7000-umd-package-entry/main.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
// A package whose main entry is TypeScript's UMD emit: each module's body is
|
||||
// a factory the wrapper calls with the module's own require and exports, so
|
||||
// its requires and export assignments sit inside a function. Node runs the
|
||||
// CommonJS branch, and the named imports read the factory's exports.
|
||||
import { Shape, area, describe, label, unitCount } from "umd-shapes";
|
||||
|
||||
console.log(label, unitCount);
|
||||
console.log(area({ kind: Shape.Square, size: 3 }), area({ kind: Shape.Circle, size: 1 }).toFixed(4));
|
||||
console.log(describe({ kind: Shape.Square, size: 2 }));
|
||||
console.log(Shape.Circle, Shape[Shape.Square]);
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
(function (factory) {
|
||||
if (typeof module === "object" && typeof module.exports === "object") {
|
||||
var v = factory(require, exports);
|
||||
if (v !== undefined) module.exports = v;
|
||||
}
|
||||
else if (typeof define === "function" && define.amd) {
|
||||
define(["require", "exports"], factory);
|
||||
}
|
||||
})(function (require, exports) {
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.units = void 0;
|
||||
exports.square = square;
|
||||
exports.circle = circle;
|
||||
exports.units = ["cm", "m"];
|
||||
function square(size) {
|
||||
return size * size;
|
||||
}
|
||||
function circle(radius) {
|
||||
return Math.PI * radius * radius;
|
||||
}
|
||||
});
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
export declare enum Shape {
|
||||
Square = 0,
|
||||
Circle = 1
|
||||
}
|
||||
export interface Figure {
|
||||
kind: Shape;
|
||||
size: number;
|
||||
}
|
||||
export declare const label: string;
|
||||
export declare const unitCount: number;
|
||||
export declare function area(figure: Figure): number;
|
||||
export declare function describe(figure: Figure): string;
|
||||
+29
@@ -0,0 +1,29 @@
|
||||
(function (factory) {
|
||||
if (typeof module === "object" && typeof module.exports === "object") {
|
||||
var v = factory(require, exports);
|
||||
if (v !== undefined) module.exports = v;
|
||||
}
|
||||
else if (typeof define === "function" && define.amd) {
|
||||
define(["require", "exports", "./impl/measure"], factory);
|
||||
}
|
||||
})(function (require, exports) {
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.unitCount = exports.label = exports.Shape = void 0;
|
||||
exports.area = area;
|
||||
exports.describe = describe;
|
||||
const measure = require("./impl/measure");
|
||||
var Shape;
|
||||
(function (Shape) {
|
||||
Shape[Shape["Square"] = 0] = "Square";
|
||||
Shape[Shape["Circle"] = 1] = "Circle";
|
||||
})(Shape || (exports.Shape = Shape = {}));
|
||||
exports.label = "shapes";
|
||||
exports.unitCount = measure.units.length;
|
||||
function area(figure) {
|
||||
return figure.kind === Shape.Square ? measure.square(figure.size) : measure.circle(figure.size);
|
||||
}
|
||||
function describe(figure) {
|
||||
return `${Shape[figure.kind]} of ${figure.size} ${measure.units[0]}: ${area(figure)}`;
|
||||
}
|
||||
});
|
||||
+1
@@ -0,0 +1 @@
|
||||
{ "name": "umd-shapes", "version": "1.0.0", "main": "./lib/main.js", "types": "./lib/main.d.ts" }
|
||||
@@ -0,0 +1 @@
|
||||
{ "plain": 3911, "san": 3835 }
|
||||
Reference in New Issue
Block a user