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.
- package/README.md +63 -8
- package/build.mjs +1 -0
- package/index.browser.mjs +23 -4
- package/index.bundle.mjs +14 -4
- package/index.edge.mjs +14 -4
- package/index.js +7 -2
- package/lib/aot-build.js +18 -0
- package/lib/codegen-engine.js +72 -3
- package/lib/compiled-common.js +2 -1
- package/lib/email-literal.js +4 -1
- package/lib/formats-source.js +39 -40
- package/lib/interpreter-core.js +148 -70
- package/lib/ir/emit-deps.mjs +131 -0
- package/lib/ir/emit.mjs +707 -0
- package/lib/ir/front.mjs +865 -0
- package/lib/ir/index.mjs +10 -0
- package/lib/ir/refs.mjs +108 -0
- package/lib/ir/run.mjs +520 -0
- package/lib/ir/table.mjs +233 -0
- package/lib/js-compiler-errors.js +9 -4
- package/lib/kernels/collapse.mjs +114 -0
- package/lib/kernels/custom.mjs +70 -0
- package/lib/kernels/fill.mjs +6 -0
- package/lib/kernels/formats-small.mjs +40 -0
- package/lib/kernels/formats.mjs +29 -0
- package/lib/kernels/index.mjs +11 -0
- package/lib/kernels/kernels.mjs +285 -0
- package/lib/kernels/leaves.mjs +28 -0
- package/lib/kernels/order.mjs +25 -0
- package/lib/kernels/plan-rich.mjs +17 -0
- package/lib/kernels/plan.d.ts +9 -0
- package/lib/kernels/plan.mjs +128 -0
- package/lib/kernels/preprocess.mjs +223 -0
- package/lib/kernels/rank.mjs +97 -0
- package/lib/kernels/rt.mjs +249 -0
- package/lib/kernels/table.mjs +182 -0
- package/lib/kernels/unique.mjs +157 -0
- package/lib/native-positions.js +1 -1
- package/lib/openapi.d.ts +2 -0
- package/lib/openapi.js +128 -0
- package/lib/plan-compiler.js +113 -14
- package/lib/public-api.js +7 -2
- package/lib/rejections.js +3 -1
- package/lib/schema-helpers.js +12 -0
- package/lib/schema-helpers.mjs +20 -0
- package/lib/schema-order.js +4 -2
- package/lib/shape-classifier.js +13 -1
- package/lib/standard-schema.js +1 -1
- package/lib/tier0.js +38 -8
- package/lib/validator-core.js +59 -36
- package/lib/version.js +1 -1
- package/lite.js +7 -2
- 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
|
|
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.
|
|
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) |
|
|
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
|
-
|
|
17
|
-
|
|
18
|
-
|
|
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
|
-
|
|
12
|
-
|
|
13
|
-
export const
|
|
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
|
-
|
|
10
|
-
|
|
11
|
-
export const
|
|
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
|
|
151
|
-
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/codegen-engine.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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,
|
package/lib/compiled-common.js
CHANGED
|
@@ -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
|
-
|
|
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.
|
package/lib/email-literal.js
CHANGED
|
@@ -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
|
-
|
|
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;
|
package/lib/formats-source.js
CHANGED
|
@@ -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
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
`const
|
|
104
|
-
`if(
|
|
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
|
|
261
|
-
//
|
|
262
|
-
//
|
|
263
|
-
//
|
|
264
|
-
//
|
|
265
|
-
//
|
|
266
|
-
|
|
267
|
-
|
|
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
|
-
//
|
|
299
|
-
//
|
|
300
|
-
//
|
|
301
|
-
// lands in every emitted module that
|
|
302
|
-
//
|
|
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
|
-
`
|
|
307
|
-
`
|
|
308
|
-
`if(_qc
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
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
|
-
`
|
|
321
|
-
`
|
|
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.
|