diff --git a/ark/attest/__tests__/externalSnapshots.test.ts b/ark/attest/__tests__/externalSnapshots.test.ts index 76b0c48f8..ce2e794fd 100644 --- a/ark/attest/__tests__/externalSnapshots.test.ts +++ b/ark/attest/__tests__/externalSnapshots.test.ts @@ -1,5 +1,5 @@ import { attest, contextualize } from "@ark/attest" -import { attestInternal } from "@ark/attest/internal/assert/attest.ts" +import { attestInternal } from "@ark/attest/internal/assert/attest.js" import { dirName, readJson, writeJson } from "@ark/fs" import * as assert from "node:assert/strict" import { rmSync } from "node:fs" diff --git a/ark/attest/package.json b/ark/attest/package.json index e349aa1ca..c32b63891 100644 --- a/ark/attest/package.json +++ b/ark/attest/package.json @@ -1,6 +1,6 @@ { "name": "@ark/attest", - "version": "0.20.0", + "version": "0.21.0", "author": { "name": "David Blass", "email": "david@arktype.io", diff --git a/ark/docs/src/components/Code.astro b/ark/docs/src/components/Code.astro index db416d453..492b5d527 100644 --- a/ark/docs/src/components/Code.astro +++ b/ark/docs/src/components/Code.astro @@ -3,7 +3,7 @@ import { arkHighlight, type HighlightArgs, type BuiltinLang -} from "./highlight.js" +} from "./highlight.ts" // ideally we could just import { Code } from "astro:components" instead of // using this custom component, but as of now, the `Code` component imported from diff --git a/ark/docs/src/components/HeroBackdrop.tsx b/ark/docs/src/components/HeroBackdrop.tsx index 5805c54b9..a94e555f9 100644 --- a/ark/docs/src/components/HeroBackdrop.tsx +++ b/ark/docs/src/components/HeroBackdrop.tsx @@ -1,6 +1,6 @@ import React from "react" -import { FloatYourBoat } from "./FloatYourBoat.js" -import { PlatformCloud } from "./PlatformCloud.js" +import { FloatYourBoat } from "./FloatYourBoat.tsx" +import { PlatformCloud } from "./PlatformCloud.tsx" // workaround for compatibility issue between MDX and Astro declare module "react" { diff --git a/ark/docs/tsconfig.json b/ark/docs/tsconfig.json index 2c036d74c..23e0dab36 100644 --- a/ark/docs/tsconfig.json +++ b/ark/docs/tsconfig.json @@ -6,7 +6,12 @@ "module": "ESNext", "moduleResolution": "Bundler", "allowJs": true, - "jsx": "preserve" + "jsx": "preserve", + // leaving this in for now as it results in some nonsense in 5.7 alpha like: + // "This relative import path is unsafe to rewrite because it looks like a file name, + // but actually resolves to ./src/components/shiki.config.js" + // can remove this line if it doesn't break the build in the future + "rewriteRelativeImportExtensions": false }, "mdx": { "checkMdx": true diff --git a/ark/fs/package.json b/ark/fs/package.json index 31843df58..7e2d60351 100644 --- a/ark/fs/package.json +++ b/ark/fs/package.json @@ -1,6 +1,6 @@ { "name": "@ark/fs", - "version": "0.15.0", + "version": "0.16.0", "author": { "name": "David Blass", "email": "david@arktype.io", diff --git a/ark/repo/build.ts b/ark/repo/build.ts index 6cc00f640..be6416862 100644 --- a/ark/repo/build.ts +++ b/ark/repo/build.ts @@ -1,15 +1,7 @@ import { symlinkSync } from "fs" import { join } from "path" // eslint-disable-next-line @typescript-eslint/no-restricted-imports -import { - fromCwd, - fromHere, - rewriteFile, - rmRf, - shell, - walkPaths, - writeJson -} from "../fs/index.ts" +import { fromCwd, fromHere, rmRf, shell, writeJson } from "../fs/index.ts" const buildKind = process.argv.includes("--cjs") || process.env.ARKTYPE_CJS ? "cjs" : "esm" @@ -25,14 +17,6 @@ try { rmRf("tsconfig.build.json") symlinkSync(`../repo/tsconfig.${buildKind}.json`, "tsconfig.build.json") buildCurrentProject() - walkPaths(outDir, { excludeDirs: true }).forEach(jsPath => - rewriteFile(jsPath, src => - src.replaceAll( - /(import|export\s+.*?from\s+["'])(.*?\.ts)(["'])/g, - (match, p1, p2, p3) => `${p1}${p2.replace(".ts", ".js")}${p3}` - ) - ) - ) rmRf("tsconfig.build.json") symlinkSync(`../repo/tsconfig.dts.json`, "tsconfig.build.json") buildCurrentProject() diff --git a/ark/repo/scratch.ts b/ark/repo/scratch.ts index b246331ce..ae2281dff 100644 --- a/ark/repo/scratch.ts +++ b/ark/repo/scratch.ts @@ -1,4 +1,4 @@ -import { type } from "arktype" +import { type, type Type } from "arktype" const out = type("string.lower").to("string.trim").to("'success'")("Success") //? diff --git a/ark/repo/tsconfig.cjs.json b/ark/repo/tsconfig.cjs.json index da386d2b2..ef169c22b 100644 --- a/ark/repo/tsconfig.cjs.json +++ b/ark/repo/tsconfig.cjs.json @@ -8,10 +8,6 @@ "outDir": "out", "noEmit": false, "declaration": false, - "noCheck": true, - // the errors that would come from this setting don't trigger because of noCheck. - // required to force TS to emit despite .ts endings, which we rewrite - "allowImportingTsExtensions": false, "customConditions": [] }, "exclude": ["out", "__tests__"] diff --git a/ark/repo/tsconfig.esm.json b/ark/repo/tsconfig.esm.json index fba917a34..0652e3a7f 100644 --- a/ark/repo/tsconfig.esm.json +++ b/ark/repo/tsconfig.esm.json @@ -6,9 +6,6 @@ "noEmit": false, "declaration": false, "noCheck": true, - // the errors that would come from this setting don't trigger because of noCheck. - // required to force TS to emit despite .ts endings, which we rewrite - "allowImportingTsExtensions": false, "customConditions": [] }, "exclude": ["out", "__tests__"] diff --git a/ark/schema/__tests__/proto.test.ts b/ark/schema/__tests__/proto.test.ts new file mode 100644 index 000000000..74c524ff0 --- /dev/null +++ b/ark/schema/__tests__/proto.test.ts @@ -0,0 +1,13 @@ +import { attest, contextualize } from "@ark/attest" +import { node, rootSchema } from "@ark/schema" + +contextualize(() => { + it("normalizes node schema", () => { + const d = node("proto", Date) + attest(d.json).snap({ proto: "Date" }) + + const reparsed = rootSchema(d) + attest(reparsed.json).equals(d.json) + attest(reparsed.id).equals(d.id) + }) +}) diff --git a/ark/schema/constraint.ts b/ark/schema/constraint.ts index 6448742e3..724990c2b 100644 --- a/ark/schema/constraint.ts +++ b/ark/schema/constraint.ts @@ -35,7 +35,10 @@ import { type UnknownAttachments, type kindLeftOf } from "./shared/implement.ts" -import { intersectNodes, intersectNodesRoot } from "./shared/intersections.ts" +import { + intersectNodesRoot, + intersectOrPipeNodes +} from "./shared/intersections.ts" import type { JsonSchema } from "./shared/jsonSchema.ts" import { $ark } from "./shared/registry.ts" import type { TraverseAllows, TraverseApply } from "./shared/traversal.ts" @@ -167,14 +170,14 @@ export const intersectConstraints = ( for (const root of s.roots) { if (result instanceof Disjoint) return result - result = intersectNodes(root, result, s.ctx)! + result = intersectOrPipeNodes(root, result, s.ctx)! } return result as never } let matched = false for (let i = 0; i < s.l.length; i++) { - const result = intersectNodes(s.l[i], head, s.ctx) + const result = intersectOrPipeNodes(s.l[i], head, s.ctx) if (result === null) continue if (result instanceof Disjoint) return result diff --git a/ark/schema/node.ts b/ark/schema/node.ts index b5109f603..a9cabf448 100644 --- a/ark/schema/node.ts +++ b/ark/schema/node.ts @@ -96,13 +96,9 @@ export abstract class BaseNode< withMeta( meta: ArkEnv.meta | ((currentMeta: ArkEnv.meta) => ArkEnv.meta) ): this { - const newMeta = - typeof meta === "function" ? - meta({ ...this.meta }) - : { ...this.meta, ...meta } return this.$.node(this.kind, { ...this.inner, - meta: newMeta + meta: typeof meta === "function" ? meta({ ...this.meta }) : meta }) as never } diff --git a/ark/schema/package.json b/ark/schema/package.json index d42d698d9..7834028b2 100644 --- a/ark/schema/package.json +++ b/ark/schema/package.json @@ -1,6 +1,6 @@ { "name": "@ark/schema", - "version": "0.15.0", + "version": "0.16.0", "license": "MIT", "author": { "name": "David Blass", diff --git a/ark/schema/roots/alias.ts b/ark/schema/roots/alias.ts index a7de46d83..092a93296 100644 --- a/ark/schema/roots/alias.ts +++ b/ark/schema/roots/alias.ts @@ -13,7 +13,7 @@ import { implementNode, type nodeImplementationOf } from "../shared/implement.ts" -import { intersectNodes } from "../shared/intersections.ts" +import { intersectOrPipeNodes } from "../shared/intersections.ts" import { writeCyclicJsonSchemaMessage, type JsonSchema @@ -76,14 +76,16 @@ const implementation: nodeImplementationOf = alias: (l, r, ctx) => ctx.$.lazilyResolve( () => - neverIfDisjoint(intersectNodes(l.resolution, r.resolution, ctx)), + neverIfDisjoint( + intersectOrPipeNodes(l.resolution, r.resolution, ctx) + ), `${l.reference}${ctx.pipe ? "=>" : "&"}${r.reference}` ), ...defineRightwardIntersections("alias", (l, r, ctx) => { if (r.isUnknown()) return l if (r.isNever()) return r return ctx.$.lazilyResolve( - () => neverIfDisjoint(intersectNodes(l.resolution, r, ctx)), + () => neverIfDisjoint(intersectOrPipeNodes(l.resolution, r, ctx)), `${l.reference}${ctx.pipe ? "=>" : "&"}${r.id}` ) }) diff --git a/ark/schema/roots/intersection.ts b/ark/schema/roots/intersection.ts index 1c37fea27..c56f5c35c 100644 --- a/ark/schema/roots/intersection.ts +++ b/ark/schema/roots/intersection.ts @@ -39,7 +39,7 @@ import { type RefinementKind, type StructuralKind } from "../shared/implement.ts" -import { intersectNodes } from "../shared/intersections.ts" +import { intersectOrPipeNodes } from "../shared/intersections.ts" import type { JsonSchema } from "../shared/jsonSchema.ts" import type { TraverseAllows, TraverseApply } from "../shared/traversal.ts" import { @@ -238,7 +238,7 @@ const implementation: nodeImplementationOf = // if l is unknown, return r if (l.children.length === 0) return r - const basis = l.basis ? intersectNodes(l.basis, r, ctx) : r + const basis = l.basis ? intersectOrPipeNodes(l.basis, r, ctx) : r return ( basis instanceof Disjoint ? basis @@ -380,7 +380,7 @@ const intersectIntersections = ( const basisResult = lBasis ? rBasis ? - (intersectNodes( + (intersectOrPipeNodes( lBasis, rBasis, ctx diff --git a/ark/schema/roots/morph.ts b/ark/schema/roots/morph.ts index bba5ac47f..c730af2eb 100644 --- a/ark/schema/roots/morph.ts +++ b/ark/schema/roots/morph.ts @@ -15,7 +15,7 @@ import { type nodeImplementationOf, type RootKind } from "../shared/implement.ts" -import { intersectNodes } from "../shared/intersections.ts" +import { intersectOrPipeNodes } from "../shared/intersections.ts" import { writeJsonSchemaMorphMessage, type JsonSchema @@ -96,7 +96,7 @@ const implementation: nodeImplementationOf = writeMorphIntersectionMessage(l.expression, r.expression) ) } - const inTersection = intersectNodes(l.in, r.in, ctx) + const inTersection = intersectOrPipeNodes(l.in, r.in, ctx) if (inTersection instanceof Disjoint) return inTersection const baseInner: Omit, "in"> = { @@ -104,7 +104,7 @@ const implementation: nodeImplementationOf = } if (l.declaredIn || r.declaredIn) { - const declaredIn = intersectNodes(l.in, r.in, ctx) + const declaredIn = intersectOrPipeNodes(l.in, r.in, ctx) // we can't treat this as a normal Disjoint since it's just declared // it should only happen if someone's essentially trying to create a broken type if (declaredIn instanceof Disjoint) return declaredIn.throw() @@ -112,7 +112,7 @@ const implementation: nodeImplementationOf = } if (l.declaredOut || r.declaredOut) { - const declaredOut = intersectNodes(l.out, r.out, ctx) + const declaredOut = intersectOrPipeNodes(l.out, r.out, ctx) if (declaredOut instanceof Disjoint) return declaredOut.throw() else baseInner.declaredOut = declaredOut } @@ -129,7 +129,7 @@ const implementation: nodeImplementationOf = ) }, ...defineRightwardIntersections("morph", (l, r, ctx) => { - const inTersection = intersectNodes(l.in, r, ctx) + const inTersection = intersectOrPipeNodes(l.in, r, ctx) return inTersection instanceof Disjoint ? inTersection : ( inTersection.distribute( branch => ({ @@ -158,7 +158,9 @@ export class MorphNode extends BaseRoot { : undefined; override get in(): BaseRoot { - return this.declaredIn ?? this.inner.in ?? $ark.intrinsic.unknown.internal + return ( + this.declaredIn ?? this.inner.in?.in ?? $ark.intrinsic.unknown.internal + ) } override get out(): BaseRoot { diff --git a/ark/schema/roots/proto.ts b/ark/schema/roots/proto.ts index 2b4bc2a18..420d9d9bd 100644 --- a/ark/schema/roots/proto.ts +++ b/ark/schema/roots/proto.ts @@ -25,6 +25,7 @@ import { } from "../shared/jsonSchema.ts" import { $ark } from "../shared/registry.ts" import type { TraverseAllows } from "../shared/traversal.ts" +import { isNode } from "../shared/utils.ts" import { InternalBasis } from "./basis.ts" import type { Domain } from "./domain.ts" @@ -75,7 +76,9 @@ const implementation: nodeImplementationOf = }, normalize: schema => typeof schema === "string" ? { proto: builtinConstructors[schema] } - : typeof schema === "function" ? { proto: schema } + : typeof schema === "function" ? + isNode(schema) ? (schema as {} as ProtoNode) + : { proto: schema } : typeof schema.proto === "string" ? { ...schema, proto: builtinConstructors[schema.proto] } : (schema as Proto.ExpandedSchema), diff --git a/ark/schema/roots/root.ts b/ark/schema/roots/root.ts index 53c492c46..a34bacfb0 100644 --- a/ark/schema/roots/root.ts +++ b/ark/schema/roots/root.ts @@ -106,6 +106,25 @@ export abstract class BaseRoot< ) } + withoutOptionalOrDefaultMeta(): this { + if (!this.optionalMeta && this.defaultMeta === unset) return this + const meta = { ...this.meta } + delete meta.default + delete meta.optional + + if ( + !this.hasKind("morph") || + (!this.in.optionalMeta && this.in.defaultMeta === unset) + ) + return this.withMeta(meta) + + return this.$.node("morph", { + ...this.inner, + in: this.in.withoutOptionalOrDefaultMeta(), + meta + }) as never + } + as(): this { return this } diff --git a/ark/schema/roots/union.ts b/ark/schema/roots/union.ts index 4bad01dfd..6752adbae 100644 --- a/ark/schema/roots/union.ts +++ b/ark/schema/roots/union.ts @@ -35,7 +35,10 @@ import { type UnionChildKind, type nodeImplementationOf } from "../shared/implement.ts" -import { intersectNodes, intersectNodesRoot } from "../shared/intersections.ts" +import { + intersectNodesRoot, + intersectOrPipeNodes +} from "../shared/intersections.ts" import type { JsonSchema } from "../shared/jsonSchema.ts" import { $ark, @@ -591,7 +594,7 @@ export const intersectBranches = ( candidatesByR = {} break } - const branchIntersection = intersectNodes(l[lIndex], r[rIndex], ctx) + const branchIntersection = intersectOrPipeNodes(l[lIndex], r[rIndex], ctx) if (branchIntersection instanceof Disjoint) { // Doesn't tell us anything useful about their relationships // with other branches diff --git a/ark/schema/shared/errors.ts b/ark/schema/shared/errors.ts index c44499a0a..0fe50527c 100644 --- a/ark/schema/shared/errors.ts +++ b/ark/schema/shared/errors.ts @@ -87,6 +87,11 @@ export class ArkErrors extends ReadonlyArray { private mutable: ArkError[] = this as never add(error: ArkError): void { + if (this.includes(error)) return + this._add(error) + } + + private _add(error: ArkError): void { const existing = this.byPath[error.propString] if (existing) { const errorIntersection = new ArkError( @@ -114,14 +119,15 @@ export class ArkErrors extends ReadonlyArray { } merge(errors: ArkErrors): void { - errors.forEach(e => - this.add( + errors.forEach(e => { + if (this.includes(e)) return + this._add( new ArkError( { ...e, path: [...e.path, ...this.ctx.path] } as never, this.ctx ) ) - ) + }) } get summary(): string { diff --git a/ark/schema/shared/intersections.ts b/ark/schema/shared/intersections.ts index 2a886469e..e2ad8ffed 100644 --- a/ark/schema/shared/intersections.ts +++ b/ark/schema/shared/intersections.ts @@ -29,60 +29,73 @@ export const intersectNodesRoot: InternalNodeIntersection = ( r, $ ) => - intersectNodes(l, r, { + intersectOrPipeNodes(l, r, { $, invert: false, pipe: false }) export const pipeNodesRoot: InternalNodeIntersection = (l, r, $) => - intersectNodes(l, r, { + intersectOrPipeNodes(l, r, { $, invert: false, pipe: true }) -export const intersectNodes: InternalNodeIntersection = ( - l, - r, - ctx -) => { - const operator = ctx.pipe ? "|>" : "&" - const lrCacheKey = `${l.hash}${operator}${r.hash}` - if (intersectionCache[lrCacheKey] !== undefined) - return intersectionCache[lrCacheKey]! as never - - if (!ctx.pipe) { - // we can only use this for the commutative & operator - const rlCacheKey = `${r.hash}${operator}${l.hash}` - if (intersectionCache[rlCacheKey] !== undefined) { - // if the cached result was a Disjoint and the operands originally - // appeared in the opposite order, we need to invert it to match - const rlResult = intersectionCache[rlCacheKey]! - const lrResult = - rlResult instanceof Disjoint ? rlResult.invert() : rlResult - // add the lr result to the cache directly to bypass this check in the future - intersectionCache[lrCacheKey] = lrResult - return lrResult as never +export const intersectOrPipeNodes: InternalNodeIntersection = + (( + l: BaseNode, + r: BaseNode, + ctx: IntersectionContext + ): BaseNode | Disjoint | null => { + const operator = ctx.pipe ? "|>" : "&" + const lrCacheKey = `${l.hash}${operator}${r.hash}` + if (intersectionCache[lrCacheKey] !== undefined) + return intersectionCache[lrCacheKey]! as never + + if (!ctx.pipe) { + // we can only use this for the commutative & operator + const rlCacheKey = `${r.hash}${operator}${l.hash}` + if (intersectionCache[rlCacheKey] !== undefined) { + // if the cached result was a Disjoint and the operands originally + // appeared in the opposite order, we need to invert it to match + const rlResult = intersectionCache[rlCacheKey]! + const lrResult = + rlResult instanceof Disjoint ? rlResult.invert() : rlResult + // add the lr result to the cache directly to bypass this check in the future + intersectionCache[lrCacheKey] = lrResult + return lrResult + } } - } - if (l.equals(r as never)) return l as never - - let result = - ctx.pipe && l.hasKindIn(...rootKinds) && r.hasKindIn(...rootKinds) ? - _pipeNodes(l, r, ctx) - : _intersectNodes(l, r, ctx) - - if (isNode(result)) { - // if the result equals one of the operands, preserve its metadata by - // returning the original reference - if (l.equals(result)) result = l as never - else if (r.equals(result)) result = r as never - } - intersectionCache[lrCacheKey] = result - return result as never -} + const isPureIntersection = + !ctx.pipe || (!l.includesMorph && !r.includesMorph) + + if (isPureIntersection && l.equals(r)) return l + + let result: BaseNode | Disjoint | null + + if (isPureIntersection) { + if (l.equals(r)) return l + result = _intersectNodes(l, r, ctx) + } else { + result = + l.hasKindIn(...rootKinds) ? + // if l is a RootNode, r will be as well + _pipeNodes(l, r as never, ctx) + : _intersectNodes(l, r, ctx) + } + + if (isNode(result)) { + // if the result equals one of the operands, preserve its metadata by + // returning the original reference + if (l.equals(result)) result = l as never + else if (r.equals(result)) result = r as never + } + + intersectionCache[lrCacheKey] = result + return result as never + }) as never const _intersectNodes = ( l: BaseNode, @@ -110,14 +123,10 @@ const _pipeNodes = ( r: nodeOfKind, ctx: IntersectionContext ) => - l.includesMorph ? - ctx.invert ? - pipeMorphed(r as never, l, ctx) - : pipeMorphed(l, r as never, ctx) - : r.includesMorph ? + l.includesMorph || r.includesMorph ? ctx.invert ? - pipeMorphed(r, l as never, ctx) - : pipeMorphed(l as never, r, ctx) + pipeMorphed(r, l, ctx) + : pipeMorphed(l, r, ctx) : _intersectNodes(l, r, ctx) const pipeMorphed = ( @@ -135,6 +144,7 @@ const pipeMorphed = ( if (viableBranches.length === 0) return Disjoint.init("union", from.branches, to.branches) + // if the input type has changed, create a new node without preserving metadata if ( viableBranches.length < from.branches.length || !from.branches.every((branch, i) => @@ -143,6 +153,8 @@ const pipeMorphed = ( ) return ctx.$.parseSchema(viableBranches) + // otherwise, the input has not changed so preserve metadata + let meta: BaseMeta | undefined if ("default" in from.meta) meta = { default: from.meta.default } @@ -178,7 +190,11 @@ const _pipeMorphed = ( const morphs = [...from.morphs] if (from.lastMorphIfNode) { // still piped from context, so allows appending additional morphs - const outIntersection = intersectNodes(from.lastMorphIfNode, to, ctx) + const outIntersection = intersectOrPipeNodes( + from.lastMorphIfNode, + to, + ctx + ) if (outIntersection instanceof Disjoint) return outIntersection morphs[morphs.length - 1] = outIntersection } else morphs.push(to) @@ -190,7 +206,7 @@ const _pipeMorphed = ( } if (to.hasKind("morph")) { - const inTersection = intersectNodes(from, to.in, ctx) + const inTersection = intersectOrPipeNodes(from, to.in, ctx) if (inTersection instanceof Disjoint) return inTersection return ctx.$.node("morph", { diff --git a/ark/schema/shared/traversal.ts b/ark/schema/shared/traversal.ts index 8166a7a75..2ba1b958e 100644 --- a/ark/schema/shared/traversal.ts +++ b/ark/schema/shared/traversal.ts @@ -8,7 +8,7 @@ import { type ArkErrorContextInput, type ArkErrorInput } from "./errors.ts" -import { pathToPropString, type TraversalPath } from "./utils.ts" +import { isNode, pathToPropString, type TraversalPath } from "./utils.ts" export type MorphsAtPath = { path: TraversalPath @@ -60,52 +60,76 @@ export class TraversalContext { ) this.root = this.config.clone(this.root) + this.applyQueuedMorphs() + + return this.hasError() ? this.errors : this.root + } + + private applyQueuedMorphs() { // invoking morphs that are Nodes will reuse this context, potentially // adding additional morphs, so we have to continue looping until // queuedMorphs is empty rather than iterating over the list once while (this.queuedMorphs.length) { - const { path, morphs } = this.queuedMorphs.shift()! - - // even if we already have an error, apply morphs that are not at a path - // with errors to capture potential validation errors - if (this.hasError()) { - const morphPropString = pathToPropString(path) - if (this.errors.some(e => morphPropString.startsWith(e.propString))) - continue + const queuedMorphs = this.queuedMorphs + this.queuedMorphs = [] + for (const { path, morphs } of queuedMorphs) { + // even if we already have an error, apply morphs that are not at a path + // with errors to capture potential validation errors + if (this.pathHasError(path)) continue + + this.applyMorphsAtPath(path, morphs) } + } + } - const key = path.at(-1) + private applyMorphsAtPath(path: TraversalPath, morphs: array): void { + const key = path.at(-1) - let parent: any + let parent: any - if (key !== undefined) { - // find the object on which the key to be morphed exists - parent = this.root - for (let pathIndex = 0; pathIndex < path.length - 1; pathIndex++) - parent = parent[path[pathIndex]] - } + if (key !== undefined) { + // find the object on which the key to be morphed exists + parent = this.root + for (let pathIndex = 0; pathIndex < path.length - 1; pathIndex++) + parent = parent[path[pathIndex]] + } + + this.path = path - this.path = path - for (const morph of morphs) { - const result = morph( - parent === undefined ? this.root : parent[key!], - this - ) - if (result instanceof ArkError) { - // if an ArkError was returned but wasn't added to these - // errors, add it - if (!this.errors.includes(result)) this.error(result) - } else if (!(result instanceof ArkErrors)) { - // if the morph was successful, assign the result to the - // corresponding property, or to root if path is empty - if (parent === undefined) this.root = result - else parent[key!] = result + for (const morph of morphs) { + const morphIsNode = isNode(morph) + + const result = morph( + parent === undefined ? this.root : parent[key!], + this + ) + + if (result instanceof ArkError) { + // if an ArkError was returned, ensure it has been added to errors + this.errors.add(result) + // skip any remaining morphs at the current path + break + } + if (result instanceof ArkErrors) { + // if the morph was a direct reference to another node, + // errors will have been added directly via this piped context + if (!morphIsNode) { + // otherwise, we have to ensure each error has been added + this.errors.merge(result) } - // if ArkErrors was returned, the morph itself was likely a type - // and the errors will have been added directly via this piped context + // skip any remaining morphs at the current path + break } + + // if the morph was successful, assign the result to the + // corresponding property, or to root if path is empty + if (parent === undefined) this.root = result + else parent[key!] = result + + // if the current morph queued additional morphs, + // applying them before subsequent morphs + this.applyQueuedMorphs() } - return this.hasError() ? this.errors : this.root } get currentErrorCount(): number { @@ -122,6 +146,13 @@ export class TraversalContext { return this.currentErrorCount !== 0 } + pathHasError(path: TraversalPath): boolean { + if (!this.hasError()) return false + + const propString = pathToPropString(path) + return this.errors.some(e => propString.startsWith(e.propString)) + } + get failFast(): boolean { return this.branches.length !== 0 } diff --git a/ark/schema/structure/index.ts b/ark/schema/structure/index.ts index 84f5c28d7..15602fc22 100644 --- a/ark/schema/structure/index.ts +++ b/ark/schema/structure/index.ts @@ -21,7 +21,7 @@ import { type RootKind, type nodeImplementationOf } from "../shared/implement.ts" -import { intersectNodes } from "../shared/intersections.ts" +import { intersectOrPipeNodes } from "../shared/intersections.ts" import { $ark } from "../shared/registry.ts" import type { TraverseAllows, TraverseApply } from "../shared/traversal.ts" @@ -93,7 +93,7 @@ const implementation: nodeImplementationOf = intersections: { index: (l, r, ctx) => { if (l.signature.equals(r.signature)) { - const valueIntersection = intersectNodes(l.value, r.value, ctx) + const valueIntersection = intersectOrPipeNodes(l.value, r.value, ctx) const value = valueIntersection instanceof Disjoint ? $ark.intrinsic.never.internal diff --git a/ark/schema/structure/prop.ts b/ark/schema/structure/prop.ts index 7db159efc..e8b02ce71 100644 --- a/ark/schema/structure/prop.ts +++ b/ark/schema/structure/prop.ts @@ -13,7 +13,7 @@ import { compileSerializedValue, type NodeCompiler } from "../shared/compile.ts" import type { BaseNormalizedSchema } from "../shared/declare.ts" import { Disjoint } from "../shared/disjoint.ts" import type { IntersectionContext, RootKind } from "../shared/implement.ts" -import { intersectNodes } from "../shared/intersections.ts" +import { intersectOrPipeNodes } from "../shared/intersections.ts" import { $ark } from "../shared/registry.ts" import type { TraverseAllows, TraverseApply } from "../shared/traversal.ts" import type { Optional } from "./optional.ts" @@ -50,7 +50,7 @@ export const intersectProps = ( if (l.key !== r.key) return null const key = l.key - let value = intersectNodes(l.value, r.value, ctx) + let value = intersectOrPipeNodes(l.value, r.value, ctx) const kind: Prop.Kind = l.required || r.required ? "required" : "optional" if (value instanceof Disjoint) { if (kind === "optional") value = $ark.intrinsic.never.internal diff --git a/ark/schema/structure/sequence.ts b/ark/schema/structure/sequence.ts index f15e5b264..de59fad85 100644 --- a/ark/schema/structure/sequence.ts +++ b/ark/schema/structure/sequence.ts @@ -31,7 +31,7 @@ import { type RootKind, type nodeImplementationOf } from "../shared/implement.ts" -import { intersectNodes } from "../shared/intersections.ts" +import { intersectOrPipeNodes } from "../shared/intersections.ts" import { writeUnsupportedJsonSchemaTypeMessage, type JsonSchema @@ -528,7 +528,7 @@ const _intersectSequences = ( s.fixedVariants.push(postfixBranchResult) } - const result = intersectNodes(lHead.node, rHead.node, s.ctx) + const result = intersectOrPipeNodes(lHead.node, rHead.node, s.ctx) if (result instanceof Disjoint) { if (kind === "prefix" || kind === "postfix") { s.disjoint.push( diff --git a/ark/schema/structure/structure.ts b/ark/schema/structure/structure.ts index a0efd07d2..7ce70a724 100644 --- a/ark/schema/structure/structure.ts +++ b/ark/schema/structure/structure.ts @@ -458,12 +458,15 @@ export class StructureNode extends BaseConstraint { const { optional, ...inner } = this.inner return this.$.node("structure", { ...inner, - required: this.props.map(prop => - prop.hasKind("optional") ? - // don't include keys like default that don't exist on required - this.$.node("required", { key: prop.key, value: prop.value }) - : prop - ) + required: this.props.map(prop => { + if (prop.hasKind("required")) return prop + // strip default/optional meta from the value so that it + // isn't reduced back to an optional prop + return this.$.node("required", { + key: prop.key, + value: prop.value.withoutOptionalOrDefaultMeta() + }) + }) }) } diff --git a/ark/type/__tests__/keywords/required.test.ts b/ark/type/__tests__/keywords/required.test.ts index 304032164..61c57c95f 100644 --- a/ark/type/__tests__/keywords/required.test.ts +++ b/ark/type/__tests__/keywords/required.test.ts @@ -34,4 +34,18 @@ contextualize(() => { attest(t.expression).snap("{ [string]: number, bar: 1, foo: 1 }") }) + + // https://github.com/arktypeio/arktype/issues/1156 + it("with default", () => { + const t = type({ foo: "string = 'bar'" }).required() + + const expected = type({ + foo: "string" + }) + + // https://github.com/arktypeio/arktype/issues/1160 + // attest(); + + attest(t.expression).equals(expected.expression) + }) }) diff --git a/ark/type/__tests__/pipe.test.ts b/ark/type/__tests__/pipe.test.ts index 03ed1cad8..b76c4c5a0 100644 --- a/ark/type/__tests__/pipe.test.ts +++ b/ark/type/__tests__/pipe.test.ts @@ -45,6 +45,43 @@ contextualize(() => { attest(tOut.expression).equals(expected.expression) }) + it("to morph", () => { + const restringifyUser = (o: object) => JSON.stringify(o) + + const t = type("string.json.parse").to([ + { + name: "string", + age: "number" + }, + "=>", + restringifyUser + ]) + + attest(t.t).type.toString.snap(`( + In: string & { + " of": { base: string; attributes: Predicate<"json"> } + } +) => Out`) + + attest(t.infer) + attest(t.json).snap({ + in: "string", + morphs: [ + "$ark.parseJson", + { + in: { + required: [ + { key: "age", value: "number" }, + { key: "name", value: "string" } + ], + domain: "object" + }, + morphs: ["$ark.restringifyUser"] + } + ] + }) + }) + describe("try", () => { it("can catch thrown errors", () => { const parseJson = type("string").pipe.try((s): object => JSON.parse(s)) @@ -790,4 +827,65 @@ Right: { foo: (In: string) => Out<{ [string]: $jsonObject | number | string | $j attest(t("SUCCESS ")).equals("success") attest(t("success ")).equals("success") }) + + const appendLengthMorph = (s: string) => `${s}${s.length}` + + // https://discord.com/channels/957797212103016458/1291014543635517542 + it("repeated Type pipe", () => { + const appendLength = type("string", "=>", appendLengthMorph) + const appendLengths = type("string").pipe(appendLength, appendLength) + + attest(appendLengths.json).snap({ + in: "string", + morphs: [ + { + in: "string", + morphs: [ + "$ark.appendLengthMorph", + { in: "string", morphs: ["$ark.appendLengthMorph"] } + ] + } + ] + }) + + attest(appendLengths("a")).snap("a12") + }) + + // https://discord.com/channels/957797212103016458/1291014543635517542 + it("repeated Type pipe with intermediate morph", () => { + const appendLength = type("string", "=>", appendLengthMorph) + + const appendSeparatorMorph = (s: string) => `${s}|` + + const appendSeparatedLengths = type("string").pipe( + appendLength, + appendLength, + appendSeparatorMorph, + appendLength, + appendLength + ) + + attest(appendSeparatedLengths.json).snap({ + in: "string", + morphs: [ + { + in: "string", + morphs: [ + "$ark.appendLengthMorph", + { in: "string", morphs: ["$ark.appendLengthMorph"] } + ] + }, + "$ark.appendSeparatorMorph", + { + in: "string", + morphs: [ + "$ark.appendLengthMorph", + { in: "string", morphs: ["$ark.appendLengthMorph"] } + ] + } + ] + }) + + attest(appendSeparatedLengths("a")).snap("a12|45") + }) }) diff --git a/ark/type/__tests__/realWorld.test.ts b/ark/type/__tests__/realWorld.test.ts index 75d8356cd..22b8c8886 100644 --- a/ark/type/__tests__/realWorld.test.ts +++ b/ark/type/__tests__/realWorld.test.ts @@ -973,4 +973,40 @@ nospace must be matched by ^\\S*$ (was "One space")`) ) .type.errors(writeUnboundableMessage("string | string[]")) }) + + // https://discord.com/channels/957797212103016458/1290304355643293747 + it("can extract proto Node at property", () => { + const d = type("Date") + + const o = type({ + last_updated: d + }) + + const t = o.get("last_updated") + + attest(t.t) + attest(d.expression).snap("Date") + attest(t.expression).equals(d.expression) + attest(t.extends(d)).equals(true) + }) + + it("piped through Type", () => { + const Letters = type("'a'|'b'|'c'") + // normally, this would just be .to(Letters), but this should work as + // well, even if it's less efficient + const Letter = type("string").pipe(s => Letters(s)) + + attest(Letter("d").toString()).snap('must be "a", "b" or "c" (was "d")') + }) + + it(".in types are always unionable", () => { + const MorphArrayMorph = type("string") + .pipe(e => e) + .array() + .pipe(e => e) + const OtherType = type("string[]") + const EitherInput = MorphArrayMorph.in.or(OtherType.in) + + attest(EitherInput(["str"])).snap(["str"]) + }) }) diff --git a/ark/type/keywords/inference.ts b/ark/type/keywords/inference.ts index 928740a0e..33bc0ed33 100644 --- a/ark/type/keywords/inference.ts +++ b/ark/type/keywords/inference.ts @@ -266,7 +266,7 @@ type distillIo = : (In: i) => Out : never -type inferredOptionalOrDefaultKeyOf = +export type inferredOptionalOrDefaultKeyOf = | inferredDefaultKeyOf | inferredOptionalKeyOf diff --git a/ark/type/keywords/string/json.ts b/ark/type/keywords/string/json.ts index b87dcbe1a..2d644477f 100644 --- a/ark/type/keywords/string/json.ts +++ b/ark/type/keywords/string/json.ts @@ -1,4 +1,9 @@ -import { intrinsic, rootSchema, type TraversalContext } from "@ark/schema" +import { + intrinsic, + rootSchema, + type Morph, + type TraversalContext +} from "@ark/schema" import type { Module, Submodule } from "../../module.ts" import type { Predicate, To, of } from "../inference.ts" import { arkModule } from "../utils.ts" @@ -33,28 +38,30 @@ const root = rootSchema({ } }) +const parseJson: Morph = (s: string, ctx: TraversalContext) => { + if (s.length === 0) { + return ctx.error({ + code: "predicate", + expected: jsonStringDescription, + actual: "empty" + }) + } + try { + return JSON.parse(s) + } catch (e) { + return ctx.error({ + code: "predicate", + expected: jsonStringDescription, + problem: writeJsonSyntaxErrorProblem(e) + }) + } +} + export const json: stringJson.module = arkModule({ root, parse: rootSchema({ in: "string", - morphs: (s: string, ctx: TraversalContext) => { - if (s.length === 0) { - return ctx.error({ - code: "predicate", - expected: jsonStringDescription, - actual: "empty" - }) - } - try { - return JSON.parse(s) - } catch (e) { - return ctx.error({ - code: "predicate", - expected: jsonStringDescription, - problem: writeJsonSyntaxErrorProblem(e) - }) - } - }, + morphs: parseJson, declaredOut: intrinsic.json }) }) diff --git a/ark/type/methods/base.ts b/ark/type/methods/base.ts index df69a9258..91cf04e43 100644 --- a/ark/type/methods/base.ts +++ b/ark/type/methods/base.ts @@ -42,12 +42,27 @@ import type { instantiateType } from "./instantiate.ts" interface Type extends Callable<(data: unknown) => distill.Out | ArkErrors> { [inferred]: t + /** + * The top-level generic parameter accepted by the `Type`.\ + * Potentially includes morphs and subtype constraints not reflected + * in the types fully-inferred input (via `inferIn`) or output (via `infer` or `inferOut`). + * @example type A = type.infer<[typeof T.t, '[]']> + */ t: t + /** + * A type representing the output the `Type` will return (after morphs are applied to valid input) + * @example export type MyType = typeof MyType.infer + * @example export interface MyType extends Identity {} + */ infer: this["inferOut"] inferBrandableIn: distill.brandable.In inferBrandableOut: distill.brandable.Out inferIntrospectableOut: distill.introspectable.Out inferOut: distill.Out + /** + * A type representing the input the `Type` will accept (before morphs are applied) + * @example export type MyTypeInput = typeof MyType.inferIn + */ inferIn: distill.In inferredOutIsIntrospectable: t extends InferredMorph ? [o] extends [anyOrNever] ? true @@ -57,6 +72,7 @@ interface Type unknown extends t ? boolean : true + /** Internal JSON representation of this `Type` */ json: Json toJSON(): Json meta: ArkAmbient.meta @@ -67,8 +83,18 @@ interface Type internal: BaseRoot $: Scope<$> + /** + * Validate data, throwing `type.errors` instance on failure + * @returns a valid value + * @example const validData = T.assert(rawData) + */ assert(data: unknown): this["infer"] + /** + * Check if data matches the input shape. + * Doesn't process any morphs, but does check narrows. + * @example type({ foo: "number" }).allows({ foo: "bar" }) // false + */ allows(data: unknown): data is this["inferIn"] traverse(data: unknown): this["infer"] | ArkErrors @@ -79,36 +105,88 @@ interface Type describe(description: string): this + /** + * Create a copy of this `Type` with updated unknown key behavior + * - `ignore`: ignore unknown properties (default) + * - 'reject': disallow objects with unknown properties + * - 'delete': clone the object and keep only known properties + */ onUndeclaredKey(behavior: UndeclaredKeyBehavior): this + /** + * Create a copy of this `Type` with updated unknown key behavior\ + * The behavior applies to the whole object tree, not just the immediate properties. + * - `ignore`: ignore unknown properties (default) + * - 'reject': disallow objects with unknown properties + * - 'delete': clone the object and keep only known properties + */ onDeepUndeclaredKey(behavior: UndeclaredKeyBehavior): this + /** + * Identical to `assert`, but with a typed input as a convenience for providing a typed value. + * @example const ConfigT = type({ foo: "string" }); export const config = ConfigT.from({ foo: "bar" }) + */ from(literal: this["inferIn"]): this["infer"] + /** + * Cast the way this `Type` is inferred (has no effect at runtime). + * const branded = type(/^a/).as<`a${string}`>() // Type<`a${string}`> + */ as(...args: validateChainedAsArgs): instantiateType // brand>>( // name: name // ): instantiateType + /** + * A `Type` representing the deeply-extracted input of the `Type` (before morphs are applied). + * @example const inputT = T.in + */ get in(): instantiateType + /** + * A `Type` representing the deeply-extracted output of the `Type` (after morphs are applied).\ + * **IMPORTANT**: If your type includes morphs, their output will likely be unknown + * unless they were defined with an explicit output validator via `.to(outputType)`, `.pipe(morph, outputType)`, etc. + * @example const outputT = T.out + */ get out(): instantiateType // inferring r into an alias improves perf and avoids return type inference // that can lead to incorrect results. See: // https://discord.com/channels/957797212103016458/1285420361415917680/1285545752172429312 + /** + * Intersect another `Type` definition, returning an introspectable `Disjoint` if the result is unsatisfiable. + * @example const intersection = type({ foo: "number" }).intersect({ bar: "string" }) // Type<{ foo: number; bar: string }> + * @example const intersection = type({ foo: "number" }).intersect({ foo: "string" }) // Disjoint + */ intersect>( def: type.validate ): instantiateType, $> | Disjoint + /** + * Intersect another `Type` definition, throwing an error if the result is unsatisfiable. + * @example const intersection = type({ foo: "number" }).intersect({ bar: "string" }) // Type<{ foo: number; bar: string }> + */ and>( def: type.validate ): instantiateType, $> + /** + * Union another `Type` definition.\ + * If the types contain morphs, input shapes should be distinct. Otherwise an error will be thrown. + * @example const union = type({ foo: "number" }).or({ foo: "string" }) // Type<{ foo: number } | { foo: string }> + * @example const union = type("string.numeric.parse").or("number") // Type<((In: string) => Out) | number> + */ or>( def: type.validate ): instantiateType + /** + * Add a custom predicate to this `Type`. + * @example const nan = type('number').narrow(n => Number.isNaN(n)) // Type + * @example const foo = type("string").narrow((s): s is `foo${string}` => s.startsWith('foo') || ctx.mustBe('string starting with "foo"')) // Type<"foo${string}"> + * @example const unique = type('string[]').narrow((a, ctx) => new Set(a).size === a.length || ctx.mustBe('array with unique elements')) + */ narrow< narrowed extends this["infer"] = never, r = [narrowed] extends [never] ? @@ -140,8 +218,17 @@ interface Type | PredicateCast ): instantiateType + /** + * Create a `Type` for array with elements of this `Type` + * @example const T = type(/^foo/); const array = T.array() // Type + */ array(): ArrayType + /** + * Morph this `Type` through a chain of morphs. + * @example const dedupe = type('string[]').pipe(a => Array.from(new Set(a))) + * @example type({codes: 'string.numeric[]'}).pipe(obj => obj.codes).to('string.numeric.parse[]') + */ pipe: ChainedPipes equals(def: type.validate): boolean @@ -175,6 +262,14 @@ interface Type // work the way it does for the other methods here optional>(): instantiateType + /** + * Add a default value for this `Type` when it is used as a property.\ + * Default value should be a valid input value for this `Type, or a function that returns a valid input value.\ + * If the type has a morph, it will be applied to the default value. + * @example const withDefault = type({ foo: type("string").default("bar") }); withDefault({}) // { foo: "bar" } + * @example const withFactory = type({ foo: type("number[]").default(() => [1])) }); withFactory({baz: 'a'}) // { foo: [1], baz: 'a' } + * @example const withMorph = type({ foo: type("string.numeric.parse").default("123") }); withMorph({}) // { foo: 123 } + */ default< const value extends this["inferIn"], r = applyAttribute> diff --git a/ark/type/methods/morph.ts b/ark/type/methods/morph.ts index 39260fa19..df6adbb80 100644 --- a/ark/type/methods/morph.ts +++ b/ark/type/methods/morph.ts @@ -6,6 +6,10 @@ import type { BaseType } from "./base.ts" // non-morph branches /** @ts-ignore cast variance */ interface Type extends BaseType { + /** + * Append extra validation shape on the pipe output + * @example type({codes: 'string.numeric[]'}).pipe(obj => obj.codes).to('string.numeric.parse[]') + */ to>( def: type.validate ): Type, $> diff --git a/ark/type/methods/object.ts b/ark/type/methods/object.ts index 399e56a72..22b280949 100644 --- a/ark/type/methods/object.ts +++ b/ark/type/methods/object.ts @@ -37,6 +37,10 @@ interface Type extends BaseType { keyof(): instantiateType, $> + /** + * Get the `Type` of a property of this `Type`. + * @example type({ foo: "string" }).get("foo") // Type + */ get, r = arkGet>( k1: k1 | type.cast ): instantiateType @@ -58,6 +62,10 @@ interface Type extends BaseType { k3: k3 | type.cast ): instantiateType + /** + * Create a copy of this `Type` with only the specified properties. + * @example type({ foo: "string", bar: "number" }).pick("foo") // Type<{ foo: string }> + */ pick = never>( ...keys: (key | type.cast)[] ): Type< @@ -67,6 +75,10 @@ interface Type extends BaseType { $ > + /** + * Create a copy of this `Type` with all properties except the specified ones. + * @example type({ foo: "string", bar: "number" }).omit("foo") // Type<{ bar: number }> + */ omit = never>( ...keys: (key | type.cast)[] ): Type< @@ -76,14 +88,26 @@ interface Type extends BaseType { $ > + /** + * Merge another `Type` definition, overriding properties of this `Type` with the duplicate keys. + * @example type({ a: "1", b: "2" }).merge({ b: "3", c: "4" }) // Type<{ a: 1, b: 3, c: 4 }> + */ merge>( def: type.validate & (r extends object ? unknown : ErrorType<"Merged type must be an object", [actual: r]>) ): Type, $> + /** + * Create a copy of this `Type` with all properties required. + * @example const T = type({ "foo?"": "string" }).required() // Type<{ foo: string }> + */ required(): Type<{ [k in keyof t]-?: t[k] }, $> + /** + * Create a copy of this `Type` with all properties optional. + * @example: const T = type({ foo: "string" }).optional() // Type<{ foo?: string }> + */ partial(): Type<{ [k in keyof t]?: t[k] }, $> map>( @@ -91,6 +115,10 @@ interface Type extends BaseType { flatMapEntry: (entry: typePropOf) => transformed ): Type, $> + /** + * List of property info of this `Type`. + * @example type({ foo: "string = "" }).props // [{ kind: "required", key: "foo", value: Type, default: "" }] + */ props: array> } diff --git a/ark/type/package.json b/ark/type/package.json index f1e4de878..b8e2bcaf6 100644 --- a/ark/type/package.json +++ b/ark/type/package.json @@ -1,7 +1,7 @@ { "name": "arktype", "description": "TypeScript's 1:1 validator, optimized from editor to runtime", - "version": "2.0.0-rc.13", + "version": "2.0.0-rc.14", "license": "MIT", "author": { "name": "David Blass", diff --git a/ark/type/type.ts b/ark/type/type.ts index a4838dae9..86a5aca21 100644 --- a/ark/type/type.ts +++ b/ark/type/type.ts @@ -86,6 +86,11 @@ export interface TypeParser<$ = {}> extends Ark.boundTypeAttachments<$> { : [] ): r + /** + * Error class for validation errors + * Calling type instance returns an instance of this class on failure + * @example if ( T(data) instanceof type.errors ) { ... } + */ errors: typeof ArkErrors hkt: typeof Hkt keywords: typeof keywords @@ -96,7 +101,18 @@ export interface TypeParser<$ = {}> extends Ark.boundTypeAttachments<$> { define: DefinitionParser<$> generic: GenericParser<$> schema: SchemaParser<$> + /** + * Create a `Type` that is satisfied only by a value strictly equal (`===`) to the argument passed to this function. + * @example const foo = type.unit('foo') // Type<'foo'> + * @example const sym: unique symbol = Symbol(); type.unit(sym) // Type + */ unit: UnitTypeParser<$> + /** + * Create a `Type` that is satisfied only by a value strictly equal (`===`) to one of the arguments passed to this function. + * @example const enum = type.enumerated('foo', 'bar', obj) // obj is a by-reference object + * @example const tupleForm = type(['===', 'foo', 'bar', obj]) + * @example const argsForm = type('===', 'foo', 'bar', obj) + */ enumerated: EnumeratedTypeParser<$> } diff --git a/ark/util/package.json b/ark/util/package.json index 8c96feb81..8bf85bd26 100644 --- a/ark/util/package.json +++ b/ark/util/package.json @@ -1,6 +1,6 @@ { "name": "@ark/util", - "version": "0.15.0", + "version": "0.16.0", "author": { "name": "David Blass", "email": "david@arktype.io", diff --git a/ark/util/registry.ts b/ark/util/registry.ts index e0641612d..c71824fab 100644 --- a/ark/util/registry.ts +++ b/ark/util/registry.ts @@ -7,7 +7,7 @@ import { FileConstructor, objectKindOf } from "./objectKinds.ts" // recent node versions (https://nodejs.org/api/esm.html#json-modules). // For now, we assert this matches the package.json version via a unit test. -export const arkUtilVersion = "0.14.0" +export const arkUtilVersion = "0.16.0" export const initialRegistryContents = { version: arkUtilVersion, diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 2bd643fc4..1387061e5 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -2,6 +2,6 @@ packages: - "ark/*" catalog: - typescript: 5.6.2 + typescript: 5.7.0-dev.20240930 "@ark/attest-ts-min": npm:typescript@5.1.6 "@ark/attest-ts-next": npm:typescript@next diff --git a/tsconfig.json b/tsconfig.json index ecec87396..c3677030b 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -2,6 +2,7 @@ "extends": "./ark/util/tsconfig.base.json", "compilerOptions": { "allowImportingTsExtensions": true, + "rewriteRelativeImportExtensions": true, "customConditions": ["ark-ts"], "types": ["mocha", "node"] // "noErrorTruncation": true