mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-10-25 06:24:27 +08:00
Close Next.js proxy gaps: fetch header and URL-error parity, JSON undefined, and twelve compile gaps (#1341)
This commit is contained in:
@@ -316,6 +316,7 @@ export const LIB_FN_SYMS: Record<string, string> = {
|
||||
"date.getUTCDate": "scr_date_get_date_utc",
|
||||
"date.getDay": "scr_date_get_day_local",
|
||||
"date.getUTCDay": "scr_date_get_day_utc",
|
||||
"date.toUTCString": "scr_date_to_utc_string",
|
||||
"date.getHours": "scr_date_get_hours_local",
|
||||
"date.getUTCHours": "scr_date_get_hours_utc",
|
||||
"date.getMinutes": "scr_date_get_minutes_local",
|
||||
@@ -820,6 +821,7 @@ export const LIB_FN_SYMS: Record<string, string> = {
|
||||
// ambient-this read never throw. The fs dyn read is the sync-fs story.
|
||||
"fetch.start": "scr_fetch_static",
|
||||
"fetch.responseNew": "scr_fetch_response_new",
|
||||
"fetch.responseJsonStatic": "scr_fetch_response_json_static",
|
||||
"fetch.input": "scr_fetch_input",
|
||||
"abort.controllerNew": "scr_fetch_abort_controller_new",
|
||||
"event.targetNew": "scr_event_target_new",
|
||||
|
||||
@@ -518,6 +518,15 @@ export const NODE24_FETCH_COMPAT_PROFILE = {
|
||||
evidence: [fixture("static")],
|
||||
}),
|
||||
),
|
||||
{
|
||||
id: "stdlib.response.static.json",
|
||||
name: "Response.json",
|
||||
kind: "static-method",
|
||||
facets: ["webidl-conversion", "error-shape"],
|
||||
scope:
|
||||
"data that crosses the checked-dynamic boundary, serialized with JSON.stringify (toJSON hooks included); ResponseInit as for the constructor",
|
||||
evidence: [fixture("static-response-json")],
|
||||
},
|
||||
...["json", "text", "bytes", "arrayBuffer"].map((member): FetchCompatOperation => ({
|
||||
id: `stdlib.response.${member}`,
|
||||
name: `Response.${member}`,
|
||||
@@ -681,7 +690,8 @@ export const NODE24_FETCH_COMPAT_PROFILE = {
|
||||
),
|
||||
|
||||
staticEntry("stdlib.response.constructor", "Response", "constructor", "constructor"),
|
||||
...["error", "json", "redirect"].map((member) =>
|
||||
staticEntry("stdlib.response.static.json", "Response", "json", "static"),
|
||||
...["error", "redirect"].map((member) =>
|
||||
unsupportedEntry(
|
||||
`stdlib.response.static.${member}`,
|
||||
"Response",
|
||||
|
||||
@@ -34,7 +34,7 @@ const DATE_GETTER_FNS: Readonly<Record<string, IrLibFn>> = {
|
||||
};
|
||||
|
||||
const DATE_METHOD_HINT =
|
||||
"getTime(), valueOf(), toISOString(), toJSON(), the local/UTC calendar getters, and getTimezoneOffset() are supported; Date setters and locale/string formatters have no lowering";
|
||||
"getTime(), valueOf(), toISOString(), toUTCString(), toJSON(), the local/UTC calendar getters, and getTimezoneOffset() are supported; Date setters and locale/string formatters have no lowering";
|
||||
|
||||
/** The Date slice: statics plus read-only Date values backed by one
|
||||
* TimeClip'd millisecond scalar. Construction lives in lowerNew;
|
||||
@@ -113,6 +113,9 @@ export function lowerDateCall(
|
||||
if (name === "toISOString") {
|
||||
return { kind: "libCall", fn: "date.toISOStringValue", args: [receiver], type: STRING, loc };
|
||||
}
|
||||
if (name === "toUTCString") {
|
||||
return { kind: "libCall", fn: "date.toUTCString", args: [receiver], type: STRING, loc };
|
||||
}
|
||||
const fn = own(DATE_GETTER_FNS, name);
|
||||
if (fn !== undefined) {
|
||||
return { kind: "libCall", fn, args: [receiver], type: F64, loc };
|
||||
|
||||
@@ -19,6 +19,7 @@ import {
|
||||
isUnitType,
|
||||
} from "../../../ir/ir.js";
|
||||
import { optionalStringTags } from "./arguments.js";
|
||||
import { constituentTypes } from "../../ts7/checker.js";
|
||||
|
||||
/** JSON parse produces checked-dynamic data; a native reviver walks it
|
||||
* bottom-up before any checked cast. Stringify without a callback keeps
|
||||
@@ -152,9 +153,18 @@ export function lowerJsonMethodCall(
|
||||
}
|
||||
const optionalString = lowerOptionalStringifyRoot(lowerer, value, indent, loc);
|
||||
if (optionalString) return optionalString;
|
||||
// A program class instance whose class (or a base) declares toJSON:
|
||||
// JSON.stringify serializes whatever toJSON(key) returns, so the
|
||||
// instance crosses into the runtime walker, which calls the hook
|
||||
// through the checked-dynamic class view.
|
||||
const toJsonClass =
|
||||
value.type.kind === "object" &&
|
||||
lowerer.findMethodOn(lowerer.classes.get(value.type.className) ?? null, "toJSON") !== null &&
|
||||
lowerer.dynConvertible(value.type);
|
||||
if (
|
||||
value.type.kind === "url" ||
|
||||
value.type.kind === "dyn" ||
|
||||
toJsonClass ||
|
||||
isJsonStringifyDynamicType(
|
||||
value.type,
|
||||
(id) => lowerer.shapes.get(id),
|
||||
@@ -191,9 +201,11 @@ export function lowerJsonMethodCall(
|
||||
type: DYN,
|
||||
loc,
|
||||
};
|
||||
// Preserve the existing dyn-root string ABI, including its textual
|
||||
// "undefined" result, while letting the runtime honor toJSON hooks.
|
||||
return value.type.kind === "dyn"
|
||||
// A dyn root can serialize to undefined (undefined, a function, a
|
||||
// symbol). Where the program can observe that (jsonResultObserved),
|
||||
// keep the actual undefined; elsewhere the dyn-root string ABI
|
||||
// stays, whose undefined result prints the same text.
|
||||
return (value.type.kind === "dyn" || toJsonClass) && !jsonResultObserved(lowerer, call)
|
||||
? { kind: "toString", operand: raw, type: STRING, loc }
|
||||
: { kind: "dynCheck", value: raw, type: lowerer.withUndefinedArm(STRING), loc };
|
||||
}
|
||||
@@ -290,6 +302,106 @@ function lowerOptionalStringifyRoot(
|
||||
return { kind: "call", callee: helper, args: [value], type: resultT, loc };
|
||||
}
|
||||
|
||||
/** True when a `JSON.stringify` result is used where JavaScript can tell
|
||||
* undefined from the text "undefined": compared with undefined/null,
|
||||
* `typeof`, `??`, `?.`, truth tests, a return from a function declaring
|
||||
* an undefined result, or an unannotated local binding whose references
|
||||
* do one of these. The lib types say `string`, so other uses (concatenation,
|
||||
* printing, string parameters) cannot tell the difference. */
|
||||
function jsonResultObserved(lowerer: Lowerer, call: ts.CallExpression): boolean {
|
||||
let node: ts.Node = call;
|
||||
while (ts.isParenthesizedExpression(node.parent)) node = node.parent;
|
||||
if (observesUndefined(node as ts.Expression)) return true;
|
||||
const parent = node.parent;
|
||||
if (ts.isReturnStatement(parent)) {
|
||||
let fn: ts.Node | undefined = parent.parent;
|
||||
while (fn && !ts.isFunctionLike(fn)) fn = fn.parent;
|
||||
const annotation = fn && ts.isFunctionLike(fn) ? fn.type : undefined;
|
||||
if (!annotation) return false;
|
||||
const type = lowerer.checker.getTypeAtLocation(annotation);
|
||||
const parts = type.flags & ts.TypeFlags.Union ? constituentTypes(type) : [type];
|
||||
return parts.some((part) => (part.flags & (ts.TypeFlags.Undefined | ts.TypeFlags.Void)) !== 0);
|
||||
}
|
||||
// Module-scope bindings are globals whose slot type comes from the
|
||||
// checker's `string`; only function- and block-scoped locals adopt the
|
||||
// initializer's checked `string | undefined`.
|
||||
if (
|
||||
!ts.isVariableDeclaration(parent) ||
|
||||
parent.initializer !== node ||
|
||||
parent.type ||
|
||||
!ts.isIdentifier(parent.name) ||
|
||||
(ts.isVariableDeclarationList(parent.parent) &&
|
||||
ts.isVariableStatement(parent.parent.parent) &&
|
||||
ts.isSourceFile(parent.parent.parent.parent))
|
||||
)
|
||||
return false;
|
||||
const name = parent.name;
|
||||
const symbol = lowerer.checker.getSymbolAtLocation(name);
|
||||
if (!symbol) return false;
|
||||
let scope: ts.Node = parent;
|
||||
while (!ts.isBlock(scope.parent) && !ts.isSourceFile(scope.parent) && scope.parent)
|
||||
scope = scope.parent;
|
||||
let observed = false;
|
||||
const visit = (current: ts.Node): void => {
|
||||
if (observed) return;
|
||||
if (ts.isIdentifier(current)) {
|
||||
if (
|
||||
current !== name &&
|
||||
current.text === name.text &&
|
||||
lowerer.checker.getSymbolAtLocation(current) === symbol &&
|
||||
observesUndefined(current)
|
||||
)
|
||||
observed = true;
|
||||
return;
|
||||
}
|
||||
ts.forEachChild(current, visit);
|
||||
};
|
||||
visit(scope.parent ?? scope);
|
||||
return observed;
|
||||
}
|
||||
|
||||
function observesUndefined(expr: ts.Expression): boolean {
|
||||
let node: ts.Node = expr;
|
||||
while (ts.isParenthesizedExpression(node.parent)) node = node.parent;
|
||||
const parent = node.parent;
|
||||
if (ts.isTypeOfExpression(parent)) return true;
|
||||
if (ts.isPrefixUnaryExpression(parent) && parent.operator === ts.SyntaxKind.ExclamationToken)
|
||||
return true;
|
||||
if (
|
||||
(ts.isPropertyAccessExpression(parent) || ts.isElementAccessExpression(parent)) &&
|
||||
parent.expression === node &&
|
||||
parent.questionDotToken
|
||||
)
|
||||
return true;
|
||||
if (
|
||||
(ts.isIfStatement(parent) || ts.isWhileStatement(parent) || ts.isDoStatement(parent)) &&
|
||||
parent.expression === node
|
||||
)
|
||||
return true;
|
||||
if (ts.isConditionalExpression(parent) && parent.condition === node) return true;
|
||||
if (ts.isBinaryExpression(parent)) {
|
||||
const op = parent.operatorToken.kind;
|
||||
if (op === ts.SyntaxKind.QuestionQuestionToken) return parent.left === node;
|
||||
if (op === ts.SyntaxKind.AmpersandAmpersandToken || op === ts.SyntaxKind.BarBarToken)
|
||||
return parent.left === node;
|
||||
if (
|
||||
op === ts.SyntaxKind.EqualsEqualsEqualsToken ||
|
||||
op === ts.SyntaxKind.ExclamationEqualsEqualsToken ||
|
||||
op === ts.SyntaxKind.EqualsEqualsToken ||
|
||||
op === ts.SyntaxKind.ExclamationEqualsToken
|
||||
) {
|
||||
let other: ts.Node = parent.left === node ? parent.right : parent.left;
|
||||
while (ts.isParenthesizedExpression(other)) other = other.expression;
|
||||
return (
|
||||
other.kind === ts.SyntaxKind.NullKeyword ||
|
||||
(ts.isIdentifier(other) && other.text === "undefined") ||
|
||||
ts.isVoidExpression(other)
|
||||
);
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/** True when a class field type holds a RegExp or a Promise outside a
|
||||
* nested class instance (whose own fields are checked separately): Node
|
||||
* writes both as {}, and the runtime JSON traversal has no writer for them. */
|
||||
|
||||
@@ -13,7 +13,13 @@ import {
|
||||
typeEquals,
|
||||
} from "../../ir/ir.js";
|
||||
import { isJsSourceFile, isNodeEsmFile } from "../program.js";
|
||||
import { genResultRecord, isAnyArrayType, jsOpenObjectType } from "../type-mapper.js";
|
||||
import {
|
||||
genResultRecord,
|
||||
isAnyArrayType,
|
||||
isUnitOnlyTsType,
|
||||
jsOpenObjectType,
|
||||
unitOnlyUnion,
|
||||
} from "../type-mapper.js";
|
||||
import { dynFallbackType } from "./lowerer.js";
|
||||
import { builtinModuleFnOf } from "./surfaces.js";
|
||||
import { mixinFnShapeOf } from "./lower-mixins.js";
|
||||
@@ -380,7 +386,7 @@ export function paramShapes(
|
||||
const tsType = symbol
|
||||
? lowerer.checker.getTypeOfSymbol(symbol)
|
||||
: lowerer.typeOf(declParam.name);
|
||||
const mapped = jsOpenObjectType(
|
||||
let mapped = jsOpenObjectType(
|
||||
declParam,
|
||||
lowerer.runtimeOptionalBindingType(
|
||||
declParam.name,
|
||||
@@ -389,6 +395,11 @@ export function paramShapes(
|
||||
lowerer.shapes,
|
||||
lowerer.unions,
|
||||
);
|
||||
// An instantiation with a literal `undefined` argument (`id(undefined)`
|
||||
// binds T = undefined): standalone undefined maps to VOID (the return
|
||||
// convention), and a parameter is a value position, so it takes the
|
||||
// unit-only slot like a declared `v: undefined` parameter.
|
||||
if (mapped?.kind === "void" && isUnitOnlyTsType(tsType)) mapped = unitOnlyUnion(lowerer.unions);
|
||||
if (!mapped || mapped.kind === "void")
|
||||
lowerer.badType(blameOf?.(declParam, i) ?? declParam.name, tsType);
|
||||
if (declParam.dotDotDotToken) {
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
import * as ts from "../ts7/adapter.js";
|
||||
import type { IrExpr, IrFunction } from "../../ir/ir.js";
|
||||
import {
|
||||
type IrExpr,
|
||||
type IrFunction,
|
||||
type IrType,
|
||||
callSiteReturnType,
|
||||
typeEquals,
|
||||
} from "../../ir/ir.js";
|
||||
import { everyStmtList, transformStmtList } from "../../ir/traverse.js";
|
||||
import type { ClassInfo } from "./lower-classes.js";
|
||||
import type { Lowerer } from "./lowerer.js";
|
||||
@@ -56,6 +62,9 @@ export function devirtualizeVirtualCalls(
|
||||
|
||||
class ConstructedClasses {
|
||||
private readonly lowered = new Set<string>();
|
||||
/** Lowered bodies' return types: an override may return a subclass
|
||||
* (`set(): this` resolves per declaring class). */
|
||||
private readonly returnTypes = new Map<string, IrType>();
|
||||
/** Classes some retained IR allocates or exposes as a class value. */
|
||||
private readonly allocated = new Set<string>();
|
||||
private readonly exportedNames: ReadonlySet<string>;
|
||||
@@ -74,6 +83,7 @@ class ConstructedClasses {
|
||||
};
|
||||
for (const fn of functions) {
|
||||
this.lowered.add(fn.name);
|
||||
this.returnTypes.set(fn.name, callSiteReturnType(fn));
|
||||
everyStmtList(fn.body, {
|
||||
stmt: () => true,
|
||||
expr: (e) => {
|
||||
@@ -102,7 +112,7 @@ class ConstructedClasses {
|
||||
this.facts.reachable.has(`%${candidate.def.name}.${e.method}`),
|
||||
)
|
||||
)
|
||||
return this.direct(e, info, nearest.declarer);
|
||||
return this.direct(e, info, nearest.declarer) ?? e;
|
||||
if (this.open(info)) return e;
|
||||
const targets: ClassInfo[] = [];
|
||||
for (const candidate of this.descendants(info)) {
|
||||
@@ -117,7 +127,7 @@ class ConstructedClasses {
|
||||
const callee = `%${target.def.name}.${e.method}`;
|
||||
if (!this.facts.reachable.has(callee) || !this.lowered.has(callee)) return e;
|
||||
}
|
||||
if (targets.length === 1) return this.direct(e, info, targets[0]!);
|
||||
if (targets.length === 1) return this.direct(e, info, targets[0]!) ?? e;
|
||||
// A small closed set: the backend tests the loaded slot against each
|
||||
// implementation and calls it directly (no indirect fallback; every
|
||||
// instance that can reach this site resolves to one of them).
|
||||
@@ -125,7 +135,19 @@ class ConstructedClasses {
|
||||
return { ...e, targets: targets.map((t) => t.def.name) };
|
||||
}
|
||||
|
||||
private direct(e: VirtualCall, info: ClassInfo, target: ClassInfo): IrExpr {
|
||||
/** The direct call, or null when the target's return type cannot stand
|
||||
* for the site's: an override returning a subclass object (a polymorphic
|
||||
* `this` return) upcasts to the site's class; any other mismatch keeps
|
||||
* the virtual call. */
|
||||
private direct(e: VirtualCall, info: ClassInfo, target: ClassInfo): IrExpr | null {
|
||||
const callee = `%${target.def.name}.${e.method}`;
|
||||
const ret = this.returnTypes.get(callee) ?? e.type;
|
||||
const upcast =
|
||||
!typeEquals(ret, e.type) &&
|
||||
ret.kind === "object" &&
|
||||
e.type.kind === "object" &&
|
||||
this.lowerer.isSubclassOf(ret.className, e.type.className);
|
||||
if (!typeEquals(ret, e.type) && !upcast) return null;
|
||||
const receiver = e.args[0]!;
|
||||
const typed: IrExpr =
|
||||
target === info || this.lowerer.isSubclassOf(info.def.name, target.def.name)
|
||||
@@ -139,13 +161,16 @@ class ConstructedClasses {
|
||||
type: { kind: "object", className: target.def.name },
|
||||
loc: receiver.loc,
|
||||
};
|
||||
return {
|
||||
const call: IrExpr = {
|
||||
kind: "call",
|
||||
callee: `%${target.def.name}.${e.method}`,
|
||||
callee,
|
||||
args: [typed, ...e.args.slice(1)],
|
||||
type: e.type,
|
||||
type: ret,
|
||||
loc: e.loc,
|
||||
};
|
||||
return upcast && e.type.kind === "object"
|
||||
? this.lowerer.upcastTo(call, e.type.className)
|
||||
: call;
|
||||
}
|
||||
|
||||
/** The class and every class below it, in the isSubclassOf sense
|
||||
|
||||
@@ -823,7 +823,7 @@ export function lowerObjectLiteral(
|
||||
// `k?: ...` — which desugar to one conditional field at the
|
||||
// spread's own position (any position: they introduce one fresh
|
||||
// name, collision-fenced below). Everything else stays fenced.
|
||||
const cs = conditionalSpreadOf(prop.expression);
|
||||
const cs = conditionalSpreadOf(prop.expression) ?? andSpreadOf(lowerer, prop.expression);
|
||||
if (cs === "unsupported" || (cs && cs.props.length !== 1)) {
|
||||
lowerer.unsupported(
|
||||
"SC1090",
|
||||
@@ -1264,6 +1264,7 @@ export function lowerObjectLiteral(
|
||||
expr.properties.length >= 2 &&
|
||||
ts.isSpreadAssignment(expr.properties[0]!) &&
|
||||
!conditionalSpreadOf(expr.properties[0]!.expression) &&
|
||||
!andSpreadOf(lowerer, expr.properties[0]!.expression) &&
|
||||
expr.properties
|
||||
.slice(1)
|
||||
.every(
|
||||
@@ -1340,7 +1341,7 @@ export function lowerObjectLiteral(
|
||||
let mergeable = true;
|
||||
for (const prop of expr.properties) {
|
||||
if (ts.isSpreadAssignment(prop)) {
|
||||
const cs = conditionalSpreadOf(prop.expression);
|
||||
const cs = conditionalSpreadOf(prop.expression) ?? andSpreadOf(lowerer, prop.expression);
|
||||
if (cs === "unsupported" || (cs && cs.props.length !== 1)) {
|
||||
lowerer.unsupported(
|
||||
"SC1090",
|
||||
@@ -1481,7 +1482,12 @@ export function lowerObjectLiteral(
|
||||
!shape.tuple &&
|
||||
shape.fields.length > 0 &&
|
||||
expr.properties.length > 0 &&
|
||||
expr.properties.every((p) => ts.isSpreadAssignment(p) && !conditionalSpreadOf(p.expression)) &&
|
||||
expr.properties.every(
|
||||
(p) =>
|
||||
ts.isSpreadAssignment(p) &&
|
||||
!conditionalSpreadOf(p.expression) &&
|
||||
!andSpreadOf(lowerer, p.expression),
|
||||
) &&
|
||||
expr.properties.some((p) => {
|
||||
const t = lowerer.mapTypeOf(lowerer.typeOf((p as ts.SpreadAssignment).expression));
|
||||
return (
|
||||
@@ -1599,7 +1605,7 @@ export function lowerObjectLiteral(
|
||||
}
|
||||
}
|
||||
if (ts.isSpreadAssignment(prop)) {
|
||||
const cs = conditionalSpreadOf(prop.expression);
|
||||
const cs = conditionalSpreadOf(prop.expression) ?? andSpreadOf(lowerer, prop.expression);
|
||||
if (cs && cs !== "unsupported") {
|
||||
// `...(c ? { k: v } : {})` — ONE conditional field at the
|
||||
// spread's own position: cond evaluates exactly once, `v` only
|
||||
@@ -1728,7 +1734,8 @@ export function lowerObjectLiteral(
|
||||
const laterNames = new Set<string>();
|
||||
for (const later of expr.properties.slice(expr.properties.indexOf(prop) + 1)) {
|
||||
if (ts.isSpreadAssignment(later)) {
|
||||
if (conditionalSpreadOf(later.expression)) continue;
|
||||
if (conditionalSpreadOf(later.expression) || andSpreadOf(lowerer, later.expression))
|
||||
continue;
|
||||
const lt = laterSpreadType(lowerer, later.expression);
|
||||
if (lt?.kind === "record") {
|
||||
// A field the later source may lack defines nothing.
|
||||
@@ -1963,7 +1970,8 @@ export function lowerObjectLiteral(
|
||||
const laterNames = new Set<string>();
|
||||
for (const later of expr.properties.slice(expr.properties.indexOf(prop) + 1)) {
|
||||
if (ts.isSpreadAssignment(later)) {
|
||||
if (conditionalSpreadOf(later.expression)) continue;
|
||||
if (conditionalSpreadOf(later.expression) || andSpreadOf(lowerer, later.expression))
|
||||
continue;
|
||||
const lt = laterSpreadType(lowerer, later.expression);
|
||||
if (lt?.kind === "record") {
|
||||
// A field the later source may lack defines nothing.
|
||||
@@ -2517,6 +2525,54 @@ function lowerUnionRecordClone(lowerer: Lowerer, expr: ts.ObjectLiteralExpressio
|
||||
};
|
||||
}
|
||||
|
||||
/** `...(c && { k: v })`: spreading a falsy primitive (false, 0, "", NaN)
|
||||
* copies nothing, so the idiom is `...(c ? { k: v } : {})` with `c`
|
||||
* evaluated once. Recognized only where the spread's own type mixes the
|
||||
* record with primitive arms; a record or record-or-unit spread keeps the
|
||||
* ordinary source-copy path. */
|
||||
export function andSpreadOf(
|
||||
lowerer: Lowerer,
|
||||
expr: ts.Expression,
|
||||
): { cond: ts.Expression; props: CondSpreadProp[]; whenTrue: boolean } | "unsupported" | null {
|
||||
let e: ts.Expression = expr;
|
||||
while (ts.isParenthesizedExpression(e)) e = e.expression;
|
||||
if (!ts.isBinaryExpression(e) || e.operatorToken.kind !== ts.SyntaxKind.AmpersandAmpersandToken)
|
||||
return null;
|
||||
let right: ts.Expression = e.right;
|
||||
while (ts.isParenthesizedExpression(right)) right = right.expression;
|
||||
if (!ts.isObjectLiteralExpression(right)) return null;
|
||||
const spreadType = lowerer.mapTypeOf(lowerer.typeOf(e));
|
||||
if (spreadType?.kind !== "union") return null;
|
||||
const arms = lowerer.unions.get(spreadType.unionId)?.arms ?? [];
|
||||
if (
|
||||
arms.filter((arm) => arm.kind === "record").length !== 1 ||
|
||||
!arms.some((arm) => arm.kind === "bool" || arm.kind === "f64" || arm.kind === "string") ||
|
||||
!arms.every(
|
||||
(arm) =>
|
||||
arm.kind === "record" ||
|
||||
arm.kind === "bool" ||
|
||||
arm.kind === "f64" ||
|
||||
arm.kind === "string" ||
|
||||
isUnitType(arm),
|
||||
)
|
||||
)
|
||||
return null;
|
||||
if (right.properties.length === 0) return "unsupported";
|
||||
const props: CondSpreadProp[] = [];
|
||||
for (const p of right.properties) {
|
||||
if (
|
||||
(ts.isPropertyAssignment(p) || ts.isShorthandPropertyAssignment(p)) &&
|
||||
p.name &&
|
||||
(ts.isIdentifier(p.name) || ts.isStringLiteral(p.name))
|
||||
) {
|
||||
props.push(p as CondSpreadProp);
|
||||
} else {
|
||||
return "unsupported";
|
||||
}
|
||||
}
|
||||
return { cond: e.left, props, whenTrue: true };
|
||||
}
|
||||
|
||||
/** A conditional spread's carrier property: `name: value` or shorthand,
|
||||
* with an identifier/string-literal name. */
|
||||
export type CondSpreadProp = (ts.PropertyAssignment | ts.ShorthandPropertyAssignment) & {
|
||||
|
||||
@@ -4,6 +4,8 @@ import { DYN, STRING, UNDEFINED_T, VOID, isUnitType, typeEquals } from "../../..
|
||||
import type { IrExpr, IrStmt, SrcLoc } from "../../../ir/ir.js";
|
||||
import { isNodeEsmFile, locOf } from "../../program.js";
|
||||
import type { Lowerer } from "../lowerer.js";
|
||||
import { tryLowerExpression } from "./try-lower-expression.js";
|
||||
import { constituentTypes } from "../../ts7/checker.js";
|
||||
|
||||
/** One optional-chain STEP: `a?.b`, `a?.m(...)`, `a?.[i]` (the token on
|
||||
* the member access) and `f?.()` (the token on the call). The receiver
|
||||
@@ -135,6 +137,31 @@ export function hasOptionalChainGuard(expr: ts.Expression): boolean {
|
||||
}
|
||||
}
|
||||
|
||||
/** `recv.m` where `m` is a required method the standard library declares
|
||||
* and `recv` itself is a non-optional, never-nullish typed receiver. */
|
||||
function alwaysPresentStdlibMethod(lowerer: Lowerer, node: ts.Expression): boolean {
|
||||
if (!ts.isPropertyAccessExpression(node) || node.questionDotToken) return false;
|
||||
if (hasOptionalChainGuard(node.expression)) return false;
|
||||
const symbol = lowerer.checker.getSymbolAtLocation(node.name);
|
||||
if (!symbol || !(symbol.flags & ts.SymbolFlags.Method) || symbol.flags & ts.SymbolFlags.Optional)
|
||||
return false;
|
||||
if (!lowerer.isStdlibSymbol(symbol)) return false;
|
||||
const nullable =
|
||||
ts.TypeFlags.Any |
|
||||
ts.TypeFlags.Unknown |
|
||||
ts.TypeFlags.Undefined |
|
||||
ts.TypeFlags.Null |
|
||||
ts.TypeFlags.Void;
|
||||
const receiverType = lowerer.checker.getTypeAtLocation(node.expression);
|
||||
const calleeType = lowerer.checker.getTypeAtLocation(node);
|
||||
const parts = (type: ts.Type): readonly ts.Type[] =>
|
||||
type.flags & ts.TypeFlags.Union ? constituentTypes(type) : [type];
|
||||
return (
|
||||
parts(receiverType).every((part) => !(part.flags & nullable)) &&
|
||||
parts(calleeType).every((part) => !(part.flags & nullable))
|
||||
);
|
||||
}
|
||||
|
||||
export function lowerOptionalChain(
|
||||
lowerer: Lowerer,
|
||||
expr: ts.CallExpression | ts.PropertyAccessExpression | ts.ElementAccessExpression,
|
||||
@@ -181,7 +208,25 @@ export function lowerOptionalChain(
|
||||
dotNode = tail;
|
||||
recvNode = tail.expression;
|
||||
}
|
||||
const loweredReceiver = lowerer.lowerExpr(recvNode);
|
||||
// `h.getSetCookie?.()`: a required standard-library METHOD on a
|
||||
// never-nullish receiver always exists, so `?.()` is the plain call.
|
||||
// Native handle methods have no first-class value to guard; when the
|
||||
// method read itself has no lowering, re-dispatch the plain call (which
|
||||
// reports its own diagnostics) instead of failing on the value read.
|
||||
let loweredReceiver: IrExpr | null;
|
||||
if (dotNode === expr && alwaysPresentStdlibMethod(lowerer, recvNode)) {
|
||||
loweredReceiver = tryLowerExpression(lowerer, recvNode);
|
||||
if (loweredReceiver === null) {
|
||||
lowerer.chainHandled.add(dotNode);
|
||||
try {
|
||||
return lowerer.lowerExpr(expr);
|
||||
} finally {
|
||||
lowerer.chainHandled.delete(dotNode);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
loweredReceiver = lowerer.lowerExpr(recvNode);
|
||||
}
|
||||
const receiver = lowerer.runtimeOptionalSourceValue(recvNode, loweredReceiver) ?? loweredReceiver;
|
||||
if (receiver.type.kind === "dyn") {
|
||||
if (dotNode === expr && ts.isCallExpression(expr)) {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { guardNullProto, nullProtoRecordShape } from "./null-prototype.js";
|
||||
import { lowerUnionEquality, tagEqualityMayMissAlias } from "./strict-equality.js";
|
||||
import { lowerHashStreamCall } from "./builtins/hash-stream.js";
|
||||
import { lowerResponseStaticJson } from "./lower-stdlib.js";
|
||||
import { lowerArrayCallbackCall } from "./containers/array-callback-call.js";
|
||||
/** Dispatch source calls, builtin members and receiver-specific operations.
|
||||
* Signature collection, argument completion and function bodies have their
|
||||
@@ -1464,6 +1465,8 @@ export function lowerCall(lowerer: Lowerer, expr: ts.CallExpression): IrExpr {
|
||||
// compile-time-known, so a literal key folds to a constant.
|
||||
lowerClassHasOwnPropertyCall(lowerer, expr, expr.expression) ??
|
||||
lowerObjectOwnMethodCall(lowerer, expr, expr.expression) ??
|
||||
// Static Response.json(data, init?) builds a native Response.
|
||||
lowerResponseStaticJson(lowerer, expr) ??
|
||||
// Refuse Response constructor operations before generic-call
|
||||
// fallbacks can report an unrelated SC1090 diagnostic.
|
||||
lowerer.fenceUnsupportedFetchConstructorMember(expr.expression) ??
|
||||
@@ -4320,8 +4323,15 @@ function lowerUnionToStringCall(
|
||||
(a.kind === "bytes" && a.elem === "u8"),
|
||||
);
|
||||
if (!stringable) return null;
|
||||
const operand = lowerer.lowerExpr(access.expression);
|
||||
let operand = lowerer.lowerExpr(access.expression);
|
||||
if (operand.type.kind !== "union") return null;
|
||||
// A flow-narrowed read keeps its wider storage union (`string | number`
|
||||
// after `!Array.isArray(h.host)` over a `string | number | string[]`
|
||||
// slot): retag it to the proven union before the per-union helper.
|
||||
if (operand.type.unionId !== recvT.unionId) {
|
||||
operand = lowerer.coerceInto(access.expression, operand, recvT);
|
||||
if (operand.type.kind !== "union" || operand.type.unionId !== recvT.unionId) return null;
|
||||
}
|
||||
return { kind: "toString", operand, type: STRING, loc: locOf(call) };
|
||||
}
|
||||
|
||||
|
||||
@@ -2661,9 +2661,15 @@ export function collectClassShapeInner(
|
||||
? String(Number(member.name.text))
|
||||
: member.name.text;
|
||||
const fieldName = symbolKey?.fieldName ?? stringKey;
|
||||
// A TypeScript field without an initializer takes its declared
|
||||
// type and the identifier fields' definite-assignment rule (an
|
||||
// unconditional `this[key] = ...` at the constructor's top
|
||||
// level), checked with the unguarded fields below.
|
||||
const declaredOnly =
|
||||
!member.initializer && !isJsSourceFile(member.getSourceFile()) && !!member.type;
|
||||
if (
|
||||
fieldName === null ||
|
||||
(!member.initializer && !isJsSourceFile(member.getSourceFile()))
|
||||
(!member.initializer && !isJsSourceFile(member.getSourceFile()) && !declaredOnly)
|
||||
) {
|
||||
lowerer.unsupported(
|
||||
"SC1090",
|
||||
@@ -2676,9 +2682,10 @@ export function collectClassShapeInner(
|
||||
member.type || !member.initializer
|
||||
? declared
|
||||
: lowerer.checker.getBaseTypeOfLiteralType(lowerer.typeOf(member.initializer));
|
||||
const mapped = member.initializer
|
||||
? (lowerer.mapTypeOf(inferred) ?? dynFallbackType(lowerer, member, inferred))
|
||||
: DYN;
|
||||
const mapped =
|
||||
member.initializer || declaredOnly
|
||||
? (lowerer.mapTypeOf(inferred) ?? dynFallbackType(lowerer, member, inferred))
|
||||
: DYN;
|
||||
let type =
|
||||
mapped?.kind === "record" && isJsSourceFile(member.getSourceFile()) && !member.type
|
||||
? DYN
|
||||
@@ -2690,7 +2697,7 @@ export function collectClassShapeInner(
|
||||
type.kind === "void" ||
|
||||
(type.kind === "dyn" &&
|
||||
!isJsSourceFile(member.getSourceFile()) &&
|
||||
!lowerer.classImplementedProtocol(inferred))
|
||||
!lowerer.classFieldStoresDyn(inferred))
|
||||
)
|
||||
lowerer.badType(member, inferred);
|
||||
if (isJsSourceFile(member.getSourceFile())) {
|
||||
@@ -2698,7 +2705,24 @@ export function collectClassShapeInner(
|
||||
if (base?.fields.get(fieldName)?.kind === "dyn") type = DYN;
|
||||
if (base && !fields.has(fieldName) && lowerer.dynConvertible(type)) type = DYN;
|
||||
}
|
||||
const undefinedInitializer = !member.initializer ? locOf(member) : undefined;
|
||||
const undefinedInitializer =
|
||||
!member.initializer && (!declaredOnly || type.kind === "dyn")
|
||||
? locOf(member)
|
||||
: undefined;
|
||||
if (
|
||||
declaredOnly &&
|
||||
type.kind !== "dyn" &&
|
||||
!(
|
||||
type.kind === "union" &&
|
||||
lowerer.unions.get(type.unionId)?.arms.some((a) => a.kind === "undefinedT")
|
||||
)
|
||||
) {
|
||||
unguardedFields.push({
|
||||
node: member.name,
|
||||
name: fieldName,
|
||||
why: `initializer-less computed fields not assigned at the constructor's top level (nothing guarantees '${symbolKey ? symbolFieldDisplayName(fieldName) : fieldName}' is assigned before a read — assign it unconditionally at the top of the constructor, give it an initializer, or include undefined in its type)`,
|
||||
});
|
||||
}
|
||||
const previous = fields.get(fieldName);
|
||||
if (previous) {
|
||||
if (symbolKey && !symbolFields.has(symbolKey.identity)) {
|
||||
@@ -3458,7 +3482,17 @@ export function collectClassShapeInner(
|
||||
}
|
||||
let sig: { params: ParamShape[]; ret: IrType };
|
||||
if (isGet) {
|
||||
const ret = lowerer.declaredReturnType(member, member.name);
|
||||
let ret = lowerer.declaredReturnType(member, member.name);
|
||||
// A getter that never returns a value: typed `never` (it only
|
||||
// throws, Next.js's removed `page`/`ua` getters) or, unannotated
|
||||
// with no return statement, inferred `void`. A read answers
|
||||
// undefined or throws, so the property takes the checked-dynamic
|
||||
// slot instead of VOID.
|
||||
if (
|
||||
ret.kind === "void" &&
|
||||
(lowerer.typeOf(member.name).flags & (ts.TypeFlags.Never | ts.TypeFlags.Void)) !== 0
|
||||
)
|
||||
ret = DYN;
|
||||
if (ret.kind === "void") lowerer.badType(member.name, lowerer.typeOf(member.name));
|
||||
sig = { params: [], ret };
|
||||
} else {
|
||||
@@ -3605,6 +3639,16 @@ export function collectClassShapeInner(
|
||||
stmt.expression.left.expression.kind === ts.SyntaxKind.ThisKeyword
|
||||
) {
|
||||
topAssigned.add(stmt.expression.left.name.text);
|
||||
} else if (
|
||||
ts.isExpressionStatement(stmt) &&
|
||||
ts.isBinaryExpression(stmt.expression) &&
|
||||
stmt.expression.operatorToken.kind === ts.SyntaxKind.EqualsToken &&
|
||||
ts.isElementAccessExpression(stmt.expression.left) &&
|
||||
stmt.expression.left.expression.kind === ts.SyntaxKind.ThisKeyword
|
||||
) {
|
||||
const key = stmt.expression.left.argumentExpression;
|
||||
const name = classSymbolKeyOf(lowerer, key)?.fieldName ?? lowerer.foldedStringKeyOf(key);
|
||||
if (name !== null) topAssigned.add(name);
|
||||
}
|
||||
}
|
||||
for (const f of unguardedFields) {
|
||||
|
||||
@@ -13,6 +13,7 @@ import {
|
||||
type IrExpr,
|
||||
type IrStmt,
|
||||
type IrType,
|
||||
type SrcLoc,
|
||||
STRING,
|
||||
VOID,
|
||||
canConvertToDyn,
|
||||
@@ -1932,11 +1933,39 @@ export function lowerResponseNew(lowerer: Lowerer, expr: ts.NewExpression): IrEx
|
||||
if (stdlibGlobalNameOf(lowerer, expr.expression) !== "Response") return null;
|
||||
const args = expr.arguments ?? [];
|
||||
if (args.some(ts.isSpreadElement)) return null;
|
||||
const loc = locOf(expr);
|
||||
return lowerResponseConstruction(lowerer, args, locOf(expr), false);
|
||||
}
|
||||
|
||||
/** Static `Response.json(data, init?)`: the data crosses into the runtime's
|
||||
* checked-dynamic JSON serializer (toJSON hooks included) after ResponseInit
|
||||
* converts, then the response initializes as the constructor does. */
|
||||
export function lowerResponseStaticJson(lowerer: Lowerer, call: ts.CallExpression): IrExpr | null {
|
||||
if (call.questionDotToken || !ts.isPropertyAccessExpression(call.expression)) return null;
|
||||
const access = call.expression;
|
||||
if (access.questionDotToken || access.name.text !== "json") return null;
|
||||
if (!lowerer.isStdlibGlobal(access.expression, "Response")) return null;
|
||||
if (call.arguments.some(ts.isSpreadElement) || call.arguments.length === 0) return null;
|
||||
return lowerResponseConstruction(lowerer, call.arguments, locOf(call), true);
|
||||
}
|
||||
|
||||
function lowerResponseConstruction(
|
||||
lowerer: Lowerer,
|
||||
args: readonly ts.Expression[],
|
||||
loc: SrcLoc,
|
||||
json: boolean,
|
||||
): IrExpr {
|
||||
const toDyn = (node: ts.Expression | undefined, value: IrExpr, what: string): IrExpr => {
|
||||
if (node === undefined) return dynUndefinedExpr(loc);
|
||||
if (value.type.kind === "dyn") return value;
|
||||
if (what === "JSON data") {
|
||||
const boxed = lowerer.coerceToExpected(value, DYN);
|
||||
if (boxed.type.kind === "dyn") return boxed;
|
||||
lowerer.noLowering(
|
||||
`Response.json of '${lowerer.fmt(value.type)}' values`,
|
||||
node,
|
||||
"the data must cross the checked-dynamic boundary (number, string, boolean, null, records, arrays, and 'unknown')",
|
||||
);
|
||||
}
|
||||
/* A user class can satisfy ResponseInit structurally through plain
|
||||
* fields. Project just the declared dictionary members into a record,
|
||||
* then use the ordinary checked-dynamic copy. objRecordWidthHelper
|
||||
@@ -2059,8 +2088,11 @@ export function lowerResponseNew(lowerer: Lowerer, expr: ts.NewExpression): IrEx
|
||||
}
|
||||
const result: IrExpr = {
|
||||
kind: "libCall",
|
||||
fn: "fetch.responseNew",
|
||||
args: [toDyn(bodyNode, bodyRef, "a body"), toDyn(initNode, initRef, "an init value")],
|
||||
fn: json ? "fetch.responseJsonStatic" : "fetch.responseNew",
|
||||
args: [
|
||||
toDyn(bodyNode, bodyRef, json ? "JSON data" : "a body"),
|
||||
toDyn(initNode, initRef, "an init value"),
|
||||
],
|
||||
type: DYN,
|
||||
loc,
|
||||
};
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { dynUndefinedExpr, numLit, varRef } from "../../ir/build.js";
|
||||
import { lowerCollectionSpread } from "./containers/collection-methods.js";
|
||||
import { lowerEventObjectPropertyWrite } from "./lower-event-target.js";
|
||||
import { lowerDynObjectLiteral } from "./expressions/object-literals.js";
|
||||
import { importMetaBindingSource } from "./import-meta.js";
|
||||
@@ -2515,6 +2516,39 @@ export function lowerBindingPattern(
|
||||
});
|
||||
return;
|
||||
}
|
||||
// Map and Set sources (`const [[k, v], ...rest] = map`): the iterator
|
||||
// yields the collection's entries (Map pairs) or values in order, so
|
||||
// the source snapshots ONCE into that array — the spread's own
|
||||
// machinery — and the pattern lowers as an array pattern over it,
|
||||
// like the string case above. Element defaults keep a fence: they
|
||||
// run between iterator steps and could mutate the live collection.
|
||||
if (srcType.kind === "map" || srcType.kind === "set") {
|
||||
const defaulted = pattern.elements.find(
|
||||
(el): el is ts.BindingElement =>
|
||||
ts.isBindingElement(el) && el.initializer !== undefined && !el.dotDotDotToken,
|
||||
);
|
||||
if (defaulted)
|
||||
lowerer.unsupported("SC1031", defaulted, "binding defaults over Map/Set sources");
|
||||
const snapshot = lowerCollectionSpread(
|
||||
lowerer,
|
||||
srcRef(),
|
||||
pattern as unknown as ts.Expression,
|
||||
);
|
||||
if (snapshot && snapshot.type.kind === "array") {
|
||||
const loc = locOf(pattern);
|
||||
const snapshotT = snapshot.type;
|
||||
const entries = lowerer.declareHiddenLocal("%dentries", snapshotT);
|
||||
out.push({ kind: "varDecl", localId: entries.id, init: snapshot, loc });
|
||||
lowerer.lowerBindingPattern(
|
||||
pattern,
|
||||
() => ({ kind: "varRef", localId: entries.id, type: snapshotT, loc }),
|
||||
snapshotT,
|
||||
isLet,
|
||||
out,
|
||||
);
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (srcType.kind !== "array") {
|
||||
lowerer.unsupported(
|
||||
"SC1031",
|
||||
|
||||
@@ -242,6 +242,7 @@ import {
|
||||
isOverloadedCallableMemberType,
|
||||
isUnitOnlyTsType,
|
||||
isWorkerHandleType,
|
||||
isFetchHandleType,
|
||||
jsOpenObjectType,
|
||||
mapType,
|
||||
ShapeRegistry,
|
||||
@@ -3113,12 +3114,14 @@ export class Lowerer {
|
||||
/** Declared class fields may store a DYN value only when its static type
|
||||
* is `unknown` or `object` (also with `null` or `undefined`, or an array
|
||||
* of those), names checked protocol identity, or is an owned runtime
|
||||
* handle (a Worker or MessagePort); `any` fields stay refused. */
|
||||
* handle (a Worker or MessagePort, or a fetch Headers/Request/Response);
|
||||
* `any` fields stay refused. */
|
||||
classFieldStoresDyn(type: ts.Type): boolean {
|
||||
return (
|
||||
this.opaqueValueType(type) ||
|
||||
this.classImplementedProtocol(type) ||
|
||||
isWorkerHandleType(type, this.typeCtx)
|
||||
isWorkerHandleType(type, this.typeCtx) ||
|
||||
isFetchHandleType(type, this.typeCtx)
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -2224,6 +2224,13 @@ export const AMBIENT_SURFACE_FNS: readonly AmbientSurfaceRow[] = [
|
||||
fns: ["date.toISOString", "date.toISOStringValue"],
|
||||
note: "UTC ISO formatting over constructed and stored Date values",
|
||||
},
|
||||
{
|
||||
id: "stdlib.date.toUTCString",
|
||||
kind: "stdlib",
|
||||
name: "Date.prototype.toUTCString",
|
||||
fns: ["date.toUTCString"],
|
||||
note: "RFC 7231 IMF-fixdate formatting over stored Date values",
|
||||
},
|
||||
{
|
||||
id: "stdlib.date.getFullYear",
|
||||
kind: "stdlib",
|
||||
|
||||
@@ -1336,6 +1336,40 @@ export function isWorkerHandleType(
|
||||
return handle;
|
||||
}
|
||||
|
||||
/** The native fetch handles (Headers, Request, Response), optionally in
|
||||
* arrays or with null/undefined: checked-dynamic handles wherever they are
|
||||
* stored, like the worker handles above. */
|
||||
export function isFetchHandleType(
|
||||
type: ts.Type,
|
||||
ctx: Pick<TypeMapperCtx, "checker" | "isStdlibFile">,
|
||||
): boolean {
|
||||
const members = type.isUnionType() ? ts.constituentTypes(type) : [type];
|
||||
let handle = false;
|
||||
for (const member of members) {
|
||||
if (member.flags & (ts.TypeFlags.Null | ts.TypeFlags.Undefined)) continue;
|
||||
if (ctx.checker.isArrayType(member)) {
|
||||
const element = ctx.checker.getTypeArguments(member as ts.TypeReference)[0];
|
||||
if (element === undefined || !isFetchHandleType(element, ctx)) return false;
|
||||
} else {
|
||||
const symbol = member.getSymbol();
|
||||
if (
|
||||
!symbol ||
|
||||
(symbol.name !== "Headers" && symbol.name !== "Request" && symbol.name !== "Response") ||
|
||||
!ctx.checker
|
||||
.declarationsOf(symbol)
|
||||
.some(
|
||||
(d) =>
|
||||
(ts.isInterfaceDeclaration(d) || ts.isClassDeclaration(d)) &&
|
||||
ctx.isStdlibFile(d.getSourceFile()),
|
||||
)
|
||||
)
|
||||
return false;
|
||||
}
|
||||
handle = true;
|
||||
}
|
||||
return handle;
|
||||
}
|
||||
|
||||
export function mapType(type: ts.Type, ctx: TypeMapperCtx): IrType | null {
|
||||
if (mapTypeDepth >= MAP_TYPE_MAX_DEPTH) return null;
|
||||
// These intrinsic domains cannot depend on generic bindings, declaration
|
||||
|
||||
@@ -119,6 +119,7 @@ export const MAY_THROW_LIB_FNS: ReadonlySet<IrLibFn> = new Set([
|
||||
"fetch.requestNew",
|
||||
"fetch.headersNew",
|
||||
"fetch.responseNew",
|
||||
"fetch.responseJsonStatic",
|
||||
"fetch.abortTimeout",
|
||||
"fetch.abortAny",
|
||||
"abort.timeout",
|
||||
|
||||
@@ -64,6 +64,7 @@ export const LIB_FN_SIGS = defineLibFnSignatures({
|
||||
* are opaque checked-dynamic handles. */
|
||||
"fetch.start": { argTypes: [STRING, DYN], result: { kind: "promise", inner: DYN } },
|
||||
"fetch.responseNew": { argTypes: [DYN, DYN], result: DYN },
|
||||
"fetch.responseJsonStatic": { argTypes: [DYN, DYN], result: DYN },
|
||||
"fetch.input": { argTypes: [DYN, DYN], result: { kind: "promise", inner: DYN } },
|
||||
"abort.controllerNew": { argTypes: [], result: DYN },
|
||||
/** EventTarget / Event / CustomEvent construction (scr_event_target.c):
|
||||
@@ -4197,6 +4198,9 @@ export const LIB_FN_SIGS = defineLibFnSignatures({
|
||||
"date.valueOf": { argTypes: [DATE_T], result: F64 },
|
||||
"date.toISOString": { argTypes: [F64], result: STRING },
|
||||
"date.toISOStringValue": { argTypes: [DATE_T], result: STRING },
|
||||
/** Date.prototype.toUTCString over a stored Date ("Invalid Date" for
|
||||
* NaN; never throws). */
|
||||
"date.toUTCString": { argTypes: [DATE_T], result: STRING },
|
||||
"date.getFullYear": { argTypes: [DATE_T], result: F64 },
|
||||
"date.getUTCFullYear": { argTypes: [DATE_T], result: F64 },
|
||||
"date.getMonth": { argTypes: [DATE_T], result: F64 },
|
||||
|
||||
@@ -879,6 +879,33 @@ export function typeKey(t: IrType): string {
|
||||
}
|
||||
}
|
||||
|
||||
/** The type a CALL SITE (or function value) receives from a module
|
||||
* function: an async body's returnType is the promise's INNER type, so
|
||||
* calls and closures over it see Promise<T>; a generator body's is the
|
||||
* TReturn channel, so call sites see the generator type. The one place
|
||||
* the body/call-site split is spelled out, for the validator and lowering. */
|
||||
export function callSiteReturnType(fn: IrFunction): IrType {
|
||||
if (fn.async && fn.generator !== undefined) {
|
||||
return {
|
||||
kind: "generator",
|
||||
async: true,
|
||||
yieldT: fn.generator.yieldT,
|
||||
retT: fn.returnType,
|
||||
nextT: fn.generator.nextT,
|
||||
};
|
||||
}
|
||||
if (fn.async) return { kind: "promise", inner: fn.returnType };
|
||||
if (fn.generator !== undefined) {
|
||||
return {
|
||||
kind: "generator",
|
||||
yieldT: fn.generator.yieldT,
|
||||
retT: fn.returnType,
|
||||
nextT: fn.generator.nextT,
|
||||
};
|
||||
}
|
||||
return fn.returnType;
|
||||
}
|
||||
|
||||
export function typeEquals(a: IrType, b: IrType): boolean {
|
||||
// Dispatch once: a chain of negative narrowing checks repeatedly retags
|
||||
// the remaining variants when this comparator runs in the native compiler.
|
||||
|
||||
@@ -56,7 +56,7 @@ import {
|
||||
maybePromiseUnionArmsOk,
|
||||
VOID,
|
||||
} from "./ir.js";
|
||||
import { DYN_CLASS_PROPERTIES } from "./ir.js";
|
||||
import { DYN_CLASS_PROPERTIES, callSiteReturnType } from "./ir.js";
|
||||
import { unionWideningTags } from "./analysis.js";
|
||||
import { alwaysReturns } from "./control-flow.js";
|
||||
import { dynamicOperationOf, functionTier } from "./tiers.js";
|
||||
@@ -85,33 +85,6 @@ export interface IrValidationError {
|
||||
loc: SrcLoc;
|
||||
}
|
||||
|
||||
/** The type a CALL SITE (or function value) receives from a module
|
||||
* function: an async body's returnType is the promise's INNER type, so
|
||||
* calls and closures over it see Promise<T>; a generator body's is the
|
||||
* TReturn channel, so call sites see the generator type. The one place
|
||||
* the body/call-site split is spelled out in the validator. */
|
||||
function callSiteReturnType(fn: IrFunction): IrType {
|
||||
if (fn.async && fn.generator !== undefined) {
|
||||
return {
|
||||
kind: "generator",
|
||||
async: true,
|
||||
yieldT: fn.generator.yieldT,
|
||||
retT: fn.returnType,
|
||||
nextT: fn.generator.nextT,
|
||||
};
|
||||
}
|
||||
if (fn.async) return { kind: "promise", inner: fn.returnType };
|
||||
if (fn.generator !== undefined) {
|
||||
return {
|
||||
kind: "generator",
|
||||
yieldT: fn.generator.yieldT,
|
||||
retT: fn.returnType,
|
||||
nextT: fn.generator.nextT,
|
||||
};
|
||||
}
|
||||
return fn.returnType;
|
||||
}
|
||||
|
||||
interface VirtualCallInfo {
|
||||
declared: boolean;
|
||||
hasOverride: boolean;
|
||||
|
||||
@@ -4216,6 +4216,13 @@
|
||||
"status": "static",
|
||||
"note": "UTC ISO formatting over constructed and stored Date values"
|
||||
},
|
||||
{
|
||||
"id": "stdlib.date.toUTCString",
|
||||
"kind": "stdlib",
|
||||
"name": "Date.prototype.toUTCString",
|
||||
"status": "static",
|
||||
"note": "RFC 7231 IMF-fixdate formatting over stored Date values"
|
||||
},
|
||||
{
|
||||
"id": "stdlib.date.valueOf",
|
||||
"kind": "stdlib",
|
||||
@@ -5293,9 +5300,8 @@
|
||||
"id": "stdlib.response.static.json",
|
||||
"kind": "stdlib",
|
||||
"name": "Response.json",
|
||||
"status": "unsupported",
|
||||
"code": "SC2020",
|
||||
"note": "Node 24.22.0 / Undici 7.29.1; Response static constructor-object operations have no compiler lowering"
|
||||
"status": "static",
|
||||
"note": "Node 24.22.0 / Undici 7.29.1; facets: webidl-conversion, error-shape; supported scope: data that crosses the checked-dynamic boundary, serialized with JSON.stringify (toJSON hooks included); ResponseInit as for the constructor; differential evidence: fixture:static-response-json"
|
||||
},
|
||||
{
|
||||
"id": "stdlib.response.static.redirect",
|
||||
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/class-devirtualized-this-return.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/class-fetch-handle-fields.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/class-never-getter.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/class-symbol-field-constructor-assigned.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/date-to-utc-string.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/destructure-map-set-iterables.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/fetch-headers-set-cookie-iteration.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/fetch-invalid-url-cause.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/fetch-request-set-cookie-wire.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/generic-undefined-argument.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/json-stringify-class-tojson.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/json-stringify-unknown-undefined-root.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/object-spread-and-conditional.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/optional-call-native-method.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/union-narrowed-tostring.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
@@ -312,6 +312,31 @@ static ScrDyn *sf_type_reason(const char *message, ScrError **error_out) {
|
||||
return d;
|
||||
}
|
||||
|
||||
/* Request's URL parse failure: with the URL parser's TypeError pending,
|
||||
* replace it by undici's TypeError "Failed to parse URL from <input>"
|
||||
* whose (non-enumerable) cause is the parser error. */
|
||||
static void sf_throw_url_parse_failure(const ScrStr *input) {
|
||||
ScrCaught *caught = scr_exc_take();
|
||||
ScrDyn *cause = scr_caught_to_dyn(caught);
|
||||
scr_caught_release(caught);
|
||||
/* Sized without the NUL so it stays out of the unit's shared mergeable
|
||||
* string section (which every program linking this unit carries). */
|
||||
static const char prefix[25] = "Failed to parse URL from ";
|
||||
size_t message_len = sizeof prefix + input->len;
|
||||
char *message = malloc(message_len);
|
||||
if (!message) sf_oom();
|
||||
memcpy(message, prefix, sizeof prefix);
|
||||
memcpy(message + sizeof prefix, input->data, input->len);
|
||||
ScrStr *text = scr_str_new(message, message_len);
|
||||
free(message);
|
||||
ScrError *e = scr_error_new(SCR_ERR_TYPE, text);
|
||||
scr_str_release(text);
|
||||
scr_error_set_cause(e, cause);
|
||||
e->cause_enumerable = false;
|
||||
scr_dyn_release(cause);
|
||||
scr_throw_obj(e, &scr_error_retain_v, &scr_error_release_v, scr_error_trace_arg());
|
||||
}
|
||||
|
||||
/* ── AbortSignal ─────────────────────────────────────────────────── */
|
||||
|
||||
static void sf_signal_track_reason(SfSignal *s) {
|
||||
@@ -2386,6 +2411,50 @@ static ScrStr **sf_headers_sorted_names(SfHeaders *h, size_t *count_out) {
|
||||
return names;
|
||||
}
|
||||
|
||||
static bool sf_headers_is_set_cookie(const ScrStr *name) {
|
||||
return name->len == 10 && memcmp(name->data, "set-cookie", 10) == 0;
|
||||
}
|
||||
|
||||
/* The `index`th entry of Fetch's "sort and combine" list: names in
|
||||
* byte order, each with its combined value, except that every set-cookie
|
||||
* value is its own entry (in insertion order), as Node's Headers
|
||||
* iterator yields them. False past the end. Outputs are +1. */
|
||||
static bool sf_headers_sorted_entry(SfHeaders *h, size_t index,
|
||||
ScrStr **name_out,
|
||||
ScrStr **value_out) {
|
||||
size_t count = 0;
|
||||
ScrStr **names = sf_headers_sorted_names(h, &count);
|
||||
bool found = false;
|
||||
size_t at = 0;
|
||||
for (size_t i = 0; i < count && !found; i++) {
|
||||
if (!sf_headers_is_set_cookie(names[i])) {
|
||||
if (at == index) {
|
||||
*name_out = scr_str_retain(names[i]);
|
||||
*value_out = sf_headers_get_value(h, names[i]);
|
||||
found = true;
|
||||
}
|
||||
at++;
|
||||
continue;
|
||||
}
|
||||
size_t n = (size_t)scr_arr_len(h->pairs);
|
||||
for (size_t j = 0; j + 1 < n && !found; j += 2) {
|
||||
ScrStr *key = scr_arr_get_ref(h->pairs, (double)j);
|
||||
bool match = sf_headers_is_set_cookie(key);
|
||||
scr_str_release(key);
|
||||
if (!match) continue;
|
||||
if (at == index) {
|
||||
*name_out = scr_str_retain(names[i]);
|
||||
*value_out = scr_arr_get_ref(h->pairs, (double)(j + 1));
|
||||
found = true;
|
||||
}
|
||||
at++;
|
||||
}
|
||||
}
|
||||
for (size_t j = 0; j < count; j++) scr_str_release(names[j]);
|
||||
free(names);
|
||||
return found;
|
||||
}
|
||||
|
||||
static ScrDyn *sf_headers_invoke(void *ptr, ScrDyn *self,
|
||||
const char *method,
|
||||
ScrDyn *const *args, size_t argc,
|
||||
@@ -2438,20 +2507,15 @@ static ScrDyn *sf_headers_invoke(void *ptr, ScrDyn *self,
|
||||
* a future name skips it, inserting a later name visits it, and
|
||||
* inserting before the current index can make the current name appear
|
||||
* again. The old one-time snapshot was only safe while every native
|
||||
* Headers handle was immutable. */
|
||||
* Headers handle was immutable. Set-Cookie values are visited one by
|
||||
* one (sf_headers_sorted_entry). */
|
||||
for (size_t i = 0;; i++) {
|
||||
size_t count = 0;
|
||||
ScrStr **names = sf_headers_sorted_names(h, &count);
|
||||
if (i >= count) {
|
||||
for (size_t j = 0; j < count; j++) scr_str_release(names[j]);
|
||||
free(names);
|
||||
break;
|
||||
}
|
||||
ScrStr *value = sf_headers_get_value(h, names[i]);
|
||||
ScrStr *name = NULL;
|
||||
ScrStr *value = NULL;
|
||||
if (!sf_headers_sorted_entry(h, i, &name, &value)) break;
|
||||
ScrDyn *boxed_value = scr_dyn_new_str(value);
|
||||
ScrDyn *boxed_name = scr_dyn_new_str(names[i]);
|
||||
for (size_t j = 0; j < count; j++) scr_str_release(names[j]);
|
||||
free(names);
|
||||
ScrDyn *boxed_name = scr_dyn_new_str(name);
|
||||
scr_str_release(name);
|
||||
ScrDyn *call_args[3] = {boxed_value, boxed_name, self};
|
||||
scr_dyn_this_push_dyn(this_arg);
|
||||
ScrDyn *result =
|
||||
@@ -4415,14 +4479,39 @@ static bool sf_start_hop(SfTransfer *t) {
|
||||
}
|
||||
if (connection_close) sf_push_header_text(headers, "connection", "close", 5);
|
||||
else sf_push_header_text(headers, "connection", "keep-alive", 10);
|
||||
/* Headers keeps each Set-Cookie value separately, but undici sends its
|
||||
* header list's combined value: one line, the values joined by ", ",
|
||||
* at the position of the first one. */
|
||||
bool set_cookie_sent = false;
|
||||
for (size_t i = 0; i + 1 < user_len; i += 2) {
|
||||
ScrStr *name = scr_arr_get_ref(t->headers, (double)i);
|
||||
ScrStr *value = scr_arr_get_ref(t->headers, (double)(i + 1));
|
||||
if (sf_eq_ci(name, "host") || sf_eq_ci(name, "connection")) {
|
||||
if (sf_eq_ci(name, "host") || sf_eq_ci(name, "connection") ||
|
||||
(set_cookie_sent && sf_eq_ci(name, "set-cookie"))) {
|
||||
scr_str_release(name);
|
||||
scr_str_release(value);
|
||||
continue;
|
||||
}
|
||||
if (sf_eq_ci(name, "set-cookie")) {
|
||||
set_cookie_sent = true;
|
||||
for (size_t j = i + 2; j + 1 < user_len; j += 2) {
|
||||
ScrStr *later = scr_arr_get_ref(t->headers, (double)j);
|
||||
bool match = sf_eq_ci(later, "set-cookie");
|
||||
scr_str_release(later);
|
||||
if (!match) continue;
|
||||
ScrStr *next = scr_arr_get_ref(t->headers, (double)(j + 1));
|
||||
size_t joined_len = value->len + 2 + next->len;
|
||||
char *joined = malloc(joined_len ? joined_len : 1);
|
||||
if (!joined) sf_oom();
|
||||
memcpy(joined, value->data, value->len);
|
||||
memcpy(joined + value->len, ", ", 2);
|
||||
memcpy(joined + value->len + 2, next->data, next->len);
|
||||
scr_str_release(next);
|
||||
scr_str_release(value);
|
||||
value = scr_str_new(joined, joined_len);
|
||||
free(joined);
|
||||
}
|
||||
}
|
||||
scr_arr_push_ref(headers, name);
|
||||
scr_arr_push_ref(headers, value);
|
||||
}
|
||||
@@ -4578,14 +4667,33 @@ static ScrDyn *sf_response_init_get(
|
||||
return NULL;
|
||||
}
|
||||
|
||||
ScrDyn *scr_fetch_response_new(ScrDyn *body, ScrDyn *init) {
|
||||
/* The Response constructor, and Response.json(data, init) when json_data
|
||||
* is non-NULL: undici converts ResponseInit, serializes the data (TypeError
|
||||
* "Value is not JSON serializable" when JSON.stringify answers undefined),
|
||||
* then initializes the response exactly as the constructor does, with
|
||||
* "application/json" as the default content type. */
|
||||
typedef ScrDyn *(*SfJsonSerialize)(const ScrDyn *value);
|
||||
|
||||
/* The JSON serializer reaches the constructor only through Response.json's
|
||||
* entry point, so programs that never call it link no JSON walker. */
|
||||
static ScrDyn *sf_response_json_serialize(const ScrDyn *value) {
|
||||
ScrStr *gap = scr_str_new("", 0);
|
||||
ScrDyn *serialized = scr_json_stringify_replacer(value, scr_dyn_undefined(), gap);
|
||||
scr_str_release(gap);
|
||||
return serialized;
|
||||
}
|
||||
|
||||
static ScrDyn *sf_response_construct(ScrDyn *body, ScrDyn *init,
|
||||
const ScrDyn *json_data,
|
||||
SfJsonSerialize serialize) {
|
||||
ScrArr *header_pairs = NULL;
|
||||
ScrArr *converted_headers = NULL;
|
||||
ScrBytes *body_bytes = NULL;
|
||||
ScrStr *status_text = NULL;
|
||||
SfStream *stream = NULL;
|
||||
bool null_body =
|
||||
!body || body->kind == SCR_DYN_UNDEF || body->kind == SCR_DYN_NULL;
|
||||
bool json_body = json_data != NULL;
|
||||
bool null_body = !json_body &&
|
||||
(!body || body->kind == SCR_DYN_UNDEF || body->kind == SCR_DYN_NULL);
|
||||
bool text_body = false;
|
||||
int status = 200;
|
||||
bool status_range_error = false;
|
||||
@@ -4594,7 +4702,7 @@ ScrDyn *scr_fetch_response_new(ScrDyn *body, ScrDyn *init) {
|
||||
* string arm therefore runs before ResponseInit's dictionary conversion,
|
||||
* while a ReadableStream is only brand-converted here — its locked/
|
||||
* disturbed state belongs to the later body-extraction phase. */
|
||||
if (!null_body &&
|
||||
if (!null_body && !json_body &&
|
||||
!(body->kind == SCR_DYN_HANDLE &&
|
||||
body->v.handle.tag == SCR_DYNH_WEB_STREAM)) {
|
||||
if (body->kind == SCR_DYN_BYTES) {
|
||||
@@ -4668,7 +4776,25 @@ ScrDyn *scr_fetch_response_new(ScrDyn *body, ScrDyn *init) {
|
||||
/* Extract BodyInit before validating the response metadata. This makes a
|
||||
* locked/disturbed stream win over the later status range, statusText HTTP
|
||||
* reason-phrase, and HeadersInit validation exactly as in pinned Node. */
|
||||
if (null_body) {
|
||||
if (json_body) {
|
||||
ScrDyn *serialized = serialize(json_data);
|
||||
if (!serialized) goto fail;
|
||||
if (serialized->kind != SCR_DYN_STR) {
|
||||
static const char unserializable[30] = "Value is not JSON serializable";
|
||||
scr_dyn_release(serialized);
|
||||
scr_throw_error_msg(SCR_ERR_TYPE, unserializable, sizeof unserializable);
|
||||
goto fail;
|
||||
}
|
||||
ScrStr *encoding = scr_str_new("utf8", 4);
|
||||
body_bytes = scr_bytes_from_str(serialized->v.str, encoding);
|
||||
scr_str_release(encoding);
|
||||
scr_dyn_release(serialized);
|
||||
stream = sf_stream_new_native();
|
||||
sf_stream_enqueue_bytes(stream, body_bytes);
|
||||
sf_stream_close(stream);
|
||||
scr_bytes_release(body_bytes);
|
||||
body_bytes = NULL;
|
||||
} else if (null_body) {
|
||||
stream = sf_stream_new_native();
|
||||
sf_stream_close(stream);
|
||||
} else if (body->kind == SCR_DYN_HANDLE &&
|
||||
@@ -4719,6 +4845,11 @@ ScrDyn *scr_fetch_response_new(ScrDyn *body, ScrDyn *init) {
|
||||
sf_push_header_text(header_pairs, "content-type",
|
||||
"text/plain;charset=UTF-8", 24);
|
||||
}
|
||||
if (json_body && !sf_pairs_have(header_pairs, "content-type")) {
|
||||
static const char json_type[16] = "application/json";
|
||||
scr_arr_push_ref(header_pairs, scr_str_new("content-type", 12));
|
||||
scr_arr_push_ref(header_pairs, scr_str_new(json_type, sizeof json_type));
|
||||
}
|
||||
|
||||
SfResponse *response = calloc(1, sizeof *response);
|
||||
if (!response) sf_oom();
|
||||
@@ -4744,6 +4875,15 @@ fail:
|
||||
return NULL;
|
||||
}
|
||||
|
||||
ScrDyn *scr_fetch_response_new(ScrDyn *body, ScrDyn *init) {
|
||||
return sf_response_construct(body, init, NULL, NULL);
|
||||
}
|
||||
|
||||
ScrDyn *scr_fetch_response_json_static(ScrDyn *data, ScrDyn *init) {
|
||||
return sf_response_construct(NULL, init, data ? data : scr_dyn_undefined(),
|
||||
&sf_response_json_serialize);
|
||||
}
|
||||
|
||||
/* Request uses the same body stream and Headers ownership as Response. */
|
||||
typedef struct SfRequest {
|
||||
size_t rc;
|
||||
@@ -4809,6 +4949,7 @@ ScrDyn *scr_fetch_request_new(ScrDyn *input, ScrDyn *init) {
|
||||
ScrStr *url_text = source ? scr_str_retain(source->url) : scr_dyn_string_coerce_js(input);
|
||||
if (!url_text) return NULL;
|
||||
ScrUrl *url = scr_url_new(url_text);
|
||||
if (!url) sf_throw_url_parse_failure(url_text);
|
||||
scr_str_release(url_text);
|
||||
if (!url) return NULL;
|
||||
if (url->userinfo->len) { scr_url_release(url); sf_type_error("Request cannot be constructed from a URL that includes credentials"); return NULL; }
|
||||
@@ -5118,6 +5259,7 @@ ScrPromise *scr_fetch_static(ScrStr *url, ScrDyn *init) {
|
||||
|
||||
ScrUrl *u = scr_url_new(url);
|
||||
if (!u) {
|
||||
sf_throw_url_parse_failure(url);
|
||||
scr_dyn_release(coerced_body);
|
||||
scr_arr_release(headers);
|
||||
scr_str_release(method);
|
||||
|
||||
@@ -166,6 +166,7 @@ ScrDyn *scr_fetch_request_new(ScrDyn *input, ScrDyn *init);
|
||||
ScrDyn *scr_fetch_headers_new(ScrDyn *init);
|
||||
ScrPromise *scr_fetch_static(ScrStr *url, ScrDyn *init); /* +1 promise<Response handle> */
|
||||
ScrDyn *scr_fetch_response_new(ScrDyn *body, ScrDyn *init); /* borrowed args; +1 Response handle or NULL pending */
|
||||
ScrDyn *scr_fetch_response_json_static(ScrDyn *data, ScrDyn *init); /* Response.json; borrowed args; +1 Response handle or NULL pending */
|
||||
ScrPromise *scr_fetch_response_json(ScrDyn *response); /* +1 promise<dyn> */
|
||||
ScrPromise *scr_fetch_response_text(ScrDyn *response); /* +1 promise<dyn> */
|
||||
ScrPromise *scr_fetch_response_array_buffer(ScrDyn *response); /* +1 promise<dyn> */
|
||||
|
||||
@@ -10489,6 +10489,72 @@ ScrStr *scr_date_to_iso(double ms) {
|
||||
return scr_str_new(buf, (size_t)len);
|
||||
}
|
||||
|
||||
/* Node's Date.prototype.toUTCString over a stored (TimeClip'd) time
|
||||
* value: "Www, DD Mmm YYYY HH:mm:ss GMT", a "-" and at least four digits
|
||||
* for negative years, and "Invalid Date" for NaN. Never throws. +1.
|
||||
* The text is assembled from function-local arrays rather than string
|
||||
* literals: literals would land in this unit's shared mergeable string
|
||||
* section and grow every executable, while these arrays are removed with
|
||||
* the function by --gc-sections when a program never calls it. */
|
||||
ScrStr *scr_date_to_utc_string(double ms) {
|
||||
/* Sized without the terminating NUL, so none is a C string the
|
||||
* compiler could move into a mergeable string section. */
|
||||
static const char invalid[12] = "Invalid Date";
|
||||
static const char weekdays[21] = "SunMonTueWedThuFriSat";
|
||||
static const char months[36] = "JanFebMarAprMayJunJulAugSepOctNovDec";
|
||||
if (!(fabs(ms) <= 8640000000000000.0)) return scr_str_new(invalid, sizeof invalid);
|
||||
double t = trunc(ms);
|
||||
double dayd = floor(t / 86400000.0);
|
||||
long long msday = (long long)(t - dayd * 86400000.0);
|
||||
long long days = (long long)dayd;
|
||||
int weekday = (int)(((days + 4) % 7 + 7) % 7);
|
||||
long long z = days + 719468;
|
||||
long long era = (z >= 0 ? z : z - 146096) / 146097;
|
||||
unsigned long long doe = (unsigned long long)(z - era * 146097);
|
||||
unsigned long long yoe = (doe - doe / 1460 + doe / 36524 - doe / 146096) / 365;
|
||||
long long y = (long long)yoe + era * 400;
|
||||
unsigned long long doy = doe - (365 * yoe + yoe / 4 - yoe / 100);
|
||||
unsigned long long mp = (5 * doy + 2) / 153;
|
||||
unsigned long long d = doy - (153 * mp + 2) / 5 + 1;
|
||||
unsigned long long m = mp < 10 ? mp + 3 : mp - 9;
|
||||
if (m <= 2) y += 1;
|
||||
int hh = (int)(msday / 3600000), mi = (int)(msday / 60000 % 60),
|
||||
ss = (int)(msday / 1000 % 60);
|
||||
char buf[40];
|
||||
size_t n = 0;
|
||||
memcpy(buf, weekdays + 3 * weekday, 3);
|
||||
n = 3;
|
||||
buf[n++] = ',';
|
||||
buf[n++] = ' ';
|
||||
buf[n++] = (char)('0' + d / 10);
|
||||
buf[n++] = (char)('0' + d % 10);
|
||||
buf[n++] = ' ';
|
||||
memcpy(buf + n, months + 3 * (m - 1), 3);
|
||||
n += 3;
|
||||
buf[n++] = ' ';
|
||||
if (y < 0) buf[n++] = '-';
|
||||
unsigned long long year = (unsigned long long)(y < 0 ? -y : y);
|
||||
char digits[20];
|
||||
size_t count = 0;
|
||||
do {
|
||||
digits[count++] = (char)('0' + year % 10);
|
||||
year /= 10;
|
||||
} while (year > 0);
|
||||
while (count < 4) digits[count++] = '0';
|
||||
while (count > 0) buf[n++] = digits[--count];
|
||||
int parts[3] = {hh, mi, ss};
|
||||
for (int i = 0; i < 3; i++) {
|
||||
buf[n++] = i == 0 ? ' ' : ':';
|
||||
buf[n++] = (char)('0' + parts[i] / 10);
|
||||
buf[n++] = (char)('0' + parts[i] % 10);
|
||||
}
|
||||
buf[n++] = ' ';
|
||||
buf[n++] = 'G';
|
||||
buf[n++] = 'M';
|
||||
buf[n++] = 'T';
|
||||
return scr_str_new(buf, n);
|
||||
}
|
||||
|
||||
/* ── new Date(dateString).getTime() ──────────────────────────────────────
|
||||
*
|
||||
* The BOUNDED date-string parse (documented divergence — V8's parser
|
||||
|
||||
@@ -102,6 +102,9 @@ double scr_date_get_time(double ms);
|
||||
* 0–9999). THROWS Node's "Invalid time value" RangeError on NaN or
|
||||
* |ms| > 8.64e15 and returns NULL; +1 otherwise. */
|
||||
ScrStr *scr_date_to_iso(double ms);
|
||||
/* Node's Date.prototype.toUTCString ("Thu, 01 Jan 1970 00:00:00 GMT";
|
||||
* "Invalid Date" for NaN). Never throws; +1. */
|
||||
ScrStr *scr_date_to_utc_string(double ms);
|
||||
/* new Date(dateString).getTime(): the BOUNDED parse (the X509 validity
|
||||
* shape + ECMA's format with an explicit offset); NaN elsewhere. */
|
||||
double scr_date_parse_get_time(ScrStr *s);
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
// A base method calling `this.set()` where only a subclass is ever
|
||||
// constructed devirtualizes to the subclass override; a polymorphic `this`
|
||||
// return resolves per declaring class, so the direct call's subclass
|
||||
// result upcasts to the base method's type (was an internal compiler error).
|
||||
class Cookies {
|
||||
values: string[] = [];
|
||||
set(value: string): this {
|
||||
this.values.push(value);
|
||||
return this;
|
||||
}
|
||||
delete(name: string): this {
|
||||
return this.set(`${name}=; Max-Age=0`);
|
||||
}
|
||||
}
|
||||
class MirroredCookies extends Cookies {
|
||||
mirror: string[] = [];
|
||||
override set(value: string): this {
|
||||
super.set(value);
|
||||
this.mirror.push(value.toUpperCase());
|
||||
return this;
|
||||
}
|
||||
}
|
||||
const cookies = new MirroredCookies();
|
||||
cookies.set("a=1").delete("b").set("c=3");
|
||||
console.log(cookies.values.join(" | "));
|
||||
console.log(cookies.mirror.join(" | "));
|
||||
console.log(cookies.delete("d") === cookies, cookies.values.length);
|
||||
@@ -0,0 +1,36 @@
|
||||
// Class fields typed as native fetch handles (Headers, Request, Response),
|
||||
// directly, optional, nullable, in arrays, and inside record fields, hold
|
||||
// the live handle: mutation through one reference is seen through another.
|
||||
class Holder {
|
||||
headers: Headers;
|
||||
list: Headers[] = [];
|
||||
maybe?: Response;
|
||||
req: Request | null = null;
|
||||
bag: { headers: Headers; status: number };
|
||||
constructor(headers: Headers) {
|
||||
this.headers = headers;
|
||||
this.bag = { headers, status: 1 };
|
||||
}
|
||||
}
|
||||
const holder = new Holder(new Headers({ a: "1" }));
|
||||
console.log(holder.headers.get("a"), holder.list.length, holder.maybe === undefined, holder.req);
|
||||
holder.list.push(new Headers({ b: "2" }));
|
||||
holder.maybe = new Response("x", { status: 201 });
|
||||
holder.req = new Request("http://localhost/p", { method: "POST" });
|
||||
console.log(holder.list[0]!.get("b"), holder.maybe.status, holder.req.method, holder.req.url);
|
||||
holder.headers.set("c", "3");
|
||||
console.log(holder.bag.headers.get("c"), holder.bag.headers === holder.headers);
|
||||
|
||||
// Symbol-keyed fields hold the handles the same way.
|
||||
const NATIVE = Symbol("native");
|
||||
class Wrapper {
|
||||
[NATIVE]: Response;
|
||||
constructor(body: string) {
|
||||
this[NATIVE] = new Response(body, { status: 202 });
|
||||
}
|
||||
get status(): number {
|
||||
return this[NATIVE].status;
|
||||
}
|
||||
}
|
||||
const wrapped = new Wrapper("ok");
|
||||
console.log(wrapped.status, wrapped[NATIVE].headers.get("content-type"));
|
||||
@@ -0,0 +1,36 @@
|
||||
// A getter that never returns a value compiles: annotated `never` or
|
||||
// unannotated (the removed-API getter idiom, which throws), and a getter
|
||||
// without a return statement, whose reads answer undefined.
|
||||
class Req {
|
||||
ok = 1;
|
||||
get page(): never {
|
||||
throw new Error("page has been removed");
|
||||
}
|
||||
get ua(): never {
|
||||
throw new TypeError("ua has been removed");
|
||||
}
|
||||
public get legacy() {
|
||||
throw new RangeError("legacy has been removed");
|
||||
}
|
||||
get nothing() {
|
||||
return;
|
||||
}
|
||||
}
|
||||
const r = new Req();
|
||||
try {
|
||||
console.log(r.page);
|
||||
} catch (e) {
|
||||
console.log(e instanceof Error ? `${e.name}: ${e.message}` : e);
|
||||
}
|
||||
try {
|
||||
const ua = r.ua;
|
||||
console.log("unreachable", ua);
|
||||
} catch (e) {
|
||||
console.log(e instanceof Error ? `${e.name}: ${e.message}` : e);
|
||||
}
|
||||
try {
|
||||
console.log(r.legacy);
|
||||
} catch (e) {
|
||||
console.log(e instanceof Error ? `${e.name}: ${e.message}` : e);
|
||||
}
|
||||
console.log(r.ok, r.nothing);
|
||||
@@ -0,0 +1,43 @@
|
||||
// Symbol-keyed (and literal-keyed) TypeScript fields without an
|
||||
// initializer take their declared type when the constructor assigns them
|
||||
// unconditionally; an optional one starts undefined. A field assigned
|
||||
// later (here from a method the constructor calls) reads through the
|
||||
// checked deferred-initialization slot.
|
||||
const Internal = Symbol("internal");
|
||||
const Registered = Symbol.for("pkg.registered");
|
||||
const Pending = Symbol("pending");
|
||||
|
||||
class Box {
|
||||
[Internal]: { n: number };
|
||||
[Registered]: string[];
|
||||
["literal"]: number;
|
||||
[Pending]?: string[];
|
||||
constructor(n: number) {
|
||||
this[Internal] = { n };
|
||||
this[Registered] = ["a"];
|
||||
this["literal"] = n * 2;
|
||||
}
|
||||
get n(): number {
|
||||
return this[Internal].n;
|
||||
}
|
||||
}
|
||||
|
||||
const b = new Box(5);
|
||||
console.log(b.n, b[Registered].length, b.literal, b[Pending] === undefined);
|
||||
b[Internal].n = 7;
|
||||
b[Registered].push("b");
|
||||
b[Pending] = ["p"];
|
||||
console.log(b.n, b[Registered].join(","), b[Pending]?.join(","));
|
||||
console.log(JSON.stringify(Object.entries({ literal: b.literal })));
|
||||
|
||||
class Late {
|
||||
[Internal]!: { n: number };
|
||||
constructor(flag: boolean) {
|
||||
if (flag) this[Internal] = { n: 1 };
|
||||
else this.init();
|
||||
}
|
||||
init(): void {
|
||||
this[Internal] = { n: 2 };
|
||||
}
|
||||
}
|
||||
console.log(new Late(true)[Internal].n, new Late(false)[Internal].n);
|
||||
@@ -0,0 +1,26 @@
|
||||
// Date.prototype.toUTCString: "Www, DD Mmm YYYY HH:mm:ss GMT" in UTC, a
|
||||
// leading "-" and at least four digits for negative years, "Invalid Date"
|
||||
// for an invalid time value.
|
||||
const times = [
|
||||
0,
|
||||
-1,
|
||||
1445412480000,
|
||||
951782400000, // 2000-02-29
|
||||
Date.UTC(1969, 11, 31, 23, 59, 59, 999),
|
||||
Date.UTC(0, 0, 1) - 1900 * 365.2425 * 86400000,
|
||||
Date.UTC(-1, 5, 15, 12, 30, 45),
|
||||
Date.UTC(-271821, 3, 20),
|
||||
8.64e15,
|
||||
-8.64e15,
|
||||
1234.9,
|
||||
-1234.9,
|
||||
NaN,
|
||||
8.64e15 + 1,
|
||||
];
|
||||
for (const t of times) {
|
||||
const d = new Date(t);
|
||||
console.log(d.toUTCString());
|
||||
}
|
||||
for (let day = 0; day < 7; day++) console.log(new Date(Date.UTC(2024, 0, day + 1)).toUTCString());
|
||||
for (let month = 0; month < 12; month++) console.log(new Date(Date.UTC(2023, month, 9, 8, 7, 6)).toUTCString());
|
||||
console.log(new Date("2015-10-21T07:28:00Z").toUTCString(), new Date("garbage").toUTCString());
|
||||
@@ -0,0 +1,18 @@
|
||||
// Array destructuring of a Map or Set walks its iterator: Map entries as
|
||||
// [key, value] pairs, Set values, holes and rest included.
|
||||
const m = new Map([["a", "1"], ["b", "2"], ["c", "3"]]);
|
||||
const [[name, value], ...rest] = m;
|
||||
console.log(name, value, rest.length, JSON.stringify(rest));
|
||||
const [first, , third] = m;
|
||||
console.log(JSON.stringify(first), JSON.stringify(third));
|
||||
const s = new Set([3, 1, 2]);
|
||||
const [x, y, ...others] = s;
|
||||
console.log(x, y, others);
|
||||
let [p] = new Map<string, number>([["only", 1]]);
|
||||
p = ["changed", 2];
|
||||
console.log(p);
|
||||
function firstKey(map: Map<string, number>): string {
|
||||
const [[k]] = map;
|
||||
return k;
|
||||
}
|
||||
console.log(firstKey(new Map([["z", 26]])));
|
||||
@@ -0,0 +1,63 @@
|
||||
// Headers iteration follows Fetch's "sort and combine": names in byte
|
||||
// order with their combined values, except that every Set-Cookie value is
|
||||
// visited on its own. get() still joins them; getSetCookie() lists them.
|
||||
const h = new Headers();
|
||||
h.append("set-cookie", "a=1; Path=/");
|
||||
h.append("x-b", "1");
|
||||
h.append("Set-Cookie", "b=2, c=3");
|
||||
h.append("x-a", "1");
|
||||
h.append("x-a", "2");
|
||||
h.append("set-cookie", "d=4");
|
||||
h.forEach((value, name) => console.log(`${name}: ${value}`));
|
||||
console.log(JSON.stringify(h.get("set-cookie")));
|
||||
console.log(JSON.stringify(h.getSetCookie()));
|
||||
|
||||
// A thisArg and the Headers object reach the callback.
|
||||
const self = { tag: "this" };
|
||||
h.forEach(function (this: typeof self, _value, name, headers) {
|
||||
if (name === "x-b") console.log(this.tag, headers === h);
|
||||
}, self);
|
||||
|
||||
// Iteration is live and indexes the expanded list: deleting the cookies
|
||||
// while visiting the first one shifts the next name into its slot.
|
||||
const live = new Headers();
|
||||
live.append("set-cookie", "a=1");
|
||||
live.append("set-cookie", "b=2");
|
||||
live.append("x-z", "z");
|
||||
const seen: string[] = [];
|
||||
live.forEach((value, name) => {
|
||||
seen.push(`${name}=${value}`);
|
||||
if (value === "a=1") live.delete("set-cookie");
|
||||
});
|
||||
console.log(seen.join(" | "));
|
||||
|
||||
// Appending a cookie during the walk is visited when it sorts later.
|
||||
const grow = new Headers();
|
||||
grow.append("set-cookie", "a=1");
|
||||
grow.append("x-y", "y");
|
||||
const grown: string[] = [];
|
||||
grow.forEach((value, name) => {
|
||||
grown.push(`${name}=${value}`);
|
||||
if (value === "a=1") grow.append("set-cookie", "b=2");
|
||||
});
|
||||
console.log(grown.join(" | "));
|
||||
|
||||
// Only set-cookie stays separate; cookie and other repeats combine.
|
||||
const plain = new Headers([
|
||||
["cookie", "a=1"],
|
||||
["cookie", "b=2"],
|
||||
["accept", "x"],
|
||||
["accept", "y"],
|
||||
]);
|
||||
plain.forEach((value, name) => console.log(`${name}: ${value}`));
|
||||
console.log(JSON.stringify(plain.getSetCookie()));
|
||||
|
||||
// A response's headers walk the same way.
|
||||
const response = new Response("ok", {
|
||||
headers: [
|
||||
["set-cookie", "s=1"],
|
||||
["set-cookie", "t=2"],
|
||||
["content-type", "text/plain"],
|
||||
],
|
||||
});
|
||||
response.headers.forEach((value, name) => console.log(`${name}: ${value}`));
|
||||
@@ -0,0 +1,41 @@
|
||||
// An unparsable fetch() or Request URL is undici's TypeError
|
||||
// "Failed to parse URL from <input>", whose non-enumerable cause is the
|
||||
// URL parser's TypeError.
|
||||
function report(label: string, e: unknown): void {
|
||||
const serialized = JSON.stringify(e);
|
||||
if (!(e instanceof Error)) {
|
||||
console.log(label, "not an Error");
|
||||
return;
|
||||
}
|
||||
console.log(label, e.name, e.message, e instanceof TypeError);
|
||||
console.log(" has cause:", "cause" in e, "own enumerable:", serialized);
|
||||
console.log(
|
||||
" cause:",
|
||||
e.cause instanceof Error ? `${e.cause.name} ${e.cause.message} ${e.cause instanceof TypeError}` : String(e.cause),
|
||||
);
|
||||
}
|
||||
|
||||
async function main(): Promise<void> {
|
||||
for (const input of ["http://127.0.0.1:1http://x/", "/relative/path", "", "http://[::1"]) {
|
||||
try {
|
||||
await fetch(input);
|
||||
console.log("resolved?");
|
||||
} catch (e) {
|
||||
report(`fetch(${JSON.stringify(input)})`, e);
|
||||
}
|
||||
}
|
||||
try {
|
||||
new Request("not a url");
|
||||
console.log("constructed?");
|
||||
} catch (e) {
|
||||
report("new Request", e);
|
||||
}
|
||||
// A valid URL with credentials keeps its own message.
|
||||
try {
|
||||
await fetch("http://user:pass@127.0.0.1:1/");
|
||||
} catch (e) {
|
||||
report("credentials", e);
|
||||
}
|
||||
}
|
||||
|
||||
void main();
|
||||
@@ -0,0 +1,44 @@
|
||||
// fetch() sends its header list's combined values: repeated Set-Cookie
|
||||
// request headers go out as one line, the values joined by ", ", at the
|
||||
// position of the first one, whatever form the headers were given in.
|
||||
import { createServer } from "node:http";
|
||||
|
||||
function shown(raw: string[]): string {
|
||||
const lines: string[] = [];
|
||||
for (let i = 0; i < raw.length; i += 2) {
|
||||
const name = raw[i]!;
|
||||
const lower = name.toLowerCase();
|
||||
if (lower === "host" || lower === "connection" || lower.startsWith("accept") || lower.startsWith("sec-") || lower === "user-agent") continue;
|
||||
lines.push(`${name}: ${raw[i + 1]}`);
|
||||
}
|
||||
return lines.join(" | ");
|
||||
}
|
||||
|
||||
const server = createServer((req, res) => {
|
||||
res.end(shown(req.rawHeaders));
|
||||
});
|
||||
|
||||
server.listen(0, "127.0.0.1", async () => {
|
||||
const addr = server.address();
|
||||
const port = typeof addr === "object" && addr ? addr.port : 0;
|
||||
const url = `http://127.0.0.1:${port}/`;
|
||||
|
||||
const headers = new Headers();
|
||||
headers.append("set-cookie", "a=1");
|
||||
headers.append("x-first", "1");
|
||||
headers.append("set-cookie", "b=2");
|
||||
console.log(await (await fetch(url, { headers })).text());
|
||||
|
||||
const pairs: [string, string][] = [
|
||||
["x-before", "0"],
|
||||
["set-cookie", "c=3"],
|
||||
["set-cookie", "d=4; Path=/"],
|
||||
];
|
||||
console.log(await (await fetch(url, { headers: pairs })).text());
|
||||
|
||||
console.log(await (await fetch(url, { headers: { "set-cookie": "e=5", "x-record": "r" } })).text());
|
||||
|
||||
const single = new Headers({ "set-cookie": "only=1" });
|
||||
console.log(await (await fetch(url, { headers: single })).text());
|
||||
server.close();
|
||||
});
|
||||
@@ -0,0 +1,19 @@
|
||||
// A generic call with a literal `undefined` argument binds T = undefined;
|
||||
// the parameter holds undefined like a declared `v: undefined` parameter.
|
||||
function kind<T>(v: T): string {
|
||||
return typeof v;
|
||||
}
|
||||
console.log(kind(undefined), kind(null), kind(1), kind("s"));
|
||||
function same<T>(a: T, b: T): boolean {
|
||||
return a === b;
|
||||
}
|
||||
console.log(same(undefined, undefined));
|
||||
function wrap<T>(v: T): { v: T } {
|
||||
return { v };
|
||||
}
|
||||
const w = wrap(undefined);
|
||||
console.log(JSON.stringify(w), w.v === undefined, "v" in w);
|
||||
function describe<T>(label: string, v: T): string {
|
||||
return `${label}=${String(v)}`;
|
||||
}
|
||||
console.log(describe("u", undefined));
|
||||
@@ -0,0 +1,44 @@
|
||||
// JSON.stringify of a class instance whose class (or a base) declares
|
||||
// toJSON serializes what toJSON(key) returns: the root key is "", nested
|
||||
// hooks run through the same walker, and a toJSON answering undefined
|
||||
// makes the whole result undefined.
|
||||
class Url {
|
||||
href: string;
|
||||
constructor(href: string) {
|
||||
this.href = href;
|
||||
}
|
||||
toJSON(): string {
|
||||
return this.href;
|
||||
}
|
||||
}
|
||||
class NextUrl extends Url {
|
||||
base = "ignored";
|
||||
}
|
||||
class Keyed {
|
||||
toJSON(key: string): { key: string; at: Url[] } {
|
||||
return { key, at: [new Url("n")] };
|
||||
}
|
||||
}
|
||||
class Missing {
|
||||
toJSON(): undefined {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
console.log(JSON.stringify(new Url("http://x/")));
|
||||
console.log(JSON.stringify(new NextUrl("http://y/")));
|
||||
console.log(JSON.stringify(new Keyed()));
|
||||
console.log(JSON.stringify(new Keyed(), null, 2));
|
||||
function serialize(value: Missing): string {
|
||||
const text = JSON.stringify(value);
|
||||
return text === undefined ? "undefined result" : `text ${text}`;
|
||||
}
|
||||
console.log(serialize(new Missing()), JSON.stringify(new Missing()) === undefined);
|
||||
let calls = 0;
|
||||
class Counted {
|
||||
toJSON(): number {
|
||||
calls++;
|
||||
return calls;
|
||||
}
|
||||
}
|
||||
const c = new Counted();
|
||||
console.log(JSON.stringify(c), JSON.stringify(c), calls);
|
||||
@@ -0,0 +1,38 @@
|
||||
// JSON.stringify of an unknown (or generic) root whose value is undefined,
|
||||
// a function, or a symbol returns undefined, not text. Wherever the program
|
||||
// can tell the two apart (undefined/null comparisons, typeof, ??, ?., truth
|
||||
// tests, returns declaring undefined, and bindings used that way) the
|
||||
// native result is the actual undefined. Nested values follow Node's
|
||||
// member rules: dropped from objects, null in arrays.
|
||||
const u: unknown = undefined;
|
||||
const f: unknown = () => 1;
|
||||
const s: unknown = Symbol("x");
|
||||
const ok: unknown = { a: [1, undefined] };
|
||||
for (const v of [u, f, s, ok]) {
|
||||
const t = JSON.stringify(v);
|
||||
console.log(t === undefined, typeof t, t ?? "fb", t?.length, t == null, t !== undefined);
|
||||
}
|
||||
for (const v of [u, ok]) {
|
||||
const t = JSON.stringify(v);
|
||||
if (t) console.log("truthy", t.length);
|
||||
else console.log("falsy");
|
||||
console.log(!t, t ? "y" : "n", t || "or", t && "and");
|
||||
}
|
||||
console.log(JSON.stringify(u) === undefined, typeof JSON.stringify(f), JSON.stringify(s) ?? "fb2", JSON.stringify(u)?.length);
|
||||
console.log(JSON.stringify(u) || "or2", JSON.stringify(ok) || "or3", !JSON.stringify(u));
|
||||
function raw<T>(v: T): string | undefined {
|
||||
return JSON.stringify(v);
|
||||
}
|
||||
console.log(typeof raw(u), raw(ok));
|
||||
function ser<T>(v: T): string {
|
||||
const text = JSON.stringify(v);
|
||||
if (text === undefined) return "undefined result";
|
||||
return `string ${text} ${text.length}`;
|
||||
}
|
||||
console.log(ser(u), ser({ a: 1 }), ser(1), ser("s"), ser(ok));
|
||||
// Unobserved uses keep the plain string path.
|
||||
const plain = JSON.stringify(ok);
|
||||
console.log(plain.length, plain, `${JSON.stringify(u)}`, "x" + JSON.stringify(u));
|
||||
// A module-scope binding keeps the string slot; direct uses still observe.
|
||||
const top = JSON.stringify(ok);
|
||||
console.log(top.length, JSON.stringify(u) === undefined);
|
||||
@@ -0,0 +1,37 @@
|
||||
// `...(cond && { k: v })` spreads nothing when cond is a falsy primitive
|
||||
// (false, 0, ""), so it is the optional-field idiom `...(cond ? { k: v } : {})`:
|
||||
// cond evaluates once, v only when taken. (Fields print one by one: the
|
||||
// declared-shape property order of spread-built records is a documented
|
||||
// limitation.)
|
||||
type C = { name: string; name2: string; httpOnly?: boolean; maxAge?: number; path?: string; sameSite?: string };
|
||||
function parse(attrs: Record<string, string | undefined>, flag: boolean): C {
|
||||
const httponly = attrs.httponly;
|
||||
const maxage = attrs["max-age"];
|
||||
const samesite = attrs.samesite;
|
||||
let calls = 0;
|
||||
const lower = (s: string): string => {
|
||||
calls++;
|
||||
return s.toLowerCase();
|
||||
};
|
||||
const c: C = {
|
||||
name: "a",
|
||||
...(flag && { httpOnly: true }),
|
||||
...(httponly && { path: httponly }),
|
||||
...(typeof maxage === "string" && { maxAge: Number(maxage) }),
|
||||
name2: "/",
|
||||
...(samesite && { sameSite: lower(samesite) }),
|
||||
};
|
||||
console.log("calls", calls);
|
||||
return c;
|
||||
}
|
||||
for (const [attrs, flag] of [
|
||||
[{}, false],
|
||||
[{ httponly: "yes", "max-age": "10", samesite: "Lax" }, true],
|
||||
[{ httponly: "", "max-age": "", samesite: "" }, true],
|
||||
] as [Record<string, string | undefined>, boolean][]) {
|
||||
const c = parse(attrs, flag);
|
||||
console.log(c.name, c.name2, c.httpOnly, c.path, c.maxAge, c.sameSite, "httpOnly" in c, "path" in c);
|
||||
}
|
||||
const n: number = Number(process.argv.length > 99 ? "1" : "0");
|
||||
const d: { v?: number; name: string } = { name: "n", ...(n && { v: n }) };
|
||||
console.log(d.name, d.v, "v" in d);
|
||||
@@ -0,0 +1,28 @@
|
||||
// An optional call of a standard-library method that always exists on a
|
||||
// never-nullish receiver is the plain call (`getSetCookie?.()` is always
|
||||
// present on Node 24).
|
||||
const h = new Headers();
|
||||
h.append("set-cookie", "a=1");
|
||||
h.append("set-cookie", "b=2");
|
||||
const cookies = h.getSetCookie?.() ?? [];
|
||||
console.log(JSON.stringify(cookies));
|
||||
console.log(h.get?.("set-cookie"));
|
||||
console.log(h.has?.("x-missing"));
|
||||
h.set?.("x-set", "1");
|
||||
h.forEach?.((value, name) => console.log(`${name}=${value}`));
|
||||
|
||||
// The receiver evaluates once; arguments evaluate after it.
|
||||
let reads = 0;
|
||||
function headers(): Headers {
|
||||
reads++;
|
||||
return h;
|
||||
}
|
||||
function arg(): string {
|
||||
console.log("argument after receiver", reads);
|
||||
return "x-set";
|
||||
}
|
||||
console.log(headers().get?.(arg()), reads);
|
||||
|
||||
// Strings and arrays: the same rule.
|
||||
const s = "Hello";
|
||||
console.log(s.toUpperCase?.(), [3, 1, 2].sort?.().join(","));
|
||||
@@ -0,0 +1,27 @@
|
||||
// .toString() on a flow-narrowed union whose storage union is wider: the
|
||||
// read retags to the proven arms, then formats like String().
|
||||
import type { OutgoingHttpHeaders } from "node:http";
|
||||
|
||||
function host(headers?: OutgoingHttpHeaders): string {
|
||||
if (headers?.host && !Array.isArray(headers.host)) return headers.host.toString();
|
||||
return "";
|
||||
}
|
||||
console.log(JSON.stringify([host({ host: "a:1" }), host({ host: 3 }), host({}), host()]));
|
||||
|
||||
function label(v: string | number | string[] | undefined): string {
|
||||
if (v === undefined) return "none";
|
||||
if (Array.isArray(v)) return `list(${v.length})`;
|
||||
return v.toString();
|
||||
}
|
||||
console.log(label("x"), label(1.5), label(-0), label(["a", "b"]), label(undefined));
|
||||
|
||||
function flag(v: boolean | number | null): string {
|
||||
return v === null ? "null" : v.toString();
|
||||
}
|
||||
console.log(flag(true), flag(false), flag(1e21), flag(null));
|
||||
|
||||
type Field = string | number | { nested: string };
|
||||
function field(f: Field): string {
|
||||
return typeof f === "object" ? f.nested : f.toString();
|
||||
}
|
||||
console.log(field("s"), field(42), field({ nested: "n" }));
|
||||
@@ -0,0 +1,30 @@
|
||||
// Response.json(data, init): ResponseInit converts, the data serializes with
|
||||
// JSON.stringify (TypeError when it answers undefined), then the response
|
||||
// initializes like the constructor, defaulting content-type to
|
||||
// application/json.
|
||||
function show(label: string, make: () => Response): void {
|
||||
try {
|
||||
const r = make();
|
||||
console.log(label, r.status, JSON.stringify(r.statusText), r.headers.get("content-type"), r.headers.get("x-a"));
|
||||
} catch (e) {
|
||||
console.log(label, e instanceof Error ? `${e.name}: ${e.message}` : String(e));
|
||||
}
|
||||
}
|
||||
show("plain", () => Response.json({ ok: true }));
|
||||
show("init", () => Response.json([1, 2], { status: 201, statusText: "Made", headers: { "x-a": "1" } }));
|
||||
show("content-type", () => Response.json("s", { headers: { "content-type": "text/x" } }));
|
||||
show("headers object", () => Response.json(null, { headers: new Headers([["x-a", "h"]]) }));
|
||||
show("undefined", () => Response.json(undefined));
|
||||
const fn: unknown = () => 1;
|
||||
show("function", () => Response.json(fn));
|
||||
show("range", () => Response.json({}, { status: 99 }));
|
||||
show("undefined before range", () => Response.json(undefined, { status: 99 }));
|
||||
show("null body status", () => Response.json({}, { status: 204 }));
|
||||
show("bad header", () => Response.json({}, { headers: { "bad name": "x" } }));
|
||||
show("undefined before bad header", () => Response.json(undefined, { headers: { "bad name": "x" } }));
|
||||
show("bad statusText", () => Response.json({}, { statusText: "a\nb" }));
|
||||
const data: unknown = JSON.parse('{"a":[1,"x",null],"n":{"deep":true}}');
|
||||
const r = Response.json({ data, d: new Date(0), skip: undefined });
|
||||
console.log(r.bodyUsed, await r.text(), r.bodyUsed);
|
||||
const parsed: unknown = await Response.json({ round: [1, 2] }).json();
|
||||
console.log(JSON.stringify(parsed));
|
||||
@@ -5,7 +5,7 @@ date-formatters.ts:4:1 - error SC2020: 'Date.prototype.toString' is part of the
|
||||
| ^~~~~~~~~~~~~~~
|
||||
5 | date.toLocaleString();
|
||||
|
||||
hint: getTime(), valueOf(), toISOString(), toJSON(), the local/UTC calendar getters, and getTimezoneOffset() are supported; Date setters and locale/string formatters have no lowering
|
||||
hint: getTime(), valueOf(), toISOString(), toUTCString(), toJSON(), the local/UTC calendar getters, and getTimezoneOffset() are supported; Date setters and locale/string formatters have no lowering
|
||||
|
||||
date-formatters.ts:5:1 - error SC2020: 'Date.prototype.toLocaleString' is part of the standard library types but has no scriptc lowering yet
|
||||
|
||||
@@ -14,4 +14,4 @@ date-formatters.ts:5:1 - error SC2020: 'Date.prototype.toLocaleString' is part o
|
||||
| ^~~~~~~~~~~~~~~~~~~~~
|
||||
6 |
|
||||
|
||||
hint: getTime(), valueOf(), toISOString(), toJSON(), the local/UTC calendar getters, and getTimezoneOffset() are supported; Date setters and locale/string formatters have no lowering
|
||||
hint: getTime(), valueOf(), toISOString(), toUTCString(), toJSON(), the local/UTC calendar getters, and getTimezoneOffset() are supported; Date setters and locale/string formatters have no lowering
|
||||
@@ -66,7 +66,7 @@ stdlib-fence.ts:25:1 - error SC2020: 'Date.prototype.setUTCFullYear with argumen
|
||||
| ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
26 | const sameDate = mutableDate === mutableDate; // scalar storage must not fake object identity
|
||||
|
||||
hint: getTime(), valueOf(), toISOString(), toJSON(), the local/UTC calendar getters, and getTimezoneOffset() are supported; Date setters and locale/string formatters have no lowering
|
||||
hint: getTime(), valueOf(), toISOString(), toUTCString(), toJSON(), the local/UTC calendar getters, and getTimezoneOffset() are supported; Date setters and locale/string formatters have no lowering
|
||||
|
||||
stdlib-fence.ts:26:18 - error SC1043: comparing non-number, non-string values are not supported yet
|
||||
|
||||
|
||||
@@ -163,6 +163,7 @@ describe(`static fetch differential${sanitize ? " (sanitized)" : ""}`, () => {
|
||||
"static-listener-noncallable",
|
||||
"static-abort-throw",
|
||||
"static-dispatch-throw",
|
||||
"static-response-json",
|
||||
] as const;
|
||||
test.for(
|
||||
staticCases.flatMap((name) => (["llvm"] as const).map((backend) => [name, backend] as const)),
|
||||
|
||||
@@ -552,6 +552,7 @@ describe("library profile fences", () => {
|
||||
"stdlib.date.now",
|
||||
"stdlib.date.parse",
|
||||
"stdlib.date.toISOString",
|
||||
"stdlib.date.toUTCString",
|
||||
"stdlib.date.valueOf",
|
||||
]);
|
||||
expect(r.profile.fences[0]!.surfaces.every((s) => s.detector !== undefined)).toBe(true);
|
||||
|
||||
Reference in New Issue
Block a user