ata-validator 0.18.1 → 0.20.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/CHANGELOG.md CHANGED
@@ -2,6 +2,50 @@
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.20.0 - 2026-05-27
6
+
7
+ ### Added
8
+
9
+ - New chainable schema builder at `ata-validator/t`. Each `t.X(...)` returns a plain JSON Schema literal, so the output drops straight into `new Validator(...)`, `defineSchema`, `Infer<S>`, and the AOT pipeline with no adapter. The migration target is TypeBox: rename `import { Type } from '@sinclair/typebox'` to `import { t } from 'ata-validator/t'` and keep the same authoring shape while picking up ata's runtime and AOT precompile.
10
+
11
+ ```ts
12
+ import { t } from 'ata-validator/t'
13
+ import { Validator, type Infer } from 'ata-validator'
14
+
15
+ const User = t.object({
16
+ id: t.integer(),
17
+ name: t.string({ minLength: 1 }),
18
+ email: t.optional(t.string({ format: 'email' })),
19
+ role: t.union([t.literal('admin'), t.literal('user')]),
20
+ })
21
+ type User = Infer<typeof User>
22
+ const v = new Validator(User)
23
+ ```
24
+
25
+ Covered: primitives (`string`, `number`, `integer`, `boolean`, `null`), composites (`object` with `optional` keys, `array`, `tuple`, `record`, `union`, `intersect`, `literal`, `const`, `enum`), and refs (`ref`). Optionality is carried by a Symbol-keyed marker that the emitted JSON Schema, `Object.keys`, `JSON.stringify`, and ata's codegen never see; the parent `t.object` reads it to compute `required`.
26
+
27
+ ### Changed
28
+
29
+ - `Infer<S>` now resolves object schemas without `properties` but with a schema-valued `additionalProperties` to `Record<string, V>` instead of `Record<string, unknown>`. Closes the last common JSON Schema shape that was not inferred.
30
+
31
+ ## 0.19.0 - 2026-05-27
32
+
33
+ ### Added
34
+
35
+ - `ata-validator/build` now exports the AOT primitives `bundleStandalone`, `bundleCompact`, and `toStandaloneModule` as named functions, so callers that want the build surface in one place (bundler plugins, build scripts) no longer have to go through the `Validator` class. Same code paths as the Validator-bound forms, no behaviour difference.
36
+ - New top-level `ARCHITECTURE.md` reference document covering design principles, runtime dispatch, AOT pipeline, error enrichment, the two TypeScript paths, and the native layer.
37
+
38
+ ### Changed
39
+
40
+ - Internal refactor: AOT (`toStandalone`, `toStandaloneModule`, `bundle`, `bundleStandalone`, `bundleCompact`, `loadBundle`) lives in `lib/aot.js`, the native addon loader in `lib/native-load.js`, the version string in `lib/version.js`. `index.js` lazy-requires the AOT module so a plain import never pays for code it does not call. The browser bundle drops `pkg-prebuilds`, `__dirname`, and `package.json` (with its dependency strings) entirely; it is roughly 15 KB smaller and contains no Node-only identifiers outside comments.
41
+ - The safe-regex engine is now embedded into standalone output from a baked string (`lib/safe-regex-source.js`, generated from `lib/safe-regex.js`) instead of a runtime `fs.readFileSync`. Browser AOT calls (`Validator.bundle`, `toStandaloneModule`, …) work in any bundler without an fs polyfill. A structural test (`tests/test_browser_imports_guard.js`) bundles both entries with esbuild and asserts no `readFileSync`, `pkg-prebuilds`, or `__dirname` survives outside comments; sync tests catch drift between the bundled strings and their sources.
42
+
43
+ ## 0.18.2 - 2026-05-26
44
+
45
+ ### Fixed
46
+
47
+ - The browser and edge build no longer touches the filesystem at import. The safe-regex engine source was embedded into standalone output through a `fs.readFileSync` that ran at module load, which crashed bundlers that stub `fs`/`path` for the browser (a regression from 0.17.3). The read is deferred to the first standalone compile that actually embeds the engine, so importing ata, validating, generating types, and compiling pattern-free schemas now run with no filesystem access in browsers and Cloudflare Workers. Added a regression test that bundles the browser entry and runs it with `fs`/`path` stubbed.
48
+
5
49
  ## 0.18.1 - 2026-05-26
6
50
 
7
51
  ### Added
package/build.d.ts CHANGED
@@ -67,3 +67,31 @@ export interface WatchHandle {
67
67
  close(): void;
68
68
  }
69
69
  export function watch(opts: BuildOptions, onReport?: (r: BuildReport) => void): Promise<WatchHandle>;
70
+
71
+ // --- AOT primitives ---
72
+ // Programmatic counterparts to `Validator.bundleStandalone` / `bundleCompact`
73
+ // and `validator.toStandaloneModule`. Kept here so callers that only want the
74
+ // build surface (e.g. a bundler plugin) don't have to import the full runtime.
75
+
76
+ export interface BundleStandaloneOptions {
77
+ format?: 'cjs' | 'esm';
78
+ formats?: Record<string, (value: unknown) => boolean>;
79
+ verbose?: boolean;
80
+ }
81
+
82
+ export interface ToStandaloneModuleOptions {
83
+ format?: 'cjs' | 'esm';
84
+ abortEarly?: boolean;
85
+ source?: boolean;
86
+ sourceMap?: unknown;
87
+ schemaFile?: string;
88
+ }
89
+
90
+ /** Bundle multiple schemas into one self-contained module (no ata-validator runtime). */
91
+ export function bundleStandalone(schemas: unknown[], options?: BundleStandaloneOptions): string;
92
+
93
+ /** Like {@link bundleStandalone} but deduplicates shared bodies for smaller output. */
94
+ export function bundleCompact(schemas: unknown[], options?: BundleStandaloneOptions): string;
95
+
96
+ /** Emit a self-contained `validate`/`isValid` module string for a single schema. */
97
+ export function toStandaloneModule(schema: unknown, options?: ToStandaloneModuleOptions): string | null;
package/build.mjs CHANGED
@@ -5,4 +5,7 @@ export const expandGlobs = mod.expandGlobs;
5
5
  export const parseSchemaFile = mod.parseSchemaFile;
6
6
  export const outputPathFor = mod.outputPathFor;
7
7
  export const watch = mod.watch;
8
+ export const bundleStandalone = mod.bundleStandalone;
9
+ export const bundleCompact = mod.bundleCompact;
10
+ export const toStandaloneModule = mod.toStandaloneModule;
8
11
  export default mod;
package/index.d.ts CHANGED
@@ -202,13 +202,19 @@ type ResolveRef<R, D> = [RefName<R>] extends [never]
202
202
  ? InferWith<D[RefName<R>], D>
203
203
  : unknown;
204
204
 
205
- /** Object shape: required keys are required, all other declared keys optional. */
205
+ /** Object shape: required keys are required, all other declared keys optional.
206
+ * When `properties` is absent and `additionalProperties` is a schema, infer
207
+ * a `Record<string, V>` (the JSON Schema "dictionary"/"record" shape). */
206
208
  type InferObject<S, D> = S extends { properties: infer P }
207
209
  ? Simplify<
208
210
  { [K in keyof P as K extends RequiredKeys<S> ? K : never]: InferWith<P[K], D> } &
209
211
  { [K in keyof P as K extends RequiredKeys<S> ? never : K]?: InferWith<P[K], D> }
210
212
  >
211
- : Record<string, unknown>;
213
+ : S extends { additionalProperties: infer A }
214
+ ? A extends boolean
215
+ ? Record<string, unknown>
216
+ : Record<string, InferWith<A, D>>
217
+ : Record<string, unknown>;
212
218
 
213
219
  /** Array shape: `prefixItems` -> tuple; `items` (single schema) -> element type; otherwise unknown[]. */
214
220
  type InferArray<S, D> = S extends { prefixItems: infer P }
package/index.js CHANGED
@@ -1,7 +1,8 @@
1
1
  // Native addon: optional. Core validate() uses JS codegen and works without it.
2
2
  // Buffer APIs (isValid, countValid, isValidParallel) require native.
3
- let native;
4
- try { native = require("pkg-prebuilds")(__dirname, require("./binding-options")); } catch {}
3
+ // Loading is delegated so this file stays free of `pkg-prebuilds`/`__dirname`/
4
+ // `path` (the browser entry must not pull those in via the bundler).
5
+ const native = require("./lib/native-load")();
5
6
  const {
6
7
  compileToJS,
7
8
  compileToJSCodegen,
@@ -287,38 +288,11 @@ const ABORT_EARLY_RESULT = Object.freeze({
287
288
  })]),
288
289
  });
289
290
 
290
- // Embedded verbatim in standalone modules so the output file has no runtime
291
- // dependency on ata-validator. ASCII fast-path plus surrogate-aware slow path.
292
- const _CP_LEN_SOURCE = `function _cpLen(s) {
293
- const len = s.length;
294
- for (let i = 0; i < len; i++) {
295
- if (s.charCodeAt(i) >= 0xD800 && s.charCodeAt(i) <= 0xDBFF) {
296
- let n = 0; for (const _ of s) n++; return n;
297
- }
298
- }
299
- return len;
300
- }`;
301
-
302
- // The linear-time regex engine, inlined verbatim into standalone output so a
303
- // compiled module that uses safe `pattern` matchers has no runtime dependency on
304
- // ata-validator. Read from the source file (one copy, no drift) with the strict
305
- // directive and CommonJS exports stripped, plus the `__ataSafeRe` alias the
306
- // emitted code calls. The engine has no eval/new Function, so the embed is
307
- // CSP-safe.
308
- const SAFE_REGEX_EMBED = (() => {
309
- const raw = require("fs").readFileSync(require("path").join(__dirname, "lib", "safe-regex.js"), "utf8");
310
- const body = raw
311
- .replace(/^'use strict'\s*\n/, "")
312
- .replace(/\nmodule\.exports[^\n]*\n?/, "\n");
313
- return body.trimEnd() + "\nconst __ataSafeRe = compileSafe;";
314
- })();
315
-
316
- // Returns the engine embed when any supplied compiled function references the
317
- // safe matcher (jsFn._usesSafeRe), else an empty string so non-pattern modules
318
- // pay zero bytes.
319
- function safeRePrelude(...fns) {
320
- return fns.some((f) => f && f._usesSafeRe) ? SAFE_REGEX_EMBED + "\n" : "";
321
- }
291
+ // `_CP_LEN_SOURCE`, the safe-regex embed, and the AOT helpers that consume them
292
+ // now live in `lib/aot.js` — keeping this file free of `fs`/`path`/`__dirname`
293
+ // references so a default import never touches disk. The instance and static
294
+ // AOT methods further down lazily require `./lib/aot`, so they pay nothing
295
+ // until a user calls `toStandaloneModule`/`bundleStandalone`/etc.
322
296
 
323
297
  // Above this size, simdjson On Demand (selective field access) beats JSON.parse
324
298
  // (which must materialize the full JS object tree). Buffer.from + NAPI ~2x faster.
@@ -1153,157 +1127,18 @@ class Validator {
1153
1127
  }
1154
1128
  }
1155
1129
 
1156
- // --- Standalone pre-compilation ---
1157
- // Generate a JS module string that can be written to a file.
1158
- // On next startup, load with Validator.fromStandalone() -- zero compile time.
1130
+ // --- AOT pre-compilation ---
1131
+ // The bodies of `toStandalone` and `toStandaloneModule` live in `lib/aot.js`
1132
+ // so this entry stays free of `fs`/`path`/`__dirname`. The lazy require runs
1133
+ // only on first call, never during a plain `import 'ata-validator'`. Browser
1134
+ // bundlers swap `lib/aot.js` with `lib/aot.browser.js`, which throws a
1135
+ // pointed error instead of attempting to read source from disk.
1159
1136
  toStandalone() {
1160
- this._ensureCompiled();
1161
- const jsFn = this._jsFn;
1162
- if (!jsFn || !jsFn._source) return null;
1163
- const src = jsFn._source;
1164
- const hybridSrc = jsFn._hybridSource || "";
1165
- const preambleSrc = jsFn._preambleSource || "";
1166
-
1167
- // Also capture error function source for zero-compile standalone load
1168
- const jsErrFn = compileToJSCodegenWithErrors(
1169
- typeof this._schemaObj === "object" ? this._schemaObj : {},
1170
- );
1171
- const errSrc = jsErrFn && jsErrFn._errSource ? jsErrFn._errSource : "";
1172
-
1173
- return `// Auto-generated by ata-validator — do not edit
1174
- 'use strict';
1175
- ${_CP_LEN_SOURCE}
1176
- ${safeRePrelude(jsFn, jsErrFn)}${preambleSrc}
1177
- const boolFn = function(d) {
1178
- ${src}
1179
- };
1180
- const hybridFactory = function(R, E) {
1181
- return function(d) {
1182
- ${hybridSrc}
1183
- };
1184
- };
1185
- ${errSrc ? `const errFn = function(d, _all) {\n ${errSrc}\n};` : "const errFn = null;"}
1186
- module.exports = { boolFn, hybridFactory, errFn };
1187
- `;
1137
+ return require('./lib/aot').toStandalone(this);
1188
1138
  }
1189
1139
 
1190
- // --- Fully standalone module ---
1191
- // Generates a self-contained module that can be imported directly without
1192
- // pulling in ata-validator at runtime. Browser bundle gets only the
1193
- // generated validator (~2 KB typical) instead of the 165 KB compiler.
1194
- //
1195
- // import { validate, isValid } from './user-validator.mjs'
1196
- // if (isValid(data)) { ... }
1197
- //
1198
- // format: 'esm' | 'cjs'. Default 'esm'.
1199
- // abortEarly: if true, invalid result is a shared stub; smaller output.
1200
1140
  toStandaloneModule(opts) {
1201
- this._ensureCompiled();
1202
- const jsFn = this._jsFn;
1203
- if (!jsFn || !jsFn._source) return null;
1204
- const format = (opts && opts.format) || 'esm';
1205
- const abortEarly = !!(opts && opts.abortEarly);
1206
- const source = !!(opts && opts.source);
1207
- const sourceMap = opts && opts.sourceMap ? opts.sourceMap : null;
1208
- const schemaFile = opts && opts.schemaFile ? opts.schemaFile : null;
1209
- const src = jsFn._source;
1210
-
1211
- let errCore = '';
1212
- let jsErrFn = null;
1213
- if (!abortEarly) {
1214
- jsErrFn = compileToJSCodegenWithErrors(
1215
- typeof this._schemaObj === 'object' ? this._schemaObj : {},
1216
- null,
1217
- this._userFormats,
1218
- (source && sourceMap && schemaFile) ? { sourceMap, schemaFile } : null,
1219
- );
1220
- const errSrc = jsErrFn && jsErrFn._errSource ? jsErrFn._errSource : '';
1221
- if (errSrc) {
1222
- errCore = `const errFn = function(d, _all) {\n ${errSrc}\n};\n`;
1223
- }
1224
- }
1225
-
1226
- // Schema-source frames are baked as literals inside each emitted error so
1227
- // consumers don't need a runtime lookup. We still expose the schema file
1228
- // as a sentinel constant when --source is on — handy for introspection
1229
- // and visible in source graphs. With --no-source, the constant is omitted
1230
- // entirely so size budgets and grep-based "is this source-mapped?" checks
1231
- // both work.
1232
- const schemaSourceConst = (source && schemaFile)
1233
- ? `const __ATA_SCHEMA_SOURCE__ = ${JSON.stringify({ file: schemaFile })};\n`
1234
- : '';
1235
-
1236
- // Serialize closure vars referenced in _fn body: regex, sub-validators, sets.
1237
- let closureDecls = '';
1238
- if (jsFn._closures && jsFn._closures.length > 0) {
1239
- const lines = [];
1240
- for (const { name, val } of jsFn._closures) {
1241
- if (Array.isArray(val)) {
1242
- lines.push(`const ${name} = ${JSON.stringify(val)};`);
1243
- continue;
1244
- }
1245
- if (val && val.__ataSafe) {
1246
- lines.push(`const ${name} = __ataSafeRe(${JSON.stringify(val.source)});`);
1247
- } else if (val instanceof RegExp) {
1248
- const flags = val.flags;
1249
- lines.push(`const ${name} = new RegExp(${JSON.stringify(val.source)}${flags ? ', ' + JSON.stringify(flags) : ''});`);
1250
- } else if (val instanceof Set) {
1251
- lines.push(`const ${name} = new Set(${JSON.stringify([...val])});`);
1252
- } else if (typeof val === 'function') {
1253
- // new Function('_ppv', body) — extract body from toString()
1254
- const str = val.toString();
1255
- // Matches: "function anonymous(_ppv\n) {\nbody\n}" or "function(_ppv){body}"
1256
- const m = str.match(/^function[^(]*\([^)]*\)\s*\{([\s\S]*)\}$/)
1257
- const body = m ? m[1].trim() : str;
1258
- lines.push(`const ${name} = function(_ppv) { ${body} };`);
1259
- }
1260
- }
1261
- if (lines.length) closureDecls = lines.join('\n') + '\n';
1262
- }
1263
-
1264
- // Hoisted oneOf/anyOf branch checks live in the boolean fn's preamble (the
1265
- // runtime emits them before the function). The standalone module must declare
1266
- // them at module scope too, or _fn references undefined names (e.g. _af1_b0).
1267
- const preambleDecls = jsFn._preambleSource ? jsFn._preambleSource + '\n' : '';
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
-
1279
- const validBody = errCore
1280
- ? 'return _fn(data) ? VALID : { valid: false, errors: errFn(data, true).errors }'
1281
- : 'return _fn(data) ? VALID : ABORT';
1282
-
1283
- const exports = format === 'esm'
1284
- ? `export { validate, isValid };\nexport default { validate, isValid };\n`
1285
- : `module.exports = { validate, isValid };\nmodule.exports.default = module.exports;\n`;
1286
-
1287
- return `// Auto-generated by ata-validator — do not edit.
1288
- // Schema is embedded; runtime has zero dependency on ata-validator.
1289
- 'use strict';
1290
- ${_CP_LEN_SOURCE}
1291
- ${safeRePrelude(jsFn, jsErrFn)}${schemaSourceConst}const VALID = Object.freeze({ valid: true, errors: Object.freeze([]) });
1292
- const ABORT = Object.freeze({
1293
- valid: false,
1294
- errors: Object.freeze([Object.freeze({
1295
- code: 'ATA9000',
1296
- message: 'validation failed',
1297
- keyword: '__abort_early__',
1298
- path: '',
1299
- })]),
1300
- });
1301
- ${closureDecls}${preambleDecls}${formatDecls}const _fn = function(d) {
1302
- ${src}
1303
- };
1304
- ${errCore}function isValid(data) { return _fn(data); }
1305
- function validate(data) { ${validBody}; }
1306
- ${exports}`;
1141
+ return require('./lib/aot').toStandaloneModule(this, opts);
1307
1142
  }
1308
1143
 
1309
1144
  // Load a pre-compiled standalone module. Zero schema compilation.
@@ -1489,200 +1324,28 @@ function validate(schema, data) {
1489
1324
 
1490
1325
  function version() {
1491
1326
  if (native) return native.version();
1492
- try { return require("./package.json").version; } catch { return "unknown"; }
1327
+ try { return require("./lib/version"); } catch { return "unknown"; }
1493
1328
  }
1494
1329
 
1495
- // Bundle multiple validators into a single JS file for fast startup.
1496
- // Usage:
1497
- // const bundle = Validator.bundle([schema1, schema2, ...]);
1498
- // fs.writeFileSync('validators.js', bundle);
1499
- // // On startup:
1500
- // const validators = Validator.loadBundle(require('./validators.js'), [schema1, schema2, ...]);
1330
+ // Static AOT entry points are thin lazy-loaders into `lib/aot.js`. The
1331
+ // implementation files (and the `fs`/`path` reads they perform) only enter
1332
+ // the process when one of these is actually called. See `lib/aot.js` for the
1333
+ // generated module shapes; browser bundles get `lib/aot.browser.js` (a stub
1334
+ // that throws) via the package.json `browser` field.
1501
1335
  Validator.bundle = function (schemas, opts) {
1502
- const parts = schemas.map((schema) => {
1503
- const v = new Validator(schema, opts);
1504
- const standalone = v.toStandalone();
1505
- if (!standalone) return "null";
1506
- return (
1507
- "(function(){" +
1508
- standalone
1509
- .replace("'use strict';", "")
1510
- .replace("module.exports = ", "return ") +
1511
- "})()"
1512
- );
1513
- });
1514
- return "'use strict';\nmodule.exports = [\n" + parts.join(",\n") + "\n];\n";
1336
+ return require('./lib/aot').bundle(Validator, schemas, opts);
1515
1337
  };
1516
1338
 
1517
- // Zero-dependency self-contained bundle — no require('ata-validator') needed at runtime.
1518
- // opts.format: 'cjs' (default) or 'esm'.
1519
- // opts.formats: { name: fn } — embedded in the output via Function#toString.
1520
1339
  Validator.bundleStandalone = function (schemas, opts) {
1521
- // Cross-schema $ref resolution: only meaningful when at least one schema has
1522
- // an $id. Skip the schemas-as-map plumbing when none of them do.
1523
- const haveIds = schemas.some((s) => s && typeof s === 'object' && s.$id);
1524
- const bundleOpts = haveIds ? { ...(opts || {}), schemas } : (opts || {});
1525
- const format = (opts && opts.format) || 'cjs';
1526
- const R = "Object.freeze({valid:true,errors:Object.freeze([])})";
1527
- let bundleUsesSafeRe = false;
1528
- const fns = schemas.map((schema) => {
1529
- const v = new Validator(schema, bundleOpts);
1530
- v._ensureCompiled();
1531
- const jsFn = v._jsFn;
1532
- if (!jsFn || !jsFn._hybridSource) return "null";
1533
- const jsErrFn = compileToJSCodegenWithErrors(
1534
- typeof schema === "string" ? JSON.parse(schema) : schema,
1535
- v._schemaMap,
1536
- v._userFormats,
1537
- );
1538
- if (jsFn._usesSafeRe || (jsErrFn && jsErrFn._usesSafeRe)) bundleUsesSafeRe = true;
1539
- const errBody =
1540
- jsErrFn && jsErrFn._errSource
1541
- ? jsErrFn._errSource
1542
- : "return{valid:false,errors:[{code:'error',path:'',message:'validation failed'}]}";
1543
- // Serialize custom format closures so the bundle has no runtime dep on ata.
1544
- let preamble = '';
1545
- if (jsFn._formatClosures) {
1546
- preamble = jsFn._formatClosures
1547
- .map(({ name, fn }) => `var ${name}=${fn.toString()};`)
1548
- .join('\n');
1549
- }
1550
- // Include hoisted anyOf/oneOf branch helpers (e.g. `_af1_b0`) so the
1551
- // bundle output is self-contained. `toStandalone` emits this same source
1552
- // for single-schema standalone output.
1553
- if (jsFn._preambleSource) {
1554
- preamble = preamble ? `${preamble}\n${jsFn._preambleSource}` : jsFn._preambleSource;
1555
- }
1556
- if (opts && opts.verbose) {
1557
- // Embed the schema and a small resolver so errors carry parentSchema.
1558
- const schemaLit = JSON.stringify(typeof schema === 'string' ? JSON.parse(schema) : schema);
1559
- 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)`;
1560
- }
1561
- return `(function(R){${preamble}var E=function(d){var _all=true;${errBody}};return function(d){${jsFn._hybridSource}}})(R)`;
1562
- });
1563
- const arr = `[${fns.join(",")}]`;
1564
- const safeEmbed = bundleUsesSafeRe ? SAFE_REGEX_EMBED + "\n" : "";
1565
- if (format === 'esm') {
1566
- return `// Auto-generated by ata-validator — do not edit\n${safeEmbed}const R=${R};\nconst validators=${arr};\nexport default validators;\nexport { validators };\n`;
1567
- }
1568
- return `'use strict';\n${safeEmbed}var R=${R};\nmodule.exports=[${fns.join(",")}];\n`;
1340
+ return require('./lib/aot').bundleStandalone(Validator, schemas, opts);
1569
1341
  };
1570
1342
 
1571
- // Compact bundle: deduplicated code. Shared template functions + per-schema params.
1572
- // Much smaller file → faster V8 parse → faster startup.
1573
- // opts.format: 'cjs' (default) or 'esm'.
1574
1343
  Validator.bundleCompact = function (schemas, opts) {
1575
- const haveIds = schemas.some((s) => s && typeof s === 'object' && s.$id);
1576
- const bundleOpts = haveIds ? { ...(opts || {}), schemas } : (opts || {});
1577
- const format = (opts && opts.format) || 'cjs';
1578
- let bundleUsesSafeRe = false;
1579
- // Analyze schemas and group by structure
1580
- const entries = schemas.map((schema) => {
1581
- const v = new Validator(schema, bundleOpts);
1582
- v._ensureCompiled();
1583
- const jsFn = v._jsFn;
1584
- if (!jsFn || !jsFn._hybridSource) return null;
1585
- const jsErrFn = compileToJSCodegenWithErrors(
1586
- typeof schema === "string" ? JSON.parse(schema) : schema,
1587
- v._schemaMap,
1588
- v._userFormats,
1589
- );
1590
- if (jsFn._usesSafeRe || (jsErrFn && jsErrFn._usesSafeRe)) bundleUsesSafeRe = true;
1591
- // Hoisted anyOf/oneOf branch helpers (e.g. `_af1_b0`) must travel with the
1592
- // hybrid body or it references undefined names. Prepending keeps dedup honest:
1593
- // schemas with different branch sets no longer collide on body alone.
1594
- const hybrid = jsFn._preambleSource
1595
- ? `${jsFn._preambleSource}\n${jsFn._hybridSource}`
1596
- : jsFn._hybridSource;
1597
- return {
1598
- hybrid,
1599
- err: jsErrFn && jsErrFn._errSource ? jsErrFn._errSource : null,
1600
- fmt: jsFn._formatClosures || null,
1601
- };
1602
- });
1603
-
1604
- // Deduplicate function bodies — many schemas produce identical or near-identical code
1605
- const bodyMap = new Map(); // body → index
1606
- const bodies = [];
1607
- const errMap = new Map();
1608
- const errBodies = [];
1609
-
1610
- const indices = entries.map((e) => {
1611
- if (!e) return [-1, -1];
1612
- let hi = bodyMap.get(e.hybrid);
1613
- if (hi === undefined) {
1614
- hi = bodies.length;
1615
- bodies.push(e.hybrid);
1616
- bodyMap.set(e.hybrid, hi);
1617
- }
1618
- let ei = -1;
1619
- if (e.err) {
1620
- ei = errMap.get(e.err);
1621
- if (ei === undefined) {
1622
- ei = errBodies.length;
1623
- errBodies.push(e.err);
1624
- errMap.set(e.err, ei);
1625
- }
1626
- }
1627
- return [hi, ei];
1628
- });
1629
-
1630
- // Generate compact bundle
1631
- const isEsm = format === 'esm';
1632
- let out = isEsm
1633
- ? "// Auto-generated by ata-validator — do not edit\n"
1634
- : "'use strict';\n";
1635
- if (bundleUsesSafeRe) out += SAFE_REGEX_EMBED + "\n";
1636
- const declKW = isEsm ? "const" : "var";
1637
- out += `${declKW} R=Object.freeze({valid:true,errors:Object.freeze([])});\n`;
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
-
1652
- // Shared hybrid factories
1653
- out += `${declKW} H=[\n`;
1654
- out += bodies
1655
- .map((b) => `function(R,E){return function(d){${b}}}`)
1656
- .join(",\n");
1657
- out += "\n];\n";
1658
-
1659
- // Shared error functions
1660
- out += `${declKW} EF=[\n`;
1661
- out += errBodies.map((b) => `function(d){var _all=true;${b}}`).join(",\n");
1662
- out += "\n];\n";
1663
-
1664
- // Build validators from shared templates
1665
- const arrBody = indices
1666
- .map(([hi, ei]) => {
1667
- if (hi < 0) return "null";
1668
- if (ei >= 0) return `H[${hi}](R,EF[${ei}])`;
1669
- return `H[${hi}](R,function(){return{valid:false,errors:[]}})`;
1670
- })
1671
- .join(",");
1672
- if (isEsm) {
1673
- out += `const validators=[${arrBody}];\nexport default validators;\nexport { validators };\n`;
1674
- } else {
1675
- out += `module.exports=[${arrBody}];\n`;
1676
- }
1677
-
1678
- return out;
1344
+ return require('./lib/aot').bundleCompact(Validator, schemas, opts);
1679
1345
  };
1680
1346
 
1681
1347
  Validator.loadBundle = function (mods, schemas, opts) {
1682
- return schemas.map((schema, i) => {
1683
- if (mods[i]) return Validator.fromStandalone(mods[i], schema, opts);
1684
- return new Validator(schema, opts);
1685
- });
1348
+ return require('./lib/aot').loadBundle(Validator, mods, schemas, opts);
1686
1349
  };
1687
1350
 
1688
1351
  const parseJSON = native ? native.parseJSON : JSON.parse;
package/lib/aot-build.js CHANGED
@@ -231,4 +231,33 @@ async function watch(opts, onReport) {
231
231
  };
232
232
  }
233
233
 
234
- module.exports = { build, expandGlobs, parseSchemaFile, outputPathFor, watch };
234
+ // AOT primitives also surface here so consumers can do
235
+ // import { bundleStandalone, toStandaloneModule, bundleCompact } from 'ata-validator/build'
236
+ // without poking at the Validator class. The Validator-bound forms (Validator.bundleStandalone,
237
+ // validator.toStandaloneModule) remain available from the default entry; this is the
238
+ // programmatic alias for code that wants the build surface in one place.
239
+ const aot = require('./aot');
240
+
241
+ function bundleStandalone(schemas, opts) {
242
+ return aot.bundleStandalone(Validator, schemas, opts);
243
+ }
244
+
245
+ function bundleCompact(schemas, opts) {
246
+ return aot.bundleCompact(Validator, schemas, opts);
247
+ }
248
+
249
+ function toStandaloneModule(schema, opts) {
250
+ const v = schema instanceof Validator ? schema : new Validator(schema, opts);
251
+ return aot.toStandaloneModule(v, opts);
252
+ }
253
+
254
+ module.exports = {
255
+ build,
256
+ expandGlobs,
257
+ parseSchemaFile,
258
+ outputPathFor,
259
+ watch,
260
+ bundleStandalone,
261
+ bundleCompact,
262
+ toStandaloneModule,
263
+ };