ata-validator 1.46.1 → 1.47.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 +14 -14
- package/bin/ata.js +9 -2
- package/build.d.ts +15 -2
- package/index.js +3 -2
- package/lib/aot-build.js +3 -1
- package/lib/aot-impl.js +178 -8
- package/lib/combined-runtime.js +1 -1
- package/lib/compile-fn.js +16 -0
- package/lib/env.js +13 -0
- package/lib/interpreter.js +3 -1
- package/lib/js-compiler-errors.js +3483 -0
- package/lib/js-compiler.js +362 -3704
- package/lib/native-load.js +10 -1
- package/lib/scan-compiler.js +2 -1
- package/lib/validator-core.js +5 -3
- package/lib/version.js +1 -1
- package/package.json +8 -8
package/README.md
CHANGED
|
@@ -103,18 +103,18 @@ file.
|
|
|
103
103
|
|
|
104
104
|
| Dimension | Schema | ata-AOT | runtime validator | Difference |
|
|
105
105
|
|---|---|---|---|---|
|
|
106
|
-
| Bundle (gzipped) | simple | 1.
|
|
107
|
-
| Bundle (gzipped) | complex |
|
|
108
|
-
| Bundle (gzipped) | nested | 4.5 KB |
|
|
109
|
-
| Cold start | simple |
|
|
110
|
-
| Throughput (1M ops) | simple |
|
|
111
|
-
| Compile time | simple |
|
|
106
|
+
| Bundle (gzipped) | simple | 1.4 KB | 52.7 KB | 38.4x smaller |
|
|
107
|
+
| Bundle (gzipped) | complex | 3.0 KB | 52.7 KB | 17.5x smaller |
|
|
108
|
+
| Bundle (gzipped) | nested | 4.5 KB | 52.7 KB | 11.6x smaller |
|
|
109
|
+
| Cold start | simple | 20 ms | 37 ms | 1.9x faster |
|
|
110
|
+
| Throughput (1M ops) | simple | 289 Mops/s | 96 Mops/s | 3.0x faster |
|
|
111
|
+
| Compile time | simple | 18 µs | 1.56 ms | 85x faster |
|
|
112
112
|
|
|
113
113
|
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-
|
|
115
|
-
on ata-validator 1.
|
|
116
|
-
|
|
117
|
-
and
|
|
114
|
+
with `npm run bench:aot-vs-ajv`. Numbers from one run on Apple M4 Pro, Node 25.2.1, 2026-10-06,
|
|
115
|
+
on ata-validator 1.46.0. Across four runs throughput moved between 288 and 329 Mops/s against
|
|
116
|
+
96 to 116, cold start between 19 and 20 ms against 37 to 39, and the compile ratio between 77x
|
|
117
|
+
and 85x. The throughput row times a single one-million-call
|
|
118
118
|
loop of a few milliseconds, so it moves the most from run to run; treat the last three rows as
|
|
119
119
|
an order of magnitude rather than a constant.
|
|
120
120
|
|
|
@@ -244,15 +244,15 @@ is the most common way to get a misleading number out of this library.
|
|
|
244
244
|
|
|
245
245
|
| | compiled with `ata build` | runtime `new Validator(schema)` |
|
|
246
246
|
|---|---|---|
|
|
247
|
-
| In a bundle, gzipped | **2.1 KB** |
|
|
248
|
-
| Time to a served request | **3.5 ms** |
|
|
247
|
+
| In a bundle, gzipped | **2.1 KB** | 125.0 KB |
|
|
248
|
+
| Time to a served request | **3.5 ms** | 7.6 ms |
|
|
249
249
|
| Schema known when | build time | any time |
|
|
250
250
|
|
|
251
251
|
The bundle row is the ten-field user schema in
|
|
252
252
|
`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.
|
|
253
|
+
`new Validator(schema)`, built with `bun build --minify --target=browser` on ata 1.47.0.
|
|
254
254
|
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.
|
|
255
|
+
`benchmark/bundle`, against 3.5 ms for the same app doing no validation at all, so the
|
|
256
256
|
compiled path costs nothing measurable to start. The runtime figure is what it is because
|
|
257
257
|
a schema that arrives at run time can use any keyword, so the whole engine has to be
|
|
258
258
|
there. The compiled module imports nothing and contains only the checks your schema asks
|
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
|
|
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
|
|
100
|
-
* error
|
|
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 =
|
|
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 =
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
318
|
-
? 'return
|
|
319
|
-
:
|
|
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({
|
package/lib/combined-runtime.js
CHANGED
|
@@ -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 }
|
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 }
|
package/lib/interpreter.js
CHANGED
|
@@ -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
|
-
|
|
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'],
|