@mfeflow/core 0.5.0 → 0.7.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,13 +1,14 @@
1
1
  import { createRequire } from "node:module";
2
- import { DOCS_URL, FEATURE_CONDITION, FEATURE_NAME, REGISTRY_NAMESPACE, SCHEMA_URLS, TRANSFORM_OPS, errorDocsUrl, isLoopbackHttp, isMfeflowProject, isPrivateHost, isRegistryProtocol, isSafeRelativePath, redactUrl, redactUrlsIn, 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";
8
- import { valid, validRange } from "semver";
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";
9
10
  import { Eta } from "eta";
10
- 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";
11
12
  import { parse, parseExpression } from "@babel/parser";
12
13
  import node_os from "node:os";
13
14
  const ERROR_CATALOGUE = {
@@ -60,8 +61,8 @@ const ERROR_CATALOGUE = {
60
61
  fix: 'Restore the folder, or remove the app from "apps" in mfeflow.json.'
61
62
  },
62
63
  E113: {
63
- title: 'mfe.json is missing or invalid',
64
- 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}.`
65
66
  },
66
67
  E114: {
67
68
  title: 'A registry entry is missing or invalid',
@@ -72,8 +73,8 @@ const ERROR_CATALOGUE = {
72
73
  fix: 'Give one of them another port in its rsbuild config and in "apps" in mfeflow.json.'
73
74
  },
74
75
  E116: {
75
- title: 'Two remotes claim routes that overlap, or an mfe.json disagrees with mfeflow.json',
76
- fix: 'Give each route to one remote (a wildcard such as /checkout/* also covers /checkout/pay), 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.'
77
78
  },
78
79
  E117: {
79
80
  title: 'A copied file changed since mfeflow wrote it',
@@ -95,6 +96,18 @@ const ERROR_CATALOGUE = {
95
96
  title: 'A server env key is not listed in turbo.json',
96
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.'
97
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
+ },
98
111
  E201: {
99
112
  title: 'Feature not found',
100
113
  fix: 'List what is available with `mfeflow features`, or check the spelling.'
@@ -161,7 +174,7 @@ const ERROR_CATALOGUE = {
161
174
  },
162
175
  E302: {
163
176
  title: 'A remote is not compatible with its shell',
164
- 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.'
165
178
  },
166
179
  E303: {
167
180
  title: 'A runtime plugin imports a shared module',
@@ -169,7 +182,7 @@ const ERROR_CATALOGUE = {
169
182
  },
170
183
  E304: {
171
184
  title: 'A bundle is over its budget',
172
- 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.'
173
186
  },
174
187
  E305: {
175
188
  title: 'There is no build output to measure',
@@ -179,6 +192,10 @@ const ERROR_CATALOGUE = {
179
192
  title: 'A dev server could not start',
180
193
  fix: "Free the port or change it in mfeflow.json and the app's rsbuild config, then run `mfeflow dev` again."
181
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
+ },
182
199
  E401: {
183
200
  title: 'Could not fetch from the registry',
184
201
  fix: 'Check the URL in "registries" and your network, then try again.'
@@ -197,7 +214,7 @@ const ERROR_CATALOGUE = {
197
214
  },
198
215
  E501: {
199
216
  title: 'A deploy failed the schema gate',
200
- fix: `Fix what the message names: the remote's mfe.json, its mf-manifest.json or the version. Nothing was uploaded. See ${DOCS_URL}/guide/deploy-and-rollback.`
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.`
201
218
  },
202
219
  E502: {
203
220
  title: 'A route of this remote overlaps a route of another remote',
@@ -209,7 +226,7 @@ const ERROR_CATALOGUE = {
209
226
  },
210
227
  E504: {
211
228
  title: 'The remote is over its first-load JS budget',
212
- fix: `Cut what the remote loads first, or raise budgets.initialJsKb in its mfe.json on purpose. Nothing was uploaded. See ${DOCS_URL}/guide/deploy-and-rollback.`
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.`
213
230
  },
214
231
  E505: {
215
232
  title: 'A rollout was asked for, and a static host cannot split traffic',
@@ -250,6 +267,22 @@ const ERROR_CATALOGUE = {
250
267
  E406: {
251
268
  title: 'The registry changed a feature since you added it',
252
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.'
253
286
  }
254
287
  };
255
288
  class MfeflowError extends Error {
@@ -288,14 +321,15 @@ function readPackageJson() {
288
321
  if (!isPackageJson(pkg)) throw new MfeflowError('E106', 'package.json is missing a name or version');
289
322
  return pkg;
290
323
  }
291
- function normalizeEol(content) {
292
- return content.replace(/\r\n/g, '\n');
293
- }
294
- function hashContent(content) {
295
- 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;
296
330
  }
297
- async function inOrder(items, step) {
298
- for (const item of items)await step(item);
331
+ function joinPosix(...parts) {
332
+ return node_path.posix.join(...parts.filter((p)=>'' !== p));
299
333
  }
300
334
  const realFs = {
301
335
  readFile: (file)=>promises.readFile(file, 'utf8'),
@@ -351,15 +385,46 @@ async function writeFileAtomic(ops, file, content) {
351
385
  });
352
386
  }
353
387
  }
354
- function resolveInRoot(root, relative) {
355
- if (!isSafeRelativePath(relative)) throw new MfeflowError('E105', `"${relative}"`);
356
- const absolute = node_path.resolve(root, ...relative.split('/'));
357
- const inside = node_path.relative(root, absolute);
358
- if (inside.startsWith('..') || node_path.isAbsolute(inside)) throw new MfeflowError('E105', `"${relative}"`);
359
- 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
+ };
360
415
  }
361
- function joinPosix(...parts) {
362
- 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);
363
428
  }
364
429
  const PROJECT_FILE = 'mfeflow.json';
365
430
  function describeIssues(issues) {
@@ -382,11 +447,22 @@ function serializeProject(project) {
382
447
  if (issues.length > 0) throw new MfeflowError('E104', describeIssues(issues));
383
448
  const { $schema, ...rest } = project;
384
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));
451
+ const envKeys = rest.envKeys && Object.fromEntries(Object.entries(rest.envKeys).toSorted(byKey).map(([file, keys])=>[
452
+ file,
453
+ keys.toSorted((a, b)=>a < b ? -1 : a > b ? 1 : 0)
454
+ ]));
385
455
  const ordered = {
386
456
  $schema: $schema ?? SCHEMA_URLS.project,
387
457
  ...rest,
388
458
  ...files ? {
389
459
  files
460
+ } : {},
461
+ ...kept ? {
462
+ kept
463
+ } : {},
464
+ ...envKeys ? {
465
+ envKeys
390
466
  } : {}
391
467
  };
392
468
  return `${JSON.stringify(ordered, null, 2)}\n`;
@@ -542,6 +618,9 @@ class PlanBuilder {
542
618
  async content(file) {
543
619
  return (await this.open(file)).current ?? void 0;
544
620
  }
621
+ async existed(file) {
622
+ return null !== (await this.open(file)).original;
623
+ }
545
624
  async captureProject() {
546
625
  this.state.projectBefore = await this.read(PROJECT_FILE) ?? null;
547
626
  }
@@ -609,26 +688,31 @@ class PlanBuilder {
609
688
  if (!isRegistryRange(range)) throw new MfeflowError('E202', `${name}: "${range}" is not a semver range or a dist-tag`);
610
689
  const source = await this.content(file);
611
690
  if (void 0 === source) throw new MfeflowError('E208', `${file} does not exist`);
612
- const existing = this.declaredRange(file, source, name);
691
+ const existing = this.declaredRange(file, source, name, range);
613
692
  if (existing === range || existing.startsWith('workspace:')) return;
614
693
  if (!existing.startsWith('catalog:') && !isRegistryRange(existing)) return void this.warn(`${file}: ${name} is ${JSON.stringify(existing)}; left as it is, not moved to ${range}`);
615
694
  if ('third-party' === this.trust) throw new MfeflowError('E207', `${file}: ${name} is already ${JSON.stringify(existing)}; the feature wants ${range}`);
616
695
  if (existing.startsWith('catalog:')) return void await this.setCatalogEntry(name, existing.slice(8), range);
617
696
  await this.patch(file, (text)=>editJsonObject(file, text, (pkg)=>{
618
- const section = DEPENDENCY_SECTIONS.find((key)=>name in asRecord(pkg[key])) ?? '';
619
- pkg[section] = {
620
- ...asRecord(pkg[section]),
621
- [name]: range
622
- };
623
- return true;
697
+ let changed = false;
698
+ for (const section of DEPENDENCY_SECTIONS){
699
+ const listed = asRecord(pkg[section])[name];
700
+ if ('string' == typeof listed && listed !== range && isRegistryRange(listed)) {
701
+ pkg[section] = {
702
+ ...asRecord(pkg[section]),
703
+ [name]: range
704
+ };
705
+ changed = true;
706
+ }
707
+ }
708
+ return changed;
624
709
  }));
625
710
  }
626
- declaredRange(file, source, name) {
711
+ declaredRange(file, source, name, wanted) {
627
712
  const pkg = parseJsonObject(file, source);
628
- for (const section of DEPENDENCY_SECTIONS){
629
- const value = asRecord(pkg[section])[name];
630
- if ('string' == typeof value) return value;
631
- }
713
+ const listed = DEPENDENCY_SECTIONS.map((section)=>asRecord(pkg[section])[name]).filter((value)=>'string' == typeof value);
714
+ const found = listed.find((value)=>value !== wanted) ?? listed[0];
715
+ if (void 0 !== found) return found;
632
716
  throw new MfeflowError('E208', `${file} does not depend on ${name}`);
633
717
  }
634
718
  async setCatalogEntry(name, catalogName, range) {
@@ -827,19 +911,19 @@ const DEVICE_NAMES = new Set([
827
911
  'lpt³'
828
912
  ]);
829
913
  const INVISIBLE = /[\p{Cc}\p{Cf}\p{Cn}\p{Co}\p{Cs}]/u;
830
- function refuse(file, why) {
914
+ function write_policy_refuse(file, why) {
831
915
  throw new MfeflowError('E105', `"${file}" ${why}`);
832
916
  }
833
917
  function assertWritable(file) {
834
- if (INVISIBLE.test(file)) refuse(file, 'has control or invisible characters');
918
+ if (INVISIBLE.test(file)) write_policy_refuse(file, 'has control or invisible characters');
835
919
  const key = policyKey(file);
836
- if ('mfeflow.json' === key) refuse(file, 'is not a place features write to');
920
+ if ('mfeflow.json' === key) write_policy_refuse(file, 'is not a place features write to');
837
921
  for (const segment of key.split('/')){
838
- if ('.git' === segment || 'node_modules' === segment) refuse(file, 'is not a place features write to');
839
- if (segment.includes(':')) refuse(file, 'has a ":" (a Windows stream)');
840
- if (/[. ]$/.test(segment)) refuse(file, 'has a name ending in a dot or space');
922
+ if ('.git' === segment || 'node_modules' === segment) write_policy_refuse(file, 'is not a place features write to');
923
+ if (segment.includes(':')) write_policy_refuse(file, 'has a ":" (a Windows stream)');
924
+ if (/[. ]$/.test(segment)) write_policy_refuse(file, 'has a name ending in a dot or space');
841
925
  const base = (segment.split('.')[0] ?? '').trimEnd();
842
- if (DEVICE_NAMES.has(base)) refuse(file, 'is a Windows device name');
926
+ if (DEVICE_NAMES.has(base)) write_policy_refuse(file, 'is a Windows device name');
843
927
  }
844
928
  }
845
929
  const THIRD_PARTY_FOLDERS = new Set([
@@ -885,7 +969,7 @@ async function applyPlan(plan, options) {
885
969
  const realRoot = await ops.realpath(options.root);
886
970
  await inOrder(targets, async ({ change, file })=>{
887
971
  await assertContained(ops, realRoot, file, change.path);
888
- await assertTrustedRoute(ops, options.root, change);
972
+ await assertTrustedRoute(ops, options.root, change, true === options.refuseSymlinks);
889
973
  });
890
974
  const current = await verifyUnchanged(targets, ops);
891
975
  if (options.dryRun) return {
@@ -902,7 +986,7 @@ async function applyPlan(plan, options) {
902
986
  const dir = await ops.mkdir(node_path.dirname(file));
903
987
  if (dir) createdDirs.push(dir);
904
988
  await assertContained(ops, realRoot, node_path.dirname(file), change.path);
905
- await assertTrustedRoute(ops, options.root, change);
989
+ await assertTrustedRoute(ops, options.root, change, true === options.refuseSymlinks);
906
990
  const temp = tempSibling(file);
907
991
  staged.set(file, temp);
908
992
  await ops.writeFile(temp, change.after);
@@ -1045,17 +1129,13 @@ async function assertContained(ops, realRoot, target, shown) {
1045
1129
  const inside = node_path.relative(realRoot, real);
1046
1130
  if (inside.startsWith('..') || node_path.isAbsolute(inside)) throw new MfeflowError('E105', `"${shown}" resolves through a symlink to ${real}`);
1047
1131
  }
1048
- async function assertTrustedRoute(ops, root, change) {
1049
- if ('third-party' !== change.trust) return;
1050
- assertWritable(change.path);
1051
- const parts = change.path.split('/');
1052
- const prefixes = parts.map((_, i)=>parts.slice(0, i + 1).join('/'));
1053
- const linked = await Promise.all(prefixes.map(async (prefix)=>({
1054
- prefix,
1055
- link: await ops.isSymlink(node_path.join(root, ...prefix.split('/')))
1056
- })));
1057
- const hit = linked.find((entry)=>entry.link);
1058
- if (hit) throw new MfeflowError('E203', `"${change.path}" goes through a symlink at "${hit.prefix}", which third-party features may not write through`);
1132
+ async function assertTrustedRoute(ops, root, change, refuseAll) {
1133
+ const thirdParty = 'third-party' === change.trust;
1134
+ if (!thirdParty && !refuseAll) return;
1135
+ if (thirdParty) assertWritable(change.path);
1136
+ const hit = await symlinkedPrefix(ops, root, change.path);
1137
+ if (hit && !thirdParty) throw new MfeflowError('E105', `"${change.path}" goes through a symlink at "${hit}", which is never written through`);
1138
+ if (hit) throw new MfeflowError('E203', `"${change.path}" goes through a symlink at "${hit}", which third-party features may not write through`);
1059
1139
  }
1060
1140
  async function deepestRealpath(ops, target) {
1061
1141
  try {
@@ -1144,11 +1224,6 @@ function substitute(source, data, name = 'template') {
1144
1224
  return String(value);
1145
1225
  });
1146
1226
  }
1147
- const RUNTIME_CONFIG_FILE = /^runtime-config(?:\.([a-z0-9-]+))?\.json$/;
1148
- async function runtimeConfigFiles(folder) {
1149
- const names = await promises.readdir(folder).catch(()=>[]);
1150
- return names.filter((name)=>RUNTIME_CONFIG_FILE.test(name)).toSorted((a, b)=>a.localeCompare(b));
1151
- }
1152
1227
  function applyEdits(source, edits) {
1153
1228
  return edits.toSorted((a, b)=>b.start - a.start).reduce((out, e)=>out.slice(0, e.start) + e.text + out.slice(e.end), source);
1154
1229
  }
@@ -1617,7 +1692,8 @@ function schemaKeys(source, file, scope, object = 'runtimeConfigSchema') {
1617
1692
  return [
1618
1693
  {
1619
1694
  name,
1620
- optional: rawKey(p) !== name || acceptsMissing(p.value)
1695
+ optional: rawKey(p) !== name || acceptsMissing(p.value),
1696
+ schema: source.slice(p.value.start ?? 0, p.value.end ?? 0)
1621
1697
  }
1622
1698
  ];
1623
1699
  });
@@ -1837,170 +1913,6 @@ function applyTransform(source, file, op) {
1837
1913
  throw new MfeflowError('E204', JSON.stringify(Reflect.get(op, 'op')));
1838
1914
  }
1839
1915
  }
1840
- const MAX_BYTES = 1000000;
1841
- const TIMEOUT_MS = 10000;
1842
- const MAX_REDIRECTS = 5;
1843
- const realFetch = (url, init)=>fetch(url, init);
1844
- function registryUrl(name, registries, version) {
1845
- const slash = name.indexOf('/');
1846
- const namespace = name.slice(0, slash);
1847
- const bare = name.slice(slash + 1);
1848
- const template = registries[namespace];
1849
- if (void 0 === template) throw new MfeflowError('E210', namespace);
1850
- const url = template.replaceAll('{name}', encodeURIComponent(bare)).replaceAll('{version}', encodeURIComponent(version ?? 'latest'));
1851
- let parsed;
1852
- try {
1853
- parsed = new URL(url);
1854
- } catch (error) {
1855
- throw new MfeflowError('E401', `${namespace} has an invalid URL (${redactUrl(url)})`, {
1856
- cause: error
1857
- });
1858
- }
1859
- if (!isRegistryProtocol(parsed)) throw new MfeflowError('E401', `${redactUrl(url)} is not https (plain http is for 127.0.0.1, localhost and [::1] only)`);
1860
- return parsed.href;
1861
- }
1862
- async function getHttps(url, fetchImpl, code, options = {}) {
1863
- const fail = (at, why, cause)=>{
1864
- throw new MfeflowError(code, redactUrlsIn(`${at}: ${why}`), void 0 === cause ? {} : {
1865
- cause
1866
- });
1867
- };
1868
- const startsPrivate = isPrivateHost(new URL(url).hostname);
1869
- const refusePrivate = (target)=>{
1870
- if (void 0 === options.blockPrivate || !isPrivateHost(target.hostname)) return;
1871
- if (!options.blockPrivate && startsPrivate) return;
1872
- const why = options.blockPrivate ? 'those are only requested from a trusted registry' : 'a public registry may not send you to one';
1873
- throw new MfeflowError('E405', `${redactUrl(target.href)} is a loopback or private address; ${why}`);
1874
- };
1875
- refusePrivate(new URL(url));
1876
- const startsLoopback = true === options.loopback && isLoopbackHttp(new URL(url));
1877
- const permitted = (target)=>'https:' === target.protocol || startsLoopback && isLoopbackHttp(target);
1878
- const addressOk = (address)=>{
1879
- try {
1880
- return permitted(new URL(address));
1881
- } catch {
1882
- return false;
1883
- }
1884
- };
1885
- let current = url;
1886
- for(let hop = 0; hop <= MAX_REDIRECTS; hop += 1){
1887
- let response;
1888
- try {
1889
- response = await fetchImpl(current, {
1890
- signal: AbortSignal.timeout(TIMEOUT_MS),
1891
- redirect: 'manual'
1892
- });
1893
- } catch (error) {
1894
- return fail(current, messageOf(error), error);
1895
- }
1896
- if (void 0 !== response.url && '' !== response.url && !addressOk(response.url)) fail(url, `ended up at ${response.url}, which is not https`);
1897
- if (void 0 !== response.url && '' !== response.url) refusePrivate(new URL(response.url));
1898
- if (response.status < 300 || response.status >= 400) return response;
1899
- const location = response.headers?.get('location');
1900
- await response.body?.cancel().catch(()=>void 0);
1901
- if (null == location || '' === location) fail(current, `redirected (${String(response.status)}) with no address`);
1902
- let next;
1903
- try {
1904
- next = new URL(location ?? '', current);
1905
- } catch (error) {
1906
- return fail(current, 'redirected to a bad address', error);
1907
- }
1908
- if (!permitted(next)) fail(url, `redirected to ${next.href}, which is not https`);
1909
- refusePrivate(next);
1910
- current = next.href;
1911
- }
1912
- return fail(url, 'redirected too many times');
1913
- }
1914
- async function fetchRemoteText(name, registries, fetchImpl = realFetch, version, trusted = false) {
1915
- const url = registryUrl(name, registries, version);
1916
- const shown = redactUrl(url);
1917
- const response = await getHttps(url, fetchImpl, 'E401', {
1918
- loopback: true,
1919
- blockPrivate: !trusted
1920
- });
1921
- if (!response.ok) {
1922
- const hint = 404 === response.status ? `${name} is not in that registry` : void 0;
1923
- throw new MfeflowError('E401', `${shown} answered ${String(response.status)}${hint ? `: ${hint}` : ''}`);
1924
- }
1925
- return readCappedText(response, MAX_BYTES, ()=>new MfeflowError('E402', `${shown} is larger than 1 MB`));
1926
- }
1927
- function checkIntegrity(name, recorded, served, accept) {
1928
- if (recorded === served) return false;
1929
- if (!accept) throw new MfeflowError('E406', `${name} was ${recorded} when you added it and is now ${served}`);
1930
- return true;
1931
- }
1932
- function integrityOf(text) {
1933
- return `sha256-${createHash('sha256').update(text, 'utf8').digest('hex')}`;
1934
- }
1935
- async function readCappedText(response, maxBytes, tooBig) {
1936
- const declared = Number(response.headers?.get('content-length') ?? 0 / 0);
1937
- if (declared > maxBytes) {
1938
- await response.body?.cancel().catch(()=>void 0);
1939
- throw tooBig();
1940
- }
1941
- if (!response.body) {
1942
- const text = await response.text();
1943
- if (Buffer.byteLength(text, 'utf8') > maxBytes) throw tooBig();
1944
- return text;
1945
- }
1946
- const chunks = [];
1947
- let total = 0;
1948
- for await (const chunk of response.body){
1949
- total += chunk.byteLength;
1950
- if (total > maxBytes) throw tooBig();
1951
- chunks.push(chunk);
1952
- }
1953
- return new TextDecoder().decode(Buffer.concat(chunks));
1954
- }
1955
- async function fetchFeatureIndex(namespace, registries, fetchImpl = realFetch, trusted = false) {
1956
- const items = await readIndex(namespace, registries, fetchImpl, trusted, false);
1957
- return items ?? [];
1958
- }
1959
- async function indexedVersion(namespace, name, registries, fetchImpl = realFetch, trusted = false) {
1960
- const items = await readIndex(namespace, registries, fetchImpl, trusted, true);
1961
- return items?.find((item)=>item.name === name)?.version;
1962
- }
1963
- async function readIndex(namespace, registries, fetchImpl, trusted, missingOk) {
1964
- const template = registries[namespace];
1965
- if (void 0 === template) throw new MfeflowError('E210', namespace);
1966
- const problem = registryTemplateProblem(template);
1967
- if (void 0 !== problem) throw new MfeflowError('E401', `${namespace}: ${problem}`);
1968
- const url = registryIndexUrl(template);
1969
- const shown = redactUrl(url);
1970
- const response = await getHttps(url, fetchImpl, 'E401', {
1971
- loopback: true,
1972
- blockPrivate: !trusted
1973
- });
1974
- if (!response.ok) {
1975
- if (missingOk && 404 === response.status) return;
1976
- const hint = 404 === response.status ? ': this registry has no index.json' : '';
1977
- throw new MfeflowError('E401', `${shown} answered ${String(response.status)}${hint}`);
1978
- }
1979
- const text = await readCappedText(response, MAX_BYTES, ()=>new MfeflowError('E403', `${shown} is larger than 1 MB`));
1980
- let value;
1981
- try {
1982
- value = JSON.parse(text);
1983
- } catch (error) {
1984
- const reason = messageOf(error).replaceAll(/\p{Cc}/gu, '?');
1985
- throw new MfeflowError('E403', `${shown} is not JSON (${reason})`, {
1986
- cause: error
1987
- });
1988
- }
1989
- const issues = validateFeatureRegistryIndex(value);
1990
- if (issues.length > 0) {
1991
- const first = issues.slice(0, 3).map((i)=>`${i.path || '(root)'} ${i.message}`);
1992
- const more = issues.length > 3 ? `; and ${String(issues.length - 3)} more` : '';
1993
- throw new MfeflowError('E403', `${shown}: ${first.join('; ')}${more}`);
1994
- }
1995
- return itemsOf(value);
1996
- }
1997
- function itemsOf(value) {
1998
- const holder = {
1999
- items: []
2000
- };
2001
- Object.assign(holder, value);
2002
- return holder.items;
2003
- }
2004
1916
  const ROOTS = new Set([
2005
1917
  'z',
2006
1918
  'v',
@@ -2179,200 +2091,14 @@ function assertThirdPartyTransform(t, label) {
2179
2091
  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`);
2180
2092
  }
2181
2093
  }
2182
- function parseFeature(text, trust, label) {
2183
- let value;
2184
- try {
2185
- value = JSON.parse(text);
2186
- } catch (error) {
2187
- throw new MfeflowError('E202', `${label}: ${messageOf(error)}`, {
2188
- cause: error
2189
- });
2190
- }
2191
- if ('object' != typeof value || null === value || Array.isArray(value)) throw new MfeflowError('E202', `${label}: must be a JSON object`);
2192
- const record = Object.fromEntries(Object.entries(value));
2193
- if ('third-party' === trust) assertAllowedKeys(record, label);
2194
- assertKnownOps(record, label);
2195
- const issues = validateFeatureManifest(record);
2196
- if (issues.length > 0) {
2197
- const detail = issues.map((i)=>`${i.path || '(root)'} ${i.message}`).join('; ');
2198
- throw new MfeflowError('E202', `${label}: ${detail}`);
2199
- }
2200
- const manifest = toManifest(record, label);
2201
- if ('third-party' === trust) assertThirdPartyContent(manifest, label);
2202
- return manifest;
2203
- }
2204
- function toManifest(record, label) {
2205
- const manifest = {
2206
- name: '',
2207
- version: ''
2208
- };
2209
- Object.assign(manifest, record);
2210
- if ('' === manifest.name) throw new MfeflowError('E202', `${label}: name is required`);
2211
- return manifest;
2212
- }
2213
- async function loadFeature(name, options) {
2214
- if (!FEATURE_NAME.test(name)) throw new MfeflowError('E201', `"${name}" is not a feature name`);
2215
- if (name.startsWith('@')) return loadRemote(name, options);
2216
- return loadLocal(name, options);
2217
- }
2218
- async function loadLocal(name, options) {
2219
- const ops = options.fs ?? realFs;
2220
- if (!options.featuresDir) throw new MfeflowError('E201', `"${name}" (no features folder configured)`);
2221
- const dir = node_path.join(options.featuresDir, name);
2222
- const text = await readIfExists(ops, node_path.join(dir, 'feature.json'));
2223
- if (void 0 === text) throw new MfeflowError('E201', `"${name}" (looked in ${options.featuresDir})`);
2224
- const manifest = parseFeature(text, 'first-party', `${name}/feature.json`);
2225
- if (manifest.name !== name) throw new MfeflowError('E202', `${name}/feature.json: name is "${manifest.name}", expected "${name}"`);
2226
- return {
2227
- manifest,
2228
- trust: 'first-party',
2229
- readAsset: async (relative)=>{
2230
- if (!isSafeRelativePath(relative)) throw new MfeflowError('E105', `"${relative}"`);
2231
- const content = await readIfExists(ops, node_path.join(dir, ...relative.split('/')));
2232
- if (void 0 === content) throw new MfeflowError('E201', `${name}: file "${relative}" is missing`);
2233
- return content;
2234
- }
2235
- };
2236
- }
2237
- async function loadRemote(name, options) {
2238
- const registries = options.registries ?? {};
2239
- const text = await fetchRemoteText(name, registries, options.fetch, options.version, true === options.trusted);
2240
- const manifest = parseFeature(text, 'third-party', name);
2241
- if (manifest.name !== name) throw new MfeflowError('E402', `asked for ${name}, got ${manifest.name}`);
2242
- if (true === options.checkIndex) await assertIndexAgrees(name, manifest.version, registries, options);
2243
- return {
2244
- manifest,
2245
- trust: 'third-party',
2246
- integrity: integrityOf(text)
2247
- };
2248
- }
2249
- async function assertIndexAgrees(name, version, registries, options) {
2250
- const slash = name.indexOf('/');
2251
- let listed;
2252
- try {
2253
- listed = await indexedVersion(name.slice(0, slash), name.slice(slash + 1), registries, options.fetch, true === options.trusted);
2254
- } catch (error) {
2255
- if (error instanceof MfeflowError && 'MFEFLOW_E401' === error.code) return;
2256
- throw error;
2257
- }
2258
- if (void 0 !== listed && listed !== version) throw new MfeflowError('E402', `the index lists ${name} at ${listed} but its feature.json is ${version}`);
2259
- }
2260
- async function listLocalFeatures(featuresDir, ops = realFs) {
2261
- let dirs;
2262
- try {
2263
- dirs = await ops.listDirs(featuresDir);
2264
- } catch {
2265
- return [];
2266
- }
2267
- const checked = await Promise.all(dirs.map(async (dir)=>await readIfExists(ops, node_path.join(featuresDir, dir, 'feature.json')) === void 0 ? void 0 : dir));
2268
- return checked.filter((d)=>void 0 !== d).toSorted((a, b)=>a.localeCompare(b));
2269
- }
2270
- const TRUST_FILE = 'trusted-registries.json';
2271
- const TRUST_ENV = 'MFEFLOW_TRUST_REGISTRIES';
2272
- const set = (value)=>void 0 === value || '' === value ? void 0 : value;
2273
- function configDirOf(env, platform = process.platform, home = node_os.homedir()) {
2274
- const override = set(env.MFEFLOW_CONFIG_DIR);
2275
- if (void 0 !== override) return override;
2276
- const xdg = set(env.XDG_CONFIG_HOME);
2277
- if (void 0 !== xdg) return node_path.join(xdg, 'mfeflow');
2278
- const appData = set(env.APPDATA);
2279
- if ('win32' === platform && void 0 !== appData) return node_path.join(appData, 'mfeflow');
2280
- return node_path.join(home, '.config', 'mfeflow');
2281
- }
2282
- function trustFileIn(configDir) {
2283
- return node_path.join(configDir, TRUST_FILE);
2284
- }
2285
- function isEntry(value) {
2286
- if ('object' != typeof value || null === value) return false;
2287
- const { namespace, template } = Object.fromEntries(Object.entries(value));
2288
- return 'string' == typeof namespace && REGISTRY_NAMESPACE.test(namespace) && 'string' == typeof template && void 0 === registryTemplateProblem(template);
2289
- }
2290
- async function readTrusted(configDir, ops = realFs) {
2291
- let text;
2292
- try {
2293
- text = await readIfExists(ops, trustFileIn(configDir));
2294
- } catch {
2295
- return [];
2296
- }
2297
- if (void 0 === text) return [];
2298
- try {
2299
- const value = JSON.parse(text);
2300
- if ('object' != typeof value || null === value) return [];
2301
- const { registries } = Object.fromEntries(Object.entries(value));
2302
- return Array.isArray(registries) ? registries.filter(isEntry) : [];
2303
- } catch {
2304
- return [];
2305
- }
2306
- }
2307
- async function save(configDir, entries, ops) {
2308
- const sorted = entries.toSorted((a, b)=>a.namespace.localeCompare(b.namespace) || a.template.localeCompare(b.template));
2309
- const body = `${JSON.stringify({
2310
- version: 1,
2311
- registries: sorted
2312
- }, null, 2)}\n`;
2313
- try {
2314
- await writeFileAtomic(ops, trustFileIn(configDir), body);
2315
- } catch (error) {
2316
- throw new MfeflowError('E106', trustFileIn(configDir), {
2317
- cause: error
2318
- });
2319
- }
2320
- }
2321
- async function addTrusted(configDir, entry, ops = realFs) {
2322
- const current = await readTrusted(configDir, ops);
2323
- if (current.some((e)=>e.namespace === entry.namespace && e.template === entry.template)) return false;
2324
- await save(configDir, [
2325
- ...current,
2326
- entry
2327
- ], ops);
2328
- return true;
2329
- }
2330
- async function removeTrusted(configDir, namespace, ops = realFs) {
2331
- const current = await readTrusted(configDir, ops);
2332
- const kept = current.filter((e)=>e.namespace !== namespace);
2333
- if (kept.length === current.length) return 0;
2334
- await save(configDir, kept, ops);
2335
- return current.length - kept.length;
2336
- }
2337
- function parseTrustEnv(value) {
2338
- return (value ?? '').split(',').map((part)=>part.trim()).flatMap((part)=>{
2339
- const eq = part.indexOf('=');
2340
- const namespace = -1 === eq ? part : part.slice(0, eq).trim();
2341
- if (!REGISTRY_NAMESPACE.test(namespace)) return [];
2342
- const template = -1 === eq ? '' : part.slice(eq + 1).trim();
2343
- return [
2344
- '' === template ? {
2345
- namespace
2346
- } : {
2347
- namespace,
2348
- template
2349
- }
2350
- ];
2351
- });
2352
- }
2353
- function trustedNamespaces(registries, recorded, fromEnv = []) {
2354
- const fetch1 = new Set();
2355
- const privateOk = new Set();
2356
- for (const [namespace, template] of Object.entries(registries)){
2357
- const inFile = recorded.some((e)=>e.namespace === namespace && e.template === template);
2358
- const items = fromEnv.filter((e)=>e.namespace === namespace);
2359
- const pinned = items.some((e)=>e.template === template);
2360
- if (inFile || pinned) privateOk.add(namespace);
2361
- if (inFile || items.some((e)=>void 0 === e.template || e.template === template)) fetch1.add(namespace);
2362
- }
2363
- return {
2364
- fetch: fetch1,
2365
- privateOk
2366
- };
2367
- }
2368
- const LETTERS = 'abcdefghijklmnopqrstuvwxyz';
2369
- function shortHash(text) {
2370
- let hash = 0x811c9dc5;
2371
- for (const char of text)hash = Math.imul(hash ^ char.codePointAt(0), 0x01000193) >>> 0;
2372
- let out = '';
2373
- for(let i = 0; i < 4; i++){
2374
- out += LETTERS[hash % 26];
2375
- hash = Math.floor(hash / 26);
2094
+ const LETTERS = 'abcdefghijklmnopqrstuvwxyz';
2095
+ function shortHash(text) {
2096
+ let hash = 0x811c9dc5;
2097
+ for (const char of text)hash = Math.imul(hash ^ char.codePointAt(0), 0x01000193) >>> 0;
2098
+ let out = '';
2099
+ for(let i = 0; i < 4; i++){
2100
+ out += LETTERS[hash % 26];
2101
+ hash = Math.floor(hash / 26);
2376
2102
  }
2377
2103
  return out;
2378
2104
  }
@@ -2471,8 +2197,15 @@ async function resolveFeature(input) {
2471
2197
  for (const needed of manifest.requires ?? [])if (!present.has(needed)) throw new MfeflowError('E206', `${manifest.name} needs ${needed}`);
2472
2198
  const options = resolveOptions(manifest, input.options);
2473
2199
  if ('tailwind' === manifest.name) assertUniqueTailwindPrefixes(Object.keys(project.apps));
2200
+ const apps = Object.entries(project.apps).map(([name, app])=>({
2201
+ name,
2202
+ kind: app.kind,
2203
+ path: app.path,
2204
+ port: app.port
2205
+ }));
2474
2206
  const dataFor = (place)=>({
2475
2207
  scope: project.scope,
2208
+ apps,
2476
2209
  topology: project.topology,
2477
2210
  packageManager: project.packageManager,
2478
2211
  options,
@@ -2559,6 +2292,2131 @@ async function editFile(builder, file, edit, op) {
2559
2292
  if (isConfig && !await builder.exists(file)) return void await builder.create(file, edit('{}\n'));
2560
2293
  await builder.patch(file, edit);
2561
2294
  }
2295
+ const upgrade_WORKSPACE_FILE = 'pnpm-workspace.yaml';
2296
+ const upgrade_TURBO_FILE = 'turbo.json';
2297
+ const upgrade_DEPENDENCY_SECTIONS = [
2298
+ 'dependencies',
2299
+ 'devDependencies',
2300
+ 'optionalDependencies'
2301
+ ];
2302
+ const OWN_PREFIX = '@mfeflow/';
2303
+ const OWN_CLI = 'mfeflow';
2304
+ const PLACEHOLDER_RANGE = 'latest';
2305
+ const USER_DATA = /^registry\/envs\//;
2306
+ const ENV_KEY_FILES = /(^|\/)(env\.ts|src\/config\/schema\.ts|runtime-config(\.[^/]+)?\.json|\.env[^/]*)$/;
2307
+ const isEnvKeyFile = (file)=>ENV_KEY_FILES.test(file);
2308
+ function recordEnvKey(project, files, key) {
2309
+ const touched = files.filter(isEnvKeyFile);
2310
+ if (0 === touched.length) return project;
2311
+ const envKeys = {
2312
+ ...project.envKeys
2313
+ };
2314
+ for (const file of touched)envKeys[file] = [
2315
+ ...new Set([
2316
+ ...envKeys[file] ?? [],
2317
+ key
2318
+ ])
2319
+ ].toSorted(byCodePoint);
2320
+ return {
2321
+ ...project,
2322
+ envKeys
2323
+ };
2324
+ }
2325
+ const KEEP_RANGE = /^(workspace|link|file|portal):/;
2326
+ const byCodePoint = (a, b)=>a < b ? -1 : a > b ? 1 : 0;
2327
+ const isOwn = (name)=>name === OWN_CLI || name.startsWith(OWN_PREFIX);
2328
+ const isPackageFile = (file)=>'package.json' === file || file.endsWith('/package.json');
2329
+ const isStructured = (file)=>isPackageFile(file) || file === upgrade_WORKSPACE_FILE || file === upgrade_TURBO_FILE;
2330
+ const sameText = (a, b)=>normalizeEol(a) === normalizeEol(b);
2331
+ async function runtimeRange(pkgFile, read) {
2332
+ const source = await read(pkgFile);
2333
+ if (void 0 === source) return;
2334
+ const pkg = parseJsonObject(pkgFile, source);
2335
+ for (const section of upgrade_DEPENDENCY_SECTIONS){
2336
+ const value = asRecord(pkg[section])['@mfeflow/runtime'];
2337
+ if ('string' == typeof value) {
2338
+ if (!value.startsWith('catalog:')) return {
2339
+ range: value,
2340
+ catalog: void 0
2341
+ };
2342
+ return {
2343
+ range: value,
2344
+ catalog: value.slice(8)
2345
+ };
2346
+ }
2347
+ }
2348
+ }
2349
+ async function catalogRange(read, catalogName) {
2350
+ const source = await read(upgrade_WORKSPACE_FILE);
2351
+ if (void 0 === source) return;
2352
+ const doc = parseDocument(source);
2353
+ const where = '' === catalogName || 'default' === catalogName ? [
2354
+ 'catalog'
2355
+ ] : [
2356
+ 'catalogs',
2357
+ catalogName
2358
+ ];
2359
+ const value = doc.getIn([
2360
+ ...where,
2361
+ '@mfeflow/runtime'
2362
+ ]);
2363
+ return 'string' == typeof value ? value : void 0;
2364
+ }
2365
+ function resolveRange(found, read) {
2366
+ return void 0 === found.catalog ? Promise.resolve(found.range) : catalogRange(read, found.catalog);
2367
+ }
2368
+ async function detectBaseline(project, read) {
2369
+ if (void 0 !== project.cli) return {
2370
+ version: project.cli,
2371
+ source: 'mfeflow.json'
2372
+ };
2373
+ const apps = Object.values(project.apps).toSorted((a, b)=>Number('shell' === b.kind) - Number('shell' === a.kind));
2374
+ for (const app of apps){
2375
+ const file = '' === app.path ? 'package.json' : `${app.path}/package.json`;
2376
+ const found = await runtimeRange(file, read);
2377
+ if (void 0 === found) continue;
2378
+ const range = await resolveRange(found, read);
2379
+ const min = void 0 === range || KEEP_RANGE.test(range) ? void 0 : minimum(range);
2380
+ if (void 0 === min) throw new MfeflowError('E601', `${file} has @mfeflow/runtime at ${JSON.stringify(found.range)}, which does not name a version`);
2381
+ return {
2382
+ version: min,
2383
+ source: 'package.json'
2384
+ };
2385
+ }
2386
+ throw new MfeflowError('E601', 'mfeflow.json has no "cli" and no app depends on @mfeflow/runtime');
2387
+ }
2388
+ function minimum(range) {
2389
+ if (null === semver.validRange(range)) return;
2390
+ const min = semver.minVersion(range)?.version;
2391
+ return '0.0.0' === min ? void 0 : min;
2392
+ }
2393
+ function recordedOptions(feature, project, warnings) {
2394
+ const recorded = project.features[feature.manifest.name]?.options ?? {};
2395
+ const known = new Set((feature.manifest.prompts ?? []).map((p)=>p.name));
2396
+ const kept = {};
2397
+ for (const [key, value] of Object.entries(recorded))if (known.has(key)) kept[key] = value;
2398
+ else warnings.push(`${feature.manifest.name} no longer has the option "${key}"; it was ignored`);
2399
+ return kept;
2400
+ }
2401
+ const depKey = (file, name)=>`${file}\0${name}`;
2402
+ async function replay(project, input) {
2403
+ const local = void 0 !== project.local;
2404
+ const builder = new PlanBuilder(()=>Promise.resolve(void 0), {
2405
+ catalog: true,
2406
+ ...local ? {} : {
2407
+ rewriteRange: (name, range)=>isOwn(name) && range === PLACEHOLDER_RANGE ? `^${input.toVersion}` : void 0
2408
+ }
2409
+ });
2410
+ const warnings = [];
2411
+ const names = [
2412
+ ...input.features.map((f)=>f.manifest.name),
2413
+ ...(input.skipped ?? []).map((s)=>s.name)
2414
+ ];
2415
+ const bare = {
2416
+ ...project,
2417
+ features: {}
2418
+ };
2419
+ const steps = [
2420
+ ...input.features.map((feature)=>({
2421
+ feature,
2422
+ options: recordedOptions(feature, project, warnings)
2423
+ })),
2424
+ ...(input.glue ?? []).map((feature)=>({
2425
+ feature,
2426
+ options: {}
2427
+ }))
2428
+ ];
2429
+ for (const step of steps)await resolveFeature({
2430
+ ...step,
2431
+ project: bare,
2432
+ builder,
2433
+ alsoAdding: names
2434
+ });
2435
+ const built = builder.build();
2436
+ const files = new Map();
2437
+ const tracked = new Set();
2438
+ const thirdParty = new Set();
2439
+ for (const change of built.changes){
2440
+ if ('third-party' === change.trust) thirdParty.add(change.path);
2441
+ if (null !== change.after) {
2442
+ files.set(change.path, change.after);
2443
+ if (change.track) tracked.add(change.path);
2444
+ }
2445
+ }
2446
+ const thirdPartyDeps = new Set(built.dependencies.filter((d)=>true === d.thirdParty).map((d)=>depKey(d.file, d.name)));
2447
+ const byName = new Map(built.features.map((f)=>[
2448
+ f.name,
2449
+ f
2450
+ ]));
2451
+ return {
2452
+ files,
2453
+ tracked,
2454
+ thirdParty,
2455
+ thirdPartyDeps,
2456
+ features: [
2457
+ ...byName.values()
2458
+ ],
2459
+ warnings
2460
+ };
2461
+ }
2462
+ function keepLineEndings(change) {
2463
+ if (null === change.after || !change.before?.includes('\r\n')) return change;
2464
+ return {
2465
+ ...change,
2466
+ after: normalizeEol(change.after).replaceAll('\n', '\r\n')
2467
+ };
2468
+ }
2469
+ function unifiedDiff(file, yours, incoming) {
2470
+ return formatPatch(structuredPatch(`a/${file} (yours)`, `b/${file} (new)`, yours, incoming));
2471
+ }
2472
+ function entriesOf(file, source) {
2473
+ const base = file.slice(file.lastIndexOf('/') + 1);
2474
+ try {
2475
+ if (base.endsWith('.json')) {
2476
+ const json = parseJsonObject(file, source);
2477
+ return new Map(Object.entries(json).map(([key, value])=>[
2478
+ key,
2479
+ JSON.stringify(value)
2480
+ ]));
2481
+ }
2482
+ if (base.startsWith('.env')) {
2483
+ const values = new Map();
2484
+ for (const line of source.split('\n')){
2485
+ const match = /^\s*(?:export\s+)?([A-Za-z_][A-Za-z0-9_]*)\s*=(.*)$/.exec(line);
2486
+ if (match?.[1] !== void 0) values.set(match[1], (match[2] ?? '').trim());
2487
+ }
2488
+ return values;
2489
+ }
2490
+ const keys = schemaKeys(source, file, 'env.ts' === base ? 'server' : 'runtime');
2491
+ return void 0 === keys ? void 0 : new Map(keys.map((k)=>[
2492
+ k.name,
2493
+ k.schema.replaceAll(/\s+/g, ' ')
2494
+ ]));
2495
+ } catch {
2496
+ return;
2497
+ }
2498
+ }
2499
+ function lostKeys(file, disk, incoming) {
2500
+ const yours = entriesOf(file, disk);
2501
+ const wanted = entriesOf(file, incoming);
2502
+ if (void 0 === yours || void 0 === wanted) return;
2503
+ const missing = [
2504
+ ...yours
2505
+ ].filter(([key])=>!wanted.has(key)).map(([key])=>key);
2506
+ const changed = [
2507
+ ...yours
2508
+ ].filter(([key, value])=>wanted.has(key) && wanted.get(key) !== value).map(([key])=>key);
2509
+ return {
2510
+ missing,
2511
+ changed
2512
+ };
2513
+ }
2514
+ const writes = (verdict)=>'create' === verdict || 'replace' === verdict || 'missing' !== verdict && 'matching' !== verdict && 'leave' !== verdict && 'symlink' !== verdict;
2515
+ function judge(file, disk, incoming, state) {
2516
+ const { recorded, kept, anySkipped, envKeys = [] } = state;
2517
+ if (void 0 === disk) {
2518
+ if (void 0 !== recorded) return 'missing';
2519
+ return USER_DATA.test(file) ? 'leave' : 'create';
2520
+ }
2521
+ if (sameText(disk, incoming)) return void 0 !== recorded && recorded !== hashContent(disk) ? 'matching' : 'leave';
2522
+ if (USER_DATA.test(file)) return 'leave';
2523
+ if (void 0 !== kept) return kept === hashContent(incoming) ? 'leave' : 'edited';
2524
+ if (void 0 !== recorded && recorded === hashContent(incoming)) return 'leave';
2525
+ if (void 0 === recorded) return 'untracked';
2526
+ if (recorded !== hashContent(disk)) return 'edited';
2527
+ if (ENV_KEY_FILES.test(file)) {
2528
+ const lost = lostKeys(file, disk, incoming);
2529
+ if (void 0 === lost || lost.missing.length > 0) return 'edited-by-cli';
2530
+ if (lost.changed.some((key)=>envKeys.includes(key))) return 'edited-by-cli';
2531
+ }
2532
+ return anySkipped ? 'skipped-feature' : 'replace';
2533
+ }
2534
+ async function compareFiles(project, fresh, read, builder, options) {
2535
+ const out = {
2536
+ diverged: [],
2537
+ kept: [],
2538
+ missing: [],
2539
+ matching: [],
2540
+ notes: []
2541
+ };
2542
+ const entries = [
2543
+ ...fresh.files
2544
+ ].toSorted(([a], [b])=>a < b ? -1 : 1);
2545
+ for (const [file, incoming] of entries){
2546
+ const disk = await read(file);
2547
+ if (isStructured(file) && void 0 !== disk) continue;
2548
+ let verdict = judge(file, disk, incoming, {
2549
+ ...project.files?.[file] === void 0 ? {} : {
2550
+ recorded: project.files[file]
2551
+ },
2552
+ ...project.kept?.[file] === void 0 ? {} : {
2553
+ kept: project.kept[file]
2554
+ },
2555
+ anySkipped: options.anySkipped,
2556
+ ...project.envKeys?.[file] === void 0 ? {} : {
2557
+ envKeys: project.envKeys[file]
2558
+ }
2559
+ });
2560
+ if (writes(verdict) && await options.isSymlink?.(file)) verdict = 'symlink';
2561
+ if ('missing' === verdict) out.missing.push(file);
2562
+ else if ('matching' === verdict) out.matching.push(file);
2563
+ else if ('create' === verdict) await builder.create(file, incoming, fresh.tracked.has(file));
2564
+ else if ('replace' === verdict) {
2565
+ const changed = ENV_KEY_FILES.test(file) ? lostKeys(file, disk ?? '', incoming)?.changed : [];
2566
+ 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`);
2567
+ await builder.replace(file, incoming);
2568
+ } else if ('leave' !== verdict) {
2569
+ const entry = diverge(file, verdict, disk ?? '', incoming);
2570
+ const decision = 'symlink' === verdict ? void 0 : decide(file, options.decisions);
2571
+ if (void 0 !== decision) entry.decision = decision;
2572
+ out.diverged.push(entry);
2573
+ if ('accept' === decision) await builder.replace(file, incoming);
2574
+ else if ('keep' === decision) out.kept.push(entry);
2575
+ }
2576
+ }
2577
+ return out;
2578
+ }
2579
+ function diverge(file, reason, yours, incoming) {
2580
+ const entry = {
2581
+ path: file,
2582
+ reason,
2583
+ yours,
2584
+ incoming,
2585
+ diff: unifiedDiff(file, yours, incoming)
2586
+ };
2587
+ const lost = ENV_KEY_FILES.test(file) ? lostKeys(file, yours, incoming) : void 0;
2588
+ if (void 0 !== lost && lost.missing.length + lost.changed.length > 0) {
2589
+ const all = new Set([
2590
+ ...lost.missing,
2591
+ ...lost.changed
2592
+ ]);
2593
+ entry.lostKeys = [
2594
+ ...entriesOf(file, yours)?.keys() ?? []
2595
+ ].filter((key)=>all.has(key));
2596
+ if (lost.changed.length > 0) entry.changedValues = lost.changed;
2597
+ }
2598
+ return entry;
2599
+ }
2600
+ const upgrade_named = (set, file)=>'all' === set || set?.has(file) === true;
2601
+ function decide(file, decisions) {
2602
+ const accept = upgrade_named(decisions.accept, file);
2603
+ const keep = upgrade_named(decisions.keep, file);
2604
+ if (accept && keep) return 'all' === decisions.accept && 'all' !== decisions.keep ? 'keep' : 'accept';
2605
+ if (accept) return 'accept';
2606
+ return keep ? 'keep' : void 0;
2607
+ }
2608
+ async function editWorkspace(fresh, read, builder, local) {
2609
+ const incoming = fresh.files.get(upgrade_WORKSPACE_FILE);
2610
+ const disk = await read(upgrade_WORKSPACE_FILE);
2611
+ if (void 0 === incoming || void 0 === disk) return;
2612
+ const wanted = asRecord(parseDocument(incoming).toJSON());
2613
+ const doc = parseDocument(disk);
2614
+ let changed = false;
2615
+ for (const section of [
2616
+ 'catalog',
2617
+ 'allowBuilds'
2618
+ ]){
2619
+ const have = doc.get(section);
2620
+ for (const [name, value] of Object.entries(asRecord(wanted[section])))if (!(isMap(have) && have.has(name) || local && isOwn(name))) {
2621
+ doc.setIn([
2622
+ section,
2623
+ name
2624
+ ], value);
2625
+ changed = true;
2626
+ }
2627
+ }
2628
+ if (changed) await builder.patch(upgrade_WORKSPACE_FILE, ()=>doc.toString());
2629
+ }
2630
+ async function editTurbo(fresh, read, builder) {
2631
+ const incoming = fresh.files.get(upgrade_TURBO_FILE);
2632
+ if (void 0 === incoming || await read(upgrade_TURBO_FILE) === void 0) return;
2633
+ const wanted = asRecord(asRecord(parseJsonObject(upgrade_TURBO_FILE, incoming)).tasks);
2634
+ await builder.patch(upgrade_TURBO_FILE, (source)=>editJsonObject(upgrade_TURBO_FILE, source, (turbo)=>{
2635
+ const tasks = asRecord(turbo.tasks);
2636
+ const missing = Object.entries(wanted).filter(([name])=>!(name in tasks));
2637
+ if (0 === missing.length) return false;
2638
+ turbo.tasks = {
2639
+ ...tasks,
2640
+ ...Object.fromEntries(missing)
2641
+ };
2642
+ return true;
2643
+ }));
2644
+ }
2645
+ function upgrade_listed(pkg) {
2646
+ return upgrade_DEPENDENCY_SECTIONS.flatMap((section)=>Object.entries(asRecord(pkg[section])).flatMap(([name, range])=>'string' == typeof range ? [
2647
+ [
2648
+ name,
2649
+ range
2650
+ ]
2651
+ ] : []));
2652
+ }
2653
+ function upgrade_declared(pkg) {
2654
+ const out = new Map();
2655
+ 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);
2656
+ return out;
2657
+ }
2658
+ async function addMissing(file, pkg, wanted, local, builder) {
2659
+ const have = upgrade_declared(pkg);
2660
+ for (const [section, dev] of [
2661
+ [
2662
+ 'dependencies',
2663
+ false
2664
+ ],
2665
+ [
2666
+ 'devDependencies',
2667
+ true
2668
+ ]
2669
+ ])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);
2670
+ const scripts = asRecord(pkg.scripts);
2671
+ for (const [name, command] of Object.entries(asRecord(wanted.scripts)))if (!(name in scripts) && 'string' == typeof command) await builder.addScript(file, name, command);
2672
+ }
2673
+ async function editPackage(file, fresh, toVersion, local, builder) {
2674
+ const source = await builder.content(file);
2675
+ if (void 0 === source) return;
2676
+ const incoming = fresh.files.get(file);
2677
+ if (void 0 !== incoming) await addMissing(file, parseJsonObject(file, source), parseJsonObject(file, incoming), local, builder);
2678
+ if (local) return;
2679
+ const target = `^${toVersion}`;
2680
+ const current = parseJsonObject(file, await builder.content(file) ?? source);
2681
+ for (const [name, range] of upgrade_listed(current))if (!(!isOwn(name) || range === target || KEEP_RANGE.test(range))) await builder.setRange(file, name, target);
2682
+ }
2683
+ function pendingMigrations(features, project) {
2684
+ const pending = [];
2685
+ for (const feature of features){
2686
+ const { name, version, migrations } = feature.manifest;
2687
+ const recorded = project.features[name]?.version;
2688
+ if (void 0 !== recorded && null !== semver.valid(recorded)) {
2689
+ if ('first-party' === feature.trust) {
2690
+ for (const m of migrations ?? [])if (null !== semver.valid(m.to) && null !== semver.valid(version)) {
2691
+ if (semver.lt(recorded, m.to) && semver.lte(m.to, version)) pending.push({
2692
+ feature,
2693
+ ...m
2694
+ });
2695
+ }
2696
+ }
2697
+ }
2698
+ }
2699
+ return pending.toSorted((a, b)=>semver.compare(a.to, b.to));
2700
+ }
2701
+ async function importDefault(file) {
2702
+ return import(pathToFileURL(file).href);
2703
+ }
2704
+ async function loadMigration(step, importModule) {
2705
+ const name = step.feature.manifest.name;
2706
+ const label = `${name} ${step.from} to ${step.to} (${step.module})`;
2707
+ const { dir } = step.feature;
2708
+ if (void 0 === dir || !isSafeRelativePath(step.module)) throw new MfeflowError('E603', `${label}: cannot find ${step.module}`);
2709
+ let loaded;
2710
+ try {
2711
+ loaded = await importModule(node_path.join(dir, ...step.module.split('/')));
2712
+ } catch (error) {
2713
+ throw new MfeflowError('E603', `${label}: ${messageOf(error)}`, {
2714
+ cause: error
2715
+ });
2716
+ }
2717
+ const exported = asRecord(loaded);
2718
+ const fn = 'function' == typeof exported.default ? exported.default : exported.migrate;
2719
+ if ('function' != typeof fn) throw new MfeflowError('E603', `${label}: ${step.module} exports no migration function`);
2720
+ return async (builder, project)=>{
2721
+ try {
2722
+ await Reflect.apply(fn, void 0, [
2723
+ builder,
2724
+ project
2725
+ ]);
2726
+ } catch (error) {
2727
+ throw new MfeflowError('E603', `${label}: ${messageOf(error)}`, {
2728
+ cause: error
2729
+ });
2730
+ }
2731
+ };
2732
+ }
2733
+ async function runMigrations(project, input, builder) {
2734
+ const importModule = input.importModule ?? importDefault;
2735
+ const ran = [];
2736
+ for (const step of pendingMigrations(input.features, project)){
2737
+ const migrate = await loadMigration(step, importModule);
2738
+ await migrate(builder.scoped('first-party'), project);
2739
+ ran.push({
2740
+ feature: step.feature.manifest.name,
2741
+ from: step.from,
2742
+ to: step.to
2743
+ });
2744
+ }
2745
+ return ran;
2746
+ }
2747
+ async function findRemoved(project, fresh, read) {
2748
+ const removed = [];
2749
+ for (const file of Object.keys(project.files ?? {}).toSorted(byCodePoint))if (!(fresh.files.has(file) || USER_DATA.test(file) || ENV_KEY_FILES.test(file))) {
2750
+ if (await read(file) !== void 0) removed.push(file);
2751
+ }
2752
+ return removed;
2753
+ }
2754
+ async function planUpgrade(project, input) {
2755
+ if (null === semver.valid(input.toVersion)) throw new MfeflowError('E601', `"${input.toVersion}" is not a version`);
2756
+ const baseline = await detectBaseline(project, input.read);
2757
+ 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}`);
2758
+ const fresh = await replay(project, input);
2759
+ const builder = new PlanBuilder(input.read, {});
2760
+ await builder.captureProject();
2761
+ const compared = await compareFiles(project, fresh, input.read, builder, {
2762
+ anySkipped: (input.skipped ?? []).length > 0,
2763
+ decisions: input.decisions ?? {},
2764
+ ...input.isSymlink ? {
2765
+ isSymlink: input.isSymlink
2766
+ } : {}
2767
+ });
2768
+ await editWorkspace(fresh, input.read, builder, void 0 !== project.local);
2769
+ await editTurbo(fresh, input.read, builder);
2770
+ const local = void 0 !== project.local;
2771
+ const packageFiles = new Set([
2772
+ ...fresh.files.keys(),
2773
+ ...Object.values(project.apps).map((a)=>('' === a.path ? '' : `${a.path}/`) + 'package.json'),
2774
+ 'package.json'
2775
+ ].filter(isPackageFile));
2776
+ for (const file of [
2777
+ ...packageFiles
2778
+ ].toSorted(byCodePoint))if (await input.read(file)) await editPackage(file, fresh, input.toVersion, local, builder);
2779
+ const migrations = await runMigrations(project, input, builder);
2780
+ const skipped = input.skipped ?? [];
2781
+ const planned = (file)=>builder.content(file);
2782
+ const removed = 0 === skipped.length ? await findRemoved(project, fresh, planned) : [];
2783
+ const built = builder.build();
2784
+ const plan = {
2785
+ ...built,
2786
+ changes: built.changes.map((change)=>{
2787
+ const kept = keepLineEndings(change);
2788
+ kept.trust = fresh.thirdParty.has(kept.path) ? 'third-party' : kept.trust;
2789
+ return kept;
2790
+ }),
2791
+ dependencies: built.dependencies.map((dep)=>{
2792
+ if (fresh.thirdPartyDeps.has(depKey(dep.file, dep.name))) dep.thirdParty = true;
2793
+ return dep;
2794
+ }),
2795
+ features: fresh.features,
2796
+ warnings: [
2797
+ ...fresh.warnings,
2798
+ ...compared.notes,
2799
+ ...built.warnings,
2800
+ ...skipped.map(({ name, reason })=>`${name} was not replayed (${reason}); its files were left as they are`)
2801
+ ]
2802
+ };
2803
+ const files = {
2804
+ ...project.files
2805
+ };
2806
+ const kept = {
2807
+ ...project.kept
2808
+ };
2809
+ for (const file of compared.matching)files[file] = hashContent(fresh.files.get(file) ?? '');
2810
+ for (const entry of compared.diverged)if ('accept' === entry.decision) {
2811
+ files[entry.path] = hashContent(entry.incoming);
2812
+ delete kept[entry.path];
2813
+ }
2814
+ for (const entry of compared.kept){
2815
+ files[entry.path] = hashContent(entry.yours);
2816
+ kept[entry.path] = hashContent(entry.incoming);
2817
+ }
2818
+ const next = {
2819
+ ...project,
2820
+ cli: input.toVersion
2821
+ };
2822
+ if (Object.keys(files).length > 0) next.files = files;
2823
+ if (Object.keys(kept).length > 0) next.kept = kept;
2824
+ else delete next.kept;
2825
+ return {
2826
+ from: baseline.version,
2827
+ fromSource: baseline.source,
2828
+ to: input.toVersion,
2829
+ plan,
2830
+ project: next,
2831
+ diverged: compared.diverged,
2832
+ migrations,
2833
+ removed,
2834
+ missing: compared.missing
2835
+ };
2836
+ }
2837
+ const RUNTIME_CONFIG_FILE = /^runtime-config(?:\.([a-z0-9-]+))?\.json$/;
2838
+ async function runtimeConfigFiles(folder) {
2839
+ const names = await promises.readdir(folder).catch(()=>[]);
2840
+ return names.filter((name)=>RUNTIME_CONFIG_FILE.test(name)).toSorted((a, b)=>a.localeCompare(b));
2841
+ }
2842
+ const ok = (value)=>({
2843
+ ok: true,
2844
+ value
2845
+ });
2846
+ const evaluate_fail = (reason)=>({
2847
+ ok: false,
2848
+ reason
2849
+ });
2850
+ function evaluate_unwrap(node) {
2851
+ if (!node) return;
2852
+ if (isTSAsExpression(node) || isTSSatisfiesExpression(node) || isTSNonNullExpression(node) || isTSTypeAssertion(node)) return evaluate_unwrap(node.expression);
2853
+ return node;
2854
+ }
2855
+ function requireSource(node) {
2856
+ const n = evaluate_unwrap(node);
2857
+ if (isCallExpression(n) && isIdentifier(n.callee, {
2858
+ name: 'require'
2859
+ })) {
2860
+ const arg = n.arguments[0];
2861
+ return isStringLiteral(arg) ? {
2862
+ source: arg.value,
2863
+ path: []
2864
+ } : void 0;
2865
+ }
2866
+ if (isMemberExpression(n) && !n.computed && isIdentifier(n.property)) {
2867
+ const inner = requireSource(n.object);
2868
+ return inner ? {
2869
+ source: inner.source,
2870
+ path: [
2871
+ ...inner.path,
2872
+ n.property.name
2873
+ ]
2874
+ } : void 0;
2875
+ }
2876
+ }
2877
+ function evaluate_keyName(key) {
2878
+ if (isIdentifier(key)) return key.name;
2879
+ if (isStringLiteral(key)) return key.value;
2880
+ }
2881
+ function bindVariables(statement, bindings) {
2882
+ if (!isVariableDeclaration(statement)) return;
2883
+ for (const decl of statement.declarations){
2884
+ const required = requireSource(decl.init);
2885
+ if (isIdentifier(decl.id)) if (required) bindings.set(decl.id.name, {
2886
+ kind: 'import',
2887
+ source: required.source,
2888
+ imported: required.path.at(-1) ?? 'default',
2889
+ depth: required.path.length
2890
+ });
2891
+ else if ('const' === statement.kind && decl.init) bindings.set(decl.id.name, {
2892
+ kind: 'const',
2893
+ init: decl.init
2894
+ });
2895
+ else bindings.set(decl.id.name, {
2896
+ kind: 'other',
2897
+ what: `a ${statement.kind} variable`
2898
+ });
2899
+ else if (isObjectPattern(decl.id)) for (const prop of decl.id.properties){
2900
+ if (!isObjectProperty(prop) || !isIdentifier(prop.value)) continue;
2901
+ const imported = evaluate_keyName(prop.key);
2902
+ if (required && imported) bindings.set(prop.value.name, {
2903
+ kind: 'import',
2904
+ source: required.source,
2905
+ imported,
2906
+ depth: required.path.length + 1
2907
+ });
2908
+ else bindings.set(prop.value.name, {
2909
+ kind: 'other',
2910
+ what: 'a destructured value'
2911
+ });
2912
+ }
2913
+ }
2914
+ }
2915
+ function isModuleExports(node) {
2916
+ return isMemberExpression(node) && isIdentifier(node.object, {
2917
+ name: 'module'
2918
+ }) && isIdentifier(node.property, {
2919
+ name: 'exports'
2920
+ });
2921
+ }
2922
+ const MUTATING_METHODS = new Set([
2923
+ 'push',
2924
+ 'pop',
2925
+ 'shift',
2926
+ 'unshift',
2927
+ 'splice',
2928
+ 'sort',
2929
+ 'reverse',
2930
+ 'fill',
2931
+ 'copyWithin',
2932
+ 'set',
2933
+ 'add',
2934
+ 'delete',
2935
+ 'clear',
2936
+ 'assign'
2937
+ ]);
2938
+ const MUTATING_STATICS = new Set([
2939
+ 'assign',
2940
+ 'defineProperty',
2941
+ 'defineProperties',
2942
+ 'setPrototypeOf'
2943
+ ]);
2944
+ function rootName(node) {
2945
+ let n = evaluate_unwrap(node);
2946
+ while(isMemberExpression(n) || n?.type === 'OptionalMemberExpression')n = evaluate_unwrap(n.object);
2947
+ return isIdentifier(n) ? n.name : void 0;
2948
+ }
2949
+ function mutatedNames(ast) {
2950
+ const names = new Set();
2951
+ const add = (name)=>{
2952
+ if (name) names.add(name);
2953
+ };
2954
+ for (const node of findAll(ast, (n)=>isAssignmentExpression(n) || 'UpdateExpression' === n.type || isUnaryExpression(n) || isCallExpression(n)))if (isAssignmentExpression(node)) add(rootName(node.left));
2955
+ else if ('UpdateExpression' === node.type) add(rootName(node.argument));
2956
+ else if (isUnaryExpression(node)) {
2957
+ if ('delete' === node.operator) add(rootName(node.argument));
2958
+ } else if (isCallExpression(node)) {
2959
+ const callee = evaluate_unwrap(node.callee);
2960
+ if (!isMemberExpression(callee) || callee.computed || !isIdentifier(callee.property)) continue;
2961
+ if (isIdentifier(callee.object, {
2962
+ name: 'Object'
2963
+ }) && MUTATING_STATICS.has(callee.property.name)) add(rootName(node.arguments[0]));
2964
+ else if (MUTATING_METHODS.has(callee.property.name)) add(rootName(callee.object));
2965
+ }
2966
+ return names;
2967
+ }
2968
+ function scopeOf(ast, file) {
2969
+ const bindings = new Map();
2970
+ const exports = new Map();
2971
+ const exportedFrom = new Map();
2972
+ const localExports = [];
2973
+ for (const statement of ast.program.body)if (isImportDeclaration(statement)) for (const spec of statement.specifiers){
2974
+ const imported = isImportDefaultSpecifier(spec) ? 'default' : isImportNamespaceSpecifier(spec) ? '*' : evaluate_keyName(spec.imported) ?? '*';
2975
+ bindings.set(spec.local.name, {
2976
+ kind: 'import',
2977
+ source: statement.source.value,
2978
+ imported
2979
+ });
2980
+ }
2981
+ else if (isVariableDeclaration(statement)) bindVariables(statement, bindings);
2982
+ else if (isFunctionDeclaration(statement) || isClassDeclaration(statement)) {
2983
+ if (statement.id) bindings.set(statement.id.name, {
2984
+ kind: 'other',
2985
+ what: 'a function or class'
2986
+ });
2987
+ } else if (isExportNamedDeclaration(statement)) {
2988
+ const decl = statement.declaration;
2989
+ if (decl && isVariableDeclaration(decl)) {
2990
+ bindVariables(decl, bindings);
2991
+ for (const d of decl.declarations)if (isIdentifier(d.id) && d.init) {
2992
+ exports.set(d.id.name, d.init);
2993
+ exportedFrom.set(d.id.name, d.id.name);
2994
+ }
2995
+ }
2996
+ if (!statement.source) for (const spec of statement.specifiers){
2997
+ if (!isExportSpecifier(spec)) continue;
2998
+ const exported = evaluate_keyName(spec.exported);
2999
+ const local = evaluate_keyName(spec.local);
3000
+ if (exported && local) localExports.push([
3001
+ exported,
3002
+ local
3003
+ ]);
3004
+ }
3005
+ } else if (isExportDefaultDeclaration(statement)) exports.set('default', statement.declaration);
3006
+ else if (isExpressionStatement(statement)) {
3007
+ const expr = statement.expression;
3008
+ if (isAssignmentExpression(expr) && isModuleExports(expr.left)) exports.set('default', expr.right);
3009
+ }
3010
+ for (const [exported, local] of localExports){
3011
+ const binding = bindings.get(local);
3012
+ if (binding?.kind === 'const') {
3013
+ exports.set(exported, binding.init);
3014
+ exportedFrom.set(exported, local);
3015
+ }
3016
+ }
3017
+ return {
3018
+ file,
3019
+ bindings,
3020
+ exports,
3021
+ exportedFrom,
3022
+ mutated: mutatedNames(ast)
3023
+ };
3024
+ }
3025
+ function isEnvObject(n) {
3026
+ if (!isMemberExpression(n) || n.computed || !isIdentifier(n.property, {
3027
+ name: 'env'
3028
+ })) return;
3029
+ if (isIdentifier(n.object, {
3030
+ name: 'process'
3031
+ })) return 'process.env';
3032
+ if (isMetaProperty(n.object)) return 'import.meta.env';
3033
+ }
3034
+ function envAccess(node) {
3035
+ for (const found of findAll(node, (n)=>isMemberExpression(n) || isMetaProperty(n))){
3036
+ if (isMemberExpression(found)) {
3037
+ const base = isEnvObject(found.object);
3038
+ if (base) {
3039
+ const key = isIdentifier(found.property) ? found.property.name : isStringLiteral(found.property) ? found.property.value : void 0;
3040
+ return key && (!found.computed || isStringLiteral(found.property)) ? `${base}.${key}` : base;
3041
+ }
3042
+ }
3043
+ const whole = isEnvObject(found);
3044
+ if (whole) return whole;
3045
+ }
3046
+ }
3047
+ function calleeName(node) {
3048
+ const n = evaluate_unwrap(node);
3049
+ if (isIdentifier(n)) return n.name;
3050
+ if (isMemberExpression(n) && isIdentifier(n.property)) return `${calleeName(n.object)}.${n.property.name}`;
3051
+ return 'a function';
3052
+ }
3053
+ const CONVERSIONS = new Set([
3054
+ 'Number',
3055
+ 'String',
3056
+ 'Boolean',
3057
+ 'parseInt',
3058
+ 'parseFloat'
3059
+ ]);
3060
+ function reasonFor(node) {
3061
+ const call = 'CallExpression' !== node.type || CONVERSIONS.has(calleeName(node.callee)) ? void 0 : calleeName(node.callee);
3062
+ if (call) return `comes from a function call (${call}())`;
3063
+ const env = envAccess(node);
3064
+ if (env) return `reads an env variable (${env})`;
3065
+ if (isCallExpression(node)) return `comes from a function call (${calleeName(node.callee)}())`;
3066
+ if (isNewExpression(node)) return `is built with new ${calleeName(node.callee)}()`;
3067
+ if (isArrowFunctionExpression(node) || isFunctionExpression(node)) return 'is a function';
3068
+ if (isMemberExpression(node)) return `is read from another value (${calleeName(node)})`;
3069
+ if (isConditionalExpression(node)) return 'depends on a condition (a ? b : c)';
3070
+ if (isLogicalExpression(node) || isBinaryExpression(node)) return `is computed with ${node.operator}`;
3071
+ if (isTemplateLiteral(node)) return 'is a template string with ${} parts';
3072
+ if (isAwaitExpression(node)) return 'is awaited';
3073
+ return `is not a literal (${node.type})`;
3074
+ }
3075
+ function identifierReason(name, binding) {
3076
+ if (!binding) return `refers to ${name}, which is not a const in this file`;
3077
+ if ('import' === binding.kind) return `is imported (${name} from '${binding.source}')`;
3078
+ return `refers to ${name}, which is ${'other' === binding.kind ? binding.what : 'not a const'}`;
3079
+ }
3080
+ function resolveNode(node, scope, seen = new Set()) {
3081
+ const n = evaluate_unwrap(node);
3082
+ if (!n) return evaluate_fail('is missing');
3083
+ if (!isIdentifier(n)) return ok(n);
3084
+ const binding = scope.bindings.get(n.name);
3085
+ if (binding?.kind === 'const' && scope.mutated.has(n.name)) return evaluate_fail(`refers to ${n.name}, which is changed later in the file`);
3086
+ 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));
3087
+ return resolveNode(binding.init, scope, new Set([
3088
+ ...seen,
3089
+ n.name
3090
+ ]));
3091
+ }
3092
+ function evaluate_entriesOf(object, scope, seen = new Set()) {
3093
+ const entries = [];
3094
+ const problems = [];
3095
+ for (const prop of object.properties){
3096
+ if ('SpreadElement' === prop.type) {
3097
+ const arg = evaluate_unwrap(prop.argument);
3098
+ if (isIdentifier(arg)) {
3099
+ const binding = scope.bindings.get(arg.name);
3100
+ if (binding?.kind === 'import') {
3101
+ problems.push({
3102
+ reason: `spreads an imported object (${arg.name} from '${binding.source}')`
3103
+ });
3104
+ continue;
3105
+ }
3106
+ }
3107
+ const target = resolveNode(arg, scope, seen);
3108
+ if (target.ok) if ('ObjectExpression' === target.value.type) {
3109
+ const name = isIdentifier(arg) ? arg.name : '';
3110
+ const inner = evaluate_entriesOf(target.value, scope, new Set([
3111
+ ...seen,
3112
+ name
3113
+ ]));
3114
+ entries.push(...inner.entries);
3115
+ problems.push(...inner.problems);
3116
+ } else problems.push({
3117
+ reason: `spreads a value that ${reasonFor(target.value)}`
3118
+ });
3119
+ else problems.push({
3120
+ reason: `spreads a value that ${target.reason}`
3121
+ });
3122
+ continue;
3123
+ }
3124
+ const key = prop.computed ? void 0 : evaluate_keyName(prop.key);
3125
+ if (void 0 === key) {
3126
+ problems.push({
3127
+ reason: 'has a computed key'
3128
+ });
3129
+ continue;
3130
+ }
3131
+ if ('ObjectMethod' === prop.type) {
3132
+ problems.push({
3133
+ key,
3134
+ reason: 'is a method'
3135
+ });
3136
+ continue;
3137
+ }
3138
+ entries.push({
3139
+ key,
3140
+ value: prop.value
3141
+ });
3142
+ }
3143
+ return {
3144
+ entries,
3145
+ problems
3146
+ };
3147
+ }
3148
+ function evaluateArray(array, scope, seen) {
3149
+ const out = [];
3150
+ for (const element of array.elements){
3151
+ if (!element) return evaluate_fail('has an empty slot');
3152
+ if ('SpreadElement' === element.type) return evaluate_fail('spreads into an array');
3153
+ const item = evaluate(element, scope, seen);
3154
+ if (!item.ok) return item;
3155
+ out.push(item.value);
3156
+ }
3157
+ return ok(out);
3158
+ }
3159
+ function evaluate(node, scope, seen = new Set()) {
3160
+ const n = evaluate_unwrap(node);
3161
+ if (!n) return evaluate_fail('is missing');
3162
+ if (isStringLiteral(n) || isNumericLiteral(n) || isBooleanLiteral(n)) return ok(n.value);
3163
+ if (isNullLiteral(n)) return ok(null);
3164
+ if (isTemplateLiteral(n)) return 0 === n.expressions.length ? ok(n.quasis.map((q)=>q.value.cooked ?? q.value.raw).join('')) : evaluate_fail(reasonFor(n));
3165
+ if (isUnaryExpression(n)) return '-' === n.operator && isNumericLiteral(n.argument) ? ok(-n.argument.value) : evaluate_fail(reasonFor(n));
3166
+ if (isIdentifier(n)) {
3167
+ const target = resolveNode(n, scope, seen);
3168
+ return target.ok ? evaluate(target.value, scope, new Set([
3169
+ ...seen,
3170
+ n.name
3171
+ ])) : target;
3172
+ }
3173
+ if (isObjectExpression(n)) {
3174
+ const { entries, problems } = evaluate_entriesOf(n, scope, seen);
3175
+ const problem = problems[0];
3176
+ if (problem) return evaluate_fail(problem.key ? `${problem.key} ${problem.reason}` : problem.reason);
3177
+ const out = {};
3178
+ for (const entry of entries){
3179
+ const value = evaluate(entry.value, scope, seen);
3180
+ if (!value.ok) return evaluate_fail(`${entry.key} ${value.reason}`);
3181
+ out[entry.key] = value.value;
3182
+ }
3183
+ return ok(out);
3184
+ }
3185
+ if (isArrayExpression(n)) return evaluateArray(n, scope, seen);
3186
+ return evaluate_fail(reasonFor(n));
3187
+ }
3188
+ const MAX_FILE_BYTES = 1000000;
3189
+ function isInside(root, target) {
3190
+ const rel = node_path.relative(root, target);
3191
+ return '' === rel || '..' !== rel && !rel.startsWith(`..${node_path.sep}`) && !node_path.isAbsolute(rel);
3192
+ }
3193
+ const inNodeModules = (root, target)=>node_path.relative(root, target).split(node_path.sep).includes('node_modules');
3194
+ const isMissing = (error)=>hasErrno(error, 'ENOENT') || hasErrno(error, 'ENOTDIR');
3195
+ function codeOf(error) {
3196
+ return 'object' == typeof error && null !== error && 'code' in error ? String(error.code) : 'unknown error';
3197
+ }
3198
+ function guardedDisk(root, refused) {
3199
+ const shown = (file)=>node_path.relative(root, file).split(node_path.sep).join('/') || '.';
3200
+ const refuse = (file, reason)=>{
3201
+ if (!refused.some((r)=>r.file === shown(file) && r.reason === reason)) refused.push({
3202
+ file: shown(file),
3203
+ reason
3204
+ });
3205
+ };
3206
+ let realRoot;
3207
+ const rootReal = ()=>realRoot ??= promises.realpath(root);
3208
+ async function allowed(file) {
3209
+ if (inNodeModules(root, file)) return void refuse(file, 'is inside node_modules, which is never read');
3210
+ try {
3211
+ const [realFile, base] = await Promise.all([
3212
+ promises.realpath(file),
3213
+ rootReal()
3214
+ ]);
3215
+ if (!isInside(base, realFile)) return void refuse(file, 'is (or goes through) a link that leads outside the project');
3216
+ if (inNodeModules(base, realFile)) return void refuse(file, 'is (or goes through) a link into node_modules');
3217
+ return realFile;
3218
+ } catch (error) {
3219
+ if (!isMissing(error)) refuse(file, `could not be read (${codeOf(error)})`);
3220
+ return;
3221
+ }
3222
+ }
3223
+ return {
3224
+ read: async (file)=>{
3225
+ const real = await allowed(file);
3226
+ if (void 0 === real) return;
3227
+ try {
3228
+ const { size } = await promises.stat(real);
3229
+ if (size > MAX_FILE_BYTES) return void refuse(file, `is larger than ${String(MAX_FILE_BYTES / 1000000)} MB, so it is not read`);
3230
+ return await promises.readFile(real, 'utf8');
3231
+ } catch (error) {
3232
+ if (!isMissing(error)) refuse(file, `could not be read (${codeOf(error)})`);
3233
+ return;
3234
+ }
3235
+ },
3236
+ listDirs: async (dir)=>{
3237
+ const real = await allowed(dir);
3238
+ if (void 0 === real) return [];
3239
+ try {
3240
+ return (await promises.readdir(real, {
3241
+ withFileTypes: true
3242
+ })).filter((e)=>e.isDirectory()).map((e)=>e.name);
3243
+ } catch (error) {
3244
+ if (!isMissing(error)) refuse(dir, `could not be read (${codeOf(error)})`);
3245
+ return [];
3246
+ }
3247
+ }
3248
+ };
3249
+ }
3250
+ const fieldOf = (base, key)=>/^[A-Za-z_$][\w$]*$/.test(key) ? `${base}.${key}` : `${base}['${key}']`;
3251
+ const isRecord = (value)=>'object' == typeof value && null !== value && !Array.isArray(value);
3252
+ function parseRemote(value) {
3253
+ if (value.startsWith('promise ')) return evaluate_fail('is loaded by inline code (promise …)');
3254
+ const match = /^([^@/:\s]+)@(.+)$/.exec(value);
3255
+ const entry = match?.[2] ?? value;
3256
+ if (!entry.includes('/')) return evaluate_fail(`has no URL ("${value}")`);
3257
+ const type = /\.json([?#]|$)/.test(entry) ? 'manifest' : "script";
3258
+ return ok(match?.[1] ? {
3259
+ name: match[1],
3260
+ entry,
3261
+ type
3262
+ } : {
3263
+ entry,
3264
+ type
3265
+ });
3266
+ }
3267
+ function objectAt(value, scope, field, report) {
3268
+ const target = resolveNode(value, scope);
3269
+ if (!target.ok) return void report(field, target.reason);
3270
+ if ('ObjectExpression' !== target.value.type) {
3271
+ const evaluated = evaluate(target.value, scope);
3272
+ report(field, evaluated.ok ? 'is not an object' : evaluated.reason);
3273
+ return;
3274
+ }
3275
+ const { entries, problems } = evaluate_entriesOf(target.value, scope);
3276
+ for (const problem of problems)report(problem.key ? fieldOf(field, problem.key) : field, problem.reason);
3277
+ return entries;
3278
+ }
3279
+ function readRemotes(value, scope, report) {
3280
+ const remotes = {};
3281
+ for (const { key, value: node } of objectAt(value, scope, 'remotes', report) ?? []){
3282
+ const field = fieldOf('remotes', key);
3283
+ const evaluated = evaluate(node, scope);
3284
+ if (!evaluated.ok) {
3285
+ report(field, evaluated.reason);
3286
+ continue;
3287
+ }
3288
+ const raw = evaluated.value;
3289
+ const text = isRecord(raw) ? raw['external'] : raw;
3290
+ if ('string' != typeof text) {
3291
+ report(field, isRecord(raw) ? 'has no string external' : 'is not a string');
3292
+ continue;
3293
+ }
3294
+ const remote = parseRemote(text);
3295
+ if (remote.ok) remotes[key] = remote.value;
3296
+ else report(field, remote.reason);
3297
+ }
3298
+ return remotes;
3299
+ }
3300
+ function readExposes(value, scope, report) {
3301
+ const exposes = {};
3302
+ const target = resolveNode(value, scope);
3303
+ if (target.ok && 'ArrayExpression' === target.value.type) {
3304
+ const evaluated = evaluate(target.value, scope);
3305
+ if (evaluated.ok) if (Array.isArray(evaluated.value)) for (const item of evaluated.value)if ('string' == typeof item) exposes[item] = item;
3306
+ else report('exposes', 'is an array with items that are not strings');
3307
+ else report('exposes', 'is not a list');
3308
+ else report('exposes', evaluated.reason);
3309
+ return exposes;
3310
+ }
3311
+ for (const { key, value: node } of objectAt(value, scope, 'exposes', report) ?? []){
3312
+ const field = fieldOf('exposes', key);
3313
+ const evaluated = evaluate(node, scope);
3314
+ if (!evaluated.ok) {
3315
+ report(field, evaluated.reason);
3316
+ continue;
3317
+ }
3318
+ const raw = evaluated.value;
3319
+ const path = isRecord(raw) ? raw['import'] : raw;
3320
+ if ('string' == typeof path) exposes[key] = path;
3321
+ else report(field, 'is not a module path');
3322
+ }
3323
+ return exposes;
3324
+ }
3325
+ const NOT_CARRIED = 'is set in the config but adopt does not carry it over';
3326
+ const NOT_CARRIED_SHARED = new Set([
3327
+ 'eager',
3328
+ 'strictVersion',
3329
+ 'import'
3330
+ ]);
3331
+ const NOT_CARRIED_OPTIONS = new Set([
3332
+ 'filename',
3333
+ 'shareStrategy',
3334
+ 'runtimePlugins'
3335
+ ]);
3336
+ function readSharedEntry(key, node, scope, report) {
3337
+ const field = fieldOf('shared', key);
3338
+ const target = resolveNode(node, scope);
3339
+ if (!target.ok) return void report(field, target.reason);
3340
+ if ('ObjectExpression' !== target.value.type) {
3341
+ const evaluated = evaluate(target.value, scope);
3342
+ if (evaluated.ok) {
3343
+ if ('string' == typeof evaluated.value) return {
3344
+ requiredVersion: evaluated.value
3345
+ };
3346
+ report(field, 'is not a version range or an options object');
3347
+ } else report(field, evaluated.reason);
3348
+ return;
3349
+ }
3350
+ const shared = {};
3351
+ const { entries, problems } = evaluate_entriesOf(target.value, scope);
3352
+ for (const problem of problems)report(problem.key ? fieldOf(field, problem.key) : field, problem.reason);
3353
+ for (const entry of entries){
3354
+ const at = fieldOf(field, entry.key);
3355
+ if (NOT_CARRIED_SHARED.has(entry.key)) {
3356
+ const evaluated = evaluate(entry.value, scope);
3357
+ const plainlyOff = 'import' !== entry.key && evaluated.ok && false === evaluated.value;
3358
+ if (!plainlyOff) report(at, NOT_CARRIED);
3359
+ continue;
3360
+ }
3361
+ if ('singleton' !== entry.key && 'requiredVersion' !== entry.key) continue;
3362
+ const evaluated = evaluate(entry.value, scope);
3363
+ if (evaluated.ok) if ('singleton' === entry.key && 'boolean' == typeof evaluated.value) shared.singleton = evaluated.value;
3364
+ else if ('requiredVersion' === entry.key && ('string' == typeof evaluated.value || false === evaluated.value)) shared.requiredVersion = evaluated.value;
3365
+ else report(at, 'singleton' === entry.key ? 'is not true or false' : 'is not a version range');
3366
+ else report(at, evaluated.reason);
3367
+ }
3368
+ return shared;
3369
+ }
3370
+ function readShared(value, scope, report) {
3371
+ const shared = {};
3372
+ const target = resolveNode(value, scope);
3373
+ if (target.ok && 'ArrayExpression' === target.value.type) {
3374
+ for (const [index, element] of target.value.elements.entries()){
3375
+ const evaluated = evaluate(element, scope);
3376
+ if (evaluated.ok && 'string' == typeof evaluated.value) shared[evaluated.value] = {};
3377
+ else report(`shared[${String(index)}]`, evaluated.ok ? 'is not a package name' : evaluated.reason);
3378
+ }
3379
+ return shared;
3380
+ }
3381
+ for (const { key, value: node } of objectAt(value, scope, 'shared', report) ?? []){
3382
+ const entry = readSharedEntry(key, node, scope, report);
3383
+ if (entry) shared[key] = entry;
3384
+ }
3385
+ return shared;
3386
+ }
3387
+ function readFederationOptions(options, scope, report) {
3388
+ const result = {
3389
+ remotes: {},
3390
+ exposes: {},
3391
+ shared: {},
3392
+ exposing: false
3393
+ };
3394
+ const { entries, problems } = evaluate_entriesOf(options, scope);
3395
+ for (const problem of problems)report(problem.key ?? 'options', problem.reason);
3396
+ for (const { key, value } of entries)switch(key){
3397
+ case 'name':
3398
+ {
3399
+ const evaluated = evaluate(value, scope);
3400
+ if (evaluated.ok && 'string' == typeof evaluated.value) result.name = evaluated.value;
3401
+ else report('name', evaluated.ok ? 'is not a string' : evaluated.reason);
3402
+ break;
3403
+ }
3404
+ case 'remotes':
3405
+ result.remotes = readRemotes(value, scope, report);
3406
+ break;
3407
+ case 'exposes':
3408
+ {
3409
+ let unread = false;
3410
+ result.exposes = readExposes(value, scope, (field, reason)=>{
3411
+ unread = true;
3412
+ report(field, reason);
3413
+ });
3414
+ result.exposing = unread || Object.keys(result.exposes).length > 0;
3415
+ break;
3416
+ }
3417
+ case 'shared':
3418
+ result.shared = readShared(value, scope, report);
3419
+ break;
3420
+ default:
3421
+ if (NOT_CARRIED_OPTIONS.has(key)) report(key, NOT_CARRIED);
3422
+ break;
3423
+ }
3424
+ return result;
3425
+ }
3426
+ const SKIPPED_DIRS = new Set([
3427
+ 'node_modules',
3428
+ 'dist',
3429
+ 'build',
3430
+ 'out',
3431
+ 'output',
3432
+ 'coverage',
3433
+ 'doc_build',
3434
+ 'storybook-static'
3435
+ ]);
3436
+ const workspace_MAX_DEPTH = 8;
3437
+ const MAX_VISITED = 5000;
3438
+ const workspace_skipped = (name)=>name.startsWith('.') || SKIPPED_DIRS.has(name) || name.startsWith('dist-');
3439
+ function toRegExp(segment) {
3440
+ const escaped = segment.replace(/[.+^${}()|[\]\\?]/g, String.raw`\$&`).replaceAll('*', '[^/]*');
3441
+ return new RegExp(`^${escaped}$`);
3442
+ }
3443
+ function globToRegExp(pattern) {
3444
+ const body = pattern.split('/').map((segment)=>'**' === segment ? '\0' : toRegExp(segment).source.slice(1, -1)).join('/').replaceAll('\0/', '(?:[^/]+/)*').replaceAll('/\0', '(?:/[^/]+)*').replaceAll('\0', '.*');
3445
+ return new RegExp(`^${body}$`);
3446
+ }
3447
+ function normalize(pattern) {
3448
+ const clean = pattern.replaceAll('\\', '/').replace(/^\.\//, '').replace(/\/+$/, '');
3449
+ if ('' === clean || clean.startsWith('/') || clean.split('/').includes('..')) return;
3450
+ return clean;
3451
+ }
3452
+ function eachInSequence(items, fn) {
3453
+ return items.reduce((done, item)=>done.then(()=>fn(item)), Promise.resolve());
3454
+ }
3455
+ const collapse = (segments)=>segments.filter((segment, i)=>!('**' === segment && '**' === segments[i - 1]));
3456
+ async function childrenOf(walk, at) {
3457
+ walk.visited += 1;
3458
+ if (walk.visited > MAX_VISITED) return [];
3459
+ try {
3460
+ return (await walk.listDirs(node_path.join(walk.root, ...at))).filter((name)=>!workspace_skipped(name)).toSorted((a, b)=>a.localeCompare(b));
3461
+ } catch (error) {
3462
+ walk.problems.push({
3463
+ file: at.join('/') || '.',
3464
+ reason: `could not be listed (${messageOf(error)})`
3465
+ });
3466
+ return [];
3467
+ }
3468
+ }
3469
+ async function expand(walk, segments, at = []) {
3470
+ const state = `${at.join('/')}|${segments.join('/')}`;
3471
+ if (walk.seen.has(state)) return;
3472
+ walk.seen.add(state);
3473
+ if (0 === segments.length) return void walk.found.add(at.join('/'));
3474
+ if (at.length > workspace_MAX_DEPTH) return;
3475
+ const [segment = '', ...rest] = segments;
3476
+ if (!segment.includes('*')) {
3477
+ if (!workspace_skipped(segment)) await expand(walk, rest, [
3478
+ ...at,
3479
+ segment
3480
+ ]);
3481
+ return;
3482
+ }
3483
+ const children = await childrenOf(walk, at);
3484
+ if ('**' === segment) {
3485
+ await expand(walk, rest, at);
3486
+ await eachInSequence(children, (child)=>expand(walk, segments, [
3487
+ ...at,
3488
+ child
3489
+ ]));
3490
+ return;
3491
+ }
3492
+ const match = toRegExp(segment);
3493
+ await eachInSequence(children.filter((c)=>match.test(c)), (child)=>expand(walk, rest, [
3494
+ ...at,
3495
+ child
3496
+ ]));
3497
+ }
3498
+ function stringList(value) {
3499
+ return Array.isArray(value) && value.every((v)=>'string' == typeof v) ? value : void 0;
3500
+ }
3501
+ async function safeRead(read, file, problems) {
3502
+ try {
3503
+ return await read(file);
3504
+ } catch (error) {
3505
+ problems.push({
3506
+ file: node_path.basename(file),
3507
+ reason: `could not be read (${messageOf(error)})`
3508
+ });
3509
+ return;
3510
+ }
3511
+ }
3512
+ async function workspacePatterns(root, read, problems) {
3513
+ const patterns = [];
3514
+ const yamlText = await safeRead(read, node_path.join(root, 'pnpm-workspace.yaml'), problems);
3515
+ if (void 0 !== yamlText) try {
3516
+ const doc = external_yaml_parse(yamlText);
3517
+ const list = 'object' == typeof doc && null !== doc && 'packages' in doc ? stringList(doc.packages) : [];
3518
+ if (list) patterns.push(...list);
3519
+ else problems.push({
3520
+ file: 'pnpm-workspace.yaml',
3521
+ reason: 'packages is not a list'
3522
+ });
3523
+ } catch {
3524
+ problems.push({
3525
+ file: 'pnpm-workspace.yaml',
3526
+ reason: 'could not be parsed'
3527
+ });
3528
+ }
3529
+ const pkgText = await safeRead(read, node_path.join(root, 'package.json'), problems);
3530
+ if (void 0 !== pkgText) try {
3531
+ const pkg = JSON.parse(pkgText);
3532
+ const workspaces = 'object' == typeof pkg && null !== pkg && 'workspaces' in pkg ? pkg.workspaces : void 0;
3533
+ const list = void 0 === workspaces ? [] : stringList(workspaces) ?? ('object' == typeof workspaces && null !== workspaces && 'packages' in workspaces ? stringList(workspaces.packages) : void 0);
3534
+ if (list) patterns.push(...list);
3535
+ else problems.push({
3536
+ file: 'package.json',
3537
+ reason: 'workspaces is not a list'
3538
+ });
3539
+ } catch {
3540
+ problems.push({
3541
+ file: 'package.json',
3542
+ reason: 'could not be parsed'
3543
+ });
3544
+ }
3545
+ return patterns;
3546
+ }
3547
+ async function findAppDirs(root, read, listDirs, problems = []) {
3548
+ const patterns = await workspacePatterns(root, read, problems);
3549
+ const include = patterns.filter((p)=>!p.startsWith('!')).map(normalize);
3550
+ const exclude = patterns.filter((p)=>p.startsWith('!')).map((p)=>normalize(p.slice(1))).filter((p)=>void 0 !== p).map(globToRegExp);
3551
+ const walk = {
3552
+ root,
3553
+ listDirs,
3554
+ problems,
3555
+ visited: 0,
3556
+ seen: new Set(),
3557
+ found: new Set()
3558
+ };
3559
+ await eachInSequence(include, async (pattern)=>{
3560
+ if (void 0 !== pattern) await expand(walk, collapse(pattern.split('/')));
3561
+ });
3562
+ if (walk.visited > MAX_VISITED) problems.push({
3563
+ file: '.',
3564
+ reason: `has more than ${String(MAX_VISITED)} folders to search, so the workspace globs were cut short`
3565
+ });
3566
+ const candidates = [
3567
+ ...walk.found
3568
+ ].filter((dir)=>'' !== dir && !exclude.some((re)=>re.test(dir)));
3569
+ const withPackage = [];
3570
+ await eachInSequence(candidates, async (dir)=>{
3571
+ const file = node_path.join(root, ...dir.split('/'), 'package.json');
3572
+ withPackage.push(await safeRead(read, file, problems) === void 0 ? void 0 : dir);
3573
+ });
3574
+ const dirs = withPackage.filter((d)=>void 0 !== d);
3575
+ return [
3576
+ '.',
3577
+ ...dirs.toSorted((a, b)=>a.localeCompare(b))
3578
+ ];
3579
+ }
3580
+ const EXTENSIONS = [
3581
+ '.ts',
3582
+ '.mts',
3583
+ '.cts',
3584
+ '.js',
3585
+ '.mjs',
3586
+ '.cjs'
3587
+ ];
3588
+ const BUILD_TOOLS = [
3589
+ 'rsbuild',
3590
+ 'rspack',
3591
+ 'webpack'
3592
+ ];
3593
+ const FEDERATION_CONFIG = 'module-federation.config';
3594
+ const MAX_IMPORT_HOPS = 3;
3595
+ const detect_relative = (root, file)=>node_path.relative(root, file).split(node_path.sep).join('/') || '.';
3596
+ function detect_report(ctx, file, field, reason) {
3597
+ ctx.notAdopted.push({
3598
+ dir: ctx.dir,
3599
+ file: detect_relative(ctx.root, file),
3600
+ field,
3601
+ reason
3602
+ });
3603
+ }
3604
+ async function firstExisting(read, candidates) {
3605
+ const sources = await Promise.all(candidates.map((file)=>read(file)));
3606
+ const index = sources.findIndex((s)=>void 0 !== s);
3607
+ const source = sources[index];
3608
+ const file = candidates[index];
3609
+ return void 0 === source || void 0 === file ? void 0 : {
3610
+ file,
3611
+ source
3612
+ };
3613
+ }
3614
+ function load(ctx, file, source) {
3615
+ try {
3616
+ const ast = parseSource(source, file);
3617
+ return {
3618
+ ast,
3619
+ scope: scopeOf(ast, file)
3620
+ };
3621
+ } catch {
3622
+ detect_report(ctx, file, 'file', 'could not be parsed');
3623
+ return;
3624
+ }
3625
+ }
3626
+ function namesFor(scope, canonical) {
3627
+ const names = new Set([
3628
+ canonical
3629
+ ]);
3630
+ for (const [local, binding] of scope.bindings)if ('import' === binding.kind && binding.imported === canonical) names.add(local);
3631
+ return names;
3632
+ }
3633
+ function patternNames(node, out) {
3634
+ if (!node) return;
3635
+ if (isIdentifier(node)) out.add(node.name);
3636
+ else if ('ObjectPattern' === node.type) for (const prop of node.properties)patternNames('RestElement' === prop.type ? prop.argument : prop.value, out);
3637
+ else if ('ArrayPattern' === node.type) for (const element of node.elements)patternNames(element, out);
3638
+ else if ('RestElement' === node.type) patternNames(node.argument, out);
3639
+ else if ('AssignmentPattern' === node.type) patternNames(node.left, out);
3640
+ }
3641
+ const isFunction = (n)=>'FunctionDeclaration' === n.type || 'FunctionExpression' === n.type || 'ArrowFunctionExpression' === n.type || 'ObjectMethod' === n.type || 'ClassMethod' === n.type;
3642
+ const isLocalScope = (n)=>isFunction(n) || 'BlockStatement' === n.type || 'CatchClause' === n.type || 'ForStatement' === n.type || 'ForInStatement' === n.type || 'ForOfStatement' === n.type || 'SwitchStatement' === n.type;
3643
+ function declaredInside(scope) {
3644
+ const names = new Set();
3645
+ if ('params' in scope) for (const param of scope.params)patternNames(param, names);
3646
+ if ('CatchClause' === scope.type) patternNames(scope.param, names);
3647
+ for (const node of findAll(scope, (n)=>'VariableDeclarator' === n.type || 'FunctionDeclaration' === n.type || 'ClassDeclaration' === n.type))if ('VariableDeclarator' === node.type) patternNames(node.id, names);
3648
+ else if ('id' in node) patternNames(node.id, names);
3649
+ return names;
3650
+ }
3651
+ function federationCalls(ast, scope) {
3652
+ const plugin = namesFor(scope, 'pluginModuleFederation');
3653
+ const webpack = namesFor(scope, 'ModuleFederationPlugin');
3654
+ const found = [];
3655
+ const matches = (n, shadowed)=>{
3656
+ if (isCallExpression(n)) {
3657
+ const callee = n.callee;
3658
+ if (isIdentifier(callee)) return plugin.has(callee.name) && !shadowed.has(callee.name);
3659
+ return isMemberExpression(callee) && isIdentifier(callee.property, {
3660
+ name: 'pluginModuleFederation'
3661
+ });
3662
+ }
3663
+ if (!isNewExpression(n)) return false;
3664
+ const callee = n.callee;
3665
+ if (isIdentifier(callee)) return webpack.has(callee.name) && !shadowed.has(callee.name);
3666
+ return isMemberExpression(callee) && isIdentifier(callee.property, {
3667
+ name: 'ModuleFederationPlugin'
3668
+ });
3669
+ };
3670
+ const visit = (value, shadowed)=>{
3671
+ if (Array.isArray(value)) {
3672
+ for (const item of value)visit(item, shadowed);
3673
+ return;
3674
+ }
3675
+ if (!isNode(value)) return;
3676
+ if (matches(value, shadowed) && (isCallExpression(value) || isNewExpression(value))) found.push({
3677
+ node: value,
3678
+ shadowed
3679
+ });
3680
+ const inner = 'Program' !== value.type && isLocalScope(value) ? new Set([
3681
+ ...shadowed,
3682
+ ...declaredInside(value)
3683
+ ]) : shadowed;
3684
+ for (const [key, child] of Object.entries(value))if (!SKIP_KEYS.has(key)) visit(child, inner);
3685
+ };
3686
+ visit(ast.program, new Set());
3687
+ return found;
3688
+ }
3689
+ function scopeAt(scope, shadowed) {
3690
+ if (![
3691
+ ...shadowed
3692
+ ].some((name)=>scope.bindings.has(name))) return scope;
3693
+ const bindings = new Map(scope.bindings);
3694
+ for (const name of shadowed)if (bindings.has(name)) bindings.set(name, {
3695
+ kind: 'other',
3696
+ what: 'declared again inside a function around the call'
3697
+ });
3698
+ return {
3699
+ ...scope,
3700
+ bindings
3701
+ };
3702
+ }
3703
+ const isRelative = (source)=>'.' === source || '..' === source || source.startsWith('./') || source.startsWith('../');
3704
+ function candidatesFor(target) {
3705
+ const ext = node_path.extname(target);
3706
+ if (EXTENSIONS.includes(ext)) {
3707
+ const stem = target.slice(0, -ext.length);
3708
+ return [
3709
+ target,
3710
+ ...'.js' === ext ? [
3711
+ `${stem}.ts`
3712
+ ] : '.mjs' === ext ? [
3713
+ `${stem}.mts`
3714
+ ] : []
3715
+ ];
3716
+ }
3717
+ return [
3718
+ ...EXTENSIONS.map((e)=>`${target}${e}`),
3719
+ ...EXTENSIONS.map((e)=>node_path.join(target, `index${e}`))
3720
+ ];
3721
+ }
3722
+ async function detect_resolveOptions(ctx, node, scope, hops = 0, seen = new Set()) {
3723
+ const n = evaluate_unwrap(node);
3724
+ if (!n) return evaluate_fail('are missing');
3725
+ if ('ObjectExpression' === n.type) return ok({
3726
+ object: n,
3727
+ scope
3728
+ });
3729
+ if (isCallExpression(n)) {
3730
+ const wrapper = namesFor(scope, 'createModuleFederationConfig');
3731
+ if (isIdentifier(n.callee) && wrapper.has(n.callee.name)) return detect_resolveOptions(ctx, n.arguments[0], scope, hops, seen);
3732
+ return evaluate_fail(reasonFor(n));
3733
+ }
3734
+ if (!isIdentifier(n)) {
3735
+ const evaluated = evaluate(n, scope);
3736
+ return evaluate_fail(evaluated.ok ? 'are not an object' : evaluated.reason);
3737
+ }
3738
+ const binding = scope.bindings.get(n.name);
3739
+ if (binding?.kind === 'const' && !seen.has(n.name)) {
3740
+ if (scope.mutated.has(n.name)) return evaluate_fail(`refers to ${n.name}, which is changed later in the file`);
3741
+ return detect_resolveOptions(ctx, binding.init, scope, hops, new Set([
3742
+ ...seen,
3743
+ n.name
3744
+ ]));
3745
+ }
3746
+ if (binding?.kind !== 'import') {
3747
+ const evaluated = evaluate(n, scope);
3748
+ return evaluate_fail(evaluated.ok ? 'are not an object' : evaluated.reason);
3749
+ }
3750
+ if (!isRelative(binding.source)) return evaluate_fail(`are imported from a package (${n.name} from '${binding.source}')`);
3751
+ if ((binding.depth ?? 0) > 1) return evaluate_fail(`are read from deep inside what '${binding.source}' exports`);
3752
+ if (hops >= MAX_IMPORT_HOPS) return evaluate_fail(`are imported through more than ${String(MAX_IMPORT_HOPS)} files`);
3753
+ const target = node_path.resolve(node_path.dirname(scope.file), binding.source);
3754
+ if (!isInside(ctx.root, target)) return evaluate_fail(`are imported from outside the project ('${binding.source}')`);
3755
+ if (node_path.relative(ctx.root, target).split(node_path.sep).includes('node_modules')) return evaluate_fail(`are imported from node_modules ('${binding.source}')`);
3756
+ const found = await firstExisting(ctx.read, candidatesFor(target));
3757
+ if (!found) return evaluate_fail(`are imported from '${binding.source}', which was not found`);
3758
+ const imported = load(ctx, found.file, found.source)?.scope;
3759
+ if (!imported) return evaluate_fail(`are imported from '${binding.source}', which could not be parsed`);
3760
+ const exported = '*' === binding.imported ? void 0 : imported.exports.get(binding.imported);
3761
+ 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}`);
3762
+ const local = imported.exportedFrom.get(binding.imported);
3763
+ if (local && imported.mutated.has(local)) return evaluate_fail(`are imported from '${binding.source}', where ${local} is changed later in the file`);
3764
+ return detect_resolveOptions(ctx, exported, imported, hops + 1);
3765
+ }
3766
+ function readPort(ctx, ast, scope, tool) {
3767
+ const key = 'rsbuild' === tool ? 'server' : 'devServer';
3768
+ const ports = [];
3769
+ for (const prop of findAll(ast, (n)=>isObjectProperty(n))){
3770
+ if (!isObjectProperty(prop) || prop.computed || !isIdentifier(prop.key, {
3771
+ name: key
3772
+ })) continue;
3773
+ const value = evaluate_unwrap(prop.value);
3774
+ if (value?.type !== 'ObjectExpression') continue;
3775
+ const port = evaluate_entriesOf(value, scope).entries.findLast((e)=>'port' === e.key);
3776
+ if (port) ports.push(port.value);
3777
+ }
3778
+ const [first, ...others] = ports;
3779
+ if (!first) return;
3780
+ if (others.length > 0) return void detect_report(ctx, scope.file, 'port', 'is set in more than one place');
3781
+ const evaluated = evaluate(first, scope);
3782
+ if (evaluated.ok && 'number' == typeof evaluated.value) return evaluated.value;
3783
+ detect_report(ctx, scope.file, 'port', evaluated.ok ? 'is not a number' : evaluated.reason);
3784
+ }
3785
+ async function packageName(read, dir) {
3786
+ const text = await read(node_path.join(dir, 'package.json'));
3787
+ if (void 0 === text) return;
3788
+ try {
3789
+ const pkg = JSON.parse(text);
3790
+ return 'object' == typeof pkg && null !== pkg && 'name' in pkg && 'string' == typeof pkg.name ? pkg.name : void 0;
3791
+ } catch {
3792
+ return;
3793
+ }
3794
+ }
3795
+ async function detectApp(ctx) {
3796
+ const abs = node_path.join(ctx.root, ...ctx.dir.split('/'));
3797
+ const builds = BUILD_TOOLS.flatMap((tool)=>EXTENSIONS.map((ext)=>({
3798
+ tool,
3799
+ file: node_path.join(abs, `${tool}.config${ext}`)
3800
+ })));
3801
+ const [build, federation] = await Promise.all([
3802
+ firstExisting(ctx.read, builds.map((b)=>b.file)),
3803
+ firstExisting(ctx.read, EXTENSIONS.map((ext)=>node_path.join(abs, `${FEDERATION_CONFIG}${ext}`)))
3804
+ ]);
3805
+ if (!build) {
3806
+ if (federation) detect_report(ctx, federation.file, 'buildTool', 'has no rsbuild, rspack or webpack config next to it');
3807
+ return;
3808
+ }
3809
+ const tool = builds.find((b)=>b.file === build.file)?.tool ?? 'rsbuild';
3810
+ const parsed = load(ctx, build.file, build.source);
3811
+ if (!parsed) return;
3812
+ const { ast, scope } = parsed;
3813
+ const calls = federationCalls(ast, scope);
3814
+ 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`);
3815
+ let options;
3816
+ const [call] = calls;
3817
+ if (call) options = await detect_resolveOptions(ctx, call.node.arguments[0], scopeAt(scope, call.shadowed));
3818
+ else {
3819
+ if (!federation) return;
3820
+ const mfScope = load(ctx, federation.file, federation.source)?.scope;
3821
+ if (!mfScope) return;
3822
+ options = await detect_resolveOptions(ctx, mfScope.exports.get('default'), mfScope);
3823
+ }
3824
+ if (!options.ok) return void detect_report(ctx, build.file, 'options', options.reason);
3825
+ const optionsFile = options.value.scope.file;
3826
+ const read = readFederationOptions(options.value.object, options.value.scope, (field, reason)=>{
3827
+ detect_report(ctx, optionsFile, field, reason);
3828
+ });
3829
+ const port = readPort(ctx, ast, scope, tool);
3830
+ const pkgName = await packageName(ctx.read, abs);
3831
+ return {
3832
+ dir: ctx.dir,
3833
+ ...void 0 === read.name ? {} : {
3834
+ name: read.name
3835
+ },
3836
+ ...void 0 === pkgName ? {} : {
3837
+ packageName: pkgName
3838
+ },
3839
+ role: read.exposing ? 'remote' : 'shell',
3840
+ buildTool: tool,
3841
+ configFile: detect_relative(ctx.root, build.file),
3842
+ ...optionsFile === build.file ? {} : {
3843
+ federationFile: detect_relative(ctx.root, optionsFile)
3844
+ },
3845
+ remotes: read.remotes,
3846
+ exposes: read.exposes,
3847
+ shared: read.shared,
3848
+ ...void 0 === port ? {} : {
3849
+ port
3850
+ }
3851
+ };
3852
+ }
3853
+ async function detectFederation(dir, options = {}) {
3854
+ const root = node_path.resolve(dir);
3855
+ const refused = [];
3856
+ const disk = guardedDisk(root, refused);
3857
+ const read = options.read ?? disk.read;
3858
+ const listDirs = options.listDirs ?? disk.listDirs;
3859
+ const notAdopted = [];
3860
+ const problems = [];
3861
+ const dirs = await findAppDirs(root, read, listDirs, problems);
3862
+ for (const problem of problems)notAdopted.push({
3863
+ dir: '.',
3864
+ file: problem.file,
3865
+ field: 'workspaces',
3866
+ reason: problem.reason
3867
+ });
3868
+ const apps = [];
3869
+ await eachInSequence(dirs, async (appDir)=>{
3870
+ const ctx = {
3871
+ root,
3872
+ read,
3873
+ dir: appDir,
3874
+ notAdopted: []
3875
+ };
3876
+ try {
3877
+ const app = await detectApp(ctx);
3878
+ if (app) apps.push(app);
3879
+ } catch (error) {
3880
+ detect_report(ctx, node_path.join(root, ...appDir.split('/')), 'file', `could not be read (${messageOf(error)})`);
3881
+ }
3882
+ notAdopted.push(...ctx.notAdopted);
3883
+ });
3884
+ for (const refusal of refused)notAdopted.push({
3885
+ dir: '.',
3886
+ file: refusal.file,
3887
+ field: 'file',
3888
+ reason: refusal.reason
3889
+ });
3890
+ return {
3891
+ apps,
3892
+ notAdopted
3893
+ };
3894
+ }
3895
+ const RESERVED = new Set([
3896
+ 'root',
3897
+ 'remotes'
3898
+ ]);
3899
+ const DEV_PORT_START = 4311;
3900
+ const OWNER_PLACEHOLDER = 'unassigned';
3901
+ const CONTROL = new RegExp(String.raw`[\u0000-\u001f\u007f-\u009f]`, 'g');
3902
+ const printable = (text)=>text.replaceAll(CONTROL, '?');
3903
+ const join = (dir, file)=>'.' === dir ? file : `${dir}/${file}`;
3904
+ function localPort(entry) {
3905
+ try {
3906
+ const url = new URL(entry);
3907
+ const local = 'localhost' === url.hostname || '127.0.0.1' === url.hostname;
3908
+ const port = Number(url.port);
3909
+ return local && Number.isInteger(port) && port > 0 ? port : void 0;
3910
+ } catch {
3911
+ return;
3912
+ }
3913
+ }
3914
+ function skip(app, field, reason) {
3915
+ return {
3916
+ dir: app.dir,
3917
+ file: app.federationFile ?? app.configFile,
3918
+ field,
3919
+ reason
3920
+ };
3921
+ }
3922
+ function nameApps(apps, notAdopted) {
3923
+ const taken = new Set();
3924
+ const named = [];
3925
+ for (const app of apps){
3926
+ const { name } = app;
3927
+ let problem;
3928
+ if (void 0 === name) problem = 'has no literal name, so there is nothing to call this app';
3929
+ else if (MFE_NAME.test(name)) {
3930
+ if (RESERVED.has(name) || 'remote' === app.role && 'shell' === name) problem = `"${name}" is a name mfeflow keeps for itself`;
3931
+ else if (taken.has(name)) problem = `"${name}" is already used by another app`;
3932
+ } else problem = `"${name}" is not a name mfeflow accepts (lowercase letters, digits and single _)`;
3933
+ if (void 0 === name || void 0 !== problem) {
3934
+ notAdopted.push(skip(app, 'name', `${problem ?? ''}; the app was left out`));
3935
+ continue;
3936
+ }
3937
+ taken.add(name);
3938
+ named.push({
3939
+ app,
3940
+ name
3941
+ });
3942
+ }
3943
+ return named;
3944
+ }
3945
+ function exposesOf(app, notAdopted) {
3946
+ const kept = [];
3947
+ for (const key of Object.keys(app.exposes))if (EXPOSE.test(key)) kept.push(key);
3948
+ else notAdopted.push(skip(app, `exposes['${key}']`, 'is not a ./Name path, which mfeflow.app.json requires'));
3949
+ return kept;
3950
+ }
3951
+ function platformOf(app) {
3952
+ const platform = {};
3953
+ for (const [pkg, shared] of Object.entries(app.shared))if ('string' == typeof shared.requiredVersion) platform[pkg] = shared.requiredVersion;
3954
+ return platform;
3955
+ }
3956
+ function shellsLoading(name, shells) {
3957
+ return shells.filter(({ app })=>Object.entries(app.remotes).some(([alias, remote])=>(remote.name ?? alias) === name)).map((shell)=>shell.name);
3958
+ }
3959
+ function portFromShells(name, shells) {
3960
+ 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);
3961
+ }
3962
+ function assignPorts(named, shells, warnings) {
3963
+ const claims = new Map();
3964
+ const claim = (port, name)=>{
3965
+ claims.set(port, [
3966
+ ...claims.get(port) ?? [],
3967
+ name
3968
+ ]);
3969
+ };
3970
+ const ports = new Map();
3971
+ const portless = [];
3972
+ for (const { app, name } of named){
3973
+ const port = app.port ?? ('remote' === app.role ? portFromShells(name, shells) : void 0);
3974
+ if (void 0 === port) {
3975
+ if ('remote' === app.role) portless.push(name);
3976
+ continue;
3977
+ }
3978
+ claim(port, name);
3979
+ if ('remote' === app.role) ports.set(name, port);
3980
+ }
3981
+ 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`);
3982
+ let next = DEV_PORT_START;
3983
+ for (const name of portless){
3984
+ while(claims.has(next))next += 1;
3985
+ claim(next, name);
3986
+ ports.set(name, next);
3987
+ 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`);
3988
+ }
3989
+ return ports;
3990
+ }
3991
+ function planAdoption(input) {
3992
+ const notAdopted = [
3993
+ ...input.report.notAdopted
3994
+ ];
3995
+ const warnings = [];
3996
+ const named = nameApps(input.report.apps, notAdopted);
3997
+ const shells = named.filter(({ app })=>'shell' === app.role);
3998
+ const remotes = named.filter(({ app })=>'remote' === app.role);
3999
+ if (0 === shells.length) {
4000
+ 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';
4001
+ throw new MfeflowError('E124', found);
4002
+ }
4003
+ const apps = {};
4004
+ const ports = assignPorts(named, shells, warnings);
4005
+ const appFiles = [];
4006
+ const adoptedRemotes = [];
4007
+ for (const { app, name } of named){
4008
+ apps[name] = {
4009
+ kind: app.role,
4010
+ path: app.dir,
4011
+ ...void 0 === app.port ? {} : {
4012
+ port: app.port
4013
+ }
4014
+ };
4015
+ const platform = platformOf(app);
4016
+ appFiles.push({
4017
+ name,
4018
+ dir: app.dir,
4019
+ role: app.role,
4020
+ content: {
4021
+ $schema: SCHEMA_URLS.mfe,
4022
+ name,
4023
+ owner: OWNER_PLACEHOLDER,
4024
+ shells: 'remote' === app.role ? shellsLoading(name, shells) : [],
4025
+ routes: [],
4026
+ exposes: exposesOf(app, notAdopted),
4027
+ ...Object.keys(platform).length > 0 ? {
4028
+ platform
4029
+ } : {}
4030
+ }
4031
+ });
4032
+ const port = ports.get(name);
4033
+ if ('remote' === app.role && void 0 !== port) adoptedRemotes.push({
4034
+ name,
4035
+ port
4036
+ });
4037
+ }
4038
+ 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(', ')}`);
4039
+ 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));
4040
+ if (scriptOnly.length > 0) warnings.push(`${[
4041
+ ...new Set(scriptOnly)
4042
+ ].join(', ')}: loaded from a remoteEntry.js, but the registry entries point at mf-manifest.json; check that the remote builds one`);
4043
+ const fromElsewhere = shells.flatMap(({ app })=>Object.entries(app.remotes)).filter(([alias, r])=>!named.some((n)=>n.name === (r.name ?? alias))).map(([alias])=>printable(alias));
4044
+ if (fromElsewhere.length > 0) warnings.push(`${[
4045
+ ...new Set(fromElsewhere)
4046
+ ].join(', ')}: loaded by a shell but not found in this folder, so no app or registry entry was written`);
4047
+ if (adoptedRemotes.length > 0) warnings.push('registry entries for staging and prod point at example.com; set the real addresses before you deploy');
4048
+ return {
4049
+ project: {
4050
+ version: 1,
4051
+ topology: 'monorepo',
4052
+ packageManager: input.packageManager,
4053
+ scope: input.scope,
4054
+ apps,
4055
+ features: {}
4056
+ },
4057
+ appFiles,
4058
+ remotes: adoptedRemotes,
4059
+ shellDirs: shells.map(({ app })=>app.dir),
4060
+ notAdopted: notAdopted.map((item)=>({
4061
+ dir: printable(item.dir),
4062
+ file: printable(item.file),
4063
+ field: printable(item.field),
4064
+ reason: printable(item.reason)
4065
+ })),
4066
+ warnings
4067
+ };
4068
+ }
4069
+ const MAX_BYTES = 1000000;
4070
+ const TIMEOUT_MS = 10000;
4071
+ const MAX_REDIRECTS = 5;
4072
+ const realFetch = (url, init)=>fetch(url, init);
4073
+ function registryUrl(name, registries, version) {
4074
+ const slash = name.indexOf('/');
4075
+ const namespace = name.slice(0, slash);
4076
+ const bare = name.slice(slash + 1);
4077
+ const template = registries[namespace];
4078
+ if (void 0 === template) throw new MfeflowError('E210', namespace);
4079
+ const url = template.replaceAll('{name}', encodeURIComponent(bare)).replaceAll('{version}', encodeURIComponent(version ?? 'latest'));
4080
+ let parsed;
4081
+ try {
4082
+ parsed = new URL(url);
4083
+ } catch (error) {
4084
+ throw new MfeflowError('E401', `${namespace} has an invalid URL (${redactUrl(url)})`, {
4085
+ cause: error
4086
+ });
4087
+ }
4088
+ if (!isRegistryProtocol(parsed)) throw new MfeflowError('E401', `${redactUrl(url)} is not https (plain http is for 127.0.0.1, localhost and [::1] only)`);
4089
+ return parsed.href;
4090
+ }
4091
+ async function getHttps(url, fetchImpl, code, options = {}) {
4092
+ const fail = (at, why, cause)=>{
4093
+ throw new MfeflowError(code, redactUrlsIn(`${at}: ${why}`), void 0 === cause ? {} : {
4094
+ cause
4095
+ });
4096
+ };
4097
+ const startsPrivate = isPrivateHost(new URL(url).hostname);
4098
+ const refusePrivate = (target)=>{
4099
+ if (void 0 === options.blockPrivate || !isPrivateHost(target.hostname)) return;
4100
+ if (!options.blockPrivate && startsPrivate) return;
4101
+ const why = options.blockPrivate ? 'those are only requested from a trusted registry' : 'a public registry may not send you to one';
4102
+ throw new MfeflowError('E405', `${redactUrl(target.href)} is a loopback or private address; ${why}`);
4103
+ };
4104
+ refusePrivate(new URL(url));
4105
+ const startsLoopback = true === options.loopback && isLoopbackHttp(new URL(url));
4106
+ const permitted = (target)=>'https:' === target.protocol || startsLoopback && isLoopbackHttp(target);
4107
+ const addressOk = (address)=>{
4108
+ try {
4109
+ return permitted(new URL(address));
4110
+ } catch {
4111
+ return false;
4112
+ }
4113
+ };
4114
+ let current = url;
4115
+ for(let hop = 0; hop <= MAX_REDIRECTS; hop += 1){
4116
+ let response;
4117
+ try {
4118
+ response = await fetchImpl(current, {
4119
+ signal: AbortSignal.timeout(TIMEOUT_MS),
4120
+ redirect: 'manual'
4121
+ });
4122
+ } catch (error) {
4123
+ return fail(current, messageOf(error), error);
4124
+ }
4125
+ if (void 0 !== response.url && '' !== response.url && !addressOk(response.url)) fail(url, `ended up at ${response.url}, which is not https`);
4126
+ if (void 0 !== response.url && '' !== response.url) refusePrivate(new URL(response.url));
4127
+ if (response.status < 300 || response.status >= 400) return response;
4128
+ const location = response.headers?.get('location');
4129
+ await response.body?.cancel().catch(()=>void 0);
4130
+ if (null == location || '' === location) fail(current, `redirected (${String(response.status)}) with no address`);
4131
+ let next;
4132
+ try {
4133
+ next = new URL(location ?? '', current);
4134
+ } catch (error) {
4135
+ return fail(current, 'redirected to a bad address', error);
4136
+ }
4137
+ if (!permitted(next)) fail(url, `redirected to ${next.href}, which is not https`);
4138
+ refusePrivate(next);
4139
+ current = next.href;
4140
+ }
4141
+ return fail(url, 'redirected too many times');
4142
+ }
4143
+ async function fetchRemoteText(name, registries, fetchImpl = realFetch, version, trusted = false) {
4144
+ const url = registryUrl(name, registries, version);
4145
+ const shown = redactUrl(url);
4146
+ const response = await getHttps(url, fetchImpl, 'E401', {
4147
+ loopback: true,
4148
+ blockPrivate: !trusted
4149
+ });
4150
+ if (!response.ok) {
4151
+ const hint = 404 === response.status ? `${name} is not in that registry` : void 0;
4152
+ throw new MfeflowError('E401', `${shown} answered ${String(response.status)}${hint ? `: ${hint}` : ''}`);
4153
+ }
4154
+ return readCappedText(response, MAX_BYTES, ()=>new MfeflowError('E402', `${shown} is larger than 1 MB`));
4155
+ }
4156
+ function checkIntegrity(name, recorded, served, accept) {
4157
+ if (recorded === served) return false;
4158
+ if (!accept) throw new MfeflowError('E406', `${name} was ${recorded} when you added it and is now ${served}`);
4159
+ return true;
4160
+ }
4161
+ function integrityOf(text) {
4162
+ return `sha256-${createHash('sha256').update(text, 'utf8').digest('hex')}`;
4163
+ }
4164
+ async function readCappedText(response, maxBytes, tooBig) {
4165
+ const declared = Number(response.headers?.get('content-length') ?? 0 / 0);
4166
+ if (declared > maxBytes) {
4167
+ await response.body?.cancel().catch(()=>void 0);
4168
+ throw tooBig();
4169
+ }
4170
+ if (!response.body) {
4171
+ const text = await response.text();
4172
+ if (Buffer.byteLength(text, 'utf8') > maxBytes) throw tooBig();
4173
+ return text;
4174
+ }
4175
+ const chunks = [];
4176
+ let total = 0;
4177
+ for await (const chunk of response.body){
4178
+ total += chunk.byteLength;
4179
+ if (total > maxBytes) throw tooBig();
4180
+ chunks.push(chunk);
4181
+ }
4182
+ return new TextDecoder().decode(Buffer.concat(chunks));
4183
+ }
4184
+ async function fetchFeatureIndex(namespace, registries, fetchImpl = realFetch, trusted = false) {
4185
+ const items = await readIndex(namespace, registries, fetchImpl, trusted, false);
4186
+ return items ?? [];
4187
+ }
4188
+ async function indexedVersion(namespace, name, registries, fetchImpl = realFetch, trusted = false) {
4189
+ const items = await readIndex(namespace, registries, fetchImpl, trusted, true);
4190
+ return items?.find((item)=>item.name === name)?.version;
4191
+ }
4192
+ async function readIndex(namespace, registries, fetchImpl, trusted, missingOk) {
4193
+ const template = registries[namespace];
4194
+ if (void 0 === template) throw new MfeflowError('E210', namespace);
4195
+ const problem = registryTemplateProblem(template);
4196
+ if (void 0 !== problem) throw new MfeflowError('E401', `${namespace}: ${problem}`);
4197
+ const url = registryIndexUrl(template);
4198
+ const shown = redactUrl(url);
4199
+ const response = await getHttps(url, fetchImpl, 'E401', {
4200
+ loopback: true,
4201
+ blockPrivate: !trusted
4202
+ });
4203
+ if (!response.ok) {
4204
+ if (missingOk && 404 === response.status) return;
4205
+ const hint = 404 === response.status ? ': this registry has no index.json' : '';
4206
+ throw new MfeflowError('E401', `${shown} answered ${String(response.status)}${hint}`);
4207
+ }
4208
+ const text = await readCappedText(response, MAX_BYTES, ()=>new MfeflowError('E403', `${shown} is larger than 1 MB`));
4209
+ let value;
4210
+ try {
4211
+ value = JSON.parse(text);
4212
+ } catch (error) {
4213
+ const reason = messageOf(error).replaceAll(/\p{Cc}/gu, '?');
4214
+ throw new MfeflowError('E403', `${shown} is not JSON (${reason})`, {
4215
+ cause: error
4216
+ });
4217
+ }
4218
+ const issues = validateFeatureRegistryIndex(value);
4219
+ if (issues.length > 0) {
4220
+ const first = issues.slice(0, 3).map((i)=>`${i.path || '(root)'} ${i.message}`);
4221
+ const more = issues.length > 3 ? `; and ${String(issues.length - 3)} more` : '';
4222
+ throw new MfeflowError('E403', `${shown}: ${first.join('; ')}${more}`);
4223
+ }
4224
+ return itemsOf(value);
4225
+ }
4226
+ function itemsOf(value) {
4227
+ const holder = {
4228
+ items: []
4229
+ };
4230
+ Object.assign(holder, value);
4231
+ return holder.items;
4232
+ }
4233
+ function parseFeature(text, trust, label) {
4234
+ let value;
4235
+ try {
4236
+ value = JSON.parse(text);
4237
+ } catch (error) {
4238
+ throw new MfeflowError('E202', `${label}: ${messageOf(error)}`, {
4239
+ cause: error
4240
+ });
4241
+ }
4242
+ if ('object' != typeof value || null === value || Array.isArray(value)) throw new MfeflowError('E202', `${label}: must be a JSON object`);
4243
+ const record = Object.fromEntries(Object.entries(value));
4244
+ if ('third-party' === trust) assertAllowedKeys(record, label);
4245
+ assertKnownOps(record, label);
4246
+ const issues = validateFeatureManifest(record);
4247
+ if (issues.length > 0) {
4248
+ const detail = issues.map((i)=>`${i.path || '(root)'} ${i.message}`).join('; ');
4249
+ throw new MfeflowError('E202', `${label}: ${detail}`);
4250
+ }
4251
+ const manifest = toManifest(record, label);
4252
+ if ('third-party' === trust) assertThirdPartyContent(manifest, label);
4253
+ return manifest;
4254
+ }
4255
+ function toManifest(record, label) {
4256
+ const manifest = {
4257
+ name: '',
4258
+ version: ''
4259
+ };
4260
+ Object.assign(manifest, record);
4261
+ if ('' === manifest.name) throw new MfeflowError('E202', `${label}: name is required`);
4262
+ return manifest;
4263
+ }
4264
+ async function loadFeature(name, options) {
4265
+ if (!FEATURE_NAME.test(name)) throw new MfeflowError('E201', `"${name}" is not a feature name`);
4266
+ if (name.startsWith('@')) return loadRemote(name, options);
4267
+ return loadLocal(name, options);
4268
+ }
4269
+ async function loadLocal(name, options) {
4270
+ const ops = options.fs ?? realFs;
4271
+ if (!options.featuresDir) throw new MfeflowError('E201', `"${name}" (no features folder configured)`);
4272
+ const dir = node_path.join(options.featuresDir, name);
4273
+ const text = await readIfExists(ops, node_path.join(dir, 'feature.json'));
4274
+ if (void 0 === text) throw new MfeflowError('E201', `"${name}" (looked in ${options.featuresDir})`);
4275
+ const manifest = parseFeature(text, 'first-party', `${name}/feature.json`);
4276
+ if (manifest.name !== name) throw new MfeflowError('E202', `${name}/feature.json: name is "${manifest.name}", expected "${name}"`);
4277
+ return {
4278
+ manifest,
4279
+ trust: 'first-party',
4280
+ dir,
4281
+ readAsset: async (relative)=>{
4282
+ if (!isSafeRelativePath(relative)) throw new MfeflowError('E105', `"${relative}"`);
4283
+ const content = await readIfExists(ops, node_path.join(dir, ...relative.split('/')));
4284
+ if (void 0 === content) throw new MfeflowError('E201', `${name}: file "${relative}" is missing`);
4285
+ return content;
4286
+ }
4287
+ };
4288
+ }
4289
+ async function loadRemote(name, options) {
4290
+ const registries = options.registries ?? {};
4291
+ const text = await fetchRemoteText(name, registries, options.fetch, options.version, true === options.trusted);
4292
+ const manifest = parseFeature(text, 'third-party', name);
4293
+ if (manifest.name !== name) throw new MfeflowError('E402', `asked for ${name}, got ${manifest.name}`);
4294
+ if (true === options.checkIndex) await assertIndexAgrees(name, manifest.version, registries, options);
4295
+ return {
4296
+ manifest,
4297
+ trust: 'third-party',
4298
+ integrity: integrityOf(text)
4299
+ };
4300
+ }
4301
+ async function assertIndexAgrees(name, version, registries, options) {
4302
+ const slash = name.indexOf('/');
4303
+ let listed;
4304
+ try {
4305
+ listed = await indexedVersion(name.slice(0, slash), name.slice(slash + 1), registries, options.fetch, true === options.trusted);
4306
+ } catch (error) {
4307
+ if (error instanceof MfeflowError && 'MFEFLOW_E401' === error.code) return;
4308
+ throw error;
4309
+ }
4310
+ if (void 0 !== listed && listed !== version) throw new MfeflowError('E402', `the index lists ${name} at ${listed} but its feature.json is ${version}`);
4311
+ }
4312
+ async function listLocalFeatures(featuresDir, ops = realFs) {
4313
+ let dirs;
4314
+ try {
4315
+ dirs = await ops.listDirs(featuresDir);
4316
+ } catch {
4317
+ return [];
4318
+ }
4319
+ const checked = await Promise.all(dirs.map(async (dir)=>await readIfExists(ops, node_path.join(featuresDir, dir, 'feature.json')) === void 0 ? void 0 : dir));
4320
+ return checked.filter((d)=>void 0 !== d).toSorted((a, b)=>a.localeCompare(b));
4321
+ }
4322
+ const TRUST_FILE = 'trusted-registries.json';
4323
+ const TRUST_ENV = 'MFEFLOW_TRUST_REGISTRIES';
4324
+ const trust_set = (value)=>void 0 === value || '' === value ? void 0 : value;
4325
+ function configDirOf(env, platform = process.platform, home = node_os.homedir()) {
4326
+ const override = trust_set(env.MFEFLOW_CONFIG_DIR);
4327
+ if (void 0 !== override) return override;
4328
+ const xdg = trust_set(env.XDG_CONFIG_HOME);
4329
+ if (void 0 !== xdg) return node_path.join(xdg, 'mfeflow');
4330
+ const appData = trust_set(env.APPDATA);
4331
+ if ('win32' === platform && void 0 !== appData) return node_path.join(appData, 'mfeflow');
4332
+ return node_path.join(home, '.config', 'mfeflow');
4333
+ }
4334
+ function trustFileIn(configDir) {
4335
+ return node_path.join(configDir, TRUST_FILE);
4336
+ }
4337
+ function isEntry(value) {
4338
+ if ('object' != typeof value || null === value) return false;
4339
+ const { namespace, template } = Object.fromEntries(Object.entries(value));
4340
+ return 'string' == typeof namespace && REGISTRY_NAMESPACE.test(namespace) && 'string' == typeof template && void 0 === registryTemplateProblem(template);
4341
+ }
4342
+ async function readTrusted(configDir, ops = realFs) {
4343
+ let text;
4344
+ try {
4345
+ text = await readIfExists(ops, trustFileIn(configDir));
4346
+ } catch {
4347
+ return [];
4348
+ }
4349
+ if (void 0 === text) return [];
4350
+ try {
4351
+ const value = JSON.parse(text);
4352
+ if ('object' != typeof value || null === value) return [];
4353
+ const { registries } = Object.fromEntries(Object.entries(value));
4354
+ return Array.isArray(registries) ? registries.filter(isEntry) : [];
4355
+ } catch {
4356
+ return [];
4357
+ }
4358
+ }
4359
+ async function save(configDir, entries, ops) {
4360
+ const sorted = entries.toSorted((a, b)=>a.namespace.localeCompare(b.namespace) || a.template.localeCompare(b.template));
4361
+ const body = `${JSON.stringify({
4362
+ version: 1,
4363
+ registries: sorted
4364
+ }, null, 2)}\n`;
4365
+ try {
4366
+ await writeFileAtomic(ops, trustFileIn(configDir), body);
4367
+ } catch (error) {
4368
+ throw new MfeflowError('E106', trustFileIn(configDir), {
4369
+ cause: error
4370
+ });
4371
+ }
4372
+ }
4373
+ async function addTrusted(configDir, entry, ops = realFs) {
4374
+ const current = await readTrusted(configDir, ops);
4375
+ if (current.some((e)=>e.namespace === entry.namespace && e.template === entry.template)) return false;
4376
+ await save(configDir, [
4377
+ ...current,
4378
+ entry
4379
+ ], ops);
4380
+ return true;
4381
+ }
4382
+ async function removeTrusted(configDir, namespace, ops = realFs) {
4383
+ const current = await readTrusted(configDir, ops);
4384
+ const kept = current.filter((e)=>e.namespace !== namespace);
4385
+ if (kept.length === current.length) return 0;
4386
+ await save(configDir, kept, ops);
4387
+ return current.length - kept.length;
4388
+ }
4389
+ function parseTrustEnv(value) {
4390
+ return (value ?? '').split(',').map((part)=>part.trim()).flatMap((part)=>{
4391
+ const eq = part.indexOf('=');
4392
+ const namespace = -1 === eq ? part : part.slice(0, eq).trim();
4393
+ if (!REGISTRY_NAMESPACE.test(namespace)) return [];
4394
+ const template = -1 === eq ? '' : part.slice(eq + 1).trim();
4395
+ return [
4396
+ '' === template ? {
4397
+ namespace
4398
+ } : {
4399
+ namespace,
4400
+ template
4401
+ }
4402
+ ];
4403
+ });
4404
+ }
4405
+ function trustedNamespaces(registries, recorded, fromEnv = []) {
4406
+ const fetch1 = new Set();
4407
+ const privateOk = new Set();
4408
+ for (const [namespace, template] of Object.entries(registries)){
4409
+ const inFile = recorded.some((e)=>e.namespace === namespace && e.template === template);
4410
+ const items = fromEnv.filter((e)=>e.namespace === namespace);
4411
+ const pinned = items.some((e)=>e.template === template);
4412
+ if (inFile || pinned) privateOk.add(namespace);
4413
+ if (inFile || items.some((e)=>void 0 === e.template || e.template === template)) fetch1.add(namespace);
4414
+ }
4415
+ return {
4416
+ fetch: fetch1,
4417
+ privateOk
4418
+ };
4419
+ }
2562
4420
  const registry_build_text = (value)=>`${JSON.stringify(value, null, 2)}\n`;
2563
4421
  async function buildRegistry(dir, ops = realFs) {
2564
4422
  let folders;
@@ -2672,4 +4530,4 @@ function packageInfo() {
2672
4530
  version
2673
4531
  };
2674
4532
  }
2675
- export { ERROR_CATALOGUE, MfeflowError, PROJECT_FILE, PlanBuilder, RUNTIME_CONFIG_FILE, TRUST_ENV, addTrusted, applyPlan, applyTransform, buildRegistry, checkIntegrity, configDirOf, dotenvKeys, emptyPlan, fetchFeatureIndex, fetchRemoteText, findDiverged, findProjectRoot, getHttps, hasErrno, hashContent, inOrder, indexedVersion, integrityOf, isMfeflowError, isProtectedPath, listLocalFeatures, loadFeature, loadProject, messageOf, packageInfo, parseFeature, parseProject, parseTrustEnv, readCappedText, readTrusted, realFs, recordPlan, registryUrl, removeTrusted, renderDiff, renderTemplate, resolveFeature, resolveOptions, runtimeConfigFiles, saveProject, schemaKeys, serializeProject, substitute, summarize, trustFileIn, trustedNamespaces, writeFileAtomic };
4533
+ 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, isEnvKeyFile, isMfeflowError, isProtectedPath, legacyAppFileNotice, listLocalFeatures, loadFeature, loadProject, messageOf, packageInfo, parseFeature, parseProject, parseTrustEnv, planAdoption, planUpgrade, readAppFile, readCappedText, readTrusted, realFs, recordEnvKey, recordPlan, registryUrl, removeTrusted, renderDiff, renderTemplate, resolveFeature, resolveOptions, runtimeConfigFiles, saveProject, schemaKeys, serializeProject, substitute, summarize, symlinkedPrefix, trustFileIn, trustedNamespaces, writeFileAtomic };