ata-validator 0.18.2 → 0.20.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +38 -0
- package/build.d.ts +28 -0
- package/build.mjs +3 -0
- package/index.d.ts +8 -2
- package/index.js +26 -370
- package/lib/aot-build.js +30 -1
- package/lib/aot.js +410 -0
- package/lib/native-load.browser.js +8 -0
- package/lib/native-load.js +21 -0
- package/lib/safe-regex-source.js +8 -0
- package/lib/t.js +140 -0
- package/lib/version.js +10 -0
- package/package.json +14 -5
- package/scripts/regen-safe-regex-source.js +33 -0
- package/t.d.ts +186 -0
- package/t.js +9 -0
- package/t.mjs +6 -0
package/lib/aot.js
ADDED
|
@@ -0,0 +1,410 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// All ahead-of-time (AOT) code generation lives here so the default and browser
|
|
4
|
+
// entries can stay free of `fs`, `path`, `__dirname`, and `pkg-prebuilds`.
|
|
5
|
+
// Browser bundles get `lib/aot.browser.js` instead (see package.json `browser`
|
|
6
|
+
// field), which throws if anyone tries to call an AOT function from the browser.
|
|
7
|
+
//
|
|
8
|
+
// Public surface (called by index.js and ata-validator/build):
|
|
9
|
+
// toStandalone(validator)
|
|
10
|
+
// toStandaloneModule(validator, opts)
|
|
11
|
+
// bundle(Validator, schemas, opts)
|
|
12
|
+
// bundleStandalone(Validator, schemas, opts)
|
|
13
|
+
// bundleCompact(Validator, schemas, opts)
|
|
14
|
+
// loadBundle(Validator, mods, schemas, opts)
|
|
15
|
+
|
|
16
|
+
const { compileToJSCodegenWithErrors } = require('./js-compiler');
|
|
17
|
+
const SAFE_REGEX_SOURCE = require('./safe-regex-source');
|
|
18
|
+
|
|
19
|
+
// Embedded verbatim in standalone modules so the output file has no runtime
|
|
20
|
+
// dependency on ata-validator. ASCII fast-path plus surrogate-aware slow path.
|
|
21
|
+
const _CP_LEN_SOURCE = `function _cpLen(s) {
|
|
22
|
+
const len = s.length;
|
|
23
|
+
for (let i = 0; i < len; i++) {
|
|
24
|
+
if (s.charCodeAt(i) >= 0xD800 && s.charCodeAt(i) <= 0xDBFF) {
|
|
25
|
+
let n = 0; for (const _ of s) n++; return n;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
return len;
|
|
29
|
+
}`;
|
|
30
|
+
|
|
31
|
+
// The linear-time regex engine, inlined verbatim into standalone output so a
|
|
32
|
+
// compiled module that uses safe `pattern` matchers has no runtime dependency
|
|
33
|
+
// on ata-validator. The engine source is baked into `lib/safe-regex-source.js`
|
|
34
|
+
// at build time (see `scripts/regen-safe-regex-source.js`), so this path has
|
|
35
|
+
// no `fs`/`path`/`__dirname` reads — safe in browser bundles too. The embed
|
|
36
|
+
// strips the strict directive and CommonJS exports and adds the `__ataSafeRe`
|
|
37
|
+
// alias the emitted code calls. The engine has no eval/new Function, so the
|
|
38
|
+
// embed is CSP-safe.
|
|
39
|
+
let _safeRegexEmbed = null;
|
|
40
|
+
function getSafeRegexEmbed() {
|
|
41
|
+
if (_safeRegexEmbed === null) {
|
|
42
|
+
const body = SAFE_REGEX_SOURCE
|
|
43
|
+
.replace(/^'use strict'\s*\n/, '')
|
|
44
|
+
.replace(/\nmodule\.exports[^\n]*\n?/, '\n');
|
|
45
|
+
_safeRegexEmbed = body.trimEnd() + '\nconst __ataSafeRe = compileSafe;';
|
|
46
|
+
}
|
|
47
|
+
return _safeRegexEmbed;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Returns the engine embed when any supplied compiled function references the
|
|
51
|
+
// safe matcher (jsFn._usesSafeRe), else an empty string so non-pattern modules
|
|
52
|
+
// pay zero bytes.
|
|
53
|
+
function safeRePrelude(...fns) {
|
|
54
|
+
return fns.some((f) => f && f._usesSafeRe) ? getSafeRegexEmbed() + '\n' : '';
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// --- Standalone pre-compilation ---
|
|
58
|
+
// Generate a JS module string that can be written to a file.
|
|
59
|
+
// On next startup, load with Validator.fromStandalone() -- zero compile time.
|
|
60
|
+
function toStandalone(validator) {
|
|
61
|
+
validator._ensureCompiled();
|
|
62
|
+
const jsFn = validator._jsFn;
|
|
63
|
+
if (!jsFn || !jsFn._source) return null;
|
|
64
|
+
const src = jsFn._source;
|
|
65
|
+
const hybridSrc = jsFn._hybridSource || '';
|
|
66
|
+
const preambleSrc = jsFn._preambleSource || '';
|
|
67
|
+
|
|
68
|
+
// Also capture error function source for zero-compile standalone load
|
|
69
|
+
const jsErrFn = compileToJSCodegenWithErrors(
|
|
70
|
+
typeof validator._schemaObj === 'object' ? validator._schemaObj : {},
|
|
71
|
+
);
|
|
72
|
+
const errSrc = jsErrFn && jsErrFn._errSource ? jsErrFn._errSource : '';
|
|
73
|
+
|
|
74
|
+
return `// Auto-generated by ata-validator — do not edit
|
|
75
|
+
'use strict';
|
|
76
|
+
${_CP_LEN_SOURCE}
|
|
77
|
+
${safeRePrelude(jsFn, jsErrFn)}${preambleSrc}
|
|
78
|
+
const boolFn = function(d) {
|
|
79
|
+
${src}
|
|
80
|
+
};
|
|
81
|
+
const hybridFactory = function(R, E) {
|
|
82
|
+
return function(d) {
|
|
83
|
+
${hybridSrc}
|
|
84
|
+
};
|
|
85
|
+
};
|
|
86
|
+
${errSrc ? `const errFn = function(d, _all) {\n ${errSrc}\n};` : 'const errFn = null;'}
|
|
87
|
+
module.exports = { boolFn, hybridFactory, errFn };
|
|
88
|
+
`;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// --- Fully standalone module ---
|
|
92
|
+
// Generates a self-contained module that can be imported directly without
|
|
93
|
+
// pulling in ata-validator at runtime. Browser bundle gets only the
|
|
94
|
+
// generated validator (~2 KB typical) instead of the 165 KB compiler.
|
|
95
|
+
//
|
|
96
|
+
// import { validate, isValid } from './user-validator.mjs'
|
|
97
|
+
// if (isValid(data)) { ... }
|
|
98
|
+
//
|
|
99
|
+
// format: 'esm' | 'cjs'. Default 'esm'.
|
|
100
|
+
// abortEarly: if true, invalid result is a shared stub; smaller output.
|
|
101
|
+
function toStandaloneModule(validator, opts) {
|
|
102
|
+
validator._ensureCompiled();
|
|
103
|
+
const jsFn = validator._jsFn;
|
|
104
|
+
if (!jsFn || !jsFn._source) return null;
|
|
105
|
+
const format = (opts && opts.format) || 'esm';
|
|
106
|
+
const abortEarly = !!(opts && opts.abortEarly);
|
|
107
|
+
const source = !!(opts && opts.source);
|
|
108
|
+
const sourceMap = opts && opts.sourceMap ? opts.sourceMap : null;
|
|
109
|
+
const schemaFile = opts && opts.schemaFile ? opts.schemaFile : null;
|
|
110
|
+
const src = jsFn._source;
|
|
111
|
+
|
|
112
|
+
let errCore = '';
|
|
113
|
+
let jsErrFn = null;
|
|
114
|
+
if (!abortEarly) {
|
|
115
|
+
jsErrFn = compileToJSCodegenWithErrors(
|
|
116
|
+
typeof validator._schemaObj === 'object' ? validator._schemaObj : {},
|
|
117
|
+
null,
|
|
118
|
+
validator._userFormats,
|
|
119
|
+
(source && sourceMap && schemaFile) ? { sourceMap, schemaFile } : null,
|
|
120
|
+
);
|
|
121
|
+
const errSrc = jsErrFn && jsErrFn._errSource ? jsErrFn._errSource : '';
|
|
122
|
+
if (errSrc) {
|
|
123
|
+
errCore = `const errFn = function(d, _all) {\n ${errSrc}\n};\n`;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// Schema-source frames are baked as literals inside each emitted error so
|
|
128
|
+
// consumers don't need a runtime lookup. We still expose the schema file
|
|
129
|
+
// as a sentinel constant when --source is on — handy for introspection
|
|
130
|
+
// and visible in source graphs. With --no-source, the constant is omitted
|
|
131
|
+
// entirely so size budgets and grep-based "is this source-mapped?" checks
|
|
132
|
+
// both work.
|
|
133
|
+
const schemaSourceConst = (source && schemaFile)
|
|
134
|
+
? `const __ATA_SCHEMA_SOURCE__ = ${JSON.stringify({ file: schemaFile })};\n`
|
|
135
|
+
: '';
|
|
136
|
+
|
|
137
|
+
// Serialize closure vars referenced in _fn body: regex, sub-validators, sets.
|
|
138
|
+
let closureDecls = '';
|
|
139
|
+
if (jsFn._closures && jsFn._closures.length > 0) {
|
|
140
|
+
const lines = [];
|
|
141
|
+
for (const { name, val } of jsFn._closures) {
|
|
142
|
+
if (Array.isArray(val)) {
|
|
143
|
+
lines.push(`const ${name} = ${JSON.stringify(val)};`);
|
|
144
|
+
continue;
|
|
145
|
+
}
|
|
146
|
+
if (val && val.__ataSafe) {
|
|
147
|
+
lines.push(`const ${name} = __ataSafeRe(${JSON.stringify(val.source)});`);
|
|
148
|
+
} else if (val instanceof RegExp) {
|
|
149
|
+
const flags = val.flags;
|
|
150
|
+
lines.push(`const ${name} = new RegExp(${JSON.stringify(val.source)}${flags ? ', ' + JSON.stringify(flags) : ''});`);
|
|
151
|
+
} else if (val instanceof Set) {
|
|
152
|
+
lines.push(`const ${name} = new Set(${JSON.stringify([...val])});`);
|
|
153
|
+
} else if (typeof val === 'function') {
|
|
154
|
+
// new Function('_ppv', body) — extract body from toString()
|
|
155
|
+
const str = val.toString();
|
|
156
|
+
// Matches: "function anonymous(_ppv\n) {\nbody\n}" or "function(_ppv){body}"
|
|
157
|
+
const m = str.match(/^function[^(]*\([^)]*\)\s*\{([\s\S]*)\}$/)
|
|
158
|
+
const body = m ? m[1].trim() : str;
|
|
159
|
+
lines.push(`const ${name} = function(_ppv) { ${body} };`);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
if (lines.length) closureDecls = lines.join('\n') + '\n';
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// Hoisted oneOf/anyOf branch checks live in the boolean fn's preamble (the
|
|
166
|
+
// runtime emits them before the function). The standalone module must declare
|
|
167
|
+
// them at module scope too, or _fn references undefined names (e.g. _af1_b0).
|
|
168
|
+
const preambleDecls = jsFn._preambleSource ? jsFn._preambleSource + '\n' : '';
|
|
169
|
+
|
|
170
|
+
// User-supplied format functions are referenced as _uf_<name> by both the
|
|
171
|
+
// boolean (_fn) and error (errFn) bodies. Embed them via Function#toString
|
|
172
|
+
// so the standalone module stays self-contained.
|
|
173
|
+
let formatDecls = '';
|
|
174
|
+
if (jsFn._formatClosures && jsFn._formatClosures.length > 0) {
|
|
175
|
+
formatDecls = jsFn._formatClosures
|
|
176
|
+
.map(({ name, fn }) => `const ${name} = ${fn.toString()};`)
|
|
177
|
+
.join('\n') + '\n';
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
const validBody = errCore
|
|
181
|
+
? 'return _fn(data) ? VALID : { valid: false, errors: errFn(data, true).errors }'
|
|
182
|
+
: 'return _fn(data) ? VALID : ABORT';
|
|
183
|
+
|
|
184
|
+
const exports = format === 'esm'
|
|
185
|
+
? `export { validate, isValid };\nexport default { validate, isValid };\n`
|
|
186
|
+
: `module.exports = { validate, isValid };\nmodule.exports.default = module.exports;\n`;
|
|
187
|
+
|
|
188
|
+
return `// Auto-generated by ata-validator — do not edit.
|
|
189
|
+
// Schema is embedded; runtime has zero dependency on ata-validator.
|
|
190
|
+
'use strict';
|
|
191
|
+
${_CP_LEN_SOURCE}
|
|
192
|
+
${safeRePrelude(jsFn, jsErrFn)}${schemaSourceConst}const VALID = Object.freeze({ valid: true, errors: Object.freeze([]) });
|
|
193
|
+
const ABORT = Object.freeze({
|
|
194
|
+
valid: false,
|
|
195
|
+
errors: Object.freeze([Object.freeze({
|
|
196
|
+
code: 'ATA9000',
|
|
197
|
+
message: 'validation failed',
|
|
198
|
+
keyword: '__abort_early__',
|
|
199
|
+
path: '',
|
|
200
|
+
})]),
|
|
201
|
+
});
|
|
202
|
+
${closureDecls}${preambleDecls}${formatDecls}const _fn = function(d) {
|
|
203
|
+
${src}
|
|
204
|
+
};
|
|
205
|
+
${errCore}function isValid(data) { return _fn(data); }
|
|
206
|
+
function validate(data) { ${validBody}; }
|
|
207
|
+
${exports}`;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// Bundle multiple validators into a single JS file for fast startup.
|
|
211
|
+
// Usage:
|
|
212
|
+
// const bundle = Validator.bundle([schema1, schema2, ...]);
|
|
213
|
+
// fs.writeFileSync('validators.js', bundle);
|
|
214
|
+
// // On startup:
|
|
215
|
+
// const validators = Validator.loadBundle(require('./validators.js'), [schema1, schema2, ...]);
|
|
216
|
+
function bundle(Validator, schemas, opts) {
|
|
217
|
+
const parts = schemas.map((schema) => {
|
|
218
|
+
const v = new Validator(schema, opts);
|
|
219
|
+
const standalone = toStandalone(v);
|
|
220
|
+
if (!standalone) return 'null';
|
|
221
|
+
return (
|
|
222
|
+
'(function(){' +
|
|
223
|
+
standalone
|
|
224
|
+
.replace("'use strict';", '')
|
|
225
|
+
.replace('module.exports = ', 'return ') +
|
|
226
|
+
'})()'
|
|
227
|
+
);
|
|
228
|
+
});
|
|
229
|
+
return "'use strict';\nmodule.exports = [\n" + parts.join(',\n') + '\n];\n';
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// Zero-dependency self-contained bundle — no require('ata-validator') needed at runtime.
|
|
233
|
+
// opts.format: 'cjs' (default) or 'esm'.
|
|
234
|
+
// opts.formats: { name: fn } — embedded in the output via Function#toString.
|
|
235
|
+
function bundleStandalone(Validator, schemas, opts) {
|
|
236
|
+
// Cross-schema $ref resolution: only meaningful when at least one schema has
|
|
237
|
+
// an $id. Skip the schemas-as-map plumbing when none of them do.
|
|
238
|
+
const haveIds = schemas.some((s) => s && typeof s === 'object' && s.$id);
|
|
239
|
+
const bundleOpts = haveIds ? { ...(opts || {}), schemas } : (opts || {});
|
|
240
|
+
const format = (opts && opts.format) || 'cjs';
|
|
241
|
+
const R = 'Object.freeze({valid:true,errors:Object.freeze([])})';
|
|
242
|
+
let bundleUsesSafeRe = false;
|
|
243
|
+
const fns = schemas.map((schema) => {
|
|
244
|
+
const v = new Validator(schema, bundleOpts);
|
|
245
|
+
v._ensureCompiled();
|
|
246
|
+
const jsFn = v._jsFn;
|
|
247
|
+
if (!jsFn || !jsFn._hybridSource) return 'null';
|
|
248
|
+
const jsErrFn = compileToJSCodegenWithErrors(
|
|
249
|
+
typeof schema === 'string' ? JSON.parse(schema) : schema,
|
|
250
|
+
v._schemaMap,
|
|
251
|
+
v._userFormats,
|
|
252
|
+
);
|
|
253
|
+
if (jsFn._usesSafeRe || (jsErrFn && jsErrFn._usesSafeRe)) bundleUsesSafeRe = true;
|
|
254
|
+
const errBody =
|
|
255
|
+
jsErrFn && jsErrFn._errSource
|
|
256
|
+
? jsErrFn._errSource
|
|
257
|
+
: "return{valid:false,errors:[{code:'error',path:'',message:'validation failed'}]}";
|
|
258
|
+
// Serialize custom format closures so the bundle has no runtime dep on ata.
|
|
259
|
+
let preamble = '';
|
|
260
|
+
if (jsFn._formatClosures) {
|
|
261
|
+
preamble = jsFn._formatClosures
|
|
262
|
+
.map(({ name, fn }) => `var ${name}=${fn.toString()};`)
|
|
263
|
+
.join('\n');
|
|
264
|
+
}
|
|
265
|
+
// Include hoisted anyOf/oneOf branch helpers (e.g. `_af1_b0`) so the
|
|
266
|
+
// bundle output is self-contained. `toStandalone` emits this same source
|
|
267
|
+
// for single-schema standalone output.
|
|
268
|
+
if (jsFn._preambleSource) {
|
|
269
|
+
preamble = preamble ? `${preamble}\n${jsFn._preambleSource}` : jsFn._preambleSource;
|
|
270
|
+
}
|
|
271
|
+
if (opts && opts.verbose) {
|
|
272
|
+
// Embed the schema and a small resolver so errors carry parentSchema.
|
|
273
|
+
const schemaLit = JSON.stringify(typeof schema === 'string' ? JSON.parse(schema) : schema);
|
|
274
|
+
return `(function(R){${preamble}var _S=${schemaLit};function _PS(p){if(!p||p[0]!=='#')return undefined;var s=p.slice(1);if(!s)return _S;var ps=s.split('/').filter(Boolean).map(function(x){return x.replace(/~1/g,'/').replace(/~0/g,'~')});var t=_S;for(var i=0;i<ps.length-1;i++){if(t==null||typeof t!=='object')return undefined;t=t[ps[i]]}return t}var E=function(d){var _all=true;${errBody}};var _v=function(d){${jsFn._hybridSource}};return function(d){var r=_v(d);if(r&&r.valid===false&&r.errors){var es=[];for(var i=0;i<r.errors.length;i++){var e=r.errors[i];es.push(Object.assign({},e,{parentSchema:_PS(e.schemaPath)}))}return{valid:false,errors:es}}return r}})(R)`;
|
|
275
|
+
}
|
|
276
|
+
return `(function(R){${preamble}var E=function(d){var _all=true;${errBody}};return function(d){${jsFn._hybridSource}}})(R)`;
|
|
277
|
+
});
|
|
278
|
+
const arr = `[${fns.join(',')}]`;
|
|
279
|
+
const safeEmbed = bundleUsesSafeRe ? getSafeRegexEmbed() + '\n' : '';
|
|
280
|
+
if (format === 'esm') {
|
|
281
|
+
return `// Auto-generated by ata-validator — do not edit\n${safeEmbed}const R=${R};\nconst validators=${arr};\nexport default validators;\nexport { validators };\n`;
|
|
282
|
+
}
|
|
283
|
+
return `'use strict';\n${safeEmbed}var R=${R};\nmodule.exports=[${fns.join(',')}];\n`;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// Compact bundle: deduplicated code. Shared template functions + per-schema params.
|
|
287
|
+
// Much smaller file → faster V8 parse → faster startup.
|
|
288
|
+
// opts.format: 'cjs' (default) or 'esm'.
|
|
289
|
+
function bundleCompact(Validator, schemas, opts) {
|
|
290
|
+
const haveIds = schemas.some((s) => s && typeof s === 'object' && s.$id);
|
|
291
|
+
const bundleOpts = haveIds ? { ...(opts || {}), schemas } : (opts || {});
|
|
292
|
+
const format = (opts && opts.format) || 'cjs';
|
|
293
|
+
let bundleUsesSafeRe = false;
|
|
294
|
+
// Analyze schemas and group by structure
|
|
295
|
+
const entries = schemas.map((schema) => {
|
|
296
|
+
const v = new Validator(schema, bundleOpts);
|
|
297
|
+
v._ensureCompiled();
|
|
298
|
+
const jsFn = v._jsFn;
|
|
299
|
+
if (!jsFn || !jsFn._hybridSource) return null;
|
|
300
|
+
const jsErrFn = compileToJSCodegenWithErrors(
|
|
301
|
+
typeof schema === 'string' ? JSON.parse(schema) : schema,
|
|
302
|
+
v._schemaMap,
|
|
303
|
+
v._userFormats,
|
|
304
|
+
);
|
|
305
|
+
if (jsFn._usesSafeRe || (jsErrFn && jsErrFn._usesSafeRe)) bundleUsesSafeRe = true;
|
|
306
|
+
// Hoisted anyOf/oneOf branch helpers (e.g. `_af1_b0`) must travel with the
|
|
307
|
+
// hybrid body or it references undefined names. Prepending keeps dedup honest:
|
|
308
|
+
// schemas with different branch sets no longer collide on body alone.
|
|
309
|
+
const hybrid = jsFn._preambleSource
|
|
310
|
+
? `${jsFn._preambleSource}\n${jsFn._hybridSource}`
|
|
311
|
+
: jsFn._hybridSource;
|
|
312
|
+
return {
|
|
313
|
+
hybrid,
|
|
314
|
+
err: jsErrFn && jsErrFn._errSource ? jsErrFn._errSource : null,
|
|
315
|
+
fmt: jsFn._formatClosures || null,
|
|
316
|
+
};
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
// Deduplicate function bodies — many schemas produce identical or near-identical code
|
|
320
|
+
const bodyMap = new Map(); // body → index
|
|
321
|
+
const bodies = [];
|
|
322
|
+
const errMap = new Map();
|
|
323
|
+
const errBodies = [];
|
|
324
|
+
|
|
325
|
+
const indices = entries.map((e) => {
|
|
326
|
+
if (!e) return [-1, -1];
|
|
327
|
+
let hi = bodyMap.get(e.hybrid);
|
|
328
|
+
if (hi === undefined) {
|
|
329
|
+
hi = bodies.length;
|
|
330
|
+
bodies.push(e.hybrid);
|
|
331
|
+
bodyMap.set(e.hybrid, hi);
|
|
332
|
+
}
|
|
333
|
+
let ei = -1;
|
|
334
|
+
if (e.err) {
|
|
335
|
+
ei = errMap.get(e.err);
|
|
336
|
+
if (ei === undefined) {
|
|
337
|
+
ei = errBodies.length;
|
|
338
|
+
errBodies.push(e.err);
|
|
339
|
+
errMap.set(e.err, ei);
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
return [hi, ei];
|
|
343
|
+
});
|
|
344
|
+
|
|
345
|
+
// Generate compact bundle
|
|
346
|
+
const isEsm = format === 'esm';
|
|
347
|
+
let out = isEsm
|
|
348
|
+
? '// Auto-generated by ata-validator — do not edit\n'
|
|
349
|
+
: "'use strict';\n";
|
|
350
|
+
if (bundleUsesSafeRe) out += getSafeRegexEmbed() + '\n';
|
|
351
|
+
const declKW = isEsm ? 'const' : 'var';
|
|
352
|
+
out += `${declKW} R=Object.freeze({valid:true,errors:Object.freeze([])});\n`;
|
|
353
|
+
|
|
354
|
+
// User format functions are referenced as _uf_<name> by the hybrid and error
|
|
355
|
+
// bodies. Collect them across all schemas (deduped by name) and embed via
|
|
356
|
+
// Function#toString so the bundle stays self-contained.
|
|
357
|
+
const fmtSeen = new Set();
|
|
358
|
+
for (const e of entries) {
|
|
359
|
+
if (!e || !e.fmt) continue;
|
|
360
|
+
for (const { name, fn } of e.fmt) {
|
|
361
|
+
if (fmtSeen.has(name)) continue;
|
|
362
|
+
fmtSeen.add(name);
|
|
363
|
+
out += `${declKW} ${name}=${fn.toString()};\n`;
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
// Shared hybrid factories
|
|
368
|
+
out += `${declKW} H=[\n`;
|
|
369
|
+
out += bodies
|
|
370
|
+
.map((b) => `function(R,E){return function(d){${b}}}`)
|
|
371
|
+
.join(',\n');
|
|
372
|
+
out += '\n];\n';
|
|
373
|
+
|
|
374
|
+
// Shared error functions
|
|
375
|
+
out += `${declKW} EF=[\n`;
|
|
376
|
+
out += errBodies.map((b) => `function(d){var _all=true;${b}}`).join(',\n');
|
|
377
|
+
out += '\n];\n';
|
|
378
|
+
|
|
379
|
+
// Build validators from shared templates
|
|
380
|
+
const arrBody = indices
|
|
381
|
+
.map(([hi, ei]) => {
|
|
382
|
+
if (hi < 0) return 'null';
|
|
383
|
+
if (ei >= 0) return `H[${hi}](R,EF[${ei}])`;
|
|
384
|
+
return `H[${hi}](R,function(){return{valid:false,errors:[]}})`;
|
|
385
|
+
})
|
|
386
|
+
.join(',');
|
|
387
|
+
if (isEsm) {
|
|
388
|
+
out += `const validators=[${arrBody}];\nexport default validators;\nexport { validators };\n`;
|
|
389
|
+
} else {
|
|
390
|
+
out += `module.exports=[${arrBody}];\n`;
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
return out;
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
function loadBundle(Validator, mods, schemas, opts) {
|
|
397
|
+
return schemas.map((schema, i) => {
|
|
398
|
+
if (mods[i]) return Validator.fromStandalone(mods[i], schema, opts);
|
|
399
|
+
return new Validator(schema, opts);
|
|
400
|
+
});
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
module.exports = {
|
|
404
|
+
toStandalone,
|
|
405
|
+
toStandaloneModule,
|
|
406
|
+
bundle,
|
|
407
|
+
bundleStandalone,
|
|
408
|
+
bundleCompact,
|
|
409
|
+
loadBundle,
|
|
410
|
+
};
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// Locate and load the optional native addon (pkg-prebuilds). Lives in its own
|
|
4
|
+
// module so the default and browser entries can stay free of `pkg-prebuilds`,
|
|
5
|
+
// `__dirname`, and `path` references. In browser bundles this file is replaced
|
|
6
|
+
// with `native-load.browser.js` via the package.json `browser` field, so the
|
|
7
|
+
// `pkg-prebuilds` import never enters the bundle in the first place.
|
|
8
|
+
//
|
|
9
|
+
// Returns the loaded native binding object, or `null` if no prebuild matches
|
|
10
|
+
// (musl/Alpine, unsupported arch, devs running before `npm run build`, etc.).
|
|
11
|
+
// The JS codegen path covers all core validation when this returns null.
|
|
12
|
+
|
|
13
|
+
module.exports = function loadNative() {
|
|
14
|
+
try {
|
|
15
|
+
const path = require('path');
|
|
16
|
+
const pkg = require('pkg-prebuilds');
|
|
17
|
+
return pkg(path.join(__dirname, '..'), require('../binding-options'));
|
|
18
|
+
} catch {
|
|
19
|
+
return null;
|
|
20
|
+
}
|
|
21
|
+
};
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// Auto-generated by scripts/regen-safe-regex-source.js — do not edit.
|
|
4
|
+
// Mirrors `lib/safe-regex.js` verbatim so `lib/aot.js` can embed the engine
|
|
5
|
+
// into standalone output without a runtime `fs` read. Kept in sync by
|
|
6
|
+
// `tests/test_safe_regex_source_sync.js`.
|
|
7
|
+
|
|
8
|
+
module.exports = "'use strict'\n\n// Linear-time regex engine for JSON Schema `pattern`, used in place of JS RegExp\n// so an adversarial input cannot trigger catastrophic backtracking (ReDoS).\n//\n// It is a Pike VM: the pattern compiles to a small instruction program, and the\n// VM simulates all NFA threads in lockstep over the input, deduping by program\n// counter. Runtime is O(input * program), with no backtracking.\n//\n// Supported (the RE2 subset, which is what ata's native path also accepts):\n// literals, ., character classes, \\d \\w \\s \\D \\W \\S, anchors ^ $, quantifiers\n// * + ? {n} {n,} {n,m} (greedy or lazy, same language for a boolean test),\n// groups ( ) (?: ), alternation |. Backreferences and lookaround are not\n// supported by linear engines; compileSafe throws on them so the caller can\n// decide (ata's codegen rejects such schemas rather than risk a hang).\n\nconst WS = [[9, 13], [32, 32], [160, 160]]\nconst DIGIT = [[48, 57]]\nconst WORD = [[48, 57], [65, 90], [97, 122], [95, 95]]\n\nfunction parse (src) {\n let i = 0\n const len = src.length\n const peek = () => src[i]\n const eof = () => i >= len\n\n function parseAlt () {\n const opts = [parseConcat()]\n while (!eof() && peek() === '|') { i++; opts.push(parseConcat()) }\n return opts.length === 1 ? opts[0] : { t: 'alt', opts }\n }\n\n function parseConcat () {\n const parts = []\n while (!eof() && peek() !== '|' && peek() !== ')') parts.push(parseRepeat())\n if (parts.length === 0) return { t: 'empty' }\n return parts.length === 1 ? parts[0] : { t: 'concat', parts }\n }\n\n function parseRepeat () {\n let node = parseAtom()\n while (!eof()) {\n const ch = peek()\n if (ch === '*') { i++; node = { t: 'star', child: node } }\n else if (ch === '+') { i++; node = { t: 'plus', child: node } }\n else if (ch === '?') { i++; node = { t: 'quest', child: node } }\n else if (ch === '{') {\n const saved = i\n const q = tryQuantifier()\n if (!q) { i = saved; break }\n node = { t: 'repeat', child: node, min: q.min, max: q.max }\n } else break\n // a trailing ? makes the quantifier lazy; same language for a boolean test\n if (!eof() && peek() === '?') i++\n }\n return node\n }\n\n function tryQuantifier () {\n // assumes current char is '{'\n i++\n let min = ''\n while (!eof() && /[0-9]/.test(peek())) { min += peek(); i++ }\n if (min === '') return null\n let max\n if (peek() === '}') { i++; return { min: +min, max: +min } }\n if (peek() === ',') {\n i++\n let m = ''\n while (!eof() && /[0-9]/.test(peek())) { m += peek(); i++ }\n if (peek() !== '}') return null\n i++\n max = m === '' ? Infinity : +m\n return { min: +min, max }\n }\n return null\n }\n\n function parseAtom () {\n const ch = peek()\n if (ch === '(') {\n i++\n if (src[i] === '?') {\n if (src[i + 1] === ':') { i += 2 }\n else throw new Error('unsupported group (lookaround/named) in pattern')\n }\n const child = parseAlt()\n if (peek() !== ')') throw new Error('unbalanced ( in pattern')\n i++\n return { t: 'group', child }\n }\n if (ch === '[') return parseClass()\n if (ch === '.') { i++; return { t: 'any' } }\n if (ch === '^') { i++; return { t: 'bol' } }\n if (ch === '$') { i++; return { t: 'eol' } }\n if (ch === '\\\\') return parseEscape(false)\n if (ch === ')' || ch === '|') return { t: 'empty' }\n i++\n return { t: 'char', c: ch.charCodeAt(0) }\n }\n\n function parseClass () {\n i++ // [\n let neg = false\n if (peek() === '^') { neg = true; i++ }\n const ranges = []\n while (!eof() && peek() !== ']') {\n let lo\n if (peek() === '\\\\') {\n const esc = parseEscape(true)\n if (esc.t === 'classpart') { for (const r of esc.ranges) ranges.push(r); continue }\n lo = esc.c\n } else { lo = peek().charCodeAt(0); i++ }\n if (peek() === '-' && src[i + 1] !== ']' && i + 1 < len) {\n i++ // -\n let hi\n if (peek() === '\\\\') { const e = parseEscape(true); hi = e.c } else { hi = peek().charCodeAt(0); i++ }\n ranges.push([lo, hi])\n } else {\n ranges.push([lo, lo])\n }\n }\n if (peek() !== ']') throw new Error('unbalanced [ in pattern')\n i++\n return { t: 'class', neg, ranges }\n }\n\n function parseEscape (inClass) {\n i++ // backslash\n if (eof()) throw new Error('trailing backslash in pattern')\n const ch = peek(); i++\n switch (ch) {\n case 'd': return inClass ? { t: 'classpart', ranges: DIGIT } : { t: 'class', neg: false, ranges: DIGIT }\n case 'w': return inClass ? { t: 'classpart', ranges: WORD } : { t: 'class', neg: false, ranges: WORD }\n case 's': return inClass ? { t: 'classpart', ranges: WS } : { t: 'class', neg: false, ranges: WS }\n case 'D': if (inClass) throw new Error('\\\\D inside a class is not supported'); return { t: 'class', neg: true, ranges: DIGIT }\n case 'W': if (inClass) throw new Error('\\\\W inside a class is not supported'); return { t: 'class', neg: true, ranges: WORD }\n case 'S': if (inClass) throw new Error('\\\\S inside a class is not supported'); return { t: 'class', neg: true, ranges: WS }\n case 'n': return { t: 'char', c: 10 }\n case 'r': return { t: 'char', c: 13 }\n case 't': return { t: 'char', c: 9 }\n case 'f': return { t: 'char', c: 12 }\n case 'v': return { t: 'char', c: 11 }\n case '0': return { t: 'char', c: 0 }\n case 'x': { const h = src.slice(i, i + 2); i += 2; return { t: 'char', c: parseInt(h, 16) } }\n case 'u': { const h = src.slice(i, i + 4); i += 4; return { t: 'char', c: parseInt(h, 16) } }\n case 'b': if (inClass) return { t: 'char', c: 8 }; throw new Error('\\\\b word boundary is not supported')\n default:\n if (/[1-9]/.test(ch)) throw new Error('backreferences are not supported in pattern')\n return { t: 'char', c: ch.charCodeAt(0) }\n }\n }\n\n const ast = parseAlt()\n if (!eof()) throw new Error('unexpected \"' + peek() + '\" in pattern')\n return ast\n}\n\nfunction compileProg (ast) {\n const prog = []\n const emit = (op, extra) => { const idx = prog.length; prog.push(Object.assign({ op }, extra)); return idx }\n\n function rec (n) {\n switch (n.t) {\n case 'empty': break\n case 'char': emit('char', { c: n.c }); break\n case 'any': emit('any'); break\n case 'class': emit('class', { neg: n.neg, ranges: n.ranges }); break\n case 'bol': emit('bol'); break\n case 'eol': emit('eol'); break\n case 'group': rec(n.child); break\n case 'concat': for (const p of n.parts) rec(p); break\n case 'alt': {\n const jmps = []\n for (let k = 0; k < n.opts.length; k++) {\n if (k < n.opts.length - 1) {\n const sp = emit('split', { x: 0, y: 0 })\n prog[sp].x = prog.length\n rec(n.opts[k])\n jmps.push(emit('jmp', { x: 0 }))\n prog[sp].y = prog.length\n } else {\n rec(n.opts[k])\n }\n }\n for (const j of jmps) prog[j].x = prog.length\n break\n }\n case 'star': {\n const sp = emit('split', { x: 0, y: 0 })\n prog[sp].x = prog.length\n rec(n.child)\n emit('jmp', { x: sp })\n prog[sp].y = prog.length\n break\n }\n case 'plus': {\n const start = prog.length\n rec(n.child)\n const sp = emit('split', { x: start, y: 0 })\n prog[sp].y = prog.length\n break\n }\n case 'quest': {\n const sp = emit('split', { x: 0, y: 0 })\n prog[sp].x = prog.length\n rec(n.child)\n prog[sp].y = prog.length\n break\n }\n case 'repeat': {\n for (let k = 0; k < n.min; k++) rec(n.child)\n if (n.max === Infinity) {\n if (n.min === 0) rec({ t: 'star', child: n.child })\n else rec({ t: 'star', child: n.child })\n } else {\n for (let k = 0; k < n.max - n.min; k++) rec({ t: 'quest', child: n.child })\n }\n break\n }\n }\n }\n\n rec(ast)\n emit('match')\n return prog\n}\n\nfunction matchClass (instr, c) {\n let inside = false\n const r = instr.ranges\n for (let k = 0; k < r.length; k++) { if (c >= r[k][0] && c <= r[k][1]) { inside = true; break } }\n return instr.neg ? !inside : inside\n}\n\nfunction makeRunner (prog) {\n const n = prog.length\n const lastGen = new Int32Array(n).fill(-1)\n let gen = 0\n const stack = []\n\n function addThread (list, pc, pos, len) {\n stack.length = 0\n stack.push(pc)\n while (stack.length) {\n const p = stack.pop()\n if (lastGen[p] === gen) continue\n lastGen[p] = gen\n const I = prog[p]\n switch (I.op) {\n case 'jmp': stack.push(I.x); break\n case 'split': stack.push(I.y); stack.push(I.x); break\n case 'bol': if (pos === 0) stack.push(p + 1); break\n case 'eol': if (pos === len) stack.push(p + 1); break\n default: list.push(p)\n }\n }\n }\n\n return function test (s) {\n const len = s.length\n let clist = []\n let nlist = []\n gen++\n addThread(clist, 0, 0, len)\n for (let pos = 0; pos <= len; pos++) {\n const c = pos < len ? s.charCodeAt(pos) : -1\n gen++\n nlist.length = 0\n for (let k = 0; k < clist.length; k++) {\n const pc = clist[k]\n const I = prog[pc]\n if (I.op === 'match') return true\n else if (I.op === 'char') { if (c === I.c) addThread(nlist, pc + 1, pos + 1, len) }\n else if (I.op === 'any') { if (c !== -1 && c !== 10) addThread(nlist, pc + 1, pos + 1, len) }\n else if (I.op === 'class') { if (c !== -1 && matchClass(I, c)) addThread(nlist, pc + 1, pos + 1, len) }\n }\n if (pos < len) addThread(nlist, 0, pos + 1, len)\n const tmp = clist; clist = nlist; nlist = tmp\n }\n return false\n }\n}\n\nfunction compileSafe (pattern) {\n const prog = compileProg(parse(pattern))\n const runner = makeRunner(prog)\n // `__ataSafe` brands the result so the standalone serializer can tell a safe\n // matcher apart from a RegExp and emit `__ataSafeRe(source)` instead.\n return { test: runner, source: pattern, __ataSafe: true }\n}\n\n// True when the linear engine can represent `src`. Used by the codegen to decide\n// between the safe matcher and a JS RegExp fallback for patterns outside the\n// supported (RE2) subset (backreferences, lookaround, etc.).\nfunction patternIsSafe (src) {\n try { compileSafe(src); return true } catch { return false }\n}\n\nmodule.exports = { compileSafe, patternIsSafe }\n";
|
package/lib/t.js
ADDED
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// Chainable schema builder. Each `t.X(...)` returns a plain JSON Schema
|
|
4
|
+
// object, so the output drops straight into `new Validator(...)`,
|
|
5
|
+
// `defineSchema`, `Infer<S>`, and the AOT pipeline with no adapter. The
|
|
6
|
+
// builder is pure: no runtime state, no class instances, no closures held
|
|
7
|
+
// across calls.
|
|
8
|
+
//
|
|
9
|
+
// Optionality is carried by a Symbol-keyed marker that JSON Schema
|
|
10
|
+
// consumers (including ata's own codegen) never see, because they iterate
|
|
11
|
+
// `Object.keys`, which excludes Symbol keys. `t.object` reads the marker
|
|
12
|
+
// to compute `required` and strips it from the emitted property schema.
|
|
13
|
+
|
|
14
|
+
const OPTIONAL = Symbol.for('ata.t.optional');
|
|
15
|
+
|
|
16
|
+
function string(opts) {
|
|
17
|
+
return Object.assign({ type: 'string' }, opts);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function number(opts) {
|
|
21
|
+
return Object.assign({ type: 'number' }, opts);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function integer(opts) {
|
|
25
|
+
return Object.assign({ type: 'integer' }, opts);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function boolean() {
|
|
29
|
+
return { type: 'boolean' };
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function nul() {
|
|
33
|
+
return { type: 'null' };
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function literal(value) {
|
|
37
|
+
return { const: value };
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function constant(value) {
|
|
41
|
+
return { const: value };
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function enumOf(values) {
|
|
45
|
+
return { enum: values };
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function array(items, opts) {
|
|
49
|
+
return Object.assign({ type: 'array', items }, opts);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function tuple(items, opts) {
|
|
53
|
+
// JSON Schema 2020-12 uses `prefixItems` for tuples. `items: false`
|
|
54
|
+
// closes the tail, and a default `minItems` matching the tuple length
|
|
55
|
+
// enforces "exactly N elements" so the runtime check matches the
|
|
56
|
+
// TypeScript tuple type that `Infer<S>` produces. A caller can override
|
|
57
|
+
// either bound through `opts`.
|
|
58
|
+
return Object.assign(
|
|
59
|
+
{ type: 'array', prefixItems: items, items: false, minItems: items.length },
|
|
60
|
+
opts,
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function record(values, opts) {
|
|
65
|
+
return Object.assign({ type: 'object', additionalProperties: values }, opts);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function object(properties, opts) {
|
|
69
|
+
const props = {};
|
|
70
|
+
const required = [];
|
|
71
|
+
const keys = Object.keys(properties);
|
|
72
|
+
for (const key of keys) {
|
|
73
|
+
const schema = properties[key];
|
|
74
|
+
if (schema && schema[OPTIONAL] === true) {
|
|
75
|
+
const { [OPTIONAL]: _drop, ...rest } = schema;
|
|
76
|
+
props[key] = rest;
|
|
77
|
+
} else {
|
|
78
|
+
props[key] = schema;
|
|
79
|
+
required.push(key);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
const out = { type: 'object', properties: props };
|
|
83
|
+
if (required.length) out.required = required;
|
|
84
|
+
return Object.assign(out, opts);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function optional(schema) {
|
|
88
|
+
// Brand the schema so the parent `t.object` can detect it. The Symbol
|
|
89
|
+
// key is invisible to Object.keys / JSON.stringify and to ata's codegen,
|
|
90
|
+
// so a standalone `t.optional(t.string())` still validates as a string
|
|
91
|
+
// if passed directly to `new Validator(...)`.
|
|
92
|
+
return Object.assign({}, schema, { [OPTIONAL]: true });
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function union(schemas) {
|
|
96
|
+
return { anyOf: schemas };
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function intersect(schemas) {
|
|
100
|
+
return { allOf: schemas };
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function ref(pointer) {
|
|
104
|
+
return { $ref: pointer };
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function any() {
|
|
108
|
+
return {};
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function unknown() {
|
|
112
|
+
return {};
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function never() {
|
|
116
|
+
return { not: {} };
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
module.exports = {
|
|
120
|
+
string,
|
|
121
|
+
number,
|
|
122
|
+
integer,
|
|
123
|
+
boolean,
|
|
124
|
+
null: nul,
|
|
125
|
+
literal,
|
|
126
|
+
const: constant,
|
|
127
|
+
enum: enumOf,
|
|
128
|
+
array,
|
|
129
|
+
tuple,
|
|
130
|
+
record,
|
|
131
|
+
object,
|
|
132
|
+
optional,
|
|
133
|
+
union,
|
|
134
|
+
intersect,
|
|
135
|
+
ref,
|
|
136
|
+
any,
|
|
137
|
+
unknown,
|
|
138
|
+
never,
|
|
139
|
+
OPTIONAL,
|
|
140
|
+
};
|
package/lib/version.js
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// The package version, exported as a plain string so neither the default nor
|
|
4
|
+
// the browser entry has to `require('./package.json')`. Bundling package.json
|
|
5
|
+
// would drag dependency strings (e.g. `pkg-prebuilds`) into browser builds
|
|
6
|
+
// even though the runtime never touches them.
|
|
7
|
+
//
|
|
8
|
+
// Kept in lockstep with package.json by `tests/test_version_sync.js`.
|
|
9
|
+
|
|
10
|
+
module.exports = '0.20.0';
|