Type user code from npm package declarations at a checked, identity-preserving boundary

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Malte Ubl
2026-10-22 18:54:55 -07:00
co-authored by Claude Opus 5.5
parent f593e17e1d
commit 17bef8c03b
48 changed files with 7196 additions and 16 deletions
+31 -1
View File
@@ -153,6 +153,8 @@ export interface DynHost extends WalkerHost {
/** See moduleUsesDynBuiltinStream. */
usesDynBuiltinStream?(): boolean;
liveDynUnionRefAdapter(t: IrType & { kind: "union" }): string;
/** The module has declaration-typed boundary casts (IrModule.declBoundary). */
declBoundary?(): boolean;
dynPromiseAdapter(t: IrType): string;
isErrorClass(className: string): boolean;
classSubtypes(className: string): readonly string[];
@@ -4526,6 +4528,31 @@ export class LlDyn {
/** The dyn argument spelling of one static value: dyn passes through
* (+1), funcs box anonymously, everything else rides toDyn. `expr` is
* BORROWED in every arm. Emits into the given builder. */
/** A live capsule of a record or array value (+1; the value is
* borrowed), as `dynFrom` with `liveRef` builds it; null for other
* types. Closures boxed in a declaration-typed boundary program return
* their records and arrays this way. */
private liveToDyn(B: BlockBuilder, t: IrType, expr: string): string | null {
const host = this.host;
if (t.kind === "union") {
const arms = host.unionsById.get(t.unionId)?.arms ?? [];
if (!arms.some((arm) => arm.kind === "record" || arm.kind === "array")) return null;
const boxed = B.tmp();
B.line(`${boxed} = call ptr @${host.liveDynUnionRefAdapter(t)}(ptr ${expr})`);
return boxed;
}
if ((t.kind !== "record" && t.kind !== "array") || !streamTypedRefEligible(t)) return null;
const key = typeKey(t);
const adapter = host.liveDynRefAdapter(t);
const rc = vAdapters(host, t);
const boxed = B.tmp();
B.line(
`${boxed} = call ptr ${typedRefConstructor(host, t)}(ptr ${expr}, ptr ${rc.retain}, ptr ${rc.release}, ptr ${host.cstr(key)}, ${host.sizeType} ${Buffer.byteLength(key, "utf8")}, ptr @${adapter.snapshot}, ptr ${adapter.commit})`,
);
for (const line of bindLiveArrayOps(host, adapter, boxed)) B.line(line);
return boxed;
}
private toDynExpr(B: BlockBuilder, t: IrType, expr: string): string {
if (t.kind === "dyn") return this.retainDyn(B, expr);
if (t.kind === "func") {
@@ -4689,7 +4716,10 @@ export class LlDyn {
const r = B.tmp();
B.line(`${r} = call ${retTy} ${fn}(${callArgs})`);
this.pendingBail(B, "dfkr", () => {}, "ptr null");
const out = this.toDynExpr(B, t.ret, r);
const out =
this.host.declBoundary?.() === true
? (this.liveToDyn(B, t.ret, r) ?? this.toDynExpr(B, t.ret, r))
: this.toDynExpr(B, t.ret, r);
if (isRefCounted(t.ret)) B.line(`call void ${releaseSym(host, t.ret)}(ptr ${r})`);
B.terminate(`ret ptr ${out}`);
}
@@ -1597,6 +1597,7 @@ export class LlEmitter {
liveDynRefAdapter: (type) => this.liveDynRefAdapter(type),
usesDynBuiltinStream: () => this.usesDynBuiltinStream,
liveDynUnionRefAdapter: (type) => this.liveDynUnionRefAdapter(type),
declBoundary: () => this.mod.declBoundary === true,
dynPromiseAdapter: (type) => this.dynPromiseAdapter(type),
isErrorClass: (name) => this.classMeta.get(name)?.root.def.name === "%Error",
classRootOf: (name) => this.classMeta.get(name)?.root.def.name,
@@ -1570,6 +1570,7 @@ export const LIB_FN_SYMS: Record<string, string> = {
"dyn.objValueList": "scr_dyn_obj_value_list",
"dyn.objEntries": "scr_dyn_obj_entries",
"dyn.structuredClone": "scr_structured_clone",
"dyn.declAdopt": "scr_dyn_decl_adopt",
"dyn.cloneMissing": "scr_structured_clone_missing",
"dyn.cloneTransferFail": "scr_structured_clone_transfer_fail",
"dyn.cloneTransferFailWith": "scr_structured_clone_transfer_fail_with",
@@ -0,0 +1,735 @@
/* The declaration-typed boundary's projection pass.
*
* An opted-in npm package compiles from its JavaScript, and the program the
* lowering compiles types it from that JavaScript: where the package's
* published declarations are richer (zod's `z.infer`), user code sees `any`.
* This pass asks the DECLARATION world (the same entry checked against the
* packages' declarations) for the types of those user-code positions. Where
* the declared type is pure data, it is printed as structural TypeScript into
* a generated global declaration file, and the user file is rewritten in the
* lowering world:
*
* - type positions that name package types (`z.infer<typeof S>`) name the
* projected alias instead;
* - package-produced expressions (`S.parse(x)`, `r.data`) are wrapped in
* `(<expr> as __scriptc_decl_N)`, the boundary cast;
* - untyped parameters of user callbacks the package calls gain the alias.
*
* The lowering world is then rebuilt from the rewritten text, so the
* checker's own inference carries the projected types through the rest of
* the user code, and the lowering sees an ordinary checked cast at each
* crossing (lowered with `adopt`, see narrowing.ts). No edit adds or removes
* a line break, so positions keep their lines. IR records are structural,
* so a printed type interns to the same shape as any equal type.
*
* Only programs whose user code has such positions are rewritten; every
* other program is untouched (the zero-cost rule). See
* scriptc-perf/results/design-decl-boundary/DESIGN.md. */
import * as ts from "./ts7/adapter.js";
/** Alias names of projected types start with this prefix; the lowering
* recognizes boundary casts and parameters by it. */
export const DECL_BOUNDARY_PREFIX = "__scriptc_decl_";
/** The generated declaration file's base name. */
export const DECL_BOUNDARY_FILE = "__scriptc_decl_boundary__.d.ts";
export function isDeclBoundaryFile(fileName: string): boolean {
return fileName.split("\\").join("/").endsWith(`/${DECL_BOUNDARY_FILE}`);
}
export function isDeclBoundaryTypeName(name: string): boolean {
return name.startsWith(DECL_BOUNDARY_PREFIX);
}
/** True for `T` in `x as T` / `(p: T) =>` when T names a projected alias. */
export function isDeclBoundaryTypeNode(node: ts.TypeNode | undefined): boolean {
return (
node !== undefined &&
ts.isTypeReferenceNode(node) &&
ts.isIdentifier(node.typeName) &&
isDeclBoundaryTypeName(node.typeName.text) &&
node.typeArguments === undefined
);
}
interface Edit {
start: number;
end: number;
text: string;
/** Tie-break among edits at one position (lower first). */
rank?: number;
}
export interface DeclProjection {
/** Rewritten text per user file. */
files: Map<string, string>;
/** The generated declaration file (path and text). */
declFile: string;
declText: string;
/** Human-readable plan (SCRIPTC_DEBUG_DECL_BOUNDARY). */
plan: string[];
}
export interface DeclProjectionInput {
jsProgram: ts.Program;
declProgram: ts.Program;
/** User source files to consider (TypeScript, outside node_modules). */
userFiles: readonly string[];
/** Directory of the generated declaration file. */
outDir: string;
}
function isPackageDeclarationFile(sf: ts.SourceFile | undefined): boolean {
if (sf === undefined || !sf.isDeclarationFile) return false;
const f = sf.fileName.split("\\").join("/");
// The TypeScript standard library ships inside node_modules too.
if (/(^|\/)lib(\.[\w.-]+)?\.d\.ts$/.test(f) || f.startsWith("bundled:")) return false;
return (
f.includes("/node_modules/") &&
!f.includes("/node_modules/@types/node/") &&
!f.includes("/node_modules/typescript/") &&
!f.includes("/node_modules/@typescript/")
);
}
function declaredInPackage(symbol: ts.Symbol | undefined): boolean {
if (symbol === undefined) return false;
return symbol.declarations.some((d) => isPackageDeclarationFile(ts.getSourceFileOfNode(d)));
}
function declaredInUserFile(symbol: ts.Symbol | undefined): boolean {
if (symbol === undefined) return false;
const decls = symbol.declarations;
return (
decls.length > 0 &&
decls.every((d) => {
const sf = ts.getSourceFileOfNode(d);
return !sf.isDeclarationFile && !sf.fileName.split("\\").join("/").includes("/node_modules/");
})
);
}
const ANYISH = ts.TypeFlags.Any | ts.TypeFlags.Unknown;
const READONLY_CHECK_FLAG = 8;
/** Prints declaration-world types as structural TypeScript, or answers null
* when a type is not pure data (DESIGN.md §4.3). */
class TypePrinter {
readonly aliases = new Map<string, string>();
readonly declarations: string[] = [];
private readonly inProgress = new Map<number, { name: string | null }>();
constructor(private readonly checker: ts.TypeChecker) {}
/** The alias naming a projectable type, or null. Top-level any/unknown
* never projects. */
project(type: ts.Type): string | null {
if (type.flags & ANYISH) return null;
const text = this.print(type, 0);
if (text === null || text === "unknown") return null;
let name = this.aliases.get(text);
if (name === undefined) {
name = `${DECL_BOUNDARY_PREFIX}${this.aliases.size}`;
this.aliases.set(text, name);
this.declarations.push(`type ${name} = ${text};`);
}
return name;
}
private print(type: ts.Type, depth: number): string | null {
if (depth > 24) return null;
const f = type.flags;
if (f & ANYISH) return "unknown";
if (f & ts.TypeFlags.String) return "string";
if (f & ts.TypeFlags.Number) return "number";
if (f & ts.TypeFlags.Boolean) return "boolean";
if (f & ts.TypeFlags.Null) return "null";
if (f & ts.TypeFlags.Undefined) return "undefined";
if (f & ts.TypeFlags.EnumLiteral || f & ts.TypeFlags.Enum) return null;
if (f & ts.TypeFlags.BooleanLiteral) return type.intrinsicName === "true" ? "true" : "false";
if (f & ts.TypeFlags.StringLiteral) return JSON.stringify(String(type.value));
if (f & ts.TypeFlags.NumberLiteral) {
const v = Number(type.value);
return Number.isFinite(v) ? String(v) : null;
}
if (type.isUnionType()) {
const parts: string[] = [];
for (const arm of type.getTypes()) {
const p = this.print(arm, depth + 1);
if (p === null || p === "unknown") return null;
if (!parts.includes(p)) parts.push(p);
}
// `true | false` reads better as boolean, and maps the same.
const t = parts.indexOf("true");
const fl = parts.indexOf("false");
if (t >= 0 && fl >= 0) {
parts.splice(Math.max(t, fl), 1);
parts.splice(Math.min(t, fl), 1, "boolean");
}
return parts.map((p) => (/=>/.test(p) ? `(${p})` : p)).join(" | ");
}
if (type.isIntersectionType()) {
// Brands: drop members that only carry unique-symbol keys.
const kept = type.getTypes().filter((m) => !this.isBrandOnly(m));
if (kept.length === 1) return this.print(kept[0]!, depth + 1);
if (kept.length === 0) return null;
if (!kept.every((m) => (m.flags & ts.TypeFlags.Object) !== 0)) return null;
return this.printObject(type, depth);
}
if (f & ts.TypeFlags.Object) return this.printObject(type, depth);
return null;
}
private isBrandOnly(type: ts.Type): boolean {
if (!(type.flags & ts.TypeFlags.Object) || type.isIntersectionType()) return false;
const props = this.checker.getPropertiesOfType(type);
return props.length > 0 && props.every((p) => String(p.escapedName).startsWith("__@"));
}
private printObject(type: ts.Type, depth: number): string | null {
const checker = this.checker;
if (checker.isArrayType(type)) {
const [elem] = checker.getTypeArguments(type);
if (elem === undefined) return null;
const e = this.print(elem, depth + 1);
if (e === null) return null;
const ro = type.getSymbol()?.escapedName === "ReadonlyArray" ? "readonly " : "";
return `${ro}(${e})[]`;
}
if (checker.isTupleType(type)) {
const target = type.getTarget() ?? type;
const flags = target.elementFlags ?? [];
if (flags.some((fl) => fl !== ts.ElementFlags.Required)) return null;
const elems = checker.getTypeArguments(type).slice(0, flags.length);
const out: string[] = [];
for (const e of elems) {
const p = this.print(e, depth + 1);
if (p === null) return null;
out.push(p);
}
return `${target.readonly === true ? "readonly " : ""}[${out.join(", ")}]`;
}
const symbol = type.getSymbol();
if (symbol !== undefined) {
if (symbol.flags & (ts.SymbolFlags.Class | ts.SymbolFlags.Function | ts.SymbolFlags.Method))
return null;
// Interfaces: only user-declared ones describe plain data reliably
// (package and lib interfaces often describe class instances).
if (symbol.flags & ts.SymbolFlags.Interface && !declaredInUserFile(symbol)) return null;
}
if (
checker.getCallSignatures(type).length > 0 ||
checker.getConstructSignatures(type).length > 0
)
return null;
const memo = this.inProgress.get(type.id);
if (memo !== undefined) {
memo.name ??= `${DECL_BOUNDARY_PREFIX}r${type.id}`;
return memo.name;
}
const frame: { name: string | null } = { name: null };
this.inProgress.set(type.id, frame);
try {
const members: string[] = [];
let readonlyCount = 0;
const props = checker.getPropertiesOfType(type);
for (const prop of props) {
const key = String(prop.escapedName);
if (key.startsWith("__@")) continue; // symbol-keyed (brands)
if (
prop.flags &
(ts.SymbolFlags.Method | ts.SymbolFlags.GetAccessor | ts.SymbolFlags.SetAccessor)
)
return null;
const propType = checker.getTypeOfSymbol(prop);
const optional = (prop.flags & ts.SymbolFlags.Optional) !== 0;
let p = this.print(propType, depth + 1);
if (p === null) return null;
const readonly = (prop.checkFlags & READONLY_CHECK_FLAG) !== 0 || hasReadonlyModifier(prop);
if (readonly) readonlyCount++;
const name = /^[A-Za-z_$][\w$]*$/.test(key) ? key : JSON.stringify(key);
if (optional && !/\bundefined\b/.test(p)) p = `${p} | undefined`;
members.push(`${readonly ? "readonly " : ""}${name}${optional ? "?" : ""}: ${p}`);
}
for (const info of checker.getIndexInfosOfType(type)) {
if (!(info.keyType.flags & ts.TypeFlags.String)) return null;
const v = this.print(info.type, depth + 1);
if (v === null) return null;
members.push(`[key: string]: ${v}`);
}
if (members.length === 0) return null; // `{}` admits any non-nullish value
// All-readonly object types may describe frozen values (.readonly()).
if (props.length > 0 && readonlyCount === props.length) return null;
const text = `{ ${members.join("; ")} }`;
if (frame.name !== null) {
this.declarations.push(`type ${frame.name} = ${text};`);
return frame.name;
}
return text;
} finally {
this.inProgress.delete(type.id);
}
}
}
function hasReadonlyModifier(prop: ts.Symbol): boolean {
return prop.declarations.some(
(d) =>
(ts.isPropertySignature(d) || ts.isPropertyDeclaration(d)) &&
(d.modifiers?.nodes ?? []).some((m) => m.kind === ts.SyntaxKind.ReadonlyKeyword),
);
}
/** Children of a node in source order (forEachChild order). */
function childrenOf(node: ts.Node): ts.Node[] {
const out: ts.Node[] = [];
ts.forEachChild(
node,
(c) => {
out.push(c);
return undefined;
},
(list) => {
for (const c of list.nodes) out.push(c);
return undefined;
},
);
return out;
}
function isWriteTarget(node: ts.Node): boolean {
let child = node;
let parent = node.parent;
while (parent !== undefined && ts.isParenthesizedExpression(parent)) {
child = parent;
parent = parent.parent;
}
if (parent === undefined) return false;
if (ts.isBinaryExpression(parent) && parent.left === child) {
const k = parent.operatorToken.kind;
return k >= ts.SyntaxKind.FirstAssignment && k <= ts.SyntaxKind.LastAssignment;
}
if (ts.isPrefixUnaryExpression(parent) || ts.isPostfixUnaryExpression(parent)) {
const op = parent.operator;
return op === ts.SyntaxKind.PlusPlusToken || op === ts.SyntaxKind.MinusMinusToken;
}
if (ts.isDeleteExpression(parent)) return true;
if (ts.isForOfStatement(parent) || ts.isForInStatement(parent)) return parent.initializer === child;
if (ts.isShorthandPropertyAssignment(parent)) return true;
if (
ts.isArrayLiteralExpression(parent) ||
ts.isPropertyAssignment(parent) ||
ts.isSpreadElement(parent)
) {
// Destructuring assignment targets: conservative, skip.
let up: ts.Node | undefined = parent;
while (up !== undefined && (ts.isArrayLiteralExpression(up) || ts.isObjectLiteralExpression(up) || ts.isPropertyAssignment(up) || ts.isSpreadElement(up)))
up = up.parent;
if (up !== undefined && ts.isBinaryExpression(up) && up.operatorToken.kind === ts.SyntaxKind.EqualsToken)
return true;
}
return false;
}
function inOptionalChain(node: ts.Node): boolean {
const parent = node.parent;
if (parent === undefined) return false;
if (
(ts.isPropertyAccessExpression(parent) ||
ts.isElementAccessExpression(parent) ||
ts.isCallExpression(parent)) &&
parent.expression === node &&
(parent.questionDotToken !== undefined || (parent.flags & ts.NodeFlags.OptionalChain) !== 0)
)
return true;
return (
(ts.isPropertyAccessExpression(node) ||
ts.isElementAccessExpression(node) ||
ts.isCallExpression(node)) &&
(node.flags & ts.NodeFlags.OptionalChain) !== 0
);
}
function isCallee(node: ts.Node): boolean {
const p = node.parent;
return (
p !== undefined &&
(ts.isCallExpression(p) || ts.isNewExpression(p) || ts.isTaggedTemplateExpression(p)) &&
(ts.isTaggedTemplateExpression(p) ? p.tag === node : p.expression === node)
);
}
function inTypeContext(node: ts.Node): boolean {
for (let p = node.parent; p !== undefined; p = p.parent) {
if (ts.isTypeNode(p) || ts.isTypeQueryNode(p)) return true;
if (ts.isExpressionWithTypeArguments(p) && p.parent && ts.isHeritageClause(p.parent))
return true;
if (ts.isStatement(p)) return false;
}
return false;
}
function unwrapTransparent(node: ts.Expression): ts.Expression {
let e = node;
while (ts.isParenthesizedExpression(e) || ts.isNonNullExpression(e) || ts.isAwaitExpression(e))
e = e.expression;
return e;
}
/** Plans the projection for one program pair. Answers null when nothing
* in the user files projects (the program stays as it is). */
export function planDeclProjection(input: DeclProjectionInput): DeclProjection | null {
const jsChecker = input.jsProgram.getTypeChecker();
const declChecker = input.declProgram.getTypeChecker();
const printer = new TypePrinter(declChecker);
const plan: string[] = [];
const edits = new Map<string, Edit[]>();
const typeAt = (checker: ts.TypeChecker, node: ts.Node): ts.Type | null => {
try {
return checker.getTypeAtLocation(node);
} catch {
return null;
}
};
const isAnyish = (t: ts.Type | null): boolean => t !== null && (t.flags & ANYISH) !== 0;
const resolveAlias = (s: ts.Symbol | undefined): ts.Symbol | undefined => {
if (s === undefined || !(s.flags & ts.SymbolFlags.Alias)) return s;
try {
return declChecker.getAliasedSymbol(s);
} catch {
return s;
}
};
const declSymbol = (n: ts.Node): ts.Symbol | undefined => {
try {
return resolveAlias(declChecker.getSymbolAtLocation(n));
} catch {
return undefined;
}
};
const namesPackageSymbol = (node: ts.Node): boolean => {
let found = false;
const visit = (n: ts.Node): void => {
if (found) return;
if (ts.isIdentifier(n) && declaredInPackage(declSymbol(n))) {
found = true;
return;
}
for (const c of childrenOf(n)) visit(c);
};
visit(node);
return found;
};
for (const file of input.userFiles) {
const jsSf = input.jsProgram.getSourceFile(file);
const declSf = input.declProgram.getSourceFile(file);
if (jsSf === undefined || declSf === undefined || jsSf.text !== declSf.text) continue;
const fileEdits: Edit[] = [];
/** Candidate expressions (by js node) with their alias. */
const exprCandidates = new Map<ts.Node, string>();
const exprOrder: ts.Node[] = [];
const considerType = (jsNode: ts.Node, declNode: ts.Node): boolean => {
if (
!ts.isTypeReferenceNode(jsNode) &&
!ts.isIndexedAccessTypeNode(jsNode) &&
!ts.isExpressionWithTypeArguments(jsNode)
)
return false;
if (ts.isExpressionWithTypeArguments(jsNode)) {
if (!jsNode.parent || !ts.isHeritageClause(jsNode.parent)) return false;
if (!jsNode.parent.parent || !ts.isInterfaceDeclaration(jsNode.parent.parent)) return false;
}
let jsType: ts.Type | null;
let declType: ts.Type | null;
try {
jsType = ts.isExpressionWithTypeArguments(jsNode)
? jsChecker.getTypeAtLocation(jsNode)
: jsChecker.getTypeFromTypeNode(jsNode as ts.TypeNode);
declType = ts.isExpressionWithTypeArguments(declNode)
? declChecker.getTypeAtLocation(declNode)
: declChecker.getTypeFromTypeNode(declNode as ts.TypeNode);
} catch {
return false;
}
if (!isAnyish(jsType) || declType === null) return false;
if (!namesPackageSymbol(declNode)) return false;
const alias = printer.project(declType);
if (alias === null) return false;
const start = ts.tokenStartOfNode(jsNode);
fileEdits.push({ start, end: jsNode.end, text: alias });
plan.push(`${file}:${start}: type ${jsSf.text.slice(start, jsNode.end)} -> ${alias}`);
return true;
};
const considerExpr = (jsNode: ts.Node, declNode: ts.Node): void => {
if (!ts.isExpression(jsNode)) return;
const isCall =
ts.isCallExpression(jsNode) || ts.isNewExpression(jsNode) || ts.isTaggedTemplateExpression(jsNode);
const isAccess = ts.isPropertyAccessExpression(jsNode) || ts.isElementAccessExpression(jsNode);
const isAwait = ts.isAwaitExpression(jsNode);
if (!isCall && !isAccess && !isAwait && !ts.isIdentifier(jsNode)) return;
if (inTypeContext(jsNode) || isCallee(jsNode) || isWriteTarget(jsNode)) return;
if (inOptionalChain(jsNode)) return;
const parent = jsNode.parent;
if (parent !== undefined && ts.isExpressionStatement(parent)) return;
if (parent !== undefined && (ts.isImportSpecifier(parent) || ts.isImportClause(parent) || ts.isNamespaceImport(parent) || ts.isExportSpecifier(parent)))
return;
if (ts.isIdentifier(jsNode)) {
if (
parent !== undefined &&
(ts.isPropertyAccessExpression(parent) && parent.name === jsNode)
)
return;
}
const jsType = typeAt(jsChecker, jsNode);
if (!isAnyish(jsType)) return;
// Package-produced?
const d = declNode as ts.Expression;
let produced = false;
if (ts.isCallExpression(d) || ts.isNewExpression(d)) {
const callee = d.expression;
const target = ts.isPropertyAccessExpression(callee) ? callee.name : callee;
produced = declaredInPackage(declSymbol(target));
if (!produced) {
try {
const sig = declChecker.getResolvedSignature(d);
const decl = sig?.declaration;
produced = decl !== undefined && isPackageDeclarationFile(ts.getSourceFileOfNode(decl));
} catch {
produced = false;
}
}
} else if (ts.isTaggedTemplateExpression(d)) {
produced = declaredInPackage(declSymbol(d.tag));
} else if (ts.isPropertyAccessExpression(d) || ts.isElementAccessExpression(d)) {
const recvDecl = typeAt(declChecker, d.expression);
const recvJs = typeAt(jsChecker, (jsNode as ts.PropertyAccessExpression).expression);
const recvProjectable = recvDecl !== null && printer.project(recvDecl) !== null;
produced = !recvProjectable || (recvJs !== null && !isAnyish(recvJs));
} else if (ts.isAwaitExpression(d)) {
const inner = unwrapTransparent(d.expression);
produced =
(ts.isCallExpression(inner) || ts.isPropertyAccessExpression(inner)) &&
(declaredInPackage(
declSymbol(
ts.isCallExpression(inner)
? ts.isPropertyAccessExpression(inner.expression)
? inner.expression.name
: inner.expression
: inner.name,
),
) ||
isAnyish(typeAt(jsChecker, inner)));
} else if (ts.isIdentifier(d)) {
const raw = (() => {
try {
return declChecker.getSymbolAtLocation(d);
} catch {
return undefined;
}
})();
produced =
raw !== undefined &&
(raw.flags & ts.SymbolFlags.Alias) !== 0 &&
declaredInPackage(resolveAlias(raw));
}
if (!produced) return;
// A consumer that takes anything needs no conversion.
try {
const ctx = declChecker.getContextualType(d);
if (ctx !== undefined && ctx.flags & ANYISH) return;
} catch {
// no contextual type
}
const declType = typeAt(declChecker, d);
if (declType === null) return;
const alias = printer.project(declType);
if (alias === null) return;
exprCandidates.set(jsNode, alias);
exprOrder.push(jsNode);
};
const annotated = new Set<ts.Node>();
const considerParam = (jsNode: ts.Node, declNode: ts.Node): void => {
if (!ts.isParameter(jsNode) || jsNode.type !== undefined || !ts.isIdentifier(jsNode.name)) return;
const fn = jsNode.parent;
if (fn === undefined || !(ts.isArrowFunction(fn) || ts.isFunctionExpression(fn))) return;
if (jsNode.dotDotDotToken !== undefined) return;
const jsType = typeAt(jsChecker, jsNode.name);
if (!isAnyish(jsType)) return;
const declType = typeAt(declChecker, (declNode as ts.ParameterDeclaration).name);
if (declType === null) return;
// Only parameters the package's declarations type (a callback the
// package calls), not ones the user typed elsewhere.
try {
const ctxFn = declChecker.getContextualType(declNode.parent as ts.Expression);
if (ctxFn === undefined) return;
const sigs = declChecker.getCallSignatures(ctxFn);
if (!sigs.some((s) => s.declaration !== undefined && isPackageDeclarationFile(ts.getSourceFileOfNode(s.declaration))))
return;
} catch {
return;
}
const alias = printer.project(declType);
if (alias === null) return;
annotated.add(jsNode);
const name = jsNode.name;
const nameStart = ts.tokenStartOfNode(name);
const text = jsSf.text;
const bareArrow =
ts.isArrowFunction(fn) && fn.parameters.nodes.length === 1 && text[ts.tokenStartOfNode(fn)] !== "(" && text.slice(ts.tokenStartOfNode(fn), nameStart).trim() === "" ;
const afterName = jsNode.questionToken !== undefined ? jsNode.questionToken.end : name.end;
if (bareArrow) {
fileEdits.push({ start: nameStart, end: nameStart, text: "(" });
fileEdits.push({ start: afterName, end: afterName, text: `: ${alias})` });
} else {
fileEdits.push({ start: afterName, end: afterName, text: `: ${alias}` });
}
plan.push(`${file}:${nameStart}: parameter ${name.text}: ${alias}`);
};
const walk = (jsNode: ts.Node, declNode: ts.Node): void => {
if (jsNode.kind !== declNode.kind) return;
if (ts.isTypeNode(jsNode) || ts.isExpressionWithTypeArguments(jsNode)) {
if (considerType(jsNode, declNode)) return;
} else {
considerParam(jsNode, declNode);
considerExpr(jsNode, declNode);
}
const a = childrenOf(jsNode);
const b = childrenOf(declNode);
if (a.length !== b.length) return;
for (let i = 0; i < a.length; i++) walk(a[i]!, b[i]!);
};
walk(jsSf, declSf);
// Suppress candidates whose receiver chain another candidate types.
const symbolTyped = new Map<ts.Symbol, boolean>();
const willBeTyped = (e: ts.Node, depth = 0): boolean => {
if (depth > 32) return false;
if (exprCandidates.has(e)) return true;
if (ts.isParenthesizedExpression(e) || ts.isNonNullExpression(e) || ts.isAwaitExpression(e))
return willBeTyped(e.expression, depth + 1);
if (ts.isAsExpression(e)) return false;
if (ts.isPropertyAccessExpression(e) || ts.isElementAccessExpression(e))
return willBeTyped(e.expression, depth + 1);
if (ts.isIdentifier(e)) {
let sym: ts.Symbol | undefined;
try {
sym = jsChecker.getSymbolAtLocation(e);
} catch {
return false;
}
if (sym === undefined) return false;
const known = symbolTyped.get(sym);
if (known !== undefined) return known;
symbolTyped.set(sym, false);
let result = false;
const decls = sym.declarations;
if (decls.length === 1) {
const decl = decls[0]!;
if (ts.isVariableDeclaration(decl) && decl.initializer !== undefined && decl.type === undefined)
result = willBeTyped(decl.initializer, depth + 1);
else if (ts.isParameter(decl) && annotated.has(decl)) result = true;
else if (ts.isBindingElement(decl)) result = false;
}
symbolTyped.set(sym, result);
return result;
}
return false;
};
const receiverOf = (e: ts.Node): ts.Node | null => {
if (ts.isPropertyAccessExpression(e) || ts.isElementAccessExpression(e)) return e.expression;
if (ts.isIdentifier(e)) return null;
return null;
};
const kept: ts.Node[] = [];
for (const e of exprOrder) {
const recv = receiverOf(e);
if (recv !== null && willBeTyped(recv)) continue;
// An identifier candidate that is a user binding never gets here
// (only package imports are candidates).
kept.push(e);
}
// Nested candidates: keep only the innermost (an outer one is typed by
// the inner wrap).
const keptSet = new Set(kept);
const containsKept = (outer: ts.Node): boolean => {
let found = false;
const visit = (n: ts.Node): void => {
if (found) return;
for (const c of childrenOf(n)) {
if (keptSet.has(c)) {
found = true;
return;
}
visit(c);
}
};
visit(outer);
return found;
};
for (const e of kept) {
if (!ts.isAwaitExpression(e) && containsKept(e) && (ts.isPropertyAccessExpression(e) || ts.isElementAccessExpression(e))) {
// `a.b.c` with `a.b` wrapped: the outer access reads typed data.
const recv = receiverOf(e);
if (recv !== null && (keptSet.has(unwrapTransparent(recv as ts.Expression)) || willBeTyped(recv)))
continue;
}
const alias = exprCandidates.get(e)!;
const start = ts.tokenStartOfNode(e);
const stmt = e.parent;
let prefix = "(";
// A statement that would start with `(` after a line without a
// semicolon continues the previous expression: guard it.
let up: ts.Node | undefined = e;
while (up?.parent !== undefined && !ts.isStatement(up.parent)) up = up.parent;
if (up?.parent !== undefined && ts.isExpressionStatement(up.parent) && ts.tokenStartOfNode(up.parent) === start)
prefix = ";(";
void stmt;
fileEdits.push({ start, end: start, text: prefix });
// Closing parts at one position close inner wraps first.
fileEdits.push({ start: e.end, end: e.end, text: ` as ${alias})`, rank: -start });
plan.push(`${file}:${start}: cast ${jsSf.text.slice(start, Math.min(e.end, start + 60)).replace(/\s+/g, " ")} as ${alias}`);
}
if (fileEdits.length === 0) continue;
edits.set(file, fileEdits);
}
if (edits.size === 0) return null;
const files = new Map<string, string>();
for (const [file, list] of edits) {
const sf = input.jsProgram.getSourceFile(file)!;
files.set(file, applyEdits(sf.text, list));
}
const declFile = `${input.outDir.split("\\").join("/")}/${DECL_BOUNDARY_FILE}`;
const declText =
"// Generated by scriptc: types projected from npm package declarations.\n" +
printer.declarations.join("\n") +
"\n";
return { files, declFile, declText, plan };
}
/** Applies insertions and replacements; overlapping replacements keep the
* first. Insertions at one position keep their order. */
export function applyEdits(text: string, edits: readonly Edit[]): string {
const sorted = edits
.map((e, i) => ({ ...e, i }))
.sort(
(a, b) => a.start - b.start || a.end - b.end || (a.rank ?? 0) - (b.rank ?? 0) || a.i - b.i,
);
let out = "";
let at = 0;
for (const e of sorted) {
if (e.start < at) continue; // overlaps an earlier replacement
out += text.slice(at, e.start) + e.text;
at = e.end;
}
return out + text.slice(at);
}
@@ -288,6 +288,10 @@ export function applyWidthLift(
case "dynIn": {
if (dst.kind !== "dyn")
throw new InternalCompilerError("lowerer bug: dynIn lift against a non-dyn slot");
// The declaration-typed boundary keeps user values' identity when
// they cross into package code (decl-projection.ts).
if (lowerer.declBoundary && !/[\\/]node_modules[\\/]/.test(loc.file) && liveBoxable(lowerer, value.type))
return { kind: "dynFrom", value, liveRef: true, type: DYN, loc };
return { kind: "dynFrom", value, type: DYN, loc };
}
case "dynOut":
@@ -775,3 +779,16 @@ function buildRecordProjection(
loc,
};
}
/** Records and arrays (and unions holding them), whose identity a live
* capsule keeps. */
function liveBoxable(lowerer: Lowerer, t: IrType, seen = new Set<string>()): boolean {
if (t.kind === "record") {
const shape = lowerer.shapes.get(t.shapeId);
return shape !== undefined && !shape.tuple;
}
if (t.kind === "array") return true;
if (t.kind !== "union" || seen.has(t.unionId)) return false;
seen.add(t.unionId);
return lowerer.unions.get(t.unionId)?.arms.some((arm) => liveBoxable(lowerer, arm, seen)) ?? false;
}
@@ -13,6 +13,7 @@ import {
STRING,
type SrcLoc,
UNDEFINED_T,
VOID,
arrayOf,
canAdaptDynFuncTo,
canDynCheckTo,
@@ -47,6 +48,7 @@ import {
import { isConstAssertionTypeNode, unitOnlyUnion } from "../../type-mapper.js";
import { assertsGlobalObject, extractionCopiesReference } from "../assertion-identity.js";
import { iterableSlotValue, iterableUnionOf } from "../iterable-values.js";
import { isDeclBoundaryTypeNode } from "../../decl-projection.js";
/** `c ? a : b`. An explicit destination constructs each branch at that
* layout before joining it. Without one, retain the expression's inferred
@@ -1516,6 +1518,12 @@ export function lowerAsExpression(
const target = lowerer.mapTypeOf(targetTs) ?? dynFallbackType(lowerer, expr.type, targetTs);
if (!target) lowerer.badType(expr.type, targetTs);
if (target.kind === "dyn") return inner; // `as unknown`: erasure
// The declaration-typed boundary (decl-projection.ts): an identity-
// preserving checked conversion of a value package code produced.
if (isDeclBoundaryTypeNode(expr.type)) {
const adopted = declBoundaryCast(lowerer, inner, target, locOf(expr));
if (adopted) return adopted;
}
// An all-`unknown`-fields record target (`err as { code?: unknown }`
// — the errno-probing idiom): there is nothing to validate (every
// field is unknown, exactly what the dyn value already answers) and
@@ -1603,3 +1611,69 @@ export function lowerAsExpression(
}
return { kind: "dynCheck", value: inner, type: target, loc: locOf(expr) };
}
/** True when a type holds a record or an array somewhere (values whose
* identity can be observed). */
function holdsReference(lowerer: Lowerer, t: IrType, seen = new Set<string>()): boolean {
if (t.kind === "record" || t.kind === "array") return true;
if (t.kind !== "union" || seen.has(t.unionId)) return false;
seen.add(t.unionId);
return lowerer.unions.get(t.unionId)?.arms.some((arm) => holdsReference(lowerer, arm, seen)) ?? false;
}
/** `(<package expr> as __scriptc_decl_N)`: check the dynamic value against
* the projected type and build the native value (dynCheck), then adopt:
* every source object or array that something else still references
* becomes a live capsule of the value built from it
* (scr_dyn_decl_adopt), so identity and mutation visibility hold on both
* sides. Scalar targets are ordinary checked casts. */
function declBoundaryCast(
lowerer: Lowerer,
inner: IrExpr,
target: IrType,
loc: SrcLoc,
): IrExpr | null {
if (
!canDynCheckTo(
target,
(id) => lowerer.shapes.get(id),
(id) => lowerer.unions.get(id),
) &&
!(target.kind === "union" && lowerer.dynConvertible(target))
)
return null;
if (!holdsReference(lowerer, target)) {
return { kind: "dynCheck", value: inner, type: target, loc };
}
const source = lowerer.declareHiddenLocal("%boundarySource", DYN);
const value = lowerer.declareHiddenLocal("%boundaryValue", target);
return {
kind: "seqExpr",
stmts: [
{ kind: "varDecl", localId: source.id, init: inner, loc },
{
kind: "varDecl",
localId: value.id,
init: { kind: "dynCheck", value: varRef(source.id, DYN, loc), adopt: true, type: target, loc },
loc,
},
{
kind: "exprStmt",
expr: {
kind: "libCall",
fn: "dyn.declAdopt",
args: [
varRef(source.id, DYN, loc),
{ kind: "dynFrom", value: varRef(value.id, target, loc), liveRef: true, type: DYN, loc },
],
type: VOID,
loc,
},
loc,
},
],
result: varRef(value.id, target, loc),
type: target,
loc,
};
}
@@ -227,6 +227,11 @@ export function applyKeyOrderWitnesses(lowerer: Lowerer, fns: readonly IrFunctio
const opaqueSeen = new Set<string>();
const runtimeShapes = new Set<string>();
const runtimeSeen = new Set<string>();
/** Shapes the declaration-typed boundary converts into (adopting casts):
* their values come from package code that builds them in the
* declaration's order, the order their table holds. */
const declaredShapes = new Set<string>();
const declaredSeen = new Set<string>();
const producedBy = (type: IrType, opaque: boolean): void => {
if (opaque) shapesIn(type, opaqueShapes, opaqueSeen);
else shapesIn(type, runtimeShapes, runtimeSeen);
@@ -332,7 +337,8 @@ export function applyKeyOrderWitnesses(lowerer: Lowerer, fns: readonly IrFunctio
producedBy(e.type, true);
return;
case "dynCheck":
if (!orderPreservingClone(e)) producedBy(e.type, false);
if (e.adopt === true) shapesIn(e.type, declaredShapes, declaredSeen);
else if (!orderPreservingClone(e)) producedBy(e.type, false);
return;
case "dynDestrCheck":
case "caughtCheck":
@@ -392,6 +398,7 @@ export function applyKeyOrderWitnesses(lowerer: Lowerer, fns: readonly IrFunctio
f.opaque = true;
}
for (const id of runtimeShapes) factsOf(id).runtime = true;
for (const id of declaredShapes) if (!runtimeShapes.has(id)) factsOf(id).pinned = true;
// ── scope ───────────────────────────────────────────────────────────
// Only plain declared-field shapes with a table take part.
@@ -1,3 +1,4 @@
import { isDeclBoundaryFile } from "../decl-projection.js";
import { relieveHeapPressure } from "../../heap-pressure.js";
import { keyedHelperName } from "./stable-names.js";
import { positionTag, StructuralPositions } from "./structural-positions.js";
@@ -3565,6 +3566,18 @@ export class Lowerer {
else if (kind === "failed") s.failed += 1;
}
/** The program holds the declaration-typed boundary's projection
* (decl-projection.ts): user-file record and array values crossing into
* dynamic slots (package parameters) box as live capsules, so a value
* that comes back through a boundary cast keeps its identity. */
private declBoundaryMemo: boolean | null = null;
get declBoundary(): boolean {
this.declBoundaryMemo ??= this.program
.getSourceFiles()
.some((sf) => isDeclBoundaryFile(sf.fileName));
return this.declBoundaryMemo;
}
constructor(
readonly program: ts.Program,
readonly entry: ts.SourceFile,
@@ -3687,8 +3700,11 @@ export class Lowerer {
classImplementedProtocol: (type) => this.classImplementedProtocol(type),
isStdlibFile: this.isStdlibFile,
isNpmFile: this.isNpmFile,
// The declaration-typed boundary's generated types (decl-projection.ts)
// describe data, like an explicitly mapped external type surface.
isExternalTypeFile: (sf) =>
this.externalTypeSpecifiersByFile.has(tsgoPath(resolve(sf.fileName))),
this.externalTypeSpecifiersByFile.has(tsgoPath(resolve(sf.fileName))) ||
isDeclBoundaryFile(sf.fileName),
typeMemo: this.typeMemo,
...(this.generalNodeStreams ? { generalNodeStreams: true } : {}),
...(this.dynNodeStreams ? { dynNodeStreams: true } : {}),
@@ -7708,6 +7724,7 @@ export class Lowerer {
entry: ENTRY_NAME,
...(this.usesWorkers ? { workers: true } : {}),
...(this.usesLifetimeRegions ? { lifetimeRegions: true as const } : {}),
...(this.declBoundary ? { declBoundary: true as const } : {}),
...(this.threadSlotKeys.size > 0
? { threadSlotKeys: [...this.threadSlotKeys].sort() }
: {}),
+135 -13
View File
@@ -139,6 +139,7 @@ import {
typeOnlyReachedFiles,
} from "./type-only-imports.js";
import { moduleSourceCandidates } from "./module-source-candidates.js";
import { planDeclProjection, type DeclProjection } from "./decl-projection.js";
import { inferredJsDiagnosticSuppressed } from "./inferred-js-diagnostics.js";
import {
containsJsx,
@@ -397,6 +398,9 @@ export interface LoadResult {
/** The program's pre-emit diagnostics, computed once. */
diagnostics: readonly ts.Diagnostic[];
projectWorld: ts.Program | null;
/** The declaration-typed boundary rewrote user files: errors fall
* back to the declaration world (load.projectWorld()), as without it. */
declWorldFallback?: true;
};
/** How scriptc's type world differs from the project's own tsc setup —
* used only to label the TypeScript errors those differences cause. */
@@ -846,6 +850,9 @@ function loadProgram7(
externalTypes: ReadonlyMap<string, string> = new Map(),
/** The --npm-static lowering world: root opted-in packages' deep edges. */
rootNpmStaticEdges = false,
/** The declaration-typed boundary's projection (decl-projection.ts):
* answers rewritten user files for the lowering world, or null. */
projector?: (program: ts.Program) => DeclProjection | null,
): LoadResult & { disposeAll: () => void } {
const config = adoptProjectConfig7(host, entryPath);
const configuredPaths = config.options["paths"];
@@ -940,6 +947,53 @@ function loadProgram7(
};
for (const [file, lowered] of jsx.sources) host.addVirtualFile(file, lowered.text);
program = host.createProgram([...programRoots, overridesDtsPath()], options);
} else if (projector !== undefined) {
// The declaration-typed boundary: user files typed from package
// declarations (decl-projection.ts). The tsc gate reads the original
// text; the lowering compiles the rewritten files.
const projection = projector(program);
if (projection !== null) {
const original = program;
const baseline = new Map<string, number>();
for (const file of projection.files.keys()) {
const sf = original.getSourceFile(file);
if (sf !== undefined) baseline.set(file, original.getSemanticDiagnostics(sf).length);
}
for (const [file, text] of projection.files) host.addVirtualFile(file, text);
host.addVirtualFile(projection.declFile, projection.declText);
programRoots.push(projection.declFile);
program = host.createProgram([...programRoots, overridesDtsPath()], options);
// A rewrite the lowering world disagrees with (new errors) is
// dropped for that file: the file lowers as it would without it.
const dropped: string[] = [];
for (const file of projection.files.keys()) {
const sf = program.getSourceFile(file);
if (sf === undefined) continue;
if (program.getSemanticDiagnostics(sf).length > (baseline.get(file) ?? 0)) {
dropped.push(file);
host.addVirtualFile(file, original.getSourceFile(file)!.text);
}
}
if (dropped.length > 0) {
program.dispose();
program = host.createProgram([...programRoots, overridesDtsPath()], options);
for (const file of dropped) {
config.diags.push(declBoundaryDroppedDiag(file));
}
}
if (dropped.length === projection.files.size) {
program.dispose();
programRoots.pop();
program = original;
} else {
tscWorld = {
program: original,
diagnostics: ts.getPreEmitDiagnostics(original),
projectWorld: null,
declWorldFallback: true,
};
}
}
}
const entry = program.getSourceFile(entryPath);
if (!entry) throw new Error(`could not load ${entryPath}`);
@@ -1156,31 +1210,69 @@ export function loadProgram(
(npmShadow?.hideFile(path) ?? false) || projectDtsRuntimeSibling(path) !== null,
};
const host = services.createProgramHost({ cwd: dirname(entryPath), fsShadow });
let authoredHost: ts.Ts7Host | null = null;
let authoredLoad: ReturnType<typeof loadProgram7> | null = null;
const authored = (): ReturnType<typeof loadProgram7> => {
if (!authoredLoad) {
authoredHost = services.createProgramHost({ cwd: dirname(entryPath) });
authoredLoad = loadProgram7(authoredHost, entryPath, externalTypes);
}
return authoredLoad;
};
const declPackages = npmStaticPackages.filter((pkg) => {
const resolved = resolveBareModule(entryPath, pkg, "types-only");
return resolved !== null && /\.d\.(?:ts|mts|cts)$/.test(resolved.typesFile);
});
const projector =
declPackages.length === 0
? undefined
: (program: ts.Program): DeclProjection | null => {
const userFiles = declBoundaryUserFiles(program, declPackages);
if (userFiles.length === 0) return null;
let projection: DeclProjection | null;
try {
projection = planDeclProjection({
jsProgram: program,
declProgram: authored().program,
userFiles,
outDir: dirname(entryPath),
});
} catch (error) {
if (process.env["SCRIPTC_DEBUG_DECL_BOUNDARY"] === "1") throw error;
return null;
}
if (process.env["SCRIPTC_DEBUG_DECL_BOUNDARY"] === "1") {
process.stderr.write(
`[decl-boundary] ${projection === null ? "no projection" : projection.plan.join("\n[decl-boundary] ")}\n`,
);
if (projection !== null) process.stderr.write(projection.declText);
}
return projection;
};
try {
const load = loadProgram7(host, entryPath, externalTypes, npmStaticPackages.length > 0);
let authoredHost: ts.Ts7Host | null = null;
let authoredLoad: ReturnType<typeof loadProgram7> | null = null;
const load = loadProgram7(
host,
entryPath,
externalTypes,
npmStaticPackages.length > 0,
projector,
);
return {
...load,
services,
projectWorld:
npmStaticPackages.length === 0
? load.projectWorld
: () => {
: () =>
// Check authored code against the package's published declarations.
// Runtime bodies still lower from JavaScript in the original world;
// inference there must not manufacture errors in valid user code.
if (!authoredLoad) {
authoredHost = services.createProgramHost({ cwd: dirname(entryPath) });
authoredLoad = loadProgram7(authoredHost, entryPath, externalTypes);
}
return authoredLoad.projectWorld();
},
authored().projectWorld(),
dispose: () => {
try {
authoredLoad?.disposeAll();
(authoredLoad as ReturnType<typeof loadProgram7> | null)?.disposeAll();
} finally {
authoredHost?.close();
(authoredHost as ts.Ts7Host | null)?.close();
try {
load.disposeAll();
} finally {
@@ -1190,11 +1282,39 @@ export function loadProgram(
},
};
} catch (error) {
(authoredLoad as ReturnType<typeof loadProgram7> | null)?.disposeAll();
(authoredHost as ts.Ts7Host | null)?.close();
host.close();
throw error;
}
}
/** User TypeScript files (outside node_modules) that import one of the
* packages whose declarations the boundary projects from. */
function declBoundaryUserFiles(program: ts.Program, packages: readonly string[]): string[] {
const out: string[] = [];
for (const sf of program.getSourceFiles()) {
const name = sf.fileName.split("\\").join("/");
if (sf.isDeclarationFile || name.includes("/node_modules/") || !/\.[mc]?tsx?$/.test(name))
continue;
if (name.endsWith(".tsx")) continue; // JSX files: not projected yet
const text = sf.text;
if (packages.some((pkg) => text.includes(`"${pkg}"`) || text.includes(`'${pkg}'`) || text.includes(`"${pkg}/`) || text.includes(`'${pkg}/`)))
out.push(sf.fileName);
}
return out;
}
/** SC6102 — a projected file the lowering world disagreed with. */
function declBoundaryDroppedDiag(file: string): ScrDiagnostic {
return {
code: "SC6102",
message:
"values from npm packages stay checked-dynamic in this file: the types projected from the packages' declarations do not type-check against their JavaScript",
loc: { file, start: 0, end: 0 },
};
}
/** tsc diagnostics (syntax + types), the supported-import fence, and the
* module evaluation order: fills load.moduleOrder (the SourceFiles
* themselves — the lowering consumes them directly) and returns the
@@ -4801,7 +4921,9 @@ function preflight7(load: LoadResult): {
tscWorld === undefined ? errorsOf(program) : keptErrors(tscWorld.program, tscWorld.diagnostics);
if (tscErrors.length > 0) {
const world =
tscWorld === undefined ? load.projectWorld() : (tscWorld.projectWorld ?? tscWorld.program);
tscWorld === undefined || tscWorld.declWorldFallback === true
? load.projectWorld()
: (tscWorld.projectWorld ?? tscWorld.program);
for (const d of errorsOf(world)) {
const diag = toPassthrough(d);
const lowered = tscWorld === undefined ? undefined : load.jsxSources?.get(diag.loc.file);
@@ -610,6 +610,7 @@ export const MAY_THROW_LIB_FNS: ReadonlySet<IrLibFn> = new Set([
// fence; the zero-argument form always throws ERR_MISSING_ARGS. The
// DOMException clone shares the option validation.
"dyn.structuredClone",
"dyn.declAdopt",
"dyn.cloneMissing",
"dyn.cloneTransferFail",
"dyn.cloneTransferFailWith",
@@ -3776,6 +3776,14 @@ export const LIB_FN_SIGS = defineLibFnSignatures({
* call: always throws Node's TypeError [ERR_MISSING_ARGS] with Node's
* own (verbatim, doubly-wrapped) message. */
"dyn.structuredClone": { argTypes: [DYN, DYN], result: DYN },
/** The declaration-typed boundary's adoption (decl-projection.ts):
* `source` is the dynamic value a boundary cast just converted, and
* `capsule` a live capsule of the native value built from it. Every
* object or array of `source` that something else still references
* becomes, in place, a live capsule of the native value built from it.
* Throws the path TypeError, changing nothing, when a converted object
* is not plain data (a prototype, accessors, symbols, frozen state). */
"dyn.declAdopt": { argTypes: [DYN, DYN], result: VOID },
"dyn.cloneMissing": { argTypes: [], result: DYN },
/** structuredClone with a NON-EMPTY transfer array of static values:
* nothing static is transferable, so the call always throws Node's
+13
View File
@@ -1044,6 +1044,10 @@ export interface IrModule {
entry: string;
/** Executable roots run in isolated native worker contexts. */
workers?: boolean;
/** The program has declaration-typed boundary casts (decl-projection.ts):
* closures boxed for package code return records and arrays as live
* capsules, so a value that comes back keeps its identity. */
declBoundary?: true;
/** The program constructs LifetimeRegion (@scriptc/experimental): every
* ProgramLifetime instance lives in a counted region (the region.*
* intrinsics; backend/llvm/regions.ts). */
@@ -3879,10 +3883,19 @@ export type IrExpr =
/** `exact`: the value is known to be a capsule this program boxed from
* exactly the class `type` names (a `dyn.typedRefKeyIs` test guards the
* expression), so the check is one load of the capsule's instance. */
/** `adopt`: the declaration-typed boundary's cast (decl-projection.ts).
* The conversion preserves identity: a source object or array that
* something else still references (reference count above one) is
* turned, after the whole check succeeded, into a live capsule of the
* value built from it, so every holder sees the same native value; an
* unshared source is simply consumed. Values must be plain data
* (ordinary objects and arrays without accessors, prototypes, symbols
* or frozen state), otherwise the cast throws the path TypeError. */
| {
kind: "dynCheck";
value: IrExpr;
preserveRefs?: true;
adopt?: true;
exact?: true;
type: IrType;
loc: SrcLoc;
+2
View File
@@ -120,6 +120,8 @@ export function validateModule(mod: IrModule): IrValidationError[] {
const moduleLoc: SrcLoc = { file: mod.sourceFile, start: 0, end: 0 };
if (mod.workers !== undefined && typeof mod.workers !== "boolean")
errors.push({ message: "worker execution mode must be a boolean", loc: moduleLoc });
if (mod.declBoundary !== undefined && mod.declBoundary !== true)
errors.push({ message: "declBoundary must be true when present", loc: moduleLoc });
if (mod.lifetimeRegions !== undefined && mod.lifetimeRegions !== true)
errors.push({ message: "lifetimeRegions must be true when present", loc: moduleLoc });
if (mod.workers && mod.lib !== undefined)
+5
View File
@@ -1000,6 +1000,11 @@ void scr_dyn_typed_ref_bind_record(ScrDyn *d, const ScrDynTypedRecordOps *ops);
* retained view released with the record. Its capsules adopt that view, and
* the typed serializers consult `has` to serialize through the capsule.
* `apply` consumes a fresh copying snapshot and returns it merged (+1). */
/* The declaration-typed boundary's adoption (scr_json.c): every object or
* array of `source` that something else still references becomes a live
* capsule of the native value `capsule` built from it. Throws the path
* TypeError, changing nothing, when a converted node is not plain data. */
void scr_dyn_decl_adopt(ScrDyn *source, ScrDyn *capsule);
void scr_record_source_note(void *rec, const ScrDyn *src, const char *const *names,
const size_t *lens, size_t count);
bool scr_record_source_has(const void *rec);
+207
View File
@@ -15755,3 +15755,210 @@ bool scr_error_has_key(ScrError *e, const ScrStr *key) {
return true;
return scr_dyn_object_proto_name(key);
}
/* ── declaration-typed boundary: adoption ───────────────────────────────
* A boundary cast (decl-projection.ts in the compiler) checks a value
* package code produced against the type the package's declarations give
* it and builds the native value. Copying would be observable whenever
* something else still references a source object or array, so this pass
* walks the source next to a live capsule of the built value and turns
* every source node with another holder (rc above one) into a live
* capsule of the native value built from it: every holder then reads and
* writes the same native value. Unshared nodes are left to be released
* with their parent, which no program can tell apart from a move.
*
* Phase one checks that every converted node is plain data (the native
* value has no prototype, accessors, symbol keys or frozen state) and
* collects the shared ones; it throws the path TypeError before anything
* changes. Phase two converts the shared nodes in place. Programs without
* boundary casts never reference this code (the linker drops it). */
typedef struct {
ScrDyn *node;
ScrDyn *capsule; /* owned */
} ScrDeclAdopt;
typedef struct {
ScrDeclAdopt *items;
size_t len, cap;
} ScrDeclAdoptList;
static void scr_decl_adopt_push(ScrDeclAdoptList *list, ScrDyn *node, ScrDyn *capsule) {
if (list->len == list->cap) {
list->cap = list->cap ? list->cap * 2 : 8;
list->items = realloc(list->items, list->cap * sizeof *list->items);
if (!list->items) scr_trap("scriptc: out of memory\n");
}
list->items[list->len].node = node;
list->items[list->len].capsule = capsule;
list->len++;
}
static bool scr_decl_adopt_plain(const ScrDyn *n, const ScrDynPath *path) {
const char *want = NULL;
if (n->kind == SCR_DYN_OBJ) {
if (n->prototype || n->null_proto || n->v.obj.class_name)
want = "a plain object (this value has a prototype)";
else if (n->symbol_properties || n->symbol_keys)
want = "a plain object (this value has symbol-keyed properties)";
else if (n->non_extensible)
want = "a plain object (this value is frozen, sealed or not extensible)";
else if (n->v.obj.source_access && !n->v.obj.source_identity)
want = "a plain object";
else {
for (size_t i = 0; i < n->v.obj.len && !want; i++) {
const ScrDynEntry *e = &n->v.obj.entries[i];
if (e->accessor) want = "a plain object (this value has accessor properties)";
else if (!e->writable || !e->enumerable || !e->configurable)
want = "a plain object (this value has non-default property attributes)";
}
}
} else if (n->kind == SCR_DYN_ARR) {
if (n->prototype)
want = "a plain array (this value has a prototype)";
else if (n->v.arr.properties || n->symbol_properties || n->symbol_keys)
want = "a plain array (this value has extra properties)";
else if (n->v.arr.sealed || n->v.arr.frozen || n->non_extensible)
want = "a plain array (this value is frozen, sealed or not extensible)";
}
if (!want) return true;
scr_dyn_check_fail(path, want, n);
return false;
}
static ScrDyn *scr_decl_capsule_ptr_holder(const ScrDeclAdoptList *list, const ScrDyn *node) {
for (size_t i = 0; i < list->len; i++)
if (list->items[i].node == node) return list->items[i].capsule;
return NULL;
}
/* `capsule` is owned by the call: pushed or released. */
static bool scr_decl_adopt_walk(ScrDeclAdoptList *list, ScrDyn *n, ScrDyn *capsule,
const ScrDynPath *path, unsigned depth) {
if (!n || (n->kind != SCR_DYN_OBJ && n->kind != SCR_DYN_ARR) || !capsule ||
capsule->kind != SCR_DYN_TYPED_REF || capsule == n || depth > 4096) {
scr_dyn_release(capsule);
return true;
}
if (!scr_decl_adopt_plain(n, path)) {
scr_dyn_release(capsule);
return false;
}
if (n->rc > 1 && n->rc != SIZE_MAX) {
ScrDyn *seen = scr_decl_capsule_ptr_holder(list, n);
if (seen) {
bool same = seen->v.typed_ref.ptr == capsule->v.typed_ref.ptr;
scr_dyn_release(capsule);
if (!same)
scr_dyn_check_fail(path, "a value that is not also stored elsewhere in this value (scriptc builds a separate native copy for each place)", n);
return same;
}
scr_decl_adopt_push(list, n, capsule);
}
void *ptr = capsule->v.typed_ref.ptr;
if (capsule->v.typed_ref.record && capsule->v.typed_ref.array && n->kind == SCR_DYN_OBJ) {
const ScrDynTypedRecordOps *ops = (const ScrDynTypedRecordOps *)(const void *)capsule->v.typed_ref.array;
for (size_t i = 0; i < ops->count; i++) {
if (!ops->has(ptr, i)) continue;
const ScrStr *name = ops->names[i];
ScrDyn *child = scr_dyn_obj_get(n, name->data, name->len);
if (!child || (child->kind != SCR_DYN_OBJ && child->kind != SCR_DYN_ARR)) continue;
ScrDynPath p = { path, name->data, 0 };
if (!scr_decl_adopt_walk(list, child, ops->get(ptr, i), &p, depth + 1)) {
if (!(n->rc > 1 && n->rc != SIZE_MAX)) scr_dyn_release(capsule);
return false;
}
}
} else if (!capsule->v.typed_ref.record && !capsule->v.typed_ref.klass &&
capsule->v.typed_ref.array && n->kind == SCR_DYN_ARR && !n->v.arr.presence) {
const ScrDynTypedArrayOps *ops = capsule->v.typed_ref.array;
for (size_t i = 0; i < n->v.arr.len; i++) {
ScrDyn *child = n->v.arr.items[i];
if (!child || (child->kind != SCR_DYN_OBJ && child->kind != SCR_DYN_ARR)) continue;
ScrDynPath p = { path, NULL, i };
if (!scr_decl_adopt_walk(list, child, ops->get(ptr, (double)i), &p, depth + 1)) {
if (!(n->rc > 1 && n->rc != SIZE_MAX)) scr_dyn_release(capsule);
return false;
}
}
}
if (!(n->rc > 1 && n->rc != SIZE_MAX)) scr_dyn_release(capsule);
return true;
}
/* Weak collections key a plain object by its node and a capsule by its
* native identity: move the node's entries to the identity it now has. */
static bool scr_decl_adopt_rekey(ScrDyn *node, void *ptr) {
bool moved = false;
if (!scr_weak_observing) return false;
for (;;) {
ScrWeakEntry **slot = &scr_weak_entries[scr_weak_bucket(node)];
while (*slot && !((*slot)->key == (void *)node && (*slot)->kind == 0)) slot = &(*slot)->all_next;
ScrWeakEntry *entry = *slot;
if (!entry) return moved;
*slot = entry->all_next;
entry->key = ptr;
entry->kind = 32;
ScrWeakEntry **head = &scr_weak_entries[scr_weak_bucket(ptr)];
entry->all_next = *head;
*head = entry;
moved = true;
}
}
static void scr_decl_adopt_become(ScrDyn *n, const ScrDyn *capsule) {
ScrDynRegion *region = scr_dyn_region_of(n);
if (region) region->intact = false;
/* The old payload moves to a fresh node, which is released below. A
* misc node carries no buffers of an earlier life. */
ScrDyn *old = scr_dyn_alloc(SCR_DYN_NULL);
old->kind = n->kind;
old->buffer = n->buffer;
old->null_proto = n->null_proto;
old->copied_from_native = n->copied_from_native;
old->non_extensible = n->non_extensible;
old->v = n->v;
old->prototype = n->prototype;
old->symbol_properties = n->symbol_properties;
old->symbol_keys = n->symbol_keys;
n->kind = SCR_DYN_TYPED_REF;
n->buffer = false;
n->null_proto = false;
n->copied_from_native = false;
n->non_extensible = false;
n->prototype = NULL;
n->symbol_properties = NULL;
n->symbol_keys = NULL;
memset(&n->v, 0, sizeof n->v);
n->v.typed_ref.ptr = capsule->v.typed_ref.retain(capsule->v.typed_ref.ptr);
n->v.typed_ref.retain = capsule->v.typed_ref.retain;
n->v.typed_ref.release = capsule->v.typed_ref.release;
n->v.typed_ref.type_key = capsule->v.typed_ref.type_key;
n->v.typed_ref.type_key_len = capsule->v.typed_ref.type_key_len;
n->v.typed_ref.materialize = capsule->v.typed_ref.materialize;
n->v.typed_ref.commit = capsule->v.typed_ref.commit;
n->v.typed_ref.array = capsule->v.typed_ref.array;
n->v.typed_ref.record = capsule->v.typed_ref.record;
n->v.typed_ref.klass = capsule->v.typed_ref.klass;
n->v.typed_ref.traced = capsule->v.typed_ref.traced;
n->v.typed_ref.observed = capsule->v.typed_ref.observed;
if (scr_decl_adopt_rekey(n, n->v.typed_ref.ptr)) n->v.typed_ref.observed = true;
/* A record with undeclared properties answers them from its retained
* view, as a freshly boxed capsule does. */
if (scr_record_source_count && n->v.typed_ref.type_key_len >= 7 &&
memcmp(n->v.typed_ref.type_key, "record:", 7) == 0) {
ScrDyn *view = scr_record_source_view(n->v.typed_ref.ptr);
if (view) n->v.typed_ref.materialized = scr_dyn_retain(view);
}
scr_dyn_release(old);
}
void scr_dyn_decl_adopt(ScrDyn *source, ScrDyn *capsule) {
ScrDeclAdoptList list = { NULL, 0, 0 };
bool ok = scr_decl_adopt_walk(&list, source, scr_dyn_retain(capsule), NULL, 0);
for (size_t i = 0; i < list.len; i++) {
ScrDeclAdopt *item = &list.items[i];
if (ok && item->node->kind != SCR_DYN_TYPED_REF) scr_decl_adopt_become(item->node, item->capsule);
scr_dyn_release(item->capsule);
}
free(list.items);
}
+83
View File
@@ -0,0 +1,83 @@
// Values that cross from zod 3.25 (compiled from its JavaScript) into code
// typed by zod's declarations: identity, aliasing and mutation after parse,
// values zod hands back unchanged, callbacks zod calls, typed array work,
// and failing checks.
import { z } from "zod";
const show = (label: string, value: unknown): void => {
console.log(label + " " + JSON.stringify(value));
};
// Typed records: field reads, writes and array work on parsed data.
const Item = z.object({
sku: z.string(),
price: z.number().nonnegative(),
qty: z.number().int(),
tags: z.array(z.string()).default([]),
});
type Item = z.infer<typeof Item>;
const items: Item[] = z.array(Item).parse([
{ sku: "a", price: 2.5, qty: 4, tags: ["x"] },
{ sku: "b", price: 10, qty: 1 },
{ sku: "c", price: 0.5, qty: 10, tags: ["x", "y"] },
]);
const total = items.reduce((sum, it) => sum + it.price * it.qty, 0);
console.log("total", total);
const byTag = new Map<string, number>();
for (const it of items) for (const t of it.tags) byTag.set(t, (byTag.get(t) ?? 0) + it.qty);
console.log("byTag", [...byTag].map(([k, v]) => `${k}=${v}`).join(","));
const sorted = [...items].sort((a, b) => b.price - a.price).map((it) => it.sku);
console.log("sorted", sorted.join(","));
// Mutation after parse is visible through every reference.
const first = items[0]!;
first.qty += 1;
first.tags.push("z");
console.log("mutated", items[0]!.qty, items[0]!.tags.join("|"), first === items[0]);
show("item", items[0]);
// A value zod hands back unchanged keeps its identity and its mutations.
const Point = z.custom<{ x: number; y: number }>();
const original = { x: 1, y: 2 };
const same = Point.parse(original);
same.x = 10;
console.log("custom", original.x, same === original);
original.y = 20;
console.log("custom back", same.y);
// A shared object returned by a transform: every parse sees the same one.
const shared = { hits: 0 };
const Counter = z.string().transform(() => shared);
const c1 = Counter.parse("a");
const c2 = Counter.parse("b");
c1.hits += 1;
console.log("shared", c2.hits, shared.hits, c1 === c2);
// A refine callback reads typed fields of the value zod checks.
const Range = z
.object({ lo: z.number(), hi: z.number() })
.refine((r) => r.lo <= r.hi, { message: "lo must not exceed hi" });
console.log("range ok", JSON.stringify(Range.parse({ lo: 1, hi: 3 })));
const bad = Range.safeParse({ lo: 5, hi: 3 });
console.log("range bad", bad.success ? "?" : bad.error.issues[0]?.message);
// safeParse data, a discriminated union, and passthrough keys.
const Event = z.discriminatedUnion("type", [
z.object({ type: z.literal("click"), x: z.number(), y: z.number() }),
z.object({ type: z.literal("key"), key: z.string() }),
]);
type Event = z.infer<typeof Event>;
const describe = (e: Event): string => (e.type === "click" ? `click@${e.x},${e.y}` : `key:${e.key}`);
const r = Event.safeParse({ type: "key", key: "Enter" });
if (r.success) console.log("event", describe(r.data));
const Loose = z.object({ id: z.number() }).passthrough();
const loose = Loose.parse({ extra: true, id: 3, more: "m" });
console.log("loose", loose.id, Object.keys(loose).join(","), JSON.stringify(loose));
// async parse
async function main(): Promise<void> {
const it = await Item.parseAsync({ sku: "d", price: 1, qty: 2 });
console.log("async", it.sku, it.price * it.qty, it.tags.length);
}
void main();
+21
View File
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2025 Colin McDonnell
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
@@ -0,0 +1,3 @@
import z3 from "./v3/index.js";
export * from "./v3/index.js";
export default z3;
@@ -0,0 +1,3 @@
{
"type": "module"
}
@@ -0,0 +1,132 @@
import { util } from "./helpers/util.js";
export const ZodIssueCode = util.arrayToEnum([
"invalid_type",
"invalid_literal",
"custom",
"invalid_union",
"invalid_union_discriminator",
"invalid_enum_value",
"unrecognized_keys",
"invalid_arguments",
"invalid_return_type",
"invalid_date",
"invalid_string",
"too_small",
"too_big",
"invalid_intersection_types",
"not_multiple_of",
"not_finite",
]);
export const quotelessJson = (obj) => {
const json = JSON.stringify(obj, null, 2);
return json.replace(/"([^"]+)":/g, "$1:");
};
export class ZodError extends Error {
get errors() {
return this.issues;
}
constructor(issues) {
super();
this.issues = [];
this.addIssue = (sub) => {
this.issues = [...this.issues, sub];
};
this.addIssues = (subs = []) => {
this.issues = [...this.issues, ...subs];
};
const actualProto = new.target.prototype;
if (Object.setPrototypeOf) {
// eslint-disable-next-line ban/ban
Object.setPrototypeOf(this, actualProto);
}
else {
this.__proto__ = actualProto;
}
this.name = "ZodError";
this.issues = issues;
}
format(_mapper) {
const mapper = _mapper ||
function (issue) {
return issue.message;
};
const fieldErrors = { _errors: [] };
const processError = (error) => {
for (const issue of error.issues) {
if (issue.code === "invalid_union") {
issue.unionErrors.map(processError);
}
else if (issue.code === "invalid_return_type") {
processError(issue.returnTypeError);
}
else if (issue.code === "invalid_arguments") {
processError(issue.argumentsError);
}
else if (issue.path.length === 0) {
fieldErrors._errors.push(mapper(issue));
}
else {
let curr = fieldErrors;
let i = 0;
while (i < issue.path.length) {
const el = issue.path[i];
const terminal = i === issue.path.length - 1;
if (!terminal) {
curr[el] = curr[el] || { _errors: [] };
// if (typeof el === "string") {
// curr[el] = curr[el] || { _errors: [] };
// } else if (typeof el === "number") {
// const errorArray: any = [];
// errorArray._errors = [];
// curr[el] = curr[el] || errorArray;
// }
}
else {
curr[el] = curr[el] || { _errors: [] };
curr[el]._errors.push(mapper(issue));
}
curr = curr[el];
i++;
}
}
}
};
processError(this);
return fieldErrors;
}
static assert(value) {
if (!(value instanceof ZodError)) {
throw new Error(`Not a ZodError: ${value}`);
}
}
toString() {
return this.message;
}
get message() {
return JSON.stringify(this.issues, util.jsonStringifyReplacer, 2);
}
get isEmpty() {
return this.issues.length === 0;
}
flatten(mapper = (issue) => issue.message) {
const fieldErrors = {};
const formErrors = [];
for (const sub of this.issues) {
if (sub.path.length > 0) {
fieldErrors[sub.path[0]] = fieldErrors[sub.path[0]] || [];
fieldErrors[sub.path[0]].push(mapper(sub));
}
else {
formErrors.push(mapper(sub));
}
}
return { formErrors, fieldErrors };
}
get formErrors() {
return this.flatten();
}
}
ZodError.create = (issues) => {
const error = new ZodError(issues);
return error;
};
@@ -0,0 +1,9 @@
import defaultErrorMap from "./locales/en.js";
let overrideErrorMap = defaultErrorMap;
export { defaultErrorMap };
export function setErrorMap(map) {
overrideErrorMap = map;
}
export function getErrorMap() {
return overrideErrorMap;
}
@@ -0,0 +1,6 @@
export * from "./errors.js";
export * from "./helpers/parseUtil.js";
export * from "./helpers/typeAliases.js";
export * from "./helpers/util.js";
export * from "./types.js";
export * from "./ZodError.js";
@@ -0,0 +1 @@
export {};
@@ -0,0 +1,6 @@
export var errorUtil;
(function (errorUtil) {
errorUtil.errToObj = (message) => typeof message === "string" ? { message } : message || {};
// biome-ignore lint:
errorUtil.toString = (message) => typeof message === "string" ? message : message?.message;
})(errorUtil || (errorUtil = {}));
@@ -0,0 +1,109 @@
import { getErrorMap } from "../errors.js";
import defaultErrorMap from "../locales/en.js";
export const makeIssue = (params) => {
const { data, path, errorMaps, issueData } = params;
const fullPath = [...path, ...(issueData.path || [])];
const fullIssue = {
...issueData,
path: fullPath,
};
if (issueData.message !== undefined) {
return {
...issueData,
path: fullPath,
message: issueData.message,
};
}
let errorMessage = "";
const maps = errorMaps
.filter((m) => !!m)
.slice()
.reverse();
for (const map of maps) {
errorMessage = map(fullIssue, { data, defaultError: errorMessage }).message;
}
return {
...issueData,
path: fullPath,
message: errorMessage,
};
};
export const EMPTY_PATH = [];
export function addIssueToContext(ctx, issueData) {
const overrideMap = getErrorMap();
const issue = makeIssue({
issueData: issueData,
data: ctx.data,
path: ctx.path,
errorMaps: [
ctx.common.contextualErrorMap, // contextual error map is first priority
ctx.schemaErrorMap, // then schema-bound map if available
overrideMap, // then global override map
overrideMap === defaultErrorMap ? undefined : defaultErrorMap, // then global default map
].filter((x) => !!x),
});
ctx.common.issues.push(issue);
}
export class ParseStatus {
constructor() {
this.value = "valid";
}
dirty() {
if (this.value === "valid")
this.value = "dirty";
}
abort() {
if (this.value !== "aborted")
this.value = "aborted";
}
static mergeArray(status, results) {
const arrayValue = [];
for (const s of results) {
if (s.status === "aborted")
return INVALID;
if (s.status === "dirty")
status.dirty();
arrayValue.push(s.value);
}
return { status: status.value, value: arrayValue };
}
static async mergeObjectAsync(status, pairs) {
const syncPairs = [];
for (const pair of pairs) {
const key = await pair.key;
const value = await pair.value;
syncPairs.push({
key,
value,
});
}
return ParseStatus.mergeObjectSync(status, syncPairs);
}
static mergeObjectSync(status, pairs) {
const finalObject = {};
for (const pair of pairs) {
const { key, value } = pair;
if (key.status === "aborted")
return INVALID;
if (value.status === "aborted")
return INVALID;
if (key.status === "dirty")
status.dirty();
if (value.status === "dirty")
status.dirty();
if (key.value !== "__proto__" && (typeof value.value !== "undefined" || pair.alwaysSet)) {
finalObject[key.value] = value.value;
}
}
return { status: status.value, value: finalObject };
}
}
export const INVALID = Object.freeze({
status: "aborted",
});
export const DIRTY = (value) => ({ status: "dirty", value });
export const OK = (value) => ({ status: "valid", value });
export const isAborted = (x) => x.status === "aborted";
export const isDirty = (x) => x.status === "dirty";
export const isValid = (x) => x.status === "valid";
export const isAsync = (x) => typeof Promise !== "undefined" && x instanceof Promise;
@@ -0,0 +1 @@
export {};
@@ -0,0 +1 @@
export {};
@@ -0,0 +1,133 @@
export var util;
(function (util) {
util.assertEqual = (_) => { };
function assertIs(_arg) { }
util.assertIs = assertIs;
function assertNever(_x) {
throw new Error();
}
util.assertNever = assertNever;
util.arrayToEnum = (items) => {
const obj = {};
for (const item of items) {
obj[item] = item;
}
return obj;
};
util.getValidEnumValues = (obj) => {
const validKeys = util.objectKeys(obj).filter((k) => typeof obj[obj[k]] !== "number");
const filtered = {};
for (const k of validKeys) {
filtered[k] = obj[k];
}
return util.objectValues(filtered);
};
util.objectValues = (obj) => {
return util.objectKeys(obj).map(function (e) {
return obj[e];
});
};
util.objectKeys = typeof Object.keys === "function" // eslint-disable-line ban/ban
? (obj) => Object.keys(obj) // eslint-disable-line ban/ban
: (object) => {
const keys = [];
for (const key in object) {
if (Object.prototype.hasOwnProperty.call(object, key)) {
keys.push(key);
}
}
return keys;
};
util.find = (arr, checker) => {
for (const item of arr) {
if (checker(item))
return item;
}
return undefined;
};
util.isInteger = typeof Number.isInteger === "function"
? (val) => Number.isInteger(val) // eslint-disable-line ban/ban
: (val) => typeof val === "number" && Number.isFinite(val) && Math.floor(val) === val;
function joinValues(array, separator = " | ") {
return array.map((val) => (typeof val === "string" ? `'${val}'` : val)).join(separator);
}
util.joinValues = joinValues;
util.jsonStringifyReplacer = (_, value) => {
if (typeof value === "bigint") {
return value.toString();
}
return value;
};
})(util || (util = {}));
export var objectUtil;
(function (objectUtil) {
objectUtil.mergeShapes = (first, second) => {
return {
...first,
...second, // second overwrites first
};
};
})(objectUtil || (objectUtil = {}));
export const ZodParsedType = util.arrayToEnum([
"string",
"nan",
"number",
"integer",
"float",
"boolean",
"date",
"bigint",
"symbol",
"function",
"undefined",
"null",
"array",
"object",
"unknown",
"promise",
"void",
"never",
"map",
"set",
]);
export const getParsedType = (data) => {
const t = typeof data;
switch (t) {
case "undefined":
return ZodParsedType.undefined;
case "string":
return ZodParsedType.string;
case "number":
return Number.isNaN(data) ? ZodParsedType.nan : ZodParsedType.number;
case "boolean":
return ZodParsedType.boolean;
case "function":
return ZodParsedType.function;
case "bigint":
return ZodParsedType.bigint;
case "symbol":
return ZodParsedType.symbol;
case "object":
if (Array.isArray(data)) {
return ZodParsedType.array;
}
if (data === null) {
return ZodParsedType.null;
}
if (data.then && typeof data.then === "function" && data.catch && typeof data.catch === "function") {
return ZodParsedType.promise;
}
if (typeof Map !== "undefined" && data instanceof Map) {
return ZodParsedType.map;
}
if (typeof Set !== "undefined" && data instanceof Set) {
return ZodParsedType.set;
}
if (typeof Date !== "undefined" && data instanceof Date) {
return ZodParsedType.date;
}
return ZodParsedType.object;
default:
return ZodParsedType.unknown;
}
};
@@ -0,0 +1,4 @@
import * as z from "./external.js";
export * from "./external.js";
export { z };
export default z;
@@ -0,0 +1,107 @@
import { ZodIssueCode } from "../ZodError.js";
import { util, ZodParsedType } from "../helpers/util.js";
const errorMap = (issue, _ctx) => {
let message;
switch (issue.code) {
case ZodIssueCode.invalid_type:
if (issue.received === ZodParsedType.undefined) {
message = "Required";
}
else {
message = `Expected ${issue.expected}, received ${issue.received}`;
}
break;
case ZodIssueCode.invalid_literal:
message = `Invalid literal value, expected ${JSON.stringify(issue.expected, util.jsonStringifyReplacer)}`;
break;
case ZodIssueCode.unrecognized_keys:
message = `Unrecognized key(s) in object: ${util.joinValues(issue.keys, ", ")}`;
break;
case ZodIssueCode.invalid_union:
message = `Invalid input`;
break;
case ZodIssueCode.invalid_union_discriminator:
message = `Invalid discriminator value. Expected ${util.joinValues(issue.options)}`;
break;
case ZodIssueCode.invalid_enum_value:
message = `Invalid enum value. Expected ${util.joinValues(issue.options)}, received '${issue.received}'`;
break;
case ZodIssueCode.invalid_arguments:
message = `Invalid function arguments`;
break;
case ZodIssueCode.invalid_return_type:
message = `Invalid function return type`;
break;
case ZodIssueCode.invalid_date:
message = `Invalid date`;
break;
case ZodIssueCode.invalid_string:
if (typeof issue.validation === "object") {
if ("includes" in issue.validation) {
message = `Invalid input: must include "${issue.validation.includes}"`;
if (typeof issue.validation.position === "number") {
message = `${message} at one or more positions greater than or equal to ${issue.validation.position}`;
}
}
else if ("startsWith" in issue.validation) {
message = `Invalid input: must start with "${issue.validation.startsWith}"`;
}
else if ("endsWith" in issue.validation) {
message = `Invalid input: must end with "${issue.validation.endsWith}"`;
}
else {
util.assertNever(issue.validation);
}
}
else if (issue.validation !== "regex") {
message = `Invalid ${issue.validation}`;
}
else {
message = "Invalid";
}
break;
case ZodIssueCode.too_small:
if (issue.type === "array")
message = `Array must contain ${issue.exact ? "exactly" : issue.inclusive ? `at least` : `more than`} ${issue.minimum} element(s)`;
else if (issue.type === "string")
message = `String must contain ${issue.exact ? "exactly" : issue.inclusive ? `at least` : `over`} ${issue.minimum} character(s)`;
else if (issue.type === "number")
message = `Number must be ${issue.exact ? `exactly equal to ` : issue.inclusive ? `greater than or equal to ` : `greater than `}${issue.minimum}`;
else if (issue.type === "date")
message = `Date must be ${issue.exact ? `exactly equal to ` : issue.inclusive ? `greater than or equal to ` : `greater than `}${new Date(Number(issue.minimum))}`;
else
message = "Invalid input";
break;
case ZodIssueCode.too_big:
if (issue.type === "array")
message = `Array must contain ${issue.exact ? `exactly` : issue.inclusive ? `at most` : `less than`} ${issue.maximum} element(s)`;
else if (issue.type === "string")
message = `String must contain ${issue.exact ? `exactly` : issue.inclusive ? `at most` : `under`} ${issue.maximum} character(s)`;
else if (issue.type === "number")
message = `Number must be ${issue.exact ? `exactly` : issue.inclusive ? `less than or equal to` : `less than`} ${issue.maximum}`;
else if (issue.type === "bigint")
message = `BigInt must be ${issue.exact ? `exactly` : issue.inclusive ? `less than or equal to` : `less than`} ${issue.maximum}`;
else if (issue.type === "date")
message = `Date must be ${issue.exact ? `exactly` : issue.inclusive ? `smaller than or equal to` : `smaller than`} ${new Date(Number(issue.maximum))}`;
else
message = "Invalid input";
break;
case ZodIssueCode.custom:
message = `Invalid input`;
break;
case ZodIssueCode.invalid_intersection_types:
message = `Intersection results could not be merged`;
break;
case ZodIssueCode.not_multiple_of:
message = `Number must be a multiple of ${issue.multipleOf}`;
break;
case ZodIssueCode.not_finite:
message = "Number must be finite";
break;
default:
message = _ctx.defaultError;
util.assertNever(issue);
}
return { message };
};
export default errorMap;
@@ -0,0 +1 @@
export {};
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,3 @@
import z3 from "./v3/index.js";
export * from "./v3/index.js";
export default z3;
@@ -0,0 +1,164 @@
import type { Primitive } from "./helpers/typeAliases.js";
import { util, type ZodParsedType } from "./helpers/util.js";
import type { TypeOf, ZodType } from "./index.js";
type allKeys<T> = T extends any ? keyof T : never;
export type inferFlattenedErrors<T extends ZodType<any, any, any>, U = string> = typeToFlattenedError<TypeOf<T>, U>;
export type typeToFlattenedError<T, U = string> = {
formErrors: U[];
fieldErrors: {
[P in allKeys<T>]?: U[];
};
};
export declare const ZodIssueCode: {
invalid_type: "invalid_type";
invalid_literal: "invalid_literal";
custom: "custom";
invalid_union: "invalid_union";
invalid_union_discriminator: "invalid_union_discriminator";
invalid_enum_value: "invalid_enum_value";
unrecognized_keys: "unrecognized_keys";
invalid_arguments: "invalid_arguments";
invalid_return_type: "invalid_return_type";
invalid_date: "invalid_date";
invalid_string: "invalid_string";
too_small: "too_small";
too_big: "too_big";
invalid_intersection_types: "invalid_intersection_types";
not_multiple_of: "not_multiple_of";
not_finite: "not_finite";
};
export type ZodIssueCode = keyof typeof ZodIssueCode;
export type ZodIssueBase = {
path: (string | number)[];
message?: string | undefined;
};
export interface ZodInvalidTypeIssue extends ZodIssueBase {
code: typeof ZodIssueCode.invalid_type;
expected: ZodParsedType;
received: ZodParsedType;
}
export interface ZodInvalidLiteralIssue extends ZodIssueBase {
code: typeof ZodIssueCode.invalid_literal;
expected: unknown;
received: unknown;
}
export interface ZodUnrecognizedKeysIssue extends ZodIssueBase {
code: typeof ZodIssueCode.unrecognized_keys;
keys: string[];
}
export interface ZodInvalidUnionIssue extends ZodIssueBase {
code: typeof ZodIssueCode.invalid_union;
unionErrors: ZodError[];
}
export interface ZodInvalidUnionDiscriminatorIssue extends ZodIssueBase {
code: typeof ZodIssueCode.invalid_union_discriminator;
options: Primitive[];
}
export interface ZodInvalidEnumValueIssue extends ZodIssueBase {
received: string | number;
code: typeof ZodIssueCode.invalid_enum_value;
options: (string | number)[];
}
export interface ZodInvalidArgumentsIssue extends ZodIssueBase {
code: typeof ZodIssueCode.invalid_arguments;
argumentsError: ZodError;
}
export interface ZodInvalidReturnTypeIssue extends ZodIssueBase {
code: typeof ZodIssueCode.invalid_return_type;
returnTypeError: ZodError;
}
export interface ZodInvalidDateIssue extends ZodIssueBase {
code: typeof ZodIssueCode.invalid_date;
}
export type StringValidation = "email" | "url" | "emoji" | "uuid" | "nanoid" | "regex" | "cuid" | "cuid2" | "ulid" | "datetime" | "date" | "time" | "duration" | "ip" | "cidr" | "base64" | "jwt" | "base64url" | {
includes: string;
position?: number | undefined;
} | {
startsWith: string;
} | {
endsWith: string;
};
export interface ZodInvalidStringIssue extends ZodIssueBase {
code: typeof ZodIssueCode.invalid_string;
validation: StringValidation;
}
export interface ZodTooSmallIssue extends ZodIssueBase {
code: typeof ZodIssueCode.too_small;
minimum: number | bigint;
inclusive: boolean;
exact?: boolean;
type: "array" | "string" | "number" | "set" | "date" | "bigint";
}
export interface ZodTooBigIssue extends ZodIssueBase {
code: typeof ZodIssueCode.too_big;
maximum: number | bigint;
inclusive: boolean;
exact?: boolean;
type: "array" | "string" | "number" | "set" | "date" | "bigint";
}
export interface ZodInvalidIntersectionTypesIssue extends ZodIssueBase {
code: typeof ZodIssueCode.invalid_intersection_types;
}
export interface ZodNotMultipleOfIssue extends ZodIssueBase {
code: typeof ZodIssueCode.not_multiple_of;
multipleOf: number | bigint;
}
export interface ZodNotFiniteIssue extends ZodIssueBase {
code: typeof ZodIssueCode.not_finite;
}
export interface ZodCustomIssue extends ZodIssueBase {
code: typeof ZodIssueCode.custom;
params?: {
[k: string]: any;
};
}
export type DenormalizedError = {
[k: string]: DenormalizedError | string[];
};
export type ZodIssueOptionalMessage = ZodInvalidTypeIssue | ZodInvalidLiteralIssue | ZodUnrecognizedKeysIssue | ZodInvalidUnionIssue | ZodInvalidUnionDiscriminatorIssue | ZodInvalidEnumValueIssue | ZodInvalidArgumentsIssue | ZodInvalidReturnTypeIssue | ZodInvalidDateIssue | ZodInvalidStringIssue | ZodTooSmallIssue | ZodTooBigIssue | ZodInvalidIntersectionTypesIssue | ZodNotMultipleOfIssue | ZodNotFiniteIssue | ZodCustomIssue;
export type ZodIssue = ZodIssueOptionalMessage & {
fatal?: boolean | undefined;
message: string;
};
export declare const quotelessJson: (obj: any) => string;
type recursiveZodFormattedError<T> = T extends [any, ...any[]] ? {
[K in keyof T]?: ZodFormattedError<T[K]>;
} : T extends any[] ? {
[k: number]: ZodFormattedError<T[number]>;
} : T extends object ? {
[K in keyof T]?: ZodFormattedError<T[K]>;
} : unknown;
export type ZodFormattedError<T, U = string> = {
_errors: U[];
} & recursiveZodFormattedError<NonNullable<T>>;
export type inferFormattedError<T extends ZodType<any, any, any>, U = string> = ZodFormattedError<TypeOf<T>, U>;
export declare class ZodError<T = any> extends Error {
issues: ZodIssue[];
get errors(): ZodIssue[];
constructor(issues: ZodIssue[]);
format(): ZodFormattedError<T>;
format<U>(mapper: (issue: ZodIssue) => U): ZodFormattedError<T, U>;
static create: (issues: ZodIssue[]) => ZodError<any>;
static assert(value: unknown): asserts value is ZodError;
toString(): string;
get message(): string;
get isEmpty(): boolean;
addIssue: (sub: ZodIssue) => void;
addIssues: (subs?: ZodIssue[]) => void;
flatten(): typeToFlattenedError<T>;
flatten<U>(mapper?: (issue: ZodIssue) => U): typeToFlattenedError<T, U>;
get formErrors(): typeToFlattenedError<T, string>;
}
type stripPath<T extends object> = T extends any ? util.OmitKeys<T, "path"> : never;
export type IssueData = stripPath<ZodIssueOptionalMessage> & {
path?: (string | number)[];
fatal?: boolean | undefined;
};
export type ErrorMapCtx = {
defaultError: string;
data: any;
};
export type ZodErrorMap = (issue: ZodIssueOptionalMessage, _ctx: ErrorMapCtx) => {
message: string;
};
export {};
@@ -0,0 +1,5 @@
import type { ZodErrorMap } from "./ZodError.js";
import defaultErrorMap from "./locales/en.js";
export { defaultErrorMap };
export declare function setErrorMap(map: ZodErrorMap): void;
export declare function getErrorMap(): ZodErrorMap;
@@ -0,0 +1,6 @@
export * from "./errors.js";
export * from "./helpers/parseUtil.js";
export * from "./helpers/typeAliases.js";
export * from "./helpers/util.js";
export * from "./types.js";
export * from "./ZodError.js";
@@ -0,0 +1,8 @@
export declare namespace enumUtil {
type UnionToIntersectionFn<T> = (T extends unknown ? (k: () => T) => void : never) extends (k: infer Intersection) => void ? Intersection : never;
type GetUnionLast<T> = UnionToIntersectionFn<T> extends () => infer Last ? Last : never;
type UnionToTuple<T, Tuple extends unknown[] = []> = [T] extends [never] ? Tuple : UnionToTuple<Exclude<T, GetUnionLast<T>>, [GetUnionLast<T>, ...Tuple]>;
type CastToStringTuple<T> = T extends [string, ...string[]] ? T : never;
export type UnionToTupleString<T> = CastToStringTuple<UnionToTuple<T>>;
export {};
}
@@ -0,0 +1,9 @@
export declare namespace errorUtil {
type ErrMessage = string | {
message?: string | undefined;
};
const errToObj: (message?: ErrMessage) => {
message?: string | undefined;
};
const toString: (message?: ErrMessage) => string | undefined;
}
@@ -0,0 +1,78 @@
import type { IssueData, ZodErrorMap, ZodIssue } from "../ZodError.js";
import type { ZodParsedType } from "./util.js";
export declare const makeIssue: (params: {
data: any;
path: (string | number)[];
errorMaps: ZodErrorMap[];
issueData: IssueData;
}) => ZodIssue;
export type ParseParams = {
path: (string | number)[];
errorMap: ZodErrorMap;
async: boolean;
};
export type ParsePathComponent = string | number;
export type ParsePath = ParsePathComponent[];
export declare const EMPTY_PATH: ParsePath;
export interface ParseContext {
readonly common: {
readonly issues: ZodIssue[];
readonly contextualErrorMap?: ZodErrorMap | undefined;
readonly async: boolean;
};
readonly path: ParsePath;
readonly schemaErrorMap?: ZodErrorMap | undefined;
readonly parent: ParseContext | null;
readonly data: any;
readonly parsedType: ZodParsedType;
}
export type ParseInput = {
data: any;
path: (string | number)[];
parent: ParseContext;
};
export declare function addIssueToContext(ctx: ParseContext, issueData: IssueData): void;
export type ObjectPair = {
key: SyncParseReturnType<any>;
value: SyncParseReturnType<any>;
};
export declare class ParseStatus {
value: "aborted" | "dirty" | "valid";
dirty(): void;
abort(): void;
static mergeArray(status: ParseStatus, results: SyncParseReturnType<any>[]): SyncParseReturnType;
static mergeObjectAsync(status: ParseStatus, pairs: {
key: ParseReturnType<any>;
value: ParseReturnType<any>;
}[]): Promise<SyncParseReturnType<any>>;
static mergeObjectSync(status: ParseStatus, pairs: {
key: SyncParseReturnType<any>;
value: SyncParseReturnType<any>;
alwaysSet?: boolean;
}[]): SyncParseReturnType;
}
export interface ParseResult {
status: "aborted" | "dirty" | "valid";
data: any;
}
export type INVALID = {
status: "aborted";
};
export declare const INVALID: INVALID;
export type DIRTY<T> = {
status: "dirty";
value: T;
};
export declare const DIRTY: <T>(value: T) => DIRTY<T>;
export type OK<T> = {
status: "valid";
value: T;
};
export declare const OK: <T>(value: T) => OK<T>;
export type SyncParseReturnType<T = any> = OK<T> | DIRTY<T> | INVALID;
export type AsyncParseReturnType<T> = Promise<SyncParseReturnType<T>>;
export type ParseReturnType<T> = SyncParseReturnType<T> | AsyncParseReturnType<T>;
export declare const isAborted: (x: ParseReturnType<any>) => x is INVALID;
export declare const isDirty: <T>(x: ParseReturnType<T>) => x is OK<T> | DIRTY<T>;
export declare const isValid: <T>(x: ParseReturnType<T>) => x is OK<T>;
export declare const isAsync: <T>(x: ParseReturnType<T>) => x is AsyncParseReturnType<T>;
@@ -0,0 +1,8 @@
import type { ZodArray, ZodNullable, ZodObject, ZodOptional, ZodRawShape, ZodTuple, ZodTupleItems, ZodTypeAny } from "../types.js";
export declare namespace partialUtil {
type DeepPartial<T extends ZodTypeAny> = T extends ZodObject<ZodRawShape> ? ZodObject<{
[k in keyof T["shape"]]: ZodOptional<DeepPartial<T["shape"][k]>>;
}, T["_def"]["unknownKeys"], T["_def"]["catchall"]> : T extends ZodArray<infer Type, infer Card> ? ZodArray<DeepPartial<Type>, Card> : T extends ZodOptional<infer Type> ? ZodOptional<DeepPartial<Type>> : T extends ZodNullable<infer Type> ? ZodNullable<DeepPartial<Type>> : T extends ZodTuple<infer Items> ? {
[k in keyof Items]: Items[k] extends ZodTypeAny ? DeepPartial<Items[k]> : never;
} extends infer PI ? PI extends ZodTupleItems ? ZodTuple<PI> : never : never : T;
}
@@ -0,0 +1,2 @@
export type Primitive = string | number | symbol | bigint | boolean | null | undefined;
export type Scalars = Primitive | Primitive[];
@@ -0,0 +1,85 @@
export declare namespace util {
type AssertEqual<T, U> = (<V>() => V extends T ? 1 : 2) extends <V>() => V extends U ? 1 : 2 ? true : false;
export type isAny<T> = 0 extends 1 & T ? true : false;
export const assertEqual: <A, B>(_: AssertEqual<A, B>) => void;
export function assertIs<T>(_arg: T): void;
export function assertNever(_x: never): never;
export type Omit<T, K extends keyof T> = Pick<T, Exclude<keyof T, K>>;
export type OmitKeys<T, K extends string> = Pick<T, Exclude<keyof T, K>>;
export type MakePartial<T, K extends keyof T> = Omit<T, K> & Partial<Pick<T, K>>;
export type Exactly<T, X> = T & Record<Exclude<keyof X, keyof T>, never>;
export type InexactPartial<T> = {
[k in keyof T]?: T[k] | undefined;
};
export const arrayToEnum: <T extends string, U extends [T, ...T[]]>(items: U) => { [k in U[number]]: k; };
export const getValidEnumValues: (obj: any) => any[];
export const objectValues: (obj: any) => any[];
export const objectKeys: ObjectConstructor["keys"];
export const find: <T>(arr: T[], checker: (arg: T) => any) => T | undefined;
export type identity<T> = objectUtil.identity<T>;
export type flatten<T> = objectUtil.flatten<T>;
export type noUndefined<T> = T extends undefined ? never : T;
export const isInteger: NumberConstructor["isInteger"];
export function joinValues<T extends any[]>(array: T, separator?: string): string;
export const jsonStringifyReplacer: (_: string, value: any) => any;
export {};
}
export declare namespace objectUtil {
export type MergeShapes<U, V> = keyof U & keyof V extends never ? U & V : {
[k in Exclude<keyof U, keyof V>]: U[k];
} & V;
type optionalKeys<T extends object> = {
[k in keyof T]: undefined extends T[k] ? k : never;
}[keyof T];
type requiredKeys<T extends object> = {
[k in keyof T]: undefined extends T[k] ? never : k;
}[keyof T];
export type addQuestionMarks<T extends object, _O = any> = {
[K in requiredKeys<T>]: T[K];
} & {
[K in optionalKeys<T>]?: T[K];
} & {
[k in keyof T]?: unknown;
};
export type identity<T> = T;
export type flatten<T> = identity<{
[k in keyof T]: T[k];
}>;
export type noNeverKeys<T> = {
[k in keyof T]: [T[k]] extends [never] ? never : k;
}[keyof T];
export type noNever<T> = identity<{
[k in noNeverKeys<T>]: k extends keyof T ? T[k] : never;
}>;
export const mergeShapes: <U, T>(first: U, second: T) => T & U;
export type extendShape<A extends object, B extends object> = keyof A & keyof B extends never ? A & B : {
[K in keyof A as K extends keyof B ? never : K]: A[K];
} & {
[K in keyof B]: B[K];
};
export {};
}
export declare const ZodParsedType: {
string: "string";
nan: "nan";
number: "number";
integer: "integer";
float: "float";
boolean: "boolean";
date: "date";
bigint: "bigint";
symbol: "symbol";
function: "function";
undefined: "undefined";
null: "null";
array: "array";
object: "object";
unknown: "unknown";
promise: "promise";
void: "void";
never: "never";
map: "map";
set: "set";
};
export type ZodParsedType = keyof typeof ZodParsedType;
export declare const getParsedType: (data: any) => ZodParsedType;
@@ -0,0 +1,4 @@
import * as z from "./external.js";
export * from "./external.js";
export { z };
export default z;
@@ -0,0 +1,3 @@
import { type ZodErrorMap } from "../ZodError.js";
declare const errorMap: ZodErrorMap;
export default errorMap;
@@ -0,0 +1,102 @@
/**
* The Standard Schema interface.
*/
export type StandardSchemaV1<Input = unknown, Output = Input> = {
/**
* The Standard Schema properties.
*/
readonly "~standard": StandardSchemaV1.Props<Input, Output>;
};
export declare namespace StandardSchemaV1 {
/**
* The Standard Schema properties interface.
*/
export interface Props<Input = unknown, Output = Input> {
/**
* The version number of the standard.
*/
readonly version: 1;
/**
* The vendor name of the schema library.
*/
readonly vendor: string;
/**
* Validates unknown input values.
*/
readonly validate: (value: unknown) => Result<Output> | Promise<Result<Output>>;
/**
* Inferred types associated with the schema.
*/
readonly types?: Types<Input, Output> | undefined;
}
/**
* The result interface of the validate function.
*/
export type Result<Output> = SuccessResult<Output> | FailureResult;
/**
* The result interface if validation succeeds.
*/
export interface SuccessResult<Output> {
/**
* The typed output value.
*/
readonly value: Output;
/**
* The non-existent issues.
*/
readonly issues?: undefined;
}
/**
* The result interface if validation fails.
*/
export interface FailureResult {
/**
* The issues of failed validation.
*/
readonly issues: ReadonlyArray<Issue>;
}
/**
* The issue interface of the failure output.
*/
export interface Issue {
/**
* The error message of the issue.
*/
readonly message: string;
/**
* The path of the issue, if any.
*/
readonly path?: ReadonlyArray<PropertyKey | PathSegment> | undefined;
}
/**
* The path segment interface of the issue.
*/
export interface PathSegment {
/**
* The key representing a path segment.
*/
readonly key: PropertyKey;
}
/**
* The Standard Schema types interface.
*/
export interface Types<Input = unknown, Output = Input> {
/**
* The input type of the schema.
*/
readonly input: Input;
/**
* The output type of the schema.
*/
readonly output: Output;
}
/**
* Infers the input type of a Standard Schema.
*/
export type InferInput<Schema extends StandardSchemaV1> = NonNullable<Schema["~standard"]["types"]>["input"];
/**
* Infers the output type of a Standard Schema.
*/
export type InferOutput<Schema extends StandardSchemaV1> = NonNullable<Schema["~standard"]["types"]>["output"];
export {};
}
File diff suppressed because it is too large Load Diff
+120
View File
@@ -0,0 +1,120 @@
{
"name": "zod",
"version": "3.25.67",
"type": "module",
"author": "Colin McDonnell <zod@colinhacks.com>",
"description": "TypeScript-first schema declaration and validation library with static type inference",
"files": ["dist", "v3", "v4", "v4-mini"],
"funding": "https://github.com/sponsors/colinhacks",
"homepage": "https://zod.dev",
"keywords": ["typescript", "schema", "validation", "type", "inference"],
"license": "MIT",
"sideEffects": false,
"main": "./dist/cjs/index.js",
"types": "./dist/types/index.d.ts",
"module": "./dist/esm/index.js",
"exports": {
".": {
"import": {
"@zod/source": "./src/index.ts",
"types": "./dist/types/index.d.ts",
"default": "./dist/esm/index.js"
},
"require": {
"@zod/source": "./src/index.ts",
"types": "./dist/types/index.d.ts",
"default": "./dist/cjs/index.js"
}
},
"./v3": {
"import": {
"@zod/source": "./src/index.ts",
"types": "./dist/types/index.d.ts",
"default": "./dist/esm/index.js"
},
"require": {
"@zod/source": "./src/index.ts",
"types": "./dist/types/index.d.ts",
"default": "./dist/cjs/index.js"
}
},
"./v4": {
"import": {
"@zod/source": "./src/v4/index.ts",
"types": "./dist/types/v4/index.d.ts",
"default": "./dist/esm/v4/index.js"
},
"require": {
"@zod/source": "./src/v4/index.ts",
"types": "./dist/types/v4/index.d.ts",
"default": "./dist/cjs/v4/index.js"
}
},
"./v4-mini": {
"@zod/source": "./src/v4/mini/index.ts",
"types": "./dist/types/v4/mini/index.d.ts",
"import": "./dist/esm/v4/mini/index.js",
"require": "./dist/cjs/v4/mini/index.js"
},
"./v4/mini": {
"@zod/source": "./src/v4/mini/index.ts",
"types": "./dist/types/v4/mini/index.d.ts",
"import": "./dist/esm/v4/mini/index.js",
"require": "./dist/cjs/v4/mini/index.js"
},
"./v4/core": {
"@zod/source": "./src/v4/core/index.ts",
"types": "./dist/types/v4/core/index.d.ts",
"import": "./dist/esm/v4/core/index.js",
"require": "./dist/cjs/v4/core/index.js"
},
"./v4/locales": {
"@zod/source": "./src/v4/locales/index.ts",
"types": "./dist/types/v4/locales/index.d.ts",
"import": "./dist/esm/v4/locales/index.js",
"require": "./dist/cjs/v4/locales/index.js"
},
"./v4/locales/*": {
"@zod/source": "./src/v4/locales/*",
"types": "./dist/types/v4/locales/*",
"import": "./dist/esm/v4/locales/*",
"require": "./dist/cjs/v4/locales/*"
},
"./package.json": "./package.json"
},
"tshy": {
"exports": {
"./package.json": "./package.json",
".": "./src/index.ts",
"./v3": "./src/v3/index.ts",
"./v4": "./src/v4/index.ts",
"./v4-mini": "./src/v4/mini/index.ts",
"./v4/core": "./src/v4/core/index.ts",
"./v4/locales": "./src/v4/locales/index.ts",
"./v4/locales/*": "./src/v4/locales/*"
},
"sourceDialects": ["@zod/source"],
"exclude": ["./src/**/*.test.ts"]
},
"repository": {
"type": "git",
"url": "git+https://github.com/colinhacks/zod.git"
},
"bugs": {
"url": "https://github.com/colinhacks/zod/issues"
},
"support": {
"backing": {
"npm-funding": true
}
},
"scripts": {
"clean": "rm -rf dist",
"build": "tsx ./build.mts",
"postbuild": "pnpm biome check --write .",
"test:watch": "pnpm vitest",
"test": "pnpm vitest run",
"bump:beta": "pnpm version \"v$(pnpm pkg get version | jq -r)-beta.$(date +%Y%m%dT%H%M%S)\"",
"pub:beta": "pnpm bump:beta && pnpm publish --tag next --publish-branch v4 --no-git-checks --dry-run"
}
}
+1
View File
@@ -0,0 +1 @@
{ "name": "zod3-boundary-case", "private": true, "type": "module" }