ata-validator 1.62.0 → 1.63.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.
Files changed (56) hide show
  1. package/README.md +68 -13
  2. package/build.mjs +1 -0
  3. package/index.browser.mjs +23 -4
  4. package/index.bundle.mjs +14 -4
  5. package/index.edge.mjs +14 -4
  6. package/index.js +7 -2
  7. package/lib/aot-build.js +18 -0
  8. package/lib/aot-impl.js +20 -0
  9. package/lib/codegen-engine.js +72 -3
  10. package/lib/compiled-common.js +2 -1
  11. package/lib/email-literal.js +4 -1
  12. package/lib/error-runtime-features.js +41 -0
  13. package/lib/error-runtime.browser.js +15 -0
  14. package/lib/error-runtime.js +1 -32
  15. package/lib/formats-source.js +39 -40
  16. package/lib/interpreter-core.js +17 -9
  17. package/lib/ir/emit-deps.mjs +131 -0
  18. package/lib/ir/emit.mjs +560 -0
  19. package/lib/ir/front.mjs +834 -0
  20. package/lib/ir/index.mjs +10 -0
  21. package/lib/ir/refs.mjs +108 -0
  22. package/lib/ir/run.mjs +520 -0
  23. package/lib/ir/table.mjs +233 -0
  24. package/lib/js-compiler-errors.js +25 -7
  25. package/lib/js-compiler.js +6 -2
  26. package/lib/kernels/collapse.mjs +114 -0
  27. package/lib/kernels/custom.mjs +70 -0
  28. package/lib/kernels/fill.mjs +6 -0
  29. package/lib/kernels/formats-small.mjs +40 -0
  30. package/lib/kernels/formats.mjs +29 -0
  31. package/lib/kernels/index.mjs +11 -0
  32. package/lib/kernels/kernels.mjs +285 -0
  33. package/lib/kernels/leaves.mjs +28 -0
  34. package/lib/kernels/order.mjs +25 -0
  35. package/lib/kernels/plan-rich.mjs +17 -0
  36. package/lib/kernels/plan.d.ts +9 -0
  37. package/lib/kernels/plan.mjs +128 -0
  38. package/lib/kernels/preprocess.mjs +223 -0
  39. package/lib/kernels/rank.mjs +97 -0
  40. package/lib/kernels/rt.mjs +249 -0
  41. package/lib/kernels/table.mjs +182 -0
  42. package/lib/kernels/unique.mjs +157 -0
  43. package/lib/native-positions.js +1 -1
  44. package/lib/openapi.d.ts +2 -0
  45. package/lib/openapi.js +128 -0
  46. package/lib/plan-compiler.js +84 -7
  47. package/lib/public-api.js +7 -2
  48. package/lib/rejections.js +3 -1
  49. package/lib/schema-helpers.js +12 -0
  50. package/lib/schema-helpers.mjs +20 -0
  51. package/lib/schema-order.js +4 -2
  52. package/lib/standard-schema.js +1 -1
  53. package/lib/validator-core.js +35 -30
  54. package/lib/version.js +1 -1
  55. package/lite.js +7 -2
  56. package/package.json +32 -13
package/README.md CHANGED
@@ -101,19 +101,14 @@ The `ata-validator` package itself is pure JavaScript. The native accelerator (s
101
101
  npm install ata-validator --omit=optional
102
102
  ```
103
103
 
104
- or set `ATA_NO_NATIVE=1` at runtime. Typical schemas compile to specialized JS; shapes the compiler cannot represent (some `$dynamicRef`, cyclic `$ref`, unusual keyword interactions) fall back to an interpreted engine, so every schema validates in every environment. The pure-JS setup scores the same on the official suite as the native one, 1301 of 1301 Draft 2020-12 cases. The buffer and parallel APIs (`isValid` on raw buffers, `isValidPrepadded`, `isValidNDJSON`, `isValidParallel`, `countValid`, `batchIsValid`, `validateAndParse`) work without the addon too, answering through the same checks as `isValidJSON()`, slower than the addon: 177 ns against 92 for a small document on Node 25. In a browser, which has no `Buffer`, they throw and name the methods to use instead.
104
+ or set `ATA_NO_NATIVE=1` at runtime. Typical schemas compile to specialized JS; shapes the compiler cannot represent (some `$dynamicRef`, cyclic `$ref`, unusual keyword interactions) fall back to an interpreted engine, so every schema validates in every environment. The pure-JS setup scores the same on the official suite as the native one, 1302 of 1302 Draft 2020-12 cases. The buffer and parallel APIs (`isValid` on raw buffers, `isValidPrepadded`, `isValidNDJSON`, `isValidParallel`, `countValid`, `batchIsValid`, `validateAndParse`) work without the addon too, answering through the same checks as `isValidJSON()`, slower than the addon: 177 ns against 92 for a small document on Node 25. In a browser, which has no `Buffer`, they throw and name the methods to use instead.
105
105
 
106
106
  In Cloudflare Workers and Vercel Edge, where generated code cannot run, `ata-validator`
107
107
  resolves to an entry without the code generator through the `workerd` and `edge-light` export
108
108
  conditions: the same names and answers, and a Worker bundled by wrangler goes from 179.1 to
109
109
  70.4 KiB gzipped.
110
110
 
111
- In a browser, compile with the bundler plugin. [`@ata-project/unplugin`](https://github.com/ata-core/unplugin-ata)
112
- turns each `new Validator(schema)` whose schema it can read at build time into a compiled
113
- validator that gives the same answers, and the runtime stays out of the page. A form with two
114
- validators, one reading errors and one asking only for a verdict, bundles to 10.7 KB gzipped,
115
- or 3.1 KB with `richErrors: false` on the one that reads errors, against 53.2 KB for the same
116
- code on the runtime (esbuild, minified, the runtime with code splitting).
111
+ In a browser, compile with the bundler plugin: see [In the browser](#in-the-browser).
117
112
 
118
113
  Without the plugin, the code generator is still not part of the page. Every schema starts on the
119
114
  interpreted engine, which gives the same answers, and a validator called a thousand times within
@@ -133,9 +128,9 @@ re-exports the npm one at the same version, and CI runs the official suite under
133
128
  details, and what Deno Deploy's refusal of dynamic code means for it, are in
134
129
  [docs/edge-runtimes.md](docs/edge-runtimes.md).
135
130
 
136
- Those four now agree with `validate()` on every case of the official suite, 3365 across three dialects. The native walker behind them does not handle every shape (`contains`, `unevaluatedProperties`, `patternProperties`, tuple `items`, cross-document `$ref`, a few formats), so for schemas using one of those the buffer APIs parse the bytes and answer through `validate()`; the list is in `lib/buffer-gate.js`. Typical request schemas stay on the zero-copy path. `npm test` holds the disagreement count at zero.
131
+ Those four now agree with `validate()` on every case of the official suite, 3366 across three dialects. The native walker behind them does not handle every shape (`contains`, `unevaluatedProperties`, `patternProperties`, tuple `items`, cross-document `$ref`, a few formats), so for schemas using one of those the buffer APIs parse the bytes and answer through `validate()`; the list is in `lib/buffer-gate.js`. Typical request schemas stay on the zero-copy path. `npm test` holds the disagreement count at zero.
137
132
 
138
- Where `new Function` is refused altogether, on Cloudflare Workers, Deno Deploy or under a strict Content-Security-Policy, ata drops to the interpreted engine and scores the same 1301 of 1301 with code generation blocked. No flags, and on Workers no `nodejs_compat` either. See [docs/edge-runtimes.md](docs/edge-runtimes.md). Where generated code runs but slowly, as in an ahead-of-time compiled Bun executable, a module from `ata build` is plain JavaScript that imports nothing and calls no `new Function`: on Bun 1.4.0, import plus a first validation takes 0.66 ms with such a module against 10.70 ms with the runtime compiler (medians of 21 runs). [docs/proof-no-code-generation.md](docs/proof-no-code-generation.md) runs one schema in five of these places, Workers and a CSP page included, and shows the same answers from each.
133
+ Where `new Function` is refused altogether, on Cloudflare Workers, Deno Deploy or under a strict Content-Security-Policy, ata drops to the interpreted engine and scores the same 1302 of 1302 with code generation blocked. No flags, and on Workers no `nodejs_compat` either. See [docs/edge-runtimes.md](docs/edge-runtimes.md). Where generated code runs but slowly, as in an ahead-of-time compiled Bun executable, a module from `ata build` is plain JavaScript that imports nothing and calls no `new Function`: on Bun 1.4.0, import plus a first validation takes 0.66 ms with such a module against 10.70 ms with the runtime compiler (medians of 21 runs). [docs/proof-no-code-generation.md](docs/proof-no-code-generation.md) runs one schema in five of these places, Workers and a CSP page included, and shows the same answers from each.
139
134
 
140
135
  Node has a switch for exactly that environment, so this takes thirty seconds to check for yourself, on ata or on whatever you use today:
141
136
 
@@ -162,7 +157,67 @@ if (isValid(req.body)) {
162
157
 
163
158
  The `.compiled.mjs` modules are self-contained: zero runtime dependency on ata-validator, fully tree-shakeable, with TypeScript types emitted alongside.
164
159
 
165
- Code already written with `new Validator(schema)` gets the same result from the bundler plugin, without changes: [`@ata-project/unplugin`](https://github.com/ata-core/unplugin-ata) replaces each call whose schema is known at build time with a compiled validator that answers the same, and the runtime leaves the bundle. For the plugin's three-schema test entry a minified Vite build goes from 122.5 KB to 16.7 KB gzipped on ata 1.39.2. Schemas that arrive at runtime keep the runtime, which is the right tool for them.
160
+ Code already written with `new Validator(schema)` gets the same result from the bundler plugin, without changes: [`@ata-project/unplugin`](https://github.com/ata-core/unplugin-ata) replaces each call whose schema is known at build time with a compiled validator that answers the same, and the runtime leaves the bundle. The sizes are in [In the browser](#in-the-browser). Schemas that arrive at runtime keep the runtime, which is the right tool for them.
161
+
162
+ ## In the browser
163
+
164
+ Add the bundler plugin. [`@ata-project/unplugin`](https://github.com/ata-core/unplugin-ata) compiles each `new Validator(schema)` whose schema it can read at build time, so the page carries that schema's checks and not the compiler. Your code does not change, and the answers, the errors and the filled defaults are the same.
165
+
166
+ ```js
167
+ // vite.config.js
168
+ import ata from '@ata-project/unplugin/vite'
169
+
170
+ export default { plugins: [ata()] }
171
+ ```
172
+
173
+ ```js
174
+ // next.config.mjs, for Turbopack (the default since Next.js 16) and webpack
175
+ import withAta from '@ata-project/unplugin/next'
176
+
177
+ export default withAta({ /* your config */ })
178
+ ```
179
+
180
+ Webpack, Rspack, Rollup, Rolldown, esbuild and Bun take the same plugin; see its [setup](https://github.com/ata-core/unplugin-ata#setup).
181
+
182
+ An order form with six fields, an email format, a pattern and a default, its errors read on submit:
183
+
184
+ | | gzipped | ready, phone profile | first submit |
185
+ |---|---|---|---|
186
+ | without the plugin (code split) | 55.3 KB | 779 ms | 13.9 ms |
187
+ | with the plugin | 4.5 KB | 372 ms | 1.6 ms |
188
+
189
+ The sizes are an esbuild build, minified, gzip level 9. The times are Chrome with Lighthouse's mobile throttling (CPU 4x slower, 1.6 Mbps, 150 ms round trip), the files served gzipped, medians of 11 runs; the submit is made as soon as the page has loaded, before the runtime warms itself up in idle time, so a later submit on the runtime takes less than that.
190
+
191
+ What a page's validators add to its first load, gzipped, in applications built the usual way (Vite + React, Next.js 16), forms of six fields reading their errors on submit:
192
+
193
+ | | ata 1.62.1, plugin 0.11.3 | ata 1.63.0, plugin 0.12.0 |
194
+ |---|---|---|
195
+ | Vite, one form | 2.33 KB | 1.41 KB |
196
+ | Vite, ten forms | 8.02 KB | 2.02 KB |
197
+ | Vite, ten forms with email and uri fields | 12.07 KB | 2.70 KB |
198
+ | Next.js with webpack, one form with rich errors | 10.71 KB | 4.63 KB |
199
+ | Next.js with Turbopack, one form with rich errors | 67.52 KB (the plugin could not run) | 4.23 KB |
200
+
201
+ Many forms go out as data for one shared interpreter instead of code, where that makes the bundle smaller; the plugin decides for the whole application, and keeps written code for a single form. Needs ata-validator 1.63.0 and the plugin's 0.12.0.
202
+
203
+ With an OpenAPI document the plugin writes the validators from it (`ata({ openapi: 'openapi.json' })`, request bodies by operation, see [its README](https://github.com/ata-core/unplugin-ata#one-openapi-document-for-the-server-and-the-page)); a server reads the same document through `operationSchemas` in `ata-validator/openapi`.
204
+
205
+ The form and the server decide alike. A validator the plugin compiles answers exactly as `new Validator(schema)` does on the server, the same schema read the same way, when the page is built with the same ata-validator version the server runs: on a nine-field signup schema (email, uri, a pattern, lengths counted in characters, a fixed set of fields) the compiled form and the server's validator disagreed on 0 of 50,000 generated documents, so a submit the page accepts is one the server accepts.
206
+
207
+ A schema that arrives at run time (a form builder, a CMS, a schema served by the backend) can arrive as a plan instead. The server writes it with `tablePlan` from `ata-validator/ir`, which does the heavy work (references, dialects, normalization); the page reads it with `fromPlan` from `ata-validator/plan`, which carries the table interpreter and loads the format checks a plan names when it names them. Verdicts, errors and filled defaults are the server's for the same schema.
208
+
209
+ ```js
210
+ // server
211
+ import { tablePlan } from 'ata-validator/ir'
212
+ app.get('/forms/signup', () => tablePlan(signupSchema, { richErrors: false }))
213
+
214
+ // page
215
+ import { fromPlan } from 'ata-validator/plan'
216
+ const v = await fromPlan(await (await fetch('/forms/signup')).json())
217
+ v.validate(data)
218
+ ```
219
+
220
+ For the nine-field signup schema above, a Vite page that receives its schema at run time loads 53.0 KB of JavaScript gzipped with `new Validator(schema)` and 6.2 KB with `fromPlan`, the errors on the page the same. `ata-validator/plan-rich` reads plans with ata's rich errors as well. A plan is as trusted as a schema, so serve it from your own server: `fromPlan` refuses one that is not well formed before it runs, and says so once when a plan was written by another ata-validator version, whose answers may differ. A plan takes the keywords forms use (types, properties, required, additionalProperties, the leaf checks and formats, items, allOf, `$ref`, not, if, dependentRequired) and defaults; `tablePlan` returns `null` for anything else, and such a schema keeps the runtime.
166
221
 
167
222
  ## Measured by others
168
223
 
@@ -994,7 +1049,7 @@ Copy-paste recipes for the common frameworks. Most need 10-20 lines of glue. See
994
1049
  | Fastify | dedicated plugin | [`fastify-ata`](https://github.com/ata-core/fastify-ata) |
995
1050
  | Vite, Webpack, Rollup, Rolldown, esbuild, Rspack (build-time compile) | dedicated plugin | [`@ata-project/unplugin`](https://github.com/ata-core/unplugin-ata) |
996
1051
  | Vite (build-time compile) | dedicated plugin | [`ata-vite`](https://github.com/ata-core/ata-vite), the Vite entry of the plugin above |
997
- | Next.js (build-time compile) | webpack entry of the plugin above | `@ata-project/unplugin/webpack` in `next.config.js` |
1052
+ | Next.js (build-time compile, Turbopack and webpack) | config wrapper of the plugin above | `withAta` from `@ata-project/unplugin/next` in `next.config.mjs` |
998
1053
  | Hono | async middleware | [docs/integrations/hono.md](docs/integrations/hono.md) |
999
1054
  | Elysia | Standard Schema V1 in the `body` slot | [docs/integrations/elysia.md](docs/integrations/elysia.md) |
1000
1055
  | tRPC | Standard Schema V1 input | [docs/integrations/trpc.md](docs/integrations/trpc.md) |
@@ -1024,7 +1079,7 @@ Copy-paste recipes for the common frameworks. Most need 10-20 lines of glue. See
1024
1079
 
1025
1080
  ### Dialects
1026
1081
 
1027
- `$schema` selects the dialect. Draft 2020-12 is the default; draft-07, draft-06, draft-04 and Draft 2019-09 are rewritten to the 2020-12 spelling on the way in (draft-06 as draft-07 without `if`/`then`/`else`, draft-04 as draft-06 with `id` for `$id` and without `const`, `contains` and `propertyNames`, 2019-09's tuple `items` and `$recursiveRef` as `prefixItems` and `$dynamicRef`); and `https://json-schema.org/v1` (or the dated `https://json-schema.org/v1/2026`) selects JSON Schema v1. The official suite passes in full on all five: 1301 of 1301 on 2020-12, 1261 of 1261 on 2019-09, 929 of 929 on draft-07, 841 of 841 on draft-06 and 1135 of 1135 on v1, with code generation allowed and blocked. Draft-04 and earlier are not supported; draft-04's boolean `exclusiveMinimum`/`exclusiveMaximum` is read wherever it appears.
1082
+ `$schema` selects the dialect. Draft 2020-12 is the default; draft-07, draft-06, draft-04 and Draft 2019-09 are rewritten to the 2020-12 spelling on the way in (draft-06 as draft-07 without `if`/`then`/`else`, draft-04 as draft-06 with `id` for `$id` and without `const`, `contains` and `propertyNames`, 2019-09's tuple `items` and `$recursiveRef` as `prefixItems` and `$dynamicRef`); and `https://json-schema.org/v1` (or the dated `https://json-schema.org/v1/2026`) selects JSON Schema v1. The official suite passes in full on all five: 1302 of 1302 on 2020-12, 1261 of 1261 on 2019-09, 929 of 929 on draft-07, 841 of 841 on draft-06 and 1135 of 1135 on v1, with code generation allowed and blocked. Draft-04 and earlier are not supported; draft-04's boolean `exclusiveMinimum`/`exclusiveMaximum` is read wherever it appears.
1028
1083
 
1029
1084
  Two things differ under v1. `propertyDependencies` selects a subschema by the value of a property rather than by its presence, which is what `dependentSchemas` does. And `$dynamicRef` no longer requires bookending: the reference resolves through the dynamic scope whether or not the schema it first lands on carries a matching `$dynamicAnchor`, so the outermost matching anchor still in scope wins. Everything else ata implements is identical under both dialects, so a schema that declares no `$schema` behaves exactly as before.
1030
1085
 
@@ -1036,7 +1091,7 @@ Both are implemented in the interpreted engine, so a v1 schema that uses `$dynam
1036
1091
 
1037
1092
  ### Known limitations
1038
1093
 
1039
- Running the whole Draft 2020-12 suite with nothing excluded, `format` and `default` under specification semantics (`assertFormat: false`, `useDefaults: false`), gives 1301 of 1301 cases. Draft 2019-09 gives 1261 of 1261, draft 7 929 of 929, draft-06 841 of 841, draft-04 618 of 618. The v1 dialect gives 1135 of 1135. `npm run test:suite` reproduces all three.
1094
+ Running the whole Draft 2020-12 suite with nothing excluded, `format` and `default` under specification semantics (`assertFormat: false`, `useDefaults: false`), gives 1302 of 1302 cases. Draft 2019-09 gives 1261 of 1261, draft 7 929 of 929, draft-06 841 of 841, draft-04 618 of 618. The v1 dialect gives 1135 of 1135. `npm run test:suite` reproduces all three.
1040
1095
 
1041
1096
  Areas that remain deliberate scope decisions for 1.x:
1042
1097
 
package/build.mjs CHANGED
@@ -12,6 +12,7 @@ export const schemaHash = mod.schemaHash;
12
12
  export const compiledEligible = mod.compiledEligible;
13
13
  export const compiledSchemaFor = mod.compiledSchemaFor;
14
14
  export const compiledModuleFor = mod.compiledModuleFor;
15
+ export const defaultsSource = mod.defaultsSource;
15
16
  export const bunTarget = mod.bunTarget;
16
17
  export const verify = mod.verify;
17
18
  export const compiledOptions = mod.compiledOptions;
package/index.browser.mjs CHANGED
@@ -12,9 +12,28 @@
12
12
  import core from './lib/validator-core.js';
13
13
  import tiering from './lib/tiering.js';
14
14
  import { toTypeScript, renderPretty, renderCompact, toOutput, toRetryMessage, describeSchema, renderJSON } from './lib/tools.mjs';
15
+ import { defineSchema, schemaOf } from './lib/schema-helpers.mjs';
15
16
 
16
- core._registerTiering(tiering.createTiering(() => import('./lib/codegen-engine.js')));
17
-
18
- export const { Validator, compile, validate, validateAsync, parseAsync, version, createPaddedBuffer, SIMDJSON_PADDING, parseJSON, defineSchema, schemaOf } = core;
17
+ // Pure-marked reads, so a bundler drops what goes unused (lib/schema-helpers.mjs).
18
+ // In development, once: a page that loads this entry compiles its schemas in
19
+ // the browser, and a schema the build can read could be compiled by the build
20
+ // instead. A production build replaces process.env.NODE_ENV and drops the
21
+ // whole statement, the try included; where nothing defines process, the
22
+ // ReferenceError is caught. Not in Node (no document).
23
+ const api = /* @__PURE__ */ (() => {
24
+ core._registerTiering(tiering.createTiering(() => import('./lib/codegen-engine.js')));
25
+ try { if (process.env.NODE_ENV !== 'production' && typeof document !== 'undefined') console.info('ata-validator: this page compiles its schemas in the browser, which takes the validator runtime (about 51 KB gzipped). A schema the build can read can be compiled by the build instead, a few KB for a form: https://github.com/ata-core/unplugin-ata') } catch {}
26
+ return core;
27
+ })();
28
+ export const Validator = /* @__PURE__ */ (() => api.Validator)();
29
+ export const compile = /* @__PURE__ */ (() => api.compile)();
30
+ export const validate = /* @__PURE__ */ (() => api.validate)();
31
+ export const validateAsync = /* @__PURE__ */ (() => api.validateAsync)();
32
+ export const parseAsync = /* @__PURE__ */ (() => api.parseAsync)();
33
+ export const version = /* @__PURE__ */ (() => api.version)();
34
+ export const createPaddedBuffer = /* @__PURE__ */ (() => api.createPaddedBuffer)();
35
+ export const SIMDJSON_PADDING = /* @__PURE__ */ (() => api.SIMDJSON_PADDING)();
36
+ export const parseJSON = /* @__PURE__ */ (() => api.parseJSON)();
37
+ export { defineSchema, schemaOf };
19
38
  export { toTypeScript, renderPretty, renderCompact, toOutput, toRetryMessage, describeSchema, renderJSON };
20
- export default { Validator, compile, validate, validateAsync, parseAsync, version, createPaddedBuffer, SIMDJSON_PADDING, parseJSON, toTypeScript, defineSchema, schemaOf, renderPretty, renderCompact, toOutput, toRetryMessage, describeSchema, renderJSON };
39
+ export default /* @__PURE__ */ (() => ({ Validator: api.Validator, compile: api.compile, validate: api.validate, validateAsync: api.validateAsync, parseAsync: api.parseAsync, version: api.version, createPaddedBuffer: api.createPaddedBuffer, SIMDJSON_PADDING: api.SIMDJSON_PADDING, parseJSON: api.parseJSON, toTypeScript, defineSchema, schemaOf, renderPretty, renderCompact, toOutput, toRetryMessage, describeSchema, renderJSON }))();
package/index.bundle.mjs CHANGED
@@ -7,9 +7,19 @@
7
7
  import core from './lib/validator-core.js';
8
8
  import engine from './lib/codegen-engine.js';
9
9
  import { toTypeScript, renderPretty, renderCompact, toOutput, toRetryMessage, describeSchema, renderJSON } from './lib/tools.mjs';
10
+ import { defineSchema, schemaOf } from './lib/schema-helpers.mjs';
10
11
 
11
- core._registerCodegen(engine);
12
-
13
- export const { Validator, compile, validate, validateAsync, parseAsync, version, createPaddedBuffer, SIMDJSON_PADDING, parseJSON, defineSchema, schemaOf } = core;
12
+ // Pure-marked reads, so a bundler drops what goes unused (lib/schema-helpers.mjs).
13
+ const api = /* @__PURE__ */ (() => { core._registerCodegen(engine); return core; })();
14
+ export const Validator = /* @__PURE__ */ (() => api.Validator)();
15
+ export const compile = /* @__PURE__ */ (() => api.compile)();
16
+ export const validate = /* @__PURE__ */ (() => api.validate)();
17
+ export const validateAsync = /* @__PURE__ */ (() => api.validateAsync)();
18
+ export const parseAsync = /* @__PURE__ */ (() => api.parseAsync)();
19
+ export const version = /* @__PURE__ */ (() => api.version)();
20
+ export const createPaddedBuffer = /* @__PURE__ */ (() => api.createPaddedBuffer)();
21
+ export const SIMDJSON_PADDING = /* @__PURE__ */ (() => api.SIMDJSON_PADDING)();
22
+ export const parseJSON = /* @__PURE__ */ (() => api.parseJSON)();
23
+ export { defineSchema, schemaOf };
14
24
  export { toTypeScript, renderPretty, renderCompact, toOutput, toRetryMessage, describeSchema, renderJSON };
15
- export default { Validator, compile, validate, validateAsync, parseAsync, version, createPaddedBuffer, SIMDJSON_PADDING, parseJSON, toTypeScript, defineSchema, schemaOf, renderPretty, renderCompact, toOutput, toRetryMessage, describeSchema, renderJSON };
25
+ export default /* @__PURE__ */ (() => ({ Validator: api.Validator, compile: api.compile, validate: api.validate, validateAsync: api.validateAsync, parseAsync: api.parseAsync, version: api.version, createPaddedBuffer: api.createPaddedBuffer, SIMDJSON_PADDING: api.SIMDJSON_PADDING, parseJSON: api.parseJSON, toTypeScript, defineSchema, schemaOf, renderPretty, renderCompact, toOutput, toRetryMessage, describeSchema, renderJSON }))();
package/index.edge.mjs CHANGED
@@ -5,9 +5,19 @@
5
5
  import core from './lib/validator-core.js';
6
6
  import aotBrowser from './lib/aot.browser.js';
7
7
  import { toTypeScript, renderPretty, renderCompact, toOutput, toRetryMessage, describeSchema, renderJSON } from './lib/tools.mjs';
8
+ import { defineSchema, schemaOf } from './lib/schema-helpers.mjs';
8
9
 
9
- core._registerAot(() => aotBrowser);
10
-
11
- export const { Validator, compile, validate, validateAsync, parseAsync, version, createPaddedBuffer, SIMDJSON_PADDING, parseJSON, defineSchema, schemaOf } = core;
10
+ // Pure-marked reads, so a bundler drops what goes unused (lib/schema-helpers.mjs).
11
+ const api = /* @__PURE__ */ (() => { core._registerAot(() => aotBrowser); return core; })();
12
+ export const Validator = /* @__PURE__ */ (() => api.Validator)();
13
+ export const compile = /* @__PURE__ */ (() => api.compile)();
14
+ export const validate = /* @__PURE__ */ (() => api.validate)();
15
+ export const validateAsync = /* @__PURE__ */ (() => api.validateAsync)();
16
+ export const parseAsync = /* @__PURE__ */ (() => api.parseAsync)();
17
+ export const version = /* @__PURE__ */ (() => api.version)();
18
+ export const createPaddedBuffer = /* @__PURE__ */ (() => api.createPaddedBuffer)();
19
+ export const SIMDJSON_PADDING = /* @__PURE__ */ (() => api.SIMDJSON_PADDING)();
20
+ export const parseJSON = /* @__PURE__ */ (() => api.parseJSON)();
21
+ export { defineSchema, schemaOf };
12
22
  export { toTypeScript, renderPretty, renderCompact, toOutput, toRetryMessage, describeSchema, renderJSON };
13
- export default { Validator, compile, validate, validateAsync, parseAsync, version, createPaddedBuffer, SIMDJSON_PADDING, parseJSON, toTypeScript, defineSchema, schemaOf, renderPretty, renderCompact, toOutput, toRetryMessage, describeSchema, renderJSON };
23
+ export default /* @__PURE__ */ (() => ({ Validator: api.Validator, compile: api.compile, validate: api.validate, validateAsync: api.validateAsync, parseAsync: api.parseAsync, version: api.version, createPaddedBuffer: api.createPaddedBuffer, SIMDJSON_PADDING: api.SIMDJSON_PADDING, parseJSON: api.parseJSON, toTypeScript, defineSchema, schemaOf, renderPretty, renderCompact, toOutput, toRetryMessage, describeSchema, renderJSON }))();
package/index.js CHANGED
@@ -136,6 +136,11 @@ function attachSuggestions (errors, data) {
136
136
  return errors;
137
137
  }
138
138
 
139
+ // Loaded on first call, like the tools: a process that never calls them does
140
+ // not read the file.
141
+ function defineSchema (schema) { return require('./lib/schema-helpers.js').defineSchema(schema); }
142
+ function schemaOf (...a) { return require('./lib/schema-helpers.js').schemaOf(...a); }
143
+
139
144
  module.exports = {
140
145
  Validator: core.Validator,
141
146
  compile: core.compile,
@@ -147,8 +152,8 @@ module.exports = {
147
152
  SIMDJSON_PADDING: core.SIMDJSON_PADDING,
148
153
  parseJSON: core.parseJSON,
149
154
  toTypeScript,
150
- defineSchema: core.defineSchema,
151
- schemaOf: core.schemaOf,
155
+ defineSchema,
156
+ schemaOf,
152
157
  renderPretty,
153
158
  renderCompact,
154
159
  toOutput,
package/lib/aot-build.js CHANGED
@@ -737,6 +737,23 @@ function compiledModuleFor(schema, opts) {
737
737
  return src && !degraded ? src : null;
738
738
  }
739
739
 
740
+ // The runtime's defaults pass for a schema as the body of a function of `d`
741
+ // (preprocessSource in lib/codegen-engine.js, over the schema the runtime
742
+ // builds it from, as the modules compiledModuleFor writes carry it), or null
743
+ // where the generated pass declines the schema. For a writer of modules that
744
+ // has no generated pass of its own for that schema (ata-validator/ir's
745
+ // specialize, through its fillSource option).
746
+ function defaultsSource(schema) {
747
+ if (!compiledEligible(schema)) return null;
748
+ try {
749
+ const v = validatorFor(schema);
750
+ const { resolveSchemaForPreprocess } = require('./validator-core')._internals;
751
+ return require('./codegen-engine').preprocessSource(resolveSchemaForPreprocess(v._schemaObj, v._schemaMap), { useDefaults: true });
752
+ } catch {
753
+ return null;
754
+ }
755
+ }
756
+
740
757
  // The Validator options fromCompiled() reproduces, for a plugin deciding whether
741
758
  // `new Validator(schema, options)` can be replaced. An older ata-validator does
742
759
  // not export this, and a plugin should then replace only calls without options.
@@ -768,6 +785,7 @@ module.exports = {
768
785
  compiledEligible,
769
786
  compiledSchemaFor,
770
787
  compiledModuleFor,
788
+ defaultsSource,
771
789
  build,
772
790
  expandGlobs,
773
791
  parseSchemaFile,
package/lib/aot-impl.js CHANGED
@@ -19,6 +19,20 @@ const VALID_RESULT = Object.freeze({ valid: true, errors: Object.freeze([]) });
19
19
  const { buildTargetedPositionMap } = require('./data-positions');
20
20
  const { schemaHash } = require('./schema-hash');
21
21
  const ATA_VERSION = require('./version');
22
+
23
+ // The emitters write what the code generator produces, so they register it.
24
+ // The browser build loads the generator on demand, once a validator is hot,
25
+ // and until it had, bundleStandalone() compiled every schema to a null
26
+ // validator without an error: a page calling it got an empty module. Done at
27
+ // the first call rather than at load, where a loader that evaluates every
28
+ // require first (esm.sh) would hand over the generator half built.
29
+ let _codegenReady = false;
30
+ function needCodegen() {
31
+ if (_codegenReady) return;
32
+ const core = require('./validator-core.js');
33
+ if (!core._codegenRegistered()) core._registerCodegen(require('./codegen-engine.js'));
34
+ _codegenReady = true;
35
+ }
22
36
  const safeRegexEngineSource = () => require('./safe-regex').engineSource();
23
37
 
24
38
  // Embedded verbatim in standalone modules so the output file has no runtime
@@ -1378,5 +1392,11 @@ module.exports = {
1378
1392
  bundleCompact: htmlSafeResult(bundleCompact),
1379
1393
  loadBundle,
1380
1394
  };
1395
+
1396
+ // Every emitter registers the code generator before it runs (needCodegen).
1397
+ for (const k of Object.keys(module.exports)) {
1398
+ const f = module.exports[k];
1399
+ if (typeof f === 'function') module.exports[k] = function (...a) { needCodegen(); return f.apply(this, a); };
1400
+ }
1381
1401
  // For the build only (lib/aot-build.js), not part of ata-validator/aot.
1382
1402
  Object.defineProperty(module.exports, '_refsResolveWithin', { value: refsResolveWithin, enumerable: false });
@@ -740,7 +740,10 @@ function emitRemovals(node, access, lines, depth, seen, own) {
740
740
  }
741
741
  seen.delete(node);
742
742
  if (body.length === 0) return;
743
- if (depth === 0) lines.push(...body);
743
+ // At the root too, an array is not the object this node describes: its
744
+ // indices are not additional properties. The pass's own guard lets arrays
745
+ // through for the coercions of a schema whose root is an array.
746
+ if (depth === 0) lines.push(`if(!Array.isArray(${access})){${body.join('\n')}}`);
744
747
  else lines.push(`if(${own}${access}!==null&&typeof ${access}==='object'&&!Array.isArray(${access})){${body.join('\n')}}`);
745
748
  }
746
749
 
@@ -1194,6 +1197,63 @@ function buildParse (self, decline, extended) {
1194
1197
  // carry. Returns the twin to answer this call, or null to compile.
1195
1198
  const COLD_CALLS = 64;
1196
1199
  const COLD_MIN = 16 * 1024;
1200
+ const MANY_SCHEMAS = 16;
1201
+ const SMALL_TWIN_OFF = typeof process !== 'undefined' && process.env !== undefined && process.env.ATA_NO_SMALL_TWIN === '1';
1202
+ let _firstCalls = 0;
1203
+ let _canGenerate = null;
1204
+ function canGenerate () {
1205
+ if (_canGenerate === null) { try { _canGenerate = require('./compile-fn').compileFunction('return 1')() === 1; } catch { _canGenerate = false; } }
1206
+ return _canGenerate;
1207
+ }
1208
+ let _core = null;
1209
+ let _keySelf = null; let _key = null;
1210
+ // The compile cache key compileCached just built for this validator, once.
1211
+ function keyFor (self) { if (_keySelf !== self) return null; const k = _key; _keySelf = null; _key = null; return k; }
1212
+ const core_ = () => _core || (_core = require('./validator-core')._internals);
1213
+ function compileCached (self) {
1214
+ if (self._userFormats || self._usesKeywords) return false;
1215
+ const { _compileCache, compileCacheKey } = core_();
1216
+ const str = self._schemaStr || (self._schemaStr = JSON.stringify(self._schemaObj));
1217
+ const key = compileCacheKey(str, self._schemaMap) + '\u0000' + (typeof jsCompiler.targetEngine === 'function' ? jsCompiler.targetEngine() : 'v8');
1218
+ // Kept for the compile that follows in the same call (keyFor), which would
1219
+ // build and hash the same key again.
1220
+ _keySelf = self; _key = key;
1221
+ const c = _compileCache.get(key);
1222
+ return c !== undefined && c.jsFn !== undefined && c.jsFn !== null;
1223
+ }
1224
+ // A small schema's twin, once the process has met MANY_SCHEMAS schemas (an
1225
+ // agent's tools, a registry of thousands): the interpreted engine alone, which
1226
+ // answers a verdict and a valid document's validate(); a document that fails,
1227
+ // or a JSON-text call, compiles the validator, which answers as always. A new
1228
+ // five-property schema's first check took 72 us compiled and 13 on it, and
1229
+ // its memory is about the compiled validator's (8.37 against 8.57 KB). A
1230
+ // server with a few routes compiles them at their first call as before, and
1231
+ // neither a cached schema nor a realm without code generation takes it.
1232
+ // ATA_NO_SMALL_TWIN=1 turns it off, for tests of the generated code.
1233
+ class SmallTwin {
1234
+ constructor (self, interp) { this.v = self; this.i = interp; }
1235
+ // A document that fails, or a call the interpreted verdict does not answer,
1236
+ // compiles the validator, which answers it as always.
1237
+ compile () { this.v._coldCalls = COLD_CALLS; this.v._twin = null; return this.v; }
1238
+ isValidObject (d) { return this.i.isValid(d); }
1239
+ validate (d) { return this.i.isValid(d) ? { valid: true, data: d, errors: [] } : this.compile().validate(d); }
1240
+ validateJSON (t) { return this.compile().validateJSON(t); }
1241
+ isValidJSON (t) { return this.compile().isValidJSON(t); }
1242
+ }
1243
+ // The twin is made before the validator compiles, so it has none of what the
1244
+ // compile prepares: the vendored meta-schemas a reference to json-schema.org
1245
+ // resolves against, vocabularies, and the reading of an `$id` fragment as an
1246
+ // anchor where the dialect is not named. A schema that needs any of these
1247
+ // compiles at its first call as before: a reference other than a JSON pointer
1248
+ // into the document itself, an identifier or anchor, or a `$schema` outside
1249
+ // json-schema.org. With them the twin answered false where the compiled
1250
+ // validator answers true, so isValidObject and validate() disagreed after a
1251
+ // process had met sixteen schemas.
1252
+ const TWIN_DECLINES = /"\$(?:id|anchor|dynamicAnchor|recursiveAnchor|dynamicRef|recursiveRef|vocabulary)"|"\$ref":"(?!#\/|#")|"\$schema":"(?!https?:\/\/json-schema\.org\/)/;
1253
+ function smallTwin (self) {
1254
+ const { _interpArgs } = core_();
1255
+ return new SmallTwin(self, require('./interpreter').createInterpreter(self._schemaObj, _interpArgs(self, self._schemaObj)));
1256
+ }
1197
1257
  // Whether the schema may expand past `limit`, answered from its text where
1198
1258
  // that settles it. Without a `$ref` nothing expands, and expandedChars counts
1199
1259
  // at most 8 for each character of the text, so a short schema is decided
@@ -1208,11 +1268,16 @@ function mayExpandPast (self, limit) {
1208
1268
 
1209
1269
  function coldTwin (self, Validator) {
1210
1270
  const n = self._coldCalls === undefined ? 0 : self._coldCalls;
1211
- if (n >= COLD_CALLS || self._validateTail !== null || self._verdictTail !== null) return null;
1271
+ // Past COLD_CALLS the validator compiles, and the twin is let go: it was
1272
+ // kept for the life of the validator, an interpreted copy of every large
1273
+ // schema in memory beside its generated code.
1274
+ if (n >= COLD_CALLS || self._validateTail !== null || self._verdictTail !== null) { self._twin = null; return null; }
1212
1275
  let t = self._twin;
1213
1276
  if (t === undefined) {
1214
1277
  t = null;
1215
- if (!self._interpretOnly && !self._initialized && mayExpandPast(self, COLD_MIN)) {
1278
+ if (!self._interpretOnly && !self._initialized && !SMALL_TWIN_OFF && ++_firstCalls > MANY_SCHEMAS && !self._usesKeywords && !self._userFormats && !self._verbose && !self._options.abortEarly && canGenerate() && !compileCached(self) && !self._needsPreprocess() && !mayExpandPast(self, COLD_MIN) && !TWIN_DECLINES.test(self._schemaStr)) {
1279
+ try { t = smallTwin(self); } catch { t = null; }
1280
+ } else if (!self._interpretOnly && !self._initialized && mayExpandPast(self, COLD_MIN)) {
1216
1281
  try {
1217
1282
  t = new Validator(self._rawSchema, Object.assign({}, self._options, { engine: 'interpreter' }));
1218
1283
  // The registry as it is now, not as the constructor options had it:
@@ -1226,6 +1291,9 @@ function coldTwin (self, Validator) {
1226
1291
  } catch { t = null; }
1227
1292
  }
1228
1293
  self._twin = t;
1294
+ // A key kept for a compile that does not follow now would go stale (a
1295
+ // registry changed before the validator compiles).
1296
+ if (t !== null) { _keySelf = null; _key = null; }
1229
1297
  }
1230
1298
  if (t === null) return null;
1231
1299
  self._coldCalls = n + 1;
@@ -1234,6 +1302,7 @@ function coldTwin (self, Validator) {
1234
1302
 
1235
1303
  module.exports = {
1236
1304
  jsCompiler,
1305
+ keyFor,
1237
1306
  coldTwin,
1238
1307
  // The native locator for error frames, loaded with the first frame.
1239
1308
  nativePositions: () => require('./native-positions').nativeTargeted,
@@ -19,7 +19,8 @@ function carriesDefaults (mod) {
19
19
  }
20
20
  function defaultsFor (mod, schema) {
21
21
  if (carriesDefaults(mod)) return typeof mod.applyDefaults === 'function' ? mod.applyDefaults : null;
22
- return require('./compiled-defaults')(schema);
22
+ const d = require('./compiled-defaults');
23
+ return (typeof d === 'function' ? d : d.default)(schema);
23
24
  }
24
25
 
25
26
  // The Validator options the wrapper answers the same way as the runtime.
@@ -24,6 +24,9 @@ const IPV6_RE = (() => {
24
24
  // function it is written into when the literal is not an address.
25
25
  let _src = null;
26
26
  module.exports = () => _src || (_src =
27
- `if((_in.charCodeAt(0)|32)===105&&(_in.charCodeAt(1)|32)===112&&(_in.charCodeAt(2)|32)===118&&_in.charCodeAt(3)===54&&_in.charCodeAt(4)===58){const _i6=_in.slice(5);if(_i6.length>45||!${IPV6_RE}.test(_i6))return false}` +
27
+ // The tag is matched without regard to case; outside the unicode flag /i
28
+ // folds no character above 127 onto an ASCII one, so this is the same test
29
+ // as comparing each code unit with `|32`.
30
+ `if(/^ipv6:/i.test(_in)){const _i6=_in.slice(5);if(_i6.length>45||!${IPV6_RE}.test(_i6))return false}` +
28
31
  `else if(_in.length>15||!${require('./formats').IPV4}.test(_in))return false`);
29
32
  module.exports.IPV6_RE = IPV6_RE;
@@ -0,0 +1,41 @@
1
+ 'use strict';
2
+
3
+ // The parts of the shared error runtime a set of schemas needs, and the
4
+ // table of error codes it carries. Pure functions over schemas, kept apart
5
+ // from lib/error-runtime.js, which reads the library's files with fs, so a
6
+ // browser bundle can use these without it (see lib/error-runtime.browser.js).
7
+
8
+ // The parts a build can leave out when none of its schemas needs them. The
9
+ // decision is made on the schemas the runtime walks (as embedded, see
10
+ // runtimeFeatures) and errs toward keeping a part: a key with that name
11
+ // anywhere, even inside an enum value, keeps it. A part left out is replaced
12
+ // by a stub that throws, which a schema without that keyword cannot reach.
13
+ const OPTIONAL = {
14
+ formats: { keys: ['format'], stub: 'module.exports = {};' },
15
+ 'safe-regex': { keys: ['pattern', 'patternProperties'], stub: "module.exports = { compileSafe() { throw new Error('ata error runtime: built without the pattern engine'); } };" },
16
+ 'unique-items': { keys: ['uniqueItems', 'enum', 'const'], stub: "function no() { throw new Error('ata error runtime: built without deep equality'); }\nmodule.exports = { allDistinct: no, duplicatePair: no, _deq: no };" },
17
+ };
18
+
19
+ // Which optional parts these schemas need: { formats, 'safe-regex',
20
+ // 'unique-items' } as booleans.
21
+ function runtimeFeatures(schemas) {
22
+ const keys = new Set();
23
+ const seen = new Set();
24
+ const walk = (n) => {
25
+ if (n === null || typeof n !== 'object' || seen.has(n)) return;
26
+ seen.add(n);
27
+ if (Array.isArray(n)) { for (const x of n) walk(x); return; }
28
+ for (const k of Object.keys(n)) { keys.add(k); walk(n[k]); }
29
+ };
30
+ for (const s of schemas) walk(s);
31
+ const out = {};
32
+ for (const [part, { keys: ks }] of Object.entries(OPTIONAL)) out[part] = ks.some((k) => keys.has(k));
33
+ return out;
34
+ }
35
+
36
+ function codeTable() {
37
+ const { byKw, byFmt } = require('./error-code-of');
38
+ return { byKw, byFmt };
39
+ }
40
+
41
+ module.exports = { OPTIONAL, runtimeFeatures, codeTable };
@@ -0,0 +1,15 @@
1
+ 'use strict';
2
+
3
+ // Browser twin of lib/error-runtime.js, wired by the package.json `browser`
4
+ // field. The shared error runtime is put together from the library's own
5
+ // files with fs, which a build tool does and a page does not; the emitters a
6
+ // page can call (bundleStandalone, toStandaloneModule) never need it. Pulling
7
+ // fs into a browser bundle stopped esbuild outright and left Vite with an
8
+ // empty stand-in.
9
+ const { runtimeFeatures, codeTable } = require('./error-runtime-features');
10
+
11
+ function errorRuntimeSource() {
12
+ throw new Error('errorRuntimeSource() reads the library files and runs at build time, not in a browser');
13
+ }
14
+
15
+ module.exports = { errorRuntimeSource, runtimeFeatures, codeTable };
@@ -26,38 +26,7 @@ const FILES = [
26
26
  // The URI walk formats.js loads; left out with formats (see OPTIONAL).
27
27
  'uri-core',
28
28
  ];
29
- // The parts a build can leave out when none of its schemas needs them. The
30
- // decision is made on the schemas the runtime walks (as embedded, see
31
- // runtimeFeatures) and errs toward keeping a part: a key with that name
32
- // anywhere, even inside an enum value, keeps it. A part left out is replaced
33
- // by a stub that throws, which a schema without that keyword cannot reach.
34
- const OPTIONAL = {
35
- formats: { keys: ['format'], stub: 'module.exports = {};' },
36
- 'safe-regex': { keys: ['pattern', 'patternProperties'], stub: "module.exports = { compileSafe() { throw new Error('ata error runtime: built without the pattern engine'); } };" },
37
- 'unique-items': { keys: ['uniqueItems', 'enum', 'const'], stub: "function no() { throw new Error('ata error runtime: built without deep equality'); }\nmodule.exports = { allDistinct: no, duplicatePair: no, _deq: no };" },
38
- };
39
-
40
- // Which optional parts these schemas need: { formats, 'safe-regex',
41
- // 'unique-items' } as booleans.
42
- function runtimeFeatures(schemas) {
43
- const keys = new Set();
44
- const seen = new Set();
45
- const walk = (n) => {
46
- if (n === null || typeof n !== 'object' || seen.has(n)) return;
47
- seen.add(n);
48
- if (Array.isArray(n)) { for (const x of n) walk(x); return; }
49
- for (const k of Object.keys(n)) { keys.add(k); walk(n[k]); }
50
- };
51
- for (const s of schemas) walk(s);
52
- const out = {};
53
- for (const [part, { keys: ks }] of Object.entries(OPTIONAL)) out[part] = ks.some((k) => keys.has(k));
54
- return out;
55
- }
56
-
57
- function codeTable() {
58
- const { byKw, byFmt } = require('./error-code-of');
59
- return { byKw, byFmt };
60
- }
29
+ const { OPTIONAL, runtimeFeatures, codeTable } = require('./error-runtime-features');
61
30
 
62
31
  // Lines that hold nothing but a // comment are about half of these files.
63
32
  // They go, unless the line sits inside a template literal (tracked by