ata-validator 1.49.0 → 1.51.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
@@ -43,10 +43,12 @@ npm install ata-validator --omit=optional
43
43
 
44
44
  or set `ATA_NO_NATIVE=1` at runtime. Typical schemas compile to specialized JS; shapes the compiler cannot represent (some `$dynamicRef`, cyclic `$ref`, unusual keyword interactions) fall back to an interpreted engine, so every schema validates in every environment. The pure-JS setup scores the same on the official suite as the native one, 1301 of 1301 Draft 2020-12 cases. The buffer and parallel APIs (`isValid` on raw buffers, `isValidPrepadded`, `isValidNDJSON`, `isValidParallel`, `countValid`, `batchIsValid`, `validateAndParse`) work without the addon too, answering through the same checks as `isValidJSON()`, slower than the addon: 177 ns against 92 for a small document on Node 25. In a browser, which has no `Buffer`, they throw and name the methods to use instead.
45
45
 
46
- Where the size of what ships matters more than nanoseconds per call, `ata-validator/lite` is
47
- the same `Validator` on the interpreted engine alone, without the code generator: the same
48
- verdicts and errors, 58 KB gzipped in a bundled worker against 136 KB for the full entry.
49
- It leaves out what needs the generator, `parse()` and the ahead-of-time bundle methods.
46
+ In Cloudflare Workers and Vercel Edge, where generated code cannot run, `ata-validator`
47
+ resolves to an entry without the code generator through the `workerd` and `edge-light` export
48
+ conditions: the same names and answers, and a Worker bundled by wrangler goes from 179.1 to
49
+ 70.4 KiB gzipped. On a page whose Content-Security-Policy blocks `unsafe-eval`, or in a
50
+ browser extension, no condition tells the bundler, so import `ata-validator/lite` there: the
51
+ same `Validator` on the interpreted engine alone.
50
52
 
51
53
  On Deno the same package is `npm:ata-validator` or `jsr:@ata/validator`; the JSR package
52
54
  re-exports the npm one at the same version, and CI runs the official suite under Deno. The
@@ -574,6 +576,19 @@ npx ata build 'schemas/*.json' --out-dir build/validators --check
574
576
 
575
577
  Run with `--watch` during development for incremental rebuilds.
576
578
 
579
+ In a production build (`--no-source`, or `NODE_ENV=production`), `ata build` can leave the
580
+ error collector out of the modules: each one carries its verdict and its schema, and takes
581
+ its errors from one `ata-errors.mjs` the build writes beside them. The build does this only
582
+ where it makes the output smaller, so a single small schema keeps its self-contained module.
583
+ The errors are the ones `new Validator(schema, { richErrors: false })` reports, in the same
584
+ order. Seven SchemaStore schemas imported into one app, minified with esbuild, go from 148.9 KB
585
+ to 63.3 KB gzipped, and from 35.8 ms to 13.8 ms to import with code generation blocked. A
586
+ rejected document costs more this way, since its errors come from an interpreted walk:
587
+ `--errors inline` keeps every module self-contained. `--mode schema` writes each module as
588
+ its schema and takes the verdict from the runtime too: forty SchemaStore schemas in one app
589
+ come to 101.6 KB gzipped against 212.6 KB as compact modules, and load and check each once in
590
+ 23.0 ms against 35.5, while a check once warm costs about five times what generated code costs.
591
+
577
592
  Bundle sizes for the 10-field user schema in `tests/fixtures/error-dx/user.schema.json`,
578
593
  minified and gzipped, measured with `bun build --minify --target=browser` on ata 1.45.0:
579
594
 
package/aot.d.ts CHANGED
@@ -24,6 +24,56 @@ export function toStandalone(validator: Validator<unknown>): string | null;
24
24
  /** Emit one standalone module (no runtime dependency) for one validator. */
25
25
  export function toStandaloneModule(validator: Validator<unknown>, opts?: StandaloneOptions): string;
26
26
 
27
+ /** Options of toCompactModule. */
28
+ export interface CompactModuleOptions {
29
+ /** Import specifier of the error runtime the module takes its errors from. */
30
+ errorRuntime: string;
31
+ /** Module format for the emitted source. Default: 'esm'. */
32
+ format?: 'esm' | 'cjs';
33
+ [key: string]: unknown;
34
+ }
35
+
36
+ /**
37
+ * Emit a compact module: the verdict function and the schema without its
38
+ * descriptive keywords, with error detail from the shared runtime at
39
+ * `opts.errorRuntime` (see errorRuntimeSource). The errors are the ones the
40
+ * runtime Validator reports with `richErrors: false`, in its order, and
41
+ * `errorMessage` is not applied, as in toStandaloneModule. Null when the validator depends on
42
+ * something outside its schema (custom keywords or formats, registered
43
+ * schemas, options other than the defaults); write the self-contained module
44
+ * then. `ata build` makes this choice by itself.
45
+ */
46
+ export function toCompactModule(validator: Validator<unknown>, opts: CompactModuleOptions): string | null;
47
+
48
+ /** Why a validator cannot be written as a compact module, or null when it can. */
49
+ export function compactDecline(validator: Validator<unknown>): string | null;
50
+
51
+ /**
52
+ * Emit a schema module: the schema without its descriptive keywords and a few
53
+ * lines; the verdict and the errors come from the runtime at `opts.runtime`
54
+ * (errorRuntimeSource with `schemaMode`). Far smaller and quicker to load than
55
+ * generated code, about five times slower per check once warm. Answers as the
56
+ * runtime Validator does with `richErrors: false`. Null where
57
+ * toCompactModule would be null. `ata build --mode schema` writes these.
58
+ */
59
+ export function toSchemaModule(validator: Validator<unknown>, opts: { runtime: string; registry?: string; format?: 'esm' | 'cjs' }): string | null;
60
+
61
+ /**
62
+ * Source of the registry module schema modules import when their schemas
63
+ * reference each other by `$id`: the schemas, without descriptive keywords,
64
+ * as one array. Pass its import specifier as `registry` to toSchemaModule,
65
+ * for a validator built with those schemas in its `schemas` option; the module
66
+ * then names its schema by `$id` and carries no copy of it. `ata build --mode
67
+ * schema` does this for inputs that reference each other.
68
+ */
69
+ export function registrySource(schemas: ReadonlyArray<object | Validator<unknown>>, format?: 'esm' | 'cjs'): string;
70
+
71
+ /** Source of the runtime compact modules (and, with `schemaMode`, schema
72
+ * modules) import. With `schemas`, the validators or schemas whose modules
73
+ * will import it, parts none of them needs are left out. Node only: it is put
74
+ * together from the library's files. */
75
+ export function errorRuntimeSource(format?: 'esm' | 'cjs', schemas?: ReadonlyArray<unknown> | null, schemaMode?: boolean): string;
76
+
27
77
  /** Emit a module of per-schema compiled validators that `loadBundle` consumes. */
28
78
  export function bundle(V: ValidatorClass, schemas: ReadonlyArray<JSONSchema | boolean | object>, opts?: StandaloneOptions): string;
29
79
 
package/aot.mjs CHANGED
@@ -1,3 +1,3 @@
1
1
  import mod from './lib/aot-impl.js';
2
- export const { toStandalone, toStandaloneModule, bundle, bundleStandalone, bundleCompact, loadBundle } = mod;
2
+ export const { toStandalone, toStandaloneModule, toCompactModule, toSchemaModule, registrySource, compactDecline, errorRuntimeSource, bundle, bundleStandalone, bundleCompact, loadBundle } = mod;
3
3
  export default mod;
package/bin/ata.js CHANGED
@@ -45,6 +45,15 @@ Build options:
45
45
  --abort-early Use stub errors (smallest bundle)
46
46
  --one-pass Report errors with the runtime's one-pass function: faster on
47
47
  rejected documents, a module 1.3 to 1.7x the size
48
+ --errors <mode> Where error detail comes from: auto | inline | shared. auto
49
+ (default) has modules share one error runtime file when that
50
+ makes the output smaller; production builds only (--no-source)
51
+ --mode <mode> code (default) | schema. schema writes each module as its
52
+ schema and takes the verdict and errors from one runtime
53
+ file: far smaller and quicker to load, slower per check
54
+ --base-uri <uri> The URI an input without $id is known by: <uri> plus its
55
+ file name, for schemas that reference a shared file by URL
56
+ or relative path
48
57
  --check Check (don't write); exit 1 if any output is stale
49
58
  --cache-file <path> Cache file for incremental builds (default: cache disabled)
50
59
  --max-size <bytes> Fail build if any compiled module exceeds this gzipped size
@@ -94,6 +103,9 @@ function parseArgs(argv) {
94
103
  if (a === '--no-types') { out.opts.types = false; continue; }
95
104
  if (a === '--abort-early') { out.opts.abortEarly = true; continue; }
96
105
  if (a === '--one-pass') { out.opts.onePass = true; continue; }
106
+ if (a === '--errors') { out.opts.errors = argv[++i]; continue; }
107
+ if (a === '--mode') { out.opts.mode = argv[++i]; continue; }
108
+ if (a === '--base-uri') { out.opts.baseUri = argv[++i]; continue; }
97
109
  if (a === '--check') { out.opts.check = true; continue; }
98
110
  if (a === '--strict') { out.opts.strict = true; continue; }
99
111
  if (a === '--strict-schema') { out.opts.strictSchema = true; continue; }
@@ -296,6 +308,9 @@ function cmdBuild(args) {
296
308
  suffix: args.opts.suffix,
297
309
  abortEarly: !!args.opts.abortEarly,
298
310
  onePass: args.opts.onePass === true ? true : undefined,
311
+ errors: args.opts.errors,
312
+ mode: args.opts.mode,
313
+ baseUri: args.opts.baseUri,
299
314
  check: !!args.opts.check,
300
315
  maxSize: args.opts.maxSize,
301
316
  strict: !!args.opts.strict,
@@ -312,7 +327,26 @@ function cmdBuild(args) {
312
327
  return;
313
328
  }
314
329
  for (const c of report.compiled) {
315
- process.stdout.write(`ata: ${c.input} -> ${c.output} (${c.bytes.toLocaleString()} bytes)\n`);
330
+ process.stdout.write(`ata: ${c.input} -> ${c.output} (${c.bytes.toLocaleString()} bytes${c.errors === 'compact' ? ', shared errors' : c.errors === 'schema' ? ', schema module' : ''})\n`);
331
+ }
332
+ if (report.runtime) {
333
+ process.stdout.write(`ata: error runtime -> ${report.runtime.output} (${report.runtime.bytes.toLocaleString()} bytes, shared by the modules above)\n`);
334
+ if (report.runtime.registry) process.stdout.write(`ata: schema registry -> ${report.runtime.registry.output} (${report.runtime.registry.bytes.toLocaleString()} bytes, the schemas the modules above reference)\n`);
335
+ }
336
+ // A module the error generator declined still has an exact verdict, but
337
+ // every failure is the one ATA9000 error. Said once per module, short,
338
+ // and once with what to do about it.
339
+ let noDetail = 0;
340
+ for (const w of report.warnings || []) {
341
+ if (w.kind === 'error-detail') {
342
+ noDetail++;
343
+ process.stderr.write(`ata: warning ${w.input}: no error detail, failures report the single ATA9000 error (the verdict is exact)\n`);
344
+ } else {
345
+ process.stderr.write(`ata: warning ${w.input}: ${w.warning}\n`);
346
+ }
347
+ }
348
+ if (noDetail > 0 && args.opts.mode !== 'schema') {
349
+ process.stderr.write(`ata: ${noDetail} module${noDetail === 1 ? '' : 's'} without error detail; with --mode schema a module takes its errors from the interpreted engine instead\n`);
316
350
  }
317
351
  for (const c of report.cached) {
318
352
  process.stdout.write(`ata: cached ${c.input}\n`);
package/build.d.ts CHANGED
@@ -190,7 +190,7 @@ export function compiledSchemaFor(schema: unknown): object;
190
190
  * {@link compiledEligible} declines, one the emitter cannot compile, and one
191
191
  * whose detailed errors the generator cannot produce.
192
192
  */
193
- export function compiledModuleFor(schema: unknown, opts?: { format?: 'esm' | 'cjs' }): string | null;
193
+ export function compiledModuleFor(schema: unknown, opts?: { format?: 'esm' | 'cjs'; errorRuntime?: string }): string | null;
194
194
 
195
195
  /**
196
196
  * The Validator options `fromCompiled()` reproduces, so a plugin can tell
@@ -0,0 +1,24 @@
1
+ /** One error as a compact module reports it. */
2
+ export interface CompactError {
3
+ code?: string;
4
+ keyword: string;
5
+ instancePath: string;
6
+ schemaPath: string;
7
+ params: Record<string, unknown>;
8
+ message: string;
9
+ docUrl?: string;
10
+ [key: string]: unknown;
11
+ }
12
+
13
+ /**
14
+ * The error function a compact module calls for a document its verdict
15
+ * rejected: the errors the runtime Validator reports with
16
+ * `richErrors: false`, in schema order, or null when it finds none.
17
+ */
18
+ export function createErrors(schema: object, options?: { v1?: boolean }): (data: unknown) => CompactError[] | null;
19
+
20
+ /** The linear-time pattern engine compact modules share (the RegExp-like object it returns has `test`). */
21
+ export function compileSafe(source: string): { test(input: string): boolean };
22
+
23
+ /** The ata-validator version this runtime belongs to. */
24
+ export const ataVersion: string;
@@ -0,0 +1,40 @@
1
+ 'use strict';
2
+
3
+ // ata-validator/error-runtime: the error runtime compact modules import, for a
4
+ // bundler plugin that writes compact modules into the application's own
5
+ // bundle. The bundler then keeps one copy, shared with ata-validator/compiled
6
+ // where both are used. `ata build` writes the same runtime as a file instead
7
+ // (errorRuntimeSource in ata-validator/aot).
8
+ const ataVersion = require('./lib/version.js');
9
+
10
+ // The interpreter and the code table load on the first call, not at import:
11
+ // a compact module calls this only when a document it rejected is asked for
12
+ // its errors.
13
+ function createErrors(schema, options) {
14
+ const { createErrors: create } = require('./lib/error-runtime-entry.js');
15
+ const { codeOf } = require('./lib/error-code-of.js');
16
+ return create(schema, codeOf, options);
17
+ }
18
+
19
+ // The linear-time pattern engine, which a compact module imports from here
20
+ // instead of embedding its own copy.
21
+ let _safe = null;
22
+ function compileSafe(src) {
23
+ if (_safe === null) _safe = require('./lib/safe-regex.js').compileSafe;
24
+ return _safe(src);
25
+ }
26
+
27
+ // Warm-up, in a browser only: the runtime's code compiles on its first use,
28
+ // which a page would otherwise pay for on its first rejected document, about
29
+ // a millisecond. When the browser is idle, one small error list is built here
30
+ // instead, once, so the first real one finds the code compiled. Nothing it
31
+ // does reaches a caller.
32
+ const WARM_SCHEMA = { type: 'object', properties: { a: { type: 'string' }, b: { type: 'integer', minimum: 0 }, c: { anyOf: [{ type: 'string' }, { type: 'null' }] }, f: { type: 'string', format: 'email' } }, required: ['d'], additionalProperties: false };
33
+ function warm() { try { createErrors(WARM_SCHEMA, { flat: true })({ a: 1, b: -1, c: 2, f: 'x', e: 0 }); } catch (_) {} }
34
+ if (typeof requestIdleCallback === 'function') { try { requestIdleCallback(warm); } catch (_) {} }
35
+ // In Node the same happens once the event loop is first free after start-up,
36
+ // on a timer that does not keep the process alive: a script that exits at
37
+ // once never runs it, and a server has it done before its first request.
38
+ else if (typeof process === 'object' && process !== null && process.versions && process.versions.node && typeof setTimeout === 'function') { try { const t = setTimeout(warm, 0); if (t && typeof t.unref === 'function') t.unref(); } catch (_) {} }
39
+
40
+ module.exports = { createErrors, compileSafe, ataVersion };
@@ -0,0 +1,3 @@
1
+ import mod from './error-runtime.js';
2
+ export const { createErrors, compileSafe, ataVersion } = mod;
3
+ export default mod;
package/index.browser.mjs CHANGED
@@ -1,5 +1,14 @@
1
- // Browser ESM entry: the same code, with the native addon stubbed out by the
2
- // bundler through the package.json "browser" field.
3
- import mod from './index.js';
4
- export const { Validator, compile, validate, validateAsync, parseAsync, version, createPaddedBuffer, SIMDJSON_PADDING, parseJSON, toTypeScript, defineSchema, renderPretty, renderCompact, toOutput, toRetryMessage, describeSchema, renderJSON } = mod;
5
- export default mod;
1
+ // Browser ESM entry, for bundlers: the validator core with the code generator
2
+ // registered, as index.js, but with each tool imported on its own (see
3
+ // lib/tools.mjs) so that the tools an application does not import stay out
4
+ // of its bundle. The native addon is stubbed by the package.json "browser"
5
+ // field.
6
+ import core from './lib/validator-core.js';
7
+ import engine from './lib/codegen-engine.js';
8
+ import { toTypeScript, renderPretty, renderCompact, toOutput, toRetryMessage, describeSchema, renderJSON } from './lib/tools.mjs';
9
+
10
+ core._registerCodegen(engine);
11
+
12
+ export const { Validator, compile, validate, validateAsync, parseAsync, version, createPaddedBuffer, SIMDJSON_PADDING, parseJSON, defineSchema } = core;
13
+ export { toTypeScript, renderPretty, renderCompact, toOutput, toRetryMessage, describeSchema, renderJSON };
14
+ export default { Validator, compile, validate, validateAsync, parseAsync, version, createPaddedBuffer, SIMDJSON_PADDING, parseJSON, toTypeScript, defineSchema, renderPretty, renderCompact, toOutput, toRetryMessage, describeSchema, renderJSON };
package/index.edge.js ADDED
@@ -0,0 +1,14 @@
1
+ 'use strict';
2
+
3
+ // The package for runtimes that cannot run generated code: Cloudflare Workers
4
+ // (the `workerd` export condition) and Vercel Edge (`edge-light`) refuse
5
+ // `new Function`, so the code generator in the full package could only ever
6
+ // fail there and fall back. This entry leaves it out and answers through the
7
+ // interpreted engine, as the full package does in those runtimes: the same
8
+ // names, the same verdicts and errors, a bundle about half the size. Modules
9
+ // from `ata build` and Validator.bundle() load as they do in a browser bundle
10
+ // (lib/aot.browser.js); the emitters are not here, as they are not there.
11
+ const core = require('./lib/validator-core.js');
12
+ core._registerAot(() => require('./lib/aot.browser.js'));
13
+
14
+ module.exports = require('./lib/public-api.js');
package/index.edge.mjs ADDED
@@ -0,0 +1,13 @@
1
+ // ESM edge entry (see index.edge.js): the core without the code generator,
2
+ // for Cloudflare Workers and Vercel Edge, with each tool imported on its own
3
+ // (lib/tools.mjs) so the ones an application does not import stay out of its
4
+ // bundle.
5
+ import core from './lib/validator-core.js';
6
+ import aotBrowser from './lib/aot.browser.js';
7
+ import { toTypeScript, renderPretty, renderCompact, toOutput, toRetryMessage, describeSchema, renderJSON } from './lib/tools.mjs';
8
+
9
+ core._registerAot(() => aotBrowser);
10
+
11
+ export const { Validator, compile, validate, validateAsync, parseAsync, version, createPaddedBuffer, SIMDJSON_PADDING, parseJSON, defineSchema } = core;
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 };