ata-validator 0.18.2 → 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,44 @@
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
+
5
43
  ## 0.18.2 - 2026-05-26
6
44
 
7
45
  ### Fixed
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,45 +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
- // Lazy so merely importing ata never touches the filesystem. Bundlers that stub
309
- // `fs`/`path` for the browser would otherwise crash at module load; the read is
310
- // deferred to the first standalone compile that actually embeds the engine.
311
- let _safeRegexEmbed = null;
312
- function getSafeRegexEmbed() {
313
- if (_safeRegexEmbed === null) {
314
- const raw = require("fs").readFileSync(require("path").join(__dirname, "lib", "safe-regex.js"), "utf8");
315
- const body = raw
316
- .replace(/^'use strict'\s*\n/, "")
317
- .replace(/\nmodule\.exports[^\n]*\n?/, "\n");
318
- _safeRegexEmbed = body.trimEnd() + "\nconst __ataSafeRe = compileSafe;";
319
- }
320
- return _safeRegexEmbed;
321
- }
322
-
323
- // Returns the engine embed when any supplied compiled function references the
324
- // safe matcher (jsFn._usesSafeRe), else an empty string so non-pattern modules
325
- // pay zero bytes.
326
- function safeRePrelude(...fns) {
327
- return fns.some((f) => f && f._usesSafeRe) ? getSafeRegexEmbed() + "\n" : "";
328
- }
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.
329
296
 
330
297
  // Above this size, simdjson On Demand (selective field access) beats JSON.parse
331
298
  // (which must materialize the full JS object tree). Buffer.from + NAPI ~2x faster.
@@ -1160,157 +1127,18 @@ class Validator {
1160
1127
  }
1161
1128
  }
1162
1129
 
1163
- // --- Standalone pre-compilation ---
1164
- // Generate a JS module string that can be written to a file.
1165
- // 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.
1166
1136
  toStandalone() {
1167
- this._ensureCompiled();
1168
- const jsFn = this._jsFn;
1169
- if (!jsFn || !jsFn._source) return null;
1170
- const src = jsFn._source;
1171
- const hybridSrc = jsFn._hybridSource || "";
1172
- const preambleSrc = jsFn._preambleSource || "";
1173
-
1174
- // Also capture error function source for zero-compile standalone load
1175
- const jsErrFn = compileToJSCodegenWithErrors(
1176
- typeof this._schemaObj === "object" ? this._schemaObj : {},
1177
- );
1178
- const errSrc = jsErrFn && jsErrFn._errSource ? jsErrFn._errSource : "";
1179
-
1180
- return `// Auto-generated by ata-validator — do not edit
1181
- 'use strict';
1182
- ${_CP_LEN_SOURCE}
1183
- ${safeRePrelude(jsFn, jsErrFn)}${preambleSrc}
1184
- const boolFn = function(d) {
1185
- ${src}
1186
- };
1187
- const hybridFactory = function(R, E) {
1188
- return function(d) {
1189
- ${hybridSrc}
1190
- };
1191
- };
1192
- ${errSrc ? `const errFn = function(d, _all) {\n ${errSrc}\n};` : "const errFn = null;"}
1193
- module.exports = { boolFn, hybridFactory, errFn };
1194
- `;
1137
+ return require('./lib/aot').toStandalone(this);
1195
1138
  }
1196
1139
 
1197
- // --- Fully standalone module ---
1198
- // Generates a self-contained module that can be imported directly without
1199
- // pulling in ata-validator at runtime. Browser bundle gets only the
1200
- // generated validator (~2 KB typical) instead of the 165 KB compiler.
1201
- //
1202
- // import { validate, isValid } from './user-validator.mjs'
1203
- // if (isValid(data)) { ... }
1204
- //
1205
- // format: 'esm' | 'cjs'. Default 'esm'.
1206
- // abortEarly: if true, invalid result is a shared stub; smaller output.
1207
1140
  toStandaloneModule(opts) {
1208
- this._ensureCompiled();
1209
- const jsFn = this._jsFn;
1210
- if (!jsFn || !jsFn._source) return null;
1211
- const format = (opts && opts.format) || 'esm';
1212
- const abortEarly = !!(opts && opts.abortEarly);
1213
- const source = !!(opts && opts.source);
1214
- const sourceMap = opts && opts.sourceMap ? opts.sourceMap : null;
1215
- const schemaFile = opts && opts.schemaFile ? opts.schemaFile : null;
1216
- const src = jsFn._source;
1217
-
1218
- let errCore = '';
1219
- let jsErrFn = null;
1220
- if (!abortEarly) {
1221
- jsErrFn = compileToJSCodegenWithErrors(
1222
- typeof this._schemaObj === 'object' ? this._schemaObj : {},
1223
- null,
1224
- this._userFormats,
1225
- (source && sourceMap && schemaFile) ? { sourceMap, schemaFile } : null,
1226
- );
1227
- const errSrc = jsErrFn && jsErrFn._errSource ? jsErrFn._errSource : '';
1228
- if (errSrc) {
1229
- errCore = `const errFn = function(d, _all) {\n ${errSrc}\n};\n`;
1230
- }
1231
- }
1232
-
1233
- // Schema-source frames are baked as literals inside each emitted error so
1234
- // consumers don't need a runtime lookup. We still expose the schema file
1235
- // as a sentinel constant when --source is on — handy for introspection
1236
- // and visible in source graphs. With --no-source, the constant is omitted
1237
- // entirely so size budgets and grep-based "is this source-mapped?" checks
1238
- // both work.
1239
- const schemaSourceConst = (source && schemaFile)
1240
- ? `const __ATA_SCHEMA_SOURCE__ = ${JSON.stringify({ file: schemaFile })};\n`
1241
- : '';
1242
-
1243
- // Serialize closure vars referenced in _fn body: regex, sub-validators, sets.
1244
- let closureDecls = '';
1245
- if (jsFn._closures && jsFn._closures.length > 0) {
1246
- const lines = [];
1247
- for (const { name, val } of jsFn._closures) {
1248
- if (Array.isArray(val)) {
1249
- lines.push(`const ${name} = ${JSON.stringify(val)};`);
1250
- continue;
1251
- }
1252
- if (val && val.__ataSafe) {
1253
- lines.push(`const ${name} = __ataSafeRe(${JSON.stringify(val.source)});`);
1254
- } else if (val instanceof RegExp) {
1255
- const flags = val.flags;
1256
- lines.push(`const ${name} = new RegExp(${JSON.stringify(val.source)}${flags ? ', ' + JSON.stringify(flags) : ''});`);
1257
- } else if (val instanceof Set) {
1258
- lines.push(`const ${name} = new Set(${JSON.stringify([...val])});`);
1259
- } else if (typeof val === 'function') {
1260
- // new Function('_ppv', body) — extract body from toString()
1261
- const str = val.toString();
1262
- // Matches: "function anonymous(_ppv\n) {\nbody\n}" or "function(_ppv){body}"
1263
- const m = str.match(/^function[^(]*\([^)]*\)\s*\{([\s\S]*)\}$/)
1264
- const body = m ? m[1].trim() : str;
1265
- lines.push(`const ${name} = function(_ppv) { ${body} };`);
1266
- }
1267
- }
1268
- if (lines.length) closureDecls = lines.join('\n') + '\n';
1269
- }
1270
-
1271
- // Hoisted oneOf/anyOf branch checks live in the boolean fn's preamble (the
1272
- // runtime emits them before the function). The standalone module must declare
1273
- // them at module scope too, or _fn references undefined names (e.g. _af1_b0).
1274
- const preambleDecls = jsFn._preambleSource ? jsFn._preambleSource + '\n' : '';
1275
-
1276
- // User-supplied format functions are referenced as _uf_<name> by both the
1277
- // boolean (_fn) and error (errFn) bodies. Embed them via Function#toString
1278
- // so the standalone module stays self-contained.
1279
- let formatDecls = '';
1280
- if (jsFn._formatClosures && jsFn._formatClosures.length > 0) {
1281
- formatDecls = jsFn._formatClosures
1282
- .map(({ name, fn }) => `const ${name} = ${fn.toString()};`)
1283
- .join('\n') + '\n';
1284
- }
1285
-
1286
- const validBody = errCore
1287
- ? 'return _fn(data) ? VALID : { valid: false, errors: errFn(data, true).errors }'
1288
- : 'return _fn(data) ? VALID : ABORT';
1289
-
1290
- const exports = format === 'esm'
1291
- ? `export { validate, isValid };\nexport default { validate, isValid };\n`
1292
- : `module.exports = { validate, isValid };\nmodule.exports.default = module.exports;\n`;
1293
-
1294
- return `// Auto-generated by ata-validator — do not edit.
1295
- // Schema is embedded; runtime has zero dependency on ata-validator.
1296
- 'use strict';
1297
- ${_CP_LEN_SOURCE}
1298
- ${safeRePrelude(jsFn, jsErrFn)}${schemaSourceConst}const VALID = Object.freeze({ valid: true, errors: Object.freeze([]) });
1299
- const ABORT = Object.freeze({
1300
- valid: false,
1301
- errors: Object.freeze([Object.freeze({
1302
- code: 'ATA9000',
1303
- message: 'validation failed',
1304
- keyword: '__abort_early__',
1305
- path: '',
1306
- })]),
1307
- });
1308
- ${closureDecls}${preambleDecls}${formatDecls}const _fn = function(d) {
1309
- ${src}
1310
- };
1311
- ${errCore}function isValid(data) { return _fn(data); }
1312
- function validate(data) { ${validBody}; }
1313
- ${exports}`;
1141
+ return require('./lib/aot').toStandaloneModule(this, opts);
1314
1142
  }
1315
1143
 
1316
1144
  // Load a pre-compiled standalone module. Zero schema compilation.
@@ -1496,200 +1324,28 @@ function validate(schema, data) {
1496
1324
 
1497
1325
  function version() {
1498
1326
  if (native) return native.version();
1499
- try { return require("./package.json").version; } catch { return "unknown"; }
1327
+ try { return require("./lib/version"); } catch { return "unknown"; }
1500
1328
  }
1501
1329
 
1502
- // Bundle multiple validators into a single JS file for fast startup.
1503
- // Usage:
1504
- // const bundle = Validator.bundle([schema1, schema2, ...]);
1505
- // fs.writeFileSync('validators.js', bundle);
1506
- // // On startup:
1507
- // 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.
1508
1335
  Validator.bundle = function (schemas, opts) {
1509
- const parts = schemas.map((schema) => {
1510
- const v = new Validator(schema, opts);
1511
- const standalone = v.toStandalone();
1512
- if (!standalone) return "null";
1513
- return (
1514
- "(function(){" +
1515
- standalone
1516
- .replace("'use strict';", "")
1517
- .replace("module.exports = ", "return ") +
1518
- "})()"
1519
- );
1520
- });
1521
- return "'use strict';\nmodule.exports = [\n" + parts.join(",\n") + "\n];\n";
1336
+ return require('./lib/aot').bundle(Validator, schemas, opts);
1522
1337
  };
1523
1338
 
1524
- // Zero-dependency self-contained bundle — no require('ata-validator') needed at runtime.
1525
- // opts.format: 'cjs' (default) or 'esm'.
1526
- // opts.formats: { name: fn } — embedded in the output via Function#toString.
1527
1339
  Validator.bundleStandalone = function (schemas, opts) {
1528
- // Cross-schema $ref resolution: only meaningful when at least one schema has
1529
- // an $id. Skip the schemas-as-map plumbing when none of them do.
1530
- const haveIds = schemas.some((s) => s && typeof s === 'object' && s.$id);
1531
- const bundleOpts = haveIds ? { ...(opts || {}), schemas } : (opts || {});
1532
- const format = (opts && opts.format) || 'cjs';
1533
- const R = "Object.freeze({valid:true,errors:Object.freeze([])})";
1534
- let bundleUsesSafeRe = false;
1535
- const fns = schemas.map((schema) => {
1536
- const v = new Validator(schema, bundleOpts);
1537
- v._ensureCompiled();
1538
- const jsFn = v._jsFn;
1539
- if (!jsFn || !jsFn._hybridSource) return "null";
1540
- const jsErrFn = compileToJSCodegenWithErrors(
1541
- typeof schema === "string" ? JSON.parse(schema) : schema,
1542
- v._schemaMap,
1543
- v._userFormats,
1544
- );
1545
- if (jsFn._usesSafeRe || (jsErrFn && jsErrFn._usesSafeRe)) bundleUsesSafeRe = true;
1546
- const errBody =
1547
- jsErrFn && jsErrFn._errSource
1548
- ? jsErrFn._errSource
1549
- : "return{valid:false,errors:[{code:'error',path:'',message:'validation failed'}]}";
1550
- // Serialize custom format closures so the bundle has no runtime dep on ata.
1551
- let preamble = '';
1552
- if (jsFn._formatClosures) {
1553
- preamble = jsFn._formatClosures
1554
- .map(({ name, fn }) => `var ${name}=${fn.toString()};`)
1555
- .join('\n');
1556
- }
1557
- // Include hoisted anyOf/oneOf branch helpers (e.g. `_af1_b0`) so the
1558
- // bundle output is self-contained. `toStandalone` emits this same source
1559
- // for single-schema standalone output.
1560
- if (jsFn._preambleSource) {
1561
- preamble = preamble ? `${preamble}\n${jsFn._preambleSource}` : jsFn._preambleSource;
1562
- }
1563
- if (opts && opts.verbose) {
1564
- // Embed the schema and a small resolver so errors carry parentSchema.
1565
- const schemaLit = JSON.stringify(typeof schema === 'string' ? JSON.parse(schema) : schema);
1566
- 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)`;
1567
- }
1568
- return `(function(R){${preamble}var E=function(d){var _all=true;${errBody}};return function(d){${jsFn._hybridSource}}})(R)`;
1569
- });
1570
- const arr = `[${fns.join(",")}]`;
1571
- const safeEmbed = bundleUsesSafeRe ? getSafeRegexEmbed() + "\n" : "";
1572
- if (format === 'esm') {
1573
- return `// Auto-generated by ata-validator — do not edit\n${safeEmbed}const R=${R};\nconst validators=${arr};\nexport default validators;\nexport { validators };\n`;
1574
- }
1575
- return `'use strict';\n${safeEmbed}var R=${R};\nmodule.exports=[${fns.join(",")}];\n`;
1340
+ return require('./lib/aot').bundleStandalone(Validator, schemas, opts);
1576
1341
  };
1577
1342
 
1578
- // Compact bundle: deduplicated code. Shared template functions + per-schema params.
1579
- // Much smaller file → faster V8 parse → faster startup.
1580
- // opts.format: 'cjs' (default) or 'esm'.
1581
1343
  Validator.bundleCompact = function (schemas, opts) {
1582
- const haveIds = schemas.some((s) => s && typeof s === 'object' && s.$id);
1583
- const bundleOpts = haveIds ? { ...(opts || {}), schemas } : (opts || {});
1584
- const format = (opts && opts.format) || 'cjs';
1585
- let bundleUsesSafeRe = false;
1586
- // Analyze schemas and group by structure
1587
- const entries = schemas.map((schema) => {
1588
- const v = new Validator(schema, bundleOpts);
1589
- v._ensureCompiled();
1590
- const jsFn = v._jsFn;
1591
- if (!jsFn || !jsFn._hybridSource) return null;
1592
- const jsErrFn = compileToJSCodegenWithErrors(
1593
- typeof schema === "string" ? JSON.parse(schema) : schema,
1594
- v._schemaMap,
1595
- v._userFormats,
1596
- );
1597
- if (jsFn._usesSafeRe || (jsErrFn && jsErrFn._usesSafeRe)) bundleUsesSafeRe = true;
1598
- // Hoisted anyOf/oneOf branch helpers (e.g. `_af1_b0`) must travel with the
1599
- // hybrid body or it references undefined names. Prepending keeps dedup honest:
1600
- // schemas with different branch sets no longer collide on body alone.
1601
- const hybrid = jsFn._preambleSource
1602
- ? `${jsFn._preambleSource}\n${jsFn._hybridSource}`
1603
- : jsFn._hybridSource;
1604
- return {
1605
- hybrid,
1606
- err: jsErrFn && jsErrFn._errSource ? jsErrFn._errSource : null,
1607
- fmt: jsFn._formatClosures || null,
1608
- };
1609
- });
1610
-
1611
- // Deduplicate function bodies — many schemas produce identical or near-identical code
1612
- const bodyMap = new Map(); // body → index
1613
- const bodies = [];
1614
- const errMap = new Map();
1615
- const errBodies = [];
1616
-
1617
- const indices = entries.map((e) => {
1618
- if (!e) return [-1, -1];
1619
- let hi = bodyMap.get(e.hybrid);
1620
- if (hi === undefined) {
1621
- hi = bodies.length;
1622
- bodies.push(e.hybrid);
1623
- bodyMap.set(e.hybrid, hi);
1624
- }
1625
- let ei = -1;
1626
- if (e.err) {
1627
- ei = errMap.get(e.err);
1628
- if (ei === undefined) {
1629
- ei = errBodies.length;
1630
- errBodies.push(e.err);
1631
- errMap.set(e.err, ei);
1632
- }
1633
- }
1634
- return [hi, ei];
1635
- });
1636
-
1637
- // Generate compact bundle
1638
- const isEsm = format === 'esm';
1639
- let out = isEsm
1640
- ? "// Auto-generated by ata-validator — do not edit\n"
1641
- : "'use strict';\n";
1642
- if (bundleUsesSafeRe) out += getSafeRegexEmbed() + "\n";
1643
- const declKW = isEsm ? "const" : "var";
1644
- out += `${declKW} R=Object.freeze({valid:true,errors:Object.freeze([])});\n`;
1645
-
1646
- // User format functions are referenced as _uf_<name> by the hybrid and error
1647
- // bodies. Collect them across all schemas (deduped by name) and embed via
1648
- // Function#toString so the bundle stays self-contained.
1649
- const fmtSeen = new Set();
1650
- for (const e of entries) {
1651
- if (!e || !e.fmt) continue;
1652
- for (const { name, fn } of e.fmt) {
1653
- if (fmtSeen.has(name)) continue;
1654
- fmtSeen.add(name);
1655
- out += `${declKW} ${name}=${fn.toString()};\n`;
1656
- }
1657
- }
1658
-
1659
- // Shared hybrid factories
1660
- out += `${declKW} H=[\n`;
1661
- out += bodies
1662
- .map((b) => `function(R,E){return function(d){${b}}}`)
1663
- .join(",\n");
1664
- out += "\n];\n";
1665
-
1666
- // Shared error functions
1667
- out += `${declKW} EF=[\n`;
1668
- out += errBodies.map((b) => `function(d){var _all=true;${b}}`).join(",\n");
1669
- out += "\n];\n";
1670
-
1671
- // Build validators from shared templates
1672
- const arrBody = indices
1673
- .map(([hi, ei]) => {
1674
- if (hi < 0) return "null";
1675
- if (ei >= 0) return `H[${hi}](R,EF[${ei}])`;
1676
- return `H[${hi}](R,function(){return{valid:false,errors:[]}})`;
1677
- })
1678
- .join(",");
1679
- if (isEsm) {
1680
- out += `const validators=[${arrBody}];\nexport default validators;\nexport { validators };\n`;
1681
- } else {
1682
- out += `module.exports=[${arrBody}];\n`;
1683
- }
1684
-
1685
- return out;
1344
+ return require('./lib/aot').bundleCompact(Validator, schemas, opts);
1686
1345
  };
1687
1346
 
1688
1347
  Validator.loadBundle = function (mods, schemas, opts) {
1689
- return schemas.map((schema, i) => {
1690
- if (mods[i]) return Validator.fromStandalone(mods[i], schema, opts);
1691
- return new Validator(schema, opts);
1692
- });
1348
+ return require('./lib/aot').loadBundle(Validator, mods, schemas, opts);
1693
1349
  };
1694
1350
 
1695
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
+ };