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 +38 -0
- package/build.d.ts +28 -0
- package/build.mjs +3 -0
- package/index.d.ts +8 -2
- package/index.js +26 -370
- package/lib/aot-build.js +30 -1
- package/lib/aot.js +410 -0
- package/lib/native-load.browser.js +8 -0
- package/lib/native-load.js +21 -0
- package/lib/safe-regex-source.js +8 -0
- package/lib/t.js +140 -0
- package/lib/version.js +10 -0
- package/package.json +14 -5
- package/scripts/regen-safe-regex-source.js +33 -0
- package/t.d.ts +186 -0
- package/t.js +9 -0
- package/t.mjs +6 -0
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
|
-
:
|
|
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
|
-
|
|
4
|
-
|
|
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
|
-
//
|
|
291
|
-
//
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
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
|
-
// ---
|
|
1164
|
-
//
|
|
1165
|
-
//
|
|
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
|
-
|
|
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
|
-
|
|
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("./
|
|
1327
|
+
try { return require("./lib/version"); } catch { return "unknown"; }
|
|
1500
1328
|
}
|
|
1501
1329
|
|
|
1502
|
-
//
|
|
1503
|
-
//
|
|
1504
|
-
//
|
|
1505
|
-
//
|
|
1506
|
-
//
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
+
};
|