ata-validator 1.60.0 → 1.61.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -12,11 +12,17 @@ JSON Schema validation that compiles for speed and still runs where code generat
12
12
 
13
13
  ## Who uses ata
14
14
 
15
- Projects whose code depends on ata, from GitHub's dependency graph, and the framework that lists
16
- its plugin, with what each does with it:
15
+ Projects whose code depends on ata, from GitHub's dependency graph, a framework that lists its
16
+ plugin, a form library that ships its resolver, and one company whose use is not public code,
17
+ with what each does with it:
17
18
 
19
+ - [Trendyol](https://github.com/Trendyol) validates configuration with ata in Puzzle, its
20
+ internal config tooling. The package is internal, so it is not in the public dependency
21
+ graph; this line rests on its maintainer.
18
22
  - [Socket](https://github.com/SocketDev) uses ata, compiled ahead of time, in the shared build
19
23
  tooling of its repositories.
24
+ - [React Hook Form](https://github.com/react-hook-form/resolvers) ships an ata resolver in
25
+ `@hookform/resolvers` (`@hookform/resolvers/ata-validator`), beside its other validators.
20
26
  - [Fastify](https://fastify.dev/ecosystem/) lists the
21
27
  [`fastify-ata`](https://github.com/ata-core/fastify-ata) plugin in its ecosystem.
22
28
  - [react-jsonschema-form](https://github.com/rjsf-team/react-jsonschema-form) ships an ata
@@ -68,7 +74,7 @@ details, and what Deno Deploy's refusal of dynamic code means for it, are in
68
74
 
69
75
  Those four now agree with `validate()` on every case of the official suite, 3365 across three dialects. The native walker behind them does not handle every shape (`contains`, `unevaluatedProperties`, `patternProperties`, tuple `items`, cross-document `$ref`, a few formats), so for schemas using one of those the buffer APIs parse the bytes and answer through `validate()`; the list is in `lib/buffer-gate.js`. Typical request schemas stay on the zero-copy path. `npm test` holds the disagreement count at zero.
70
76
 
71
- Where `new Function` is refused altogether, on Cloudflare Workers, Deno Deploy or under a strict Content-Security-Policy, ata drops to the interpreted engine and scores the same 1301 of 1301 with code generation blocked. No flags, and on Workers no `nodejs_compat` either. See [docs/edge-runtimes.md](docs/edge-runtimes.md).
77
+ Where `new Function` is refused altogether, on Cloudflare Workers, Deno Deploy or under a strict Content-Security-Policy, ata drops to the interpreted engine and scores the same 1301 of 1301 with code generation blocked. No flags, and on Workers no `nodejs_compat` either. See [docs/edge-runtimes.md](docs/edge-runtimes.md). Where generated code runs but slowly, as in an ahead-of-time compiled Bun executable, a module from `ata build` is plain JavaScript that imports nothing and calls no `new Function`: on Bun 1.4.0, import plus a first validation takes 0.66 ms with such a module against 10.70 ms with the runtime compiler (medians of 21 runs). [docs/proof-no-code-generation.md](docs/proof-no-code-generation.md) runs one schema in five of these places, Workers and a CSP page included, and shows the same answers from each.
72
78
 
73
79
  Node has a switch for exactly that environment, so this takes thirty seconds to check for yourself, on ata or on whatever you use today:
74
80
 
@@ -667,6 +673,102 @@ first request, so counting only registration would overstate the gap. The first
667
673
  here comes in the same tick as setup; a server that listens first has the code generator
668
674
  loaded by then. Reproduce with `node benchmark/bench_fastify_boot.mjs`.
669
675
 
676
+ A server that listens first also gets the engine warmed: once the event loop is free, if a
677
+ validator was built and not yet called, ata runs one check of a small schema, accepted and
678
+ rejected, so that V8 compiles the generator's functions and the error builders then rather
679
+ than on the first request. Measured on a four-field schema with the generator already
680
+ loaded: the first verdict 3.1 to 3.4 ms without it, 0.5 to 0.6 with it; the first error
681
+ list 6.7 against 0.8; a second route's first verdict 0.9 to 1.0 against 0.7 to 0.9. It costs
682
+ the process 1.5 to 2 ms of CPU once. A script that builds a validator and checks in the
683
+ same tick never runs it, and the timer is unref'd, so a finished script does not stay for
684
+ it; `ATA_NO_WARM=1` turns it off.
685
+
686
+ ### Proving the build (`ata verify`)
687
+
688
+ A module written at build time is only a good idea if it answers as the
689
+ runtime it replaced. `ata verify` checks that on the project's own schemas:
690
+
691
+ ```bash
692
+ npx ata verify 'schemas/*.json' --samples test/fixtures
693
+ ```
694
+
695
+ For each schema it loads the module `ata build` wrote beside it, makes
696
+ documents from the schema (200 by default, half of them with one rule broken),
697
+ reads the project's sample documents (JSON files, JSON Lines, or a directory
698
+ of them), and has both the module and a `Validator` built from the schema now
699
+ answer every one. Verdicts and error lists have to agree; a module whose
700
+ schema hash no longer matches the schema is reported stale. The exit code is
701
+ non-zero on any disagreement, stale module or missing module, so the command
702
+ works as a CI step. This is the check that found the two silent divergences
703
+ fixed in 1.59.0 and 1.60.0, now in a form a project can run on every change.
704
+ It does not run the `.bun.ts` target, which needs Bun's compiler.
705
+
706
+ ### Bun, compiled ahead of time
707
+
708
+ Bun's ahead-of-time compiler (`bun build --compile --aot`, an open pull
709
+ request as of October 2026) runs code that a library hands to `new Function`
710
+ through an interpreter. Measured with its pull request build on one schema
711
+ and 1024 parsed documents, nanoseconds per document (`benchmark/proof/aot`):
712
+
713
+ | | JIT | ahead of time |
714
+ |---|---|---|
715
+ | a module from `ata build --target bun` | 25 | 24 |
716
+ | `new Validator(schema)`, generated code | 28 | 492 |
717
+ | `new Validator(schema)`, interpreted engine | 52 | 80 |
718
+
719
+ So, in order of preference:
720
+
721
+ 1. Compile the schema at build time. `@ata-project/unplugin/bun` does it in
722
+ `Bun.build`, including `Bun.build({ compile })`; `ata build` writes the
723
+ module by hand. The module is plain JavaScript and compiles with the
724
+ program: 24 ns, the same errors as the runtime.
725
+ 2. Where the runtime `Validator` stays, let it take the interpreted engine.
726
+ It does so by itself when Bun reports `Bun.isAOT`, when the executable is
727
+ built with `--disallow-code-generation-from-strings` (the program then
728
+ counts as closed for the compiler, too), or when `ATA_ENGINE=interpreter`
729
+ is set: 80 ns against 492 for the generated code there.
730
+ 3. Write the module for the compiler. `ata build schema.json --target bun`
731
+ writes it with a closed object's keys read once and compared with the
732
+ declared list in order (on a ten-item order with 1024 distinct documents,
733
+ 10% of them invalid, 105 to 107 ns under the compiler against 223 for the
734
+ form V8 gets; on V8 the module keeps for...in, on Bun's JIT a plain count,
735
+ each the cheaper form there), and answers a rejection with the one-pass
736
+ error function (189 ns with the error list on that order, against 724
737
+ with the older collector), the form every other build takes only with
738
+ `--one-pass` since it is 20% larger gzipped. Beside the module it writes
739
+ `schema.compiled.bun.ts`: the schema as a TypeScript type (kinds,
740
+ properties and which are required, items and fixed tuples, literals,
741
+ local definitions as named types), `validate` and `isValid` with that
742
+ type on them, and `parse(text)`, which declares the type where it parses
743
+ and has the module check the content. Every check is the module's, and
744
+ the file is a type for the program, not a faster path: on that order,
745
+ with the pull request build of 2026-10-25, `isValid` 118 ns through the
746
+ file against 118 to 121 through the module, `parse` 564 to 574 against
747
+ 570 to 578 for a plain parse and the module, and reading the parsed
748
+ object after a typed parse 593 to 595 against 587 to 591 after a plain
749
+ one. The file is for executables built with
750
+ `--aot`; a project points that compiler at it with an `"aot"` condition
751
+ in its `package.json` `imports`, which nothing else reads:
752
+
753
+ ```json
754
+ { "imports": { "#schemas/*": { "aot": "./schemas/*.bun.ts", "default": "./schemas/*.js" } } }
755
+ ```
756
+
757
+ A schema no type can be written for (a remote `$ref`, `$dynamicRef`, a
758
+ root the type says nothing about) gets a warning and the plain module
759
+ alone.
760
+
761
+ The pull request carried its own typed validator until 2026-10-25, and
762
+ `--target bun` wrote the schema's rules as a type for it; it left the pull
763
+ request to return as a change of its own. `docs/API.md` has what ata keeps
764
+ for it. A schema's `pattern` is a `new RegExp` at run time in the runtime
765
+ `Validator`, which that compiler runs through its regular expression
766
+ interpreter; the module writes each pattern as a literal, which it compiles.
767
+ `--aot` makes TypeScript errors fatal; ata's declarations say `message?:
768
+ string` on an error, so read it as such. The numbers are from the pull
769
+ request build of 2026-10-24 on one machine and will be run again when the
770
+ compiler ships.
771
+
670
772
  ### Standard Schema V1
671
773
 
672
774
  ```javascript
@@ -745,9 +847,10 @@ Copy-paste recipes for the common frameworks. Most need 10-20 lines of glue. See
745
847
  | Vite (build-time compile) | dedicated plugin | [`ata-vite`](https://github.com/ata-core/ata-vite), the Vite entry of the plugin above |
746
848
  | Next.js (build-time compile) | webpack entry of the plugin above | `@ata-project/unplugin/webpack` in `next.config.js` |
747
849
  | Hono | async middleware | [docs/integrations/hono.md](docs/integrations/hono.md) |
748
- | Elysia | direct handler check | [docs/integrations/elysia.md](docs/integrations/elysia.md) |
850
+ | Elysia | Standard Schema V1 in the `body` slot | [docs/integrations/elysia.md](docs/integrations/elysia.md) |
749
851
  | tRPC | Standard Schema V1 input | [docs/integrations/trpc.md](docs/integrations/trpc.md) |
750
852
  | TanStack Form | Standard Schema V1 validator | [docs/integrations/tanstack-form.md](docs/integrations/tanstack-form.md) |
853
+ | React Hook Form | resolver | [`@hookform/resolvers/ata-validator`](https://github.com/react-hook-form/resolvers/tree/master/ata-validator) |
751
854
  | Express | sync middleware | [docs/integrations/express.md](docs/integrations/express.md) |
752
855
  | Koa | async ctx middleware | [docs/integrations/koa.md](docs/integrations/koa.md) |
753
856
  | NestJS | validation pipe | [docs/integrations/nestjs.md](docs/integrations/nestjs.md) |
package/bin/ata.js CHANGED
@@ -13,6 +13,15 @@ Usage:
13
13
  ata build <glob>... [options] Compile a project's schemas (glob pattern) per file.
14
14
  ata validate <schema> <data> [options] Validate a JSON data file against a schema.
15
15
  ata migrate [dir] [options] Report what switching from ajv would change; --write applies it.
16
+ ata verify <glob>... [options] Check that each schema's compiled module answers as the runtime
17
+ does, on generated documents and the project's samples.
18
+
19
+ Verify options:
20
+ --generate <n> Documents made from each schema, half with one rule broken. Default: 200
21
+ --samples <path> A JSON, JSON Lines file or a directory of them, checked too (repeatable)
22
+ --seed <n> Seed for the generated documents. Default: 1
23
+ --json Report as JSON
24
+ --out-dir, --suffix, --format Where the modules are, as given to ata build
16
25
 
17
26
  Validate options:
18
27
  --pretty Render errors with source frames (default on TTY)
@@ -65,6 +74,9 @@ Build options:
65
74
  --cache-file <path> Cache file for incremental builds (default: cache disabled)
66
75
  --max-size <bytes> Fail build if any compiled module exceeds this gzipped size
67
76
  --strict Treat any AOT-incompatible schema as a build error (default: skip + warn)
77
+ --target bun Write every module for Bun's ahead-of-time compiler, and beside
78
+ each a .bun.ts with the schema as a TypeScript type and a typed
79
+ parse(text); the plain module answers every check
68
80
  --strict-schema Fail the build on schema authoring mistakes: unknown
69
81
  keywords (with a spelling suggestion), keywords the
70
82
  node's type makes inert, unresolvable local $refs,
@@ -117,6 +129,11 @@ function parseArgs(argv) {
117
129
  if (a === '--check') { out.opts.check = true; continue; }
118
130
  if (a === '--strict') { out.opts.strict = true; continue; }
119
131
  if (a === '--strict-schema') { out.opts.strictSchema = true; continue; }
132
+ if (a === '--target') { out.opts.target = argv[++i]; continue; }
133
+ if (a === '--generate') { out.opts.generate = Number(argv[++i]); continue; }
134
+ if (a === '--samples') { (out.opts.samples = out.opts.samples || []).push(argv[++i]); continue; }
135
+ if (a === '--seed') { out.opts.seed = Number(argv[++i]); continue; }
136
+ if (a === '--json') { out.opts.json = true; continue; }
120
137
  if (a === '--out-dir') { out.opts.outDir = argv[++i]; continue; }
121
138
  if (a === '--suffix') { out.opts.suffix = argv[++i]; continue; }
122
139
  if (a === '--cache-file') { out.opts.cacheFile = argv[++i]; continue; }
@@ -239,6 +256,15 @@ function cmdCompile(args) {
239
256
 
240
257
  const { Validator } = require('..');
241
258
  const aot = require('../lib/aot');
259
+ // `--target bun`: the module is written for JavaScriptCore, the engine the
260
+ // executable the target is for runs on.
261
+ if (args.opts.target === 'bun') {
262
+ require('../lib/js-compiler').setTargetEngine('jsc-aot');
263
+ // The executable's module answers rejections with the one-pass error
264
+ // function and carries no schema-source frames unless asked for.
265
+ if (args.opts.onePass === undefined) args.opts.onePass = true;
266
+ if (args.opts.source === undefined) args.opts.source = false;
267
+ }
242
268
  let v;
243
269
  try {
244
270
  v = new Validator(schema);
@@ -277,6 +303,25 @@ function cmdCompile(args) {
277
303
  const sizeBytes = Buffer.byteLength(src, 'utf8');
278
304
  process.stdout.write(`ata: compiled ${input} -> ${output} (${sizeBytes.toLocaleString()} bytes, ${format}${abortEarly ? ', abort-early' : ''})\n`);
279
305
 
306
+ if (args.opts.target !== undefined) {
307
+ if (args.opts.target !== 'bun') {
308
+ reportCompileError(input, `--target must be bun (got ${JSON.stringify(args.opts.target)})`);
309
+ process.exit(1);
310
+ }
311
+ const { writeBunTarget, Declined } = require('../lib/bun-target');
312
+ const tn = (args.opts.name || path.basename(input, path.extname(input)).replace(/[^A-Za-z0-9_]/g, '_').replace(/^([a-z])/, (m) => m.toUpperCase())) || 'Data';
313
+ try {
314
+ const w = writeBunTarget({ schema, input, output, typeName: tn });
315
+ process.stdout.write(`ata: bun target -> ${w.bunPath} (for bun build --compile --aot)\n`);
316
+ } catch (e) {
317
+ if (!(e instanceof Declined)) throw e;
318
+ // No type can be written; the plain module stands alone.
319
+ const msg = `--target bun: ${e.message}; the plain module stands alone`;
320
+ if (args.opts.strict) { reportCompileError(input, msg); process.exit(1); }
321
+ process.stderr.write(`ata: warning: ${input}: ${msg}\n`);
322
+ }
323
+ }
324
+
280
325
  // Emit paired declaration file unless --no-types.
281
326
  // TypeScript resolution: .mjs -> .d.mts, .cjs -> .d.cts, .js -> .d.ts.
282
327
  const emitTypes = args.opts.types !== false;
@@ -298,6 +343,10 @@ function cmdCompile(args) {
298
343
  }
299
344
 
300
345
  function cmdBuild(args) {
346
+ if (args.opts.target !== undefined && args.opts.target !== 'bun') {
347
+ process.stderr.write(`error: --target must be bun (got ${JSON.stringify(args.opts.target)})\n`);
348
+ process.exit(1);
349
+ }
301
350
  if (args._.length === 0) {
302
351
  process.stderr.write('error: missing <glob>\n\n');
303
352
  usage();
@@ -323,6 +372,7 @@ function cmdBuild(args) {
323
372
  check: !!args.opts.check,
324
373
  maxSize: args.opts.maxSize,
325
374
  strict: !!args.opts.strict,
375
+ target: args.opts.target,
326
376
  types: args.opts.types,
327
377
  cacheFile: args.opts.cacheFile,
328
378
  // Forward the explicit user choice (true / false / undefined). The
@@ -337,6 +387,8 @@ function cmdBuild(args) {
337
387
  }
338
388
  for (const c of report.compiled) {
339
389
  process.stdout.write(`ata: ${c.input} -> ${c.output} (${c.bytes.toLocaleString()} bytes${c.errors === 'compact' ? ', shared errors' : c.errors === 'schema' ? ', schema module' : ''})\n`);
390
+ for (const t of (report.bunTargets || []).filter((t) => t.input === c.input)) process.stdout.write(`ata: bun target -> ${t.output} (for bun build --compile --aot)\n`);
391
+ for (const d of (report.bunDeclined || []).filter((d) => d.input === c.input)) process.stderr.write(`ata: warning: ${d.input}: ${d.reason}\n`);
340
392
  }
341
393
  if (report.runtime) {
342
394
  process.stdout.write(`ata: error runtime -> ${report.runtime.output} (${report.runtime.bytes.toLocaleString()} bytes, shared by the modules above)\n`);
@@ -408,6 +460,33 @@ function cmdBuild(args) {
408
460
  });
409
461
  }
410
462
 
463
+ function cmdVerify (args) {
464
+ if (args._.length === 0) {
465
+ process.stderr.write('error: missing <glob>\n\n');
466
+ usage();
467
+ process.exit(1);
468
+ }
469
+ const buildLib = require('../lib/aot-build');
470
+ const { verifyOne, renderReport, ok } = require('../lib/verify');
471
+ const format = args.opts.format || 'esm';
472
+ (async () => {
473
+ const inputs = await buildLib.expandGlobs(args._);
474
+ if (inputs.length === 0) {
475
+ process.stderr.write(`error: no schema files match ${args._.join(' ')}\n`);
476
+ process.exit(1);
477
+ }
478
+ const reports = [];
479
+ for (const input of inputs) {
480
+ let schema;
481
+ try { schema = buildLib.parseSchemaFile(input); } catch (e) { reports.push({ schema: input, module: '', error: e.message, documents: 0, agree: 0, disagreements: [] }); continue; }
482
+ const moduleFile = buildLib.outputPathFor(input, { format, outDir: args.opts.outDir, suffix: args.opts.suffix });
483
+ reports.push(await verifyOne({ schemaFile: input, moduleFile, schema, generate: Number.isFinite(args.opts.generate) ? args.opts.generate : 200, seed: Number.isFinite(args.opts.seed) ? args.opts.seed : 1, samples: args.opts.samples || [] }));
484
+ }
485
+ process.stdout.write(renderReport(reports, { json: !!args.opts.json }) + '\n');
486
+ process.exit(reports.every(ok) ? 0 : 1);
487
+ })().catch((e) => { process.stderr.write(`error: ${e.message}\n`); process.exit(1); });
488
+ }
489
+
411
490
  function cmdValidate (args) {
412
491
  if (args._.length < 2) {
413
492
  process.stderr.write('error: ata validate <schema> <data-file>\n');
@@ -508,6 +587,11 @@ function main() {
508
587
  return;
509
588
  }
510
589
 
590
+ if (cmd === 'verify') {
591
+ cmdVerify(args);
592
+ return;
593
+ }
594
+
511
595
  if (cmd === 'migrate') {
512
596
  const { migrate, formatReport } = require('../lib/migrate');
513
597
  const result = migrate(args._[0] || '.', { write: !!args.opts.write });
package/build.d.ts CHANGED
@@ -204,3 +204,32 @@ export function compiledModuleFor(schema: unknown, opts?: { format?: 'esm' | 'cj
204
204
  * ata-validator 1.37.0, where only calls without options can be.
205
205
  */
206
206
  export const compiledOptions: readonly string[];
207
+
208
+ /**
209
+ * The Bun target, for a build plugin: the schema as a TypeScript type and the
210
+ * `.bun.ts` module `ata build --target bun` writes beside a compiled module.
211
+ * With `tags: true` the type carries Bun's `Schema` tags and `residual` holds
212
+ * the schema of every rule the tagged type does not carry, the split made for
213
+ * Bun's `validate<T>`, which left the compiler's pull request on 2026-10-25
214
+ * and is to return as a change of its own; without tags `residual` is null.
215
+ */
216
+ export const bunTarget: {
217
+ schemaToBunType(schema: unknown, opts?: { name?: string; aliasPrefix?: string; tags?: boolean; assertFormat?: boolean }): { name: string; type: string; aliases: Array<[string, string]>; residual: unknown | null; carried: string[] };
218
+ toBunModule(args: { schemaFile: string; moduleImport: string; typeName: string; typeSource: string; aliases: Array<[string, string]> }): string;
219
+ writeBunTarget(args: { schema: unknown; input: string; output: string; typeName: string }): { bunPath: string; definitions: number; carried: number };
220
+ Declined: new (reason: string, pointer: string) => Error & { reason: string; pointer: string };
221
+ };
222
+
223
+ /** `ata verify` as functions: a compiled module held against the runtime on generated documents and samples. */
224
+ export const verify: {
225
+ verifyOne(args: { schemaFile: string; moduleFile: string; schema: unknown; generate?: number; seed?: number; samples?: string[] }): Promise<VerifyReport>;
226
+ verifySource(schema: unknown, src: string, opts?: { name?: string; format?: 'esm' | 'cjs'; generate?: number; seed?: number; samples?: string[] }): Promise<VerifyReport>;
227
+ generate(schema: unknown, count: number, seed: number): unknown[];
228
+ renderReport(reports: VerifyReport[], opts?: { json?: boolean }): string;
229
+ ok(report: VerifyReport): boolean;
230
+ };
231
+ export interface VerifyReport {
232
+ schema: string; module: string; documents: number; generated: number; samples: number; valid: number; agree: number;
233
+ disagreements: Array<{ source: string; document: string; runtime: string; module: string; runtimeIsValid: boolean; moduleIsValid: boolean } | { more: number }>;
234
+ stale: boolean; missing: boolean; ataVersion: string | null; error: string | null;
235
+ }
package/build.mjs CHANGED
@@ -12,5 +12,7 @@ export const schemaHash = mod.schemaHash;
12
12
  export const compiledEligible = mod.compiledEligible;
13
13
  export const compiledSchemaFor = mod.compiledSchemaFor;
14
14
  export const compiledModuleFor = mod.compiledModuleFor;
15
+ export const bunTarget = mod.bunTarget;
16
+ export const verify = mod.verify;
15
17
  export const compiledOptions = mod.compiledOptions;
16
18
  export default mod;
package/compiled.d.ts CHANGED
@@ -3,7 +3,9 @@
3
3
  import type { ValidationResult } from './index.js';
4
4
 
5
5
  export interface CompiledModule {
6
- validate(data: unknown): { valid: boolean; errors: unknown[] };
6
+ // A module's accepted result is frozen, so the array is read-only here;
7
+ // under Bun's ahead-of-time compiler a type error fails the build.
8
+ validate(data: unknown): { valid: boolean; errors: readonly unknown[] };
7
9
  isValid(data: unknown): boolean;
8
10
  }
9
11
 
package/index.browser.mjs CHANGED
@@ -15,6 +15,6 @@ import { toTypeScript, renderPretty, renderCompact, toOutput, toRetryMessage, de
15
15
 
16
16
  core._registerTiering(tiering.createTiering(() => import('./lib/codegen-engine.js')));
17
17
 
18
- export const { Validator, compile, validate, validateAsync, parseAsync, version, createPaddedBuffer, SIMDJSON_PADDING, parseJSON, defineSchema } = core;
18
+ export const { Validator, compile, validate, validateAsync, parseAsync, version, createPaddedBuffer, SIMDJSON_PADDING, parseJSON, defineSchema, schemaOf } = core;
19
19
  export { toTypeScript, renderPretty, renderCompact, toOutput, toRetryMessage, describeSchema, renderJSON };
20
- export default { Validator, compile, validate, validateAsync, parseAsync, version, createPaddedBuffer, SIMDJSON_PADDING, parseJSON, toTypeScript, defineSchema, renderPretty, renderCompact, toOutput, toRetryMessage, describeSchema, renderJSON };
20
+ export default { Validator, compile, validate, validateAsync, parseAsync, version, createPaddedBuffer, SIMDJSON_PADDING, parseJSON, toTypeScript, defineSchema, schemaOf, renderPretty, renderCompact, toOutput, toRetryMessage, describeSchema, renderJSON };
package/index.bundle.mjs CHANGED
@@ -10,6 +10,6 @@ import { toTypeScript, renderPretty, renderCompact, toOutput, toRetryMessage, de
10
10
 
11
11
  core._registerCodegen(engine);
12
12
 
13
- export const { Validator, compile, validate, validateAsync, parseAsync, version, createPaddedBuffer, SIMDJSON_PADDING, parseJSON, defineSchema } = core;
13
+ export const { Validator, compile, validate, validateAsync, parseAsync, version, createPaddedBuffer, SIMDJSON_PADDING, parseJSON, defineSchema, schemaOf } = core;
14
14
  export { toTypeScript, renderPretty, renderCompact, toOutput, toRetryMessage, describeSchema, renderJSON };
15
- export default { Validator, compile, validate, validateAsync, parseAsync, version, createPaddedBuffer, SIMDJSON_PADDING, parseJSON, toTypeScript, defineSchema, renderPretty, renderCompact, toOutput, toRetryMessage, describeSchema, renderJSON };
15
+ export default { Validator, compile, validate, validateAsync, parseAsync, version, createPaddedBuffer, SIMDJSON_PADDING, parseJSON, toTypeScript, defineSchema, schemaOf, renderPretty, renderCompact, toOutput, toRetryMessage, describeSchema, renderJSON };
package/index.d.ts CHANGED
@@ -678,3 +678,12 @@ export function toTypeScript(schema: object, options?: { name?: string }): strin
678
678
  * Requires TypeScript >= 5.0 for the `const` type parameter.
679
679
  */
680
680
  export declare function defineSchema<const S extends JSONSchema>(schema: S): S;
681
+
682
+ /**
683
+ * The JSON Schema of a TypeScript type, written in at build time by
684
+ * `@ata-project/unplugin` from the type and the JSDoc tags on its properties
685
+ * (`@format email`, `@minimum 1`, `@pattern`, `@minLength`, `@maxItems`,
686
+ * `@asType integer`). `new Validator(schemaOf<User>())` then compiles like any
687
+ * static schema. Without the plugin the call throws: it has no run-time form.
688
+ */
689
+ export declare function schemaOf<T>(): JSONSchema;
package/index.edge.mjs CHANGED
@@ -8,6 +8,6 @@ import { toTypeScript, renderPretty, renderCompact, toOutput, toRetryMessage, de
8
8
 
9
9
  core._registerAot(() => aotBrowser);
10
10
 
11
- export const { Validator, compile, validate, validateAsync, parseAsync, version, createPaddedBuffer, SIMDJSON_PADDING, parseJSON, defineSchema } = core;
11
+ export const { Validator, compile, validate, validateAsync, parseAsync, version, createPaddedBuffer, SIMDJSON_PADDING, parseJSON, defineSchema, schemaOf } = core;
12
12
  export { toTypeScript, renderPretty, renderCompact, toOutput, toRetryMessage, describeSchema, renderJSON };
13
- export default { Validator, compile, validate, validateAsync, parseAsync, version, createPaddedBuffer, SIMDJSON_PADDING, parseJSON, toTypeScript, defineSchema, renderPretty, renderCompact, toOutput, toRetryMessage, describeSchema, renderJSON };
13
+ export default { Validator, compile, validate, validateAsync, parseAsync, version, createPaddedBuffer, SIMDJSON_PADDING, parseJSON, toTypeScript, defineSchema, schemaOf, renderPretty, renderCompact, toOutput, toRetryMessage, describeSchema, renderJSON };
package/index.js CHANGED
@@ -20,6 +20,53 @@ core._setCodegenLoader(loadCodegen);
20
20
  const warm = setTimeout(loadCodegen, 0);
21
21
  if (typeof warm === 'object' && typeof warm.unref === 'function') warm.unref();
22
22
 
23
+ // A process whose first check comes after the event loop has turned, a
24
+ // server's first request, pays on that check for V8 compiling the generator's
25
+ // functions and the error builders: on a four-field schema 5 ms for the
26
+ // verdict and 7 to 10 ms for the first error list, measured with the
27
+ // generator already loaded. One check of a small schema, accepted and
28
+ // rejected, run once the loop is free, moves that off the request: 0.5 and
29
+ // 0.7 ms after it (the same warm-up as the browser build's, lib/tiering.js).
30
+ // It runs only where a validator was built and is still uncompiled when the
31
+ // loop turns: a script that builds one and checks in the same tick never runs
32
+ // it, and the timer is unref'd, so a finished script does not stay for it.
33
+ // ATA_NO_WARM=1 turns it off.
34
+ let built = null;
35
+ core._registerBuiltHook((v) => {
36
+ if (built === null) {
37
+ built = [];
38
+ const t = setTimeout(warmUp, 0);
39
+ if (typeof t === 'object' && typeof t.unref === 'function') t.unref();
40
+ }
41
+ if (built.length < 8) built.push(new WeakRef(v));
42
+ });
43
+ function warmUp () {
44
+ const list = built;
45
+ built = [];
46
+ core._registerBuiltHook(null);
47
+ if (process.env.ATA_NO_WARM === '1') return;
48
+ // Untouched: neither compiled for validate() nor a verdict or validate
49
+ // method read off it (the prototype's accessors bind the method on the
50
+ // instance at the first read, a verdict-only compile sets no other mark).
51
+ const untouched = (v) => !v._initialized && !Object.prototype.hasOwnProperty.call(v, 'isValidObject') && !Object.prototype.hasOwnProperty.call(v, 'validate');
52
+ if (!list.some((r) => { const v = r.deref(); return v !== undefined && untouched(v); })) return;
53
+ try {
54
+ const v = new core.Validator({
55
+ type: 'object',
56
+ properties: {
57
+ a: { type: 'string', minLength: 2, maxLength: 9, pattern: '^[a-z]+$', format: 'email' },
58
+ b: { type: 'integer', minimum: 1, maximum: 9 },
59
+ c: { type: 'array', items: { type: 'string' }, maxItems: 3 },
60
+ d: { const: true },
61
+ e: { type: 'object', properties: { f: { type: 'string' } }, required: ['f'] },
62
+ },
63
+ required: ['a', 'b'],
64
+ });
65
+ v.isValidObject({ a: 'ab@c.de', b: 2, c: ['x'], d: true, e: { f: 'x' } });
66
+ void v.validate({ a: '1', b: 0, c: [1], d: false, e: {} }).errors.length;
67
+ } catch { /* a warm-up never reaches the caller */ }
68
+ }
69
+
23
70
  // The public names. lib/public-api.js holds the same for index.edge.js; they
24
71
  // are written out here rather than required so that loading the package in
25
72
  // Node reads one file fewer (tests/test_edge_entry.js and
@@ -91,6 +138,7 @@ module.exports = {
91
138
  parseJSON: core.parseJSON,
92
139
  toTypeScript,
93
140
  defineSchema: core.defineSchema,
141
+ schemaOf: core.schemaOf,
94
142
  renderPretty,
95
143
  renderCompact,
96
144
  toOutput,
package/index.mjs CHANGED
@@ -1,3 +1,3 @@
1
1
  import mod from './index.js';
2
- export const { Validator, compile, validate, validateAsync, parseAsync, version, createPaddedBuffer, SIMDJSON_PADDING, parseJSON, toTypeScript, defineSchema, renderPretty, renderCompact, toOutput, toRetryMessage, describeSchema, renderJSON } = mod;
2
+ export const { Validator, compile, validate, validateAsync, parseAsync, version, createPaddedBuffer, SIMDJSON_PADDING, parseJSON, toTypeScript, defineSchema, schemaOf, renderPretty, renderCompact, toOutput, toRetryMessage, describeSchema, renderJSON } = mod;
3
3
  export default mod;
package/index.node.mjs CHANGED
@@ -6,5 +6,5 @@
6
6
  // static import they can follow.
7
7
  import { createRequire } from 'node:module';
8
8
  const mod = createRequire(import.meta.url)('./index.js');
9
- export const { Validator, compile, validate, validateAsync, parseAsync, version, createPaddedBuffer, SIMDJSON_PADDING, parseJSON, toTypeScript, defineSchema, renderPretty, renderCompact, toOutput, toRetryMessage, describeSchema, renderJSON } = mod;
9
+ export const { Validator, compile, validate, validateAsync, parseAsync, version, createPaddedBuffer, SIMDJSON_PADDING, parseJSON, toTypeScript, defineSchema, schemaOf, renderPretty, renderCompact, toOutput, toRetryMessage, describeSchema, renderJSON } = mod;
10
10
  export default mod;
package/lib/aot-build.js CHANGED
@@ -202,7 +202,29 @@ function pruneRegistry(roots, byId) {
202
202
  return out;
203
203
  }
204
204
 
205
+ function bunTargetPath (output) {
206
+ const ext = path.extname(output);
207
+ return output.slice(0, output.length - ext.length) + '.bun.ts';
208
+ }
209
+
205
210
  async function build(opts) {
211
+ // `--target bun`: every module written for JavaScriptCore, the plain ones
212
+ // included, since the executable the target is for runs on it.
213
+ const { setTargetEngine } = require('./js-compiler');
214
+ const prevEngine = setTargetEngine(opts.target === 'bun' ? 'jsc-aot' : null);
215
+ // An executable compiled ahead of time pays no bundle size and answers
216
+ // rejections: the one-pass error function (3.6x cheaper on a rejection, 20%
217
+ // larger gzipped) is the default there, and schema-source frames, which it
218
+ // does not carry, are off unless asked for.
219
+ if (opts.target === 'bun') opts = { ...opts, onePass: opts.onePass === undefined ? true : opts.onePass, source: opts.source === true ? true : false };
220
+ try {
221
+ return await buildWith(opts);
222
+ } finally {
223
+ setTargetEngine(prevEngine);
224
+ }
225
+ }
226
+
227
+ async function buildWith(opts) {
206
228
  const globs = opts.globs || [];
207
229
  if (globs.length === 0) throw new Error('build: at least one glob required');
208
230
  const format = opts.format || 'esm';
@@ -226,6 +248,10 @@ async function build(opts) {
226
248
 
227
249
  const compiled = [];
228
250
  const cached = [];
251
+ // --target bun: the .bun.ts written beside a module, and the schemas whose
252
+ // rules a type cannot carry.
253
+ const bunTargets = [];
254
+ const bunDeclined = [];
229
255
  const skipped = [];
230
256
  const failed = [];
231
257
  const warnings = [];
@@ -345,7 +371,10 @@ async function build(opts) {
345
371
  // the registry is put together from such modules: always rebuilt.
346
372
  !cacheEntry.usesSiblings &&
347
373
  fs.existsSync(output) &&
348
- cacheEntry.outputHash === hashContent(fs.readFileSync(output))
374
+ cacheEntry.outputHash === hashContent(fs.readFileSync(output)) &&
375
+ // The Bun target is written beside the module; when it is asked for
376
+ // and missing, the module is rebuilt so that it gets written.
377
+ (opts.target !== 'bun' || fs.existsSync(bunTargetPath(output)))
349
378
  );
350
379
  if (opts.check) {
351
380
  if (fresh) cached.push({ input, output });
@@ -460,12 +489,25 @@ async function build(opts) {
460
489
  continue;
461
490
  }
462
491
  compiled.push({ input, output, bytes: outBytes, gzipBytes: gzBytes, errors: e.mode });
492
+ const stem = path.basename(input, path.extname(input)).replace(/\.schema$/, '');
493
+ const typeName = stem
494
+ .replace(/[^A-Za-z0-9_]/g, '_')
495
+ .replace(/^([a-z])/, (m) => m.toUpperCase()) || 'Data';
496
+ if (opts.target === 'bun') {
497
+ const { writeBunTarget, Declined } = require('./bun-target');
498
+ try {
499
+ const w = writeBunTarget({ schema: e.schema, input, output, typeName });
500
+ bunTargets.push({ input, output: w.bunPath, definitions: w.definitions });
501
+ } catch (err) {
502
+ if (!(err instanceof Declined)) throw err;
503
+ // No type can be written; the plain module stands alone.
504
+ const reason = `--target bun: ${err.message}; the plain module stands alone`;
505
+ if (opts.strict) { failed.push({ input, error: reason }); continue; }
506
+ bunDeclined.push({ input, reason });
507
+ }
508
+ }
463
509
  if (opts.types !== false) {
464
510
  const { toTypeScript } = require('./ts-gen');
465
- const stem = path.basename(input, path.extname(input)).replace(/\.schema$/, '');
466
- const typeName = stem
467
- .replace(/[^A-Za-z0-9_]/g, '_')
468
- .replace(/^([a-z])/, (m) => m.toUpperCase()) || 'Data';
469
511
  const dts = toTypeScript(e.schema, { name: typeName, applyDefaults: /\nfunction applyDefaults\(/.test(src) });
470
512
  const ext = path.extname(output);
471
513
  const dtsExt = ext === '.mjs' ? '.d.mts'
@@ -514,7 +556,7 @@ async function build(opts) {
514
556
  // module, whose errors come from the runtime. Only the written one counts.
515
557
  const fromRuntime = new Set(entries.filter((e) => e.mode !== 'inline').map((e) => e.input));
516
558
  const kept = warnings.filter((w) => !(w.kind === 'error-detail' && fromRuntime.has(w.input)));
517
- return { compiled, cached, skipped, failed, warnings: kept, runtime: runtimeReport };
559
+ return { compiled, cached, skipped, failed, warnings: kept, runtime: runtimeReport, bunTargets, bunDeclined };
518
560
  }
519
561
 
520
562
  async function watch(opts, onReport) {
@@ -647,6 +689,10 @@ function compiledModuleFor(schema, opts) {
647
689
  try {
648
690
  src = aot.toStandaloneModule(validatorFor(schema), {
649
691
  format: (opts && opts.format) || 'esm',
692
+ // The one-pass error function where the caller asks (a plugin writing
693
+ // for a Bun executable): 3.6x cheaper on a rejection, 20% larger gzipped.
694
+ onePass: opts && opts.onePass === true ? true : undefined,
695
+ source: opts && opts.onePass === true ? false : undefined,
650
696
  // Errors in the runtime's order, for a reader that does not sort them
651
697
  // itself (a form resolver); fromCompiled sorts, so a plugin putting
652
698
  // that wrapper around the module leaves it off.
@@ -664,7 +710,28 @@ function compiledModuleFor(schema, opts) {
664
710
  // not export this, and a plugin should then replace only calls without options.
665
711
  const { COMPILED_OPTIONS: compiledOptions } = require('./compiled');
666
712
 
713
+ // For a build plugin: the Bun target (lib/bun-target.js) and the verification
714
+ // (lib/verify.js), loaded on first use.
715
+ const bunTarget = {
716
+ get schemaToBunType () { return require('./bun-target').schemaToBunType },
717
+ get toBunModule () { return require('./bun-target').toBunModule },
718
+ get writeBunTarget () { return require('./bun-target').writeBunTarget },
719
+ get Declined () { return require('./bun-target').Declined },
720
+ // The generated code's target engine (lib/js-compiler): a plugin that
721
+ // writes for a Bun executable compiled ahead of time sets 'jsc-aot'.
722
+ setTargetEngine (engine) { return require('./js-compiler').setTargetEngine(engine) },
723
+ }
724
+ const verify = {
725
+ get verifyOne () { return require('./verify').verifyOne },
726
+ get verifySource () { return require('./verify').verifySource },
727
+ get generate () { return require('./verify').generate },
728
+ get renderReport () { return require('./verify').renderReport },
729
+ get ok () { return require('./verify').ok },
730
+ }
731
+
667
732
  module.exports = {
733
+ bunTarget,
734
+ verify,
668
735
  compiledOptions,
669
736
  compiledEligible,
670
737
  compiledSchemaFor,