ata-validator 1.56.0 → 1.58.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 +6 -2
- package/bin/ata.js +11 -2
- package/build.d.ts +7 -1
- package/lib/aot-build.js +6 -1
- package/lib/aot-impl.js +59 -11
- package/lib/aot-order.js +115 -0
- package/lib/buffer-gate.js +33 -0
- package/lib/clone-emit.js +11 -25
- package/lib/codegen-engine.js +159 -19
- package/lib/defaults.js +213 -56
- package/lib/error-codes.js +6 -0
- package/lib/formats-source.js +1 -1
- package/lib/interpreter-core.js +39 -2
- package/lib/interpreter.browser.js +34 -0
- package/lib/schema-strip.js +15 -8
- package/lib/tiering.js +88 -1
- package/lib/ts-gen.js +6 -2
- package/lib/validator-core.js +58 -30
- package/lib/version.js +1 -1
- package/package.json +10 -9
package/README.md
CHANGED
|
@@ -53,8 +53,9 @@ interpreted engine, which gives the same answers, and a validator called a thous
|
|
|
53
53
|
a second loads the generator in the background, through a dynamic import that Vite, webpack and
|
|
54
54
|
Rollup put in a chunk of their own, and is rebuilt on generated code when the page is idle. A
|
|
55
55
|
form, checked as the user types, never loads it. On a react-jsonschema-form page this takes the validator's
|
|
56
|
-
share of the first load from 134.7 KB to about
|
|
57
|
-
more when it does load
|
|
56
|
+
share of the first load from 134.7 KB to about 55 KB gzipped; the generator chunk is about 78 KB
|
|
57
|
+
more when it does load, and the plan compiler, which speeds up a schema the interpreted engine checks
|
|
58
|
+
often, comes in a 6 KB chunk once the page is idle. Where `unsafe-eval` is blocked, on a page with a strict
|
|
58
59
|
Content-Security-Policy or in a Manifest V3 extension, it is never fetched and every schema
|
|
59
60
|
stays on the interpreted engine. To have generated code from the first call, import
|
|
60
61
|
`ata-validator/codegen` once; it also makes `parse()` available in the browser, which otherwise
|
|
@@ -584,8 +585,11 @@ CLI options:
|
|
|
584
585
|
| `-f, --format <fmt>` | `esm` | `esm` or `cjs` |
|
|
585
586
|
| `--name <TypeName>` | from filename | Root type name in the `.d.ts` |
|
|
586
587
|
| `--abort-early` | off | Generate the stub-error variant, about a third of the source |
|
|
588
|
+
| `--order` | off | Hand errors out in the runtime's order (schema declaration order), not the order the collector found them in |
|
|
587
589
|
| `--no-types` | off | Skip the `.d.mts` / `.d.cts` output |
|
|
588
590
|
|
|
591
|
+
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.
|
|
592
|
+
|
|
589
593
|
For a project with many schemas, `ata build <glob>` compiles them all in one command:
|
|
590
594
|
|
|
591
595
|
```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,31 @@ 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
|
+
const schema = resolveSchemaForPreprocess(validator._schemaObj, validator._schemaMap);
|
|
359
|
+
let src = null;
|
|
360
|
+
try { src = require('./codegen-engine').preprocessSource(schema, { useDefaults: true }); } catch {}
|
|
361
|
+
if (src !== null) return `function applyDefaults(d) {\n${src}\n}\n`;
|
|
362
|
+
// The generated pass declines a recursive reference and keys matched by
|
|
363
|
+
// patterns; the runtime fills those with the closure pass, so the module
|
|
364
|
+
// carries it, as its own text (lib/defaults.js), with its schema less the
|
|
365
|
+
// descriptive keywords, `default` kept. Built on the first call.
|
|
366
|
+
const defaults = require('./defaults');
|
|
367
|
+
if (defaults.buildDefaultsApplier(schema) === null) return '';
|
|
368
|
+
const embedded = require('./schema-strip').stripAnnotationsKeepingDefaults(schema);
|
|
369
|
+
const text = JSON.stringify(JSON.stringify(embedded)).replace(/</g, '\\u003c');
|
|
370
|
+
return `const _ataDefaults = (function () {\nconst MAX_DEPTH = ${defaults.MAX_DEPTH};\n${require(AOT_ORDER_MODULE).take('defaults.js', defaults.EMBED)}\nreturn buildDefaultsApplier;\n})();\n` +
|
|
371
|
+
`let _ataFill;\nfunction applyDefaults(d) { (_ataFill || (_ataFill = _ataDefaults(JSON.parse(${text}))))(d); }\n`;
|
|
372
|
+
}
|
|
373
|
+
|
|
349
374
|
function toStandaloneModule(validator, opts) {
|
|
350
375
|
assertEmittable(validator, 'toStandaloneModule');
|
|
351
376
|
validator._ensureCompiled();
|
|
@@ -374,6 +399,7 @@ function toStandaloneModule(validator, opts) {
|
|
|
374
399
|
// notices a rejected document's cost. A build that validates large
|
|
375
400
|
// documents against large schemas asks for it.
|
|
376
401
|
let onePass = null;
|
|
402
|
+
let ordered = false;
|
|
377
403
|
const wantOnePass = opts && opts.onePass === true ? 'always' : (opts && (opts.onePass === false || opts.noOnePass)) ? 'never' : 'auto';
|
|
378
404
|
// Asked for explicitly, the one-pass function wins over schema-source
|
|
379
405
|
// frames, which `ata compile` writes by default outside production; the
|
|
@@ -394,14 +420,20 @@ function toStandaloneModule(validator, opts) {
|
|
|
394
420
|
`const _ER = function(e) { _ataStamp(e); this.valid = false; this.errors = e; };\n` +
|
|
395
421
|
`${codeTableSource(onePass.keywords, onePass.formats)}const _vC = (function() {\n${onePass.core}\n})();\n`;
|
|
396
422
|
} else if (!abortEarly) {
|
|
397
|
-
//
|
|
398
|
-
//
|
|
399
|
-
//
|
|
423
|
+
// With `order: true` the module hands its errors out in the runtime's
|
|
424
|
+
// order (lib/aot-order.js); without it, in the collector's, as it always
|
|
425
|
+
// did, since the sort costs every module bytes (12.6% over SchemaStore's,
|
|
426
|
+
// gzipped, most of it the key skeleton of schemas with references) and a
|
|
427
|
+
// reader that sorts (fromCompiled) does not need it. Where the module
|
|
428
|
+
// sorts by each error site's ordinal, the collector writes it (`_o`).
|
|
429
|
+
const schemaForErrors = typeof validator._schemaObj === 'object' ? validator._schemaObj : {};
|
|
430
|
+
ordered = !!(opts && opts.order === true);
|
|
431
|
+
const noOrdinal = !ordered || !require(AOT_ORDER_MODULE).wantsOrdinals(schemaForErrors);
|
|
400
432
|
jsErrFn = compileToJSCodegenWithErrors(
|
|
401
|
-
|
|
433
|
+
schemaForErrors,
|
|
402
434
|
null,
|
|
403
435
|
validator._userFormats,
|
|
404
|
-
(source && sourceMap && schemaFile) ? { sourceMap, schemaFile, frames: sourceFrames, noOrdinal
|
|
436
|
+
(source && sourceMap && schemaFile) ? { sourceMap, schemaFile, frames: sourceFrames, noOrdinal } : { noOrdinal },
|
|
405
437
|
);
|
|
406
438
|
const errSrc = jsErrFn && jsErrFn._errSource ? jsErrFn._errSource : '';
|
|
407
439
|
if (errSrc && jsErrFn._errFactory) {
|
|
@@ -537,12 +569,22 @@ function toStandaloneModule(validator, opts) {
|
|
|
537
569
|
// module that only accepts costs no more to load than its verdict.
|
|
538
570
|
compactCore = `let _ataErrs = null;\nfunction _ataErrors(d) { if (_ataErrs === null) _ataErrs = _ataCreateErrors(JSON.parse(${text})${rtOpts ? ', { ' + rtOpts + ' }' : ''}); return _ataErrs(d); }\n`;
|
|
539
571
|
}
|
|
572
|
+
const orderDecl = errCore && ordered && errorRuntime === null && onePass === null
|
|
573
|
+
? require(AOT_ORDER_MODULE).orderDecl(validator._schemaObj)
|
|
574
|
+
: '';
|
|
575
|
+
// Asked for and not given: stub errors, the one-pass function, or no error
|
|
576
|
+
// code generated. The caller is told rather than handed the collector's
|
|
577
|
+
// order as if it were the runtime's. Errors from the shared runtime come in
|
|
578
|
+
// the runtime's order already (errorsOf in lib/error-runtime-entry.js).
|
|
579
|
+
if (opts && opts.order === true && orderDecl === '' && errorRuntime === null && typeof opts.onWarning === 'function') {
|
|
580
|
+
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' });
|
|
581
|
+
}
|
|
540
582
|
const validBody = errorRuntime !== null
|
|
541
583
|
? 'if (_fn(data)) return VALID; const e = _ataErrors(data); return e === null ? ABORT : { valid: false, errors: e }'
|
|
542
584
|
: onePass !== null
|
|
543
585
|
? 'return _vC(data)'
|
|
544
586
|
: errCore
|
|
545
|
-
? 'return _fn(data) ? VALID : { valid: false, errors: errFn(data, true).errors }'
|
|
587
|
+
? (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
588
|
: 'return _fn(data) ? VALID : ABORT';
|
|
547
589
|
|
|
548
590
|
// parse(): validate, then hand back a copy holding only the properties the
|
|
@@ -625,6 +667,8 @@ function validateJSON(text) {
|
|
|
625
667
|
const hashDecl = `const schemaHash = ${JSON.stringify(hashSrc)};\nconst ataVersion = ${JSON.stringify(ATA_VERSION)};\n`;
|
|
626
668
|
|
|
627
669
|
let names = fmt.exportsSetFormats ? baseNames + ', setFormats' : baseNames;
|
|
670
|
+
const fillDecl = defaultsDecl(validator);
|
|
671
|
+
if (fillDecl) names += ', applyDefaults';
|
|
628
672
|
names += ', schemaHash, ataVersion';
|
|
629
673
|
if (positionsCore) names += ', validateJSON';
|
|
630
674
|
const parseAlias = cloneExpr ? ', _ataParse as parse' : '';
|
|
@@ -657,9 +701,9 @@ const ABORT = Object.freeze({
|
|
|
657
701
|
${closureDecls}${sharedBlock}${preambleDecls}${formatDecls}const _fn = function(d) {
|
|
658
702
|
${src}
|
|
659
703
|
};
|
|
660
|
-
${errCore}${compactCore}function isValid(data) { return _fn(data); }
|
|
704
|
+
${errCore}${orderDecl}${compactCore}function isValid(data) { return _fn(data); }
|
|
661
705
|
function validate(data) { ${validBody}; }
|
|
662
|
-
${parseCore}${positionsCore}${hashDecl}${exports}`;
|
|
706
|
+
${parseCore}${positionsCore}${fillDecl}${hashDecl}${exports}`;
|
|
663
707
|
}
|
|
664
708
|
|
|
665
709
|
// Why a validator cannot be written as a compact module, or null when it can.
|
|
@@ -673,6 +717,8 @@ ${parseCore}${positionsCore}${hashDecl}${exports}`;
|
|
|
673
717
|
// along: 1.49.0 bundled for such runtimes, and the generator must not break
|
|
674
718
|
// that.
|
|
675
719
|
const ERROR_RUNTIME_MODULE = './error-runtime';
|
|
720
|
+
// Reads the library's files too, at build time (see lib/aot-order.js).
|
|
721
|
+
const AOT_ORDER_MODULE = './aot-order';
|
|
676
722
|
function loadErrorRuntime() { return require(ERROR_RUNTIME_MODULE); }
|
|
677
723
|
|
|
678
724
|
// The schema hash a module stamps, once per validator: a plugin writes both
|
|
@@ -806,9 +852,11 @@ function toSchemaModule(validator, opts) {
|
|
|
806
852
|
? `import { createValidator as _ataCreateValidator } from ${spec};\n`
|
|
807
853
|
: `const { createValidator: _ataCreateValidator } = require(${spec});\n`)
|
|
808
854
|
+ (regSpec === null ? '' : format === 'esm' ? `import _ataRegistry from ${regSpec};\n` : `const _ataRegistry = require(${regSpec});\n`);
|
|
855
|
+
const fillDecl = defaultsDecl(validator);
|
|
856
|
+
const expNames = 'validate, isValid' + (fillDecl ? ', applyDefaults' : '') + ', schemaHash, ataVersion';
|
|
809
857
|
const exp = format === 'esm'
|
|
810
|
-
?
|
|
811
|
-
:
|
|
858
|
+
? `export { ${expNames} };\nexport default { ${expNames} };\n`
|
|
859
|
+
: `module.exports = { ${expNames} };\nmodule.exports.default = module.exports;\n`;
|
|
812
860
|
return `// Auto-generated by ata-validator ${ATA_VERSION}, do not edit.
|
|
813
861
|
// Schema module: the schema; the verdict and the errors come from the runtime it imports.
|
|
814
862
|
// Re-run the build after upgrading ata: the version above is what wrote this.
|
|
@@ -819,7 +867,7 @@ let _ataV = null;
|
|
|
819
867
|
function _ataGet() { return _ataV !== null ? _ataV : (_ataV = _ataCreateValidator(${ownId !== null ? 'null' : 'JSON.parse(' + text + ')'}${rtOpts ? ', { ' + rtOpts + ' }' : ''})); }
|
|
820
868
|
function isValid(data) { return _ataGet().isValid(data); }
|
|
821
869
|
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)};
|
|
870
|
+
${fillDecl}const schemaHash = ${JSON.stringify(hashSrc)};
|
|
823
871
|
const ataVersion = ${JSON.stringify(ATA_VERSION)};
|
|
824
872
|
${exp}`;
|
|
825
873
|
}
|
package/lib/aot-order.js
ADDED
|
@@ -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, take };
|
package/lib/buffer-gate.js
CHANGED
|
@@ -61,7 +61,39 @@ function re2Diverges(pattern) {
|
|
|
61
61
|
return false;
|
|
62
62
|
}
|
|
63
63
|
|
|
64
|
+
// The walker gives up past a depth of its own (100 schema frames, about 50
|
|
65
|
+
// levels of data under a recursive schema) and rejects the document there. A
|
|
66
|
+
// schema that refers back into itself, or that is nested that deep, can reach
|
|
67
|
+
// it with a document validate() accepts, so it is answered through validate(),
|
|
68
|
+
// which has no such limit.
|
|
69
|
+
const NATIVE_DEPTH = 40;
|
|
70
|
+
|
|
71
|
+
// Whether a `$ref` into this document leads back to a schema it is inside.
|
|
72
|
+
function refersBack(root) {
|
|
73
|
+
const { localRef } = require('./defaults.js');
|
|
74
|
+
const done = new Set();
|
|
75
|
+
const onPath = new Set();
|
|
76
|
+
const visit = (node) => {
|
|
77
|
+
if (node === null || typeof node !== 'object') return false;
|
|
78
|
+
if (onPath.has(node)) return true;
|
|
79
|
+
if (done.has(node)) return false;
|
|
80
|
+
onPath.add(node);
|
|
81
|
+
let back = false;
|
|
82
|
+
for (const key of Object.keys(node)) {
|
|
83
|
+
const v = node[key];
|
|
84
|
+
if (key === '$ref' && typeof v === 'string' && v.startsWith('#')) back = visit(localRef(root, v));
|
|
85
|
+
else if (key !== 'enum' && key !== 'const' && key !== 'default' && key !== 'examples' && v !== null && typeof v === 'object') back = visit(v);
|
|
86
|
+
if (back) break;
|
|
87
|
+
}
|
|
88
|
+
onPath.delete(node);
|
|
89
|
+
done.add(node);
|
|
90
|
+
return back;
|
|
91
|
+
};
|
|
92
|
+
return visit(root);
|
|
93
|
+
}
|
|
94
|
+
|
|
64
95
|
function walk(schema, depth) {
|
|
96
|
+
if (depth > NATIVE_DEPTH) return true;
|
|
65
97
|
if (schema === true || schema === false) {
|
|
66
98
|
// A boolean root is answered wrong by the walker; nested booleans are
|
|
67
99
|
// handled where they appear (items and prefixItems below).
|
|
@@ -110,6 +142,7 @@ function walk(schema, depth) {
|
|
|
110
142
|
function bufferNeedsSlowPath(schema, schemaMap, keywords) {
|
|
111
143
|
if (keywords && schemaUsesKeywords(schema, keywords)) return true;
|
|
112
144
|
if (walk(schema, 0)) return true;
|
|
145
|
+
if (refersBack(schema)) return true;
|
|
113
146
|
if (schemaMap && schemaMap.size > 0) {
|
|
114
147
|
for (const s of schemaMap.values()) if (walk(s, 1)) return true;
|
|
115
148
|
}
|
package/lib/clone-emit.js
CHANGED
|
@@ -50,12 +50,13 @@ const _INLINE_ANNOTATIONS = new Set(['$ref', 'title', 'description', '$comment',
|
|
|
50
50
|
const _INLINE_DATA_KEYS = new Set(['default', 'const', 'enum', 'examples']);
|
|
51
51
|
function inlineRefsForClone(root) {
|
|
52
52
|
const state = { budget: 512 };
|
|
53
|
-
//
|
|
54
|
-
//
|
|
55
|
-
// defaults
|
|
56
|
-
//
|
|
57
|
-
//
|
|
58
|
-
//
|
|
53
|
+
// The runtime's useDefaults fills a property's default from the schema
|
|
54
|
+
// written under the property, the $ref node with any default next to it,
|
|
55
|
+
// and then the defaults inside the referenced definition, as it fills them
|
|
56
|
+
// inline. The definition's own top-level default is not the property's, so
|
|
57
|
+
// the inlined copy drops that one and keeps everything below it. parse()
|
|
58
|
+
// must stay exactly as generous as validate(); a parse that fills more, or
|
|
59
|
+
// less, is a disagreement.
|
|
59
60
|
const walk = (node, active, inRef) => {
|
|
60
61
|
if (!node || typeof node !== 'object') return node;
|
|
61
62
|
if (Array.isArray(node)) return node.map((n) => walk(n, active, inRef));
|
|
@@ -73,14 +74,13 @@ function inlineRefsForClone(root) {
|
|
|
73
74
|
cur = target;
|
|
74
75
|
entered = true;
|
|
75
76
|
}
|
|
76
|
-
const strip = inRef || entered;
|
|
77
77
|
const out = {};
|
|
78
78
|
for (const k of Object.keys(cur)) {
|
|
79
|
-
if (
|
|
79
|
+
if (entered && k === 'default') continue;
|
|
80
80
|
const v = cur[k];
|
|
81
81
|
// A property named "__proto__" is an own key of the copy: assigned, it
|
|
82
82
|
// set the copy's prototype and the property was gone from parse().
|
|
83
|
-
const w = (_INLINE_DATA_KEYS.has(k) || k === '$defs' || k === 'definitions') ? v : walk(v, active,
|
|
83
|
+
const w = (_INLINE_DATA_KEYS.has(k) || k === '$defs' || k === 'definitions') ? v : walk(v, active, false);
|
|
84
84
|
if (k === '__proto__') Object.defineProperty(out, k, { value: w, writable: true, enumerable: true, configurable: true });
|
|
85
85
|
else out[k] = w;
|
|
86
86
|
}
|
|
@@ -197,7 +197,7 @@ function emitClone(node, access, depth) {
|
|
|
197
197
|
if (apSchema) {
|
|
198
198
|
const kVar = `_k${depth}`;
|
|
199
199
|
const vVar = `_v${depth}`;
|
|
200
|
-
const valueExpr = emitValueExpr(
|
|
200
|
+
const valueExpr = emitValueExpr(apSchema, vVar, depth);
|
|
201
201
|
if (valueExpr === null) return null;
|
|
202
202
|
let skip = '';
|
|
203
203
|
let declSet = '';
|
|
@@ -255,20 +255,6 @@ function emitClone(node, access, depth) {
|
|
|
255
255
|
// carry keys this code cannot see, and copying the reference would smuggle
|
|
256
256
|
// them into a value that claims to be sanitised, so the caller declines the
|
|
257
257
|
// whole clone instead.
|
|
258
|
-
// Drop every schema-position `default` in a subtree, leaving data positions
|
|
259
|
-
// (const, enum, examples) untouched. Used on a record's value schema, where
|
|
260
|
-
// the runtime's useDefaults fills nothing.
|
|
261
|
-
function stripDefaultsDeep(node) {
|
|
262
|
-
if (!node || typeof node !== 'object') return node;
|
|
263
|
-
if (Array.isArray(node)) return node.map(stripDefaultsDeep);
|
|
264
|
-
const out = {};
|
|
265
|
-
for (const k of Object.keys(node)) {
|
|
266
|
-
if (k === 'default') continue;
|
|
267
|
-
out[k] = _INLINE_DATA_KEYS.has(k) ? node[k] : stripDefaultsDeep(node[k]);
|
|
268
|
-
}
|
|
269
|
-
return out;
|
|
270
|
-
}
|
|
271
|
-
|
|
272
258
|
const _CLONE_PRIMITIVE = new Set(['string', 'number', 'integer', 'boolean', 'null']);
|
|
273
259
|
// Whether every value this schema accepts is a primitive, so a copy has
|
|
274
260
|
// nothing inside it to strip and takes the value as it is. A primitive type
|
|
@@ -329,4 +315,4 @@ function cloneExprFor(schema) {
|
|
|
329
315
|
return emitClone(inlineRefsForClone(schema), 'data', 0);
|
|
330
316
|
}
|
|
331
317
|
|
|
332
|
-
module.exports = { cloneExprFor, emitClone, inlineRefsForClone,
|
|
318
|
+
module.exports = { cloneExprFor, emitClone, inlineRefsForClone, resolveLocalPointer };
|