ata-validator 0.12.6 → 0.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 +30 -0
- package/README.md +43 -139
- package/bin/ata.js +105 -4
- package/build.d.ts +63 -0
- package/build.js +6 -0
- package/build.mjs +8 -0
- package/index.js +27 -1
- package/lib/aot-build.js +210 -0
- package/lib/js-compiler.js +23 -2
- package/package.json +22 -3
- package/prebuilds/ata-darwin-arm64/node-napi-v10.node +0 -0
- package/prebuilds/darwin-arm64/ata-validator.node +0 -0
- package/prebuilds/ata-linux-arm64/node-napi-v10.node +0 -0
- package/prebuilds/ata-linux-arm64-musl/node-napi-v10.node +0 -0
- package/prebuilds/ata-linux-x64/node-napi-v10.node +0 -0
- package/prebuilds/ata-linux-x64-musl/node-napi-v10.node +0 -0
- package/prebuilds/ata-win32-x64/node-napi-v10.node +0 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
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
|
+
|
|
5
|
+
## 0.13.1 — 2026-05-09
|
|
6
|
+
|
|
7
|
+
### Fixed
|
|
8
|
+
|
|
9
|
+
- **Custom format checkers in `validate()`** are now actually applied. The combined codegen path (used by `Validator#validate` and one-shot `validate()`) silently dropped the `userFormats` argument, so schemas with `format: <user-defined>` returned `{ valid: true }` regardless of the checker function's return value. The boolean (`isValidObject`) and error-only paths were already wired correctly. Fixes RJSF integration where custom formats are routed through `customFormats` (rjsf-team/react-jsonschema-form#5052).
|
|
10
|
+
- **Glob patterns with backslash separators on Windows** now resolve correctly in `ata build`. The Node 18 fallback regex only recognized forward slashes, so `path.join(dir, '*.json')` produced patterns the matcher couldn't parse on `windows-latest` runners.
|
|
11
|
+
|
|
12
|
+
## 0.13.0 — 2026-05-09
|
|
13
|
+
|
|
14
|
+
### Added
|
|
15
|
+
|
|
16
|
+
- **`ata build <glob>`** subcommand for project-wide AOT compilation. Compiles each matched schema to a per-file `.compiled.mjs` ESM module with a sibling `.d.mts` TypeScript declaration. Production bundles can drop the runtime ata-validator dependency entirely and import compiled validators as plain ESM modules.
|
|
17
|
+
- **`ata-validator/build` programmatic subpath export.** `import { build, watch } from 'ata-validator/build'` exposes the same engine the CLI uses, so build pipelines and bundler plugins can integrate without going through the CLI.
|
|
18
|
+
- **CLI flags for `ata build`:** `--out-dir`, `--suffix`, `--format esm|cjs`, `--abort-early`, `--no-types`, `--cache-file`, `--check`, `--watch`, `--max-size`, `--strict`.
|
|
19
|
+
- **Incremental cache** via content-hashed `--cache-file`. Second run on unchanged inputs skips compilation.
|
|
20
|
+
- **YAML schema support** when the `yaml` peer dependency is installed (optional). `.yaml` and `.yml` inputs parse the same as `.json`.
|
|
21
|
+
- **AOT vs AJV-runtime benchmark** at `benchmark/bench_aot_vs_ajv.mjs`. On the included fixtures, ata-AOT outputs are 25-56x smaller gzipped than the AJV runtime, cold start is ~2x faster, throughput is 2-4.5x faster, and compile time is 71-246x shorter.
|
|
22
|
+
|
|
23
|
+
### Fixed
|
|
24
|
+
|
|
25
|
+
- **Standalone modules now correctly serialize closure-bound helpers** (RegExp, Set, sub-validator functions, branch-property arrays) into the emitted `.mjs`. Previously, schemas using `patternProperties`, `propertyNames` with regex, or `unevaluatedProperties` with `anyOf`/`oneOf` produced standalone output that referenced undefined variables (`_ppf0_0`, `_re*`, `_es*`, `_bk*`) and threw `ReferenceError` at runtime. The runtime validation path was unaffected.
|
|
26
|
+
|
|
27
|
+
### Notes
|
|
28
|
+
|
|
29
|
+
- The runtime `Validator` API and the `ata-validator/compat` AJV-shim remain unchanged. Existing dynamic-schema users have no migration to do.
|
|
30
|
+
- Bundler plugins (ata-vite v0.2.0, ata-webpack, ata-codemod-ajv) are out of scope for this release and will land in 0.14.0+.
|
package/README.md
CHANGED
|
@@ -1,161 +1,57 @@
|
|
|
1
|
-
<p align="center">
|
|
2
|
-
<img src="./assets/ata-validator.svg" alt="ata-validator" width="640" />
|
|
3
|
-
</p>
|
|
4
|
-
|
|
5
1
|
# ata-validator
|
|
6
2
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
**[ata-validator.com](https://ata-validator.com)** | **[API Docs](docs/API.md)** | **[Migrate from ajv](docs/migration-from-ajv.md)** | **[Framework integrations](docs/integrations/)** | **[Contributing](CONTRIBUTING.md)**
|
|
10
|
-
|
|
11
|
-
## Performance
|
|
12
|
-
|
|
13
|
-
### Simple Schema (7 properties, type + format + range + nested object)
|
|
14
|
-
|
|
15
|
-
| Scenario | ata | ajv | |
|
|
16
|
-
|---|---|---|---|
|
|
17
|
-
| **validate(obj)** valid | 21ns | 108ns | **ata 5.1x faster** |
|
|
18
|
-
| **validate(obj)** invalid | 86ns | 104ns | **ata 1.2x faster** |
|
|
19
|
-
| **isValidObject(obj)** | 20ns | 109ns | **ata 5.4x faster** |
|
|
20
|
-
| **Schema instantiation** (lazy compile) | 8ns | 1.33ms | **ata 159,000x faster** |
|
|
21
|
-
| **First validation** (compile + validate) | 28ns | 1.21ms | **ata 43,000x faster** |
|
|
22
|
-
|
|
23
|
-
> **Honest read of the three rows above:**
|
|
24
|
-
>
|
|
25
|
-
> - **Hot loop** (millions of `validate(obj)` calls on a warm validator): ata is **~5× faster** than ajv. This is the steady-state advantage and what most apps care about most of the time.
|
|
26
|
-
> - **Cold start** (construct + first validate, apples-to-apples vs `ajv.compile(schema) + validate(obj)`): ata is **~43,000× faster**. Matters for serverless cold starts, CLI tools, batch workers — anywhere you instantiate a schema and exercise it once or a few times.
|
|
27
|
-
> - **Instantiation only** (`new Validator(schema)` with no validation yet): ata is **~159,000× faster**, but only because ata defers codegen to first use (lazy compile + a tier-0 interpreter for low-traffic schemas). The number is real but it is constructor cost vs ajv's full compile cost — not the same unit of work. Quote it carefully.
|
|
28
|
-
>
|
|
29
|
-
> The lazy compile architecture is also why an instantiated-but-never-validated schema is essentially free in ata, while in ajv it costs the full compile. That's the underlying real win, beyond the multiplier above.
|
|
30
|
-
|
|
31
|
-
### Complex Schema (patternProperties + dependentSchemas + propertyNames + additionalProperties)
|
|
32
|
-
|
|
33
|
-
| Scenario | ata | ajv | |
|
|
34
|
-
|---|---|---|---|
|
|
35
|
-
| **validate(obj)** valid | 19ns | 116ns | **ata 6.1x faster** |
|
|
36
|
-
| **validate(obj)** invalid | 62ns | 195ns | **ata 3.1x faster** |
|
|
37
|
-
| **isValidObject(obj)** | 18ns | 122ns | **ata 6.8x faster** |
|
|
38
|
-
|
|
39
|
-
### Cross-Schema `$ref` (multi-schema with `$id` registry)
|
|
40
|
-
|
|
41
|
-
| Scenario | ata | ajv | |
|
|
42
|
-
|---|---|---|---|
|
|
43
|
-
| **validate(obj)** valid | 13ns | 25ns | **ata 2.0x faster** |
|
|
44
|
-
| **validate(obj)** invalid | 28ns | 56ns | **ata 2.0x faster** |
|
|
45
|
-
|
|
46
|
-
> Measured with [mitata](https://github.com/evanwashere/mitata) on Apple M4 Pro (process-isolated). [Benchmark code](benchmark/bench_complex_mitata.mjs)
|
|
47
|
-
|
|
48
|
-
### unevaluatedProperties / unevaluatedItems
|
|
49
|
-
|
|
50
|
-
| Scenario | ata | ajv | |
|
|
51
|
-
|---|---|---|---|
|
|
52
|
-
| **Tier 1** (properties only) valid | 3.3ns | 8.5ns | **ata 2.6x faster** |
|
|
53
|
-
| **Tier 1** invalid | 3.6ns | 18.6ns | **ata 5.2x faster** |
|
|
54
|
-
| **Tier 2** (allOf) valid | 3.3ns | 10.1ns | **ata 3.0x faster** |
|
|
55
|
-
| **Tier 3** (anyOf) valid | 6.7ns | 22.9ns | **ata 3.4x faster** |
|
|
56
|
-
| **Tier 3** invalid | 7.5ns | 41.8ns | **ata 5.6x faster** |
|
|
57
|
-
| **unevaluatedItems** valid | 0.97ns | 5.4ns | **ata 5.6x faster** |
|
|
58
|
-
| **unevaluatedItems** invalid | 0.99ns | 14.9ns | **ata 15.0x faster** |
|
|
59
|
-
| **Compilation** | 8.8ns | 2.64ms | **ata 298,000x faster** |
|
|
60
|
-
|
|
61
|
-
Three-tier hybrid codegen: static schemas compile to zero-overhead key checks, dynamic schemas (anyOf/oneOf) use bitmask tracking with V8-inlined branch functions. [Benchmark code](benchmark/bench_unevaluated_mitata.mjs)
|
|
62
|
-
|
|
63
|
-
### vs Ecosystem (Zod, Valibot, TypeBox)
|
|
64
|
-
|
|
65
|
-
| Scenario | ata | ajv | typebox | zod | valibot |
|
|
66
|
-
|---|---|---|---|---|---|
|
|
67
|
-
| **validate (valid)** | **7ns** | 38ns | 50ns | 342ns | 337ns |
|
|
68
|
-
| **validate (invalid, all errors)** | **38ns** | 102ns | n/a | 11.9μs | 855ns |
|
|
69
|
-
| **isValid (invalid, boolean)** | **0.93ns** | 16ns | 2.3ns | n/a | n/a |
|
|
70
|
-
| **compilation** | **9ns** | 1.20ms | 53μs | n/a | n/a |
|
|
71
|
-
| **first validation** | **16ns** | 1.16ms | 54μs | n/a | n/a |
|
|
72
|
-
|
|
73
|
-
> Different categories: ata/ajv/typebox are JSON Schema validators, zod/valibot are schema-builder DSLs. The two invalid-path rows compare different units of work — `validate(invalid, all errors)` walks the full schema and builds an errors array (apples-to-apples vs ajv `{allErrors: true}`), while `isValid(invalid, boolean)` returns false on the first failed check (apples-to-apples vs typebox `Check()` and ajv `{allErrors: false}`). Reading both rows together avoids the trap of comparing a full error walk against a first-fail boolean. [Benchmark code](benchmark/bench_all_mitata.mjs)
|
|
74
|
-
|
|
75
|
-
### Large Data - JS Object Validation
|
|
76
|
-
|
|
77
|
-
| Size | ata | ajv | |
|
|
78
|
-
|---|---|---|---|
|
|
79
|
-
| 10 users (2KB) | 6.0M ops/sec | 2.4M ops/sec | **ata 2.5x faster** |
|
|
80
|
-
| 100 users (20KB) | 621K ops/sec | 229K ops/sec | **ata 2.7x faster** |
|
|
81
|
-
| 1,000 users (205KB) | 63K ops/sec | 22.5K ops/sec | **ata 2.8x faster** |
|
|
3
|
+
Compile JSON Schema files into per-schema ESM modules at build time. Drop the runtime validator from your production bundle. Optional runtime API for dynamic schemas.
|
|
82
4
|
|
|
83
|
-
|
|
5
|
+
[](https://www.npmjs.com/package/ata-validator)
|
|
6
|
+
[](LICENSE)
|
|
84
7
|
|
|
85
|
-
|
|
86
|
-
|---|---|---|---|
|
|
87
|
-
| **Serverless cold start** (50 schemas) | 0.087ms | 3.67ms | **ata 42x faster** |
|
|
88
|
-
| **ReDoS protection** (`^(a+)+$`) | 0.3ms | 765ms | **ata immune (RE2)** |
|
|
89
|
-
| **Batch NDJSON** (10K items, multi-core) | 13.4M/sec | 5.1M/sec | **ata 2.6x faster** |
|
|
90
|
-
| **Fastify startup** (5 routes) | 0.5ms | 6.0ms | **ata 12x faster** |
|
|
8
|
+
## Quick start
|
|
91
9
|
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
**Combined single-pass validator**: ata compiles schemas into a single function that validates and collects errors in one pass. Valid data returns `VALID_RESULT` with zero allocation. Invalid data collects errors inline with pre-allocated frozen error objects - no double validation, no try/catch (3.3x V8 deopt). Lazy compilation defers all work to first usage - constructor is near-zero cost.
|
|
97
|
-
|
|
98
|
-
**JS codegen**: Schemas are compiled to monolithic JS functions (like ajv). Full keyword support including `patternProperties`, `dependentSchemas`, `propertyNames`, `unevaluatedProperties`, `unevaluatedItems`, cross-schema `$ref` with `$id` registry, and Draft 7 auto-detection. Three-tier hybrid approach for unevaluated keywords: compile-time resolution for static schemas, bitmask tracking for dynamic ones. charCodeAt prefix matching replaces regex for simple patterns (4x faster). Merged key iteration loops (patternProperties + propertyNames + additionalProperties in a single `for..in`).
|
|
99
|
-
|
|
100
|
-
**V8 TurboFan optimizations**: Destructuring batch reads, `undefined` checks instead of `in` operator, context-aware type guard elimination, property hoisting to local variables, tiered uniqueItems (nested loop for small arrays), inline key comparison for small property sets (no Set.has overhead).
|
|
101
|
-
|
|
102
|
-
**Adaptive simdjson**: For large documents (>8KB) with selective schemas, simdjson On Demand seeks only the needed fields - skipping irrelevant data at GB/s speeds.
|
|
103
|
-
|
|
104
|
-
### $dynamicRef / $dynamicAnchor / $anchor
|
|
10
|
+
```bash
|
|
11
|
+
npm install --save-dev ata-validator
|
|
12
|
+
npx ata build 'schemas/*.json' --out-dir src/generated
|
|
13
|
+
```
|
|
105
14
|
|
|
106
|
-
|
|
107
|
-
|---|---|---|---|
|
|
108
|
-
| **$dynamicRef tree** valid | 22ns | 54ns | **ata 2.4x faster** |
|
|
109
|
-
| **$dynamicRef tree** invalid | 71ns | 77ns | **ata 1.1x faster** |
|
|
110
|
-
| **$dynamicRef override** valid | 2.6ns | 187ns | **ata 71x faster** |
|
|
111
|
-
| **$dynamicRef override** invalid | 50ns | 189ns | **ata 3.8x faster** |
|
|
112
|
-
| **$anchor array** valid | 2.2ns | 3.2ns | **ata 1.4x faster** |
|
|
15
|
+
In your code:
|
|
113
16
|
|
|
114
|
-
|
|
17
|
+
```ts
|
|
18
|
+
import { validate, isValid, type User } from './generated/user.compiled.mjs'
|
|
115
19
|
|
|
116
|
-
|
|
20
|
+
if (isValid(req.body)) {
|
|
21
|
+
const user: User = req.body
|
|
22
|
+
// ...
|
|
23
|
+
}
|
|
24
|
+
```
|
|
117
25
|
|
|
118
|
-
|
|
26
|
+
The `.compiled.mjs` modules are self-contained: zero runtime dependency on ata-validator, fully tree-shakeable, with TypeScript types emitted alongside.
|
|
119
27
|
|
|
120
|
-
##
|
|
28
|
+
## Why AOT
|
|
121
29
|
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
- **Standard Schema V1** - native support for Fastify v5, tRPC, TanStack
|
|
130
|
-
- **C/C++ embedding** - native library, no JS runtime needed
|
|
30
|
+
| Dimension | Schema | ata-AOT | AJV-runtime | Difference |
|
|
31
|
+
|---|---|---|---|---|
|
|
32
|
+
| Bundle (gzipped) | simple | 955 B | 52.7 KB | 56x smaller |
|
|
33
|
+
| Bundle (gzipped) | complex | 1.6 KB | 52.7 KB | 32x smaller |
|
|
34
|
+
| Cold start | simple | 21 ms | 38 ms | 1.8x faster |
|
|
35
|
+
| Throughput (10M ops) | simple | 345 Mops/s | 116 Mops/s | 3.0x faster |
|
|
36
|
+
| Compile time | simple | 6 µs | 1.5 ms | 246x faster |
|
|
131
37
|
|
|
132
|
-
|
|
38
|
+
Reproduce on your machine with `npm run bench:aot-vs-ajv`. Numbers measured on Apple M4 Pro, Node 25.2.1.
|
|
133
39
|
|
|
134
|
-
|
|
135
|
-
- **Full unevaluatedProperties/Items** - ata covers most cases but some edge cases remain
|
|
40
|
+
The wins are largest on bundle size and compile time because AOT moves work from runtime to build time. Throughput and cold start are also faster because the compiled validator is a tight straight-line function with no schema-walk overhead.
|
|
136
41
|
|
|
137
|
-
##
|
|
42
|
+
## When to use the runtime API instead
|
|
138
43
|
|
|
139
|
-
|
|
140
|
-
- **$dynamicRef / $dynamicAnchor / $anchor**: Full Draft 2020-12 dynamic reference support. Self-recursive named functions, compile-time cross-schema resolution (42/42 spec tests)
|
|
141
|
-
- **Cross-schema `$ref`**: `schemas` option and `addSchema()` API. Compile-time resolution with `$id` registry, zero runtime overhead
|
|
142
|
-
- **Draft 7 support**: Auto-detects `$schema` field, normalizes `dependencies`/`additionalItems`/`definitions` transparently
|
|
143
|
-
- **Multi-core**: Parallel validation across all CPU cores - 13.4M validations/sec
|
|
144
|
-
- **simdjson**: SIMD-accelerated JSON parsing at GB/s speeds, adaptive On Demand for large docs
|
|
145
|
-
- **RE2 regex**: Linear-time guarantees, immune to ReDoS attacks (2391x faster on pathological input)
|
|
146
|
-
- **V8-optimized codegen**: Destructuring batch reads, type guard elimination, property hoisting
|
|
147
|
-
- **Standard Schema V1**: Compatible with Fastify, tRPC, TanStack, Drizzle
|
|
148
|
-
- **Zero-copy paths**: Buffer and pre-padded input support - no unnecessary copies
|
|
149
|
-
- **Defaults + coercion**: `default` values, `coerceTypes`, `removeAdditional` support
|
|
150
|
-
- **C/C++ library**: Native API for non-Node.js environments
|
|
151
|
-
- **98.5% spec compliant**: Draft 2020-12
|
|
44
|
+
`ata build` is for schemas you know at build time. If your schemas are user-supplied at runtime (form builders, no-code platforms, dynamic API ingestion), use the runtime API:
|
|
152
45
|
|
|
153
|
-
|
|
46
|
+
```js
|
|
47
|
+
import { Validator } from 'ata-validator'
|
|
154
48
|
|
|
155
|
-
|
|
156
|
-
|
|
49
|
+
const v = new Validator(schema)
|
|
50
|
+
const result = v.validate(data)
|
|
157
51
|
```
|
|
158
52
|
|
|
53
|
+
The runtime API is unchanged from previous releases. AJV-shim users continue importing from `ata-validator/compat`.
|
|
54
|
+
|
|
159
55
|
## Usage
|
|
160
56
|
|
|
161
57
|
### Node.js
|
|
@@ -265,6 +161,14 @@ CLI options:
|
|
|
265
161
|
| `--abort-early` | off | Generate the stub-error variant (~0.5 KB gzipped) |
|
|
266
162
|
| `--no-types` | off | Skip the `.d.mts` / `.d.cts` output |
|
|
267
163
|
|
|
164
|
+
For a project with many schemas, `ata build <glob>` compiles them all in one command:
|
|
165
|
+
|
|
166
|
+
```bash
|
|
167
|
+
npx ata build 'schemas/*.json' --out-dir build/validators --check
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
Run with `--watch` during development for incremental rebuilds.
|
|
171
|
+
|
|
268
172
|
Typical bundle sizes (10-field user schema, gzipped):
|
|
269
173
|
|
|
270
174
|
| Variant | Size | Notes |
|
package/bin/ata.js
CHANGED
|
@@ -8,20 +8,34 @@ function usage() {
|
|
|
8
8
|
process.stdout.write(`ata-validator CLI
|
|
9
9
|
|
|
10
10
|
Usage:
|
|
11
|
-
ata compile <schema-file> [options]
|
|
11
|
+
ata compile <schema-file> [options] Compile one schema to a standalone module.
|
|
12
|
+
ata build <glob>... [options] Compile a project's schemas (glob pattern) per file.
|
|
12
13
|
|
|
13
|
-
|
|
14
|
+
Compile options:
|
|
14
15
|
-o, --output <file> Output path. Default: <schema-file>.validator.mjs
|
|
15
16
|
-f, --format <fmt> Module format: esm | cjs. Default: esm
|
|
16
17
|
--name <TypeName> Name of the top-level type in .d.ts. Default: inferred from filename
|
|
17
18
|
--no-types Skip .d.ts generation
|
|
18
19
|
--abort-early Use stub errors (smallest bundle)
|
|
20
|
+
|
|
21
|
+
Build options:
|
|
22
|
+
--out-dir <dir> Write outputs into this directory instead of alongside sources
|
|
23
|
+
--suffix <str> Output filename suffix (default: ".compiled")
|
|
24
|
+
-f, --format <fmt> Module format: esm | cjs. Default: esm
|
|
25
|
+
--abort-early Use stub errors (smallest bundle)
|
|
26
|
+
--check Check (don't write); exit 1 if any output is stale
|
|
27
|
+
--cache-file <path> Cache file for incremental builds (default: cache disabled)
|
|
28
|
+
--max-size <bytes> Fail build if any compiled module exceeds this gzipped size
|
|
29
|
+
--strict Treat any AOT-incompatible schema as a build error (default: skip + warn)
|
|
30
|
+
--watch Re-emit on schema change (Ctrl-C to exit)
|
|
31
|
+
--no-types Skip .d.mts/.d.cts emission alongside compiled modules
|
|
32
|
+
|
|
19
33
|
-h, --help Show this message
|
|
20
34
|
|
|
21
35
|
Examples:
|
|
22
36
|
ata compile schemas/user.json -o src/generated/user.validator.mjs
|
|
23
|
-
ata
|
|
24
|
-
ata
|
|
37
|
+
ata build 'schemas/*.json'
|
|
38
|
+
ata build 'src/**/*.schema.json' --out-dir build/validators
|
|
25
39
|
`);
|
|
26
40
|
}
|
|
27
41
|
|
|
@@ -35,6 +49,21 @@ function parseArgs(argv) {
|
|
|
35
49
|
if (a === '--name') { out.opts.name = argv[++i]; continue; }
|
|
36
50
|
if (a === '--no-types') { out.opts.types = false; continue; }
|
|
37
51
|
if (a === '--abort-early') { out.opts.abortEarly = true; continue; }
|
|
52
|
+
if (a === '--check') { out.opts.check = true; continue; }
|
|
53
|
+
if (a === '--strict') { out.opts.strict = true; continue; }
|
|
54
|
+
if (a === '--out-dir') { out.opts.outDir = argv[++i]; continue; }
|
|
55
|
+
if (a === '--suffix') { out.opts.suffix = argv[++i]; continue; }
|
|
56
|
+
if (a === '--cache-file') { out.opts.cacheFile = argv[++i]; continue; }
|
|
57
|
+
if (a === '--max-size') {
|
|
58
|
+
const v = argv[++i];
|
|
59
|
+
const n = Number(v);
|
|
60
|
+
if (!Number.isFinite(n) || n <= 0 || !Number.isInteger(n)) {
|
|
61
|
+
throw new Error(`--max-size requires a positive integer (got "${v}")`);
|
|
62
|
+
}
|
|
63
|
+
out.opts.maxSize = n;
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
66
|
+
if (a === '--watch') { out.opts.watch = true; continue; }
|
|
38
67
|
if (a.startsWith('-')) { throw new Error(`Unknown option: ${a}`); }
|
|
39
68
|
out._.push(a);
|
|
40
69
|
}
|
|
@@ -113,6 +142,73 @@ function cmdCompile(args) {
|
|
|
113
142
|
}
|
|
114
143
|
}
|
|
115
144
|
|
|
145
|
+
function cmdBuild(args) {
|
|
146
|
+
if (args._.length === 0) {
|
|
147
|
+
process.stderr.write('error: missing <glob>\n\n');
|
|
148
|
+
usage();
|
|
149
|
+
process.exit(1);
|
|
150
|
+
}
|
|
151
|
+
const buildLib = require('../lib/aot-build');
|
|
152
|
+
const format = args.opts.format || 'esm';
|
|
153
|
+
if (format !== 'esm' && format !== 'cjs') {
|
|
154
|
+
process.stderr.write(`error: --format must be esm or cjs (got "${format}")\n`);
|
|
155
|
+
process.exit(1);
|
|
156
|
+
}
|
|
157
|
+
const buildOpts = {
|
|
158
|
+
globs: args._,
|
|
159
|
+
format,
|
|
160
|
+
outDir: args.opts.outDir,
|
|
161
|
+
suffix: args.opts.suffix,
|
|
162
|
+
abortEarly: !!args.opts.abortEarly,
|
|
163
|
+
check: !!args.opts.check,
|
|
164
|
+
maxSize: args.opts.maxSize,
|
|
165
|
+
strict: !!args.opts.strict,
|
|
166
|
+
types: args.opts.types,
|
|
167
|
+
cacheFile: args.opts.cacheFile,
|
|
168
|
+
};
|
|
169
|
+
|
|
170
|
+
const printReport = (report) => {
|
|
171
|
+
if (args.opts.check) {
|
|
172
|
+
process.stdout.write(`ata: check — ${report.cached.length} up to date, ${report.staleCount} stale\n`);
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
for (const c of report.compiled) {
|
|
176
|
+
process.stdout.write(`ata: ${c.input} -> ${c.output} (${c.bytes.toLocaleString()} bytes)\n`);
|
|
177
|
+
}
|
|
178
|
+
for (const c of report.cached) {
|
|
179
|
+
process.stdout.write(`ata: cached ${c.input}\n`);
|
|
180
|
+
}
|
|
181
|
+
for (const s of report.skipped) {
|
|
182
|
+
process.stdout.write(`ata: skipped ${s.input}: ${s.reason}\n`);
|
|
183
|
+
}
|
|
184
|
+
for (const f of report.failed) {
|
|
185
|
+
process.stderr.write(`ata: failed ${f.input}: ${f.error}\n`);
|
|
186
|
+
}
|
|
187
|
+
};
|
|
188
|
+
|
|
189
|
+
if (args.opts.watch) {
|
|
190
|
+
let handle;
|
|
191
|
+
buildLib.watch(buildOpts, printReport).then((h) => {
|
|
192
|
+
handle = h;
|
|
193
|
+
process.stdout.write('ata: watching for changes (Ctrl-C to exit)\n');
|
|
194
|
+
});
|
|
195
|
+
process.on('SIGINT', () => {
|
|
196
|
+
if (handle) handle.close();
|
|
197
|
+
process.exit(0);
|
|
198
|
+
});
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
buildLib.build(buildOpts).then((report) => {
|
|
203
|
+
printReport(report);
|
|
204
|
+
if (args.opts.check && report.staleCount > 0) process.exit(1);
|
|
205
|
+
if (report.failed.length > 0) process.exit(1);
|
|
206
|
+
}).catch((e) => {
|
|
207
|
+
process.stderr.write(`error: ${e.message}\n`);
|
|
208
|
+
process.exit(1);
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
|
|
116
212
|
function main() {
|
|
117
213
|
const argv = process.argv.slice(2);
|
|
118
214
|
if (argv.length === 0) { usage(); process.exit(0); }
|
|
@@ -136,6 +232,11 @@ function main() {
|
|
|
136
232
|
return;
|
|
137
233
|
}
|
|
138
234
|
|
|
235
|
+
if (cmd === 'build') {
|
|
236
|
+
cmdBuild(args);
|
|
237
|
+
return;
|
|
238
|
+
}
|
|
239
|
+
|
|
139
240
|
process.stderr.write(`error: unknown command "${cmd}"\n\n`);
|
|
140
241
|
usage();
|
|
141
242
|
process.exit(1);
|
package/build.d.ts
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
export interface BuildOptions {
|
|
2
|
+
/** Glob patterns to expand into input schema files. */
|
|
3
|
+
globs: string[];
|
|
4
|
+
/** Module format for compiled outputs. Default: 'esm'. */
|
|
5
|
+
format?: 'esm' | 'cjs';
|
|
6
|
+
/** Write outputs into this directory instead of alongside sources. */
|
|
7
|
+
outDir?: string;
|
|
8
|
+
/** Output filename suffix. Default: '.compiled'. */
|
|
9
|
+
suffix?: string;
|
|
10
|
+
/** Use stub error functions for the smallest output. Default: false. */
|
|
11
|
+
abortEarly?: boolean;
|
|
12
|
+
/** Path to incremental cache file. Default: cache disabled. */
|
|
13
|
+
cacheFile?: string;
|
|
14
|
+
/** When true, do not write outputs; only report stale count. */
|
|
15
|
+
check?: boolean;
|
|
16
|
+
/** Maximum gzipped output size per compiled module, in bytes. */
|
|
17
|
+
maxSize?: number;
|
|
18
|
+
/** When true, AOT-incompatible schemas become failures (default: skipped). */
|
|
19
|
+
strict?: boolean;
|
|
20
|
+
/** Emit a .d.mts/.d.cts/.d.ts sibling for each compiled module. Default: true. */
|
|
21
|
+
types?: boolean;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface CompiledEntry {
|
|
25
|
+
input: string;
|
|
26
|
+
output: string;
|
|
27
|
+
bytes: number;
|
|
28
|
+
gzipBytes?: number;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface CachedEntry {
|
|
32
|
+
input: string;
|
|
33
|
+
output: string;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface SkippedEntry {
|
|
37
|
+
input: string;
|
|
38
|
+
reason: string;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export interface FailedEntry {
|
|
42
|
+
input: string;
|
|
43
|
+
error: string;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export interface BuildReport {
|
|
47
|
+
compiled: CompiledEntry[];
|
|
48
|
+
cached: CachedEntry[];
|
|
49
|
+
skipped: SkippedEntry[];
|
|
50
|
+
failed: FailedEntry[];
|
|
51
|
+
/** Only set when opts.check === true. */
|
|
52
|
+
staleCount?: number;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function build(opts: BuildOptions): Promise<BuildReport>;
|
|
56
|
+
export function expandGlobs(globs: string[]): Promise<string[]>;
|
|
57
|
+
export function parseSchemaFile(filePath: string): unknown;
|
|
58
|
+
export function outputPathFor(input: string, opts: { format?: 'esm' | 'cjs'; outDir?: string; suffix?: string }): string;
|
|
59
|
+
|
|
60
|
+
export interface WatchHandle {
|
|
61
|
+
close(): void;
|
|
62
|
+
}
|
|
63
|
+
export function watch(opts: BuildOptions, onReport?: (r: BuildReport) => void): Promise<WatchHandle>;
|
package/build.js
ADDED
package/build.mjs
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import mod from './build.js';
|
|
2
|
+
|
|
3
|
+
export const build = mod.build;
|
|
4
|
+
export const expandGlobs = mod.expandGlobs;
|
|
5
|
+
export const parseSchemaFile = mod.parseSchemaFile;
|
|
6
|
+
export const outputPathFor = mod.outputPathFor;
|
|
7
|
+
export const watch = mod.watch;
|
|
8
|
+
export default mod;
|
package/index.js
CHANGED
|
@@ -964,6 +964,32 @@ module.exports = { boolFn, hybridFactory, errFn };
|
|
|
964
964
|
}
|
|
965
965
|
}
|
|
966
966
|
|
|
967
|
+
// Serialize closure vars referenced in _fn body: regex, sub-validators, sets.
|
|
968
|
+
let closureDecls = '';
|
|
969
|
+
if (jsFn._closures && jsFn._closures.length > 0) {
|
|
970
|
+
const lines = [];
|
|
971
|
+
for (const { name, val } of jsFn._closures) {
|
|
972
|
+
if (Array.isArray(val)) {
|
|
973
|
+
lines.push(`const ${name} = ${JSON.stringify(val)};`);
|
|
974
|
+
continue;
|
|
975
|
+
}
|
|
976
|
+
if (val instanceof RegExp) {
|
|
977
|
+
const flags = val.flags;
|
|
978
|
+
lines.push(`const ${name} = new RegExp(${JSON.stringify(val.source)}${flags ? ', ' + JSON.stringify(flags) : ''});`);
|
|
979
|
+
} else if (val instanceof Set) {
|
|
980
|
+
lines.push(`const ${name} = new Set(${JSON.stringify([...val])});`);
|
|
981
|
+
} else if (typeof val === 'function') {
|
|
982
|
+
// new Function('_ppv', body) — extract body from toString()
|
|
983
|
+
const str = val.toString();
|
|
984
|
+
// Matches: "function anonymous(_ppv\n) {\nbody\n}" or "function(_ppv){body}"
|
|
985
|
+
const m = str.match(/^function[^(]*\([^)]*\)\s*\{([\s\S]*)\}$/)
|
|
986
|
+
const body = m ? m[1].trim() : str;
|
|
987
|
+
lines.push(`const ${name} = function(_ppv) { ${body} };`);
|
|
988
|
+
}
|
|
989
|
+
}
|
|
990
|
+
if (lines.length) closureDecls = lines.join('\n') + '\n';
|
|
991
|
+
}
|
|
992
|
+
|
|
967
993
|
const validBody = errCore
|
|
968
994
|
? 'return _fn(data) ? VALID : { valid: false, errors: errFn(data, true).errors }'
|
|
969
995
|
: 'return _fn(data) ? VALID : ABORT';
|
|
@@ -978,7 +1004,7 @@ module.exports = { boolFn, hybridFactory, errFn };
|
|
|
978
1004
|
${_CP_LEN_SOURCE}
|
|
979
1005
|
const VALID = Object.freeze({ valid: true, errors: Object.freeze([]) });
|
|
980
1006
|
const ABORT = Object.freeze({ valid: false, errors: Object.freeze([Object.freeze({ message: 'validation failed' })]) });
|
|
981
|
-
const _fn = function(d) {
|
|
1007
|
+
${closureDecls}const _fn = function(d) {
|
|
982
1008
|
${src}
|
|
983
1009
|
};
|
|
984
1010
|
${errCore}function isValid(data) { return _fn(data); }
|
package/lib/aot-build.js
ADDED
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const crypto = require('crypto');
|
|
4
|
+
const fs = require('fs');
|
|
5
|
+
const path = require('path');
|
|
6
|
+
const zlib = require('zlib');
|
|
7
|
+
const { Validator } = require('..');
|
|
8
|
+
|
|
9
|
+
async function expandGlobs(globs) {
|
|
10
|
+
const out = [];
|
|
11
|
+
for (const raw of globs) {
|
|
12
|
+
// Glob patterns use forward slashes; normalize Windows backslashes so the
|
|
13
|
+
// matcher (Node 22+ fs.glob or the fallback regex) sees a consistent
|
|
14
|
+
// separator. Node accepts forward slashes in paths on Windows.
|
|
15
|
+
const g = raw.replace(/\\/g, '/');
|
|
16
|
+
if (typeof fs.promises.glob === 'function') {
|
|
17
|
+
// Node 22+
|
|
18
|
+
for await (const f of fs.promises.glob(g)) out.push(path.resolve(f));
|
|
19
|
+
} else {
|
|
20
|
+
// Node 18-21 fallback: simple non-recursive directory + extension match
|
|
21
|
+
// Pattern accepted: '<dir>/*.<ext>' or absolute file path.
|
|
22
|
+
if (fs.existsSync(g) && fs.statSync(g).isFile()) {
|
|
23
|
+
out.push(path.resolve(g));
|
|
24
|
+
continue;
|
|
25
|
+
}
|
|
26
|
+
const m = g.match(/^(.*?)(?:\/\*\.(.+))?$/);
|
|
27
|
+
const dir = m && m[1] ? m[1] : '.';
|
|
28
|
+
const ext = m && m[2] ? '.' + m[2] : null;
|
|
29
|
+
if (!fs.existsSync(dir)) continue;
|
|
30
|
+
for (const entry of fs.readdirSync(dir)) {
|
|
31
|
+
if (ext && !entry.endsWith(ext)) continue;
|
|
32
|
+
const full = path.join(dir, entry);
|
|
33
|
+
if (fs.statSync(full).isFile()) out.push(path.resolve(full));
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
return [...new Set(out)];
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function parseSchemaFile(filePath) {
|
|
41
|
+
const text = fs.readFileSync(filePath, 'utf8');
|
|
42
|
+
const ext = path.extname(filePath).toLowerCase();
|
|
43
|
+
if (ext === '.json') return JSON.parse(text);
|
|
44
|
+
if (ext === '.yaml' || ext === '.yml') {
|
|
45
|
+
let yaml;
|
|
46
|
+
try { yaml = require('yaml'); }
|
|
47
|
+
catch { throw new Error(`install the 'yaml' package to compile YAML schemas (file: ${filePath})`); }
|
|
48
|
+
return yaml.parse(text);
|
|
49
|
+
}
|
|
50
|
+
throw new Error(`unsupported schema extension: ${ext} (file: ${filePath})`);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function outputPathFor(input, opts) {
|
|
54
|
+
const suffix = opts.suffix || '.compiled';
|
|
55
|
+
const ext = opts.format === 'cjs' ? '.cjs' : '.mjs';
|
|
56
|
+
const dir = opts.outDir || path.dirname(input);
|
|
57
|
+
const base = path.basename(input, path.extname(input));
|
|
58
|
+
// Strip a trailing ".schema" for cleaner output names: foo.schema.json -> foo.compiled.mjs
|
|
59
|
+
const stem = base.endsWith('.schema') ? base.slice(0, -('.schema'.length)) : base;
|
|
60
|
+
return path.join(dir, stem + suffix + ext);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function readCache(cacheFile) {
|
|
64
|
+
if (!cacheFile || !fs.existsSync(cacheFile)) return {};
|
|
65
|
+
try { return JSON.parse(fs.readFileSync(cacheFile, 'utf8')); } catch { return {}; }
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function writeCache(cacheFile, data) {
|
|
69
|
+
if (!cacheFile) return;
|
|
70
|
+
fs.mkdirSync(path.dirname(cacheFile), { recursive: true });
|
|
71
|
+
fs.writeFileSync(cacheFile, JSON.stringify(data, null, 2));
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function hashContent(buf) {
|
|
75
|
+
return crypto.createHash('sha256').update(buf).digest('hex').slice(0, 32);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
async function build(opts) {
|
|
79
|
+
const globs = opts.globs || [];
|
|
80
|
+
if (globs.length === 0) throw new Error('build: at least one glob required');
|
|
81
|
+
const format = opts.format || 'esm';
|
|
82
|
+
const inputs = await expandGlobs(globs);
|
|
83
|
+
const cache = readCache(opts.cacheFile);
|
|
84
|
+
const newCache = {};
|
|
85
|
+
|
|
86
|
+
const compiled = [];
|
|
87
|
+
const cached = [];
|
|
88
|
+
const skipped = [];
|
|
89
|
+
const failed = [];
|
|
90
|
+
|
|
91
|
+
for (const input of inputs) {
|
|
92
|
+
try {
|
|
93
|
+
const raw = fs.readFileSync(input);
|
|
94
|
+
const inputHash = hashContent(raw);
|
|
95
|
+
const output = outputPathFor(input, { ...opts, format });
|
|
96
|
+
const cacheEntry = cache[input];
|
|
97
|
+
if (opts.check) {
|
|
98
|
+
const upToDate = (
|
|
99
|
+
cacheEntry &&
|
|
100
|
+
cacheEntry.inputHash === inputHash &&
|
|
101
|
+
cacheEntry.output === output &&
|
|
102
|
+
fs.existsSync(output) &&
|
|
103
|
+
cacheEntry.outputHash === hashContent(fs.readFileSync(output))
|
|
104
|
+
);
|
|
105
|
+
if (upToDate) {
|
|
106
|
+
cached.push({ input, output });
|
|
107
|
+
}
|
|
108
|
+
continue;
|
|
109
|
+
}
|
|
110
|
+
if (
|
|
111
|
+
cacheEntry &&
|
|
112
|
+
cacheEntry.inputHash === inputHash &&
|
|
113
|
+
cacheEntry.output === output &&
|
|
114
|
+
fs.existsSync(output) &&
|
|
115
|
+
cacheEntry.outputHash === hashContent(fs.readFileSync(output))
|
|
116
|
+
) {
|
|
117
|
+
cached.push({ input, output });
|
|
118
|
+
newCache[input] = cacheEntry;
|
|
119
|
+
continue;
|
|
120
|
+
}
|
|
121
|
+
const schema = parseSchemaFile(input);
|
|
122
|
+
const v = new Validator(schema);
|
|
123
|
+
const src = v.toStandaloneModule({ format, abortEarly: !!opts.abortEarly });
|
|
124
|
+
if (!src) {
|
|
125
|
+
const reason = 'schema is not AOT-compatible (toStandaloneModule returned null)';
|
|
126
|
+
if (opts.strict) failed.push({ input, error: reason });
|
|
127
|
+
else skipped.push({ input, reason });
|
|
128
|
+
continue;
|
|
129
|
+
}
|
|
130
|
+
fs.mkdirSync(path.dirname(output), { recursive: true });
|
|
131
|
+
fs.writeFileSync(output, src);
|
|
132
|
+
const outBytes = Buffer.byteLength(src, 'utf8');
|
|
133
|
+
const gz = zlib.gzipSync(src);
|
|
134
|
+
const gzBytes = gz.length;
|
|
135
|
+
if (typeof opts.maxSize === 'number' && gzBytes > opts.maxSize) {
|
|
136
|
+
// Roll back the write so a failed build doesn't leave a stale artifact.
|
|
137
|
+
try { fs.unlinkSync(output); } catch {}
|
|
138
|
+
failed.push({ input, error: `output ${output} exceeds --max-size: ${gzBytes} > ${opts.maxSize} (gzipped bytes)` });
|
|
139
|
+
continue;
|
|
140
|
+
}
|
|
141
|
+
compiled.push({ input, output, bytes: outBytes, gzipBytes: gzBytes });
|
|
142
|
+
if (opts.types !== false) {
|
|
143
|
+
const { toTypeScript } = require('./ts-gen');
|
|
144
|
+
const stem = path.basename(input, path.extname(input)).replace(/\.schema$/, '');
|
|
145
|
+
const typeName = stem
|
|
146
|
+
.replace(/[^A-Za-z0-9_]/g, '_')
|
|
147
|
+
.replace(/^([a-z])/, (m) => m.toUpperCase()) || 'Data';
|
|
148
|
+
const dts = toTypeScript(schema, { name: typeName });
|
|
149
|
+
const ext = path.extname(output);
|
|
150
|
+
const dtsExt = ext === '.mjs' ? '.d.mts'
|
|
151
|
+
: ext === '.cjs' ? '.d.cts'
|
|
152
|
+
: '.d.ts';
|
|
153
|
+
const base = output.slice(0, output.length - ext.length);
|
|
154
|
+
fs.writeFileSync(base + dtsExt, dts);
|
|
155
|
+
}
|
|
156
|
+
newCache[input] = {
|
|
157
|
+
inputHash,
|
|
158
|
+
output,
|
|
159
|
+
outputHash: hashContent(Buffer.from(src, 'utf8')),
|
|
160
|
+
};
|
|
161
|
+
} catch (e) {
|
|
162
|
+
failed.push({ input, error: e.message });
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
if (opts.check) {
|
|
167
|
+
const staleCount = inputs.length - cached.length;
|
|
168
|
+
return { compiled: [], cached, skipped, failed, staleCount };
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
writeCache(opts.cacheFile, newCache);
|
|
172
|
+
|
|
173
|
+
return { compiled, cached, skipped, failed };
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
async function watch(opts, onReport) {
|
|
177
|
+
const initial = await build(opts);
|
|
178
|
+
if (typeof onReport === 'function') onReport(initial);
|
|
179
|
+
|
|
180
|
+
const inputs = await expandGlobs(opts.globs || []);
|
|
181
|
+
const dirs = [...new Set(inputs.map((p) => path.dirname(p)))];
|
|
182
|
+
let debounceTimer = null;
|
|
183
|
+
|
|
184
|
+
const runOnce = async () => {
|
|
185
|
+
debounceTimer = null;
|
|
186
|
+
try {
|
|
187
|
+
const r = await build(opts);
|
|
188
|
+
if (typeof onReport === 'function') onReport(r);
|
|
189
|
+
} catch (e) {
|
|
190
|
+
if (typeof onReport === 'function') onReport({ compiled: [], cached: [], skipped: [], failed: [{ input: '<watch>', error: e.message }] });
|
|
191
|
+
}
|
|
192
|
+
};
|
|
193
|
+
|
|
194
|
+
const watchers = dirs.map((d) => fs.watch(d, (_event, filename) => {
|
|
195
|
+
if (!filename) return;
|
|
196
|
+
const ext = path.extname(filename).toLowerCase();
|
|
197
|
+
if (ext !== '.json' && ext !== '.yaml' && ext !== '.yml') return;
|
|
198
|
+
if (debounceTimer) clearTimeout(debounceTimer);
|
|
199
|
+
debounceTimer = setTimeout(runOnce, 100);
|
|
200
|
+
}));
|
|
201
|
+
|
|
202
|
+
return {
|
|
203
|
+
close() {
|
|
204
|
+
if (debounceTimer) clearTimeout(debounceTimer);
|
|
205
|
+
for (const w of watchers) w.close();
|
|
206
|
+
},
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
module.exports = { build, expandGlobs, parseSchemaFile, outputPathFor, watch };
|
package/lib/js-compiler.js
CHANGED
|
@@ -957,6 +957,18 @@ function compileToJSCodegen(schema, schemaMap, userFormats) {
|
|
|
957
957
|
}
|
|
958
958
|
if (fmtEntries.length) boolFn._formatClosures = fmtEntries
|
|
959
959
|
}
|
|
960
|
+
// Closure variables (regex, sub-validators, sets) referenced in _source that
|
|
961
|
+
// standalone module output must declare. Excludes _cpLen (emitted by _CP_LEN_SOURCE)
|
|
962
|
+
// and _uf_* (emitted via _formatClosures).
|
|
963
|
+
{
|
|
964
|
+
const entries = []
|
|
965
|
+
for (let i = 0; i < closureNames.length; i++) {
|
|
966
|
+
const name = closureNames[i]
|
|
967
|
+
if (name === '_cpLen' || name.startsWith('_uf_')) continue
|
|
968
|
+
entries.push({ name, val: closureValues[i] })
|
|
969
|
+
}
|
|
970
|
+
if (entries.length) boolFn._closures = entries
|
|
971
|
+
}
|
|
960
972
|
|
|
961
973
|
return boolFn
|
|
962
974
|
} catch {
|
|
@@ -2989,7 +3001,7 @@ function genCodeE(schema, v, pathExpr, lines, ctx, schemaPrefix) {
|
|
|
2989
3001
|
// Returns VALID_RESULT for valid data, {valid:false, errors} for invalid.
|
|
2990
3002
|
// Avoids double-pass (jsFn → false → errFn runs same checks again).
|
|
2991
3003
|
// Uses type-aware optimizations: after type check passes, skip guards.
|
|
2992
|
-
function compileToJSCombined(schema, VALID_RESULT, schemaMap) {
|
|
3004
|
+
function compileToJSCombined(schema, VALID_RESULT, schemaMap, userFormats) {
|
|
2993
3005
|
// Bail on unevaluated keywords — combined codegen doesn't support them yet
|
|
2994
3006
|
if (typeof schema === 'object' && schema !== null) {
|
|
2995
3007
|
const s = JSON.stringify(schema)
|
|
@@ -3051,7 +3063,7 @@ function compileToJSCombined(schema, VALID_RESULT, schemaMap) {
|
|
|
3051
3063
|
}
|
|
3052
3064
|
|
|
3053
3065
|
const ctx = { varCounter: 0, helperCode: [], closureVars: ['_cpLen'], closureVals: [_cpLen],
|
|
3054
|
-
rootDefs: cRootDefs, refStack: new Set(), schemaMap: schemaMap || null, anchors: cAnchors, rootSchema: schema }
|
|
3066
|
+
rootDefs: cRootDefs, refStack: new Set(), schemaMap: schemaMap || null, anchors: cAnchors, rootSchema: schema, userFormats: userFormats || null }
|
|
3055
3067
|
const lines = []
|
|
3056
3068
|
genCodeC(schema, 'd', '', lines, ctx, '#')
|
|
3057
3069
|
if (lines.length === 0) return () => VALID_RESULT
|
|
@@ -3309,6 +3321,15 @@ function genCodeC(schema, v, pathExpr, lines, ctx, schemaPrefix) {
|
|
|
3309
3321
|
if (fc) {
|
|
3310
3322
|
const code = fc(v, isStr).replace(/return false/g, `{${fail('format', 'format', `{format:'${esc(schema.format)}'}`, `'must match format "${esc(schema.format)}"'`)}}`)
|
|
3311
3323
|
lines.push(code)
|
|
3324
|
+
} else if (ctx.userFormats && typeof ctx.userFormats[schema.format] === 'function') {
|
|
3325
|
+
const safeName = schema.format.replace(/[^a-zA-Z0-9_]/g, '_')
|
|
3326
|
+
const closureName = `_uf_${safeName}`
|
|
3327
|
+
if (!ctx.closureVars.includes(closureName)) {
|
|
3328
|
+
ctx.closureVars.push(closureName)
|
|
3329
|
+
ctx.closureVals.push(ctx.userFormats[schema.format])
|
|
3330
|
+
}
|
|
3331
|
+
const guard = isStr ? '' : `typeof ${v}==='string'&&`
|
|
3332
|
+
lines.push(`if(${guard}!${closureName}(${v})){${fail('format', 'format', `{format:'${esc(schema.format)}'}`, `'must match format "${esc(schema.format)}"'`)}}`)
|
|
3312
3333
|
}
|
|
3313
3334
|
}
|
|
3314
3335
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ata-validator",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.13.1",
|
|
4
4
|
"description": "Ultra-fast JSON Schema validator. 5x faster validation, 159,000x faster compilation. Works without native addon. Cross-schema $ref, Draft 2020-12 + Draft 7, V8-optimized JS codegen, simdjson, RE2, multi-core. 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
|
+
"./build": {
|
|
24
|
+
"types": "./build.d.ts",
|
|
25
|
+
"import": "./build.mjs",
|
|
26
|
+
"require": "./build.js"
|
|
27
|
+
},
|
|
23
28
|
"./package.json": "./package.json"
|
|
24
29
|
},
|
|
25
30
|
"sideEffects": false,
|
|
@@ -35,7 +40,7 @@
|
|
|
35
40
|
"rebuild": "cmake-js rebuild --target ata",
|
|
36
41
|
"prebuild": "pkg-prebuilds-copy --baseDir build/Release --source ata.node --name=ata --strip --napi_version=10",
|
|
37
42
|
"prebuild-all": "npm run prebuild -- --arch x64 && npm run prebuild -- --arch arm64",
|
|
38
|
-
"test": "node test.js",
|
|
43
|
+
"test": "node test.js && node tests/test_aot_build.js && node tests/test_aot_differential.js && node tests/test_aot_cli_build.js && node tests/test_aot_cli_smoke.js",
|
|
39
44
|
"test:suite": "node tests/run_suite.js",
|
|
40
45
|
"test:compat": "node tests/test_compat.js",
|
|
41
46
|
"test:standard-schema": "node tests/test_standard_schema.js",
|
|
@@ -46,7 +51,11 @@
|
|
|
46
51
|
"bench": "node benchmark/bench_large.js",
|
|
47
52
|
"fuzz": "node tests/fuzz_differential.js",
|
|
48
53
|
"fuzz:long": "FUZZ_ITERATIONS=100000 node tests/fuzz_differential.js",
|
|
49
|
-
"test:
|
|
54
|
+
"test:aot": "node tests/test_aot_build.js",
|
|
55
|
+
"test:aot-differential": "node tests/test_aot_differential.js",
|
|
56
|
+
"test:aot-cli": "node tests/test_aot_cli_build.js",
|
|
57
|
+
"test:json-suite": "node tests/run_json_test_suite.js",
|
|
58
|
+
"bench:aot-vs-ajv": "node benchmark/bench_aot_vs_ajv.mjs"
|
|
50
59
|
},
|
|
51
60
|
"keywords": [
|
|
52
61
|
"json",
|
|
@@ -85,6 +94,9 @@
|
|
|
85
94
|
"compat.js",
|
|
86
95
|
"compat.mjs",
|
|
87
96
|
"compat.d.ts",
|
|
97
|
+
"build.js",
|
|
98
|
+
"build.mjs",
|
|
99
|
+
"build.d.ts",
|
|
88
100
|
"binding-options.js",
|
|
89
101
|
"binding/",
|
|
90
102
|
"include/",
|
|
@@ -94,6 +106,7 @@
|
|
|
94
106
|
"scripts/",
|
|
95
107
|
"CMakeLists.txt",
|
|
96
108
|
"README.md",
|
|
109
|
+
"CHANGELOG.md",
|
|
97
110
|
"LICENSE"
|
|
98
111
|
],
|
|
99
112
|
"dependencies": {
|
|
@@ -101,6 +114,12 @@
|
|
|
101
114
|
"node-api-headers": "^1.8.0",
|
|
102
115
|
"pkg-prebuilds": "^1.0.0"
|
|
103
116
|
},
|
|
117
|
+
"peerDependencies": {
|
|
118
|
+
"yaml": "^2.0.0"
|
|
119
|
+
},
|
|
120
|
+
"peerDependenciesMeta": {
|
|
121
|
+
"yaml": { "optional": true }
|
|
122
|
+
},
|
|
104
123
|
"devDependencies": {
|
|
105
124
|
"@sinclair/typebox": "^0.34.49",
|
|
106
125
|
"cmake-js": "^8.0.0",
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|