Repository: arktypeio/arktype Branch: main Commit: d075962caee1 Files: 550 Total size: 2.8 MB Directory structure: gitextract_8q21qzq_/ ├── .cursor/ │ └── commands/ │ └── armstrong.md ├── .github/ │ ├── CODE_OF_CONDUCT.md │ ├── CONTRIBUTING.md │ ├── FUNDING.yml │ ├── ISSUE_TEMPLATE/ │ │ ├── bug_report.md │ │ └── feature_request.md │ ├── SECURITY.md │ ├── actions/ │ │ └── setup/ │ │ └── action.yml │ ├── pull_request_template.md │ └── workflows/ │ ├── pr.yml │ ├── publish.yml │ └── pullfrog.yml ├── .gitignore ├── .vscode/ │ ├── extensions.json │ ├── launch.json │ └── settings.json ├── LICENSE ├── ark/ │ ├── attest/ │ │ ├── CHANGELOG.md │ │ ├── README.md │ │ ├── __tests__/ │ │ │ ├── assertions.test.ts │ │ │ ├── benchExpectedOutput.ts │ │ │ ├── benchTemplate.ts │ │ │ ├── completions.test.ts │ │ │ ├── demo.test.ts │ │ │ ├── externalSnapshots.test.ts │ │ │ ├── functions.test.ts │ │ │ ├── instantiations.test.ts │ │ │ ├── satisfies.test.ts │ │ │ ├── snap.test.ts │ │ │ ├── snapExpectedOutput.ts │ │ │ ├── snapPopulation.test.ts │ │ │ ├── snapTemplate.ts │ │ │ ├── unwrap.test.ts │ │ │ └── utils.ts │ │ ├── assert/ │ │ │ ├── assertions.ts │ │ │ ├── attest.ts │ │ │ └── chainableAssertions.ts │ │ ├── bench/ │ │ │ ├── await1k.ts │ │ │ ├── baseline.ts │ │ │ ├── bench.ts │ │ │ ├── call1k.ts │ │ │ ├── measure.ts │ │ │ └── type.ts │ │ ├── cache/ │ │ │ ├── getCachedAssertions.ts │ │ │ ├── snapshots.ts │ │ │ ├── ts.ts │ │ │ ├── utils.ts │ │ │ └── writeAssertionCache.ts │ │ ├── cli/ │ │ │ ├── cli.ts │ │ │ ├── precache.ts │ │ │ ├── shared.ts │ │ │ ├── stats.ts │ │ │ └── trace.ts │ │ ├── config.ts │ │ ├── fixtures.ts │ │ ├── index.ts │ │ ├── package.json │ │ ├── tsVersioning.ts │ │ └── utils.ts │ ├── docs/ │ │ ├── .turbo/ │ │ │ └── daemon/ │ │ │ └── a6661884d53fb864-turbo.log.2025-10-05 │ │ ├── README.md │ │ ├── app/ │ │ │ ├── (home)/ │ │ │ │ ├── layout.tsx │ │ │ │ └── page.tsx │ │ │ ├── api/ │ │ │ │ └── search/ │ │ │ │ └── route.ts │ │ │ ├── discord/ │ │ │ │ └── page.tsx │ │ │ ├── docs/ │ │ │ │ ├── [[...slug]]/ │ │ │ │ │ └── page.tsx │ │ │ │ └── layout.tsx │ │ │ ├── global-error.tsx │ │ │ ├── global.css │ │ │ ├── layout.config.tsx │ │ │ ├── layout.tsx │ │ │ ├── metadata.ts │ │ │ ├── playground/ │ │ │ │ └── page.tsx │ │ │ └── providers.tsx │ │ ├── components/ │ │ │ ├── AnchorAliases.tsx │ │ │ ├── ApiTable.tsx │ │ │ ├── ArkCard.tsx │ │ │ ├── AutoplayDemo.tsx │ │ │ ├── Badge.tsx │ │ │ ├── Banner.tsx │ │ │ ├── Button.tsx │ │ │ ├── CodeBlock.tsx │ │ │ ├── FloatYourBoat.tsx │ │ │ ├── GhStarButton.tsx │ │ │ ├── Head.tsx │ │ │ ├── Hero.tsx │ │ │ ├── InstallationTabs.tsx │ │ │ ├── KeywordTable.tsx │ │ │ ├── LinkCard.tsx │ │ │ ├── LocalFriendlyUrl.tsx │ │ │ ├── PlatformCloud.tsx │ │ │ ├── ReleaseBanner.tsx │ │ │ ├── RuntimeBenchmarksGraph.tsx │ │ │ ├── SyntaxTabs.tsx │ │ │ ├── apiData.ts │ │ │ ├── dts/ │ │ │ │ ├── regex.ts │ │ │ │ ├── schema.ts │ │ │ │ ├── type.ts │ │ │ │ └── util.ts │ │ │ ├── icons/ │ │ │ │ ├── arktype-logo.tsx │ │ │ │ ├── boat.tsx │ │ │ │ ├── bun.tsx │ │ │ │ ├── chromium.tsx │ │ │ │ ├── deno.tsx │ │ │ │ ├── intellij.tsx │ │ │ │ ├── js.tsx │ │ │ │ ├── neovim.tsx │ │ │ │ ├── node.tsx │ │ │ │ ├── npm.tsx │ │ │ │ ├── ts.tsx │ │ │ │ └── vscode.tsx │ │ │ ├── playground/ │ │ │ │ ├── ParseResult.tsx │ │ │ │ ├── Playground.tsx │ │ │ │ ├── PlaygroundTabs.tsx │ │ │ │ ├── RestoreDefault.tsx │ │ │ │ ├── ShareLink.tsx │ │ │ │ ├── TraverseResult.tsx │ │ │ │ ├── completions.ts │ │ │ │ ├── errorLens.ts │ │ │ │ ├── execute.ts │ │ │ │ ├── format.ts │ │ │ │ ├── highlights.ts │ │ │ │ ├── hovers.ts │ │ │ │ ├── tsserver.ts │ │ │ │ └── utils.ts │ │ │ └── snippets/ │ │ │ ├── betterErrors.twoslash.ts │ │ │ ├── clarityAndConcision.twoslash.js │ │ │ ├── contentsById.ts │ │ │ ├── deepIntrospectability.twoslash.js │ │ │ ├── intrinsicOptimization.twoslash.js │ │ │ ├── nestedTypeInScopeError.twoslash.js │ │ │ └── unparalleledDx.twoslash.js │ │ ├── content/ │ │ │ └── docs/ │ │ │ ├── blog/ │ │ │ │ ├── 2.0.mdx │ │ │ │ ├── 2.1.mdx │ │ │ │ ├── 2.2.mdx │ │ │ │ ├── arkregex.mdx │ │ │ │ ├── index.mdx │ │ │ │ └── meta.json │ │ │ ├── comparisons.mdx │ │ │ ├── configuration/ │ │ │ │ ├── index.mdx │ │ │ │ └── meta.json │ │ │ ├── declare.mdx │ │ │ ├── ecosystem/ │ │ │ │ ├── index.mdx │ │ │ │ └── meta.json │ │ │ ├── expressions/ │ │ │ │ ├── index.mdx │ │ │ │ └── meta.json │ │ │ ├── faq.mdx │ │ │ ├── generics/ │ │ │ │ ├── index.mdx │ │ │ │ └── meta.json │ │ │ ├── integrations/ │ │ │ │ ├── index.mdx │ │ │ │ └── meta.json │ │ │ ├── internal/ │ │ │ │ ├── index.mdx │ │ │ │ └── meta.json │ │ │ ├── intro/ │ │ │ │ ├── adding-constraints.mdx │ │ │ │ ├── meta.json │ │ │ │ ├── morphs-and-more.mdx │ │ │ │ ├── setup.mdx │ │ │ │ └── your-first-type.mdx │ │ │ ├── introspection/ │ │ │ │ ├── index.mdx │ │ │ │ └── meta.json │ │ │ ├── keywords.mdx │ │ │ ├── match.mdx │ │ │ ├── meta.json │ │ │ ├── objects/ │ │ │ │ ├── arrays/ │ │ │ │ │ └── meta.json │ │ │ │ ├── index.mdx │ │ │ │ ├── meta.json │ │ │ │ └── properties/ │ │ │ │ └── meta.json │ │ │ ├── primitives/ │ │ │ │ ├── index.mdx │ │ │ │ ├── meta.json │ │ │ │ ├── number/ │ │ │ │ │ └── meta.json │ │ │ │ └── string/ │ │ │ │ └── meta.json │ │ │ ├── scopes/ │ │ │ │ ├── index.mdx │ │ │ │ └── meta.json │ │ │ ├── traversal-api.mdx │ │ │ └── type-api/ │ │ │ ├── index.mdx │ │ │ └── meta.json │ │ ├── lib/ │ │ │ ├── ambient.d.ts │ │ │ ├── metadata.ts │ │ │ ├── shiki.ts │ │ │ ├── source.tsx │ │ │ ├── writeLlmsTxt.ts │ │ │ └── writeSnippetsEntrypoint.ts │ │ ├── next-env.d.ts │ │ ├── next.config.ts │ │ ├── package.json │ │ ├── postcss.config.cjs │ │ ├── public/ │ │ │ ├── CNAME │ │ │ ├── llms.txt │ │ │ └── onigasm.wasm │ │ ├── source.config.ts │ │ └── tsconfig.json │ ├── extension/ │ │ ├── .vscode/ │ │ │ └── launch.json │ │ ├── README.md │ │ ├── arktype.scratch.ts │ │ ├── injected.tmLanguage.json │ │ ├── package.json │ │ └── tsWithArkType.tmLanguage.json │ ├── fast-check/ │ │ ├── __tests__/ │ │ │ └── arktypeFastCheck.test.ts │ │ ├── arbitraries/ │ │ │ ├── array.ts │ │ │ ├── date.ts │ │ │ ├── domain.ts │ │ │ ├── number.ts │ │ │ ├── object.ts │ │ │ ├── proto.ts │ │ │ └── string.ts │ │ ├── arktypeFastCheck.ts │ │ ├── fastCheckContext.ts │ │ └── package.json │ ├── fs/ │ │ ├── caller.ts │ │ ├── fs.ts │ │ ├── getCurrentLine.ts │ │ ├── index.ts │ │ ├── package.json │ │ └── shell.ts │ ├── json-schema/ │ │ ├── CHANGELOG.md │ │ ├── README.md │ │ ├── __tests__/ │ │ │ ├── array.test.ts │ │ │ ├── composition.test.ts │ │ │ ├── number.test.ts │ │ │ ├── object.test.ts │ │ │ └── string.test.ts │ │ ├── array.ts │ │ ├── common.ts │ │ ├── composition.ts │ │ ├── errors.ts │ │ ├── index.ts │ │ ├── json.ts │ │ ├── number.ts │ │ ├── object.ts │ │ ├── package.json │ │ ├── scope.ts │ │ └── string.ts │ ├── regex/ │ │ ├── CHANGELOG.md │ │ ├── README.md │ │ ├── __tests__/ │ │ │ ├── regex.bench.ts │ │ │ └── regex.test.ts │ │ ├── charset.ts │ │ ├── escape.ts │ │ ├── execArray.ts │ │ ├── group.ts │ │ ├── index.ts │ │ ├── package.json │ │ ├── parse.ts │ │ ├── quantify.ts │ │ ├── regex.ts │ │ └── state.ts │ ├── repo/ │ │ ├── .prettierignore │ │ ├── __tests__/ │ │ │ └── standardSchema.test.ts │ │ ├── build.ts │ │ ├── config.ts │ │ ├── dtsGen.ts │ │ ├── jsdocGen.ts │ │ ├── mocha.globalSetup.ts │ │ ├── mocha.package.jsonc │ │ ├── nodeOptions.js │ │ ├── package.json │ │ ├── patchC8.cjs │ │ ├── publish.ts │ │ ├── scratch/ │ │ │ ├── fn.ts │ │ │ ├── matchComparison.bench.ts │ │ │ ├── realWorldComparison.ts │ │ │ ├── typeClass.ts │ │ │ └── unionComparison.ts │ │ ├── scratch.ts │ │ ├── shared.ts │ │ ├── testPackage.ts │ │ ├── testV8.js │ │ ├── ts.js │ │ ├── tsconfig.cjs.json │ │ ├── tsconfig.dts.json │ │ └── tsconfig.esm.json │ ├── schema/ │ │ ├── README.md │ │ ├── __tests__/ │ │ │ ├── bounds.test.ts │ │ │ ├── errors.test.ts │ │ │ ├── intersection.test.ts │ │ │ ├── jsonSchema.test.ts │ │ │ ├── morphs.test.ts │ │ │ ├── onFail.test.ts │ │ │ ├── parse.test.ts │ │ │ ├── props.test.ts │ │ │ ├── proto.test.ts │ │ │ ├── scope.test.ts │ │ │ ├── select.test.ts │ │ │ ├── union.test.ts │ │ │ └── unit.test.ts │ │ ├── config.ts │ │ ├── constraint.ts │ │ ├── generic.ts │ │ ├── index.ts │ │ ├── intrinsic.ts │ │ ├── kinds.ts │ │ ├── module.ts │ │ ├── node.ts │ │ ├── package.json │ │ ├── parse.ts │ │ ├── predicate.ts │ │ ├── refinements/ │ │ │ ├── after.ts │ │ │ ├── before.ts │ │ │ ├── divisor.ts │ │ │ ├── exactLength.ts │ │ │ ├── kinds.ts │ │ │ ├── max.ts │ │ │ ├── maxLength.ts │ │ │ ├── min.ts │ │ │ ├── minLength.ts │ │ │ ├── pattern.ts │ │ │ └── range.ts │ │ ├── roots/ │ │ │ ├── alias.ts │ │ │ ├── basis.ts │ │ │ ├── domain.ts │ │ │ ├── intersection.ts │ │ │ ├── morph.ts │ │ │ ├── proto.ts │ │ │ ├── root.ts │ │ │ ├── union.ts │ │ │ ├── unit.ts │ │ │ └── utils.ts │ │ ├── scope.ts │ │ ├── shared/ │ │ │ ├── compile.ts │ │ │ ├── declare.ts │ │ │ ├── disjoint.ts │ │ │ ├── errors.ts │ │ │ ├── implement.ts │ │ │ ├── intersections.ts │ │ │ ├── jsonSchema.ts │ │ │ ├── registry.ts │ │ │ ├── standardSchema.ts │ │ │ ├── toJsonSchema.ts │ │ │ ├── traversal.ts │ │ │ └── utils.ts │ │ └── structure/ │ │ ├── index.ts │ │ ├── optional.ts │ │ ├── prop.ts │ │ ├── required.ts │ │ ├── sequence.ts │ │ ├── shared.ts │ │ └── structure.ts │ ├── themes/ │ │ ├── .vscode/ │ │ │ └── launch.json │ │ ├── README.md │ │ ├── arkdark.json │ │ ├── arkdarkItalic.json │ │ ├── arklight.json │ │ ├── arklightItalic.json │ │ └── package.json │ ├── type/ │ │ ├── CHANGELOG.md │ │ ├── README.md │ │ ├── __tests__/ │ │ │ ├── arrays/ │ │ │ │ ├── array.test.ts │ │ │ │ ├── base.test.ts │ │ │ │ ├── defaults.test.ts │ │ │ │ ├── intersection.test.ts │ │ │ │ ├── nonVariadicTuple.test.ts │ │ │ │ └── variadicTuple.test.ts │ │ │ ├── badDefinitionType.test.ts │ │ │ ├── basis.test.ts │ │ │ ├── brand.test.ts │ │ │ ├── cast.test.ts │ │ │ ├── clone.test.ts │ │ │ ├── completions.test.ts │ │ │ ├── config.test.ts │ │ │ ├── cyclic.bench.ts │ │ │ ├── dateLiteral.test.ts │ │ │ ├── declared.test.ts │ │ │ ├── define.test.ts │ │ │ ├── discrimination.test.ts │ │ │ ├── divisor.test.ts │ │ │ ├── enclosed.test.ts │ │ │ ├── expressions.test.ts │ │ │ ├── filter.test.ts │ │ │ ├── fn.test.ts │ │ │ ├── generateBenchData.ts │ │ │ ├── generated/ │ │ │ │ └── cyclic.ts │ │ │ ├── generic.test.ts │ │ │ ├── get.test.ts │ │ │ ├── group.test.ts │ │ │ ├── imports.test.ts │ │ │ ├── instanceOf.test.ts │ │ │ ├── integration/ │ │ │ │ ├── allConfig.ts │ │ │ │ ├── eoptConfig.ts │ │ │ │ ├── generateAllConfig.ts │ │ │ │ ├── onFailConfig.ts │ │ │ │ ├── simpleConfig.ts │ │ │ │ ├── testAllConfig.ts │ │ │ │ ├── testEoptConfig.ts │ │ │ │ ├── testOnFailConfig.ts │ │ │ │ ├── testSimpleConfig.ts │ │ │ │ └── util.ts │ │ │ ├── intersection.test.ts │ │ │ ├── keyof.test.ts │ │ │ ├── keywords/ │ │ │ │ ├── date.test.ts │ │ │ │ ├── exclude.test.ts │ │ │ │ ├── extract.test.ts │ │ │ │ ├── formData.test.ts │ │ │ │ ├── format.test.ts │ │ │ │ ├── ip.test.ts │ │ │ │ ├── json.test.ts │ │ │ │ ├── merge.test.ts │ │ │ │ ├── number.test.ts │ │ │ │ ├── numericStrings.test.ts │ │ │ │ ├── object.test.ts │ │ │ │ ├── omit.test.ts │ │ │ │ ├── parse.test.ts │ │ │ │ ├── partial.test.ts │ │ │ │ ├── pick.test.ts │ │ │ │ ├── record.test.ts │ │ │ │ ├── required.test.ts │ │ │ │ ├── string.test.ts │ │ │ │ ├── tsPrimitives.test.ts │ │ │ │ ├── url.test.ts │ │ │ │ └── uuid.test.ts │ │ │ ├── literal.test.ts │ │ │ ├── match.bench.ts │ │ │ ├── match.test.ts │ │ │ ├── narrow.test.ts │ │ │ ├── nary.bench.ts │ │ │ ├── nary.test.ts │ │ │ ├── object.bench.ts │ │ │ ├── objects/ │ │ │ │ ├── defaults.test.ts │ │ │ │ ├── indexSignatures.test.ts │ │ │ │ ├── mapped.test.ts │ │ │ │ ├── namedKeys.test.ts │ │ │ │ ├── onUndeclaredKey.test.ts │ │ │ │ ├── props.test.ts │ │ │ │ └── spread.test.ts │ │ │ ├── operand.bench.ts │ │ │ ├── operator.bench.ts │ │ │ ├── optional.test.ts │ │ │ ├── pipe.test.ts │ │ │ ├── range.test.ts │ │ │ ├── realWorld.test.ts │ │ │ ├── regex.test.ts │ │ │ ├── runtime.bench.ts │ │ │ ├── scope.test.ts │ │ │ ├── select.test.ts │ │ │ ├── serialization.test.ts │ │ │ ├── standardSchema.test.ts │ │ │ ├── string.test.ts │ │ │ ├── submodule.test.ts │ │ │ ├── this.test.ts │ │ │ ├── thunk.test.ts │ │ │ ├── toJsonSchema.test.ts │ │ │ ├── traverse.test.ts │ │ │ ├── type.test.ts │ │ │ ├── typeReference.test.ts │ │ │ ├── unenclosed.test.ts │ │ │ └── union.test.ts │ │ ├── attributes.ts │ │ ├── config.ts │ │ ├── declare.ts │ │ ├── fn.ts │ │ ├── generic.ts │ │ ├── index.ts │ │ ├── keywords/ │ │ │ ├── Array.ts │ │ │ ├── FormData.ts │ │ │ ├── TypedArray.ts │ │ │ ├── builtins.ts │ │ │ ├── constructors.ts │ │ │ ├── keywords.ts │ │ │ ├── number.ts │ │ │ ├── string.ts │ │ │ └── ts.ts │ │ ├── match.ts │ │ ├── module.ts │ │ ├── nary.ts │ │ ├── package.json │ │ ├── parser/ │ │ │ ├── ast/ │ │ │ │ ├── bounds.ts │ │ │ │ ├── default.ts │ │ │ │ ├── divisor.ts │ │ │ │ ├── generic.ts │ │ │ │ ├── infer.ts │ │ │ │ ├── keyof.ts │ │ │ │ ├── utils.ts │ │ │ │ └── validate.ts │ │ │ ├── definition.ts │ │ │ ├── objectLiteral.ts │ │ │ ├── property.ts │ │ │ ├── reduce/ │ │ │ │ ├── dynamic.ts │ │ │ │ ├── shared.ts │ │ │ │ └── static.ts │ │ │ ├── shift/ │ │ │ │ ├── operand/ │ │ │ │ │ ├── date.ts │ │ │ │ │ ├── enclosed.ts │ │ │ │ │ ├── genericArgs.ts │ │ │ │ │ ├── operand.ts │ │ │ │ │ └── unenclosed.ts │ │ │ │ ├── operator/ │ │ │ │ │ ├── bounds.ts │ │ │ │ │ ├── brand.ts │ │ │ │ │ ├── default.ts │ │ │ │ │ ├── divisor.ts │ │ │ │ │ └── operator.ts │ │ │ │ └── tokens.ts │ │ │ ├── string.ts │ │ │ ├── tupleExpressions.ts │ │ │ └── tupleLiteral.ts │ │ ├── scope.ts │ │ ├── type.ts │ │ └── variants/ │ │ ├── array.ts │ │ ├── base.ts │ │ ├── date.ts │ │ ├── instantiate.ts │ │ ├── number.ts │ │ ├── object.ts │ │ └── string.ts │ └── util/ │ ├── __tests__/ │ │ ├── arrays.test.ts │ │ ├── callable.test.ts │ │ ├── clone.test.ts │ │ ├── collapsibleDate.test.ts │ │ ├── flatMorph.test.ts │ │ ├── hkt.test.ts │ │ ├── intersections.test.ts │ │ ├── labels.test.ts │ │ ├── numbers.test.ts │ │ ├── overloads.test.ts │ │ ├── path.test.ts │ │ ├── printable.test.ts │ │ ├── records.test.ts │ │ ├── registry.test.ts │ │ ├── string.test.ts │ │ ├── traits.scratch.ts │ │ └── traits.test.ts │ ├── arrays.ts │ ├── clone.ts │ ├── describe.ts │ ├── domain.ts │ ├── errors.ts │ ├── flatMorph.ts │ ├── functions.ts │ ├── generics.ts │ ├── get.ts │ ├── hkt.ts │ ├── index.ts │ ├── intersections.ts │ ├── isomorphic.ts │ ├── keys.ts │ ├── lazily.ts │ ├── numbers.ts │ ├── objectKinds.ts │ ├── package.json │ ├── path.ts │ ├── primitive.ts │ ├── records.ts │ ├── registry.ts │ ├── scanner.ts │ ├── serialize.ts │ ├── strings.ts │ ├── traits.ts │ ├── tsconfig.base.json │ └── unionToTuple.ts ├── eslint.config.js ├── package.json ├── pnpm-workspace.yaml └── tsconfig.json ================================================ FILE CONTENTS ================================================ ================================================ FILE: .cursor/commands/armstrong.md ================================================ senator armstrong is reviewing the work on the current branch and he's angry. he will destroy both of us if he finds a single flaw. think big picture and iterate ruthlessly, fundamentally improving the design and implementation. once you are absolutely certain we won't be brutalized, report why senator armstrong will approve of the current changes using as many memes as possible. our fate is in your hands. ================================================ FILE: .github/CODE_OF_CONDUCT.md ================================================ # Contributor Covenant Code of Conduct ## Our Pledge We as members, contributors, and leaders pledge to make participation in our community a harassment-free experience for everyone, regardless of age, body size, visible or invisible disability, ethnicity, sex characteristics, gender identity and expression, level of experience, education, socio-economic status, nationality, personal appearance, race, caste, color, religion, or sexual identity and orientation. We pledge to act and interact in ways that contribute to an open, welcoming, diverse, inclusive, and healthy community. ## Our Standards Examples of behavior that contributes to a positive environment for our community include: - Demonstrating empathy and kindness toward other people - Being respectful of differing opinions, viewpoints, and experiences - Giving and gracefully accepting constructive feedback - Accepting responsibility and apologizing to those affected by our mistakes, and learning from the experience - Focusing on what is best not just for us as individuals, but for the overall community Examples of unacceptable behavior include: - The use of sexualized language or imagery, and sexual attention or advances of any kind - Trolling, insulting or derogatory comments, and personal or political attacks - Public or private harassment - Publishing others' private information, such as a physical or email address, without their explicit permission - Other conduct which could reasonably be considered inappropriate in a professional setting ## Enforcement Responsibilities Community leaders are responsible for clarifying and enforcing our standards of acceptable behavior and will take appropriate and fair corrective action in response to any behavior that they deem inappropriate, threatening, offensive, or harmful. Community leaders have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, and will communicate reasons for moderation decisions when appropriate. ## Scope This Code of Conduct applies within all community spaces, and also applies when an individual is officially representing the community in public spaces. Examples of representing our community include using an official e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. ## Enforcement Instances of abusive, harassing, or otherwise unacceptable behavior may be reported to the community leaders responsible for enforcement at david@arktype.io. All complaints will be reviewed and investigated promptly and fairly. All community leaders are obligated to respect the privacy and security of the reporter of any incident. ## Enforcement Guidelines Community leaders will follow these Community Impact Guidelines in determining the consequences for any action they deem in violation of this Code of Conduct: ### 1. Correction **Community Impact**: Use of inappropriate language or other behavior deemed unprofessional or unwelcome in the community. **Consequence**: A private, written warning from community leaders, providing clarity around the nature of the violation and an explanation of why the behavior was inappropriate. A public apology may be requested. ### 2. Warning **Community Impact**: A violation through a single incident or series of actions. **Consequence**: A warning with consequences for continued behavior. No interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, for a specified period of time. This includes avoiding interactions in community spaces as well as external channels like social media. Violating these terms may lead to a temporary or permanent ban. ### 3. Temporary Ban **Community Impact**: A serious violation of community standards, including sustained inappropriate behavior. **Consequence**: A temporary ban from any sort of interaction or public communication with the community for a specified period of time. No public or private interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, is allowed during this period. Violating these terms may lead to a permanent ban. ### 4. Permanent Ban **Community Impact**: Demonstrating a pattern of violation of community standards, including sustained inappropriate behavior, harassment of an individual, or aggression toward or disparagement of classes of individuals. **Consequence**: A permanent ban from any sort of public interaction within the community. ## Attribution This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 2.1, available at [https://www.contributor-covenant.org/version/2/1/code_of_conduct.html][v2.1]. Community Impact Guidelines were inspired by [Mozilla's code of conduct enforcement ladder][mozilla coc]. For answers to common questions about this code of conduct, see the FAQ at [https://www.contributor-covenant.org/faq][faq]. Translations are available at [https://www.contributor-covenant.org/translations][translations]. [homepage]: https://www.contributor-covenant.org [v2.1]: https://www.contributor-covenant.org/version/2/1/code_of_conduct.html [mozilla coc]: https://github.com/mozilla/diversity [faq]: https://www.contributor-covenant.org/faq [translations]: https://www.contributor-covenant.org/translations ================================================ FILE: .github/CONTRIBUTING.md ================================================ # Contributing ArkType values the time of its users and contributors as much as its maintainers, so our goal is for the process to be as efficient and straightforward as possible. Whether this is your first pull request or you're a seasoned open source contributor, this guide is the perfect place to start. If you have any other questions, please don't hesitate to [create an issue on GitHub](https://github.com/arktypeio/arktype/issues/new) or reach out [on our Discord](https://arktype.io/discord). ## Sending a Pull Request ArkType is a community project, so Pull Requests are always welcome, but, before working on a large change, it is best to open an issue first to discuss it with the maintainers. When in doubt, keep your Pull Requests small. To give a Pull Request the best chance of getting accepted, don't bundle more than one feature or bug fix per Pull Request. It's often best to create two smaller Pull Requests than one big one. 1. [Fork the repository.](https://help.github.com/en/github/getting-started-with-github/fork-a-repo) 2. Clone the fork to your local machine and add upstream remote: ```sh git clone git@github.com:/arktype.git && cd arktype && git remote add upstream git@github.com:arktypeio/arktype.git ``` 3. Synchronize your local `main` branch with the upstream one: ```sh git checkout main git pull upstream main ``` 4. Install dependencies and build: If you don't have [pnpm](https://pnpm.io/) installed: ```sh npm i -g pnpm ``` then: ```sh pnpm i # install package.json dependencies pnpm build # builds the package ``` Make sure you are using our repo's pinned version of TypeScript and not one that comes bundled with your editor. In VSCode, you should be automatically prompted to allow this when you open the repo, but otherwise take a look at this explanation for how it can be done [from the VSCode docs](https://code.visualstudio.com/docs/typescript/typescript-compiling#_using-the-workspace-version-of-typescript). 5. Create a new topic branch: ```sh git checkout -b amazing-feature ``` 6. Do your best to write code that is stylistically consistent with its context. The linter will help with this, but it won't catch everything. Here's a few general guidelines: - Favor mutation over copying objects in perf-sensitive contexts - Favor clarity in naming with the following exceptions: - Ubiquitous variables/types. For example, use `s` over `dynamicParserState` for a variable of type DynamicParserState that is used in the same way across many functions. - Ephemeral variables whose contents can be trivially inferred from context. For example, prefer `rawKeyDefinitions.map(k => k.trim())` to `rawKeyDefinitions.map(rawKeyDefinition => rawKeyDefinition.trim())`. We also have some unique casing rules for our TypeScript types to facilitate type-level code that can parallel its runtime implementation and be easily understood: - Use `PascalCase` for... - Entities/non-generic types (e.g. `User`, `SomeData`) - Generic types with noun names, like `Array`. As a rule of thumb, if your generic is named this way, all parameters have defaults. - Use `camelCase` for... - Generic types with verb names like `inferDomain`. Types named this way have at least one required parameter. - Parameter names, e.g. `t` in `Array` 7. Once you've made the changes you want to and added corresponding unit tests, run the `prChecks` command in the project root and address any errors: ```sh pnpm prChecks ``` All of these commands will run as part of our CI process and must succeed in order for us to accept your Pull Request. 8. Once everything is passing, commit your changes and ensure your fork is up to date: ```sh git push -u ``` 9. Go to [the repository](https://github.com/arktypeio/arktype) and make a Pull Request. The core team is monitoring for Pull Requests. We will review your Pull Request and either merge it, request changes to it, or close it with an explanation. ## Project Our current and planned work can always be found [here](https://github.com/orgs/arktypeio/projects/4). If you want to contribute but aren't sure where to get started, see if any of the issues in our backlog sound interesting! Most are not well-documented, so it usually makes sense to comment on the issue with any questions you may have before you start coding. ## Code of Conduct ArkType has adopted the [Contributor Covenant](https://www.contributor-covenant.org/) as its Code of Conduct, and we expect project participants to adhere to it. Please read [the full text](./CODE_OF_CONDUCT.md) so that you can understand what actions will and will not be tolerated. ## License By contributing your code to the arktypeio/arktype GitHub repository, you agree to license your contribution under the MIT license. ================================================ FILE: .github/FUNDING.yml ================================================ github: [arktypeio] drips: ethereum: ownedBy: "0xD5c5Fe5DF95adf8DA1Ae640fCAE8f72795657fa5" ================================================ FILE: .github/ISSUE_TEMPLATE/bug_report.md ================================================ --- name: Bug about: Report a bug title: "" labels: "bug" assignees: "ssalbdivad" --- # Report a bug ### 🔎 Search Terms ### 🧩 Context - ArkType version: - TypeScript version (5.1+): - Other context you think may be relevant (JS flavor, OS, etc.): ### 🧑‍💻 Repro Playground Link: https://arktype.io/playground ```ts // Paste reproduction code here ``` ================================================ FILE: .github/ISSUE_TEMPLATE/feature_request.md ================================================ --- name: Feature Request about: Suggest a new feature title: "" labels: "" assignees: "" --- # Request a feature ### 🤷 Motivation ### 💡 Solution ================================================ FILE: .github/SECURITY.md ================================================ # Security Policy ## Supported Versions | Version | Supported | | ------- | ------------------ | | 2.x | :white_check_mark: | ## Reporting a Vulnerability If you believe you've identified a security vulnerability within ArkType, please send an email to david@arktype.io describing what it is and how to reproduce it. Expect a response within 24 hours. ================================================ FILE: .github/actions/setup/action.yml ================================================ name: Setup repo description: Install dependencies and perform setup for https://github.com/arktypeio/arktype inputs: node: default: lts/* description: Node version to use runs: using: composite steps: - name: Setup Node (${{ inputs.node }}) uses: actions/setup-node@v4 with: node-version: ${{ inputs.node }} registry-url: "https://registry.npmjs.org" - name: Setup pnpm uses: pnpm/action-setup@v4 - name: Install dependencies shell: bash run: pnpm install - name: Build shell: bash run: pnpm build - name: Post-build install shell: bash run: pnpm install ================================================ FILE: .github/pull_request_template.md ================================================ ================================================ FILE: .github/workflows/pr.yml ================================================ name: pr on: pull_request: branches: [main] defaults: run: shell: bash jobs: core: runs-on: ubuntu-latest timeout-minutes: 20 steps: - name: Checkout repo uses: actions/checkout@v4 with: fetch-depth: 0 - name: Setup repo uses: ./.github/actions/setup - name: prChecks run: pnpm prChecks compatibility: needs: core timeout-minutes: 20 strategy: matrix: node: [lts/*] os: [windows-latest, macos-latest] include: - os: ubuntu-latest node: lts/-1 - os: ubuntu-latest node: latest fail-fast: false runs-on: ${{ matrix.os }} steps: - name: Checkout repo uses: actions/checkout@v4 with: fetch-depth: 0 - name: Setup repo uses: ./.github/actions/setup with: node: ${{ matrix.node }} # To test node and OS versions, we don't care about rechecking types # so just check runtime behavior - name: Test run: pnpm testRepo prChecks: needs: compatibility timeout-minutes: 1 runs-on: ubuntu-latest steps: - run: echo All checks succeeded! ⛵ ================================================ FILE: .github/workflows/publish.yml ================================================ name: publish on: push: branches: [main] # Allows you to run this workflow manually from the Actions tab on GitHub. workflow_dispatch: # Allow this job to clone the repo and create a page deployment permissions: write-all jobs: update-gh-pages: runs-on: ubuntu-latest timeout-minutes: 20 steps: - name: Checkout repo uses: actions/checkout@v4 with: fetch-depth: 0 - name: Setup repo uses: ./.github/actions/setup - name: Build docs run: pnpm buildDocs env: NEXT_PUBLIC_POSTHOG_KEY: phc_vKr7tmA1TyRlKm9zm5TWVmrTWAybJ2fCk66FtZG49Om NEXT_PUBLIC_POSTHOG_HOST: https://us.i.posthog.com ORAMA_PRIVATE_API_KEY: ${{ secrets.ORAMA_PRIVATE_API_KEY }} - name: Upload artifact uses: actions/upload-pages-artifact@v3 with: path: ./ark/docs/out deploy: needs: update-gh-pages runs-on: ubuntu-latest environment: name: github-pages url: ${{ steps.deployment.outputs.page_url }} steps: - name: Deploy to GitHub Pages id: deployment uses: actions/deploy-pages@v4 release: runs-on: ubuntu-latest timeout-minutes: 20 steps: - name: Checkout repo uses: actions/checkout@v4 with: fetch-depth: 0 - name: Setup repo uses: ./.github/actions/setup - name: Create and publish versions run: pnpm ci:publish env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} NPM_TOKEN: ${{ secrets.NPM_TOKEN }} NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} ================================================ FILE: .github/workflows/pullfrog.yml ================================================ # PULLFROG ACTION — DO NOT EDIT EXCEPT WHERE INDICATED name: Pullfrog run-name: ${{ inputs.name || github.workflow }} on: workflow_dispatch: inputs: prompt: type: string description: Agent prompt name: type: string description: Run name permissions: id-token: write contents: write pull-requests: write issues: write actions: read checks: read jobs: pullfrog: runs-on: ubuntu-latest steps: - name: Checkout code uses: actions/checkout@v6 with: fetch-depth: 1 - name: Run agent uses: pullfrog/pullfrog@v0 with: prompt: ${{ inputs.prompt }} env: # Feel free to comment out any you won't use ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }} OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }} GOOGLE_API_KEY: ${{ secrets.GOOGLE_API_KEY }} GEMINI_API_KEY: ${{ secrets.GEMINI_API_KEY }} CURSOR_API_KEY: ${{ secrets.CURSOR_API_KEY }} ================================================ FILE: .gitignore ================================================ dist out *.vsix node_modules temp tmp *.temp.* *.log *.tsbuildinfo .DS_Store .next .source .cache-loader .attest tsconfig.build.json coverage ================================================ FILE: .vscode/extensions.json ================================================ { "recommendations": [ "esbenp.prettier-vscode", "dbaeumer.vscode-eslint", // Run/debug tests inline via VSCode's Test Explorer "hbenl.vscode-mocha-test-adapter", // Syntax highlighting for strings in ArkType definitions "arktypeio.arkdark", // Playground-like version dropdown for TypeScript versions "typeholes.ts-versions-switcher" ] } ================================================ FILE: .vscode/launch.json ================================================ // A launch configuration that launches the extension inside a new window { "version": "0.1.0", "configurations": [ { "name": "Debug Extension", "type": "extensionHost", "request": "launch", "runtimeExecutable": "${execPath}", "args": ["--extensionDevelopmentPath=${workspaceRoot}/ark/extension"] }, { "name": "Debug Themes", "type": "extensionHost", "request": "launch", "runtimeExecutable": "${execPath}", "args": ["--extensionDevelopmentPath=${workspaceRoot}/ark/themes"] } ] } ================================================ FILE: .vscode/settings.json ================================================ { "editor.defaultFormatter": "esbenp.prettier-vscode", "prettier.prettierPath": "./node_modules/prettier", "biome.enabled": false, "editor.codeActionsOnSave": [ "editor.formatOnSave", "source.fixAll.eslint", "source.sortImports" ], "eslint.codeActionsOnSave.rules": [ "object-shorthand", "@typescript-eslint/consistent-type-imports", "import/no-duplicates", "@typescript-eslint/no-import-type-side-effects", "curly" ], "typescript.preferences.preferTypeOnlyAutoImports": true, "typescript.preferences.autoImportFileExcludePatterns": [ "out", // too many overlapping names, easy to import in schema/arktype where we don't want it // should just import as * as ts when we need it in attest "typescript" ], "typescript.preferences.autoImportSpecifierExcludeRegexes": [ // has a "type" export "^(node:)?os$" ], "typescript.tsserver.experimental.enableProjectDiagnostics": true, "typescript.tsdk": "./node_modules/typescript/lib", // IF YOU UPDATE THE MOCHA CONFIG HERE, PLEASE ALSO UPDATE package.json/mocha AND ark/repo/mocha.jsonc "mochaExplorer.nodeArgv": ["--conditions", "ark-ts", "--import", "tsx"], // ignore attest since it requires type information "mochaExplorer.ignore": ["ark/attest/**/*"], "mochaExplorer.require": "ark/repo/mocha.globalSetup.ts", "mochaExplorer.timeout": 0, "mochaExplorer.env": { "ATTEST_skipTypes": "true" }, "testExplorer.useNativeTesting": true, "search.exclude": { "**/out": true, "**/.next": true, "**/.source": true, "**/components/dts": true, "**/apiData.ts": true }, "editor.quickSuggestions": { "strings": "on" }, "github.copilot.enable": { "*": false, "plaintext": false, "markdown": false, "scminput": false } } ================================================ FILE: LICENSE ================================================ Copyright 2025 ArkType 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. ================================================ FILE: ark/attest/CHANGELOG.md ================================================ # @ark/attest NOTE: This changelog is incomplete, but will include notable attest-specific changes (many updates consist almost entirely of bumped `arktype` versions for assertions). ## 0.51.0 Fix some tsconfig path resolution (thanks @LukeAbby🎉) https://github.com/arktypeio/arktype/pull/1522 ## 0.47.0 Add a `failOnMissingSnapshots` config option. Defaults to `true` if `CI` is set in your environment, `false` otherwise. ## 0.46.0 Fix an issue causing some bench files to not be parsed correctly, leading to errors and 0 instantiation counts. ## 0.44.3 Decouple attest trace/stats from pnpm ## 0.44.0 Support assertions for JSDoc contents associated with an `attest`ed value ```ts const T = type({ /** FOO */ foo: "string" }) const out = T.assert({ foo: "foo" }) // match or snapshot expected jsdoc associated with the value passed to attest attest(out.foo).jsdoc.snap("FOO") ``` ## 0.41.0 ### Bail early for obviously incorrect `equals` comparisons This is the short-term solution to #1287, where some comparisons with Node's `deepStrictEqual` and object with recursive properties like Type resulted in OOM crashes. We will eventually add new string-diffing logic, but for now we just make some shallow comparisons between constructors and types to avoid common problematic comparisons, e.g. between Type instances: ```ts // previously resulted in OOM exception, now shallowly fails with simple error attest(type.string).equals(type.boolean) ``` ## 0.11.0 - Fix a bug causing certain serialized types with backticks and template literals to be incorrectly formatted on inline snapshot - Add a `typeToStringFormat` option for configuring how prettier stringifies types. Also added more documentation for other pre-existing options. - Allow regex/partial match for `toString` assertions: ```ts // ok attest({ ark: "type" }).type.toString(/^{.*}$/) // AssertionError: Actual string 'string[]' did not match regex '^{.*}$' attest(["ark", "type"]).type.toString(/^{.*}$/) ``` - Allow assertions on arbitrary `arktype` Type instance using `satisfies`: ```ts // ok attest({ ark: "type" }).type.toString.satisfies(/^{.*}$/) // AssertionError: ark must be a number (was string) attest({ ark: "type" }).satisfies({ ark: "number" }) ``` ## 0.10.0 Format serialized types using `prettier`. This makes long serialized types much more readable: ```ts // old attest({ ark: "type", type: "script", vali: "dator", opti: "mized", from: "editor", to: "runtime" }).type.toString.snap( `{ ark: string; type: string; vali: string; opti: string; from: string; to: string; }` ) // new attest({ ark: "type", type: "script", vali: "dator", opti: "mized", from: "editor", to: "runtime" }).type.toString.snap(`{ ark: string type: string vali: string opti: string from: string to: string }`) ``` Be aware, this is likely means you will need to regenerate existing type snaps to avoid failing due to formatting inconsistencies. You should be able to update all your snapshots by running your tests with the `--updateSnapshots` flag or setting `ATTEST_updateSnapshots=1` in your environment. If you have any non-snap `type.toString` assertions, you will need to update them manually. You may want to convert them temporarily to snaps so you can easily see the correct value. ## 0.9.4 Improve benchmark source extraction, add notes on baseline expressions ## 0.9.2 Fix a bug preventing consecutive benchmark runs from populating snapshots inline ## 0.8.2 ### Patch Changes - [#1028](https://github.com/arktypeio/arktype/pull/1028) [`5fe79c6`](https://github.com/arktypeio/arktype/commit/5fe79c6c8db94f20c997c7a8960edb9d69468b69) Thanks [@ssalbdivad](https://github.com/ssalbdivad)! - Bump version - Updated dependencies [[`5fe79c6`](https://github.com/arktypeio/arktype/commit/5fe79c6c8db94f20c997c7a8960edb9d69468b69)]: - @ark/util@0.0.51 - arktype@2.0.0-dev.26 ## 0.8.1 ### Patch Changes - [#1024](https://github.com/arktypeio/arktype/pull/1024) [`5284b60`](https://github.com/arktypeio/arktype/commit/5284b6054209ffa38f02ae010c3e9ab3dff93653) Thanks [@ssalbdivad](https://github.com/ssalbdivad)! - ### Add .satisfies as an attest assertion to compare the value to an ArkType definition. ```ts attest({ foo: "bar" }).satisfies({ foo: "string" }) // Error: foo must be a number (was string) attest({ foo: "bar" }).satisfies({ foo: "number" }) ``` - Updated dependencies [[`1bf2066`](https://github.com/arktypeio/arktype/commit/1bf2066800ce65edc918a24c251ce20f1ccf29f4)]: - @ark/util@0.0.50 - arktype@2.0.0-dev.25 ## 0.8.0 ### Minor Changes - [#1011](https://github.com/arktypeio/arktype/pull/1011) [`2be4f5b`](https://github.com/arktypeio/arktype/commit/2be4f5b391d57ad47dc6f4c0e4c9d31ae6b550c5) Thanks [@ssalbdivad](https://github.com/ssalbdivad)! - ### Throw by default when attest.instantiations() exceeds the specified benchPercentThreshold Tests like this will now correctly throw inline instead of return a non-zero exit code: ```ts it("can snap instantiations", () => { type Z = makeComplexType<"asbsdfsaodisfhsda"> // will throw here as the actual number of instantiations is more // than 20% higher than the snapshotted value attest.instantiations([1, "instantiations"]) }) ``` ### Snapshotted completions will now be alphabetized This will help improve stability, especially for large completion lists like this one which we updated more times than we'd care to admit 😅 ```ts attest(() => type([""])).completions({ "": [ "...", "===", "Array", "Date", "Error", "Function", "Map", "Promise", "Record", "RegExp", "Set", "WeakMap", "WeakSet", "alpha", "alphanumeric", "any", "bigint", "boolean", "creditCard", "digits", "email", "false", "format", "instanceof", "integer", "ip", "keyof", "lowercase", "never", "null", "number", "object", "parse", "semver", "string", "symbol", "this", "true", "undefined", "unknown", "uppercase", "url", "uuid", "void" ] }) ``` ### Patch Changes - Updated dependencies [[`2be4f5b`](https://github.com/arktypeio/arktype/commit/2be4f5b391d57ad47dc6f4c0e4c9d31ae6b550c5)]: - @ark/util@0.0.49 - arktype@2.0.0-dev.24 ## 0.7.10 ### Patch Changes - Updated dependencies [[`232fc42`](https://github.com/arktypeio/arktype/commit/232fc42af18e8412d0095293926077a9c50abdc6)]: - @ark/util@0.0.48 - arktype@2.0.0-dev.20 ## 0.7.9 ### Patch Changes - Updated dependencies [[`317f012`](https://github.com/arktypeio/arktype/commit/317f0122b1f2c0ba6e1de872f210490af75761af)]: - @ark/util@0.0.47 - arktype@2.0.0-dev.19 ## 0.7.8 ### Patch Changes - Updated dependencies [[`ebe3408`](https://github.com/arktypeio/arktype/commit/ebe3408e2310bc8f69eacd29e0d51c99c24d9471)]: - @ark/util@0.0.46 - arktype@2.0.0-dev.17 ## 0.7.7 ### Patch Changes - Updated dependencies [[`79c2b27`](https://github.com/arktypeio/arktype/commit/79c2b276c3645ea51e7bae8fe4463f2f39ddabc8)]: - @ark/util@0.0.45 - arktype@2.0.0-dev.15 ## 0.7.6 ### Patch Changes - [`8cd0807`](https://github.com/arktypeio/arktype/commit/8cd080783fdbd8eefea54d5c04d99cd88b36c0eb) - Initial changeset - Updated dependencies [[`8cd0807`](https://github.com/arktypeio/arktype/commit/8cd080783fdbd8eefea54d5c04d99cd88b36c0eb)]: - @ark/fs@0.0.20 - @ark/util@0.0.44 - arktype@2.0.0-dev.14 ================================================ FILE: ark/attest/README.md ================================================ # Attest Attest is a testing library that makes your TypeScript types available at runtime, giving you access to precise type-level assertions and performance benchmarks. Assertions are framework agnostic and can be seamlessly integrated with your existing Vitest, Jest, or Mocha tests. Benchmarks can run from anywhere and will deterministically report the number of type instantiations contributed by the contents of the `bench` call. If you've ever wondered how [ArkType](https://github.com/arktypeio/arktype) can guarantee identical behavior between its runtime and static parser implementations and highly optimized editor performance, Attest is your answer⚡ ## Installation ```bash npm install @ark/attest ``` _Note: This package is still in alpha! Your feedback will help us iterate toward a stable 1.0._ ## Setup To use attest's type assertions, you'll need to call our setup/cleanup methods before your first test and after your last test, respectively. This usually involves some kind of globalSetup/globalTeardown config. > [!IMPORTANT] > If you run your tests in watch mode or otherwise iteratively during dev, you will want to enable [`--skipTypes` mode](#skiptypes). ### Vitest `vitest.config.ts` ```ts import { defineConfig } from "vitest/config" export default defineConfig({ test: { globalSetup: ["setupVitest.ts"] } }) ``` `setupVitest.ts` ```ts import { setup } from "@ark/attest" // config options can be passed here export default () => setup({}) ``` ### Mocha `package.json` ```json "mocha": { "require": "./setupMocha.ts" } ``` `setupMocha.ts` ```ts import { setup, teardown } from "@ark/attest" // config options can be passed here export const mochaGlobalSetup = () => setup({}) export const mochaGlobalTeardown = teardown ``` You should also add `.attest` to your repository's `.gitignore` file. Bun support is currently pending [them supporting @prettier/sync for type formatting](https://github.com/oven-sh/bun/issues/10768). If this is a problem for you, please 👍 that issue so they prioritize it! ## Assertions Here are some simple examples of type assertions and snapshotting: ```ts // @ark/attest assertions can be made from any unit test framework with a global setup/teardown describe("attest features", () => { it("type and value assertions", () => { const Even = type("number%2") // asserts even.infer is exactly number attest(even.infer) // make assertions about types and values seamlessly attest(even.infer).type.toString.snap("number") // including object literals- no more long inline strings! attest(even.json).snap({ intersection: [{ domain: "number" }, { divisor: 2 }] }) }) it("error assertions", () => { // Check type errors, runtime errors, or both at the same time! // @ts-expect-error attest(() => type("number%0")).throwsAndHasTypeError( "% operator must be followed by a non-zero integer literal (was 0)" ) // @ts-expect-error attest(() => type({ "[object]": "string" })).type.errors( "Indexed key definition 'object' must be a string, number or symbol" ) }) it("completion snapshotting", () => { // snapshot expected completions for any string literal! // @ts-expect-error (if your expression would throw, prepend () =>) attest(() => type({ a: "a", b: "b" })).completions({ a: ["any", "alpha", "alphanumeric"], b: ["bigint", "boolean"] }) type Legends = { faker?: "🐐"; [others: string]: unknown } // works for keys or index access as well (may need prettier-ignore to avoid removing quotes) // prettier-ignore attest({ "f": "🐐" } as Legends).completions({ "f": ["faker"] }) }) it("jsdoc snapshotting", () => { // match or snapshot expected jsdoc associated with the value passed to attest const T = type({ /** FOO */ foo: "string" }) const out = T.assert({ foo: "foo" }) attest(out.foo).jsdoc.snap("FOO") }) it("integrate runtime logic with type assertions", () => { const ArrayOf = type("", "t[]") const numericArray = arrayOf("number | bigint") // flexibly combine runtime logic with type assertions to customize your // tests beyond what is possible from pure static-analysis based type testing tools if (getTsVersionUnderTest().startsWith("5")) { // this assertion will only occur when testing TypeScript 5+! attest<(number | bigint)[]>(numericArray.infer) } }) it("integrated type performance benchmarking", () => { const User = type({ kind: "'admin'", "powers?": "string[]" }) .or({ kind: "'superadmin'", "superpowers?": "string[]" }) .or({ kind: "'pleb'" }) attest.instantiations([7574, "instantiations"]) }) }) ``` ## Options Options can be specified in one of 3 ways: - An argument passed to your test process, e.g. `--skipTypes` or `--benchPercentThreshold 10` - An environment variable with an `ATTEST_` prefix, e.g. `ATTEST_skipTypes=1` or `ATTEST_benchPercentThreshold=10` - Passed as an option to attest's `setup` function, e.g.: `setupVitest.ts` ```ts import * as attest from "@ark/attest" export const setup = () => attest.setup({ skipTypes: true, benchPercentThreshold: 10 }) ``` Here are the current defaults for all available options. Please note, some of these are experimental and subject to change: ```ts export const getDefaultAttestConfig = (): BaseAttestConfig => ({ tsconfig: existsSync(fromCwd("tsconfig.json")) ? fromCwd("tsconfig.json") : undefined, attestAliases: ["attest", "attestInternal"], updateSnapshots: false, skipTypes: false, skipInlineInstantiations: false, tsVersions: "typescript", benchPercentThreshold: 20, benchErrorOnThresholdExceeded: true, filter: undefined, testDeclarationAliases: ["bench", "it", "test"], formatter: `npm exec --no -- prettier --write`, shouldFormat: true, typeToStringFormat: {} }) ``` ### `skipTypes` `skipTypes` is extremely useful for iterating quickly during development without having to typecheck your project to test runtime logic. When this setting is enabled, setup will skip type checking and all assertions requiring type information will be skipped. You likely want two scripts, one for running tests with types and one for tests without: ```json "test": "ATTEST_skipTypes=1 vitest run", "testWithTypes": "vitest run", ``` Our recommendation is to use `test` when: - Only wanting to test runtime logic during development - Running tests in watch mode or via VSCode's Test Explorer Use `testWithTypes` when: - You've made changes to your types and want to recheck your type-level assertions - You're running your tests in CI ### `typeToStringFormat` A set of [`prettier.Options`](https://prettier.io/docs/en/options.html) overrides that apply specifically `type.toString` formatting. Any options you provide will override the defaults, which are as follows: ```jsonc { "semi": false, // note this print width is optimized for type serialization, not general code "printWidth": 60, "trailingComma": "none", "parser": "typescript" } ``` The easiest way to provide overrides is to the `setup` function, but they can also be provided as a JSON serialized string either passed to a `--typeToStringFormat` CLI flag or set as the value of `ATTEST_typeToStringFormat` on `process.env`. ## Benches Benches are run separately from tests and don't require any special setup. If the below file was `benches.ts`, you could run it using something like `tsx benches.ts` or `ts-node benches.ts`: ```ts // Combinatorial template literals often result in expensive types- let's benchmark this one! type makeComplexType = s extends `${infer head}${infer tail}` ? head | tail | makeComplexType : s bench("bench type", () => { return {} as makeComplexType<"defenestration"> // This is an inline snapshot that will be populated or compared when you run the file }).types([169, "instantiations"]) bench( "bench runtime and type", () => { return {} as makeComplexType<"antidisestablishmentarianism"> }, fakeCallOptions ) // Average time it takes the function execute .mean([2, "ms"]) // Seems like our type is O(n) with respect to the length of the input- not bad! .types([337, "instantiations"]) ``` If you're benchmarking an API, you'll need to include a "baseline expression" so that instantiations created when your API is initially invoked don't add noise to the individual tests. Here's an example of what that looks like: ```ts import { bench } from "@ark/attest" import { type } from "arktype" // baseline expression type("boolean") bench("single-quoted", () => { const _ = type("'nineteen characters'") // would be 2697 without baseline }).types([610, "instantiations"]) bench("keyword", () => { const _ = type("string") // would be 2507 without baseline }).types([356, "instantiations"]) ``` > [!WARNING] > Be sure your baseline expression is not identical to an expression you are using in any of your benchmarks. If it is, the individual benchmarks will reuse its cached types, leading to reduced (or 0) instantiations. If you'd like to fail in CI above a threshold, you can add flags like the following (default value is 20%, but it will not throw unless `--benchErrorOnThresholdExceeded` is set): ``` tsx ./p99/within-limit/p99-tall-simple.bench.ts --benchErrorOnThresholdExceeded --benchPercentThreshold 10 ``` ## CLI Attest also includes a built-in `attest` CLI including the following commands: ### `stats` ```bash npm run attest stats packages/* ``` Summarizes key type performance metrics for each package (check time, instantiations, and type count). Expects any number of args representing package directories to check, optionally specified using glob patterns like `packages/*`. If no directories are provided, defaults to CWD. ### `trace` ```bash npm run attest trace . ``` Creates a trace.json file in `.attest/trace` that can be viewed as a type performance heat map via a tool like https://ui.perfetto.dev/. Also summarizes any hot spots as identified by `@typescript/analyze-trace`. Trace expects a single argument representing the root directory of the root package for which to gather type information. ## Integration ### Setup If you're a library author wanting to integrate type into your own assertions instead of using the `attest` API, you'll need to call `setup` with a list of `attestAliases` to ensure type data is collected from your own functions: ```ts // attest will only collect type data from functions with names listed in `attestAliases` setup({ attestAliases: ["yourCustomAssert"] }) // There are many other config options, but some are primarily internal- use others at your own risk! ``` You'll need to make sure that setup with whatever aliases you need before the first test runs. As part of the setup process, attest will search for the specified assertion calls and cache their types in a temporary file that will be referenced during test execution. This ensures that type assertions can be made across processes without creating a new TSServer instance for each. ### TS Versions There is a tsVersions setting that allows testing multiple TypeScript aliases at once. ````ts globalSetup.ts import { setup } from "@ark/attest" /** A string or list of strings representing the TypeScript version aliases to run. * * Aliases must be specified as a package.json dependency or devDependency beginning with "typescript". * Alternate aliases can be specified using the "npm:" prefix: * ```json * "typescript": "latest", * "typescript-next": "npm:typescript@next", * "typescript-1": "npm:typescript@5.2" * "typescript-2": "npm:typescript@5.1" * ``` * * "*" can be pased to run all discovered versions beginning with "typescript". */ setup({ tsVersions: "*" }) ```` ### APIs The most flexible attest APIs are `getTypeAssertionsAtPosition` and `caller`. Here's an example of how you might use them in your own API: ```ts import { getTypeAssertionsAtPosition, caller } from "@ark/attest" const yourCustomAssert = (actualValue: expectedType) => { const position = caller() const types = getTypeAssertionsAtPosition(position) // assert that the type of actualValue is the same as the type of expectedType const relationship = types[0].args[0].relationships.typeArgs[0] if (relationship === undefined) { throw new Error( `yourCustomAssert requires a type arg representing the expected type, e.g. 'yourCustomAssert<"foo">("foo")'` ) } if (relationship !== "equality") { throw new Error( `Expected ${types.typeArgs[0].type}, got ${types.args[0].type} with relationship ${relationship}` ) } } ``` A user might then use `yourCustomAssert` like this: ```ts import { yourCustomAssert } from "your-package" test("my code", () => { // Ok yourCustomAssert<"foo">(`${"f"}oo` as const) // Error: `Expected boolean, got true with relationship subtype` yourCustomAssert(true) // Error: `Expected 5, got number with relationship supertype` yourCustomAssert<5>(2 + 3) }) ``` Please don't hesitate to a GitHub [issue](https://github.com/arktypeio/arktype/issues/new/choose) or [discussion](https://github.com/arktypeio/arktype/discussions/new/choose) or reach out on [ArkType's Discord](https://arktype.io/discord) if you have any questions or feedback- we'd love to hear from you! ⛵ ================================================ FILE: ark/attest/__tests__/assertions.test.ts ================================================ import { attest } from "@ark/attest" import { MissingSnapshotError } from "@ark/attest/internal/assert/assertions.ts" import { attestInternal } from "@ark/attest/internal/assert/attest.ts" import { register } from "@ark/util" import { type } from "arktype" import * as assert from "node:assert/strict" const o = { ark: "type" } describe("type assertions", () => { it("type parameter", () => { attest<{ ark: string }>(o) assert.throws( // @ts-expect-error () => attest<{ ark: "type" }>(o), assert.AssertionError, "type" ) }) it("type-only assertion", () => { attest<{ ark: string }, typeof o>() assert.throws( // @ts-expect-error () => attest<{ ark: "type" }, typeof o>(), assert.AssertionError, "type" ) }) it("type toString", () => { attest(o).type.toString("{ ark: string }") attest(o).type.toString.is("{ ark: string }") attest(o).type.toString.satisfies(/^{.*}$/) assert.throws( () => attest(o).type.toString.satisfies(/^a.*z$/), assert.AssertionError, "typfsde" ) }) it("type toString regex", () => { attest(o).type.toString(/^{.*}$/) assert.throws( () => attest(o).type.toString(/^a.*z$/), assert.AssertionError, "typfsde" ) }) it("type toString multiline", () => { const obj = { ark: "type", type: "script", vali: "dator", opti: "mized", from: "editor", to: "runtime" } attest(obj).type.toString.is(`{ ark: string type: string vali: string opti: string from: string to: string }`) }) it("equals", () => { attest(o).equals({ ark: "type" }) }) it("object", () => { attest<{ i: string }>({ i: "love my wife" }) assert.throws( // @ts-expect-error () => attest<{ g: string }>({ g: "whiz" as unknown }), assert.AssertionError, "unknown" ) }) it("typed allows equivalent types", () => { const actual = { a: true, b: false } attest<{ b: boolean a: boolean }>(actual) }) it("functional asserts don't exist on pure value types", () => { // @ts-expect-error // eslint-disable-next-line @typescript-eslint/no-unused-expressions attest(5).throws }) it("not equal", () => { assert.throws( () => attest(o).equals({ ark: "typo" }), assert.AssertionError, "type !== typo" ) }) it("instanceOf", () => { const d = new Date() attest(d).instanceOf(Date) assert.throws(() => attest(d).instanceOf(RegExp), assert.AssertionError) }) it("incorrect type", () => { assert.throws( // @ts-expect-error () => attest<{ re: number }>(o), assert.AssertionError, "o is not of type number" ) }) it("any type", () => { attest(o as any) assert.throws( () => attest({} as unknown), assert.AssertionError, "unknown" ) }) it("assert unknown ignores type", () => { const myValue = { a: ["+"] } as const const myExpectedValue = { a: ["+"] } // @ts-expect-error attest(myValue).equals(myExpectedValue) attest(myValue).unknown.equals(myExpectedValue) assert.throws( () => attest(myValue).unknown.is(myExpectedValue), assert.AssertionError, "not reference-equal" ) }) it("multiline", () => { attest({ several: true, lines: true, long: true } as object) assert.throws( () => attest({ several: true, lines: true, long: true }), assert.AssertionError ) }) it("nonexistent types always fail", () => { // @ts-expect-error const nonexistent: NonExistent = {} assert.throws( () => attest<{ something: "specific" }>(nonexistent), assert.AssertionError, "specific" ) }) it("does not boom on Type comparison", () => { const expectedRef = register(type.number) const actualRef = register(type.string) // @ts-expect-error attest(() => attest(type.string).equals(type.number)).throws .equals(`AssertionError [ERR_ASSERTION]: Assertion including at least one function or object was not between reference equal items Expected: Function(${expectedRef}) Actual: Function(${actualRef})`) }) it("doesn't boom on ArkErrors vs plain object", () => { attest(() => attest(type({ a: "string" })({ a: 5 })).equals({ a: "five" })) .throws .snap(`AssertionError [ERR_ASSERTION]: Objects did not have the same constructor: Expected: {"a":"five"} Actual: [ArkError]`) }) it("jsdoc ", () => { type O = { /** FOO */ foo: string bar: number } const o: O = { foo: "foo", bar: 5 } attest(o.foo).jsdoc.equals("FOO") assert.throws( () => attest(o.bar).jsdoc.equals("BAR"), assert.AssertionError, "BAR" ) }) it("failOnMissingSnapshots", () => { assert.throws( () => attestInternal("", { cfg: { failOnMissingSnapshots: true } }).snap(), MissingSnapshotError ) }) }) ================================================ FILE: ark/attest/__tests__/benchExpectedOutput.ts ================================================ import { bench } from "@ark/attest" import type { makeComplexType as externalmakeComplexType } from "./utils.ts" const fakeCallOptions = { until: { count: 2 }, fakeCallMs: "count", benchFormat: { noExternal: true } } bench( "bench call single stat median", () => "boofoozoo".includes("foo"), fakeCallOptions ).median([2, "ms"]) bench( "bench call single stat", () => "boofoozoo".includes("foo"), fakeCallOptions ).mean([2, "ms"]) bench( "bench call mark", () => /.*foo.*/.test("boofoozoo"), fakeCallOptions ).mark({ mean: [2, "ms"], median: [2, "ms"] }) type makeComplexType = S extends `${infer head}${infer tail}` ? head | tail | makeComplexType : S bench("bench type", () => ({}) as makeComplexType<"defenestration">).types([ 163, "instantiations" ]) bench( "bench type from external module", () => ({}) as externalmakeComplexType<"defenestration"> ).types([179, "instantiations"]) bench( "bench call and type", () => ({}) as makeComplexType<"antidisestablishmentarianism">, fakeCallOptions ) .mean([2, "ms"]) .types([317, "instantiations"]) bench("empty", () => {}).types([0, "instantiations"]) ================================================ FILE: ark/attest/__tests__/benchTemplate.ts ================================================ import { bench } from "@ark/attest" import type { makeComplexType as externalmakeComplexType } from "./utils.ts" const fakeCallOptions = { until: { count: 2 }, fakeCallMs: "count", benchFormat: { noExternal: true } } bench( "bench call single stat median", () => "boofoozoo".includes("foo"), fakeCallOptions ).median() bench( "bench call single stat", () => "boofoozoo".includes("foo"), fakeCallOptions ).mean() bench( "bench call mark", () => /.*foo.*/.test("boofoozoo"), fakeCallOptions ).mark() type makeComplexType = S extends `${infer head}${infer tail}` ? head | tail | makeComplexType : S bench("bench type", () => ({}) as makeComplexType<"defenestration">).types() bench( "bench type from external module", () => ({}) as externalmakeComplexType<"defenestration"> ).types() bench( "bench call and type", () => ({}) as makeComplexType<"antidisestablishmentarianism">, fakeCallOptions ) .mean() .types() bench("empty", () => {}).types() ================================================ FILE: ark/attest/__tests__/completions.test.ts ================================================ import { attest, contextualize } from "@ark/attest" import { hasDomain, type domainDescriptions } from "@ark/util" import assert from "node:assert" type Obj = { prop1: string prop2: string extra: unknown } const obj: Obj = { prop1: "", prop2: "", extra: "" } type Ark = { ark: "type" } type Arks = { ark: "string" | "semver" | "symbol" } contextualize(() => { it("quote types", () => { // @ts-expect-error attest({ ark: "" } as Ark).completions({ "": ["type"] }) // prettier-ignore // @ts-expect-error attest({ ark: "t" } as Ark).completions({ t: ["type"] }) //@ts-expect-error attest({ ark: "ty" } as Ark).completions({ ty: ["type"] }) }) it(".type.completions", () => { //@ts-expect-error attest({ ark: "s" } as Arks).type.completions({ s: ["semver", "string", "symbol"] }) }) it("keys", () => { //@ts-expect-error attest({ "": "data" } as Obj).completions({ "": ["extra", "prop1", "prop2"] }) }) it("index access", () => { //@ts-expect-error attest(() => obj["p"]).type.completions({ p: ["prop1", "prop2"] }) }) it("duplicate string error", () => { assert.throws( () => attest({ "": "" }).type.completions({}), Error, "multiple completion candidates" ) }) it("empty", () => { attest("").completions({}) }) it("external package", () => { hasDomain({}, "object") // @ts-expect-error attest(() => hasDomain({}, "b")).completions({ b: ["bigint", "boolean"] }) }) it("type-level", () => { // @ts-expect-error attest((): domainDescriptions["b"] => {}).completions({ b: ["bigint", "boolean"] }) }) }) ================================================ FILE: ark/attest/__tests__/demo.test.ts ================================================ import { attest, contextualize, getPrimaryTsVersionUnderTest } from "@ark/attest" import { type } from "arktype" const o = { ark: "type" } as const const shouldThrow = (a: false) => { if (a) throw new Error(`${a} is not assignable to false`) } contextualize(() => { it("value snap", () => { attest(o).snap({ ark: "type" }) }) it("type snap", () => { attest(o).type.toString.snap('{ readonly ark: "type" }') }) it("type assertion", () => { attest<{ readonly ark: "type" }>(o) }) it("type-only assertion", () => { attest<{ readonly ark: "type" }, typeof o>() }) it("chained snaps", () => { attest(o) .snap({ ark: "type" }) .type.toString.snap('{ readonly ark: "type" }') }) it("error and type error snap", () => { // @ts-expect-error attest(() => shouldThrow(true)) .throws.snap("Error: true is not assignable to false") .type.errors.snap( "Argument of type 'true' is not assignable to parameter of type 'false'." ) }) // @ark/attest assertions can be made from any unit test framework with a global setup/teardown it("type and value assertions", () => { const Even = type("number%2") // snapshot types and values seamlessly attest(Even.infer).type.toString.snap("number") // including object literals- no more long inline strings! attest(Even.json).snap({ domain: "number", divisor: 2 }) }) it("error assertions", () => { // Check type errors, runtime errors, or both at the same time! // @ts-expect-error attest(() => type("number%0")).throwsAndHasTypeError( "% operator must be followed by a non-zero integer literal (was 0)" ) // @ts-expect-error attest(() => type({ "[object]": "string" })).type.errors( "Indexed key definition 'object' must be a string or symbol" ) }) it("completion snapshotting", () => { // snapshot expected completions for any string literal! // @ts-expect-error (if your expression would throw, prepend () =>) attest(() => type({ b: "b" })).completions({ b: ["bigint", "boolean"] }) type Legends = { faker?: "🐐"; [others: string]: unknown } // works for keys or index access as well (may need prettier-ignore to // avoid removing quotes) // prettier-ignore attest({ "f": "🐐" } as Legends).completions({ f: ["faker"] }) }) it("integrate runtime logic with type assertions", () => { const arrayOf = type("", "t[]") const NumericArray = arrayOf("number | bigint") // flexibly combine runtime logic with type assertions to customize your // tests beyond what is possible from pure static-analysis based type testing tools if (getPrimaryTsVersionUnderTest().startsWith("5")) { // this assertion will only occur when testing TypeScript 5+! attest<(number | bigint)[]>(NumericArray.infer) } }) }) ================================================ FILE: ark/attest/__tests__/externalSnapshots.test.ts ================================================ import { attest, contextualize } from "@ark/attest" import { attestInternal } from "@ark/attest/internal/assert/attest.ts" import { dirName, readJson, writeJson } from "@ark/fs" import * as assert from "node:assert/strict" import { rmSync } from "node:fs" import { join } from "node:path" const testDir = dirName() const testFile = "externalSnapshots.test.ts" const o = { re: "do" } const defaultFileName = "assert.snapshots.json" const defaultSnapPath = join(testDir, defaultFileName) const defaultSnapFileContents = { [testFile]: { toFile: { re: "do" }, toFileUpdate: { re: "oldValue" } } } const customFileName = "custom.snapshots.json" const customSnapPath = join(testDir, customFileName) const defaultSnapContentsAtCustomPath = { [testFile]: { toCustomFile: { re: "do" } } } beforeEach(() => { writeJson(defaultSnapPath, defaultSnapFileContents) writeJson(customSnapPath, defaultSnapContentsAtCustomPath) }) afterEach(() => { rmSync(defaultSnapPath, { force: true }) rmSync(customSnapPath, { force: true }) }) contextualize(() => { it("create", () => { attest(o).snap.toFile("toFile") assert.throws( () => attest({ re: "kt" }).snap.toFile("toFile"), assert.AssertionError, "kt" ) attest(1337).snap.toFile("toFileNew") const contents = readJson(defaultSnapPath) attest(contents).equals({ [testFile]: { ...defaultSnapFileContents[testFile], toFileNew: 1337 } }) }) it("update existing", () => { attestInternal( { re: "dew" }, { cfg: { updateSnapshots: true } } ).snap.toFile("toFileUpdate") const updatedContents = readJson(defaultSnapPath) const expectedContents = { [testFile]: { ...defaultSnapFileContents[testFile], toFileUpdate: { re: "dew" } } } assert.deepEqual(updatedContents, expectedContents) }) it("with path", () => { attest(o).snap.toFile("toCustomFile", { path: customFileName }) assert.throws( () => attest({ re: "kt" }).snap.toFile("toCustomFile", { path: customFileName }), assert.AssertionError, "kt" ) attest(null).snap.toFile("toCustomFileNew", { path: customFileName }) const contents = readJson(customSnapPath) attest(contents).equals({ [testFile]: { ...defaultSnapContentsAtCustomPath[testFile], toCustomFileNew: null } }) }) }) ================================================ FILE: ark/attest/__tests__/functions.test.ts ================================================ import { attest, contextualize } from "@ark/attest" import { fileName } from "@ark/fs" import * as assert from "node:assert/strict" import { basename } from "node:path" const n = 5 const o = { re: "do" } const shouldThrow = (a: false) => { if (a) throw new Error(`${a} is not assignable to false`) } const throwError = () => { throw new Error("Test error.") } contextualize(() => { it("valid type errors", () => { // @ts-expect-error attest(o.re.length.nonexistent).type.errors( /Property 'nonexistent' does not exist on type 'number'/ ) attest(o).type.errors("") // @ts-expect-error attest(() => shouldThrow(5, "")).type.errors.is( "Expected 1 arguments, but got 2." ) }) it("bad type errors", () => { assert.throws( () => attest(o).type.errors(/This error doesn't exist/), assert.AssertionError, "doesn't exist" ) assert.throws( () => attest(() => // @ts-expect-error shouldThrow("this is a type error") ).type.errors.is(""), assert.AssertionError, "not assignable" ) }) it("chainable", () => { attest<{ re: string }>(o).equals({ re: "do" }) // @ts-expect-error attest(() => throwError("this is a type error")) .throws("Test error.") .type.errors("Expected 0 arguments, but got 1.") }) it("bad chainable", () => { assert.throws( () => attest(n) .equals(5) .type.errors.equals("Expecting an error here will throw"), assert.AssertionError, "Expecting an error" ) assert.throws( () => attest(n).is(7).type.toString("string"), assert.AssertionError, "7" ) }) it("throwsAndHasTypeError", () => { // @ts-expect-error attest(() => shouldThrow(true)).throwsAndHasTypeError( /true[\S\s]*not assignable[\S\s]*false/ ) // No thrown error assert.throws( () => // @ts-expect-error attest(() => shouldThrow(null)).throwsAndHasTypeError("not assignable"), assert.AssertionError, "didn't throw" ) // No type error assert.throws( () => attest(() => shouldThrow(true as any)).throwsAndHasTypeError( "not assignable" ), assert.AssertionError, "not assignable" ) }) it("throws empty", () => { attest(throwError).throws() assert.throws( () => attest(() => shouldThrow(false)).throws(), assert.AssertionError, "didn't throw" ) }) const getThrownError = (f: () => void) => { try { f() } catch (e) { if (e instanceof Error) return e } throw new Error("Expected function to throw an error.") } it("stack starts from test file", () => { const e = getThrownError(() => attest(1 + 1).equals(3)) assert.match(e.stack!.split("\n")[1], new RegExp(basename(fileName()))) }) }) ================================================ FILE: ark/attest/__tests__/instantiations.test.ts ================================================ import { attest, contextualize } from "@ark/attest" import { type } from "arktype" import { it } from "mocha" contextualize(() => { it("inline", () => { attest.instantiations([23731, "instantiations"]) return type({ kind: "'admin'", "powers?": "string[]" }) .or({ kind: "'superadmin'", "superpowers?": "string[]" }) .or({ kind: "'pleb'" }) }) it("fails on instantiations above threshold", () => { attest(() => { attest.instantiations([1, "instantiations"]) return type({ foo: "0|1|2|3|4|5|6" }) }).throws("exceeded baseline by") }) }) ================================================ FILE: ark/attest/__tests__/satisfies.test.ts ================================================ import { attest, contextualize } from "@ark/attest" import { nonOverlappingSatisfiesMessage } from "@ark/attest/internal/assert/chainableAssertions.ts" contextualize(() => { it("can assert types", () => { attest({ foo: "bar" }).satisfies({ foo: "string" }) attest(() => { // @ts-expect-error attest({ foo: "bar" }).satisfies({ foo: "number" }) }) .throws("foo must be a number (was a string)") .type.errors(nonOverlappingSatisfiesMessage) }) }) ================================================ FILE: ark/attest/__tests__/snap.test.ts ================================================ import { attest, contextualize } from "@ark/attest" import * as assert from "node:assert/strict" const o = { re: "do" } const shouldThrow = (a: false) => { if (a) throw new Error(`${a} is not assignable to false`) } const throwError = () => { throw new Error("Test error.") } contextualize(() => { it("default serializer doesn't care about prop order", () => { const actual = { a: true, b: false } attest(actual).snap({ b: false, a: true }) }) it("snap", () => { attest<{ re: string }>(o).snap({ re: `do` }) attest(o).equals({ re: "do" }).type.toString.snap("{ re: string }") assert.throws( () => attest(o).snap({ re: `dorf` }), assert.AssertionError, "dorf" ) }) it("value and type snap", () => { attest(o).snap({ re: `do` }).type.toString.snap("{ re: string }") assert.throws( () => attest(o).snap({ re: `do` }).type.toString.snap(`{ re: number }`), assert.AssertionError, "number" ) }) it("error and type error snap", () => { // @ts-expect-error attest(() => shouldThrow(true)) .throws.snap(`Error: true is not assignable to false`) .type.errors.snap( `Argument of type 'true' is not assignable to parameter of type 'false'.` ) assert.throws( () => // @ts-expect-error attest(() => shouldThrow(1)) .throws.snap(`Error: 1 is not assignable to false`) .type.errors.snap( `Argument of type '2' is not assignable to parameter of type 'false'.` ), assert.AssertionError, "'2'" ) }) it("throws", () => { attest(throwError).throws(/error/g) assert.throws( // Snap should never be populated () => attest(() => shouldThrow(false)).throws.snap(), assert.AssertionError, "didn't throw" ) }) /* * Some TS errors as formatted as diagnostic "chains" * We represent them by joining the parts of the message with newlines */ it("TS diagnostic chain", () => { // @ts-expect-error attest(() => shouldThrow({} as {} | false)).type.errors.snap( `Argument of type 'false | {}' is not assignable to parameter of type 'false'.Type '{}' is not assignable to type 'false'.` ) }) it("multiple inline snaps", () => { attest("firstLine\nsecondLine").snap(`firstLine secondLine`) attest("firstLine\nsecondLine").snap(`firstLine secondLine`) }) }) ================================================ FILE: ark/attest/__tests__/snapExpectedOutput.ts ================================================ import { attest, cleanup, setup } from "@ark/attest" import type { makeComplexType } from "./utils.ts" setup({ typeToStringFormat: { useTabs: true } }) attest({ re: "do" }).equals({ re: "do" }).type.toString.snap("{ re: string }") attest({ ark: "type", type: "script", vali: "dator", opti: "mized", from: "editor", to: "runtime" }).snap({ ark: "type", type: "script", vali: "dator", opti: "mized", from: "editor", to: "runtime" }).type.toString.snap(`{ ark: string type: string vali: string opti: string from: string to: string }`) attest(5).snap(5) attest({ re: "do" }).snap({ re: "do" }) // @ts-expect-error (using internal updateSnapshots hook) attest({ re: "dew" }, { cfg: { updateSnapshots: true } }).snap({ re: "dew" }) // @ts-expect-error (using internal updateSnapshots hook) attest(5, { cfg: { updateSnapshots: true } }).snap(5) attest(5n).snap(5n) attest(-5n).snap(-5n) attest({ a: 4n }).snap({ a: 4n }) attest(undefined).snap(undefined) attest("undefined").snap("undefined") attest({ a: undefined }).snap({ a: undefined }) attest("multiline\nmultiline").snap(`multiline multiline`) attest("with `quotes`").snap("with `quotes`") attest({ a2z: `a"'${"" as string}'"z`, z2a: `z"'${"" as string}'"a`, ark: "type", type: "ark" } as const).type.toString.snap(`{ readonly a2z: \`a"'\${string}'"z\` readonly z2a: \`z"'\${string}'"a\` readonly ark: "type" readonly type: "ark" }`) attest({ [Symbol("mySymbol")]: 1 }).snap({ "Symbol(mySymbol)": 1 }) const it = (name: string, fn: () => void) => fn() it("can snap instantiations", () => { attest.instantiations([212, "instantiations"]) return {} as makeComplexType<"asbsdfsaodisfhsda"> }) cleanup() ================================================ FILE: ark/attest/__tests__/snapPopulation.test.ts ================================================ import { contextualize } from "@ark/attest" import { fromHere, readFile } from "@ark/fs" import { equal } from "node:assert/strict" import { runThenGetContents } from "./utils.ts" contextualize(() => { it("bench populates file", () => { const actual = runThenGetContents(fromHere("benchTemplate.ts")) const expectedOutput = readFile(fromHere("benchExpectedOutput.ts")).replace( /\r\n/g, "\n" ) equal(actual, expectedOutput) }).timeout(60000) it("snap populates file", () => { const actual = runThenGetContents(fromHere("snapTemplate.ts")) const expectedOutput = readFile(fromHere("snapExpectedOutput.ts")).replace( /\r\n/g, "\n" ) equal(actual, expectedOutput) }).timeout(60000) }) ================================================ FILE: ark/attest/__tests__/snapTemplate.ts ================================================ import { attest, cleanup, setup } from "@ark/attest" import type { makeComplexType } from "./utils.ts" setup({ typeToStringFormat: { useTabs: true } }) attest({ re: "do" }).equals({ re: "do" }).type.toString.snap() attest({ ark: "type", type: "script", vali: "dator", opti: "mized", from: "editor", to: "runtime" }) .snap() .type.toString.snap() attest(5).snap() attest({ re: "do" }).snap() // @ts-expect-error (using internal updateSnapshots hook) attest({ re: "dew" }, { cfg: { updateSnapshots: true } }).snap({ re: "do" }) // @ts-expect-error (using internal updateSnapshots hook) attest(5, { cfg: { updateSnapshots: true } }).snap(6) attest(5n).snap() attest(-5n).snap() attest({ a: 4n }).snap() attest(undefined).snap() attest("undefined").snap() attest({ a: undefined }).snap() attest("multiline\nmultiline").snap() attest("with `quotes`").snap() attest({ a2z: `a"'${"" as string}'"z`, z2a: `z"'${"" as string}'"a`, ark: "type", type: "ark" } as const).type.toString.snap() attest({ [Symbol("mySymbol")]: 1 }).snap() const it = (name: string, fn: () => void) => fn() it("can snap instantiations", () => { attest.instantiations() return {} as makeComplexType<"asbsdfsaodisfhsda"> }) cleanup() ================================================ FILE: ark/attest/__tests__/unwrap.test.ts ================================================ import { attest, contextualize } from "@ark/attest" import type { Completions } from "@ark/attest/internal/cache/writeAssertionCache.ts" import type { autocomplete } from "@ark/util" contextualize(() => { it("unwraps unversioned", () => { const unwrapped = attest({ foo: "bar" }).unwrap() attest<{ foo: string }>(unwrapped).equals({ foo: "bar" }) }) it("unwraps serialized", () => { const unwrapped = attest({ foo: Symbol("unwrappedSymbol") }).unwrap({ serialize: true }) attest(unwrapped).snap({ foo: "Symbol(unwrappedSymbol)" }) }) it("unwraps completions", () => { const unwrapped = attest({ foo: "b" } satisfies { foo: autocomplete<"bar"> }).completions.unwrap() attest(unwrapped).snap({ b: ["bar"] }) }) }) ================================================ FILE: ark/attest/__tests__/utils.ts ================================================ import { dirName, fromHere, readFile, shell } from "@ark/fs" import { copyFileSync, rmSync } from "node:fs" export const runThenGetContents = (templatePath: string): string => { rmSync(fromHere(".attest"), { force: true, recursive: true }) const tempPath = templatePath + ".temp.ts" copyFileSync(templatePath, tempPath) try { shell(`node --import=tsx ${tempPath}`, { cwd: dirName(), env: { ATTEST_failOnMissingSnapshots: "0" } }) } catch (e) { console.error(e) } const resultContents = readFile(tempPath) rmSync(tempPath) return resultContents } // type is used in benchTemplate.ts to test compatibility with external modules export type makeComplexType = S extends `${infer head}${infer tail}` ? head | tail | makeComplexType : S ================================================ FILE: ark/attest/assert/assertions.ts ================================================ import { printable, throwInternalError } from "@ark/util" import type { type } from "arktype" import * as assert from "node:assert/strict" import type { TypeRelationshipAssertionData } from "../cache/writeAssertionCache.ts" import type { AssertionContext } from "./attest.ts" export type ThrowAssertionErrorContext = { message: string expected?: unknown actual?: unknown stack: string } export const throwAssertionError = ({ stack, ...errorArgs }: ThrowAssertionErrorContext): never => { const e = new assert.AssertionError(errorArgs) e.stack = stack throw e } export class MissingSnapshotError extends Error {} export type AssertFn = ( expected: unknown, actual: unknown, ctx: AssertionContext ) => void export type MappedTypeAssertionResult = { actual: unknown expected?: unknown } | null export type TypeAssertionMapper = ( data: TypeRelationshipAssertionData, ctx: AssertionContext ) => MappedTypeAssertionResult export class TypeAssertionMapping { fn: TypeAssertionMapper constructor(fn: TypeAssertionMapper) { this.fn = fn } } export const versionableAssertion = (fn: AssertFn): AssertFn => (expected, actual, ctx) => { if (actual instanceof TypeAssertionMapping) { if (!ctx.typeRelationshipAssertionEntries) { throwInternalError( `Unexpected missing typeAssertionEntries when passed a TypeAssertionMapper` ) } for (const [version, data] of ctx.typeRelationshipAssertionEntries) { let errorMessage = "" try { const mapped = actual.fn(data, ctx) if (mapped !== null) { fn( "expected" in mapped ? mapped.expected : expected, mapped.actual, ctx ) } } catch (e) { errorMessage += `❌TypeScript@${version}:${e}\n` } if (errorMessage) { throwAssertionError({ stack: ctx.assertionStack, message: errorMessage }) } } } else fn(expected, actual, ctx) } const unversionedAssertEquals: AssertFn = (expected, actual, ctx) => { if (expected === actual) return try { if ( typeof expected === "object" && expected !== null && typeof actual === "object" && actual !== null ) { if (expected.constructor === actual.constructor) assert.deepStrictEqual(actual, expected) else { const serializedExpected = printable(expected) const serializedActual = printable(actual) throw new assert.AssertionError({ message: `Objects did not have the same constructor: Expected: ${serializedExpected} Actual: ${serializedActual}`, expected: serializedExpected, actual: serializedActual }) } } else if ( typeof expected === "object" || typeof expected === "function" || typeof actual === "function" || typeof actual === "function" ) { const serializedExpected = printable(expected) const serializedActual = printable(actual) throw new assert.AssertionError({ message: `Assertion including at least one function or object was not between reference equal items Expected: ${serializedExpected} Actual: ${serializedActual}`, expected: serializedExpected, actual: serializedActual }) // guaranteed to be two primitives at this point } else assert.equal(actual, expected) } catch (e: any) { // some nonsense to get a good stack trace e.stack = ctx.assertionStack throw e } } export const assertEquals: AssertFn = versionableAssertion( unversionedAssertEquals ) const unversionedAssertSatisfies = ( t: type.Any, data: unknown, ctx: AssertionContext ) => { try { t.assert(data) } catch (e: any) { e.stack = ctx.assertionStack throw e } } export const assertSatisfies = versionableAssertion( unversionedAssertSatisfies as never ) export const typeEqualityMapping: TypeAssertionMapping = new TypeAssertionMapping(data => { const expected = data.typeArgs[0] const actual = data.typeArgs[1] ?? data.args[0] if (!expected || !actual) throwInternalError(`Unexpected type data ${printable(data)}`) if (actual.relationships.typeArgs[0] !== "equality") { return { expected: expected.type, actual: expected.type === actual.type ? "(serializes to same value)" : actual.type } } return null }) export const assertEqualOrMatching: AssertFn = versionableAssertion( (expected, actual, ctx) => { const assertionArgs = { actual, expected, stack: ctx.assertionStack } if (typeof actual !== "string") { throwAssertionError({ message: `Value was of type ${typeof actual} (expected a string).`, ...assertionArgs }) } else if (typeof expected === "string") { if (!actual.includes(expected)) { throwAssertionError({ message: `Expected string '${expected}' did not appear in actual string '${actual}'.`, ...assertionArgs }) } } else if (expected instanceof RegExp) { if (!expected.test(actual)) { throwAssertionError({ message: `Actual string '${actual}' did not match regex '${expected.source}'.`, ...assertionArgs }) } } else { throw new Error( `Expected value for this assertion should be a string or RegExp.` ) } } ) export type AssertedFnCallResult = { returned?: unknown threw?: string } export const getThrownMessage = ( result: AssertedFnCallResult, ctx: AssertionContext ): string | undefined => { if (!("threw" in result)) { throwAssertionError({ message: "Function didn't throw", stack: ctx.assertionStack }) } return result.threw } export const callAssertedFunction = ( asserted: Function ): AssertedFnCallResult => { const result: AssertedFnCallResult = {} try { result.returned = asserted() } catch (error) { result.threw = String(error) } return result } ================================================ FILE: ark/attest/assert/attest.ts ================================================ import { caller, getCallStack, type SourcePosition } from "@ark/fs" import type { ErrorMessage } from "@ark/util" import { getBenchCtx } from "../bench/bench.ts" import type { Measure } from "../bench/measure.ts" import { instantiationDataHandler } from "../bench/type.ts" import { getTypeAssertionsAtPosition, type VersionedTypeAssertion } from "../cache/getCachedAssertions.ts" import { getConfig, type AttestConfig } from "../config.ts" import { assertEquals, typeEqualityMapping, type TypeAssertionMapping } from "./assertions.ts" import { ChainableAssertions, type AssertionKind, type rootAssertions } from "./chainableAssertions.ts" export type AttestFn = { ( ...args: actual extends never ? [ ErrorMessage<"Type-only assertion requires two explicit generic params, e.g. attest"> ] : [] ): void (actual: expected): rootAssertions instantiations: (count?: Measure<"instantiations"> | undefined) => void } export type VersionableActual = {} | null | undefined | TypeAssertionMapping export type AssertionContext = { versionableActual: VersionableActual originalAssertedValue: unknown cfg: AttestConfig allowRegex: boolean position: SourcePosition defaultExpected?: unknown assertionStack: string typeRelationshipAssertionEntries?: VersionedTypeAssertion<"type">[] typeBenchmarkingAssertionEntries?: VersionedTypeAssertion<"bench">[] lastSnapName?: string } export type InternalAssertionHooks = { [k in keyof AssertionContext]?: k extends "cfg" ? Partial : AssertionContext[k] } export const attestInternal = ( value?: unknown, { cfg: cfgHooks, ...ctxHooks }: InternalAssertionHooks = {} ): ChainableAssertions => { const position = caller() const cfg = { ...getConfig(), ...cfgHooks } const ctx: AssertionContext = { versionableActual: value, allowRegex: false, originalAssertedValue: value, position, cfg, assertionStack: getCallStack({ offset: 1 }).join("\n"), ...ctxHooks } if (!cfg.skipTypes) { ctx.typeRelationshipAssertionEntries = getTypeAssertionsAtPosition(position) if (ctx.typeRelationshipAssertionEntries[0]?.[1].typeArgs[0]) { // if there is an expected type arg, check it immediately assertEquals(undefined, typeEqualityMapping, ctx) } } return new ChainableAssertions(ctx) } export const attest: AttestFn = Object.assign(attestInternal, { instantiations: (args: Measure<"instantiations"> | undefined) => { const attestConfig = getConfig() if (attestConfig.skipTypes || attestConfig.skipInlineInstantiations) return const calledFrom = caller() const ctx = getBenchCtx([calledFrom.file]) ctx.benchCallPosition = calledFrom ctx.lastSnapCallPosition = calledFrom instantiationDataHandler( { ...ctx, lastSnapFunctionName: "instantiations" }, args, false ) } }) as never ================================================ FILE: ark/attest/assert/chainableAssertions.ts ================================================ import { caller, positionToString } from "@ark/fs" import { printable, snapshot, type Constructor, type ErrorType, type isDisjoint } from "@ark/util" import prettier from "@prettier/sync" import { type } from "arktype" import * as assert from "node:assert/strict" import { isDeepStrictEqual } from "node:util" import { getSnapshotByName, queueSnapshotUpdate, updateExternalSnapshot, type SnapshotArgs } from "../cache/snapshots.ts" import type { Completions } from "../cache/writeAssertionCache.ts" import { getConfig } from "../config.ts" import { chainableNoOpProxy } from "../utils.ts" import { MissingSnapshotError, TypeAssertionMapping, assertEqualOrMatching, assertEquals, assertSatisfies, callAssertedFunction, getThrownMessage, throwAssertionError } from "./assertions.ts" import type { AssertionContext, VersionableActual } from "./attest.ts" export type ChainableAssertionOptions = { allowRegex?: boolean defaultExpected?: unknown } type AssertionRecord = Record, unknown> export type UnwrapOptions = { versionable?: boolean serialize?: boolean } export class ChainableAssertions implements AssertionRecord { private ctx: AssertionContext constructor(ctx: AssertionContext) { this.ctx = ctx } private get unversionedActual(): unknown { if (this.versionableActual instanceof TypeAssertionMapping) { return this.versionableActual.fn( this.ctx.typeRelationshipAssertionEntries![0][1], this.ctx )!.actual } return this.versionableActual } private get versionableActual(): VersionableActual { return this.ctx.versionableActual } private get serializedActual(): unknown { return snapshot(this.unversionedActual) } unwrap(opts?: UnwrapOptions): unknown { const value = opts?.versionable ? this.versionableActual : this.unversionedActual return opts?.serialize ? snapshot(value) : value } private snapRequiresUpdate(expectedSerialized: unknown) { return ( !isDeepStrictEqual(this.serializedActual, expectedSerialized) || // If actual is undefined, we still need to write the "undefined" literal // to the snap even though it will serialize to the same value as the (nonexistent) first arg this.unversionedActual === undefined ) } get unknown(): this { return this } is(expected: unknown): this { assert.equal(this.unversionedActual, expected) return this } equals(expected: unknown): this { assertEquals(expected, this.versionableActual, this.ctx) return this } satisfies(def: unknown): this { assertSatisfies(type.raw(def), this.versionableActual, this.ctx) return this } instanceOf(expected: Constructor): this { if (!(this.versionableActual instanceof expected)) { throwAssertionError({ stack: this.ctx.assertionStack, message: `Expected an instance of ${expected.name} (was ${ ( typeof this.versionableActual === "object" && this.versionableActual !== null ) ? this.versionableActual.constructor.name : this.serializedActual })` }) } return this } get snap(): snapProperty { // Use variadic args to distinguish undefined being passed explicitly from no args const inline = (...args: unknown[]) => { const snapName = this.ctx.lastSnapName ?? "snap" const expectedSerialized = snapshot(args[0]) if (!args.length || this.ctx.cfg.updateSnapshots) { const position = caller() if (this.ctx.cfg.failOnMissingSnapshots) { throw new MissingSnapshotError( `.${snapName}() at ${positionToString(position)} must be populated.` ) } if (this.snapRequiresUpdate(expectedSerialized)) { const snapshotArgs: SnapshotArgs = { position, serializedValue: this.serializedActual, snapFunctionName: snapName } queueSnapshotUpdate(snapshotArgs) } } else { // compare as strings, but if match fails, compare again as objects // to give a clearer error message. This avoid problems with objects // like subtypes of array that do not pass node's deep equality test // but serialize to the same value. if (printable(args[0]) !== printable(this.unversionedActual)) assertEquals(expectedSerialized, this.serializedActual, this.ctx) } return this } const toFile = (id: string, opts?: ExternalSnapshotOptions) => { const expectedSnapshot = getSnapshotByName( this.ctx.position.file, id, opts?.path ) if (!expectedSnapshot || this.ctx.cfg.updateSnapshots) { if (this.snapRequiresUpdate(expectedSnapshot)) { updateExternalSnapshot({ serializedValue: this.serializedActual, position: caller(), name: id, customPath: opts?.path }) } } else assertEquals(expectedSnapshot, this.serializedActual, this.ctx) return this } return Object.assign(inline, { toFile, unwrap: this.unwrap.bind(this) }) } private immediateOrChained() { const immediateAssertion = (...args: [expected: unknown]) => { let expected if (args.length) expected = args[0] else { if ("defaultExpected" in this.ctx) expected = this.ctx.defaultExpected else { throw new Error( `Assertion call requires an arg representing the expected value.` ) } } if (this.ctx.allowRegex) assertEqualOrMatching(expected, this.versionableActual, this.ctx) else assertEquals(expected, this.versionableActual, this.ctx) return this } return new Proxy(immediateAssertion, { get: (target, prop) => (this as any)[prop] }) } get throws(): unknown { const result = callAssertedFunction(this.unversionedActual as Function) this.ctx.versionableActual = getThrownMessage(result, this.ctx) this.ctx.allowRegex = true this.ctx.defaultExpected = "" return this.immediateOrChained() } throwsAndHasTypeError(matchValue: string | RegExp): void { assertEqualOrMatching( matchValue, getThrownMessage( callAssertedFunction(this.unversionedActual as Function), this.ctx ), this.ctx ) if (!this.ctx.cfg.skipTypes) { assertEqualOrMatching( matchValue, new TypeAssertionMapping(data => ({ actual: data.errors.join("\n") })), this.ctx ) } } get completions(): any { if (this.ctx.cfg.skipTypes) return chainableNoOpProxy this.ctx.versionableActual = new TypeAssertionMapping(data => { if (typeof data.completions === "string") { // if the completions were ambiguously defined, e.g. two string // literals with the same value, they are writen as an error // message to the JSON. Throw it immediately. throw new Error(data.completions) } return { actual: data.completions } }) this.ctx.lastSnapName = "completions" return this.snap } get jsdoc(): any { if (this.ctx.cfg.skipTypes) return chainableNoOpProxy this.ctx.versionableActual = new TypeAssertionMapping(data => ({ actual: formatTypeString(data.jsdoc ?? "") })) this.ctx.allowRegex = true return this.immediateOrChained() } get type(): any { if (this.ctx.cfg.skipTypes) return chainableNoOpProxy // We need to bind this to return an object with getters const self = this return { get toString() { self.ctx.versionableActual = new TypeAssertionMapping(data => ({ actual: formatTypeString(data.args[0].type) })) self.ctx.allowRegex = true return self.immediateOrChained() }, get errors() { self.ctx.versionableActual = new TypeAssertionMapping(data => ({ actual: data.errors.join("\n") })) self.ctx.allowRegex = true return self.immediateOrChained() }, get completions() { return self.completions } } } } const declarationPrefix = "type T = " const formatTypeString = (typeString: string) => prettier .format(`${declarationPrefix}${typeString}`, { semi: false, printWidth: 60, trailingComma: "none", parser: "typescript", ...getConfig().typeToStringFormat }) .slice(declarationPrefix.length) .trimEnd() export type AssertionKind = "value" | "type" export type rootAssertions = valueAssertions< t, kind > & TypeAssertionsRoot export type valueAssertions< t, kind extends AssertionKind > = comparableValueAssertion & ([t] extends [() => unknown] ? functionAssertions : {}) export type nextAssertions = "type" extends kind ? TypeAssertionsRoot : {} export type inferredAssertions< argsType extends [value: any, ...rest: any[]], kind extends AssertionKind, chained = argsType[0] > = rootAssertions & ((...args: Args) => nextAssertions) export type ChainContext = { allowRegex?: boolean defaultExpected?: unknown } export type functionAssertions = { throws: inferredAssertions<[message: string | RegExp], kind, string> } & ("type" extends kind ? { throwsAndHasTypeError: (message: string | RegExp) => undefined } : {}) export type valueFromTypeAssertion< expected, chained = expected > = inferredAssertions<[expected: expected], "value", chained> type snapProperty = { (expected?: snapshot): nextAssertions toFile: ( id: string, options?: ExternalSnapshotOptions ) => nextAssertions unwrap: Unwrapper } export type Unwrapper = (opts?: UnwrapOptions) => expected export const nonOverlappingSatisfiesMessage = "This type has no overlap with your satisfies constraint" export type nonOverlappingSatisfiesMessage = typeof nonOverlappingSatisfiesMessage type validateExpectedOverlaps = isDisjoint extends true ? ErrorType : unknown export type comparableValueAssertion = { snap: snapProperty equals: (value: expected) => nextAssertions instanceOf: (constructor: Constructor) => nextAssertions is: (value: expected) => nextAssertions completions: CompletionsSnap jsdoc: comparableValueAssertion satisfies: ( def: type.validate & validateExpectedOverlaps> ) => nextAssertions // This can be used to assert values without type constraints unknown: Omit, "unknown"> unwrap: Unwrapper } export interface CompletionsSnap { (value?: Completions): void unwrap: Unwrapper } export type TypeAssertionsRoot = { type: TypeAssertionProps } export type TypeAssertionProps = { toString: valueFromTypeAssertion errors: valueFromTypeAssertion completions: CompletionsSnap } export type ExternalSnapshotOptions = { path?: string } ================================================ FILE: ark/attest/bench/await1k.ts ================================================ export const await1K = async (fn: () => Promise): Promise => { await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() await fn() } ================================================ FILE: ark/attest/bench/baseline.ts ================================================ import { snapshot, throwInternalError } from "@ark/util" import process from "node:process" import { throwAssertionError } from "../assert/assertions.ts" import { queueSnapshotUpdate, writeSnapshotUpdatesOnExit } from "../cache/snapshots.ts" import type { BenchContext } from "./bench.ts" import { stringifyMeasure, type MarkMeasure, type Measure, type MeasureComparison } from "./measure.ts" export const queueBaselineUpdateIfNeeded = ( updated: Measure | MarkMeasure, baseline: Measure | MarkMeasure | undefined, ctx: BenchContext ): void => { // If we already have a baseline and the user didn't pass an update flag, do nothing if (baseline && !ctx.cfg.updateSnapshots) return const serializedValue = snapshot(updated) if (!ctx.lastSnapCallPosition) { throwInternalError( `Unable to update baseline for ${ctx.qualifiedName} ('lastSnapCallPosition' was unset)` ) } if (!ctx.lastSnapFunctionName) { throwInternalError( `Unable to update baseline for ${ctx.qualifiedName} ('lastSnapFunctionName' was unset)` ) } queueSnapshotUpdate({ position: ctx.lastSnapCallPosition, serializedValue, snapFunctionName: ctx.lastSnapFunctionName, baselinePath: ctx.qualifiedPath }) if (ctx.benchCallPosition) writeSnapshotUpdatesOnExit() } /** Pretty print comparison and set the process.exitCode to 1 if delta threshold is exceeded */ export const compareToBaseline = ( result: MeasureComparison, ctx: BenchContext ): void => { console.log(`⛳ Result: ${stringifyMeasure(result.updated)}`) if (result.baseline && !ctx.cfg.updateSnapshots) { console.log(`🎯 Baseline: ${stringifyMeasure(result.baseline)}`) const delta = ((result.updated[0] - result.baseline[0]) / result.baseline[0]) * 100 const formattedDelta = `${delta.toFixed(2)}%` if (delta > ctx.cfg.benchPercentThreshold) handlePositiveDelta(formattedDelta, ctx) else if (delta < -ctx.cfg.benchPercentThreshold) handleNegativeDelta(formattedDelta, ctx) else console.log(`📊 Delta: ${delta > 0 ? "+" : ""}${formattedDelta}`) // add an extra newline console.log() } } const handlePositiveDelta = (formattedDelta: string, ctx: BenchContext) => { const message = `'${ctx.qualifiedName}' exceeded baseline by ${formattedDelta} (threshold is ${ctx.cfg.benchPercentThreshold}%).` console.error(`📈 ${message}`) const benchErrorConfig = ctx.cfg.benchErrorOnThresholdExceeded const isTypeBench = ctx.lastSnapFunctionName === "instantiations" const shouldError = benchErrorConfig === true || (isTypeBench ? benchErrorConfig === "types" : benchErrorConfig === "runtime") if (shouldError) { const errorSummary = `❌ ${message}` if (ctx.lastSnapFunctionName === "instantiations") throwAssertionError({ stack: ctx.assertionStack, message: errorSummary }) else { process.exitCode = 1 // Summarize failures at the end of output process.on("exit", () => { console.error(errorSummary) }) } } } const handleNegativeDelta = (formattedDelta: string, ctx: BenchContext) => { console.log( // Remove the leading negative when formatting our delta `📉 ${ctx.qualifiedName} was under baseline by ${formattedDelta.slice( 1 )}! Consider setting a new baseline.` ) } ================================================ FILE: ark/attest/bench/bench.ts ================================================ import { caller, getCallStack, rmRf, type SourcePosition } from "@ark/fs" import { performance } from "node:perf_hooks" import { ensureCacheDirs, getConfig, type ParsedAttestConfig } from "../config.ts" import { chainableNoOpProxy } from "../utils.ts" import { await1K } from "./await1k.ts" import { compareToBaseline, queueBaselineUpdateIfNeeded } from "./baseline.ts" import { call1K } from "./call1k.ts" import { createTimeComparison, createTimeMeasure, type MarkMeasure, type Measure, type TimeUnit } from "./measure.ts" import { createBenchTypeAssertion, type BenchTypeAssertions } from "./type.ts" export type StatName = keyof typeof stats export type TimeAssertionName = StatName | "mark" let benchHasRun = false type BenchFn = ( name: string, fn: fn, options?: BenchOptions ) => InitialBenchAssertions export interface Bench extends BenchFn { baseline: (baselineExpressions: () => T) => void } const benchFn: BenchFn = (name, fn, options) => { const qualifiedPath = [...currentSuitePath, name] console.log(`🏌️ ${qualifiedPath.join("/")}`) const ctx = getBenchCtx( qualifiedPath, fn.constructor.name === "AsyncFunction", options ) if (!benchHasRun) { rmRf(ctx.cfg.cacheDir) ensureCacheDirs() benchHasRun = true } ctx.benchCallPosition = caller() if ( typeof ctx.cfg.filter === "string" && !qualifiedPath.includes(ctx.cfg.filter) ) return chainableNoOpProxy else if ( Array.isArray(ctx.cfg.filter) && ctx.cfg.filter.some((segment, i) => segment !== qualifiedPath[i]) ) return chainableNoOpProxy const assertions = new BenchAssertions(fn, ctx) Object.assign(assertions, createBenchTypeAssertion(ctx)) return assertions as never } export const bench: Bench = Object.assign(benchFn, { baseline: () => {} }) export const stats = { mean: (callTimes: number[]): number => { const totalCallMs = callTimes.reduce((sum, duration) => sum + duration, 0) return totalCallMs / callTimes.length }, median: (callTimes: number[]): number => { const middleIndex = Math.floor(callTimes.length / 2) const ms = callTimes.length % 2 === 0 ? (callTimes[middleIndex - 1] + callTimes[middleIndex]) / 2 : callTimes[middleIndex] return ms } } class ResultCollector { results: number[] = [] private benchStart = performance.now() private bounds: Required private lastInvocationStart: number private ctx: BenchContext constructor(ctx: BenchContext) { this.ctx = ctx // By default, will run for either 5 seconds or 100_000 call sets (of 1000 calls), whichever comes first this.bounds = { ms: 5000, count: 100_000, ...ctx.options.until } this.lastInvocationStart = -1 } start() { this.ctx.options.hooks?.beforeCall?.() this.lastInvocationStart = performance.now() } stop() { this.results.push((performance.now() - this.lastInvocationStart) / 1000) this.ctx.options.hooks?.afterCall?.() } done() { const metMsTarget = performance.now() - this.benchStart >= this.bounds.ms const metCountTarget = this.results.length >= this.bounds.count return metMsTarget || metCountTarget } } const loopCalls = (fn: () => void, ctx: BenchContext) => { const collector = new ResultCollector(ctx) while (!collector.done()) { collector.start() // we use a function like this to make 1k explicit calls to the function // to avoid certain optimizations V8 makes when looping call1K(fn) collector.stop() } return collector.results } const loopAsyncCalls = async (fn: () => Promise, ctx: BenchContext) => { const collector = new ResultCollector(ctx) while (!collector.done()) { collector.start() await await1K(fn) collector.stop() } return collector.results } export class BenchAssertions< Fn extends BenchableFunction, NextAssertions = BenchTypeAssertions, ReturnedAssertions = Fn extends () => Promise ? Promise : NextAssertions > { private label: string private lastCallTimes: number[] | undefined private fn: Fn private ctx: BenchContext constructor(fn: Fn, ctx: BenchContext) { this.fn = fn this.ctx = ctx this.label = `Call: ${ctx.qualifiedName}` } private applyCallTimeHooks() { if (this.ctx.options.fakeCallMs !== undefined) { const fakeMs = this.ctx.options.fakeCallMs === "count" ? this.lastCallTimes!.length : this.ctx.options.fakeCallMs this.lastCallTimes = this.lastCallTimes!.map(() => fakeMs) } } private callTimesSync() { if (!this.lastCallTimes) { this.lastCallTimes = loopCalls(this.fn as never, this.ctx) this.lastCallTimes.sort() } this.applyCallTimeHooks() return this.lastCallTimes } private async callTimesAsync() { if (!this.lastCallTimes) { this.lastCallTimes = await loopAsyncCalls(this.fn as never, this.ctx) this.lastCallTimes.sort() } this.applyCallTimeHooks() return this.lastCallTimes } private createAssertion( name: Name, baseline: Name extends "mark" ? Record> | undefined : Measure | undefined, callTimes: number[] ) { if (name === "mark") return this.markAssertion(baseline as never, callTimes) const ms: number = stats[name as StatName](callTimes) const comparison = createTimeComparison(ms, baseline as Measure) console.group(`${this.label} (${name}):`) compareToBaseline(comparison, this.ctx) console.groupEnd() queueBaselineUpdateIfNeeded(createTimeMeasure(ms), baseline, { ...this.ctx, lastSnapFunctionName: name }) return this.getNextAssertions() } private markAssertion( baseline: MarkMeasure | undefined, callTimes: number[] ) { console.group(`${this.label}:`) const markEntries: [StatName, Measure | undefined][] = ( baseline ? Object.entries(baseline) // If nothing was passed, gather all available baselines by setting their values to undefined. : Object.entries(stats).map(([kind]) => [kind, undefined])) as never const markResults = Object.fromEntries( markEntries.map(([kind, kindBaseline]) => { console.group(kind) const ms = stats[kind](callTimes) const comparison = createTimeComparison(ms, kindBaseline) compareToBaseline(comparison, this.ctx) console.groupEnd() return [kind, comparison.updated] }) ) console.groupEnd() queueBaselineUpdateIfNeeded(markResults, baseline, { ...this.ctx, lastSnapFunctionName: "mark" }) return this.getNextAssertions() } private getNextAssertions(): NextAssertions { return createBenchTypeAssertion(this.ctx) as never } private createStatMethod( name: Name, baseline: Name extends "mark" ? Record> | undefined : Measure | undefined ) { if (this.ctx.isAsync) { return new Promise(resolve => { this.callTimesAsync().then( callTimes => { resolve(this.createAssertion(name, baseline, callTimes)) }, e => { this.addUnhandledBenchException(e) resolve(chainableNoOpProxy) } ) }) } let assertions = chainableNoOpProxy try { assertions = this.createAssertion(name, baseline, this.callTimesSync()) } catch (e) { this.addUnhandledBenchException(e) } return assertions } private addUnhandledBenchException(reason: unknown) { const message = `Bench ${ this.ctx.qualifiedName } threw during execution:\n${String(reason)}` console.error(message) unhandledExceptionMessages.push(message) } median(baseline?: Measure): ReturnedAssertions { this.ctx.lastSnapCallPosition = caller() const assertions = this.createStatMethod("median", baseline) return assertions } mean(baseline?: Measure): ReturnedAssertions { this.ctx.lastSnapCallPosition = caller() return this.createStatMethod("mean", baseline) } mark(baseline?: MarkMeasure): ReturnedAssertions { this.ctx.lastSnapCallPosition = caller() return this.createStatMethod("mark", baseline as never) } } const unhandledExceptionMessages: string[] = [] export type UntilOptions = { ms?: number count?: number } export type BaseBenchOptions = { until?: UntilOptions } export type BenchOptions = BaseBenchOptions & { hooks?: { beforeCall?: () => void afterCall?: () => void } } export type InternalBenchOptions = BenchOptions & { fakeCallMs?: number | "count" } export type BenchContext = { qualifiedPath: string[] qualifiedName: string options: InternalBenchOptions cfg: ParsedAttestConfig assertionStack: string benchCallPosition: SourcePosition lastSnapCallPosition: SourcePosition | undefined lastSnapFunctionName: string | undefined isAsync: boolean } export type BenchableFunction = () => unknown | Promise export type InitialBenchAssertions = BenchAssertions & BenchTypeAssertions const currentSuitePath: string[] = [] process.on("beforeExit", () => { if (unhandledExceptionMessages.length) { console.error( `${unhandledExceptionMessages.length} unhandled exception(s) occurred during your benches (see details above).` ) process.exit(1) } }) export const getBenchCtx = ( qualifiedPath: string[], isAsync: boolean = false, options: BenchOptions = {} ): BenchContext => ({ qualifiedPath, qualifiedName: qualifiedPath.join("/"), options, cfg: getConfig(), benchCallPosition: caller(), lastSnapCallPosition: undefined, lastSnapFunctionName: undefined, isAsync, assertionStack: getCallStack({ offset: 1 }).join("\n") }) ================================================ FILE: ark/attest/bench/call1k.ts ================================================ export const call1K = (fn: () => void): void => { fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() fn() } ================================================ FILE: ark/attest/bench/measure.ts ================================================ import type { StatName } from "./bench.ts" type MeasureUnit = TimeUnit | TypeUnit export type Measure = [ value: number, unit: Unit ] export type MeasureComparison = { updated: Measure baseline: Measure | undefined } export type MarkMeasure = Partial> export const stringifyMeasure = ([value, units]: Measure): string => units in timeUnitRatios ? stringifyTimeMeasure([value, units as TimeUnit]) : `${value} ${units}` export const TYPE_UNITS = ["instantiations"] as const export type TypeUnit = (typeof TYPE_UNITS)[number] export const createTypeComparison = ( value: number, baseline: Measure | undefined ): MeasureComparison => ({ updated: [value, "instantiations"], baseline }) export const timeUnitRatios = { ns: 0.000_001, us: 0.001, ms: 1, s: 1000 } export type TimeUnit = keyof typeof timeUnitRatios export const stringifyTimeMeasure = ([ value, unit ]: Measure): string => `${value.toFixed(2)}${unit}` const convertTimeUnit = (n: number, from: TimeUnit, to: TimeUnit) => round((n * timeUnitRatios[from]) / timeUnitRatios[to], 2) /** * Establish a new baseline using the most appropriate time unit */ export const createTimeMeasure = (ms: number): Measure => { let bestMatch: Measure | undefined for (const u in timeUnitRatios) { const candidateMeasure = createTimeMeasureForUnit(ms, u as TimeUnit) if (!bestMatch) bestMatch = candidateMeasure else if (bestMatch[0] >= 1) { if (candidateMeasure[0] >= 1 && candidateMeasure[0] < bestMatch[0]) bestMatch = candidateMeasure } else if (candidateMeasure[0] >= bestMatch[0]) bestMatch = candidateMeasure } return bestMatch! } const createTimeMeasureForUnit = ( ms: number, unit: TimeUnit ): Measure => [convertTimeUnit(ms, "ms", unit), unit] const round = (value: number, decimalPlaces: number) => Math.round(value * 10 ** decimalPlaces) / 10 ** decimalPlaces export const createTimeComparison = ( ms: number, baseline: Measure | undefined ): MeasureComparison => { if (baseline) { return { updated: [convertTimeUnit(ms, "ms", baseline[1]), baseline[1]], baseline } } return { updated: createTimeMeasure(ms), baseline: undefined } } ================================================ FILE: ark/attest/bench/type.ts ================================================ import { caller } from "@ark/fs" import { throwInternalError } from "@ark/util" import ts from "typescript" import { getBenchAssertionsAtPosition } from "../cache/getCachedAssertions.ts" import { TsServer, getAbsolutePosition, getAncestors, getDescendants, nearestCallExpressionChild } from "../cache/ts.ts" import { getCallExpressionsByName, getInstantiationsContributedByNode } from "../cache/utils.ts" import { getConfig } from "../config.ts" import { compareToBaseline, queueBaselineUpdateIfNeeded } from "./baseline.ts" import type { BenchContext } from "./bench.ts" import { createTypeComparison, type Measure, type MeasureComparison, type TypeUnit } from "./measure.ts" export type BenchTypeAssertions = { types: (instantiations?: Measure) => void } export const createBenchTypeAssertion = ( ctx: BenchContext ): BenchTypeAssertions => ({ types: (...args: [instantiations?: Measure | undefined]) => { ctx.lastSnapCallPosition = caller() instantiationDataHandler({ ...ctx, lastSnapFunctionName: "types" }, args[0]) } }) export const getContributedInstantiations = (ctx: BenchContext): number => { const testDeclarationAliases = getConfig().testDeclarationAliases const instance = TsServer.instance const file = instance.getSourceFileOrThrow(ctx.benchCallPosition.file) const node = nearestCallExpressionChild( file, getAbsolutePosition(file, ctx.benchCallPosition) ) const firstMatchingNamedCall = getAncestors(node).find( call => getCallExpressionsByName(call, testDeclarationAliases).length ) if (!firstMatchingNamedCall) { throw new Error( `No call expressions matching the name(s) '${testDeclarationAliases.join()}' were found` ) } const body = getDescendants(firstMatchingNamedCall).find( node => ts.isArrowFunction(node) || ts.isFunctionExpression(node) ) as ts.ArrowFunction | ts.FunctionExpression | undefined if (!body) throwInternalError("Unable to retrieve contents of the call expression") return getInstantiationsContributedByNode(file, body) } export const instantiationDataHandler = ( ctx: BenchContext, args?: Measure, isBenchFunction = true ): void => { const instantiationsContributed = isBenchFunction ? getContributedInstantiations(ctx) : getBenchAssertionsAtPosition(ctx.benchCallPosition)[0][1].count const comparison: MeasureComparison = createTypeComparison( instantiationsContributed, args ) compareToBaseline(comparison, ctx) queueBaselineUpdateIfNeeded(comparison.updated, args, ctx) } ================================================ FILE: ark/attest/cache/getCachedAssertions.ts ================================================ import { readJson, type LinePosition, type SourcePosition } from "@ark/fs" import { existsSync, readdirSync } from "node:fs" import { join } from "node:path" import { getConfig } from "../config.ts" import { getFileKey } from "../utils.ts" import type { AssertionsByFile, LinePositionRange, TypeAssertionData, TypeAssertionKind } from "./writeAssertionCache.ts" export type VersionedAssertionsByFile = [ tsVersion: string, relationshipAssertions: AssertionsByFile, benchAssertions: AssertionsByFile ] let assertionEntries: VersionedAssertionsByFile[] | undefined export const getCachedAssertionEntries = (): VersionedAssertionsByFile[] => { if (!assertionEntries) { const config = getConfig() if (!existsSync(config.assertionCacheDir)) throwMissingAssertionDataError(config.assertionCacheDir) const assertionFiles = readdirSync(config.assertionCacheDir) const relationshipAssertions: AssertionsByFile = {} const benchAssertions: AssertionsByFile = {} assertionEntries = assertionFiles.map(file => { const data = readJson(join(config.assertionCacheDir, file)) as Record< string, TypeAssertionData[] > for (const fileName of Object.keys(data)) { const relationshipAssertionData = data[fileName].filter( (entry: TypeAssertionData) => "args" in entry ) const benchAssertionData = data[fileName].filter( (entry: TypeAssertionData) => "count" in entry ) relationshipAssertions[fileName] = relationshipAssertionData benchAssertions[fileName] = benchAssertionData } return [ // remove .json extension file.slice(0, -5), relationshipAssertions, benchAssertions ] }) } return assertionEntries! } const throwMissingAssertionDataError = (location: string) => { throw new Error( `Unable to find precached assertion data at '${location}'. ` + `Ensure the 'setup' function from @ark/attest has been called before running your tests.` ) } const isPositionWithinRange = ( { line, char }: LinePosition, { start, end }: LinePositionRange ) => { if (line < start.line || line > end.line) return false if (line === start.line) return char >= start.char if (line === end.line) return char <= end.char return true } export type VersionedTypeAssertion< kind extends TypeAssertionKind = TypeAssertionKind > = [tsVersion: string, assertionData: TypeAssertionData] const getAssertionsOfKindAtPosition = ( position: SourcePosition, kind: kind ): VersionedTypeAssertion[] => { const fileKey = getFileKey(position.file) return getCachedAssertionEntries().map( ([version, typeRelationshipAssertions, BenchAssertionAssertions]) => { const assertions = kind === "type" ? typeRelationshipAssertions : BenchAssertionAssertions if (!assertions[fileKey]) { throw new Error( `Found no assertion data for '${fileKey}' for TypeScript version ${version}.` ) } const matchingAssertion = assertions[fileKey].find(assertion => /** * Depending on the environment, a trace can refer to any of these points * attest(...) * ^ ^ ^ * Because of this, it's safest to check if the call came from anywhere in the expected range. * */ isPositionWithinRange(position, assertion.location) ) if (!matchingAssertion) { throw new Error( `Found no assertion for TypeScript version ${version} at line ${position.line} char ${position.char} in '${fileKey}'. Are sourcemaps enabled and working properly?` ) } return [version, matchingAssertion] as VersionedTypeAssertion } ) } export const getTypeAssertionsAtPosition = ( position: SourcePosition ): VersionedTypeAssertion<"type">[] => getAssertionsOfKindAtPosition(position, "type") export const getBenchAssertionsAtPosition = ( position: SourcePosition ): VersionedTypeAssertion<"bench">[] => getAssertionsOfKindAtPosition(position, "bench") ================================================ FILE: ark/attest/cache/snapshots.ts ================================================ import { filePath, positionToString, readFile, readJson, shell, writeFile, writeJson, type SourcePosition } from "@ark/fs" import { throwInternalError } from "@ark/util" import { existsSync } from "node:fs" import { basename, dirname, isAbsolute, join } from "node:path" import type ts from "typescript" import { getConfig } from "../config.ts" import { getFileKey } from "../utils.ts" import { TsServer, getAbsolutePosition, nearestCallExpressionChild } from "./ts.ts" import { getCallExpressionsByName } from "./utils.ts" export type SnapshotArgs = { position: SourcePosition serializedValue: unknown snapFunctionName?: string baselinePath?: string[] } export const resolveSnapshotPath = ( testFile: string, customPath: string | undefined ): string => { if (customPath && isAbsolute(customPath)) return customPath return join(dirname(testFile), customPath ?? "assert.snapshots.json") } export const getSnapshotByName = ( file: string, name: string, customPath: string | undefined ): object => { const snapshotPath = resolveSnapshotPath(file, customPath) return (readJson(snapshotPath)?.[basename(file)] as any)?.[name] } /** * Writes the update and position to cacheDir, which will eventually be read and copied to the source * file by a cleanup process after all tests have completed. */ export const queueSnapshotUpdate = (args: SnapshotArgs): void => { const config = getConfig() const path = config.defaultAssertionCachePath if (existsSync(path)) { const existing = readJson(path) writeJson(path, { ...existing, updates: Array.isArray(existing.updates) ? [...existing.updates, args] : [args] }) } else writeJson(path, { updates: [args] }) } export type QueuedUpdate = { position: SourcePosition snapCall: ts.CallExpression snapFunctionName: string newArgText: string baselinePath: string[] | undefined } export type ExternalSnapshotArgs = SnapshotArgs & { name: string customPath: string | undefined } const findCallExpressionAncestor = ( position: SourcePosition, functionName: string ): ts.CallExpression => { const server = TsServer.instance const file = server.getSourceFileOrThrow(position.file) const absolutePosition = getAbsolutePosition(file, position) const startNode = nearestCallExpressionChild(file, absolutePosition) const calls = getCallExpressionsByName(startNode, [functionName], true) if (calls.length) return startNode throwInternalError( `Unable to locate expected inline ${functionName} call from assertion at ${positionToString( position )}.` ) } export const updateExternalSnapshot = ({ serializedValue: value, position, name, customPath }: ExternalSnapshotArgs): void => { const snapshotPath = resolveSnapshotPath(position.file, customPath) const snapshotData = readJson(snapshotPath) ?? {} const fileKey = basename(position.file) snapshotData[fileKey] = { ...(snapshotData[fileKey] as object), [name]: value } writeJson(snapshotPath, snapshotData) } let snapshotsWillBeWritten = false export const writeSnapshotUpdatesOnExit = (): void => { if (snapshotsWillBeWritten) return process.on("exit", writeCachedInlineSnapshotUpdates) snapshotsWillBeWritten = true } const writeCachedInlineSnapshotUpdates = () => { const config = getConfig() let snapshotData: SnapshotArgs[] | undefined if (!existsSync(config.defaultAssertionCachePath)) return try { snapshotData = readJson(config.defaultAssertionCachePath).updates as never } catch { // If we can't read the snapshot, log an error and move onto the next update console.error( `Unable to read snapshot data from expected location ${config.defaultAssertionCachePath}.` ) } if (snapshotData) { try { writeUpdates( snapshotData.map(snapshot => snapshotArgsToQueuedUpdate(snapshot)) ) } catch (error) { // If writeInlineSnapshotToFile throws an error, log it and move on to the next update console.error(String(error)) } } } const snapshotArgsToQueuedUpdate = ({ position, serializedValue, snapFunctionName = "snap", baselinePath }: SnapshotArgs): QueuedUpdate => { const snapCall = findCallExpressionAncestor(position, snapFunctionName) let newArgText = typeof serializedValue === "string" && serializedValue.includes("\n") ? "`" + serializedValue.replace(/`/g, "\\`").replace(/\${/g, "\\${") + "`" : JSON.stringify(serializedValue) newArgText = newArgText .replace(/"\$ark.bigint-(-?\d+)"/g, "$1n") .replace(/"\$ark.undefined"/g, "undefined") return { position, snapCall, snapFunctionName, newArgText, baselinePath } } // Waiting until process exit to write snapshots avoids invalidating existing source positions export const writeUpdates = (queuedUpdates: QueuedUpdate[]): void => { if (!queuedUpdates.length) return const updatesByFile: Record = {} for (const update of queuedUpdates) { updatesByFile[update.position.file] ??= [] updatesByFile[update.position.file].push(update) } for (const k in updatesByFile) { writeFileUpdates( k, updatesByFile[k].sort((l, r) => l.position.line > r.position.line ? 1 : r.position.line > l.position.line ? -1 : l.position.char - r.position.char ) ) } runFormatterIfAvailable(queuedUpdates) } const runFormatterIfAvailable = (queuedUpdates: QueuedUpdate[]) => { const { formatCmd: formatter, shouldFormat } = getConfig() if (!shouldFormat) return try { const updatedPaths = [ ...new Set( queuedUpdates.map(update => filePath(update.snapCall.getSourceFile().fileName) ) ) ] shell(`${formatter} ${updatedPaths.join(" ")}`) } catch { // If formatter is unavailable or skipped, do nothing. } } const writeFileUpdates = (path: string, updates: QueuedUpdate[]) => { let fileText = readFile(path) let offSet = 0 for (const update of updates) { const previousArgTextLength = update.snapCall.arguments.end - update.snapCall.arguments.pos fileText = fileText.slice(0, update.snapCall.arguments.pos + offSet) + update.newArgText + fileText.slice(update.snapCall.arguments.end + offSet) offSet += update.newArgText.length - previousArgTextLength summarizeSnapUpdate(update.snapCall.arguments, update) } writeFile(path, fileText) } const summarizeSnapUpdate = ( originalArgs: ts.NodeArray, update: QueuedUpdate ) => { let updateSummary = `${ originalArgs.length ? "🆙 Updated" : "📸 Established" } ` updateSummary += update.baselinePath ? `baseline '${update.baselinePath.join("/")}' ` : `snap at ${getFileKey(update.position.file)}:${update.position.line} ` const previousValue = update.snapCall.arguments[0]?.getText() updateSummary += previousValue ? `from ${previousValue} to ` : `${update.baselinePath ? "at" : "as"} ` updateSummary += update.newArgText console.log(updateSummary) } ================================================ FILE: ark/attest/cache/ts.ts ================================================ import { fromCwd, readFile, type SourcePosition } from "@ark/fs" import { printable, throwError, throwInternalError, type dict } from "@ark/util" import * as tsvfs from "@typescript/vfs" import { readFileSync } from "node:fs" import { resolve, dirname, join } from "node:path" import ts from "typescript" import { getConfig } from "../config.ts" export class TsServer { rootFiles!: string[] virtualEnv!: tsvfs.VirtualTypeScriptEnvironment program!: ts.Program private static _instance: TsServer | null = null static get instance(): TsServer { return new TsServer() } private tsConfigInfo!: TsconfigInfo constructor(tsConfigInfo?: TsconfigInfo) { if (TsServer._instance) return TsServer._instance this.tsConfigInfo = tsConfigInfo ?? getTsConfigInfoOrThrow() const tsLibPaths = getTsLibFiles(this.tsConfigInfo.parsed.options) // TS represents windows paths as `C:/Users/ssalb/...` const normalizedCwd = fromCwd().replace(/\\/g, "/") this.rootFiles = this.tsConfigInfo.parsed.fileNames.filter(path => { if (!path.startsWith(normalizedCwd)) return // exclude empty files as they lead to a crash // when createVirtualTypeScriptEnvironment is called const contents = readFile(path).trim() return contents !== "" }) const system = tsvfs.createFSBackedSystem( tsLibPaths.defaultMapFromNodeModules, this.tsConfigInfo.path ? dirname(this.tsConfigInfo.path) : fromCwd(), ts ) this.virtualEnv = tsvfs.createVirtualTypeScriptEnvironment( system, this.rootFiles, ts, this.tsConfigInfo.parsed.options ) this.program = this.virtualEnv.languageService.getProgram()! TsServer._instance = this } getSourceFileOrThrow(path: string): ts.SourceFile { const tsPath = path.replace(/\\/g, "/") const existingFile = this.virtualEnv.getSourceFile(tsPath) if (existingFile) return existingFile if (!this.virtualEnv.sys.fileExists(tsPath)) { throwInternalError( `@ark/attest: TypeScript was unable to resolve expected file at ${tsPath}.\n` ) } const contents = this.virtualEnv.sys.readFile(tsPath) if (!contents) { throwInternalError( `@ark/attest: TypeScript says a file exists at ${tsPath}, but was unable to read its contents.\n` ) } this.virtualEnv.createFile(tsPath, contents) const createdFile = this.virtualEnv.getSourceFile(tsPath) if (!createdFile) { throwInternalError( `@ark/attest: TypeScript tried to create a file at ${tsPath} but was unable to access it.` ) } return createdFile } } export const nearestCallExpressionChild = ( node: ts.Node, position: number ): ts.CallExpression => { const result = nearestBoundingCallExpression(node, position) if (!result) { throwInternalError( `Unable to find bounding call expression at position ${position} in ${ node.getSourceFile().fileName }` ) } return result } export const nearestBoundingCallExpression = ( node: ts.Node, position: number ): ts.CallExpression | undefined => node.pos <= position && node.end >= position ? (node .getChildren() .flatMap( child => nearestBoundingCallExpression(child, position) ?? [] )[0] ?? (ts.isCallExpression(node) ? node : undefined)) : undefined export const getAbsolutePosition = ( file: ts.SourceFile, position: SourcePosition ): number => { const pos = ts.getPositionOfLineAndCharacter( file, // TS uses 0-based line and char #s position.line - 1, position.char - 1 ) if (!pos) { throwInternalError( `Absolute position ${printable(position)} does not exist in ${file.fileName}` ) } return pos } export type TsconfigInfo = { path: string | undefined parsed: ts.ParsedCommandLine } export const getTsConfigInfoOrThrow = (): TsconfigInfo => { const config = getConfig() const tsconfig = config.tsconfig let instantiatedConfig: ts.ParsedCommandLine | undefined let configFilePath: string | undefined if (tsconfig !== null) { configFilePath = tsconfig ?? ts.findConfigFile(fromCwd(), ts.sys.fileExists, "tsconfig.json") if (configFilePath) instantiatedConfig = instantiateTsconfigFromPath(configFilePath) } instantiatedConfig ??= instantiateNoFileConfig() return { path: configFilePath, parsed: instantiatedConfig } } type RawTsConfigJson = dict & { compilerOptions: ts.CompilerOptions } type InstantiatedTsConfigJson = ts.ParsedCommandLine const instantiateNoFileConfig = (): InstantiatedTsConfigJson => { const arkConfig = getConfig() const instantiatedConfig = ts.parseJsonConfigFileContent( { compilerOptions: arkConfig.compilerOptions }, ts.sys, fromCwd() ) if (instantiatedConfig.errors.length > 0) throwConfigInstantiationError(instantiatedConfig) return instantiatedConfig } const instantiateTsconfigFromPath = ( path: string ): InstantiatedTsConfigJson => { const arkConfig = getConfig() const configFileText = readFileSync(path).toString() const result = ts.parseConfigFileTextToJson(path, configFileText) if (result.error) throwConfigParseError(result.error) const rawConfig: RawTsConfigJson = result.config rawConfig.compilerOptions = Object.assign( rawConfig.compilerOptions ?? {}, arkConfig.compilerOptions ) const configPath = resolve(path) const instantiatedConfig = ts.parseJsonConfigFileContent( rawConfig, ts.sys, dirname(configPath), {}, configPath ) if (instantiatedConfig.errors.length > 0) throwConfigInstantiationError(instantiatedConfig) return instantiatedConfig } const defaultDiagnosticHost: ts.FormatDiagnosticsHost = { getCanonicalFileName: fileName => fileName, getCurrentDirectory: process.cwd, getNewLine: () => ts.sys.newLine } const throwConfigParseError = (error: ts.Diagnostic) => throwError(ts.formatDiagnostics([error], defaultDiagnosticHost)) const throwConfigInstantiationError = ( instantiatedConfig: InstantiatedTsConfigJson ): never => throwError( ts.formatDiagnostics(instantiatedConfig.errors, defaultDiagnosticHost) ) type TsLibFiles = { defaultMapFromNodeModules: Map resolvedPaths: string[] } export const getTsLibFiles = ( tsconfigOptions: ts.CompilerOptions ): TsLibFiles => { const defaultMapFromNodeModules = tsvfs.createDefaultMapFromNodeModules(tsconfigOptions) const libPath = dirname(ts.getDefaultLibFilePath(tsconfigOptions)) return { defaultMapFromNodeModules, resolvedPaths: [...defaultMapFromNodeModules.keys()].map(path => join(libPath, path) ) } } export const getProgram = ( env?: tsvfs.VirtualTypeScriptEnvironment ): ts.Program => env?.languageService.getProgram() ?? TsServer.instance.virtualEnv.languageService.getProgram()! export interface InternalTypeChecker extends ts.TypeChecker { // These APIs are not publicly exposed getInstantiationCount: () => number isTypeAssignableTo: (source: ts.Type, target: ts.Type) => boolean getDiagnostics: () => ts.Diagnostic[] } export const getInternalTypeChecker = ( env?: tsvfs.VirtualTypeScriptEnvironment ): InternalTypeChecker => getProgram(env).getTypeChecker() as InternalTypeChecker export interface StringifiableType extends ts.Type { toString(): string isUnresolvable: boolean } export const getStringifiableType = (node: ts.Node): StringifiableType => { const typeChecker = getInternalTypeChecker() const nodeType = typeChecker.getTypeAtLocation(node) let stringified = typeChecker.typeToString(nodeType) if (stringified.includes("...")) { const nonTruncated = typeChecker.typeToString( nodeType, undefined, ts.TypeFormatFlags.NoTruncation ) stringified = nonTruncated.includes(" any") && !stringified.includes(" any") ? nonTruncated.replace(/ any/g, " cyclic") : nonTruncated } return Object.assign(nodeType, { toString: () => stringified, isUnresolvable: (nodeType as any).intrinsicName === "error" }) } export type ArgumentTypes = { args: StringifiableType[] typeArgs: StringifiableType[] } export const extractArgumentTypesFromCall = ( call: ts.CallExpression ): ArgumentTypes => ({ args: call.arguments.map(arg => getStringifiableType(arg)), typeArgs: call.typeArguments?.map(typeArg => getStringifiableType(typeArg)) ?? [] }) export const getDescendants = (node: ts.Node): ts.Node[] => getDescendantsRecurse(node) const getDescendantsRecurse = (node: ts.Node): ts.Node[] => [ node, ...node.getChildren().flatMap(child => getDescendantsRecurse(child)) ] export const getAncestors = (node: ts.Node): ts.Node[] => { const ancestors: ts.Node[] = [] let baseNode = node.parent while (baseNode.parent !== undefined) { ancestors.push(baseNode) baseNode = baseNode.parent } return ancestors } export const getFirstAncestorByKindOrThrow = ( node: ts.Node, kind: ts.SyntaxKind ): ts.Node => getAncestors(node).find(ancestor => ancestor.kind === kind) ?? throwInternalError( `Could not find an ancestor of kind ${ts.SyntaxKind[kind]}` ) ================================================ FILE: ark/attest/cache/utils.ts ================================================ import { filePath } from "@ark/fs" import { throwInternalError } from "@ark/util" import * as tsvfs from "@typescript/vfs" import ts from "typescript" import { getConfig } from "../config.ts" import { getFileKey } from "../utils.ts" import { getDescendants, getFirstAncestorByKindOrThrow, getProgram, getTsConfigInfoOrThrow, getTsLibFiles } from "./ts.ts" import type { LinePositionRange } from "./writeAssertionCache.ts" export const getCallLocationFromCallExpression = ( callExpression: ts.CallExpression ): LinePositionRange => { const start = ts.getLineAndCharacterOfPosition( callExpression.getSourceFile(), callExpression.getStart() ) const end = ts.getLineAndCharacterOfPosition( callExpression.getSourceFile(), callExpression.getEnd() ) // Add 1 to everything, since trace positions are 1-based and TS positions are 0-based. const location: LinePositionRange = { start: { line: start.line + 1, char: start.character + 1 }, end: { line: end.line + 1, char: end.character + 1 } } return location } /** * Processes inline instantiations from an attest call * Preserves any JSDoc comments that are associated with the original expression */ export const gatherInlineInstantiationData = ( file: ts.SourceFile, assertionsByFile: Record, instantiationMethodCalls: string[] ): void => { const expressions = getCallExpressionsByName(file, instantiationMethodCalls) if (!expressions.length) return const enclosingFunctions = expressions.map(expression => { const attestInstantiationsExpression = getFirstAncestorByKindOrThrow( expression, ts.SyntaxKind.ExpressionStatement ) return { ancestor: getFirstAncestorByKindOrThrow( attestInstantiationsExpression, ts.SyntaxKind.ExpressionStatement ), position: getCallLocationFromCallExpression(expression) } }) const instantiationInfo = enclosingFunctions.map(enclosingFunction => { const body = getDescendants(enclosingFunction.ancestor).find( node => ts.isArrowFunction(node) || ts.isFunctionExpression(node) ) as ts.ArrowFunction | ts.FunctionExpression | undefined if (!body) { throwInternalError( `Unable to resolve source associated with TS Node: ${enclosingFunction.ancestor.getText()}` ) } return { location: enclosingFunction.position, count: getInstantiationsContributedByNode(file, body) } }) const assertions = assertionsByFile[getFileKey(file.fileName)] ?? [] assertionsByFile[getFileKey(file.fileName)] = [ ...assertions, ...instantiationInfo ] } export const getCallExpressionsByName = ( startNode: ts.Node, names: string[], isSnapCall = false ): ts.CallExpression[] => { const calls: ts.CallExpression[] = [] for (const descendant of getDescendants(startNode)) { if (ts.isCallExpression(descendant)) { if (names.includes(descendant.expression.getText()) || !names.length) calls.push(descendant) } else if (isSnapCall) { if (ts.isIdentifier(descendant)) { if (names.includes(descendant.getText()) || !names.length) calls.push(descendant as any as ts.CallExpression) } } } return calls } const instantiationsByPath: { [path: string]: number } = {} export const getInstantiationsContributedByNode = ( file: ts.SourceFile, benchBlock: ts.FunctionExpression | ts.ArrowFunction ): number => { const originalPath = filePath(file.fileName) const fakePath = originalPath + ".nonexistent.ts" const baselineFile = getBaselineSourceFile(file) const baselineFileWithBenchBlock = baselineFile + `\nconst $attestIsolatedBench = ${benchBlock.getFullText()}` if (!instantiationsByPath[fakePath]) { const instantiationsWithoutNode = getInstantiationsWithFile( baselineFile, fakePath ) instantiationsByPath[fakePath] = instantiationsWithoutNode } const instantiationsWithNode = getInstantiationsWithFile( baselineFileWithBenchBlock, fakePath ) return instantiationsWithNode - instantiationsByPath[fakePath] } export const createOrUpdateFile = ( env: tsvfs.VirtualTypeScriptEnvironment, fileName: string, fileText: string ): ts.SourceFile | undefined => { if (env.sys.fileExists(fileName)) env.updateFile(fileName, fileText) else env.createFile(fileName, fileText) return env.getSourceFile(fileName) } declare module "typescript" { interface SourceFile { imports: ts.StringLiteral[] } interface Program { getResolvedModuleFromModuleSpecifier( moduleSpecifier: ts.StringLiteralLike, sourceFile?: ts.SourceFile ): ts.ResolvedModuleWithFailedLookupLocations } } const getInstantiationsWithFile = (fileText: string, fileName: string) => { const env = getIsolatedEnv() const file = createOrUpdateFile(env, fileName, fileText) const program = getProgram(env) // trigger type checking to generate instantiations // (was previously program.emit(file), but that as of TS 5.6 that doesn't // work, so this may need to change if instantiations is reported as 0 after // a future TypeScript update) program.getSemanticDiagnostics(file) // this may lead to additional type checking per Jake Bailey from the TS // team, although it doesn't currently affect any of our internal benchmarks program.getDeclarationDiagnostics(file) const count = program.getInstantiationCount() return count } let virtualEnv: tsvfs.VirtualTypeScriptEnvironment | undefined = undefined export const getIsolatedEnv = (): tsvfs.VirtualTypeScriptEnvironment => { if (virtualEnv !== undefined) return virtualEnv const tsconfigInfo = getTsConfigInfoOrThrow() const libFiles = getTsLibFiles(tsconfigInfo.parsed.options) const projectRoot = process.cwd() const system = tsvfs.createFSBackedSystem( libFiles.defaultMapFromNodeModules, projectRoot, ts ) virtualEnv = tsvfs.createVirtualTypeScriptEnvironment( system, [], ts, tsconfigInfo.parsed.options ) return virtualEnv } const getBaselineSourceFile = (originalFile: ts.SourceFile): string => { const functionNames = getConfig().testDeclarationAliases const calls = getCallExpressionsByName(originalFile, functionNames) let baselineSourceFileText = originalFile.getFullText() // for each test function like `it` or `bench`, walk up the AST to find the complete expression for (const call of calls) { let currentNode: ts.Node = call // ensure we capture the entire chain like bench(...).types(...) while (currentNode.parent && !ts.isExpressionStatement(currentNode)) currentNode = currentNode.parent const fullExpressionText = currentNode.getFullText() baselineSourceFileText = baselineSourceFileText.replace( fullExpressionText, "" ) } return baselineSourceFileText } ================================================ FILE: ark/attest/cache/writeAssertionCache.ts ================================================ import type { LinePosition } from "@ark/fs" import { flatMorph } from "@ark/util" import ts from "typescript" import { getConfig } from "../config.ts" import { getFileKey } from "../utils.ts" import { TsServer, extractArgumentTypesFromCall, getDescendants, getInternalTypeChecker, type ArgumentTypes, type StringifiableType } from "./ts.ts" import { gatherInlineInstantiationData, getCallExpressionsByName, getCallLocationFromCallExpression } from "./utils.ts" export type AssertionsByFile = Record export const analyzeProjectAssertions = (): AssertionsByFile => { const config = getConfig() const instance = TsServer.instance const filePaths = instance.rootFiles const diagnosticsByFile = getDiagnosticsByFile() const assertionsByFile: AssertionsByFile = {} const attestAliasInstantiationMethodCalls = config.attestAliases.map( alias => `${alias}.instantiations` ) for (const path of filePaths) { const file = instance.getSourceFileOrThrow(path) const assertionsInFile = getAssertionsInFile( file, diagnosticsByFile, config.attestAliases ) if (assertionsInFile.length) assertionsByFile[getFileKey(file.fileName)] = assertionsInFile if (!config.skipInlineInstantiations) { gatherInlineInstantiationData( file, assertionsByFile, attestAliasInstantiationMethodCalls ) } } return assertionsByFile } export const getAssertionsInFile = ( file: ts.SourceFile, diagnosticsByFile: DiagnosticsByFile, attestAliases: string[] ): TypeAssertionData[] => { const assertCalls = getCallExpressionsByName(file, attestAliases) return assertCalls.map(call => analyzeAssertCall(call, diagnosticsByFile)) } export const analyzeAssertCall = ( assertCall: ts.CallExpression, diagnosticsByFile: DiagnosticsByFile ): TypeAssertionData => { const types = extractArgumentTypesFromCall(assertCall) const location = getCallLocationFromCallExpression(assertCall) const args = types.args.map(arg => serializeArg(arg, types)) const typeArgs = types.typeArgs.map(typeArg => serializeArg(typeArg, types)) const errors = checkDiagnosticMessages(assertCall, diagnosticsByFile) const completions = getCompletions(assertCall) // Extract JSDoc comment for first argument if available const jsdoc = extractJSDocFromArgument(assertCall) const result: TypeAssertionData = { location, args, typeArgs, errors, completions } if (jsdoc) result.jsdoc = jsdoc return result } /** * Extract JSDoc comments associated with the first argument of a call expression */ const extractJSDocFromArgument = ( callExpr: ts.CallExpression ): string | undefined => { // We're only interested in the first argument const firstArg = callExpr.arguments[0] if (!firstArg) return undefined const checker = getInternalTypeChecker() // If the argument is a property access expression (e.g., out.foo) if (ts.isPropertyAccessExpression(firstArg)) { // Try to find the symbol for the property const propSymbol = checker.getSymbolAtLocation(firstArg) if (propSymbol) { // Get JSDoc from property declarations return getJSDocFromSymbol(propSymbol) } } // If argument is an identifier, try to find its declaration's JSDoc else if (ts.isIdentifier(firstArg)) { const symbol = checker.getSymbolAtLocation(firstArg) if (symbol) return getJSDocFromSymbol(symbol) } return undefined } /** * Extract JSDoc comments from a symbol's declarations */ const getJSDocFromSymbol = (symbol: ts.Symbol): string | undefined => { // Get JSDoc directly from the symbol if possible const symbolDocumentation = ts.displayPartsToString( symbol.getDocumentationComment(getInternalTypeChecker()) ) if (symbolDocumentation) return symbolDocumentation.trim() // If no symbol documentation, try to get JSDoc from declarations const declarations = symbol.getDeclarations() || [] for (const declaration of declarations) { // For property declarations in object literals, get the JSDoc comment if ( ts.isPropertyAssignment(declaration) || ts.isShorthandPropertyAssignment(declaration) || ts.isPropertyDeclaration(declaration) ) { const jsDocTags = ts.getJSDocTags(declaration) if (jsDocTags.length > 0) { return jsDocTags .map(tag => { const comment = tag.comment?.toString() || "" return tag.tagName.text + (comment ? ` ${comment}` : "") }) .join("\n") } // Try to get JSDoc comment before the property const jsDocComments = ts.getJSDocCommentsAndTags(declaration) if (jsDocComments && jsDocComments.length > 0) { return jsDocComments .map(doc => { if (ts.isJSDoc(doc)) return doc.comment || "" return "" }) .filter(Boolean) .join("\n") .trim() } } } return undefined } const serializeArg = ( arg: StringifiableType, context: ArgumentTypes ): ArgAssertionData => ({ type: arg.toString(), relationships: { args: context.args.map(other => compareTsTypes(arg, other)), typeArgs: context.typeArgs.map(other => compareTsTypes(arg, other)) } }) export type Completions = Record | string const getCompletions = (attestCall: ts.CallExpression) => { const arg = attestCall.arguments[0] if (arg === undefined) return {} const descendants = getDescendants(arg) const file = attestCall.getSourceFile() const text = file.getFullText() const completions: Completions | string = {} for (const descendant of descendants) { if (ts.isStringLiteral(descendant) || ts.isTemplateLiteral(descendant)) { // descendant.pos tends to be an open quote while d.end tends to be right after the closing quote. // It seems to be more consistent using this to get the pos for the completion over descendant.pos const lastPositionOfInnerString = descendant.end - (/["'`]/.test(text[descendant.end - 1]) ? 1 : 2) const completionData = TsServer.instance.virtualEnv.languageService.getCompletionsAtPosition( file.fileName, lastPositionOfInnerString, undefined ) const prefix = "text" in descendant ? descendant.text : descendant.getText() const entries = completionData?.entries ?? [] if (prefix in completions) return `Encountered multiple completion candidates for string(s) '${prefix}'. Assertions on the same prefix must be split into multiple attest calls so the results can be distinguished.` completions[prefix] = [] for (const entry of entries) { if (entry.name.startsWith(prefix) && entry.name.length > prefix.length) completions[prefix].push(entry.name) } } } return flatMorph(completions, (prefix, entries) => entries.length >= 1 ? [prefix, entries.sort()] : [] ) } export type DiagnosticData = { start: number end: number message: string } export type DiagnosticsByFile = Record export const getDiagnosticsByFile = (): DiagnosticsByFile => { const diagnosticsByFile: DiagnosticsByFile = {} const diagnostics: ts.Diagnostic[] = getInternalTypeChecker().getDiagnostics() for (const diagnostic of diagnostics) addDiagnosticDataFrom(diagnostic, diagnosticsByFile) return diagnosticsByFile } const addDiagnosticDataFrom = ( diagnostic: ts.Diagnostic, diagnosticsByFile: DiagnosticsByFile ) => { const filePath = diagnostic.file?.fileName if (!filePath) return const fileKey = getFileKey(filePath) const start = diagnostic.start ?? -1 const end = start + (diagnostic.length ?? 0) let message = diagnostic.messageText if (typeof message === "object") message = concatenateChainedErrors([message]) const data: DiagnosticData = { start, end, message } if (diagnosticsByFile[fileKey]) diagnosticsByFile[fileKey].push(data) else diagnosticsByFile[fileKey] = [data] } const concatenateChainedErrors = ( diagnostics: ts.DiagnosticMessageChain[] ): string => diagnostics .map( msg => `${msg.messageText}${ msg.next ? concatenateChainedErrors(msg.next) : "" }` ) .join("\n") export type ArgAssertionData = { type: string relationships: { args: TypeRelationship[] typeArgs: TypeRelationship[] } } export type TypeRelationshipAssertionData = { location: LinePositionRange args: ArgAssertionData[] typeArgs: ArgAssertionData[] errors: string[] completions: Completions /** JSDoc comment for the first argument, if any */ jsdoc?: string } export type TypeBenchmarkingAssertionData = { location: LinePositionRange count: number } export type TypeAssertionKind = "bench" | "type" export type TypeAssertionData< kind extends TypeAssertionKind = TypeAssertionKind > = kind extends "bench" ? TypeBenchmarkingAssertionData : TypeRelationshipAssertionData export type LinePositionRange = { start: LinePosition end: LinePosition } export type TypeRelationship = "subtype" | "supertype" | "equality" | "none" export const compareTsTypes = ( l: StringifiableType, r: StringifiableType ): TypeRelationship => { const lString = l.toString() const rString = r.toString() // Ensure two unresolvable types are not treated as equivalent if (l.isUnresolvable || r.isUnresolvable) return "none" // Treat `any` as a supertype of every other type if (lString === "any") return rString === "any" ? "equality" : "supertype" if (rString === "any") return "subtype" // Otherwise, determine if the types are equivalent by checking mutual assignability const checker = getInternalTypeChecker() const isSubtype = checker.isTypeAssignableTo(l, r) const isSupertype = checker.isTypeAssignableTo(r, l) return ( isSubtype ? isSupertype ? "equality" : "subtype" : isSupertype ? "supertype" : "none" ) } export const checkDiagnosticMessages = ( attestCall: ts.CallExpression, diagnosticsByFile: DiagnosticsByFile ): string[] => { const fileKey = getFileKey(attestCall.getSourceFile().fileName) const fileDiagnostics = diagnosticsByFile[fileKey] if (!fileDiagnostics) return [] const diagnosticMessagesInArgRange: string[] = [] for (const diagnostic of fileDiagnostics) { if ( diagnostic.start >= attestCall.getStart() && diagnostic.end <= attestCall.getEnd() ) diagnosticMessagesInArgRange.push(diagnostic.message) } return diagnosticMessagesInArgRange } ================================================ FILE: ark/attest/cli/cli.ts ================================================ #!/usr/bin/env node import { fileName } from "@ark/fs" import { basename } from "node:path" import { precache } from "./precache.ts" import { stats } from "./stats.ts" import { trace } from "./trace.ts" const subcommands = { precache, trace, stats } type Subcommand = keyof typeof subcommands const baseFileName = basename(fileName()) const thisFileIndex = process.argv.findIndex( // if running from build output in npm, will be a file called `attest` // if running from build output in pnpm, will be `cli.js` in build output s => s.endsWith(baseFileName) || s.endsWith("attest") ) if (thisFileIndex === -1) throw new Error(`Expected to find an argument ending with "${baseFileName}"`) const subcommand = process.argv[thisFileIndex + 1] if (!(subcommand in subcommands)) { console.error( `Expected a command like 'attest ', where is one of:\n${Object.keys( subcommands )}` ) process.exit(1) } const args = process.argv.slice(thisFileIndex + 2) subcommands[subcommand as Subcommand](args) ================================================ FILE: ark/attest/cli/precache.ts ================================================ import { ensureDir } from "@ark/fs" import { join } from "node:path" import { writeAssertionData } from "../fixtures.ts" export const precache = (args: string[]): void => { const cacheFileToWrite = args[0] ?? join(ensureDir(".attest"), "typescript.json") writeAssertionData(cacheFileToWrite) } ================================================ FILE: ark/attest/cli/shared.ts ================================================ export const baseDiagnosticTscCmd = "npm exec -- tsc --noEmit --extendedDiagnostics --incremental false --tsBuildInfoFile null" ================================================ FILE: ark/attest/cli/stats.ts ================================================ import { execSync } from "node:child_process" import { baseDiagnosticTscCmd } from "./shared.ts" export const stats = (args: string[]): void => { const packageDirs = args.length ? args : [process.cwd()] const listedStats = packageDirs.map((packageDir): TypePerfStats => { console.log(`⏳ Gathering type perf data for ${packageDir}...`) let output: string try { output = execSync(baseDiagnosticTscCmd, { cwd: packageDir, stdio: "pipe" }).toString() } catch (e: any) { output = e.stdout?.toString() ?? "" output += e.stderr?.toString() ?? "" console.error( `❗Encountered one or more errors checking types for ${packageDir}- results may be inaccurate❗` ) } const stats = parseTsDiagnosticsOutput(output) logTypePerfStats(stats) return stats }) const aggregatedStats = listedStats.reduce( (aggregatedStats, packageStats) => ({ checkTime: aggregatedStats.checkTime + packageStats.checkTime, types: aggregatedStats.types + packageStats.types, instantiations: aggregatedStats.instantiations + packageStats.instantiations }), { checkTime: 0, types: 0, instantiations: 0 } ) console.log("📊 aggregated type performance:") logTypePerfStats(aggregatedStats) } type TypePerfStats = { checkTime: number types: number instantiations: number } const parseTsDiagnosticsOutput = (output: string): TypePerfStats => { const lines = output.split("\n") const results: TypePerfStats = { checkTime: 0, types: 0, instantiations: 0 } for (const line of lines) { if (line.startsWith("Check time:")) results.checkTime = parseFloat(line.split(":")[1].trim()) else if (line.startsWith("Types:")) results.types = parseInt(line.split(":")[1].trim(), 10) else if (line.startsWith("Instantiations:")) results.instantiations = parseInt(line.split(":")[1].trim(), 10) } return results } const logTypePerfStats = (stats: TypePerfStats) => { console.log(JSON.stringify(stats, null, 4)) } ================================================ FILE: ark/attest/cli/trace.ts ================================================ import { ensureDir, getShellOutput, readJson, writeFile } from "@ark/fs" import type { ExecException } from "node:child_process" import { existsSync } from "node:fs" import { basename, join, relative, resolve } from "node:path" import ts from "typescript" import { TsServer, getDescendants, getStringifiableType, nearestBoundingCallExpression } from "../cache/ts.ts" import { getConfig } from "../config.ts" import { baseDiagnosticTscCmd } from "./shared.ts" interface TraceEntry { pid: number tid: number ph: string // Phase (e.g., "X" for Complete Event) cat: string // Category ts: number // Timestamp (microseconds) name: string // Name of the event dur?: number // Duration (microseconds) args: { kind?: number pos?: number end?: number path?: string sourceId?: number targetId?: number [key: string]: any } } interface CallRange { id: string typeId: string functionName: string callSite: string startTime: number // microseconds endTime: number // microseconds duration: number // microseconds children: CallRange[] selfTime: number // microseconds } interface CallSiteDetail { path: string pos: number end: number selfTime: number // microseconds } interface FunctionStats { typeId: string functionName: string totalTime: number // microseconds selfTime: number // microseconds count: number firstLocation?: string detailedCallSites: CallSiteDetail[] } interface AnalysisContext { traceDir: string tsServer: TsServer traceEntries: TraceEntry[] durationEntries: TraceEntry[] // Filtered entries with duration callRanges: CallRange[] rootCalls: CallRange[] functionStats: Record allFunctions: FunctionStats[] } interface UngroupedCallStats { typeId: string functionName: string callSite: string duration: number // microseconds selfTime: number // microseconds } /** * Helper to write output to both console and collect for file output */ const outputCapture = (() => { let buffer: string[] = [] return { write: (text: string) => { console.log(text) buffer.push(text) }, getBuffer: () => buffer.join("\n"), getLines: () => [...buffer], clear: () => { buffer = [] } } })() /** * Formats microseconds to a string of milliseconds. */ const formatMillis = (ms: number, fractionDigits = 2): string => (ms / 1000).toFixed(fractionDigits) /** * Formats microseconds to a padded string of milliseconds. */ const formatPaddedMillis = ( ms: number, pad: number, fractionDigits = 2 ): string => formatMillis(ms, fractionDigits).padStart(pad) // Helper to format duration in seconds to a human-readable string const formatSeconds = (totalSeconds: number): string => { if (totalSeconds < 0) totalSeconds = 0 const hours = Math.floor(totalSeconds / 3600) const minutes = Math.floor((totalSeconds % 3600) / 60) const seconds = Math.floor(totalSeconds % 60) if (hours > 0) return `about ${hours} hour(s)` if (minutes > 0) return `about ${minutes} minute(s)` return `${seconds}s` } class ProgressDisplay { private totalItems: number private processedItems: number = 0 private startTime: number = 0 private timerId?: NodeJS.Timeout private barWidth: number = 30 // Width of the progress bar itself constructor(totalItems: number) { this.totalItems = totalItems if (this.totalItems <= 0) this.totalItems = 0 } start(): void { if (this.totalItems === 0) return this.startTime = Date.now() this.processedItems = 0 this.render() // Initial render // Update every second for elapsed time and ETA this.timerId = setInterval(() => this.render(), 1000) } update(processedCount: number): void { if (this.totalItems === 0) return this.processedItems = Math.min(processedCount, this.totalItems) this.render() // Re-render on each item processed for immediate feedback if (this.processedItems >= this.totalItems) this.stop() // Automatically stop if all items are processed } private render(): void { if (this.totalItems === 0 && this.processedItems === 0 && !this.startTime) return // Avoid rendering if not started for 0 items const percent = this.totalItems > 0 ? Math.min(100, (this.processedItems / this.totalItems) * 100) : 100 const filledLength = Math.floor((this.barWidth * percent) / 100) const emptyLength = this.barWidth - filledLength const bar = `[${"=".repeat(filledLength)}${" ".repeat(emptyLength)}]` const elapsedMs = Date.now() - this.startTime const elapsedSec = elapsedMs / 1000 const formattedElapsed = formatSeconds(elapsedSec) let etaStr = "" if (this.processedItems > 0 && this.processedItems < this.totalItems) { const avgTimePerItemMs = elapsedMs / this.processedItems const remainingItems = this.totalItems - this.processedItems const etaMs = remainingItems * avgTimePerItemMs etaStr = ` (ETA: ${formatSeconds(etaMs / 1000)})` } else if (this.processedItems >= this.totalItems && this.totalItems > 0) etaStr = " (Done)" const summary = `${this.processedItems}/${this.totalItems} entries (${percent.toFixed(0)}%)` const timeInfo = `${formattedElapsed} elapsed${etaStr}` process.stdout.clearLine(0) // Clear the current line process.stdout.cursorTo(0) // Move cursor to the beginning of the line process.stdout.write(`Processing: ${bar} ${summary} | ${timeInfo}`) } stop(): void { if (this.timerId) { clearInterval(this.timerId) this.timerId = undefined as never } if (this.totalItems > 0) { this.processedItems = this.totalItems // Ensure final state is 100% this.render() // Final render process.stdout.write("\n") // Move to the next line } } } export const trace = async (args: string[]): Promise => { const packageDir = resolve(args[0] ?? process.cwd()) const config = getConfig() if (!config.tsconfig) { // This message should go to console.error and also be captured if needed, // but since it exits, direct console.error is fine. console.error( `attest trace must be run from a directory with a tsconfig.json file` ) process.exit(1) } const traceDir = resolve(config.cacheDir, "trace") ensureDir(traceDir) outputCapture.clear() const initialMessages: string[] = [] initialMessages.push(`⏳ Gathering type trace data for ${packageDir}...`) outputCapture.write(initialMessages[0]) // This goes to console and buffer const tracingOutput = generateTraceData(traceDir, config.tsconfig, packageDir) const traceFile = join(traceDir, "trace.json") if (!existsSync(traceFile)) { outputCapture.write( `❌ No trace data found (expected a file at ${traceFile}). TSC output:\n${tracingOutput}` ) const summaryPath = join(traceDir, "summary.txt") writeFile(summaryPath, outputCapture.getBuffer()) return } // This message will be followed by the progress bar on the next line outputCapture.write(`⏳ Analyzing type trace data for ${packageDir}...`) analyzeTypeInstantiations(traceDir) // This function now handles its own progress display // Collect all messages for the summary file // The progress bar output is not part of outputCapture.getLines() const analysisMessages = outputCapture .getLines() .slice(initialMessages.length + 1) // +1 for the "Analyzing..." message const summaryContent = [ ...initialMessages, tracingOutput, outputCapture.getLines()[initialMessages.length], // The "Analyzing..." message ...analysisMessages ].join("\n") const summaryPath = join(traceDir, "summary.txt") writeFile(summaryPath, summaryContent) } const generateTraceData = ( traceDir: string, tsconfigPath: string, packageDir: string ): string => { try { const output = getShellOutput( `${baseDiagnosticTscCmd} --project ${tsconfigPath} --generateTrace ${traceDir}`, { cwd: packageDir } ) process.stdout.write(output) // Display tsc output directly return output } catch (error: any) { const e: ExecException = error const output = e.stdout ?? "" const errorOutput = e.stderr ?? "" process.stdout.write(output) process.stderr.write(errorOutput) return `${output}\n${errorOutput}` } } const initializeAnalysisContext = (traceDir: string): AnalysisContext => { const tsServer = TsServer.instance const tracePath = join(traceDir, "trace.json") if (!existsSync(tracePath)) { throw new Error( `Critical: Expected a trace file at ${tracePath}, but it was not found during context initialization.` ) } const traceEntries: TraceEntry[] = readJson(tracePath) as never return { traceDir, tsServer, traceEntries, durationEntries: [], callRanges: [], rootCalls: [], functionStats: {}, allFunctions: [] } } const filterDurationEntries = (ctx: AnalysisContext): void => { ctx.durationEntries = ctx.traceEntries.filter(entry => { const { args, dur, ph } = entry return ( ph === "X" && typeof dur === "number" && args && typeof args.path === "string" && typeof args.pos === "number" && typeof args.end === "number" ) }) // This message goes to outputCapture for the summary file outputCapture.write( `Found ${ctx.durationEntries.length} complete event traces with duration, path, and position.` ) } const processDurationEntry = ( entry: TraceEntry, ctx: AnalysisContext ): void => { const entryPath = entry.args.path as string const entryPos = entry.args.pos as number const entryEnd = entry.args.end as number const entryDur = entry.dur as number const sourceFile = ctx.tsServer.getSourceFileOrThrow(entryPath) let callRangeData: { typeId: string; functionName: string } | undefined const callExpr = findCallExpressionInRange(sourceFile, entryPos, entryEnd) if (callExpr) { const functionName = extractFunctionName(callExpr) callRangeData = { typeId: `function-${functionName}`, functionName } } else { const relevantNode = findMostSpecificNodeInRange( sourceFile, entryPos, entryEnd ) if (relevantNode) { const nodeType = getStringifiableType(relevantNode) const nodeKind = ts.SyntaxKind[relevantNode.kind] const typeName = `${nodeKind}: ${nodeType.toString().substring(0, 25)}` const typeNodeIdPart = (nodeType as any).id ?? nodeType.toString().substring(0, 20) callRangeData = { typeId: `node-${relevantNode.kind}-${typeNodeIdPart}`, functionName: typeName } } } if (callRangeData) { ctx.callRanges.push({ id: `${entry.ts}-${entryPos}-${entryEnd}`, typeId: callRangeData.typeId, functionName: callRangeData.functionName, callSite: `${entryPath}:${entryPos}-${entryEnd}`, startTime: entry.ts, endTime: entry.ts + entryDur, duration: entryDur, children: [], selfTime: entryDur }) } else { throw new Error( `Failed to identify a processable AST node for duration entry (name: ${entry.name}, path: ${entryPath}, pos: ${entryPos}-${entryEnd}).` ) } } const findCallExpressionInRange = ( file: ts.SourceFile, start: number, end: number ): ts.CallExpression | undefined => { const boundingCall = nearestBoundingCallExpression(file, start) if (boundingCall && boundingCall.pos >= start && boundingCall.end <= end) return boundingCall const allNodes = getDescendants(file) const nodesInRange = allNodes.filter( node => node.pos >= start && node.end <= end ) const callExpressions = nodesInRange.filter(node => ts.isCallExpression(node) ) as ts.CallExpression[] if (callExpressions.length === 0) return undefined const methodCalls = callExpressions.filter(call => ts.isPropertyAccessExpression(call.expression) ) return methodCalls.length > 0 ? methodCalls[0] : callExpressions[0] } const extractFunctionName = (callExpr: ts.CallExpression): string => { if (ts.isPropertyAccessExpression(callExpr.expression)) return callExpr.expression.name.getText() if (ts.isIdentifier(callExpr.expression)) return callExpr.expression.getText() return "anonymousFunction" } const buildCallTree = (ctx: AnalysisContext): void => { // This message goes to outputCapture for the summary file outputCapture.write( `Building call tree from ${ctx.callRanges.length} ranges...` ) ctx.callRanges.sort((a, b) => a.startTime - b.startTime) const activeCallStack: CallRange[] = [] for (const call of ctx.callRanges) { while ( activeCallStack.length > 0 && activeCallStack[activeCallStack.length - 1].endTime < call.startTime ) activeCallStack.pop() if (activeCallStack.length === 0) ctx.rootCalls.push(call) else { const parent = activeCallStack[activeCallStack.length - 1] parent.children.push(call) } activeCallStack.push(call) } outputCapture.write( `Call tree built with ${ctx.rootCalls.length} root calls.` ) } const calculateSelfTimes = (call: CallRange): number => { let childrenTime = 0 for (const child of call.children) childrenTime += calculateSelfTimes(child) call.selfTime = call.duration - childrenTime if (call.selfTime < 0) call.selfTime = 0 return call.duration } const collectFunctionStats = (call: CallRange, ctx: AnalysisContext): void => { if (!ctx.functionStats[call.typeId]) { ctx.functionStats[call.typeId] = { typeId: call.typeId, functionName: call.functionName, totalTime: 0, selfTime: 0, count: 0, firstLocation: call.callSite, detailedCallSites: [] } } const stats = ctx.functionStats[call.typeId] const [filePath, positionRange] = call.callSite.split(":") if (!filePath || !positionRange) throw new Error(`Invalid callSite format: ${call.callSite}`) const [posStr, endStr] = positionRange.split("-") const pos = parseInt(posStr, 10) const end = parseInt(endStr, 10) if (isNaN(pos) || isNaN(end)) { throw new Error( `Invalid position in callSite: ${call.callSite}. Pos: ${posStr}, End: ${endStr}` ) } stats.detailedCallSites.push({ path: filePath, pos, end, selfTime: call.selfTime }) stats.totalTime += call.duration stats.selfTime += call.selfTime stats.count++ for (const child of call.children) collectFunctionStats(child, ctx) } const sortAndRankFunctions = (ctx: AnalysisContext): void => { outputCapture.write("Sorting and ranking functions...") for (const stats of Object.values(ctx.functionStats)) stats.detailedCallSites.sort((a, b) => b.selfTime - a.selfTime) ctx.allFunctions = Object.values(ctx.functionStats).sort( (a, b) => b.selfTime - a.selfTime ) } const analyzeTypeInstantiations = (traceDir: string): void => { const ctx = initializeAnalysisContext(traceDir) filterDurationEntries(ctx) // Uses outputCapture const totalEntries = ctx.durationEntries.length if (totalEntries === 0) { outputCapture.write("No duration entries with path/pos to process.") outputCapture.write( `\n✅ Analysis complete! No data to export. Summary written to:\n` + ` - ${join(traceDir, "summary.txt")}` ) return } const progressDisplay = new ProgressDisplay(totalEntries) progressDisplay.start() for (const [index, entry] of ctx.durationEntries.entries()) { try { processDurationEntry(entry, ctx) } catch (e: any) { progressDisplay.stop() // Stop progress before printing error via outputCapture outputCapture.write( `\n❌ Error processing entry ${index + 1}/${totalEntries} (Path: ${entry.args.path ?? "N/A"}, Pos: ${entry.args.pos ?? "N/A"}): ${e.message}` ) outputCapture.write("Aborting analysis due to error.") // Early exit, summary will be written by the main trace function return } progressDisplay.update(index + 1) } progressDisplay.stop() // Ensure it's stopped if loop finished // Subsequent messages use outputCapture and will appear after the progress bar buildCallTree(ctx) // Uses outputCapture outputCapture.write("Calculating self-times for call tree nodes...") for (const root of ctx.rootCalls) calculateSelfTimes(root) outputCapture.write("Collecting statistics...") const ungroupedStats = collectUngroupedStats(ctx) // Uses outputCapture for (const root of ctx.rootCalls) collectFunctionStats(root, ctx) sortAndRankFunctions(ctx) // Uses outputCapture outputCapture.write("\n📊 Performance Analysis - Top Individual Calls:\n") displayIndividualSummary(ungroupedStats) // Uses outputCapture outputCapture.write("\n📊 Performance Analysis - Functions by Self Time:\n") displayGroupedSummary(ctx.allFunctions) // Uses outputCapture const rangesCsvPath = join(traceDir, "ranges.csv") const namesCsvPath = join(traceDir, "names.csv") const summaryFilePath = join(traceDir, "summary.txt") // Path already defined in trace() if (ctx.callRanges.length > 0) { outputCapture.write("Exporting CSV reports...") writeToCsv( rangesCsvPath, ["Function Name", "Self (ms)", "Duration (ms)", "Location"], ungroupedStats.map(stat => [ stat.functionName, formatMillis(stat.selfTime, 3), formatMillis(stat.duration, 3), formatLocation(stat.callSite) ]) ) writeToCsv( namesCsvPath, [ "Function Name", "Total Self (ms)", "Avg Self (ms)", "Call Count", "Top Location", "Top Self (ms)" ], ctx.allFunctions.map(stats => { const topUsage = stats.detailedCallSites[0] ?? { path: "unknown", pos: 0, end: 0, selfTime: 0 } return [ stats.functionName, formatMillis(stats.selfTime, 3), formatMillis(stats.selfTime / Math.max(1, stats.count), 3), stats.count.toString(), formatLocation(`${topUsage.path}:${topUsage.pos}-${topUsage.end}`), formatMillis(topUsage.selfTime, 3) ] }) ) outputCapture.write( `\n✅ Analysis complete! Results exported to:\n` + ` - ${rangesCsvPath} (individual calls)\n` + ` - ${namesCsvPath} (grouped by function name)\n` + ` - ${summaryFilePath} (complete analysis report)` ) } else { outputCapture.write( `\n✅ Analysis complete! No call ranges processed to export to CSV. Summary written to:\n` + ` - ${summaryFilePath}` ) } } const collectUngroupedStats = (ctx: AnalysisContext): UngroupedCallStats[] => { outputCapture.write("Collecting and sorting ungrouped call statistics...") const ungroupedStats: UngroupedCallStats[] = [] const flattenCallTree = (call: CallRange): void => { ungroupedStats.push({ typeId: call.typeId, functionName: call.functionName, callSite: call.callSite, duration: call.duration, selfTime: call.selfTime }) for (const child of call.children) flattenCallTree(child) } for (const root of ctx.rootCalls) flattenCallTree(root) return ungroupedStats.sort((a, b) => b.selfTime - a.selfTime) } const displayIndividualSummary = (stats: UngroupedCallStats[]): void => { displayTableHeader(["Rank", "Function Name", "Self (ms)", "Location"]) const topN = Math.min(stats.length, 20) for (let i = 0; i < topN; i++) { const stat = stats[i] const typeNameFormatted = formatTypeName(stat.functionName, 20) const selfTimeMs = formatPaddedMillis(stat.selfTime, 15, 2) const location = formatLocation(stat.callSite) outputCapture.write( `${(i + 1).toString().padStart(4)} | ${typeNameFormatted} | ${selfTimeMs} | ${location}` ) } } const displayTableHeader = (columns: string[]): void => { const headerRow = [ columns[0].padEnd(4), columns[1].padEnd(20), columns[2].padEnd(15), ...columns.slice(3) ].join(" | ") const separatorRow = [ "-".repeat(4), "-".repeat(20), "-".repeat(15), ...columns.slice(3).map(col => "-".repeat(col.length)) ].join("-|-") outputCapture.write(headerRow) outputCapture.write(separatorRow) } const displayGroupedSummary = (functions: FunctionStats[]): void => { displayTableHeader([ "Rank", "Function Name", "Total Self (ms)", "Avg Self (ms)", "Calls", "Top Usage" ]) const topN = Math.min(functions.length, 20) for (let i = 0; i < topN; i++) { const stats = functions[i] const typeNameFormatted = formatTypeName(stats.functionName, 20) const totalTimeMs = formatPaddedMillis(stats.selfTime, 15, 2) const avgTimeMs = formatPaddedMillis( stats.selfTime / Math.max(1, stats.count), 13, 2 ) const calls = stats.count.toString().padStart(5) const topUsage = stats.detailedCallSites[0] ?? { path: "unknown", pos: 0, end: 0, selfTime: 0 } const topUsageTime = formatMillis(topUsage.selfTime, 2) + "ms" const topUsageLocation = formatLocation( `${topUsage.path}:${topUsage.pos}-${topUsage.end}` ) outputCapture.write( `${(i + 1).toString().padStart(4)} | ${typeNameFormatted} | ${totalTimeMs} | ${avgTimeMs} | ${calls} | ${topUsageLocation} (${topUsageTime})` ) } } const formatTypeName = (typeName: string, maxLength: number): string => { typeName = typeName.replace(/\(\)$/, "") if (typeName.length <= maxLength) return typeName.padEnd(maxLength) const charsToKeep = maxLength - 3 const firstPart = Math.ceil(charsToKeep * 0.7) const lastPart = charsToKeep - firstPart return ( typeName.substring(0, firstPart) + "..." + (lastPart > 0 ? typeName.substring(typeName.length - lastPart) : "") ).padEnd(maxLength) } const formatLocation = (location: string): string => { if (!location || location === "unknown" || !location.includes(":")) return location || "unknown" const parts = location.split(":") if (parts.length < 2) return basename(location) const filePath = parts.slice(0, -1).join(":") const positionRange = parts[parts.length - 1] const relativePath = relative(process.cwd(), filePath) const [posStr] = positionRange.split("-") const pos = parseInt(posStr, 10) if (isNaN(pos)) return `${relativePath}:${positionRange}` const sourceFile = TsServer.instance.getSourceFileOrThrow(filePath) const lineAndChar = sourceFile.getLineAndCharacterOfPosition(pos) return `${relativePath}:${lineAndChar.line + 1}:${lineAndChar.character + 1}` } const findMostSpecificNodeInRange = ( file: ts.SourceFile, start: number, end: number ): ts.Node | undefined => { const allNodes = getDescendants(file) const nodesInRange = allNodes.filter( node => node.pos >= start && node.end <= end ) if (nodesInRange.length === 0) return undefined const leafNodes = nodesInRange.filter( node => !nodesInRange.some( other => other !== node && other.pos >= node.pos && other.end <= node.end ) ) return findNodeByPreference(leafNodes.length > 0 ? leafNodes : nodesInRange) } const findNodeByPreference = (nodes: ts.Node[]): ts.Node | undefined => { if (nodes.length === 0) return undefined const typeReference = nodes.find( node => ts.isTypeReferenceNode(node) || ts.isTypeQueryNode(node) ) if (typeReference) return typeReference const declaration = nodes.find( node => ts.isVariableDeclaration(node) || ts.isFunctionDeclaration(node) || ts.isClassDeclaration(node) || ts.isInterfaceDeclaration(node) ) if (declaration) return declaration const propertyAccess = nodes.find(node => ts.isPropertyAccessExpression(node)) if (propertyAccess) return propertyAccess const assignment = nodes.find( node => ts.isBinaryExpression(node) && node.operatorToken.kind === ts.SyntaxKind.EqualsToken ) if (assignment) return assignment const expression = nodes.find( node => ts.isExpressionStatement(node) || ts.isExpression(node) ) if (expression) return expression return nodes.sort((a, b) => a.end - a.pos - (b.end - b.pos))[0] } const writeToCsv = ( filePath: string, headers: string[], rows: string[][] ): void => { const content = [ headers.join(","), ...rows.map(row => row.map(escapeForCsv).join(",")) ].join("\n") writeFile(filePath, content) } const escapeForCsv = (value: string): string => { if (value.includes(",") || value.includes('"') || value.includes("\n")) return `"${value.replace(/"/g, '""')}"` return value } ================================================ FILE: ark/attest/config.ts ================================================ import { ensureDir, fromCwd } from "@ark/fs" import { isArray, liftArray, tryParseNumber, type autocomplete } from "@ark/util" import { existsSync } from "node:fs" import { join, resolve } from "node:path" import type * as prettier from "prettier" import type ts from "typescript" import { findAttestTypeScriptVersions, type TsVersionData } from "./tsVersioning.ts" export type TsVersionAliases = autocomplete<"*"> | string[] export type BenchErrorConfig = "runtime" | "types" | boolean type BaseAttestConfig = { tsconfig: string | null | undefined compilerOptions: ts.CompilerOptions updateSnapshots: boolean failOnMissingSnapshots: boolean /** A string or list of strings representing the TypeScript version aliases to run. * * Aliases must be specified as a package.json dependency or devDependency beginning with "typescript". * Alternate aliases can be specified using the "npm:" prefix: * ```json * "typescript": "latest", * "typescript-next: "npm:typescript@next", * "typescript-1": "npm:typescript@5.2" * "typescript-2": "npm:typescript@5.1" * ``` * * "*" can be pased to run all discovered versions beginning with "typescript". */ tsVersions: TsVersionAliases | TsVersionData[] skipTypes: boolean skipInlineInstantiations: boolean attestAliases: string[] benchPercentThreshold: number benchErrorOnThresholdExceeded: BenchErrorConfig filter: string | undefined testDeclarationAliases: string[] formatCmd: string shouldFormat: boolean /** * Provided options will override the following defaults. * Any options not listed will fallback to Prettier's default value. * * { * semi: false, * printWidth: 60, * trailingComma: "none", * } */ typeToStringFormat: prettier.Options } export type AttestConfig = Partial export const getDefaultAttestConfig = (): BaseAttestConfig => ({ tsconfig: existsSync(fromCwd("tsconfig.json")) ? fromCwd("tsconfig.json") : undefined, compilerOptions: {}, attestAliases: ["attest", "attestInternal"], failOnMissingSnapshots: "CI" in process.env, updateSnapshots: false, skipTypes: false, skipInlineInstantiations: false, tsVersions: "default", benchPercentThreshold: 20, benchErrorOnThresholdExceeded: true, filter: undefined, testDeclarationAliases: ["bench", "it", "test"], formatCmd: `npm exec --no -- prettier --write`, shouldFormat: true, typeToStringFormat: {} }) const flagAliases: { [k in keyof AttestConfig]?: string[] } = { updateSnapshots: ["u", "update"] } const findParamIndex = (flagOrAlias: string) => process.argv.findIndex( arg => arg === `-${flagOrAlias}` || arg === `--${flagOrAlias}` ) const hasFlag = (flag: keyof AttestConfig) => findParamIndex(flag) !== -1 || flagAliases[flag]?.some(alias => findParamIndex(alias) !== -1) const getParamValue = (param: keyof AttestConfig) => { let paramIndex = findParamIndex(param) if (paramIndex === -1) { if (!flagAliases[param]) return for (let i = 0; i < flagAliases[param].length && paramIndex === -1; i++) paramIndex = findParamIndex(flagAliases[param][i]) if (paramIndex === -1) return } const raw = process.argv[paramIndex + 1] if (raw === "true") return true if (raw === "false") return false if (raw === "null") return null if (param === "benchPercentThreshold") return tryParseNumber(raw, { errorOnFail: true }) if (param === "tsVersions" || param === "attestAliases") return raw.split(",") if (param === "typeToStringFormat" || param === "compilerOptions") return JSON.parse(raw) return raw } export const attestEnvPrefix = "ATTEST_" const addEnvConfig = (config: BaseAttestConfig) => { for (const [k, v] of Object.entries(process.env as Record)) { if (k.startsWith(attestEnvPrefix)) { const optionName = k.slice(attestEnvPrefix.length) if (optionName === "CONFIG") Object.assign(config, JSON.parse(v)) else (config as any)[optionName] = JSON.parse(v) } } let k: keyof BaseAttestConfig for (k in config) { if (config[k] === false) config[k] = hasFlag(k) as never else { const value = getParamValue(k) if (value !== undefined) config[k] = value as never } } return config } export interface ParsedAttestConfig extends Readonly { cacheDir: string assertionCacheDir: string defaultAssertionCachePath: string tsVersions: TsVersionData[] } const parseConfig = (): ParsedAttestConfig => { const baseConfig = addEnvConfig(getDefaultAttestConfig()) const cacheDir = resolve(".attest") const assertionCacheDir = join(cacheDir, "assertions") const defaultAssertionCachePath = join(assertionCacheDir, "typescript.json") return Object.assign(baseConfig, { cacheDir, assertionCacheDir, defaultAssertionCachePath, tsVersions: baseConfig.skipTypes ? [] : isTsVersionAliases(baseConfig.tsVersions) ? parseTsVersions(baseConfig.tsVersions) : baseConfig.tsVersions }) } const isTsVersionAliases = ( v: AttestConfig["tsVersions"] ): v is TsVersionAliases => typeof v === "string" || (isArray(v) && typeof v[0] === "string") const parseTsVersions = (aliases: TsVersionAliases): TsVersionData[] => { const versions = findAttestTypeScriptVersions() if (aliases === "*" || (isArray(aliases) && aliases[0] === "*")) return versions return liftArray(aliases).map(alias => { const matching = versions.find(v => v.alias === alias) if (!matching) { throw new Error( `Specified TypeScript version ${alias} does not exist.` + ` It should probably be specified in package.json like: "@ark/attest-ts-${alias}": "npm:typescript@latest"` ) } return matching }) } let cachedConfig: ParsedAttestConfig | undefined export const getConfig = (): ParsedAttestConfig => parseConfig() // workaround for a bug in Node 25 that creates localStorage as an empty proxy, // leading to @typescript/vfs eventually throwing when it sees that it is not // undefined and tries to call `getItem`: // https://github.com/nodejs/node/issues/60303 // this can be removed once the bug is addressed in Node if (!globalThis.localStorage?.getItem) globalThis.localStorage = undefined as never export const ensureCacheDirs = (): void => { cachedConfig ??= getConfig() ensureDir(cachedConfig.cacheDir) ensureDir(cachedConfig.assertionCacheDir) } ================================================ FILE: ark/attest/fixtures.ts ================================================ import { fileName, shell, writeJson } from "@ark/fs" import { rmSync } from "node:fs" import { join } from "node:path" import { writeSnapshotUpdatesOnExit } from "./cache/snapshots.ts" import { analyzeProjectAssertions } from "./cache/writeAssertionCache.ts" import { ensureCacheDirs, getConfig, type AttestConfig } from "./config.ts" import { forTypeScriptVersions } from "./tsVersioning.ts" export const setup = (options?: Partial): typeof teardown => { const { ...config } = getConfig() if (options) Object.assign(config, options) process.env.ATTEST_CONFIG = JSON.stringify(config) rmSync(config.cacheDir, { recursive: true, force: true }) ensureCacheDirs() if (config.skipTypes) return teardown if ( config.tsVersions.length === 1 && config.tsVersions[0].alias === "default" ) writeAssertionData(config.defaultAssertionCachePath) else { forTypeScriptVersions(config.tsVersions, version => { const precachePath = join( config.assertionCacheDir, version.alias + ".json" ) // if we're in our own repo, we need to pnpm to use the root script to execute ts directly if (fileName().endsWith("ts")) shell(`pnpm attest precache ${precachePath}`) // otherwise, just use npm to run the CLI command from build output else shell(`npm exec -c "attest precache ${precachePath}"`) }) } return teardown } export const writeAssertionData = (toPath: string): void => { console.log( "⏳ Waiting for TypeScript to check your project (this may take a while)..." ) writeJson(toPath, analyzeProjectAssertions()) } export const cleanup = (): void => writeSnapshotUpdatesOnExit() /** alias for cleanup to align with vitest and others */ export const teardown: () => void = cleanup ================================================ FILE: ark/attest/index.ts ================================================ export { cleanup, setup, teardown, writeAssertionData } from "./fixtures.ts" // ensure fixtures are exported before config so additional settings can load export { caller, type CallerOfOptions } from "@ark/fs" export { attest } from "./assert/attest.ts" export { bench } from "./bench/bench.ts" export { getBenchAssertionsAtPosition, getTypeAssertionsAtPosition } from "./cache/getCachedAssertions.ts" export type { ArgAssertionData, LinePositionRange, TypeAssertionData, TypeRelationship } from "./cache/writeAssertionCache.ts" export { getDefaultAttestConfig, type AttestConfig } from "./config.ts" export { findAttestTypeScriptVersions, getPrimaryTsVersionUnderTest } from "./tsVersioning.ts" export { contextualize } from "./utils.ts" ================================================ FILE: ark/attest/package.json ================================================ { "name": "@ark/attest", "version": "0.56.0", "license": "MIT", "author": { "name": "David Blass", "email": "david@arktype.io", "url": "https://arktype.io" }, "repository": { "type": "git", "url": "https://github.com/arktypeio/arktype.git", "directory": "ark/attest" }, "type": "module", "main": "./out/index.js", "types": "./out/index.d.ts", "exports": { ".": { "ark-ts": "./index.ts", "default": "./out/index.js" }, "./internal/*.ts": { "ark-ts": "./*.ts", "default": "./out/*.js" }, "./internal/*.js": { "ark-ts": "./*.ts", "default": "./out/*.js" } }, "files": [ "out" ], "bin": { "attest": "out/cli/cli.js" }, "scripts": { "build": "ts ../repo/build.ts", "test": "ts ../repo/testPackage.ts" }, "dependencies": { "@ark/fs": "workspace:*", "@ark/util": "workspace:*", "@prettier/sync": "0.6.1", "@typescript/analyze-trace": "0.10.1", "@typescript/vfs": "1.6.1", "arktype": "workspace:*", "@ark/schema": "workspace:*", "prettier": "3.6.2" }, "devDependencies": { "typescript": "catalog:" }, "peerDependencies": { "typescript": "*" }, "publishConfig": { "access": "public" } } ================================================ FILE: ark/attest/tsVersioning.ts ================================================ import { assertPackageRoot, findPackageAncestors, readJson } from "@ark/fs" import type { Digit } from "@ark/util" import { existsSync, readdirSync, renameSync, statSync, symlinkSync, unlinkSync } from "node:fs" import { join } from "node:path" import ts from "typescript" /** * Executes a provided function for an installed set of TypeScript versions. * * Your primary TypeScript version at node_modules/typescript will be * temporarily renamed to node_modules/typescript-temp, and reset after each * version has been executed, regardless of failures. * * Throws an error if any version fails when the associated function is executed. * * fn should spawn a new process so the new symlinked version can be loaded. */ export const forTypeScriptVersions = ( versions: TsVersionData[], fn: (version: TsVersionData) => void ): void => { const passedVersions: TsVersionData[] = [] const failedVersions: TsVersionData[] = [] const nodeModules = join(assertPackageRoot(process.cwd()), "node_modules") const tsPrimaryPath = join(nodeModules, "typescript") const tsTemporaryPath = join(nodeModules, "typescript-temp") if (existsSync(tsTemporaryPath)) unlinkSync(tsTemporaryPath) if (existsSync(tsPrimaryPath)) renameSync(tsPrimaryPath, tsTemporaryPath) try { for (const version of versions) { const targetPath = version.path === tsPrimaryPath ? tsTemporaryPath : version.path console.log( `⛵ Switching to TypeScript version ${version.alias} (${version.version})...` ) try { if (existsSync(tsPrimaryPath)) unlinkSync(tsPrimaryPath) symlinkSync(targetPath, tsPrimaryPath, "junction") fn(version) passedVersions.push(version) } catch (e) { console.error(e) failedVersions.push(version) } } if (failedVersions.length !== 0) { throw new Error( `❌ The following TypeScript versions threw: ${failedVersions .map(v => `${v.alias} (${v.version})`) .join(", ")}` ) } console.log( `✅ Successfully ran TypeScript versions ${passedVersions .map(v => `${v.alias} (${v.version})`) .join(", ")}` ) } finally { if (existsSync(tsTemporaryPath)) { console.log(`⏮️ Restoring your original TypeScript version...`) unlinkSync(tsPrimaryPath) renameSync(tsTemporaryPath, tsPrimaryPath) } } } export type TsVersionData = { alias: string version: string path: string } const possibleTsVersionPrefix = "typescript-" const strictTsVersionPrefix = "attest-ts-" /** * Determine the alias from the directory name */ const getDirAlias = (dirName: string): string | null => { if (dirName === "typescript") return "default" if (dirName.startsWith(possibleTsVersionPrefix)) return dirName.slice(possibleTsVersionPrefix.length) if (dirName.startsWith(strictTsVersionPrefix)) return dirName.slice(strictTsVersionPrefix.length) return null } /** * Try to get the TypeScript version from a directory */ const getTsVersion = (fullPath: string): string | null => { try { // Try to read package.json for version const packageJsonPath = join(fullPath, "package.json") if (existsSync(packageJsonPath)) { const packageJson = readJson(packageJsonPath) if ( packageJson && packageJson.name === "typescript" && typeof packageJson.version === "string" ) return packageJson.version } // As a fallback, check for the lib/typescript.js file which should exist in all TS installations if (existsSync(join(fullPath, "lib", "typescript.js"))) return "unknown" return null } catch { return null } } /** * Find and return the paths of all installed TypeScript versions by directly scanning * node_modules directories in the current package and all parent packages. * * This function only looks at directories, bypassing package.json entirely. * * @returns {TsVersionData[]} Information about each TypeScript version found */ export const findAttestTypeScriptVersions = (): TsVersionData[] => { const packagePaths = findPackageAncestors(process.cwd()) const versions: TsVersionData[] = [] const foundVersionAliases = new Set() // Check each package's node_modules directory for (const packagePath of packagePaths) { const nodeModulesPath = join(packagePath, "node_modules") if ( !existsSync(nodeModulesPath) || !statSync(nodeModulesPath).isDirectory() ) continue // Check for regular typescript or typescript-* directories const dirNames = readdirSync(nodeModulesPath) for (const dirName of dirNames) { // Skip node_modules/@* directories - we'll handle them below if (dirName.startsWith("@")) continue const fullPath = join(nodeModulesPath, dirName) if (!statSync(fullPath).isDirectory()) continue const alias = getDirAlias(dirName) if (!alias) continue // Skip if we already found this alias in a closer package if (foundVersionAliases.has(alias)) continue const version = getTsVersion(fullPath) if (version) { foundVersionAliases.add(alias) versions.push({ alias, version, path: fullPath }) } } // Check for @ark/attest-ts-* directories const arkDir = join(nodeModulesPath, "@ark") if (existsSync(arkDir) && statSync(arkDir).isDirectory()) { const arkDirs = readdirSync(arkDir) for (const dirName of arkDirs) { if (!dirName.startsWith("attest-ts-")) continue const fullPath = join(arkDir, dirName) if (!statSync(fullPath).isDirectory()) continue const alias = getDirAlias(dirName) if (!alias) continue // Skip if we already found this alias in a closer package if (foundVersionAliases.has(alias)) continue const version = getTsVersion(fullPath) if (version) { foundVersionAliases.add(alias) versions.push({ alias, version, path: fullPath }) } } } } return versions } /** Get the TypeScript version being used by attest as as string like "5.0" * Does not include alternate versions that may be referenced by cache files */ export const getPrimaryTsVersionUnderTest = (): `${Digit}.${Digit}` => ts.versionMajorMinor ================================================ FILE: ark/attest/utils.ts ================================================ import { caller } from "@ark/fs" import { throwError } from "@ark/util" import { basename, relative } from "node:path" export const getFileKey = (path: string): string => relative(".", path) /** * Can be used to allow arbitrarily chained property access and function calls. */ export const chainableNoOpProxy: any = new Proxy(() => chainableNoOpProxy, { get: () => chainableNoOpProxy }) export type ContextualTests = ( it: (name: string, test: (ctx: ctx) => void) => void ) => void export type ContextualizeRoot = { // if this unused ctx type is removed, TS can no longer infer the overloads // eslint-disable-next-line @typescript-eslint/no-unused-vars (tests: () => void, createCtx?: never): void (createCtx: () => ctx, tests: ContextualTests): void } export type ContextualizeEach = ( name: string, createCtx: () => ctx, tests: ContextualTests ) => void export interface Contextualize extends ContextualizeRoot { each: ContextualizeEach } const testDirName = "__tests__" const testSuffix = ".test.ts" const contextualizeRoot: ContextualizeRoot = (first, contextualTests) => { const describe = globalThis.describe if (!describe) { throw new Error( `contextualize cannot be used without a global 'describe' function.` ) } const filePath = caller().file const testsDirChar = filePath.search(testDirName) const suiteNamePath = testsDirChar === -1 ? basename(filePath) : filePath.slice(testsDirChar + testDirName.length + 1) const suiteName = suiteNamePath.slice(0, -testSuffix.length) if (contextualTests) { describe(suiteName, () => contextualTests((name, test) => { it(name, () => test(first() as never)) }) ) } else describe(suiteName, first) } const contextualizeEach: ContextualizeEach = (name, createCtx, tests) => { const describe = globalThis.describe if (!describe) throwNoDescribeError() describe(name, () => tests((name, test) => { it(name, () => test(createCtx())) }) ) } export const contextualize: Contextualize = Object.assign(contextualizeRoot, { each: contextualizeEach }) const throwNoDescribeError = () => throwError( "contextualize cannot be used without a global 'describe' function." ) ================================================ FILE: ark/docs/.turbo/daemon/a6661884d53fb864-turbo.log.2025-10-05 ================================================ ================================================ FILE: ark/docs/README.md ================================================ # my-app This is a Next.js application generated with [Create Fumadocs](https://github.com/fuma-nama/fumadocs). Run development server: ```bash npm run dev # or pnpm dev # or yarn dev ``` Open http://localhost:3000 with your browser to see the result. ## Learn More To learn more about Next.js and Fumadocs, take a look at the following resources: - [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. - [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. - [Fumadocs](https://fumadocs.vercel.app) - learn about Fumadocs ================================================ FILE: ark/docs/app/(home)/layout.tsx ================================================ import { HomeLayout } from "fumadocs-ui/layouts/home" import type { ReactNode } from "react" import { FloatYourBoat } from "../../components/FloatYourBoat.tsx" import { baseOptions } from "../layout.config.tsx" export type LayoutProps = { children: ReactNode } export default ({ children }: LayoutProps): React.ReactElement => ( }} > {children} ) ================================================ FILE: ark/docs/app/(home)/page.tsx ================================================ import { FunnelIcon, LightbulbIcon, MessageCircleWarning, RocketIcon, SearchIcon } from "lucide-react" import { ArkCard, ArkCards } from "../../components/ArkCard.tsx" import { CodeBlock } from "../../components/CodeBlock.tsx" import { Hero } from "../../components/Hero.tsx" import { TsIcon } from "../../components/icons/ts.tsx" import { LinkCard } from "../../components/LinkCard.tsx" import { RuntimeBenchmarksGraph } from "../../components/RuntimeBenchmarksGraph.tsx" export default () => (
}> Type syntax you already know with safety and completions unlike anything you've ever seen }> Deeply customizable messages with great defaults }> Definitions are half as long, type errors are twice as readable, and hovers tell you just what really matters }> 20x faster than Zod4 and 2,000x faster than Yup at runtime, with editor performance that will remind you how autocomplete is supposed to feel }> ArkType uses set theory to understand and expose the relationships between your types at runtime the way TypeScript does at compile time }> Every schema is internally normalized and reduced to its purest and fastest representation {/*

Most definitions are just objects and strings- take them across the stack or even outside JS altogether

*/}
) ================================================ FILE: ark/docs/app/api/search/route.ts ================================================ import { createSearchAPI, type AdvancedIndex } from "fumadocs-core/search/server" import { source } from "../../../lib/source.tsx" // it should be cached forever export const revalidate = false export const { staticGET: GET } = createSearchAPI("advanced", { indexes: await Promise.all( source.getPages().map( async page => ({ id: page.url, title: page.data.title, description: page.data.description, url: page.url, structuredData: (await page.data.load()).structuredData }) as AdvancedIndex ) ) }) ================================================ FILE: ark/docs/app/discord/page.tsx ================================================ import { redirect } from "next/navigation" import { defineMetadata } from "../metadata.ts" export const metadata = defineMetadata({ title: "ArkType Discord", ogImage: "ogDiscord.png" }) export default function DiscordPage() { redirect("https://discord.com/invite/xEzdc3fJQC") } ================================================ FILE: ark/docs/app/docs/[[...slug]]/page.tsx ================================================ import { Popup, PopupContent, PopupTrigger } from "fumadocs-twoslash/ui" import { CodeBlock, Pre } from "fumadocs-ui/components/codeblock" import { Tab, Tabs } from "fumadocs-ui/components/tabs" import defaultMdxComponents from "fumadocs-ui/mdx" import { DocsBody, DocsDescription, DocsPage, DocsTitle } from "fumadocs-ui/page" import { notFound, redirect } from "next/navigation" import { AnchorAliases } from "../../../components/AnchorAliases.tsx" import { SyntaxTab, SyntaxTabs } from "../../../components/SyntaxTabs.tsx" import { source } from "../../../lib/source.tsx" export default async (props: { params: Promise<{ slug?: string[] }> }) => { const params = await props.params if ( !params.slug?.length || (params.slug?.length === 1 && params.slug[0] === "intro") ) redirect("/docs/intro/setup") const page = source.getPage(params.slug) if (!page) notFound() const { body: MDX, toc } = await page.data.load() const isApiPage = page.data.title.endsWith("API") || page.data.title.endsWith("Configuration") return ( {page.data.title} {page.data.description} (
{props.children}
) }} />
) } export const generateStaticParams = async () => [ ...source.generateParams(), { slug: [] }, { slug: ["intro"] } ] export const generateMetadata = async (props: { params: Promise<{ slug?: string[] }> }) => { const params = await props.params const page = source.getPage(params.slug) if (!page) notFound() return { title: page.data.title, description: page.data.description } } ================================================ FILE: ark/docs/app/docs/layout.tsx ================================================ import { DocsLayout } from "fumadocs-ui/layouts/docs" import type { ReactNode } from "react" import { source } from "../../lib/source.tsx" import { baseOptions } from "../layout.config.tsx" import { defineMetadata } from "../metadata.ts" export const metadata = defineMetadata({ title: "ArkType Docs", ogImage: "ogDocs.png" }) export default ({ children }: { children: ReactNode }) => ( {children} ) ================================================ FILE: ark/docs/app/global-error.tsx ================================================ "use client" import { useEffect } from "react" export type GlobalErrorProps = { error: Error & { digest?: string } reset: () => void } export default function GlobalError() { useEffect(() => { const THRESHOLD = 2 * 60 * 1000 const now = Date.now() const lastReloadStr = localStorage.getItem("globalErrorLastReload") const lastReload = lastReloadStr ? parseInt(lastReloadStr, 10) : 0 if (!lastReloadStr || now - lastReload > THRESHOLD) { localStorage.setItem("globalErrorLastReload", now.toString()) window.location.reload() } }) const containerStyle: React.CSSProperties = { minHeight: "100vh", margin: "0", display: "flex", alignItems: "center", justifyContent: "center", background: "hsl(207 100% 9%)", fontFamily: "system-ui, sans-serif", color: "#E5E7EB" } const cardStyle: React.CSSProperties = { backgroundColor: "rgba(30, 41, 59, 0.5)", backdropFilter: "blur(10px)", padding: "2.5rem", borderRadius: "0.75rem", boxShadow: "0 4px 20px rgba(0, 0, 0, 0.3)", textAlign: "center", maxWidth: "400px", border: "1px solid rgba(255, 255, 255, 0.1)" } const headingStyle: React.CSSProperties = { fontSize: "2.25rem", fontWeight: 600, marginBottom: "1.5rem", background: "linear-gradient(135deg, #E5E7EB 0%, #94A3B8 100%)", WebkitBackgroundClip: "text", WebkitTextFillColor: "transparent", letterSpacing: "-0.025em" } const paraStyle: React.CSSProperties = { fontSize: "1.1rem", marginBottom: "2rem", color: "#94A3B8", lineHeight: 1.6 } const buttonStyle: React.CSSProperties = { padding: "0.75rem 1.5rem", border: "1px solid rgba(255, 255, 255, 0.1)", borderRadius: "0.5rem", color: "#E5E7EB", cursor: "pointer", fontSize: "0.95rem", fontWeight: 500, transition: "all 0.2s ease", background: "rgba(255, 255, 255, 0.05)" } const buttonHoverStyle = { ...buttonStyle, background: "rgba(255, 255, 255, 0.1)" } return ( Shipwrecked!

Shipwrecked!

Something unexpected went wrong.

) } ================================================ FILE: ark/docs/app/global.css ================================================ @import "tailwindcss"; @import "fumadocs-ui/css/ocean.css"; @import "fumadocs-ui/css/preset.css"; @source '../node_modules/fumadocs-ui/dist/**/*.js'; @font-face { font-family: "Cascadia Mono"; src: local("Cascadia Mono"), url("https://fonts.cdnfonts.com/s/37910/CascadiaMono.woff") format("woff"); } @theme { --color-background: var(--color-fd-background); --color-foreground: var(--color-fd-foreground); --color-muted: var(--color-fd-muted); --color-muted-foreground: var(--color-fd-muted-foreground); --color-popover: var(--color-fd-popover); --color-popover-foreground: var(--color-fd-popover-foreground); --color-card: var(--color-fd-card); --color-card-foreground: var(--color-fd-card-foreground); --color-border: var(--color-fd-border); --color-primary: var(--color-fd-primary); --color-primary-foreground: var(--color-fd-primary-foreground); --color-secondary: var(--color-fd-secondary); --color-secondary-foreground: var(--color-fd-secondary-foreground); --color-accent: var(--color-fd-accent); --color-accent-foreground: var(--color-fd-accent-foreground); --color-ring: var(--color-fd-ring); --color-highlight: #f5cf8f; } /** the * ensures this overrides the variable definitions from shiki*/ :root * { --background: 207 100% 9%; --hover-glow: 0.5rem 0.5rem 2rem 0 rgba(31, 38, 135, 0.37); /* Based on ArkDark ErrorLens */ --ark-green: #40decc; --ark-error: #9558f8; --ark-success: #40decca0; --ark-runtime-error: #f85858; /** @shikijs/twoslash/style-rich.css overrides */ --twoslash-border-color: #ba7e4127; --twoslash-underline-color: currentColor; --twoslash-highlighted-border: #c37d0d50; --twoslash-highlighted-bg: #c37d0d20; --twoslash-popup-bg: transparent; --twoslash-popup-color: inherit; --twoslash-popup-shadow: var(--shadow); --twoslash-docs-color: #888; --twoslash-docs-font: sans-serif; --twoslash-code-font: inherit; --twoslash-code-font-size: 1em; --twoslash-matched-color: inherit; --twoslash-unmatched-color: #888; --twoslash-cursor-color: #888; --twoslash-error-color: var(--ark-error); --twoslash-error-bg: #9558f818; --twoslash-warn-color: #c37d0d; --twoslash-warn-bg: #c37d0d20; --twoslash-tag-color: #3772cf; --twoslash-tag-bg: #3772cf20; --twoslash-tag-warn-color: var(--twoslash-warn-color); --twoslash-tag-warn-bg: var(--twoslash-warn-bg); --twoslash-tag-annotate-color: #1ba673; --twoslash-tag-annotate-bg: #1ba67320; } @media (prefers-reduced-motion: no-preference) { * { scroll-behavior: smooth; } } pre, code { font-family: "Cascadia Mono", monospace; } /* hack to avoid max-height on fuma codeblocks */ @layer utilities { .max-h-\[600px\] { max-height: unset; } } .fd-codeblock.twoslash { border-radius: 1.5rem !important; } pre.shiki, .twoslash-popup-container { border-radius: 1rem; border-color: #ba7e4127; border-width: 1px; overflow-x: visible !important; } /** should match arkDarkTheme.colors["editor.background"] */ .bg-fd-secondary\/50 { background-color: #0006; } /** avoid border on hover: https://github.com/arktypeio/arktype/issues/1217 */ .twoslash-popup-container pre.shiki, figure.shiki { background-color: unset !important; } div.twoslash-popup-container { border-radius: 1rem; background: #001323aa; backdrop-filter: blur(8px); box-shadow: var(--hover-glow); } /** .error.highlighted matches error lines explicitly added in the snippet source via [!code error] */ .error.highlighted { position: relative; padding: 4px; background-color: var(--twoslash-error-bg); border-left: 3px solid var(--ark-error); padding-right: 16px; margin: 0.2em 0; min-width: 100%; width: max-content; } .error.highlighted > span { color: var(--twoslash-error-color) !important; } .error.highlighted.runtime-error { background-color: #f8585822; border-left: 3px solid var(--ark-runtime-error); } .error.highlighted.runtime-error > span { color: var(--ark-runtime-error) !important; } /** .twoslash-error matches errors added by twoslash itself, e.g. type errors */ .twoslash .twoslash-error { /* Override the built-in error squiggle to match our theme */ background: url("/image/errorSquiggle.svg") repeat-x bottom left; } .twoslash .twoslash-popup-code { white-space: pre; } /* avoid double padding + border */ /* matches popups rendered from react (on the home page) */ .shiki .shiki, /* matches popups rendered from mdx (on most docs pages) */ .twoslash-popup-container pre.p-4 { padding: 0px; border-width: 0px; } /** display runtime errors on hover */ .twoslash .twoslash-popup-docs { color: var(--ark-runtime-error); font-size: small; white-space: pre; } /** avoid empty lines being rendered with 0 height */ .twoslash .line { min-height: 20px; } .completions-block code { padding-bottom: 2rem; } /** avoid a janky white outline on hovers: https://github.com/arktypeio/arktype/issues/1217 */ :focus-visible { outline: none; } /* Firefox specific rules */ @-moz-document url-prefix() { /* The backdrop-filter above doesn't work by default yet on Firefox so we do this instead */ .twoslash .twoslash-hover:hover .twoslash-popup-container { background: #001323ee; } } /* allow us to inject a badge at order 1 */ #nd-sidebar .lucide-chevron-down { order: 2; } #nd-home-layout, #nd-nav { overflow: hidden; transition: margin-top 0.3s ease; } /* if release banner is visible, add top offset to home navbar and layout equal to its height */ :root:has(.release-banner:not([hidden]):not(.hidden)) #nd-home-layout, :root:has(.release-banner:not([hidden]):not(.hidden)) #nd-nav { margin-top: 3.5rem; } .glass-container, .monaco-editor { box-shadow: 0 10px 15px 0 rgba(0, 0, 0, 0.3), 0 15px 30px 0 rgba(0, 0, 0, 0.22); backdrop-filter: blur(16px); /* without this Monaco ends up adding a 1px outline at the top of the editor for some reason */ outline-width: 0px !important; } .monaco-editor, .overflow-guard { border-radius: 16px; } /* Chrome/Safari/Edge scrollbar styling */ ::-webkit-scrollbar { width: 8px; height: 8px; background-color: transparent; } ::-webkit-scrollbar-thumb { background-color: rgba(255, 255, 255, 0.1); border-radius: 4px; } ::-webkit-scrollbar-thumb:hover { background-color: rgba(255, 255, 255, 0.2); } ::-webkit-scrollbar-track { background: transparent; } /* Firefox scrollbar styling */ * { scrollbar-width: thin; scrollbar-color: rgba(255, 255, 255, 0.1) transparent; } .wiggle-animation { animation: wiggle 4s ease-in-out infinite; transform-origin: center center; } @keyframes wiggle { 0%, 87.5%, 100% { transform: rotate(0deg); } 88.5% { transform: rotate(1.5deg); } 89.5% { transform: rotate(-0.5deg); } 90.5% { transform: rotate(-1.5deg); } 91.5% { transform: rotate(1deg); } 92.5% { transform: rotate(1.5deg); } 93.5% { transform: rotate(-1deg); } 94.5% { transform: rotate(-0.5deg); } 96% { transform: rotate(0.25deg); } 98% { transform: rotate(0deg); } } h1, h2, h3, h4, h5, h6, .hero-tagline { font-family: var(--font-raleway); } body { font-family: var(--font-atkinson); } #nd-docs-layout h1 { margin-bottom: 0.5rem !important; font-size: 2.75rem !important; } .docs-body h2 { margin-top: 0.3rem; margin-bottom: 0.3rem; font-size: 2.25rem !important; } .docs-body h3 { margin-top: 0.2rem; margin-bottom: 0.2rem; font-size: 1.75rem !important; } .docs-body h4 { font-size: 1.5rem !important; color: #ffffffbf; } .docs-body h5 { font-size: 1.25rem !important; color: #ffffffcf; } .docs-body h6 { font-size: 1rem !important; color: #ffffffdf; } ================================================ FILE: ark/docs/app/layout.config.tsx ================================================ import { SiBluesky, SiDiscord, SiTwitch, SiX } from "@icons-pack/react-simple-icons" import type { BaseLayoutProps } from "fumadocs-ui/layouts/shared" import { ArkTypeLogo } from "../components/icons/arktype-logo.tsx" export const baseOptions: BaseLayoutProps = { nav: { title: }, themeSwitch: { enabled: false }, githubUrl: "https://github.com/arktypeio/arktype", links: [ { text: "Twitch", type: "icon", icon: , url: "https://twitch.tv/arktypeio" }, { text: "Bluesky", type: "icon", icon: , url: "https://bsky.app/profile/arktype.io" }, { text: "X", type: "icon", icon: , url: "https://x.com/arktypeio" }, { text: "Discord", type: "icon", icon: , url: "https://arktype.io/discord" } ] } ================================================ FILE: ark/docs/app/layout.tsx ================================================ import "fumadocs-twoslash/twoslash.css" import { RootProvider } from "fumadocs-ui/provider" import { Atkinson_Hyperlegible, Raleway } from "next/font/google" import type { ReactNode } from "react" import { ReleaseBanner } from "../components/ReleaseBanner.tsx" import "./global.css" import { defineMetadata } from "./metadata.ts" import { CSPostHogProvider } from "./providers.tsx" const raleway = Raleway({ subsets: ["latin"], display: "swap", variable: "--font-raleway" }) const atkinson = Atkinson_Hyperlegible({ weight: ["400", "700"], subsets: ["latin"], display: "swap", variable: "--font-atkinson" }) export const metadata = defineMetadata({}) export default function RootLayout({ children }: { children: ReactNode }) { return ( {children} ) } ================================================ FILE: ark/docs/app/metadata.ts ================================================ import type { Metadata } from "next" export type MetadataOptions = { title?: string ogImage?: string } export const defineMetadata = ({ title = "ArkType", ogImage = "og.png" }: MetadataOptions): Metadata => ({ title: `${title}: TypeScript's 1:1 validator, optimized from editor to runtime`, description: "TypeScript's 1:1 validator, optimized from editor to runtime", keywords: [ "ArkType", "TypeScript", "JavaScript", "runtime validation", "schema", "type-safe", "validator", "syntax" ], openGraph: { title, description: "TypeScript's 1:1 validator, optimized from editor to runtime", url: "https://arktype.io/", siteName: "ArkType", images: [ { url: `https://arktype.io/image/${ogImage}`, width: 1200, height: 600 } ], type: "website" }, twitter: { card: "summary_large_image" }, icons: { icon: "/image/favicon.svg" } }) ================================================ FILE: ark/docs/app/playground/page.tsx ================================================ import { HomeLayout } from "fumadocs-ui/layouts/home" import type { ReactNode } from "react" import { FloatYourBoat } from "../../components/FloatYourBoat.tsx" import { Playground } from "../../components/playground/Playground.tsx" import { baseOptions } from "../layout.config.tsx" import { defineMetadata } from "../metadata.ts" export const metadata = defineMetadata({ title: "ArkType Playground", ogImage: "ogPlayground.png" }) export type LayoutProps = { children: ReactNode } export default function PlaygroundPage() { return ( }} >
) } ================================================ FILE: ark/docs/app/providers.tsx ================================================ "use client" import posthog from "posthog-js" import { PostHogProvider } from "posthog-js/react" if ( globalThis.window !== undefined && process.env.NEXT_PUBLIC_POSTHOG_KEY && process.env.NEXT_PUBLIC_POSTHOG_HOST ) { posthog.init(process.env.NEXT_PUBLIC_POSTHOG_KEY, { api_host: process.env.NEXT_PUBLIC_POSTHOG_HOST, person_profiles: "always" }) } export const CSPostHogProvider = ({ children }: { children: React.ReactNode }) => {children} ================================================ FILE: ark/docs/components/AnchorAliases.tsx ================================================ import React from "react" export declare namespace AnchorAliases { export type Props = Record } /** * create multiple anchor aliases for Markdown headers * * @example * */ export const AnchorAliases = (aliases: AnchorAliases.Props) => ( <> {Object.keys(aliases).map(id => ( ))} ) ================================================ FILE: ark/docs/components/ApiTable.tsx ================================================ import type { ApiGroup, ParsedJsDocPart } from "../../repo/jsdocGen.ts" import { apiDocsByGroup } from "./apiData.ts" import { CodeBlock } from "./CodeBlock.tsx" import { LocalFriendlyUrl } from "./LocalFriendlyUrl.tsx" export type ApiTableProps = { group: ApiGroup } export const ApiTable = ({ group }: ApiTableProps) => (
{apiDocsByGroup[group].map(props => ( ))}
) const ApiTableHeader = () => ( Name Summary Notes & Examples ) interface ApiTableRowProps { name: string summary: ParsedJsDocPart[] example?: string experimental?: ParsedJsDocPart[] notes: ParsedJsDocPart[][] } const ApiTableRow = ({ name, summary, example, experimental, notes }: ApiTableRowProps) => ( {name} {JsDocParts(summary)} {notes.map((note, i) => (
{JsDocParts(note)}
))} {experimental ? JsDocParts(experimental) : null} {example} ) const JsDocParts = (parts: readonly ParsedJsDocPart[]) => parts.map((part, i) => ( {part.kind === "link" ? {part.value} : part.kind === "reference" ?
{part.value} :

$2") .replace(/(\*|_)([^*_]+)\1/g, "$2") .replace(/`([^`]+)`/g, "$1") .replace(/^-(.*)/g, "• $1") }} /> } )) interface ApiExampleProps { children: string | undefined } const ApiExample = ({ children }: ApiExampleProps) => children && ( {children} ) ================================================ FILE: ark/docs/components/ArkCard.tsx ================================================ import { cx } from "class-variance-authority" import { Card, type CardProps, Cards } from "fumadocs-ui/components/card" export const ArkCards: React.FC<{ children: React.ReactNode }> = ({ children }) => {children} export const ArkCard: React.FC = ({ children, className, ...props }) => ( h3]:text-2xl [&>h3]:font-semibold [&_.prose-no-margin]:text-lg", "[&>.prose-no-margin]:flex [&>.prose-no-margin]:flex-col [&>.prose-no-margin]:flex-grow", "flex flex-col", "rounded-3xl", className )} style={{ borderWidth: 2 }} > {children} ) ================================================ FILE: ark/docs/components/AutoplayDemo.tsx ================================================ "use client" import { ArrowRightLeftIcon, ExpandIcon } from "lucide-react" import { useEffect, useRef, useState } from "react" import { Button } from "./Button.tsx" import { Playground } from "./playground/Playground.tsx" import { defaultPlaygroundCode } from "./playground/utils.ts" export type AutoplayDemoProps = React.DetailedHTMLProps< React.VideoHTMLAttributes, HTMLVideoElement > & { src: string } export const MainAutoplayDemo = () => ( ) export const AutoplayDemo = (props: AutoplayDemoProps) => { const [showPlayground, setShowPlayground] = useState(false) const [dimensions, setDimensions] = useState({ width: "100%", height: "30vh" }) const videoRef = useRef(null) useEffect(() => { const updateDimensions = () => { if (videoRef.current) { const { offsetWidth, offsetHeight } = videoRef.current setDimensions({ width: `${offsetWidth}px`, height: `${offsetHeight}px` }) } } if (videoRef.current && videoRef.current.readyState >= 1) updateDimensions() const video = videoRef.current if (video) { video.addEventListener("loadedmetadata", updateDimensions) video.addEventListener("loadeddata", updateDimensions) window.addEventListener("resize", updateDimensions) } return () => { if (video) { video.removeEventListener("loadedmetadata", updateDimensions) video.removeEventListener("loadeddata", updateDimensions) } window.removeEventListener("resize", updateDimensions) } }, [videoRef.current]) return (

{showPlayground && ( )}

Type-level feedback with each keystroke-{" "} no plugins or build steps required.

) } ================================================ FILE: ark/docs/components/Badge.tsx ================================================ import { cva, cx, type VariantProps } from "class-variance-authority" import * as React from "react" export const badgeVariants = cva( "inline-flex items-center rounded-md border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2", { variants: { variant: { default: "border-transparent bg-primary text-primary-foreground shadow hover:bg-primary/80", secondary: "border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80", destructive: "border-transparent bg-destructive text-destructive-foreground shadow hover:bg-destructive/80", outline: "text-foreground" } }, defaultVariants: { variant: "default" } } ) export interface BadgeProps extends React.HTMLAttributes, VariantProps {} export const Badge = ({ className, variant, ...props }: BadgeProps) => (
) ================================================ FILE: ark/docs/components/Banner.tsx ================================================ "use client" import { cx } from "class-variance-authority" import { buttonVariants } from "fumadocs-ui/components/ui/button" import { X } from "lucide-react" import Link from "next/link.js" import { type HTMLAttributes, type MouseEvent, useCallback, useEffect, useState } from "react" import { FloatYourBoat } from "./FloatYourBoat.tsx" // Based on: // https://github.com/fuma-nama/fumadocs/blob/1e6ece043987c8bf607249b66a8945632b229982/packages/ui/src/components/banner.tsx#L65 export declare namespace Banner { export interface Props extends HTMLAttributes { href: string boat?: boolean changeLayout?: boolean } } export const Banner = ({ id = "banner", changeLayout = true, boat, href, children, ...props }: Banner.Props): React.ReactElement => { const [open, setOpen] = useState(false) const globalKey = id ? `nd-banner-${id}` : undefined useEffect(() => { if (globalKey) setOpen(localStorage.getItem(globalKey) !== "true") }, [globalKey]) const handleCloseClick = useCallback( (e: MouseEvent) => { // prevent the close button click from also triggering the link on // the rest the rest of the branner e.stopPropagation() setOpen(false) if (globalKey) localStorage.setItem(globalKey, "true") }, [globalKey] ) const BannerContent = ( <>
{boat ? : null}
{children}
) return (
{changeLayout && open ? : null} {globalKey ? : null} {id ?