Compile the remaining safe-fetch shapes: Promise<never> race entries, typed Array(n).fill, narrowed header entries, lookup option unions and CommonJS export aliases

- Promise.race admits a Promise<never> entry (the deadline idiom) under
  any result type: it can only reject, so its fulfillment adapter traps.
  A Promise<never> executor's one-parameter resolve validates against the
  void inner.
- Array(n).fill(v) and new Array(n).fill(v), which the checker types as
  any[], take v's type.
- Object.entries/values over an index-signature record cast to a narrower
  value type check each value into the element type instead of refusing.
- dns.LookupAllOptions and the lookup-function option union that holds it
  (LookupOneOptions | LookupAllOptions | number | undefined) ride
  checked-dynamic storage, and dns.lookup passes a cast of a
  checked-dynamic options value through unchanged.
- npm packages: a CommonJS module that names its export object through a
  variable (`var ip = exports; ip.isPrivate = ...`, the ip package) is
  respelled through `exports`, and tsc output that adds a default-export
  object (`exports.default = { metrics: metrics_api_1.metrics }`, the
  @opentelemetry/api index) keeps its named exports linked.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Malte Ubl
2026-10-20 06:36:48 -07:00
co-authored by Claude Opus 5.5
parent e1c27176a6
commit 465abda816
32 changed files with 651 additions and 18 deletions
@@ -55,6 +55,23 @@ export function raceAdapterFor(host: LlvmEmitterContext, from: IrType, to: IrTyp
if (sym) return sym;
sym = `sc_race_${host.resolveThunks.size}`;
host.resolveThunks.set(key, sym);
const toArms = to.kind === "union" ? (host.unionsById.get(to.unionId)?.arms ?? []) : [];
if (from.kind === "void" && !toArms.some((a) => a.kind === "undefinedT")) {
// A Promise<never> entry (the frontend admits void entries without an
// undefined result arm only for those): it can only reject, so its
// fulfillment adapter never runs.
host.declare(`declare void @scr_trap(ptr)`);
const msg = host.cstr("scriptc: a Promise<never> race entry fulfilled\n");
host.resolveThunkDefs.push(
`define internal void @${sym}(ptr %dst, ptr %src) ${FN_ATTRS} { ; race ${key}`,
`entry:`,
` call void @scr_trap(ptr ${msg})`,
` unreachable`,
`}`,
``,
);
return sym;
}
if (to.kind !== "union")
throw new InternalCompilerError("llvm emitter bug: race adapter to a non-union");
const toDef = host.unionsById.get(to.unionId);
@@ -74,6 +74,17 @@ export function lowerPromiseStaticCall(
}
return resultArms.some((a) => typeEquals(a, inner));
};
// A Promise<never> entry (the deadline idiom: `new Promise<never>((_,
// reject) => setTimeout(reject, ms))`) can only reject, so it races
// under any result type; its inner type maps to void.
const neverEntry = (el: ts.Expression): boolean => {
const t = lowerer.typeOf(el);
const arg =
t.getSymbol()?.name === "Promise"
? lowerer.checker.getTypeArguments(t as ts.TypeReference)[0]
: undefined;
return arg !== undefined && (arg.flags & ts.TypeFlags.Never) !== 0;
};
const entries = argNode.elements.nodes.map((el) => {
const entry = lowerer.lowerExpr(el);
if (entry.type.kind !== "promise") {
@@ -83,7 +94,7 @@ export function lowerPromiseStaticCall(
"JS would resolve plain values — wrap them: new Promise((r) => r(v))",
);
}
if (!compatible(entry.type.inner)) {
if (!compatible(entry.type.inner) && !(entry.type.inner.kind === "void" && neverEntry(el))) {
lowerer.unsupported(
"SC1090",
el,
@@ -265,6 +265,22 @@ function appendWidenedArrayValues(
}
/** The Array constructor's element and count forms, shared by calls and new. */
/** The value of `.fill(v)` when `expr` is that call's receiver. */
function fillArgumentOfReceiver(expr: ts.Expression): ts.Expression | null {
let receiver: ts.Node = expr;
let parent = expr.parent;
while (ts.isParenthesizedExpression(parent)) {
receiver = parent;
parent = parent.parent;
}
if (!ts.isPropertyAccessExpression(parent) || parent.expression !== receiver) return null;
if (parent.name.text !== "fill" || parent.questionDotToken) return null;
const call = parent.parent;
if (!ts.isCallExpression(call) || call.expression !== parent) return null;
const value = call.arguments.nodes[0];
return value !== undefined && !ts.isSpreadElement(value) ? value : null;
}
export function lowerArrayConstructor(
lowerer: Lowerer,
expr: ts.CallExpression | ts.NewExpression,
@@ -295,6 +311,15 @@ export function lowerArrayConstructor(
const contextual = lowerer.checker.getContextualType(expr);
if (contextual) result = lowerer.mapTypeOf(contextual);
}
// `Array(n).fill(v)`: the checker types the length-constructed array as
// any[], but fill gives every element v's type.
if (result?.kind !== "array") {
const fillValue = fillArgumentOfReceiver(expr);
const elem = fillValue
? lowerer.mapTypeOf(lowerer.checker.getBaseTypeOfLiteralType(lowerer.typeOf(fillValue)))
: null;
if (elem !== null && isSupportedArrayElem(elem)) result = arrayOf(elem);
}
if (isJsSourceFile(expr.getSourceFile()) && (result === null || result.kind === "dyn"))
result = arrayOf(DYN);
if (result?.kind !== "array" || !isSupportedArrayElem(result.elem)) {
@@ -148,12 +148,29 @@ export function objectIterOverIndexShape(
}
// The overflow value must surface as the element type (identity, an
// arm of a union element, or a dyn element over a dyn signature).
// A cast that narrows the signature's union (`Object.entries(headers as
// Record<string, string | string[] | undefined>)` over a signature that
// also holds numbers): each overflow value checks into the element type,
// so a value of a dropped arm throws instead of mistyping.
let ivNarrows = false;
if (valueT) {
const ivSurfaces =
typeEquals(iv, valueT) ||
(valueT.kind === "union" && lowerer.armTag(valueT.unionId, iv) >= 0) ||
(valueT.kind === "dyn" && iv.kind === "dyn");
if (!ivSurfaces) {
if (
!ivSurfaces &&
iv.kind === "union" &&
lowerer.dynConvertible(iv) &&
lowerer.dynConvertible(valueT)
) {
const ivUnion = iv;
const narrowArms =
valueT.kind === "union" ? (lowerer.unions.get(valueT.unionId)?.arms ?? []) : [valueT];
ivNarrows =
narrowArms.length > 0 && narrowArms.every((a) => lowerer.armTag(ivUnion.unionId, a) >= 0);
}
if (!ivSurfaces && !ivNarrows) {
lowerer.unsupported(
"SC1090",
node,
@@ -267,6 +284,21 @@ export function objectIterOverIndexShape(
}
}
}
// A cast that narrows the field's type (the element type keeps
// some of its arms): the value checks into the element type, so a
// dropped arm throws instead of mistyping.
if (lowerer.dynConvertible(f.type) && lowerer.dynConvertible(valueT)) {
const armsOf = (t: IrType): IrType[] =>
t.kind === "union" ? (lowerer.unions.get(t.unionId)?.arms ?? []) : [t];
const target = armsOf(valueT);
if (armsOf(f.type).some((a) => target.some((b) => typeEquals(a, b))))
return {
kind: "dynCheck",
value: { kind: "dynFrom", value: raw, type: DYN, loc },
type: valueT,
loc,
};
}
return null;
};
const pushedOf = (element: IrExpr): IrExpr =>
@@ -337,17 +369,37 @@ export function objectIterOverIndexShape(
const ksT = arrayOf(STRING);
const ksRef = varRef("ks.0", ksT, loc);
const kRef = varRef("k.0", STRING, loc);
const readValue: IrExpr | null = valueT
? {
kind: "recordKeyGet",
obj: rRef,
shapeId: argIr.shapeId,
key: kRef,
overflowOnly: true,
type: valueT,
loc,
}
: null;
const readValue: IrExpr | null = !valueT
? null
: ivNarrows
? {
kind: "dynCheck",
value: {
kind: "dynFrom",
value: {
kind: "recordKeyGet",
obj: rRef,
shapeId: argIr.shapeId,
key: kRef,
overflowOnly: true,
type: iv,
loc,
},
type: DYN,
loc,
},
type: valueT,
loc,
}
: {
kind: "recordKeyGet",
obj: rRef,
shapeId: argIr.shapeId,
key: kRef,
overflowOnly: true,
type: valueT,
loc,
};
const loopPushed: IrExpr =
member === "keys"
? kRef
@@ -290,8 +290,17 @@ function lowerDnsModuleCall(
loc: SrcLoc,
): IrExpr {
const args = expr.arguments.nodes;
const dyn = (node: ts.Expression): IrExpr =>
lowerer.coerceToExpected(lowerer.lowerExpr(node), DYN);
const dyn = (node: ts.Expression): IrExpr => {
// `opts as dns.LookupOneOptions` over a checked-dynamic value (the
// overload-satisfying cast of a pass-through lookup): the cast changes
// no value, so the options reach the runtime as they are.
let inner = node;
while (ts.isAsExpression(inner) || ts.isParenthesizedExpression(inner))
inner = inner.expression;
if (inner !== node && lowerer.mapTypeOf(lowerer.typeOf(inner))?.kind === "dyn")
return lowerer.lowerExpr(inner);
return lowerer.coerceToExpected(lowerer.lowerExpr(node), DYN);
};
// A callback whose second parameter is typed (string, LookupAddress[])
// cannot take the undefined Node passes after an error.
const typedResult = (node: ts.Expression): IrExpr => {
@@ -0,0 +1,198 @@
/* CommonJS modules that name their export object through a variable:
*
* var ip = exports;
* ip.isPrivate = function (addr) { … };
* ip.isPublic = function (addr) { return !ip.isPrivate(addr); };
*
* `ip` IS the export object, so every member read or write through it is
* an export. The pass respells those accesses as `exports.<name>`, the
* form the static export protocol models, and drops the alias declaration
* (the export object has no storage of its own to bind).
*
* Exact only while the variable is nothing but that alias: one top-level
* declaration initialized with the CommonJS `exports` global, never
* reassigned, never declared again anywhere in the file (so no nested
* binding shadows it), and used only as the object of member accesses.
* `exports` itself must be the global (never declared in the file) and is
* never reassigned. Other shapes answer null and keep their existing
* treatment. */
import * as ts from "./ts7/syntax.js";
function unparen(e: ts.Expression): ts.Expression {
let x = e;
while (ts.isParenthesizedExpression(x)) x = x.expression;
return x;
}
function isDeclarationName(id: ts.Identifier): boolean {
const p = id.parent;
return (
((ts.isVariableDeclaration(p) ||
ts.isParameter(p) ||
ts.isBindingElement(p) ||
ts.isFunctionDeclaration(p) ||
ts.isFunctionExpression(p) ||
ts.isClassDeclaration(p) ||
ts.isClassExpression(p)) &&
p.name === id) ||
(ts.isPropertyAccessExpression(p) && p.name === id) ||
((ts.isPropertyAssignment(p) || ts.isMethodDeclaration(p) || ts.isPropertyDeclaration(p)) &&
p.name === id)
);
}
function isFunctionScope(n: ts.Node): boolean {
return (
ts.isFunctionDeclaration(n) ||
ts.isFunctionExpression(n) ||
ts.isArrowFunction(n) ||
ts.isMethodDeclaration(n) ||
ts.isGetAccessorDeclaration(n) ||
ts.isSetAccessorDeclaration(n) ||
ts.isConstructorDeclaration(n)
);
}
function bindsName(name: ts.BindingName | undefined, text: string): boolean {
if (name === undefined) return false;
if (ts.isIdentifier(name)) return name.text === text;
for (const element of name.elements.nodes) {
if (!ts.isOmittedExpression(element) && bindsName(element.name, text)) return true;
}
return false;
}
/** True when `scope` (a function, or a block or catch clause) declares
* `text` itself, so references inside it name that binding instead of the
* module-level alias: a parameter, a var/let/const or a function or class
* declaration in a function (nested functions excluded), a let/const or
* class in a block, a catch binding. */
function shadows(scope: ts.Node, text: string): boolean {
if (isFunctionScope(scope)) {
const fn = scope as ts.SignatureDeclaration;
if (fn.parameters.nodes.some((p) => bindsName(p.name, text))) return true;
if (
(ts.isFunctionExpression(scope) || ts.isClassExpression(scope)) &&
scope.name?.text === text
)
return true;
} else if (ts.isCatchClause(scope)) {
if (bindsName(scope.variableDeclaration?.name, text)) return true;
} else if (!ts.isBlock(scope)) {
return false;
}
const functionScope = isFunctionScope(scope);
let found = false;
ts.walkPreorder(scope, (n) => {
if (found) return "stop";
if (n === scope) return undefined;
if (isFunctionScope(n)) {
if (ts.isFunctionDeclaration(n) && n.name?.text === text) found = true;
return "skip";
}
if (!functionScope && ts.isBlock(n)) return "skip"; /* its own scope */
if (ts.isVariableDeclaration(n) && bindsName(n.name, text)) {
const list = n.parent;
const blockScoped =
ts.isVariableDeclarationList(list) && (list.flags & ts.NodeFlags.BlockScoped) !== 0;
if (functionScope || blockScoped) found = true;
}
if (ts.isClassDeclaration(n) && n.name?.text === text) found = true;
return undefined;
});
return found;
}
export function normalizeCjsExportsVar(sf: ts.SourceFile): string | null {
const source = sf.text;
if (!/\b(?:var|let|const)\s+[A-Za-z_$][\w$]*\s*=\s*exports\s*[;,\n]/.test(source)) return null;
if (!sf.fileName.endsWith(".cjs") && ts.isExternalModule(sf)) return null;
let alias: { name: string; stmt: ts.VariableStatement } | null = null;
for (const stmt of sf.statements.nodes) {
if (!ts.isVariableStatement(stmt)) continue;
const decls = stmt.declarationList.declarations.nodes;
if (decls.length !== 1) continue;
const decl = decls[0]!;
const init = decl.initializer !== undefined ? unparen(decl.initializer) : undefined;
if (
ts.isIdentifier(decl.name) &&
init !== undefined &&
ts.isIdentifier(init) &&
init.text === "exports"
) {
if (alias !== null) return null; /* two aliases: keep the plain treatment */
alias = { name: decl.name.text, stmt };
}
}
if (alias === null) return null;
const name = alias.name;
const aliasDecl = alias.stmt.declarationList.declarations.nodes[0]!;
const edits: { start: number; end: number; text: string }[] = [];
let ok = true;
ts.walkPreorder(sf, (n) => {
if (!ok) return "stop";
if (n.kind >= ts.SyntaxKind.FirstJSDocNode && n.kind <= ts.SyntaxKind.LastJSDocNode)
return "skip";
if (
ts.isBinaryExpression(n) &&
n.operatorToken.kind === ts.SyntaxKind.EqualsToken &&
ts.isPropertyAccessExpression(unparen(n.left)) &&
(unparen(n.left) as ts.PropertyAccessExpression).name.text === "exports" &&
ts.isIdentifier((unparen(n.left) as ts.PropertyAccessExpression).expression) &&
((unparen(n.left) as ts.PropertyAccessExpression).expression as ts.Identifier).text ===
"module"
) {
ok = false; /* module.exports replaced: the alias no longer names the exports */
return "stop";
}
// A scope that declares the alias's name again: its references name
// that binding, not the export object.
if (
n !== sf &&
(isFunctionScope(n) || ts.isBlock(n) || ts.isCatchClause(n)) &&
shadows(n, name)
)
return "skip";
if (!ts.isIdentifier(n)) return undefined;
if (n.text === "exports" && n !== aliasDecl.initializer) {
// `exports` declared in the file, or the export object replaced
if (isDeclarationName(n) && !ts.isPropertyAccessExpression(n.parent)) ok = false;
const p = n.parent;
if (
ts.isBinaryExpression(p) &&
p.left === n &&
p.operatorToken.kind === ts.SyntaxKind.EqualsToken
)
ok = false;
return undefined;
}
if (n.text !== name) return undefined;
if (n === aliasDecl.name) return undefined;
const p = n.parent;
if (ts.isPropertyAccessExpression(p) && p.name === n) return undefined;
if ((ts.isPropertyAssignment(p) || ts.isMethodDeclaration(p)) && p.name === n) return undefined;
if (isDeclarationName(n)) {
ok = false; /* declared again (a shadowing binding): not a plain alias */
return undefined;
}
if (
(ts.isPropertyAccessExpression(p) || ts.isElementAccessExpression(p)) &&
p.expression === n
) {
edits.push({ start: n.getStart(sf), end: n.getEnd(), text: "exports" });
return undefined;
}
ok = false; /* used as a value, assigned, or passed along */
return undefined;
});
if (!ok || edits.length === 0) return null;
edits.push({ start: alias.stmt.getStart(sf), end: alias.stmt.getEnd(), text: "" });
edits.sort((a, b) => a.start - b.start);
let out = "";
let at = 0;
for (const e of edits) {
out += source.slice(at, e.start) + e.text;
at = e.end;
}
return out + source.slice(at);
}
@@ -158,6 +158,26 @@ function chaseValue(e: ts.Expression): { kind: "ident" | "hoist"; text: string }
return null;
}
/** An object literal of side-effect-free member values (`exports.default
* = { context: context_api_1.context, metrics: metrics_api_1.metrics }`,
* tsc's default-export object): the tail const can build it once, like a
* chased value. Null for any other shape (spreads, methods, accessors,
* computed keys, or a value chaseValue rejects). */
function chaseObjectLiteral(e: ts.Expression): { kind: "hoist"; text: string } | null {
let cur: ts.Expression = e;
while (ts.isParenthesizedExpression(cur)) cur = cur.expression;
if (!ts.isObjectLiteralExpression(cur)) return null;
for (const prop of cur.properties.nodes) {
if (
!ts.isPropertyAssignment(prop) ||
!(ts.isIdentifier(prop.name) || ts.isStringLiteral(prop.name)) ||
(chaseValue(prop.initializer) === null && chaseObjectLiteral(prop.initializer) === null)
)
return null;
}
return { kind: "hoist", text: cur.getText() };
}
/** The getter body's returned expression for the two descriptor getter
* spellings (`get() { return X }` / `get: function () { return X }`) and
* the arrow forms the esbuild table uses (`() => X`, `() => { return X }`). */
@@ -792,7 +812,7 @@ export function rewriteBundlerCjsSyntax(
// anything else keeps the file untouched (the offender path owns
// it: neutralizing would drop an evaluation, keeping it meets the
// lowering's discard fence at load).
const chased = chaseValue(cur);
const chased = chaseValue(cur) ?? chaseObjectLiteral(cur);
if (chased === null) {
return toEsm.pads.length > 0 ? { degrade: TO_ESM_MIXED_DEGRADE } : null;
}
+5 -1
View File
@@ -407,6 +407,10 @@ export function npmStaticFsShadow(services: FrontendServices): NpmStaticFsShadow
for (const chunk of split.chunks) setVirtualSource(chunk.path, chunk.source);
original = split.text;
}
// A module that names its export object through a variable reaches
// its members through `exports`.
const exportsVar = original === null ? null : services.cjsExportsVar(original, path);
if (exportsVar !== null) original = exportsVar;
if (original !== null) {
const namepaths = services.jsDocNamepaths(path, original);
const source = namepaths ?? original;
@@ -441,7 +445,7 @@ export function npmStaticFsShadow(services: FrontendServices): NpmStaticFsShadow
findWidened?.text ??
classFields?.text ??
namepaths ??
(split !== null ? original : null);
(split !== null || exportsVar !== null ? original : null);
}
}
} catch {
@@ -8,6 +8,7 @@ import {
type BundledChunkSplit,
} from "./npm-static-bundled-chunks.js";
import * as syntax from "./npm-static-declaration-syntax.js";
import { normalizeCjsExportsVar } from "./npm-static-cjs-exports-var.js";
import type { SourceFile } from "./ts7/ast-types.js";
/** Evaluate an isolated, self-contained TypeScript callback under a finite
@@ -76,6 +77,14 @@ export class FrontendServices {
if (!mayBundleChunks(source)) return null;
return splitBundledChunks(this.parse(path, source, "js"), path);
}
/** A CommonJS module that names its export object through a variable
* (`var ip = exports`), its member accesses respelled through `exports`
* (null: none). */
cjsExportsVar(source: string, path: string): string | null {
this.ensureOpen();
if (!source.includes("exports")) return null;
return normalizeCjsExportsVar(this.parse(path, source, "js"));
}
nullableClassFields(path: string, source: string): syntax.NpmStaticOverloadRewrite | null {
return syntax.applyNpmStaticNullableClassFields(this.parse(path, source, "js"), source);
}
@@ -1355,6 +1355,26 @@ function isWorkerHandleSymbol(
/** The `{ message: any }` object type (or its union with undefined) that
* worker_threads declares as receiveMessageOnPort's result. */
/** A union of dns lookup option bags with numbers and nullish values that
* includes LookupAllOptions (whose `all: true` arm has no record form). */
function isLookupOptionsUnion(type: ts.Type, ctx: Pick<TypeMapperCtx, "isStdlibFile">): boolean {
if (!type.isUnionType()) return false;
let all = false;
for (const member of ts.constituentTypes(type)) {
if (member.flags & (ts.TypeFlags.NumberLike | ts.TypeFlags.Null | ts.TypeFlags.Undefined))
continue;
const symbol = member.getSymbol();
if (
symbol === undefined ||
!/^Lookup(One|All)?Options$/.test(symbol.name) ||
!symbol.declarations.some((d) => ctx.isStdlibFile(d.getSourceFile()))
)
return false;
if (symbol.name === "LookupAllOptions") all = true;
}
return all;
}
function isReceiveMessageResult(
type: ts.Type,
ctx: Pick<TypeMapperCtx, "checker" | "isStdlibFile">,
@@ -1545,6 +1565,10 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null {
// worker_threads.receiveMessageOnPort's `{ message: any } | undefined`:
// the runtime's { message } object, a checked-dynamic value.
if (isReceiveMessageResult(type, ctx)) return DYN;
// The lookup-function option union (`LookupOneOptions | LookupAllOptions
// | number | undefined`): with a LookupAllOptions arm it is an option bag
// dns.lookup reads at run time, so it rides checked-dynamic storage.
if (isLookupOptionsUnion(type, ctx)) return DYN;
// @types/node spells child_process.ExecFileException as an intersection
// type alias over two Error-derived metadata interfaces. The value is
// still one ordinary Error; recognize the owned alias before generic
@@ -2511,6 +2535,11 @@ function mapTypeInner(type: ts.Type, ctx: TypeMapperCtx): IrType | null {
// spawn consumes a presence-sensitive option bag, including optional
// stdio and env values. Keep it checked-dynamic for native normalization.
if (isStdlibInterface("SpawnOptions")) return DYN;
// dns.LookupAllOptions (`all: true`, the overload whose callback takes
// an address list): a lookup option bag dns.lookup reads at run time.
// It rides checked-dynamic storage, so the lookup function type that
// takes `LookupOneOptions | LookupAllOptions | number` compiles.
if (isStdlibInterface("LookupAllOptions")) return DYN;
// http.RequestOptions / https.RequestOptions as a declared type: an
// option record whose members include functions, agents and signals.
// Values ride checked-dynamic storage, like SpawnOptions.
+4 -1
View File
@@ -6200,7 +6200,10 @@ function validateFunction(
if (expectsArg && (rp.params.length !== 1 || !typeEquals(rp.params[0]!, inner))) {
err(`newPromise resolve param must be (${inner.kind}) => void`, e.loc);
}
if (!expectsArg && rp.params.length !== 0) {
// A Promise<never> executor types resolve as `(value: never) =>
// void` (one parameter over a void inner): the void resolve never
// reads an argument, and a never value cannot be produced.
if (!expectsArg && rp.params.length > 1) {
err("newPromise<void> resolve takes no argument", e.loc);
}
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/6450-promise-race-never-entry.ts"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/6451-array-length-fill-spread.ts"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/fixtures/node-types/lookup-and-header-entries.ts"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/fixtures/npm/cases/cjs-exports-var/main.ts"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/fixtures/npm/cases/tsc-default-object/main.ts"
],
"diags": []
}
@@ -0,0 +1,22 @@
// Promise.race with a Promise<never> deadline entry under a typed result:
// the never entry can only reject, so it races with any result type. A
// read that settles first wins; a deadline that fires first rejects.
class Deadline extends Error {}
async function readWithDeadline(read: () => Promise<{ done: boolean; value?: Uint8Array }>, ms: number): Promise<{ done: boolean; value?: Uint8Array }> {
let timer: ReturnType<typeof setTimeout> | undefined;
try {
return await Promise.race([
read(),
new Promise<never>((_, reject) => {
timer = setTimeout(() => reject(new Deadline()), ms);
}),
]);
} catch (error) {
if (error instanceof Deadline) return { done: true };
throw error;
} finally {
if (timer !== undefined) clearTimeout(timer);
}
}
readWithDeadline(async () => ({ done: false, value: new Uint8Array([1, 2]) }), 1000).then((r) => console.log(r.done, r.value?.length));
readWithDeadline(() => new Promise((res) => setTimeout(() => res({ done: false }), 200)), 20).then((r) => console.log("deadline", r.done));
@@ -0,0 +1,15 @@
// Array(n).fill(v): the checker types the length-constructed array as
// any[], but fill gives every element v's type, so the array (and an
// array literal spreading it) holds strings.
function expand(head: string, tail: string): number[] | null {
const headParts = head ? head.split(":") : [];
const tailParts = tail ? tail.split(":") : [];
const missing = 8 - headParts.length - tailParts.length;
if (missing < 0) return null;
const all = [...headParts, ...Array(missing).fill("0"), ...tailParts];
if (all.length !== 8) return null;
return all.map((part) => Number.parseInt(part || "0", 16));
}
console.log(JSON.stringify(expand("2001:db8", "1")), JSON.stringify(expand("fe80", "")));
const pad: string[] = Array(3).fill("-");
console.log(pad.join(""), [..."ab", ...new Array(2).fill("c")].join(""));
+47
View File
@@ -0,0 +1,47 @@
// A dns lookup function whose options parameter is Node's lookup-option
// union (`LookupOneOptions | LookupAllOptions | number | undefined`) passes
// the options to dns.lookup as they are, and Object.entries over a header
// record cast to a narrower value type reads every entry.
import * as dns from "node:dns";
import type { OutgoingHttpHeaders } from "node:http";
type LookupCallback = (
err: NodeJS.ErrnoException | null,
address: string | dns.LookupAddress[],
family?: number,
) => void;
function createLookup(): (
hostname: string,
opts: dns.LookupOneOptions | dns.LookupAllOptions | number | undefined,
callback: LookupCallback,
) => void {
return function lookup(hostname, opts, callback) {
dns.lookup(hostname, opts as dns.LookupOneOptions, (err, address, family) => {
if (err) return callback(err, "", 0);
callback(null, address, family);
});
};
}
type HeadersLike = Headers | [string, string][] | OutgoingHttpHeaders;
function flatten(input: HeadersLike | undefined): string[] {
const out: string[] = [];
if (!input || input instanceof Headers || Array.isArray(input)) return out;
for (const [name, value] of Object.entries(input as Record<string, string | string[] | undefined>)) {
if (value === undefined) continue;
if (Array.isArray(value)) for (const v of value) out.push(`${name}=${v}`);
else out.push(`${name}=${value}`);
}
return out;
}
const lookup = createLookup();
lookup("localhost", { family: 4 }, (err, address, family) => {
console.log("one", err, address, family);
lookup("127.0.0.1", 4, (err3, address3, family3) => {
console.log("family", err3, address3, family3);
console.log(flatten({ accept: "text/plain", "x-list": ["a", "b"], "x-skip": undefined }).join(","));
});
});
+12
View File
@@ -0,0 +1,12 @@
// A CommonJS module that names its export object through a variable
// (`var ip = exports; ip.isPrivate = function …`), the ip package's
// shape, imported by default as Node does: its members are the exports,
// including the ones it calls on itself through the alias.
import ip from "exportsvar";
for (const addr of ["10.0.0.1", "192.168.1.20", "127.0.0.1", "::1", "8.8.8.8"]) {
console.log(addr, ip.isPrivate(addr), ip.isLoopback(addr), ip.isPublic(addr));
}
console.log(ip.toLong("192.168.1.1"));
// Members whose parameter shadows the alias name.
console.log(ip.isV4Format("1.2.3.4"), ip.isV4Format("::1"), ip.normalize(" FE80::1 "));
+7
View File
@@ -0,0 +1,7 @@
// tsc's CommonJS output for a module with named re-exports AND a default
// export object (`exports.default = { registry: registry_api_1.registry }`,
// the @opentelemetry/api index shape): the named exports still link, read
// through their defineProperty getters.
import { registry, VERSION } from "tscdefault";
console.log(VERSION, registry.count("a", 2), registry.count("a", 3), registry.count("b", 1));
+6
View File
@@ -0,0 +1,6 @@
export function isPrivate(addr: string): boolean;
export function isLoopback(addr: string): boolean;
export function isPublic(addr: string): boolean;
export function toLong(addr: string): number;
export function isV4Format(ip: string): boolean;
export function normalize(ip: string): string;
+35
View File
@@ -0,0 +1,35 @@
'use strict';
var ip = exports;
ip.isPrivate = function(addr) {
return /^(::f{4}:)?10\.([0-9]{1,3})\.([0-9]{1,3})\.([0-9]{1,3})$/i.test(addr) ||
/^(::f{4}:)?192\.168\.([0-9]{1,3})\.([0-9]{1,3})$/i.test(addr);
};
ip.isLoopback = function(addr) {
return /^(::f{4}:)?127\.([0-9]{1,3})\.([0-9]{1,3})\.([0-9]{1,3})/.test(addr) ||
/^::1$/.test(addr);
};
ip.isPublic = function(addr) {
return !ip.isPrivate(addr) && !ip.isLoopback(addr);
};
ip.toLong = function(addr) {
var n = 0;
addr.split('.').forEach(function(octet) {
n <<= 8;
n += parseInt(octet, 10);
});
return n >>> 0;
};
ip.isV4Format = function(ip) {
return /^(\d{1,3}\.){3,3}\d{1,3}$/.test(ip);
};
ip.normalize = function(ip) {
var trimmed = ip.trim();
return trimmed.toLowerCase();
};
+6
View File
@@ -0,0 +1,6 @@
{
"name": "exportsvar",
"version": "1.0.0",
"main": "lib/ip",
"types": "index.d.ts"
}
+8
View File
@@ -0,0 +1,8 @@
import { registry } from './registry-api';
export { registry };
export declare const VERSION = "1.0.0";
declare const _default: {
registry: import("./registry").Registry;
version: string;
};
export default _default;
+11
View File
@@ -0,0 +1,11 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.VERSION = exports.registry = void 0;
const registry_api_1 = require("./registry-api");
Object.defineProperty(exports, "registry", { enumerable: true, get: function () { return registry_api_1.registry; } });
exports.VERSION = "1.0.0";
// Default export.
exports.default = {
registry: registry_api_1.registry,
version: "1.0.0",
};
+2
View File
@@ -0,0 +1,2 @@
import { Registry } from './registry';
export declare const registry: Registry;
+5
View File
@@ -0,0 +1,5 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.registry = void 0;
const registry_1 = require("./registry");
exports.registry = registry_1.Registry.getInstance();
+5
View File
@@ -0,0 +1,5 @@
export declare class Registry {
private counts;
static getInstance(): Registry;
count(name: string, n: number): number;
}
+20
View File
@@ -0,0 +1,20 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.Registry = void 0;
class Registry {
constructor() {
this.counts = new Map();
}
static getInstance() {
if (!this._instance) {
this._instance = new Registry();
}
return this._instance;
}
count(name, n) {
const next = (this.counts.get(name) ?? 0) + n;
this.counts.set(name, next);
return next;
}
}
exports.Registry = Registry;
+6
View File
@@ -0,0 +1,6 @@
{
"name": "tscdefault",
"version": "1.0.0",
"main": "build/index.js",
"types": "build/index.d.ts"
}
+19
View File
@@ -443,6 +443,25 @@ test("node-types: util.promisify over typed callback-last functions settles like
expect(comparableStderr(native.stderr)).toBe(node.stderr);
});
test("node-types: lookup-option unions and narrowed header-record entries under @types/node", async () => {
const outDir = outDirFor("lookup-and-header-entries");
const entry = join(nodeTypesDir, "lookup-and-header-entries.ts");
const result = await compile(entry, {
outPath: join(outDir, "lookup-and-header-entries"),
outDir,
sanitize,
});
expect(result.ok, !result.ok ? JSON.stringify(result.diagnostics, null, 2) : "").toBe(true);
if (!result.ok) return;
const native = await execFileAsync(result.binaryPath);
const node = await 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: stream subclasses extend the @types/node stream classes", async () => {
const outDir = outDirFor("stream-subclass-hooks");
const entry = join(nodeTypesDir, "stream-subclass-hooks.ts");