ata-validator 1.49.0 → 1.50.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/README.md +19 -4
- package/aot.d.ts +40 -0
- package/aot.mjs +1 -1
- package/bin/ata.js +14 -1
- package/build.d.ts +1 -1
- package/error-runtime.d.ts +24 -0
- package/error-runtime.js +40 -0
- package/error-runtime.mjs +3 -0
- package/index.browser.mjs +14 -5
- package/index.edge.js +14 -0
- package/index.edge.mjs +13 -0
- package/index.js +8 -1014
- package/lib/aot-build.js +211 -68
- package/lib/aot-impl.js +207 -7
- package/lib/codegen-engine.js +1024 -0
- package/lib/error-code-of.js +22 -0
- package/lib/error-runtime-entry.js +85 -0
- package/lib/error-runtime.js +152 -0
- package/lib/error-sort-rank.js +61 -0
- package/lib/interpreter-core.js +1356 -0
- package/lib/interpreter.js +8 -1260
- package/lib/public-api.js +80 -0
- package/lib/schema-runtime-entry.js +21 -0
- package/lib/schema-strip.js +115 -0
- package/lib/tools.mjs +21 -0
- package/lib/validator-core.js +17 -5
- package/lib/version.js +1 -1
- package/package.json +28 -8
package/lib/aot-build.js
CHANGED
|
@@ -87,19 +87,102 @@ function hashContent(buf) {
|
|
|
87
87
|
return crypto.createHash('sha256').update(buf).digest('hex').slice(0, 32);
|
|
88
88
|
}
|
|
89
89
|
|
|
90
|
+
// Where the shared error runtime goes: the output directory when there is
|
|
91
|
+
// one, otherwise the deepest directory that holds every output.
|
|
92
|
+
function runtimePathFor(outputs, opts, format) {
|
|
93
|
+
const name = 'ata-errors' + (format === 'cjs' ? '.cjs' : '.mjs');
|
|
94
|
+
if (opts.outDir) return path.join(opts.outDir, name);
|
|
95
|
+
let dir = path.resolve(path.dirname(outputs[0]));
|
|
96
|
+
for (const o of outputs) {
|
|
97
|
+
const d = path.resolve(path.dirname(o));
|
|
98
|
+
while (dir !== d && !d.startsWith(dir + path.sep)) dir = path.dirname(dir);
|
|
99
|
+
}
|
|
100
|
+
return path.join(dir, name);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function importSpecifier(fromFile, toFile) {
|
|
104
|
+
let rel = path.relative(path.dirname(path.resolve(fromFile)), path.resolve(toFile)).split(path.sep).join('/');
|
|
105
|
+
if (!rel.startsWith('.')) rel = './' + rel;
|
|
106
|
+
return rel;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const RUNTIME_PLACEHOLDER = '__ATA_ERROR_RUNTIME__';
|
|
110
|
+
const ERRORS_MODES = new Set(['auto', 'inline', 'shared']);
|
|
111
|
+
|
|
90
112
|
async function build(opts) {
|
|
91
113
|
const globs = opts.globs || [];
|
|
92
114
|
if (globs.length === 0) throw new Error('build: at least one glob required');
|
|
93
115
|
const format = opts.format || 'esm';
|
|
116
|
+
const errorsMode = opts.errors || 'auto';
|
|
117
|
+
if (!ERRORS_MODES.has(errorsMode)) throw new Error(`build: errors must be auto, inline or shared (got "${errorsMode}")`);
|
|
94
118
|
const inputs = await expandGlobs(globs);
|
|
95
119
|
const cache = readCache(opts.cacheFile);
|
|
96
120
|
const newCache = {};
|
|
121
|
+
const source = resolveSourceDefault(opts);
|
|
122
|
+
// Error detail from the shared runtime replaces generated error code, so it
|
|
123
|
+
// is only on offer where that code is what the build would emit: not with
|
|
124
|
+
// schema-source frames (development builds), stub errors or the one-pass
|
|
125
|
+
// function, which are each asked for by name.
|
|
126
|
+
const sharable = errorsMode !== 'inline' && !source && !opts.abortEarly && opts.onePass !== true;
|
|
127
|
+
// `mode: 'schema'`, asked for by name: every module that can be is a schema
|
|
128
|
+
// module (its schema and a few lines), with the verdict and the errors from
|
|
129
|
+
// one runtime. Smaller and quicker to load than generated code, slower per
|
|
130
|
+
// check once warm.
|
|
131
|
+
if (opts.mode !== undefined && opts.mode !== 'code' && opts.mode !== 'schema') throw new Error(`build: mode must be code or schema (got "${opts.mode}")`);
|
|
132
|
+
const schemaMode = opts.mode === 'schema' && !opts.abortEarly;
|
|
97
133
|
|
|
98
134
|
const compiled = [];
|
|
99
135
|
const cached = [];
|
|
100
136
|
const skipped = [];
|
|
101
137
|
const failed = [];
|
|
102
138
|
const warnings = [];
|
|
139
|
+
const entries = [];
|
|
140
|
+
|
|
141
|
+
const compileOne = (input, raw) => {
|
|
142
|
+
const schema = parseSchemaFile(input);
|
|
143
|
+
// The build is where an authoring mistake is cheapest to stop: a typo
|
|
144
|
+
// like maxLenght compiles into a validator that simply lacks the rule,
|
|
145
|
+
// and a compiled module fails even more quietly than a runtime one,
|
|
146
|
+
// since nobody ever revisits it. Findings fail the input's build.
|
|
147
|
+
if (opts.strictSchema) {
|
|
148
|
+
const { checkSchemaStrict } = require('./strict-check');
|
|
149
|
+
const problems = checkSchemaStrict(schema, {});
|
|
150
|
+
if (problems.length > 0) {
|
|
151
|
+
return { error: problems.map((x) => `strict mode: ${x.message} at ${x.path}`).join('; ') };
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
const v = new Validator(schema);
|
|
155
|
+
let sourceMap = null;
|
|
156
|
+
if (source) {
|
|
157
|
+
// Only attempt position map for JSON (the scanner is JSON-only).
|
|
158
|
+
const ext = path.extname(input).toLowerCase();
|
|
159
|
+
if (ext === '.json') {
|
|
160
|
+
try { sourceMap = buildPositionMap(raw.toString('utf8')); }
|
|
161
|
+
catch { sourceMap = null; }
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
const schemaFile = path.relative(process.cwd(), input) || input;
|
|
165
|
+
const inputWarnings = [];
|
|
166
|
+
const src = aot.toStandaloneModule(v, {
|
|
167
|
+
format,
|
|
168
|
+
abortEarly: !!opts.abortEarly,
|
|
169
|
+
onePass: opts.onePass === true ? true : undefined,
|
|
170
|
+
source,
|
|
171
|
+
sourceMap,
|
|
172
|
+
schemaFile,
|
|
173
|
+
formatMode: opts.formatMode,
|
|
174
|
+
onWarning: (w, meta) => inputWarnings.push({ message: w, kind: (meta && meta.kind) || 'general' }),
|
|
175
|
+
});
|
|
176
|
+
const compact = schemaMode && src
|
|
177
|
+
? aot.toSchemaModule(v, { format, runtime: RUNTIME_PLACEHOLDER })
|
|
178
|
+
: sharable && src ? aot.toCompactModule(v, { format, formatMode: opts.formatMode, errorRuntime: RUNTIME_PLACEHOLDER }) : null;
|
|
179
|
+
// The runtime parts this module's schema needs (see runtimeFeatures).
|
|
180
|
+
const features = compact ? require('./error-runtime').runtimeFeatures([require('./schema-strip').stripAnnotations(v._schemaObj)]) : null;
|
|
181
|
+
// A module that imports the pattern engine needs it in the runtime,
|
|
182
|
+
// whatever the schema scan said.
|
|
183
|
+
if (features && compact.includes('_ataCompileSafe')) features['safe-regex'] = true;
|
|
184
|
+
return { schema, src, compact, features, inputWarnings };
|
|
185
|
+
};
|
|
103
186
|
|
|
104
187
|
for (const input of inputs) {
|
|
105
188
|
try {
|
|
@@ -107,100 +190,119 @@ async function build(opts) {
|
|
|
107
190
|
const inputHash = hashContent(raw);
|
|
108
191
|
const output = outputPathFor(input, { ...opts, format });
|
|
109
192
|
const cacheEntry = cache[input];
|
|
110
|
-
|
|
111
|
-
const upToDate = (
|
|
112
|
-
cacheEntry &&
|
|
113
|
-
cacheEntry.inputHash === inputHash &&
|
|
114
|
-
cacheEntry.output === output &&
|
|
115
|
-
fs.existsSync(output) &&
|
|
116
|
-
cacheEntry.outputHash === hashContent(fs.readFileSync(output))
|
|
117
|
-
);
|
|
118
|
-
if (upToDate) {
|
|
119
|
-
cached.push({ input, output });
|
|
120
|
-
}
|
|
121
|
-
continue;
|
|
122
|
-
}
|
|
123
|
-
if (
|
|
193
|
+
const fresh = !!(
|
|
124
194
|
cacheEntry &&
|
|
125
195
|
cacheEntry.inputHash === inputHash &&
|
|
126
196
|
cacheEntry.output === output &&
|
|
197
|
+
!!cacheEntry.sharable === sharable &&
|
|
198
|
+
!!cacheEntry.schemaMode === schemaMode &&
|
|
127
199
|
fs.existsSync(output) &&
|
|
128
200
|
cacheEntry.outputHash === hashContent(fs.readFileSync(output))
|
|
129
|
-
)
|
|
130
|
-
|
|
131
|
-
|
|
201
|
+
);
|
|
202
|
+
if (opts.check) {
|
|
203
|
+
if (fresh) cached.push({ input, output });
|
|
132
204
|
continue;
|
|
133
205
|
}
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
// and a compiled module fails even more quietly than a runtime one,
|
|
138
|
-
// since nobody ever revisits it. Findings fail the input's build.
|
|
139
|
-
if (opts.strictSchema) {
|
|
140
|
-
const { checkSchemaStrict } = require('./strict-check');
|
|
141
|
-
const problems = checkSchemaStrict(schema, {});
|
|
142
|
-
if (problems.length > 0) {
|
|
143
|
-
failed.push({ input, error: problems.map((x) => `strict mode: ${x.message} at ${x.path}`).join('; ') });
|
|
144
|
-
continue;
|
|
145
|
-
}
|
|
146
|
-
}
|
|
147
|
-
const v = new Validator(schema);
|
|
148
|
-
const source = resolveSourceDefault(opts);
|
|
149
|
-
let sourceMap = null;
|
|
150
|
-
if (source) {
|
|
151
|
-
// Only attempt position map for JSON (the scanner is JSON-only).
|
|
152
|
-
const ext = path.extname(input).toLowerCase();
|
|
153
|
-
if (ext === '.json') {
|
|
154
|
-
try { sourceMap = buildPositionMap(raw.toString('utf8')); }
|
|
155
|
-
catch { sourceMap = null; }
|
|
156
|
-
}
|
|
206
|
+
if (fresh) {
|
|
207
|
+
entries.push({ input, output, raw, inputHash, cacheEntry, fullGz: cacheEntry.fullGz, compactGz: cacheEntry.compactGz, features: cacheEntry.features || null });
|
|
208
|
+
continue;
|
|
157
209
|
}
|
|
158
|
-
const
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
format,
|
|
162
|
-
abortEarly: !!opts.abortEarly,
|
|
163
|
-
onePass: opts.onePass === true ? true : undefined,
|
|
164
|
-
source,
|
|
165
|
-
sourceMap,
|
|
166
|
-
schemaFile,
|
|
167
|
-
formatMode: opts.formatMode,
|
|
168
|
-
onWarning: (w, meta) => inputWarnings.push({ message: w, kind: (meta && meta.kind) || 'general' }),
|
|
169
|
-
});
|
|
170
|
-
if (inputWarnings.length > 0) {
|
|
210
|
+
const r = compileOne(input, raw);
|
|
211
|
+
if (r.error) { failed.push({ input, error: r.error }); continue; }
|
|
212
|
+
if (r.inputWarnings.length > 0) {
|
|
171
213
|
if (opts.strict) {
|
|
172
|
-
failed.push({ input, error: inputWarnings.map((x) => x.message).join('; ') });
|
|
214
|
+
failed.push({ input, error: r.inputWarnings.map((x) => x.message).join('; ') });
|
|
173
215
|
continue;
|
|
174
216
|
}
|
|
175
217
|
// `kind` says which capability degraded ('error-detail', 'parse'),
|
|
176
218
|
// so a caller that requested both can tell the warnings apart.
|
|
177
|
-
for (const x of inputWarnings) warnings.push({ input, warning: x.message, kind: x.kind });
|
|
219
|
+
for (const x of r.inputWarnings) warnings.push({ input, warning: x.message, kind: x.kind });
|
|
178
220
|
}
|
|
179
|
-
if (!src) {
|
|
221
|
+
if (!r.src) {
|
|
180
222
|
const reason = 'schema is not AOT-compatible (toStandaloneModule returned null)';
|
|
181
223
|
if (opts.strict) failed.push({ input, error: reason });
|
|
182
224
|
else skipped.push({ input, reason });
|
|
183
225
|
continue;
|
|
184
226
|
}
|
|
227
|
+
entries.push({
|
|
228
|
+
input, output, raw, inputHash, schema: r.schema, src: r.src, compact: r.compact, features: r.features,
|
|
229
|
+
fullGz: zlib.gzipSync(r.src).length,
|
|
230
|
+
compactGz: r.compact ? zlib.gzipSync(r.compact).length : null,
|
|
231
|
+
});
|
|
232
|
+
} catch (e) {
|
|
233
|
+
failed.push({ input, error: e.message });
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
if (opts.check) {
|
|
238
|
+
const staleCount = inputs.length - cached.length;
|
|
239
|
+
return { compiled: [], cached, skipped, failed, warnings, staleCount };
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// A module takes its errors from the shared runtime when that makes it
|
|
243
|
+
// smaller, and the runtime is written when the modules that use it save
|
|
244
|
+
// more than it weighs. With `errors: 'shared'` every module that can, does.
|
|
245
|
+
// The runtime carries only the parts the compact modules' schemas need
|
|
246
|
+
// (formats, the pattern engine, deep equality); an entry with no record of
|
|
247
|
+
// its needs (a cache written before this) counts as needing everything.
|
|
248
|
+
const { errorRuntimeSource, runtimeFeatures } = require('./error-runtime');
|
|
249
|
+
const ATA_VERSION = require('./version');
|
|
250
|
+
const unionOf = (list) => {
|
|
251
|
+
const out = runtimeFeatures([]);
|
|
252
|
+
for (const e of list) for (const k of Object.keys(out)) if (!e.features || e.features[k]) out[k] = true;
|
|
253
|
+
return out;
|
|
254
|
+
};
|
|
255
|
+
const candidates = entries.filter((e) => typeof e.compactGz === 'number' && (schemaMode || errorsMode === 'shared' || e.compactGz < e.fullGz));
|
|
256
|
+
let runtime = null;
|
|
257
|
+
if ((sharable || schemaMode) && candidates.length > 0) {
|
|
258
|
+
const runtimeSrc = errorRuntimeSource(format, ATA_VERSION, unionOf(candidates), schemaMode);
|
|
259
|
+
const runtimeGz = zlib.gzipSync(runtimeSrc).length;
|
|
260
|
+
let saving = 0;
|
|
261
|
+
for (const e of candidates) saving += e.fullGz - e.compactGz;
|
|
262
|
+
const use = schemaMode || errorsMode === 'shared' || saving > runtimeGz;
|
|
263
|
+
if (use) runtime = { path: runtimePathFor(entries.map((e) => e.output), opts, format), src: runtimeSrc, gz: runtimeGz };
|
|
264
|
+
}
|
|
265
|
+
for (const e of entries) {
|
|
266
|
+
e.mode = runtime !== null && candidates.includes(e) ? (schemaMode ? 'schema' : 'compact') : 'inline';
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
for (const e of entries) {
|
|
270
|
+
const { input, output } = e;
|
|
271
|
+
try {
|
|
272
|
+
if (e.cacheEntry) {
|
|
273
|
+
const sameRuntime = e.mode === 'inline' || (runtime !== null && e.cacheEntry.runtime === runtime.path);
|
|
274
|
+
if (e.cacheEntry.mode === e.mode && sameRuntime) {
|
|
275
|
+
cached.push({ input, output });
|
|
276
|
+
newCache[input] = e.cacheEntry;
|
|
277
|
+
continue;
|
|
278
|
+
}
|
|
279
|
+
// The build's choice for this module changed: compile it again.
|
|
280
|
+
const r = compileOne(input, e.raw);
|
|
281
|
+
if (r.error || !r.src) { failed.push({ input, error: r.error || 'schema is not AOT-compatible' }); continue; }
|
|
282
|
+
e.schema = r.schema; e.src = r.src; e.compact = r.compact; e.features = r.features;
|
|
283
|
+
if (e.mode !== 'inline' && !r.compact) e.mode = 'inline';
|
|
284
|
+
}
|
|
285
|
+
const src = e.mode !== 'inline'
|
|
286
|
+
? e.compact.split(JSON.stringify(RUNTIME_PLACEHOLDER)).join(JSON.stringify(importSpecifier(output, runtime.path)))
|
|
287
|
+
: e.src;
|
|
185
288
|
fs.mkdirSync(path.dirname(output), { recursive: true });
|
|
186
289
|
fs.writeFileSync(output, src);
|
|
187
290
|
const outBytes = Buffer.byteLength(src, 'utf8');
|
|
188
|
-
const
|
|
189
|
-
const gzBytes = gz.length;
|
|
291
|
+
const gzBytes = zlib.gzipSync(src).length;
|
|
190
292
|
if (typeof opts.maxSize === 'number' && gzBytes > opts.maxSize) {
|
|
191
293
|
// Roll back the write so a failed build doesn't leave a stale artifact.
|
|
192
294
|
try { fs.unlinkSync(output); } catch {}
|
|
193
295
|
failed.push({ input, error: `output ${output} exceeds --max-size: ${gzBytes} > ${opts.maxSize} (gzipped bytes)` });
|
|
194
296
|
continue;
|
|
195
297
|
}
|
|
196
|
-
compiled.push({ input, output, bytes: outBytes, gzipBytes: gzBytes });
|
|
298
|
+
compiled.push({ input, output, bytes: outBytes, gzipBytes: gzBytes, errors: e.mode });
|
|
197
299
|
if (opts.types !== false) {
|
|
198
300
|
const { toTypeScript } = require('./ts-gen');
|
|
199
301
|
const stem = path.basename(input, path.extname(input)).replace(/\.schema$/, '');
|
|
200
302
|
const typeName = stem
|
|
201
303
|
.replace(/[^A-Za-z0-9_]/g, '_')
|
|
202
304
|
.replace(/^([a-z])/, (m) => m.toUpperCase()) || 'Data';
|
|
203
|
-
const dts = toTypeScript(schema, { name: typeName });
|
|
305
|
+
const dts = toTypeScript(e.schema, { name: typeName });
|
|
204
306
|
const ext = path.extname(output);
|
|
205
307
|
const dtsExt = ext === '.mjs' ? '.d.mts'
|
|
206
308
|
: ext === '.cjs' ? '.d.cts'
|
|
@@ -209,23 +311,35 @@ async function build(opts) {
|
|
|
209
311
|
fs.writeFileSync(base + dtsExt, dts);
|
|
210
312
|
}
|
|
211
313
|
newCache[input] = {
|
|
212
|
-
inputHash,
|
|
314
|
+
inputHash: e.inputHash,
|
|
213
315
|
output,
|
|
214
316
|
outputHash: hashContent(Buffer.from(src, 'utf8')),
|
|
317
|
+
sharable,
|
|
318
|
+
schemaMode,
|
|
319
|
+
mode: e.mode,
|
|
320
|
+
runtime: e.mode !== 'inline' ? runtime.path : undefined,
|
|
321
|
+
fullGz: e.fullGz,
|
|
322
|
+
compactGz: e.compactGz,
|
|
323
|
+
features: e.features || undefined,
|
|
215
324
|
};
|
|
216
|
-
} catch (
|
|
217
|
-
failed.push({ input, error:
|
|
325
|
+
} catch (err) {
|
|
326
|
+
failed.push({ input, error: err.message });
|
|
218
327
|
}
|
|
219
328
|
}
|
|
220
329
|
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
330
|
+
let runtimeReport = null;
|
|
331
|
+
if (runtime !== null && entries.some((e) => e.mode !== 'inline')) {
|
|
332
|
+
const current = fs.existsSync(runtime.path) ? fs.readFileSync(runtime.path, 'utf8') : null;
|
|
333
|
+
if (current !== runtime.src) {
|
|
334
|
+
fs.mkdirSync(path.dirname(runtime.path), { recursive: true });
|
|
335
|
+
fs.writeFileSync(runtime.path, runtime.src);
|
|
336
|
+
}
|
|
337
|
+
runtimeReport = { output: runtime.path, bytes: Buffer.byteLength(runtime.src, 'utf8'), gzipBytes: runtime.gz };
|
|
224
338
|
}
|
|
225
339
|
|
|
226
340
|
writeCache(opts.cacheFile, newCache);
|
|
227
341
|
|
|
228
|
-
return { compiled, cached, skipped, failed, warnings };
|
|
342
|
+
return { compiled, cached, skipped, failed, warnings, runtime: runtimeReport };
|
|
229
343
|
}
|
|
230
344
|
|
|
231
345
|
async function watch(opts, onReport) {
|
|
@@ -322,12 +436,41 @@ function compiledSchemaFor(schema) {
|
|
|
322
436
|
// error function the generator declines (unevaluated* among them), where the
|
|
323
437
|
// module would report every failure as the ATA9000 stub while the runtime
|
|
324
438
|
// reports the real errors.
|
|
439
|
+
//
|
|
440
|
+
// With `errorRuntime` (an import specifier, usually 'ata-validator/error-runtime'),
|
|
441
|
+
// the module is written compact where the validator allows it: the verdict and
|
|
442
|
+
// the schema, with errors from that runtime (see toCompactModule). A plugin
|
|
443
|
+
// writes it in place of the self-contained module when that is smaller for
|
|
444
|
+
// the whole bundle; the runtime is shared by every compact module in it.
|
|
445
|
+
// A plugin asks for both modules of one schema (the self-contained one, and
|
|
446
|
+
// the compact one to compare): one Validator answers both, so the verdict
|
|
447
|
+
// function compiles once. Keyed by the schema object the plugin passes.
|
|
448
|
+
const _validators = new WeakMap();
|
|
449
|
+
function validatorFor(schema) {
|
|
450
|
+
let v = _validators.get(schema);
|
|
451
|
+
// Built with an options object, empty, so it stays out of the identity
|
|
452
|
+
// cache: `new Validator(schema)` in the caller's own code must get its own
|
|
453
|
+
// instance, not this compiled one.
|
|
454
|
+
if (v === undefined) { v = new Validator(schema, {}); _validators.set(schema, v); }
|
|
455
|
+
return v;
|
|
456
|
+
}
|
|
457
|
+
|
|
325
458
|
function compiledModuleFor(schema, opts) {
|
|
326
459
|
if (!compiledEligible(schema)) return null;
|
|
460
|
+
if (opts && typeof opts.errorRuntime === 'string') {
|
|
461
|
+
try {
|
|
462
|
+
const v = validatorFor(schema);
|
|
463
|
+
const src = aot.toCompactModule(v, { format: opts.format || 'esm', errorRuntime: opts.errorRuntime });
|
|
464
|
+
if (src) return src;
|
|
465
|
+
} catch {
|
|
466
|
+
return null;
|
|
467
|
+
}
|
|
468
|
+
// Not writable as a compact module: the self-contained one below.
|
|
469
|
+
}
|
|
327
470
|
let degraded = false;
|
|
328
471
|
let src;
|
|
329
472
|
try {
|
|
330
|
-
src = toStandaloneModule(schema, {
|
|
473
|
+
src = aot.toStandaloneModule(validatorFor(schema), {
|
|
331
474
|
format: (opts && opts.format) || 'esm',
|
|
332
475
|
onWarning: (msg, info) => { if (info && info.kind === 'error-detail') degraded = true; },
|
|
333
476
|
});
|
package/lib/aot-impl.js
CHANGED
|
@@ -352,7 +352,10 @@ function toStandaloneModule(validator, opts) {
|
|
|
352
352
|
const jsFn = validator._jsFn;
|
|
353
353
|
if (!jsFn || !jsFn._source) return null;
|
|
354
354
|
const format = (opts && opts.format) || 'esm';
|
|
355
|
-
|
|
355
|
+
// A compact module (toCompactModule) carries the verdict and its schema, and
|
|
356
|
+
// takes its errors from the shared runtime at this import specifier.
|
|
357
|
+
const errorRuntime = opts && typeof opts._errorRuntime === 'string' ? opts._errorRuntime : null;
|
|
358
|
+
const abortEarly = !!(opts && opts.abortEarly) || errorRuntime !== null;
|
|
356
359
|
const source = !!(opts && opts.source);
|
|
357
360
|
const sourceMap = opts && opts.sourceMap ? opts.sourceMap : null;
|
|
358
361
|
const schemaFile = opts && opts.schemaFile ? opts.schemaFile : null;
|
|
@@ -488,7 +491,43 @@ function toStandaloneModule(validator, opts) {
|
|
|
488
491
|
const fmt = emitFormatDecls(jsFn._formatClosures, opts && opts.formatMode, 'const');
|
|
489
492
|
const formatDecls = fmt.decls;
|
|
490
493
|
|
|
491
|
-
|
|
494
|
+
// A compact module takes the linear-time pattern engine from the runtime,
|
|
495
|
+
// which carries it anyway, instead of embedding a copy (2.9 KB gzipped,
|
|
496
|
+
// minified) in every module that has a pattern the platform RegExp does not
|
|
497
|
+
// run.
|
|
498
|
+
const usesSafeRe = errorRuntime !== null && ((jsFn && jsFn._usesSafeRe) || (jsErrFn && jsErrFn._usesSafeRe));
|
|
499
|
+
let compactCore = '';
|
|
500
|
+
if (errorRuntime !== null) {
|
|
501
|
+
const { stripAnnotations } = require('./schema-strip');
|
|
502
|
+
const { isV1Dialect } = require('./dialect');
|
|
503
|
+
const sch = typeof validator._schemaObj === 'object' ? validator._schemaObj : validator._schemaObj;
|
|
504
|
+
// Embedded as JSON text and parsed: a literal would turn a "__proto__"
|
|
505
|
+
// key into the object's prototype, and V8 reads JSON faster than an
|
|
506
|
+
// object literal of the same size.
|
|
507
|
+
const text = JSON.stringify(JSON.stringify(stripAnnotations(sch))).replace(/</g, '\\u003c');
|
|
508
|
+
const v1 = sch !== null && typeof sch === 'object' && isV1Dialect(sch);
|
|
509
|
+
// No embedded resource and no anchor anywhere (a key of that name inside
|
|
510
|
+
// a value counts too): the runtime can skip indexing the schema.
|
|
511
|
+
const flat = (() => {
|
|
512
|
+
const seen = new Set();
|
|
513
|
+
const walk = (n, root) => {
|
|
514
|
+
if (n === null || typeof n !== 'object' || seen.has(n)) return true;
|
|
515
|
+
seen.add(n);
|
|
516
|
+
if (Array.isArray(n)) return n.every((x) => walk(x, false));
|
|
517
|
+
if ('$anchor' in n || '$dynamicAnchor' in n || (!root && '$id' in n)) return false;
|
|
518
|
+
return Object.keys(n).every((k) => walk(n[k], false));
|
|
519
|
+
};
|
|
520
|
+
return walk(stripAnnotations(sch), true);
|
|
521
|
+
})();
|
|
522
|
+
const rtOpts = [v1 ? 'v1: true' : '', flat ? 'flat: true' : ''].filter(Boolean).join(', ');
|
|
523
|
+
// Nothing of it runs at import: the schema is parsed and the runtime
|
|
524
|
+
// set up on the first rejected document whose errors are read, so a
|
|
525
|
+
// module that only accepts costs no more to load than its verdict.
|
|
526
|
+
compactCore = `let _ataErrs = null;\nfunction _ataErrors(d) { if (_ataErrs === null) _ataErrs = _ataCreateErrors(JSON.parse(${text})${rtOpts ? ', { ' + rtOpts + ' }' : ''}); return _ataErrs(d); }\n`;
|
|
527
|
+
}
|
|
528
|
+
const validBody = errorRuntime !== null
|
|
529
|
+
? 'if (_fn(data)) return VALID; const e = _ataErrors(data); return e === null ? ABORT : { valid: false, errors: e }'
|
|
530
|
+
: onePass !== null
|
|
492
531
|
? 'return _vC(data)'
|
|
493
532
|
: errCore
|
|
494
533
|
? 'return _fn(data) ? VALID : { valid: false, errors: errFn(data, true).errors }'
|
|
@@ -565,7 +604,7 @@ function validateJSON(text) {
|
|
|
565
604
|
// schema as the caller wrote it (before dialect normalization), so a build
|
|
566
605
|
// can compare it against schemaHash(currentSchema) and know the module is
|
|
567
606
|
// stale without embedding its own fingerprint.
|
|
568
|
-
const hashSrc =
|
|
607
|
+
const hashSrc = hashOf(validator);
|
|
569
608
|
// The schema hash answers "is this module built from a different schema" and
|
|
570
609
|
// nothing else. Upgrading ata and not re-running the generate step leaves the
|
|
571
610
|
// hash matching, so the stale module reads as current. The module cannot ask
|
|
@@ -589,11 +628,11 @@ function validateJSON(text) {
|
|
|
589
628
|
degradedNote += '// NOTE: parse() was requested but could not be generated for this schema;\n// the module has no parse export. Validate and strip with the runtime Validator.\n';
|
|
590
629
|
}
|
|
591
630
|
return `// Auto-generated by ata-validator ${ATA_VERSION}, do not edit.
|
|
592
|
-
// Schema is embedded; runtime has zero dependency on ata-validator.
|
|
631
|
+
${errorRuntime === null ? '// Schema is embedded; runtime has zero dependency on ata-validator.' : '// Compact module: the verdict and the schema; errors come from the runtime it imports.'}
|
|
593
632
|
// Re-run the build after upgrading ata: the version above is what wrote this.
|
|
594
633
|
${degradedNote}'use strict';
|
|
595
|
-
${_CP_LEN_SOURCE}
|
|
596
|
-
${safeRePrelude(jsFn, jsErrFn, onePass !== null && onePass.usesSafeRe ? { _usesSafeRe: true } : null)}${schemaSourceConst}const VALID = Object.freeze({ valid: true, errors: Object.freeze([]) });
|
|
634
|
+
${errorRuntime === null ? '' : format === 'esm' ? `import { createErrors as _ataCreateErrors${usesSafeRe ? ', compileSafe as _ataCompileSafe' : ''} } from ${JSON.stringify(errorRuntime)};\n` : `const { createErrors: _ataCreateErrors${usesSafeRe ? ', compileSafe: _ataCompileSafe' : ''} } = require(${JSON.stringify(errorRuntime)});\n`}${_CP_LEN_SOURCE}
|
|
635
|
+
${errorRuntime !== null ? (usesSafeRe ? 'const __ataSafeRe = _ataCompileSafe;\n' : '') : safeRePrelude(jsFn, jsErrFn, onePass !== null && onePass.usesSafeRe ? { _usesSafeRe: true } : null)}${schemaSourceConst}const VALID = Object.freeze({ valid: true, errors: Object.freeze([]) });
|
|
597
636
|
const ABORT = Object.freeze({
|
|
598
637
|
valid: false,
|
|
599
638
|
errors: Object.freeze([Object.freeze({
|
|
@@ -606,11 +645,155 @@ const ABORT = Object.freeze({
|
|
|
606
645
|
${closureDecls}${sharedBlock}${preambleDecls}${formatDecls}const _fn = function(d) {
|
|
607
646
|
${src}
|
|
608
647
|
};
|
|
609
|
-
${errCore}function isValid(data) { return _fn(data); }
|
|
648
|
+
${errCore}${compactCore}function isValid(data) { return _fn(data); }
|
|
610
649
|
function validate(data) { ${validBody}; }
|
|
611
650
|
${parseCore}${positionsCore}${hashDecl}${exports}`;
|
|
612
651
|
}
|
|
613
652
|
|
|
653
|
+
// Why a validator cannot be written as a compact module, or null when it can.
|
|
654
|
+
// The error runtime walks the embedded schema with the interpreted engine and
|
|
655
|
+
// knows nothing the schema does not say, so anything the validator adds from
|
|
656
|
+
// outside the schema keeps the module self-contained.
|
|
657
|
+
// The error runtime's generator reads the library's own files with fs, and
|
|
658
|
+
// runs in Node only (ata build, errorRuntimeSource). It is required through a
|
|
659
|
+
// name rather than a literal so that a bundler following this module, in a
|
|
660
|
+
// build of the package for a runtime without fs, does not try to bring fs
|
|
661
|
+
// along: 1.49.0 bundled for such runtimes, and the generator must not break
|
|
662
|
+
// that.
|
|
663
|
+
const ERROR_RUNTIME_MODULE = './error-runtime';
|
|
664
|
+
function loadErrorRuntime() { return require(ERROR_RUNTIME_MODULE); }
|
|
665
|
+
|
|
666
|
+
// The schema hash a module stamps, once per validator: a plugin writes both
|
|
667
|
+
// modules of a schema (self-contained and compact) from one validator, and
|
|
668
|
+
// hashing a large schema canonically is not free.
|
|
669
|
+
const _hashes = new WeakMap();
|
|
670
|
+
function hashOf(validator) {
|
|
671
|
+
let h = _hashes.get(validator);
|
|
672
|
+
if (h === undefined) {
|
|
673
|
+
h = schemaHash(validator._rawSchema !== undefined ? validator._rawSchema : validator._schemaObj);
|
|
674
|
+
_hashes.set(validator, h);
|
|
675
|
+
}
|
|
676
|
+
return h;
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
// True when every $ref and $dynamicRef in the schema resolves inside it, by
|
|
680
|
+
// the interpreter's own resolver.
|
|
681
|
+
function hasRefKey(n, seen) {
|
|
682
|
+
if (n === null || typeof n !== 'object' || seen.has(n)) return false;
|
|
683
|
+
seen.add(n);
|
|
684
|
+
if (Array.isArray(n)) { for (const x of n) if (hasRefKey(x, seen)) return true; return false; }
|
|
685
|
+
if ('$ref' in n || '$dynamicRef' in n) return true;
|
|
686
|
+
for (const k of Object.keys(n)) if (hasRefKey(n[k], seen)) return true;
|
|
687
|
+
return false;
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
function refsResolveWithin(schema) {
|
|
691
|
+
// No reference anywhere (a key of that name inside a value counts too):
|
|
692
|
+
// nothing to resolve, and no index to build.
|
|
693
|
+
if (!hasRefKey(schema, new Set())) return true;
|
|
694
|
+
const { indexSchemas, resolveRef } = require('./interpreter-core')._refInternals;
|
|
695
|
+
let state;
|
|
696
|
+
try { state = indexSchemas(schema, null); } catch { return false; }
|
|
697
|
+
for (const [node, base] of state.nodeBase) {
|
|
698
|
+
if (node === null || typeof node !== 'object') continue;
|
|
699
|
+
for (const k of ['$ref', '$dynamicRef']) {
|
|
700
|
+
const r = node[k];
|
|
701
|
+
if (r === undefined) continue;
|
|
702
|
+
if (typeof r !== 'string') return false;
|
|
703
|
+
let hit;
|
|
704
|
+
try { hit = resolveRef(r, base, state); } catch { return false; }
|
|
705
|
+
if (hit.node === undefined) return false;
|
|
706
|
+
}
|
|
707
|
+
}
|
|
708
|
+
return true;
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
const COMPACT_OPTIONS = new Set(['engine', 'strictSchema', 'logger', 'source']);
|
|
712
|
+
function compactDecline(validator) {
|
|
713
|
+
// Options are allowed by name, not refused by name: one this list does not
|
|
714
|
+
// know (assertFormat: false, baseURI, a future one) may change what the
|
|
715
|
+
// verdict means, and the runtime would not know to follow it.
|
|
716
|
+
// What the validator registers beside its schema (a meta-schema it
|
|
717
|
+
// resolves, for one) is known only once it has compiled.
|
|
718
|
+
validator._ensureCompiled();
|
|
719
|
+
const o = validator._options || {};
|
|
720
|
+
for (const k of Object.keys(o)) {
|
|
721
|
+
if (o[k] === undefined || COMPACT_OPTIONS.has(k)) continue;
|
|
722
|
+
return 'option ' + k;
|
|
723
|
+
}
|
|
724
|
+
if (validator._usesKeywords || validator._externalChecks) return 'custom keywords';
|
|
725
|
+
if (validator._userFormats && Object.keys(validator._userFormats).length) return 'custom formats';
|
|
726
|
+
if (validator._schemaMap && validator._schemaMap.size > 0) return 'schemas registered beside the root';
|
|
727
|
+
if (validator._schemaObj === null || typeof validator._schemaObj !== 'object') return 'boolean schema';
|
|
728
|
+
// The runtime has the embedded schema and nothing else: a reference that
|
|
729
|
+
// lands outside it (a meta-schema the runtime Validator resolves from its
|
|
730
|
+
// vendored copies, a schema registered later) would come back unresolved,
|
|
731
|
+
// and the errors with it. The check runs on the schema as embedded.
|
|
732
|
+
const { stripAnnotations } = require('./schema-strip');
|
|
733
|
+
if (!refsResolveWithin(stripAnnotations(validator._schemaObj))) return 'a reference outside the schema';
|
|
734
|
+
return null;
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
// A module that carries its verdict and its schema without the descriptive
|
|
738
|
+
// keywords, and imports its errors from the shared runtime at `errorRuntime`
|
|
739
|
+
// (written beside it by the build, see errorRuntimeSource). Null when the
|
|
740
|
+
// validator cannot be written this way; the caller writes the
|
|
741
|
+
// self-contained module instead.
|
|
742
|
+
function toCompactModule(validator, opts) {
|
|
743
|
+
assertEmittable(validator, 'toCompactModule');
|
|
744
|
+
if (!opts || typeof opts.errorRuntime !== 'string') throw new TypeError('toCompactModule: opts.errorRuntime (the import specifier of the error runtime) is required');
|
|
745
|
+
if (compactDecline(validator) !== null) return null;
|
|
746
|
+
return toStandaloneModule(validator, { format: opts.format, formatMode: opts.formatMode, _errorRuntime: opts.errorRuntime });
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
// A schema module: the schema without its descriptive keywords and a few lines
|
|
750
|
+
// that hand it to the runtime at `opts.runtime`, which answers both the
|
|
751
|
+
// verdict and the errors with the interpreted engine. Smaller and quicker to
|
|
752
|
+
// load than generated code, slower per check once warm (about five times on
|
|
753
|
+
// SchemaStore documents). Answers as toCompactModule's module does. Null
|
|
754
|
+
// where toCompactModule would be null.
|
|
755
|
+
function toSchemaModule(validator, opts) {
|
|
756
|
+
assertEmittable(validator, 'toSchemaModule');
|
|
757
|
+
if (!opts || typeof opts.runtime !== 'string') throw new TypeError('toSchemaModule: opts.runtime (the import specifier of the schema runtime) is required');
|
|
758
|
+
if (compactDecline(validator) !== null) return null;
|
|
759
|
+
const format = opts.format || 'esm';
|
|
760
|
+
const { stripAnnotations } = require('./schema-strip');
|
|
761
|
+
const { isV1Dialect } = require('./dialect');
|
|
762
|
+
const sch = validator._schemaObj;
|
|
763
|
+
const stripped = stripAnnotations(sch);
|
|
764
|
+
const text = JSON.stringify(JSON.stringify(stripped)).replace(/</g, '\\u003c');
|
|
765
|
+
const seen = new Set();
|
|
766
|
+
const flatWalk = (n, root) => {
|
|
767
|
+
if (n === null || typeof n !== 'object' || seen.has(n)) return true;
|
|
768
|
+
seen.add(n);
|
|
769
|
+
if (Array.isArray(n)) return n.every((x) => flatWalk(x, false));
|
|
770
|
+
if ('$anchor' in n || '$dynamicAnchor' in n || (!root && '$id' in n)) return false;
|
|
771
|
+
return Object.keys(n).every((k) => flatWalk(n[k], false));
|
|
772
|
+
};
|
|
773
|
+
const rtOpts = [isV1Dialect(sch) ? 'v1: true' : '', flatWalk(stripped, true) ? 'flat: true' : ''].filter(Boolean).join(', ');
|
|
774
|
+
const hashSrc = hashOf(validator);
|
|
775
|
+
const spec = JSON.stringify(opts.runtime);
|
|
776
|
+
const imp = format === 'esm'
|
|
777
|
+
? `import { createValidator as _ataCreateValidator } from ${spec};\n`
|
|
778
|
+
: `const { createValidator: _ataCreateValidator } = require(${spec});\n`;
|
|
779
|
+
const exp = format === 'esm'
|
|
780
|
+
? 'export { validate, isValid, schemaHash, ataVersion };\nexport default { validate, isValid, schemaHash, ataVersion };\n'
|
|
781
|
+
: 'module.exports = { validate, isValid, schemaHash, ataVersion };\nmodule.exports.default = module.exports;\n';
|
|
782
|
+
return `// Auto-generated by ata-validator ${ATA_VERSION}, do not edit.
|
|
783
|
+
// Schema module: the schema; the verdict and the errors come from the runtime it imports.
|
|
784
|
+
// Re-run the build after upgrading ata: the version above is what wrote this.
|
|
785
|
+
'use strict';
|
|
786
|
+
${imp}const VALID = Object.freeze({ valid: true, errors: Object.freeze([]) });
|
|
787
|
+
const ABORT = Object.freeze({ valid: false, errors: Object.freeze([Object.freeze({ code: 'ATA9000', message: 'validation failed', keyword: '__abort_early__', path: '' })]) });
|
|
788
|
+
let _ataV = null;
|
|
789
|
+
function _ataGet() { return _ataV !== null ? _ataV : (_ataV = _ataCreateValidator(JSON.parse(${text})${rtOpts ? ', { ' + rtOpts + ' }' : ''})); }
|
|
790
|
+
function isValid(data) { return _ataGet().isValid(data); }
|
|
791
|
+
function validate(data) { const v = _ataGet(); if (v.isValid(data)) return VALID; const e = v.errors(data); return e === null ? ABORT : { valid: false, errors: e }; }
|
|
792
|
+
const schemaHash = ${JSON.stringify(hashSrc)};
|
|
793
|
+
const ataVersion = ${JSON.stringify(ATA_VERSION)};
|
|
794
|
+
${exp}`;
|
|
795
|
+
}
|
|
796
|
+
|
|
614
797
|
// Bundle multiple validators into a single JS file for fast startup.
|
|
615
798
|
// Usage:
|
|
616
799
|
// const bundle = Validator.bundle([schema1, schema2, ...]);
|
|
@@ -925,6 +1108,23 @@ const htmlSafeResult = (fn) => function () { return htmlSafe(fn.apply(this, argu
|
|
|
925
1108
|
module.exports = {
|
|
926
1109
|
toStandalone: htmlSafeResult(toStandalone),
|
|
927
1110
|
toStandaloneModule: htmlSafeResult(toStandaloneModule),
|
|
1111
|
+
toCompactModule: htmlSafeResult(toCompactModule),
|
|
1112
|
+
toSchemaModule: htmlSafeResult(toSchemaModule),
|
|
1113
|
+
compactDecline,
|
|
1114
|
+
// `schemas`: the validators (or schemas) whose compact modules will import
|
|
1115
|
+
// this runtime; parts none of them needs are left out. Omitted, the
|
|
1116
|
+
// runtime carries every part.
|
|
1117
|
+
// `schemaMode`: the runtime schema modules import, which also answers the
|
|
1118
|
+
// verdict (createValidator).
|
|
1119
|
+
errorRuntimeSource: htmlSafeResult((format, schemas, schemaMode) => {
|
|
1120
|
+
const er = loadErrorRuntime();
|
|
1121
|
+
if (!Array.isArray(schemas)) return er.errorRuntimeSource(format, ATA_VERSION, undefined, !!schemaMode);
|
|
1122
|
+
const { stripAnnotations } = require('./schema-strip');
|
|
1123
|
+
const embedded = schemas.map((x) => stripAnnotations(x && typeof x === 'object' && x._schemaObj !== undefined ? x._schemaObj : x));
|
|
1124
|
+
const features = er.runtimeFeatures(embedded);
|
|
1125
|
+
for (const x of schemas) if (x && x._jsFn && x._jsFn._usesSafeRe) features['safe-regex'] = true;
|
|
1126
|
+
return er.errorRuntimeSource(format, ATA_VERSION, features, !!schemaMode);
|
|
1127
|
+
}),
|
|
928
1128
|
bundle: htmlSafeResult(bundle),
|
|
929
1129
|
bundleStandalone: htmlSafeResult(bundleStandalone),
|
|
930
1130
|
bundleCompact: htmlSafeResult(bundleCompact),
|