ata-validator 1.47.0 → 1.48.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -43,6 +43,16 @@ npm install ata-validator --omit=optional
43
43
 
44
44
  or set `ATA_NO_NATIVE=1` at runtime. Typical schemas compile to specialized JS; shapes the compiler cannot represent (some `$dynamicRef`, cyclic `$ref`, unusual keyword interactions) fall back to an interpreted engine, so every schema validates in every environment. The pure-JS setup scores the same on the official suite as the native one, 1301 of 1301 Draft 2020-12 cases. The buffer and parallel APIs (`isValid` on raw buffers, `isValidPrepadded`, `isValidNDJSON`, `isValidParallel`, `countValid`, `batchIsValid`, `validateAndParse`) work without the addon too, answering through the same checks as `isValidJSON()`, slower than the addon: 177 ns against 92 for a small document on Node 25. In a browser, which has no `Buffer`, they throw and name the methods to use instead.
45
45
 
46
+ Where the size of what ships matters more than nanoseconds per call, `ata-validator/lite` is
47
+ the same `Validator` on the interpreted engine alone, without the code generator: the same
48
+ verdicts and errors, 58 KB gzipped in a bundled worker against 136 KB for the full entry.
49
+ It leaves out what needs the generator, `parse()` and the ahead-of-time bundle methods.
50
+
51
+ On Deno the same package is `npm:ata-validator` or `jsr:@ata/validator`; the JSR package
52
+ re-exports the npm one at the same version, and CI runs the official suite under Deno. The
53
+ details, and what Deno Deploy's refusal of dynamic code means for it, are in
54
+ [docs/edge-runtimes.md](docs/edge-runtimes.md).
55
+
46
56
  Those four now agree with `validate()` on every case of the official suite, 3365 across three dialects. The native walker behind them does not handle every shape (`contains`, `unevaluatedProperties`, `patternProperties`, tuple `items`, cross-document `$ref`, a few formats), so for schemas using one of those the buffer APIs parse the bytes and answer through `validate()`; the list is in `lib/buffer-gate.js`. Typical request schemas stay on the zero-copy path. `npm test` holds the disagreement count at zero.
47
57
 
48
58
  Where `new Function` is refused altogether, on Cloudflare Workers, Deno Deploy or under a strict Content-Security-Policy, ata drops to the interpreted engine and scores the same 1301 of 1301 with code generation blocked. No flags, and on Workers no `nodejs_compat` either. See [docs/edge-runtimes.md](docs/edge-runtimes.md).
@@ -280,6 +290,15 @@ const result = v.validate(data)
280
290
 
281
291
  The runtime API is unchanged from previous releases. Code written against the default validator's class keeps working through `ata-validator/compat`, which covers `compile`, `addSchema`, `addFormat`, `addKeyword`, `errorsText` and the rest of that surface, and reports errors in the same shape and order. `npx ata migrate` reports what the switch would change in a project, and what it cannot translate, before anything is written. See [docs/migration-from-ajv.md](docs/migration-from-ajv.md).
282
292
 
293
+ Coming from the default validator, one command reports what the switch touches and a second applies the mechanical part:
294
+
295
+ ```bash
296
+ npx ata migrate # per file: imports to rewrite, addFormats() calls that go, sites to look at by hand
297
+ npx ata migrate --write # rewrites the imports to ata-validator/compat and removes the calls, leaves the rest
298
+ ```
299
+
300
+ Coercion, defaults, `allErrors` and the error objects behave as before; the details and the limits are in [docs/migration-from-ajv.md](docs/migration-from-ajv.md).
301
+
283
302
  ## Usage
284
303
 
285
304
  ### Node.js
@@ -254,9 +254,17 @@ function enrich (rawErr, opts) {
254
254
  // that answered wrote, the generator's internal ordering key included.
255
255
  if (rawErr.branchErrors) out.branchErrors = rawErr.branchErrors.map((b) => enrich(b, data !== undefined ? { data } : null));
256
256
 
257
+ // The frames come from the position map, which is a walk of the document.
258
+ // Given lazily (validateJSON's rejections), the walk waits for the first
259
+ // read of any error's `dataFrame` or `anchor`: a caller that reads the
260
+ // messages and paths never pays for it, and one that reads every frame
261
+ // pays once, as before. Given eagerly, the fields are set as before.
262
+ const named = (rawErr.params && (rawErr.params.additionalProperty || rawErr.params.unevaluatedProperty)) || null;
257
263
  if (positions && positions[path]) {
258
264
  const p = positions[path];
259
265
  out.dataFrame = { byteOffset: p.byteOffset, length: p.length, line: p.line, col: p.col, text: p.text };
266
+ } else if (!positions && opts && opts.lazyPositions) {
267
+ defineLazyFrames(out, path, named, opts.lazyPositions);
260
268
  }
261
269
 
262
270
  // Attach schema source frame when the validator was constructed with a
@@ -286,7 +294,6 @@ function enrich (rawErr, opts) {
286
294
  // adds the key span so a caret can sit on the property that is wrong rather
287
295
  // than on the object containing it.
288
296
  if (opts && opts.positions) {
289
- const named = (rawErr.params && (rawErr.params.additionalProperty || rawErr.params.unevaluatedProperty)) || null;
290
297
  const own = opts.positions[path];
291
298
  const child = named ? opts.positions[(path === '' ? '' : path) + '/' + named] : null;
292
299
  const src = child || own;
@@ -303,4 +310,42 @@ function enrich (rawErr, opts) {
303
310
  return out;
304
311
  }
305
312
 
313
+ // `dataFrame` and `anchor` as accessors that resolve the position map on the
314
+ // first read of either, on any error of the list (the map is shared through
315
+ // `lazy`), and then become plain data properties, or go away when the map has
316
+ // no entry for the error, which is the shape the eager path gives. Both are
317
+ // enumerable, so JSON.stringify and a deep comparison read them as values;
318
+ // a write replaces the accessor the way an assignment to a plain field would.
319
+ function defineLazyFrames (out, path, named, lazy) {
320
+ let done = false;
321
+ let frames = null;
322
+ const resolve = () => {
323
+ if (done) return frames;
324
+ done = true;
325
+ const positions = lazy();
326
+ const own = positions ? positions[path] : null;
327
+ const dataFrame = own ? { byteOffset: own.byteOffset, length: own.length, line: own.line, col: own.col, text: own.text } : undefined;
328
+ const child = positions && named ? positions[(path === '' ? '' : path) + '/' + named] : null;
329
+ const src = child || own;
330
+ let anchor;
331
+ if (src) {
332
+ anchor = { line: src.line, col: src.col, length: src.length };
333
+ if (src.keyOffset !== undefined) {
334
+ anchor.keyLine = src.keyLine;
335
+ anchor.keyCol = src.keyCol;
336
+ anchor.keyLength = src.keyLength;
337
+ }
338
+ }
339
+ frames = { dataFrame, anchor };
340
+ if (dataFrame !== undefined) Object.defineProperty(out, 'dataFrame', { value: dataFrame, enumerable: true, configurable: true, writable: true });
341
+ else delete out.dataFrame;
342
+ if (anchor !== undefined) Object.defineProperty(out, 'anchor', { value: anchor, enumerable: true, configurable: true, writable: true });
343
+ else delete out.anchor;
344
+ return frames;
345
+ };
346
+ const settable = (name) => (v) => Object.defineProperty(out, name, { value: v, enumerable: true, configurable: true, writable: true });
347
+ Object.defineProperty(out, 'dataFrame', { get () { return resolve().dataFrame; }, set: settable('dataFrame'), enumerable: true, configurable: true });
348
+ Object.defineProperty(out, 'anchor', { get () { return resolve().anchor; }, set: settable('anchor'), enumerable: true, configurable: true });
349
+ }
350
+
306
351
  module.exports = { enrich, reprValue, expectedFor, detailFor, rankFor, MISSING, DOC_BASE };
@@ -1351,9 +1351,15 @@ function genCodeENode(schema, v, pathExpr, lines, ctx, schemaPrefix) {
1351
1351
  if (schema.properties) {
1352
1352
  for (const [key, prop] of Object.entries(schema.properties)) {
1353
1353
  const childPath = childPathExpr(pathExpr, ptrSeg(key))
1354
+ const sp = schemaPrefix+'/properties/'+ptrSeg(key)
1355
+ const from = lines.length
1354
1356
  lines.push(`if(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v})&&${ownKeyExpr(ctx, v, key)}){`)
1355
- genCodeE(prop, `${v}[${JSON.stringify(key)}]`, childPath, lines, ctx, schemaPrefix+'/properties/'+ptrSeg(key))
1357
+ // A large subschema as a function of its own (emitPropFnE); false means inline.
1358
+ if (!propFnCandidateE(prop, ctx, sp) || !emitPropFnE(prop, `${v}[${JSON.stringify(key)}]`, childPath, lines, ctx, sp)) {
1359
+ genCodeE(prop, `${v}[${JSON.stringify(key)}]`, childPath, lines, ctx, sp)
1360
+ }
1356
1361
  lines.push(`}`)
1362
+ countInlineE(lines, from, ctx)
1357
1363
  }
1358
1364
  }
1359
1365
 
@@ -2039,6 +2045,86 @@ function emitSharedDefC(defName, v, pathExpr, lines, ctx, schemaPrefix) {
2039
2045
  return true
2040
2046
  }
2041
2047
 
2048
+ // The same split for the error collector (genCodeE): a large property
2049
+ // subschema becomes `_pe<n>(d,_p,_all,_e)` in the helper code, pushing into
2050
+ // the caller's list as an inlined body would, generated against the real
2051
+ // schema path since it has one call site. Not inside a shared definition's
2052
+ // body, whose placeholder path is substituted after the fact.
2053
+ function propFnCandidateE (prop, ctx, schemaPrefix) {
2054
+ if (!ctx || ctx.nestedBoolean || ctx.condDepth) return false
2055
+ if (typeof schemaPrefix !== 'string' || schemaPrefix.includes(SP_PLACEHOLDER)) return false
2056
+ if (!prop || typeof prop !== 'object' || Array.isArray(prop)) return false
2057
+ let size = 0
2058
+ try { size = JSON.stringify(prop).length } catch { return false }
2059
+ return size >= SPLIT_C_SCHEMA || ((ctx.fnBytesE || 0) > SPLIT_C_BUDGET && size >= SPLIT_C_SMALL)
2060
+ }
2061
+ function emitPropFnE (prop, v, pathExpr, lines, ctx, schemaPrefix) {
2062
+ const outerBytes = ctx.fnBytesE
2063
+ ctx.fnBytesE = 0
2064
+ const bodyLines = []
2065
+ try { genCodeE(prop, 'd', '_p', bodyLines, ctx, schemaPrefix) } finally { ctx.fnBytesE = outerBytes }
2066
+ if (bodyLines.length === 0) return false
2067
+ const body = bodyLines.join('\n ')
2068
+ if (body.includes(SP_PLACEHOLDER)) return false
2069
+ const fnName = '_pe' + (ctx.propFnsE = (ctx.propFnsE || 0) + 1)
2070
+ ctx.helperCode.push(`function ${fnName}(d,_p,_all,_e){${body}}`)
2071
+ lines.push(`${fnName}(${v},${pathExpr || '""'},_all,_e);if(!_all&&_e.length)return{valid:false,errors:_e}`)
2072
+ return true
2073
+ }
2074
+ function countInlineE (lines, from, ctx) {
2075
+ let n = 0
2076
+ for (let i = from; i < lines.length; i++) n += lines[i].length
2077
+ ctx.fnBytesE = (ctx.fnBytesE || 0) + n
2078
+ }
2079
+
2080
+ // A large property subschema as a function of its own, for the reason the
2081
+ // verdict generator has (emitPropFn in js-compiler.js): one function per
2082
+ // schema outgrew what V8 optimizes, and a 167 KB configuration schema's
2083
+ // one-pass function was 3.8 MB of source that ran in the interpreter. The
2084
+ // function is used at one site, so it is generated against the real schema
2085
+ // path and its errors keep their precise ordinal keys; it is not written
2086
+ // inside a shared definition's body, whose placeholder path is substituted
2087
+ // after the fact and would not reach a helper of its own.
2088
+ const SPLIT_C_SCHEMA = 1500
2089
+ const SPLIT_C_SMALL = 160
2090
+ const SPLIT_C_BUDGET = 16 * 1024
2091
+ function propFnCandidateC (prop, ctx, schemaPrefix) {
2092
+ if (!ctx || ctx.inDefC || ctx.nestedBoolean || ctx.condDepth) return false
2093
+ if (typeof schemaPrefix !== 'string' || schemaPrefix.includes(SP_PLACEHOLDER)) return false
2094
+ if (!prop || typeof prop !== 'object' || Array.isArray(prop)) return false
2095
+ let size = 0
2096
+ try { size = JSON.stringify(prop).length } catch { return false }
2097
+ return size >= SPLIT_C_SCHEMA || ((ctx.fnBytesC || 0) > SPLIT_C_BUDGET && size >= SPLIT_C_SMALL)
2098
+ }
2099
+ function emitPropFnC (prop, v, pathExpr, lines, ctx, schemaPrefix) {
2100
+ const outerHoisted = ctx.hoisted
2101
+ ctx.hoisted = []
2102
+ const outerBytes = ctx.fnBytesC
2103
+ ctx.fnBytesC = 0
2104
+ const bodyLines = []
2105
+ let hoisted
2106
+ try {
2107
+ genGuardedC(prop, '_dv', '_p', bodyLines, ctx, schemaPrefix)
2108
+ } finally {
2109
+ hoisted = ctx.hoisted
2110
+ ctx.hoisted = outerHoisted
2111
+ ctx.fnBytesC = outerBytes
2112
+ }
2113
+ if (bodyLines.length === 0) return false
2114
+ const body = bodyLines.join('\n ')
2115
+ if (body.includes(SP_PLACEHOLDER) || /\b_so\b/.test(body)) return false
2116
+ const decl = hoisted.length ? `let ${hoisted.map((n) => n + '=null').join(',')};` : ''
2117
+ const fnName = '_pc' + (ctx.propFnsC = (ctx.propFnsC || 0) + 1)
2118
+ ctx.helperCode.push(`function ${fnName}(_dv,_p,d){const _all=true;${decl}let _e;\n ${body}\n return _e}`)
2119
+ lines.push(`{const _r=${fnName}(${v},${pathExpr || '""'},d);if(_r!==undefined){if(_e===undefined)_e=_r;else for(let _i=0;_i<_r.length;_i++)_e.push(_r[_i])}}`)
2120
+ return true
2121
+ }
2122
+ function countInlineC (lines, from, ctx) {
2123
+ let n = 0
2124
+ for (let i = from; i < lines.length; i++) n += lines[i].length
2125
+ ctx.fnBytesC = (ctx.fnBytesC || 0) + n
2126
+ }
2127
+
2042
2128
  // One schema as a function of its own in the combined function (see
2043
2129
  // emitSharedDefC). The name is registered before the body is written, so a
2044
2130
  // reference back to it inside becomes a call. One that can re-enter itself,
@@ -2507,21 +2593,28 @@ function genCodeCNode(schema, v, pathExpr, lines, ctx, schemaPrefix) {
2507
2593
  for (const [key, prop] of Object.entries(schema.properties)) {
2508
2594
  const pv = hoisted[key] || `${v}[${JSON.stringify(key)}]`
2509
2595
  const childPath = childPathExpr(pathExpr, ptrSeg(key))
2596
+ const sp = schemaPrefix+'/properties/'+ptrSeg(key)
2597
+ const from = lines.length
2598
+ const split = propFnCandidateC(prop, ctx, sp)
2599
+ // A large subschema as a function of its own (emitPropFnC), called
2600
+ // where the inline code would be; false means it is written inline.
2601
+ const sub = (pvExpr) => { if (!split || !emitPropFnC(prop, pvExpr, childPath, lines, ctx, sp)) genGuardedC(prop, pvExpr, childPath, lines, ctx, sp) }
2510
2602
  if (requiredSet.has(key) && isObj) {
2511
2603
  // A hoisted local was already cleared if the value is only inherited.
2512
2604
  lines.push(`if(${pv}!==undefined${hoisted[key] ? '' : '&&' + ownGuard(ctx, v, key)}){`)
2513
- genGuardedC(prop, pv, childPath, lines, ctx, schemaPrefix+'/properties/'+ptrSeg(key))
2605
+ sub(pv)
2514
2606
  lines.push(`}`)
2515
2607
  } else if (isObj) {
2516
2608
  const oi = ctx.varCounter++
2517
2609
  lines.push(`{const _o${oi}=${v}[${JSON.stringify(key)}];if(_o${oi}!==undefined&&${ownGuard(ctx, v, key)}){`)
2518
- genGuardedC(prop, `_o${oi}`, childPath, lines, ctx, schemaPrefix+'/properties/'+ptrSeg(key))
2610
+ sub(`_o${oi}`)
2519
2611
  lines.push(`}}`)
2520
2612
  } else {
2521
2613
  lines.push(`if(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v})&&${ownKeyExpr(ctx, v, key)}){`)
2522
- genGuardedC(prop, `${v}[${JSON.stringify(key)}]`, childPath, lines, ctx, schemaPrefix+'/properties/'+ptrSeg(key))
2614
+ sub(`${v}[${JSON.stringify(key)}]`)
2523
2615
  lines.push(`}`)
2524
2616
  }
2617
+ countInlineC(lines, from, ctx)
2525
2618
  }
2526
2619
  }
2527
2620
 
@@ -1161,6 +1161,60 @@ function _setCodeBudget (n) { codeBudget = n === undefined ? CODE_BUDGET : n }
1161
1161
  // document that reached it, and the second cut the cycle, so the reference
1162
1162
  // checked nothing. While `nestedBoolean` is set, genCode declines at those
1163
1163
  // points instead, and the caller's generator declines the schema.
1164
+ // A property whose subschema is large becomes a function of its own in the
1165
+ // preamble, called where the property is checked, instead of its code inline.
1166
+ // V8 does not optimize a function past its bytecode budget (about 60 KB), and
1167
+ // a configuration schema of a few hundred properties compiled to one function
1168
+ // of a megabyte or more that ran in the interpreter for its whole life: on a
1169
+ // 167 KB schema the verdict took 407 us where the same checks in functions of
1170
+ // their own take 40. The threshold is on the schema's own size, which is
1171
+ // known before any code is written, so nothing is generated twice. A
1172
+ // property moves out on its own only when its schema is plainly large; the
1173
+ // usual case is the budget: once a function holds SPLIT_FN_BUDGET bytes of
1174
+ // inline code, every property past SPLIT_PROP_SMALL moves out, so a schema
1175
+ // of many middling properties splits after its first functions' worth and a
1176
+ // small or medium one, whose whole program fits the budget, never does and
1177
+ // pays no call.
1178
+ const SPLIT_PROP_SCHEMA = 2000
1179
+ const SPLIT_PROP_SMALL = 220
1180
+ const SPLIT_FN_BUDGET = 24 * 1024
1181
+ function propSchemaSize (prop) {
1182
+ try { return JSON.stringify(prop).length } catch { return 0 }
1183
+ }
1184
+ function propFnCandidate (prop, ctx) {
1185
+ if (!ctx.preamble || ctx.nestedBoolean || ctx.condDepth) return false
1186
+ if (!prop || typeof prop !== 'object' || Array.isArray(prop)) return false
1187
+ const size = propSchemaSize(prop)
1188
+ return size >= SPLIT_PROP_SCHEMA || ((ctx.fnBytes || 0) > SPLIT_FN_BUDGET && size >= SPLIT_PROP_SMALL)
1189
+ }
1190
+ // Generated over its own `d`, the way a shared definition is (see the $ref
1191
+ // case in genCodeNode): the deferred checks belong to this body, and the
1192
+ // plain-object reuse state must not leak in or out. Null means "write it
1193
+ // inline after all": an empty body (the inline path then asserts it the
1194
+ // usual way) or one that calls the recursive entry, which is out of scope
1195
+ // in the preamble.
1196
+ function emitPropFn (prop, ctx) {
1197
+ const bodyLines = []
1198
+ const outerDeferred = ctx.deferredChecks
1199
+ ctx.deferredChecks = null
1200
+ const outerReuse = ctx.plainReuse
1201
+ ctx.plainReuse = undefined
1202
+ const outerBytes = ctx.fnBytes
1203
+ ctx.fnBytes = 0
1204
+ try { genCode(prop, 'd', bodyLines, ctx) } finally { ctx.plainReuse = outerReuse; ctx.fnBytes = outerBytes }
1205
+ if (ctx.deferredChecks) for (const dc of ctx.deferredChecks) bodyLines.push(dc)
1206
+ ctx.deferredChecks = outerDeferred
1207
+ if (bodyLines.length === 0 || bodyLines.some((l) => /\b_validate\b/.test(l))) return null
1208
+ const fnName = '_pf' + (ctx.propFns = (ctx.propFns || 0) + 1)
1209
+ hoist(ctx, `function ${fnName}(d){${bodyLines.join('\n ')}\n return true}`)
1210
+ return fnName
1211
+ }
1212
+ function countInline (lines, from, ctx) {
1213
+ let n = 0
1214
+ for (let i = from; i < lines.length; i++) n += lines[i].length
1215
+ ctx.fnBytes = (ctx.fnBytes || 0) + n
1216
+ }
1217
+
1164
1218
  function nestedGenCode(schema, v, lines, ctx) {
1165
1219
  ctx.nestedBoolean = (ctx.nestedBoolean || 0) + 1
1166
1220
  const before = lines.length
@@ -3498,11 +3552,17 @@ function genCodeNode(schema, v, lines, ctx, knownType) {
3498
3552
  // properties — use hoisted vars for required props, hoist optional to locals too
3499
3553
  if (schema.properties) {
3500
3554
  for (const [key, prop] of Object.entries(schema.properties)) {
3555
+ const from = lines.length
3556
+ // A large subschema as a function of its own (emitPropFn); the call
3557
+ // takes the place of the inline code in each of the three shapes below.
3558
+ const fn = propFnCandidate(prop, ctx) ? emitPropFn(prop, ctx) : null
3501
3559
  if (requiredSet.has(key) && isObj) {
3502
3560
  // Required + type:object — hoist to local to reduce repeated property lookups
3503
3561
  const access = hoisted[key] || `${v}[${JSON.stringify(key)}]`
3504
- const combined = tryGenCombined(prop, access, ctx)
3505
- if (combined) {
3562
+ const combined = fn ? null : tryGenCombined(prop, access, ctx)
3563
+ if (fn) {
3564
+ lines.push(`if(!${fn}(${access}))return false`)
3565
+ } else if (combined) {
3506
3566
  lines.push(combined)
3507
3567
  } else if (needsLocal(prop) || isPlainContainer(prop)) {
3508
3568
  const oi = ctx.varCounter++
@@ -3518,8 +3578,10 @@ function genCodeNode(schema, v, lines, ctx, knownType) {
3518
3578
  const oi = ctx.varCounter++
3519
3579
  const local = `_o${oi}`
3520
3580
  lines.push(`{const ${local}=${v}[${JSON.stringify(key)}];if(${local}!==undefined&&${ownGuard(ctx, v, key)}){`)
3521
- const combined = tryGenCombined(prop, local, ctx)
3522
- if (combined) {
3581
+ const combined = fn ? null : tryGenCombined(prop, local, ctx)
3582
+ if (fn) {
3583
+ lines.push(`if(!${fn}(${local}))return false`)
3584
+ } else if (combined) {
3523
3585
  lines.push(combined)
3524
3586
  } else {
3525
3587
  genCode(prop, local, lines, ctx)
@@ -3527,9 +3589,11 @@ function genCodeNode(schema, v, lines, ctx, knownType) {
3527
3589
  lines.push(`}}`)
3528
3590
  } else {
3529
3591
  lines.push(`if(typeof ${v}==='object'&&${v}!==null&&${ownKeyExpr(ctx, v, key)}){`)
3530
- genCode(prop, `${v}[${JSON.stringify(key)}]`, lines, ctx)
3592
+ if (fn) lines.push(`if(!${fn}(${v}[${JSON.stringify(key)}]))return false`)
3593
+ else genCode(prop, `${v}[${JSON.stringify(key)}]`, lines, ctx)
3531
3594
  lines.push(`}`)
3532
3595
  }
3596
+ countInline(lines, from, ctx)
3533
3597
  }
3534
3598
  }
3535
3599
 
package/lib/rejections.js CHANGED
@@ -84,17 +84,16 @@ class RichRejection {
84
84
  // legacy key set, or enriched. Shared by both rejection classes below.
85
85
  function presentErrors(raw, data, rawInput, self, root, enrich) {
86
86
  if (needsOrdering(raw)) raw = sortErrorsBySchemaOrder(root, raw);
87
- // The position map, resolved now that an error is actually being read.
88
- let positions = null;
89
- if (enrich && raw.length && rawInput != null) {
90
- positions = self._pos().targeted(rawInput, wantedPointersFor(raw));
91
- if (positions) self._posCache.reset();
92
- }
87
+ // The position map, resolved on the first read of an error's frame (see
88
+ // defineLazyFrames in lib/enrich-error.js): a walk of the document that a
89
+ // caller reading messages and paths never needs.
90
+ const lazyPositions = enrich && raw.length && rawInput != null ? lazyPositionsFor(self, rawInput, raw) : null;
93
91
  // One options object for the whole list, not one per error.
94
92
  const opts = enrich && raw.length
95
93
  ? {
96
94
  data,
97
- positions,
95
+ positions: null,
96
+ lazyPositions,
98
97
  schemaPositions: self._schemaPositions,
99
98
  schemaFile: self._source ? self._source.path : undefined,
100
99
  }
@@ -180,13 +179,16 @@ Object.defineProperty(LazyJsonRejection.prototype, 'errors', {
180
179
  // on raw errors; taking it as the signal left those errors without
181
180
  // `received` or a suggestion on the text path while validate() had both.
182
181
  if (!raw[0] || !('received' in raw[0])) {
183
- const positions = self._pos().targeted(jsonStr, wantedPointersFor(raw));
184
182
  // Declaration order, as validate() applies it; the text path used to
185
183
  // enrich in emission order.
186
184
  const ordered = needsOrdering(raw) ? sortErrorsBySchemaOrder(self._schemaObj, raw) : raw;
185
+ // The position map waits for the first read of a frame (lib/enrich-error
186
+ // defineLazyFrames); until then reading the list costs the enrichment alone.
187
+ const lazyPositions = lazyPositionsFor(self, jsonStr, ordered);
187
188
  const enrichOpts = {
188
189
  data: parsedData,
189
- positions,
190
+ positions: null,
191
+ lazyPositions,
190
192
  schemaPositions: self._schemaPositions,
191
193
  schemaFile: self._source ? self._source.path : undefined,
192
194
  };
@@ -195,11 +197,11 @@ Object.defineProperty(LazyJsonRejection.prototype, 'errors', {
195
197
  attachDiagnosticSource(enriched, {
196
198
  data: parsedData,
197
199
  text: jsonStr,
198
- positions,
200
+ // Read by the diagnostic renderer only, which resolves the same map.
201
+ get positions () { return lazyPositions(); },
199
202
  schema: self._schemaObj,
200
203
  mutatesInput: self._mutatesInput === true,
201
204
  });
202
- if (positions) self._posCache.reset();
203
205
  this._cached = enriched;
204
206
  return enriched;
205
207
  }
@@ -232,6 +234,20 @@ Object.defineProperty(LazyJsonRejection.prototype, 'errors', {
232
234
  },
233
235
  });
234
236
 
237
+ // The targeted position map for a list, built on the first call and shared
238
+ // by every error's frame accessor (lib/enrich-error defineLazyFrames).
239
+ function lazyPositionsFor (self, text, errors) {
240
+ let done = false;
241
+ let positions = null;
242
+ return () => {
243
+ if (done) return positions;
244
+ done = true;
245
+ positions = self._pos().targeted(text, wantedPointersFor(errors));
246
+ if (positions) self._posCache.reset();
247
+ return positions;
248
+ };
249
+ }
250
+
235
251
  // The pointers a set of errors will ask the position map about. lib/enrich-error
236
252
  // looks up the error's own path, and for an additional or unevaluated property
237
253
  // the child pointer named in `params`, unescaped, which is the form it asks for.
@@ -892,7 +892,7 @@ function emitArray(schema, ctx, out, depth) {
892
892
  // Every `return 0` inside the member becomes that record, which is also how
893
893
  // the rule reaches nested objects: an inner object's own verdict is a value
894
894
  // failure at the level above it.
895
- function emitMember(sub, ctx, out, depth, objId) {
895
+ function emitMember(sub, ctx, out, depth, objId, kn) {
896
896
  let lines = [];
897
897
  emitValue(sub, ctx, lines, depth + 1);
898
898
  // A large member value becomes a function of its own, so no one function
@@ -921,9 +921,17 @@ function emitMember(sub, ctx, out, depth, objId) {
921
921
  out.push(label + ':{');
922
922
  out.push(body.split('return 0;').join('{_f' + objId + '=2;break ' + label + ';}'));
923
923
  out.push('}');
924
- // Resync from where the value started: a scan that stopped partway through
925
- // one cannot say where it ends, and the skipper can.
926
- out.push('if(_f' + objId + '===2){_f' + objId + '=1;i=_r.skipValue(s,_vs' + label + ');if(i<0)return 0;c=s.charCodeAt(i);}');
924
+ // A failed member is the verdict unless this object repeats the key later,
925
+ // since the parser keeps the last value. Whether it can is answered by two
926
+ // native searches over the rest of the text, for the key as written and
927
+ // for any backslash, which is the only way to spell that key differently:
928
+ // when neither is there the scan ends here, and a 69 KB body with a bad
929
+ // first item is refused in the time of an indexOf rather than a full pass.
930
+ // Otherwise resync from where the value started, since a scan that stopped
931
+ // partway through one cannot say where it ends and the skipper can, and
932
+ // go on, so a repeat still bails to the parser.
933
+ out.push('if(_f' + objId + '===2){if(s.indexOf(s.slice(_p' + kn + '-1,_e' + kn + '+1),i)<0&&s.indexOf("\\\\",i)<0)return 0;');
934
+ out.push('_f' + objId + '=1;i=_r.skipValue(s,_vs' + label + ');if(i<0)return 0;c=s.charCodeAt(i);}');
927
935
  }
928
936
 
929
937
  function emitObject(schema, ctx, out, depth) {
@@ -1111,7 +1119,7 @@ function emitObject(schema, ctx, out, depth) {
1111
1119
  const cond = gi === bounds.length - 1 ? '' : 'if(_kid' + kn + '<' + g.end + ')';
1112
1120
  out.push((gi === 0 ? '' : 'else ') + cond + '{');
1113
1121
  if (g.sub === undefined) out.push(addH);
1114
- else emitMember(g.sub, ctx, out, depth, n);
1122
+ else emitMember(g.sub, ctx, out, depth, n, kn);
1115
1123
  out.push('}');
1116
1124
  prev = g.end;
1117
1125
  });
@@ -1160,7 +1168,7 @@ function emitObject(schema, ctx, out, depth) {
1160
1168
  out.push('_s' + n + '_' + slot.w + '|=' + slot.b + ';');
1161
1169
  const sub = props && Object.prototype.hasOwnProperty.call(props, name) ? props[name] : undefined;
1162
1170
  if (sub === undefined) out.push(additional.join('\n'));
1163
- else emitMember(sub, ctx, out, depth, n);
1171
+ else emitMember(sub, ctx, out, depth, n, kn);
1164
1172
  out.push('}');
1165
1173
  }
1166
1174
  out.push('else{' + additional.join('\n') + '}');
@@ -1802,8 +1802,12 @@ Validator.loadBundle = function (mods, schemas, opts) {
1802
1802
  // simdjson when the addon is there, JSON.parse otherwise; the addon loads on
1803
1803
  // the first call rather than when the package is required.
1804
1804
  function parseJSON(input) {
1805
+ // The addon has not carried parseJSON since the per-platform packages
1806
+ // (1.1.0); the call threw a TypeError whenever the engine was loaded and
1807
+ // returned JSON.parse's answer otherwise. A Buffer is decoded first.
1805
1808
  const native = getNative();
1806
- return native ? native.parseJSON(input) : JSON.parse(input);
1809
+ if (native && typeof native.parseJSON === 'function') return native.parseJSON(input);
1810
+ return JSON.parse(typeof input === 'string' ? input : String(input));
1807
1811
  }
1808
1812
 
1809
1813
  // Ultra-fast compile: returns validate function directly, no Validator wrapper
package/lib/version.js CHANGED
@@ -7,4 +7,4 @@
7
7
  //
8
8
  // Kept in lockstep with package.json by `tests/test_version_sync.js`.
9
9
 
10
- module.exports = '1.47.0';
10
+ module.exports = '1.48.0';
package/lite.d.ts ADDED
@@ -0,0 +1,12 @@
1
+ // ata-validator/lite: the same Validator and one-shot functions as
2
+ // ata-validator, on the interpreted engine and without the code generator.
3
+ // Validator#parse and the static bundle methods throw a TypeError here.
4
+ export {
5
+ Validator,
6
+ validate,
7
+ validateAsync,
8
+ parseAsync,
9
+ defineSchema,
10
+ version,
11
+ } from './index.js';
12
+ export type * from './index.js';
package/lite.js ADDED
@@ -0,0 +1,20 @@
1
+ 'use strict';
2
+
3
+ // ata-validator/lite: the Validator without the code generator. Every schema
4
+ // runs on the interpreted engine, which passes the same official test suite as
5
+ // the compiled path and never calls `new Function`, so it also runs under a
6
+ // strict Content-Security-Policy. The results are the ones `ata-validator`
7
+ // gives: the same verdicts, the same errors. What it leaves out is what needs
8
+ // the code generator (parse(), the ahead-of-time bundle methods) and the tools
9
+ // no Validator calls (TypeScript generation, the renderers). Made for pages
10
+ // where the size of what ships matters more than nanoseconds per call.
11
+ const core = require('./lib/validator-core.js');
12
+
13
+ module.exports = {
14
+ Validator: core.Validator,
15
+ validate: core.validate,
16
+ validateAsync: core.validateAsync,
17
+ parseAsync: core.parseAsync,
18
+ defineSchema: core.defineSchema,
19
+ version: core.version,
20
+ };
package/lite.mjs ADDED
@@ -0,0 +1,3 @@
1
+ import mod from './lite.js';
2
+ export const { Validator, validate, validateAsync, parseAsync, defineSchema, version } = mod;
3
+ export default mod;
package/package.json CHANGED
@@ -1,7 +1,8 @@
1
1
  {
2
2
  "name": "ata-validator",
3
- "version": "1.47.0",
3
+ "version": "1.48.0",
4
4
  "description": "JSON Schema validator that compiles to plain JavaScript, still runs where code generation is blocked, and can be compiled away at build time. Passes the full official test suite.",
5
+ "type": "commonjs",
5
6
  "main": "index.js",
6
7
  "module": "index.mjs",
7
8
  "types": "index.d.ts",
@@ -48,6 +49,11 @@
48
49
  "import": "./compiled-verdict.mjs",
49
50
  "require": "./compiled-verdict.js"
50
51
  },
52
+ "./lite": {
53
+ "types": "./lite.d.ts",
54
+ "import": "./lite.mjs",
55
+ "require": "./lite.js"
56
+ },
51
57
  "./package.json": "./package.json"
52
58
  },
53
59
  "sideEffects": false,
@@ -138,18 +144,21 @@
138
144
  "compiled-verdict.js",
139
145
  "compiled-verdict.mjs",
140
146
  "compiled-verdict.d.ts",
147
+ "lite.js",
148
+ "lite.mjs",
149
+ "lite.d.ts",
141
150
  "bin/",
142
151
  "README.md",
143
152
  "LICENSE"
144
153
  ],
145
154
  "optionalDependencies": {
146
- "@ata-validator/native-darwin-arm64": "1.47.0",
147
- "@ata-validator/native-darwin-x64": "1.47.0",
148
- "@ata-validator/native-linux-arm64-gnu": "1.47.0",
149
- "@ata-validator/native-linux-arm64-musl": "1.47.0",
150
- "@ata-validator/native-linux-x64-gnu": "1.47.0",
151
- "@ata-validator/native-linux-x64-musl": "1.47.0",
152
- "@ata-validator/native-win32-x64": "1.47.0"
155
+ "@ata-validator/native-darwin-arm64": "1.48.0",
156
+ "@ata-validator/native-darwin-x64": "1.48.0",
157
+ "@ata-validator/native-linux-arm64-gnu": "1.48.0",
158
+ "@ata-validator/native-linux-arm64-musl": "1.48.0",
159
+ "@ata-validator/native-linux-x64-gnu": "1.48.0",
160
+ "@ata-validator/native-linux-x64-musl": "1.48.0",
161
+ "@ata-validator/native-win32-x64": "1.48.0"
153
162
  },
154
163
  "peerDependencies": {
155
164
  "yaml": "^2.0.0"