@mfeflow/contracts 0.0.0-stage → 0.1.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/dist/index.js ADDED
@@ -0,0 +1,1417 @@
1
+ import { ENTRY_TYPES, GLOBAL_NAME, pickRegistryFields } from "./entry.js";
2
+ function isObj(value) {
3
+ return 'object' == typeof value && null !== value && !Array.isArray(value);
4
+ }
5
+ class Checker {
6
+ issues = [];
7
+ add(path, message) {
8
+ this.issues.push({
9
+ path,
10
+ message
11
+ });
12
+ }
13
+ string(obj, key, path, opts = {}) {
14
+ const value = obj[key];
15
+ const at = path ? `${path}.${key}` : key;
16
+ if (void 0 === value) {
17
+ if (opts.required ?? true) this.add(at, 'is required');
18
+ return;
19
+ }
20
+ if ('string' != typeof value) return void this.add(at, 'must be a string');
21
+ if (opts.pattern && !opts.pattern.test(value)) return void this.add(at, opts.hint ?? `must match ${String(opts.pattern)}`);
22
+ return value;
23
+ }
24
+ stringArray(obj, key, path, opts = {}) {
25
+ const value = obj[key];
26
+ const at = path ? `${path}.${key}` : key;
27
+ if (void 0 === value) {
28
+ if (opts.required ?? true) this.add(at, 'is required');
29
+ return [];
30
+ }
31
+ if (!Array.isArray(value)) {
32
+ this.add(at, 'must be an array of strings');
33
+ return [];
34
+ }
35
+ const out = [];
36
+ value.forEach((item, i)=>{
37
+ if ('string' != typeof item) this.add(`${at}[${String(i)}]`, 'must be a string');
38
+ else if (opts.pattern && !opts.pattern.test(item)) this.add(`${at}[${String(i)}]`, opts.hint ?? `must match ${String(opts.pattern)}`);
39
+ else out.push(item);
40
+ });
41
+ return out;
42
+ }
43
+ number(obj, key, path, opts = {}) {
44
+ const value = obj[key];
45
+ const at = path ? `${path}.${key}` : key;
46
+ if (void 0 === value) {
47
+ if (opts.required ?? true) this.add(at, 'is required');
48
+ return;
49
+ }
50
+ if ('number' != typeof value || Number.isNaN(value)) return void this.add(at, 'must be a number');
51
+ if (void 0 !== opts.min && value < opts.min) this.add(at, `must be ≥ ${String(opts.min)}`);
52
+ if (void 0 !== opts.max && value > opts.max) this.add(at, `must be ≤ ${String(opts.max)}`);
53
+ return value;
54
+ }
55
+ object(obj, key, path, required = true) {
56
+ const value = obj[key];
57
+ const at = path ? `${path}.${key}` : key;
58
+ if (void 0 === value) {
59
+ if (required) this.add(at, 'is required');
60
+ return;
61
+ }
62
+ if (!isObj(value)) return void this.add(at, 'must be an object');
63
+ return value;
64
+ }
65
+ }
66
+ function isUrl(value) {
67
+ try {
68
+ const u = new URL(value);
69
+ return 'https:' === u.protocol || 'http:' === u.protocol;
70
+ } catch {
71
+ return false;
72
+ }
73
+ }
74
+ const SEGMENT = String.raw`(?!\.\.?(?:/|$))[^/\\\u0000]+`;
75
+ const SAFE_RELATIVE_PATH = new RegExp(`^(?![A-Za-z]:)${SEGMENT}(?:/${SEGMENT})*$`);
76
+ function isSafeRelativePath(value) {
77
+ if ('' === value || value.includes('\\') || value.includes('\0')) return false;
78
+ if (value.startsWith('/') || /^[A-Za-z]:/.test(value)) return false;
79
+ return value.split('/').every((part)=>'' !== part && '.' !== part && '..' !== part);
80
+ }
81
+ const MFE_NAME = /^[a-z][a-z0-9]*(?:_[a-z][a-z0-9]*)*$/;
82
+ const ROUTE = /^\/[A-Za-z0-9/_:*.-]*$/;
83
+ const EXPOSE = /^\.\/[A-Za-z0-9_/-]+$/;
84
+ const EVENT_NAME = /^[a-z][a-z0-9-]*\.[a-z][a-z0-9_]*@[0-9]+$/;
85
+ const ANALYTICS_EVENT = /^[a-z][a-z0-9]*(_[a-z0-9]+)+$/;
86
+ function validateMfeManifest(value) {
87
+ const c = new Checker();
88
+ if (!isObj(value)) {
89
+ c.add('', 'must be an object');
90
+ return c.issues;
91
+ }
92
+ c.string(value, 'name', '', {
93
+ pattern: MFE_NAME,
94
+ hint: 'use lowercase words of letters and digits joined by single _ (a Module Federation name)'
95
+ });
96
+ c.string(value, 'owner', '');
97
+ c.string(value, 'contact', '', {
98
+ required: false
99
+ });
100
+ c.stringArray(value, 'shells', '', {
101
+ pattern: MFE_NAME,
102
+ hint: 'must be a shell name'
103
+ });
104
+ const routes = c.stringArray(value, 'routes', '', {
105
+ pattern: ROUTE,
106
+ hint: 'must start with "/"'
107
+ });
108
+ if (new Set(routes).size !== routes.length) c.add('routes', 'has duplicates');
109
+ c.stringArray(value, 'exposes', '', {
110
+ pattern: EXPOSE,
111
+ hint: 'must look like "./App"'
112
+ });
113
+ const events = c.object(value, 'events', '', false);
114
+ if (events) {
115
+ c.stringArray(events, 'publishes', 'events', {
116
+ pattern: EVENT_NAME,
117
+ hint: 'must look like "cart.item_added@1"'
118
+ });
119
+ c.stringArray(events, 'consumes', 'events', {
120
+ pattern: EVENT_NAME,
121
+ hint: 'must look like "cart.item_added@1"'
122
+ });
123
+ }
124
+ c.stringArray(value, 'analyticsEvents', '', {
125
+ required: false,
126
+ pattern: ANALYTICS_EVENT,
127
+ hint: 'must look like "checkout_order_completed"'
128
+ });
129
+ const budgets = c.object(value, 'budgets', '', false);
130
+ if (budgets) {
131
+ c.number(budgets, 'initialJsKb', 'budgets', {
132
+ required: false,
133
+ min: 1
134
+ });
135
+ c.number(budgets, 'lcpMs', 'budgets', {
136
+ required: false,
137
+ min: 1
138
+ });
139
+ }
140
+ const platform = c.object(value, 'platform', '', false);
141
+ if (platform) {
142
+ for (const [pkg, range] of Object.entries(platform))if ('string' != typeof range) c.add(`platform.${pkg}`, 'must be a semver range string');
143
+ }
144
+ return c.issues;
145
+ }
146
+ function isMfeManifest(value) {
147
+ return 0 === validateMfeManifest(value).length;
148
+ }
149
+ function fail(why) {
150
+ throw new Error(`mf-manifest.json: ${why}`);
151
+ }
152
+ function strings(value) {
153
+ return Array.isArray(value) ? value.filter((v)=>'string' == typeof v) : [];
154
+ }
155
+ function manifest_assets_list(value) {
156
+ return Array.isArray(value) ? value : [];
157
+ }
158
+ function joinEntry(dir, name) {
159
+ if ('' === dir) return name;
160
+ let path = dir;
161
+ if ('.' === path) path = '';
162
+ else if (path.startsWith('./')) path = path.slice(2);
163
+ else if (path.startsWith('/')) {
164
+ path = path.slice(1);
165
+ if (path.endsWith('/')) path = path.slice(0, -1);
166
+ }
167
+ if ('' === path) return name;
168
+ return path.endsWith('/') ? `${path}${name}` : `${path}/${name}`;
169
+ }
170
+ const RELATIVE_WITHOUT_PAGE = 'metaData.publicPath is relative, so the file URLs resolve against the shell page; pass pageUrl to compute them';
171
+ function isAbsolute(publicPath) {
172
+ return /^[a-z][a-z\d+.-]*:/i.test(publicPath) || publicPath.startsWith('//');
173
+ }
174
+ function resolveFile(publicPath, manifestUrl, pageUrl, file) {
175
+ if ('' === publicPath || 'auto' === publicPath) return new URL(file, manifestUrl).href;
176
+ return new URL(`${publicPath}${file}`, pageUrl).href;
177
+ }
178
+ function sync(assets, kind) {
179
+ const group = isObj(assets) ? assets[kind] : void 0;
180
+ return isObj(group) ? strings(group.sync) : [];
181
+ }
182
+ function assetsFromManifest(manifest, manifestUrl, options = {}) {
183
+ if (!isUrl(manifestUrl)) fail(`manifest URL "${manifestUrl}" is not an http(s) URL`);
184
+ if (!isObj(manifest)) fail('must be an object');
185
+ const meta = manifest.metaData;
186
+ if (!isObj(meta)) fail('has no metaData');
187
+ const entry = meta.remoteEntry;
188
+ if (!isObj(entry) || 'string' != typeof entry.name || '' === entry.name) fail('metaData.remoteEntry has no name');
189
+ if ('string' == typeof meta.getPublicPath && '' !== meta.getPublicPath) return {
190
+ exposes: {},
191
+ manifestOnly: 'metaData.getPublicPath computes the file URLs in the browser, so they cannot be known at publish time'
192
+ };
193
+ const publicPath = 'string' == typeof meta.publicPath ? meta.publicPath : '';
194
+ const { pageUrl } = options;
195
+ if (void 0 !== pageUrl && !isUrl(pageUrl)) fail(`page URL "${pageUrl}" is not an http(s) URL`);
196
+ const relative = '' !== publicPath && 'auto' !== publicPath && !isAbsolute(publicPath);
197
+ if (relative && void 0 === pageUrl) return {
198
+ exposes: {},
199
+ manifestOnly: RELATIVE_WITHOUT_PAGE
200
+ };
201
+ const at = (file)=>resolveFile(publicPath, manifestUrl, pageUrl ?? manifestUrl, file);
202
+ const exposes = {};
203
+ for (const expose of manifest_assets_list(manifest.exposes)){
204
+ if (!isObj(expose) || 'string' != typeof expose.path) continue;
205
+ const files = [
206
+ ...sync(expose.assets, 'css'),
207
+ ...sync(expose.assets, 'js')
208
+ ];
209
+ exposes[expose.path] = [
210
+ ...new Set(files)
211
+ ].map(at);
212
+ }
213
+ const entryPath = 'string' == typeof entry.path ? entry.path : '';
214
+ const type = 'string' == typeof entry.type && '' !== entry.type ? entry.type : void 0;
215
+ if (void 0 !== type && !ENTRY_TYPES.includes(type)) fail(`metaData.remoteEntry.type "${type}" is not a Module Federation entry type`);
216
+ const globalName = 'string' == typeof meta.globalName && '' !== meta.globalName ? meta.globalName : void 0;
217
+ if (void 0 !== globalName && !GLOBAL_NAME.test(globalName)) fail(`metaData.globalName "${globalName}" is not a valid global name`);
218
+ return {
219
+ remoteEntry: at(joinEntry(entryPath, entry.name)),
220
+ exposes,
221
+ ...void 0 === type ? {} : {
222
+ type
223
+ },
224
+ ...void 0 === globalName ? {} : {
225
+ entryGlobalName: globalName
226
+ }
227
+ };
228
+ }
229
+ const SCRIPT_UNSAFE = new RegExp('[<\\u2028\\u2029]', 'g');
230
+ function jsonForScript(value) {
231
+ return JSON.stringify(value).replace(SCRIPT_UNSAFE, (c)=>`\\u${c.charCodeAt(0).toString(16).padStart(4, '0')}`);
232
+ }
233
+ function attribute(value) {
234
+ return value.replace(/[&"<>]/g, (c)=>`&#${String(c.charCodeAt(0))};`);
235
+ }
236
+ const ESM_TYPES = new Set([
237
+ 'module',
238
+ 'esm'
239
+ ]);
240
+ function registryInjection(index, needs, options = {}) {
241
+ const inline = {
242
+ remotes: index.remotes.map((remote)=>pickRegistryFields(remote))
243
+ };
244
+ let html = `<script>window.__MFE_REGISTRY__=${jsonForScript(inline)}</script>`;
245
+ const seen = new Set();
246
+ const hint = (attrs, href)=>{
247
+ const tag = `<link ${attrs} href="${attribute(href)}">`;
248
+ if (seen.has(tag)) return;
249
+ seen.add(tag);
250
+ html += tag;
251
+ };
252
+ for (const need of needs){
253
+ const remote = index.remotes.find((r)=>r.name === need.remote);
254
+ if (void 0 === remote) continue;
255
+ if (void 0 === remote.remoteEntry) {
256
+ if (true === options.snapshot) hint('rel="preload" as="fetch" crossorigin', remote.entry);
257
+ continue;
258
+ }
259
+ const esm = void 0 !== remote.type && ESM_TYPES.has(remote.type);
260
+ const script = (href)=>hint(esm ? 'rel="modulepreload"' : 'rel="preload" as="script"', href);
261
+ script(remote.remoteEntry);
262
+ for (const file of remote.exposes?.[need.expose] ?? [])if (/\.css(\?|$)/.test(file)) hint('rel="preload" as="style"', file);
263
+ else script(file);
264
+ }
265
+ return html;
266
+ }
267
+ function tagEnd(html, from) {
268
+ const close = html.indexOf('>', from);
269
+ return -1 === close ? -1 : close + 1;
270
+ }
271
+ function sameChar(code, lower) {
272
+ return code === lower || lower >= 97 && lower <= 122 && code === lower - 32;
273
+ }
274
+ function indexOfCI(html, needle, from, to = html.length) {
275
+ const last = to - needle.length;
276
+ for(let at = from; at <= last; at++){
277
+ let i = 0;
278
+ while(i < needle.length && sameChar(html.charCodeAt(at + i), needle.charCodeAt(i)))i++;
279
+ if (i === needle.length) return at;
280
+ }
281
+ return -1;
282
+ }
283
+ function headEnd(html) {
284
+ let at = indexOfCI(html, '<head', 0);
285
+ while(-1 !== at){
286
+ const next = html.charAt(at + 5);
287
+ if ('>' === next || /\s/.test(next)) return tagEnd(html, at);
288
+ at = indexOfCI(html, '<head', at + 5);
289
+ }
290
+ return -1;
291
+ }
292
+ function charsetEnd(html, from) {
293
+ const stop = indexOfCI(html, '</head', from);
294
+ for(let at = indexOfCI(html, '<meta', from); -1 !== at;){
295
+ if (-1 !== stop && at > stop) break;
296
+ const end = tagEnd(html, at);
297
+ if (-1 === end) break;
298
+ if (-1 !== indexOfCI(html, 'charset', at, end)) return end;
299
+ at = indexOfCI(html, '<meta', end);
300
+ }
301
+ return -1;
302
+ }
303
+ function injectIntoHead(html, snippet) {
304
+ const open = headEnd(html);
305
+ if (-1 === open) throw new Error('@mfeflow/contracts: the HTML has no <head> to inject into');
306
+ const charset = charsetEnd(html, open);
307
+ const end = -1 === charset ? open : charset;
308
+ return html.slice(0, end) + snippet + html.slice(end);
309
+ }
310
+ const ENTRY_TYPE = new RegExp(`^(?:${ENTRY_TYPES.join('|')})$`);
311
+ const VERSION = /^[0-9A-Za-z.+-]+$/;
312
+ function checkRemoteFiles(c, obj, at) {
313
+ const entry = c.string(obj, 'remoteEntry', at, {
314
+ required: false
315
+ });
316
+ if (void 0 !== entry && !isUrl(entry)) c.add(at ? `${at}.remoteEntry` : 'remoteEntry', 'must be an http(s) URL');
317
+ c.string(obj, 'type', at, {
318
+ required: false,
319
+ pattern: ENTRY_TYPE,
320
+ hint: `must be a Module Federation entry type: ${ENTRY_TYPES.join(', ')}`
321
+ });
322
+ c.string(obj, 'entryGlobalName', at, {
323
+ required: false,
324
+ pattern: GLOBAL_NAME,
325
+ hint: 'must be a JavaScript global name (letters, digits, _, $, -; not starting with a digit)'
326
+ });
327
+ const exposes = c.object(obj, 'exposes', at, false);
328
+ if (!exposes) return;
329
+ const base = at ? `${at}.exposes` : 'exposes';
330
+ for (const [expose, files] of Object.entries(exposes)){
331
+ if (!EXPOSE.test(expose)) c.add(`${base}.${expose}`, 'must look like "./App"');
332
+ if (!Array.isArray(files)) {
333
+ c.add(`${base}.${expose}`, 'must be an array of URLs');
334
+ continue;
335
+ }
336
+ files.forEach((file, i)=>{
337
+ if ('string' != typeof file || !isUrl(file)) c.add(`${base}.${expose}[${String(i)}]`, 'must be an http(s) URL');
338
+ });
339
+ }
340
+ }
341
+ function validateRegistryEntry(value) {
342
+ const c = new Checker();
343
+ if (!isObj(value)) {
344
+ c.add('', 'must be an object');
345
+ return c.issues;
346
+ }
347
+ c.string(value, 'version', '', {
348
+ pattern: VERSION,
349
+ hint: 'must be a version like 1.4.2+a1b2c3d'
350
+ });
351
+ const url = c.string(value, 'manifestUrl', '');
352
+ if (void 0 !== url && !isUrl(url)) c.add('manifestUrl', 'must be an http(s) URL');
353
+ checkRemoteFiles(c, value, '');
354
+ c.string(value, 'previous', '', {
355
+ required: false,
356
+ pattern: VERSION,
357
+ hint: 'must be a version'
358
+ });
359
+ c.string(value, 'owner', '', {
360
+ required: false
361
+ });
362
+ const rollout = c.object(value, 'rollout', '', false);
363
+ if (rollout) {
364
+ c.string(rollout, 'candidate', 'rollout', {
365
+ pattern: VERSION,
366
+ hint: 'must be a version'
367
+ });
368
+ c.string(rollout, 'stable', 'rollout', {
369
+ pattern: VERSION,
370
+ hint: 'must be a version'
371
+ });
372
+ c.number(rollout, 'percent', 'rollout', {
373
+ min: 0,
374
+ max: 100
375
+ });
376
+ }
377
+ return c.issues;
378
+ }
379
+ function validateRegistryIndex(value) {
380
+ const c = new Checker();
381
+ if (!isObj(value)) {
382
+ c.add('', 'must be an object');
383
+ return c.issues;
384
+ }
385
+ for (const key of [
386
+ 'shell',
387
+ 'env',
388
+ 'generatedAt'
389
+ ])c.string(value, key, '', {
390
+ required: false
391
+ });
392
+ const remotes = value.remotes;
393
+ if (!Array.isArray(remotes)) {
394
+ c.add('remotes', 'must be an array');
395
+ return c.issues;
396
+ }
397
+ const names = new Set();
398
+ const claimed = new Map();
399
+ remotes.forEach((remote, i)=>{
400
+ const at = `remotes[${String(i)}]`;
401
+ if (!isObj(remote)) return void c.add(at, 'must be an object');
402
+ const name = c.string(remote, 'name', at, {
403
+ pattern: MFE_NAME,
404
+ hint: 'must be a Module Federation name'
405
+ });
406
+ const entry = c.string(remote, 'entry', at);
407
+ if (void 0 !== entry && !isUrl(entry)) c.add(`${at}.entry`, 'must be an http(s) URL');
408
+ checkRemoteFiles(c, remote, at);
409
+ c.string(remote, 'version', at, {
410
+ required: false
411
+ });
412
+ c.string(remote, 'owner', at, {
413
+ required: false
414
+ });
415
+ const routes = c.stringArray(remote, 'routes', at, {
416
+ required: false,
417
+ pattern: ROUTE,
418
+ hint: 'must start with "/"'
419
+ });
420
+ if (void 0 !== name) {
421
+ if (names.has(name)) c.add(`${at}.name`, `duplicate remote "${name}"`);
422
+ names.add(name);
423
+ for (const route of routes){
424
+ const owner = claimed.get(route);
425
+ if (void 0 !== owner && owner !== name) c.add(`${at}.routes`, `route "${route}" is already claimed by "${owner}"`);
426
+ claimed.set(route, name);
427
+ }
428
+ }
429
+ });
430
+ return c.issues;
431
+ }
432
+ function isRegistryEntry(value) {
433
+ return 0 === validateRegistryEntry(value).length;
434
+ }
435
+ function isRegistryIndex(value) {
436
+ return 0 === validateRegistryIndex(value).length;
437
+ }
438
+ const project_SEGMENT = String.raw`(?!\.\.?(?:/|$))[^/\\\u0000]+`;
439
+ const CHECKOUT_PATH = new RegExp(`^(?:(?![A-Za-z]:)(?:\\.\\.(?:/\\.\\.)*(?:/${project_SEGMENT})*|${project_SEGMENT}(?:/${project_SEGMENT})*)|(?:/|[A-Za-z]:/)${project_SEGMENT}(?:/${project_SEGMENT})*)$`);
440
+ const FILE_HASH = /^sha256-[0-9a-f]{64}$/;
441
+ const REGISTRY_NAMESPACE = /^@[a-z0-9][a-z0-9-]*$/;
442
+ function isCheckoutPath(value) {
443
+ if ('' === value || value.includes('\\') || value.includes('\0')) return false;
444
+ const absolute = /^(?:\/|[A-Za-z]:\/)/.exec(value);
445
+ if (absolute) {
446
+ const parts = value.slice(absolute[0].length).split('/');
447
+ return parts.every((part)=>'' !== part && '.' !== part && '..' !== part);
448
+ }
449
+ if (/^[A-Za-z]:/.test(value)) return false;
450
+ const parts = value.split('/');
451
+ const climbing = parts.findIndex((part)=>'..' !== part);
452
+ const rest = -1 === climbing ? [] : parts.slice(climbing);
453
+ return rest.every((part)=>'' !== part && '.' !== part && '..' !== part);
454
+ }
455
+ const SCOPE = /^@[a-z0-9][a-z0-9-._]*$/;
456
+ const FEATURE = /^(@[a-z0-9-]+\/)?[a-z][a-z0-9-]*$/;
457
+ function validateMfeflowProject(value) {
458
+ const c = new Checker();
459
+ if (!isObj(value)) {
460
+ c.add('', 'must be an object');
461
+ return c.issues;
462
+ }
463
+ if (1 !== value.version) c.add('version', 'must be 1');
464
+ const topology = c.string(value, 'topology', '');
465
+ if (void 0 !== topology && ![
466
+ 'monorepo',
467
+ 'polyrepo',
468
+ 'hybrid'
469
+ ].includes(topology)) c.add('topology', 'must be monorepo, polyrepo or hybrid');
470
+ const pm = c.string(value, 'packageManager', '');
471
+ if (void 0 !== pm && ![
472
+ 'pnpm',
473
+ 'npm'
474
+ ].includes(pm)) c.add('packageManager', 'must be pnpm or npm');
475
+ c.string(value, 'scope', '', {
476
+ pattern: SCOPE,
477
+ hint: 'must be an npm scope like "@shopco"'
478
+ });
479
+ const apps = c.object(value, 'apps', '');
480
+ let shells = 0;
481
+ if (apps) {
482
+ for (const [name, app] of Object.entries(apps)){
483
+ const at = `apps.${name}`;
484
+ if (!MFE_NAME.test(name)) c.add(at, 'app names must be Module Federation names');
485
+ if (!isObj(app)) {
486
+ c.add(at, 'must be an object');
487
+ continue;
488
+ }
489
+ const kind = c.string(app, 'kind', at);
490
+ if ('shell' === kind) shells++;
491
+ else if (void 0 !== kind && 'remote' !== kind) c.add(`${at}.kind`, 'must be shell or remote');
492
+ c.string(app, 'path', at);
493
+ const port = c.number(app, 'port', at, {
494
+ required: false,
495
+ min: 1,
496
+ max: 65535
497
+ });
498
+ if (void 0 !== port && !Number.isInteger(port)) c.add(`${at}.port`, 'must be a whole number');
499
+ }
500
+ if (0 === shells) c.add('apps', 'needs at least one shell');
501
+ }
502
+ const features = c.object(value, 'features', '');
503
+ if (features) for (const [name, feature] of Object.entries(features)){
504
+ const at = `features.${name}`;
505
+ if (!FEATURE.test(name)) c.add(at, 'feature names look like "analytics-gtm" or "@someone/feature"');
506
+ if (isObj(feature)) {
507
+ c.string(feature, 'version', at);
508
+ c.object(feature, 'options', at, false);
509
+ } else c.add(at, 'must be an object');
510
+ }
511
+ const files = c.object(value, 'files', '', false);
512
+ if (files) for (const [file, hash] of Object.entries(files)){
513
+ const at = `files.${file}`;
514
+ if (!isSafeRelativePath(file)) c.add(at, 'must be a relative path with forward slashes');
515
+ if ('string' != typeof hash || !FILE_HASH.test(hash)) c.add(at, 'must be a hash like "sha256-<64 hex characters>"');
516
+ }
517
+ const registries = c.object(value, 'registries', '', false);
518
+ if (registries) for (const [ns, url] of Object.entries(registries)){
519
+ const at = `registries.${ns}`;
520
+ if ('@mfeflow' === ns) c.add(at, '@mfeflow is reserved for first-party features');
521
+ else if (!REGISTRY_NAMESPACE.test(ns)) c.add(at, 'namespaces look like "@someone"');
522
+ if ('string' != typeof url || !url.startsWith('https://') || !url.includes('{name}')) c.add(at, 'must be an https URL containing {name}');
523
+ }
524
+ if ('local' in value) {
525
+ const local = c.string(value, 'local', '');
526
+ if (void 0 !== local && !isCheckoutPath(local)) c.add('local', 'must be a path with forward slashes: relative like "../mfeflow", or absolute when no relative path exists');
527
+ }
528
+ const budget = c.object(value, 'budget', '', false);
529
+ if (budget) for (const key of [
530
+ 'shellKb',
531
+ 'remoteKb',
532
+ 'lcpDeltaMs',
533
+ 'baselineKb'
534
+ ])c.number(budget, key, 'budget', {
535
+ required: false,
536
+ min: 1
537
+ });
538
+ return c.issues;
539
+ }
540
+ function isMfeflowProject(value) {
541
+ return 0 === validateMfeflowProject(value).length;
542
+ }
543
+ const COST_MODEL = {
544
+ lcpDeltaMs: 300,
545
+ shellKb: 32,
546
+ remoteKb: 10
547
+ };
548
+ const KB = 1024;
549
+ const round1 = (value)=>Math.round(10 * value) / 10;
550
+ const line = (actual, budget)=>({
551
+ budget,
552
+ actual,
553
+ pass: actual <= budget
554
+ });
555
+ function judgeCost(sample, model = COST_MODEL) {
556
+ const lcp = line(Math.round(sample.lcpDeltaMs), model.lcpDeltaMs);
557
+ const shell = line(round1((sample.shellBytes - sample.monolithBytes) / KB), model.shellKb);
558
+ const remotes = Object.entries(sample.remoteBytes).toSorted(([a], [b])=>a < b ? -1 : a > b ? 1 : 0).map(([name, bytes])=>Object.assign(line(round1(bytes / KB), model.remoteKb), {
559
+ name
560
+ }));
561
+ return {
562
+ pass: lcp.pass && shell.pass && remotes.every((r)=>r.pass),
563
+ lcp,
564
+ shell,
565
+ remotes
566
+ };
567
+ }
568
+ const REMOTE_PATH = /^\/mfe\/([^/]+)\//;
569
+ function splitScripts(requests) {
570
+ let shellBytes = 0;
571
+ const remoteBytes = {};
572
+ for (const request of requests){
573
+ if ('Script' !== request.type) continue;
574
+ const remote = REMOTE_PATH.exec(request.path)?.[1];
575
+ if (void 0 === remote) shellBytes += request.bytes;
576
+ else remoteBytes[remote] = (remoteBytes[remote] ?? 0) + request.bytes;
577
+ }
578
+ return {
579
+ shellBytes,
580
+ remoteBytes
581
+ };
582
+ }
583
+ const TRANSFORM_OPS = [
584
+ 'add-import',
585
+ 'wrap-provider',
586
+ 'add-shared',
587
+ 'add-boot-step',
588
+ 'add-env-key',
589
+ 'add-route',
590
+ 'add-workflow-step'
591
+ ];
592
+ const FEATURE_CONDITION = 'feature:';
593
+ const FEATURE_NAME = /^(@[a-z0-9-]+\/)?[a-z][a-z0-9-]*$/;
594
+ const IDENT = /^[A-Za-z_$][A-Za-z0-9_$]*$/;
595
+ const TARGET = /^(root|shell|remotes|(app:)?[a-z][a-z0-9_]*)$/;
596
+ const ENV_KEY = /^[A-Za-z_][A-Za-z0-9_]*$/;
597
+ const JOB_ID = /^[A-Za-z_][A-Za-z0-9_-]*$/;
598
+ const SCRIPT_NAME = /^[A-Za-z0-9:_.-]+$/;
599
+ const PACKAGE = /^(@[a-z0-9-~][a-z0-9-._~]*\/)?[a-z0-9-~][a-z0-9-._~]*$/;
600
+ function isTransformOp(op) {
601
+ return TRANSFORM_OPS.some((name)=>name === op);
602
+ }
603
+ function isLiteral(value) {
604
+ return null === value || [
605
+ 'string',
606
+ 'number',
607
+ 'boolean'
608
+ ].includes(typeof value);
609
+ }
610
+ function checkTarget(c, obj, at) {
611
+ c.string(obj, 'target', at, {
612
+ required: false,
613
+ pattern: TARGET,
614
+ hint: 'must be root, shell, remotes, an app name or app:<name>'
615
+ });
616
+ }
617
+ function checkLiteralMap(c, value, at) {
618
+ if (void 0 === value) return;
619
+ if (!isObj(value)) return c.add(at, 'must be an object');
620
+ for (const [k, v] of Object.entries(value))if (!isLiteral(v)) c.add(`${at}.${k}`, 'must be a string, number, boolean or null');
621
+ }
622
+ function checkWhen(c, obj, at) {
623
+ const when = obj.when;
624
+ if (void 0 === when) return;
625
+ if (!isObj(when) || 0 === Object.keys(when).length) return c.add(`${at}.when`, 'must be an object of option names and values');
626
+ for (const [name, value] of Object.entries(when)){
627
+ const feature = name.startsWith(FEATURE_CONDITION) ? name.slice(FEATURE_CONDITION.length) : '';
628
+ if ('' !== feature) {
629
+ if (!FEATURE_NAME.test(feature)) c.add(`${at}.when`, `"${name}" must name a feature, like feature:env-t3`);
630
+ if ('boolean' != typeof value) c.add(`${at}.when.${name}`, 'must be true or false');
631
+ continue;
632
+ }
633
+ if (!IDENT.test(name)) c.add(`${at}.when`, `"${name}" must be a JavaScript identifier`);
634
+ if ('string' != typeof value && 'boolean' != typeof value) c.add(`${at}.when.${name}`, 'must be a string or boolean');
635
+ }
636
+ }
637
+ function checkPath(c, obj, key, at) {
638
+ const value = c.string(obj, key, at);
639
+ if (void 0 !== value && !isSafeRelativePath(value)) c.add(`${at}.${key}`, 'must be a relative path with forward slashes and no ".."');
640
+ }
641
+ const VALIDATORS = [
642
+ 'zod',
643
+ 'valibot',
644
+ 'arktype'
645
+ ];
646
+ function checkSchema(c, t, at) {
647
+ if (!isObj(t.schema)) return void c.string(t, 'schema', at);
648
+ const entries = Object.entries(t.schema);
649
+ if (0 === entries.length) c.add(`${at}.schema`, 'needs at least one validator');
650
+ for (const [validator, text] of entries)if (VALIDATORS.some((v)=>v === validator)) {
651
+ if ('string' != typeof text) c.add(`${at}.schema.${validator}`, 'must be text');
652
+ } else c.add(`${at}.schema`, `"${validator}" must be one of ${VALIDATORS.join(', ')}`);
653
+ }
654
+ function checkTransform(c, t, at) {
655
+ if (!isObj(t)) return c.add(at, 'must be an object');
656
+ const op = c.string(t, 'op', at);
657
+ if (void 0 === op) return;
658
+ if (!isTransformOp(op)) return void c.add(`${at}.op`, `unknown op "${op}"; built-in ops: ${TRANSFORM_OPS.join(', ')}`);
659
+ checkPath(c, t, 'file', at);
660
+ checkTarget(c, t, at);
661
+ checkWhen(c, t, at);
662
+ if (void 0 !== t.defaultImport && 'boolean' != typeof t.defaultImport) c.add(`${at}.defaultImport`, 'must be true or false');
663
+ if (void 0 !== t.typeOnly && 'boolean' != typeof t.typeOnly) c.add(`${at}.typeOnly`, 'must be true or false');
664
+ if (void 0 !== t.await && 'boolean' != typeof t.await) c.add(`${at}.await`, 'must be true or false');
665
+ const ident = (key, required = true)=>c.string(t, key, at, {
666
+ required,
667
+ pattern: IDENT,
668
+ hint: 'must be a JavaScript identifier'
669
+ });
670
+ switch(op){
671
+ case 'add-import':
672
+ c.string(t, 'from', at);
673
+ c.stringArray(t, 'named', at, {
674
+ required: false,
675
+ pattern: IDENT
676
+ });
677
+ ident('default', false);
678
+ ident('namespace', false);
679
+ if (!t.named && !t.default && !t.namespace && void 0 !== t.typeOnly) c.add(at, 'typeOnly needs named, default or namespace');
680
+ break;
681
+ case 'wrap-provider':
682
+ ident('component');
683
+ c.string(t, 'from', at);
684
+ ident('function', false);
685
+ checkLiteralMap(c, t.props, `${at}.props`);
686
+ break;
687
+ case 'add-shared':
688
+ c.string(t, 'name', at, {
689
+ pattern: PACKAGE,
690
+ hint: 'must be an npm package name'
691
+ });
692
+ ident('list', false);
693
+ break;
694
+ case 'add-boot-step':
695
+ ident('call');
696
+ c.string(t, 'from', at);
697
+ ident('function', false);
698
+ c.string(t, 'before', at, {
699
+ required: false,
700
+ pattern: IDENT
701
+ });
702
+ c.string(t, 'after', at, {
703
+ required: false,
704
+ pattern: IDENT
705
+ });
706
+ if (void 0 !== t.before && void 0 !== t.after) c.add(at, 'use before or after, not both');
707
+ if (void 0 !== t.args) if (Array.isArray(t.args)) t.args.forEach((a, i)=>{
708
+ if (!isLiteral(a)) c.add(`${at}.args[${String(i)}]`, 'must be a string, number, boolean or null');
709
+ });
710
+ else c.add(`${at}.args`, 'must be an array');
711
+ break;
712
+ case 'add-env-key':
713
+ {
714
+ c.string(t, 'key', at, {
715
+ pattern: ENV_KEY,
716
+ hint: 'must be a plain key'
717
+ });
718
+ const scope = c.string(t, 'scope', at);
719
+ if (void 0 !== scope && 'server' !== scope && 'runtime' !== scope) c.add(`${at}.scope`, 'must be server or runtime');
720
+ checkSchema(c, t, at);
721
+ ident('object', false);
722
+ if (void 0 !== t.example) checkPath(c, t, 'example', at);
723
+ if (void 0 !== t.configFiles) {
724
+ for (const f of c.stringArray(t, 'configFiles', at))if (!isSafeRelativePath(f)) c.add(`${at}.configFiles`, `"${f}" must be a relative path with no ".."`);
725
+ }
726
+ if (void 0 !== t.value && !isLiteral(t.value) && !(Array.isArray(t.value) && t.value.every((v)=>'string' == typeof v))) c.add(`${at}.value`, 'must be a string, number, boolean, null or a list of strings');
727
+ break;
728
+ }
729
+ case 'add-route':
730
+ c.string(t, 'path', at, {
731
+ pattern: ROUTE,
732
+ hint: 'must be a route path'
733
+ });
734
+ ident('component');
735
+ c.string(t, 'from', at);
736
+ ident('routes', false);
737
+ break;
738
+ case 'add-workflow-step':
739
+ {
740
+ c.string(t, 'job', at, {
741
+ pattern: JOB_ID,
742
+ hint: 'must be a job id'
743
+ });
744
+ c.string(t, 'after', at, {
745
+ required: false
746
+ });
747
+ const step = c.object(t, 'step', at);
748
+ if (step) {
749
+ c.string(step, 'name', `${at}.step`);
750
+ for (const key of [
751
+ 'id',
752
+ 'uses',
753
+ 'run',
754
+ 'if'
755
+ ])c.string(step, key, `${at}.step`, {
756
+ required: false
757
+ });
758
+ checkLiteralMap(c, step.with, `${at}.step.with`);
759
+ checkLiteralMap(c, step.env, `${at}.step.env`);
760
+ if (!step.uses && !step.run) c.add(`${at}.step`, 'needs uses or run');
761
+ }
762
+ break;
763
+ }
764
+ }
765
+ }
766
+ function eachObject(c, value, key, check) {
767
+ const list = value[key];
768
+ if (void 0 === list) return;
769
+ if (!Array.isArray(list)) return c.add(key, 'must be an array');
770
+ list.forEach((item, i)=>{
771
+ const at = `${key}[${String(i)}]`;
772
+ if (isObj(item)) check(item, at);
773
+ else c.add(at, 'must be an object');
774
+ });
775
+ }
776
+ function validateFeatureManifest(value) {
777
+ const c = new Checker();
778
+ if (!isObj(value)) {
779
+ c.add('', 'must be an object');
780
+ return c.issues;
781
+ }
782
+ c.string(value, 'name', '', {
783
+ pattern: FEATURE_NAME,
784
+ hint: 'feature names look like "analytics-gtm" or "@someone/feature"'
785
+ });
786
+ c.string(value, 'version', '');
787
+ c.string(value, "description", '', {
788
+ required: false
789
+ });
790
+ c.stringArray(value, 'requires', '', {
791
+ required: false,
792
+ pattern: FEATURE_NAME
793
+ });
794
+ c.stringArray(value, 'conflicts', '', {
795
+ required: false,
796
+ pattern: FEATURE_NAME
797
+ });
798
+ eachObject(c, value, 'prompts', (p, at)=>{
799
+ c.string(p, 'name', at, {
800
+ pattern: IDENT,
801
+ hint: 'must be a JavaScript identifier'
802
+ });
803
+ const type = c.string(p, 'type', at);
804
+ if (void 0 !== type && ![
805
+ 'boolean',
806
+ 'string',
807
+ 'select'
808
+ ].includes(type)) c.add(`${at}.type`, 'must be boolean, string or select');
809
+ c.string(p, 'message', at, {
810
+ required: false
811
+ });
812
+ if (void 0 !== p.default && 'string' != typeof p.default && 'boolean' != typeof p.default) c.add(`${at}.default`, 'must be a string or boolean');
813
+ const choices = c.stringArray(p, 'choices', at, {
814
+ required: 'select' === type
815
+ });
816
+ if ('select' === type && 'string' == typeof p.default && !choices.includes(p.default)) c.add(`${at}.default`, 'must be one of the choices');
817
+ });
818
+ eachObject(c, value, 'files', (f, at)=>{
819
+ checkPath(c, f, 'to', at);
820
+ checkTarget(c, f, at);
821
+ checkWhen(c, f, at);
822
+ c.string(f, 'from', at, {
823
+ required: false
824
+ });
825
+ c.string(f, 'content', at, {
826
+ required: false
827
+ });
828
+ if ('string' == typeof f.from && !isSafeRelativePath(f.from)) c.add(`${at}.from`, 'must be a relative path with forward slashes and no ".."');
829
+ if (void 0 === f.from === (void 0 === f.content)) c.add(at, 'needs exactly one of from or content');
830
+ });
831
+ eachObject(c, value, 'dependencies', (d, at)=>{
832
+ c.string(d, 'name', at, {
833
+ pattern: PACKAGE,
834
+ hint: 'must be an npm package name'
835
+ });
836
+ c.string(d, 'range', at);
837
+ checkTarget(c, d, at);
838
+ checkWhen(c, d, at);
839
+ });
840
+ eachObject(c, value, "scripts", (s, at)=>{
841
+ c.string(s, 'name', at, {
842
+ pattern: SCRIPT_NAME,
843
+ hint: "must be a script name"
844
+ });
845
+ c.string(s, 'command', at);
846
+ checkTarget(c, s, at);
847
+ checkWhen(c, s, at);
848
+ });
849
+ eachObject(c, value, 'transforms', (t, at)=>{
850
+ checkTransform(c, t, at);
851
+ });
852
+ eachObject(c, value, 'migrations', (m, at)=>{
853
+ c.string(m, 'from', at);
854
+ c.string(m, 'to', at);
855
+ checkPath(c, m, 'module', at);
856
+ });
857
+ return c.issues;
858
+ }
859
+ function isFeatureManifest(value) {
860
+ return 0 === validateFeatureManifest(value).length;
861
+ }
862
+ const DOCS_URL = 'https://mfeflow.github.io/mfeflow';
863
+ const SCHEMA_URLS = {
864
+ mfe: `${DOCS_URL}/schema/mfe.json`,
865
+ project: `${DOCS_URL}/schema/mfeflow.json`,
866
+ feature: `${DOCS_URL}/schema/feature.json`,
867
+ registryEntry: `${DOCS_URL}/schema/registry-entry.json`,
868
+ registryIndex: `${DOCS_URL}/schema/registry-index.json`
869
+ };
870
+ function errorDocsUrl(code) {
871
+ return `${DOCS_URL}/errors/${code}`;
872
+ }
873
+ const DRAFT = 'https://json-schema.org/draft/2020-12/schema';
874
+ const str = (pattern)=>pattern ? {
875
+ type: 'string',
876
+ pattern: pattern.source
877
+ } : {
878
+ type: 'string'
879
+ };
880
+ const json_schema_strings = (pattern)=>({
881
+ type: 'array',
882
+ items: str(pattern)
883
+ });
884
+ const literal = {
885
+ type: [
886
+ 'string',
887
+ 'number',
888
+ 'boolean',
889
+ 'null'
890
+ ]
891
+ };
892
+ const literalMap = {
893
+ type: 'object',
894
+ additionalProperties: literal
895
+ };
896
+ const json_schema_url = {
897
+ type: 'string',
898
+ format: 'uri',
899
+ pattern: '^https?://'
900
+ };
901
+ const relativePath = {
902
+ type: 'string',
903
+ pattern: SAFE_RELATIVE_PATH.source
904
+ };
905
+ const exposeFiles = {
906
+ type: 'object',
907
+ propertyNames: str(EXPOSE),
908
+ additionalProperties: {
909
+ type: 'array',
910
+ items: json_schema_url
911
+ }
912
+ };
913
+ function root(name, title, body) {
914
+ return {
915
+ $schema: DRAFT,
916
+ $id: SCHEMA_URLS[name],
917
+ title,
918
+ type: 'object',
919
+ properties: {
920
+ $schema: {
921
+ type: 'string'
922
+ },
923
+ ...body.properties
924
+ },
925
+ required: body.required
926
+ };
927
+ }
928
+ const remoteFiles = {
929
+ remoteEntry: json_schema_url,
930
+ type: {
931
+ type: 'string',
932
+ enum: [
933
+ ...ENTRY_TYPES
934
+ ]
935
+ },
936
+ entryGlobalName: str(GLOBAL_NAME),
937
+ exposes: exposeFiles
938
+ };
939
+ const json_schema_when = {
940
+ type: 'object',
941
+ minProperties: 1,
942
+ propertyNames: str(IDENT),
943
+ additionalProperties: {
944
+ type: [
945
+ 'string',
946
+ 'boolean'
947
+ ]
948
+ }
949
+ };
950
+ const target = str(TARGET);
951
+ const transformBase = {
952
+ file: relativePath,
953
+ target: target,
954
+ when: json_schema_when
955
+ };
956
+ function json_schema_op(name, required, properties) {
957
+ return {
958
+ type: 'object',
959
+ properties: {
960
+ op: {
961
+ const: name
962
+ },
963
+ ...transformBase,
964
+ ...properties
965
+ },
966
+ required: [
967
+ 'op',
968
+ 'file',
969
+ ...required
970
+ ]
971
+ };
972
+ }
973
+ const defaultImport = {
974
+ type: 'boolean'
975
+ };
976
+ const transform = {
977
+ type: 'object',
978
+ properties: {
979
+ op: {
980
+ enum: [
981
+ ...TRANSFORM_OPS
982
+ ]
983
+ }
984
+ },
985
+ required: [
986
+ 'op'
987
+ ],
988
+ oneOf: [
989
+ json_schema_op('add-import', [
990
+ 'from'
991
+ ], {
992
+ from: str(),
993
+ named: json_schema_strings(IDENT),
994
+ default: str(IDENT),
995
+ namespace: str(IDENT),
996
+ typeOnly: {
997
+ type: 'boolean'
998
+ }
999
+ }),
1000
+ json_schema_op('wrap-provider', [
1001
+ 'component',
1002
+ 'from'
1003
+ ], {
1004
+ component: str(IDENT),
1005
+ from: str(),
1006
+ props: literalMap,
1007
+ defaultImport: defaultImport,
1008
+ function: str(IDENT)
1009
+ }),
1010
+ json_schema_op('add-shared', [
1011
+ 'name'
1012
+ ], {
1013
+ name: str(PACKAGE),
1014
+ list: str(IDENT)
1015
+ }),
1016
+ json_schema_op('add-boot-step', [
1017
+ 'call',
1018
+ 'from'
1019
+ ], {
1020
+ call: str(IDENT),
1021
+ from: str(),
1022
+ args: {
1023
+ type: 'array',
1024
+ items: literal
1025
+ },
1026
+ await: {
1027
+ type: 'boolean'
1028
+ },
1029
+ defaultImport: defaultImport,
1030
+ before: str(IDENT),
1031
+ after: str(IDENT),
1032
+ function: str(IDENT)
1033
+ }),
1034
+ json_schema_op('add-env-key', [
1035
+ 'key',
1036
+ 'scope',
1037
+ 'schema'
1038
+ ], {
1039
+ key: str(ENV_KEY),
1040
+ scope: {
1041
+ enum: [
1042
+ 'server',
1043
+ 'runtime'
1044
+ ]
1045
+ },
1046
+ schema: str(),
1047
+ value: literal,
1048
+ object: str(IDENT),
1049
+ example: relativePath,
1050
+ configFiles: {
1051
+ type: 'array',
1052
+ items: relativePath
1053
+ }
1054
+ }),
1055
+ json_schema_op('add-route', [
1056
+ 'path',
1057
+ 'component',
1058
+ 'from'
1059
+ ], {
1060
+ path: str(ROUTE),
1061
+ component: str(IDENT),
1062
+ from: str(),
1063
+ defaultImport: defaultImport,
1064
+ routes: str(IDENT)
1065
+ }),
1066
+ json_schema_op('add-workflow-step', [
1067
+ 'job',
1068
+ 'step'
1069
+ ], {
1070
+ job: str(JOB_ID),
1071
+ after: str(),
1072
+ step: {
1073
+ type: 'object',
1074
+ properties: {
1075
+ name: str(),
1076
+ id: str(),
1077
+ uses: str(),
1078
+ run: str(),
1079
+ with: literalMap,
1080
+ env: literalMap,
1081
+ if: str()
1082
+ },
1083
+ required: [
1084
+ 'name'
1085
+ ],
1086
+ anyOf: [
1087
+ {
1088
+ required: [
1089
+ 'uses'
1090
+ ]
1091
+ },
1092
+ {
1093
+ required: [
1094
+ 'run'
1095
+ ]
1096
+ }
1097
+ ]
1098
+ }
1099
+ })
1100
+ ]
1101
+ };
1102
+ const json_schema_item = (properties, required)=>({
1103
+ type: 'object',
1104
+ properties,
1105
+ required
1106
+ });
1107
+ const JSON_SCHEMAS = {
1108
+ mfe: root('mfe', 'mfe.json', {
1109
+ properties: {
1110
+ name: str(MFE_NAME),
1111
+ owner: str(),
1112
+ contact: str(),
1113
+ shells: json_schema_strings(MFE_NAME),
1114
+ routes: {
1115
+ ...json_schema_strings(ROUTE),
1116
+ uniqueItems: true
1117
+ },
1118
+ exposes: json_schema_strings(EXPOSE),
1119
+ events: {
1120
+ type: 'object',
1121
+ properties: {
1122
+ publishes: json_schema_strings(EVENT_NAME),
1123
+ consumes: json_schema_strings(EVENT_NAME)
1124
+ },
1125
+ required: [
1126
+ 'publishes',
1127
+ 'consumes'
1128
+ ]
1129
+ },
1130
+ analyticsEvents: json_schema_strings(ANALYTICS_EVENT),
1131
+ budgets: {
1132
+ type: 'object',
1133
+ properties: {
1134
+ initialJsKb: {
1135
+ type: 'number',
1136
+ minimum: 1
1137
+ },
1138
+ lcpMs: {
1139
+ type: 'number',
1140
+ minimum: 1
1141
+ }
1142
+ }
1143
+ },
1144
+ platform: {
1145
+ type: 'object',
1146
+ additionalProperties: {
1147
+ type: 'string'
1148
+ }
1149
+ }
1150
+ },
1151
+ required: [
1152
+ 'name',
1153
+ 'owner',
1154
+ 'shells',
1155
+ 'routes',
1156
+ 'exposes'
1157
+ ]
1158
+ }),
1159
+ project: root('project', 'mfeflow.json', {
1160
+ properties: {
1161
+ version: {
1162
+ const: 1
1163
+ },
1164
+ topology: {
1165
+ enum: [
1166
+ 'monorepo',
1167
+ 'polyrepo',
1168
+ 'hybrid'
1169
+ ]
1170
+ },
1171
+ packageManager: {
1172
+ enum: [
1173
+ 'pnpm',
1174
+ 'npm'
1175
+ ]
1176
+ },
1177
+ scope: str(SCOPE),
1178
+ apps: {
1179
+ type: 'object',
1180
+ propertyNames: str(MFE_NAME),
1181
+ minProperties: 1,
1182
+ additionalProperties: json_schema_item({
1183
+ kind: {
1184
+ enum: [
1185
+ 'shell',
1186
+ 'remote'
1187
+ ]
1188
+ },
1189
+ path: str(),
1190
+ port: {
1191
+ type: 'integer',
1192
+ minimum: 1,
1193
+ maximum: 65535
1194
+ }
1195
+ }, [
1196
+ 'kind',
1197
+ 'path'
1198
+ ])
1199
+ },
1200
+ features: {
1201
+ type: 'object',
1202
+ propertyNames: str(FEATURE_NAME),
1203
+ additionalProperties: json_schema_item({
1204
+ version: str(),
1205
+ options: {
1206
+ type: 'object'
1207
+ }
1208
+ }, [
1209
+ 'version'
1210
+ ])
1211
+ },
1212
+ files: {
1213
+ type: 'object',
1214
+ propertyNames: relativePath,
1215
+ additionalProperties: str(FILE_HASH)
1216
+ },
1217
+ registries: {
1218
+ type: 'object',
1219
+ propertyNames: {
1220
+ type: 'string',
1221
+ pattern: REGISTRY_NAMESPACE.source,
1222
+ not: {
1223
+ const: '@mfeflow'
1224
+ }
1225
+ },
1226
+ additionalProperties: {
1227
+ type: 'string',
1228
+ pattern: '^https://.*\\{name\\}'
1229
+ }
1230
+ },
1231
+ local: str(CHECKOUT_PATH),
1232
+ budget: {
1233
+ type: 'object',
1234
+ properties: Object.fromEntries([
1235
+ 'shellKb',
1236
+ 'remoteKb',
1237
+ 'lcpDeltaMs',
1238
+ 'baselineKb'
1239
+ ].map((key)=>[
1240
+ key,
1241
+ {
1242
+ type: 'number',
1243
+ minimum: 1
1244
+ }
1245
+ ]))
1246
+ }
1247
+ },
1248
+ required: [
1249
+ 'version',
1250
+ 'topology',
1251
+ 'packageManager',
1252
+ 'scope',
1253
+ 'apps',
1254
+ 'features'
1255
+ ]
1256
+ }),
1257
+ feature: root('feature', 'feature.json', {
1258
+ properties: {
1259
+ name: str(FEATURE_NAME),
1260
+ version: str(),
1261
+ description: str(),
1262
+ prompts: {
1263
+ type: 'array',
1264
+ items: json_schema_item({
1265
+ name: str(IDENT),
1266
+ type: {
1267
+ enum: [
1268
+ 'boolean',
1269
+ 'string',
1270
+ 'select'
1271
+ ]
1272
+ },
1273
+ message: str(),
1274
+ default: {
1275
+ type: [
1276
+ 'string',
1277
+ 'boolean'
1278
+ ]
1279
+ },
1280
+ choices: json_schema_strings()
1281
+ }, [
1282
+ 'name',
1283
+ 'type'
1284
+ ])
1285
+ },
1286
+ requires: json_schema_strings(FEATURE_NAME),
1287
+ conflicts: json_schema_strings(FEATURE_NAME),
1288
+ files: {
1289
+ type: 'array',
1290
+ items: {
1291
+ ...json_schema_item({
1292
+ to: relativePath,
1293
+ target: target,
1294
+ when: json_schema_when,
1295
+ from: relativePath,
1296
+ content: str(),
1297
+ template: {
1298
+ type: 'boolean'
1299
+ }
1300
+ }, [
1301
+ 'to'
1302
+ ]),
1303
+ oneOf: [
1304
+ {
1305
+ required: [
1306
+ 'from'
1307
+ ]
1308
+ },
1309
+ {
1310
+ required: [
1311
+ 'content'
1312
+ ]
1313
+ }
1314
+ ]
1315
+ }
1316
+ },
1317
+ dependencies: {
1318
+ type: 'array',
1319
+ items: json_schema_item({
1320
+ name: str(PACKAGE),
1321
+ range: str(),
1322
+ dev: {
1323
+ type: 'boolean'
1324
+ },
1325
+ target: target,
1326
+ when: json_schema_when
1327
+ }, [
1328
+ 'name',
1329
+ 'range'
1330
+ ])
1331
+ },
1332
+ scripts: {
1333
+ type: 'array',
1334
+ items: json_schema_item({
1335
+ name: str(SCRIPT_NAME),
1336
+ command: str(),
1337
+ target: target,
1338
+ when: json_schema_when
1339
+ }, [
1340
+ 'name',
1341
+ 'command'
1342
+ ])
1343
+ },
1344
+ transforms: {
1345
+ type: 'array',
1346
+ items: transform
1347
+ },
1348
+ migrations: {
1349
+ type: 'array',
1350
+ items: json_schema_item({
1351
+ from: str(),
1352
+ to: str(),
1353
+ module: relativePath
1354
+ }, [
1355
+ 'from',
1356
+ 'to',
1357
+ 'module'
1358
+ ])
1359
+ }
1360
+ },
1361
+ required: [
1362
+ 'name',
1363
+ 'version'
1364
+ ]
1365
+ }),
1366
+ registryEntry: root('registryEntry', 'registry entry', {
1367
+ properties: {
1368
+ version: str(VERSION),
1369
+ manifestUrl: json_schema_url,
1370
+ ...remoteFiles,
1371
+ previous: str(VERSION),
1372
+ owner: str(),
1373
+ rollout: json_schema_item({
1374
+ candidate: str(VERSION),
1375
+ stable: str(VERSION),
1376
+ percent: {
1377
+ type: 'number',
1378
+ minimum: 0,
1379
+ maximum: 100
1380
+ }
1381
+ }, [
1382
+ 'candidate',
1383
+ 'stable',
1384
+ 'percent'
1385
+ ])
1386
+ },
1387
+ required: [
1388
+ 'version',
1389
+ 'manifestUrl'
1390
+ ]
1391
+ }),
1392
+ registryIndex: root('registryIndex', 'registry index', {
1393
+ properties: {
1394
+ shell: str(),
1395
+ env: str(),
1396
+ generatedAt: str(),
1397
+ remotes: {
1398
+ type: 'array',
1399
+ items: json_schema_item({
1400
+ name: str(MFE_NAME),
1401
+ entry: json_schema_url,
1402
+ ...remoteFiles,
1403
+ version: str(),
1404
+ routes: json_schema_strings(ROUTE),
1405
+ owner: str()
1406
+ }, [
1407
+ 'name',
1408
+ 'entry'
1409
+ ])
1410
+ }
1411
+ },
1412
+ required: [
1413
+ 'remotes'
1414
+ ]
1415
+ })
1416
+ };
1417
+ export { ANALYTICS_EVENT, COST_MODEL, DOCS_URL, EVENT_NAME, EXPOSE, FEATURE_CONDITION, FEATURE_NAME, FILE_HASH, JSON_SCHEMAS, MFE_NAME, ROUTE, SCHEMA_URLS, TRANSFORM_OPS, assetsFromManifest, errorDocsUrl, injectIntoHead, isFeatureManifest, isMfeManifest, isMfeflowProject, isRegistryEntry, isRegistryIndex, isSafeRelativePath, judgeCost, registryInjection, round1, splitScripts, validateFeatureManifest, validateMfeManifest, validateMfeflowProject, validateRegistryEntry, validateRegistryIndex };