mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-11-05 08:13:06 +08:00
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:
co-authored by
Claude Opus 5.5
parent
f593e17e1d
commit
17bef8c03b
@@ -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() }
|
||||
: {}),
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
@@ -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
@@ -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.
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
import z3 from "./v3/index.js";
|
||||
export * from "./v3/index.js";
|
||||
export default z3;
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"type": "module"
|
||||
}
|
||||
+132
@@ -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;
|
||||
};
|
||||
+9
@@ -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;
|
||||
}
|
||||
+6
@@ -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";
|
||||
Generated
Vendored
+1
@@ -0,0 +1 @@
|
||||
export {};
|
||||
Generated
Vendored
+6
@@ -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 = {}));
|
||||
Generated
Vendored
+109
@@ -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;
|
||||
Generated
Vendored
+1
@@ -0,0 +1 @@
|
||||
export {};
|
||||
Generated
Vendored
+1
@@ -0,0 +1 @@
|
||||
export {};
|
||||
Generated
Vendored
+133
@@ -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;
|
||||
}
|
||||
};
|
||||
+4
@@ -0,0 +1,4 @@
|
||||
import * as z from "./external.js";
|
||||
export * from "./external.js";
|
||||
export { z };
|
||||
export default z;
|
||||
Generated
Vendored
+107
@@ -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;
|
||||
Generated
Vendored
+1
@@ -0,0 +1 @@
|
||||
export {};
|
||||
+3691
File diff suppressed because it is too large
Load Diff
+3
@@ -0,0 +1,3 @@
|
||||
import z3 from "./v3/index.js";
|
||||
export * from "./v3/index.js";
|
||||
export default z3;
|
||||
Generated
Vendored
+164
@@ -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 {};
|
||||
Generated
Vendored
+5
@@ -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;
|
||||
Generated
Vendored
+6
@@ -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";
|
||||
Generated
Vendored
+8
@@ -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 {};
|
||||
}
|
||||
Generated
Vendored
+9
@@ -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;
|
||||
}
|
||||
Generated
Vendored
+78
@@ -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>;
|
||||
Generated
Vendored
+8
@@ -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;
|
||||
}
|
||||
Generated
Vendored
+2
@@ -0,0 +1,2 @@
|
||||
export type Primitive = string | number | symbol | bigint | boolean | null | undefined;
|
||||
export type Scalars = Primitive | Primitive[];
|
||||
Generated
Vendored
+85
@@ -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;
|
||||
+4
@@ -0,0 +1,4 @@
|
||||
import * as z from "./external.js";
|
||||
export * from "./external.js";
|
||||
export { z };
|
||||
export default z;
|
||||
Generated
Vendored
+3
@@ -0,0 +1,3 @@
|
||||
import { type ZodErrorMap } from "../ZodError.js";
|
||||
declare const errorMap: ZodErrorMap;
|
||||
export default errorMap;
|
||||
Generated
Vendored
+102
@@ -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 {};
|
||||
}
|
||||
+1031
File diff suppressed because it is too large
Load Diff
+120
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
{ "name": "zod3-boundary-case", "private": true, "type": "module" }
|
||||
Reference in New Issue
Block a user