ata-validator 0.17.3 → 0.17.5

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/CHANGELOG.md CHANGED
@@ -2,6 +2,20 @@
2
2
 
3
3
  All notable changes to ata-validator are documented here. The format follows [Keep a Changelog](https://keepachangelog.com/), and this project adheres to semantic versioning.
4
4
 
5
+ ## 0.17.5 - 2026-05-25
6
+
7
+ ### Fixed
8
+
9
+ - Compiled validators resolve draft-07 plain-name anchors. A `$defs`/`definitions` entry that declares an anchor with `$id: "#name"` and is referenced by `$ref: "#name"` now compiles through the codegen on every path (boolean, error, combined) instead of bailing. The bail forced a fallback that could not resolve sibling cross-schema refs, which surfaced as `cannot resolve $ref`. This is how shared schemas are referenced under Fastify.
10
+
11
+ ## 0.17.4 - 2026-05-25
12
+
13
+ ### Fixed
14
+
15
+ - Standalone output now embeds user-supplied format functions. `toStandaloneModule` and `bundleCompact` referenced the `_uf_<name>` format helpers without declaring them, so the generated module threw `_uf_<name> is not defined` on the first validation, and their error path skipped the custom format entirely (so `validate` disagreed with `isValid`). Both now serialize the format functions via `Function#toString` and run them on the error path too, matching `bundleStandalone`.
16
+ - Compiled validators report per-property errors for schema-valued `additionalProperties`. The AOT error path previously emitted a single generic `validation failed`; it now validates each undeclared property against the subschema and reports the precise `/<key>` error, matching the runtime validator.
17
+ - `ata --version` (and `-V`) prints the CLI version instead of failing with `unknown command`.
18
+
5
19
  ## 0.17.3 - 2026-05-25
6
20
 
7
21
  ### Security
package/bin/ata.js CHANGED
@@ -45,6 +45,7 @@ Build options:
45
45
  --dual Emit both .compiled.mjs (with source) and .compiled.min.mjs (without)
46
46
 
47
47
  -h, --help Show this message
48
+ -V, --version Print the ata-validator version
48
49
 
49
50
  Examples:
50
51
  ata compile schemas/user.json -o src/generated/user.validator.mjs
@@ -386,6 +387,10 @@ function main() {
386
387
 
387
388
  const cmd = argv[0];
388
389
  if (cmd === '-h' || cmd === '--help' || cmd === 'help') { usage(); return; }
390
+ if (cmd === '-V' || cmd === '--version' || cmd === 'version') {
391
+ process.stdout.write(require('../package.json').version + '\n');
392
+ return;
393
+ }
389
394
 
390
395
  const rest = argv.slice(1);
391
396
  let args;
package/index.js CHANGED
@@ -1214,7 +1214,7 @@ module.exports = { boolFn, hybridFactory, errFn };
1214
1214
  jsErrFn = compileToJSCodegenWithErrors(
1215
1215
  typeof this._schemaObj === 'object' ? this._schemaObj : {},
1216
1216
  null,
1217
- undefined,
1217
+ this._userFormats,
1218
1218
  (source && sourceMap && schemaFile) ? { sourceMap, schemaFile } : null,
1219
1219
  );
1220
1220
  const errSrc = jsErrFn && jsErrFn._errSource ? jsErrFn._errSource : '';
@@ -1266,6 +1266,16 @@ module.exports = { boolFn, hybridFactory, errFn };
1266
1266
  // them at module scope too, or _fn references undefined names (e.g. _af1_b0).
1267
1267
  const preambleDecls = jsFn._preambleSource ? jsFn._preambleSource + '\n' : '';
1268
1268
 
1269
+ // User-supplied format functions are referenced as _uf_<name> by both the
1270
+ // boolean (_fn) and error (errFn) bodies. Embed them via Function#toString
1271
+ // so the standalone module stays self-contained.
1272
+ let formatDecls = '';
1273
+ if (jsFn._formatClosures && jsFn._formatClosures.length > 0) {
1274
+ formatDecls = jsFn._formatClosures
1275
+ .map(({ name, fn }) => `const ${name} = ${fn.toString()};`)
1276
+ .join('\n') + '\n';
1277
+ }
1278
+
1269
1279
  const validBody = errCore
1270
1280
  ? 'return _fn(data) ? VALID : { valid: false, errors: errFn(data, true).errors }'
1271
1281
  : 'return _fn(data) ? VALID : ABORT';
@@ -1288,7 +1298,7 @@ const ABORT = Object.freeze({
1288
1298
  path: '',
1289
1299
  })]),
1290
1300
  });
1291
- ${closureDecls}${preambleDecls}const _fn = function(d) {
1301
+ ${closureDecls}${preambleDecls}${formatDecls}const _fn = function(d) {
1292
1302
  ${src}
1293
1303
  };
1294
1304
  ${errCore}function isValid(data) { return _fn(data); }
@@ -1575,6 +1585,7 @@ Validator.bundleCompact = function (schemas, opts) {
1575
1585
  const jsErrFn = compileToJSCodegenWithErrors(
1576
1586
  typeof schema === "string" ? JSON.parse(schema) : schema,
1577
1587
  v._schemaMap,
1588
+ v._userFormats,
1578
1589
  );
1579
1590
  if (jsFn._usesSafeRe || (jsErrFn && jsErrFn._usesSafeRe)) bundleUsesSafeRe = true;
1580
1591
  // Hoisted anyOf/oneOf branch helpers (e.g. `_af1_b0`) must travel with the
@@ -1586,6 +1597,7 @@ Validator.bundleCompact = function (schemas, opts) {
1586
1597
  return {
1587
1598
  hybrid,
1588
1599
  err: jsErrFn && jsErrFn._errSource ? jsErrFn._errSource : null,
1600
+ fmt: jsFn._formatClosures || null,
1589
1601
  };
1590
1602
  });
1591
1603
 
@@ -1624,6 +1636,19 @@ Validator.bundleCompact = function (schemas, opts) {
1624
1636
  const declKW = isEsm ? "const" : "var";
1625
1637
  out += `${declKW} R=Object.freeze({valid:true,errors:Object.freeze([])});\n`;
1626
1638
 
1639
+ // User format functions are referenced as _uf_<name> by the hybrid and error
1640
+ // bodies. Collect them across all schemas (deduped by name) and embed via
1641
+ // Function#toString so the bundle stays self-contained.
1642
+ const fmtSeen = new Set();
1643
+ for (const e of entries) {
1644
+ if (!e || !e.fmt) continue;
1645
+ for (const { name, fn } of e.fmt) {
1646
+ if (fmtSeen.has(name)) continue;
1647
+ fmtSeen.add(name);
1648
+ out += `${declKW} ${name}=${fn.toString()};\n`;
1649
+ }
1650
+ }
1651
+
1627
1652
  // Shared hybrid factories
1628
1653
  out += `${declKW} H=[\n`;
1629
1654
  out += bodies
@@ -699,23 +699,6 @@ function canResolveDynamicRefs(target, callingSchema, schemaMap) {
699
699
 
700
700
  // Recursively check if a schema can be safely compiled to JS codegen.
701
701
  // Returns false if any sub-schema contains features codegen gets wrong.
702
- // Does the schema (recursively) contain an anchor-style `$ref` equal to anchorId
703
- // (e.g. `$ref: '#address'`)? Used to keep codegen away from anchor refs it can't
704
- // resolve in the combined/error paths.
705
- function schemaHasAnchorRef(node, anchorId) {
706
- if (!node || typeof node !== 'object') return false
707
- if (Array.isArray(node)) {
708
- for (const x of node) if (schemaHasAnchorRef(x, anchorId)) return true
709
- return false
710
- }
711
- if (node.$ref === anchorId) return true
712
- for (const k in node) {
713
- if (k === '$ref') continue
714
- if (schemaHasAnchorRef(node[k], anchorId)) return true
715
- }
716
- return false
717
- }
718
-
719
702
  function codegenSafe(schema, schemaMap) {
720
703
  if (typeof schema === 'boolean') return true
721
704
  if (typeof schema !== 'object' || schema === null) return true
@@ -826,14 +809,10 @@ function codegenSafe(schema, schemaMap) {
826
809
  if (typeof def === 'boolean') return false
827
810
  if (typeof def === 'object' && def !== null) {
828
811
  // A non-fragment $id (e.g. 'sub.json', 'http://...') opens a new base URI
829
- // scope — bail. A fragment-only $id ('#name') is a draft-07 anchor; it is
830
- // safe to codegen when reached by JSON pointer (#/definitions/name), but the
831
- // combined/error codegen paths do not resolve anchor refs (`$ref: '#name'`)
832
- // to it, so bail if the schema anchor-references this def.
833
- if (def.$id) {
834
- if (!def.$id.startsWith('#')) return false
835
- if (schemaHasAnchorRef(schema, def.$id)) return false
836
- }
812
+ // scope — bail. A fragment-only $id ('#name') is a draft-07 anchor; the
813
+ // codegen anchor maps register it (see "Build anchors map"), so anchor
814
+ // refs (`$ref: '#name'`) resolve to it on all paths.
815
+ if (def.$id && !def.$id.startsWith('#')) return false
837
816
  if (def.$ref) return false // nested ref chain — bail
838
817
  if (!codegenSafe(def, schemaMap)) return false
839
818
  }
@@ -933,12 +912,15 @@ function compileToJSCodegen(schema, schemaMap, userFormats) {
933
912
  // Root schema's own $dynamicAnchor / $anchor
934
913
  if (schema.$dynamicAnchor) anchors['#' + schema.$dynamicAnchor] = schema
935
914
  if (schema.$anchor) anchors['#' + schema.$anchor] = schema
915
+ // Draft-07 plain-name anchor: declared as `$id: "#name"`.
916
+ if (typeof schema.$id === 'string' && schema.$id.startsWith('#')) anchors[schema.$id] = schema
936
917
  // Anchors from $defs
937
918
  if (rootDefs) {
938
919
  for (const def of Object.values(rootDefs)) {
939
920
  if (def && typeof def === 'object') {
940
921
  if (def.$dynamicAnchor) anchors['#' + def.$dynamicAnchor] = def
941
922
  if (def.$anchor) anchors['#' + def.$anchor] = def
923
+ if (typeof def.$id === 'string' && def.$id.startsWith('#')) anchors[def.$id] = def
942
924
  }
943
925
  }
944
926
  }
@@ -948,6 +930,7 @@ function compileToJSCodegen(schema, schemaMap, userFormats) {
948
930
  if (ext && typeof ext === 'object') {
949
931
  if (ext.$dynamicAnchor && !anchors['#' + ext.$dynamicAnchor]) anchors['#' + ext.$dynamicAnchor] = ext
950
932
  if (ext.$anchor && !anchors['#' + ext.$anchor]) anchors['#' + ext.$anchor] = ext
933
+ if (typeof ext.$id === 'string' && ext.$id.startsWith('#') && !anchors[ext.$id]) anchors[ext.$id] = ext
951
934
  }
952
935
  }
953
936
  }
@@ -2558,9 +2541,6 @@ function compileToJSCodegenWithErrors(schema, schemaMap, userFormats, sourceOpts
2558
2541
  if (s.includes('unevaluatedProperties') || s.includes('unevaluatedItems')) return null
2559
2542
  // Bail on self-referencing schemas — error codegen doesn't support recursion
2560
2543
  if (s.includes('"$ref":"#"')) return null
2561
- // Bail on additionalProperties as a schema — genCodeE does not emit the
2562
- // per-key sub-schema loop. Let the closure path handle detailed errors.
2563
- if (hasAdditionalPropertiesSchema(schema)) return null
2564
2544
  }
2565
2545
  if (typeof schema === 'boolean') {
2566
2546
  return schema
@@ -2595,11 +2575,13 @@ function compileToJSCodegenWithErrors(schema, schemaMap, userFormats, sourceOpts
2595
2575
  const eAnchors = {}
2596
2576
  if (schema.$dynamicAnchor) eAnchors['#' + schema.$dynamicAnchor] = schema
2597
2577
  if (schema.$anchor) eAnchors['#' + schema.$anchor] = schema
2578
+ if (typeof schema.$id === 'string' && schema.$id.startsWith('#')) eAnchors[schema.$id] = schema
2598
2579
  if (eRootDefs) {
2599
2580
  for (const def of Object.values(eRootDefs)) {
2600
2581
  if (def && typeof def === 'object') {
2601
2582
  if (def.$dynamicAnchor) eAnchors['#' + def.$dynamicAnchor] = def
2602
2583
  if (def.$anchor) eAnchors['#' + def.$anchor] = def
2584
+ if (typeof def.$id === 'string' && def.$id.startsWith('#')) eAnchors[def.$id] = def
2603
2585
  }
2604
2586
  }
2605
2587
  }
@@ -2608,6 +2590,7 @@ function compileToJSCodegenWithErrors(schema, schemaMap, userFormats, sourceOpts
2608
2590
  if (ext && typeof ext === 'object') {
2609
2591
  if (ext.$dynamicAnchor && !eAnchors['#' + ext.$dynamicAnchor]) eAnchors['#' + ext.$dynamicAnchor] = ext
2610
2592
  if (ext.$anchor && !eAnchors['#' + ext.$anchor]) eAnchors['#' + ext.$anchor] = ext
2593
+ if (typeof ext.$id === 'string' && ext.$id.startsWith('#') && !eAnchors[ext.$id]) eAnchors[ext.$id] = ext
2611
2594
  }
2612
2595
  }
2613
2596
  }
@@ -3012,6 +2995,21 @@ function genCodeE(schema, v, pathExpr, lines, ctx, schemaPrefix) {
3012
2995
  }
3013
2996
  }
3014
2997
 
2998
+ // additionalProperties as a schema — validate each property not declared in
2999
+ // `properties` against the subschema, with a dynamic /<key> path. codegenSafe
3000
+ // guarantees no patternProperties or composition here, so "additional" is
3001
+ // simply "not declared".
3002
+ if (typeof schema.additionalProperties === 'object' && schema.additionalProperties !== null) {
3003
+ const ki = ctx.varCounter++
3004
+ const known = Object.keys(schema.properties || {})
3005
+ const guard = known.length ? `const _ak${ki}=new Set([${known.map((k) => JSON.stringify(k)).join(',')}]);` : ''
3006
+ const keep = known.length ? `if(!_ak${ki}.has(_k${ki})){` : `{`
3007
+ lines.push(`if(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v})){${guard}for(const _k${ki} in ${v}){${keep}`)
3008
+ const p = pathExpr ? `${pathExpr}+'/'+_k${ki}` : `'/'+_k${ki}`
3009
+ genCodeE(schema.additionalProperties, `${v}[_k${ki}]`, p, lines, ctx, schemaPrefix + '/additionalProperties')
3010
+ lines.push(`}}}`)
3011
+ }
3012
+
3015
3013
  // dependentSchemas
3016
3014
  if (schema.dependentSchemas) {
3017
3015
  for (const [key, depSchema] of Object.entries(schema.dependentSchemas)) {
@@ -3198,11 +3196,13 @@ function compileToJSCombined(schema, VALID_RESULT, schemaMap, userFormats) {
3198
3196
  const cAnchors = {}
3199
3197
  if (schema.$dynamicAnchor) cAnchors['#' + schema.$dynamicAnchor] = schema
3200
3198
  if (schema.$anchor) cAnchors['#' + schema.$anchor] = schema
3199
+ if (typeof schema.$id === 'string' && schema.$id.startsWith('#')) cAnchors[schema.$id] = schema
3201
3200
  if (cRootDefs) {
3202
3201
  for (const def of Object.values(cRootDefs)) {
3203
3202
  if (def && typeof def === 'object') {
3204
3203
  if (def.$dynamicAnchor) cAnchors['#' + def.$dynamicAnchor] = def
3205
3204
  if (def.$anchor) cAnchors['#' + def.$anchor] = def
3205
+ if (typeof def.$id === 'string' && def.$id.startsWith('#')) cAnchors[def.$id] = def
3206
3206
  }
3207
3207
  }
3208
3208
  }
@@ -3211,6 +3211,7 @@ function compileToJSCombined(schema, VALID_RESULT, schemaMap, userFormats) {
3211
3211
  if (ext && typeof ext === 'object') {
3212
3212
  if (ext.$dynamicAnchor && !cAnchors['#' + ext.$dynamicAnchor]) cAnchors['#' + ext.$dynamicAnchor] = ext
3213
3213
  if (ext.$anchor && !cAnchors['#' + ext.$anchor]) cAnchors['#' + ext.$anchor] = ext
3214
+ if (typeof ext.$id === 'string' && ext.$id.startsWith('#') && !cAnchors[ext.$id]) cAnchors[ext.$id] = ext
3214
3215
  }
3215
3216
  }
3216
3217
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ata-validator",
3
- "version": "0.17.3",
3
+ "version": "0.17.5",
4
4
  "description": "JSON Schema validation with first-class TypeScript and zero runtime cost. AOT compile to per-schema ESM modules with zero validator dependency. Generic Validator<T> for TypeBox/Zod/Valibot composition. Optional runtime API. Standard Schema V1 compatible.",
5
5
  "main": "index.js",
6
6
  "module": "index.mjs",
@@ -42,7 +42,7 @@
42
42
  "rebuild": "cmake-js rebuild --target ata",
43
43
  "prebuild": "pkg-prebuilds-copy --baseDir build/Release --source ata.node --name=ata --strip --napi_version=10",
44
44
  "prebuild-all": "npm run prebuild -- --arch x64 && npm run prebuild -- --arch arm64",
45
- "test": "node test.js && node tests/test_no_native.js && node tests/test_safe_regex.js && node tests/test_safe_regex_integration.js && node tests/test_aot_build.js && node tests/test_aot_differential.js && node tests/test_aot_cli_build.js && node tests/test_aot_cli_smoke.js && node tests/test_bundle_standalone.js && node tests/test_standalone_anyof.js && node tests/test_typed_validator_runner.js && node tests/test_define_schema.js && node tests/test_error_codes_lock.js && node tests/test_nullable.js && node tests/test_validate_and_parse.js && node tests/test_validate_data.js && node tests/test_enrich_error.js && node tests/test_rich_errors_optout.js && node tests/test_source_positions.js && node tests/fuzz_positions.js && node tests/test_data_positions.js && node tests/test_render_shared.js && node tests/test_renderers.js && node tests/test_runtime_error_dx.js && node tests/test_aot_error_dx.js && node tests/test_abort_early.js && node tests/test_branch_collapse.js && node tests/test_suggestions.js && node tests/test_cli_validate.js && node benchmark/bench_aot_size.mjs",
45
+ "test": "node test.js && node tests/test_no_native.js && node tests/test_safe_regex.js && node tests/test_safe_regex_integration.js && node tests/test_aot_build.js && node tests/test_aot_differential.js && node tests/test_aot_cli_build.js && node tests/test_aot_cli_smoke.js && node tests/test_bundle_standalone.js && node tests/test_standalone_anyof.js && node tests/test_standalone_formats.js && node tests/test_aot_additional_props_errors.js && node tests/test_id_anchor_refs.js && node tests/test_typed_validator_runner.js && node tests/test_define_schema.js && node tests/test_error_codes_lock.js && node tests/test_nullable.js && node tests/test_validate_and_parse.js && node tests/test_validate_data.js && node tests/test_enrich_error.js && node tests/test_rich_errors_optout.js && node tests/test_source_positions.js && node tests/fuzz_positions.js && node tests/test_data_positions.js && node tests/test_render_shared.js && node tests/test_renderers.js && node tests/test_runtime_error_dx.js && node tests/test_aot_error_dx.js && node tests/test_abort_early.js && node tests/test_branch_collapse.js && node tests/test_suggestions.js && node tests/test_cli_validate.js && node tests/test_cli_version.js && node benchmark/bench_aot_size.mjs",
46
46
  "bench:size": "node benchmark/bench_aot_size.mjs",
47
47
  "test:suite": "node tests/run_suite.js",
48
48
  "test:compat": "node tests/test_compat.js",