From 762718a254b9ae4063cc3a167a70dcf906b2e2ab Mon Sep 17 00:00:00 2001 From: Malte Ubl Date: Mon, 19 Oct 2026 20:26:47 -0700 Subject: [PATCH] Compile TLSSocket.isSessionReused, keyed short-circuit assignment and Object.assign into class instances - TLSSocket.isSessionReused() is declared in the fallback types and lowers to false: the TLS client never offers a cached session (no session resumption is a documented divergence), so no handshake resumes one. - `o[k] ??= v`, `||=` and `&&=` through a pure index-signature record with a string key use the keyed read and write `o[k]` and `o[k] = v` lower to. - Object.assign(instance, record) writes the record's fields into the class instance's declared fields (same or width-compatible types; optional source fields only when present) and answers the instance. - Object.assign({}, instance) whose result the checker types as the class is refused at compile time: the copy is a plain object, so the checked cast into the class threw at run time. Co-Authored-By: Claude Opus 5.5 --- .../ambient/scriptc-node-fallback.d.ts | 2 + .../expressions/nullish-assignment.ts | 39 +++++ .../src/frontend/lowering/lower-calls.ts | 134 ++++++++++++++++++ .../src/frontend/lowering/lower-server.ts | 14 ++ .../corpus/6416-tls-session-reused.ts.json | 6 + .../6417-object-assign-class-target.ts.json | 6 + .../6418-nullish-assign-index-record.ts.json | 6 + .../object-assign-class-copy.ts.json | 6 + .../node-types/tls-session-timings.ts.json | 6 + tests/corpus/6416-tls-session-reused.ts | 33 +++++ .../corpus/6417-object-assign-class-target.ts | 36 +++++ .../6418-nullish-assign-index-record.ts | 29 ++++ tests/diagnostics/object-assign-class-copy.ts | 11 ++ .../node-types/tls-session-timings.ts | 31 ++++ .../object-assign-class-copy.ts.txt | 6 + tests/harness/project-config.test.ts | 21 +++ 16 files changed, 386 insertions(+) create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6416-tls-session-reused.ts.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6417-object-assign-class-target.ts.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6418-nullish-assign-index-record.ts.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/object-assign-class-copy.ts.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/node-types/tls-session-timings.ts.json create mode 100644 tests/corpus/6416-tls-session-reused.ts create mode 100644 tests/corpus/6417-object-assign-class-target.ts create mode 100644 tests/corpus/6418-nullish-assign-index-record.ts create mode 100644 tests/diagnostics/object-assign-class-copy.ts create mode 100644 tests/fixtures/node-types/tls-session-timings.ts create mode 100644 tests/harness/__snapshots__/object-assign-class-copy.ts.txt diff --git a/packages/compiler/ambient/scriptc-node-fallback.d.ts b/packages/compiler/ambient/scriptc-node-fallback.d.ts index a8f1f8a8b..628e14f25 100644 --- a/packages/compiler/ambient/scriptc-node-fallback.d.ts +++ b/packages/compiler/ambient/scriptc-node-fallback.d.ts @@ -5238,6 +5238,8 @@ declare module "tls" { getProtocol(): string | null; /* OpenSSL's suite name, the IANA name and the suite's version */ getCipher(): CipherNameAndProtocol; + /* whether the handshake resumed a cached session */ + isSessionReused(): boolean; } export interface CipherNameAndProtocol { name: string; diff --git a/packages/compiler/src/frontend/lowering/expressions/nullish-assignment.ts b/packages/compiler/src/frontend/lowering/expressions/nullish-assignment.ts index 5ef3991da..4366b8d6b 100644 --- a/packages/compiler/src/frontend/lowering/expressions/nullish-assignment.ts +++ b/packages/compiler/src/frontend/lowering/expressions/nullish-assignment.ts @@ -214,6 +214,37 @@ export function lowerShortCircuitAssignment(lowerer: Lowerer, expr: ts.BinaryExp !nullish && !and && element.kind === "f64" ? arrayNumberRead(receiver, key, elementLoc) : arrayValueRead(lowerer, receiver, key, element, elementLoc); + } else if ( + receiver.type.kind === "record" && + key.type.kind === "string" && + pureIndexShapeOf(lowerer, receiver.type.shapeId) !== null + ) { + // `timings[phase] ??= v` over a pure index-signature record + // (`Record`): the keyed read answers the value or + // undefined for a missing key, and the write inserts or replaces + // the key, exactly the two halves `o[k]` and `o[k] = v` lower to. + const shapeId = receiver.type.shapeId; + const indexValue = pureIndexShapeOf(lowerer, shapeId)!; + key = save(key, "%shortCircuitKey"); + const readType = + indexValue.kind === "dyn" ? indexValue : lowerer.withUndefinedArmOf(indexValue); + if (!readType) { + lowerer.unsupported( + "SC1090", + target, + "short-circuit assignment through this indexed receiver", + ); + } + storage = indexValue; + read = { kind: "recordKeyGet", obj: receiver, shapeId, key, type: readType, loc }; + write = (value) => ({ + kind: "recordKeySet", + obj: receiver, + shapeId, + key, + value: lowerer.coerceToExpected(value, indexValue), + loc, + }); } else { lowerer.unsupported( "SC1090", @@ -265,3 +296,11 @@ export function lowerShortCircuitAssignment(lowerer: Lowerer, expr: ts.BinaryExp } return finish({ kind: "nullish", left: read, right, type: read.type, loc }); } + +/** The value type of a pure index-signature record shape (no declared + * fields, not a tuple), or null. */ +function pureIndexShapeOf(lowerer: Lowerer, shapeId: string): IrType | null { + const shape = lowerer.shapes.get(shapeId); + if (!shape || shape.tuple || shape.fields.length > 0 || !shape.indexValue) return null; + return shape.indexValue; +} diff --git a/packages/compiler/src/frontend/lowering/lower-calls.ts b/packages/compiler/src/frontend/lowering/lower-calls.ts index 5a4838782..699f04ec8 100644 --- a/packages/compiler/src/frontend/lowering/lower-calls.ts +++ b/packages/compiler/src/frontend/lowering/lower-calls.ts @@ -6769,6 +6769,99 @@ function recordHasOwnHelper(lowerer: Lowerer, shapeId: string, loc: SrcLoc): str return helper; } +/** Whether `Object.assign(instance, record)` can copy every field of the + * record shape into a field the class declares with the same type: a plain + * record (no tuple, index signature or accessor slots) and a compiled + * class whose chain has no builtin or runtime-provided storage and no + * accessor for the written names. */ +function classAssignPlan(lowerer: Lowerer, className: string, srcShapeId: string): boolean { + const info = lowerer.classes.get(className); + const shape = lowerer.shapes.get(srcShapeId); + if (!info || !shape || shape.tuple || shape.indexValue || shapeHasAccessorSlots(shape)) + return false; + for (let c: ClassInfo | null = info; c; c = c.base) { + if (c.builtinError || c.builtinEmitter || c.builtinStream !== undefined || c.def.runtime) + return false; + } + return shape.fields.every((f) => { + const declared = info.fields.get(f.name); + return ( + declared !== undefined && + (typeEquals(declared, f.type) || lowerer.widthLiftPlan(f.type, declared) !== null) && + !lowerer.findMethodOn(info, f.name) && + !lowerer.findMethodOn(info, `get:${f.name}`) && + !lowerer.findMethodOn(info, `set:${f.name}`) + ); + }); +} + +/** Interned `%obj.assignClass.(o, s)` — Object.assign's per-field + * write of a record into a class instance (classAssignPlan's gate): + * optional source fields write only when present, and the instance + * returns. */ +function classAssignHelper( + lowerer: Lowerer, + className: string, + srcShapeId: string, + loc: SrcLoc, +): string | null { + if (!classAssignPlan(lowerer, className, srcShapeId)) return null; + const key = `obj.assignClass:${className}:${srcShapeId}`; + const existing = lowerer.arrHofHelpers.get(key); + if (existing) return existing; + const helper = `%obj.assignClass.${lowerer.arrHofHelpers.size}`; + lowerer.arrHofHelpers.set(key, helper); + const sShape = lowerer.shapes.get(srcShapeId)!; + const oT: IrType = { kind: "object", className }; + const sT: IrType = { kind: "record", shapeId: srcShapeId }; + const oRef: IrExpr = { kind: "varRef", localId: "o.0", type: oT, loc }; + const sRef: IrExpr = { kind: "varRef", localId: "s.0", type: sT, loc }; + const body: IrStmt[] = []; + const info = lowerer.classes.get(className)!; + for (const f of sShape.fields) { + const declared = info.fields.get(f.name)!; + const read: IrExpr = { + kind: "recordGet", + obj: sRef, + shapeId: srcShapeId, + field: f.name, + type: f.type, + loc, + }; + const lift = typeEquals(declared, f.type) ? null : lowerer.widthLiftPlan(f.type, declared); + const set: IrStmt = { + kind: "fieldSet", + obj: oRef, + className, + field: f.name, + value: lift === null ? read : lowerer.applyWidthLift(lift, read, declared, loc), + loc, + }; + const present = lowerer.recordFieldPresent(sRef, srcShapeId, f.name, loc); + body.push( + present.kind === "recordHas" + ? { kind: "if", cond: present, then: [set], else_: null, loc } + : set, + ); + } + body.push({ kind: "return", value: oRef, loc }); + lowerer.liftedFns.push({ + name: helper, + params: [ + { localId: "o.0", name: "o", type: oT }, + { localId: "s.0", name: "s", type: sT }, + ], + returnType: oT, + locals: [ + { id: "o.0", name: "o", type: oT, mutable: true }, + { id: "s.0", name: "s", type: sT, mutable: true }, + ], + body, + loc, + }); + return helper; +} + /** Interned `%obj.assign.(t, s)` — Object.assign's per-field copy * over signature-free records (every source field lands on a same-named, * same-typed target field — the caller's gate): optional source fields @@ -7410,6 +7503,28 @@ function lowerObjectStaticCall( return { kind: "libCall", fn: "dyn.assign", args: [target, source], type: DYN, loc }; } } + // A CLASS-INSTANCE target and a plain record source whose fields are + // all fields the class declares with the same types + // (`Object.assign(this, { retries: 3 })`): the per-field write into + // the instance, returning it — the instance mutates in place. + if (tProbe?.type.kind === "object" && sProbe?.type.kind === "record") { + const helper = classAssignPlan(lowerer, tProbe.type.className, sProbe.type.shapeId); + if (helper) { + const loc = locOf(call); + const target = lowerer.lowerExpr(call.arguments.nodes[0]!); + const source = lowerer.lowerExpr(call.arguments.nodes[1]!); + if (target.type.kind === "object" && source.type.kind === "record") { + const name = classAssignHelper( + lowerer, + target.type.className, + source.type.shapeId, + loc, + ); + if (name) + return { kind: "call", callee: name, args: [target, source], type: target.type, loc }; + } + } + } if (tProbe?.type.kind === "record" && sProbe?.type.kind === "record") { const tShape = lowerer.shapes.get(tProbe.type.shapeId); const sShape = lowerer.shapes.get(sProbe.type.shapeId); @@ -7500,6 +7615,25 @@ function lowerObjectStaticCall( const srcNode = spread ? argNode.expression : argNode; const rawSource = lowerer.lowerExpr(srcNode); fenceSymbolFieldCopy(lowerer, srcNode, rawSource.type, spread); + // A class instance copied into a fresh object + // (`Object.assign({}, instance)`): the result is a plain + // object holding the instance's own fields, never an + // instance, so a result the checker types as the class has + // no representation (the checked cast would throw). + if ( + rawSource.type.kind === "object" && + freshLiteralTarget && + !ts.isExpressionStatement(call.parent) + ) { + const resultT = lowerer.mapTypeOf(lowerer.typeOf(call)); + if (resultT !== null && resultT.kind === "object") { + lowerer.unsupported( + "SC1090", + srcNode, + `Object.assign copying a '${lowerer.fmt(rawSource.type)}' instance into a fresh object typed as the class (the copy is a plain object holding the instance's own fields, not an instance — build the object literal from the fields you need)`, + ); + } + } const src = lowerer.coerceToExpected(rawSource, DYN); if (src.type.kind !== "dyn") { ok = false; diff --git a/packages/compiler/src/frontend/lowering/lower-server.ts b/packages/compiler/src/frontend/lowering/lower-server.ts index 0e1438fc9..7cdb9ef5d 100644 --- a/packages/compiler/src/frontend/lowering/lower-server.ts +++ b/packages/compiler/src/frontend/lowering/lower-server.ts @@ -2476,6 +2476,20 @@ function lowerNetSocketMethodCall( ? value : lowerer.coerceInto(call, value, declared); } + if (name === "isSessionReused" && args.length === 0) { + // TLSSocket.isSessionReused(): the client never offers a cached + // session (no session resumption, a documented TLS divergence), so + // no handshake resumes one. A plain socket asserted to be a + // TLSSocket answers false too, where Node has no such method. + const receiver = coerceToHandle(lowerer, access.expression, NETSOCKET_T); + return { + kind: "seqExpr", + stmts: [{ kind: "exprStmt", expr: receiver, loc }], + result: boolLit(false, loc), + type: BOOL, + loc, + }; + } if (name === "resetAndDestroy") { // An RST close; a connecting socket resets once it connects. Answers // the socket, Node's chaining. diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6416-tls-session-reused.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6416-tls-session-reused.ts.json new file mode 100644 index 000000000..8b84ce6d3 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6416-tls-session-reused.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6416-tls-session-reused.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6417-object-assign-class-target.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6417-object-assign-class-target.ts.json new file mode 100644 index 000000000..5832b91f9 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6417-object-assign-class-target.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6417-object-assign-class-target.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6418-nullish-assign-index-record.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6418-nullish-assign-index-record.ts.json new file mode 100644 index 000000000..7a030cd19 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/6418-nullish-assign-index-record.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/6418-nullish-assign-index-record.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/object-assign-class-copy.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/object-assign-class-copy.ts.json new file mode 100644 index 000000000..085f01796 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/diagnostics/object-assign-class-copy.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/diagnostics/object-assign-class-copy.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/node-types/tls-session-timings.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/node-types/tls-session-timings.ts.json new file mode 100644 index 000000000..953193649 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/fixtures/node-types/tls-session-timings.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/fixtures/node-types/tls-session-timings.ts" + ], + "diags": [] +} diff --git a/tests/corpus/6416-tls-session-reused.ts b/tests/corpus/6416-tls-session-reused.ts new file mode 100644 index 000000000..a30f4f1b0 --- /dev/null +++ b/tests/corpus/6416-tls-session-reused.ts @@ -0,0 +1,33 @@ +// TLSSocket.isSessionReused() on both ends of a fresh TLS connection, +// before and after the handshake: a first handshake never resumes a +// session, so every answer is false. +import * as tls from "node:tls"; +import { readFileSync } from "node:fs"; + +const cert = readFileSync("tests/fixtures/server/certs/localhost.pem"); +const key = readFileSync("tests/fixtures/server/certs/localhost-key.pem"); +const ca = readFileSync("tests/fixtures/server/certs/ca.pem"); + +let serverLine: string | null = null; +let clientLine: string | null = null; +function both(): void { + if (serverLine === null || clientLine === null) return; + console.log(serverLine); + console.log(clientLine); +} + +const server = tls.createServer({ key, cert }, (socket) => { + serverLine = `server reused ${socket.isSessionReused()}`; + both(); + socket.end("hi"); +}); +server.listen(0, "127.0.0.1", () => { + const port = (server.address() as { port: number }).port; + const client = tls.connect({ port, host: "127.0.0.1", servername: "localhost", ca }, () => { + clientLine = `client reused ${client.isSessionReused()}`; + both(); + }); + console.log("before handshake", client.isSessionReused()); + client.on("data", (d: Buffer) => console.log("data", d.toString())); + client.on("close", () => server.close()); +}); diff --git a/tests/corpus/6417-object-assign-class-target.ts b/tests/corpus/6417-object-assign-class-target.ts new file mode 100644 index 000000000..4c2746693 --- /dev/null +++ b/tests/corpus/6417-object-assign-class-target.ts @@ -0,0 +1,36 @@ +// Object.assign with a class instance as the target and a plain record as +// the source: the record's fields land on the instance's own fields (also +// readonly ones, which only the checker guards), optional source fields +// only when present, and the call answers the instance itself. +class Settings { + retries = 1; + readonly name: string = "default"; + verbose = false; + label?: string; + describe(): string { + return `${this.name}:${this.retries}:${this.verbose}:${this.label ?? "-"}`; + } +} + +const s = new Settings(); +const same = Object.assign(s, { retries: 3, verbose: true }); +console.log(same === s, s.describe()); + +function patch(target: Settings, update: { label?: string }): Settings { + return Object.assign(target, update); +} +console.log(patch(s, { label: "x" }).describe()); +console.log(patch(s, {}).describe()); +console.log(patch(s, { label: undefined }).describe(), Object.assign(s, { retries: 0 }).describe()); + +class Point { + x: number; + y: number; + constructor(x: number, y: number) { + this.x = x; + this.y = y; + } +} +const p = new Point(1, 2); +Object.assign(p, { y: 5 }); +console.log(p.x, p.y, JSON.stringify(p), p instanceof Point); diff --git a/tests/corpus/6418-nullish-assign-index-record.ts b/tests/corpus/6418-nullish-assign-index-record.ts new file mode 100644 index 000000000..97918cd98 --- /dev/null +++ b/tests/corpus/6418-nullish-assign-index-record.ts @@ -0,0 +1,29 @@ +// Short-circuit assignment through a string-keyed index-signature record +// (`timings[phase] ??= value`): a missing key takes the right side, a +// present one keeps its value, and `||=` and `&&=` test truthiness. +class Telemetry { + private readonly timings: Record = {}; + private readonly labels: { [key: string]: string } = {}; + mark(phase: string, at: number): number { + return (this.timings[phase] ??= at); + } + label(key: string, value: string): string { + this.labels[key] ||= value; + this.labels[key] &&= this.labels[key].toUpperCase(); + return this.labels[key]; + } + dump(): string { + return JSON.stringify(this.timings) + " " + JSON.stringify(this.labels); + } +} + +const t = new Telemetry(); +console.log(t.mark("dns", 5), t.mark("tcp", 7), t.mark("dns", 9)); +console.log(t.label("a", "first"), t.label("a", "second"), t.label("", "empty")); +console.log(t.dump()); + +const counts: Record = { zero: 0 }; +const keys = ["zero", "one", "zero"]; +for (const k of keys) counts[k] ??= 10; +for (const k of keys) counts[k] ||= 20; +console.log(JSON.stringify(counts)); diff --git a/tests/diagnostics/object-assign-class-copy.ts b/tests/diagnostics/object-assign-class-copy.ts new file mode 100644 index 000000000..4899306a9 --- /dev/null +++ b/tests/diagnostics/object-assign-class-copy.ts @@ -0,0 +1,11 @@ +// Object.assign copying a class instance into a fresh object: the result +// is a plain object holding the instance's own fields, never an instance, +// so a result the checker types as the class is refused rather than +// failing its checked cast at run time. +class Config { + b = 1; + a = 2; +} +const config = new Config(); +const copy = Object.assign({}, config); +console.log(JSON.stringify(copy), Object.keys(copy).join(",")); diff --git a/tests/fixtures/node-types/tls-session-timings.ts b/tests/fixtures/node-types/tls-session-timings.ts new file mode 100644 index 000000000..7ebb53ce9 --- /dev/null +++ b/tests/fixtures/node-types/tls-session-timings.ts @@ -0,0 +1,31 @@ +// Under @types/node: a connector-style TLS client reads +// isSessionReused() through a TLSSocket assertion and keeps per-phase +// timings in a string-keyed record with `??=`. +import * as net from "node:net"; +import * as tls from "node:tls"; +import { readFileSync } from "node:fs"; + +const cert = readFileSync("tests/fixtures/server/certs/localhost.pem"); +const key = readFileSync("tests/fixtures/server/certs/localhost-key.pem"); +const ca = readFileSync("tests/fixtures/server/certs/ca.pem"); + +const timings: Record = {}; +function mark(phase: string, value: number): void { + timings[phase] ??= value; +} + +const server = tls.createServer({ key, cert }, (socket) => socket.end("ok")); +server.listen(0, "127.0.0.1", () => { + const port = (server.address() as net.AddressInfo).port; + const socket: net.Socket = tls.connect({ port, host: "127.0.0.1", servername: "localhost", ca }, () => { + mark("secure", 1); + mark("secure", 2); + console.log("reused", (socket as tls.TLSSocket).isSessionReused()); + }); + socket.on("data", (d: Buffer) => console.log("data", d.toString())); + socket.on("close", () => { + mark("close", 3); + console.log(JSON.stringify(timings)); + server.close(); + }); +}); diff --git a/tests/harness/__snapshots__/object-assign-class-copy.ts.txt b/tests/harness/__snapshots__/object-assign-class-copy.ts.txt new file mode 100644 index 000000000..ce25cc38f --- /dev/null +++ b/tests/harness/__snapshots__/object-assign-class-copy.ts.txt @@ -0,0 +1,6 @@ +object-assign-class-copy.ts:10:32 - error SC1090: Object.assign copying a 'Config' instance into a fresh object typed as the class (the copy is a plain object holding the instance's own fields, not an instance — build the object literal from the fields you need) is not supported yet + + 9 | const config = new Config(); + 10 | const copy = Object.assign({}, config); + | ^~~~~~ + 11 | console.log(JSON.stringify(copy), Object.keys(copy).join(",")); \ No newline at end of file diff --git a/tests/harness/project-config.test.ts b/tests/harness/project-config.test.ts index 802fbd3e1..e979c772c 100644 --- a/tests/harness/project-config.test.ts +++ b/tests/harness/project-config.test.ts @@ -200,6 +200,27 @@ test("node-types: socket.address and resetAndDestroy under @types/node", async ( expect(comparableStderr(native.stderr)).toBe(node.stderr); }); +test("node-types: TLSSocket.isSessionReused and keyed ??= timings under @types/node", async () => { + const outDir = outDirFor("tls-session-timings"); + const entry = join(nodeTypesDir, "tls-session-timings.ts"); + const result = await compile(entry, { + outPath: join( + outDir, + process.platform === "win32" ? "tls-session-timings.exe" : "tls-session-timings", + ), + outDir, + sanitize, + }); + expect(result.ok, !result.ok ? JSON.stringify(result.diagnostics, null, 2) : "").toBe(true); + if (!result.ok) return; + const [native, node] = await Promise.all([ + execFileAsync(result.binaryPath), + execFileAsync(process.execPath, ["--disable-warning=MODULE_TYPELESS_PACKAGE_JSON", entry]), + ]); + expect(native.stdout).toBe(node.stdout); + expect(comparableStderr(native.stderr)).toBe(node.stderr); +}); + test("node-types: net.SocketAddress and net.BlockList under @types/node", async () => { const outDir = outDirFor("net-blocklist"); const entry = join(nodeTypesDir, "net-blocklist.ts");