Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
85608b1
Export settled CommonJS variables by value, and members of var-bound …
cramforce Oct 20, 2026
c673dc6
Split minified declarations of requires and the bindings read from them
cramforce Oct 20, 2026
cd07bce
Keep semver's token table a plain object behind an early function export
cramforce Oct 20, 2026
b8e1c1b
Merge remote-tracking branch 'origin/opt/npm-polyfill-shims' into opt…
cramforce Oct 20, 2026
c0224f2
Store fs.realpathSync as a function value
cramforce Oct 20, 2026
2f14ef3
Read and call members a module attaches to its whole export
cramforce Oct 20, 2026
4a28050
Format and record the new fixtures' order baselines
cramforce Oct 20, 2026
a80604c
Resolve whole-export members only for function exports the requirer d…
cramforce Oct 20, 2026
d60d1f2
Unwrap tsc's interop helpers around builtin modules in packages
cramforce Oct 20, 2026
24963c8
Lower star re-exports of empty namespaces and reads of a bundle's ent…
cramforce Oct 20, 2026
d0ab52a
Merge remote-tracking branch 'origin/opt/npm-polyfill-shims' into opt…
cramforce Oct 20, 2026
1b6aa95
Merge branch 'opt/npm-cjs-var-export' into opt/npm-tsc-helpers
cramforce Oct 20, 2026
278f6ca
Format and record the new fixtures' order baselines
cramforce Oct 20, 2026
391175d
Respell members of a whole function export behind requires only for v…
cramforce Oct 20, 2026
8d392b8
Merge branch 'opt/npm-cjs-var-export' into opt/npm-tsc-helpers
cramforce Oct 20, 2026
9d7e398
Record the 2596 order baseline
cramforce Oct 20, 2026
8d98630
Merge branch 'opt/npm-cjs-var-export' into opt/npm-tsc-helpers
cramforce Oct 20, 2026
5616526
Count builtin calls with callbacks and every write of a whole export'…
ctate Oct 20, 2026
89f2eee
Merge remote-tracking branch 'origin/opt/npm-cjs-var-export' into opt…
cramforce Oct 20, 2026
d5a93a2
Merge branch 'opt/npm-cjs-var-export' into opt/npm-tsc-helpers
cramforce Oct 20, 2026
739f150
Pass the exports object as `this` to exported functions that call sib…
cramforce Oct 21, 2026
8ac501b
Refuse namespace reads of inherited names and of names an entry star …
ctate Oct 21, 2026
f54f79e
Unwrap tsc's interop helpers around builtins; lower re-exports of emp…
ctate Oct 21, 2026
d5044b9
Count a module's own member export of its function as no write of its…
cramforce Oct 21, 2026
4847ec9
Merge branch 'opt/npm-cjs-var-export' into opt/npm-tsc-helpers
cramforce Oct 21, 2026
d0f546e
Merge branch 'opt/npm-tsc-helpers' into opt/npm-exports-this
cramforce Oct 21, 2026
669c229
Merge remote-tracking branch 'origin/opt/npm-cjs-var-export' into opt…
cramforce Oct 21, 2026
24de997
Merge remote-tracking branch 'origin/opt/npm-cjs-var-export' into opt…
cramforce Oct 21, 2026
4dd3cfe
Format the exports-this stage
cramforce Oct 21, 2026
6b6df7e
Look inside classes for early calls, count only member writes of func…
ctate Oct 21, 2026
691e218
Record order baselines for the exports-this cases
cramforce Oct 21, 2026
6fd1804
Accept destructured requires above a function export
cramforce Oct 21, 2026
3c77a2d
Merge remote-tracking branch 'origin/opt/npm-cjs-var-export' into opt…
cramforce Oct 21, 2026
4da1ea9
Merge branch 'opt/npm-cjs-var-export' into opt/npm-exports-this
cramforce Oct 21, 2026
9ddbd94
Keep `this` in exported functions that another export detaches or tha…
ctate Oct 21, 2026
fdcc97e
Pass the exports object as this to exported functions that read sibli…
ctate Oct 21, 2026
5c7dd8b
Merge main
ctate Oct 21, 2026
ac05861
Count a function export handed to other code as a write of every member
ctate Oct 21, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/content/limitations/builtin-function-values.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ Some builtins can be stored and passed as functions:
- Supported zero-argument `node:os` functions and `querystring.escape`/`unescape`.
- `util.isDeepStrictEqual`, `styleText`, `stripVTControlCharacters`, `toUSVString`, `inherits`, `deprecate`, `callbackify`, `debuglog`, and `debug`.
- `fs.existsSync`, `unlinkSync`, `chmodSync`, `chownSync`, `renameSync`, and `closeSync`, and `fs/promises.unlink`, `chmod`, and `rename`.
- `fs.realpathSync`, called with a string path and options that keep the UTF-8 encoding (none, `"utf8"`, or an object without another `encoding`, such as fs.realpath's cache object). Other paths or encodings throw `SC1090` where the call runs.

In JavaScript, `setTimeout`, `setInterval`, `setImmediate`, `clearTimeout`, `clearInterval`, `clearImmediate`, `queueMicrotask`, and `process.nextTick` are function values wherever they are stored, including conditional and logical expressions and assignments: `typeof` answers `"function"`, identity holds across spellings, and calls schedule as the direct calls do, returning the same numeric timer handles.
Calls through these values bypass `node:test` mock timers.
Expand Down
67 changes: 67 additions & 0 deletions packages/compiler/src/frontend/lowering/binding-analysis.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,73 @@ export function bindingNeverReassigned(lowerer: Lowerer, sym: ts.Symbol, decl: t
return !written;
}

/** True when a module-scope variable's value is settled before the
* top-level statement `at` runs, and stays that way: its declaration is a
* top-level statement above `at`, and every write (the initializer
* included) sits in top-level code above `at`, outside any function or
* class body. A CommonJS export of the binding at `at` then copies the
* value the binding keeps forever, so reading the binding itself is
* exact (esbuild hoists a bundled module's top-level `const` to such a
* `var`). */
export function bindingSettledBefore(lowerer: Lowerer, sym: ts.Symbol, at: ts.Statement): boolean {
const sf = at.getSourceFile();
if (at.parent !== sf) return false;
const decls = sym.declarations;
if (decls.length !== 1) return false;
const decl = decls[0]!;
if (!ts.isVariableDeclaration(decl) || !ts.isIdentifier(decl.name)) return false;
const stmt = decl.parent?.parent;
if (stmt === undefined || !ts.isVariableStatement(stmt) || stmt.parent !== sf) return false;
const limit = at.getStart(sf);
if (stmt.getEnd() > limit) return false;
const symText = ts.symbolName(sym);
const namesSym = (e: ts.Node): boolean =>
ts.isIdentifier(e) && e.text === symText && lowerer.resolveValueSymbol(e) === sym;
let settled = true;
// `nested`: inside a function or class body, where code can run later.
const scanTarget = (t: ts.Expression, nested: boolean): void => {
let e: ts.Expression = t;
while (ts.isParenthesizedExpression(e)) e = e.expression;
let hit = namesSym(e);
if (!hit && (ts.isArrayLiteralExpression(e) || ts.isObjectLiteralExpression(e))) {
const walk = (n: ts.Node): void => {
if (namesSym(n)) hit = true;
else n.forEachChild(walk);
};
walk(e);
}
if (hit && (nested || t.getEnd() > limit)) settled = false;
};
const visit = (n: ts.Node, nested: boolean): void => {
if (!settled) return;
const inner =
nested ||
ts.isFunctionLike(n) ||
ts.isClassDeclaration(n) ||
ts.isClassExpression(n) ||
ts.isClassStaticBlockDeclaration(n);
if (ts.isBinaryExpression(n)) {
const k = n.operatorToken.kind;
if (k >= ts.SyntaxKind.FirstAssignment && k <= ts.SyntaxKind.LastAssignment) {
scanTarget(n.left, nested);
}
} else if (
(ts.isPrefixUnaryExpression(n) || ts.isPostfixUnaryExpression(n)) &&
(n.operator === ts.SyntaxKind.PlusPlusToken || n.operator === ts.SyntaxKind.MinusMinusToken)
) {
scanTarget(n.operand as ts.Expression, nested);
} else if (
(ts.isForOfStatement(n) || ts.isForInStatement(n)) &&
!ts.isVariableDeclarationList(n.initializer)
) {
scanTarget(n.initializer as ts.Expression, nested);
}
n.forEachChild((c) => visit(c, inner));
};
sf.forEachChild((c) => visit(c, false));
return settled;
}

/** Strips the value-preserving wrappers off an expression: parens,
* non-null assertions, `as`/`satisfies`/angle-bracket casts. What
* remains is the expression that actually evaluates. */
Expand Down
Loading