mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-10-27 06:43:19 +08:00
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:
co-authored by
Claude Opus 5.5
parent
e1c27176a6
commit
465abda816
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/6450-promise-race-never-entry.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/6451-array-length-fill-spread.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/fixtures/node-types/lookup-and-header-entries.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/fixtures/npm/cases/cjs-exports-var/main.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -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(""));
|
||||
@@ -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(","));
|
||||
});
|
||||
});
|
||||
@@ -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 "));
|
||||
@@ -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
@@ -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
@@ -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
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"name": "exportsvar",
|
||||
"version": "1.0.0",
|
||||
"main": "lib/ip",
|
||||
"types": "index.d.ts"
|
||||
}
|
||||
+8
@@ -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
@@ -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
@@ -0,0 +1,2 @@
|
||||
import { Registry } from './registry';
|
||||
export declare const registry: Registry;
|
||||
+5
@@ -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
@@ -0,0 +1,5 @@
|
||||
export declare class Registry {
|
||||
private counts;
|
||||
static getInstance(): Registry;
|
||||
count(name: string, n: number): number;
|
||||
}
|
||||
+20
@@ -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
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"name": "tscdefault",
|
||||
"version": "1.0.0",
|
||||
"main": "build/index.js",
|
||||
"types": "build/index.d.ts"
|
||||
}
|
||||
@@ -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");
|
||||
|
||||
Reference in New Issue
Block a user