ata-validator 1.62.1 → 1.63.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.
Files changed (53) hide show
  1. package/README.md +63 -8
  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/codegen-engine.js +72 -3
  9. package/lib/compiled-common.js +2 -1
  10. package/lib/email-literal.js +4 -1
  11. package/lib/formats-source.js +39 -40
  12. package/lib/interpreter-core.js +148 -70
  13. package/lib/ir/emit-deps.mjs +131 -0
  14. package/lib/ir/emit.mjs +707 -0
  15. package/lib/ir/front.mjs +865 -0
  16. package/lib/ir/index.mjs +10 -0
  17. package/lib/ir/refs.mjs +108 -0
  18. package/lib/ir/run.mjs +520 -0
  19. package/lib/ir/table.mjs +233 -0
  20. package/lib/js-compiler-errors.js +9 -4
  21. package/lib/kernels/collapse.mjs +114 -0
  22. package/lib/kernels/custom.mjs +70 -0
  23. package/lib/kernels/fill.mjs +6 -0
  24. package/lib/kernels/formats-small.mjs +40 -0
  25. package/lib/kernels/formats.mjs +29 -0
  26. package/lib/kernels/index.mjs +11 -0
  27. package/lib/kernels/kernels.mjs +285 -0
  28. package/lib/kernels/leaves.mjs +28 -0
  29. package/lib/kernels/order.mjs +25 -0
  30. package/lib/kernels/plan-rich.mjs +17 -0
  31. package/lib/kernels/plan.d.ts +9 -0
  32. package/lib/kernels/plan.mjs +128 -0
  33. package/lib/kernels/preprocess.mjs +223 -0
  34. package/lib/kernels/rank.mjs +97 -0
  35. package/lib/kernels/rt.mjs +249 -0
  36. package/lib/kernels/table.mjs +182 -0
  37. package/lib/kernels/unique.mjs +157 -0
  38. package/lib/native-positions.js +1 -1
  39. package/lib/openapi.d.ts +2 -0
  40. package/lib/openapi.js +128 -0
  41. package/lib/plan-compiler.js +113 -14
  42. package/lib/public-api.js +7 -2
  43. package/lib/rejections.js +3 -1
  44. package/lib/schema-helpers.js +12 -0
  45. package/lib/schema-helpers.mjs +20 -0
  46. package/lib/schema-order.js +4 -2
  47. package/lib/shape-classifier.js +13 -1
  48. package/lib/standard-schema.js +1 -1
  49. package/lib/tier0.js +38 -8
  50. package/lib/validator-core.js +59 -36
  51. package/lib/version.js +1 -1
  52. package/lite.js +7 -2
  53. package/package.json +26 -8
package/README.md CHANGED
@@ -108,12 +108,7 @@ resolves to an entry without the code generator through the `workerd` and `edge-
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
@@ -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) |
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,
@@ -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;
@@ -96,14 +96,23 @@ function dateTimeSource (v, isStr) {
96
96
  // third parameter here would silently receive a context object and emit
97
97
  // `_st=[object Object]`. That declines the whole compile, which is how it was
98
98
  // found.
99
+ // Membership in a set of ASCII characters as one shift: the 32-bit word for
100
+ // c's quarter of the range is picked by comparison and `>>>` reads bit c&31.
101
+ // Four numbers rather than a 128-entry table, so a module carries a few bytes,
102
+ // and one test per character where a chain of comparisons ran up to twelve.
103
+ // tests/test_format_char_masks.js checks every code point against the rule.
104
+ const maskSource = (v, w) => `((${v}<64?${v}<32?${w[0]}:${w[1]}:${v}<96?${w[2]}:${v}<128?${w[3]}:0)>>>${v}&1)`;
105
+ // A hostname label character: a letter, a digit or the hyphen.
106
+ const HOST_CHAR_WORDS = [0, 67051520, 134217726, 134217726];
107
+ const HOST_CHAR_SRC = (v) => maskSource(v, HOST_CHAR_WORDS);
108
+
99
109
  function hostnameRangeSource (v, st, en) {
100
- return `const _st=${st},_en=${en},_n=_en-_st;if(_n===0||_n>253)return false;let _ll=0,_pv=46;` +
101
- `for(let _i=_st;_i<_en;_i++){const _c=${v}.charCodeAt(_i);` +
102
- `if(_c===46){if(_ll===0||_pv===45)return false;_ll=0;_pv=_c;continue}` +
103
- `const _an=(_c>=48&&_c<=57)||(_c>=97&&_c<=122)||(_c>=65&&_c<=90);` +
104
- `if(!_an&&_c!==45)return false;` +
105
- `if(_c===45&&_pv===46)return false;` +
106
- `if(++_ll>63)return false;_pv=_c}` +
110
+ // An empty name never counts a label, so the `_ll===0` test at the end
111
+ // refuses it without a length test of its own.
112
+ return `if(${en}-(${st})>253)return false;let _ll=0,_pv=46;` +
113
+ `for(let _i=${st};_i<${en};_i++){const _c=${v}.charCodeAt(_i);` +
114
+ `if(_c===46){if(_ll===0||_pv===45)return false;_ll=0}` +
115
+ `else if(!${HOST_CHAR_SRC('_c')}||_c===45&&_pv===46||++_ll>63)return false;_pv=_c}` +
107
116
  `if(_ll===0||_pv===45)return false`;
108
117
  }
109
118
 
@@ -257,19 +266,14 @@ function uriTemplateSource (v, isStr) {
257
266
  // trailing or doubled dot is not a mailbox. Domain is a hostname or a bracketed
258
267
  // address literal.
259
268
  // Local-part characters: alphanumerics, the RFC 5322 atext punctuation, and
260
- // the dot that separates atoms. Written as code ranges rather than a regular
261
- // expression because this lands inside every emitted module that validates an
262
- // email, and the expression cost more bytes there than the whole check.
263
- // The emitted form keeps the comparison chain: a standalone module pays for the
264
- // bytes of a 128-entry table in every file that validates an email, and the
265
- // runtime pays for the chain on every call. Different budgets, same answers,
266
- // which `tests/test_format_engine_parity.js` holds.
267
- // Stated as the complement: of the printable range, only twelve characters are
268
- // not atext or the dot, so excluding them is 54 source bytes shorter than listing
269
- // what is allowed, and a standalone module carries this expression. Checked
270
- // against the allowed-list form over every code point from 0 to 65536 in
271
- // tests/test_formats_single_pass.js.
272
- const EMAIL_LOCAL_SRC = (v) => `(${v}>32&&${v}<127&&${v}!==34&&${v}!==40&&${v}!==41&&${v}!==44&&${v}!==58&&${v}!==59&&${v}!==60&&${v}!==62&&${v}!==64&&${v}!==91&&${v}!==92&&${v}!==93)`;
269
+ // the dot that separates atoms, as a mask (maskSource above): every printable
270
+ // character except the twelve that are neither atext nor the dot. It replaced a
271
+ // chain of twelve comparisons, which was more bytes in every module that checks
272
+ // an email and up to twelve tests per character. Checked against the allowed-list
273
+ // form over every code point from 0 to 65536 in tests/test_format_char_masks.js,
274
+ // and against the runtime's check by `tests/test_format_engine_parity.js`.
275
+ const EMAIL_LOCAL_WORDS = [0, 2751458554, 3355443198, 2147483647];
276
+ const EMAIL_LOCAL_SRC = (v) => maskSource(v, EMAIL_LOCAL_WORDS);
273
277
  // Emitted rather than bound as a closure: a standalone module embeds format
274
278
  // functions by their source text, so anything reaching a free variable would
275
279
  // not survive the trip. The domain half reuses hostnameSource, which keeps
@@ -295,31 +299,26 @@ function emailSource (v, isStr) {
295
299
  // character, so in an unquoted address the first one is the separator and the
296
300
  // local part is checked on the way to it. A quoted local part may contain
297
301
  // '@' and keeps the backward scan, which is the rare shape.
298
- // The bounds tests are merged into one condition per branch, and there is no
299
- // separate length check: an empty local part fails `_at<=0` and an empty
300
- // domain fails `_at===_n0-1`, so a short string is already refused. This
301
- // lands in every emitted module that validates an email, and 25 bytes of it
302
- // put the CLI smoke build over its --max-size once.
303
- `const _n0=${v}.length;let _at;` +
302
+ // Both shapes share their variables and one bounds test after the local
303
+ // part: an empty local part fails `_at<1`, an empty domain `_at===_n0-1`,
304
+ // and `_pd` holds the character before the '@' (46 when it is a dot, which
305
+ // a quoted part sets aside). This lands in every emitted module that
306
+ // validates an email, so its bytes are counted (tests/test_email_bytes.js).
307
+ `const _n0=${v}.length;let _at=-1,_qi,_qc,_pd=46;` +
304
308
  `if(${v}.charCodeAt(0)===34){` +
305
- `_at=${v}.lastIndexOf('@');` +
306
- `if(_at<2||_at===_n0-1||_at>64||${v}.charCodeAt(_at-1)!==34)return false;` +
307
- `for(let _qi=1;_qi<_at-1;_qi++){const _qc=${v}.charCodeAt(_qi);` +
308
- `if(_qc===92){const _qp=${v}.charCodeAt(_qi+1);if(_qp<32||_qp>126||_qi+1>=_at-1)return false;_qi++;continue}` +
309
- `if(_qc<32||_qc>126||_qc===34)return false}` +
310
- `}else{` +
311
- `if(${v}.charCodeAt(0)===46)return false;` +
312
- `_at=-1;let _pd=false;` +
313
- `for(let _i=0;_i<_n0;_i++){const _lc=${v}.charCodeAt(_i);if(_lc===64){_at=_i;break}if(!${EMAIL_LOCAL_SRC('_lc')})return false;if(_lc===46){if(_pd)return false;_pd=true}else _pd=false}` +
314
- `if(_at<=0||_at===_n0-1||_at>64||${v}.charCodeAt(_at-1)===46)return false` +
315
- `}` +
309
+ `_at=${v}.lastIndexOf('@');if(_at<2||${v}.charCodeAt(_at-1)!==34)return false;` +
310
+ `for(_qi=1;_qi<_at-1;_qi++){_qc=${v}.charCodeAt(_qi);` +
311
+ `if(_qc===92){_qc=${v}.charCodeAt(++_qi);if(_qc<32||_qc>126||_qi>=_at-1)return false}` +
312
+ `else if(_qc<32||_qc>126||_qc===34)return false}_pd=0` +
313
+ `}else for(_qi=0;_qi<_n0;_qi++){_qc=${v}.charCodeAt(_qi);if(_qc===64){_at=_qi;break}` +
314
+ `if(_qc===46?_pd===46:!${EMAIL_LOCAL_SRC('_qc')})return false;_pd=_qc}` +
315
+ `if(_at<1||_at>64||_at===_n0-1||_pd===46)return false;` +
316
316
  // An address literal is rare enough to pay for its own slice inside the
317
317
  // branch that needs it; the hostname path, which is every real address,
318
318
  // reads the domain in place.
319
319
  `if(${v}.charCodeAt(_at+1)===91){` +
320
- `const _dm=${v}.slice(_at+1);` +
321
- `if(_dm.charCodeAt(_dm.length-1)!==93||_dm.length<=2)return false;` +
322
- `const _in=_dm.slice(1,-1);` +
320
+ `if(${v}.charCodeAt(_n0-1)!==93||_n0-_at<4)return false;` +
321
+ `const _in=${v}.slice(_at+2,-1);` +
323
322
  // The literal is rare, and checked by expression rather than by the
324
323
  // IPv6 and IPv4 parsers, which made it 72% of this check's source:
325
324
  // the same answers (tests/test_ipv6_expression.js), a fraction of the bytes.