ata-validator 1.49.0 → 1.51.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 +50 -0
- package/aot.mjs +1 -1
- package/bin/ata.js +35 -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 +386 -69
- package/lib/aot-impl.js +240 -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-order.js +64 -2
- package/lib/schema-runtime-entry.js +45 -0
- package/lib/schema-strip.js +115 -0
- package/lib/tools.mjs +21 -0
- package/lib/validator-core.js +20 -5
- package/lib/version.js +1 -1
- package/package.json +28 -8
package/lib/aot-build.js
CHANGED
|
@@ -87,19 +87,246 @@ 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
|
+
|
|
112
|
+
// The registry without the definitions no module reaches. A shared
|
|
113
|
+
// definitions file (Kubernetes ships one with hundreds) is mostly what a
|
|
114
|
+
// given set of schemas never uses. Only `definitions` and `$defs` entries are
|
|
115
|
+
// dropped, and only from a document every reference into which is a plain
|
|
116
|
+
// pointer into them; a document reached as a whole, through an anchor, or
|
|
117
|
+
// holding an embedded $id is kept as it is. The result is checked: every
|
|
118
|
+
// root's references must still resolve against it, or the whole registry is
|
|
119
|
+
// kept.
|
|
120
|
+
function pruneRegistry(roots, byId) {
|
|
121
|
+
const used = new Map();
|
|
122
|
+
const keepWhole = new Set();
|
|
123
|
+
const seen = new Set();
|
|
124
|
+
const queue = [];
|
|
125
|
+
const hasInnerId = new Map();
|
|
126
|
+
const innerId = (doc) => {
|
|
127
|
+
let hit = hasInnerId.get(doc);
|
|
128
|
+
if (hit !== undefined) return hit;
|
|
129
|
+
hit = false;
|
|
130
|
+
const stack = [doc];
|
|
131
|
+
const visited = new Set();
|
|
132
|
+
while (stack.length && !hit) {
|
|
133
|
+
const n = stack.pop();
|
|
134
|
+
if (n === null || typeof n !== 'object' || visited.has(n)) continue;
|
|
135
|
+
visited.add(n);
|
|
136
|
+
if (n !== doc && !Array.isArray(n) && typeof n.$id === 'string') hit = true;
|
|
137
|
+
for (const k of Object.keys(n)) stack.push(n[k]);
|
|
138
|
+
}
|
|
139
|
+
hasInnerId.set(doc, hit);
|
|
140
|
+
return hit;
|
|
141
|
+
};
|
|
142
|
+
const docOf = new Map();
|
|
143
|
+
for (const [id, doc] of byId) docOf.set(id.replace(/#$/, ''), doc);
|
|
144
|
+
const reach = (doc, base, node) => {
|
|
145
|
+
if (node === null || typeof node !== 'object' || seen.has(node)) return;
|
|
146
|
+
seen.add(node);
|
|
147
|
+
queue.push([doc, base, node]);
|
|
148
|
+
};
|
|
149
|
+
const defsOf = (doc) => (doc.$defs && typeof doc.$defs === 'object' ? '$defs' : doc.definitions && typeof doc.definitions === 'object' ? 'definitions' : null);
|
|
150
|
+
for (const root of roots) reach(root, typeof root.$id === 'string' ? root.$id : null, root);
|
|
151
|
+
while (queue.length) {
|
|
152
|
+
const [doc, base, node] = queue.pop();
|
|
153
|
+
if (Array.isArray(node)) { for (const x of node) reach(doc, base, x); continue; }
|
|
154
|
+
for (const k of Object.keys(node)) {
|
|
155
|
+
const v = node[k];
|
|
156
|
+
if ((k === '$ref' || k === '$dynamicRef' || k === '$recursiveRef') && typeof v === 'string') {
|
|
157
|
+
let abs;
|
|
158
|
+
try { abs = base ? new URL(v, base).href : v.charCodeAt(0) === 35 ? null : new URL(v).href; } catch { abs = null; }
|
|
159
|
+
const local = v.charCodeAt(0) === 35;
|
|
160
|
+
const hash = abs === null ? -1 : abs.indexOf('#');
|
|
161
|
+
const uri = abs === null ? null : hash < 0 ? abs : abs.slice(0, hash);
|
|
162
|
+
const target = local ? doc : uri !== null ? docOf.get(uri) : undefined;
|
|
163
|
+
if (target === undefined) continue;
|
|
164
|
+
const frag = local ? v.slice(1) : hash < 0 ? '' : abs.slice(hash + 1);
|
|
165
|
+
const tbase = local ? base : uri;
|
|
166
|
+
const m = /^\/(\$defs|definitions)\/([^/]+)/.exec(frag);
|
|
167
|
+
const which = defsOf(target);
|
|
168
|
+
if (k !== '$ref' || m === null || m[1] !== which) {
|
|
169
|
+
keepWhole.add(target);
|
|
170
|
+
reach(target, tbase, target);
|
|
171
|
+
continue;
|
|
172
|
+
}
|
|
173
|
+
let name = m[2];
|
|
174
|
+
try { name = decodeURIComponent(name); } catch {}
|
|
175
|
+
name = name.replace(/~1/g, '/').replace(/~0/g, '~');
|
|
176
|
+
let set = used.get(target);
|
|
177
|
+
if (set === undefined) { set = new Set(); used.set(target, set); }
|
|
178
|
+
set.add(name);
|
|
179
|
+
// The rest of the document around its definitions is used too.
|
|
180
|
+
for (const key of Object.keys(target)) if (key !== which) reach(target, tbase, target[key]);
|
|
181
|
+
reach(target, tbase, target[which][name]);
|
|
182
|
+
} else if (v !== null && typeof v === 'object') {
|
|
183
|
+
reach(doc, base, v);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
const out = new Map();
|
|
188
|
+
for (const [id, doc] of byId) {
|
|
189
|
+
const which = defsOf(doc);
|
|
190
|
+
if (which === null || keepWhole.has(doc) || innerId(doc) || roots.includes(doc)) { out.set(id, doc); continue; }
|
|
191
|
+
const names = used.get(doc) || new Set();
|
|
192
|
+
const defs = {};
|
|
193
|
+
for (const name of Object.keys(doc[which])) if (names.has(name)) Object.defineProperty(defs, name, { value: doc[which][name], enumerable: true, writable: true, configurable: true });
|
|
194
|
+
out.set(id, { ...doc, [which]: defs });
|
|
195
|
+
}
|
|
196
|
+
for (const root of roots) {
|
|
197
|
+
if (!aot._refsResolveWithin(root, out)) return byId;
|
|
198
|
+
}
|
|
199
|
+
// The registry documents' own references too, as the runtime may walk any
|
|
200
|
+
// part of a definition it keeps.
|
|
201
|
+
for (const doc of out.values()) if (!aot._refsResolveWithin(doc, out)) return byId;
|
|
202
|
+
return out;
|
|
203
|
+
}
|
|
204
|
+
|
|
90
205
|
async function build(opts) {
|
|
91
206
|
const globs = opts.globs || [];
|
|
92
207
|
if (globs.length === 0) throw new Error('build: at least one glob required');
|
|
93
208
|
const format = opts.format || 'esm';
|
|
209
|
+
const errorsMode = opts.errors || 'auto';
|
|
210
|
+
if (!ERRORS_MODES.has(errorsMode)) throw new Error(`build: errors must be auto, inline or shared (got "${errorsMode}")`);
|
|
94
211
|
const inputs = await expandGlobs(globs);
|
|
95
212
|
const cache = readCache(opts.cacheFile);
|
|
96
213
|
const newCache = {};
|
|
214
|
+
const source = resolveSourceDefault(opts);
|
|
215
|
+
// Error detail from the shared runtime replaces generated error code, so it
|
|
216
|
+
// is only on offer where that code is what the build would emit: not with
|
|
217
|
+
// schema-source frames (development builds), stub errors or the one-pass
|
|
218
|
+
// function, which are each asked for by name.
|
|
219
|
+
const sharable = errorsMode !== 'inline' && !source && !opts.abortEarly && opts.onePass !== true;
|
|
220
|
+
// `mode: 'schema'`, asked for by name: every module that can be is a schema
|
|
221
|
+
// module (its schema and a few lines), with the verdict and the errors from
|
|
222
|
+
// one runtime. Smaller and quicker to load than generated code, slower per
|
|
223
|
+
// check once warm.
|
|
224
|
+
if (opts.mode !== undefined && opts.mode !== 'code' && opts.mode !== 'schema') throw new Error(`build: mode must be code or schema (got "${opts.mode}")`);
|
|
225
|
+
const schemaMode = opts.mode === 'schema' && !opts.abortEarly;
|
|
97
226
|
|
|
98
227
|
const compiled = [];
|
|
99
228
|
const cached = [];
|
|
100
229
|
const skipped = [];
|
|
101
230
|
const failed = [];
|
|
102
231
|
const warnings = [];
|
|
232
|
+
const entries = [];
|
|
233
|
+
|
|
234
|
+
// Schemas that reference each other: a schema whose references do not all
|
|
235
|
+
// land inside it gets the other inputs registered beside it, each under its
|
|
236
|
+
// $id, or, with `baseUri`, an input without one under the base URI and its
|
|
237
|
+
// file name. In schema mode its module then resolves against one registry
|
|
238
|
+
// module the build writes (registrySource); otherwise it is a
|
|
239
|
+
// self-contained module carrying what it references. A schema that
|
|
240
|
+
// resolves within itself is built exactly as it would be alone.
|
|
241
|
+
if (opts.baseUri !== undefined) {
|
|
242
|
+
try { new URL(opts.baseUri); } catch { throw new Error(`build: baseUri must be an absolute URI (got "${opts.baseUri}")`); }
|
|
243
|
+
}
|
|
244
|
+
let siblings = [];
|
|
245
|
+
for (const input of inputs) {
|
|
246
|
+
try {
|
|
247
|
+
const sch = parseSchemaFile(input);
|
|
248
|
+
if (sch === null || typeof sch !== 'object' || Array.isArray(sch)) continue;
|
|
249
|
+
if (typeof sch.$id === 'string' && sch.$id !== '') siblings.push({ input, schema: sch });
|
|
250
|
+
else if (opts.baseUri !== undefined) siblings.push({ input, schema: { $id: new URL(path.basename(input), opts.baseUri).href, ...sch } });
|
|
251
|
+
} catch {}
|
|
252
|
+
}
|
|
253
|
+
if (siblings.length === 0) siblings = null;
|
|
254
|
+
const REGISTRY_PLACEHOLDER = '__ATA_SCHEMA_REGISTRY__';
|
|
255
|
+
|
|
256
|
+
const compileOne = (input, raw) => {
|
|
257
|
+
const schema = parseSchemaFile(input);
|
|
258
|
+
// The build is where an authoring mistake is cheapest to stop: a typo
|
|
259
|
+
// like maxLenght compiles into a validator that simply lacks the rule,
|
|
260
|
+
// and a compiled module fails even more quietly than a runtime one,
|
|
261
|
+
// since nobody ever revisits it. Findings fail the input's build.
|
|
262
|
+
if (opts.strictSchema) {
|
|
263
|
+
const { checkSchemaStrict } = require('./strict-check');
|
|
264
|
+
const problems = checkSchemaStrict(schema, {});
|
|
265
|
+
if (problems.length > 0) {
|
|
266
|
+
return { error: problems.map((x) => `strict mode: ${x.message} at ${x.path}`).join('; ') };
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
const needs = siblings !== null && schema !== null && typeof schema === 'object' && !Array.isArray(schema) && !aot._refsResolveWithin(schema);
|
|
270
|
+
const self = needs ? siblings.find((x) => x.input === input) : undefined;
|
|
271
|
+
const others = needs ? siblings.filter((x) => x.input !== input && (self === undefined || x.schema.$id !== self.schema.$id)).map((x) => x.schema) : null;
|
|
272
|
+
// Known under the base URI too, so its own relative references resolve
|
|
273
|
+
// against it.
|
|
274
|
+
const own = self !== undefined ? self.schema : schema;
|
|
275
|
+
const v = others !== null && others.length > 0 ? new Validator(own, { schemas: others }) : new Validator(schema);
|
|
276
|
+
let sourceMap = null;
|
|
277
|
+
if (source) {
|
|
278
|
+
// Only attempt position map for JSON (the scanner is JSON-only).
|
|
279
|
+
const ext = path.extname(input).toLowerCase();
|
|
280
|
+
if (ext === '.json') {
|
|
281
|
+
try { sourceMap = buildPositionMap(raw.toString('utf8')); }
|
|
282
|
+
catch { sourceMap = null; }
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
const schemaFile = path.relative(process.cwd(), input) || input;
|
|
286
|
+
const inputWarnings = [];
|
|
287
|
+
const src = aot.toStandaloneModule(v, {
|
|
288
|
+
format,
|
|
289
|
+
abortEarly: !!opts.abortEarly,
|
|
290
|
+
onePass: opts.onePass === true ? true : undefined,
|
|
291
|
+
source,
|
|
292
|
+
sourceMap,
|
|
293
|
+
schemaFile,
|
|
294
|
+
formatMode: opts.formatMode,
|
|
295
|
+
onWarning: (w, meta) => inputWarnings.push({ message: w, kind: (meta && meta.kind) || 'general' }),
|
|
296
|
+
});
|
|
297
|
+
const compact = schemaMode && src
|
|
298
|
+
? aot.toSchemaModule(v, others !== null && others.length > 0 ? { format, runtime: RUNTIME_PLACEHOLDER, registry: REGISTRY_PLACEHOLDER } : { format, runtime: RUNTIME_PLACEHOLDER })
|
|
299
|
+
: sharable && src ? aot.toCompactModule(v, { format, formatMode: opts.formatMode, errorRuntime: RUNTIME_PLACEHOLDER }) : null;
|
|
300
|
+
// The runtime parts this module's schema needs (see runtimeFeatures).
|
|
301
|
+
// A registry module's schema reaches the others: what it needs from the
|
|
302
|
+
// registry, by $id, is every schema registered beside it. A key that is
|
|
303
|
+
// not its schema's $id cannot be found by the runtime, which keys the
|
|
304
|
+
// registry by $id: such a module is written self-contained.
|
|
305
|
+
let registryEntries = null;
|
|
306
|
+
let compactOut = compact;
|
|
307
|
+
if (compactOut && others !== null && others.length > 0) {
|
|
308
|
+
// A schema without an $id is not in the registry: its module carries it.
|
|
309
|
+
registryEntries = typeof v._schemaObj.$id === 'string' ? [v._schemaObj] : [];
|
|
310
|
+
for (const [id, sub] of v._schemaMap) {
|
|
311
|
+
if (!sub || typeof sub !== 'object' || sub.$id !== id) { compactOut = null; registryEntries = null; break; }
|
|
312
|
+
registryEntries.push(sub);
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
const { stripAnnotations: _strip } = require('./schema-strip');
|
|
316
|
+
const features = compactOut ? require('./error-runtime').runtimeFeatures((registryEntries || [v._schemaObj]).map(_strip)) : null;
|
|
317
|
+
// A module that imports the pattern engine needs it in the runtime,
|
|
318
|
+
// whatever the schema scan said.
|
|
319
|
+
if (features && compactOut.includes('_ataCompileSafe')) features['safe-regex'] = true;
|
|
320
|
+
// Why there is no module, when the reason is a reference: which one, and
|
|
321
|
+
// what would resolve it.
|
|
322
|
+
let unresolved = null;
|
|
323
|
+
if (!src) {
|
|
324
|
+
const miss = {};
|
|
325
|
+
const registry = v._schemaMap && v._schemaMap.size > 0 ? v._schemaMap : null;
|
|
326
|
+
if (!aot._refsResolveWithin(v._schemaObj, registry, miss) && typeof miss.ref === 'string') unresolved = miss.ref;
|
|
327
|
+
}
|
|
328
|
+
return { schema, src, compact: compactOut, features, registryEntries, root: v._schemaObj, usesSiblings: others !== null, unresolved, inputWarnings };
|
|
329
|
+
};
|
|
103
330
|
|
|
104
331
|
for (const input of inputs) {
|
|
105
332
|
try {
|
|
@@ -107,100 +334,138 @@ async function build(opts) {
|
|
|
107
334
|
const inputHash = hashContent(raw);
|
|
108
335
|
const output = outputPathFor(input, { ...opts, format });
|
|
109
336
|
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 (
|
|
337
|
+
const fresh = !!(
|
|
124
338
|
cacheEntry &&
|
|
125
339
|
cacheEntry.inputHash === inputHash &&
|
|
126
340
|
cacheEntry.output === output &&
|
|
341
|
+
!!cacheEntry.sharable === sharable &&
|
|
342
|
+
!!cacheEntry.schemaMode === schemaMode &&
|
|
343
|
+
// A module built against the other inputs depends on them too, and
|
|
344
|
+
// the registry is put together from such modules: always rebuilt.
|
|
345
|
+
!cacheEntry.usesSiblings &&
|
|
127
346
|
fs.existsSync(output) &&
|
|
128
347
|
cacheEntry.outputHash === hashContent(fs.readFileSync(output))
|
|
129
|
-
)
|
|
130
|
-
|
|
131
|
-
|
|
348
|
+
);
|
|
349
|
+
if (opts.check) {
|
|
350
|
+
if (fresh) cached.push({ input, output });
|
|
132
351
|
continue;
|
|
133
352
|
}
|
|
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
|
-
}
|
|
353
|
+
if (fresh) {
|
|
354
|
+
entries.push({ input, output, raw, inputHash, cacheEntry, fullGz: cacheEntry.fullGz, compactGz: cacheEntry.compactGz, features: cacheEntry.features || null });
|
|
355
|
+
continue;
|
|
157
356
|
}
|
|
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) {
|
|
357
|
+
const r = compileOne(input, raw);
|
|
358
|
+
if (r.error) { failed.push({ input, error: r.error }); continue; }
|
|
359
|
+
if (r.inputWarnings.length > 0) {
|
|
171
360
|
if (opts.strict) {
|
|
172
|
-
failed.push({ input, error: inputWarnings.map((x) => x.message).join('; ') });
|
|
361
|
+
failed.push({ input, error: r.inputWarnings.map((x) => x.message).join('; ') });
|
|
173
362
|
continue;
|
|
174
363
|
}
|
|
175
364
|
// `kind` says which capability degraded ('error-detail', 'parse'),
|
|
176
365
|
// 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 });
|
|
366
|
+
for (const x of r.inputWarnings) warnings.push({ input, warning: x.message, kind: x.kind });
|
|
178
367
|
}
|
|
179
|
-
if (!src) {
|
|
180
|
-
const reason =
|
|
368
|
+
if (!r.src) {
|
|
369
|
+
const reason = r.unresolved !== null
|
|
370
|
+
? `$ref "${r.unresolved}" does not resolve: no input has that $id. Add the schema it names as an input${opts.baseUri === undefined ? ', and if that file has no $id, give it one with --base-uri' : ''}`
|
|
371
|
+
: 'schema is not AOT-compatible (toStandaloneModule returned null)';
|
|
181
372
|
if (opts.strict) failed.push({ input, error: reason });
|
|
182
373
|
else skipped.push({ input, reason });
|
|
183
374
|
continue;
|
|
184
375
|
}
|
|
376
|
+
entries.push({
|
|
377
|
+
input, output, raw, inputHash, schema: r.schema, src: r.src, compact: r.compact, features: r.features, registryEntries: r.registryEntries, root: r.root, usesSiblings: r.usesSiblings,
|
|
378
|
+
fullGz: zlib.gzipSync(r.src).length,
|
|
379
|
+
compactGz: r.compact ? zlib.gzipSync(r.compact).length : null,
|
|
380
|
+
});
|
|
381
|
+
} catch (e) {
|
|
382
|
+
failed.push({ input, error: e.message });
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
if (opts.check) {
|
|
387
|
+
const staleCount = inputs.length - cached.length;
|
|
388
|
+
return { compiled: [], cached, skipped, failed, warnings, staleCount };
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
// A module takes its errors from the shared runtime when that makes it
|
|
392
|
+
// smaller, and the runtime is written when the modules that use it save
|
|
393
|
+
// more than it weighs. With `errors: 'shared'` every module that can, does.
|
|
394
|
+
// The runtime carries only the parts the compact modules' schemas need
|
|
395
|
+
// (formats, the pattern engine, deep equality); an entry with no record of
|
|
396
|
+
// its needs (a cache written before this) counts as needing everything.
|
|
397
|
+
const { errorRuntimeSource, runtimeFeatures } = require('./error-runtime');
|
|
398
|
+
const ATA_VERSION = require('./version');
|
|
399
|
+
const unionOf = (list) => {
|
|
400
|
+
const out = runtimeFeatures([]);
|
|
401
|
+
for (const e of list) for (const k of Object.keys(out)) if (!e.features || e.features[k]) out[k] = true;
|
|
402
|
+
return out;
|
|
403
|
+
};
|
|
404
|
+
const candidates = entries.filter((e) => typeof e.compactGz === 'number' && (schemaMode || errorsMode === 'shared' || e.compactGz < e.fullGz));
|
|
405
|
+
let runtime = null;
|
|
406
|
+
if ((sharable || schemaMode) && candidates.length > 0) {
|
|
407
|
+
const runtimeSrc = errorRuntimeSource(format, ATA_VERSION, unionOf(candidates), schemaMode);
|
|
408
|
+
const runtimeGz = zlib.gzipSync(runtimeSrc).length;
|
|
409
|
+
let saving = 0;
|
|
410
|
+
for (const e of candidates) saving += e.fullGz - e.compactGz;
|
|
411
|
+
const use = schemaMode || errorsMode === 'shared' || saving > runtimeGz;
|
|
412
|
+
if (use) runtime = { path: runtimePathFor(entries.map((e) => e.output), opts, format), src: runtimeSrc, gz: runtimeGz };
|
|
413
|
+
}
|
|
414
|
+
for (const e of entries) {
|
|
415
|
+
e.mode = runtime !== null && candidates.includes(e) ? (schemaMode ? 'schema' : 'compact') : 'inline';
|
|
416
|
+
}
|
|
417
|
+
// The registry the schema modules resolve against: every schema any of them
|
|
418
|
+
// needs, once, by $id.
|
|
419
|
+
let registryPath = null;
|
|
420
|
+
let registrySrc = null;
|
|
421
|
+
if (runtime !== null) {
|
|
422
|
+
const byId = new Map();
|
|
423
|
+
for (const e of entries) if (e.mode !== 'inline' && e.registryEntries) for (const sub of e.registryEntries) if (!byId.has(sub.$id)) byId.set(sub.$id, sub);
|
|
424
|
+
if (byId.size > 0) {
|
|
425
|
+
registryPath = path.join(path.dirname(runtime.path), 'ata-registry' + (format === 'cjs' ? '.cjs' : '.mjs'));
|
|
426
|
+
const roots = entries.filter((e) => e.mode !== 'inline' && e.registryEntries).map((e) => e.root);
|
|
427
|
+
registrySrc = aot.registrySource([...pruneRegistry(roots, byId).values()], format);
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
for (const e of entries) {
|
|
432
|
+
const { input, output } = e;
|
|
433
|
+
try {
|
|
434
|
+
if (e.cacheEntry) {
|
|
435
|
+
const sameRuntime = e.mode === 'inline' || (runtime !== null && e.cacheEntry.runtime === runtime.path);
|
|
436
|
+
if (e.cacheEntry.mode === e.mode && sameRuntime) {
|
|
437
|
+
cached.push({ input, output });
|
|
438
|
+
newCache[input] = e.cacheEntry;
|
|
439
|
+
continue;
|
|
440
|
+
}
|
|
441
|
+
// The build's choice for this module changed: compile it again.
|
|
442
|
+
const r = compileOne(input, e.raw);
|
|
443
|
+
if (r.error || !r.src) { failed.push({ input, error: r.error || 'schema is not AOT-compatible' }); continue; }
|
|
444
|
+
e.schema = r.schema; e.src = r.src; e.compact = r.compact; e.features = r.features;
|
|
445
|
+
if (e.mode !== 'inline' && !r.compact) e.mode = 'inline';
|
|
446
|
+
}
|
|
447
|
+
let src = e.mode !== 'inline'
|
|
448
|
+
? e.compact.split(JSON.stringify(RUNTIME_PLACEHOLDER)).join(JSON.stringify(importSpecifier(output, runtime.path)))
|
|
449
|
+
: e.src;
|
|
450
|
+
if (e.mode !== 'inline' && e.registryEntries) src = src.split(JSON.stringify(REGISTRY_PLACEHOLDER)).join(JSON.stringify(importSpecifier(output, registryPath)));
|
|
185
451
|
fs.mkdirSync(path.dirname(output), { recursive: true });
|
|
186
452
|
fs.writeFileSync(output, src);
|
|
187
453
|
const outBytes = Buffer.byteLength(src, 'utf8');
|
|
188
|
-
const
|
|
189
|
-
const gzBytes = gz.length;
|
|
454
|
+
const gzBytes = zlib.gzipSync(src).length;
|
|
190
455
|
if (typeof opts.maxSize === 'number' && gzBytes > opts.maxSize) {
|
|
191
456
|
// Roll back the write so a failed build doesn't leave a stale artifact.
|
|
192
457
|
try { fs.unlinkSync(output); } catch {}
|
|
193
458
|
failed.push({ input, error: `output ${output} exceeds --max-size: ${gzBytes} > ${opts.maxSize} (gzipped bytes)` });
|
|
194
459
|
continue;
|
|
195
460
|
}
|
|
196
|
-
compiled.push({ input, output, bytes: outBytes, gzipBytes: gzBytes });
|
|
461
|
+
compiled.push({ input, output, bytes: outBytes, gzipBytes: gzBytes, errors: e.mode });
|
|
197
462
|
if (opts.types !== false) {
|
|
198
463
|
const { toTypeScript } = require('./ts-gen');
|
|
199
464
|
const stem = path.basename(input, path.extname(input)).replace(/\.schema$/, '');
|
|
200
465
|
const typeName = stem
|
|
201
466
|
.replace(/[^A-Za-z0-9_]/g, '_')
|
|
202
467
|
.replace(/^([a-z])/, (m) => m.toUpperCase()) || 'Data';
|
|
203
|
-
const dts = toTypeScript(schema, { name: typeName });
|
|
468
|
+
const dts = toTypeScript(e.schema, { name: typeName });
|
|
204
469
|
const ext = path.extname(output);
|
|
205
470
|
const dtsExt = ext === '.mjs' ? '.d.mts'
|
|
206
471
|
: ext === '.cjs' ? '.d.cts'
|
|
@@ -209,23 +474,46 @@ async function build(opts) {
|
|
|
209
474
|
fs.writeFileSync(base + dtsExt, dts);
|
|
210
475
|
}
|
|
211
476
|
newCache[input] = {
|
|
212
|
-
inputHash,
|
|
477
|
+
inputHash: e.inputHash,
|
|
213
478
|
output,
|
|
214
479
|
outputHash: hashContent(Buffer.from(src, 'utf8')),
|
|
480
|
+
sharable,
|
|
481
|
+
schemaMode,
|
|
482
|
+
mode: e.mode,
|
|
483
|
+
runtime: e.mode !== 'inline' ? runtime.path : undefined,
|
|
484
|
+
fullGz: e.fullGz,
|
|
485
|
+
compactGz: e.compactGz,
|
|
486
|
+
features: e.features || undefined,
|
|
487
|
+
usesSiblings: e.usesSiblings || undefined,
|
|
215
488
|
};
|
|
216
|
-
} catch (
|
|
217
|
-
failed.push({ input, error:
|
|
489
|
+
} catch (err) {
|
|
490
|
+
failed.push({ input, error: err.message });
|
|
218
491
|
}
|
|
219
492
|
}
|
|
220
493
|
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
494
|
+
let runtimeReport = null;
|
|
495
|
+
if (runtime !== null && entries.some((e) => e.mode !== 'inline')) {
|
|
496
|
+
const current = fs.existsSync(runtime.path) ? fs.readFileSync(runtime.path, 'utf8') : null;
|
|
497
|
+
if (current !== runtime.src) {
|
|
498
|
+
fs.mkdirSync(path.dirname(runtime.path), { recursive: true });
|
|
499
|
+
fs.writeFileSync(runtime.path, runtime.src);
|
|
500
|
+
}
|
|
501
|
+
runtimeReport = { output: runtime.path, bytes: Buffer.byteLength(runtime.src, 'utf8'), gzipBytes: runtime.gz };
|
|
502
|
+
if (registrySrc !== null) {
|
|
503
|
+
const cur = fs.existsSync(registryPath) ? fs.readFileSync(registryPath, 'utf8') : null;
|
|
504
|
+
if (cur !== registrySrc) fs.writeFileSync(registryPath, registrySrc);
|
|
505
|
+
runtimeReport.registry = { output: registryPath, bytes: Buffer.byteLength(registrySrc, 'utf8'), gzipBytes: zlib.gzipSync(registrySrc).length };
|
|
506
|
+
}
|
|
224
507
|
}
|
|
225
508
|
|
|
226
509
|
writeCache(opts.cacheFile, newCache);
|
|
227
510
|
|
|
228
|
-
|
|
511
|
+
// The self-contained source is built for every input, so its error-detail
|
|
512
|
+
// decline is recorded even where the module written is a compact or schema
|
|
513
|
+
// module, whose errors come from the runtime. Only the written one counts.
|
|
514
|
+
const fromRuntime = new Set(entries.filter((e) => e.mode !== 'inline').map((e) => e.input));
|
|
515
|
+
const kept = warnings.filter((w) => !(w.kind === 'error-detail' && fromRuntime.has(w.input)));
|
|
516
|
+
return { compiled, cached, skipped, failed, warnings: kept, runtime: runtimeReport };
|
|
229
517
|
}
|
|
230
518
|
|
|
231
519
|
async function watch(opts, onReport) {
|
|
@@ -322,12 +610,41 @@ function compiledSchemaFor(schema) {
|
|
|
322
610
|
// error function the generator declines (unevaluated* among them), where the
|
|
323
611
|
// module would report every failure as the ATA9000 stub while the runtime
|
|
324
612
|
// reports the real errors.
|
|
613
|
+
//
|
|
614
|
+
// With `errorRuntime` (an import specifier, usually 'ata-validator/error-runtime'),
|
|
615
|
+
// the module is written compact where the validator allows it: the verdict and
|
|
616
|
+
// the schema, with errors from that runtime (see toCompactModule). A plugin
|
|
617
|
+
// writes it in place of the self-contained module when that is smaller for
|
|
618
|
+
// the whole bundle; the runtime is shared by every compact module in it.
|
|
619
|
+
// A plugin asks for both modules of one schema (the self-contained one, and
|
|
620
|
+
// the compact one to compare): one Validator answers both, so the verdict
|
|
621
|
+
// function compiles once. Keyed by the schema object the plugin passes.
|
|
622
|
+
const _validators = new WeakMap();
|
|
623
|
+
function validatorFor(schema) {
|
|
624
|
+
let v = _validators.get(schema);
|
|
625
|
+
// Built with an options object, empty, so it stays out of the identity
|
|
626
|
+
// cache: `new Validator(schema)` in the caller's own code must get its own
|
|
627
|
+
// instance, not this compiled one.
|
|
628
|
+
if (v === undefined) { v = new Validator(schema, {}); _validators.set(schema, v); }
|
|
629
|
+
return v;
|
|
630
|
+
}
|
|
631
|
+
|
|
325
632
|
function compiledModuleFor(schema, opts) {
|
|
326
633
|
if (!compiledEligible(schema)) return null;
|
|
634
|
+
if (opts && typeof opts.errorRuntime === 'string') {
|
|
635
|
+
try {
|
|
636
|
+
const v = validatorFor(schema);
|
|
637
|
+
const src = aot.toCompactModule(v, { format: opts.format || 'esm', errorRuntime: opts.errorRuntime });
|
|
638
|
+
if (src) return src;
|
|
639
|
+
} catch {
|
|
640
|
+
return null;
|
|
641
|
+
}
|
|
642
|
+
// Not writable as a compact module: the self-contained one below.
|
|
643
|
+
}
|
|
327
644
|
let degraded = false;
|
|
328
645
|
let src;
|
|
329
646
|
try {
|
|
330
|
-
src = toStandaloneModule(schema, {
|
|
647
|
+
src = aot.toStandaloneModule(validatorFor(schema), {
|
|
331
648
|
format: (opts && opts.format) || 'esm',
|
|
332
649
|
onWarning: (msg, info) => { if (info && info.kind === 'error-detail') degraded = true; },
|
|
333
650
|
});
|