ata-validator 1.12.1 → 1.13.1

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,22 @@
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
+ ## 1.13.1 - 2026-09-06
6
+
7
+ ### Fixed
8
+
9
+ - The native engine stops printing to stderr for patterns outside RE2's subset. A pattern with lookahead or a backreference is an expected input, not a fault: construction is probed, and the JavaScript engines answer for the pattern instead. RE2 logged every such probe to stderr anyway, so a schema carrying zod's email pattern printed two C++ error lines on compile while validating correctly. RE2 is now constructed with logging off; verdicts are unchanged.
10
+
11
+ ## 1.13.0 - 2026-09-06
12
+
13
+ ### Changed
14
+
15
+ - The default browser bundle stops carrying the AOT emitters. `lib/aot.js` has promised a browser stub via the package.json `browser` field since the fs-free build, but the mapping was never actually in the field, so every browser bundle shipped the emitters and the embedded safe-regex engine source they pull in, along with the native walker's routing table that only matters when the native addon loads. Both now map to stubs. Emitting validators in a browser still works, through the new `ata-validator/aot` entry, and the `Validator.bundle*` statics in a browser bundle throw a message that points there; `Validator.loadBundle` keeps working in the default bundle, since loading a bundle somebody already built is a runtime act. Measured with the schema-benchmarks rolldown pipeline: minified and gzipped 73,491 to 64,812 bytes, 11.8 percent smaller. The browser-imports guard now bans a token from each excluded file so the mapping cannot regress silently.
16
+
17
+ ### Added
18
+
19
+ - `ata-validator/aot`: the standalone emitters (`toStandalone`, `toStandaloneModule`, `bundle`, `bundleStandalone`, `bundleCompact`, `loadBundle`) as an explicit, typed import. In Node it is the same module the statics use; in a browser it is the way to opt back into emission without paying for it on pages that never emit.
20
+
5
21
  ## 1.12.1 - 2026-09-06
6
22
 
7
23
  ### Fixed
package/README.md CHANGED
@@ -358,7 +358,7 @@ const v = new Validator(schema, {
358
358
 
359
359
  ### Build-time compile (`ata compile`)
360
360
 
361
- The `ata` CLI turns a JSON Schema file into a self-contained JavaScript module. No runtime dependency on `ata-validator`, so only the generated validator ships to the browser. Typical output is ~1 KB gzipped compared to ~27 KB for the full runtime.
361
+ The `ata` CLI turns a JSON Schema file into a self-contained JavaScript module. No runtime dependency on `ata-validator`, so only the generated validator ships to the browser. Typical output is ~1 KB gzipped compared to ~65 KB for the full runtime bundled for the browser.
362
362
 
363
363
  ```bash
364
364
  npx ata compile schemas/user.json -o src/generated/user.validator.mjs
package/aot.d.ts ADDED
@@ -0,0 +1,37 @@
1
+ import { Validator, JSONSchema } from './index';
2
+
3
+ /** Options accepted by the standalone emitters. See docs/aot.md. */
4
+ export interface StandaloneOptions {
5
+ /** Module format for the emitted source. Default: 'esm'. */
6
+ format?: 'esm' | 'cjs';
7
+ /** Use stub error functions for the smallest output. Default: false. */
8
+ abortEarly?: boolean;
9
+ [key: string]: unknown;
10
+ }
11
+
12
+ /** The constructor of Validator, as the emitters take it explicitly. */
13
+ export type ValidatorClass = typeof Validator;
14
+
15
+ /** Emit one standalone module's source for one validator, or null when the
16
+ * schema compiled through an engine that has no source form. */
17
+ export function toStandalone(validator: Validator<unknown>): string | null;
18
+
19
+ /** Emit one standalone module (no runtime dependency) for one validator. */
20
+ export function toStandaloneModule(validator: Validator<unknown>, opts?: StandaloneOptions): string;
21
+
22
+ /** Emit a module of per-schema compiled validators that `loadBundle` consumes. */
23
+ export function bundle(V: ValidatorClass, schemas: Array<JSONSchema | boolean>, opts?: StandaloneOptions): string;
24
+
25
+ /** Emit one self-contained module for many schemas, zero runtime dependency. */
26
+ export function bundleStandalone(V: ValidatorClass, schemas: Array<JSONSchema | boolean>, opts?: StandaloneOptions): string;
27
+
28
+ /** Like bundleStandalone with shared helpers deduplicated across schemas. */
29
+ export function bundleCompact(V: ValidatorClass, schemas: Array<JSONSchema | boolean>, opts?: StandaloneOptions): string;
30
+
31
+ /** Turn the modules a bundle produced back into Validator instances. */
32
+ export function loadBundle(
33
+ V: ValidatorClass,
34
+ mods: Array<unknown>,
35
+ schemas: Array<JSONSchema | boolean>,
36
+ opts?: object,
37
+ ): Array<Validator<unknown>>;
package/aot.js ADDED
@@ -0,0 +1,7 @@
1
+ 'use strict';
2
+
3
+ // Explicit entry for the AOT emitters, `require('ata-validator/aot')`. In
4
+ // Node this is the same module `Validator.bundleStandalone` and friends use.
5
+ // In a browser bundle it is the only way to get the emitters: the default
6
+ // entry maps them to a stub so pages that never emit code never carry it.
7
+ module.exports = require('./lib/aot-impl.js');
package/aot.mjs ADDED
@@ -0,0 +1,3 @@
1
+ import mod from './lib/aot-impl.js';
2
+ export const { toStandalone, toStandaloneModule, bundle, bundleStandalone, bundleCompact, loadBundle } = mod;
3
+ export default mod;
package/index.js CHANGED
@@ -1602,7 +1602,7 @@ class Validator {
1602
1602
  // validate() on shapes listed in lib/buffer-gate.js. For those schemas
1603
1603
  // every buffer entry point goes through validate() instead.
1604
1604
  if (native) {
1605
- const { bufferNeedsSlowPath, installSlowBufferApis } = require('./lib/buffer-gate');
1605
+ const { bufferNeedsSlowPath, installSlowBufferApis } = require('./lib/buffer-gate.js');
1606
1606
  if (bufferNeedsSlowPath(schemaObj, this._schemaMap)) installSlowBufferApis(this);
1607
1607
  }
1608
1608
 
@@ -1925,19 +1925,19 @@ function version() {
1925
1925
  // generated module shapes; browser bundles get `lib/aot.browser.js` (a stub
1926
1926
  // that throws) via the package.json `browser` field.
1927
1927
  Validator.bundle = function (schemas, opts) {
1928
- return require('./lib/aot').bundle(Validator, schemas, opts);
1928
+ return require('./lib/aot.js').bundle(Validator, schemas, opts);
1929
1929
  };
1930
1930
 
1931
1931
  Validator.bundleStandalone = function (schemas, opts) {
1932
- return require('./lib/aot').bundleStandalone(Validator, schemas, opts);
1932
+ return require('./lib/aot.js').bundleStandalone(Validator, schemas, opts);
1933
1933
  };
1934
1934
 
1935
1935
  Validator.bundleCompact = function (schemas, opts) {
1936
- return require('./lib/aot').bundleCompact(Validator, schemas, opts);
1936
+ return require('./lib/aot.js').bundleCompact(Validator, schemas, opts);
1937
1937
  };
1938
1938
 
1939
1939
  Validator.loadBundle = function (mods, schemas, opts) {
1940
- return require('./lib/aot').loadBundle(Validator, mods, schemas, opts);
1940
+ return require('./lib/aot.js').loadBundle(Validator, mods, schemas, opts);
1941
1941
  };
1942
1942
 
1943
1943
  const parseJSON = native ? native.parseJSON : JSON.parse;
@@ -0,0 +1,470 @@
1
+ 'use strict';
2
+
3
+ // All ahead-of-time (AOT) code generation lives here so the default and browser
4
+ // entries can stay free of `fs`, `path`, and `__dirname`.
5
+ // Browser bundles get `lib/aot.browser.js` instead (see package.json `browser`
6
+ // field), which throws if anyone tries to call an AOT function from the browser.
7
+ //
8
+ // Public surface (called by index.js and ata-validator/build):
9
+ // toStandalone(validator)
10
+ // toStandaloneModule(validator, opts)
11
+ // bundle(Validator, schemas, opts)
12
+ // bundleStandalone(Validator, schemas, opts)
13
+ // bundleCompact(Validator, schemas, opts)
14
+ // loadBundle(Validator, mods, schemas, opts)
15
+
16
+ const { compileToJSCodegenWithErrors } = require('./js-compiler');
17
+ const SAFE_REGEX_SOURCE = require('./safe-regex-source');
18
+
19
+ // Embedded verbatim in standalone modules so the output file has no runtime
20
+ // dependency on ata-validator. ASCII fast-path plus surrogate-aware slow path.
21
+ const _CP_LEN_SOURCE = `function _cpLen(s) {
22
+ const len = s.length;
23
+ for (let i = 0; i < len; i++) {
24
+ if (((s.charCodeAt(i) - 0xD800) >>> 0) < 0x400) {
25
+ let n = 0; for (const _ of s) n++; return n;
26
+ }
27
+ }
28
+ return len;
29
+ }`;
30
+
31
+ // The linear-time regex engine, inlined verbatim into standalone output so a
32
+ // compiled module that uses safe `pattern` matchers has no runtime dependency
33
+ // on ata-validator. The engine source is baked into `lib/safe-regex-source.js`
34
+ // at build time (see `scripts/regen-safe-regex-source.js`), so this path has
35
+ // no `fs`/`path`/`__dirname` reads — safe in browser bundles too. The embed
36
+ // strips the strict directive and CommonJS exports and adds the `__ataSafeRe`
37
+ // alias the emitted code calls. The engine has no eval/new Function, so the
38
+ // embed is CSP-safe.
39
+ let _safeRegexEmbed = null;
40
+ function getSafeRegexEmbed() {
41
+ if (_safeRegexEmbed === null) {
42
+ const body = SAFE_REGEX_SOURCE
43
+ .replace(/^'use strict'\s*\n/, '')
44
+ .replace(/\nmodule\.exports[^\n]*\n?/, '\n');
45
+ _safeRegexEmbed = body.trimEnd() + '\nconst __ataSafeRe = compileSafe;';
46
+ }
47
+ return _safeRegexEmbed;
48
+ }
49
+
50
+ // Returns the engine embed when any supplied compiled function references the
51
+ // safe matcher (jsFn._usesSafeRe), else an empty string so non-pattern modules
52
+ // pay zero bytes.
53
+ function safeRePrelude(...fns) {
54
+ return fns.some((f) => f && f._usesSafeRe) ? getSafeRegexEmbed() + '\n' : '';
55
+ }
56
+
57
+ // --- Standalone pre-compilation ---
58
+ // Generate a JS module string that can be written to a file.
59
+ // On next startup, load with Validator.fromStandalone() -- zero compile time.
60
+ function toStandalone(validator) {
61
+ validator._ensureCompiled();
62
+ const jsFn = validator._jsFn;
63
+ if (!jsFn || !jsFn._source) return null;
64
+ const src = jsFn._source;
65
+ const hybridSrc = jsFn._hybridSource || '';
66
+ const preambleSrc = jsFn._preambleSource || '';
67
+
68
+ // Also capture error function source for zero-compile standalone load
69
+ const jsErrFn = compileToJSCodegenWithErrors(
70
+ typeof validator._schemaObj === 'object' ? validator._schemaObj : {},
71
+ );
72
+ const errSrc = jsErrFn && jsErrFn._errSource ? jsErrFn._errSource : '';
73
+
74
+ return `// Auto-generated by ata-validator — do not edit
75
+ 'use strict';
76
+ ${_CP_LEN_SOURCE}
77
+ ${safeRePrelude(jsFn, jsErrFn)}${preambleSrc}
78
+ const boolFn = function(d) {
79
+ ${src}
80
+ };
81
+ const hybridFactory = function(R, E) {
82
+ return function(d) {
83
+ ${hybridSrc}
84
+ };
85
+ };
86
+ ${errSrc ? `const errFn = function(d, _all) {\n ${errSrc}\n};` : 'const errFn = null;'}
87
+ module.exports = { boolFn, hybridFactory, errFn };
88
+ `;
89
+ }
90
+
91
+ // --- Fully standalone module ---
92
+ // Generates a self-contained module that can be imported directly without
93
+ // pulling in ata-validator at runtime. Browser bundle gets only the
94
+ // generated validator (~2 KB typical) instead of the 165 KB compiler.
95
+ //
96
+ // import { validate, isValid } from './user-validator.mjs'
97
+ // if (isValid(data)) { ... }
98
+ //
99
+ // format: 'esm' | 'cjs'. Default 'esm'.
100
+ // abortEarly: if true, invalid result is a shared stub; smaller output.
101
+
102
+ // Custom format functions in standalone output.
103
+ //
104
+ // 'embed' (default) writes each function's source into the module through
105
+ // Function#toString. That only works for a plain function: one that closes
106
+ // over nothing and is not rewritten by a coverage or transpile step (istanbul
107
+ // injects `cov_` counters; a bundler may hoist helpers). The check below
108
+ // rejects those at build time with a named error instead of emitting a module
109
+ // that throws on first use.
110
+ //
111
+ // 'inject' writes no function source. The module exports `setFormats(map)`
112
+ // and each format is looked up from that registry when a value is checked,
113
+ // so the caller supplies the functions at load time.
114
+ function emitFormatDecls(closures, mode, declKW) {
115
+ if (!closures || closures.length === 0) return { decls: '', exportsSetFormats: false };
116
+ if (mode === 'inject') {
117
+ let out = `${declKW} __formats = Object.create(null);\n`;
118
+ out += `function setFormats(map) { for (const k in map) __formats[k] = map[k]; }\n`;
119
+ for (const { name, format } of closures) {
120
+ const key = JSON.stringify(format === null ? name.slice(4) : format);
121
+ out += `${declKW} ${name} = function (v) { const f = __formats[${key}]; if (typeof f !== 'function') throw new Error('ata: format ' + ${key} + ' is not registered; call setFormats({ [' + ${key} + ']: fn }) before validating'); return f(v); };\n`;
122
+ }
123
+ return { decls: out, exportsSetFormats: true };
124
+ }
125
+ let out = '';
126
+ for (const { name, fn, format } of closures) {
127
+ const src = fn.toString();
128
+ const label = format === null ? name : format;
129
+ if (/\bcov_[A-Za-z0-9_$]+\b/.test(src)) {
130
+ throw new Error(`ata: custom format "${label}" is instrumented for coverage and cannot be embedded; use { formatMode: 'inject' } and register it with setFormats() at load time`);
131
+ }
132
+ try {
133
+ // eslint-disable-next-line no-new-func
134
+ new Function('return (' + src + ')');
135
+ } catch {
136
+ throw new Error(`ata: custom format "${label}" has no standalone source (a bound function, a class method, or a native); use { formatMode: 'inject' } and register it with setFormats() at load time`);
137
+ }
138
+ out += `${declKW} ${name} = ${src};\n`;
139
+ }
140
+ return { decls: out, exportsSetFormats: false };
141
+ }
142
+
143
+ function toStandaloneModule(validator, opts) {
144
+ validator._ensureCompiled();
145
+ const jsFn = validator._jsFn;
146
+ if (!jsFn || !jsFn._source) return null;
147
+ const format = (opts && opts.format) || 'esm';
148
+ const abortEarly = !!(opts && opts.abortEarly);
149
+ const source = !!(opts && opts.source);
150
+ const sourceMap = opts && opts.sourceMap ? opts.sourceMap : null;
151
+ const schemaFile = opts && opts.schemaFile ? opts.schemaFile : null;
152
+ const src = jsFn._source;
153
+
154
+ let errCore = '';
155
+ let jsErrFn = null;
156
+ if (!abortEarly) {
157
+ jsErrFn = compileToJSCodegenWithErrors(
158
+ typeof validator._schemaObj === 'object' ? validator._schemaObj : {},
159
+ null,
160
+ validator._userFormats,
161
+ (source && sourceMap && schemaFile) ? { sourceMap, schemaFile } : null,
162
+ );
163
+ const errSrc = jsErrFn && jsErrFn._errSource ? jsErrFn._errSource : '';
164
+ if (errSrc) {
165
+ errCore = `const errFn = function(d, _all) {\n ${errSrc}\n};\n`;
166
+ }
167
+ }
168
+
169
+ // Schema-source frames are baked as literals inside each emitted error so
170
+ // consumers don't need a runtime lookup. We still expose the schema file
171
+ // as a sentinel constant when --source is on — handy for introspection
172
+ // and visible in source graphs. With --no-source, the constant is omitted
173
+ // entirely so size budgets and grep-based "is this source-mapped?" checks
174
+ // both work.
175
+ const schemaSourceConst = (source && schemaFile)
176
+ ? `const __ATA_SCHEMA_SOURCE__ = ${JSON.stringify({ file: schemaFile })};\n`
177
+ : '';
178
+
179
+ // Serialize closure vars referenced in _fn body: regex, sub-validators, sets.
180
+ let closureDecls = '';
181
+ if (jsFn._closures && jsFn._closures.length > 0) {
182
+ const lines = [];
183
+ for (const { name, val } of jsFn._closures) {
184
+ if (Array.isArray(val)) {
185
+ lines.push(`const ${name} = ${JSON.stringify(val)};`);
186
+ continue;
187
+ }
188
+ if (val && val.__ataSafe) {
189
+ lines.push(`const ${name} = __ataSafeRe(${JSON.stringify(val.source)});`);
190
+ } else if (val instanceof RegExp) {
191
+ const flags = val.flags;
192
+ lines.push(`const ${name} = new RegExp(${JSON.stringify(val.source)}${flags ? ', ' + JSON.stringify(flags) : ''});`);
193
+ } else if (val instanceof Set) {
194
+ lines.push(`const ${name} = new Set(${JSON.stringify([...val])});`);
195
+ } else if (typeof val === 'function') {
196
+ // new Function('_ppv', body) — extract body from toString()
197
+ const str = val.toString();
198
+ // Matches: "function anonymous(_ppv\n) {\nbody\n}" or "function(_ppv){body}"
199
+ const m = str.match(/^function[^(]*\([^)]*\)\s*\{([\s\S]*)\}$/)
200
+ const body = m ? m[1].trim() : str;
201
+ lines.push(`const ${name} = function(_ppv) { ${body} };`);
202
+ }
203
+ }
204
+ if (lines.length) closureDecls = lines.join('\n') + '\n';
205
+ }
206
+
207
+ // Hoisted oneOf/anyOf branch checks live in the boolean fn's preamble (the
208
+ // runtime emits them before the function). The standalone module must declare
209
+ // them at module scope too, or _fn references undefined names (e.g. _af1_b0).
210
+ const preambleDecls = jsFn._preambleSource ? jsFn._preambleSource + '\n' : '';
211
+
212
+ // User-supplied format functions are referenced as _uf_<name> by both the
213
+ // boolean (_fn) and error (errFn) bodies. Embed them via Function#toString
214
+ // so the standalone module stays self-contained.
215
+ const fmt = emitFormatDecls(jsFn._formatClosures, opts && opts.formatMode, 'const');
216
+ const formatDecls = fmt.decls;
217
+
218
+ const validBody = errCore
219
+ ? 'return _fn(data) ? VALID : { valid: false, errors: errFn(data, true).errors }'
220
+ : 'return _fn(data) ? VALID : ABORT';
221
+
222
+ const names = fmt.exportsSetFormats ? 'validate, isValid, setFormats' : 'validate, isValid';
223
+ const exports = format === 'esm'
224
+ ? `export { ${names} };\nexport default { ${names} };\n`
225
+ : `module.exports = { ${names} };\nmodule.exports.default = module.exports;\n`;
226
+
227
+ return `// Auto-generated by ata-validator — do not edit.
228
+ // Schema is embedded; runtime has zero dependency on ata-validator.
229
+ 'use strict';
230
+ ${_CP_LEN_SOURCE}
231
+ ${safeRePrelude(jsFn, jsErrFn)}${schemaSourceConst}const VALID = Object.freeze({ valid: true, errors: Object.freeze([]) });
232
+ const ABORT = Object.freeze({
233
+ valid: false,
234
+ errors: Object.freeze([Object.freeze({
235
+ code: 'ATA9000',
236
+ message: 'validation failed',
237
+ keyword: '__abort_early__',
238
+ path: '',
239
+ })]),
240
+ });
241
+ ${closureDecls}${preambleDecls}${formatDecls}const _fn = function(d) {
242
+ ${src}
243
+ };
244
+ ${errCore}function isValid(data) { return _fn(data); }
245
+ function validate(data) { ${validBody}; }
246
+ ${exports}`;
247
+ }
248
+
249
+ // Bundle multiple validators into a single JS file for fast startup.
250
+ // Usage:
251
+ // const bundle = Validator.bundle([schema1, schema2, ...]);
252
+ // fs.writeFileSync('validators.js', bundle);
253
+ // // On startup:
254
+ // const validators = Validator.loadBundle(require('./validators.js'), [schema1, schema2, ...]);
255
+ function bundle(Validator, schemas, opts) {
256
+ const parts = schemas.map((schema) => {
257
+ const v = new Validator(schema, opts);
258
+ const standalone = toStandalone(v);
259
+ if (!standalone) return 'null';
260
+ return (
261
+ '(function(){' +
262
+ standalone
263
+ .replace("'use strict';", '')
264
+ .replace('module.exports = ', 'return ') +
265
+ '})()'
266
+ );
267
+ });
268
+ return "'use strict';\nmodule.exports = [\n" + parts.join(',\n') + '\n];\n';
269
+ }
270
+
271
+ // Zero-dependency self-contained bundle — no require('ata-validator') needed at runtime.
272
+ // opts.format: 'cjs' (default) or 'esm'.
273
+ // opts.formats: { name: fn } — embedded in the output via Function#toString.
274
+ function bundleStandalone(Validator, schemas, opts) {
275
+ // Cross-schema $ref resolution: only meaningful when at least one schema has
276
+ // an $id. Skip the schemas-as-map plumbing when none of them do.
277
+ const haveIds = schemas.some((s) => s && typeof s === 'object' && s.$id);
278
+ const bundleOpts = haveIds ? { ...(opts || {}), schemas } : (opts || {});
279
+ const format = (opts && opts.format) || 'cjs';
280
+ const R = 'Object.freeze({valid:true,errors:Object.freeze([])})';
281
+ let bundleUsesSafeRe = false;
282
+ let bundleInjects = false;
283
+ const fns = schemas.map((schema) => {
284
+ const v = new Validator(schema, bundleOpts);
285
+ v._ensureCompiled();
286
+ const jsFn = v._jsFn;
287
+ if (!jsFn || !jsFn._hybridSource) return 'null';
288
+ // The schema the validator compiled, not the one the caller passed. They
289
+ // differ whenever anything prepared the schema: draft-07 normalization,
290
+ // `format` removed under assertFormat: false, keywords outside the
291
+ // dialect's vocabularies. Reading the original here would leave the
292
+ // boolean path and the error path disagreeing about the same document.
293
+ const jsErrFn = compileToJSCodegenWithErrors(
294
+ v._schemaObj,
295
+ v._schemaMap,
296
+ v._userFormats,
297
+ );
298
+ if (jsFn._usesSafeRe || (jsErrFn && jsErrFn._usesSafeRe)) bundleUsesSafeRe = true;
299
+ const errBody =
300
+ jsErrFn && jsErrFn._errSource
301
+ ? jsErrFn._errSource
302
+ : "return{valid:false,errors:[{code:'error',path:'',message:'validation failed'}]}";
303
+ // Custom format closures: embedded, or bound to the bundle-level
304
+ // registry when opts.formats is 'inject'.
305
+ let preamble = '';
306
+ if (jsFn._formatClosures) {
307
+ const f = emitFormatDecls(jsFn._formatClosures, opts && opts.formatMode, 'var');
308
+ preamble = f.decls.replace(/^var __formats = [^\n]*\n|^function setFormats[^\n]*\n/gm, '');
309
+ if (f.exportsSetFormats) bundleInjects = true;
310
+ }
311
+ // Include hoisted anyOf/oneOf branch helpers (e.g. `_af1_b0`) so the
312
+ // bundle output is self-contained. `toStandalone` emits this same source
313
+ // for single-schema standalone output.
314
+ if (jsFn._preambleSource) {
315
+ preamble = preamble ? `${preamble}\n${jsFn._preambleSource}` : jsFn._preambleSource;
316
+ }
317
+ if (opts && opts.verbose) {
318
+ // Embed the schema and a small resolver so errors carry parentSchema.
319
+ const schemaLit = JSON.stringify(v._schemaObj);
320
+ return `(function(R){${preamble}var _S=${schemaLit};function _PS(p){if(!p||p[0]!=='#')return undefined;var s=p.slice(1);if(!s)return _S;var ps=s.split('/').filter(Boolean).map(function(x){return x.replace(/~1/g,'/').replace(/~0/g,'~')});var t=_S;for(var i=0;i<ps.length-1;i++){if(t==null||typeof t!=='object')return undefined;t=t[ps[i]]}return t}var E=function(d){var _all=true;${errBody}};var _v=function(d){${jsFn._hybridSource}};return function(d){var r=_v(d);if(r&&r.valid===false&&r.errors){var es=[];for(var i=0;i<r.errors.length;i++){var e=r.errors[i];es.push(Object.assign({},e,{parentSchema:_PS(e.schemaPath)}))}return{valid:false,errors:es}}return r}})(R)`;
321
+ }
322
+ return `(function(R){${preamble}var E=function(d){var _all=true;${errBody}};return function(d){${jsFn._hybridSource}}})(R)`;
323
+ });
324
+ const arr = `[${fns.join(',')}]`;
325
+ const safeEmbed = bundleUsesSafeRe ? getSafeRegexEmbed() + '\n' : '';
326
+ const registry = bundleInjects
327
+ ? `var __formats=Object.create(null);\nfunction setFormats(map){for(var k in map)__formats[k]=map[k]}\n`
328
+ : '';
329
+ if (format === 'esm') {
330
+ const extra = bundleInjects ? 'export { validators, setFormats };' : 'export { validators };';
331
+ return `// Auto-generated by ata-validator — do not edit\n${safeEmbed}${registry}const R=${R};\nconst validators=${arr};\nexport default validators;\n${extra}\n`;
332
+ }
333
+ const attach = bundleInjects ? 'module.exports.setFormats=setFormats;\n' : '';
334
+ return `'use strict';\n${safeEmbed}${registry}var R=${R};\nmodule.exports=[${fns.join(',')}];\n${attach}`;
335
+ }
336
+
337
+ // Compact bundle: deduplicated code. Shared template functions + per-schema params.
338
+ // Much smaller file → faster V8 parse → faster startup.
339
+ // opts.format: 'cjs' (default) or 'esm'.
340
+ function bundleCompact(Validator, schemas, opts) {
341
+ const haveIds = schemas.some((s) => s && typeof s === 'object' && s.$id);
342
+ const bundleOpts = haveIds ? { ...(opts || {}), schemas } : (opts || {});
343
+ const format = (opts && opts.format) || 'cjs';
344
+ let bundleUsesSafeRe = false;
345
+ // Analyze schemas and group by structure
346
+ const entries = schemas.map((schema) => {
347
+ const v = new Validator(schema, bundleOpts);
348
+ v._ensureCompiled();
349
+ const jsFn = v._jsFn;
350
+ if (!jsFn || !jsFn._hybridSource) return null;
351
+ // The schema the validator compiled, not the one the caller passed. They
352
+ // differ whenever anything prepared the schema: draft-07 normalization,
353
+ // `format` removed under assertFormat: false, keywords outside the
354
+ // dialect's vocabularies. Reading the original here would leave the
355
+ // boolean path and the error path disagreeing about the same document.
356
+ const jsErrFn = compileToJSCodegenWithErrors(
357
+ v._schemaObj,
358
+ v._schemaMap,
359
+ v._userFormats,
360
+ );
361
+ if (jsFn._usesSafeRe || (jsErrFn && jsErrFn._usesSafeRe)) bundleUsesSafeRe = true;
362
+ // Hoisted anyOf/oneOf branch helpers (e.g. `_af1_b0`) must travel with the
363
+ // hybrid body or it references undefined names. Prepending keeps dedup honest:
364
+ // schemas with different branch sets no longer collide on body alone.
365
+ const hybrid = jsFn._preambleSource
366
+ ? `${jsFn._preambleSource}\n${jsFn._hybridSource}`
367
+ : jsFn._hybridSource;
368
+ return {
369
+ hybrid,
370
+ err: jsErrFn && jsErrFn._errSource ? jsErrFn._errSource : null,
371
+ fmt: jsFn._formatClosures || null,
372
+ };
373
+ });
374
+
375
+ // Deduplicate function bodies — many schemas produce identical or near-identical code
376
+ const bodyMap = new Map(); // body → index
377
+ const bodies = [];
378
+ const errMap = new Map();
379
+ const errBodies = [];
380
+
381
+ const indices = entries.map((e) => {
382
+ if (!e) return [-1, -1];
383
+ let hi = bodyMap.get(e.hybrid);
384
+ if (hi === undefined) {
385
+ hi = bodies.length;
386
+ bodies.push(e.hybrid);
387
+ bodyMap.set(e.hybrid, hi);
388
+ }
389
+ let ei = -1;
390
+ if (e.err) {
391
+ ei = errMap.get(e.err);
392
+ if (ei === undefined) {
393
+ ei = errBodies.length;
394
+ errBodies.push(e.err);
395
+ errMap.set(e.err, ei);
396
+ }
397
+ }
398
+ return [hi, ei];
399
+ });
400
+
401
+ // Generate compact bundle
402
+ const isEsm = format === 'esm';
403
+ let out = isEsm
404
+ ? '// Auto-generated by ata-validator — do not edit\n'
405
+ : "'use strict';\n";
406
+ if (bundleUsesSafeRe) out += getSafeRegexEmbed() + '\n';
407
+ const declKW = isEsm ? 'const' : 'var';
408
+ out += `${declKW} R=Object.freeze({valid:true,errors:Object.freeze([])});\n`;
409
+
410
+ // User format functions are referenced as _uf_<name> by the hybrid and error
411
+ // bodies. Collect them across all schemas (deduped by name) and embed via
412
+ // Function#toString so the bundle stays self-contained.
413
+ const fmtSeen = new Set();
414
+ const fmtAll = [];
415
+ for (const e of entries) {
416
+ if (!e || !e.fmt) continue;
417
+ for (const entry of e.fmt) {
418
+ if (fmtSeen.has(entry.name)) continue;
419
+ fmtSeen.add(entry.name);
420
+ fmtAll.push(entry);
421
+ }
422
+ }
423
+ const fmtOut = emitFormatDecls(fmtAll, opts && opts.formatMode, declKW);
424
+ out += fmtOut.decls;
425
+
426
+ // Shared hybrid factories
427
+ out += `${declKW} H=[\n`;
428
+ out += bodies
429
+ .map((b) => `function(R,E){return function(d){${b}}}`)
430
+ .join(',\n');
431
+ out += '\n];\n';
432
+
433
+ // Shared error functions
434
+ out += `${declKW} EF=[\n`;
435
+ out += errBodies.map((b) => `function(d){var _all=true;${b}}`).join(',\n');
436
+ out += '\n];\n';
437
+
438
+ // Build validators from shared templates
439
+ const arrBody = indices
440
+ .map(([hi, ei]) => {
441
+ if (hi < 0) return 'null';
442
+ if (ei >= 0) return `H[${hi}](R,EF[${ei}])`;
443
+ return `H[${hi}](R,function(){return{valid:false,errors:[]}})`;
444
+ })
445
+ .join(',');
446
+ if (isEsm) {
447
+ out += `const validators=[${arrBody}];\nexport default validators;\nexport { validators${fmtOut.exportsSetFormats ? ', setFormats' : ''} };\n`;
448
+ } else {
449
+ out += `module.exports=[${arrBody}];\n`;
450
+ if (fmtOut.exportsSetFormats) out += 'module.exports.setFormats=setFormats;\n';
451
+ }
452
+
453
+ return out;
454
+ }
455
+
456
+ function loadBundle(Validator, mods, schemas, opts) {
457
+ return schemas.map((schema, i) => {
458
+ if (mods[i]) return Validator.fromStandalone(mods[i], schema, opts);
459
+ return new Validator(schema, opts);
460
+ });
461
+ }
462
+
463
+ module.exports = {
464
+ toStandalone,
465
+ toStandaloneModule,
466
+ bundle,
467
+ bundleStandalone,
468
+ bundleCompact,
469
+ loadBundle,
470
+ };
@@ -0,0 +1,33 @@
1
+ 'use strict';
2
+
3
+ // Browser twin of lib/aot.js, wired by the package.json `browser` field. The
4
+ // emitters write module source for a bundler or the filesystem, which is build
5
+ // tooling, not something a page does; leaving them out keeps their code and
6
+ // the embedded safe-regex engine source out of the browser bundle. Loading a
7
+ // bundle somebody already built is a runtime act, so that one works here too.
8
+
9
+ function unavailable(name) {
10
+ return function () {
11
+ throw new Error(
12
+ name + '() is not in the default browser bundle. ' +
13
+ "Import it from 'ata-validator/aot', which works in the browser too; " +
14
+ 'pages that never emit code stay free of the emitters this way.'
15
+ );
16
+ };
17
+ }
18
+
19
+ function loadBundle(Validator, mods, schemas, opts) {
20
+ return schemas.map((schema, i) => {
21
+ if (mods[i]) return Validator.fromStandalone(mods[i], schema, opts);
22
+ return new Validator(schema, opts);
23
+ });
24
+ }
25
+
26
+ module.exports = {
27
+ toStandalone: unavailable('toStandalone'),
28
+ toStandaloneModule: unavailable('toStandaloneModule'),
29
+ bundle: unavailable('bundle'),
30
+ bundleStandalone: unavailable('bundleStandalone'),
31
+ bundleCompact: unavailable('bundleCompact'),
32
+ loadBundle,
33
+ };
package/lib/aot.js CHANGED
@@ -1,470 +1,8 @@
1
1
  'use strict';
2
2
 
3
- // All ahead-of-time (AOT) code generation lives here so the default and browser
4
- // entries can stay free of `fs`, `path`, and `__dirname`.
5
- // Browser bundles get `lib/aot.browser.js` instead (see package.json `browser`
6
- // field), which throws if anyone tries to call an AOT function from the browser.
7
- //
8
- // Public surface (called by index.js and ata-validator/build):
9
- // toStandalone(validator)
10
- // toStandaloneModule(validator, opts)
11
- // bundle(Validator, schemas, opts)
12
- // bundleStandalone(Validator, schemas, opts)
13
- // bundleCompact(Validator, schemas, opts)
14
- // loadBundle(Validator, mods, schemas, opts)
15
-
16
- const { compileToJSCodegenWithErrors } = require('./js-compiler');
17
- const SAFE_REGEX_SOURCE = require('./safe-regex-source');
18
-
19
- // Embedded verbatim in standalone modules so the output file has no runtime
20
- // dependency on ata-validator. ASCII fast-path plus surrogate-aware slow path.
21
- const _CP_LEN_SOURCE = `function _cpLen(s) {
22
- const len = s.length;
23
- for (let i = 0; i < len; i++) {
24
- if (((s.charCodeAt(i) - 0xD800) >>> 0) < 0x400) {
25
- let n = 0; for (const _ of s) n++; return n;
26
- }
27
- }
28
- return len;
29
- }`;
30
-
31
- // The linear-time regex engine, inlined verbatim into standalone output so a
32
- // compiled module that uses safe `pattern` matchers has no runtime dependency
33
- // on ata-validator. The engine source is baked into `lib/safe-regex-source.js`
34
- // at build time (see `scripts/regen-safe-regex-source.js`), so this path has
35
- // no `fs`/`path`/`__dirname` reads — safe in browser bundles too. The embed
36
- // strips the strict directive and CommonJS exports and adds the `__ataSafeRe`
37
- // alias the emitted code calls. The engine has no eval/new Function, so the
38
- // embed is CSP-safe.
39
- let _safeRegexEmbed = null;
40
- function getSafeRegexEmbed() {
41
- if (_safeRegexEmbed === null) {
42
- const body = SAFE_REGEX_SOURCE
43
- .replace(/^'use strict'\s*\n/, '')
44
- .replace(/\nmodule\.exports[^\n]*\n?/, '\n');
45
- _safeRegexEmbed = body.trimEnd() + '\nconst __ataSafeRe = compileSafe;';
46
- }
47
- return _safeRegexEmbed;
48
- }
49
-
50
- // Returns the engine embed when any supplied compiled function references the
51
- // safe matcher (jsFn._usesSafeRe), else an empty string so non-pattern modules
52
- // pay zero bytes.
53
- function safeRePrelude(...fns) {
54
- return fns.some((f) => f && f._usesSafeRe) ? getSafeRegexEmbed() + '\n' : '';
55
- }
56
-
57
- // --- Standalone pre-compilation ---
58
- // Generate a JS module string that can be written to a file.
59
- // On next startup, load with Validator.fromStandalone() -- zero compile time.
60
- function toStandalone(validator) {
61
- validator._ensureCompiled();
62
- const jsFn = validator._jsFn;
63
- if (!jsFn || !jsFn._source) return null;
64
- const src = jsFn._source;
65
- const hybridSrc = jsFn._hybridSource || '';
66
- const preambleSrc = jsFn._preambleSource || '';
67
-
68
- // Also capture error function source for zero-compile standalone load
69
- const jsErrFn = compileToJSCodegenWithErrors(
70
- typeof validator._schemaObj === 'object' ? validator._schemaObj : {},
71
- );
72
- const errSrc = jsErrFn && jsErrFn._errSource ? jsErrFn._errSource : '';
73
-
74
- return `// Auto-generated by ata-validator — do not edit
75
- 'use strict';
76
- ${_CP_LEN_SOURCE}
77
- ${safeRePrelude(jsFn, jsErrFn)}${preambleSrc}
78
- const boolFn = function(d) {
79
- ${src}
80
- };
81
- const hybridFactory = function(R, E) {
82
- return function(d) {
83
- ${hybridSrc}
84
- };
85
- };
86
- ${errSrc ? `const errFn = function(d, _all) {\n ${errSrc}\n};` : 'const errFn = null;'}
87
- module.exports = { boolFn, hybridFactory, errFn };
88
- `;
89
- }
90
-
91
- // --- Fully standalone module ---
92
- // Generates a self-contained module that can be imported directly without
93
- // pulling in ata-validator at runtime. Browser bundle gets only the
94
- // generated validator (~2 KB typical) instead of the 165 KB compiler.
95
- //
96
- // import { validate, isValid } from './user-validator.mjs'
97
- // if (isValid(data)) { ... }
98
- //
99
- // format: 'esm' | 'cjs'. Default 'esm'.
100
- // abortEarly: if true, invalid result is a shared stub; smaller output.
101
-
102
- // Custom format functions in standalone output.
103
- //
104
- // 'embed' (default) writes each function's source into the module through
105
- // Function#toString. That only works for a plain function: one that closes
106
- // over nothing and is not rewritten by a coverage or transpile step (istanbul
107
- // injects `cov_` counters; a bundler may hoist helpers). The check below
108
- // rejects those at build time with a named error instead of emitting a module
109
- // that throws on first use.
110
- //
111
- // 'inject' writes no function source. The module exports `setFormats(map)`
112
- // and each format is looked up from that registry when a value is checked,
113
- // so the caller supplies the functions at load time.
114
- function emitFormatDecls(closures, mode, declKW) {
115
- if (!closures || closures.length === 0) return { decls: '', exportsSetFormats: false };
116
- if (mode === 'inject') {
117
- let out = `${declKW} __formats = Object.create(null);\n`;
118
- out += `function setFormats(map) { for (const k in map) __formats[k] = map[k]; }\n`;
119
- for (const { name, format } of closures) {
120
- const key = JSON.stringify(format === null ? name.slice(4) : format);
121
- out += `${declKW} ${name} = function (v) { const f = __formats[${key}]; if (typeof f !== 'function') throw new Error('ata: format ' + ${key} + ' is not registered; call setFormats({ [' + ${key} + ']: fn }) before validating'); return f(v); };\n`;
122
- }
123
- return { decls: out, exportsSetFormats: true };
124
- }
125
- let out = '';
126
- for (const { name, fn, format } of closures) {
127
- const src = fn.toString();
128
- const label = format === null ? name : format;
129
- if (/\bcov_[A-Za-z0-9_$]+\b/.test(src)) {
130
- throw new Error(`ata: custom format "${label}" is instrumented for coverage and cannot be embedded; use { formatMode: 'inject' } and register it with setFormats() at load time`);
131
- }
132
- try {
133
- // eslint-disable-next-line no-new-func
134
- new Function('return (' + src + ')');
135
- } catch {
136
- throw new Error(`ata: custom format "${label}" has no standalone source (a bound function, a class method, or a native); use { formatMode: 'inject' } and register it with setFormats() at load time`);
137
- }
138
- out += `${declKW} ${name} = ${src};\n`;
139
- }
140
- return { decls: out, exportsSetFormats: false };
141
- }
142
-
143
- function toStandaloneModule(validator, opts) {
144
- validator._ensureCompiled();
145
- const jsFn = validator._jsFn;
146
- if (!jsFn || !jsFn._source) return null;
147
- const format = (opts && opts.format) || 'esm';
148
- const abortEarly = !!(opts && opts.abortEarly);
149
- const source = !!(opts && opts.source);
150
- const sourceMap = opts && opts.sourceMap ? opts.sourceMap : null;
151
- const schemaFile = opts && opts.schemaFile ? opts.schemaFile : null;
152
- const src = jsFn._source;
153
-
154
- let errCore = '';
155
- let jsErrFn = null;
156
- if (!abortEarly) {
157
- jsErrFn = compileToJSCodegenWithErrors(
158
- typeof validator._schemaObj === 'object' ? validator._schemaObj : {},
159
- null,
160
- validator._userFormats,
161
- (source && sourceMap && schemaFile) ? { sourceMap, schemaFile } : null,
162
- );
163
- const errSrc = jsErrFn && jsErrFn._errSource ? jsErrFn._errSource : '';
164
- if (errSrc) {
165
- errCore = `const errFn = function(d, _all) {\n ${errSrc}\n};\n`;
166
- }
167
- }
168
-
169
- // Schema-source frames are baked as literals inside each emitted error so
170
- // consumers don't need a runtime lookup. We still expose the schema file
171
- // as a sentinel constant when --source is on — handy for introspection
172
- // and visible in source graphs. With --no-source, the constant is omitted
173
- // entirely so size budgets and grep-based "is this source-mapped?" checks
174
- // both work.
175
- const schemaSourceConst = (source && schemaFile)
176
- ? `const __ATA_SCHEMA_SOURCE__ = ${JSON.stringify({ file: schemaFile })};\n`
177
- : '';
178
-
179
- // Serialize closure vars referenced in _fn body: regex, sub-validators, sets.
180
- let closureDecls = '';
181
- if (jsFn._closures && jsFn._closures.length > 0) {
182
- const lines = [];
183
- for (const { name, val } of jsFn._closures) {
184
- if (Array.isArray(val)) {
185
- lines.push(`const ${name} = ${JSON.stringify(val)};`);
186
- continue;
187
- }
188
- if (val && val.__ataSafe) {
189
- lines.push(`const ${name} = __ataSafeRe(${JSON.stringify(val.source)});`);
190
- } else if (val instanceof RegExp) {
191
- const flags = val.flags;
192
- lines.push(`const ${name} = new RegExp(${JSON.stringify(val.source)}${flags ? ', ' + JSON.stringify(flags) : ''});`);
193
- } else if (val instanceof Set) {
194
- lines.push(`const ${name} = new Set(${JSON.stringify([...val])});`);
195
- } else if (typeof val === 'function') {
196
- // new Function('_ppv', body) — extract body from toString()
197
- const str = val.toString();
198
- // Matches: "function anonymous(_ppv\n) {\nbody\n}" or "function(_ppv){body}"
199
- const m = str.match(/^function[^(]*\([^)]*\)\s*\{([\s\S]*)\}$/)
200
- const body = m ? m[1].trim() : str;
201
- lines.push(`const ${name} = function(_ppv) { ${body} };`);
202
- }
203
- }
204
- if (lines.length) closureDecls = lines.join('\n') + '\n';
205
- }
206
-
207
- // Hoisted oneOf/anyOf branch checks live in the boolean fn's preamble (the
208
- // runtime emits them before the function). The standalone module must declare
209
- // them at module scope too, or _fn references undefined names (e.g. _af1_b0).
210
- const preambleDecls = jsFn._preambleSource ? jsFn._preambleSource + '\n' : '';
211
-
212
- // User-supplied format functions are referenced as _uf_<name> by both the
213
- // boolean (_fn) and error (errFn) bodies. Embed them via Function#toString
214
- // so the standalone module stays self-contained.
215
- const fmt = emitFormatDecls(jsFn._formatClosures, opts && opts.formatMode, 'const');
216
- const formatDecls = fmt.decls;
217
-
218
- const validBody = errCore
219
- ? 'return _fn(data) ? VALID : { valid: false, errors: errFn(data, true).errors }'
220
- : 'return _fn(data) ? VALID : ABORT';
221
-
222
- const names = fmt.exportsSetFormats ? 'validate, isValid, setFormats' : 'validate, isValid';
223
- const exports = format === 'esm'
224
- ? `export { ${names} };\nexport default { ${names} };\n`
225
- : `module.exports = { ${names} };\nmodule.exports.default = module.exports;\n`;
226
-
227
- return `// Auto-generated by ata-validator — do not edit.
228
- // Schema is embedded; runtime has zero dependency on ata-validator.
229
- 'use strict';
230
- ${_CP_LEN_SOURCE}
231
- ${safeRePrelude(jsFn, jsErrFn)}${schemaSourceConst}const VALID = Object.freeze({ valid: true, errors: Object.freeze([]) });
232
- const ABORT = Object.freeze({
233
- valid: false,
234
- errors: Object.freeze([Object.freeze({
235
- code: 'ATA9000',
236
- message: 'validation failed',
237
- keyword: '__abort_early__',
238
- path: '',
239
- })]),
240
- });
241
- ${closureDecls}${preambleDecls}${formatDecls}const _fn = function(d) {
242
- ${src}
243
- };
244
- ${errCore}function isValid(data) { return _fn(data); }
245
- function validate(data) { ${validBody}; }
246
- ${exports}`;
247
- }
248
-
249
- // Bundle multiple validators into a single JS file for fast startup.
250
- // Usage:
251
- // const bundle = Validator.bundle([schema1, schema2, ...]);
252
- // fs.writeFileSync('validators.js', bundle);
253
- // // On startup:
254
- // const validators = Validator.loadBundle(require('./validators.js'), [schema1, schema2, ...]);
255
- function bundle(Validator, schemas, opts) {
256
- const parts = schemas.map((schema) => {
257
- const v = new Validator(schema, opts);
258
- const standalone = toStandalone(v);
259
- if (!standalone) return 'null';
260
- return (
261
- '(function(){' +
262
- standalone
263
- .replace("'use strict';", '')
264
- .replace('module.exports = ', 'return ') +
265
- '})()'
266
- );
267
- });
268
- return "'use strict';\nmodule.exports = [\n" + parts.join(',\n') + '\n];\n';
269
- }
270
-
271
- // Zero-dependency self-contained bundle — no require('ata-validator') needed at runtime.
272
- // opts.format: 'cjs' (default) or 'esm'.
273
- // opts.formats: { name: fn } — embedded in the output via Function#toString.
274
- function bundleStandalone(Validator, schemas, opts) {
275
- // Cross-schema $ref resolution: only meaningful when at least one schema has
276
- // an $id. Skip the schemas-as-map plumbing when none of them do.
277
- const haveIds = schemas.some((s) => s && typeof s === 'object' && s.$id);
278
- const bundleOpts = haveIds ? { ...(opts || {}), schemas } : (opts || {});
279
- const format = (opts && opts.format) || 'cjs';
280
- const R = 'Object.freeze({valid:true,errors:Object.freeze([])})';
281
- let bundleUsesSafeRe = false;
282
- let bundleInjects = false;
283
- const fns = schemas.map((schema) => {
284
- const v = new Validator(schema, bundleOpts);
285
- v._ensureCompiled();
286
- const jsFn = v._jsFn;
287
- if (!jsFn || !jsFn._hybridSource) return 'null';
288
- // The schema the validator compiled, not the one the caller passed. They
289
- // differ whenever anything prepared the schema: draft-07 normalization,
290
- // `format` removed under assertFormat: false, keywords outside the
291
- // dialect's vocabularies. Reading the original here would leave the
292
- // boolean path and the error path disagreeing about the same document.
293
- const jsErrFn = compileToJSCodegenWithErrors(
294
- v._schemaObj,
295
- v._schemaMap,
296
- v._userFormats,
297
- );
298
- if (jsFn._usesSafeRe || (jsErrFn && jsErrFn._usesSafeRe)) bundleUsesSafeRe = true;
299
- const errBody =
300
- jsErrFn && jsErrFn._errSource
301
- ? jsErrFn._errSource
302
- : "return{valid:false,errors:[{code:'error',path:'',message:'validation failed'}]}";
303
- // Custom format closures: embedded, or bound to the bundle-level
304
- // registry when opts.formats is 'inject'.
305
- let preamble = '';
306
- if (jsFn._formatClosures) {
307
- const f = emitFormatDecls(jsFn._formatClosures, opts && opts.formatMode, 'var');
308
- preamble = f.decls.replace(/^var __formats = [^\n]*\n|^function setFormats[^\n]*\n/gm, '');
309
- if (f.exportsSetFormats) bundleInjects = true;
310
- }
311
- // Include hoisted anyOf/oneOf branch helpers (e.g. `_af1_b0`) so the
312
- // bundle output is self-contained. `toStandalone` emits this same source
313
- // for single-schema standalone output.
314
- if (jsFn._preambleSource) {
315
- preamble = preamble ? `${preamble}\n${jsFn._preambleSource}` : jsFn._preambleSource;
316
- }
317
- if (opts && opts.verbose) {
318
- // Embed the schema and a small resolver so errors carry parentSchema.
319
- const schemaLit = JSON.stringify(v._schemaObj);
320
- return `(function(R){${preamble}var _S=${schemaLit};function _PS(p){if(!p||p[0]!=='#')return undefined;var s=p.slice(1);if(!s)return _S;var ps=s.split('/').filter(Boolean).map(function(x){return x.replace(/~1/g,'/').replace(/~0/g,'~')});var t=_S;for(var i=0;i<ps.length-1;i++){if(t==null||typeof t!=='object')return undefined;t=t[ps[i]]}return t}var E=function(d){var _all=true;${errBody}};var _v=function(d){${jsFn._hybridSource}};return function(d){var r=_v(d);if(r&&r.valid===false&&r.errors){var es=[];for(var i=0;i<r.errors.length;i++){var e=r.errors[i];es.push(Object.assign({},e,{parentSchema:_PS(e.schemaPath)}))}return{valid:false,errors:es}}return r}})(R)`;
321
- }
322
- return `(function(R){${preamble}var E=function(d){var _all=true;${errBody}};return function(d){${jsFn._hybridSource}}})(R)`;
323
- });
324
- const arr = `[${fns.join(',')}]`;
325
- const safeEmbed = bundleUsesSafeRe ? getSafeRegexEmbed() + '\n' : '';
326
- const registry = bundleInjects
327
- ? `var __formats=Object.create(null);\nfunction setFormats(map){for(var k in map)__formats[k]=map[k]}\n`
328
- : '';
329
- if (format === 'esm') {
330
- const extra = bundleInjects ? 'export { validators, setFormats };' : 'export { validators };';
331
- return `// Auto-generated by ata-validator — do not edit\n${safeEmbed}${registry}const R=${R};\nconst validators=${arr};\nexport default validators;\n${extra}\n`;
332
- }
333
- const attach = bundleInjects ? 'module.exports.setFormats=setFormats;\n' : '';
334
- return `'use strict';\n${safeEmbed}${registry}var R=${R};\nmodule.exports=[${fns.join(',')}];\n${attach}`;
335
- }
336
-
337
- // Compact bundle: deduplicated code. Shared template functions + per-schema params.
338
- // Much smaller file → faster V8 parse → faster startup.
339
- // opts.format: 'cjs' (default) or 'esm'.
340
- function bundleCompact(Validator, schemas, opts) {
341
- const haveIds = schemas.some((s) => s && typeof s === 'object' && s.$id);
342
- const bundleOpts = haveIds ? { ...(opts || {}), schemas } : (opts || {});
343
- const format = (opts && opts.format) || 'cjs';
344
- let bundleUsesSafeRe = false;
345
- // Analyze schemas and group by structure
346
- const entries = schemas.map((schema) => {
347
- const v = new Validator(schema, bundleOpts);
348
- v._ensureCompiled();
349
- const jsFn = v._jsFn;
350
- if (!jsFn || !jsFn._hybridSource) return null;
351
- // The schema the validator compiled, not the one the caller passed. They
352
- // differ whenever anything prepared the schema: draft-07 normalization,
353
- // `format` removed under assertFormat: false, keywords outside the
354
- // dialect's vocabularies. Reading the original here would leave the
355
- // boolean path and the error path disagreeing about the same document.
356
- const jsErrFn = compileToJSCodegenWithErrors(
357
- v._schemaObj,
358
- v._schemaMap,
359
- v._userFormats,
360
- );
361
- if (jsFn._usesSafeRe || (jsErrFn && jsErrFn._usesSafeRe)) bundleUsesSafeRe = true;
362
- // Hoisted anyOf/oneOf branch helpers (e.g. `_af1_b0`) must travel with the
363
- // hybrid body or it references undefined names. Prepending keeps dedup honest:
364
- // schemas with different branch sets no longer collide on body alone.
365
- const hybrid = jsFn._preambleSource
366
- ? `${jsFn._preambleSource}\n${jsFn._hybridSource}`
367
- : jsFn._hybridSource;
368
- return {
369
- hybrid,
370
- err: jsErrFn && jsErrFn._errSource ? jsErrFn._errSource : null,
371
- fmt: jsFn._formatClosures || null,
372
- };
373
- });
374
-
375
- // Deduplicate function bodies — many schemas produce identical or near-identical code
376
- const bodyMap = new Map(); // body → index
377
- const bodies = [];
378
- const errMap = new Map();
379
- const errBodies = [];
380
-
381
- const indices = entries.map((e) => {
382
- if (!e) return [-1, -1];
383
- let hi = bodyMap.get(e.hybrid);
384
- if (hi === undefined) {
385
- hi = bodies.length;
386
- bodies.push(e.hybrid);
387
- bodyMap.set(e.hybrid, hi);
388
- }
389
- let ei = -1;
390
- if (e.err) {
391
- ei = errMap.get(e.err);
392
- if (ei === undefined) {
393
- ei = errBodies.length;
394
- errBodies.push(e.err);
395
- errMap.set(e.err, ei);
396
- }
397
- }
398
- return [hi, ei];
399
- });
400
-
401
- // Generate compact bundle
402
- const isEsm = format === 'esm';
403
- let out = isEsm
404
- ? '// Auto-generated by ata-validator — do not edit\n'
405
- : "'use strict';\n";
406
- if (bundleUsesSafeRe) out += getSafeRegexEmbed() + '\n';
407
- const declKW = isEsm ? 'const' : 'var';
408
- out += `${declKW} R=Object.freeze({valid:true,errors:Object.freeze([])});\n`;
409
-
410
- // User format functions are referenced as _uf_<name> by the hybrid and error
411
- // bodies. Collect them across all schemas (deduped by name) and embed via
412
- // Function#toString so the bundle stays self-contained.
413
- const fmtSeen = new Set();
414
- const fmtAll = [];
415
- for (const e of entries) {
416
- if (!e || !e.fmt) continue;
417
- for (const entry of e.fmt) {
418
- if (fmtSeen.has(entry.name)) continue;
419
- fmtSeen.add(entry.name);
420
- fmtAll.push(entry);
421
- }
422
- }
423
- const fmtOut = emitFormatDecls(fmtAll, opts && opts.formatMode, declKW);
424
- out += fmtOut.decls;
425
-
426
- // Shared hybrid factories
427
- out += `${declKW} H=[\n`;
428
- out += bodies
429
- .map((b) => `function(R,E){return function(d){${b}}}`)
430
- .join(',\n');
431
- out += '\n];\n';
432
-
433
- // Shared error functions
434
- out += `${declKW} EF=[\n`;
435
- out += errBodies.map((b) => `function(d){var _all=true;${b}}`).join(',\n');
436
- out += '\n];\n';
437
-
438
- // Build validators from shared templates
439
- const arrBody = indices
440
- .map(([hi, ei]) => {
441
- if (hi < 0) return 'null';
442
- if (ei >= 0) return `H[${hi}](R,EF[${ei}])`;
443
- return `H[${hi}](R,function(){return{valid:false,errors:[]}})`;
444
- })
445
- .join(',');
446
- if (isEsm) {
447
- out += `const validators=[${arrBody}];\nexport default validators;\nexport { validators${fmtOut.exportsSetFormats ? ', setFormats' : ''} };\n`;
448
- } else {
449
- out += `module.exports=[${arrBody}];\n`;
450
- if (fmtOut.exportsSetFormats) out += 'module.exports.setFormats=setFormats;\n';
451
- }
452
-
453
- return out;
454
- }
455
-
456
- function loadBundle(Validator, mods, schemas, opts) {
457
- return schemas.map((schema, i) => {
458
- if (mods[i]) return Validator.fromStandalone(mods[i], schema, opts);
459
- return new Validator(schema, opts);
460
- });
461
- }
462
-
463
- module.exports = {
464
- toStandalone,
465
- toStandaloneModule,
466
- bundle,
467
- bundleStandalone,
468
- bundleCompact,
469
- loadBundle,
470
- };
3
+ // Node resolution of the AOT emitters. Browser bundles swap this file for
4
+ // `lib/aot.browser.js` via the package.json `browser` field, so the default
5
+ // browser bundle carries no emitters and no embedded safe-regex source; a
6
+ // page that wants to emit validators imports `ata-validator/aot`, which
7
+ // reaches `lib/aot-impl.js` directly and works without `fs`.
8
+ module.exports = require('./aot-impl.js');
@@ -0,0 +1,13 @@
1
+ 'use strict';
2
+
3
+ // Browser twin of lib/buffer-gate.js, wired by the package.json `browser`
4
+ // field. The gate exists to route schemas the native walker answers wrongly;
5
+ // a browser bundle never loads the native walker, so there is nothing to
6
+ // route and the shape list can stay out of the bundle. index.js only consults
7
+ // the gate when the native addon loaded, which it never does here, so these
8
+ // are unreachable, kept callable for safety.
9
+
10
+ module.exports = {
11
+ bufferNeedsSlowPath: () => false,
12
+ installSlowBufferApis() {},
13
+ };
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.12.1';
10
+ module.exports = '1.13.1';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ata-validator",
3
- "version": "1.12.1",
3
+ "version": "1.13.1",
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",
@@ -20,6 +20,11 @@
20
20
  "import": "./compat.mjs",
21
21
  "require": "./compat.js"
22
22
  },
23
+ "./aot": {
24
+ "types": "./aot.d.ts",
25
+ "import": "./aot.mjs",
26
+ "require": "./aot.js"
27
+ },
23
28
  "./build": {
24
29
  "types": "./build.d.ts",
25
30
  "import": "./build.mjs",
@@ -37,7 +42,9 @@
37
42
  "ata": "bin/ata.js"
38
43
  },
39
44
  "browser": {
40
- "./lib/native-load.js": "./lib/native-load.browser.js"
45
+ "./lib/native-load.js": "./lib/native-load.browser.js",
46
+ "./lib/aot.js": "./lib/aot.browser.js",
47
+ "./lib/buffer-gate.js": "./lib/buffer-gate.browser.js"
41
48
  },
42
49
  "scripts": {
43
50
  "prepublishOnly": "node scripts/regen-safe-regex-source.js && node tests/test_pack_purity.js && ATA_ALLOW_DOC_PLACEHOLDERS=1 node scripts/check-doc-coverage.js && node tests/test_error_codes_lock.js && node tests/test_safe_regex_source_sync.js && node tests/test_version_sync.js",
@@ -96,6 +103,9 @@
96
103
  "index.mjs",
97
104
  "index.browser.mjs",
98
105
  "index.d.ts",
106
+ "aot.js",
107
+ "aot.mjs",
108
+ "aot.d.ts",
99
109
  "lib/",
100
110
  "compat.js",
101
111
  "compat.mjs",
@@ -112,13 +122,13 @@
112
122
  "LICENSE"
113
123
  ],
114
124
  "optionalDependencies": {
115
- "@ata-validator/native-darwin-arm64": "1.12.1",
116
- "@ata-validator/native-darwin-x64": "1.12.1",
117
- "@ata-validator/native-linux-x64-gnu": "1.12.1",
118
- "@ata-validator/native-linux-arm64-gnu": "1.12.1",
119
- "@ata-validator/native-linux-x64-musl": "1.12.1",
120
- "@ata-validator/native-linux-arm64-musl": "1.12.1",
121
- "@ata-validator/native-win32-x64": "1.12.1"
125
+ "@ata-validator/native-darwin-arm64": "1.13.1",
126
+ "@ata-validator/native-darwin-x64": "1.13.1",
127
+ "@ata-validator/native-linux-x64-gnu": "1.13.1",
128
+ "@ata-validator/native-linux-arm64-gnu": "1.13.1",
129
+ "@ata-validator/native-linux-x64-musl": "1.13.1",
130
+ "@ata-validator/native-linux-arm64-musl": "1.13.1",
131
+ "@ata-validator/native-win32-x64": "1.13.1"
122
132
  },
123
133
  "peerDependencies": {
124
134
  "yaml": "^2.0.0"