ata-validator 1.46.1 → 1.48.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,6 +43,16 @@ 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.
50
+
51
+ On Deno the same package is `npm:ata-validator` or `jsr:@ata/validator`; the JSR package
52
+ re-exports the npm one at the same version, and CI runs the official suite under Deno. The
53
+ details, and what Deno Deploy's refusal of dynamic code means for it, are in
54
+ [docs/edge-runtimes.md](docs/edge-runtimes.md).
55
+
46
56
  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.
47
57
 
48
58
  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).
@@ -103,18 +113,18 @@ file.
103
113
 
104
114
  | Dimension | Schema | ata-AOT | runtime validator | Difference |
105
115
  |---|---|---|---|---|
106
- | Bundle (gzipped) | simple | 1.3 KB | 58.1 KB | 43.9x smaller |
107
- | Bundle (gzipped) | complex | 7.8 KB | 58.1 KB | 7.4x smaller |
108
- | Bundle (gzipped) | nested | 4.5 KB | 58.1 KB | 12.9x smaller |
109
- | Cold start | simple | 22 ms | 40 ms | 1.8x faster |
110
- | Throughput (1M ops) | simple | 269 Mops/s | 116 Mops/s | 2.3x faster |
111
- | Compile time | simple | 19 µs | 1.63 ms | 85x faster |
116
+ | Bundle (gzipped) | simple | 1.4 KB | 52.7 KB | 38.4x smaller |
117
+ | Bundle (gzipped) | complex | 3.0 KB | 52.7 KB | 17.5x smaller |
118
+ | Bundle (gzipped) | nested | 4.5 KB | 52.7 KB | 11.6x smaller |
119
+ | Cold start | simple | 20 ms | 37 ms | 1.9x faster |
120
+ | Throughput (1M ops) | simple | 289 Mops/s | 96 Mops/s | 3.0x faster |
121
+ | Compile time | simple | 18 µs | 1.56 ms | 85x faster |
112
122
 
113
123
  The runtime column is the default validator most frameworks ship. Reproduce on your machine
114
- with `npm run bench:aot-vs-ajv`. Numbers from one run on Apple M4 Pro, Node 25.2.1, 2026-09-28,
115
- on ata-validator 1.36.0. Across four runs throughput moved between 213 and 269 Mops/s against
116
- 94 to 116, cold start between 21 and 22 ms against 40 to 42, and the compile ratio between 79x
117
- and 90x. The throughput row times a single one-million-call
124
+ with `npm run bench:aot-vs-ajv`. Numbers from one run on Apple M4 Pro, Node 25.2.1, 2026-10-06,
125
+ on ata-validator 1.46.0. Across four runs throughput moved between 288 and 329 Mops/s against
126
+ 96 to 116, cold start between 19 and 20 ms against 37 to 39, and the compile ratio between 77x
127
+ and 85x. The throughput row times a single one-million-call
118
128
  loop of a few milliseconds, so it moves the most from run to run; treat the last three rows as
119
129
  an order of magnitude rather than a constant.
120
130
 
@@ -244,15 +254,15 @@ is the most common way to get a misleading number out of this library.
244
254
 
245
255
  | | compiled with `ata build` | runtime `new Validator(schema)` |
246
256
  |---|---|---|
247
- | In a bundle, gzipped | **2.1 KB** | 113.3 KB |
248
- | Time to a served request | **3.5 ms** | 8.5 ms |
257
+ | In a bundle, gzipped | **2.1 KB** | 125.0 KB |
258
+ | Time to a served request | **3.5 ms** | 7.6 ms |
249
259
  | Schema known when | build time | any time |
250
260
 
251
261
  The bundle row is the ten-field user schema in
252
262
  `tests/fixtures/error-dx/user.schema.json`, every export of the compiled module against
253
- `new Validator(schema)`, built with `bun build --minify --target=browser` on ata 1.45.0.
263
+ `new Validator(schema)`, built with `bun build --minify --target=browser` on ata 1.47.0.
254
264
  The startup row is a Hono route on Bun 1.4, the median of three rounds of best-of-seven, from
255
- `benchmark/bundle`, against 3.7 ms for the same app doing no validation at all, so the
265
+ `benchmark/bundle`, against 3.5 ms for the same app doing no validation at all, so the
256
266
  compiled path costs nothing measurable to start. The runtime figure is what it is because
257
267
  a schema that arrives at run time can use any keyword, so the whole engine has to be
258
268
  there. The compiled module imports nothing and contains only the checks your schema asks
@@ -280,6 +290,15 @@ const result = v.validate(data)
280
290
 
281
291
  The runtime API is unchanged from previous releases. Code written against the default validator's class keeps working through `ata-validator/compat`, which covers `compile`, `addSchema`, `addFormat`, `addKeyword`, `errorsText` and the rest of that surface, and reports errors in the same shape and order. `npx ata migrate` reports what the switch would change in a project, and what it cannot translate, before anything is written. See [docs/migration-from-ajv.md](docs/migration-from-ajv.md).
282
292
 
293
+ Coming from the default validator, one command reports what the switch touches and a second applies the mechanical part:
294
+
295
+ ```bash
296
+ npx ata migrate # per file: imports to rewrite, addFormats() calls that go, sites to look at by hand
297
+ npx ata migrate --write # rewrites the imports to ata-validator/compat and removes the calls, leaves the rest
298
+ ```
299
+
300
+ Coercion, defaults, `allErrors` and the error objects behave as before; the details and the limits are in [docs/migration-from-ajv.md](docs/migration-from-ajv.md).
301
+
283
302
  ## Usage
284
303
 
285
304
  ### Node.js
package/bin/ata.js CHANGED
@@ -1,5 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
  'use strict';
3
+ const { env } = require('../lib/env');
3
4
 
4
5
  const fs = require('fs');
5
6
  const path = require('path');
@@ -27,6 +28,8 @@ Compile options:
27
28
  --name <TypeName> Name of the top-level type in .d.ts. Default: inferred from filename
28
29
  --no-types Skip .d.ts generation
29
30
  --abort-early Use stub errors (smallest bundle)
31
+ --one-pass Report errors with the runtime's one-pass function: faster on
32
+ rejected documents, a module 1.3 to 1.7x the size
30
33
  --source Embed schema source map (default in development)
31
34
  --no-source Omit source map (default in production, NODE_ENV=production)
32
35
 
@@ -40,6 +43,8 @@ Build options:
40
43
  --suffix <str> Output filename suffix (default: ".compiled")
41
44
  -f, --format <fmt> Module format: esm | cjs. Default: esm
42
45
  --abort-early Use stub errors (smallest bundle)
46
+ --one-pass Report errors with the runtime's one-pass function: faster on
47
+ rejected documents, a module 1.3 to 1.7x the size
43
48
  --check Check (don't write); exit 1 if any output is stale
44
49
  --cache-file <path> Cache file for incremental builds (default: cache disabled)
45
50
  --max-size <bytes> Fail build if any compiled module exceeds this gzipped size
@@ -88,6 +93,7 @@ function parseArgs(argv) {
88
93
  if (a === '--name') { out.opts.name = argv[++i]; continue; }
89
94
  if (a === '--no-types') { out.opts.types = false; continue; }
90
95
  if (a === '--abort-early') { out.opts.abortEarly = true; continue; }
96
+ if (a === '--one-pass') { out.opts.onePass = true; continue; }
91
97
  if (a === '--check') { out.opts.check = true; continue; }
92
98
  if (a === '--strict') { out.opts.strict = true; continue; }
93
99
  if (a === '--strict-schema') { out.opts.strictSchema = true; continue; }
@@ -130,7 +136,7 @@ function parseArgs(argv) {
130
136
  function resolveSourceDefault (opts) {
131
137
  if (opts.source === true) return true;
132
138
  if (opts.source === false) return false;
133
- return process.env.NODE_ENV !== 'production';
139
+ return env('NODE_ENV') !== 'production';
134
140
  }
135
141
 
136
142
  function inferOutput(inputPath, format) {
@@ -232,7 +238,7 @@ function cmdCompile(args) {
232
238
  }
233
239
  const schemaFile = path.relative(process.cwd(), input) || input;
234
240
  const compileWarnings = [];
235
- const src = aot.toStandaloneModule(v, { format, abortEarly, source, sourceMap, schemaFile, onWarning: (w) => compileWarnings.push(w) });
241
+ const src = aot.toStandaloneModule(v, { format, abortEarly, source, sourceMap, schemaFile, onePass: args.opts.onePass === true ? true : undefined, onWarning: (w) => compileWarnings.push(w) });
236
242
  if (compileWarnings.length > 0) {
237
243
  if (args.opts.strict) {
238
244
  for (const w of compileWarnings) reportCompileError(input, w);
@@ -289,6 +295,7 @@ function cmdBuild(args) {
289
295
  outDir: args.opts.outDir,
290
296
  suffix: args.opts.suffix,
291
297
  abortEarly: !!args.opts.abortEarly,
298
+ onePass: args.opts.onePass === true ? true : undefined,
292
299
  check: !!args.opts.check,
293
300
  maxSize: args.opts.maxSize,
294
301
  strict: !!args.opts.strict,
package/build.d.ts CHANGED
@@ -96,13 +96,26 @@ export interface ToStandaloneModuleOptions {
96
96
  */
97
97
  strictSchema?: boolean;
98
98
  /**
99
- * Called when the module ships degraded: error detail was requested but the
100
- * error generator declined this schema, so failures report the single
99
+ * Called when the module ships degraded: error detail was requested but
100
+ * neither error path takes this schema, so failures report the single
101
101
  * ATA9000 abort-early error while the verdict stays exact.
102
102
  */
103
103
  onWarning?: (message: string) => void;
104
104
  format?: 'cjs' | 'esm';
105
105
  abortEarly?: boolean;
106
+ /**
107
+ * Report errors with the runtime's one-pass function, the program
108
+ * `validate()` runs once a validator's errors have been read: one walk that
109
+ * checks and collects, with a verdict per subtree. On a 175 KB schema and a
110
+ * 15 KB document it rejects in 39 µs where the older collector took 89. Its
111
+ * module is 1.3 to 1.7 times the older one, gzipped, so it is not the
112
+ * default; `true` asks for it, `false` refuses it. Without the option the
113
+ * one-pass function is taken only where the older collector declines the
114
+ * schema, which reported one stub error before. A module built with
115
+ * `source` frames, or from a validator with a custom format, keeps the
116
+ * older collector either way.
117
+ */
118
+ onePass?: boolean;
106
119
  source?: boolean;
107
120
  sourceMap?: unknown;
108
121
  schemaFile?: string;
package/index.js CHANGED
@@ -1,4 +1,5 @@
1
1
  'use strict';
2
+ const { compileFunction } = require('./lib/compile-fn')
2
3
 
3
4
  // The full package: the validator core with the code generator registered,
4
5
  // plus the tools no Validator calls (TypeScript generation, renderers, output
@@ -878,7 +879,7 @@ function buildPreprocessCodegen(schema, options) {
878
879
  let fn = _preprocessBySource.get(src);
879
880
  if (fn === undefined) {
880
881
  try {
881
- fn = new Function('d', src);
882
+ fn = compileFunction('d', src);
882
883
  } catch {
883
884
  fn = null;
884
885
  }
@@ -902,7 +903,7 @@ function buildParse (self, decline, extended) {
902
903
  let copy;
903
904
  try {
904
905
  // eslint-disable-next-line no-new-func
905
- copy = new Function('data', 'return ' + expr);
906
+ copy = compileFunction('data', 'return ' + expr);
906
907
  } catch {
907
908
  return decline('code generation is not allowed here');
908
909
  }
package/lib/aot-build.js CHANGED
@@ -1,4 +1,5 @@
1
1
  'use strict';
2
+ const { env } = require('./env');
2
3
 
3
4
  const crypto = require('crypto');
4
5
  const fs = require('fs');
@@ -10,7 +11,7 @@ const { buildPositionMap } = require('./source-positions');
10
11
  function resolveSourceDefault (opts) {
11
12
  if (opts.source === true) return true;
12
13
  if (opts.source === false) return false;
13
- return process.env.NODE_ENV !== 'production';
14
+ return env('NODE_ENV') !== 'production';
14
15
  }
15
16
 
16
17
  async function expandGlobs(globs) {
@@ -159,6 +160,7 @@ async function build(opts) {
159
160
  const src = aot.toStandaloneModule(v, {
160
161
  format,
161
162
  abortEarly: !!opts.abortEarly,
163
+ onePass: opts.onePass === true ? true : undefined,
162
164
  source,
163
165
  sourceMap,
164
166
  schemaFile,
package/lib/aot-impl.js CHANGED
@@ -1,4 +1,5 @@
1
1
  'use strict';
2
+ const { compileFunction } = require('./compile-fn');
2
3
 
3
4
  // All ahead-of-time (AOT) code generation lives here so the default and browser
4
5
  // entries can stay free of `fs`, `path`, and `__dirname`.
@@ -13,11 +14,12 @@
13
14
  // bundleCompact(Validator, schemas, opts)
14
15
  // loadBundle(Validator, mods, schemas, opts)
15
16
 
16
- const { compileToJSCodegenWithErrors, compileToJSCodegen, unevalContributions } = require('./js-compiler');
17
+ const { compileToJSCodegenWithErrors, compileToJSCodegen, compileToJSCombined, unevalContributions } = require('./js-compiler');
18
+ const VALID_RESULT = Object.freeze({ valid: true, errors: Object.freeze([]) });
17
19
  const { buildTargetedPositionMap } = require('./data-positions');
18
20
  const { schemaHash } = require('./schema-hash');
19
21
  const ATA_VERSION = require('./version');
20
- const { engineSource: safeRegexEngineSource } = require('./safe-regex');
22
+ const safeRegexEngineSource = () => require('./safe-regex').engineSource();
21
23
 
22
24
  // Embedded verbatim in standalone modules so the output file has no runtime
23
25
  // dependency on ata-validator. ASCII fast-path plus surrogate-aware slow path.
@@ -175,7 +177,7 @@ function emitFormatDecls(closures, mode, declKW) {
175
177
  }
176
178
  try {
177
179
  // eslint-disable-next-line no-new-func
178
- new Function('return (' + src + ')');
180
+ compileFunction('return (' + src + ')');
179
181
  } catch {
180
182
  throw new Error(`ata: custom format "${label}" has no standalone source (a bound function, a class method, or a native); use { formatMode: 'inject' } and register it with setFormats() at load time`);
181
183
  }
@@ -204,6 +206,143 @@ function sourceFrameDecls(frames, schemaFile) {
204
206
  `const __ataSS = [${rows.join(',')}].map((r) => Object.freeze({ file: __ataSF, line: r[0], col: r[1], text: __ataSL[r[2]] }));\n`;
205
207
  }
206
208
 
209
+ // The one-pass function as the module's error path. The runtime's validate()
210
+ // becomes this function once a validator's errors are read (compileToJSCombined):
211
+ // it checks and collects in one walk, with a verdict per subtree so an
212
+ // accepted subtree is never collected over. The module used to carry the
213
+ // older collector instead, which walked everything and built every path: on
214
+ // a 175 KB SchemaStore schema and a 15 KB document with two errors, 61 µs
215
+ // against 12 for this function (a report from a config validator measured
216
+ // 2 to 2.7x against the default validator on 84 to 234 KB schemas, and
217
+ // guessed at the position pass, which costs 11). The program's closure
218
+ // values are written out as declarations, the runtime helpers it shares
219
+ // with every other program as their source, and a oneOf/anyOf branch
220
+ // verdict compiled on its own as a module of its own inside an IIFE. Null
221
+ // where a value has no source form (a custom format, a keyword check), and
222
+ // the module keeps the older collector.
223
+ const ONE_PASS_RT = new Set(['_cpLen', '_A', '_ap', '_sr', '_srX', '_ER', '_EE', '_SRT', '_FB', '_VF', '__ataSafeRe']);
224
+ function onePassCore(validator, opts) {
225
+ if (validator._userFormats && Object.keys(validator._userFormats).length) return null;
226
+ const schemaObj = typeof validator._schemaObj === 'object' ? validator._schemaObj : null;
227
+ if (schemaObj === null) return null;
228
+ let emit;
229
+ try {
230
+ // No schema map, as for the older collector: a module carries one
231
+ // document. (The program could embed what a remote reference resolves to,
232
+ // but the wrapper that applies defaults around a module cannot see into
233
+ // another document, and would then answer differently from the runtime.)
234
+ emit = compileToJSCombined(schemaObj, VALID_RESULT, null, null, {
235
+ // runtimeShape: the program the runtime runs, literal for literal (the
236
+ // other shape of this generator is not what validate() runs and is
237
+ // not held to the suite); the codes and links its literals leave out
238
+ // are added once in _ER below.
239
+ moduleShape: true, emit: true, runtimeShape: true, noCounting: true,
240
+ resultShape: { Rejection: function (e) { this.valid = false; this.errors = e; }, empty: Object.freeze([]), sort: (e) => e, fallback: () => ABORT_SHAPE, verdict: null },
241
+ });
242
+ } catch { return null; }
243
+ if (!emit || typeof emit !== 'object') return null;
244
+ const rt = require('./combined-runtime');
245
+ const { DEQ_HELPER, PTR_ESC_HELPER, OWN_HELPER_CODE } = require('./js-compiler')._helperSources;
246
+ // The helpers' source without their comments and indentation.
247
+ const compact = (fn) => fn.toString().replace(/^\s*\/\/.*$/mg, '').replace(/\n\s*/g, '\n').replace(/\n+/g, '\n');
248
+ const decls = [];
249
+ const args = [];
250
+ const own = new Set();
251
+ const addOwn = (n) => { if (own.has(n)) return; own.add(n); if (n === '_h' || n === '_hall') addOwn('_hop'); if (n === '_ok') { addOwn('_PN'); addOwn('_hop'); } decls.push(OWN_HELPER_CODE[n]); };
252
+ let usesSafeRe = emit.usesSafeRe, usesCollapse = false;
253
+ for (let i = 0; i < emit.names.length; i++) {
254
+ const name = emit.names[i], val = emit.values[i];
255
+ if (name === 'R') { args.push('VALID'); continue; }
256
+ if (name === '_cpLen') { args.push('_cpLen'); continue; }
257
+ if (name === '__ataSafeRe') { usesSafeRe = true; args.push('__ataSafeRe'); continue; }
258
+ if (name === '_A') { decls.push('const _A={mo:true,lo:-1,on:0,os:[]};'); args.push('_A'); continue; }
259
+ if (name === '_ap') { decls.push(compact(rt.multiBranch) + '\nconst _ap=' + compact(rt.ap) + ';'); args.push('_ap'); continue; }
260
+ if (name === '_sr') { decls.push('const _sr=' + compact(rt.sr) + ';'); args.push('_sr'); continue; }
261
+ // The checked sort compares with the reader, which a module has no use for.
262
+ if (name === '_srX') { args.push('null'); continue; }
263
+ // Rejections: the plain shape every module reports, with the code and the
264
+ // documentation link the runtime would derive from the keyword.
265
+ if (name === '_ER') { args.push('_ER'); continue; }
266
+ if (name === '_EE') { args.push('Object.freeze([])'); continue; }
267
+ if (name === '_SRT') { args.push('function(e){return e}'); continue; }
268
+ if (name === '_FB') { args.push('function(){return ABORT}'); continue; }
269
+ if (name === '_VF') { args.push('null'); continue; }
270
+ if (name === '_PN') { addOwn('_PN'); args.push('_PN'); continue; }
271
+ if (name === '_OP' || name === '_gp') { addOwn('_PN'); args.push(name); continue; }
272
+ if (name === '_hop' || name === '_h' || name === '_ok' || name === '_hall') { addOwn(name); args.push(name); continue; }
273
+ if (name === '_pe') { decls.push(PTR_ESC_HELPER); args.push('_pe'); continue; }
274
+ if (name === '_deq') { decls.push(DEQ_HELPER); args.push('_deq'); continue; }
275
+ if (name === '_anT') { decls.push('function _anT(){return true}'); args.push('_anT'); continue; }
276
+ if (name === '_anF') { decls.push('function _anF(){return false}'); args.push('_anF'); continue; }
277
+ if (name === '_rb') { decls.push('function _rb(S,m){if(S.length!==m)S.length=m}'); args.push('_rb'); continue; }
278
+ if (name === '__ataCollapse' || name === '__ataMulti') { usesCollapse = true; args.push(name); continue; }
279
+ const expr = serializeClosure(val, name);
280
+ if (expr === null) return null;
281
+ decls.push(`const ${name}=${expr};`);
282
+ args.push(name);
283
+ }
284
+ if (usesCollapse) decls.unshift(require('./branch-collapse').embedSource());
285
+ const core = `${decls.join('\n')}\nreturn (function(${emit.names.join(',')}){${emit.source}})(${args.join(',')});`;
286
+ // The keywords and formats the program can report, for the code table.
287
+ const keywords = new Set(), formats = new Set();
288
+ for (const m of core.matchAll(/keyword:'([A-Za-z$]+)'/g)) keywords.add(m[1]);
289
+ for (const m of core.matchAll(/format:'([A-Za-z0-9-]+)'/g)) formats.add(m[1]);
290
+ return { core, usesSafeRe, keywords, formats };
291
+ }
292
+ // A closure value as source: a pattern, a name set, a frozen error literal,
293
+ // or a verdict compiled on its own (compileToJSCodegen output, which carries
294
+ // its source and its own closures). Null for anything else.
295
+ function serializeClosure(val, name) {
296
+ if (val === null || val === undefined) return String(val);
297
+ if (val && val.__ataSafe) return `__ataSafeRe(${JSON.stringify(val.source)})`;
298
+ if (val instanceof RegExp) return `new RegExp(${JSON.stringify(val.source)}${val.flags ? ', ' + JSON.stringify(val.flags) : ''})`;
299
+ if (val instanceof Set) return `new Set(${JSON.stringify([...val])})`;
300
+ if (Array.isArray(val)) return JSON.stringify(val);
301
+ if (typeof val === 'function') {
302
+ if (typeof val._source !== 'string') return null;
303
+ const inner = [];
304
+ if (val._closures) {
305
+ for (const { name: n, val: v } of val._closures) {
306
+ if (n === '__ataSafeRe') continue;
307
+ const e = serializeClosure(v, n);
308
+ if (e === null) return null;
309
+ inner.push(`const ${n}=${e};`);
310
+ }
311
+ }
312
+ return `(function(){${inner.join('\n')}\n${val._preambleSource || ''}return function(d){${val._source}}})()`;
313
+ }
314
+ if (typeof val === 'object') {
315
+ // A frozen error literal of a static site: it gets its code and link
316
+ // here, once, since the module runs in strict mode and could not add
317
+ // them to a frozen object when it is reported.
318
+ let v = val;
319
+ if (typeof val.keyword === 'string' && typeof val.schemaPath === 'string' && val.code === undefined) {
320
+ // The same rule as enrich (lib/enrich-error.js): a property that
321
+ // dependentRequired asks for is `required` with code ATA7005.
322
+ const c = val.keyword === 'required' && val.schemaPath.endsWith('/dependentRequired') ? 'ATA7005' : require('./error-codes').codeFor(val.keyword, val.params && val.params.format);
323
+ if (c !== null) v = Object.assign({ code: c }, val, { docUrl: 'https://ata-validator.com/e/' + c });
324
+ }
325
+ try { const t = JSON.stringify(v); if (t === undefined) return null; return `Object.freeze(${t})`; } catch { return null; }
326
+ }
327
+ if (typeof val === 'number' || typeof val === 'boolean' || typeof val === 'string') return JSON.stringify(val);
328
+ return null;
329
+ }
330
+ // keyword -> code for the one-pass module's rejections (lib/error-codes.js),
331
+ // holding only the keywords and formats the program can report.
332
+ function codeTableSource(keywords, formats) {
333
+ const { CODES } = require('./error-codes');
334
+ const byKw = {}, byFmt = {};
335
+ // The same first-writer rule as codeFor in lib/error-codes.js.
336
+ for (const code of Object.keys(CODES).sort()) {
337
+ const c = CODES[code];
338
+ if (keywords.has(c.keyword) && byKw[c.keyword] === undefined) byKw[c.keyword] = code;
339
+ if (c.keyword === 'format' && c.format && formats.has(c.format) && byFmt[c.format] === undefined) byFmt[c.format] = code;
340
+ }
341
+ return `const _ataKw = ${JSON.stringify(byKw)};\nconst _ataFmt = ${JSON.stringify(byFmt)};\nfunction _ataCode(k, f) { if (k === 'format' && f) { const h = _ataFmt[f]; return h === undefined ? 'ATA3099' : h; } const h = _ataKw[k]; return h === undefined ? null : h; }\n`;
342
+ }
343
+
344
+ const ABORT_SHAPE = Object.freeze({ valid: false, errors: Object.freeze([Object.freeze({ code: 'ATA9000', message: 'validation failed', keyword: '__abort_early__', path: '' })]) });
345
+
207
346
  function toStandaloneModule(validator, opts) {
208
347
  assertEmittable(validator, 'toStandaloneModule');
209
348
  validator._ensureCompiled();
@@ -219,7 +358,36 @@ function toStandaloneModule(validator, opts) {
219
358
  let errCore = '';
220
359
  let jsErrFn = null;
221
360
  const sourceFrames = new Map();
222
- if (!abortEarly) {
361
+ // Schema-source frames are attached by the older collector only; a module
362
+ // built with them keeps it. Otherwise the one-pass function takes the
363
+ // error path when asked for (`onePass: true`) and when the older collector
364
+ // declines the schema, where the module reported one stub error before.
365
+ // Not by default elsewhere: the one-pass program carries a verdict per
366
+ // subtree and a counting copy per branch, and its module is 1.3 to 1.7
367
+ // times the older one, gzipped, which a bundle notices more than it
368
+ // notices a rejected document's cost. A build that validates large
369
+ // documents against large schemas asks for it.
370
+ let onePass = null;
371
+ const wantOnePass = opts && opts.onePass === true ? 'always' : (opts && (opts.onePass === false || opts.noOnePass)) ? 'never' : 'auto';
372
+ // Asked for explicitly, the one-pass function wins over schema-source
373
+ // frames, which `ata compile` writes by default outside production; the
374
+ // caller is told, since it asked for both.
375
+ if (wantOnePass === 'always' && source && sourceMap && schemaFile) {
376
+ if (opts && typeof opts.onWarning === 'function') opts.onWarning('schema-source frames are not written with the one-pass error function; the module carries the one-pass function and no frames (pass --no-source to silence this)', { kind: 'source-frames' });
377
+ }
378
+ if (!abortEarly && (wantOnePass === 'always' || !(source && sourceMap && schemaFile)) && wantOnePass !== 'never') {
379
+ let errLen = 1;
380
+ if (wantOnePass === 'auto') {
381
+ const probe = compileToJSCodegenWithErrors(typeof validator._schemaObj === 'object' ? validator._schemaObj : {}, null, validator._userFormats, null);
382
+ errLen = probe && probe._errSource ? probe._errSource.length : 0;
383
+ }
384
+ if (wantOnePass === 'always' || errLen === 0) onePass = onePassCore(validator, opts);
385
+ }
386
+ if (onePass !== null) {
387
+ errCore = `function _ataStamp(e) { for (let i = 0; i < e.length; i++) { const x = e[i]; if (x.code === undefined) { const c = x.keyword === 'required' && typeof x.schemaPath === 'string' && x.schemaPath.endsWith('/dependentRequired') ? 'ATA7005' : _ataCode(x.keyword, x.params && x.params.format); if (c !== null) { x.code = c; x.docUrl = 'https://ata-validator.com/e/' + c; } } else if (x.docUrl === undefined) x.docUrl = 'https://ata-validator.com/e/' + x.code; if (x.branchErrors !== undefined) _ataStamp(x.branchErrors); } }\n` +
388
+ `const _ER = function(e) { _ataStamp(e); this.valid = false; this.errors = e; };\n` +
389
+ `${codeTableSource(onePass.keywords, onePass.formats)}const _vC = (function() {\n${onePass.core}\n})();\n`;
390
+ } else if (!abortEarly) {
223
391
  jsErrFn = compileToJSCodegenWithErrors(
224
392
  typeof validator._schemaObj === 'object' ? validator._schemaObj : {},
225
393
  null,
@@ -314,9 +482,11 @@ function toStandaloneModule(validator, opts) {
314
482
  const fmt = emitFormatDecls(jsFn._formatClosures, opts && opts.formatMode, 'const');
315
483
  const formatDecls = fmt.decls;
316
484
 
317
- const validBody = errCore
318
- ? 'return _fn(data) ? VALID : { valid: false, errors: errFn(data, true).errors }'
319
- : 'return _fn(data) ? VALID : ABORT';
485
+ const validBody = onePass !== null
486
+ ? 'return _vC(data)'
487
+ : errCore
488
+ ? 'return _fn(data) ? VALID : { valid: false, errors: errFn(data, true).errors }'
489
+ : 'return _fn(data) ? VALID : ABORT';
320
490
 
321
491
  // parse(): validate, then hand back a copy holding only the properties the
322
492
  // schema declares. Off by default: it costs bytes in every emitted module,
@@ -417,7 +587,7 @@ function validateJSON(text) {
417
587
  // Re-run the build after upgrading ata: the version above is what wrote this.
418
588
  ${degradedNote}'use strict';
419
589
  ${_CP_LEN_SOURCE}
420
- ${safeRePrelude(jsFn, jsErrFn)}${schemaSourceConst}const VALID = Object.freeze({ valid: true, errors: Object.freeze([]) });
590
+ ${safeRePrelude(jsFn, jsErrFn, onePass !== null && onePass.usesSafeRe ? { _usesSafeRe: true } : null)}${schemaSourceConst}const VALID = Object.freeze({ valid: true, errors: Object.freeze([]) });
421
591
  const ABORT = Object.freeze({
422
592
  valid: false,
423
593
  errors: Object.freeze([Object.freeze({
@@ -139,7 +139,7 @@ function _anF () { return false }
139
139
  function _rb (S, m) { if (S.length !== m) S.length = m }
140
140
 
141
141
  module.exports = {
142
- apState, ap, sr, srX,
142
+ apState, ap, sr, srX, multiBranch,
143
143
  _PN, _OP, _gp, _hop, _h, _ok, _hall, _pe, _anT, _anF, _rb,
144
144
  }
145
145
  // Loaded when a program first asks for them: a schema without const, enum,
@@ -0,0 +1,16 @@
1
+ 'use strict'
2
+
3
+ // The one place the package turns generated source into a function. Every
4
+ // generator (the verdict, error and one-pass generators, the JSON text
5
+ // scanner, the standalone-module probes) calls this instead of `new Function`
6
+ // itself, so a reader auditing what the package evaluates has one file to
7
+ // read, and a supply-chain scanner reports one site rather than seven files.
8
+ // `Function` is looked up at the call, not captured here, so an environment
9
+ // that blocks dynamic code (a Content Security Policy, a test that replaces
10
+ // the global) blocks this too, and the caller falls back to the interpreted
11
+ // engine as before.
12
+ function compileFunction (...args) {
13
+ return new Function(...args)
14
+ }
15
+
16
+ module.exports = { compileFunction }
@@ -254,9 +254,17 @@ function enrich (rawErr, opts) {
254
254
  // that answered wrote, the generator's internal ordering key included.
255
255
  if (rawErr.branchErrors) out.branchErrors = rawErr.branchErrors.map((b) => enrich(b, data !== undefined ? { data } : null));
256
256
 
257
+ // The frames come from the position map, which is a walk of the document.
258
+ // Given lazily (validateJSON's rejections), the walk waits for the first
259
+ // read of any error's `dataFrame` or `anchor`: a caller that reads the
260
+ // messages and paths never pays for it, and one that reads every frame
261
+ // pays once, as before. Given eagerly, the fields are set as before.
262
+ const named = (rawErr.params && (rawErr.params.additionalProperty || rawErr.params.unevaluatedProperty)) || null;
257
263
  if (positions && positions[path]) {
258
264
  const p = positions[path];
259
265
  out.dataFrame = { byteOffset: p.byteOffset, length: p.length, line: p.line, col: p.col, text: p.text };
266
+ } else if (!positions && opts && opts.lazyPositions) {
267
+ defineLazyFrames(out, path, named, opts.lazyPositions);
260
268
  }
261
269
 
262
270
  // Attach schema source frame when the validator was constructed with a
@@ -286,7 +294,6 @@ function enrich (rawErr, opts) {
286
294
  // adds the key span so a caret can sit on the property that is wrong rather
287
295
  // than on the object containing it.
288
296
  if (opts && opts.positions) {
289
- const named = (rawErr.params && (rawErr.params.additionalProperty || rawErr.params.unevaluatedProperty)) || null;
290
297
  const own = opts.positions[path];
291
298
  const child = named ? opts.positions[(path === '' ? '' : path) + '/' + named] : null;
292
299
  const src = child || own;
@@ -303,4 +310,42 @@ function enrich (rawErr, opts) {
303
310
  return out;
304
311
  }
305
312
 
313
+ // `dataFrame` and `anchor` as accessors that resolve the position map on the
314
+ // first read of either, on any error of the list (the map is shared through
315
+ // `lazy`), and then become plain data properties, or go away when the map has
316
+ // no entry for the error, which is the shape the eager path gives. Both are
317
+ // enumerable, so JSON.stringify and a deep comparison read them as values;
318
+ // a write replaces the accessor the way an assignment to a plain field would.
319
+ function defineLazyFrames (out, path, named, lazy) {
320
+ let done = false;
321
+ let frames = null;
322
+ const resolve = () => {
323
+ if (done) return frames;
324
+ done = true;
325
+ const positions = lazy();
326
+ const own = positions ? positions[path] : null;
327
+ const dataFrame = own ? { byteOffset: own.byteOffset, length: own.length, line: own.line, col: own.col, text: own.text } : undefined;
328
+ const child = positions && named ? positions[(path === '' ? '' : path) + '/' + named] : null;
329
+ const src = child || own;
330
+ let anchor;
331
+ if (src) {
332
+ anchor = { line: src.line, col: src.col, length: src.length };
333
+ if (src.keyOffset !== undefined) {
334
+ anchor.keyLine = src.keyLine;
335
+ anchor.keyCol = src.keyCol;
336
+ anchor.keyLength = src.keyLength;
337
+ }
338
+ }
339
+ frames = { dataFrame, anchor };
340
+ if (dataFrame !== undefined) Object.defineProperty(out, 'dataFrame', { value: dataFrame, enumerable: true, configurable: true, writable: true });
341
+ else delete out.dataFrame;
342
+ if (anchor !== undefined) Object.defineProperty(out, 'anchor', { value: anchor, enumerable: true, configurable: true, writable: true });
343
+ else delete out.anchor;
344
+ return frames;
345
+ };
346
+ const settable = (name) => (v) => Object.defineProperty(out, name, { value: v, enumerable: true, configurable: true, writable: true });
347
+ Object.defineProperty(out, 'dataFrame', { get () { return resolve().dataFrame; }, set: settable('dataFrame'), enumerable: true, configurable: true });
348
+ Object.defineProperty(out, 'anchor', { get () { return resolve().anchor; }, set: settable('anchor'), enumerable: true, configurable: true });
349
+ }
350
+
306
351
  module.exports = { enrich, reprValue, expectedFor, detailFor, rankFor, MISSING, DOC_BASE };
package/lib/env.js ADDED
@@ -0,0 +1,13 @@
1
+ 'use strict'
2
+
3
+ // The one place the package reads an environment variable. Everything the
4
+ // package honours is listed here: the diagnostic switches the generators
5
+ // read, the engine override, and NODE_ENV for the CLI's source-map default.
6
+ // Read at the call, not at load, so a test or a host that sets one after
7
+ // requiring the package is still heard; absent `process` (a browser) every
8
+ // read is undefined.
9
+ function env (name) {
10
+ return typeof process !== 'undefined' && process.env ? process.env[name] : undefined
11
+ }
12
+
13
+ module.exports = { env }
@@ -10,7 +10,9 @@ const { collapseBranches } = require('./branch-collapse');
10
10
  // looked-up format, a flag per keyword group) so the walk itself reads no
11
11
  // schema properties; codegen and the native engine remain the fast paths.
12
12
 
13
- const { compileSafe } = require('./safe-regex');
13
+ // Read on the first pattern (see lib/js-compiler.js for why).
14
+ let _safeRe = null;
15
+ const compileSafe = (src) => (_safeRe || (_safeRe = require('./safe-regex'))).compileSafe(src);
14
16
 
15
17
  const SCHEMA_KEYWORDS = {
16
18
  single: ['additionalProperties', 'contains', 'propertyNames', 'if', 'then', 'else', 'not', 'items', 'unevaluatedItems', 'unevaluatedProperties'],