@mfeflow/core 0.4.0 → 0.6.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 CHANGED
@@ -1,14 +1,16 @@
1
1
  import { createRequire } from "node:module";
2
- import { FEATURE_CONDITION, FEATURE_NAME, REGISTRY_NAMESPACE, SCHEMA_URLS, TRANSFORM_OPS, errorDocsUrl, isLoopbackHttp, isMfeflowProject, isRegistryProtocol, isSafeRelativePath, registryIndexUrl, registryTemplateProblem, validateFeatureManifest, validateFeatureRegistryIndex, validateMfeflowProject } from "@mfeflow/contracts";
3
- import { createHash } from "node:crypto";
4
- import promises from "node:fs/promises";
2
+ import { DOCS_URL, EXPOSE, FEATURE_CONDITION, FEATURE_NAME, MFE_NAME, REGISTRY_NAMESPACE, SCHEMA_URLS, TRANSFORM_OPS, errorDocsUrl, isLoopbackHttp, isMfeflowProject, isPrivateHost, isRegistryProtocol, isSafeRelativePath, redactUrl, redactUrlsIn, registryIndexUrl, registryTemplateProblem, validateFeatureManifest, validateFeatureRegistryIndex, validateMfeflowProject } from "@mfeflow/contracts";
5
3
  import node_path from "node:path";
4
+ import promises from "node:fs/promises";
5
+ import { createHash } from "node:crypto";
6
6
  import { formatPatch, structuredPatch } from "diff";
7
- import { isMap, isSeq, parseDocument, stringify } from "yaml";
7
+ import { isMap, isSeq, parse as external_yaml_parse, parseDocument, stringify } from "yaml";
8
+ import semver, { valid, validRange } from "semver";
9
+ import { pathToFileURL } from "node:url";
8
10
  import { Eta } from "eta";
9
- import { isArrayExpression, isArrowFunctionExpression, isBooleanLiteral, isCallExpression, isFunctionDeclaration, isFunctionExpression, isIdentifier, isImportDeclaration, isImportDefaultSpecifier, isImportNamespaceSpecifier, isImportSpecifier, isJSXElement, isJSXFragment, isJSXIdentifier, isMemberExpression, isNode, isNullLiteral, isNumericLiteral, isObjectExpression, isObjectProperty, isParenthesizedExpression, isReturnStatement, isStringLiteral, isTSAsExpression, isTSNonNullExpression, isTSSatisfiesExpression, isUnaryExpression, isValidIdentifier, isVariableDeclaration } from "@babel/types";
11
+ import { isArrayExpression, isArrowFunctionExpression, isAssignmentExpression, isAwaitExpression, isBinaryExpression, isBooleanLiteral, isCallExpression, isClassDeclaration, isConditionalExpression, isExportDefaultDeclaration, isExportNamedDeclaration, isExportSpecifier, isExpressionStatement, isFunctionDeclaration, isFunctionExpression, isIdentifier, isImportDeclaration, isImportDefaultSpecifier, isImportNamespaceSpecifier, isImportSpecifier, isJSXElement, isJSXFragment, isJSXIdentifier, isLogicalExpression, isMemberExpression, isMetaProperty, isNewExpression, isNode, isNullLiteral, isNumericLiteral, isObjectExpression, isObjectPattern, isObjectProperty, isParenthesizedExpression, isReturnStatement, isStringLiteral, isTSAsExpression, isTSNonNullExpression, isTSSatisfiesExpression, isTSTypeAssertion, isTemplateLiteral, isUnaryExpression, isValidIdentifier, isVariableDeclaration } from "@babel/types";
10
12
  import { parse, parseExpression } from "@babel/parser";
11
- import { valid, validRange } from "semver";
13
+ import node_os from "node:os";
12
14
  const ERROR_CATALOGUE = {
13
15
  E101: {
14
16
  title: 'A shared singleton is missing from the shared list',
@@ -59,8 +61,8 @@ const ERROR_CATALOGUE = {
59
61
  fix: 'Restore the folder, or remove the app from "apps" in mfeflow.json.'
60
62
  },
61
63
  E113: {
62
- title: 'mfe.json is missing or invalid',
63
- fix: `Give the app a valid mfe.json (name, owner, shells, routes, exposes). The schema is at ${SCHEMA_URLS.mfe}.`
64
+ title: 'mfeflow.app.json is missing or invalid',
65
+ fix: `Give the app a valid mfeflow.app.json (name, owner, shells, routes, exposes). The schema is at ${SCHEMA_URLS.mfe}.`
64
66
  },
65
67
  E114: {
66
68
  title: 'A registry entry is missing or invalid',
@@ -71,8 +73,8 @@ const ERROR_CATALOGUE = {
71
73
  fix: 'Give one of them another port in its rsbuild config and in "apps" in mfeflow.json.'
72
74
  },
73
75
  E116: {
74
- title: 'Two remotes claim the same route, or an mfe.json disagrees with mfeflow.json',
75
- fix: 'Give each route to one remote, and keep the name in mfe.json equal to the app name.'
76
+ title: 'Two remotes claim routes that overlap, or an app file disagrees with mfeflow.json',
77
+ fix: 'Give each route to one remote (a wildcard such as /checkout/* also covers /checkout/pay), and keep the name in mfeflow.app.json equal to the app name.'
76
78
  },
77
79
  E117: {
78
80
  title: 'A copied file changed since mfeflow wrote it',
@@ -94,6 +96,18 @@ const ERROR_CATALOGUE = {
94
96
  title: 'A server env key is not listed in turbo.json',
95
97
  fix: 'Add the key to "globalEnv" in turbo.json. Turborepo hides unlisted variables from tasks and replays a cached build without running env.ts, so a missing value would not fail the build.'
96
98
  },
99
+ E122: {
100
+ title: 'An app has both mfeflow.app.json and the old mfe.json',
101
+ fix: `Keep mfeflow.app.json and delete mfe.json. Move any field you still need into mfeflow.app.json first. See ${DOCS_URL}/reference/mfeflow-app-json.`
102
+ },
103
+ E123: {
104
+ title: 'This folder already has an mfeflow.json',
105
+ fix: 'adopt only starts a project file; it never changes one. Use `mfeflow add` or `mfeflow upgrade` on this project, or run adopt in a folder that has no mfeflow.json.'
106
+ },
107
+ E124: {
108
+ title: 'adopt found nothing it can set up',
109
+ fix: 'adopt needs a Module Federation host (shell) it can read, and a literal `name` for each app. Run it in the folder that holds the shell, or a workspace root that contains it, and read `mfeflow adopt --dry-run` for what was skipped.'
110
+ },
97
111
  E201: {
98
112
  title: 'Feature not found',
99
113
  fix: 'List what is available with `mfeflow features`, or check the spelling.'
@@ -160,7 +174,7 @@ const ERROR_CATALOGUE = {
160
174
  },
161
175
  E302: {
162
176
  title: 'A remote is not compatible with its shell',
163
- fix: 'Align the remote and the shell on the same major of the package, or update the "platform" range in the remote\'s mfe.json.'
177
+ fix: 'Align the remote and the shell on the same major of the package, or update the "platform" range in the remote\'s mfeflow.app.json.'
164
178
  },
165
179
  E303: {
166
180
  title: 'A runtime plugin imports a shared module',
@@ -168,7 +182,7 @@ const ERROR_CATALOGUE = {
168
182
  },
169
183
  E304: {
170
184
  title: 'A bundle is over its budget',
171
- fix: 'Cut what the app ships on first load, or raise the budget in mfeflow.json ("budget") or mfe.json ("budgets") on purpose.'
185
+ fix: 'Cut what the app ships on first load, or raise the budget in mfeflow.json ("budget") or mfeflow.app.json ("budgets") on purpose.'
172
186
  },
173
187
  E305: {
174
188
  title: 'There is no build output to measure',
@@ -178,6 +192,10 @@ const ERROR_CATALOGUE = {
178
192
  title: 'A dev server could not start',
179
193
  fix: "Free the port or change it in mfeflow.json and the app's rsbuild config, then run `mfeflow dev` again."
180
194
  },
195
+ E307: {
196
+ title: 'The shell to check against could not be read',
197
+ fix: 'Pass --against a registry index, a deployed shell mf-manifest.json or a local file of either. A URL must be https (http only for localhost) and under 1 MB.'
198
+ },
181
199
  E401: {
182
200
  title: 'Could not fetch from the registry',
183
201
  fix: 'Check the URL in "registries" and your network, then try again.'
@@ -193,6 +211,78 @@ const ERROR_CATALOGUE = {
193
211
  E404: {
194
212
  title: 'A feature in the registry folder cannot be published',
195
213
  fix: 'Fix the features listed above. They are checked with the same rules `mfeflow add @namespace/name` applies, so a feature that passes here installs for users.'
214
+ },
215
+ E501: {
216
+ title: 'A deploy failed the schema gate',
217
+ fix: `Fix what the message names: the remote's mfeflow.app.json, its mf-manifest.json or the version. Nothing was uploaded. See ${DOCS_URL}/guide/deploy-and-rollback.`
218
+ },
219
+ E502: {
220
+ title: 'A route of this remote overlaps a route of another remote',
221
+ fix: `Give each URL to one remote (/checkout/* also covers /checkout/pay), then deploy again. Nothing was uploaded. See ${DOCS_URL}/guide/deploy-and-rollback.`
222
+ },
223
+ E503: {
224
+ title: "The remote's shared ranges do not fit the shell that is deployed",
225
+ fix: `Align the remote with the shell on the package named above, or deploy the shell first. Nothing was uploaded. See ${DOCS_URL}/guide/deploy-and-rollback.`
226
+ },
227
+ E504: {
228
+ title: 'The remote is over its first-load JS budget',
229
+ fix: `Cut what the remote loads first, or raise budgets.initialJsKb in its mfeflow.app.json on purpose. Nothing was uploaded. See ${DOCS_URL}/guide/deploy-and-rollback.`
230
+ },
231
+ E505: {
232
+ title: 'A rollout was asked for, and a static host cannot split traffic',
233
+ fix: `Deploy to one version at a time, or use the registry server for percentage rollouts. See ${DOCS_URL}/guide/deploy-and-rollback.`
234
+ },
235
+ E506: {
236
+ title: 'The registry host is not set up or cannot be used',
237
+ fix: `Pass --host file:<dir> or s3://<bucket>[/prefix], or set registry.host in mfeflow.json. Deploys also need --base-url or registry.baseUrl. S3 settings come from MFEFLOW_S3_ENDPOINT, MFEFLOW_S3_REGION, AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY. See ${DOCS_URL}/guide/deploy-and-rollback.`
238
+ },
239
+ E507: {
240
+ title: 'There is no build output to deploy',
241
+ fix: `Run \`pnpm build\` in the remote first. The build folder needs an mf-manifest.json, and the remote needs a version in package.json or --version. See ${DOCS_URL}/guide/deploy-and-rollback.`
242
+ },
243
+ E508: {
244
+ title: 'There is no such version to move to',
245
+ fix: `Run \`mfeflow status\` to see what each environment serves and the versions you can go back to. See ${DOCS_URL}/guide/deploy-and-rollback.`
246
+ },
247
+ E509: {
248
+ title: 'Files of the target version are missing on the host',
249
+ fix: `Deploy that version again from its build, or pick another version with --to. The index was not changed. See ${DOCS_URL}/guide/deploy-and-rollback.`
250
+ },
251
+ E510: {
252
+ title: 'Another deploy changed the index at the same time',
253
+ fix: `Run \`mfeflow status\` to see what is served now, then run the command again. The index was not changed by this run. See ${DOCS_URL}/guide/deploy-and-rollback.`
254
+ },
255
+ E511: {
256
+ title: 'This version is already published with different content',
257
+ fix: `A published version never changes. Bump the version and deploy again. See ${DOCS_URL}/guide/deploy-and-rollback.`
258
+ },
259
+ E512: {
260
+ title: 'Local registry files and the host disagree',
261
+ fix: `Run the deploy, promote or rollback that was meant to move the environment, or fix registry/envs by hand. Artifacts uploaded with --no-publish are not served until you deploy them. See ${DOCS_URL}/guide/deploy-and-rollback.`
262
+ },
263
+ E405: {
264
+ title: 'The registry is not trusted yet',
265
+ fix: 'Run `mfeflow registry trust @namespace` once to allow requests to it, or remove it from "registries".'
266
+ },
267
+ E406: {
268
+ title: 'The registry changed a feature since you added it',
269
+ fix: 'Nothing was written. Look at what changed; if you accept the new content, run the command again with --accept-changed-content.'
270
+ },
271
+ E601: {
272
+ title: 'Cannot tell which version of mfeflow made this project',
273
+ fix: 'Add "cli": "<version>" to mfeflow.json (the version you created the project with), or give the shell a plain @mfeflow/runtime range in its package.json, then run the command again.'
274
+ },
275
+ E602: {
276
+ title: 'The project is newer than the version you are upgrading to',
277
+ fix: 'Upgrade only moves forward. Install a newer mfeflow, or leave the project as it is.'
278
+ },
279
+ E603: {
280
+ title: 'A feature migration could not be loaded or failed',
281
+ fix: 'Nothing was written. Update mfeflow to a build that ships the migration, or report the feature and versions named above.'
282
+ },
283
+ E604: {
284
+ title: 'Files you changed need a decision',
285
+ fix: 'Nothing was written. Run `mfeflow upgrade --dry-run` to see each diff, then pass --accept <path> to take the new version of a file, --keep <path> to keep yours, or `all` for every file.'
196
286
  }
197
287
  };
198
288
  class MfeflowError extends Error {
@@ -231,14 +321,15 @@ function readPackageJson() {
231
321
  if (!isPackageJson(pkg)) throw new MfeflowError('E106', 'package.json is missing a name or version');
232
322
  return pkg;
233
323
  }
234
- function normalizeEol(content) {
235
- return content.replace(/\r\n/g, '\n');
236
- }
237
- function hashContent(content) {
238
- return `sha256-${createHash('sha256').update(normalizeEol(content)).digest('hex')}`;
324
+ function resolveInRoot(root, relative) {
325
+ if (!isSafeRelativePath(relative)) throw new MfeflowError('E105', `"${relative}"`);
326
+ const absolute = node_path.resolve(root, ...relative.split('/'));
327
+ const inside = node_path.relative(root, absolute);
328
+ if (inside.startsWith('..') || node_path.isAbsolute(inside)) throw new MfeflowError('E105', `"${relative}"`);
329
+ return absolute;
239
330
  }
240
- async function inOrder(items, step) {
241
- for (const item of items)await step(item);
331
+ function joinPosix(...parts) {
332
+ return node_path.posix.join(...parts.filter((p)=>'' !== p));
242
333
  }
243
334
  const realFs = {
244
335
  readFile: (file)=>promises.readFile(file, 'utf8'),
@@ -294,15 +385,46 @@ async function writeFileAtomic(ops, file, content) {
294
385
  });
295
386
  }
296
387
  }
297
- function resolveInRoot(root, relative) {
298
- if (!isSafeRelativePath(relative)) throw new MfeflowError('E105', `"${relative}"`);
299
- const absolute = node_path.resolve(root, ...relative.split('/'));
300
- const inside = node_path.relative(root, absolute);
301
- if (inside.startsWith('..') || node_path.isAbsolute(inside)) throw new MfeflowError('E105', `"${relative}"`);
302
- return absolute;
388
+ async function symlinkedPrefix(ops, root, relative) {
389
+ const parts = relative.split('/');
390
+ const prefixes = parts.map((_, i)=>parts.slice(0, i + 1).join('/'));
391
+ const linked = await Promise.all(prefixes.map(async (prefix)=>({
392
+ prefix,
393
+ link: await ops.isSymlink(resolveInRoot(root, prefix))
394
+ })));
395
+ return linked.find((entry)=>entry.link)?.prefix;
396
+ }
397
+ const APP_FILE = 'mfeflow.app.json';
398
+ const LEGACY_APP_FILE = 'mfe.json';
399
+ async function readAppFile(dir, ops = realFs) {
400
+ const [text, legacyText] = await Promise.all([
401
+ readIfExists(ops, node_path.join(dir, APP_FILE)),
402
+ readIfExists(ops, node_path.join(dir, LEGACY_APP_FILE))
403
+ ]);
404
+ if (void 0 !== text && void 0 !== legacyText) throw new MfeflowError('E122', `${dir} has both ${APP_FILE} and ${LEGACY_APP_FILE}`);
405
+ if (void 0 !== text) return {
406
+ name: APP_FILE,
407
+ text,
408
+ legacy: false
409
+ };
410
+ if (void 0 !== legacyText) return {
411
+ name: LEGACY_APP_FILE,
412
+ text: legacyText,
413
+ legacy: true
414
+ };
303
415
  }
304
- function joinPosix(...parts) {
305
- return node_path.posix.join(...parts.filter((p)=>'' !== p));
416
+ function legacyAppFileNotice(folders) {
417
+ const files = folders.map((dir)=>`${dir}/${LEGACY_APP_FILE}`).join(', ');
418
+ return `Notice: ${LEGACY_APP_FILE} is now ${APP_FILE}. Rename ${files}; the old name still works for now.`;
419
+ }
420
+ function normalizeEol(content) {
421
+ return content.replace(/\r\n/g, '\n');
422
+ }
423
+ function hashContent(content) {
424
+ return `sha256-${createHash('sha256').update(normalizeEol(content)).digest('hex')}`;
425
+ }
426
+ async function inOrder(items, step) {
427
+ for (const item of items)await step(item);
306
428
  }
307
429
  const PROJECT_FILE = 'mfeflow.json';
308
430
  function describeIssues(issues) {
@@ -325,11 +447,15 @@ function serializeProject(project) {
325
447
  if (issues.length > 0) throw new MfeflowError('E104', describeIssues(issues));
326
448
  const { $schema, ...rest } = project;
327
449
  const files = rest.files && Object.fromEntries(Object.entries(rest.files).toSorted(byKey));
450
+ const kept = rest.kept && Object.fromEntries(Object.entries(rest.kept).toSorted(byKey));
328
451
  const ordered = {
329
452
  $schema: $schema ?? SCHEMA_URLS.project,
330
453
  ...rest,
331
454
  ...files ? {
332
455
  files
456
+ } : {},
457
+ ...kept ? {
458
+ kept
333
459
  } : {}
334
460
  };
335
461
  return `${JSON.stringify(ordered, null, 2)}\n`;
@@ -367,11 +493,14 @@ function recordPlan(project, plan) {
367
493
  const features = {
368
494
  ...project.features
369
495
  };
370
- for (const f of plan.features)features[f.name] = f.options ? {
496
+ for (const f of plan.features)features[f.name] = {
371
497
  version: f.version,
372
- options: f.options
373
- } : {
374
- version: f.version
498
+ ...f.options ? {
499
+ options: f.options
500
+ } : {},
501
+ ...void 0 === f.integrity ? {} : {
502
+ integrity: f.integrity
503
+ }
375
504
  };
376
505
  const next = {
377
506
  ...project,
@@ -431,6 +560,11 @@ function emptyPlan() {
431
560
  };
432
561
  }
433
562
  const WORKSPACE_FILE = 'pnpm-workspace.yaml';
563
+ const DEPENDENCY_SECTIONS = [
564
+ 'dependencies',
565
+ 'devDependencies',
566
+ 'optionalDependencies'
567
+ ];
434
568
  class PlanBuilder {
435
569
  read;
436
570
  options;
@@ -477,6 +611,9 @@ class PlanBuilder {
477
611
  async content(file) {
478
612
  return (await this.open(file)).current ?? void 0;
479
613
  }
614
+ async existed(file) {
615
+ return null !== (await this.open(file)).original;
616
+ }
480
617
  async captureProject() {
481
618
  this.state.projectBefore = await this.read(PROJECT_FILE) ?? null;
482
619
  }
@@ -499,6 +636,13 @@ class PlanBuilder {
499
636
  this.touch(entry);
500
637
  entry.current = edit(entry.current);
501
638
  }
639
+ async replace(file, content) {
640
+ const entry = await this.open(file);
641
+ if (null === entry.current) throw new MfeflowError('E208', `${file} does not exist`);
642
+ if ('third-party' === this.trust && entry.current !== content) throw new MfeflowError('E207', file);
643
+ this.touch(entry);
644
+ entry.current = content;
645
+ }
502
646
  async delete(file) {
503
647
  const entry = await this.open(file);
504
648
  this.touch(entry);
@@ -527,8 +671,73 @@ class PlanBuilder {
527
671
  file,
528
672
  name,
529
673
  range: wanted,
530
- dev
674
+ dev,
675
+ ...'third-party' === this.trust ? {
676
+ thirdParty: true
677
+ } : {}
678
+ });
679
+ }
680
+ async setRange(file, name, range) {
681
+ if (!isRegistryRange(range)) throw new MfeflowError('E202', `${name}: "${range}" is not a semver range or a dist-tag`);
682
+ const source = await this.content(file);
683
+ if (void 0 === source) throw new MfeflowError('E208', `${file} does not exist`);
684
+ const existing = this.declaredRange(file, source, name, range);
685
+ if (existing === range || existing.startsWith('workspace:')) return;
686
+ if (!existing.startsWith('catalog:') && !isRegistryRange(existing)) return void this.warn(`${file}: ${name} is ${JSON.stringify(existing)}; left as it is, not moved to ${range}`);
687
+ if ('third-party' === this.trust) throw new MfeflowError('E207', `${file}: ${name} is already ${JSON.stringify(existing)}; the feature wants ${range}`);
688
+ if (existing.startsWith('catalog:')) return void await this.setCatalogEntry(name, existing.slice(8), range);
689
+ await this.patch(file, (text)=>editJsonObject(file, text, (pkg)=>{
690
+ let changed = false;
691
+ for (const section of DEPENDENCY_SECTIONS){
692
+ const listed = asRecord(pkg[section])[name];
693
+ if ('string' == typeof listed && listed !== range && isRegistryRange(listed)) {
694
+ pkg[section] = {
695
+ ...asRecord(pkg[section]),
696
+ [name]: range
697
+ };
698
+ changed = true;
699
+ }
700
+ }
701
+ return changed;
702
+ }));
703
+ }
704
+ declaredRange(file, source, name, wanted) {
705
+ const pkg = parseJsonObject(file, source);
706
+ const listed = DEPENDENCY_SECTIONS.map((section)=>asRecord(pkg[section])[name]).filter((value)=>'string' == typeof value);
707
+ const found = listed.find((value)=>value !== wanted) ?? listed[0];
708
+ if (void 0 !== found) return found;
709
+ throw new MfeflowError('E208', `${file} does not depend on ${name}`);
710
+ }
711
+ async setCatalogEntry(name, catalogName, range) {
712
+ const path = '' === catalogName || 'default' === catalogName ? [
713
+ 'catalog'
714
+ ] : [
715
+ 'catalogs',
716
+ catalogName
717
+ ];
718
+ const source = await this.content(WORKSPACE_FILE);
719
+ const doc = void 0 === source ? void 0 : parseDocument(source);
720
+ const problem = doc?.errors[0];
721
+ if (problem) throw new MfeflowError('E208', `${WORKSPACE_FILE} is not valid YAML (${problem.message})`, {
722
+ cause: problem
531
723
  });
724
+ if (void 0 === doc || !isMap(doc.getIn(path)) || !doc.hasIn([
725
+ ...path,
726
+ name
727
+ ])) throw new MfeflowError('E208', `${WORKSPACE_FILE} has no catalog entry for ${name}`);
728
+ doc.setIn([
729
+ ...path,
730
+ name
731
+ ], range);
732
+ let text;
733
+ try {
734
+ text = doc.toString();
735
+ } catch (error) {
736
+ throw new MfeflowError('E208', `${WORKSPACE_FILE} could not be written back (${messageOf(error)})`, {
737
+ cause: error
738
+ });
739
+ }
740
+ await this.patch(WORKSPACE_FILE, ()=>text);
532
741
  }
533
742
  async addScript(file, name, command) {
534
743
  let added = true;
@@ -600,6 +809,10 @@ class PlanBuilder {
600
809
  };
601
810
  }
602
811
  }
812
+ function isRegistryRange(range) {
813
+ if ('' === range || range !== range.trim() || range.includes('/')) return false;
814
+ return /^[a-z][a-z0-9-]*$/.test(range) || null !== validRange(range);
815
+ }
603
816
  function isPinnedElsewhere(range) {
604
817
  return range.startsWith('catalog:') || range.startsWith('workspace:');
605
818
  }
@@ -625,16 +838,20 @@ const VERB = {
625
838
  patch: 'patch ',
626
839
  delete: 'delete'
627
840
  };
841
+ const dependencyLine = (d)=>` ${d.file} ${d.name}@${d.range}${d.dev ? ' (dev)' : ''}`;
628
842
  function summarize(plan) {
629
843
  const count = (action)=>plan.changes.filter((c)=>c.action === action).length;
630
844
  const lines = [
631
845
  `${String(count('create'))} to create, ${String(count('patch'))} to patch, ${String(count('delete'))} to delete`
632
846
  ];
633
847
  for (const c of plan.changes)lines.push(` ${VERB[c.action]} ${c.path}`);
634
- if (plan.dependencies.length > 0) {
635
- lines.push(`Dependencies (${String(plan.dependencies.length)}):`);
636
- for (const d of plan.dependencies)lines.push(` ${d.file} ${d.name}@${d.range}${d.dev ? ' (dev)' : ''}`);
848
+ const dependencies = plan.dependencies.filter((d)=>true !== d.thirdParty);
849
+ const newPackages = plan.dependencies.filter((d)=>true === d.thirdParty);
850
+ if (dependencies.length > 0) {
851
+ lines.push(`Dependencies (${String(dependencies.length)}):`);
852
+ for (const d of dependencies)lines.push(dependencyLine(d));
637
853
  }
854
+ if (newPackages.length > 0) lines.push('', `NEW PACKAGES from a third-party feature (${String(newPackages.length)}):`, ...newPackages.map(dependencyLine), " Check each one before you install. npm and yarn run a package's install scripts;", ' pnpm 10 and later blocks them until you allow them.', '');
638
855
  if (plan.scripts.length > 0) {
639
856
  lines.push(`Scripts added to package.json, not run (${String(plan.scripts.length)}):`);
640
857
  for (const s of plan.scripts)lines.push(` ${s.file} ${s.name}: ${s.command}`);
@@ -687,19 +904,19 @@ const DEVICE_NAMES = new Set([
687
904
  'lpt³'
688
905
  ]);
689
906
  const INVISIBLE = /[\p{Cc}\p{Cf}\p{Cn}\p{Co}\p{Cs}]/u;
690
- function refuse(file, why) {
907
+ function write_policy_refuse(file, why) {
691
908
  throw new MfeflowError('E105', `"${file}" ${why}`);
692
909
  }
693
910
  function assertWritable(file) {
694
- if (INVISIBLE.test(file)) refuse(file, 'has control or invisible characters');
911
+ if (INVISIBLE.test(file)) write_policy_refuse(file, 'has control or invisible characters');
695
912
  const key = policyKey(file);
696
- if ('mfeflow.json' === key) refuse(file, 'is not a place features write to');
913
+ if ('mfeflow.json' === key) write_policy_refuse(file, 'is not a place features write to');
697
914
  for (const segment of key.split('/')){
698
- if ('.git' === segment || 'node_modules' === segment) refuse(file, 'is not a place features write to');
699
- if (segment.includes(':')) refuse(file, 'has a ":" (a Windows stream)');
700
- if (/[. ]$/.test(segment)) refuse(file, 'has a name ending in a dot or space');
915
+ if ('.git' === segment || 'node_modules' === segment) write_policy_refuse(file, 'is not a place features write to');
916
+ if (segment.includes(':')) write_policy_refuse(file, 'has a ":" (a Windows stream)');
917
+ if (/[. ]$/.test(segment)) write_policy_refuse(file, 'has a name ending in a dot or space');
701
918
  const base = (segment.split('.')[0] ?? '').trimEnd();
702
- if (DEVICE_NAMES.has(base)) refuse(file, 'is a Windows device name');
919
+ if (DEVICE_NAMES.has(base)) write_policy_refuse(file, 'is a Windows device name');
703
920
  }
704
921
  }
705
922
  const THIRD_PARTY_FOLDERS = new Set([
@@ -745,7 +962,7 @@ async function applyPlan(plan, options) {
745
962
  const realRoot = await ops.realpath(options.root);
746
963
  await inOrder(targets, async ({ change, file })=>{
747
964
  await assertContained(ops, realRoot, file, change.path);
748
- await assertTrustedRoute(ops, options.root, change);
965
+ await assertTrustedRoute(ops, options.root, change, true === options.refuseSymlinks);
749
966
  });
750
967
  const current = await verifyUnchanged(targets, ops);
751
968
  if (options.dryRun) return {
@@ -762,7 +979,7 @@ async function applyPlan(plan, options) {
762
979
  const dir = await ops.mkdir(node_path.dirname(file));
763
980
  if (dir) createdDirs.push(dir);
764
981
  await assertContained(ops, realRoot, node_path.dirname(file), change.path);
765
- await assertTrustedRoute(ops, options.root, change);
982
+ await assertTrustedRoute(ops, options.root, change, true === options.refuseSymlinks);
766
983
  const temp = tempSibling(file);
767
984
  staged.set(file, temp);
768
985
  await ops.writeFile(temp, change.after);
@@ -905,17 +1122,13 @@ async function assertContained(ops, realRoot, target, shown) {
905
1122
  const inside = node_path.relative(realRoot, real);
906
1123
  if (inside.startsWith('..') || node_path.isAbsolute(inside)) throw new MfeflowError('E105', `"${shown}" resolves through a symlink to ${real}`);
907
1124
  }
908
- async function assertTrustedRoute(ops, root, change) {
909
- if ('third-party' !== change.trust) return;
910
- assertWritable(change.path);
911
- const parts = change.path.split('/');
912
- const prefixes = parts.map((_, i)=>parts.slice(0, i + 1).join('/'));
913
- const linked = await Promise.all(prefixes.map(async (prefix)=>({
914
- prefix,
915
- link: await ops.isSymlink(node_path.join(root, ...prefix.split('/')))
916
- })));
917
- const hit = linked.find((entry)=>entry.link);
918
- if (hit) throw new MfeflowError('E203', `"${change.path}" goes through a symlink at "${hit.prefix}", which third-party features may not write through`);
1125
+ async function assertTrustedRoute(ops, root, change, refuseAll) {
1126
+ const thirdParty = 'third-party' === change.trust;
1127
+ if (!thirdParty && !refuseAll) return;
1128
+ if (thirdParty) assertWritable(change.path);
1129
+ const hit = await symlinkedPrefix(ops, root, change.path);
1130
+ if (hit && !thirdParty) throw new MfeflowError('E105', `"${change.path}" goes through a symlink at "${hit}", which is never written through`);
1131
+ if (hit) throw new MfeflowError('E203', `"${change.path}" goes through a symlink at "${hit}", which third-party features may not write through`);
919
1132
  }
920
1133
  async function deepestRealpath(ops, target) {
921
1134
  try {
@@ -1004,11 +1217,6 @@ function substitute(source, data, name = 'template') {
1004
1217
  return String(value);
1005
1218
  });
1006
1219
  }
1007
- const RUNTIME_CONFIG_FILE = /^runtime-config(?:\.([a-z0-9-]+))?\.json$/;
1008
- async function runtimeConfigFiles(folder) {
1009
- const names = await promises.readdir(folder).catch(()=>[]);
1010
- return names.filter((name)=>RUNTIME_CONFIG_FILE.test(name)).toSorted((a, b)=>a.localeCompare(b));
1011
- }
1012
1220
  function applyEdits(source, edits) {
1013
1221
  return edits.toSorted((a, b)=>b.start - a.start).reduce((out, e)=>out.slice(0, e.start) + e.text + out.slice(e.end), source);
1014
1222
  }
@@ -1477,7 +1685,8 @@ function schemaKeys(source, file, scope, object = 'runtimeConfigSchema') {
1477
1685
  return [
1478
1686
  {
1479
1687
  name,
1480
- optional: rawKey(p) !== name || acceptsMissing(p.value)
1688
+ optional: rawKey(p) !== name || acceptsMissing(p.value),
1689
+ schema: source.slice(p.value.start ?? 0, p.value.end ?? 0)
1481
1690
  }
1482
1691
  ];
1483
1692
  });
@@ -1697,139 +1906,6 @@ function applyTransform(source, file, op) {
1697
1906
  throw new MfeflowError('E204', JSON.stringify(Reflect.get(op, 'op')));
1698
1907
  }
1699
1908
  }
1700
- const MAX_BYTES = 1000000;
1701
- const TIMEOUT_MS = 10000;
1702
- const MAX_REDIRECTS = 5;
1703
- const realFetch = (url, init)=>fetch(url, init);
1704
- function registryUrl(name, registries, version) {
1705
- const slash = name.indexOf('/');
1706
- const namespace = name.slice(0, slash);
1707
- const bare = name.slice(slash + 1);
1708
- const template = registries[namespace];
1709
- if (void 0 === template) throw new MfeflowError('E210', namespace);
1710
- const url = template.replaceAll('{name}', encodeURIComponent(bare)).replaceAll('{version}', encodeURIComponent(version ?? 'latest'));
1711
- let parsed;
1712
- try {
1713
- parsed = new URL(url);
1714
- } catch (error) {
1715
- throw new MfeflowError('E401', `${namespace} has an invalid URL (${url})`, {
1716
- cause: error
1717
- });
1718
- }
1719
- if (!isRegistryProtocol(parsed)) throw new MfeflowError('E401', `${url} is not https (plain http is for 127.0.0.1, localhost and [::1] only)`);
1720
- return parsed.href;
1721
- }
1722
- async function getHttps(url, fetchImpl, code, options = {}) {
1723
- const fail = (at, why, cause)=>{
1724
- throw new MfeflowError(code, `${at}: ${why}`, void 0 === cause ? {} : {
1725
- cause
1726
- });
1727
- };
1728
- const startsLoopback = true === options.loopback && isLoopbackHttp(new URL(url));
1729
- const permitted = (target)=>'https:' === target.protocol || startsLoopback && isLoopbackHttp(target);
1730
- const addressOk = (address)=>{
1731
- try {
1732
- return permitted(new URL(address));
1733
- } catch {
1734
- return false;
1735
- }
1736
- };
1737
- let current = url;
1738
- for(let hop = 0; hop <= MAX_REDIRECTS; hop += 1){
1739
- let response;
1740
- try {
1741
- response = await fetchImpl(current, {
1742
- signal: AbortSignal.timeout(TIMEOUT_MS),
1743
- redirect: 'manual'
1744
- });
1745
- } catch (error) {
1746
- return fail(current, messageOf(error), error);
1747
- }
1748
- if (void 0 !== response.url && '' !== response.url && !addressOk(response.url)) fail(url, `ended up at ${response.url}, which is not https`);
1749
- if (response.status < 300 || response.status >= 400) return response;
1750
- const location = response.headers?.get('location');
1751
- await response.body?.cancel().catch(()=>void 0);
1752
- if (null == location || '' === location) fail(current, `redirected (${String(response.status)}) with no address`);
1753
- let next;
1754
- try {
1755
- next = new URL(location ?? '', current);
1756
- } catch (error) {
1757
- return fail(current, 'redirected to a bad address', error);
1758
- }
1759
- if (!permitted(next)) fail(url, `redirected to ${next.href}, which is not https`);
1760
- current = next.href;
1761
- }
1762
- return fail(url, 'redirected too many times');
1763
- }
1764
- async function fetchRemoteText(name, registries, fetchImpl = realFetch, version) {
1765
- const url = registryUrl(name, registries, version);
1766
- const response = await getHttps(url, fetchImpl, 'E401', {
1767
- loopback: true
1768
- });
1769
- if (!response.ok) {
1770
- const hint = 404 === response.status ? `${name} is not in that registry` : void 0;
1771
- throw new MfeflowError('E401', `${url} answered ${String(response.status)}${hint ? `: ${hint}` : ''}`);
1772
- }
1773
- return readCappedText(response, MAX_BYTES, ()=>new MfeflowError('E402', `${url} is larger than 1 MB`));
1774
- }
1775
- async function readCappedText(response, maxBytes, tooBig) {
1776
- const declared = Number(response.headers?.get('content-length') ?? 0 / 0);
1777
- if (declared > maxBytes) {
1778
- await response.body?.cancel().catch(()=>void 0);
1779
- throw tooBig();
1780
- }
1781
- if (!response.body) {
1782
- const text = await response.text();
1783
- if (Buffer.byteLength(text, 'utf8') > maxBytes) throw tooBig();
1784
- return text;
1785
- }
1786
- const chunks = [];
1787
- let total = 0;
1788
- for await (const chunk of response.body){
1789
- total += chunk.byteLength;
1790
- if (total > maxBytes) throw tooBig();
1791
- chunks.push(chunk);
1792
- }
1793
- return new TextDecoder().decode(Buffer.concat(chunks));
1794
- }
1795
- async function fetchFeatureIndex(namespace, registries, fetchImpl = realFetch) {
1796
- const template = registries[namespace];
1797
- if (void 0 === template) throw new MfeflowError('E210', namespace);
1798
- const problem = registryTemplateProblem(template);
1799
- if (void 0 !== problem) throw new MfeflowError('E401', `${namespace}: ${problem}`);
1800
- const url = registryIndexUrl(template);
1801
- const response = await getHttps(url, fetchImpl, 'E401', {
1802
- loopback: true
1803
- });
1804
- if (!response.ok) {
1805
- const hint = 404 === response.status ? ': this registry has no index.json' : '';
1806
- throw new MfeflowError('E401', `${url} answered ${String(response.status)}${hint}`);
1807
- }
1808
- const text = await readCappedText(response, MAX_BYTES, ()=>new MfeflowError('E403', `${url} is larger than 1 MB`));
1809
- let value;
1810
- try {
1811
- value = JSON.parse(text);
1812
- } catch (error) {
1813
- const reason = messageOf(error).replaceAll(/\p{Cc}/gu, '?');
1814
- throw new MfeflowError('E403', `${url} is not JSON (${reason})`, {
1815
- cause: error
1816
- });
1817
- }
1818
- const issues = validateFeatureRegistryIndex(value);
1819
- if (issues.length > 0) {
1820
- const shown = issues.slice(0, 3).map((i)=>`${i.path || '(root)'} ${i.message}`);
1821
- const more = issues.length > 3 ? `; and ${String(issues.length - 3)} more` : '';
1822
- throw new MfeflowError('E403', `${url}: ${shown.join('; ')}${more}`);
1823
- }
1824
- return itemsOf(value);
1825
- }
1826
- function itemsOf(value) {
1827
- const holder = {
1828
- items: []
1829
- };
1830
- Object.assign(holder, value);
1831
- return holder.items;
1832
- }
1833
1909
  const ROOTS = new Set([
1834
1910
  'z',
1835
1911
  'v',
@@ -1948,7 +2024,7 @@ function isModuleSpecifier(value) {
1948
2024
  }
1949
2025
  const DIST_TAG = /^[a-z][a-z0-9-]*$/;
1950
2026
  const TARBALL_NAME = /[.](?:tgz|tar\.gz|tar)$/i;
1951
- function isRegistryRange(range) {
2027
+ function policy_isRegistryRange(range) {
1952
2028
  if ('' === range || range !== range.trim()) return false;
1953
2029
  if (range.includes('/') || range.includes('\\')) return false;
1954
2030
  if (range.split(/\s+|\|\|/).some((part)=>TARBALL_NAME.test(part))) return false;
@@ -1992,7 +2068,7 @@ function assertThirdPartyContent(manifest, label) {
1992
2068
  }
1993
2069
  for (const dep of manifest.dependencies ?? []){
1994
2070
  if (dep.name.includes('{{')) throw new MfeflowError('E203', `${label}: ${dep.name} names a package in the project's scope`);
1995
- if (!isRegistryRange(dep.range)) throw new MfeflowError('E203', `${label}: ${dep.name}@${dep.range} is not a registry version range`);
2071
+ if (!policy_isRegistryRange(dep.range)) throw new MfeflowError('E203', `${label}: ${dep.name}@${dep.range} is not a registry version range`);
1996
2072
  }
1997
2073
  for (const t of manifest.transforms ?? [])assertThirdPartyTransform(t, label);
1998
2074
  for (const script of manifest.scripts ?? [])assertPlainScript(script.name, manifest.name, label);
@@ -2008,90 +2084,16 @@ function assertThirdPartyTransform(t, label) {
2008
2084
  for (const key of Object.keys(t.props ?? {}))if (!JSX_ATTRIBUTE.test(key)) deny(`has a prop "${key}" that is not a JSX attribute name`);
2009
2085
  }
2010
2086
  }
2011
- function parseFeature(text, trust, label) {
2012
- let value;
2013
- try {
2014
- value = JSON.parse(text);
2015
- } catch (error) {
2016
- throw new MfeflowError('E202', `${label}: ${messageOf(error)}`, {
2017
- cause: error
2018
- });
2019
- }
2020
- if ('object' != typeof value || null === value || Array.isArray(value)) throw new MfeflowError('E202', `${label}: must be a JSON object`);
2021
- const record = Object.fromEntries(Object.entries(value));
2022
- if ('third-party' === trust) assertAllowedKeys(record, label);
2023
- assertKnownOps(record, label);
2024
- const issues = validateFeatureManifest(record);
2025
- if (issues.length > 0) {
2026
- const detail = issues.map((i)=>`${i.path || '(root)'} ${i.message}`).join('; ');
2027
- throw new MfeflowError('E202', `${label}: ${detail}`);
2087
+ const LETTERS = 'abcdefghijklmnopqrstuvwxyz';
2088
+ function shortHash(text) {
2089
+ let hash = 0x811c9dc5;
2090
+ for (const char of text)hash = Math.imul(hash ^ char.codePointAt(0), 0x01000193) >>> 0;
2091
+ let out = '';
2092
+ for(let i = 0; i < 4; i++){
2093
+ out += LETTERS[hash % 26];
2094
+ hash = Math.floor(hash / 26);
2028
2095
  }
2029
- const manifest = toManifest(record, label);
2030
- if ('third-party' === trust) assertThirdPartyContent(manifest, label);
2031
- return manifest;
2032
- }
2033
- function toManifest(record, label) {
2034
- const manifest = {
2035
- name: '',
2036
- version: ''
2037
- };
2038
- Object.assign(manifest, record);
2039
- if ('' === manifest.name) throw new MfeflowError('E202', `${label}: name is required`);
2040
- return manifest;
2041
- }
2042
- async function loadFeature(name, options) {
2043
- if (!FEATURE_NAME.test(name)) throw new MfeflowError('E201', `"${name}" is not a feature name`);
2044
- if (name.startsWith('@')) return loadRemote(name, options);
2045
- return loadLocal(name, options);
2046
- }
2047
- async function loadLocal(name, options) {
2048
- const ops = options.fs ?? realFs;
2049
- if (!options.featuresDir) throw new MfeflowError('E201', `"${name}" (no features folder configured)`);
2050
- const dir = node_path.join(options.featuresDir, name);
2051
- const text = await readIfExists(ops, node_path.join(dir, 'feature.json'));
2052
- if (void 0 === text) throw new MfeflowError('E201', `"${name}" (looked in ${options.featuresDir})`);
2053
- const manifest = parseFeature(text, 'first-party', `${name}/feature.json`);
2054
- if (manifest.name !== name) throw new MfeflowError('E202', `${name}/feature.json: name is "${manifest.name}", expected "${name}"`);
2055
- return {
2056
- manifest,
2057
- trust: 'first-party',
2058
- readAsset: async (relative)=>{
2059
- if (!isSafeRelativePath(relative)) throw new MfeflowError('E105', `"${relative}"`);
2060
- const content = await readIfExists(ops, node_path.join(dir, ...relative.split('/')));
2061
- if (void 0 === content) throw new MfeflowError('E201', `${name}: file "${relative}" is missing`);
2062
- return content;
2063
- }
2064
- };
2065
- }
2066
- async function loadRemote(name, options) {
2067
- const text = await fetchRemoteText(name, options.registries ?? {}, options.fetch, options.version);
2068
- const manifest = parseFeature(text, 'third-party', name);
2069
- if (manifest.name !== name) throw new MfeflowError('E402', `asked for ${name}, got ${manifest.name}`);
2070
- return {
2071
- manifest,
2072
- trust: 'third-party'
2073
- };
2074
- }
2075
- async function listLocalFeatures(featuresDir, ops = realFs) {
2076
- let dirs;
2077
- try {
2078
- dirs = await ops.listDirs(featuresDir);
2079
- } catch {
2080
- return [];
2081
- }
2082
- const checked = await Promise.all(dirs.map(async (dir)=>await readIfExists(ops, node_path.join(featuresDir, dir, 'feature.json')) === void 0 ? void 0 : dir));
2083
- return checked.filter((d)=>void 0 !== d).toSorted((a, b)=>a.localeCompare(b));
2084
- }
2085
- const LETTERS = 'abcdefghijklmnopqrstuvwxyz';
2086
- function shortHash(text) {
2087
- let hash = 0x811c9dc5;
2088
- for (const char of text)hash = Math.imul(hash ^ char.codePointAt(0), 0x01000193) >>> 0;
2089
- let out = '';
2090
- for(let i = 0; i < 4; i++){
2091
- out += LETTERS[hash % 26];
2092
- hash = Math.floor(hash / 26);
2093
- }
2094
- return out;
2096
+ return out;
2095
2097
  }
2096
2098
  function tailwindPrefix(name) {
2097
2099
  if (/^[a-z]+$/.test(name)) return name;
@@ -2263,7 +2265,10 @@ async function resolveFeature(input) {
2263
2265
  version: manifest.version,
2264
2266
  ...Object.keys(options).length > 0 ? {
2265
2267
  options
2266
- } : {}
2268
+ } : {},
2269
+ ...void 0 === feature.integrity ? {} : {
2270
+ integrity: feature.integrity
2271
+ }
2267
2272
  });
2268
2273
  }
2269
2274
  async function editFile(builder, file, edit, op) {
@@ -2273,6 +2278,2109 @@ async function editFile(builder, file, edit, op) {
2273
2278
  if (isConfig && !await builder.exists(file)) return void await builder.create(file, edit('{}\n'));
2274
2279
  await builder.patch(file, edit);
2275
2280
  }
2281
+ const upgrade_WORKSPACE_FILE = 'pnpm-workspace.yaml';
2282
+ const upgrade_TURBO_FILE = 'turbo.json';
2283
+ const upgrade_DEPENDENCY_SECTIONS = [
2284
+ 'dependencies',
2285
+ 'devDependencies',
2286
+ 'optionalDependencies'
2287
+ ];
2288
+ const OWN_PREFIX = '@mfeflow/';
2289
+ const OWN_CLI = 'mfeflow';
2290
+ const PLACEHOLDER_RANGE = 'latest';
2291
+ const USER_DATA = /^registry\/envs\//;
2292
+ const ENV_KEY_FILES = /(^|\/)(env\.ts|src\/config\/schema\.ts|runtime-config(\.[^/]+)?\.json|\.env[^/]*)$/;
2293
+ const KEEP_RANGE = /^(workspace|link|file|portal):/;
2294
+ const byCodePoint = (a, b)=>a < b ? -1 : a > b ? 1 : 0;
2295
+ const isOwn = (name)=>name === OWN_CLI || name.startsWith(OWN_PREFIX);
2296
+ const isPackageFile = (file)=>'package.json' === file || file.endsWith('/package.json');
2297
+ const isStructured = (file)=>isPackageFile(file) || file === upgrade_WORKSPACE_FILE || file === upgrade_TURBO_FILE;
2298
+ const sameText = (a, b)=>normalizeEol(a) === normalizeEol(b);
2299
+ async function runtimeRange(pkgFile, read) {
2300
+ const source = await read(pkgFile);
2301
+ if (void 0 === source) return;
2302
+ const pkg = parseJsonObject(pkgFile, source);
2303
+ for (const section of upgrade_DEPENDENCY_SECTIONS){
2304
+ const value = asRecord(pkg[section])['@mfeflow/runtime'];
2305
+ if ('string' == typeof value) {
2306
+ if (!value.startsWith('catalog:')) return {
2307
+ range: value,
2308
+ catalog: void 0
2309
+ };
2310
+ return {
2311
+ range: value,
2312
+ catalog: value.slice(8)
2313
+ };
2314
+ }
2315
+ }
2316
+ }
2317
+ async function catalogRange(read, catalogName) {
2318
+ const source = await read(upgrade_WORKSPACE_FILE);
2319
+ if (void 0 === source) return;
2320
+ const doc = parseDocument(source);
2321
+ const where = '' === catalogName || 'default' === catalogName ? [
2322
+ 'catalog'
2323
+ ] : [
2324
+ 'catalogs',
2325
+ catalogName
2326
+ ];
2327
+ const value = doc.getIn([
2328
+ ...where,
2329
+ '@mfeflow/runtime'
2330
+ ]);
2331
+ return 'string' == typeof value ? value : void 0;
2332
+ }
2333
+ function resolveRange(found, read) {
2334
+ return void 0 === found.catalog ? Promise.resolve(found.range) : catalogRange(read, found.catalog);
2335
+ }
2336
+ async function detectBaseline(project, read) {
2337
+ if (void 0 !== project.cli) return {
2338
+ version: project.cli,
2339
+ source: 'mfeflow.json'
2340
+ };
2341
+ const apps = Object.values(project.apps).toSorted((a, b)=>Number('shell' === b.kind) - Number('shell' === a.kind));
2342
+ for (const app of apps){
2343
+ const file = '' === app.path ? 'package.json' : `${app.path}/package.json`;
2344
+ const found = await runtimeRange(file, read);
2345
+ if (void 0 === found) continue;
2346
+ const range = await resolveRange(found, read);
2347
+ const min = void 0 === range || KEEP_RANGE.test(range) ? void 0 : minimum(range);
2348
+ if (void 0 === min) throw new MfeflowError('E601', `${file} has @mfeflow/runtime at ${JSON.stringify(found.range)}, which does not name a version`);
2349
+ return {
2350
+ version: min,
2351
+ source: 'package.json'
2352
+ };
2353
+ }
2354
+ throw new MfeflowError('E601', 'mfeflow.json has no "cli" and no app depends on @mfeflow/runtime');
2355
+ }
2356
+ function minimum(range) {
2357
+ if (null === semver.validRange(range)) return;
2358
+ const min = semver.minVersion(range)?.version;
2359
+ return '0.0.0' === min ? void 0 : min;
2360
+ }
2361
+ function recordedOptions(feature, project, warnings) {
2362
+ const recorded = project.features[feature.manifest.name]?.options ?? {};
2363
+ const known = new Set((feature.manifest.prompts ?? []).map((p)=>p.name));
2364
+ const kept = {};
2365
+ for (const [key, value] of Object.entries(recorded))if (known.has(key)) kept[key] = value;
2366
+ else warnings.push(`${feature.manifest.name} no longer has the option "${key}"; it was ignored`);
2367
+ return kept;
2368
+ }
2369
+ const depKey = (file, name)=>`${file}\0${name}`;
2370
+ async function replay(project, input) {
2371
+ const local = void 0 !== project.local;
2372
+ const builder = new PlanBuilder(()=>Promise.resolve(void 0), {
2373
+ catalog: true,
2374
+ ...local ? {} : {
2375
+ rewriteRange: (name, range)=>isOwn(name) && range === PLACEHOLDER_RANGE ? `^${input.toVersion}` : void 0
2376
+ }
2377
+ });
2378
+ const warnings = [];
2379
+ const names = [
2380
+ ...input.features.map((f)=>f.manifest.name),
2381
+ ...(input.skipped ?? []).map((s)=>s.name)
2382
+ ];
2383
+ const bare = {
2384
+ ...project,
2385
+ features: {}
2386
+ };
2387
+ const steps = [
2388
+ ...input.features.map((feature)=>({
2389
+ feature,
2390
+ options: recordedOptions(feature, project, warnings)
2391
+ })),
2392
+ ...(input.glue ?? []).map((feature)=>({
2393
+ feature,
2394
+ options: {}
2395
+ }))
2396
+ ];
2397
+ for (const step of steps)await resolveFeature({
2398
+ ...step,
2399
+ project: bare,
2400
+ builder,
2401
+ alsoAdding: names
2402
+ });
2403
+ const built = builder.build();
2404
+ const files = new Map();
2405
+ const tracked = new Set();
2406
+ const thirdParty = new Set();
2407
+ for (const change of built.changes){
2408
+ if ('third-party' === change.trust) thirdParty.add(change.path);
2409
+ if (null !== change.after) {
2410
+ files.set(change.path, change.after);
2411
+ if (change.track) tracked.add(change.path);
2412
+ }
2413
+ }
2414
+ const thirdPartyDeps = new Set(built.dependencies.filter((d)=>true === d.thirdParty).map((d)=>depKey(d.file, d.name)));
2415
+ const byName = new Map(built.features.map((f)=>[
2416
+ f.name,
2417
+ f
2418
+ ]));
2419
+ return {
2420
+ files,
2421
+ tracked,
2422
+ thirdParty,
2423
+ thirdPartyDeps,
2424
+ features: [
2425
+ ...byName.values()
2426
+ ],
2427
+ warnings
2428
+ };
2429
+ }
2430
+ function keepLineEndings(change) {
2431
+ if (null === change.after || !change.before?.includes('\r\n')) return change;
2432
+ return {
2433
+ ...change,
2434
+ after: normalizeEol(change.after).replaceAll('\n', '\r\n')
2435
+ };
2436
+ }
2437
+ function unifiedDiff(file, yours, incoming) {
2438
+ return formatPatch(structuredPatch(`a/${file} (yours)`, `b/${file} (new)`, yours, incoming));
2439
+ }
2440
+ function entriesOf(file, source) {
2441
+ const base = file.slice(file.lastIndexOf('/') + 1);
2442
+ try {
2443
+ if (base.endsWith('.json')) {
2444
+ const json = parseJsonObject(file, source);
2445
+ return new Map(Object.entries(json).map(([key, value])=>[
2446
+ key,
2447
+ JSON.stringify(value)
2448
+ ]));
2449
+ }
2450
+ if (base.startsWith('.env')) {
2451
+ const values = new Map();
2452
+ for (const line of source.split('\n')){
2453
+ const match = /^\s*(?:export\s+)?([A-Za-z_][A-Za-z0-9_]*)\s*=(.*)$/.exec(line);
2454
+ if (match?.[1] !== void 0) values.set(match[1], (match[2] ?? '').trim());
2455
+ }
2456
+ return values;
2457
+ }
2458
+ const keys = schemaKeys(source, file, 'env.ts' === base ? 'server' : 'runtime');
2459
+ return void 0 === keys ? void 0 : new Map(keys.map((k)=>[
2460
+ k.name,
2461
+ k.schema.replaceAll(/\s+/g, ' ')
2462
+ ]));
2463
+ } catch {
2464
+ return;
2465
+ }
2466
+ }
2467
+ function lostKeys(file, disk, incoming) {
2468
+ const yours = entriesOf(file, disk);
2469
+ const wanted = entriesOf(file, incoming);
2470
+ if (void 0 === yours || void 0 === wanted) return;
2471
+ const missing = [
2472
+ ...yours
2473
+ ].filter(([key])=>!wanted.has(key)).map(([key])=>key);
2474
+ const changed = [
2475
+ ...yours
2476
+ ].filter(([key, value])=>wanted.has(key) && wanted.get(key) !== value).map(([key])=>key);
2477
+ return {
2478
+ missing,
2479
+ changed
2480
+ };
2481
+ }
2482
+ const writes = (verdict)=>'create' === verdict || 'replace' === verdict || 'missing' !== verdict && 'matching' !== verdict && 'leave' !== verdict && 'symlink' !== verdict;
2483
+ function judge(file, disk, incoming, state) {
2484
+ const { recorded, kept, anySkipped } = state;
2485
+ if (void 0 === disk) {
2486
+ if (void 0 !== recorded) return 'missing';
2487
+ return USER_DATA.test(file) ? 'leave' : 'create';
2488
+ }
2489
+ if (sameText(disk, incoming)) return void 0 !== recorded && recorded !== hashContent(disk) ? 'matching' : 'leave';
2490
+ if (USER_DATA.test(file)) return 'leave';
2491
+ if (void 0 !== kept) return kept === hashContent(incoming) ? 'leave' : 'edited';
2492
+ if (void 0 !== recorded && recorded === hashContent(incoming)) return 'leave';
2493
+ if (void 0 === recorded) return 'untracked';
2494
+ if (recorded !== hashContent(disk)) return 'edited';
2495
+ if (ENV_KEY_FILES.test(file)) {
2496
+ const lost = lostKeys(file, disk, incoming);
2497
+ if (void 0 === lost || lost.missing.length > 0) return 'edited-by-cli';
2498
+ }
2499
+ return anySkipped ? 'skipped-feature' : 'replace';
2500
+ }
2501
+ async function compareFiles(project, fresh, read, builder, options) {
2502
+ const out = {
2503
+ diverged: [],
2504
+ kept: [],
2505
+ missing: [],
2506
+ matching: [],
2507
+ notes: []
2508
+ };
2509
+ const entries = [
2510
+ ...fresh.files
2511
+ ].toSorted(([a], [b])=>a < b ? -1 : 1);
2512
+ for (const [file, incoming] of entries){
2513
+ const disk = await read(file);
2514
+ if (isStructured(file) && void 0 !== disk) continue;
2515
+ let verdict = judge(file, disk, incoming, {
2516
+ ...project.files?.[file] === void 0 ? {} : {
2517
+ recorded: project.files[file]
2518
+ },
2519
+ ...project.kept?.[file] === void 0 ? {} : {
2520
+ kept: project.kept[file]
2521
+ },
2522
+ anySkipped: options.anySkipped
2523
+ });
2524
+ if (writes(verdict) && await options.isSymlink?.(file)) verdict = 'symlink';
2525
+ if ('missing' === verdict) out.missing.push(file);
2526
+ else if ('matching' === verdict) out.matching.push(file);
2527
+ else if ('create' === verdict) await builder.create(file, incoming, fresh.tracked.has(file));
2528
+ else if ('replace' === verdict) {
2529
+ const changed = ENV_KEY_FILES.test(file) ? lostKeys(file, disk ?? '', incoming)?.changed : [];
2530
+ if (void 0 !== changed && changed.length > 0) out.notes.push(`${file}: the values of ${changed.join(', ')} change to the new version's; if you set them with mfeflow add env-key, set them again`);
2531
+ await builder.replace(file, incoming);
2532
+ } else if ('leave' !== verdict) {
2533
+ const entry = diverge(file, verdict, disk ?? '', incoming);
2534
+ const decision = 'symlink' === verdict ? void 0 : decide(file, options.decisions);
2535
+ if (void 0 !== decision) entry.decision = decision;
2536
+ out.diverged.push(entry);
2537
+ if ('accept' === decision) await builder.replace(file, incoming);
2538
+ else if ('keep' === decision) out.kept.push(entry);
2539
+ }
2540
+ }
2541
+ return out;
2542
+ }
2543
+ function diverge(file, reason, yours, incoming) {
2544
+ const entry = {
2545
+ path: file,
2546
+ reason,
2547
+ yours,
2548
+ incoming,
2549
+ diff: unifiedDiff(file, yours, incoming)
2550
+ };
2551
+ const lost = ENV_KEY_FILES.test(file) ? lostKeys(file, yours, incoming) : void 0;
2552
+ if (void 0 !== lost && lost.missing.length + lost.changed.length > 0) {
2553
+ const all = new Set([
2554
+ ...lost.missing,
2555
+ ...lost.changed
2556
+ ]);
2557
+ entry.lostKeys = [
2558
+ ...entriesOf(file, yours)?.keys() ?? []
2559
+ ].filter((key)=>all.has(key));
2560
+ if (lost.changed.length > 0) entry.changedValues = lost.changed;
2561
+ }
2562
+ return entry;
2563
+ }
2564
+ const upgrade_named = (set, file)=>'all' === set || set?.has(file) === true;
2565
+ function decide(file, decisions) {
2566
+ const accept = upgrade_named(decisions.accept, file);
2567
+ const keep = upgrade_named(decisions.keep, file);
2568
+ if (accept && keep) return 'all' === decisions.accept && 'all' !== decisions.keep ? 'keep' : 'accept';
2569
+ if (accept) return 'accept';
2570
+ return keep ? 'keep' : void 0;
2571
+ }
2572
+ async function editWorkspace(fresh, read, builder, local) {
2573
+ const incoming = fresh.files.get(upgrade_WORKSPACE_FILE);
2574
+ const disk = await read(upgrade_WORKSPACE_FILE);
2575
+ if (void 0 === incoming || void 0 === disk) return;
2576
+ const wanted = asRecord(parseDocument(incoming).toJSON());
2577
+ const doc = parseDocument(disk);
2578
+ let changed = false;
2579
+ for (const section of [
2580
+ 'catalog',
2581
+ 'allowBuilds'
2582
+ ]){
2583
+ const have = doc.get(section);
2584
+ for (const [name, value] of Object.entries(asRecord(wanted[section])))if (!(isMap(have) && have.has(name) || local && isOwn(name))) {
2585
+ doc.setIn([
2586
+ section,
2587
+ name
2588
+ ], value);
2589
+ changed = true;
2590
+ }
2591
+ }
2592
+ if (changed) await builder.patch(upgrade_WORKSPACE_FILE, ()=>doc.toString());
2593
+ }
2594
+ async function editTurbo(fresh, read, builder) {
2595
+ const incoming = fresh.files.get(upgrade_TURBO_FILE);
2596
+ if (void 0 === incoming || await read(upgrade_TURBO_FILE) === void 0) return;
2597
+ const wanted = asRecord(asRecord(parseJsonObject(upgrade_TURBO_FILE, incoming)).tasks);
2598
+ await builder.patch(upgrade_TURBO_FILE, (source)=>editJsonObject(upgrade_TURBO_FILE, source, (turbo)=>{
2599
+ const tasks = asRecord(turbo.tasks);
2600
+ const missing = Object.entries(wanted).filter(([name])=>!(name in tasks));
2601
+ if (0 === missing.length) return false;
2602
+ turbo.tasks = {
2603
+ ...tasks,
2604
+ ...Object.fromEntries(missing)
2605
+ };
2606
+ return true;
2607
+ }));
2608
+ }
2609
+ function upgrade_listed(pkg) {
2610
+ return upgrade_DEPENDENCY_SECTIONS.flatMap((section)=>Object.entries(asRecord(pkg[section])).flatMap(([name, range])=>'string' == typeof range ? [
2611
+ [
2612
+ name,
2613
+ range
2614
+ ]
2615
+ ] : []));
2616
+ }
2617
+ function upgrade_declared(pkg) {
2618
+ const out = new Map();
2619
+ for (const section of upgrade_DEPENDENCY_SECTIONS)for (const [name, range] of Object.entries(asRecord(pkg[section])))if ('string' == typeof range) out.set(name, range);
2620
+ return out;
2621
+ }
2622
+ async function addMissing(file, pkg, wanted, local, builder) {
2623
+ const have = upgrade_declared(pkg);
2624
+ for (const [section, dev] of [
2625
+ [
2626
+ 'dependencies',
2627
+ false
2628
+ ],
2629
+ [
2630
+ 'devDependencies',
2631
+ true
2632
+ ]
2633
+ ])for (const [name, range] of Object.entries(asRecord(wanted[section])))if (!(have.has(name) || 'string' != typeof range || local && isOwn(name))) await builder.addDependency(file, name, range, dev);
2634
+ const scripts = asRecord(pkg.scripts);
2635
+ for (const [name, command] of Object.entries(asRecord(wanted.scripts)))if (!(name in scripts) && 'string' == typeof command) await builder.addScript(file, name, command);
2636
+ }
2637
+ async function editPackage(file, fresh, toVersion, local, builder) {
2638
+ const source = await builder.content(file);
2639
+ if (void 0 === source) return;
2640
+ const incoming = fresh.files.get(file);
2641
+ if (void 0 !== incoming) await addMissing(file, parseJsonObject(file, source), parseJsonObject(file, incoming), local, builder);
2642
+ if (local) return;
2643
+ const target = `^${toVersion}`;
2644
+ const current = parseJsonObject(file, await builder.content(file) ?? source);
2645
+ for (const [name, range] of upgrade_listed(current))if (!(!isOwn(name) || range === target || KEEP_RANGE.test(range))) await builder.setRange(file, name, target);
2646
+ }
2647
+ function pendingMigrations(features, project) {
2648
+ const pending = [];
2649
+ for (const feature of features){
2650
+ const { name, version, migrations } = feature.manifest;
2651
+ const recorded = project.features[name]?.version;
2652
+ if (void 0 !== recorded && null !== semver.valid(recorded)) {
2653
+ if ('first-party' === feature.trust) {
2654
+ for (const m of migrations ?? [])if (null !== semver.valid(m.to) && null !== semver.valid(version)) {
2655
+ if (semver.lt(recorded, m.to) && semver.lte(m.to, version)) pending.push({
2656
+ feature,
2657
+ ...m
2658
+ });
2659
+ }
2660
+ }
2661
+ }
2662
+ }
2663
+ return pending.toSorted((a, b)=>semver.compare(a.to, b.to));
2664
+ }
2665
+ async function importDefault(file) {
2666
+ return import(pathToFileURL(file).href);
2667
+ }
2668
+ async function loadMigration(step, importModule) {
2669
+ const name = step.feature.manifest.name;
2670
+ const label = `${name} ${step.from} to ${step.to} (${step.module})`;
2671
+ const { dir } = step.feature;
2672
+ if (void 0 === dir || !isSafeRelativePath(step.module)) throw new MfeflowError('E603', `${label}: cannot find ${step.module}`);
2673
+ let loaded;
2674
+ try {
2675
+ loaded = await importModule(node_path.join(dir, ...step.module.split('/')));
2676
+ } catch (error) {
2677
+ throw new MfeflowError('E603', `${label}: ${messageOf(error)}`, {
2678
+ cause: error
2679
+ });
2680
+ }
2681
+ const exported = asRecord(loaded);
2682
+ const fn = 'function' == typeof exported.default ? exported.default : exported.migrate;
2683
+ if ('function' != typeof fn) throw new MfeflowError('E603', `${label}: ${step.module} exports no migration function`);
2684
+ return async (builder, project)=>{
2685
+ try {
2686
+ await Reflect.apply(fn, void 0, [
2687
+ builder,
2688
+ project
2689
+ ]);
2690
+ } catch (error) {
2691
+ throw new MfeflowError('E603', `${label}: ${messageOf(error)}`, {
2692
+ cause: error
2693
+ });
2694
+ }
2695
+ };
2696
+ }
2697
+ async function runMigrations(project, input, builder) {
2698
+ const importModule = input.importModule ?? importDefault;
2699
+ const ran = [];
2700
+ for (const step of pendingMigrations(input.features, project)){
2701
+ const migrate = await loadMigration(step, importModule);
2702
+ await migrate(builder.scoped('first-party'), project);
2703
+ ran.push({
2704
+ feature: step.feature.manifest.name,
2705
+ from: step.from,
2706
+ to: step.to
2707
+ });
2708
+ }
2709
+ return ran;
2710
+ }
2711
+ async function findRemoved(project, fresh, read) {
2712
+ const removed = [];
2713
+ for (const file of Object.keys(project.files ?? {}).toSorted(byCodePoint))if (!(fresh.files.has(file) || USER_DATA.test(file) || ENV_KEY_FILES.test(file))) {
2714
+ if (await read(file) !== void 0) removed.push(file);
2715
+ }
2716
+ return removed;
2717
+ }
2718
+ async function planUpgrade(project, input) {
2719
+ if (null === semver.valid(input.toVersion)) throw new MfeflowError('E601', `"${input.toVersion}" is not a version`);
2720
+ const baseline = await detectBaseline(project, input.read);
2721
+ if (null === semver.valid(baseline.version) || semver.gt(baseline.version, input.toVersion)) throw new MfeflowError('E602', `project is at ${baseline.version}, target is ${input.toVersion}`);
2722
+ const fresh = await replay(project, input);
2723
+ const builder = new PlanBuilder(input.read, {});
2724
+ await builder.captureProject();
2725
+ const compared = await compareFiles(project, fresh, input.read, builder, {
2726
+ anySkipped: (input.skipped ?? []).length > 0,
2727
+ decisions: input.decisions ?? {},
2728
+ ...input.isSymlink ? {
2729
+ isSymlink: input.isSymlink
2730
+ } : {}
2731
+ });
2732
+ await editWorkspace(fresh, input.read, builder, void 0 !== project.local);
2733
+ await editTurbo(fresh, input.read, builder);
2734
+ const local = void 0 !== project.local;
2735
+ const packageFiles = new Set([
2736
+ ...fresh.files.keys(),
2737
+ ...Object.values(project.apps).map((a)=>('' === a.path ? '' : `${a.path}/`) + 'package.json'),
2738
+ 'package.json'
2739
+ ].filter(isPackageFile));
2740
+ for (const file of [
2741
+ ...packageFiles
2742
+ ].toSorted(byCodePoint))if (await input.read(file)) await editPackage(file, fresh, input.toVersion, local, builder);
2743
+ const migrations = await runMigrations(project, input, builder);
2744
+ const skipped = input.skipped ?? [];
2745
+ const planned = (file)=>builder.content(file);
2746
+ const removed = 0 === skipped.length ? await findRemoved(project, fresh, planned) : [];
2747
+ const built = builder.build();
2748
+ const plan = {
2749
+ ...built,
2750
+ changes: built.changes.map((change)=>{
2751
+ const kept = keepLineEndings(change);
2752
+ kept.trust = fresh.thirdParty.has(kept.path) ? 'third-party' : kept.trust;
2753
+ return kept;
2754
+ }),
2755
+ dependencies: built.dependencies.map((dep)=>{
2756
+ if (fresh.thirdPartyDeps.has(depKey(dep.file, dep.name))) dep.thirdParty = true;
2757
+ return dep;
2758
+ }),
2759
+ features: fresh.features,
2760
+ warnings: [
2761
+ ...fresh.warnings,
2762
+ ...compared.notes,
2763
+ ...built.warnings,
2764
+ ...skipped.map(({ name, reason })=>`${name} was not replayed (${reason}); its files were left as they are`)
2765
+ ]
2766
+ };
2767
+ const files = {
2768
+ ...project.files
2769
+ };
2770
+ const kept = {
2771
+ ...project.kept
2772
+ };
2773
+ for (const file of compared.matching)files[file] = hashContent(fresh.files.get(file) ?? '');
2774
+ for (const entry of compared.diverged)if ('accept' === entry.decision) {
2775
+ files[entry.path] = hashContent(entry.incoming);
2776
+ delete kept[entry.path];
2777
+ }
2778
+ for (const entry of compared.kept){
2779
+ files[entry.path] = hashContent(entry.yours);
2780
+ kept[entry.path] = hashContent(entry.incoming);
2781
+ }
2782
+ const next = {
2783
+ ...project,
2784
+ cli: input.toVersion
2785
+ };
2786
+ if (Object.keys(files).length > 0) next.files = files;
2787
+ if (Object.keys(kept).length > 0) next.kept = kept;
2788
+ else delete next.kept;
2789
+ return {
2790
+ from: baseline.version,
2791
+ fromSource: baseline.source,
2792
+ to: input.toVersion,
2793
+ plan,
2794
+ project: next,
2795
+ diverged: compared.diverged,
2796
+ migrations,
2797
+ removed,
2798
+ missing: compared.missing
2799
+ };
2800
+ }
2801
+ const RUNTIME_CONFIG_FILE = /^runtime-config(?:\.([a-z0-9-]+))?\.json$/;
2802
+ async function runtimeConfigFiles(folder) {
2803
+ const names = await promises.readdir(folder).catch(()=>[]);
2804
+ return names.filter((name)=>RUNTIME_CONFIG_FILE.test(name)).toSorted((a, b)=>a.localeCompare(b));
2805
+ }
2806
+ const ok = (value)=>({
2807
+ ok: true,
2808
+ value
2809
+ });
2810
+ const evaluate_fail = (reason)=>({
2811
+ ok: false,
2812
+ reason
2813
+ });
2814
+ function evaluate_unwrap(node) {
2815
+ if (!node) return;
2816
+ if (isTSAsExpression(node) || isTSSatisfiesExpression(node) || isTSNonNullExpression(node) || isTSTypeAssertion(node)) return evaluate_unwrap(node.expression);
2817
+ return node;
2818
+ }
2819
+ function requireSource(node) {
2820
+ const n = evaluate_unwrap(node);
2821
+ if (isCallExpression(n) && isIdentifier(n.callee, {
2822
+ name: 'require'
2823
+ })) {
2824
+ const arg = n.arguments[0];
2825
+ return isStringLiteral(arg) ? {
2826
+ source: arg.value,
2827
+ path: []
2828
+ } : void 0;
2829
+ }
2830
+ if (isMemberExpression(n) && !n.computed && isIdentifier(n.property)) {
2831
+ const inner = requireSource(n.object);
2832
+ return inner ? {
2833
+ source: inner.source,
2834
+ path: [
2835
+ ...inner.path,
2836
+ n.property.name
2837
+ ]
2838
+ } : void 0;
2839
+ }
2840
+ }
2841
+ function evaluate_keyName(key) {
2842
+ if (isIdentifier(key)) return key.name;
2843
+ if (isStringLiteral(key)) return key.value;
2844
+ }
2845
+ function bindVariables(statement, bindings) {
2846
+ if (!isVariableDeclaration(statement)) return;
2847
+ for (const decl of statement.declarations){
2848
+ const required = requireSource(decl.init);
2849
+ if (isIdentifier(decl.id)) if (required) bindings.set(decl.id.name, {
2850
+ kind: 'import',
2851
+ source: required.source,
2852
+ imported: required.path.at(-1) ?? 'default',
2853
+ depth: required.path.length
2854
+ });
2855
+ else if ('const' === statement.kind && decl.init) bindings.set(decl.id.name, {
2856
+ kind: 'const',
2857
+ init: decl.init
2858
+ });
2859
+ else bindings.set(decl.id.name, {
2860
+ kind: 'other',
2861
+ what: `a ${statement.kind} variable`
2862
+ });
2863
+ else if (isObjectPattern(decl.id)) for (const prop of decl.id.properties){
2864
+ if (!isObjectProperty(prop) || !isIdentifier(prop.value)) continue;
2865
+ const imported = evaluate_keyName(prop.key);
2866
+ if (required && imported) bindings.set(prop.value.name, {
2867
+ kind: 'import',
2868
+ source: required.source,
2869
+ imported,
2870
+ depth: required.path.length + 1
2871
+ });
2872
+ else bindings.set(prop.value.name, {
2873
+ kind: 'other',
2874
+ what: 'a destructured value'
2875
+ });
2876
+ }
2877
+ }
2878
+ }
2879
+ function isModuleExports(node) {
2880
+ return isMemberExpression(node) && isIdentifier(node.object, {
2881
+ name: 'module'
2882
+ }) && isIdentifier(node.property, {
2883
+ name: 'exports'
2884
+ });
2885
+ }
2886
+ const MUTATING_METHODS = new Set([
2887
+ 'push',
2888
+ 'pop',
2889
+ 'shift',
2890
+ 'unshift',
2891
+ 'splice',
2892
+ 'sort',
2893
+ 'reverse',
2894
+ 'fill',
2895
+ 'copyWithin',
2896
+ 'set',
2897
+ 'add',
2898
+ 'delete',
2899
+ 'clear',
2900
+ 'assign'
2901
+ ]);
2902
+ const MUTATING_STATICS = new Set([
2903
+ 'assign',
2904
+ 'defineProperty',
2905
+ 'defineProperties',
2906
+ 'setPrototypeOf'
2907
+ ]);
2908
+ function rootName(node) {
2909
+ let n = evaluate_unwrap(node);
2910
+ while(isMemberExpression(n) || n?.type === 'OptionalMemberExpression')n = evaluate_unwrap(n.object);
2911
+ return isIdentifier(n) ? n.name : void 0;
2912
+ }
2913
+ function mutatedNames(ast) {
2914
+ const names = new Set();
2915
+ const add = (name)=>{
2916
+ if (name) names.add(name);
2917
+ };
2918
+ for (const node of findAll(ast, (n)=>isAssignmentExpression(n) || 'UpdateExpression' === n.type || isUnaryExpression(n) || isCallExpression(n)))if (isAssignmentExpression(node)) add(rootName(node.left));
2919
+ else if ('UpdateExpression' === node.type) add(rootName(node.argument));
2920
+ else if (isUnaryExpression(node)) {
2921
+ if ('delete' === node.operator) add(rootName(node.argument));
2922
+ } else if (isCallExpression(node)) {
2923
+ const callee = evaluate_unwrap(node.callee);
2924
+ if (!isMemberExpression(callee) || callee.computed || !isIdentifier(callee.property)) continue;
2925
+ if (isIdentifier(callee.object, {
2926
+ name: 'Object'
2927
+ }) && MUTATING_STATICS.has(callee.property.name)) add(rootName(node.arguments[0]));
2928
+ else if (MUTATING_METHODS.has(callee.property.name)) add(rootName(callee.object));
2929
+ }
2930
+ return names;
2931
+ }
2932
+ function scopeOf(ast, file) {
2933
+ const bindings = new Map();
2934
+ const exports = new Map();
2935
+ const exportedFrom = new Map();
2936
+ const localExports = [];
2937
+ for (const statement of ast.program.body)if (isImportDeclaration(statement)) for (const spec of statement.specifiers){
2938
+ const imported = isImportDefaultSpecifier(spec) ? 'default' : isImportNamespaceSpecifier(spec) ? '*' : evaluate_keyName(spec.imported) ?? '*';
2939
+ bindings.set(spec.local.name, {
2940
+ kind: 'import',
2941
+ source: statement.source.value,
2942
+ imported
2943
+ });
2944
+ }
2945
+ else if (isVariableDeclaration(statement)) bindVariables(statement, bindings);
2946
+ else if (isFunctionDeclaration(statement) || isClassDeclaration(statement)) {
2947
+ if (statement.id) bindings.set(statement.id.name, {
2948
+ kind: 'other',
2949
+ what: 'a function or class'
2950
+ });
2951
+ } else if (isExportNamedDeclaration(statement)) {
2952
+ const decl = statement.declaration;
2953
+ if (decl && isVariableDeclaration(decl)) {
2954
+ bindVariables(decl, bindings);
2955
+ for (const d of decl.declarations)if (isIdentifier(d.id) && d.init) {
2956
+ exports.set(d.id.name, d.init);
2957
+ exportedFrom.set(d.id.name, d.id.name);
2958
+ }
2959
+ }
2960
+ if (!statement.source) for (const spec of statement.specifiers){
2961
+ if (!isExportSpecifier(spec)) continue;
2962
+ const exported = evaluate_keyName(spec.exported);
2963
+ const local = evaluate_keyName(spec.local);
2964
+ if (exported && local) localExports.push([
2965
+ exported,
2966
+ local
2967
+ ]);
2968
+ }
2969
+ } else if (isExportDefaultDeclaration(statement)) exports.set('default', statement.declaration);
2970
+ else if (isExpressionStatement(statement)) {
2971
+ const expr = statement.expression;
2972
+ if (isAssignmentExpression(expr) && isModuleExports(expr.left)) exports.set('default', expr.right);
2973
+ }
2974
+ for (const [exported, local] of localExports){
2975
+ const binding = bindings.get(local);
2976
+ if (binding?.kind === 'const') {
2977
+ exports.set(exported, binding.init);
2978
+ exportedFrom.set(exported, local);
2979
+ }
2980
+ }
2981
+ return {
2982
+ file,
2983
+ bindings,
2984
+ exports,
2985
+ exportedFrom,
2986
+ mutated: mutatedNames(ast)
2987
+ };
2988
+ }
2989
+ function isEnvObject(n) {
2990
+ if (!isMemberExpression(n) || n.computed || !isIdentifier(n.property, {
2991
+ name: 'env'
2992
+ })) return;
2993
+ if (isIdentifier(n.object, {
2994
+ name: 'process'
2995
+ })) return 'process.env';
2996
+ if (isMetaProperty(n.object)) return 'import.meta.env';
2997
+ }
2998
+ function envAccess(node) {
2999
+ for (const found of findAll(node, (n)=>isMemberExpression(n) || isMetaProperty(n))){
3000
+ if (isMemberExpression(found)) {
3001
+ const base = isEnvObject(found.object);
3002
+ if (base) {
3003
+ const key = isIdentifier(found.property) ? found.property.name : isStringLiteral(found.property) ? found.property.value : void 0;
3004
+ return key && (!found.computed || isStringLiteral(found.property)) ? `${base}.${key}` : base;
3005
+ }
3006
+ }
3007
+ const whole = isEnvObject(found);
3008
+ if (whole) return whole;
3009
+ }
3010
+ }
3011
+ function calleeName(node) {
3012
+ const n = evaluate_unwrap(node);
3013
+ if (isIdentifier(n)) return n.name;
3014
+ if (isMemberExpression(n) && isIdentifier(n.property)) return `${calleeName(n.object)}.${n.property.name}`;
3015
+ return 'a function';
3016
+ }
3017
+ const CONVERSIONS = new Set([
3018
+ 'Number',
3019
+ 'String',
3020
+ 'Boolean',
3021
+ 'parseInt',
3022
+ 'parseFloat'
3023
+ ]);
3024
+ function reasonFor(node) {
3025
+ const call = 'CallExpression' !== node.type || CONVERSIONS.has(calleeName(node.callee)) ? void 0 : calleeName(node.callee);
3026
+ if (call) return `comes from a function call (${call}())`;
3027
+ const env = envAccess(node);
3028
+ if (env) return `reads an env variable (${env})`;
3029
+ if (isCallExpression(node)) return `comes from a function call (${calleeName(node.callee)}())`;
3030
+ if (isNewExpression(node)) return `is built with new ${calleeName(node.callee)}()`;
3031
+ if (isArrowFunctionExpression(node) || isFunctionExpression(node)) return 'is a function';
3032
+ if (isMemberExpression(node)) return `is read from another value (${calleeName(node)})`;
3033
+ if (isConditionalExpression(node)) return 'depends on a condition (a ? b : c)';
3034
+ if (isLogicalExpression(node) || isBinaryExpression(node)) return `is computed with ${node.operator}`;
3035
+ if (isTemplateLiteral(node)) return 'is a template string with ${} parts';
3036
+ if (isAwaitExpression(node)) return 'is awaited';
3037
+ return `is not a literal (${node.type})`;
3038
+ }
3039
+ function identifierReason(name, binding) {
3040
+ if (!binding) return `refers to ${name}, which is not a const in this file`;
3041
+ if ('import' === binding.kind) return `is imported (${name} from '${binding.source}')`;
3042
+ return `refers to ${name}, which is ${'other' === binding.kind ? binding.what : 'not a const'}`;
3043
+ }
3044
+ function resolveNode(node, scope, seen = new Set()) {
3045
+ const n = evaluate_unwrap(node);
3046
+ if (!n) return evaluate_fail('is missing');
3047
+ if (!isIdentifier(n)) return ok(n);
3048
+ const binding = scope.bindings.get(n.name);
3049
+ if (binding?.kind === 'const' && scope.mutated.has(n.name)) return evaluate_fail(`refers to ${n.name}, which is changed later in the file`);
3050
+ if (binding?.kind !== 'const' || seen.has(n.name)) return evaluate_fail(seen.has(n.name) ? `refers to ${n.name}, which refers back to itself` : identifierReason(n.name, binding));
3051
+ return resolveNode(binding.init, scope, new Set([
3052
+ ...seen,
3053
+ n.name
3054
+ ]));
3055
+ }
3056
+ function evaluate_entriesOf(object, scope, seen = new Set()) {
3057
+ const entries = [];
3058
+ const problems = [];
3059
+ for (const prop of object.properties){
3060
+ if ('SpreadElement' === prop.type) {
3061
+ const arg = evaluate_unwrap(prop.argument);
3062
+ if (isIdentifier(arg)) {
3063
+ const binding = scope.bindings.get(arg.name);
3064
+ if (binding?.kind === 'import') {
3065
+ problems.push({
3066
+ reason: `spreads an imported object (${arg.name} from '${binding.source}')`
3067
+ });
3068
+ continue;
3069
+ }
3070
+ }
3071
+ const target = resolveNode(arg, scope, seen);
3072
+ if (target.ok) if ('ObjectExpression' === target.value.type) {
3073
+ const name = isIdentifier(arg) ? arg.name : '';
3074
+ const inner = evaluate_entriesOf(target.value, scope, new Set([
3075
+ ...seen,
3076
+ name
3077
+ ]));
3078
+ entries.push(...inner.entries);
3079
+ problems.push(...inner.problems);
3080
+ } else problems.push({
3081
+ reason: `spreads a value that ${reasonFor(target.value)}`
3082
+ });
3083
+ else problems.push({
3084
+ reason: `spreads a value that ${target.reason}`
3085
+ });
3086
+ continue;
3087
+ }
3088
+ const key = prop.computed ? void 0 : evaluate_keyName(prop.key);
3089
+ if (void 0 === key) {
3090
+ problems.push({
3091
+ reason: 'has a computed key'
3092
+ });
3093
+ continue;
3094
+ }
3095
+ if ('ObjectMethod' === prop.type) {
3096
+ problems.push({
3097
+ key,
3098
+ reason: 'is a method'
3099
+ });
3100
+ continue;
3101
+ }
3102
+ entries.push({
3103
+ key,
3104
+ value: prop.value
3105
+ });
3106
+ }
3107
+ return {
3108
+ entries,
3109
+ problems
3110
+ };
3111
+ }
3112
+ function evaluateArray(array, scope, seen) {
3113
+ const out = [];
3114
+ for (const element of array.elements){
3115
+ if (!element) return evaluate_fail('has an empty slot');
3116
+ if ('SpreadElement' === element.type) return evaluate_fail('spreads into an array');
3117
+ const item = evaluate(element, scope, seen);
3118
+ if (!item.ok) return item;
3119
+ out.push(item.value);
3120
+ }
3121
+ return ok(out);
3122
+ }
3123
+ function evaluate(node, scope, seen = new Set()) {
3124
+ const n = evaluate_unwrap(node);
3125
+ if (!n) return evaluate_fail('is missing');
3126
+ if (isStringLiteral(n) || isNumericLiteral(n) || isBooleanLiteral(n)) return ok(n.value);
3127
+ if (isNullLiteral(n)) return ok(null);
3128
+ if (isTemplateLiteral(n)) return 0 === n.expressions.length ? ok(n.quasis.map((q)=>q.value.cooked ?? q.value.raw).join('')) : evaluate_fail(reasonFor(n));
3129
+ if (isUnaryExpression(n)) return '-' === n.operator && isNumericLiteral(n.argument) ? ok(-n.argument.value) : evaluate_fail(reasonFor(n));
3130
+ if (isIdentifier(n)) {
3131
+ const target = resolveNode(n, scope, seen);
3132
+ return target.ok ? evaluate(target.value, scope, new Set([
3133
+ ...seen,
3134
+ n.name
3135
+ ])) : target;
3136
+ }
3137
+ if (isObjectExpression(n)) {
3138
+ const { entries, problems } = evaluate_entriesOf(n, scope, seen);
3139
+ const problem = problems[0];
3140
+ if (problem) return evaluate_fail(problem.key ? `${problem.key} ${problem.reason}` : problem.reason);
3141
+ const out = {};
3142
+ for (const entry of entries){
3143
+ const value = evaluate(entry.value, scope, seen);
3144
+ if (!value.ok) return evaluate_fail(`${entry.key} ${value.reason}`);
3145
+ out[entry.key] = value.value;
3146
+ }
3147
+ return ok(out);
3148
+ }
3149
+ if (isArrayExpression(n)) return evaluateArray(n, scope, seen);
3150
+ return evaluate_fail(reasonFor(n));
3151
+ }
3152
+ const MAX_FILE_BYTES = 1000000;
3153
+ function isInside(root, target) {
3154
+ const rel = node_path.relative(root, target);
3155
+ return '' === rel || '..' !== rel && !rel.startsWith(`..${node_path.sep}`) && !node_path.isAbsolute(rel);
3156
+ }
3157
+ const inNodeModules = (root, target)=>node_path.relative(root, target).split(node_path.sep).includes('node_modules');
3158
+ const isMissing = (error)=>hasErrno(error, 'ENOENT') || hasErrno(error, 'ENOTDIR');
3159
+ function codeOf(error) {
3160
+ return 'object' == typeof error && null !== error && 'code' in error ? String(error.code) : 'unknown error';
3161
+ }
3162
+ function guardedDisk(root, refused) {
3163
+ const shown = (file)=>node_path.relative(root, file).split(node_path.sep).join('/') || '.';
3164
+ const refuse = (file, reason)=>{
3165
+ if (!refused.some((r)=>r.file === shown(file) && r.reason === reason)) refused.push({
3166
+ file: shown(file),
3167
+ reason
3168
+ });
3169
+ };
3170
+ let realRoot;
3171
+ const rootReal = ()=>realRoot ??= promises.realpath(root);
3172
+ async function allowed(file) {
3173
+ if (inNodeModules(root, file)) return void refuse(file, 'is inside node_modules, which is never read');
3174
+ try {
3175
+ const [realFile, base] = await Promise.all([
3176
+ promises.realpath(file),
3177
+ rootReal()
3178
+ ]);
3179
+ if (!isInside(base, realFile)) return void refuse(file, 'is (or goes through) a link that leads outside the project');
3180
+ if (inNodeModules(base, realFile)) return void refuse(file, 'is (or goes through) a link into node_modules');
3181
+ return realFile;
3182
+ } catch (error) {
3183
+ if (!isMissing(error)) refuse(file, `could not be read (${codeOf(error)})`);
3184
+ return;
3185
+ }
3186
+ }
3187
+ return {
3188
+ read: async (file)=>{
3189
+ const real = await allowed(file);
3190
+ if (void 0 === real) return;
3191
+ try {
3192
+ const { size } = await promises.stat(real);
3193
+ if (size > MAX_FILE_BYTES) return void refuse(file, `is larger than ${String(MAX_FILE_BYTES / 1000000)} MB, so it is not read`);
3194
+ return await promises.readFile(real, 'utf8');
3195
+ } catch (error) {
3196
+ if (!isMissing(error)) refuse(file, `could not be read (${codeOf(error)})`);
3197
+ return;
3198
+ }
3199
+ },
3200
+ listDirs: async (dir)=>{
3201
+ const real = await allowed(dir);
3202
+ if (void 0 === real) return [];
3203
+ try {
3204
+ return (await promises.readdir(real, {
3205
+ withFileTypes: true
3206
+ })).filter((e)=>e.isDirectory()).map((e)=>e.name);
3207
+ } catch (error) {
3208
+ if (!isMissing(error)) refuse(dir, `could not be read (${codeOf(error)})`);
3209
+ return [];
3210
+ }
3211
+ }
3212
+ };
3213
+ }
3214
+ const fieldOf = (base, key)=>/^[A-Za-z_$][\w$]*$/.test(key) ? `${base}.${key}` : `${base}['${key}']`;
3215
+ const isRecord = (value)=>'object' == typeof value && null !== value && !Array.isArray(value);
3216
+ function parseRemote(value) {
3217
+ if (value.startsWith('promise ')) return evaluate_fail('is loaded by inline code (promise …)');
3218
+ const match = /^([^@/:\s]+)@(.+)$/.exec(value);
3219
+ const entry = match?.[2] ?? value;
3220
+ if (!entry.includes('/')) return evaluate_fail(`has no URL ("${value}")`);
3221
+ const type = /\.json([?#]|$)/.test(entry) ? 'manifest' : "script";
3222
+ return ok(match?.[1] ? {
3223
+ name: match[1],
3224
+ entry,
3225
+ type
3226
+ } : {
3227
+ entry,
3228
+ type
3229
+ });
3230
+ }
3231
+ function objectAt(value, scope, field, report) {
3232
+ const target = resolveNode(value, scope);
3233
+ if (!target.ok) return void report(field, target.reason);
3234
+ if ('ObjectExpression' !== target.value.type) {
3235
+ const evaluated = evaluate(target.value, scope);
3236
+ report(field, evaluated.ok ? 'is not an object' : evaluated.reason);
3237
+ return;
3238
+ }
3239
+ const { entries, problems } = evaluate_entriesOf(target.value, scope);
3240
+ for (const problem of problems)report(problem.key ? fieldOf(field, problem.key) : field, problem.reason);
3241
+ return entries;
3242
+ }
3243
+ function readRemotes(value, scope, report) {
3244
+ const remotes = {};
3245
+ for (const { key, value: node } of objectAt(value, scope, 'remotes', report) ?? []){
3246
+ const field = fieldOf('remotes', key);
3247
+ const evaluated = evaluate(node, scope);
3248
+ if (!evaluated.ok) {
3249
+ report(field, evaluated.reason);
3250
+ continue;
3251
+ }
3252
+ const raw = evaluated.value;
3253
+ const text = isRecord(raw) ? raw['external'] : raw;
3254
+ if ('string' != typeof text) {
3255
+ report(field, isRecord(raw) ? 'has no string external' : 'is not a string');
3256
+ continue;
3257
+ }
3258
+ const remote = parseRemote(text);
3259
+ if (remote.ok) remotes[key] = remote.value;
3260
+ else report(field, remote.reason);
3261
+ }
3262
+ return remotes;
3263
+ }
3264
+ function readExposes(value, scope, report) {
3265
+ const exposes = {};
3266
+ const target = resolveNode(value, scope);
3267
+ if (target.ok && 'ArrayExpression' === target.value.type) {
3268
+ const evaluated = evaluate(target.value, scope);
3269
+ if (evaluated.ok) if (Array.isArray(evaluated.value)) for (const item of evaluated.value)if ('string' == typeof item) exposes[item] = item;
3270
+ else report('exposes', 'is an array with items that are not strings');
3271
+ else report('exposes', 'is not a list');
3272
+ else report('exposes', evaluated.reason);
3273
+ return exposes;
3274
+ }
3275
+ for (const { key, value: node } of objectAt(value, scope, 'exposes', report) ?? []){
3276
+ const field = fieldOf('exposes', key);
3277
+ const evaluated = evaluate(node, scope);
3278
+ if (!evaluated.ok) {
3279
+ report(field, evaluated.reason);
3280
+ continue;
3281
+ }
3282
+ const raw = evaluated.value;
3283
+ const path = isRecord(raw) ? raw['import'] : raw;
3284
+ if ('string' == typeof path) exposes[key] = path;
3285
+ else report(field, 'is not a module path');
3286
+ }
3287
+ return exposes;
3288
+ }
3289
+ const NOT_CARRIED = 'is set in the config but adopt does not carry it over';
3290
+ const NOT_CARRIED_SHARED = new Set([
3291
+ 'eager',
3292
+ 'strictVersion',
3293
+ 'import'
3294
+ ]);
3295
+ const NOT_CARRIED_OPTIONS = new Set([
3296
+ 'filename',
3297
+ 'shareStrategy',
3298
+ 'runtimePlugins'
3299
+ ]);
3300
+ function readSharedEntry(key, node, scope, report) {
3301
+ const field = fieldOf('shared', key);
3302
+ const target = resolveNode(node, scope);
3303
+ if (!target.ok) return void report(field, target.reason);
3304
+ if ('ObjectExpression' !== target.value.type) {
3305
+ const evaluated = evaluate(target.value, scope);
3306
+ if (evaluated.ok) {
3307
+ if ('string' == typeof evaluated.value) return {
3308
+ requiredVersion: evaluated.value
3309
+ };
3310
+ report(field, 'is not a version range or an options object');
3311
+ } else report(field, evaluated.reason);
3312
+ return;
3313
+ }
3314
+ const shared = {};
3315
+ const { entries, problems } = evaluate_entriesOf(target.value, scope);
3316
+ for (const problem of problems)report(problem.key ? fieldOf(field, problem.key) : field, problem.reason);
3317
+ for (const entry of entries){
3318
+ const at = fieldOf(field, entry.key);
3319
+ if (NOT_CARRIED_SHARED.has(entry.key)) {
3320
+ const evaluated = evaluate(entry.value, scope);
3321
+ const plainlyOff = 'import' !== entry.key && evaluated.ok && false === evaluated.value;
3322
+ if (!plainlyOff) report(at, NOT_CARRIED);
3323
+ continue;
3324
+ }
3325
+ if ('singleton' !== entry.key && 'requiredVersion' !== entry.key) continue;
3326
+ const evaluated = evaluate(entry.value, scope);
3327
+ if (evaluated.ok) if ('singleton' === entry.key && 'boolean' == typeof evaluated.value) shared.singleton = evaluated.value;
3328
+ else if ('requiredVersion' === entry.key && ('string' == typeof evaluated.value || false === evaluated.value)) shared.requiredVersion = evaluated.value;
3329
+ else report(at, 'singleton' === entry.key ? 'is not true or false' : 'is not a version range');
3330
+ else report(at, evaluated.reason);
3331
+ }
3332
+ return shared;
3333
+ }
3334
+ function readShared(value, scope, report) {
3335
+ const shared = {};
3336
+ const target = resolveNode(value, scope);
3337
+ if (target.ok && 'ArrayExpression' === target.value.type) {
3338
+ for (const [index, element] of target.value.elements.entries()){
3339
+ const evaluated = evaluate(element, scope);
3340
+ if (evaluated.ok && 'string' == typeof evaluated.value) shared[evaluated.value] = {};
3341
+ else report(`shared[${String(index)}]`, evaluated.ok ? 'is not a package name' : evaluated.reason);
3342
+ }
3343
+ return shared;
3344
+ }
3345
+ for (const { key, value: node } of objectAt(value, scope, 'shared', report) ?? []){
3346
+ const entry = readSharedEntry(key, node, scope, report);
3347
+ if (entry) shared[key] = entry;
3348
+ }
3349
+ return shared;
3350
+ }
3351
+ function readFederationOptions(options, scope, report) {
3352
+ const result = {
3353
+ remotes: {},
3354
+ exposes: {},
3355
+ shared: {},
3356
+ exposing: false
3357
+ };
3358
+ const { entries, problems } = evaluate_entriesOf(options, scope);
3359
+ for (const problem of problems)report(problem.key ?? 'options', problem.reason);
3360
+ for (const { key, value } of entries)switch(key){
3361
+ case 'name':
3362
+ {
3363
+ const evaluated = evaluate(value, scope);
3364
+ if (evaluated.ok && 'string' == typeof evaluated.value) result.name = evaluated.value;
3365
+ else report('name', evaluated.ok ? 'is not a string' : evaluated.reason);
3366
+ break;
3367
+ }
3368
+ case 'remotes':
3369
+ result.remotes = readRemotes(value, scope, report);
3370
+ break;
3371
+ case 'exposes':
3372
+ {
3373
+ let unread = false;
3374
+ result.exposes = readExposes(value, scope, (field, reason)=>{
3375
+ unread = true;
3376
+ report(field, reason);
3377
+ });
3378
+ result.exposing = unread || Object.keys(result.exposes).length > 0;
3379
+ break;
3380
+ }
3381
+ case 'shared':
3382
+ result.shared = readShared(value, scope, report);
3383
+ break;
3384
+ default:
3385
+ if (NOT_CARRIED_OPTIONS.has(key)) report(key, NOT_CARRIED);
3386
+ break;
3387
+ }
3388
+ return result;
3389
+ }
3390
+ const SKIPPED_DIRS = new Set([
3391
+ 'node_modules',
3392
+ 'dist',
3393
+ 'build',
3394
+ 'out',
3395
+ 'output',
3396
+ 'coverage',
3397
+ 'doc_build',
3398
+ 'storybook-static'
3399
+ ]);
3400
+ const workspace_MAX_DEPTH = 8;
3401
+ const MAX_VISITED = 5000;
3402
+ const workspace_skipped = (name)=>name.startsWith('.') || SKIPPED_DIRS.has(name) || name.startsWith('dist-');
3403
+ function toRegExp(segment) {
3404
+ const escaped = segment.replace(/[.+^${}()|[\]\\?]/g, String.raw`\$&`).replaceAll('*', '[^/]*');
3405
+ return new RegExp(`^${escaped}$`);
3406
+ }
3407
+ function globToRegExp(pattern) {
3408
+ const body = pattern.split('/').map((segment)=>'**' === segment ? '\0' : toRegExp(segment).source.slice(1, -1)).join('/').replaceAll('\0/', '(?:[^/]+/)*').replaceAll('/\0', '(?:/[^/]+)*').replaceAll('\0', '.*');
3409
+ return new RegExp(`^${body}$`);
3410
+ }
3411
+ function normalize(pattern) {
3412
+ const clean = pattern.replaceAll('\\', '/').replace(/^\.\//, '').replace(/\/+$/, '');
3413
+ if ('' === clean || clean.startsWith('/') || clean.split('/').includes('..')) return;
3414
+ return clean;
3415
+ }
3416
+ function eachInSequence(items, fn) {
3417
+ return items.reduce((done, item)=>done.then(()=>fn(item)), Promise.resolve());
3418
+ }
3419
+ const collapse = (segments)=>segments.filter((segment, i)=>!('**' === segment && '**' === segments[i - 1]));
3420
+ async function childrenOf(walk, at) {
3421
+ walk.visited += 1;
3422
+ if (walk.visited > MAX_VISITED) return [];
3423
+ try {
3424
+ return (await walk.listDirs(node_path.join(walk.root, ...at))).filter((name)=>!workspace_skipped(name)).toSorted((a, b)=>a.localeCompare(b));
3425
+ } catch (error) {
3426
+ walk.problems.push({
3427
+ file: at.join('/') || '.',
3428
+ reason: `could not be listed (${messageOf(error)})`
3429
+ });
3430
+ return [];
3431
+ }
3432
+ }
3433
+ async function expand(walk, segments, at = []) {
3434
+ const state = `${at.join('/')}|${segments.join('/')}`;
3435
+ if (walk.seen.has(state)) return;
3436
+ walk.seen.add(state);
3437
+ if (0 === segments.length) return void walk.found.add(at.join('/'));
3438
+ if (at.length > workspace_MAX_DEPTH) return;
3439
+ const [segment = '', ...rest] = segments;
3440
+ if (!segment.includes('*')) {
3441
+ if (!workspace_skipped(segment)) await expand(walk, rest, [
3442
+ ...at,
3443
+ segment
3444
+ ]);
3445
+ return;
3446
+ }
3447
+ const children = await childrenOf(walk, at);
3448
+ if ('**' === segment) {
3449
+ await expand(walk, rest, at);
3450
+ await eachInSequence(children, (child)=>expand(walk, segments, [
3451
+ ...at,
3452
+ child
3453
+ ]));
3454
+ return;
3455
+ }
3456
+ const match = toRegExp(segment);
3457
+ await eachInSequence(children.filter((c)=>match.test(c)), (child)=>expand(walk, rest, [
3458
+ ...at,
3459
+ child
3460
+ ]));
3461
+ }
3462
+ function stringList(value) {
3463
+ return Array.isArray(value) && value.every((v)=>'string' == typeof v) ? value : void 0;
3464
+ }
3465
+ async function safeRead(read, file, problems) {
3466
+ try {
3467
+ return await read(file);
3468
+ } catch (error) {
3469
+ problems.push({
3470
+ file: node_path.basename(file),
3471
+ reason: `could not be read (${messageOf(error)})`
3472
+ });
3473
+ return;
3474
+ }
3475
+ }
3476
+ async function workspacePatterns(root, read, problems) {
3477
+ const patterns = [];
3478
+ const yamlText = await safeRead(read, node_path.join(root, 'pnpm-workspace.yaml'), problems);
3479
+ if (void 0 !== yamlText) try {
3480
+ const doc = external_yaml_parse(yamlText);
3481
+ const list = 'object' == typeof doc && null !== doc && 'packages' in doc ? stringList(doc.packages) : [];
3482
+ if (list) patterns.push(...list);
3483
+ else problems.push({
3484
+ file: 'pnpm-workspace.yaml',
3485
+ reason: 'packages is not a list'
3486
+ });
3487
+ } catch {
3488
+ problems.push({
3489
+ file: 'pnpm-workspace.yaml',
3490
+ reason: 'could not be parsed'
3491
+ });
3492
+ }
3493
+ const pkgText = await safeRead(read, node_path.join(root, 'package.json'), problems);
3494
+ if (void 0 !== pkgText) try {
3495
+ const pkg = JSON.parse(pkgText);
3496
+ const workspaces = 'object' == typeof pkg && null !== pkg && 'workspaces' in pkg ? pkg.workspaces : void 0;
3497
+ const list = void 0 === workspaces ? [] : stringList(workspaces) ?? ('object' == typeof workspaces && null !== workspaces && 'packages' in workspaces ? stringList(workspaces.packages) : void 0);
3498
+ if (list) patterns.push(...list);
3499
+ else problems.push({
3500
+ file: 'package.json',
3501
+ reason: 'workspaces is not a list'
3502
+ });
3503
+ } catch {
3504
+ problems.push({
3505
+ file: 'package.json',
3506
+ reason: 'could not be parsed'
3507
+ });
3508
+ }
3509
+ return patterns;
3510
+ }
3511
+ async function findAppDirs(root, read, listDirs, problems = []) {
3512
+ const patterns = await workspacePatterns(root, read, problems);
3513
+ const include = patterns.filter((p)=>!p.startsWith('!')).map(normalize);
3514
+ const exclude = patterns.filter((p)=>p.startsWith('!')).map((p)=>normalize(p.slice(1))).filter((p)=>void 0 !== p).map(globToRegExp);
3515
+ const walk = {
3516
+ root,
3517
+ listDirs,
3518
+ problems,
3519
+ visited: 0,
3520
+ seen: new Set(),
3521
+ found: new Set()
3522
+ };
3523
+ await eachInSequence(include, async (pattern)=>{
3524
+ if (void 0 !== pattern) await expand(walk, collapse(pattern.split('/')));
3525
+ });
3526
+ if (walk.visited > MAX_VISITED) problems.push({
3527
+ file: '.',
3528
+ reason: `has more than ${String(MAX_VISITED)} folders to search, so the workspace globs were cut short`
3529
+ });
3530
+ const candidates = [
3531
+ ...walk.found
3532
+ ].filter((dir)=>'' !== dir && !exclude.some((re)=>re.test(dir)));
3533
+ const withPackage = [];
3534
+ await eachInSequence(candidates, async (dir)=>{
3535
+ const file = node_path.join(root, ...dir.split('/'), 'package.json');
3536
+ withPackage.push(await safeRead(read, file, problems) === void 0 ? void 0 : dir);
3537
+ });
3538
+ const dirs = withPackage.filter((d)=>void 0 !== d);
3539
+ return [
3540
+ '.',
3541
+ ...dirs.toSorted((a, b)=>a.localeCompare(b))
3542
+ ];
3543
+ }
3544
+ const EXTENSIONS = [
3545
+ '.ts',
3546
+ '.mts',
3547
+ '.cts',
3548
+ '.js',
3549
+ '.mjs',
3550
+ '.cjs'
3551
+ ];
3552
+ const BUILD_TOOLS = [
3553
+ 'rsbuild',
3554
+ 'rspack',
3555
+ 'webpack'
3556
+ ];
3557
+ const FEDERATION_CONFIG = 'module-federation.config';
3558
+ const MAX_IMPORT_HOPS = 3;
3559
+ const detect_relative = (root, file)=>node_path.relative(root, file).split(node_path.sep).join('/') || '.';
3560
+ function detect_report(ctx, file, field, reason) {
3561
+ ctx.notAdopted.push({
3562
+ dir: ctx.dir,
3563
+ file: detect_relative(ctx.root, file),
3564
+ field,
3565
+ reason
3566
+ });
3567
+ }
3568
+ async function firstExisting(read, candidates) {
3569
+ const sources = await Promise.all(candidates.map((file)=>read(file)));
3570
+ const index = sources.findIndex((s)=>void 0 !== s);
3571
+ const source = sources[index];
3572
+ const file = candidates[index];
3573
+ return void 0 === source || void 0 === file ? void 0 : {
3574
+ file,
3575
+ source
3576
+ };
3577
+ }
3578
+ function load(ctx, file, source) {
3579
+ try {
3580
+ const ast = parseSource(source, file);
3581
+ return {
3582
+ ast,
3583
+ scope: scopeOf(ast, file)
3584
+ };
3585
+ } catch {
3586
+ detect_report(ctx, file, 'file', 'could not be parsed');
3587
+ return;
3588
+ }
3589
+ }
3590
+ function namesFor(scope, canonical) {
3591
+ const names = new Set([
3592
+ canonical
3593
+ ]);
3594
+ for (const [local, binding] of scope.bindings)if ('import' === binding.kind && binding.imported === canonical) names.add(local);
3595
+ return names;
3596
+ }
3597
+ function patternNames(node, out) {
3598
+ if (!node) return;
3599
+ if (isIdentifier(node)) out.add(node.name);
3600
+ else if ('ObjectPattern' === node.type) for (const prop of node.properties)patternNames('RestElement' === prop.type ? prop.argument : prop.value, out);
3601
+ else if ('ArrayPattern' === node.type) for (const element of node.elements)patternNames(element, out);
3602
+ else if ('RestElement' === node.type) patternNames(node.argument, out);
3603
+ else if ('AssignmentPattern' === node.type) patternNames(node.left, out);
3604
+ }
3605
+ const isFunction = (n)=>'FunctionDeclaration' === n.type || 'FunctionExpression' === n.type || 'ArrowFunctionExpression' === n.type || 'ObjectMethod' === n.type || 'ClassMethod' === n.type;
3606
+ const isLocalScope = (n)=>isFunction(n) || 'BlockStatement' === n.type || 'CatchClause' === n.type || 'ForStatement' === n.type || 'ForInStatement' === n.type || 'ForOfStatement' === n.type || 'SwitchStatement' === n.type;
3607
+ function declaredInside(scope) {
3608
+ const names = new Set();
3609
+ if ('params' in scope) for (const param of scope.params)patternNames(param, names);
3610
+ if ('CatchClause' === scope.type) patternNames(scope.param, names);
3611
+ for (const node of findAll(scope, (n)=>'VariableDeclarator' === n.type || 'FunctionDeclaration' === n.type || 'ClassDeclaration' === n.type))if ('VariableDeclarator' === node.type) patternNames(node.id, names);
3612
+ else if ('id' in node) patternNames(node.id, names);
3613
+ return names;
3614
+ }
3615
+ function federationCalls(ast, scope) {
3616
+ const plugin = namesFor(scope, 'pluginModuleFederation');
3617
+ const webpack = namesFor(scope, 'ModuleFederationPlugin');
3618
+ const found = [];
3619
+ const matches = (n, shadowed)=>{
3620
+ if (isCallExpression(n)) {
3621
+ const callee = n.callee;
3622
+ if (isIdentifier(callee)) return plugin.has(callee.name) && !shadowed.has(callee.name);
3623
+ return isMemberExpression(callee) && isIdentifier(callee.property, {
3624
+ name: 'pluginModuleFederation'
3625
+ });
3626
+ }
3627
+ if (!isNewExpression(n)) return false;
3628
+ const callee = n.callee;
3629
+ if (isIdentifier(callee)) return webpack.has(callee.name) && !shadowed.has(callee.name);
3630
+ return isMemberExpression(callee) && isIdentifier(callee.property, {
3631
+ name: 'ModuleFederationPlugin'
3632
+ });
3633
+ };
3634
+ const visit = (value, shadowed)=>{
3635
+ if (Array.isArray(value)) {
3636
+ for (const item of value)visit(item, shadowed);
3637
+ return;
3638
+ }
3639
+ if (!isNode(value)) return;
3640
+ if (matches(value, shadowed) && (isCallExpression(value) || isNewExpression(value))) found.push({
3641
+ node: value,
3642
+ shadowed
3643
+ });
3644
+ const inner = 'Program' !== value.type && isLocalScope(value) ? new Set([
3645
+ ...shadowed,
3646
+ ...declaredInside(value)
3647
+ ]) : shadowed;
3648
+ for (const [key, child] of Object.entries(value))if (!SKIP_KEYS.has(key)) visit(child, inner);
3649
+ };
3650
+ visit(ast.program, new Set());
3651
+ return found;
3652
+ }
3653
+ function scopeAt(scope, shadowed) {
3654
+ if (![
3655
+ ...shadowed
3656
+ ].some((name)=>scope.bindings.has(name))) return scope;
3657
+ const bindings = new Map(scope.bindings);
3658
+ for (const name of shadowed)if (bindings.has(name)) bindings.set(name, {
3659
+ kind: 'other',
3660
+ what: 'declared again inside a function around the call'
3661
+ });
3662
+ return {
3663
+ ...scope,
3664
+ bindings
3665
+ };
3666
+ }
3667
+ const isRelative = (source)=>'.' === source || '..' === source || source.startsWith('./') || source.startsWith('../');
3668
+ function candidatesFor(target) {
3669
+ const ext = node_path.extname(target);
3670
+ if (EXTENSIONS.includes(ext)) {
3671
+ const stem = target.slice(0, -ext.length);
3672
+ return [
3673
+ target,
3674
+ ...'.js' === ext ? [
3675
+ `${stem}.ts`
3676
+ ] : '.mjs' === ext ? [
3677
+ `${stem}.mts`
3678
+ ] : []
3679
+ ];
3680
+ }
3681
+ return [
3682
+ ...EXTENSIONS.map((e)=>`${target}${e}`),
3683
+ ...EXTENSIONS.map((e)=>node_path.join(target, `index${e}`))
3684
+ ];
3685
+ }
3686
+ async function detect_resolveOptions(ctx, node, scope, hops = 0, seen = new Set()) {
3687
+ const n = evaluate_unwrap(node);
3688
+ if (!n) return evaluate_fail('are missing');
3689
+ if ('ObjectExpression' === n.type) return ok({
3690
+ object: n,
3691
+ scope
3692
+ });
3693
+ if (isCallExpression(n)) {
3694
+ const wrapper = namesFor(scope, 'createModuleFederationConfig');
3695
+ if (isIdentifier(n.callee) && wrapper.has(n.callee.name)) return detect_resolveOptions(ctx, n.arguments[0], scope, hops, seen);
3696
+ return evaluate_fail(reasonFor(n));
3697
+ }
3698
+ if (!isIdentifier(n)) {
3699
+ const evaluated = evaluate(n, scope);
3700
+ return evaluate_fail(evaluated.ok ? 'are not an object' : evaluated.reason);
3701
+ }
3702
+ const binding = scope.bindings.get(n.name);
3703
+ if (binding?.kind === 'const' && !seen.has(n.name)) {
3704
+ if (scope.mutated.has(n.name)) return evaluate_fail(`refers to ${n.name}, which is changed later in the file`);
3705
+ return detect_resolveOptions(ctx, binding.init, scope, hops, new Set([
3706
+ ...seen,
3707
+ n.name
3708
+ ]));
3709
+ }
3710
+ if (binding?.kind !== 'import') {
3711
+ const evaluated = evaluate(n, scope);
3712
+ return evaluate_fail(evaluated.ok ? 'are not an object' : evaluated.reason);
3713
+ }
3714
+ if (!isRelative(binding.source)) return evaluate_fail(`are imported from a package (${n.name} from '${binding.source}')`);
3715
+ if ((binding.depth ?? 0) > 1) return evaluate_fail(`are read from deep inside what '${binding.source}' exports`);
3716
+ if (hops >= MAX_IMPORT_HOPS) return evaluate_fail(`are imported through more than ${String(MAX_IMPORT_HOPS)} files`);
3717
+ const target = node_path.resolve(node_path.dirname(scope.file), binding.source);
3718
+ if (!isInside(ctx.root, target)) return evaluate_fail(`are imported from outside the project ('${binding.source}')`);
3719
+ if (node_path.relative(ctx.root, target).split(node_path.sep).includes('node_modules')) return evaluate_fail(`are imported from node_modules ('${binding.source}')`);
3720
+ const found = await firstExisting(ctx.read, candidatesFor(target));
3721
+ if (!found) return evaluate_fail(`are imported from '${binding.source}', which was not found`);
3722
+ const imported = load(ctx, found.file, found.source)?.scope;
3723
+ if (!imported) return evaluate_fail(`are imported from '${binding.source}', which could not be parsed`);
3724
+ const exported = '*' === binding.imported ? void 0 : imported.exports.get(binding.imported);
3725
+ if (!exported) return evaluate_fail('default' === binding.imported ? `are imported from '${binding.source}', which has no default export` : `are imported from '${binding.source}', which has no export ${binding.imported}`);
3726
+ const local = imported.exportedFrom.get(binding.imported);
3727
+ if (local && imported.mutated.has(local)) return evaluate_fail(`are imported from '${binding.source}', where ${local} is changed later in the file`);
3728
+ return detect_resolveOptions(ctx, exported, imported, hops + 1);
3729
+ }
3730
+ function readPort(ctx, ast, scope, tool) {
3731
+ const key = 'rsbuild' === tool ? 'server' : 'devServer';
3732
+ const ports = [];
3733
+ for (const prop of findAll(ast, (n)=>isObjectProperty(n))){
3734
+ if (!isObjectProperty(prop) || prop.computed || !isIdentifier(prop.key, {
3735
+ name: key
3736
+ })) continue;
3737
+ const value = evaluate_unwrap(prop.value);
3738
+ if (value?.type !== 'ObjectExpression') continue;
3739
+ const port = evaluate_entriesOf(value, scope).entries.findLast((e)=>'port' === e.key);
3740
+ if (port) ports.push(port.value);
3741
+ }
3742
+ const [first, ...others] = ports;
3743
+ if (!first) return;
3744
+ if (others.length > 0) return void detect_report(ctx, scope.file, 'port', 'is set in more than one place');
3745
+ const evaluated = evaluate(first, scope);
3746
+ if (evaluated.ok && 'number' == typeof evaluated.value) return evaluated.value;
3747
+ detect_report(ctx, scope.file, 'port', evaluated.ok ? 'is not a number' : evaluated.reason);
3748
+ }
3749
+ async function packageName(read, dir) {
3750
+ const text = await read(node_path.join(dir, 'package.json'));
3751
+ if (void 0 === text) return;
3752
+ try {
3753
+ const pkg = JSON.parse(text);
3754
+ return 'object' == typeof pkg && null !== pkg && 'name' in pkg && 'string' == typeof pkg.name ? pkg.name : void 0;
3755
+ } catch {
3756
+ return;
3757
+ }
3758
+ }
3759
+ async function detectApp(ctx) {
3760
+ const abs = node_path.join(ctx.root, ...ctx.dir.split('/'));
3761
+ const builds = BUILD_TOOLS.flatMap((tool)=>EXTENSIONS.map((ext)=>({
3762
+ tool,
3763
+ file: node_path.join(abs, `${tool}.config${ext}`)
3764
+ })));
3765
+ const [build, federation] = await Promise.all([
3766
+ firstExisting(ctx.read, builds.map((b)=>b.file)),
3767
+ firstExisting(ctx.read, EXTENSIONS.map((ext)=>node_path.join(abs, `${FEDERATION_CONFIG}${ext}`)))
3768
+ ]);
3769
+ if (!build) {
3770
+ if (federation) detect_report(ctx, federation.file, 'buildTool', 'has no rsbuild, rspack or webpack config next to it');
3771
+ return;
3772
+ }
3773
+ const tool = builds.find((b)=>b.file === build.file)?.tool ?? 'rsbuild';
3774
+ const parsed = load(ctx, build.file, build.source);
3775
+ if (!parsed) return;
3776
+ const { ast, scope } = parsed;
3777
+ const calls = federationCalls(ast, scope);
3778
+ if (calls.length > 1) return void detect_report(ctx, build.file, 'options', `has ${String(calls.length)} Module Federation plugins in one config, so none of them is read`);
3779
+ let options;
3780
+ const [call] = calls;
3781
+ if (call) options = await detect_resolveOptions(ctx, call.node.arguments[0], scopeAt(scope, call.shadowed));
3782
+ else {
3783
+ if (!federation) return;
3784
+ const mfScope = load(ctx, federation.file, federation.source)?.scope;
3785
+ if (!mfScope) return;
3786
+ options = await detect_resolveOptions(ctx, mfScope.exports.get('default'), mfScope);
3787
+ }
3788
+ if (!options.ok) return void detect_report(ctx, build.file, 'options', options.reason);
3789
+ const optionsFile = options.value.scope.file;
3790
+ const read = readFederationOptions(options.value.object, options.value.scope, (field, reason)=>{
3791
+ detect_report(ctx, optionsFile, field, reason);
3792
+ });
3793
+ const port = readPort(ctx, ast, scope, tool);
3794
+ const pkgName = await packageName(ctx.read, abs);
3795
+ return {
3796
+ dir: ctx.dir,
3797
+ ...void 0 === read.name ? {} : {
3798
+ name: read.name
3799
+ },
3800
+ ...void 0 === pkgName ? {} : {
3801
+ packageName: pkgName
3802
+ },
3803
+ role: read.exposing ? 'remote' : 'shell',
3804
+ buildTool: tool,
3805
+ configFile: detect_relative(ctx.root, build.file),
3806
+ ...optionsFile === build.file ? {} : {
3807
+ federationFile: detect_relative(ctx.root, optionsFile)
3808
+ },
3809
+ remotes: read.remotes,
3810
+ exposes: read.exposes,
3811
+ shared: read.shared,
3812
+ ...void 0 === port ? {} : {
3813
+ port
3814
+ }
3815
+ };
3816
+ }
3817
+ async function detectFederation(dir, options = {}) {
3818
+ const root = node_path.resolve(dir);
3819
+ const refused = [];
3820
+ const disk = guardedDisk(root, refused);
3821
+ const read = options.read ?? disk.read;
3822
+ const listDirs = options.listDirs ?? disk.listDirs;
3823
+ const notAdopted = [];
3824
+ const problems = [];
3825
+ const dirs = await findAppDirs(root, read, listDirs, problems);
3826
+ for (const problem of problems)notAdopted.push({
3827
+ dir: '.',
3828
+ file: problem.file,
3829
+ field: 'workspaces',
3830
+ reason: problem.reason
3831
+ });
3832
+ const apps = [];
3833
+ await eachInSequence(dirs, async (appDir)=>{
3834
+ const ctx = {
3835
+ root,
3836
+ read,
3837
+ dir: appDir,
3838
+ notAdopted: []
3839
+ };
3840
+ try {
3841
+ const app = await detectApp(ctx);
3842
+ if (app) apps.push(app);
3843
+ } catch (error) {
3844
+ detect_report(ctx, node_path.join(root, ...appDir.split('/')), 'file', `could not be read (${messageOf(error)})`);
3845
+ }
3846
+ notAdopted.push(...ctx.notAdopted);
3847
+ });
3848
+ for (const refusal of refused)notAdopted.push({
3849
+ dir: '.',
3850
+ file: refusal.file,
3851
+ field: 'file',
3852
+ reason: refusal.reason
3853
+ });
3854
+ return {
3855
+ apps,
3856
+ notAdopted
3857
+ };
3858
+ }
3859
+ const RESERVED = new Set([
3860
+ 'root',
3861
+ 'remotes'
3862
+ ]);
3863
+ const DEV_PORT_START = 4311;
3864
+ const OWNER_PLACEHOLDER = 'unassigned';
3865
+ const CONTROL = new RegExp(String.raw`[\u0000-\u001f\u007f-\u009f]`, 'g');
3866
+ const printable = (text)=>text.replaceAll(CONTROL, '?');
3867
+ const join = (dir, file)=>'.' === dir ? file : `${dir}/${file}`;
3868
+ function localPort(entry) {
3869
+ try {
3870
+ const url = new URL(entry);
3871
+ const local = 'localhost' === url.hostname || '127.0.0.1' === url.hostname;
3872
+ const port = Number(url.port);
3873
+ return local && Number.isInteger(port) && port > 0 ? port : void 0;
3874
+ } catch {
3875
+ return;
3876
+ }
3877
+ }
3878
+ function skip(app, field, reason) {
3879
+ return {
3880
+ dir: app.dir,
3881
+ file: app.federationFile ?? app.configFile,
3882
+ field,
3883
+ reason
3884
+ };
3885
+ }
3886
+ function nameApps(apps, notAdopted) {
3887
+ const taken = new Set();
3888
+ const named = [];
3889
+ for (const app of apps){
3890
+ const { name } = app;
3891
+ let problem;
3892
+ if (void 0 === name) problem = 'has no literal name, so there is nothing to call this app';
3893
+ else if (MFE_NAME.test(name)) {
3894
+ if (RESERVED.has(name) || 'remote' === app.role && 'shell' === name) problem = `"${name}" is a name mfeflow keeps for itself`;
3895
+ else if (taken.has(name)) problem = `"${name}" is already used by another app`;
3896
+ } else problem = `"${name}" is not a name mfeflow accepts (lowercase letters, digits and single _)`;
3897
+ if (void 0 === name || void 0 !== problem) {
3898
+ notAdopted.push(skip(app, 'name', `${problem ?? ''}; the app was left out`));
3899
+ continue;
3900
+ }
3901
+ taken.add(name);
3902
+ named.push({
3903
+ app,
3904
+ name
3905
+ });
3906
+ }
3907
+ return named;
3908
+ }
3909
+ function exposesOf(app, notAdopted) {
3910
+ const kept = [];
3911
+ for (const key of Object.keys(app.exposes))if (EXPOSE.test(key)) kept.push(key);
3912
+ else notAdopted.push(skip(app, `exposes['${key}']`, 'is not a ./Name path, which mfeflow.app.json requires'));
3913
+ return kept;
3914
+ }
3915
+ function platformOf(app) {
3916
+ const platform = {};
3917
+ for (const [pkg, shared] of Object.entries(app.shared))if ('string' == typeof shared.requiredVersion) platform[pkg] = shared.requiredVersion;
3918
+ return platform;
3919
+ }
3920
+ function shellsLoading(name, shells) {
3921
+ return shells.filter(({ app })=>Object.entries(app.remotes).some(([alias, remote])=>(remote.name ?? alias) === name)).map((shell)=>shell.name);
3922
+ }
3923
+ function portFromShells(name, shells) {
3924
+ return shells.flatMap((shell)=>Object.entries(shell.app.remotes)).filter(([alias, r])=>(r.name ?? alias) === name).map(([, r])=>localPort(r.entry)).find((port)=>void 0 !== port);
3925
+ }
3926
+ function assignPorts(named, shells, warnings) {
3927
+ const claims = new Map();
3928
+ const claim = (port, name)=>{
3929
+ claims.set(port, [
3930
+ ...claims.get(port) ?? [],
3931
+ name
3932
+ ]);
3933
+ };
3934
+ const ports = new Map();
3935
+ const portless = [];
3936
+ for (const { app, name } of named){
3937
+ const port = app.port ?? ('remote' === app.role ? portFromShells(name, shells) : void 0);
3938
+ if (void 0 === port) {
3939
+ if ('remote' === app.role) portless.push(name);
3940
+ continue;
3941
+ }
3942
+ claim(port, name);
3943
+ if ('remote' === app.role) ports.set(name, port);
3944
+ }
3945
+ for (const [port, names] of claims)if (names.length > 1) warnings.push(`${names.join(', ')} are all on port ${String(port)}; give each app its own`);
3946
+ let next = DEV_PORT_START;
3947
+ for (const name of portless){
3948
+ while(claims.has(next))next += 1;
3949
+ claim(next, name);
3950
+ ports.set(name, next);
3951
+ warnings.push(`${name}: no dev port found in its config, so its dev registry entry uses ${String(next)}; change it if the app runs elsewhere`);
3952
+ }
3953
+ return ports;
3954
+ }
3955
+ function planAdoption(input) {
3956
+ const notAdopted = [
3957
+ ...input.report.notAdopted
3958
+ ];
3959
+ const warnings = [];
3960
+ const named = nameApps(input.report.apps, notAdopted);
3961
+ const shells = named.filter(({ app })=>'shell' === app.role);
3962
+ const remotes = named.filter(({ app })=>'remote' === app.role);
3963
+ if (0 === shells.length) {
3964
+ const found = 0 === input.report.apps.length ? 'no Module Federation config was found' : remotes.length > 0 ? `only remotes were found (${remotes.map((r)=>r.name).join(', ')}), and no shell that loads them` : 'no app had a usable name';
3965
+ throw new MfeflowError('E124', found);
3966
+ }
3967
+ const apps = {};
3968
+ const ports = assignPorts(named, shells, warnings);
3969
+ const appFiles = [];
3970
+ const adoptedRemotes = [];
3971
+ for (const { app, name } of named){
3972
+ apps[name] = {
3973
+ kind: app.role,
3974
+ path: app.dir,
3975
+ ...void 0 === app.port ? {} : {
3976
+ port: app.port
3977
+ }
3978
+ };
3979
+ const platform = platformOf(app);
3980
+ appFiles.push({
3981
+ name,
3982
+ dir: app.dir,
3983
+ role: app.role,
3984
+ content: {
3985
+ $schema: SCHEMA_URLS.mfe,
3986
+ name,
3987
+ owner: OWNER_PLACEHOLDER,
3988
+ shells: 'remote' === app.role ? shellsLoading(name, shells) : [],
3989
+ routes: [],
3990
+ exposes: exposesOf(app, notAdopted),
3991
+ ...Object.keys(platform).length > 0 ? {
3992
+ platform
3993
+ } : {}
3994
+ }
3995
+ });
3996
+ const port = ports.get(name);
3997
+ if ('remote' === app.role && void 0 !== port) adoptedRemotes.push({
3998
+ name,
3999
+ port
4000
+ });
4001
+ }
4002
+ warnings.unshift(`routes are empty (mfeflow cannot read them from a build config); add the paths each app serves to ${appFiles.map((f)=>printable(join(f.dir, 'mfeflow.app.json'))).join(', ')}`);
4003
+ const scriptOnly = remotes.flatMap(({ name })=>shells.flatMap(({ app })=>Object.entries(app.remotes)).filter(([alias, r])=>(r.name ?? alias) === name && "script" === r.type)).map(([alias])=>printable(alias));
4004
+ if (scriptOnly.length > 0) warnings.push(`${[
4005
+ ...new Set(scriptOnly)
4006
+ ].join(', ')}: loaded from a remoteEntry.js, but the registry entries point at mf-manifest.json; check that the remote builds one`);
4007
+ const fromElsewhere = shells.flatMap(({ app })=>Object.entries(app.remotes)).filter(([alias, r])=>!named.some((n)=>n.name === (r.name ?? alias))).map(([alias])=>printable(alias));
4008
+ if (fromElsewhere.length > 0) warnings.push(`${[
4009
+ ...new Set(fromElsewhere)
4010
+ ].join(', ')}: loaded by a shell but not found in this folder, so no app or registry entry was written`);
4011
+ if (adoptedRemotes.length > 0) warnings.push('registry entries for staging and prod point at example.com; set the real addresses before you deploy');
4012
+ return {
4013
+ project: {
4014
+ version: 1,
4015
+ topology: 'monorepo',
4016
+ packageManager: input.packageManager,
4017
+ scope: input.scope,
4018
+ apps,
4019
+ features: {}
4020
+ },
4021
+ appFiles,
4022
+ remotes: adoptedRemotes,
4023
+ shellDirs: shells.map(({ app })=>app.dir),
4024
+ notAdopted: notAdopted.map((item)=>({
4025
+ dir: printable(item.dir),
4026
+ file: printable(item.file),
4027
+ field: printable(item.field),
4028
+ reason: printable(item.reason)
4029
+ })),
4030
+ warnings
4031
+ };
4032
+ }
4033
+ const MAX_BYTES = 1000000;
4034
+ const TIMEOUT_MS = 10000;
4035
+ const MAX_REDIRECTS = 5;
4036
+ const realFetch = (url, init)=>fetch(url, init);
4037
+ function registryUrl(name, registries, version) {
4038
+ const slash = name.indexOf('/');
4039
+ const namespace = name.slice(0, slash);
4040
+ const bare = name.slice(slash + 1);
4041
+ const template = registries[namespace];
4042
+ if (void 0 === template) throw new MfeflowError('E210', namespace);
4043
+ const url = template.replaceAll('{name}', encodeURIComponent(bare)).replaceAll('{version}', encodeURIComponent(version ?? 'latest'));
4044
+ let parsed;
4045
+ try {
4046
+ parsed = new URL(url);
4047
+ } catch (error) {
4048
+ throw new MfeflowError('E401', `${namespace} has an invalid URL (${redactUrl(url)})`, {
4049
+ cause: error
4050
+ });
4051
+ }
4052
+ if (!isRegistryProtocol(parsed)) throw new MfeflowError('E401', `${redactUrl(url)} is not https (plain http is for 127.0.0.1, localhost and [::1] only)`);
4053
+ return parsed.href;
4054
+ }
4055
+ async function getHttps(url, fetchImpl, code, options = {}) {
4056
+ const fail = (at, why, cause)=>{
4057
+ throw new MfeflowError(code, redactUrlsIn(`${at}: ${why}`), void 0 === cause ? {} : {
4058
+ cause
4059
+ });
4060
+ };
4061
+ const startsPrivate = isPrivateHost(new URL(url).hostname);
4062
+ const refusePrivate = (target)=>{
4063
+ if (void 0 === options.blockPrivate || !isPrivateHost(target.hostname)) return;
4064
+ if (!options.blockPrivate && startsPrivate) return;
4065
+ const why = options.blockPrivate ? 'those are only requested from a trusted registry' : 'a public registry may not send you to one';
4066
+ throw new MfeflowError('E405', `${redactUrl(target.href)} is a loopback or private address; ${why}`);
4067
+ };
4068
+ refusePrivate(new URL(url));
4069
+ const startsLoopback = true === options.loopback && isLoopbackHttp(new URL(url));
4070
+ const permitted = (target)=>'https:' === target.protocol || startsLoopback && isLoopbackHttp(target);
4071
+ const addressOk = (address)=>{
4072
+ try {
4073
+ return permitted(new URL(address));
4074
+ } catch {
4075
+ return false;
4076
+ }
4077
+ };
4078
+ let current = url;
4079
+ for(let hop = 0; hop <= MAX_REDIRECTS; hop += 1){
4080
+ let response;
4081
+ try {
4082
+ response = await fetchImpl(current, {
4083
+ signal: AbortSignal.timeout(TIMEOUT_MS),
4084
+ redirect: 'manual'
4085
+ });
4086
+ } catch (error) {
4087
+ return fail(current, messageOf(error), error);
4088
+ }
4089
+ if (void 0 !== response.url && '' !== response.url && !addressOk(response.url)) fail(url, `ended up at ${response.url}, which is not https`);
4090
+ if (void 0 !== response.url && '' !== response.url) refusePrivate(new URL(response.url));
4091
+ if (response.status < 300 || response.status >= 400) return response;
4092
+ const location = response.headers?.get('location');
4093
+ await response.body?.cancel().catch(()=>void 0);
4094
+ if (null == location || '' === location) fail(current, `redirected (${String(response.status)}) with no address`);
4095
+ let next;
4096
+ try {
4097
+ next = new URL(location ?? '', current);
4098
+ } catch (error) {
4099
+ return fail(current, 'redirected to a bad address', error);
4100
+ }
4101
+ if (!permitted(next)) fail(url, `redirected to ${next.href}, which is not https`);
4102
+ refusePrivate(next);
4103
+ current = next.href;
4104
+ }
4105
+ return fail(url, 'redirected too many times');
4106
+ }
4107
+ async function fetchRemoteText(name, registries, fetchImpl = realFetch, version, trusted = false) {
4108
+ const url = registryUrl(name, registries, version);
4109
+ const shown = redactUrl(url);
4110
+ const response = await getHttps(url, fetchImpl, 'E401', {
4111
+ loopback: true,
4112
+ blockPrivate: !trusted
4113
+ });
4114
+ if (!response.ok) {
4115
+ const hint = 404 === response.status ? `${name} is not in that registry` : void 0;
4116
+ throw new MfeflowError('E401', `${shown} answered ${String(response.status)}${hint ? `: ${hint}` : ''}`);
4117
+ }
4118
+ return readCappedText(response, MAX_BYTES, ()=>new MfeflowError('E402', `${shown} is larger than 1 MB`));
4119
+ }
4120
+ function checkIntegrity(name, recorded, served, accept) {
4121
+ if (recorded === served) return false;
4122
+ if (!accept) throw new MfeflowError('E406', `${name} was ${recorded} when you added it and is now ${served}`);
4123
+ return true;
4124
+ }
4125
+ function integrityOf(text) {
4126
+ return `sha256-${createHash('sha256').update(text, 'utf8').digest('hex')}`;
4127
+ }
4128
+ async function readCappedText(response, maxBytes, tooBig) {
4129
+ const declared = Number(response.headers?.get('content-length') ?? 0 / 0);
4130
+ if (declared > maxBytes) {
4131
+ await response.body?.cancel().catch(()=>void 0);
4132
+ throw tooBig();
4133
+ }
4134
+ if (!response.body) {
4135
+ const text = await response.text();
4136
+ if (Buffer.byteLength(text, 'utf8') > maxBytes) throw tooBig();
4137
+ return text;
4138
+ }
4139
+ const chunks = [];
4140
+ let total = 0;
4141
+ for await (const chunk of response.body){
4142
+ total += chunk.byteLength;
4143
+ if (total > maxBytes) throw tooBig();
4144
+ chunks.push(chunk);
4145
+ }
4146
+ return new TextDecoder().decode(Buffer.concat(chunks));
4147
+ }
4148
+ async function fetchFeatureIndex(namespace, registries, fetchImpl = realFetch, trusted = false) {
4149
+ const items = await readIndex(namespace, registries, fetchImpl, trusted, false);
4150
+ return items ?? [];
4151
+ }
4152
+ async function indexedVersion(namespace, name, registries, fetchImpl = realFetch, trusted = false) {
4153
+ const items = await readIndex(namespace, registries, fetchImpl, trusted, true);
4154
+ return items?.find((item)=>item.name === name)?.version;
4155
+ }
4156
+ async function readIndex(namespace, registries, fetchImpl, trusted, missingOk) {
4157
+ const template = registries[namespace];
4158
+ if (void 0 === template) throw new MfeflowError('E210', namespace);
4159
+ const problem = registryTemplateProblem(template);
4160
+ if (void 0 !== problem) throw new MfeflowError('E401', `${namespace}: ${problem}`);
4161
+ const url = registryIndexUrl(template);
4162
+ const shown = redactUrl(url);
4163
+ const response = await getHttps(url, fetchImpl, 'E401', {
4164
+ loopback: true,
4165
+ blockPrivate: !trusted
4166
+ });
4167
+ if (!response.ok) {
4168
+ if (missingOk && 404 === response.status) return;
4169
+ const hint = 404 === response.status ? ': this registry has no index.json' : '';
4170
+ throw new MfeflowError('E401', `${shown} answered ${String(response.status)}${hint}`);
4171
+ }
4172
+ const text = await readCappedText(response, MAX_BYTES, ()=>new MfeflowError('E403', `${shown} is larger than 1 MB`));
4173
+ let value;
4174
+ try {
4175
+ value = JSON.parse(text);
4176
+ } catch (error) {
4177
+ const reason = messageOf(error).replaceAll(/\p{Cc}/gu, '?');
4178
+ throw new MfeflowError('E403', `${shown} is not JSON (${reason})`, {
4179
+ cause: error
4180
+ });
4181
+ }
4182
+ const issues = validateFeatureRegistryIndex(value);
4183
+ if (issues.length > 0) {
4184
+ const first = issues.slice(0, 3).map((i)=>`${i.path || '(root)'} ${i.message}`);
4185
+ const more = issues.length > 3 ? `; and ${String(issues.length - 3)} more` : '';
4186
+ throw new MfeflowError('E403', `${shown}: ${first.join('; ')}${more}`);
4187
+ }
4188
+ return itemsOf(value);
4189
+ }
4190
+ function itemsOf(value) {
4191
+ const holder = {
4192
+ items: []
4193
+ };
4194
+ Object.assign(holder, value);
4195
+ return holder.items;
4196
+ }
4197
+ function parseFeature(text, trust, label) {
4198
+ let value;
4199
+ try {
4200
+ value = JSON.parse(text);
4201
+ } catch (error) {
4202
+ throw new MfeflowError('E202', `${label}: ${messageOf(error)}`, {
4203
+ cause: error
4204
+ });
4205
+ }
4206
+ if ('object' != typeof value || null === value || Array.isArray(value)) throw new MfeflowError('E202', `${label}: must be a JSON object`);
4207
+ const record = Object.fromEntries(Object.entries(value));
4208
+ if ('third-party' === trust) assertAllowedKeys(record, label);
4209
+ assertKnownOps(record, label);
4210
+ const issues = validateFeatureManifest(record);
4211
+ if (issues.length > 0) {
4212
+ const detail = issues.map((i)=>`${i.path || '(root)'} ${i.message}`).join('; ');
4213
+ throw new MfeflowError('E202', `${label}: ${detail}`);
4214
+ }
4215
+ const manifest = toManifest(record, label);
4216
+ if ('third-party' === trust) assertThirdPartyContent(manifest, label);
4217
+ return manifest;
4218
+ }
4219
+ function toManifest(record, label) {
4220
+ const manifest = {
4221
+ name: '',
4222
+ version: ''
4223
+ };
4224
+ Object.assign(manifest, record);
4225
+ if ('' === manifest.name) throw new MfeflowError('E202', `${label}: name is required`);
4226
+ return manifest;
4227
+ }
4228
+ async function loadFeature(name, options) {
4229
+ if (!FEATURE_NAME.test(name)) throw new MfeflowError('E201', `"${name}" is not a feature name`);
4230
+ if (name.startsWith('@')) return loadRemote(name, options);
4231
+ return loadLocal(name, options);
4232
+ }
4233
+ async function loadLocal(name, options) {
4234
+ const ops = options.fs ?? realFs;
4235
+ if (!options.featuresDir) throw new MfeflowError('E201', `"${name}" (no features folder configured)`);
4236
+ const dir = node_path.join(options.featuresDir, name);
4237
+ const text = await readIfExists(ops, node_path.join(dir, 'feature.json'));
4238
+ if (void 0 === text) throw new MfeflowError('E201', `"${name}" (looked in ${options.featuresDir})`);
4239
+ const manifest = parseFeature(text, 'first-party', `${name}/feature.json`);
4240
+ if (manifest.name !== name) throw new MfeflowError('E202', `${name}/feature.json: name is "${manifest.name}", expected "${name}"`);
4241
+ return {
4242
+ manifest,
4243
+ trust: 'first-party',
4244
+ dir,
4245
+ readAsset: async (relative)=>{
4246
+ if (!isSafeRelativePath(relative)) throw new MfeflowError('E105', `"${relative}"`);
4247
+ const content = await readIfExists(ops, node_path.join(dir, ...relative.split('/')));
4248
+ if (void 0 === content) throw new MfeflowError('E201', `${name}: file "${relative}" is missing`);
4249
+ return content;
4250
+ }
4251
+ };
4252
+ }
4253
+ async function loadRemote(name, options) {
4254
+ const registries = options.registries ?? {};
4255
+ const text = await fetchRemoteText(name, registries, options.fetch, options.version, true === options.trusted);
4256
+ const manifest = parseFeature(text, 'third-party', name);
4257
+ if (manifest.name !== name) throw new MfeflowError('E402', `asked for ${name}, got ${manifest.name}`);
4258
+ if (true === options.checkIndex) await assertIndexAgrees(name, manifest.version, registries, options);
4259
+ return {
4260
+ manifest,
4261
+ trust: 'third-party',
4262
+ integrity: integrityOf(text)
4263
+ };
4264
+ }
4265
+ async function assertIndexAgrees(name, version, registries, options) {
4266
+ const slash = name.indexOf('/');
4267
+ let listed;
4268
+ try {
4269
+ listed = await indexedVersion(name.slice(0, slash), name.slice(slash + 1), registries, options.fetch, true === options.trusted);
4270
+ } catch (error) {
4271
+ if (error instanceof MfeflowError && 'MFEFLOW_E401' === error.code) return;
4272
+ throw error;
4273
+ }
4274
+ if (void 0 !== listed && listed !== version) throw new MfeflowError('E402', `the index lists ${name} at ${listed} but its feature.json is ${version}`);
4275
+ }
4276
+ async function listLocalFeatures(featuresDir, ops = realFs) {
4277
+ let dirs;
4278
+ try {
4279
+ dirs = await ops.listDirs(featuresDir);
4280
+ } catch {
4281
+ return [];
4282
+ }
4283
+ const checked = await Promise.all(dirs.map(async (dir)=>await readIfExists(ops, node_path.join(featuresDir, dir, 'feature.json')) === void 0 ? void 0 : dir));
4284
+ return checked.filter((d)=>void 0 !== d).toSorted((a, b)=>a.localeCompare(b));
4285
+ }
4286
+ const TRUST_FILE = 'trusted-registries.json';
4287
+ const TRUST_ENV = 'MFEFLOW_TRUST_REGISTRIES';
4288
+ const trust_set = (value)=>void 0 === value || '' === value ? void 0 : value;
4289
+ function configDirOf(env, platform = process.platform, home = node_os.homedir()) {
4290
+ const override = trust_set(env.MFEFLOW_CONFIG_DIR);
4291
+ if (void 0 !== override) return override;
4292
+ const xdg = trust_set(env.XDG_CONFIG_HOME);
4293
+ if (void 0 !== xdg) return node_path.join(xdg, 'mfeflow');
4294
+ const appData = trust_set(env.APPDATA);
4295
+ if ('win32' === platform && void 0 !== appData) return node_path.join(appData, 'mfeflow');
4296
+ return node_path.join(home, '.config', 'mfeflow');
4297
+ }
4298
+ function trustFileIn(configDir) {
4299
+ return node_path.join(configDir, TRUST_FILE);
4300
+ }
4301
+ function isEntry(value) {
4302
+ if ('object' != typeof value || null === value) return false;
4303
+ const { namespace, template } = Object.fromEntries(Object.entries(value));
4304
+ return 'string' == typeof namespace && REGISTRY_NAMESPACE.test(namespace) && 'string' == typeof template && void 0 === registryTemplateProblem(template);
4305
+ }
4306
+ async function readTrusted(configDir, ops = realFs) {
4307
+ let text;
4308
+ try {
4309
+ text = await readIfExists(ops, trustFileIn(configDir));
4310
+ } catch {
4311
+ return [];
4312
+ }
4313
+ if (void 0 === text) return [];
4314
+ try {
4315
+ const value = JSON.parse(text);
4316
+ if ('object' != typeof value || null === value) return [];
4317
+ const { registries } = Object.fromEntries(Object.entries(value));
4318
+ return Array.isArray(registries) ? registries.filter(isEntry) : [];
4319
+ } catch {
4320
+ return [];
4321
+ }
4322
+ }
4323
+ async function save(configDir, entries, ops) {
4324
+ const sorted = entries.toSorted((a, b)=>a.namespace.localeCompare(b.namespace) || a.template.localeCompare(b.template));
4325
+ const body = `${JSON.stringify({
4326
+ version: 1,
4327
+ registries: sorted
4328
+ }, null, 2)}\n`;
4329
+ try {
4330
+ await writeFileAtomic(ops, trustFileIn(configDir), body);
4331
+ } catch (error) {
4332
+ throw new MfeflowError('E106', trustFileIn(configDir), {
4333
+ cause: error
4334
+ });
4335
+ }
4336
+ }
4337
+ async function addTrusted(configDir, entry, ops = realFs) {
4338
+ const current = await readTrusted(configDir, ops);
4339
+ if (current.some((e)=>e.namespace === entry.namespace && e.template === entry.template)) return false;
4340
+ await save(configDir, [
4341
+ ...current,
4342
+ entry
4343
+ ], ops);
4344
+ return true;
4345
+ }
4346
+ async function removeTrusted(configDir, namespace, ops = realFs) {
4347
+ const current = await readTrusted(configDir, ops);
4348
+ const kept = current.filter((e)=>e.namespace !== namespace);
4349
+ if (kept.length === current.length) return 0;
4350
+ await save(configDir, kept, ops);
4351
+ return current.length - kept.length;
4352
+ }
4353
+ function parseTrustEnv(value) {
4354
+ return (value ?? '').split(',').map((part)=>part.trim()).flatMap((part)=>{
4355
+ const eq = part.indexOf('=');
4356
+ const namespace = -1 === eq ? part : part.slice(0, eq).trim();
4357
+ if (!REGISTRY_NAMESPACE.test(namespace)) return [];
4358
+ const template = -1 === eq ? '' : part.slice(eq + 1).trim();
4359
+ return [
4360
+ '' === template ? {
4361
+ namespace
4362
+ } : {
4363
+ namespace,
4364
+ template
4365
+ }
4366
+ ];
4367
+ });
4368
+ }
4369
+ function trustedNamespaces(registries, recorded, fromEnv = []) {
4370
+ const fetch1 = new Set();
4371
+ const privateOk = new Set();
4372
+ for (const [namespace, template] of Object.entries(registries)){
4373
+ const inFile = recorded.some((e)=>e.namespace === namespace && e.template === template);
4374
+ const items = fromEnv.filter((e)=>e.namespace === namespace);
4375
+ const pinned = items.some((e)=>e.template === template);
4376
+ if (inFile || pinned) privateOk.add(namespace);
4377
+ if (inFile || items.some((e)=>void 0 === e.template || e.template === template)) fetch1.add(namespace);
4378
+ }
4379
+ return {
4380
+ fetch: fetch1,
4381
+ privateOk
4382
+ };
4383
+ }
2276
4384
  const registry_build_text = (value)=>`${JSON.stringify(value, null, 2)}\n`;
2277
4385
  async function buildRegistry(dir, ops = realFs) {
2278
4386
  let folders;
@@ -2386,4 +4494,4 @@ function packageInfo() {
2386
4494
  version
2387
4495
  };
2388
4496
  }
2389
- export { ERROR_CATALOGUE, MfeflowError, PROJECT_FILE, PlanBuilder, RUNTIME_CONFIG_FILE, applyPlan, applyTransform, buildRegistry, dotenvKeys, emptyPlan, fetchFeatureIndex, fetchRemoteText, findDiverged, findProjectRoot, getHttps, hasErrno, hashContent, inOrder, isMfeflowError, isProtectedPath, listLocalFeatures, loadFeature, loadProject, messageOf, packageInfo, parseFeature, parseProject, readCappedText, realFs, recordPlan, registryUrl, renderDiff, renderTemplate, resolveFeature, resolveOptions, runtimeConfigFiles, saveProject, schemaKeys, serializeProject, substitute, summarize, writeFileAtomic };
4497
+ export { APP_FILE, ERROR_CATALOGUE, LEGACY_APP_FILE, MfeflowError, PROJECT_FILE, PlanBuilder, RUNTIME_CONFIG_FILE, TRUST_ENV, addTrusted, applyPlan, applyTransform, buildRegistry, checkIntegrity, configDirOf, detectBaseline, detectFederation, dotenvKeys, emptyPlan, fetchFeatureIndex, fetchRemoteText, findDiverged, findProjectRoot, getHttps, hasErrno, hashContent, inOrder, indexedVersion, integrityOf, isMfeflowError, isProtectedPath, legacyAppFileNotice, listLocalFeatures, loadFeature, loadProject, messageOf, packageInfo, parseFeature, parseProject, parseTrustEnv, planAdoption, planUpgrade, readAppFile, readCappedText, readTrusted, realFs, recordPlan, registryUrl, removeTrusted, renderDiff, renderTemplate, resolveFeature, resolveOptions, runtimeConfigFiles, saveProject, schemaKeys, serializeProject, substitute, summarize, symlinkedPrefix, trustFileIn, trustedNamespaces, writeFileAtomic };