ata-validator 1.56.0 → 1.57.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
@@ -584,8 +584,11 @@ CLI options:
584
584
  | `-f, --format <fmt>` | `esm` | `esm` or `cjs` |
585
585
  | `--name <TypeName>` | from filename | Root type name in the `.d.ts` |
586
586
  | `--abort-early` | off | Generate the stub-error variant, about a third of the source |
587
+ | `--order` | off | Hand errors out in the runtime's order (schema declaration order), not the order the collector found them in |
587
588
  | `--no-types` | off | Skip the `.d.mts` / `.d.cts` output |
588
589
 
590
+ A module answers `validate()` as the runtime does, with two differences a caller reading the module directly should know. The runtime fills in `default` values before it checks (`useDefaults` is on unless turned off), and the module does not: where the schema declares a default the module exports `applyDefaults(data)`, the runtime's own pass, to call first. And the module returns its errors in the order its collector found them, which can put a field's `format` error before its `pattern` error, or behind a `$ref` a definition's `required` before its `properties`; `--order` sorts them as the runtime does. It is off by default because it costs bytes: 225 more gzipped for an eight-field form, about 1 KB for a small schema with references, whose key skeleton the module then carries, and 12.6% over 267 SchemaStore schemas. With both, a module matched the runtime's verdict, filled data and error list, in order, on every case of the official suites it compiles (1,500 groups, 5,324 cases across six dialects) and on 1,952 of SchemaStore's sample documents. `fromCompiled()` and modules that share the error runtime sort already and need neither.
591
+
589
592
  For a project with many schemas, `ata build <glob>` compiles them all in one command:
590
593
 
591
594
  ```bash
package/bin/ata.js CHANGED
@@ -30,6 +30,9 @@ Compile options:
30
30
  --abort-early Use stub errors (smallest bundle)
31
31
  --one-pass Report errors with the runtime's one-pass function: faster on
32
32
  rejected documents, a module 1.3 to 1.7x the size
33
+ --order Hand errors out in the runtime's order (schema declaration
34
+ order) rather than the order they were found in; adds a sort,
35
+ and for a schema with $ref its key skeleton
33
36
  --source Embed schema source map (default in development)
34
37
  --no-source Omit source map (default in production, NODE_ENV=production)
35
38
 
@@ -45,6 +48,10 @@ Build options:
45
48
  --abort-early Use stub errors (smallest bundle)
46
49
  --one-pass Report errors with the runtime's one-pass function: faster on
47
50
  rejected documents, a module 1.3 to 1.7x the size
51
+ --order Hand errors out in the runtime's order (schema declaration
52
+ order) rather than the order they were found in; adds a sort,
53
+ and for a schema with $ref its key skeleton (modules that share
54
+ the error runtime are in that order already)
48
55
  --errors <mode> Where error detail comes from: auto | inline | shared. auto
49
56
  (default) has modules share one error runtime file when that
50
57
  makes the output smaller; production builds only (--no-source)
@@ -103,6 +110,7 @@ function parseArgs(argv) {
103
110
  if (a === '--no-types') { out.opts.types = false; continue; }
104
111
  if (a === '--abort-early') { out.opts.abortEarly = true; continue; }
105
112
  if (a === '--one-pass') { out.opts.onePass = true; continue; }
113
+ if (a === '--order') { out.opts.order = true; continue; }
106
114
  if (a === '--errors') { out.opts.errors = argv[++i]; continue; }
107
115
  if (a === '--mode') { out.opts.mode = argv[++i]; continue; }
108
116
  if (a === '--base-uri') { out.opts.baseUri = argv[++i]; continue; }
@@ -250,7 +258,7 @@ function cmdCompile(args) {
250
258
  }
251
259
  const schemaFile = path.relative(process.cwd(), input) || input;
252
260
  const compileWarnings = [];
253
- const src = aot.toStandaloneModule(v, { format, abortEarly, source, sourceMap, schemaFile, onePass: args.opts.onePass === true ? true : undefined, onWarning: (w) => compileWarnings.push(w) });
261
+ const src = aot.toStandaloneModule(v, { format, abortEarly, source, sourceMap, schemaFile, onePass: args.opts.onePass === true ? true : undefined, order: args.opts.order === true ? true : undefined, onWarning: (w) => compileWarnings.push(w) });
254
262
  if (compileWarnings.length > 0) {
255
263
  if (args.opts.strict) {
256
264
  for (const w of compileWarnings) reportCompileError(input, w);
@@ -277,7 +285,7 @@ function cmdCompile(args) {
277
285
  const typeName = args.opts.name || path.basename(input, path.extname(input))
278
286
  .replace(/[^A-Za-z0-9_]/g, '_')
279
287
  .replace(/^([a-z])/, (m) => m.toUpperCase()) || 'Data';
280
- const dts = toTypeScript(schema, { name: typeName });
288
+ const dts = toTypeScript(schema, { name: typeName, applyDefaults: /\nfunction applyDefaults\(/.test(src) });
281
289
  const ext = path.extname(output);
282
290
  const dtsExt = ext === '.mjs' ? '.d.mts'
283
291
  : ext === '.cjs' ? '.d.cts'
@@ -308,6 +316,7 @@ function cmdBuild(args) {
308
316
  suffix: args.opts.suffix,
309
317
  abortEarly: !!args.opts.abortEarly,
310
318
  onePass: args.opts.onePass === true ? true : undefined,
319
+ order: args.opts.order === true ? true : undefined,
311
320
  errors: args.opts.errors,
312
321
  mode: args.opts.mode,
313
322
  baseUri: args.opts.baseUri,
package/build.d.ts CHANGED
@@ -116,6 +116,12 @@ export interface ToStandaloneModuleOptions {
116
116
  * older collector either way.
117
117
  */
118
118
  onePass?: boolean;
119
+ /**
120
+ * Hand errors out in the runtime's order, schema declaration order, rather
121
+ * than the order the collector found them in (`ata compile --order`). Adds a
122
+ * sort, and for a schema with `$ref` its key skeleton; off by default.
123
+ */
124
+ order?: boolean;
119
125
  source?: boolean;
120
126
  sourceMap?: unknown;
121
127
  schemaFile?: string;
@@ -190,7 +196,7 @@ export function compiledSchemaFor(schema: unknown): object;
190
196
  * {@link compiledEligible} declines, one the emitter cannot compile, and one
191
197
  * whose detailed errors the generator cannot produce.
192
198
  */
193
- export function compiledModuleFor(schema: unknown, opts?: { format?: 'esm' | 'cjs'; errorRuntime?: string }): string | null;
199
+ export function compiledModuleFor(schema: unknown, opts?: { format?: 'esm' | 'cjs'; errorRuntime?: string; order?: boolean }): string | null;
194
200
 
195
201
  /**
196
202
  * The Validator options `fromCompiled()` reproduces, so a plugin can tell
package/lib/aot-build.js CHANGED
@@ -288,6 +288,7 @@ async function build(opts) {
288
288
  format,
289
289
  abortEarly: !!opts.abortEarly,
290
290
  onePass: opts.onePass === true ? true : undefined,
291
+ order: opts.order === true ? true : undefined,
291
292
  source,
292
293
  sourceMap,
293
294
  schemaFile,
@@ -465,7 +466,7 @@ async function build(opts) {
465
466
  const typeName = stem
466
467
  .replace(/[^A-Za-z0-9_]/g, '_')
467
468
  .replace(/^([a-z])/, (m) => m.toUpperCase()) || 'Data';
468
- const dts = toTypeScript(e.schema, { name: typeName });
469
+ const dts = toTypeScript(e.schema, { name: typeName, applyDefaults: /\nfunction applyDefaults\(/.test(src) });
469
470
  const ext = path.extname(output);
470
471
  const dtsExt = ext === '.mjs' ? '.d.mts'
471
472
  : ext === '.cjs' ? '.d.cts'
@@ -646,6 +647,10 @@ function compiledModuleFor(schema, opts) {
646
647
  try {
647
648
  src = aot.toStandaloneModule(validatorFor(schema), {
648
649
  format: (opts && opts.format) || 'esm',
650
+ // Errors in the runtime's order, for a reader that does not sort them
651
+ // itself (a form resolver); fromCompiled sorts, so a plugin putting
652
+ // that wrapper around the module leaves it off.
653
+ order: !!(opts && opts.order === true),
649
654
  onWarning: (msg, info) => { if (info && info.kind === 'error-detail') degraded = true; },
650
655
  });
651
656
  } catch {
package/lib/aot-impl.js CHANGED
@@ -346,6 +346,20 @@ function codeTableSource(keywords, formats) {
346
346
 
347
347
  const ABORT_SHAPE = Object.freeze({ valid: false, errors: Object.freeze([Object.freeze({ code: 'ATA9000', message: 'validation failed', keyword: '__abort_early__', path: '' })]) });
348
348
 
349
+ // The runtime's defaults pass (preprocessSource in lib/codegen-engine.js),
350
+ // over the schema the runtime builds it from, as a function the module
351
+ // exports as applyDefaults. Not called by validate(): the runtime fills
352
+ // defaults before checking because useDefaults is on unless turned off, and
353
+ // a module cannot see that option, so the caller who wants the runtime's
354
+ // answer calls applyDefaults(data) first. Empty where the schema has no
355
+ // default, or where the pass declines it (a schema object containing itself).
356
+ function defaultsDecl(validator) {
357
+ const { resolveSchemaForPreprocess } = require('./validator-core')._internals;
358
+ let src = null;
359
+ try { src = require('./codegen-engine').preprocessSource(resolveSchemaForPreprocess(validator._schemaObj, validator._schemaMap), { useDefaults: true }); } catch {}
360
+ return src === null ? '' : `function applyDefaults(d) {\n${src}\n}\n`;
361
+ }
362
+
349
363
  function toStandaloneModule(validator, opts) {
350
364
  assertEmittable(validator, 'toStandaloneModule');
351
365
  validator._ensureCompiled();
@@ -374,6 +388,7 @@ function toStandaloneModule(validator, opts) {
374
388
  // notices a rejected document's cost. A build that validates large
375
389
  // documents against large schemas asks for it.
376
390
  let onePass = null;
391
+ let ordered = false;
377
392
  const wantOnePass = opts && opts.onePass === true ? 'always' : (opts && (opts.onePass === false || opts.noOnePass)) ? 'never' : 'auto';
378
393
  // Asked for explicitly, the one-pass function wins over schema-source
379
394
  // frames, which `ata compile` writes by default outside production; the
@@ -394,14 +409,20 @@ function toStandaloneModule(validator, opts) {
394
409
  `const _ER = function(e) { _ataStamp(e); this.valid = false; this.errors = e; };\n` +
395
410
  `${codeTableSource(onePass.keywords, onePass.formats)}const _vC = (function() {\n${onePass.core}\n})();\n`;
396
411
  } else if (!abortEarly) {
397
- // A module returns the collector's errors as they are and never sorts
398
- // them, so the ordinal the runtime sorts by (`_o`) has no reader there;
399
- // it leaked into every error object a module reported.
412
+ // With `order: true` the module hands its errors out in the runtime's
413
+ // order (lib/aot-order.js); without it, in the collector's, as it always
414
+ // did, since the sort costs every module bytes (12.6% over SchemaStore's,
415
+ // gzipped, most of it the key skeleton of schemas with references) and a
416
+ // reader that sorts (fromCompiled) does not need it. Where the module
417
+ // sorts by each error site's ordinal, the collector writes it (`_o`).
418
+ const schemaForErrors = typeof validator._schemaObj === 'object' ? validator._schemaObj : {};
419
+ ordered = !!(opts && opts.order === true);
420
+ const noOrdinal = !ordered || !require(AOT_ORDER_MODULE).wantsOrdinals(schemaForErrors);
400
421
  jsErrFn = compileToJSCodegenWithErrors(
401
- typeof validator._schemaObj === 'object' ? validator._schemaObj : {},
422
+ schemaForErrors,
402
423
  null,
403
424
  validator._userFormats,
404
- (source && sourceMap && schemaFile) ? { sourceMap, schemaFile, frames: sourceFrames, noOrdinal: true } : { noOrdinal: true },
425
+ (source && sourceMap && schemaFile) ? { sourceMap, schemaFile, frames: sourceFrames, noOrdinal } : { noOrdinal },
405
426
  );
406
427
  const errSrc = jsErrFn && jsErrFn._errSource ? jsErrFn._errSource : '';
407
428
  if (errSrc && jsErrFn._errFactory) {
@@ -537,12 +558,22 @@ function toStandaloneModule(validator, opts) {
537
558
  // module that only accepts costs no more to load than its verdict.
538
559
  compactCore = `let _ataErrs = null;\nfunction _ataErrors(d) { if (_ataErrs === null) _ataErrs = _ataCreateErrors(JSON.parse(${text})${rtOpts ? ', { ' + rtOpts + ' }' : ''}); return _ataErrs(d); }\n`;
539
560
  }
561
+ const orderDecl = errCore && ordered && errorRuntime === null && onePass === null
562
+ ? require(AOT_ORDER_MODULE).orderDecl(validator._schemaObj)
563
+ : '';
564
+ // Asked for and not given: stub errors, the one-pass function, or no error
565
+ // code generated. The caller is told rather than handed the collector's
566
+ // order as if it were the runtime's. Errors from the shared runtime come in
567
+ // the runtime's order already (errorsOf in lib/error-runtime-entry.js).
568
+ if (opts && opts.order === true && orderDecl === '' && errorRuntime === null && typeof opts.onWarning === 'function') {
569
+ opts.onWarning('errors are not in the runtime order: order applies to modules with generated error code, not to stub errors or the one-pass function', { kind: 'order' });
570
+ }
540
571
  const validBody = errorRuntime !== null
541
572
  ? 'if (_fn(data)) return VALID; const e = _ataErrors(data); return e === null ? ABORT : { valid: false, errors: e }'
542
573
  : onePass !== null
543
574
  ? 'return _vC(data)'
544
575
  : errCore
545
- ? 'return _fn(data) ? VALID : { valid: false, errors: errFn(data, true).errors }'
576
+ ? (orderDecl !== '' ? 'return _fn(data) ? VALID : { valid: false, errors: _ataOrder(errFn(data, true).errors) }' : 'return _fn(data) ? VALID : { valid: false, errors: errFn(data, true).errors }')
546
577
  : 'return _fn(data) ? VALID : ABORT';
547
578
 
548
579
  // parse(): validate, then hand back a copy holding only the properties the
@@ -625,6 +656,8 @@ function validateJSON(text) {
625
656
  const hashDecl = `const schemaHash = ${JSON.stringify(hashSrc)};\nconst ataVersion = ${JSON.stringify(ATA_VERSION)};\n`;
626
657
 
627
658
  let names = fmt.exportsSetFormats ? baseNames + ', setFormats' : baseNames;
659
+ const fillDecl = defaultsDecl(validator);
660
+ if (fillDecl) names += ', applyDefaults';
628
661
  names += ', schemaHash, ataVersion';
629
662
  if (positionsCore) names += ', validateJSON';
630
663
  const parseAlias = cloneExpr ? ', _ataParse as parse' : '';
@@ -657,9 +690,9 @@ const ABORT = Object.freeze({
657
690
  ${closureDecls}${sharedBlock}${preambleDecls}${formatDecls}const _fn = function(d) {
658
691
  ${src}
659
692
  };
660
- ${errCore}${compactCore}function isValid(data) { return _fn(data); }
693
+ ${errCore}${orderDecl}${compactCore}function isValid(data) { return _fn(data); }
661
694
  function validate(data) { ${validBody}; }
662
- ${parseCore}${positionsCore}${hashDecl}${exports}`;
695
+ ${parseCore}${positionsCore}${fillDecl}${hashDecl}${exports}`;
663
696
  }
664
697
 
665
698
  // Why a validator cannot be written as a compact module, or null when it can.
@@ -673,6 +706,8 @@ ${parseCore}${positionsCore}${hashDecl}${exports}`;
673
706
  // along: 1.49.0 bundled for such runtimes, and the generator must not break
674
707
  // that.
675
708
  const ERROR_RUNTIME_MODULE = './error-runtime';
709
+ // Reads the library's files too, at build time (see lib/aot-order.js).
710
+ const AOT_ORDER_MODULE = './aot-order';
676
711
  function loadErrorRuntime() { return require(ERROR_RUNTIME_MODULE); }
677
712
 
678
713
  // The schema hash a module stamps, once per validator: a plugin writes both
@@ -806,9 +841,11 @@ function toSchemaModule(validator, opts) {
806
841
  ? `import { createValidator as _ataCreateValidator } from ${spec};\n`
807
842
  : `const { createValidator: _ataCreateValidator } = require(${spec});\n`)
808
843
  + (regSpec === null ? '' : format === 'esm' ? `import _ataRegistry from ${regSpec};\n` : `const _ataRegistry = require(${regSpec});\n`);
844
+ const fillDecl = defaultsDecl(validator);
845
+ const expNames = 'validate, isValid' + (fillDecl ? ', applyDefaults' : '') + ', schemaHash, ataVersion';
809
846
  const exp = format === 'esm'
810
- ? 'export { validate, isValid, schemaHash, ataVersion };\nexport default { validate, isValid, schemaHash, ataVersion };\n'
811
- : 'module.exports = { validate, isValid, schemaHash, ataVersion };\nmodule.exports.default = module.exports;\n';
847
+ ? `export { ${expNames} };\nexport default { ${expNames} };\n`
848
+ : `module.exports = { ${expNames} };\nmodule.exports.default = module.exports;\n`;
812
849
  return `// Auto-generated by ata-validator ${ATA_VERSION}, do not edit.
813
850
  // Schema module: the schema; the verdict and the errors come from the runtime it imports.
814
851
  // Re-run the build after upgrading ata: the version above is what wrote this.
@@ -819,7 +856,7 @@ let _ataV = null;
819
856
  function _ataGet() { return _ataV !== null ? _ataV : (_ataV = _ataCreateValidator(${ownId !== null ? 'null' : 'JSON.parse(' + text + ')'}${rtOpts ? ', { ' + rtOpts + ' }' : ''})); }
820
857
  function isValid(data) { return _ataGet().isValid(data); }
821
858
  function validate(data) { const v = _ataGet(); if (v.isValid(data)) return VALID; const e = v.errors(data); return e === null ? ABORT : { valid: false, errors: e }; }
822
- const schemaHash = ${JSON.stringify(hashSrc)};
859
+ ${fillDecl}const schemaHash = ${JSON.stringify(hashSrc)};
823
860
  const ataVersion = ${JSON.stringify(ATA_VERSION)};
824
861
  ${exp}`;
825
862
  }
@@ -0,0 +1,115 @@
1
+ 'use strict';
2
+
3
+ // How a module written ahead of time hands out its errors in the order the
4
+ // runtime does: schema declaration order (sortErrorsBySchemaOrder in
5
+ // lib/rejections.js). A module used to return them in the order its collector
6
+ // found them, which put `additionalProperties` before `properties` and
7
+ // `format` before `pattern`, and behind a reference a definition's `required`
8
+ // before its `properties`; the first error of a field could differ from the
9
+ // runtime's.
10
+ //
11
+ // The runtime orders by the ordinal of each error's schema path and breaks
12
+ // ties by the path's rank, and comparing ranks alone gives the same order
13
+ // (lib/schema-order.js). Two ways to know them in a module:
14
+ //
15
+ // - Without a reference, the collector writes each error site's ordinal into
16
+ // the error (`_o`, noteOrdinal in lib/js-compiler-errors.js) and no two
17
+ // paths share one, so a stable sort by it is the runtime's order.
18
+ // - With one, an error behind a shared definition has its path built when the
19
+ // definition runs and carries no ordinal, and paths through a `$ref` share
20
+ // the ordinal of the `$ref`. The module ranks each path itself with
21
+ // lib/schema-order.js's own functions, embedded as their text so module and
22
+ // runtime rank with one source, over the schema's skeleton: its schema
23
+ // objects with every key, subschemas kept, every other value 0, which ranks
24
+ // every path an error can name exactly as the schema does.
25
+ //
26
+ // Node only, at build time: it reads the library's own files.
27
+ const fs = require('fs');
28
+ const path = require('path');
29
+
30
+ const _files = new Map();
31
+ function take(file, names) {
32
+ let src = _files.get(file);
33
+ if (src === undefined) { src = fs.readFileSync(path.join(__dirname, file), 'utf8'); _files.set(file, src); }
34
+ return names.map((n) => {
35
+ const m = new RegExp('^function ' + n + ' ?\\([^]*?^}$', 'm').exec(src);
36
+ if (m === null) throw new Error('aot-order: lib/' + file + ' has no top-level function ' + n);
37
+ return m[0].replace(/^\s*\/\/.*\n/gm, '');
38
+ }).join('\n');
39
+ }
40
+
41
+ // Keywords whose value is a schema, an array of schemas, or an object of them:
42
+ // the only places an error's schema path goes below a schema object.
43
+ const ONE = new Set(['additionalProperties', 'additionalItems', 'items', 'contains', 'propertyNames', 'if', 'then', 'else', 'not', 'unevaluatedItems', 'unevaluatedProperties']);
44
+ const LIST = new Set(['allOf', 'anyOf', 'oneOf', 'prefixItems', 'items']);
45
+ const MAP = new Set(['properties', 'patternProperties', '$defs', 'definitions', 'dependentSchemas', 'dependencies']);
46
+
47
+ function skeleton(node) {
48
+ if (node === null || typeof node !== 'object' || Array.isArray(node)) return 0;
49
+ const out = {};
50
+ for (const k of Object.keys(node)) {
51
+ const v = node[k];
52
+ let s = 0;
53
+ if (k === '$ref' && typeof v === 'string') s = v;
54
+ else if (Array.isArray(v) && LIST.has(k)) s = v.map(skeleton);
55
+ else if (MAP.has(k) && v !== null && typeof v === 'object' && !Array.isArray(v)) {
56
+ s = {};
57
+ for (const name of Object.keys(v)) Object.defineProperty(s, name, { value: Array.isArray(v[name]) ? 0 : skeleton(v[name]), enumerable: true, writable: true, configurable: true });
58
+ } else if (ONE.has(k) && v !== null && typeof v === 'object') s = skeleton(v);
59
+ Object.defineProperty(out, k, { value: s, enumerable: true, writable: true, configurable: true });
60
+ }
61
+ return out;
62
+ }
63
+
64
+ function hasReference(schemaObj) {
65
+ const text = JSON.stringify(schemaObj);
66
+ return text.includes('"$ref"') || text.includes('"$dynamicRef"') || text.includes('"$recursiveRef"');
67
+ }
68
+
69
+ // Whether the collector should write ordinals into the errors: only where the
70
+ // module sorts by them.
71
+ function wantsOrdinals(schemaObj) { return !hasReference(schemaObj); }
72
+
73
+ // The sort, by a key per error, stable as the runtime's insertion sort is, and
74
+ // the copies the caller gets, without the ordinal; a collapsed error's branch
75
+ // errors are ordered and copied the same way.
76
+ const ORDER = (key, cmp) => `function _ataOrder(list) {
77
+ const n = list.length, out = new Array(n), keys = new Array(n);
78
+ let prev = ${key === '_o' ? '0' : 'null'};
79
+ for (let i = 0; i < n; i++) {
80
+ const src = list[i];
81
+ const { _o, ...e } = src;
82
+ if (Array.isArray(e.branchErrors)) e.branchErrors = _ataOrder(e.branchErrors);
83
+ let k = ${key === '_o' ? "typeof src._o === 'number' ? src._o : prev" : '_ataRank(src.schemaPath)'};
84
+ prev = k;
85
+ let j = i - 1;
86
+ while (j >= 0 && ${cmp}) { keys[j + 1] = keys[j]; out[j + 1] = out[j]; j--; }
87
+ keys[j + 1] = k;
88
+ out[j + 1] = e;
89
+ }
90
+ return out;
91
+ }
92
+ `;
93
+
94
+ // Source declaring `_ataOrder(errors)`: the errors sorted as the runtime sorts
95
+ // them, without their ordinals.
96
+ function orderDecl(schemaObj) {
97
+ if (!hasReference(schemaObj)) return ORDER('_o', 'keys[j] > k');
98
+ const sk = JSON.stringify(skeleton(schemaObj));
99
+ return `const _ataSkeleton = JSON.parse(${JSON.stringify(sk).replace(/</g, '\\u003c')});
100
+ const _keyIndexCache = new WeakMap();
101
+ const _rankCache = new WeakMap();
102
+ const _documents = new WeakMap();
103
+ const CONFLICT = null;
104
+ ${take('schema-order.js', ['keyIndex', 'unescapePointerSegment', 'rankFor', 'computeRank', 'localTarget'])}
105
+ function _ataRank(p) { return rankFor(_ataSkeleton, p); }
106
+ function _ataAfter(a, b) {
107
+ if (a === null || b === null) return false;
108
+ const m = Math.min(a.length, b.length);
109
+ for (let i = 0; i < m; i++) if (a[i] !== b[i]) return a[i] > b[i];
110
+ return a.length > b.length;
111
+ }
112
+ ${ORDER('rank', '_ataAfter(keys[j], k)')}`;
113
+ }
114
+
115
+ module.exports = { orderDecl, wantsOrdinals };
@@ -857,8 +857,10 @@ function emitDefaults(node, ov, lines, st) {
857
857
  st.seen.delete(node);
858
858
  }
859
859
 
860
- // Generate a fast preprocess function via codegen instead of closure arrays
861
- function buildPreprocessCodegen(schema, options) {
860
+ // The source of the preprocess pass, or null when there is nothing to do or
861
+ // the closure passes must take the schema. A module written ahead of time
862
+ // embeds it as applyDefaults (lib/aot-impl.js), so it runs the runtime's code.
863
+ function preprocessSource(schema, options) {
862
864
  if (typeof schema !== 'object' || schema === null || !schema.properties) return null;
863
865
  const lines = [];
864
866
 
@@ -889,13 +891,19 @@ function buildPreprocessCodegen(schema, options) {
889
891
  // Data may legitimately be null or a non-object (e.g. a `['object','null']`
890
892
  // schema), so the per-property mutations must not run on it.
891
893
  lines.unshift(`if(d===null||typeof d!=='object')return`);
894
+ return lines.join('\n');
895
+ }
896
+
897
+ // Generate a fast preprocess function via codegen instead of closure arrays
898
+ function buildPreprocessCodegen(schema, options) {
899
+ const src = preprocessSource(schema, options);
900
+ if (src === null) return null;
892
901
  // The pass depends on property names, types, defaults and which objects
893
902
  // are closed, not on the constraints the verdict checks, so routes that
894
903
  // take the same shape with different limits (a page/limit querystring, an
895
904
  // id param) emit the same source. Compiling it is most of what building the
896
905
  // pass costs, so the function is kept by its source and compiled once. It
897
906
  // holds no state: it rewrites the object it is given and nothing else.
898
- const src = lines.join('\n');
899
907
  let fn = _preprocessBySource.get(src);
900
908
  if (fn === undefined) {
901
909
  try {
@@ -1020,6 +1028,7 @@ module.exports = {
1020
1028
  compileVerdict: installCodegenPaths.compileVerdict,
1021
1029
  installScanner: installCodegenPaths.installScanner,
1022
1030
  buildPreprocess: buildPreprocessCodegen,
1031
+ preprocessSource,
1023
1032
  buildParse,
1024
1033
  aot: () => require('./aot.js'),
1025
1034
  };
@@ -13,6 +13,34 @@
13
13
  let _safeRe = null;
14
14
  const compileSafe = (src) => (_safeRe || (_safeRe = require('./safe-regex'))).compileSafe(src);
15
15
 
16
+ // A pattern with no group or alternation, few bounded choices in all, and at
17
+ // most one unbounded repeat, on one character or class and only when the
18
+ // pattern starts with `^`, runs in linear time on a backtracking RegExp, so it
19
+ // takes the platform's: built in about a microsecond where the linear engine's
20
+ // automaton took 40 to 100, and faster to test. Every pattern accepted here
21
+ // also gets a RegExp from the code generator (lib/regex-linear.js), so both
22
+ // engines answer with the same matcher; tests/test_regex_plain_linear.js holds
23
+ // that and the answers.
24
+ const PLAIN_ATOM = /(?:\\[dDwWsS\\/.^$|?*+()[\]{}-]|\[\^?(?:[^\\\][^\ud800-\udfff]|\\[dws\\/.^$[\]-])(?:[^\\\][\ud800-\udfff]|\\[dws\\/.^$[\]-])*\]|[^\\[\](){}|*+?^$\ud800-\udfff])(\?|[*+]|\{(\d{1,3})(?:,(\d{1,3}))?\})?/y;
25
+ function plainLinear(src) {
26
+ const n = src.length, anchored = src[0] === '^';
27
+ if (n > 256) return false;
28
+ let i = anchored ? 1 : 0, choices = 1, loops = 0;
29
+ while (i < n) {
30
+ if (src[i] === '$' && i === n - 1) return true;
31
+ PLAIN_ATOM.lastIndex = i;
32
+ const m = PLAIN_ATOM.exec(src);
33
+ if (m === null) return false;
34
+ i = PLAIN_ATOM.lastIndex;
35
+ const q = m[1];
36
+ if (q === '?') choices *= 2;
37
+ else if (q === '*' || q === '+') { if (!anchored || ++loops > 1) return false; }
38
+ else if (q !== undefined) { const lo = +m[2], hi = m[3] === undefined ? lo : +m[3]; if (hi < lo) return false; choices *= hi - lo + 1; }
39
+ if (choices > 64) return false;
40
+ }
41
+ return true;
42
+ }
43
+
16
44
  const SCHEMA_KEYWORDS = {
17
45
  single: ['additionalProperties', 'contains', 'propertyNames', 'if', 'then', 'else', 'not', 'items', 'unevaluatedItems', 'unevaluatedProperties'],
18
46
  maps: ['$defs', 'definitions', 'properties', 'patternProperties', 'dependentSchemas'],
@@ -912,6 +940,8 @@ class Interpreter {
912
940
  // Unicode property escapes need a real RegExp in unicode mode; the
913
941
  // linear engine does not understand them.
914
942
  try { re = new RegExp(src, 'u'); } catch { re = new RegExp(src); }
943
+ } else if (plainLinear(src)) {
944
+ re = new RegExp(src);
915
945
  } else {
916
946
  try {
917
947
  re = compileSafe(src);
@@ -1534,6 +1564,7 @@ function createInterpreter(schema, options) {
1534
1564
  }
1535
1565
 
1536
1566
  module.exports = { createInterpreter };
1567
+ Object.defineProperty(module.exports, '_plainLinear', { value: plainLinear, enumerable: false });
1537
1568
  Object.defineProperty(module.exports, '_setPlanCompiler', { value: setPlanCompiler, enumerable: false });
1538
1569
  Object.defineProperty(module.exports, '_setCompat', { value: setCompat, enumerable: false });
1539
1570
  // The resolver, for the code generator: it resolves references at compile
package/lib/tiering.js CHANGED
@@ -111,6 +111,7 @@ function createTiering (load) {
111
111
  // A call counter for one validator: the TIER_CALLS-th call asks for the
112
112
  // generator and has the validator rebuilt once it is in.
113
113
  function counter (v) {
114
+ if (warm === 1) warm = 2;
114
115
  let calls = 0, start = 0;
115
116
  return () => {
116
117
  if (++calls < TIER_CALLS) { if (calls === 1) start = now(); return; }
@@ -123,8 +124,94 @@ function createTiering (load) {
123
124
  };
124
125
  }
125
126
 
127
+ // A page's first check pays for V8 compiling the engine and the error
128
+ // builders (3.1 ms of an eight-field form's first submit, 0.4 ms once any
129
+ // other schema had been checked) and for building its own schema's engine.
130
+ // Both move to when the page is idle, or 200 ms later where there is no
131
+ // requestIdleCallback (Safari, workers): one check of a small schema, once
132
+ // per page and dropped when a validator compiles first, then the engine for
133
+ // each validator built since, at most eight, kept aside. A validator adopts
134
+ // it at its first check only if every input is still the same, its schema
135
+ // text included, so addSchema() or a change to the schema in between is
136
+ // honored as before.
137
+ // warm: 0 none, 1 scheduled, 2 done or dropped.
138
+ let warm = 0, scheduled = false, warming = false, adopted = 0;
139
+ const pending = [];
140
+ const shadows = new WeakMap();
141
+ const idle = (f) => {
142
+ if (typeof requestIdleCallback === 'function') return requestIdleCallback(f, { timeout: 2000 });
143
+ const t = setTimeout(f, 200);
144
+ if (t && typeof t.unref === 'function') t.unref();
145
+ };
146
+ function built (v) {
147
+ if (warming) return;
148
+ if (warm === 0) warm = 1;
149
+ if (pending.length < 8 && typeof WeakRef === 'function') pending.push(new WeakRef(v));
150
+ if (!scheduled) { scheduled = true; idle(run); }
151
+ }
152
+ function run (deadline) {
153
+ scheduled = false;
154
+ if (warm === 1) {
155
+ warm = 2;
156
+ warming = true;
157
+ try { warmUp(); } catch { /* a warm-up never reaches the page */ }
158
+ warming = false;
159
+ }
160
+ while (pending.length > 0) {
161
+ if (deadline !== undefined && deadline.timeRemaining() < 1) { scheduled = true; idle(run); return; }
162
+ const v = pending.shift().deref();
163
+ if (v === undefined || core._codegenRegistered()) continue;
164
+ let s = null;
165
+ try { s = prebuild(v); } catch { /* built as usual at the first check */ }
166
+ if (s !== null) shadows.set(v, s);
167
+ }
168
+ }
169
+
170
+ // One check of a small schema, rejected and accepted, compiles the engine's
171
+ // code for the page: the plan builder, the walk, the regex and format
172
+ // checks, the error builders.
173
+ function warmUp () {
174
+ const v = new core.Validator({
175
+ type: 'object',
176
+ properties: {
177
+ a: { type: 'string', minLength: 2, maxLength: 9, pattern: '^[a-z]+$', format: 'email' },
178
+ b: { type: 'integer', minimum: 1, maximum: 9 },
179
+ c: { type: 'array', items: { type: 'string' }, maxItems: 3 },
180
+ d: { const: true },
181
+ e: { type: 'object', properties: { f: { type: 'string' } }, required: ['f'] },
182
+ },
183
+ required: ['a', 'b'],
184
+ });
185
+ void v.validate({ a: '1', b: 0, c: [1], d: false, e: {} }).errors.length;
186
+ v.validate({ a: 'ab@c.de', b: 2, c: ['x'], d: true, e: { f: 'x' } });
187
+ }
188
+
189
+ // The engine a validator will build at its first check, built now from the
190
+ // same inputs and kept aside; null where something besides the schema object
191
+ // decides the build: custom keywords (their compile functions are the
192
+ // caller's code), registered schemas, a schema that needs rewriting.
193
+ function prebuild (v) {
194
+ if (v._initialized || v._keywords !== null || v._schemaMap.size !== 0) return null;
195
+ const raw = v._rawSchema;
196
+ const { _interpArgs, _normalizeCallerSchemaUncached } = core._internals;
197
+ if (raw === null || typeof raw !== 'object' || _normalizeCallerSchemaUncached(raw) !== raw) return null;
198
+ const args = _interpArgs(v, raw);
199
+ return { schema: raw, text: JSON.stringify(raw), args, interp: require('./interpreter').createInterpreter(raw, args) };
200
+ }
201
+
202
+ function adopt (v, schemaObj, args) {
203
+ const s = shadows.get(v);
204
+ if (s === undefined) return null;
205
+ shadows.delete(v);
206
+ if (s.schema !== schemaObj || s.text !== v._schemaStr) return null;
207
+ for (const k of Object.keys(args)) if (args[k] !== s.args[k]) return null;
208
+ adopted++;
209
+ return s.interp;
210
+ }
211
+
126
212
  // Count the interpreted engine's calls for this validator.
127
213
  function watch (v, interp) {
214
+ if (warm === 1) warm = 2;
128
215
  const v0 = interp.validate, i0 = interp.isValid;
129
216
  const tick = counter(v);
130
217
  interp.validate = (d) => { tick(); return v0.call(interp, d); };
@@ -143,7 +230,7 @@ function createTiering (load) {
143
230
  while (draining !== null) await draining;
144
231
  }
145
232
 
146
- return { watch, counter, aotStub, loadCodegen, tierUp, settled, TIER_CALLS, WINDOW_MS, get loading () { return loading; } };
233
+ return { watch, counter, built, aotStub, loadCodegen, settled, TIER_CALLS, WINDOW_MS, get loading () { return loading; }, get warm () { return warm; }, get adopted () { return adopted; }, adopt };
147
234
  }
148
235
 
149
236
  module.exports = { createTiering, TIER_CALLS };
package/lib/ts-gen.js CHANGED
@@ -197,6 +197,8 @@ function renderJsDoc(schema, indent) {
197
197
  function toTypeScript(schema, opts) {
198
198
  const options = opts || {};
199
199
  const rootName = toTypeName(options.name || 'Data');
200
+ // Whether the module beside these types exports applyDefaults (lib/aot-impl.js).
201
+ const fill = options.applyDefaults === true;
200
202
  const defs = schema && (schema.$defs || schema.definitions);
201
203
 
202
204
  const defLines = [];
@@ -240,9 +242,11 @@ export type Result = ValidResult | InvalidResult;
240
242
 
241
243
  export declare function isValid(data: unknown): data is ${rootName};
242
244
  export declare function validate(data: unknown): Result;
243
- export declare const schemaHash: string;
245
+ ${fill ? `/** Fills in the schema's \`default\` values where a key is missing, as the runtime Validator does before checking. Call it before validate() for the runtime's answer. */
246
+ export declare function applyDefaults(data: unknown): void;
247
+ ` : ''}export declare const schemaHash: string;
244
248
  export declare const ataVersion: string;
245
- declare const _default: { validate: typeof validate; isValid: typeof isValid; schemaHash: typeof schemaHash; ataVersion: typeof ataVersion };
249
+ declare const _default: { validate: typeof validate; isValid: typeof isValid; ${fill ? 'applyDefaults: typeof applyDefaults; ' : ''}schemaHash: typeof schemaHash; ataVersion: typeof ataVersion };
246
250
  export default _default;
247
251
  `;
248
252
  }
@@ -333,7 +333,13 @@ function resolveSchemaByPath(rootSchema, schemaPath) {
333
333
  //
334
334
  // A failing route sees the same handful of schemaPaths over and over, which is
335
335
  // what makes the first one worth having.
336
- const { LazyRejection, RichRejection, LazyJsonRejection, _enrichLazy, presentErrors, needsOrdering, sortErrorsBySchemaOrder, stripOrdinal } = require('./rejections');
336
+ // Loaded at the first compile, where every use below is, rather than with the
337
+ // core: with it came the schema order and diagnostic modules, run at import
338
+ // by every page that loads the package, validating or not.
339
+ let LazyRejection, RichRejection, LazyJsonRejection, _enrichLazy, presentErrors, needsOrdering, sortErrorsBySchemaOrder, stripOrdinal;
340
+ function _loadRejections() {
341
+ ({ LazyRejection, RichRejection, LazyJsonRejection, _enrichLazy, presentErrors, needsOrdering, sortErrorsBySchemaOrder, stripOrdinal } = require('./rejections'));
342
+ }
337
343
 
338
344
  // Text that does not parse, reported the same way on every path and platform:
339
345
  // ATA9001 with the parser's own message. The native addon used to say only
@@ -716,6 +722,21 @@ function resolveSchemaForPreprocess(schema, schemaMap) {
716
722
  // touched), `format` stripped under `assertFormat: false`, and the custom
717
723
  // keyword scan. The accessors then step aside for own data properties, so
718
724
  // every later read is a plain field.
725
+ // What the interpreted engine is built from, besides the schema.
726
+ function _interpArgs(self, schemaObj) {
727
+ const options = self._options;
728
+ return {
729
+ schemaMap: self._schemaMap.size > 0 ? self._schemaMap : null,
730
+ formats: self._userFormats,
731
+ v1: isV1Dialect(schemaObj),
732
+ keywords: self._keywords,
733
+ // compat's coercion where each type is checked (see the interpreter).
734
+ coerce: self._interpretOnly ? options._coerceInWalk : undefined,
735
+ discriminator: self._interpretOnly && options._discriminator === true,
736
+ numberFormats: self._interpretOnly ? options._numberFormats : undefined,
737
+ };
738
+ }
739
+
719
740
  function _materializeSchema(self) {
720
741
  const raw = self._rawSchema;
721
742
  const options = self._options;
@@ -895,6 +916,8 @@ class Validator {
895
916
  this._verdictTail = null;
896
917
  this._validateTail = null;
897
918
  this._entryExt = null;
919
+ // The browser build warms the engine once, when the page is idle (lib/tiering.js).
920
+ if (_tiering !== null) _tiering.built(this);
898
921
 
899
922
  // Public methods start as memoized accessors on the prototype; nothing is
900
923
  // allocated per instance until one is first read. See _defineLazyMethod
@@ -947,6 +970,7 @@ class Validator {
947
970
 
948
971
  _ensureCompiled() {
949
972
  if (this._initialized) return;
973
+ if (LazyRejection === undefined) _loadRejections();
950
974
  this._ensureVocabularies();
951
975
  this._initialized = true;
952
976
  // Errors behind a reference into a registered document are ordered as
@@ -1201,17 +1225,10 @@ class Validator {
1201
1225
  // No JS codegen: the interpreted engine, with or without the addon. The
1202
1226
  // buffer APIs come from _installBufferApis on first use, as they do on
1203
1227
  // every other path.
1204
- const { createInterpreter } = require('./interpreter');
1205
- const interp = createInterpreter(schemaObj, {
1206
- schemaMap: this._schemaMap.size > 0 ? this._schemaMap : null,
1207
- formats: this._userFormats,
1208
- v1: isV1Dialect(schemaObj),
1209
- keywords: this._keywords,
1210
- // compat's coercion where each type is checked (see the interpreter).
1211
- coerce: this._interpretOnly ? options._coerceInWalk : undefined,
1212
- discriminator: this._interpretOnly && options._discriminator === true,
1213
- numberFormats: this._interpretOnly ? options._numberFormats : undefined,
1214
- });
1228
+ const iargs = _interpArgs(this, schemaObj);
1229
+ // The browser build may have built it while the page was idle, from
1230
+ // the same inputs (lib/tiering.js).
1231
+ const interp = (_tiering !== null && _tiering.adopt(this, schemaObj, iargs)) || require('./interpreter').createInterpreter(schemaObj, iargs);
1215
1232
  this._engine = 'interpreter';
1216
1233
  if (_codegen === null && _tiering !== null && !_forceNapi && !this._v1Dynamic) _tiering.watch(this, interp);
1217
1234
  // A keyword that reads its data's context may also change the data (a
@@ -2506,4 +2523,4 @@ Object.defineProperty(module.exports, '_codegenRegistered', { value: () => _code
2506
2523
  Object.defineProperty(module.exports, '_tiering', { get: () => _tiering, enumerable: false });
2507
2524
  Object.defineProperty(module.exports, '_registerAot', { value: _registerAot, enumerable: false });
2508
2525
  // What the compiled paths in index.js need from this module.
2509
- Object.defineProperty(module.exports, '_internals', { value: { _jsonSyntaxRejection, ABORT_EARLY_RESULT, HYBRID_TIER_CALLS, SIMDJSON_THRESHOLD, VALID_RESULT, _bindEntry, _bindVerdict, _compileCache, _compileCacheSet, _mustReject, _rememberInstance, _verboseWrap, compileCacheKey, getNative, isV1Dialect, resolveSchemaByPath }, enumerable: false });
2526
+ Object.defineProperty(module.exports, '_internals', { value: { _interpArgs, _normalizeCallerSchemaUncached, resolveSchemaForPreprocess, _jsonSyntaxRejection, ABORT_EARLY_RESULT, HYBRID_TIER_CALLS, SIMDJSON_THRESHOLD, VALID_RESULT, _bindEntry, _bindVerdict, _compileCache, _compileCacheSet, _mustReject, _rememberInstance, _verboseWrap, compileCacheKey, getNative, isV1Dialect, resolveSchemaByPath }, enumerable: false });
package/lib/version.js CHANGED
@@ -7,4 +7,4 @@
7
7
  //
8
8
  // Kept in lockstep with package.json by `tests/test_version_sync.js`.
9
9
 
10
- module.exports = '1.56.0';
10
+ module.exports = '1.57.0';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ata-validator",
3
- "version": "1.56.0",
3
+ "version": "1.57.0",
4
4
  "description": "JSON Schema validator that compiles to plain JavaScript, still runs where code generation is blocked, and can be compiled away at build time. Passes the full official test suite.",
5
5
  "type": "commonjs",
6
6
  "main": "index.js",
@@ -185,13 +185,13 @@
185
185
  "codegen.d.ts"
186
186
  ],
187
187
  "optionalDependencies": {
188
- "@ata-validator/native-darwin-arm64": "1.56.0",
189
- "@ata-validator/native-darwin-x64": "1.56.0",
190
- "@ata-validator/native-linux-arm64-gnu": "1.56.0",
191
- "@ata-validator/native-linux-arm64-musl": "1.56.0",
192
- "@ata-validator/native-linux-x64-gnu": "1.56.0",
193
- "@ata-validator/native-linux-x64-musl": "1.56.0",
194
- "@ata-validator/native-win32-x64": "1.56.0"
188
+ "@ata-validator/native-darwin-arm64": "1.57.0",
189
+ "@ata-validator/native-darwin-x64": "1.57.0",
190
+ "@ata-validator/native-linux-arm64-gnu": "1.57.0",
191
+ "@ata-validator/native-linux-arm64-musl": "1.57.0",
192
+ "@ata-validator/native-linux-x64-gnu": "1.57.0",
193
+ "@ata-validator/native-linux-x64-musl": "1.57.0",
194
+ "@ata-validator/native-win32-x64": "1.57.0"
195
195
  },
196
196
  "peerDependencies": {
197
197
  "yaml": "^2.0.0"