ata-validator 1.49.0 → 1.51.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/aot-impl.js CHANGED
@@ -352,7 +352,10 @@ function toStandaloneModule(validator, opts) {
352
352
  const jsFn = validator._jsFn;
353
353
  if (!jsFn || !jsFn._source) return null;
354
354
  const format = (opts && opts.format) || 'esm';
355
- const abortEarly = !!(opts && opts.abortEarly);
355
+ // A compact module (toCompactModule) carries the verdict and its schema, and
356
+ // takes its errors from the shared runtime at this import specifier.
357
+ const errorRuntime = opts && typeof opts._errorRuntime === 'string' ? opts._errorRuntime : null;
358
+ const abortEarly = !!(opts && opts.abortEarly) || errorRuntime !== null;
356
359
  const source = !!(opts && opts.source);
357
360
  const sourceMap = opts && opts.sourceMap ? opts.sourceMap : null;
358
361
  const schemaFile = opts && opts.schemaFile ? opts.schemaFile : null;
@@ -488,7 +491,43 @@ function toStandaloneModule(validator, opts) {
488
491
  const fmt = emitFormatDecls(jsFn._formatClosures, opts && opts.formatMode, 'const');
489
492
  const formatDecls = fmt.decls;
490
493
 
491
- const validBody = onePass !== null
494
+ // A compact module takes the linear-time pattern engine from the runtime,
495
+ // which carries it anyway, instead of embedding a copy (2.9 KB gzipped,
496
+ // minified) in every module that has a pattern the platform RegExp does not
497
+ // run.
498
+ const usesSafeRe = errorRuntime !== null && ((jsFn && jsFn._usesSafeRe) || (jsErrFn && jsErrFn._usesSafeRe));
499
+ let compactCore = '';
500
+ if (errorRuntime !== null) {
501
+ const { stripAnnotations } = require('./schema-strip');
502
+ const { isV1Dialect } = require('./dialect');
503
+ const sch = typeof validator._schemaObj === 'object' ? validator._schemaObj : validator._schemaObj;
504
+ // Embedded as JSON text and parsed: a literal would turn a "__proto__"
505
+ // key into the object's prototype, and V8 reads JSON faster than an
506
+ // object literal of the same size.
507
+ const text = JSON.stringify(JSON.stringify(stripAnnotations(sch))).replace(/</g, '\\u003c');
508
+ const v1 = sch !== null && typeof sch === 'object' && isV1Dialect(sch);
509
+ // No embedded resource and no anchor anywhere (a key of that name inside
510
+ // a value counts too): the runtime can skip indexing the schema.
511
+ const flat = (() => {
512
+ const seen = new Set();
513
+ const walk = (n, root) => {
514
+ if (n === null || typeof n !== 'object' || seen.has(n)) return true;
515
+ seen.add(n);
516
+ if (Array.isArray(n)) return n.every((x) => walk(x, false));
517
+ if ('$anchor' in n || '$dynamicAnchor' in n || (!root && '$id' in n)) return false;
518
+ return Object.keys(n).every((k) => walk(n[k], false));
519
+ };
520
+ return walk(stripAnnotations(sch), true);
521
+ })();
522
+ const rtOpts = [v1 ? 'v1: true' : '', flat ? 'flat: true' : ''].filter(Boolean).join(', ');
523
+ // Nothing of it runs at import: the schema is parsed and the runtime
524
+ // set up on the first rejected document whose errors are read, so a
525
+ // module that only accepts costs no more to load than its verdict.
526
+ compactCore = `let _ataErrs = null;\nfunction _ataErrors(d) { if (_ataErrs === null) _ataErrs = _ataCreateErrors(JSON.parse(${text})${rtOpts ? ', { ' + rtOpts + ' }' : ''}); return _ataErrs(d); }\n`;
527
+ }
528
+ const validBody = errorRuntime !== null
529
+ ? 'if (_fn(data)) return VALID; const e = _ataErrors(data); return e === null ? ABORT : { valid: false, errors: e }'
530
+ : onePass !== null
492
531
  ? 'return _vC(data)'
493
532
  : errCore
494
533
  ? 'return _fn(data) ? VALID : { valid: false, errors: errFn(data, true).errors }'
@@ -565,7 +604,7 @@ function validateJSON(text) {
565
604
  // schema as the caller wrote it (before dialect normalization), so a build
566
605
  // can compare it against schemaHash(currentSchema) and know the module is
567
606
  // stale without embedding its own fingerprint.
568
- const hashSrc = schemaHash(validator._rawSchema !== undefined ? validator._rawSchema : validator._schemaObj);
607
+ const hashSrc = hashOf(validator);
569
608
  // The schema hash answers "is this module built from a different schema" and
570
609
  // nothing else. Upgrading ata and not re-running the generate step leaves the
571
610
  // hash matching, so the stale module reads as current. The module cannot ask
@@ -589,11 +628,11 @@ function validateJSON(text) {
589
628
  degradedNote += '// NOTE: parse() was requested but could not be generated for this schema;\n// the module has no parse export. Validate and strip with the runtime Validator.\n';
590
629
  }
591
630
  return `// Auto-generated by ata-validator ${ATA_VERSION}, do not edit.
592
- // Schema is embedded; runtime has zero dependency on ata-validator.
631
+ ${errorRuntime === null ? '// Schema is embedded; runtime has zero dependency on ata-validator.' : '// Compact module: the verdict and the schema; errors come from the runtime it imports.'}
593
632
  // Re-run the build after upgrading ata: the version above is what wrote this.
594
633
  ${degradedNote}'use strict';
595
- ${_CP_LEN_SOURCE}
596
- ${safeRePrelude(jsFn, jsErrFn, onePass !== null && onePass.usesSafeRe ? { _usesSafeRe: true } : null)}${schemaSourceConst}const VALID = Object.freeze({ valid: true, errors: Object.freeze([]) });
634
+ ${errorRuntime === null ? '' : format === 'esm' ? `import { createErrors as _ataCreateErrors${usesSafeRe ? ', compileSafe as _ataCompileSafe' : ''} } from ${JSON.stringify(errorRuntime)};\n` : `const { createErrors: _ataCreateErrors${usesSafeRe ? ', compileSafe: _ataCompileSafe' : ''} } = require(${JSON.stringify(errorRuntime)});\n`}${_CP_LEN_SOURCE}
635
+ ${errorRuntime !== null ? (usesSafeRe ? 'const __ataSafeRe = _ataCompileSafe;\n' : '') : safeRePrelude(jsFn, jsErrFn, onePass !== null && onePass.usesSafeRe ? { _usesSafeRe: true } : null)}${schemaSourceConst}const VALID = Object.freeze({ valid: true, errors: Object.freeze([]) });
597
636
  const ABORT = Object.freeze({
598
637
  valid: false,
599
638
  errors: Object.freeze([Object.freeze({
@@ -606,11 +645,185 @@ const ABORT = Object.freeze({
606
645
  ${closureDecls}${sharedBlock}${preambleDecls}${formatDecls}const _fn = function(d) {
607
646
  ${src}
608
647
  };
609
- ${errCore}function isValid(data) { return _fn(data); }
648
+ ${errCore}${compactCore}function isValid(data) { return _fn(data); }
610
649
  function validate(data) { ${validBody}; }
611
650
  ${parseCore}${positionsCore}${hashDecl}${exports}`;
612
651
  }
613
652
 
653
+ // Why a validator cannot be written as a compact module, or null when it can.
654
+ // The error runtime walks the embedded schema with the interpreted engine and
655
+ // knows nothing the schema does not say, so anything the validator adds from
656
+ // outside the schema keeps the module self-contained.
657
+ // The error runtime's generator reads the library's own files with fs, and
658
+ // runs in Node only (ata build, errorRuntimeSource). It is required through a
659
+ // name rather than a literal so that a bundler following this module, in a
660
+ // build of the package for a runtime without fs, does not try to bring fs
661
+ // along: 1.49.0 bundled for such runtimes, and the generator must not break
662
+ // that.
663
+ const ERROR_RUNTIME_MODULE = './error-runtime';
664
+ function loadErrorRuntime() { return require(ERROR_RUNTIME_MODULE); }
665
+
666
+ // The schema hash a module stamps, once per validator: a plugin writes both
667
+ // modules of a schema (self-contained and compact) from one validator, and
668
+ // hashing a large schema canonically is not free.
669
+ const _hashes = new WeakMap();
670
+ function hashOf(validator) {
671
+ let h = _hashes.get(validator);
672
+ if (h === undefined) {
673
+ h = schemaHash(validator._rawSchema !== undefined ? validator._rawSchema : validator._schemaObj);
674
+ _hashes.set(validator, h);
675
+ }
676
+ return h;
677
+ }
678
+
679
+ // True when every $ref and $dynamicRef in the schema resolves inside it, by
680
+ // the interpreter's own resolver.
681
+ function hasRefKey(n, seen) {
682
+ if (n === null || typeof n !== 'object' || seen.has(n)) return false;
683
+ seen.add(n);
684
+ if (Array.isArray(n)) { for (const x of n) if (hasRefKey(x, seen)) return true; return false; }
685
+ if ('$ref' in n || '$dynamicRef' in n) return true;
686
+ for (const k of Object.keys(n)) if (hasRefKey(n[k], seen)) return true;
687
+ return false;
688
+ }
689
+
690
+ // miss: an object that gets `ref`, the first reference that does not resolve.
691
+ function refsResolveWithin(schema, registry, miss) {
692
+ // No reference anywhere (a key of that name inside a value counts too):
693
+ // nothing to resolve, and no index to build.
694
+ if (!registry && !hasRefKey(schema, new Set())) return true;
695
+ const { indexSchemas, resolveRef } = require('./interpreter-core')._refInternals;
696
+ let state;
697
+ try { state = indexSchemas(schema, registry || null); } catch { return false; }
698
+ for (const [node, base] of state.nodeBase) {
699
+ if (node === null || typeof node !== 'object') continue;
700
+ for (const k of ['$ref', '$dynamicRef']) {
701
+ const r = node[k];
702
+ if (r === undefined) continue;
703
+ if (typeof r !== 'string') return false;
704
+ let hit;
705
+ try { hit = resolveRef(r, base, state); } catch { if (miss) miss.ref = r; return false; }
706
+ if (hit.node === undefined) { if (miss) miss.ref = r; return false; }
707
+ }
708
+ }
709
+ return true;
710
+ }
711
+
712
+ const COMPACT_OPTIONS = new Set(['engine', 'strictSchema', 'logger', 'source']);
713
+ // registryMode: for a schema module that resolves its references against the
714
+ // build's registry (toSchemaModule with `registry`): schemas registered beside
715
+ // the root are allowed, and every reference has to land in the schema or in
716
+ // one of them.
717
+ function compactDecline(validator, registryMode) {
718
+ // Options are allowed by name, not refused by name: one this list does not
719
+ // know (assertFormat: false, baseURI, a future one) may change what the
720
+ // verdict means, and the runtime would not know to follow it.
721
+ // What the validator registers beside its schema (a meta-schema it
722
+ // resolves, for one) is known only once it has compiled.
723
+ validator._ensureCompiled();
724
+ const o = validator._options || {};
725
+ for (const k of Object.keys(o)) {
726
+ if (o[k] === undefined || COMPACT_OPTIONS.has(k) || (registryMode === true && k === 'schemas')) continue;
727
+ return 'option ' + k;
728
+ }
729
+ if (validator._usesKeywords || validator._externalChecks) return 'custom keywords';
730
+ if (validator._userFormats && Object.keys(validator._userFormats).length) return 'custom formats';
731
+ if (registryMode !== true && validator._schemaMap && validator._schemaMap.size > 0) return 'schemas registered beside the root';
732
+ if (validator._schemaObj === null || typeof validator._schemaObj !== 'object') return 'boolean schema';
733
+ // The runtime has the embedded schema and nothing else: a reference that
734
+ // lands outside it (a meta-schema the runtime Validator resolves from its
735
+ // vendored copies, a schema registered later) would come back unresolved,
736
+ // and the errors with it. The check runs on the schema as embedded.
737
+ const { stripAnnotations } = require('./schema-strip');
738
+ let registry = null;
739
+ if (registryMode === true && validator._schemaMap && validator._schemaMap.size > 0) {
740
+ registry = new Map();
741
+ for (const [id, sub] of validator._schemaMap) registry.set(id, stripAnnotations(sub));
742
+ }
743
+ if (!refsResolveWithin(stripAnnotations(validator._schemaObj), registry)) return registry ? 'a reference outside the schema and the registry' : 'a reference outside the schema';
744
+ return null;
745
+ }
746
+
747
+ // A module that carries its verdict and its schema without the descriptive
748
+ // keywords, and imports its errors from the shared runtime at `errorRuntime`
749
+ // (written beside it by the build, see errorRuntimeSource). Null when the
750
+ // validator cannot be written this way; the caller writes the
751
+ // self-contained module instead.
752
+ function toCompactModule(validator, opts) {
753
+ assertEmittable(validator, 'toCompactModule');
754
+ if (!opts || typeof opts.errorRuntime !== 'string') throw new TypeError('toCompactModule: opts.errorRuntime (the import specifier of the error runtime) is required');
755
+ if (compactDecline(validator) !== null) return null;
756
+ return toStandaloneModule(validator, { format: opts.format, formatMode: opts.formatMode, _errorRuntime: opts.errorRuntime });
757
+ }
758
+
759
+ // A schema module: the schema without its descriptive keywords and a few lines
760
+ // that hand it to the runtime at `opts.runtime`, which answers both the
761
+ // verdict and the errors with the interpreted engine. Smaller and quicker to
762
+ // load than generated code, slower per check once warm (about five times on
763
+ // SchemaStore documents). Answers as toCompactModule's module does. Null
764
+ // where toCompactModule would be null.
765
+ function toSchemaModule(validator, opts) {
766
+ assertEmittable(validator, 'toSchemaModule');
767
+ if (!opts || typeof opts.runtime !== 'string') throw new TypeError('toSchemaModule: opts.runtime (the import specifier of the schema runtime) is required');
768
+ // opts.registry: the import specifier of the build's registry module (the
769
+ // array of schemas, written by errorRegistrySource), for schemas that
770
+ // reference each other by $id. The module then names its schema by $id and
771
+ // carries no copy of it.
772
+ const registry = typeof opts.registry === 'string' ? opts.registry : null;
773
+ if (compactDecline(validator, registry !== null) !== null) return null;
774
+ const format = opts.format || 'esm';
775
+ const { stripAnnotations } = require('./schema-strip');
776
+ const { isV1Dialect } = require('./dialect');
777
+ const sch = validator._schemaObj;
778
+ const stripped = stripAnnotations(sch);
779
+ const text = JSON.stringify(JSON.stringify(stripped)).replace(/</g, '\\u003c');
780
+ const seen = new Set();
781
+ const flatWalk = (n, root) => {
782
+ if (n === null || typeof n !== 'object' || seen.has(n)) return true;
783
+ seen.add(n);
784
+ if (Array.isArray(n)) return n.every((x) => flatWalk(x, false));
785
+ if ('$anchor' in n || '$dynamicAnchor' in n || (!root && '$id' in n)) return false;
786
+ return Object.keys(n).every((k) => flatWalk(n[k], false));
787
+ };
788
+ const ownId = registry !== null && typeof sch.$id === 'string' ? sch.$id : null;
789
+ const rtOpts = [isV1Dialect(sch) ? 'v1: true' : '', registry === null && flatWalk(stripped, true) ? 'flat: true' : '', registry !== null ? 'registry: _ataRegistry' : '', ownId !== null ? 'id: ' + JSON.stringify(ownId) : ''].filter(Boolean).join(', ');
790
+ const hashSrc = hashOf(validator);
791
+ const spec = JSON.stringify(opts.runtime);
792
+ const regSpec = registry !== null ? JSON.stringify(registry) : null;
793
+ const imp = (format === 'esm'
794
+ ? `import { createValidator as _ataCreateValidator } from ${spec};\n`
795
+ : `const { createValidator: _ataCreateValidator } = require(${spec});\n`)
796
+ + (regSpec === null ? '' : format === 'esm' ? `import _ataRegistry from ${regSpec};\n` : `const _ataRegistry = require(${regSpec});\n`);
797
+ const exp = format === 'esm'
798
+ ? 'export { validate, isValid, schemaHash, ataVersion };\nexport default { validate, isValid, schemaHash, ataVersion };\n'
799
+ : 'module.exports = { validate, isValid, schemaHash, ataVersion };\nmodule.exports.default = module.exports;\n';
800
+ return `// Auto-generated by ata-validator ${ATA_VERSION}, do not edit.
801
+ // Schema module: the schema; the verdict and the errors come from the runtime it imports.
802
+ // Re-run the build after upgrading ata: the version above is what wrote this.
803
+ 'use strict';
804
+ ${imp}const VALID = Object.freeze({ valid: true, errors: Object.freeze([]) });
805
+ const ABORT = Object.freeze({ valid: false, errors: Object.freeze([Object.freeze({ code: 'ATA9000', message: 'validation failed', keyword: '__abort_early__', path: '' })]) });
806
+ let _ataV = null;
807
+ function _ataGet() { return _ataV !== null ? _ataV : (_ataV = _ataCreateValidator(${ownId !== null ? 'null' : 'JSON.parse(' + text + ')'}${rtOpts ? ', { ' + rtOpts + ' }' : ''})); }
808
+ function isValid(data) { return _ataGet().isValid(data); }
809
+ 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 }; }
810
+ const schemaHash = ${JSON.stringify(hashSrc)};
811
+ const ataVersion = ${JSON.stringify(ATA_VERSION)};
812
+ ${exp}`;
813
+ }
814
+
815
+ // The registry module schema modules import when their schemas reference each
816
+ // other: every schema of the build, without its descriptive keywords, as one
817
+ // array the runtime turns into its id-to-schema table.
818
+ function registrySource(schemas, format) {
819
+ const { stripAnnotations } = require('./schema-strip');
820
+ const text = JSON.stringify(JSON.stringify(schemas.map((x) => stripAnnotations(x && x._schemaObj !== undefined ? x._schemaObj : x)))).replace(/</g, '\\u003c');
821
+ const head = `// Auto-generated by ata-validator ${ATA_VERSION}, do not edit.\n// The schemas of one build, which its schema modules resolve references against.\n'use strict';\n`;
822
+ return format === 'cjs'
823
+ ? head + `module.exports = JSON.parse(${text});\n`
824
+ : head + `export default JSON.parse(${text});\n`;
825
+ }
826
+
614
827
  // Bundle multiple validators into a single JS file for fast startup.
615
828
  // Usage:
616
829
  // const bundle = Validator.bundle([schema1, schema2, ...]);
@@ -925,8 +1138,28 @@ const htmlSafeResult = (fn) => function () { return htmlSafe(fn.apply(this, argu
925
1138
  module.exports = {
926
1139
  toStandalone: htmlSafeResult(toStandalone),
927
1140
  toStandaloneModule: htmlSafeResult(toStandaloneModule),
1141
+ toCompactModule: htmlSafeResult(toCompactModule),
1142
+ toSchemaModule: htmlSafeResult(toSchemaModule),
1143
+ registrySource: htmlSafeResult(registrySource),
1144
+ compactDecline,
1145
+ // `schemas`: the validators (or schemas) whose compact modules will import
1146
+ // this runtime; parts none of them needs are left out. Omitted, the
1147
+ // runtime carries every part.
1148
+ // `schemaMode`: the runtime schema modules import, which also answers the
1149
+ // verdict (createValidator).
1150
+ errorRuntimeSource: htmlSafeResult((format, schemas, schemaMode) => {
1151
+ const er = loadErrorRuntime();
1152
+ if (!Array.isArray(schemas)) return er.errorRuntimeSource(format, ATA_VERSION, undefined, !!schemaMode);
1153
+ const { stripAnnotations } = require('./schema-strip');
1154
+ const embedded = schemas.map((x) => stripAnnotations(x && typeof x === 'object' && x._schemaObj !== undefined ? x._schemaObj : x));
1155
+ const features = er.runtimeFeatures(embedded);
1156
+ for (const x of schemas) if (x && x._jsFn && x._jsFn._usesSafeRe) features['safe-regex'] = true;
1157
+ return er.errorRuntimeSource(format, ATA_VERSION, features, !!schemaMode);
1158
+ }),
928
1159
  bundle: htmlSafeResult(bundle),
929
1160
  bundleStandalone: htmlSafeResult(bundleStandalone),
930
1161
  bundleCompact: htmlSafeResult(bundleCompact),
931
1162
  loadBundle,
932
1163
  };
1164
+ // For the build only (lib/aot-build.js), not part of ata-validator/aot.
1165
+ Object.defineProperty(module.exports, '_refsResolveWithin', { value: refsResolveWithin, enumerable: false });