@mfeflow/core 0.5.0 → 0.6.0

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