Use the events module and EventEmitter as untyped constructor values

In JavaScript, `ReleaseEmitter = EventEmitter` over `const EventEmitter =
require("events")` was refused with SC1090 ("a runtime-provided class value
as an untyped value"), and `class Channel extends require("events")` failed
on the require call as a value. Bundled npm code does both, for example a
small semaphore that aliases EventEmitter and constructs its release channel
through the alias.

- The events module binding and the EventEmitter or stream class itself
  turn untyped as the runtime's untyped constructor that constructor
  functions already use (scr_dyn_builtin_class.c). `new` through it builds
  an emitter object with Node's listener API, and `===` against the class
  compares identity.
- `x instanceof EventEmitter` on an untyped value also recognizes a boxed
  compiled emitter (an instance of a program class extending EventEmitter),
  which the prototype test alone answered false for. The same holds through
  an untyped binding narrowed to the class.
- `class X extends require("events")` extends the runtime emitter, as
  through a require binding.
- A typed array of the class built from such values (`[EventEmitter]`) is
  refused with SC1090 rather than failing its class check at run time.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Malte Ubl
2026-10-20 12:41:53 -07:00
co-authored by Claude Opus 5.5
parent a77ee000e7
commit fec779abbb
20 changed files with 424 additions and 10 deletions
@@ -8,6 +8,8 @@ Some builtins can be stored and passed as functions:
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.
Other builtin globals stored in JavaScript remain opaque identity values.
In JavaScript, `require("events")` and the `EventEmitter` class assigned to an untyped binding or stored in an object (`ReleaseEmitter = EventEmitter`) are the untyped `EventEmitter` constructor that constructor functions inherit from (see below): `new` through it builds such an emitter object, and identity and `instanceof` agree with the class.
A class can extend `require("events")` directly.
Other immutable aliases of table-backed Node builtins support direct calls, but cannot be passed elsewhere.
`util.promisify` supports compile-time conversions of `child_process.execFile`, `child_process.exec`, `setTimeout` and `setImmediate` (to the `timers/promises` functions, as in Node), `stream.finished` and `stream.pipeline` (to the `stream/promises` functions, as in Node), the `zlib` callback one-shots `deflate`, `inflate`, `deflateRaw`, `inflateRaw`, `gzip`, `gunzip` and `unzip` (with or without options), the `crypto` callback functions `randomBytes`, `pbkdf2`, `scrypt`, `hkdf`, `randomFill`, `randomInt` and `generateKey`, `dns.lookup` (settling with `{ address, family }`, or the address array with `all: true`) and `dns.lookupService` (settling with `{ hostname, service }`), and the `fs` callback functions that have an `fs/promises` counterpart (`access`, `appendFile`, `chmod`, `chown`, `copyFile`, `lchown`, `link`, `lstat`, `lutimes`, `mkdir`, `mkdtemp`, `readFile`, `readdir`, `readlink`, `realpath`, `rename`, `rm`, `rmdir`, `stat`, `statfs`, `symlink`, `truncate`, `unlink`, `utimes`, and `writeFile`); calls through the converted function lower as the `fs/promises` call.
@@ -1,8 +1,18 @@
import { numLit } from "../../../ir/build.js";
import { BOOL, DYN, RUNTIME_EMITTER_CLASS, type IrExpr, type SrcLoc } from "../../../ir/ir.js";
import {
BOOL,
DYN,
RUNTIME_EMITTER_CLASS,
type IrExpr,
type IrStmt,
type SrcLoc,
} from "../../../ir/ir.js";
import * as ts from "../../ts7/adapter.js";
import { locOf } from "../../program.js";
import type { Lowerer } from "../lowerer.js";
import type { ClassInfo } from "../lower-classes.js";
import { classInstanceOf } from "../class-dynamic-dispatch.js";
import { isSafeToRepeat } from "../expressions/evaluation-safety.js";
/* EventEmitter and the stream classes as untyped constructor values, the
* bases of JavaScript constructor functions:
@@ -147,3 +157,55 @@ export function builtinClassInstanceOf(value: IrExpr, kind: number, loc: SrcLoc)
loc,
};
}
/** `value instanceof Base` for an untyped value that may also hold a
* compiled instance: an object built by a constructor function inherits
* the untyped constructor's prototype, and an instance of a program class
* extending Base boxes as a typed reference the class test recognizes.
* With `callee`, the right-hand side is an untyped value and the class
* test applies only when it is Base's untyped constructor. */
export function builtinClassInstanceOfAny(
lowerer: Lowerer,
value: IrExpr,
kind: number,
info: ClassInfo,
loc: SrcLoc,
callee?: IrExpr,
): IrExpr {
const stmts: IrStmt[] = [];
const once = (expr: IrExpr, name: string): IrExpr => {
if (isSafeToRepeat(expr)) return expr;
const local = lowerer.declareHiddenLocal(name, DYN);
stmts.push({ kind: "varDecl", localId: local.id, init: expr, loc });
return { kind: "varRef", localId: local.id, type: DYN, loc };
};
const tested = once(value, "%instanceValue");
const ctor = callee ? once(callee, "%instanceCallee") : null;
const compiled = classInstanceOf(lowerer, tested, info, loc);
const result: IrExpr = {
kind: "logical",
op: "||",
left: ctor
? { kind: "libCall", fn: "dyn.functionInstanceOf", args: [tested, ctor], type: BOOL, loc }
: builtinClassInstanceOf(tested, kind, loc),
right: ctor
? {
kind: "logical",
op: "&&",
left: {
kind: "dynScalarEq",
left: ctor,
right: builtinClassValue(kind, loc),
type: BOOL,
loc,
},
right: compiled,
type: BOOL,
loc,
}
: compiled,
type: BOOL,
loc,
};
return stmts.length ? { kind: "seqExpr", stmts, result, type: BOOL, loc } : result;
}
@@ -45,6 +45,7 @@ import { lowerElementCompoundParts } from "./element-access.js";
import { InternalCompilerError } from "../../../errors.js";
import { isJsSourceFile, locOf } from "../../program.js";
import { storedClassValueType } from "../lower-classes.js";
import { builtinClassKind } from "../builtins/builtin-class-dyn.js";
import { hasRuntimeStatics } from "../class-runtime-statics.js";
import { tryLowerExpression } from "./try-lower-expression.js";
import { fenceNodeModuleMutation, lowerRequireCacheHas } from "../lower-node-module.js";
@@ -1486,6 +1487,25 @@ export function lowerBinary(lowerer: Lowerer, expr: ts.BinaryExpression): IrExpr
loc,
};
}
// EventEmitter or a stream class against an untyped value in
// JavaScript: the untyped value can hold the class's untyped
// constructor (coerceToExpected), which compares by identity.
if (
dynSide.type.kind === "dyn" &&
isJsSourceFile(expr.getSourceFile()) &&
((scalarSide.type.kind === "moduleNs" && scalarSide.type.moduleId === "builtin:events") ||
(scalarSide.kind === "classRef" && builtinClassKind(scalarSide.className) !== null))
) {
const boxed = lowerer.coerceToExpected(scalarSide, DYN);
return {
kind: "dynScalarEq",
left: scalarSide === left ? boxed : left,
right: scalarSide === right ? boxed : right,
...(negated ? { negated: true as const } : {}),
type: BOOL,
loc,
};
}
if (
dynSide.type.kind === "dyn" &&
(scalarSide.type.kind === "f64" ||
@@ -1,6 +1,6 @@
/* instanceof and in, including private-name and runtime-key in. */
import * as ts from "../../ts7/adapter.js";
import { builtinClassInstanceOf, builtinClassKind } from "../builtins/builtin-class-dyn.js";
import { builtinClassInstanceOfAny, builtinClassKind } from "../builtins/builtin-class-dyn.js";
import {
checkedPrivateIn,
localPrivateBrandClass,
@@ -401,6 +401,16 @@ export function lowerInstanceOf(lowerer: Lowerer, expr: ts.BinaryExpression, loc
}
const left = lowerer.lowerExpr(expr.left);
if (left.type.kind === "dyn") {
// An untyped binding the checker narrowed to EventEmitter or a
// stream class (`ReleaseEmitter = EventEmitter` in JavaScript):
// the binding holds an untyped constructor, so the test is
// Function.prototype[Symbol.hasInstance] over it.
const builtinKind = targetInfo.def.runtime ? builtinClassKind(targetInfo.def.name) : null;
if (builtinKind !== null) {
const right = lowerer.lowerExpr(expr.right);
if (right.type.kind === "dyn")
return builtinClassInstanceOfAny(lowerer, left, builtinKind, targetInfo, loc, right);
}
if (targetInfo.def.runtime) {
lowerer.unsupported(
"SC1090",
@@ -600,11 +610,11 @@ export function lowerInstanceOf(lowerer: Lowerer, expr: ts.BinaryExpression, loc
}
// `u instanceof EventEmitter` (or a stream class): an untyped object
// built by a constructor function inherits the untyped constructor's
// prototype (builtin-class-dyn.ts). Compiled emitters and streams have
// no untyped form, so the prototype chain answers for every value.
// prototype (builtin-class-dyn.ts), and a compiled emitter or stream
// boxes as a typed reference the class test answers for.
const builtinKind = builtinClassKind(target.def.name);
if (left.type.kind === "dyn" && builtinKind !== null) {
return builtinClassInstanceOf(left, builtinKind, loc);
return builtinClassInstanceOfAny(lowerer, left, builtinKind, target, loc);
}
if (left.type.kind === "dyn") {
lowerer.unsupported(
@@ -89,14 +89,17 @@ import {
withUnitArm,
} from "../type-mapper.js";
import {
canonicalBuiltinModule,
cjsClassExprWholeExportOf,
isCjsJsFile,
isJsSourceFile,
isModuleExportsAccess,
locOf,
requireSpecOf,
} from "../program.js";
import { PoisonError, dynFallbackType, jsFuncNameOf, newFnCtx, own } from "./lowerer.js";
import { isAssertClassNew } from "./assert-instances.js";
import { isCjsRequireGlobal } from "./lower-node-module.js";
import { lowerArrayConstructor } from "./containers/array-construction.js";
import { lowerMapSeedNew, lowerSetSeedNew } from "./containers/collection-methods.js";
import { lowerObjectConstructor } from "./containers/indexed-objects.js";
@@ -978,6 +981,19 @@ export function registerBuiltinEmitterClass(lowerer: Lowerer): void {
lowerer.classes.set(RUNTIME_EMITTER_CLASS, info);
}
/** `class Bus extends require("events") {}` in a CommonJS module: the
* events module is the EventEmitter class itself, so the base is the
* runtime emitter, exactly as through a `require("events")` binding. */
export function builtinRequireBaseOf(lowerer: Lowerer, node: ts.Expression): ClassInfo | null {
let expr = node;
while (ts.isParenthesizedExpression(expr)) expr = expr.expression;
if (!ts.isCallExpression(expr) || !isJsSourceFile(expr.getSourceFile())) return null;
const spec = requireSpecOf(expr);
if (spec === null || canonicalBuiltinModule(spec) !== "events") return null;
if (!isCjsRequireGlobal(lowerer, expr.expression)) return null;
return lowerer.classes.get(RUNTIME_EMITTER_CLASS) ?? null;
}
/** The runtime-provided node:stream classes as eagerly-registered
* ClassInfos (the emitter story): mapType names `%Readable` et al the
* moment a stream type appears, so the infos must exist before any
@@ -2092,6 +2108,11 @@ export function collectClassShapeInner(
// callee is NOT a mixin function keeps the computed-expression
// fence below.
if (t && ts.isCallExpression(t.expression) && !t.typeArguments?.nodes) {
const requiredBase = builtinRequireBaseOf(lowerer, t.expression);
if (requiredBase) {
base = requiredBase;
continue;
}
const mixinBase = lowerer.mixinCallClassInfoOf(t.expression);
if (mixinBase) {
base = mixinBase;
@@ -5724,7 +5745,7 @@ export function lowerClassExpressionInfo(
)?.types?.nodes[0]?.expression;
if (
heritage &&
(ts.isCallExpression(heritage) ||
((ts.isCallExpression(heritage) && !builtinRequireBaseOf(lowerer, heritage)) ||
(ts.isIdentifier(heritage) &&
(lowerer.peekLocal(heritage) ?? lowerer.globalOf(heritage)) != null))
) {
@@ -132,6 +132,7 @@ import { recordTextCodecClass } from "../../ir/ir.js";
import { isUnitOnlyTsType, unitOnlyUnion } from "../type-mapper.js";
import type { ClassInfo } from "./lower-classes.js";
import {
builtinRequireBaseOf,
collectInheritedJsFieldWrites,
collectVirtualJsMethods,
decoratorNodesOf,
@@ -373,7 +374,8 @@ export function collectProgram(lowerer: Lowerer, parts: FileParts[]): void {
const heritage = decl.heritageClauses?.nodes.find(
(clause) => clause.token === ts.SyntaxKind.ExtendsKeyword,
)?.types?.nodes[0]?.expression;
if (heritage && ts.isCallExpression(heritage)) computed.add(decl);
if (heritage && ts.isCallExpression(heritage) && !builtinRequireBaseOf(lowerer, heritage))
computed.add(decl);
}
// Descendants need the computed ancestor's completed layout before
// their own fields and inherited methods can be collected.
@@ -6,7 +6,12 @@ import {
} from "./general-streams.js";
import { extendAssertedRecordMembers } from "./assertion-members.js";
import { jsDynamicDeclarations } from "./js-dynamic-objects.js";
import { builtinClassValue, builtinMemberClassKind } from "./builtins/builtin-class-dyn.js";
import { isJsSourceFileName } from "../tsc-codes.js";
import {
builtinClassKind,
builtinClassValue,
builtinMemberClassKind,
} from "./builtins/builtin-class-dyn.js";
import {
lifetimeRegionType,
lowerLifetimeRegionCall,
@@ -8925,7 +8930,33 @@ export class Lowerer {
}
// A runtime-provided class value has no checked-dynamic form that
// inspects as Node's does (`[Function: EventEmitter] { ... }`).
// JavaScript sources already read EventEmitter and the stream
// classes as the runtime's untyped constructors (builtin-class-dyn.ts),
// so one of those classes turning untyped there becomes the same
// constructor. `require("events")` is the EventEmitter class itself,
// so its module binding is too (`ReleaseEmitter = EventEmitter` over
// `const EventEmitter = require("events")`).
if (
expr.type.kind === "moduleNs" &&
expr.type.moduleId === "builtin:events" &&
isJsSourceFileName(expr.loc.file)
) {
const value = builtinClassValue(0, expr.loc);
if (expr.kind === "moduleNsRef") return value;
return {
kind: "seqExpr",
stmts: [{ kind: "exprStmt", expr, loc: expr.loc }],
result: value,
type: DYN,
loc: expr.loc,
};
}
if (expr.type.kind === "classval" && this.classes.get(expr.type.className)?.def.runtime) {
// Only the class itself: a slot of the class's type may hold a
// program subclass, which this constructor would misidentify.
const kind = builtinClassKind(expr.type.className);
if (kind !== null && expr.kind === "classRef" && isJsSourceFileName(expr.loc.file))
return builtinClassValue(kind, expr.loc);
this.pushDiag(
unsupportedDiag(
"SC1090",
@@ -9005,6 +9036,24 @@ export class Lowerer {
);
const handleOk = DYN_HANDLE_KINDS.has(expected.kind);
noteUntypedStreamFlow(this, expected);
// `[EventEmitter]` in JavaScript builds an untyped array of the
// untyped constructor, which a typed array of the class cannot
// hold: no class check admits that constructor.
if (
expr.kind === "dynArrLit" &&
expected.kind === "array" &&
expected.elem.kind === "classval" &&
expr.elems.some((elem) => elem.kind === "libCall" && elem.fn === "dyn.builtinClass")
) {
this.pushDiag(
unsupportedDiag(
"SC1090",
expr.loc,
`holding runtime-provided class values ('${expected.elem.className.slice(1)}') in a typed array`,
),
);
throw new PoisonError();
}
if (
canDynCheckTo(
expected,
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/6571-events-module-class-value.cjs"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/6572-extends-require-events.cjs"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/6573-semaphore-release-emitter.cjs"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/fixtures/npm/cases/2595-bundled-chunk-emitter/main.ts"
],
"diags": []
}
@@ -0,0 +1,30 @@
// require("events") is the EventEmitter class. Assigned to an untyped
// binding or stored in an object, it stays a constructor: construction
// through it builds an emitter with the whole listener API, and identity,
// instanceof, typeof and .name agree with the class.
var EventEmitter = require("events"), ReleaseEmitter;
ReleaseEmitter = EventEmitter;
const e = new ReleaseEmitter();
e.on("x", (v) => console.log("x", v));
e.prependListener("x", (v) => console.log("first x", v));
e.once("x", (v) => console.log("once x", v));
console.log("emit", e.emit("x", 1), e.emit("x", 2), e.emit("missing"));
console.log("count", e.listenerCount("x"), e.eventNames(), e.getMaxListeners());
console.log("instanceof", e instanceof EventEmitter, e instanceof ReleaseEmitter);
console.log("identity", ReleaseEmitter === EventEmitter, ReleaseEmitter !== EventEmitter);
console.log("typeof", typeof ReleaseEmitter, ReleaseEmitter.name);
const holder = { E: EventEmitter };
const h = new holder.E();
h.on("error", (err) => console.log("error", err.message));
h.emit("error", new Error("boom"));
console.log("holder", h instanceof EventEmitter, h instanceof holder.E, holder.E === EventEmitter);
try {
new ReleaseEmitter().emit("error", new Error("unhandled"));
} catch (err) {
console.log("caught", err.message);
}
e.removeAllListeners("x");
console.log("removed", e.listenerCount("x"), e.emit("x", 3));
@@ -0,0 +1,27 @@
// A class whose heritage is the require call itself extends EventEmitter,
// as through a require binding. The class is a value: stored in an untyped
// binding, construction through it runs the subclass constructor.
class Channel extends require("events") {
constructor(name) {
super();
this.name = name;
}
send(text) {
return this.emit("message", this.name, text);
}
}
const a = new Channel("a");
a.on("message", (from, text) => console.log("message", from, text));
console.log("sent", a.send("hello"), new Channel("b").send("nobody"));
let Ctor;
Ctor = Channel;
const c = new Ctor("c");
c.once("message", (from, text) => console.log("once", from, text));
c.send("one");
c.send("two");
const EventEmitter = require("events");
console.log("instanceof", a instanceof EventEmitter, c instanceof EventEmitter, c instanceof Channel);
console.log("class", typeof Ctor, Ctor.name, Ctor === Channel, a.listenerCount("message"));
@@ -0,0 +1,62 @@
// A token semaphore whose constructor builds its release channel from the
// events module, the way small npm semaphores do: once through a subclass
// declared beside the require, once through an untyped alias of the class.
// acquire() hands out free tokens and queues the rest; release() emits, and
// the listener wakes the oldest waiter or returns the token.
var EventEmitter = require("events"), Subclassed = class extends EventEmitter {}, Aliased;
Aliased = EventEmitter;
function defaultInit() {
return "1";
}
class Semaphore {
constructor(nr, { initFn = defaultInit, aliased = false } = {}) {
this.nrTokens = nr;
this.free = [];
this.waiting = [];
this.releaseEmitter = aliased ? new Aliased() : new Subclassed();
this.noTokens = initFn === defaultInit;
this.releaseEmitter.on("release", (token) => {
const p = this.waiting.shift();
if (p) p.resolve(token);
else this.free.push(token);
});
for (let i = 0; i < nr; i++) this.free.push(initFn());
}
async acquire() {
const token = this.free.pop();
if (token !== undefined) return token;
return new Promise((resolve, reject) => {
this.waiting.push({ resolve, reject });
});
}
release(token) {
this.releaseEmitter.emit("release", this.noTokens ? "1" : token);
}
nrWaiting() {
return this.waiting.length;
}
}
async function run(label, sema) {
const order = [];
const tasks = [1, 2, 3, 4].map(async (n) => {
const token = await sema.acquire();
order.push(`${n}:${token}`);
await new Promise((resolve) => setTimeout(resolve, 2));
sema.release(token);
});
console.log(label, "waiting", sema.nrWaiting());
await Promise.all(tasks);
console.log(label, order.join(" "), "free", sema.free.length, "waiting", sema.nrWaiting());
}
(async () => {
await run("plain", new Semaphore(2));
let next = 0;
await run("tokens", new Semaphore(2, { initFn: () => `t${next++}` }));
await run("aliased", new Semaphore(1, { aliased: true }));
const s = new Semaphore(1);
console.log("channel", s.releaseEmitter instanceof EventEmitter, s.releaseEmitter instanceof Subclassed);
})();
@@ -0,0 +1,11 @@
// A bundled CommonJS chunk whose top-level var statement mixes
// require("events"), require("util"), a sibling chunk's require and a class
// expression extending the events binding, followed by an untyped alias of
// the events module. The require bindings own no storage: the semaphore's
// constructor builds its release channel through them, its queue through
// the sibling chunk's constructor function, and both channels emit.
import { aliased, limited } from "gtchunkee";
console.log(aliased());
console.log(await limited("two slots", 4, 2));
console.log(await limited("one slot", 3, 1));
+2
View File
@@ -0,0 +1,2 @@
export declare function limited(label: string, jobs: number, slots: number): Promise<string>;
export declare function aliased(): string;
+75
View File
@@ -0,0 +1,75 @@
var __create = Object.create;
var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __getProtoOf = Object.getPrototypeOf;
var __hasOwnProp = Object.prototype.hasOwnProperty;
var __commonJS = (cb, mod) => function __require() {
return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports;
};
var __export = (target, all) => {
for (var name in all)
__defProp(target, name, { get: all[name], enumerable: true });
};
var __copyProps = (to, from, except, desc) => {
if (from && typeof from === "object" || typeof from === "function") {
for (let key of __getOwnPropNames(from))
if (!__hasOwnProp.call(to, key) && key !== except)
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
}
return to;
};
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
mod
));
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
// node_modules/queue/index.js
var require_queue = __commonJS({
"node_modules/queue/index.js"(exports2, module2) {
"use strict";function Queue(capacity){this._items=[],this._capacity=capacity||16}Queue.prototype.push=function(item){return this._items.push(item)};Queue.prototype.shift=function(){return this._items.shift()};Queue.prototype.pop=function(){return this._items.pop()};Object.defineProperty(Queue.prototype,"length",{get:function(){return this._items.length}});module2.exports=Queue;
}
});
// node_modules/sema/index.js
var require_sema = __commonJS({
"node_modules/sema/index.js"(exports2) {
var EventEmitter=require("events"),util=require("util"),Queue=require_queue(),ReleaseEmitter=class extends EventEmitter{},AliasedEmitter;AliasedEmitter=EventEmitter;function defaultInit(){return"1"}class Sema{constructor(nr,{initFn=defaultInit,aliased=!1}={}){this.nrTokens=nr,this.free=new Queue(nr),this.waiting=new Queue(10),this.releaseEmitter=aliased?new AliasedEmitter:new ReleaseEmitter,this.noTokens=initFn===defaultInit,this.releaseEmitter.on("release",token=>{let p=this.waiting.shift();p?p.resolve(token):this.free.push(token)});for(let i=0;i<nr;i++)this.free.push(initFn())}async acquire(){let token=this.free.pop();return token||new Promise((resolve,reject)=>{this.waiting.push({resolve,reject})})}release(token){this.releaseEmitter.emit("release",this.noTokens?"1":token)}nrWaiting(){return this.waiting.length}describe(){return util.format("%d tokens, %s",this.nrTokens,this.releaseEmitter instanceof EventEmitter?"emitter":"other")}}exports2.create=function(nr,options){return new Sema(nr,options)};
}
});
// src/index.ts
var index_exports = {};
__export(index_exports, {
aliased: () => aliased,
limited: () => limited
});
module.exports = __toCommonJS(index_exports);
var import_sema = __toESM(require_sema());
async function limited(label, jobs, slots) {
let next = 0;
const sema = (0, import_sema.create)(slots, { initFn: () => "t" + next++ });
const order = [];
const tasks = [];
for (let i = 0; i < jobs; i++) {
tasks.push((async () => {
const token = await sema.acquire();
order.push(i + ":" + token);
await new Promise((resolve) => setTimeout(resolve, 2));
sema.release(token);
})());
}
const queued = sema.nrWaiting();
await Promise.all(tasks);
return label + " queued " + queued + " order " + order.join(" ") + " (" + sema.describe() + ")";
}
function aliased() {
const sema = (0, import_sema.create)(1, { aliased: true });
return sema.describe() + " waiting " + sema.nrWaiting();
}
// Annotate the CommonJS export names for ESM import in node:
0 && (module.exports = {
aliased,
limited
});
+7
View File
@@ -0,0 +1,7 @@
{
"name": "gtchunkee",
"version": "1.0.0",
"description": "esbuild-style CommonJS bundle (--bundle --platform=node --format=cjs, whitespace+syntax minified chunk bodies): a semaphore chunk whose top-level var statement mixes require(\"events\"), require(\"util\"), a sibling chunk require and a class extending the events binding",
"main": "./dist/index.js",
"types": "./dist/index.d.ts"
}
+3 -2
View File
@@ -19,13 +19,14 @@ export function npmCases(fixturesRoot: string): NpmCase[] {
return [
...globSync(join(fixturesRoot, "npm/cases/*/main.ts"))
.sort()
// 2465-2469, 2556-2567, 2571 and 2580-2584 are the --npm-static bundler-emitted-CJS
// 2465-2469, 2556-2567, 2571, 2580-2584 and 2595 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-4])-[^/]+\/main\.ts$/.test(entry),
(entry) =>
!/\/(246[5-9]|255[6-9]|256[0-7]|2571|258[0-4]|2595)-[^/]+\/main\.ts$/.test(entry),
)
.map((entry) => ({
name: entry.split("/").at(-2)!,
+9
View File
@@ -1438,6 +1438,15 @@ describe(`npm-static pilots${sanitize ? " (sanitized)" : ""}`, () => {
]);
}, 180_000);
// 2595: a minified chunk whose top-level var statement mixes require
// bindings with a class extending the events binding; the bindings own no
// storage, so the constructor reaches the modules through them.
test("bundled chunk 2595-bundled-chunk-emitter builds its emitters through require bindings", async () => {
await expectNodeParity(join(fixturesRoot, "npm/cases/2595-bundled-chunk-emitter/main.ts"), [
"gtchunkee",
]);
}, 180_000);
// A whitespace/syntax-minified bundle whose helpers keep their names is
// judged by the recognizers, not refused up front as minified.
test("--npm-static=auto opts a minified bundle with named helpers in", () => {