mfeflow 0.0.0-stage → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (120) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +126 -2
  3. package/assets/features/analytics-gtm/feature.json +117 -0
  4. package/assets/features/analytics-gtm/files/Analytics.tsx +13 -0
  5. package/assets/features/analytics-gtm/files/ConsentBanner.tsx +47 -0
  6. package/assets/features/analytics-gtm/files/consent-banner-test.tsx +36 -0
  7. package/assets/features/analytics-gtm/files/init.ts +26 -0
  8. package/assets/features/analytics-gtm/files/remote-analytics-test.ts.eta +18 -0
  9. package/assets/features/analytics-gtm/files/remote-analytics.ts.eta +13 -0
  10. package/assets/features/analytics-gtm/files/shell-init-test.ts +42 -0
  11. package/assets/features/ci-github/feature.json +26 -0
  12. package/assets/features/ci-github/files/ci.md +41 -0
  13. package/assets/features/ci-github/files/ci.yml +63 -0
  14. package/assets/features/env-t3/feature.json +378 -0
  15. package/assets/features/env-t3/files/check-runtime-config.ts +105 -0
  16. package/assets/features/env-t3/files/env.arktype.ts +12 -0
  17. package/assets/features/env-t3/files/env.example +1 -0
  18. package/assets/features/env-t3/files/env.valibot.ts +12 -0
  19. package/assets/features/env-t3/files/env.zod.ts +12 -0
  20. package/assets/features/env-t3/files/runtime.ts.eta +30 -0
  21. package/assets/features/env-t3/files/schema.arktype.ts +8 -0
  22. package/assets/features/env-t3/files/schema.valibot.ts +8 -0
  23. package/assets/features/env-t3/files/schema.zod.ts +8 -0
  24. package/assets/features/git-hooks/feature.json +42 -0
  25. package/assets/features/git-hooks/files/commitlint.config.js +1 -0
  26. package/assets/features/git-hooks/files/lefthook.yml +9 -0
  27. package/assets/features/lint-oxlint/feature.json +60 -0
  28. package/assets/features/lint-oxlint/files/oxlintrc.json +5 -0
  29. package/assets/features/lint-oxlint/files/prettierignore +5 -0
  30. package/assets/features/lint-oxlint/files/prettierrc.json +5 -0
  31. package/assets/features/telemetry-otel/feature.json +159 -0
  32. package/assets/features/telemetry-otel/files/init.ts.eta +53 -0
  33. package/assets/features/telemetry-otel/files/remote-telemetry-test.ts.eta +13 -0
  34. package/assets/features/telemetry-otel/files/remote-telemetry.ts.eta +11 -0
  35. package/assets/features/telemetry-otel/files/shell-init-test.ts.eta +36 -0
  36. package/assets/features/testing/feature.json +202 -0
  37. package/assets/features/testing/files/handlers.ts +5 -0
  38. package/assets/features/testing/files/playwright.config.ts +30 -0
  39. package/assets/features/testing/files/root-vitest.config.ts +6 -0
  40. package/assets/features/testing/files/sample-handlers-test.ts +16 -0
  41. package/assets/features/testing/files/sample-remote-test.tsx +12 -0
  42. package/assets/features/testing/files/sample-shell-test.tsx +11 -0
  43. package/assets/features/testing/files/sample-smoke-e2e.ts +53 -0
  44. package/assets/features/testing/files/server.ts +4 -0
  45. package/assets/features/testing/files/setup.ts +18 -0
  46. package/assets/features/testing/files/vitest.config.ts +11 -0
  47. package/assets/features/tsconfig/feature.json +78 -0
  48. package/assets/features/tsconfig/files/root.tsconfig.json +4 -0
  49. package/assets/features/tsconfig/files/tsconfig.json +5 -0
  50. package/assets/features/url-factory/feature.json +30 -0
  51. package/assets/features/url-factory/files/endpoints.ts +12 -0
  52. package/assets/templates/monorepo/feature.json +295 -0
  53. package/assets/templates/monorepo/files/remote/mfe.json.eta +7 -0
  54. package/assets/templates/monorepo/files/remote/package.json.eta +6 -0
  55. package/assets/templates/monorepo/files/remote/rsbuild.config.ts.eta +7 -0
  56. package/assets/templates/monorepo/files/remote/src/bootstrap.tsx.eta +6 -0
  57. package/assets/templates/monorepo/files/remote/src/env.d.ts.eta +1 -0
  58. package/assets/templates/monorepo/files/remote/src/exposed/App.tsx.eta +18 -0
  59. package/assets/templates/monorepo/files/remote/src/index.ts.eta +2 -0
  60. package/assets/templates/monorepo/files/root/AGENTS.md.eta +17 -0
  61. package/assets/templates/monorepo/files/root/README.md.eta +52 -0
  62. package/assets/templates/monorepo/files/root/gitignore.eta +11 -0
  63. package/assets/templates/monorepo/files/root/nvmrc.eta +1 -0
  64. package/assets/templates/monorepo/files/root/package.json.eta +9 -0
  65. package/assets/templates/monorepo/files/root/pnpm-workspace.yaml.eta +7 -0
  66. package/assets/templates/monorepo/files/root/turbo.json.eta +16 -0
  67. package/assets/templates/monorepo/files/shell/mfe.json.eta +7 -0
  68. package/assets/templates/monorepo/files/shell/package.json.eta +6 -0
  69. package/assets/templates/monorepo/files/shell/rsbuild.config.ts.eta +74 -0
  70. package/assets/templates/monorepo/files/shell/src/App.tsx.eta +46 -0
  71. package/assets/templates/monorepo/files/shell/src/bootstrap.tsx.eta +24 -0
  72. package/assets/templates/monorepo/files/shell/src/env.d.ts.eta +1 -0
  73. package/assets/templates/monorepo/files/shell/src/index.ts.eta +4 -0
  74. package/assets/templates/monorepo/files/shell/src/pages/Home.tsx.eta +12 -0
  75. package/assets/templates/monorepo/files/shell/src/remotes/route-table.tsx.eta +12 -0
  76. package/assets/templates/monorepo/glue/registry-entry.json.eta +5 -0
  77. package/assets/templates/monorepo/glue/route.tsx.eta +5 -0
  78. package/bin/mfeflow.js +4 -0
  79. package/dist/args.d.ts +68 -0
  80. package/dist/assets.d.ts +13 -0
  81. package/dist/catalog.d.ts +24 -0
  82. package/dist/checks/compat.d.ts +2 -0
  83. package/dist/checks/config.d.ts +8 -0
  84. package/dist/checks/env.d.ts +6 -0
  85. package/dist/checks/index.d.ts +6 -0
  86. package/dist/checks/plugins.d.ts +4 -0
  87. package/dist/checks/singletons.d.ts +9 -0
  88. package/dist/checks/workspace.d.ts +2 -0
  89. package/dist/cli.d.ts +11 -0
  90. package/dist/commands/add.d.ts +84 -0
  91. package/dist/commands/budget.d.ts +56 -0
  92. package/dist/commands/dev.d.ts +62 -0
  93. package/dist/commands/doctor.d.ts +141 -0
  94. package/dist/commands/env-key.d.ts +4 -0
  95. package/dist/commands/features.d.ts +53 -0
  96. package/dist/commands/init.d.ts +102 -0
  97. package/dist/devregistry.d.ts +19 -0
  98. package/dist/exit.d.ts +31 -0
  99. package/dist/findings.d.ts +35 -0
  100. package/dist/glue.d.ts +41 -0
  101. package/dist/help.d.ts +12 -0
  102. package/dist/index.d.ts +8 -0
  103. package/dist/index.js +4154 -0
  104. package/dist/inspect.d.ts +57 -0
  105. package/dist/io.d.ts +10 -0
  106. package/dist/local.d.ts +28 -0
  107. package/dist/options.d.ts +7 -0
  108. package/dist/output.d.ts +157 -0
  109. package/dist/pipeline.d.ts +7 -0
  110. package/dist/preset.d.ts +19 -0
  111. package/dist/proc.d.ts +79 -0
  112. package/dist/prompter.d.ts +28 -0
  113. package/dist/report.d.ts +26 -0
  114. package/dist/runtime.d.ts +17 -0
  115. package/dist/singletons.d.ts +11 -0
  116. package/dist/sizes.d.ts +17 -0
  117. package/dist/system.d.ts +20 -0
  118. package/dist/version.d.ts +8 -0
  119. package/dist/write.d.ts +26 -0
  120. package/package.json +66 -3
package/dist/index.js ADDED
@@ -0,0 +1,4154 @@
1
+ import { stripVTControlCharacters } from "node:util";
2
+ import { MfeflowError, PlanBuilder, applyPlan, dotenvKeys, findDiverged, findProjectRoot, getHttps, hasErrno, inOrder, isMfeflowError, loadFeature, loadProject, messageOf, parseFeature, readCappedText, realFs, recordPlan, renderDiff, renderTemplate, resolveFeature, runtimeConfigFiles, schemaKeys, serializeProject, summarize } from "@mfeflow/core";
3
+ import { defineCommand, runCommand } from "citty";
4
+ import node_path from "node:path";
5
+ import { COST_MODEL, DOCS_URL, MFE_NAME, assetsFromManifest, isSafeRelativePath, judgeCost, round1, validateMfeManifest, validateRegistryEntry } from "@mfeflow/contracts";
6
+ import promises from "node:fs/promises";
7
+ import node_fs from "node:fs";
8
+ import { createRequire } from "node:module";
9
+ import semver from "semver";
10
+ import { parse } from "yaml";
11
+ import node_zlib from "node:zlib";
12
+ import { spawn, spawnSync } from "node:child_process";
13
+ import node_net from "node:net";
14
+ import { fileURLToPath } from "node:url";
15
+ import * as __rspack_external__clack_prompts_d2d5c36d from "@clack/prompts";
16
+ const GLOBAL_ARGS = {
17
+ yes: {
18
+ type: 'boolean',
19
+ alias: 'y',
20
+ description: 'Take the defaults and never prompt'
21
+ },
22
+ preset: {
23
+ type: 'string',
24
+ description: 'Replay options from a JSON file or an https URL',
25
+ valueHint: 'file|url'
26
+ },
27
+ json: {
28
+ type: 'boolean',
29
+ description: 'Print one JSON document on stdout, nothing else'
30
+ },
31
+ 'dry-run': {
32
+ type: 'boolean',
33
+ description: 'Print the summary and diff, write nothing'
34
+ },
35
+ cwd: {
36
+ type: 'string',
37
+ description: 'Run as if started in this folder',
38
+ valueHint: 'dir'
39
+ },
40
+ install: {
41
+ type: 'boolean',
42
+ default: true,
43
+ description: 'Run the package manager after writing (--no-install to skip)'
44
+ },
45
+ verbose: {
46
+ type: 'boolean',
47
+ description: 'Show package manager output and stack traces'
48
+ },
49
+ local: {
50
+ type: 'string',
51
+ description: 'Link @mfeflow/* from a built mfeflow checkout (env MFEFLOW_LOCAL)',
52
+ valueHint: 'path'
53
+ },
54
+ help: {
55
+ type: 'boolean',
56
+ alias: 'h',
57
+ description: 'Show this help'
58
+ }
59
+ };
60
+ function args_text(args, key) {
61
+ const value = args[key];
62
+ return 'string' == typeof value && '' !== value ? value : void 0;
63
+ }
64
+ function readGlobals(args, env, cwd, interactive = false) {
65
+ const folder = args_text(args, 'cwd');
66
+ const yes = true === args.yes;
67
+ const json = true === args.json;
68
+ return {
69
+ yes,
70
+ preset: args_text(args, 'preset'),
71
+ json,
72
+ dryRun: true === args['dry-run'],
73
+ cwd: void 0 === folder ? cwd : node_path.resolve(cwd, folder),
74
+ install: false !== args.install,
75
+ verbose: true === args.verbose,
76
+ local: args_text(args, 'local') ?? ('' === env.MFEFLOW_LOCAL ? void 0 : env.MFEFLOW_LOCAL),
77
+ canPrompt: interactive && !yes && !json
78
+ };
79
+ }
80
+ function flagNames(raw) {
81
+ const names = [];
82
+ for (const token of raw){
83
+ if ('--' === token) break;
84
+ if (!token.startsWith('-') || '-' === token) continue;
85
+ const body = token.replace(/^--?/, '').split('=')[0] ?? '';
86
+ names.push(body);
87
+ }
88
+ return names;
89
+ }
90
+ function assertKnownFlags(raw, defs) {
91
+ const known = new Set();
92
+ for (const [name, def] of Object.entries(defs))if ('positional' !== def.type) {
93
+ known.add(name);
94
+ if ('alias' in def) for (const alias of [
95
+ def.alias ?? []
96
+ ].flat())known.add(alias);
97
+ if ('boolean' === def.type) known.add(`no-${name}`);
98
+ }
99
+ for (const name of flagNames(raw))if (!known.has(name)) throw new MfeflowError('E107', `unknown flag "${1 === name.length ? '-' : '--'}${name}"`);
100
+ }
101
+ function repeated(raw, name) {
102
+ const values = [];
103
+ const flag = `--${name}`;
104
+ for (const [index, token] of raw.entries()){
105
+ if ('--' === token) break;
106
+ if (token === flag) {
107
+ const next = raw[index + 1];
108
+ if (void 0 !== next) values.push(next);
109
+ } else if (token.startsWith(`${flag}=`)) values.push(token.slice(flag.length + 1));
110
+ }
111
+ return values;
112
+ }
113
+ function splitList(value) {
114
+ if (void 0 === value) return;
115
+ const parts = 'string' == typeof value ? value.split(',') : value;
116
+ return parts.map((part)=>part.trim()).filter((part)=>'' !== part);
117
+ }
118
+ const FIRST_PARTY = [
119
+ {
120
+ name: 'core',
121
+ always: true,
122
+ default: true
123
+ },
124
+ {
125
+ name: 'tsconfig',
126
+ always: true,
127
+ default: true
128
+ },
129
+ {
130
+ name: 'lint-oxlint',
131
+ always: false,
132
+ default: true
133
+ },
134
+ {
135
+ name: 'url-factory',
136
+ always: false,
137
+ default: true
138
+ },
139
+ {
140
+ name: 'env-t3',
141
+ always: false,
142
+ default: true
143
+ },
144
+ {
145
+ name: 'testing',
146
+ always: false,
147
+ default: true
148
+ },
149
+ {
150
+ name: 'analytics-gtm',
151
+ always: false,
152
+ default: false
153
+ },
154
+ {
155
+ name: 'telemetry-otel',
156
+ always: false,
157
+ default: false
158
+ },
159
+ {
160
+ name: 'ci-github',
161
+ always: false,
162
+ default: false
163
+ },
164
+ {
165
+ name: 'git-hooks',
166
+ always: false,
167
+ default: false
168
+ }
169
+ ];
170
+ function isFirstParty(name) {
171
+ return FIRST_PARTY.some((feature)=>feature.name === name);
172
+ }
173
+ const OPTIONAL_FEATURES = FIRST_PARTY.filter((f)=>!f.always).map((f)=>f.name);
174
+ const DEFAULT_FEATURES = FIRST_PARTY.filter((f)=>!f.always && f.default).map((f)=>f.name);
175
+ async function loadFirstParty(name, roots) {
176
+ if ('core' !== name) return loadFeature(name, {
177
+ featuresDir: roots.features
178
+ });
179
+ const dir = node_path.join(roots.templates, 'monorepo');
180
+ let text;
181
+ try {
182
+ text = await realFs.readFile(node_path.join(dir, 'feature.json'));
183
+ } catch (error) {
184
+ throw new MfeflowError('E201', `core (looked in ${dir})`, {
185
+ cause: error
186
+ });
187
+ }
188
+ return {
189
+ manifest: parseFeature(text, 'first-party', 'core/feature.json'),
190
+ trust: 'first-party',
191
+ readAsset: async (relative)=>{
192
+ if (!isSafeRelativePath(relative)) throw new MfeflowError('E105', `"${relative}"`);
193
+ return realFs.readFile(node_path.join(dir, ...relative.split('/')));
194
+ }
195
+ };
196
+ }
197
+ function loadAny(name, source) {
198
+ if (isFirstParty(name)) return loadFirstParty(name, source.roots);
199
+ return loadFeature(name, {
200
+ featuresDir: source.roots.features,
201
+ ...source.registries ? {
202
+ registries: source.registries
203
+ } : {},
204
+ ...source.fetch ? {
205
+ fetch: source.fetch
206
+ } : {},
207
+ ...void 0 === source.version ? {} : {
208
+ version: source.version
209
+ }
210
+ });
211
+ }
212
+ const RESERVED = new Set([
213
+ 'root',
214
+ 'shell',
215
+ 'remotes'
216
+ ]);
217
+ function assertAppName(name, apps = {}) {
218
+ if (!MFE_NAME.test(name)) throw new MfeflowError('E215', `"${name}" is not a Module Federation name`);
219
+ if (RESERVED.has(name)) throw new MfeflowError('E215', `"${name}" is a reserved name`);
220
+ if (name in apps) throw new MfeflowError('E215', `"${name}" is already an app`);
221
+ }
222
+ function usedPorts(project) {
223
+ return new Set(Object.values(project.apps).flatMap((app)=>void 0 === app.port ? [] : [
224
+ app.port
225
+ ]));
226
+ }
227
+ function freePort(project, start) {
228
+ const used = usedPorts(project);
229
+ let port = start;
230
+ while(used.has(port))port += 1;
231
+ return port;
232
+ }
233
+ function pascal(name) {
234
+ return name.split('_').map((part)=>part.charAt(0).toUpperCase() + part.slice(1)).join('');
235
+ }
236
+ function routeComponent(remote) {
237
+ return `${pascal(remote)}Route`;
238
+ }
239
+ const REGISTRY_ENVS = [
240
+ 'dev',
241
+ 'staging',
242
+ 'prod'
243
+ ];
244
+ const ENV_HOSTS = {
245
+ staging: 'https://staging.example.com',
246
+ prod: 'https://app.example.com'
247
+ };
248
+ async function glueTemplate(roots, file) {
249
+ return promises.readFile(node_path.join(roots.templates, 'monorepo', 'glue', file), 'utf8');
250
+ }
251
+ const asFeatureFile = (file)=>({
252
+ to: file.path,
253
+ content: file.content
254
+ });
255
+ async function registryEntryFiles(roots, remote, envs = REGISTRY_ENVS) {
256
+ const { name, port } = remote;
257
+ const template = await glueTemplate(roots, 'registry-entry.json.eta');
258
+ const urls = {
259
+ dev: {
260
+ version: 'dev',
261
+ manifestUrl: `http://localhost:${String(port)}/mf-manifest.json`
262
+ },
263
+ staging: {
264
+ version: '0.0.0',
265
+ manifestUrl: `${ENV_HOSTS.staging}/mfe/${name}/mf-manifest.json`
266
+ },
267
+ prod: {
268
+ version: '0.0.0',
269
+ manifestUrl: `${ENV_HOSTS.prod}/mfe/${name}/mf-manifest.json`
270
+ }
271
+ };
272
+ return envs.flatMap((env)=>{
273
+ const entry = urls[env];
274
+ if (void 0 === entry) return [];
275
+ return [
276
+ {
277
+ path: `registry/envs/${env}/${name}.json`,
278
+ content: renderTemplate(template, {
279
+ name,
280
+ ...entry
281
+ }, `registry/${env}`)
282
+ }
283
+ ];
284
+ });
285
+ }
286
+ async function remoteGlue(options) {
287
+ const { name } = options.remote;
288
+ const files = [];
289
+ const transforms = [];
290
+ if (options.registry) files.push(...(await registryEntryFiles(options.roots, options.remote)).map(asFeatureFile));
291
+ if (options.shells.length > 0) {
292
+ const route = await glueTemplate(options.roots, 'route.tsx.eta');
293
+ const component = routeComponent(name);
294
+ for (const target of options.shells){
295
+ files.push({
296
+ to: `src/remotes/${name}.tsx`,
297
+ target,
298
+ content: renderTemplate(route, {
299
+ name,
300
+ component
301
+ }, 'route')
302
+ });
303
+ transforms.push({
304
+ op: 'add-route',
305
+ file: 'src/remotes/route-table.tsx',
306
+ target,
307
+ path: `/${name}`,
308
+ component,
309
+ from: `./${name}`
310
+ });
311
+ }
312
+ }
313
+ return {
314
+ manifest: {
315
+ name: 'core',
316
+ version: options.version,
317
+ files,
318
+ transforms
319
+ },
320
+ trust: 'first-party'
321
+ };
322
+ }
323
+ function partsForApp(feature, from, app) {
324
+ const { manifest } = feature;
325
+ const mine = (items)=>(items ?? []).filter((item)=>item.target === from).map((item)=>Object.assign({}, item, {
326
+ target: app
327
+ }));
328
+ const files = mine(manifest.files);
329
+ const dependencies = mine(manifest.dependencies);
330
+ const scripts = mine(manifest.scripts);
331
+ const transforms = mine(manifest.transforms);
332
+ if (files.length + dependencies.length + scripts.length + transforms.length === 0) return;
333
+ return {
334
+ ...feature,
335
+ manifest: {
336
+ name: manifest.name,
337
+ version: manifest.version,
338
+ ...manifest.prompts ? {
339
+ prompts: manifest.prompts
340
+ } : {},
341
+ files,
342
+ dependencies,
343
+ scripts,
344
+ transforms
345
+ }
346
+ };
347
+ }
348
+ function options_value(raw) {
349
+ if ('true' === raw) return true;
350
+ if ('false' === raw) return false;
351
+ return raw;
352
+ }
353
+ function parseFeatureOptions(flags) {
354
+ const out = {};
355
+ for (const flag of flags){
356
+ const equals = flag.indexOf('=');
357
+ const dot = flag.indexOf('.');
358
+ if (equals < 1 || dot < 1 || dot > equals - 1) throw new MfeflowError('E107', `--option "${flag}" must look like feature.key=value`);
359
+ const feature = flag.slice(0, dot);
360
+ const key = flag.slice(dot + 1, equals);
361
+ (out[feature] ??= {})[key] = options_value(flag.slice(equals + 1));
362
+ }
363
+ return out;
364
+ }
365
+ function mergeOptions(earlier, later) {
366
+ const out = {};
367
+ for (const source of [
368
+ earlier,
369
+ later
370
+ ])for (const [feature, given] of Object.entries(source))out[feature] = {
371
+ ...out[feature],
372
+ ...given
373
+ };
374
+ return out;
375
+ }
376
+ function assertOptionsAreForFeatures(options, features) {
377
+ for (const feature of Object.keys(options))if (!features.includes(feature)) throw new MfeflowError('E214', `options given for "${feature}", which is not one of the features in this run (${features.join(', ')})`);
378
+ }
379
+ const EXIT = {
380
+ ok: 0,
381
+ unexpected: 1,
382
+ usage: 2,
383
+ project: 3,
384
+ feature: 4,
385
+ build: 5,
386
+ registry: 6,
387
+ upgrade: 7,
388
+ cancelled: 130
389
+ };
390
+ class Cancelled extends Error {
391
+ constructor(){
392
+ super('Cancelled');
393
+ this.name = 'Cancelled';
394
+ }
395
+ }
396
+ const USAGE_ERRORS = new Set([
397
+ 'MFEFLOW_E107',
398
+ 'MFEFLOW_E215'
399
+ ]);
400
+ const BY_RANGE = {
401
+ 1: EXIT.project,
402
+ 2: EXIT.feature,
403
+ 3: EXIT.build,
404
+ 4: EXIT.registry,
405
+ 5: EXIT.upgrade
406
+ };
407
+ function exitCodeOfCode(code) {
408
+ if (USAGE_ERRORS.has(code)) return EXIT.usage;
409
+ return BY_RANGE[code.charAt(9)] ?? EXIT.unexpected;
410
+ }
411
+ function exitCodeFor(error) {
412
+ if (error instanceof Cancelled) return EXIT.cancelled;
413
+ if (!isMfeflowError(error)) return EXIT.unexpected;
414
+ return exitCodeOfCode(error.code);
415
+ }
416
+ function reportOf(error) {
417
+ return {
418
+ code: error.code,
419
+ message: error.message,
420
+ fix: error.fix,
421
+ docs: error.docs
422
+ };
423
+ }
424
+ function unexpectedReport(error) {
425
+ return {
426
+ code: 'UNEXPECTED',
427
+ message: messageOf(error),
428
+ fix: 'This is a bug in mfeflow. Run again with --verbose for the stack, and report it.',
429
+ docs: 'https://github.com/mfeflow/mfeflow/issues'
430
+ };
431
+ }
432
+ function unexpectedText(error, verbose) {
433
+ const stack = error instanceof Error && void 0 !== error.stack ? error.stack : messageOf(error);
434
+ return verbose ? stack : `Unexpected error: ${messageOf(error)}\n Run again with --verbose for the stack.`;
435
+ }
436
+ async function runChecks(view, checks) {
437
+ const outcomes = await Promise.all(checks.map((check)=>check.run(view)));
438
+ const reports = checks.map((check, i)=>{
439
+ const outcome = outcomes[i] ?? {
440
+ findings: []
441
+ };
442
+ const hasError = outcome.findings.some((f)=>'error' === f.severity);
443
+ const status = void 0 !== outcome.skipped ? 'skipped' : hasError ? 'fail' : outcome.findings.length > 0 ? 'warn' : 'pass';
444
+ return {
445
+ id: check.id,
446
+ title: check.title,
447
+ status,
448
+ ...void 0 === outcome.skipped ? {} : {
449
+ skipped: outcome.skipped
450
+ },
451
+ findings: outcome.findings.length
452
+ };
453
+ });
454
+ return {
455
+ checks: reports,
456
+ findings: outcomes.flatMap((o)=>o.findings)
457
+ };
458
+ }
459
+ function summarizeReport(report, fixed) {
460
+ return {
461
+ errors: report.findings.filter((f)=>'error' === f.severity).length,
462
+ warnings: report.findings.filter((f)=>'warning' === f.severity).length,
463
+ skipped: report.checks.filter((c)=>'skipped' === c.status).length,
464
+ fixed
465
+ };
466
+ }
467
+ function exitForFindings(findings) {
468
+ const codes = findings.filter((f)=>'error' === f.severity).map((f)=>exitCodeOfCode(f.code));
469
+ return 0 === codes.length ? EXIT.ok : Math.min(...codes);
470
+ }
471
+ const MARK = {
472
+ pass: 'ok ',
473
+ warn: 'warn',
474
+ fail: 'FAIL',
475
+ skipped: 'skip'
476
+ };
477
+ function formatReport(report, summary) {
478
+ const lines = report.checks.map((c)=>`${MARK[c.status]} ${c.title}${void 0 === c.skipped ? '' : ` (${c.skipped})`}`);
479
+ for (const f of report.findings)lines.push('', `${'error' === f.severity ? 'error' : 'warning'} ${f.code} ${f.message}`, ` Fix: ${f.fix}${f.fixable ? ' (mfeflow doctor --fix)' : ''}`, ` ${f.docs}`);
480
+ lines.push('', `${String(summary.errors)} error(s), ${String(summary.warnings)} warning(s), ${String(summary.skipped)} skipped${summary.fixed > 0 ? `, ${String(summary.fixed)} fixed` : ''}`);
481
+ return lines.join('\n');
482
+ }
483
+ function planReport(plan) {
484
+ const features = new Map(plan.features.map((f)=>[
485
+ f.name,
486
+ f.version
487
+ ]));
488
+ return {
489
+ features: [
490
+ ...features
491
+ ].map(([name, version])=>({
492
+ name,
493
+ version
494
+ })),
495
+ changes: plan.changes.map((c)=>({
496
+ path: c.path,
497
+ action: c.action
498
+ })),
499
+ dependencies: plan.dependencies.map((d)=>({
500
+ ...d
501
+ })),
502
+ scripts: plan.scripts.map((s)=>({
503
+ file: s.file,
504
+ name: s.name,
505
+ command: s.command
506
+ })),
507
+ warnings: [
508
+ ...plan.warnings
509
+ ]
510
+ };
511
+ }
512
+ const isObj = (value)=>'object' == typeof value && null !== value && !Array.isArray(value);
513
+ class Check {
514
+ issues = [];
515
+ add(path, message) {
516
+ this.issues.push({
517
+ path,
518
+ message
519
+ });
520
+ }
521
+ string(obj, key, at) {
522
+ if ('string' != typeof obj[key]) this.add(`${at}${key}`, 'must be text');
523
+ }
524
+ boolean(obj, key, at) {
525
+ if ('boolean' != typeof obj[key]) this.add(`${at}${key}`, 'must be true or false');
526
+ }
527
+ list(obj, key, at, each) {
528
+ const value = obj[key];
529
+ if (!Array.isArray(value)) return void this.add(`${at}${key}`, 'must be a list');
530
+ value.forEach((item, i)=>{
531
+ each(item, `${at}${key}[${String(i)}]`);
532
+ });
533
+ }
534
+ texts(obj, key, at) {
535
+ this.list(obj, key, at, (item, path)=>{
536
+ if ('string' != typeof item) this.add(path, 'must be text');
537
+ });
538
+ }
539
+ objects(obj, key, at, check) {
540
+ this.list(obj, key, at, (item, path)=>{
541
+ if (isObj(item)) check(item, `${path}.`);
542
+ else this.add(path, 'must be an object');
543
+ });
544
+ }
545
+ }
546
+ function checkPlan(c, plan) {
547
+ if (!isObj(plan)) return void c.add('plan', 'must be an object');
548
+ const at = 'plan.';
549
+ c.objects(plan, 'features', at, (f, p)=>{
550
+ c.string(f, 'name', p);
551
+ c.string(f, 'version', p);
552
+ });
553
+ c.objects(plan, 'changes', at, (f, p)=>{
554
+ c.string(f, 'path', p);
555
+ if (![
556
+ 'create',
557
+ 'patch',
558
+ 'delete'
559
+ ].includes(String(f.action))) c.add(`${p}action`, 'must be create, patch or delete');
560
+ });
561
+ c.objects(plan, 'dependencies', at, (d, p)=>{
562
+ for (const key of [
563
+ 'file',
564
+ 'name',
565
+ 'range'
566
+ ])c.string(d, key, p);
567
+ c.boolean(d, 'dev', p);
568
+ });
569
+ c.objects(plan, "scripts", at, (s, p)=>{
570
+ for (const key of [
571
+ 'file',
572
+ 'name',
573
+ 'command'
574
+ ])c.string(s, key, p);
575
+ });
576
+ c.texts(plan, 'warnings', at);
577
+ }
578
+ function checkWrite(c, value) {
579
+ c.boolean(value, 'dryRun', '');
580
+ c.string(value, 'root', '');
581
+ checkPlan(c, value.plan);
582
+ c.texts(value, 'written', '');
583
+ c.boolean(value, 'installed', '');
584
+ if (void 0 !== value.diff && 'string' != typeof value.diff) c.add('diff', 'must be text');
585
+ if (true === value.dryRun && 'string' != typeof value.diff) c.add('diff', 'is required for a dry run');
586
+ if (true === value.dryRun && Array.isArray(value.written) && value.written.length > 0) c.add('written', 'must be empty for a dry run');
587
+ }
588
+ function checkReport(c, value) {
589
+ c.string(value, 'root', '');
590
+ const summary = value.summary;
591
+ if (isObj(summary)) {
592
+ for (const key of [
593
+ 'errors',
594
+ 'warnings',
595
+ 'skipped',
596
+ 'fixed'
597
+ ])if ('number' != typeof summary[key]) c.add(`summary.${key}`, 'must be a number');
598
+ } else c.add('summary', 'must be an object');
599
+ c.objects(value, 'checks', '', (check, p)=>{
600
+ for (const key of [
601
+ 'id',
602
+ 'title'
603
+ ])c.string(check, key, p);
604
+ if (![
605
+ 'pass',
606
+ 'warn',
607
+ 'fail',
608
+ 'skipped'
609
+ ].includes(String(check.status))) c.add(`${p}status`, 'must be pass, warn, fail or skipped');
610
+ if ('number' != typeof check.findings) c.add(`${p}findings`, 'must be a number');
611
+ });
612
+ c.objects(value, 'findings', '', (f, p)=>{
613
+ for (const key of [
614
+ 'code',
615
+ 'check',
616
+ 'message',
617
+ 'fix',
618
+ 'docs'
619
+ ])c.string(f, key, p);
620
+ if ('error' !== f.severity && 'warning' !== f.severity) c.add(`${p}severity`, 'must be error or warning');
621
+ c.boolean(f, 'fixable', p);
622
+ });
623
+ if (value.ok !== (isObj(summary) && 0 === summary.errors)) c.add('ok', 'must be true exactly when there are no errors');
624
+ }
625
+ function checkBudget(c, value) {
626
+ c.string(value, 'root', '');
627
+ const model = value.model;
628
+ if (isObj(model)) {
629
+ for (const key of [
630
+ 'lcpDeltaMs',
631
+ 'shellKb',
632
+ 'remoteKb'
633
+ ])if ('number' != typeof model[key]) c.add(`model.${key}`, 'must be a number');
634
+ } else c.add('model', 'must be an object');
635
+ c.objects(value, 'shells', '', (s, p)=>{
636
+ c.string(s, 'name', p);
637
+ for (const key of [
638
+ 'initialKb',
639
+ "scripts"
640
+ ])if ('number' != typeof s[key]) c.add(`${p}${key}`, 'must be a number');
641
+ });
642
+ c.objects(value, 'remotes', '', (r, p)=>{
643
+ c.string(r, 'name', p);
644
+ c.boolean(r, 'pass', p);
645
+ for (const key of [
646
+ 'entryKb',
647
+ 'budgetKb'
648
+ ])if ('number' != typeof r[key]) c.add(`${p}${key}`, 'must be a number');
649
+ c.objects(r, 'routes', p, (route, q)=>{
650
+ c.string(route, 'route', q);
651
+ if ('number' != typeof route.kb) c.add(`${q}kb`, 'must be a number');
652
+ });
653
+ });
654
+ c.objects(value, 'findings', '', (f, p)=>{
655
+ for (const key of [
656
+ 'code',
657
+ 'message',
658
+ 'fix',
659
+ 'docs'
660
+ ])c.string(f, key, p);
661
+ });
662
+ }
663
+ function validateResult(value) {
664
+ const c = new Check();
665
+ if (!isObj(value)) {
666
+ c.add('', 'must be an object');
667
+ return c.issues;
668
+ }
669
+ if (false === value.ok && void 0 === value.command) {
670
+ if (isObj(value.error)) for (const key of [
671
+ 'code',
672
+ 'message',
673
+ 'fix',
674
+ 'docs'
675
+ ])c.string(value.error, key, 'error.');
676
+ else c.add('error', 'must be an object');
677
+ if ('number' != typeof value.exitCode) c.add('exitCode', 'must be a number');
678
+ return c.issues;
679
+ }
680
+ if ('boolean' != typeof value.ok) c.add('ok', 'must be true or false');
681
+ switch(value.command){
682
+ case 'init':
683
+ checkWrite(c, value);
684
+ c.string(value, 'scope', '');
685
+ c.texts(value, 'next', '');
686
+ c.objects(value, 'apps', '', (app, p)=>{
687
+ c.string(app, 'name', p);
688
+ c.string(app, 'path', p);
689
+ if ('shell' !== app.kind && 'remote' !== app.kind) c.add(`${p}kind`, 'must be shell or remote');
690
+ if (null !== app.port && 'number' != typeof app.port) c.add(`${p}port`, 'must be a number or null');
691
+ });
692
+ break;
693
+ case 'add':
694
+ checkWrite(c, value);
695
+ if (![
696
+ 'remote',
697
+ 'shell',
698
+ 'feature',
699
+ 'env-key'
700
+ ].includes(String(value.kind))) c.add('kind', 'must be remote, shell, feature or env-key');
701
+ c.texts(value, 'added', '');
702
+ c.texts(value, 'skipped', '');
703
+ break;
704
+ case 'features':
705
+ if (null !== value.search && 'string' != typeof value.search) c.add('search', 'must be text or null');
706
+ c.objects(value, 'features', '', (f, p)=>{
707
+ for (const key of [
708
+ 'name',
709
+ 'version',
710
+ "description"
711
+ ])c.string(f, key, p);
712
+ for (const key of [
713
+ 'always',
714
+ 'default',
715
+ 'installed'
716
+ ])c.boolean(f, key, p);
717
+ });
718
+ break;
719
+ case 'doctor':
720
+ case 'validate':
721
+ case 'check-compat':
722
+ checkReport(c, value);
723
+ break;
724
+ case 'budget':
725
+ checkBudget(c, value);
726
+ break;
727
+ case 'dev':
728
+ c.string(value, 'root', '');
729
+ c.string(value, 'registry', '');
730
+ c.boolean(value, 'dryRun', '');
731
+ c.boolean(value, 'interrupted', '');
732
+ c.objects(value, 'apps', '', (app, p)=>{
733
+ c.string(app, 'name', p);
734
+ c.string(app, 'command', p);
735
+ if ('started' !== app.status && 'overridden' !== app.status) c.add(`${p}status`, 'must be started or overridden');
736
+ });
737
+ if (!isObj(value.overrides)) c.add('overrides', 'must be an object');
738
+ break;
739
+ default:
740
+ c.add('command', 'must be init, add, features, doctor, validate, check-compat, budget or dev');
741
+ }
742
+ return c.issues;
743
+ }
744
+ function exitOf(result) {
745
+ if ('findings' in result) return exitForFindings(result.findings);
746
+ if (result.ok && 'interrupted' in result && result.interrupted) return EXIT.cancelled;
747
+ return result.ok ? EXIT.ok : EXIT.unexpected;
748
+ }
749
+ const PLACEHOLDER_RANGE = 'latest';
750
+ const PREFIX = '@mfeflow/';
751
+ const CLI = 'mfeflow';
752
+ const isOwn = (name)=>name === CLI || name.startsWith(PREFIX);
753
+ function posix(file) {
754
+ return file.split(node_path.sep).join('/');
755
+ }
756
+ function realpathLoose(file) {
757
+ try {
758
+ return node_fs.realpathSync.native(file);
759
+ } catch {
760
+ const parent = node_path.dirname(file);
761
+ return parent === file ? file : node_path.join(realpathLoose(parent), node_path.basename(file));
762
+ }
763
+ }
764
+ function readName(dir) {
765
+ const file = node_path.join(dir, 'package.json');
766
+ if (!node_fs.existsSync(file)) return;
767
+ const pkg = JSON.parse(node_fs.readFileSync(file, 'utf8'));
768
+ if ('object' != typeof pkg || null === pkg || !('name' in pkg) || 'string' != typeof pkg.name) return;
769
+ const needsBuild = JSON.stringify('exports' in pkg ? pkg.exports : '').includes('./dist/');
770
+ return {
771
+ name: pkg.name,
772
+ built: !needsBuild || node_fs.existsSync(node_path.join(dir, 'dist'))
773
+ };
774
+ }
775
+ function scanCheckout(checkout) {
776
+ const packages = node_path.join(checkout, 'packages');
777
+ if (!node_fs.existsSync(packages)) throw new MfeflowError('E110', `${checkout} has no packages/ folder`);
778
+ const found = new Map();
779
+ for (const entry of node_fs.readdirSync(packages, {
780
+ withFileTypes: true
781
+ })){
782
+ if (!entry.isDirectory()) continue;
783
+ const dir = node_path.join(packages, entry.name);
784
+ const info = readName(dir);
785
+ if (info && isOwn(info.name)) found.set(info.name, {
786
+ dir,
787
+ built: info.built
788
+ });
789
+ }
790
+ return found;
791
+ }
792
+ function createRangeRewriter(version, local) {
793
+ const packages = void 0 === local ? void 0 : scanCheckout(node_path.resolve(local));
794
+ if (packages) {
795
+ const unbuilt = [
796
+ ...packages
797
+ ].filter(([, p])=>!p.built).map(([name])=>name);
798
+ if (unbuilt.length > 0) throw new MfeflowError('E110', `${unbuilt.join(', ')} ${unbuilt.length > 1 ? 'have' : 'has'} no dist/ yet`);
799
+ }
800
+ return (name, range)=>{
801
+ if (!isOwn(name) || range !== PLACEHOLDER_RANGE) return;
802
+ if (!packages) return `^${version}`;
803
+ const found = packages.get(name);
804
+ if (!found) throw new MfeflowError('E110', `${name} is not in ${local ?? ''}/packages`);
805
+ return `link:${posix(found.dir)}`;
806
+ };
807
+ }
808
+ const LOCAL_NONE = 'none';
809
+ function chooseLocal(project, root, cwd, flag) {
810
+ const { local: recorded, ...rest } = project;
811
+ const realRoot = realpathLoose(root);
812
+ if (flag === LOCAL_NONE) return {
813
+ checkout: void 0,
814
+ project: rest
815
+ };
816
+ if (void 0 === flag) return {
817
+ checkout: void 0 === recorded ? void 0 : node_path.resolve(realRoot, recorded),
818
+ project
819
+ };
820
+ const checkout = realpathLoose(node_path.resolve(cwd, flag));
821
+ const relative = node_path.relative(realRoot, checkout);
822
+ if ('' === relative) throw new MfeflowError('E107', `--local ${flag} is the project itself`);
823
+ const local = posix(node_path.isAbsolute(relative) ? checkout : relative);
824
+ return {
825
+ checkout,
826
+ project: {
827
+ ...rest,
828
+ local
829
+ }
830
+ };
831
+ }
832
+ function isPackageJson(value) {
833
+ return 'object' == typeof value && null !== value && 'name' in value && 'version' in value && 'string' == typeof value.name && 'string' == typeof value.version;
834
+ }
835
+ function readPackageJson() {
836
+ const require = createRequire(import.meta.url);
837
+ const pkg = require('../package.json');
838
+ if (!isPackageJson(pkg)) throw new Error('package.json is missing a name or version');
839
+ return pkg;
840
+ }
841
+ function projectReader(root) {
842
+ return async (file)=>{
843
+ try {
844
+ return await promises.readFile(node_path.join(root, ...file.split('/')), 'utf8');
845
+ } catch (error) {
846
+ if (hasErrno(error, 'ENOENT') || hasErrno(error, 'ENOTDIR')) return;
847
+ throw error;
848
+ }
849
+ };
850
+ }
851
+ function createBuilder(root, checkout) {
852
+ return new PlanBuilder(projectReader(root), {
853
+ catalog: true,
854
+ rewriteRange: createRangeRewriter(readPackageJson().version, checkout)
855
+ });
856
+ }
857
+ function withProjectChange(plan, project) {
858
+ const before = plan.projectBefore ?? null;
859
+ const after = serializeProject(recordPlan(project, plan));
860
+ if (before === after) return plan;
861
+ return {
862
+ ...plan,
863
+ changes: [
864
+ ...plan.changes,
865
+ {
866
+ path: 'mfeflow.json',
867
+ action: null === before ? 'create' : 'patch',
868
+ before,
869
+ after,
870
+ track: false,
871
+ trust: 'first-party'
872
+ }
873
+ ]
874
+ };
875
+ }
876
+ async function missingFolders(root) {
877
+ const missing = [];
878
+ let dir = root;
879
+ while(!await write_exists(dir)){
880
+ missing.push(dir);
881
+ const parent = node_path.dirname(dir);
882
+ if (parent === dir) break;
883
+ dir = parent;
884
+ }
885
+ return missing;
886
+ }
887
+ async function removeCreated(missing) {
888
+ const [root, ...parents] = missing;
889
+ if (void 0 === root) return;
890
+ try {
891
+ await promises.rm(root, {
892
+ recursive: true,
893
+ force: true
894
+ });
895
+ } catch (error) {
896
+ return messageOf(error);
897
+ }
898
+ for (const dir of parents)try {
899
+ await promises.rmdir(dir);
900
+ } catch {
901
+ break;
902
+ }
903
+ }
904
+ async function removeContents(root) {
905
+ try {
906
+ const entries = await promises.readdir(root);
907
+ await Promise.all(entries.map((entry)=>promises.rm(node_path.join(root, entry), {
908
+ recursive: true,
909
+ force: true
910
+ })));
911
+ } catch (error) {
912
+ return messageOf(error);
913
+ }
914
+ }
915
+ async function isEmptyFolder(dir) {
916
+ try {
917
+ return 0 === (await promises.readdir(dir)).length;
918
+ } catch {
919
+ return false;
920
+ }
921
+ }
922
+ function installFailed(error, root, stuck, emptied) {
923
+ const reason = messageOf(error.cause ?? error);
924
+ if (void 0 !== stuck) return new MfeflowError('E213', `${reason}; could not clean up ${root} (${stuck})`, {
925
+ cause: error.cause,
926
+ fix: `Delete what is inside ${root} by hand, check your network and package manager, then run the command again.`
927
+ });
928
+ return new MfeflowError('E213', `${reason}; ${emptied ? 'emptied the folder' : 'removed the new folder'} ${root}`, {
929
+ cause: error.cause,
930
+ fix: 'Nothing was left behind. Check your network and package manager, then run the command again.'
931
+ });
932
+ }
933
+ async function write_exists(dir) {
934
+ try {
935
+ await promises.access(dir);
936
+ return true;
937
+ } catch {
938
+ return false;
939
+ }
940
+ }
941
+ async function writePlan(request) {
942
+ const { rt, globals, root, plan, project } = request;
943
+ const shown = withProjectChange(plan, project);
944
+ if (!globals.json) {
945
+ rt.io.stdout(summarize(shown));
946
+ if (globals.dryRun || globals.verbose) rt.io.stdout(`\n${renderDiff(shown)}`);
947
+ }
948
+ if (globals.dryRun) {
949
+ if (await write_exists(root)) await applyPlan(plan, {
950
+ root,
951
+ project,
952
+ dryRun: true
953
+ });
954
+ return {
955
+ dryRun: true,
956
+ shown,
957
+ written: [],
958
+ installed: false,
959
+ diff: renderDiff(shown)
960
+ };
961
+ }
962
+ if (globals.canPrompt && !await rt.prompter.confirm({
963
+ message: 'Write these changes?'
964
+ })) throw new Cancelled();
965
+ const installing = globals.install && plan.dependencies.length > 0;
966
+ const missing = await missingFolders(root);
967
+ const wasEmpty = 0 === missing.length && await isEmptyFolder(root);
968
+ const gitDir = node_path.join(root, '.git');
969
+ let madeGit = false;
970
+ await promises.mkdir(root, {
971
+ recursive: true
972
+ });
973
+ let result;
974
+ try {
975
+ result = await applyPlan(plan, {
976
+ root,
977
+ project,
978
+ ...installing ? {
979
+ install: async ()=>{
980
+ madeGit = void 0 !== request.afterWrite && !await write_exists(gitDir);
981
+ await request.afterWrite?.();
982
+ await rt.system.install(root, project.packageManager, globals.verbose);
983
+ }
984
+ } : {}
985
+ });
986
+ } catch (error) {
987
+ if (madeGit) await promises.rm(gitDir, {
988
+ recursive: true,
989
+ force: true
990
+ }).catch(()=>void 0);
991
+ const stuck = wasEmpty ? await removeContents(root) : await removeCreated(missing);
992
+ const cleaned = missing.length > 0 || wasEmpty;
993
+ if (cleaned && isMfeflowError(error) && 'MFEFLOW_E213' === error.code) throw installFailed(error, root, stuck, wasEmpty);
994
+ throw error;
995
+ }
996
+ if (!installing) await request.afterWrite?.();
997
+ return {
998
+ dryRun: false,
999
+ shown,
1000
+ written: result.written,
1001
+ installed: installing
1002
+ };
1003
+ }
1004
+ const KEY = /^[A-Za-z_][A-Za-z0-9_]*$/;
1005
+ const GROUPS = [
1006
+ 'root',
1007
+ 'shell',
1008
+ 'remotes'
1009
+ ];
1010
+ function groupOf(value) {
1011
+ if (void 0 === value) return;
1012
+ const group = GROUPS.find((g)=>g === value);
1013
+ if (void 0 === group) throw new MfeflowError('E107', '--group must be root, shell or remotes');
1014
+ return group;
1015
+ }
1016
+ const exactApp = (name)=>`app:${name}`;
1017
+ function serverTargets(project, app, group) {
1018
+ if (void 0 !== app) {
1019
+ if (!(app in project.apps)) throw new MfeflowError('E107', `--app ${app} is not an app (${Object.keys(project.apps).join(', ')})`);
1020
+ return [
1021
+ exactApp(app)
1022
+ ];
1023
+ }
1024
+ const kinds = new Set(Object.values(project.apps).map((a)=>a.kind));
1025
+ if (void 0 === group) {
1026
+ const targets = [
1027
+ ...kinds.has('shell') ? [
1028
+ 'shell'
1029
+ ] : [],
1030
+ ...kinds.has('remote') ? [
1031
+ 'remotes'
1032
+ ] : []
1033
+ ];
1034
+ if (0 === targets.length) throw new MfeflowError('E107', 'the project has no app to add a key to');
1035
+ return targets;
1036
+ }
1037
+ if ('shell' === group && !kinds.has('shell')) throw new MfeflowError('E107', 'the project has no shell to add a key to');
1038
+ if ('remotes' === group && !kinds.has('remote')) throw new MfeflowError('E107', 'the project has no remotes to add a key to');
1039
+ return [
1040
+ group
1041
+ ];
1042
+ }
1043
+ function runtimeTargets(project, app, group) {
1044
+ const shells = Object.entries(project.apps).filter(([, a])=>'shell' === a.kind).map(([name, a])=>({
1045
+ name,
1046
+ path: a.path
1047
+ }));
1048
+ if (void 0 !== app) {
1049
+ const shell = shells.find((s)=>s.name === app);
1050
+ if (void 0 === shell) throw new MfeflowError('E107', `--app ${app} is not a shell; runtime config lives in a shell (${shells.map((s)=>s.name).join(', ')})`);
1051
+ return [
1052
+ {
1053
+ target: exactApp(app),
1054
+ path: shell.path
1055
+ }
1056
+ ];
1057
+ }
1058
+ if (void 0 !== group && 'shell' !== group) throw new MfeflowError('E107', `--group ${group} has no runtime config; it lives in a shell`);
1059
+ return shells.map((s)=>({
1060
+ target: exactApp(s.name),
1061
+ path: s.path
1062
+ }));
1063
+ }
1064
+ async function configFilesOf(root, dir) {
1065
+ const folder = node_path.join(root, ...dir.split('/'), 'public', 'config');
1066
+ return (await runtimeConfigFiles(folder)).map((name)=>`public/config/${name}`);
1067
+ }
1068
+ function envKeyOp(input) {
1069
+ const { target, key, scope, schema, value, configFiles } = input;
1070
+ const op = {
1071
+ op: 'add-env-key',
1072
+ file: 'server' === scope ? 'env.ts' : 'src/config/schema.ts',
1073
+ target,
1074
+ key,
1075
+ scope,
1076
+ schema
1077
+ };
1078
+ if (void 0 !== value) op.value = value;
1079
+ if ('runtime' === scope) op.configFiles = configFiles;
1080
+ return op;
1081
+ }
1082
+ async function addEnvKey(rt, globals, args, key) {
1083
+ const scope = args_text(args, 'scope');
1084
+ const schema = args_text(args, 'schema');
1085
+ if (void 0 === key || !KEY.test(key)) throw new MfeflowError('E107', 'add env-key needs a key like API_ORIGIN: mfeflow add env-key <KEY>');
1086
+ if ('server' !== scope && 'runtime' !== scope) throw new MfeflowError('E107', '--scope must be server or runtime');
1087
+ if (void 0 === schema) throw new MfeflowError('E107', '--schema is required, for example --schema "z.url()"');
1088
+ const root = await findProjectRoot(globals.cwd);
1089
+ const chosen = chooseLocal(await loadProject(root), root, globals.cwd, args_text(args, 'local'));
1090
+ const { project } = chosen;
1091
+ const installed = project.features['env-t3'];
1092
+ if (void 0 === installed) throw new MfeflowError('E206', 'add env-key needs env-t3; run `mfeflow add env-t3` first');
1093
+ const value = args_text(args, 'value');
1094
+ const app = args_text(args, 'app');
1095
+ const group = groupOf(args_text(args, 'group'));
1096
+ if (void 0 !== app && void 0 !== group) throw new MfeflowError('E107', '--app and --group cannot be used together');
1097
+ const targets = 'server' === scope ? serverTargets(project, app, group).map((target)=>({
1098
+ target,
1099
+ path: ''
1100
+ })) : runtimeTargets(project, app, group);
1101
+ const transforms = await Promise.all(targets.map(async ({ target, path: dir })=>envKeyOp({
1102
+ target,
1103
+ key,
1104
+ scope,
1105
+ schema,
1106
+ value,
1107
+ configFiles: 'runtime' === scope ? await configFilesOf(root, dir) : []
1108
+ })));
1109
+ const builder = createBuilder(root, chosen.checkout);
1110
+ await resolveFeature({
1111
+ feature: {
1112
+ manifest: {
1113
+ name: 'env-t3',
1114
+ version: installed.version,
1115
+ transforms
1116
+ },
1117
+ trust: 'first-party'
1118
+ },
1119
+ project,
1120
+ builder
1121
+ });
1122
+ const plan = {
1123
+ ...builder.build(),
1124
+ features: []
1125
+ };
1126
+ const outcome = await writePlan({
1127
+ rt,
1128
+ globals: {
1129
+ ...globals,
1130
+ install: false
1131
+ },
1132
+ root,
1133
+ plan,
1134
+ project
1135
+ });
1136
+ const result = {
1137
+ ok: true,
1138
+ command: 'add',
1139
+ kind: 'env-key',
1140
+ dryRun: outcome.dryRun,
1141
+ root,
1142
+ added: 0 === plan.changes.length ? [] : [
1143
+ key
1144
+ ],
1145
+ skipped: 0 === plan.changes.length ? [
1146
+ key
1147
+ ] : [],
1148
+ plan: planReport(outcome.shown),
1149
+ written: outcome.written,
1150
+ installed: false,
1151
+ ...void 0 === outcome.diff ? {} : {
1152
+ diff: outcome.diff
1153
+ }
1154
+ };
1155
+ if (!globals.json) if (outcome.dryRun) rt.io.stdout('\nDry run: nothing was written.');
1156
+ else rt.io.stdout(0 === plan.changes.length ? `${key} is already in the ${scope} schema; nothing to do.` : `Added ${key} to the ${scope} schema${'runtime' === scope ? ' and the runtime-config files' : ' and .env.example'}.`);
1157
+ return result;
1158
+ }
1159
+ const MAX_BYTES = 100000;
1160
+ const KEYS = new Set([
1161
+ '$schema',
1162
+ 'scope',
1163
+ 'remotes',
1164
+ 'features',
1165
+ 'packageManager',
1166
+ 'topology',
1167
+ 'options'
1168
+ ]);
1169
+ function bad(source, why) {
1170
+ throw new MfeflowError('E109', `${source}: ${why}`);
1171
+ }
1172
+ function isRecord(value) {
1173
+ return 'object' == typeof value && null !== value && !Array.isArray(value);
1174
+ }
1175
+ function stringList(source, key, value) {
1176
+ if (!Array.isArray(value) || !value.every((item)=>'string' == typeof item)) bad(source, `${key} must be a list of text`);
1177
+ return value;
1178
+ }
1179
+ function optionsOf(source, value) {
1180
+ if (!isRecord(value)) bad(source, 'options must be an object of feature names');
1181
+ const out = {};
1182
+ for (const [feature, given] of Object.entries(value)){
1183
+ if (!isRecord(given)) bad(source, `options.${feature} must be an object`);
1184
+ out[feature] = given;
1185
+ }
1186
+ return out;
1187
+ }
1188
+ function parsePreset(source, value) {
1189
+ if (!isRecord(value)) bad(source, 'must be a JSON object');
1190
+ const preset = {};
1191
+ for (const [key, item] of Object.entries(value)){
1192
+ if (!KEYS.has(key)) bad(source, `"${key}" is not a preset option`);
1193
+ if ('scope' === key || 'packageManager' === key || 'topology' === key) {
1194
+ if ('string' != typeof item) bad(source, `${key} must be text`);
1195
+ preset[key] = item;
1196
+ } else if ('remotes' === key || 'features' === key) preset[key] = stringList(source, key, item);
1197
+ else if ('options' === key) preset.options = optionsOf(source, item);
1198
+ }
1199
+ return preset;
1200
+ }
1201
+ async function readUrl(url, fetchImpl) {
1202
+ const response = await getHttps(url, fetchImpl, 'E109');
1203
+ if (!response.ok) bad(url, `answered ${String(response.status)}`);
1204
+ return readCappedText(response, MAX_BYTES, ()=>new MfeflowError('E109', `${url}: is larger than 100 KB`));
1205
+ }
1206
+ async function loadPreset(source, sources) {
1207
+ let text;
1208
+ if (/^https?:\/\//i.test(source)) {
1209
+ if (!source.toLowerCase().startsWith('https://')) bad(source, 'must be https');
1210
+ text = await readUrl(source, sources.fetch);
1211
+ } else {
1212
+ const file = node_path.resolve(sources.cwd, source);
1213
+ try {
1214
+ text = await (sources.readFile ?? ((f)=>promises.readFile(f, 'utf8')))(file);
1215
+ } catch (error) {
1216
+ throw new MfeflowError('E109', `${file}: ${messageOf(error)}`, {
1217
+ cause: error
1218
+ });
1219
+ }
1220
+ }
1221
+ let value;
1222
+ try {
1223
+ value = JSON.parse(text);
1224
+ } catch (error) {
1225
+ throw new MfeflowError('E109', `${source}: not valid JSON (${messageOf(error)})`, {
1226
+ cause: error
1227
+ });
1228
+ }
1229
+ return parsePreset(source, value);
1230
+ }
1231
+ const ADD_ARGS = {
1232
+ port: {
1233
+ type: 'string',
1234
+ description: 'Dev server port for a new app (default: the next free one)',
1235
+ valueHint: 'n'
1236
+ },
1237
+ option: {
1238
+ type: 'string',
1239
+ description: 'A feature option, repeatable: --option feature.key=value',
1240
+ valueHint: 'f.k=v'
1241
+ },
1242
+ scope: {
1243
+ type: 'string',
1244
+ description: 'add env-key: server (env.ts, .env.example) or runtime (runtime-config files)',
1245
+ valueHint: 'server|runtime'
1246
+ },
1247
+ schema: {
1248
+ type: 'string',
1249
+ description: 'add env-key: the validator expression, like "z.url()"',
1250
+ valueHint: 'expr'
1251
+ },
1252
+ value: {
1253
+ type: 'string',
1254
+ description: 'add env-key: the placeholder written to .env.example or the config files',
1255
+ valueHint: 'text'
1256
+ },
1257
+ app: {
1258
+ type: 'string',
1259
+ description: 'add env-key: exactly this app (default: every app; runtime: every shell)',
1260
+ valueHint: 'name'
1261
+ },
1262
+ group: {
1263
+ type: 'string',
1264
+ description: 'add env-key: root, every shell or every remote, instead of --app',
1265
+ valueHint: 'root|shell|remotes'
1266
+ },
1267
+ ...GLOBAL_ARGS
1268
+ };
1269
+ function portFor(args, project, start) {
1270
+ const given = args.port;
1271
+ if ('string' != typeof given || '' === given) return freePort(project, start);
1272
+ const port = Number(given);
1273
+ if (!Number.isInteger(port) || port < 1 || port > 65535) throw new MfeflowError('E107', `--port "${given}" must be a whole number from 1 to 65535`);
1274
+ const owner = Object.entries(project.apps).find(([, app])=>app.port === port);
1275
+ if (owner) throw new MfeflowError('E107', `--port ${given} is already used by ${owner[0]}`);
1276
+ return port;
1277
+ }
1278
+ async function nameFrom(rt, globals, given, what) {
1279
+ if (void 0 !== given) return given;
1280
+ if (globals.canPrompt) return rt.prompter.text({
1281
+ message: `Name of the new ${what}`
1282
+ });
1283
+ throw new MfeflowError('E107', `add ${what} needs a name: mfeflow add ${what} <name>`);
1284
+ }
1285
+ async function addAppParts(rt, globals, builder, project, next, kind, name) {
1286
+ const names = [
1287
+ 'core',
1288
+ ...Object.keys(project.features).filter((n)=>'core' !== n)
1289
+ ];
1290
+ const { canPrompt } = globals;
1291
+ const loadedAll = await Promise.all(names.map((feature)=>loadAny(feature, {
1292
+ roots: rt.roots,
1293
+ registries: project.registries,
1294
+ fetch: rt.fetch,
1295
+ version: project.features[feature]?.version
1296
+ })));
1297
+ let core;
1298
+ const fromRegistry = [];
1299
+ const planned = [];
1300
+ names.forEach((feature, index)=>{
1301
+ const loaded = loadedAll[index];
1302
+ if (void 0 === loaded) return;
1303
+ if ('core' === feature) core = loaded;
1304
+ const recorded = project.features[feature]?.version;
1305
+ const parts = partsForApp(loaded, kind, name);
1306
+ if (!parts) return;
1307
+ if ('third-party' === loaded.trust) {
1308
+ if (void 0 !== recorded && loaded.manifest.version !== recorded) throw new MfeflowError('E401', `${feature} is installed at ${recorded} but the registry now serves ${loaded.manifest.version}`, {
1309
+ fix: `Nothing was written. Use a registry URL with {version} so ${recorded} can be fetched, or add the app by hand.`
1310
+ });
1311
+ fromRegistry.push(feature);
1312
+ }
1313
+ planned.push({
1314
+ feature,
1315
+ parts
1316
+ });
1317
+ });
1318
+ assertThirdPartyAllowed(fromRegistry, globals, canPrompt);
1319
+ await inOrder(planned, ({ feature, parts })=>resolveFeature({
1320
+ feature: parts,
1321
+ project: next,
1322
+ builder,
1323
+ options: project.features[feature]?.options ?? {}
1324
+ }));
1325
+ if (void 0 === core) throw new MfeflowError('E201', 'core');
1326
+ return core;
1327
+ }
1328
+ async function addApp(rt, globals, args, kind, given) {
1329
+ const root = await findProjectRoot(globals.cwd);
1330
+ const chosen = chooseLocal(await loadProject(root), root, globals.cwd, args_text(args, 'local'));
1331
+ const { project } = chosen;
1332
+ const name = await nameFrom(rt, globals, given, kind);
1333
+ assertAppName(name, project.apps);
1334
+ const port = portFor(args, project, 'remote' === kind ? 4311 : 4300);
1335
+ const next = {
1336
+ ...project,
1337
+ apps: {
1338
+ ...project.apps,
1339
+ [name]: {
1340
+ kind,
1341
+ path: `apps/${name}`,
1342
+ port
1343
+ }
1344
+ }
1345
+ };
1346
+ const builder = createBuilder(root, chosen.checkout);
1347
+ const core = await addAppParts(rt, globals, builder, project, next, 'remote' === kind ? 'remotes' : 'shell', name);
1348
+ const version = core.manifest.version;
1349
+ if ('remote' === kind) await resolveFeature({
1350
+ feature: await remoteGlue({
1351
+ roots: rt.roots,
1352
+ version,
1353
+ remote: {
1354
+ name,
1355
+ port
1356
+ },
1357
+ registry: true,
1358
+ shells: [
1359
+ 'shell'
1360
+ ]
1361
+ }),
1362
+ project: next,
1363
+ builder
1364
+ });
1365
+ else await inOrder(Object.entries(project.apps), async ([remote, app])=>{
1366
+ if ('remote' !== app.kind || void 0 === app.port) return;
1367
+ await resolveFeature({
1368
+ feature: await remoteGlue({
1369
+ roots: rt.roots,
1370
+ version,
1371
+ remote: {
1372
+ name: remote,
1373
+ port: app.port
1374
+ },
1375
+ registry: false,
1376
+ shells: [
1377
+ name
1378
+ ]
1379
+ }),
1380
+ project: next,
1381
+ builder
1382
+ });
1383
+ });
1384
+ const plan = {
1385
+ ...builder.build(),
1386
+ features: []
1387
+ };
1388
+ const outcome = await writePlan({
1389
+ rt,
1390
+ globals,
1391
+ root,
1392
+ plan,
1393
+ project: next
1394
+ });
1395
+ return finish(rt, globals, root, kind, [
1396
+ name
1397
+ ], [], outcome);
1398
+ }
1399
+ function finish(rt, globals, root, kind, added, skipped, outcome) {
1400
+ const result = {
1401
+ ok: true,
1402
+ command: 'add',
1403
+ kind,
1404
+ dryRun: outcome.dryRun,
1405
+ root,
1406
+ added,
1407
+ skipped,
1408
+ plan: planReport(outcome.shown),
1409
+ written: outcome.written,
1410
+ installed: outcome.installed,
1411
+ ...void 0 === outcome.diff ? {} : {
1412
+ diff: outcome.diff
1413
+ }
1414
+ };
1415
+ if (!globals.json) if (outcome.dryRun) rt.io.stdout('\nDry run: nothing was written.');
1416
+ else rt.io.stdout(`\nAdded ${added.join(', ')}${'feature' === kind ? '' : ` (${kind})`}.`);
1417
+ return result;
1418
+ }
1419
+ async function askOptions(rt, feature, given, canPrompt) {
1420
+ const answers = {
1421
+ ...given
1422
+ };
1423
+ if (!canPrompt) return answers;
1424
+ await inOrder(feature.manifest.prompts ?? [], async (prompt)=>{
1425
+ if (prompt.name in answers) return;
1426
+ const message = prompt.message ?? prompt.name;
1427
+ const initial = 'string' == typeof prompt.default ? {
1428
+ initialValue: prompt.default
1429
+ } : {};
1430
+ if ('boolean' === prompt.type) answers[prompt.name] = await rt.prompter.confirm({
1431
+ message,
1432
+ initialValue: true === prompt.default
1433
+ });
1434
+ else if ('select' === prompt.type) answers[prompt.name] = await rt.prompter.select({
1435
+ message,
1436
+ choices: (prompt.choices ?? []).map((c)=>({
1437
+ value: c,
1438
+ label: c
1439
+ })),
1440
+ ...initial
1441
+ });
1442
+ else answers[prompt.name] = await rt.prompter.text({
1443
+ message,
1444
+ ...initial
1445
+ });
1446
+ });
1447
+ return answers;
1448
+ }
1449
+ function assertThirdPartyAllowed(names, globals, canPrompt) {
1450
+ if (names.length > 0 && !globals.yes && !canPrompt && !globals.dryRun) throw new MfeflowError('E107', `${names.join(', ')} comes from a registry; pass --yes to apply it without a terminal to confirm in`);
1451
+ }
1452
+ const rank = (name)=>OPTIONAL_FEATURES.indexOf(name);
1453
+ function inCatalogueOrder(names) {
1454
+ const known = names.filter((n)=>-1 !== rank(n)).toSorted((a, b)=>rank(a) - rank(b));
1455
+ return [
1456
+ ...known,
1457
+ ...names.filter((n)=>-1 === rank(n))
1458
+ ];
1459
+ }
1460
+ async function addFeatures(rt, globals, local, requested, options) {
1461
+ const root = await findProjectRoot(globals.cwd);
1462
+ const chosen = chooseLocal(await loadProject(root), root, globals.cwd, local);
1463
+ const { project } = chosen;
1464
+ const { canPrompt } = globals;
1465
+ let names = [
1466
+ ...new Set(requested)
1467
+ ];
1468
+ if (0 === names.length && canPrompt) {
1469
+ const choices = OPTIONAL_FEATURES.filter((n)=>!(n in project.features));
1470
+ names = await rt.prompter.multiselect({
1471
+ message: 'Add which features?',
1472
+ choices: choices.map((value)=>({
1473
+ value,
1474
+ label: value
1475
+ })),
1476
+ initialValues: []
1477
+ });
1478
+ }
1479
+ if (0 === names.length) throw new MfeflowError('E107', 'add needs a feature: mfeflow add <feature...>, or `mfeflow features` to list them');
1480
+ const skipped = names.filter((n)=>n in project.features);
1481
+ const adding = inCatalogueOrder(names.filter((n)=>!(n in project.features)));
1482
+ if (0 === adding.length) {
1483
+ if (!globals.json) rt.io.stdout(`${skipped.join(', ')} already added; nothing to do.`);
1484
+ return {
1485
+ ok: true,
1486
+ command: 'add',
1487
+ kind: 'feature',
1488
+ dryRun: globals.dryRun,
1489
+ root,
1490
+ added: [],
1491
+ skipped,
1492
+ plan: planReport({
1493
+ version: 1,
1494
+ features: [],
1495
+ changes: [],
1496
+ dependencies: [],
1497
+ scripts: [],
1498
+ env: [],
1499
+ warnings: []
1500
+ }),
1501
+ written: [],
1502
+ installed: false,
1503
+ ...globals.dryRun ? {
1504
+ diff: ''
1505
+ } : {}
1506
+ };
1507
+ }
1508
+ assertOptionsAreForFeatures(options, adding);
1509
+ const loaded = await Promise.all(adding.map((name)=>loadAny(name, {
1510
+ roots: rt.roots,
1511
+ registries: project.registries,
1512
+ fetch: rt.fetch
1513
+ })));
1514
+ const thirdParty = loaded.filter((f)=>'third-party' === f.trust).map((f)=>f.manifest.name);
1515
+ assertThirdPartyAllowed(thirdParty, globals, canPrompt);
1516
+ const builder = createBuilder(root, chosen.checkout);
1517
+ await inOrder(loaded, async (feature)=>{
1518
+ const given = await askOptions(rt, feature, options[feature.manifest.name] ?? {}, canPrompt);
1519
+ await resolveFeature({
1520
+ feature,
1521
+ project,
1522
+ builder,
1523
+ options: given,
1524
+ alsoAdding: adding
1525
+ });
1526
+ });
1527
+ const plan = builder.build();
1528
+ const outcome = await writePlan({
1529
+ rt,
1530
+ globals,
1531
+ root,
1532
+ plan,
1533
+ project,
1534
+ afterWrite: async ()=>{
1535
+ if (adding.includes('git-hooks')) await rt.system.gitInit(root);
1536
+ }
1537
+ });
1538
+ return finish(rt, globals, root, 'feature', adding, skipped, outcome);
1539
+ }
1540
+ async function runAdd(rt, args, raw) {
1541
+ assertKnownFlags(raw, ADD_ARGS);
1542
+ const globals = readGlobals(args, rt.io.env, rt.io.cwd, rt.io.interactive);
1543
+ const positional = Array.isArray(args._) ? args._.map(String) : [];
1544
+ const [first, ...rest] = positional;
1545
+ if ('remote' === first || 'shell' === first) return addApp(rt, globals, args, first, rest[0]);
1546
+ if ('env-key' === first) return addEnvKey(rt, globals, args, rest[0]);
1547
+ const preset = globals.preset ? await loadPreset(globals.preset, {
1548
+ cwd: globals.cwd,
1549
+ fetch: rt.fetch
1550
+ }) : {};
1551
+ const options = mergeOptions(preset.options ?? {}, parseFeatureOptions(repeated(raw, 'option')));
1552
+ return addFeatures(rt, globals, args_text(args, 'local'), positional, options);
1553
+ }
1554
+ function finding(check, code, detail, options = {}) {
1555
+ const error = new MfeflowError(code, detail);
1556
+ const out = {
1557
+ code: error.code,
1558
+ severity: options.severity ?? 'error',
1559
+ check,
1560
+ message: error.message,
1561
+ fix: error.fix,
1562
+ docs: error.docs,
1563
+ fixable: options.fixable ?? false
1564
+ };
1565
+ if (void 0 !== options.app) out.app = options.app;
1566
+ if (void 0 !== options.path) out.path = options.path;
1567
+ return out;
1568
+ }
1569
+ const done = (findings)=>Promise.resolve({
1570
+ findings
1571
+ });
1572
+ const SINGLETON_SPECIFIERS = [
1573
+ 'react',
1574
+ 'react/jsx-runtime',
1575
+ 'react/jsx-dev-runtime',
1576
+ 'react-dom',
1577
+ 'react-dom/client',
1578
+ 'react-router',
1579
+ '@tanstack/react-query',
1580
+ 'i18next',
1581
+ 'react-i18next',
1582
+ '@mfeflow/runtime',
1583
+ '@mfeflow/analytics',
1584
+ '@mfeflow/analytics/react',
1585
+ '@mfeflow/telemetry',
1586
+ '@mfeflow/storage',
1587
+ '@mfeflow/storage/react',
1588
+ '@mfeflow/theme',
1589
+ '@mfeflow/theme/react',
1590
+ '@mfeflow/i18n',
1591
+ '@mfeflow/i18n/react'
1592
+ ];
1593
+ function packageNameOf(specifier) {
1594
+ const parts = specifier.split('/');
1595
+ return specifier.startsWith('@') ? parts.slice(0, 2).join('/') : parts[0] ?? specifier;
1596
+ }
1597
+ const SINGLETON_PACKAGES = [
1598
+ ...new Set(SINGLETON_SPECIFIERS.map(packageNameOf))
1599
+ ];
1600
+ function specifiersOf(name) {
1601
+ return SINGLETON_SPECIFIERS.filter((s)=>packageNameOf(s) === name);
1602
+ }
1603
+ const ENVS = [
1604
+ 'dev',
1605
+ 'staging',
1606
+ 'prod'
1607
+ ];
1608
+ const inspect_isObj = (value)=>'object' == typeof value && null !== value && !Array.isArray(value);
1609
+ async function readText(file) {
1610
+ try {
1611
+ return await promises.readFile(file, 'utf8');
1612
+ } catch (error) {
1613
+ if (hasErrno(error, 'ENOENT') || hasErrno(error, 'ENOTDIR') || hasErrno(error, 'EISDIR')) return;
1614
+ throw error;
1615
+ }
1616
+ }
1617
+ async function readJson(file) {
1618
+ const text = await readText(file);
1619
+ if (void 0 === text) return {
1620
+ state: 'missing'
1621
+ };
1622
+ try {
1623
+ return {
1624
+ state: 'ok',
1625
+ value: JSON.parse(text)
1626
+ };
1627
+ } catch (error) {
1628
+ return {
1629
+ state: 'invalid',
1630
+ issues: [
1631
+ {
1632
+ path: '',
1633
+ message: `not valid JSON (${messageOf(error)})`
1634
+ }
1635
+ ]
1636
+ };
1637
+ }
1638
+ }
1639
+ function strings(value) {
1640
+ if (!inspect_isObj(value)) return {};
1641
+ return Object.fromEntries(Object.entries(value).filter((e)=>'string' == typeof e[1]));
1642
+ }
1643
+ async function readWorkspaceFile(root) {
1644
+ const text = await readText(node_path.join(root, 'pnpm-workspace.yaml'));
1645
+ const empty = {
1646
+ globs: [],
1647
+ catalogs: {
1648
+ default: {},
1649
+ named: {}
1650
+ }
1651
+ };
1652
+ if (void 0 === text) return empty;
1653
+ let doc;
1654
+ try {
1655
+ doc = parse(text);
1656
+ } catch {
1657
+ return empty;
1658
+ }
1659
+ if (!inspect_isObj(doc)) return empty;
1660
+ const globs = Array.isArray(doc.packages) ? doc.packages.filter((g)=>'string' == typeof g) : [];
1661
+ const named = inspect_isObj(doc.catalogs) ? Object.fromEntries(Object.entries(doc.catalogs).map(([k, v])=>[
1662
+ k,
1663
+ strings(v)
1664
+ ])) : {};
1665
+ return {
1666
+ globs,
1667
+ catalogs: {
1668
+ default: strings(doc.catalog),
1669
+ named
1670
+ }
1671
+ };
1672
+ }
1673
+ async function subfolders(dir) {
1674
+ try {
1675
+ return (await promises.readdir(dir, {
1676
+ withFileTypes: true
1677
+ })).filter((e)=>e.isDirectory() && 'node_modules' !== e.name).map((e)=>e.name).toSorted((a, b)=>a.localeCompare(b));
1678
+ } catch (error) {
1679
+ if (hasErrno(error, 'ENOENT') || hasErrno(error, 'ENOTDIR')) return [];
1680
+ throw error;
1681
+ }
1682
+ }
1683
+ function excluder(pattern) {
1684
+ const body = pattern.replace(/\/$/, '').split('/').map((part)=>'**' === part ? '\u0000' : part.replaceAll(/[.+?^${}()|[\]\\]/g, String.raw`\$&`).replaceAll('*', '[^/]*')).join('/').replaceAll('\u0000/', '(?:.*/)?').replaceAll('/\u0000', '(?:/.*)?').replaceAll('\u0000', '.*');
1685
+ return new RegExp(`^${body}$`);
1686
+ }
1687
+ async function expandGlobs(root, globs) {
1688
+ const out = [];
1689
+ const excluded = globs.filter((g)=>g.startsWith('!')).map((g)=>excluder(g.slice(1)));
1690
+ for (const glob of globs){
1691
+ if (glob.startsWith('!')) continue;
1692
+ const clean = glob.replace(/\/$/, '');
1693
+ if (clean.endsWith('/*') || clean.endsWith('/**')) {
1694
+ const base = clean.replace(/\/\*{1,2}$/, '');
1695
+ for (const name of (await subfolders(node_path.join(root, base))))out.push(`${base}/${name}`);
1696
+ } else out.push(clean);
1697
+ }
1698
+ return [
1699
+ ...new Set(out)
1700
+ ].filter((rel)=>!excluded.some((pattern)=>pattern.test(rel)));
1701
+ }
1702
+ function resolveSpec(name, spec, catalogs) {
1703
+ if ('catalog:' === spec || 'catalog:default' === spec) return catalogs.default[name] ?? spec;
1704
+ if (spec.startsWith('catalog:')) return catalogs.named[spec.slice(8)]?.[name] ?? spec;
1705
+ return spec;
1706
+ }
1707
+ async function readPackage(dir, catalogs) {
1708
+ const read = await readJson(node_path.join(dir, 'package.json'));
1709
+ if ('ok' !== read.state || !inspect_isObj(read.value)) return;
1710
+ const deps = {};
1711
+ for (const key of [
1712
+ 'peerDependencies',
1713
+ 'devDependencies',
1714
+ 'dependencies'
1715
+ ])for (const [name, spec] of Object.entries(strings(read.value[key])))deps[name] = resolveSpec(name, spec, catalogs);
1716
+ const view = {
1717
+ deps
1718
+ };
1719
+ if ('string' == typeof read.value.name) view.name = read.value.name;
1720
+ if ('string' == typeof read.value.version) view.version = read.value.version;
1721
+ return view;
1722
+ }
1723
+ async function readMfe(dir) {
1724
+ const read = await readJson(node_path.join(dir, 'mfe.json'));
1725
+ if ('ok' !== read.state) return read;
1726
+ const issues = validateMfeManifest(read.value);
1727
+ if (issues.length > 0) return {
1728
+ state: 'invalid',
1729
+ issues
1730
+ };
1731
+ return {
1732
+ state: 'ok',
1733
+ value: read.value
1734
+ };
1735
+ }
1736
+ async function readRegistry(root) {
1737
+ const envsDir = node_path.join(root, 'registry', 'envs');
1738
+ const envDirs = await subfolders(envsDir);
1739
+ const registry = {};
1740
+ for (const env of envDirs){
1741
+ const entries = {};
1742
+ const dir = node_path.join(envsDir, env);
1743
+ const files = (await promises.readdir(dir)).filter((f)=>f.endsWith('.json')).toSorted((a, b)=>a.localeCompare(b));
1744
+ for (const file of files){
1745
+ const read = await readJson(node_path.join(dir, file));
1746
+ const name = file.slice(0, -5);
1747
+ if ('ok' !== read.state) {
1748
+ entries[name] = read;
1749
+ continue;
1750
+ }
1751
+ const issues = validateRegistryEntry(read.value);
1752
+ if (issues.length > 0) {
1753
+ entries[name] = {
1754
+ state: 'invalid',
1755
+ issues
1756
+ };
1757
+ continue;
1758
+ }
1759
+ entries[name] = {
1760
+ state: 'ok',
1761
+ value: read.value
1762
+ };
1763
+ }
1764
+ registry[env] = entries;
1765
+ }
1766
+ return {
1767
+ registry,
1768
+ envDirs
1769
+ };
1770
+ }
1771
+ async function inspectProject(root) {
1772
+ const project = await loadProject(root);
1773
+ const { globs, catalogs } = await readWorkspaceFile(root);
1774
+ const apps = await Promise.all(Object.entries(project.apps).map(async ([name, app])=>{
1775
+ const dir = node_path.join(root, ...app.path.split('/'));
1776
+ const pkg = await readPackage(dir, catalogs);
1777
+ return {
1778
+ name,
1779
+ kind: app.kind,
1780
+ rel: app.path,
1781
+ dir,
1782
+ port: app.port,
1783
+ present: void 0 !== pkg,
1784
+ pkg,
1785
+ mfe: await readMfe(dir)
1786
+ };
1787
+ }));
1788
+ const folders = await expandGlobs(root, globs);
1789
+ const workspace = (await Promise.all(folders.map(async (rel)=>{
1790
+ const dir = node_path.join(root, ...rel.split('/'));
1791
+ const pkg = await readPackage(dir, catalogs);
1792
+ return pkg && {
1793
+ dir,
1794
+ rel,
1795
+ pkg,
1796
+ hasVersion: void 0 !== pkg.version
1797
+ };
1798
+ }))).filter((p)=>void 0 !== p);
1799
+ return {
1800
+ root,
1801
+ project,
1802
+ apps,
1803
+ workspace,
1804
+ ...await readRegistry(root)
1805
+ };
1806
+ }
1807
+ async function versionIn(app, name, root) {
1808
+ const declared = app.pkg?.deps[name];
1809
+ if (void 0 === declared && !SINGLETON_PACKAGES.includes(name)) return;
1810
+ let dir = app.dir;
1811
+ while(!node_path.relative(root, dir).startsWith('..')){
1812
+ const text = await readText(node_path.join(dir, 'node_modules', ...name.split('/'), 'package.json'));
1813
+ if (void 0 !== text) try {
1814
+ const parsed = JSON.parse(text);
1815
+ if (inspect_isObj(parsed) && 'string' == typeof parsed.version) return parsed.version;
1816
+ } catch {
1817
+ return;
1818
+ }
1819
+ const parent = node_path.dirname(dir);
1820
+ if (parent === dir || dir === root) break;
1821
+ dir = parent;
1822
+ }
1823
+ return declared;
1824
+ }
1825
+ function comparable(version) {
1826
+ const exact = semver.valid(version);
1827
+ if (null !== exact) return exact;
1828
+ return null === semver.validRange(version) ? void 0 : semver.minVersion(version)?.version ?? void 0;
1829
+ }
1830
+ function unresolved(check, code, name, specs) {
1831
+ return finding(check, code, `${name} is ${specs.join(', ')}: the version cannot be compared until install, so it is not checked`, {
1832
+ severity: 'warning'
1833
+ });
1834
+ }
1835
+ const singletonVersions = {
1836
+ id: 'singletons.versions',
1837
+ title: 'each singleton (react, react-dom, …) is one version across the apps',
1838
+ async run ({ apps, root }) {
1839
+ const present = apps.filter((app)=>app.present);
1840
+ const out = [];
1841
+ for (const name of SINGLETON_PACKAGES){
1842
+ const found = await Promise.all(present.map(async (app)=>({
1843
+ app: app.name,
1844
+ version: await versionIn(app, name, root)
1845
+ })));
1846
+ const used = found.flatMap((f)=>{
1847
+ const version = void 0 === f.version ? void 0 : comparable(f.version);
1848
+ return void 0 === version ? [] : [
1849
+ {
1850
+ app: f.app,
1851
+ version
1852
+ }
1853
+ ];
1854
+ });
1855
+ const open = found.filter((f)=>void 0 !== f.version && void 0 === comparable(f.version));
1856
+ if (open.length > 0) out.push(unresolved('singletons.versions', 'E301', name, open.map((f)=>`${f.app} ${String(f.version)}`)));
1857
+ const versions = new Set(used.map((u)=>u.version));
1858
+ if (versions.size > 1) {
1859
+ const list = used.map((u)=>`${u.app} ${u.version}`).join(', ');
1860
+ out.push(finding('singletons.versions', 'E301', `${name} (${specifiersOf(name).join(', ')}) is ${[
1861
+ ...versions
1862
+ ].join(' and ')}: ${list}`));
1863
+ }
1864
+ }
1865
+ return {
1866
+ findings: out
1867
+ };
1868
+ }
1869
+ };
1870
+ function fits(range, shellVersion) {
1871
+ if (void 0 === shellVersion) return true;
1872
+ const have = comparable(shellVersion);
1873
+ const valid = semver.validRange(range);
1874
+ return void 0 === have || null === valid || semver.satisfies(have, valid);
1875
+ }
1876
+ async function compatFor(shell, remote, root) {
1877
+ const out = [];
1878
+ const at = {
1879
+ app: remote.name,
1880
+ path: `${remote.rel}/mfe.json`
1881
+ };
1882
+ const platform = 'ok' === remote.mfe.state ? remote.mfe.value.platform ?? {} : {};
1883
+ for (const [name, range] of Object.entries(platform)){
1884
+ const have = await versionIn(shell, name, root);
1885
+ if (void 0 !== have && void 0 === comparable(have)) out.push(unresolved('compat', 'E302', name, [
1886
+ `${shell.name} ${have}`
1887
+ ]));
1888
+ else if (!fits(range, have)) out.push(finding('compat', 'E302', `${remote.name} needs ${name} ${range} (mfe.json platform) but ${shell.name} has ${String(have)}`, at));
1889
+ }
1890
+ for (const name of SINGLETON_PACKAGES){
1891
+ const [theirs, mine] = await Promise.all([
1892
+ versionIn(remote, name, root),
1893
+ versionIn(shell, name, root)
1894
+ ]);
1895
+ const want = void 0 === theirs ? void 0 : comparable(theirs);
1896
+ const open = [
1897
+ ...void 0 !== theirs && void 0 === want ? [
1898
+ `${remote.name} ${theirs}`
1899
+ ] : [],
1900
+ ...void 0 !== mine && void 0 === comparable(mine) ? [
1901
+ `${shell.name} ${mine}`
1902
+ ] : []
1903
+ ];
1904
+ if (open.length > 0) out.push(unresolved('compat', 'E302', name, open));
1905
+ if (void 0 !== want && void 0 !== mine) {
1906
+ if (!fits(`^${want}`, mine) && !(name in platform)) out.push(finding('compat', 'E302', `${remote.name} uses ${name} ${want}, ${shell.name} provides ${mine}`, {
1907
+ app: remote.name
1908
+ }));
1909
+ }
1910
+ }
1911
+ return out;
1912
+ }
1913
+ const compat = {
1914
+ id: 'compat',
1915
+ title: "each remote's shared ranges fit the shells it runs in",
1916
+ async run (view) {
1917
+ const out = [];
1918
+ const remotes = view.apps.filter((a)=>'remote' === a.kind && a.present);
1919
+ for (const remote of remotes){
1920
+ const names = 'ok' === remote.mfe.state ? remote.mfe.value.shells : view.apps.filter((a)=>'shell' === a.kind).map((a)=>a.name);
1921
+ for (const shell of view.apps.filter((a)=>'shell' === a.kind && a.present && names.includes(a.name)))out.push(...await compatFor(shell, remote, view.root));
1922
+ }
1923
+ const seen = new Set();
1924
+ return {
1925
+ findings: out.filter((f)=>!seen.has(f.message) && Boolean(seen.add(f.message)))
1926
+ };
1927
+ }
1928
+ };
1929
+ function issueText(read) {
1930
+ return 'invalid' === read.state ? read.issues.map((i)=>`${i.path || '(root)'} ${i.message}`).join('; ') : '';
1931
+ }
1932
+ const appsOnDisk = {
1933
+ id: 'config.apps',
1934
+ title: 'every app in mfeflow.json is on disk',
1935
+ run: ({ apps })=>done(apps.filter((app)=>!app.present).map((app)=>finding('config.apps', 'E112', `${app.name} (${app.rel}/package.json not found)`, {
1936
+ app: app.name,
1937
+ path: `${app.rel}/package.json`
1938
+ })))
1939
+ };
1940
+ const mfeJson = {
1941
+ id: 'config.mfe-json',
1942
+ title: 'every app has a valid mfe.json that matches mfeflow.json',
1943
+ run: ({ apps })=>done(apps.filter((app)=>app.present).flatMap((app)=>{
1944
+ const at = {
1945
+ app: app.name,
1946
+ path: `${app.rel}/mfe.json`
1947
+ };
1948
+ if ('missing' === app.mfe.state) return [
1949
+ finding('config.mfe-json', 'E113', `${app.name} has no mfe.json`, at)
1950
+ ];
1951
+ if ('invalid' === app.mfe.state) return [
1952
+ finding('config.mfe-json', 'E113', `${app.name}: ${issueText(app.mfe)}`, at)
1953
+ ];
1954
+ const { name } = app.mfe.value;
1955
+ return name === app.name ? [] : [
1956
+ finding('config.mfe-json', 'E116', `mfe.json names "${name}" but the app is "${app.name}"`, at)
1957
+ ];
1958
+ }))
1959
+ };
1960
+ const config_ports = {
1961
+ id: 'config.ports',
1962
+ title: 'no two apps share a port',
1963
+ run: ({ apps })=>{
1964
+ const byPort = new Map();
1965
+ for (const app of apps)if (void 0 !== app.port) byPort.set(app.port, [
1966
+ ...byPort.get(app.port) ?? [],
1967
+ app.name
1968
+ ]);
1969
+ return done([
1970
+ ...byPort
1971
+ ].filter(([, names])=>names.length > 1).map(([port, names])=>finding('config.ports', 'E115', `${names.join(' and ')} both use ${String(port)}`)));
1972
+ }
1973
+ };
1974
+ const config_routes = {
1975
+ id: 'config.routes',
1976
+ title: 'no two remotes claim the same route',
1977
+ run: ({ apps })=>{
1978
+ const claimed = new Map();
1979
+ const out = [];
1980
+ for (const app of apps)if ('remote' === app.kind && 'ok' === app.mfe.state) for (const route of app.mfe.value.routes){
1981
+ const owner = claimed.get(route);
1982
+ if (void 0 !== owner && owner !== app.name) out.push(finding('config.routes', 'E116', `${route} is claimed by ${owner} and ${app.name}`, {
1983
+ app: app.name,
1984
+ path: `${app.rel}/mfe.json`
1985
+ }));
1986
+ else claimed.set(route, app.name);
1987
+ }
1988
+ return done(out);
1989
+ }
1990
+ };
1991
+ const config_registry = {
1992
+ id: 'config.registry',
1993
+ title: 'every remote has a valid registry entry in dev, staging and prod',
1994
+ run: ({ apps, registry: entries })=>{
1995
+ const out = [];
1996
+ const remotes = apps.filter((app)=>'remote' === app.kind);
1997
+ for (const env of ENVS){
1998
+ for (const app of remotes){
1999
+ const entry = entries[env]?.[app.name] ?? {
2000
+ state: 'missing'
2001
+ };
2002
+ const file = `registry/envs/${env}/${app.name}.json`;
2003
+ if ('missing' === entry.state) out.push(finding('config.registry', 'E114', `${file} is missing`, {
2004
+ app: app.name,
2005
+ path: file,
2006
+ fixable: true
2007
+ }));
2008
+ else if ('invalid' === entry.state) out.push(finding('config.registry', 'E114', `${file}: ${issueText(entry)}`, {
2009
+ app: app.name,
2010
+ path: file
2011
+ }));
2012
+ }
2013
+ const known = new Set(remotes.map((r)=>r.name));
2014
+ for (const name of Object.keys(entries[env] ?? {}))if (!known.has(name)) out.push(finding('config.registry', 'E114', `registry/envs/${env}/${name}.json has no remote in mfeflow.json`, {
2015
+ severity: 'warning',
2016
+ path: `registry/envs/${env}/${name}.json`
2017
+ }));
2018
+ }
2019
+ return done(out);
2020
+ }
2021
+ };
2022
+ const localCheckout = {
2023
+ id: 'config.local',
2024
+ title: 'the linked mfeflow checkout is built',
2025
+ run: ({ project, root })=>{
2026
+ if (void 0 === project.local) return Promise.resolve({
2027
+ findings: [],
2028
+ skipped: 'not linked with --local'
2029
+ });
2030
+ try {
2031
+ const unbuilt = [
2032
+ ...scanCheckout(node_path.resolve(root, project.local))
2033
+ ].filter(([, p])=>!p.built).map(([name])=>name);
2034
+ return done(0 === unbuilt.length ? [] : [
2035
+ finding('config.local', 'E110', `${unbuilt.join(', ')} not built in ${project.local}`)
2036
+ ]);
2037
+ } catch (error) {
2038
+ return done([
2039
+ finding('config.local', 'E110', error instanceof Error ? error.message : String(error))
2040
+ ]);
2041
+ }
2042
+ }
2043
+ };
2044
+ const drift = {
2045
+ id: 'files.drift',
2046
+ title: 'copied files match the hashes in mfeflow.json',
2047
+ run: async ({ root, project })=>({
2048
+ findings: (await findDiverged(root, project)).map((file)=>finding('files.drift', 'E117', `${file.path} is ${file.state}`, {
2049
+ severity: 'warning',
2050
+ path: file.path
2051
+ }))
2052
+ })
2053
+ };
2054
+ const NOT_INSTALLED = 'the env-t3 feature is not installed';
2055
+ const env_installed = ({ project })=>'env-t3' in project.features;
2056
+ const PUBLIC_CONSTANTS = new Set([
2057
+ 'PUBLIC_APP_NAME',
2058
+ 'PUBLIC_BUILD_VERSION'
2059
+ ]);
2060
+ const PUBLIC_VAR = /\bPUBLIC_[A-Z0-9_]+\b/g;
2061
+ const SOURCE_FILE = /\.(?:[cm]?[jt]sx?)$/;
2062
+ const placeOf = (a)=>{
2063
+ const place = {
2064
+ name: a.name,
2065
+ dir: a.dir,
2066
+ rel: a.rel
2067
+ };
2068
+ const pkg = a.pkg?.name;
2069
+ if (void 0 !== pkg) place.pkg = pkg;
2070
+ return place;
2071
+ };
2072
+ const places = (view)=>[
2073
+ {
2074
+ name: 'root',
2075
+ dir: view.root,
2076
+ rel: ''
2077
+ },
2078
+ ...view.apps.filter((a)=>a.present).map(placeOf)
2079
+ ];
2080
+ const warning = (app, file)=>({
2081
+ app,
2082
+ path: file,
2083
+ severity: 'warning'
2084
+ });
2085
+ const env_at = (place, file)=>'' === place.rel ? file : `${place.rel}/${file}`;
2086
+ async function eachPlace(view, check) {
2087
+ if (!env_installed(view)) return {
2088
+ findings: [],
2089
+ skipped: NOT_INSTALLED
2090
+ };
2091
+ return done((await Promise.all(places(view).map(check))).flat());
2092
+ }
2093
+ async function serverKeysOf(place) {
2094
+ const source = await readText(node_path.join(place.dir, 'env.ts'));
2095
+ if (void 0 === source) return;
2096
+ try {
2097
+ const keys = schemaKeys(source, 'env.ts', 'server');
2098
+ if (void 0 === keys) return {
2099
+ keys: [],
2100
+ problem: 'it has no createEnv({ server: { … } })'
2101
+ };
2102
+ return {
2103
+ keys
2104
+ };
2105
+ } catch (error) {
2106
+ return {
2107
+ keys: [],
2108
+ problem: messageOf(error)
2109
+ };
2110
+ }
2111
+ }
2112
+ const envExample = {
2113
+ id: 'env.example',
2114
+ title: '.env.example lists the required keys of env.ts and no others (decision 0020)',
2115
+ run: (view)=>eachPlace(view, async (place)=>{
2116
+ const server = await serverKeysOf(place);
2117
+ if (void 0 === server) return [];
2118
+ if (void 0 !== server.problem) {
2119
+ const env = env_at(place, 'env.ts');
2120
+ return [
2121
+ finding('env.example', 'E118', `${env} cannot be checked: ${server.problem}`, {
2122
+ app: place.name,
2123
+ path: env,
2124
+ severity: 'warning'
2125
+ })
2126
+ ];
2127
+ }
2128
+ const names = new Set(server.keys.map((k)=>k.name));
2129
+ const required = server.keys.filter((k)=>!k.optional).map((k)=>k.name);
2130
+ const file = env_at(place, '.env.example');
2131
+ const example = await readText(node_path.join(place.dir, '.env.example'));
2132
+ const options = {
2133
+ app: place.name,
2134
+ path: file,
2135
+ severity: 'warning'
2136
+ };
2137
+ if (void 0 === example) return 0 === required.length ? [] : [
2138
+ finding('env.example', 'E118', `${file} is missing`, options)
2139
+ ];
2140
+ const listed = dotenvKeys(example);
2141
+ return [
2142
+ ...required.filter((key)=>!listed.includes(key)).map((key)=>finding('env.example', 'E118', `${key} is in ${env_at(place, 'env.ts')} but not in ${file}`, options)),
2143
+ ...listed.filter((key)=>!names.has(key)).map((key)=>finding('env.example', 'E118', `${key} is in ${file} but not in ${env_at(place, 'env.ts')}`, options))
2144
+ ];
2145
+ })
2146
+ };
2147
+ async function checkConfigFile(place, folder, name, keys) {
2148
+ const file = env_at(place, `public/config/${name}`);
2149
+ const options = {
2150
+ app: place.name,
2151
+ path: file
2152
+ };
2153
+ const extra = warning(place.name, file);
2154
+ let json;
2155
+ try {
2156
+ json = JSON.parse(await readText(node_path.join(folder, name)) ?? '');
2157
+ } catch (error) {
2158
+ return [
2159
+ finding('env.runtime-config', 'E119', `${file} is not valid JSON (${messageOf(error)})`, options)
2160
+ ];
2161
+ }
2162
+ if ('object' != typeof json || null === json || Array.isArray(json)) return [
2163
+ finding('env.runtime-config', 'E119', `${file} is not a JSON object`, options)
2164
+ ];
2165
+ const present = Object.keys(json);
2166
+ return [
2167
+ ...keys.filter((k)=>!k.optional && !present.includes(k.name)).map((k)=>finding('env.runtime-config', 'E119', `${file} is missing ${k.name}`, options)),
2168
+ ...present.filter((key)=>!keys.some((k)=>k.name === key)).map((key)=>finding('env.runtime-config', 'E119', `${file} has ${key}, which the schema does not`, extra))
2169
+ ];
2170
+ }
2171
+ const runtimeConfig = {
2172
+ id: 'env.runtime-config',
2173
+ title: 'every runtime-config.<env>.json has the keys of the runtime-config schema (decision 0020)',
2174
+ run: (view)=>eachPlace(view, async (place)=>{
2175
+ const source = await readText(node_path.join(place.dir, 'src', 'config', 'schema.ts'));
2176
+ if (void 0 === source) return [];
2177
+ const schemaFile = env_at(place, 'src/config/schema.ts');
2178
+ let keys;
2179
+ try {
2180
+ keys = schemaKeys(source, schemaFile, 'runtime');
2181
+ } catch (error) {
2182
+ return [
2183
+ finding('env.runtime-config', 'E119', `${schemaFile}: ${messageOf(error)}`, {
2184
+ app: place.name,
2185
+ path: schemaFile
2186
+ })
2187
+ ];
2188
+ }
2189
+ if (void 0 === keys) return [
2190
+ finding('env.runtime-config', 'E119', `${schemaFile} has no runtimeConfigSchema object`, {
2191
+ app: place.name,
2192
+ path: schemaFile
2193
+ })
2194
+ ];
2195
+ const folder = node_path.join(place.dir, 'public', 'config');
2196
+ const files = await runtimeConfigFiles(folder);
2197
+ const found = await Promise.all(files.map((name)=>checkConfigFile(place, folder, name, keys)));
2198
+ return found.flat();
2199
+ })
2200
+ };
2201
+ async function sourceFiles(dir) {
2202
+ const entries = await promises.readdir(dir, {
2203
+ withFileTypes: true
2204
+ }).catch(()=>[]);
2205
+ const nested = await Promise.all(entries.map(async (entry)=>{
2206
+ const full = node_path.join(dir, entry.name);
2207
+ if (entry.isDirectory()) return 'node_modules' === entry.name ? [] : sourceFiles(full);
2208
+ return SOURCE_FILE.test(entry.name) ? [
2209
+ full
2210
+ ] : [];
2211
+ }));
2212
+ return nested.flat();
2213
+ }
2214
+ const publicPrefix = {
2215
+ id: 'env.public-prefix',
2216
+ title: 'no PUBLIC_ variable is baked into a bundle except constants (decision 0020)',
2217
+ run: (view)=>eachPlace(view, async (place)=>{
2218
+ const files = [
2219
+ node_path.join(place.dir, 'env.ts'),
2220
+ node_path.join(place.dir, '.env.example'),
2221
+ ...await sourceFiles(node_path.join(place.dir, 'src'))
2222
+ ];
2223
+ const found = await Promise.all(files.map(async (file)=>{
2224
+ const source = await readText(file);
2225
+ if (void 0 === source) return [];
2226
+ const names = [
2227
+ ...new Set(source.match(PUBLIC_VAR) ?? [])
2228
+ ].filter((name)=>!PUBLIC_CONSTANTS.has(name));
2229
+ const shown = node_path.relative(view.root, file).split(node_path.sep).join('/');
2230
+ return names.map((name)=>finding('env.public-prefix', 'E120', `${name} in ${shown}`, {
2231
+ app: place.name,
2232
+ path: shown,
2233
+ severity: 'warning'
2234
+ }));
2235
+ }));
2236
+ return found.flat();
2237
+ })
2238
+ };
2239
+ const turboEnv = {
2240
+ id: 'env.turbo',
2241
+ title: 'every required server env key is listed in turbo.json (decision 0020)',
2242
+ run: async (view)=>{
2243
+ if (!env_installed(view)) return {
2244
+ findings: [],
2245
+ skipped: NOT_INSTALLED
2246
+ };
2247
+ const source = await readText(node_path.join(view.root, 'turbo.json'));
2248
+ if (void 0 === source) return {
2249
+ findings: [],
2250
+ skipped: 'there is no turbo.json'
2251
+ };
2252
+ let turbo;
2253
+ try {
2254
+ turbo = JSON.parse(source);
2255
+ } catch {
2256
+ return {
2257
+ findings: [],
2258
+ skipped: 'turbo.json is not valid JSON'
2259
+ };
2260
+ }
2261
+ return eachPlace(view, async (place)=>{
2262
+ const server = await serverKeysOf(place);
2263
+ const listed = turboBuildEnv(turbo, place);
2264
+ return (server?.keys ?? []).filter((key)=>!key.optional && !envListed(listed, key.name)).map((key)=>finding('env.turbo', 'E121', `${key.name} from ${env_at(place, 'env.ts')}`, {
2265
+ app: place.name,
2266
+ path: 'turbo.json',
2267
+ severity: 'warning'
2268
+ }));
2269
+ });
2270
+ }
2271
+ };
2272
+ const env_isRecord = (value)=>'object' == typeof value && null !== value && !Array.isArray(value);
2273
+ const env_strings = (value)=>Array.isArray(value) ? value.filter((v)=>'string' == typeof v) : [];
2274
+ function turboBuildEnv(turbo, place) {
2275
+ if (!env_isRecord(turbo)) return [];
2276
+ const tasks = env_isRecord(turbo.tasks) ? turbo.tasks : {};
2277
+ const own = void 0 === place.pkg ? void 0 : tasks[`${place.pkg}#build`];
2278
+ return [
2279
+ ...env_strings(turbo.globalEnv),
2280
+ ...env_isRecord(tasks.build) ? env_strings(tasks.build.env) : [],
2281
+ ...env_isRecord(own) ? env_strings(own.env) : []
2282
+ ];
2283
+ }
2284
+ function matchesEnv(pattern, name) {
2285
+ const parts = pattern.split('*').map((part)=>part.replaceAll(/[$()+.?[\\\]^{|}]/g, String.raw`\$&`));
2286
+ return new RegExp(`^${parts.join('.*')}$`).test(name);
2287
+ }
2288
+ function envListed(patterns, name) {
2289
+ const included = patterns.some((p)=>!p.startsWith('!') && matchesEnv(p, name));
2290
+ return included && !patterns.some((p)=>p.startsWith('!') && matchesEnv(p.slice(1), name));
2291
+ }
2292
+ const envChecks = [
2293
+ envExample,
2294
+ runtimeConfig,
2295
+ publicPrefix,
2296
+ turboEnv
2297
+ ];
2298
+ const SOURCE = /\.(?:[cm]?[jt]s|[jt]sx)$/;
2299
+ const EXTENSIONS = [
2300
+ '',
2301
+ '.ts',
2302
+ '.tsx',
2303
+ '.js',
2304
+ '.mjs',
2305
+ '/index.ts',
2306
+ '/index.js'
2307
+ ];
2308
+ function withoutComments(code) {
2309
+ return code.replaceAll(/\/\*[\s\S]*?\*\//g, '').replaceAll(/(^|[^:])\/\/.*$/gm, '$1');
2310
+ }
2311
+ const FROM_CLAUSE = /\b(?:import|export)\s+(type\s+(?!from\b))?([\w\s{},*$]*?)\bfrom\s*['"]([^'"]+)['"]/g;
2312
+ function onlyTypes(clause) {
2313
+ const named = /^\s*\{([^}]*)\}\s*$/.exec(clause)?.[1];
2314
+ if (void 0 === named) return false;
2315
+ const specifiers = named.split(',').filter((part)=>'' !== part.trim());
2316
+ return specifiers.length > 0 && specifiers.every((part)=>/^\s*type\s+\S/.test(part));
2317
+ }
2318
+ function importsOf(code) {
2319
+ const clean = withoutComments(code);
2320
+ const found = new Set();
2321
+ for (const m of clean.matchAll(FROM_CLAUSE))if (void 0 === m[1] && !onlyTypes(m[2] ?? '')) found.add(m[3] ?? '');
2322
+ const patterns = [
2323
+ /\bimport\s*['"]([^'"]+)['"]/g,
2324
+ /\bimport\s*\(\s*['"]([^'"]+)['"]\s*\)/g,
2325
+ /\brequire\s*\(\s*['"]([^'"]+)['"]\s*\)/g
2326
+ ];
2327
+ for (const pattern of patterns)for (const m of clean.matchAll(pattern))found.add(m[1] ?? '');
2328
+ return [
2329
+ ...found
2330
+ ];
2331
+ }
2332
+ async function pluginFiles(app) {
2333
+ const found = new Set();
2334
+ const config = await readText(node_path.join(app.dir, 'rsbuild.config.ts'));
2335
+ const listed = void 0 === config ? null : /runtimePlugins\s*:\s*\[([^\]]*)\]/.exec(withoutComments(config));
2336
+ for (const m of (listed?.[1] ?? '').matchAll(/['"]([^'"]+)['"]/g)){
2337
+ const spec = m[1] ?? '';
2338
+ if (spec.startsWith('.')) found.add(node_path.resolve(app.dir, spec));
2339
+ }
2340
+ const walk = async (dir)=>{
2341
+ let entries;
2342
+ try {
2343
+ entries = await promises.readdir(dir, {
2344
+ withFileTypes: true
2345
+ });
2346
+ } catch {
2347
+ return;
2348
+ }
2349
+ for (const entry of entries){
2350
+ const full = node_path.join(dir, entry.name);
2351
+ if (entry.isDirectory()) await walk(full);
2352
+ else if (SOURCE.test(entry.name) && /runtime-plugin/.test(node_path.relative(app.dir, full))) found.add(full);
2353
+ }
2354
+ };
2355
+ await walk(node_path.join(app.dir, 'src'));
2356
+ return [
2357
+ ...found
2358
+ ];
2359
+ }
2360
+ async function resolveFile(base) {
2361
+ for (const ext of EXTENSIONS)if (await readText(base + ext) !== void 0) return base + ext;
2362
+ }
2363
+ const runtimePlugins = {
2364
+ id: 'plugins.imports',
2365
+ title: 'runtime plugins import no shared modules',
2366
+ async run (view) {
2367
+ const shared = new Set([
2368
+ ...SINGLETON_PACKAGES,
2369
+ ...view.workspace.filter((p)=>!view.apps.some((a)=>a.dir === p.dir)).flatMap((p)=>p.pkg.name ? [
2370
+ p.pkg.name
2371
+ ] : [])
2372
+ ]);
2373
+ const out = [];
2374
+ for (const app of view.apps.filter((a)=>a.present)){
2375
+ const queue = await pluginFiles(app);
2376
+ const seen = new Set();
2377
+ for(let file = queue.shift(); void 0 !== file; file = queue.shift()){
2378
+ if (seen.has(file)) continue;
2379
+ seen.add(file);
2380
+ const code = await readText(file);
2381
+ if (void 0 !== code) {
2382
+ for (const spec of importsOf(code))if (spec.startsWith('.')) {
2383
+ const next = await resolveFile(node_path.resolve(node_path.dirname(file), spec));
2384
+ if (void 0 !== next) queue.push(next);
2385
+ } else if (!spec.startsWith('node:') && shared.has(packageNameOf(spec))) {
2386
+ const rel = node_path.relative(view.root, file).split(node_path.sep).join('/');
2387
+ out.push(finding('plugins.imports', 'E303', `${rel} imports ${spec}`, {
2388
+ app: app.name,
2389
+ path: rel
2390
+ }));
2391
+ }
2392
+ }
2393
+ }
2394
+ }
2395
+ return {
2396
+ findings: out
2397
+ };
2398
+ }
2399
+ };
2400
+ const workspaceVersions = {
2401
+ id: 'singletons.workspace-version',
2402
+ title: 'every workspace package has a version',
2403
+ run: ({ workspace })=>done(workspace.filter((p)=>!p.hasVersion).map((p)=>finding('singletons.workspace-version', 'E111', `${p.pkg.name ?? p.rel} (${p.rel}/package.json)`, {
2404
+ path: `${p.rel}/package.json`,
2405
+ fixable: true
2406
+ })))
2407
+ };
2408
+ const VALIDATE_CHECKS = [
2409
+ appsOnDisk,
2410
+ mfeJson,
2411
+ config_routes,
2412
+ config_registry
2413
+ ];
2414
+ const COMPAT_CHECKS = [
2415
+ compat
2416
+ ];
2417
+ const DOCTOR_CHECKS = [
2418
+ appsOnDisk,
2419
+ mfeJson,
2420
+ config_ports,
2421
+ config_routes,
2422
+ config_registry,
2423
+ localCheckout,
2424
+ singletonVersions,
2425
+ workspaceVersions,
2426
+ compat,
2427
+ drift,
2428
+ runtimePlugins,
2429
+ ...envChecks
2430
+ ];
2431
+ const DOCTOR_ARGS = {
2432
+ fix: {
2433
+ type: 'boolean',
2434
+ description: 'Repair what is safe: missing workspace versions and registry entries'
2435
+ },
2436
+ ...GLOBAL_ARGS
2437
+ };
2438
+ const VALIDATE_ARGS = {
2439
+ ...GLOBAL_ARGS
2440
+ };
2441
+ const COMPAT_ARGS = {
2442
+ ...GLOBAL_ARGS
2443
+ };
2444
+ const DEFAULT_VERSION = '0.0.0';
2445
+ function withVersion(source) {
2446
+ const doc = JSON.parse(source);
2447
+ if ('object' != typeof doc || null === doc || Array.isArray(doc)) return source;
2448
+ const indent = /^(\s+)"/m.exec(source)?.[1] ?? ' ';
2449
+ const entries = Object.entries(doc);
2450
+ const at = Math.max(1, entries.findIndex(([key])=>'name' === key) + 1);
2451
+ const next = Object.fromEntries([
2452
+ ...entries.slice(0, at),
2453
+ [
2454
+ 'version',
2455
+ DEFAULT_VERSION
2456
+ ],
2457
+ ...entries.slice(at)
2458
+ ]);
2459
+ return `${JSON.stringify(next, null, indent)}\n`;
2460
+ }
2461
+ function withPorts(project, ports) {
2462
+ return Object.fromEntries(Object.entries(project.apps).map(([name, app])=>{
2463
+ const port = ports[name];
2464
+ return [
2465
+ name,
2466
+ void 0 === port ? app : {
2467
+ ...app,
2468
+ port
2469
+ }
2470
+ ];
2471
+ }));
2472
+ }
2473
+ async function plannedFixes(rt, view, findings, builder) {
2474
+ let count = 0;
2475
+ const assigned = {};
2476
+ const missingEnvs = new Map();
2477
+ for (const f of findings.filter((x)=>x.fixable))if ('MFEFLOW_E111' === f.code && void 0 !== f.path) {
2478
+ await builder.patch(f.path, withVersion);
2479
+ count += 1;
2480
+ } else if ('MFEFLOW_E114' === f.code && void 0 !== f.app && void 0 !== f.path) {
2481
+ const env = f.path.split('/')[2] ?? '';
2482
+ missingEnvs.set(f.app, [
2483
+ ...missingEnvs.get(f.app) ?? [],
2484
+ env
2485
+ ]);
2486
+ }
2487
+ for (const [name, envs] of missingEnvs){
2488
+ let port = view.project.apps[name]?.port;
2489
+ if (void 0 === port) {
2490
+ port = freePort({
2491
+ apps: withPorts(view.project, assigned)
2492
+ }, 4311);
2493
+ assigned[name] = port;
2494
+ }
2495
+ for (const file of (await registryEntryFiles(rt.roots, {
2496
+ name,
2497
+ port
2498
+ }, envs))){
2499
+ await builder.create(file.path, file.content, false);
2500
+ count += 1;
2501
+ }
2502
+ }
2503
+ return {
2504
+ count,
2505
+ project: {
2506
+ ...view.project,
2507
+ apps: withPorts(view.project, assigned)
2508
+ }
2509
+ };
2510
+ }
2511
+ async function fixAll(rt, globals, view, findings) {
2512
+ const { checkout } = chooseLocal(view.project, view.root, globals.cwd, void 0);
2513
+ const builder = createBuilder(view.root, checkout);
2514
+ await builder.captureProject();
2515
+ const { count, project } = await plannedFixes(rt, view, findings, builder);
2516
+ if (0 === count) return 0;
2517
+ const outcome = await writePlan({
2518
+ rt,
2519
+ globals: {
2520
+ ...globals,
2521
+ install: false
2522
+ },
2523
+ root: view.root,
2524
+ plan: builder.build(),
2525
+ project
2526
+ });
2527
+ return outcome.dryRun ? 0 : count;
2528
+ }
2529
+ async function runGroup(rt, args, raw, defs, command, checks, fix) {
2530
+ assertKnownFlags(raw, defs);
2531
+ const globals = readGlobals(args, rt.io.env, rt.io.cwd);
2532
+ const root = await findProjectRoot(globals.cwd);
2533
+ let view = await inspectProject(root);
2534
+ let report = await runChecks(view, checks);
2535
+ let fixed = 0;
2536
+ if (fix && report.findings.some((f)=>f.fixable)) {
2537
+ fixed = await fixAll(rt, globals, view, report.findings);
2538
+ if (fixed > 0) {
2539
+ view = await inspectProject(root);
2540
+ report = await runChecks(view, checks);
2541
+ }
2542
+ }
2543
+ const summary = summarizeReport(report, fixed);
2544
+ if (!globals.json) rt.io.stdout(formatReport(report, summary));
2545
+ return {
2546
+ ok: 0 === summary.errors,
2547
+ command,
2548
+ root,
2549
+ summary,
2550
+ checks: report.checks,
2551
+ findings: report.findings
2552
+ };
2553
+ }
2554
+ function runDoctor(rt, args, raw) {
2555
+ return runGroup(rt, args, raw, DOCTOR_ARGS, 'doctor', DOCTOR_CHECKS, true === args.fix);
2556
+ }
2557
+ function runValidate(rt, args, raw) {
2558
+ return runGroup(rt, args, raw, VALIDATE_ARGS, 'validate', VALIDATE_CHECKS, false);
2559
+ }
2560
+ function runCheckCompat(rt, args, raw) {
2561
+ return runGroup(rt, args, raw, COMPAT_ARGS, 'check-compat', COMPAT_CHECKS, false);
2562
+ }
2563
+ const KB = 1024;
2564
+ const kb = (bytes)=>round1(bytes / KB);
2565
+ async function gzipBytes(file) {
2566
+ try {
2567
+ return node_zlib.gzipSync(await promises.readFile(file), {
2568
+ level: 6
2569
+ }).length;
2570
+ } catch {
2571
+ return;
2572
+ }
2573
+ }
2574
+ async function sumGzip(dist, files) {
2575
+ const sizes = await Promise.all(files.map((f)=>gzipBytes(node_path.join(dist, ...f.split('/')))));
2576
+ return sizes.some((s)=>void 0 === s) ? void 0 : sizes.reduce((a, s)=>a + (s ?? 0), 0);
2577
+ }
2578
+ async function shellInitial(dist) {
2579
+ const html = await readText(node_path.join(dist, 'index.html'));
2580
+ if (void 0 === html) return;
2581
+ const files = [
2582
+ ...html.matchAll(/<script\b[^>]*\bsrc=["']([^"']+)["']/g)
2583
+ ].map((m)=>m[1] ?? '').filter((src)=>!/^[a-z]+:\/\//.test(src)).map((src)=>src.replace(/^\.?\//, '').split(/[?#]/)[0] ?? '');
2584
+ const bytes = await sumGzip(dist, files);
2585
+ return void 0 === bytes ? void 0 : {
2586
+ files,
2587
+ bytes
2588
+ };
2589
+ }
2590
+ const sizes_isObj = (v)=>'object' == typeof v && null !== v && !Array.isArray(v);
2591
+ const ORIGIN = 'https://dist.invalid';
2592
+ const SCRIPT = /\.[cm]?js$/;
2593
+ function distPath(url) {
2594
+ try {
2595
+ return decodeURIComponent(new URL(url).pathname).replace(/^\//, '');
2596
+ } catch {
2597
+ return;
2598
+ }
2599
+ }
2600
+ async function remoteSizes(dist) {
2601
+ const text = await readText(node_path.join(dist, 'mf-manifest.json'));
2602
+ if (void 0 === text) return;
2603
+ let manifest;
2604
+ try {
2605
+ manifest = JSON.parse(text);
2606
+ } catch {
2607
+ return;
2608
+ }
2609
+ if (!sizes_isObj(manifest) || !sizes_isObj(manifest.metaData) || !sizes_isObj(manifest.metaData.remoteEntry)) return;
2610
+ const local = {
2611
+ ...manifest,
2612
+ metaData: {
2613
+ ...manifest.metaData,
2614
+ publicPath: 'auto',
2615
+ getPublicPath: void 0,
2616
+ globalName: void 0,
2617
+ remoteEntry: {
2618
+ ...manifest.metaData.remoteEntry,
2619
+ type: void 0
2620
+ }
2621
+ }
2622
+ };
2623
+ let assets;
2624
+ try {
2625
+ assets = assetsFromManifest(local, `${ORIGIN}/mf-manifest.json`);
2626
+ } catch {
2627
+ return;
2628
+ }
2629
+ const entry = void 0 === assets.remoteEntry ? void 0 : distPath(assets.remoteEntry);
2630
+ if (void 0 === entry) return;
2631
+ const entryBytes = await gzipBytes(node_path.join(dist, ...entry.split('/')));
2632
+ if (void 0 === entryBytes) return;
2633
+ const exposes = {};
2634
+ for (const [name, urls] of Object.entries(assets.exposes)){
2635
+ const files = urls.flatMap((url)=>distPath(url) ?? []).filter((f)=>SCRIPT.test(f));
2636
+ const bytes = await sumGzip(dist, files);
2637
+ if (void 0 !== bytes) exposes[name] = bytes;
2638
+ }
2639
+ return {
2640
+ entryBytes,
2641
+ exposes
2642
+ };
2643
+ }
2644
+ const BUDGET_ARGS = {
2645
+ dist: {
2646
+ type: 'string',
2647
+ description: 'Build output folder inside each app (default dist)',
2648
+ valueHint: 'dir'
2649
+ },
2650
+ ...GLOBAL_ARGS
2651
+ };
2652
+ function modelOf(view) {
2653
+ const given = view.project.budget ?? {};
2654
+ return {
2655
+ lcpDeltaMs: given.lcpDeltaMs ?? COST_MODEL.lcpDeltaMs,
2656
+ shellKb: given.shellKb ?? COST_MODEL.shellKb,
2657
+ remoteKb: given.remoteKb ?? COST_MODEL.remoteKb
2658
+ };
2659
+ }
2660
+ const notBuilt = (app, what)=>finding('budget', 'E305', `${app.name}: ${what} not found in ${app.rel}`, {
2661
+ app: app.name
2662
+ });
2663
+ async function shellBudget(app, dist, model, baselineKb, out) {
2664
+ const size = await shellInitial(node_path.join(app.dir, dist));
2665
+ if (void 0 === size) return void out.push(notBuilt(app, `${dist}/index.html and its scripts`));
2666
+ const initialKb = kb(size.bytes);
2667
+ const budgetKb = void 0 === baselineKb ? null : baselineKb + model.shellKb;
2668
+ const pass = void 0 === baselineKb ? null : judgeCost({
2669
+ shellBytes: size.bytes,
2670
+ monolithBytes: 1024 * baselineKb,
2671
+ remoteBytes: {},
2672
+ lcpDeltaMs: 0
2673
+ }, model).shell.pass;
2674
+ if (false === pass) out.push(finding('budget', 'E304', `${app.name} loads ${String(initialKb)} KB of JS first; your one-app baseline is ${String(baselineKb)} KB and the shell may add ${String(model.shellKb)} KB`, {
2675
+ app: app.name
2676
+ }));
2677
+ return {
2678
+ name: app.name,
2679
+ initialKb,
2680
+ scripts: size.files.length,
2681
+ baselineKb: baselineKb ?? null,
2682
+ budgetKb,
2683
+ pass
2684
+ };
2685
+ }
2686
+ async function remoteBudget(app, dist, model, out) {
2687
+ const size = await remoteSizes(node_path.join(app.dir, dist));
2688
+ if (void 0 === size) return void out.push(notBuilt(app, `${dist}/mf-manifest.json and its entry`));
2689
+ const entryKb = kb(size.entryBytes);
2690
+ const pass = judgeCost({
2691
+ shellBytes: 0,
2692
+ monolithBytes: 0,
2693
+ remoteBytes: {
2694
+ [app.name]: size.entryBytes
2695
+ },
2696
+ lcpDeltaMs: 0
2697
+ }, model).remotes.every((r)=>r.pass);
2698
+ if (!pass) out.push(finding('budget', 'E304', `${app.name}'s entry is ${String(entryKb)} KB; a remote may add ${String(model.remoteKb)} KB`, {
2699
+ app: app.name
2700
+ }));
2701
+ const own = 'ok' === app.mfe.state ? app.mfe.value : void 0;
2702
+ const budgetKb = own?.budgets?.initialJsKb ?? null;
2703
+ const exposeNames = Object.keys(size.exposes);
2704
+ const expose = exposeNames.includes('./App') ? './App' : exposeNames[0];
2705
+ const routes = (own?.routes ?? []).map((route)=>{
2706
+ const total = round1((size.entryBytes + (void 0 === expose ? 0 : size.exposes[expose] ?? 0)) / 1024);
2707
+ const routePass = null === budgetKb ? null : total <= budgetKb;
2708
+ if (false === routePass) out.push(finding('budget', 'E304', `${app.name} on ${route} loads ${String(total)} KB; mfe.json budgets.initialJsKb is ${String(budgetKb)}`, {
2709
+ app: app.name,
2710
+ path: `${app.rel}/mfe.json`
2711
+ }));
2712
+ return {
2713
+ route,
2714
+ expose: expose ?? null,
2715
+ kb: total,
2716
+ budgetKb,
2717
+ pass: routePass
2718
+ };
2719
+ });
2720
+ return {
2721
+ name: app.name,
2722
+ entryKb,
2723
+ budgetKb: model.remoteKb,
2724
+ pass,
2725
+ routes
2726
+ };
2727
+ }
2728
+ async function runBudget(rt, args, raw) {
2729
+ assertKnownFlags(raw, BUDGET_ARGS);
2730
+ const globals = readGlobals(args, rt.io.env, rt.io.cwd);
2731
+ const dist = 'string' == typeof args.dist && '' !== args.dist ? args.dist : 'dist';
2732
+ const view = await inspectProject(await findProjectRoot(globals.cwd));
2733
+ const model = modelOf(view);
2734
+ const findings = [];
2735
+ const baseline = view.project.budget?.baselineKb;
2736
+ const present = view.apps.filter((a)=>a.present);
2737
+ const shells = (await Promise.all(present.filter((a)=>'shell' === a.kind).map((a)=>shellBudget(a, dist, model, baseline, findings)))).filter((s)=>void 0 !== s);
2738
+ const remotes = (await Promise.all(present.filter((a)=>'remote' === a.kind).map((a)=>remoteBudget(a, dist, model, findings)))).filter((r)=>void 0 !== r);
2739
+ const result = {
2740
+ ok: 0 === exitForFindings(findings),
2741
+ command: 'budget',
2742
+ root: view.root,
2743
+ model,
2744
+ shells,
2745
+ remotes,
2746
+ findings
2747
+ };
2748
+ if (!globals.json) rt.io.stdout(formatBudget(result));
2749
+ return result;
2750
+ }
2751
+ function formatBudget(result) {
2752
+ const lines = [
2753
+ 'Initial JS, gzip (1 KB = 1024 bytes):'
2754
+ ];
2755
+ for (const s of result.shells){
2756
+ const limit = null === s.budgetKb ? 'no baseline set (budget.baselineKb), not checked' : `limit ${String(s.budgetKb)}`;
2757
+ lines.push(` shell ${s.name}: ${String(s.initialKb)} KB in ${String(s.scripts)} scripts, ${limit}`);
2758
+ }
2759
+ for (const r of result.remotes){
2760
+ lines.push(` remote ${r.name}: entry ${String(r.entryKb)} KB, limit ${String(r.budgetKb)}`);
2761
+ for (const route of r.routes){
2762
+ const limit = null === route.budgetKb ? 'no budget' : `limit ${String(route.budgetKb)}`;
2763
+ lines.push(` ${route.route} (${route.expose ?? 'no expose'}): ${String(route.kb)} KB, ${limit}`);
2764
+ }
2765
+ }
2766
+ lines.push(`LCP (+${String(result.model.lcpDeltaMs)} ms) is measured by the perf job, not here.`);
2767
+ for (const f of result.findings)lines.push('', `error ${f.code} ${f.message}`, ` Fix: ${f.fix}`, ` ${f.docs}`);
2768
+ return lines.join('\n');
2769
+ }
2770
+ const LOCAL_HOSTS = new Set([
2771
+ 'localhost',
2772
+ '127.0.0.1',
2773
+ '[::1]'
2774
+ ]);
2775
+ function parseOverrides(flags, remotes) {
2776
+ const out = new Map();
2777
+ for (const flag of flags){
2778
+ const equals = flag.indexOf('=');
2779
+ if (equals < 1) throw new MfeflowError('E107', `--remote "${flag}" must look like name=url`);
2780
+ const name = flag.slice(0, equals);
2781
+ const raw = flag.slice(equals + 1);
2782
+ if (!remotes.includes(name)) throw new MfeflowError('E107', `--remote ${name}: no remote called "${name}" (remotes: ${remotes.join(', ')})`);
2783
+ let url;
2784
+ try {
2785
+ url = new URL(raw);
2786
+ } catch {
2787
+ throw new MfeflowError('E107', `--remote ${name}=${raw}: not a URL`);
2788
+ }
2789
+ if ('http:' !== url.protocol && 'https:' !== url.protocol) throw new MfeflowError('E107', `--remote ${name}=${raw}: only http and https URLs`);
2790
+ if ('/' === url.pathname && '' === url.search && '' === url.hash) url = new URL('/mf-manifest.json', url);
2791
+ out.set(name, url.href);
2792
+ }
2793
+ return out;
2794
+ }
2795
+ function assertAllowed(overrides, allowed) {
2796
+ const origins = new Set(allowed.map((o)=>originOf(o)));
2797
+ for (const [name, href] of overrides){
2798
+ const url = new URL(href);
2799
+ if (!LOCAL_HOSTS.has(url.hostname) && !origins.has(url.origin)) throw new MfeflowError('E107', `--remote ${name}=${href} is not on localhost; pass --allow-origin ${url.origin} to allow it`);
2800
+ }
2801
+ }
2802
+ function originOf(value) {
2803
+ try {
2804
+ return new URL(value).origin;
2805
+ } catch {
2806
+ throw new MfeflowError('E107', `--allow-origin "${value}" is not an origin like https://dev.example.com`);
2807
+ }
2808
+ }
2809
+ function devIndex(view, overrides = new Map()) {
2810
+ const remotes = view.apps.filter((app)=>'remote' === app.kind).toSorted((a, b)=>a.name < b.name ? -1 : a.name > b.name ? 1 : 0).map((app)=>{
2811
+ const entry = view.registry.dev?.[app.name];
2812
+ const fromRegistry = entry?.state === 'ok' ? entry.value : void 0;
2813
+ const fallback = void 0 === app.port ? '' : `http://localhost:${String(app.port)}/mf-manifest.json`;
2814
+ return {
2815
+ name: app.name,
2816
+ entry: overrides.get(app.name) ?? fromRegistry?.manifestUrl ?? fallback,
2817
+ version: overrides.has(app.name) ? 'override' : fromRegistry?.version ?? 'dev'
2818
+ };
2819
+ }).filter((remote)=>'' !== remote.entry);
2820
+ return {
2821
+ remotes
2822
+ };
2823
+ }
2824
+ const serializeIndex = (index)=>`${JSON.stringify(index, null, 2)}\n`;
2825
+ const DEFAULT_WAIT = {
2826
+ intervalMs: 2000,
2827
+ timeoutMs: 300000
2828
+ };
2829
+ const GRACE_MS = 5000;
2830
+ const DEFAULT_PATHEXT = '.COM;.EXE;.BAT;.CMD';
2831
+ function resolveCommand(command, env) {
2832
+ const win = 'win32' === env.platform;
2833
+ const flavour = win ? node_path.win32 : node_path.posix;
2834
+ if (command.includes('/') || win && command.includes('\\')) return flavour.isAbsolute(command) ? command : flavour.resolve(command);
2835
+ const extensions = win ? (env.pathext ?? DEFAULT_PATHEXT).split(';').filter((ext)=>'' !== ext) : [];
2836
+ const hasExtension = extensions.some((ext)=>command.toLowerCase().endsWith(ext.toLowerCase()));
2837
+ const names = win ? hasExtension ? [
2838
+ command
2839
+ ] : extensions.map((ext)=>command + ext) : [
2840
+ command
2841
+ ];
2842
+ for (const dir of (env.path ?? '').split(flavour.delimiter))if ('' !== dir) for (const name of names){
2843
+ const file = flavour.join(dir, name);
2844
+ if (env.isFile(file)) return file;
2845
+ }
2846
+ }
2847
+ function isRunnableFile(file) {
2848
+ try {
2849
+ if (!node_fs.statSync(file).isFile()) return false;
2850
+ if ('win32' !== process.platform) node_fs.accessSync(file, node_fs.constants.X_OK);
2851
+ return true;
2852
+ } catch {
2853
+ return false;
2854
+ }
2855
+ }
2856
+ const resolved = new Map();
2857
+ function absoluteCommand(command) {
2858
+ if (!resolved.has(command)) {
2859
+ const key = Object.keys(process.env).find((k)=>'PATH' === k.toUpperCase());
2860
+ resolved.set(command, resolveCommand(command, {
2861
+ path: void 0 === key ? void 0 : process.env[key],
2862
+ pathext: process.env.PATHEXT,
2863
+ platform: process.platform,
2864
+ isFile: isRunnableFile
2865
+ }));
2866
+ }
2867
+ return resolved.get(command) ?? command;
2868
+ }
2869
+ const taskkillExe = ()=>node_path.win32.join(process.env.SystemRoot ?? String.raw`C:\Windows`, 'System32', 'taskkill.exe');
2870
+ const alive = (kill, pid)=>{
2871
+ try {
2872
+ kill(-pid, 0);
2873
+ return true;
2874
+ } catch {
2875
+ return false;
2876
+ }
2877
+ };
2878
+ const sleep = (ms)=>new Promise((resolve)=>setTimeout(resolve, ms));
2879
+ async function killTree(pid, how) {
2880
+ if ('win32' === how.platform) return void await how.taskkill(pid);
2881
+ for (const signal of [
2882
+ 'SIGTERM',
2883
+ 'SIGKILL'
2884
+ ]){
2885
+ try {
2886
+ how.kill(-pid, signal);
2887
+ } catch {
2888
+ return;
2889
+ }
2890
+ const deadline = Date.now() + ('SIGTERM' === signal ? how.graceMs : 2000);
2891
+ while(alive(how.kill, pid) && Date.now() < deadline)await sleep(50);
2892
+ if (!alive(how.kill, pid)) return;
2893
+ }
2894
+ }
2895
+ const realKiller = {
2896
+ platform: process.platform,
2897
+ kill: (pid, signal)=>process.kill(pid, signal),
2898
+ taskkill: (pid)=>new Promise((resolve)=>{
2899
+ const child = spawn(taskkillExe(), [
2900
+ '/pid',
2901
+ String(pid),
2902
+ '/T',
2903
+ '/F'
2904
+ ], {
2905
+ stdio: 'ignore'
2906
+ });
2907
+ child.on('error', ()=>{
2908
+ resolve();
2909
+ });
2910
+ child.on('close', ()=>{
2911
+ resolve();
2912
+ });
2913
+ }),
2914
+ graceMs: GRACE_MS
2915
+ };
2916
+ function wrap(child, killer) {
2917
+ const exit = new Promise((resolve)=>{
2918
+ child.once('close', ()=>{
2919
+ resolve();
2920
+ });
2921
+ child.once('error', ()=>{
2922
+ resolve();
2923
+ });
2924
+ });
2925
+ return {
2926
+ pid: child.pid,
2927
+ onOutput (listener) {
2928
+ for (const stream of [
2929
+ child.stdout,
2930
+ child.stderr
2931
+ ])stream?.on('data', (chunk)=>{
2932
+ listener(chunk.toString('utf8'));
2933
+ });
2934
+ },
2935
+ onExit (listener) {
2936
+ child.once('close', (code)=>{
2937
+ listener(code);
2938
+ });
2939
+ child.once('error', ()=>{
2940
+ listener(null);
2941
+ });
2942
+ },
2943
+ async stop () {
2944
+ if (void 0 !== child.pid) await killTree(child.pid, killer);
2945
+ await Promise.race([
2946
+ exit,
2947
+ sleep(2000)
2948
+ ]);
2949
+ }
2950
+ };
2951
+ }
2952
+ function createReaper(proc, killNow) {
2953
+ const live = new Set();
2954
+ let listening = false;
2955
+ return {
2956
+ track (pid) {
2957
+ if (!listening) {
2958
+ listening = true;
2959
+ proc.on('exit', ()=>{
2960
+ for (const left of live)killNow(left);
2961
+ });
2962
+ }
2963
+ live.add(pid);
2964
+ },
2965
+ untrack (pid) {
2966
+ live.delete(pid);
2967
+ }
2968
+ };
2969
+ }
2970
+ const reaper = createReaper(process, (pid)=>{
2971
+ if ('win32' === process.platform) return void spawnSync(taskkillExe(), [
2972
+ '/pid',
2973
+ String(pid),
2974
+ '/T',
2975
+ '/F'
2976
+ ], {
2977
+ stdio: 'ignore'
2978
+ });
2979
+ try {
2980
+ process.kill(-pid, 'SIGKILL');
2981
+ } catch {}
2982
+ });
2983
+ const spawnDev = (spec)=>{
2984
+ const command = absoluteCommand(spec.command);
2985
+ const shell = 'win32' === process.platform && (!node_path.isAbsolute(command) || /\.(?:cmd|bat)$/i.test(command));
2986
+ const child = spawn(shell && /\s/.test(command) ? `"${command}"` : command, [
2987
+ ...spec.args
2988
+ ], {
2989
+ cwd: spec.cwd,
2990
+ env: {
2991
+ ...process.env,
2992
+ ...spec.env
2993
+ },
2994
+ stdio: [
2995
+ 'ignore',
2996
+ 'pipe',
2997
+ 'pipe'
2998
+ ],
2999
+ detached: 'win32' !== process.platform,
3000
+ shell
3001
+ });
3002
+ const proc = wrap(child, realKiller);
3003
+ if (void 0 === child.pid) return proc;
3004
+ const { pid } = child;
3005
+ reaper.track(pid);
3006
+ child.once('close', ()=>{
3007
+ if (!alive(realKiller.kill, pid)) reaper.untrack(pid);
3008
+ });
3009
+ return {
3010
+ ...proc,
3011
+ async stop () {
3012
+ await proc.stop();
3013
+ reaper.untrack(pid);
3014
+ }
3015
+ };
3016
+ };
3017
+ function portFree(port) {
3018
+ return new Promise((resolve)=>{
3019
+ const probe = node_net.createServer();
3020
+ probe.once('error', ()=>{
3021
+ resolve(false);
3022
+ });
3023
+ probe.listen(port, ()=>{
3024
+ probe.close(()=>{
3025
+ resolve(true);
3026
+ });
3027
+ });
3028
+ });
3029
+ }
3030
+ const DEBOUNCE_MS = 150;
3031
+ function watchPaths(paths, listener) {
3032
+ let timer;
3033
+ const fire = ()=>{
3034
+ clearTimeout(timer);
3035
+ timer = setTimeout(listener, DEBOUNCE_MS);
3036
+ };
3037
+ const watchers = [];
3038
+ for (const target of paths){
3039
+ let folder = target;
3040
+ let only;
3041
+ try {
3042
+ if (!node_fs.statSync(target).isDirectory()) {
3043
+ folder = node_path.dirname(target);
3044
+ only = node_path.basename(target);
3045
+ }
3046
+ } catch {
3047
+ continue;
3048
+ }
3049
+ try {
3050
+ watchers.push(node_fs.watch(folder, (_event, name)=>{
3051
+ if (void 0 === only || null === name || name === only) fire();
3052
+ }));
3053
+ } catch {}
3054
+ }
3055
+ return ()=>{
3056
+ clearTimeout(timer);
3057
+ for (const watcher of watchers)watcher.close();
3058
+ };
3059
+ }
3060
+ function onInterrupt(listener, source = process) {
3061
+ const signals = [
3062
+ 'SIGINT',
3063
+ 'SIGTERM',
3064
+ 'SIGHUP'
3065
+ ];
3066
+ for (const signal of signals)source.on(signal, listener);
3067
+ return ()=>{
3068
+ for (const signal of signals)source.off(signal, listener);
3069
+ };
3070
+ }
3071
+ function realDevHost() {
3072
+ return {
3073
+ spawn: spawnDev,
3074
+ onInterrupt: onInterrupt,
3075
+ watch: watchPaths,
3076
+ portFree: portFree
3077
+ };
3078
+ }
3079
+ const DEV_ARGS = {
3080
+ only: {
3081
+ type: 'string',
3082
+ description: 'Run only these apps, comma separated (default: the shell and every remote)',
3083
+ valueHint: 'a,b'
3084
+ },
3085
+ remote: {
3086
+ type: 'string',
3087
+ description: 'Use a server you already run for a remote, repeatable: --remote checkout=http://localhost:3001',
3088
+ valueHint: 'name=url'
3089
+ },
3090
+ 'allow-origin': {
3091
+ type: 'string',
3092
+ description: 'Let --remote point at this origin; localhost is always allowed. Repeatable',
3093
+ valueHint: 'origin'
3094
+ },
3095
+ ...GLOBAL_ARGS
3096
+ };
3097
+ const PALETTE = [
3098
+ 36,
3099
+ 35,
3100
+ 33,
3101
+ 32,
3102
+ 34,
3103
+ 91
3104
+ ];
3105
+ const REGISTRY_DIR = '.mfeflow';
3106
+ const REGISTRY_FILE = 'dev-registry.json';
3107
+ async function writeIfChanged(file, text) {
3108
+ if (await readText(file) === text) return false;
3109
+ await promises.mkdir(node_path.dirname(file), {
3110
+ recursive: true
3111
+ });
3112
+ const temp = `${file}.${String(process.pid)}.tmp`;
3113
+ await promises.writeFile(temp, text, 'utf8');
3114
+ await promises.rename(temp, file);
3115
+ return true;
3116
+ }
3117
+ class Session {
3118
+ host;
3119
+ out;
3120
+ registryFile;
3121
+ names;
3122
+ running = new Map();
3123
+ buffers = new Map();
3124
+ stopping = false;
3125
+ failed = new Set();
3126
+ finish;
3127
+ done = new Promise((resolve)=>{
3128
+ this.finish = resolve;
3129
+ });
3130
+ width;
3131
+ color;
3132
+ constructor(host, io, out, registryFile, names){
3133
+ this.host = host;
3134
+ this.out = out;
3135
+ this.registryFile = registryFile;
3136
+ this.names = names;
3137
+ this.width = Math.max(...names.map((n)=>n.length), 4);
3138
+ this.color = void 0 === io.env.NO_COLOR && (io.interactive || void 0 !== io.env.FORCE_COLOR);
3139
+ }
3140
+ get hasFailed() {
3141
+ return this.failed.size > 0;
3142
+ }
3143
+ has(name) {
3144
+ return this.running.has(name);
3145
+ }
3146
+ runningNames() {
3147
+ return [
3148
+ ...this.running.keys()
3149
+ ];
3150
+ }
3151
+ prefix(name) {
3152
+ const index = Math.max(0, this.names.indexOf(name));
3153
+ const label = name.padEnd(this.width);
3154
+ const code = PALETTE[index % PALETTE.length] ?? 36;
3155
+ return this.color ? `\u001B[${String(code)}m${label} |\u001B[0m ` : `${label} | `;
3156
+ }
3157
+ line(name, text) {
3158
+ this.out(`${this.prefix(name)}${text}`);
3159
+ }
3160
+ feed(name, chunk) {
3161
+ const text = (this.buffers.get(name) ?? '') + chunk;
3162
+ const lines = text.split(/\r?\n/);
3163
+ this.buffers.set(name, lines.pop() ?? '');
3164
+ for (const line of lines)if ('' !== line.trim()) this.line(name, line);
3165
+ }
3166
+ flush(name) {
3167
+ const rest = this.buffers.get(name) ?? '';
3168
+ this.buffers.delete(name);
3169
+ if ('' !== rest.trim()) this.line(name, rest);
3170
+ }
3171
+ start(app, packageManager) {
3172
+ const child = this.host.spawn({
3173
+ command: packageManager,
3174
+ args: [
3175
+ 'run',
3176
+ 'dev'
3177
+ ],
3178
+ cwd: app.dir,
3179
+ env: {
3180
+ MFEFLOW_DEV_REGISTRY: this.registryFile,
3181
+ ...this.color ? {
3182
+ FORCE_COLOR: '1'
3183
+ } : {}
3184
+ }
3185
+ });
3186
+ this.running.set(app.name, child);
3187
+ this.failed.delete(app.name);
3188
+ child.onOutput((chunk)=>{
3189
+ this.feed(app.name, chunk);
3190
+ });
3191
+ child.onExit((code)=>{
3192
+ this.flush(app.name);
3193
+ if (this.running.get(app.name) !== child) return;
3194
+ this.running.delete(app.name);
3195
+ if (!this.stopping) {
3196
+ this.line(app.name, 0 === code ? 'stopped' : `exited with code ${String(code)}`);
3197
+ if (0 !== code) this.failed.add(app.name);
3198
+ if (0 === this.running.size) this.finish?.();
3199
+ }
3200
+ });
3201
+ }
3202
+ async stopOne(name) {
3203
+ const child = this.running.get(name);
3204
+ if (void 0 === child) return;
3205
+ this.running.delete(name);
3206
+ await child.stop();
3207
+ this.flush(name);
3208
+ }
3209
+ async stopAll() {
3210
+ this.stopping = true;
3211
+ await Promise.all([
3212
+ ...this.running.keys()
3213
+ ].map((name)=>this.stopOne(name)));
3214
+ this.finish?.();
3215
+ }
3216
+ }
3217
+ function describe(app, packageManager) {
3218
+ return {
3219
+ name: app.name,
3220
+ kind: app.kind,
3221
+ port: app.port ?? null,
3222
+ url: void 0 === app.port ? null : `http://localhost:${String(app.port)}`,
3223
+ command: `${packageManager} run dev`,
3224
+ status: 'started'
3225
+ };
3226
+ }
3227
+ async function isLinked(app, root, name) {
3228
+ let dir = app.dir;
3229
+ while(!node_path.relative(root, dir).startsWith('..')){
3230
+ try {
3231
+ await promises.access(node_path.join(dir, 'node_modules', ...name.split('/')));
3232
+ return true;
3233
+ } catch {}
3234
+ const parent = node_path.dirname(dir);
3235
+ if (parent === dir || dir === root) break;
3236
+ dir = parent;
3237
+ }
3238
+ return false;
3239
+ }
3240
+ async function hasDependencies(app, root) {
3241
+ const text = await readText(node_path.join(app.dir, 'package.json'));
3242
+ let doc;
3243
+ try {
3244
+ doc = void 0 === text ? void 0 : JSON.parse(text);
3245
+ } catch {
3246
+ return false;
3247
+ }
3248
+ if ('object' != typeof doc || null === doc) return false;
3249
+ const names = [
3250
+ 'dependencies',
3251
+ 'devDependencies'
3252
+ ].flatMap((key)=>{
3253
+ const group = Reflect.get(doc, key);
3254
+ return 'object' == typeof group && null !== group ? Object.keys(group) : [];
3255
+ });
3256
+ const linked = await Promise.all(names.map((name)=>isLinked(app, root, name)));
3257
+ return linked.every(Boolean);
3258
+ }
3259
+ async function busyPorts(host, apps) {
3260
+ const checked = await Promise.all(apps.map(async (app)=>void 0 === app.port ? void 0 : await host.portFree(app.port) ? void 0 : app));
3261
+ return checked.filter((a)=>void 0 !== a);
3262
+ }
3263
+ function assertPortsFree(busy) {
3264
+ if (0 === busy.length) return;
3265
+ throw new MfeflowError('E306', busy.map((a)=>`${String(a.port)} is in use (${a.name})`).join('; '), {
3266
+ fix: "Stop whatever listens there (an earlier `mfeflow dev`?), or change the port in mfeflow.json and the app's rsbuild config."
3267
+ });
3268
+ }
3269
+ async function runDev(rt, args, raw) {
3270
+ assertKnownFlags(raw, DEV_ARGS);
3271
+ const globals = readGlobals(args, rt.io.env, rt.io.cwd);
3272
+ const host = rt.dev ?? realDevHost();
3273
+ const root = await findProjectRoot(globals.cwd);
3274
+ let view = await inspectProject(root);
3275
+ const { packageManager } = view.project;
3276
+ const known = view.apps.map((a)=>a.name);
3277
+ const only = splitList('string' == typeof args.only ? args.only : void 0);
3278
+ const unknown = (only ?? []).filter((n)=>!known.includes(n));
3279
+ if (unknown.length > 0) throw new MfeflowError('E107', `--only ${unknown.join(', ')}: no such app (apps: ${known.join(', ')})`);
3280
+ const overrides = parseOverrides(repeated(raw, 'remote'), view.apps.filter((a)=>'remote' === a.kind).map((a)=>a.name));
3281
+ assertAllowed(overrides, repeated(raw, 'allow-origin'));
3282
+ const wanted = (v)=>v.apps.filter((a)=>a.present && !overrides.has(a.name) && (void 0 === only || only.includes(a.name)));
3283
+ const first = wanted(view);
3284
+ if (0 === first.length) throw new MfeflowError('E107', 'nothing to run: every app is missing, filtered out by --only, or overridden');
3285
+ const registryFile = node_path.join(root, REGISTRY_DIR, REGISTRY_FILE);
3286
+ const out = (text)=>{
3287
+ if (globals.json) rt.io.stderr(text);
3288
+ else rt.io.stdout(text);
3289
+ };
3290
+ const apps = [
3291
+ ...first.map((a)=>describe(a, packageManager)),
3292
+ ...[
3293
+ ...overrides.keys()
3294
+ ].map((name)=>({
3295
+ name,
3296
+ kind: 'remote',
3297
+ port: null,
3298
+ url: overrides.get(name) ?? null,
3299
+ command: '',
3300
+ status: 'overridden'
3301
+ }))
3302
+ ];
3303
+ const base = {
3304
+ ok: true,
3305
+ command: 'dev',
3306
+ root,
3307
+ registry: registryFile,
3308
+ apps,
3309
+ overrides: Object.fromEntries(overrides)
3310
+ };
3311
+ if (globals.dryRun) {
3312
+ if (!globals.json) {
3313
+ for (const app of apps)out('overridden' === app.status ? `${app.name}: uses ${app.url ?? ''}` : `${app.name}: ${app.command} in ${view.apps.find((a)=>a.name === app.name)?.rel ?? ''} (port ${String(app.port)})`);
3314
+ out(`dev registry: ${JSON.stringify(devIndex(view, overrides).remotes.map((r)=>`${r.name} -> ${r.entry}`))}`);
3315
+ }
3316
+ return {
3317
+ ...base,
3318
+ dryRun: true,
3319
+ interrupted: false
3320
+ };
3321
+ }
3322
+ assertPortsFree(await busyPorts(host, first));
3323
+ await writeIfChanged(registryFile, serializeIndex(devIndex(view, overrides)));
3324
+ const session = new Session(host, rt.io, out, registryFile, known);
3325
+ let busy = Promise.resolve();
3326
+ const waiting = new Set();
3327
+ const policy = host.waitPolicy ?? DEFAULT_WAIT;
3328
+ let poll;
3329
+ let waitingSince = 0;
3330
+ let closed = false;
3331
+ const stopPolling = ()=>{
3332
+ clearInterval(poll);
3333
+ poll = void 0;
3334
+ };
3335
+ const keepPolling = ()=>{
3336
+ if (0 === waiting.size || closed) stopPolling();
3337
+ else if (void 0 === poll) {
3338
+ waitingSince = Date.now();
3339
+ poll = setInterval(()=>reload(true), policy.intervalMs);
3340
+ } else if (Date.now() - waitingSince > policy.timeoutMs) {
3341
+ out(`mfeflow: gave up waiting for ${[
3342
+ ...waiting
3343
+ ].join(', ')} to be installed. Run \`${packageManager} install\`, then save mfeflow.json or run \`mfeflow dev\` again.`);
3344
+ waiting.clear();
3345
+ stopPolling();
3346
+ }
3347
+ };
3348
+ const reload = (fromPoll = false)=>{
3349
+ busy = busy.then(async ()=>{
3350
+ if (fromPoll && void 0 === poll) return;
3351
+ let next;
3352
+ try {
3353
+ next = await inspectProject(root);
3354
+ } catch (error) {
3355
+ out(`mfeflow: ${messageOf(error)} (keeping what is running)`);
3356
+ return;
3357
+ }
3358
+ view = next;
3359
+ if (await writeIfChanged(registryFile, serializeIndex(devIndex(next, overrides)))) out(`mfeflow: dev registry updated (${devIndex(next, overrides).remotes.map((r)=>r.name).join(', ') || 'no remotes'})`);
3360
+ const now = wanted(next);
3361
+ for (const name of session.runningNames())if (!now.some((a)=>a.name === name)) {
3362
+ out(`mfeflow: ${name} left the project, stopping it`);
3363
+ await session.stopOne(name);
3364
+ }
3365
+ const candidates = now.filter((a)=>!session.has(a.name));
3366
+ const installed = await Promise.all(candidates.map((app)=>hasDependencies(app, root)));
3367
+ const fresh = candidates.filter((app, i)=>{
3368
+ if (true === installed[i]) {
3369
+ waiting.delete(app.name);
3370
+ return true;
3371
+ }
3372
+ if (!waiting.has(app.name)) {
3373
+ out(`mfeflow: ${app.name} is waiting for its install to finish`);
3374
+ waiting.add(app.name);
3375
+ }
3376
+ return false;
3377
+ });
3378
+ const stuck = await busyPorts(host, fresh);
3379
+ for (const app of fresh)if (stuck.includes(app)) out(`mfeflow: not starting ${app.name}: port ${String(app.port)} is in use`);
3380
+ else {
3381
+ out(`mfeflow: starting ${app.name}${void 0 === app.port ? '' : ` on ${String(app.port)}`}`);
3382
+ session.start(app, packageManager);
3383
+ }
3384
+ keepPolling();
3385
+ });
3386
+ };
3387
+ const stopWatching = host.watch([
3388
+ node_path.join(root, 'mfeflow.json'),
3389
+ node_path.join(root, 'registry', 'envs', 'dev'),
3390
+ node_path.join(root, 'pnpm-lock.yaml')
3391
+ ], ()=>reload());
3392
+ let interrupted = false;
3393
+ const stopListening = host.onInterrupt(()=>{
3394
+ if (interrupted) return;
3395
+ interrupted = true;
3396
+ out('\nmfeflow: stopping…');
3397
+ session.stopAll();
3398
+ });
3399
+ for (const app of first)session.start(app, packageManager);
3400
+ out(`mfeflow: running ${first.map((a)=>a.name).join(', ')}${overrides.size > 0 ? `; using your ${[
3401
+ ...overrides.keys()
3402
+ ].join(', ')}` : ''}. Ctrl+C stops everything.`);
3403
+ try {
3404
+ await session.done;
3405
+ } finally{
3406
+ stopWatching();
3407
+ stopListening();
3408
+ closed = true;
3409
+ stopPolling();
3410
+ await busy;
3411
+ await session.stopAll();
3412
+ }
3413
+ return {
3414
+ ...base,
3415
+ ok: !session.hasFailed,
3416
+ dryRun: false,
3417
+ interrupted
3418
+ };
3419
+ }
3420
+ const FEATURES_ARGS = {
3421
+ search: {
3422
+ type: 'positional',
3423
+ required: false,
3424
+ description: "Only features whose name or description contains this"
3425
+ },
3426
+ ...GLOBAL_ARGS
3427
+ };
3428
+ async function installedFeatures(cwd) {
3429
+ try {
3430
+ const project = await loadProject(await findProjectRoot(cwd));
3431
+ return new Set(Object.keys(project.features));
3432
+ } catch {
3433
+ return new Set();
3434
+ }
3435
+ }
3436
+ function matches(feature, search) {
3437
+ const needle = search.toLowerCase();
3438
+ return feature.name.toLowerCase().includes(needle) || feature.description.toLowerCase().includes(needle);
3439
+ }
3440
+ function features_line(feature) {
3441
+ const tags = [
3442
+ feature.always ? 'always' : feature.default ? 'default' : 'opt-in',
3443
+ ...feature.installed ? [
3444
+ 'installed'
3445
+ ] : []
3446
+ ];
3447
+ return ` ${feature.name.padEnd(14)} ${feature.version.padEnd(7)} ${feature.description} [${tags.join(', ')}]`;
3448
+ }
3449
+ async function runFeatures(rt, args, raw) {
3450
+ assertKnownFlags(raw, FEATURES_ARGS);
3451
+ const globals = readGlobals(args, rt.io.env, rt.io.cwd);
3452
+ const search = 'string' == typeof args.search && '' !== args.search ? args.search : null;
3453
+ const installed = await installedFeatures(globals.cwd);
3454
+ const all = await Promise.all(FIRST_PARTY.map(async (first)=>{
3455
+ const { manifest } = await loadAny(first.name, {
3456
+ roots: rt.roots
3457
+ });
3458
+ return {
3459
+ name: first.name,
3460
+ version: manifest.version,
3461
+ description: manifest.description ?? '',
3462
+ always: first.always,
3463
+ default: first.default,
3464
+ installed: installed.has(first.name)
3465
+ };
3466
+ }));
3467
+ const features = null === search ? all : all.filter((f)=>matches(f, search));
3468
+ if (!globals.json) {
3469
+ if (0 === features.length) rt.io.stdout(`No features match "${search ?? ''}".`);
3470
+ else rt.io.stdout(features.map(features_line).join('\n'));
3471
+ rt.io.stdout('\nFrom a registry: mfeflow add @namespace/name (configure it under "registries" in mfeflow.json).');
3472
+ }
3473
+ return {
3474
+ ok: true,
3475
+ command: 'features',
3476
+ search,
3477
+ features
3478
+ };
3479
+ }
3480
+ const INIT_ARGS = {
3481
+ dir: {
3482
+ type: 'positional',
3483
+ required: false,
3484
+ description: 'Folder to create (default mfeflow-app)'
3485
+ },
3486
+ scope: {
3487
+ type: 'string',
3488
+ description: 'npm scope for the project packages, like @my-org',
3489
+ valueHint: '@scope'
3490
+ },
3491
+ remotes: {
3492
+ type: 'string',
3493
+ description: 'Remotes to create, comma separated (default catalog,cart)',
3494
+ valueHint: 'a,b'
3495
+ },
3496
+ features: {
3497
+ type: 'string',
3498
+ description: `Optional features, comma separated (${OPTIONAL_FEATURES.join(', ')})`,
3499
+ valueHint: 'a,b'
3500
+ },
3501
+ 'package-manager': {
3502
+ type: 'string',
3503
+ description: 'pnpm (the only one in this release)',
3504
+ valueHint: 'pm'
3505
+ },
3506
+ topology: {
3507
+ type: 'string',
3508
+ description: 'monorepo (the only one in this release)',
3509
+ valueHint: 'name'
3510
+ },
3511
+ option: {
3512
+ type: 'string',
3513
+ description: 'A feature option, repeatable: --option feature.key=value',
3514
+ valueHint: 'f.k=v'
3515
+ },
3516
+ ...GLOBAL_ARGS
3517
+ };
3518
+ const DEFAULT_DIR = 'mfeflow-app';
3519
+ const DEFAULT_REMOTES = [
3520
+ 'catalog',
3521
+ 'cart'
3522
+ ];
3523
+ const SCOPE = /^@[a-z0-9][a-z0-9-._]*$/;
3524
+ function scopeFor(dir, cwd = process.cwd()) {
3525
+ const name = node_path.basename(node_path.resolve(cwd, dir)).toLowerCase().replaceAll(/[^a-z0-9-._]+/g, '-').replaceAll(/^[^a-z0-9]+|[^a-z0-9]+$/g, '');
3526
+ return `@${'' === name ? 'app' : name}`;
3527
+ }
3528
+ function checkRemotes(remotes) {
3529
+ const seen = new Set();
3530
+ for (const name of remotes){
3531
+ assertAppName(name);
3532
+ if (seen.has(name)) throw new MfeflowError('E215', `"${name}" is listed twice`);
3533
+ seen.add(name);
3534
+ }
3535
+ }
3536
+ function checkFeatures(requested) {
3537
+ const optional = new Set();
3538
+ for (const name of requested)if (!FIRST_PARTY.some((f)=>f.name === name && f.always)) {
3539
+ if (!isFirstParty(name)) throw new MfeflowError('E201', `"${name}" (init takes ${OPTIONAL_FEATURES.join(', ')}; add others later with \`mfeflow add\`)`);
3540
+ optional.add(name);
3541
+ }
3542
+ return OPTIONAL_FEATURES.filter((name)=>optional.has(name));
3543
+ }
3544
+ function init_only(flag, value, allowed) {
3545
+ if (void 0 !== value && value !== allowed) throw new MfeflowError('E107', `--${flag} ${value}: only ${allowed} is available in this release`);
3546
+ }
3547
+ async function resolveInitOptions(rt, globals, args, raw, preset) {
3548
+ init_only('package-manager', args_text(args, 'package-manager') ?? preset.packageManager, 'pnpm');
3549
+ init_only('topology', args_text(args, 'topology') ?? preset.topology, 'monorepo');
3550
+ const { canPrompt } = globals;
3551
+ const { prompter } = rt;
3552
+ const given = 'string' == typeof args.dir && '' !== args.dir ? args.dir : void 0;
3553
+ const dir = given ?? (canPrompt ? await prompter.text({
3554
+ message: 'Where should the project go?',
3555
+ initialValue: DEFAULT_DIR
3556
+ }) : DEFAULT_DIR);
3557
+ const scopeFlag = args_text(args, 'scope') ?? preset.scope;
3558
+ const scope = scopeFlag ?? (canPrompt ? await prompter.text({
3559
+ message: 'npm scope for your packages',
3560
+ initialValue: scopeFor(dir, globals.cwd),
3561
+ validate: (v)=>SCOPE.test(v) ? void 0 : 'Use a scope like @my-org'
3562
+ }) : scopeFor(dir, globals.cwd));
3563
+ if (!SCOPE.test(scope)) throw new MfeflowError('E107', `--scope "${scope}" must look like @my-org`);
3564
+ const remotesFlag = splitList(args_text(args, 'remotes')) ?? preset.remotes;
3565
+ const remotes = remotesFlag ?? (canPrompt ? splitList(await prompter.text({
3566
+ message: 'Remotes, comma separated',
3567
+ initialValue: DEFAULT_REMOTES.join(',')
3568
+ })) ?? [] : DEFAULT_REMOTES);
3569
+ checkRemotes(remotes);
3570
+ const featuresFlag = splitList(args_text(args, 'features')) ?? preset.features;
3571
+ const features = checkFeatures(featuresFlag ?? (canPrompt ? await prompter.multiselect({
3572
+ message: 'Features',
3573
+ choices: OPTIONAL_FEATURES.map((name)=>({
3574
+ value: name,
3575
+ label: name
3576
+ })),
3577
+ initialValues: DEFAULT_FEATURES
3578
+ }) : DEFAULT_FEATURES));
3579
+ const featureOptions = mergeOptions(preset.options ?? {}, parseFeatureOptions(repeated(raw, 'option')));
3580
+ return {
3581
+ dir,
3582
+ scope,
3583
+ remotes,
3584
+ features,
3585
+ featureOptions
3586
+ };
3587
+ }
3588
+ async function assertEmpty(root, readdir = promises.readdir) {
3589
+ let entries;
3590
+ try {
3591
+ entries = await readdir(root);
3592
+ } catch (error) {
3593
+ if (hasErrno(error, 'ENOENT')) return;
3594
+ if (hasErrno(error, 'ENOTDIR')) throw new MfeflowError('E108', `${root} is not a folder, or part of its path is a file`, {
3595
+ cause: error
3596
+ });
3597
+ throw new MfeflowError('E106', `could not look inside ${root} (${messageOf(error)})`, {
3598
+ cause: error
3599
+ });
3600
+ }
3601
+ if (entries.length > 0) {
3602
+ const shown = entries.slice(0, 3).join(', ');
3603
+ throw new MfeflowError('E108', `${root} already has ${shown}${entries.length > 3 ? ', …' : ''}`);
3604
+ }
3605
+ }
3606
+ function skeleton(options) {
3607
+ const apps = {
3608
+ shell: {
3609
+ kind: 'shell',
3610
+ path: 'apps/shell',
3611
+ port: 4300
3612
+ }
3613
+ };
3614
+ options.remotes.forEach((name, index)=>{
3615
+ apps[name] = {
3616
+ kind: 'remote',
3617
+ path: `apps/${name}`,
3618
+ port: 4311 + index
3619
+ };
3620
+ });
3621
+ return {
3622
+ version: 1,
3623
+ topology: 'monorepo',
3624
+ packageManager: 'pnpm',
3625
+ scope: options.scope,
3626
+ apps,
3627
+ features: {}
3628
+ };
3629
+ }
3630
+ async function runInit(rt, args, raw) {
3631
+ assertKnownFlags(raw, INIT_ARGS);
3632
+ const globals = readGlobals(args, rt.io.env, rt.io.cwd, rt.io.interactive);
3633
+ const preset = globals.preset ? await loadPreset(globals.preset, {
3634
+ cwd: globals.cwd,
3635
+ fetch: rt.fetch
3636
+ }) : {};
3637
+ const options = await resolveInitOptions(rt, globals, args, raw, preset);
3638
+ const root = node_path.resolve(globals.cwd, options.dir);
3639
+ await assertEmpty(root);
3640
+ const chosen = chooseLocal(skeleton(options), root, globals.cwd, globals.local);
3641
+ const { project } = chosen;
3642
+ const builder = createBuilder(root, chosen.checkout);
3643
+ const names = [
3644
+ 'core',
3645
+ 'tsconfig',
3646
+ ...options.features
3647
+ ];
3648
+ assertOptionsAreForFeatures(options.featureOptions, names);
3649
+ const loaded = await Promise.all(names.map((name)=>loadAny(name, {
3650
+ roots: rt.roots
3651
+ })));
3652
+ await inOrder(loaded, (feature)=>resolveFeature({
3653
+ feature,
3654
+ project,
3655
+ builder,
3656
+ options: options.featureOptions[feature.manifest.name] ?? {},
3657
+ alsoAdding: names
3658
+ }));
3659
+ const core = loaded[0];
3660
+ await inOrder(options.remotes, async (remote)=>{
3661
+ const port = project.apps[remote]?.port;
3662
+ if (void 0 === core || void 0 === port) return;
3663
+ await resolveFeature({
3664
+ feature: await remoteGlue({
3665
+ roots: rt.roots,
3666
+ version: core.manifest.version,
3667
+ remote: {
3668
+ name: remote,
3669
+ port
3670
+ },
3671
+ registry: true,
3672
+ shells: [
3673
+ 'shell'
3674
+ ]
3675
+ }),
3676
+ project,
3677
+ builder,
3678
+ alsoAdding: names
3679
+ });
3680
+ });
3681
+ const plan = builder.build();
3682
+ const outcome = await writePlan({
3683
+ rt,
3684
+ globals,
3685
+ root,
3686
+ plan,
3687
+ project,
3688
+ afterWrite: async ()=>{
3689
+ if (options.features.includes('git-hooks')) await rt.system.gitInit(root);
3690
+ }
3691
+ });
3692
+ const next = [
3693
+ ...node_path.resolve(globals.cwd) === root ? [] : [
3694
+ `cd ${options.dir}`
3695
+ ],
3696
+ ...outcome.installed || outcome.dryRun ? [] : [
3697
+ 'pnpm install'
3698
+ ],
3699
+ 'pnpm dev'
3700
+ ];
3701
+ const result = {
3702
+ ok: true,
3703
+ command: 'init',
3704
+ dryRun: outcome.dryRun,
3705
+ root,
3706
+ scope: options.scope,
3707
+ apps: Object.entries(project.apps).map(([name, app])=>({
3708
+ name,
3709
+ kind: app.kind,
3710
+ path: app.path,
3711
+ port: app.port ?? null
3712
+ })),
3713
+ plan: planReport(outcome.shown),
3714
+ written: outcome.written,
3715
+ installed: outcome.installed,
3716
+ next,
3717
+ ...void 0 === outcome.diff ? {} : {
3718
+ diff: outcome.diff
3719
+ }
3720
+ };
3721
+ if (!globals.json) printInit(rt, result);
3722
+ return result;
3723
+ }
3724
+ function printInit(rt, result) {
3725
+ if (result.dryRun) return void rt.io.stdout('\nDry run: nothing was written.');
3726
+ const remotes = result.apps.filter((a)=>'remote' === a.kind).map((a)=>a.name);
3727
+ rt.io.stdout(`\nCreated ${result.root} with a shell and ${String(remotes.length)} remote${1 === remotes.length ? '' : 's'} (${remotes.join(', ')}).`);
3728
+ rt.io.stdout(`Next:\n${result.next.map((line)=>` ${line}`).join('\n')}`);
3729
+ }
3730
+ const REPO_URL = 'https://github.com/mfeflow/mfeflow';
3731
+ function flagLabel(name, def) {
3732
+ const alias = 'alias' in def ? [
3733
+ def.alias ?? []
3734
+ ].flat().map((a)=>`-${a}, `).join('') : '';
3735
+ if ('boolean' === def.type) return `${alias}--${name}`;
3736
+ return `${alias}--${name} <${'valueHint' in def ? def.valueHint ?? 'value' : 'value'}>`;
3737
+ }
3738
+ function table(args, skip) {
3739
+ const rows = Object.entries(args).filter(([name, def])=>'positional' !== def.type && !skip.has(name)).map(([name, def])=>[
3740
+ flagLabel(name, def),
3741
+ def.description ?? ''
3742
+ ]);
3743
+ const width = Math.max(0, ...rows.map(([label])=>label.length));
3744
+ return rows.map(([label, text])=>` ${label.padEnd(width)} ${text}`);
3745
+ }
3746
+ function commandHelp(help) {
3747
+ const globals = new Set(Object.keys(GLOBAL_ARGS));
3748
+ const own = table(help.args, globals);
3749
+ const lines = [
3750
+ `mfeflow ${help.name} - ${help.summary}`,
3751
+ '',
3752
+ `Usage: ${help.usage}`
3753
+ ];
3754
+ if (own.length > 0) lines.push('', 'Options:', ...own);
3755
+ lines.push('', 'Global options:', ...table(GLOBAL_ARGS, new Set()));
3756
+ lines.push('', 'Examples:', ...help.examples.map((example)=>` ${example}`));
3757
+ lines.push('', `Docs: ${DOCS_URL}/`);
3758
+ return lines.join('\n');
3759
+ }
3760
+ function exitCodesText() {
3761
+ return [
3762
+ ` ${String(EXIT.ok)} success`,
3763
+ ` ${String(EXIT.unexpected)} unexpected error`,
3764
+ ` ${String(EXIT.usage)} bad command line (MFEFLOW_E107)`,
3765
+ ` ${String(EXIT.project)} project or config problem (MFEFLOW_E1xx)`,
3766
+ ` ${String(EXIT.feature)} feature problem (MFEFLOW_E2xx)`,
3767
+ ` ${String(EXIT.build)} build or runtime problem (MFEFLOW_E3xx)`,
3768
+ ` ${String(EXIT.registry)} registry problem (MFEFLOW_E4xx)`,
3769
+ ` ${String(EXIT.upgrade)} upgrade problem (MFEFLOW_E5xx)`,
3770
+ ` ${String(EXIT.cancelled)} cancelled at a prompt, or Ctrl+C in mfeflow dev`
3771
+ ].join('\n');
3772
+ }
3773
+ function rootHelp(commands) {
3774
+ const width = Math.max(...commands.map((c)=>c.name.length));
3775
+ return [
3776
+ `mfeflow ${readPackageJson().version} - React micro-frontend platform kit`,
3777
+ '',
3778
+ 'Usage: mfeflow <command> [options]',
3779
+ '',
3780
+ 'Commands:',
3781
+ ...commands.map((c)=>` ${c.name.padEnd(width)} ${c.summary}`),
3782
+ '',
3783
+ "Run `mfeflow <command> --help` for a command's options. Every command takes",
3784
+ '--yes, --preset, --json, --dry-run, --cwd, --no-install, --verbose.',
3785
+ '',
3786
+ 'Examples:',
3787
+ ' npm create mfeflow@latest my-app -- --yes',
3788
+ ' mfeflow init my-app --remotes catalog,cart,checkout --dry-run',
3789
+ ' mfeflow add remote checkout',
3790
+ ' mfeflow features lint',
3791
+ '',
3792
+ 'Exit codes:',
3793
+ exitCodesText(),
3794
+ '',
3795
+ `Docs: ${DOCS_URL}/ Source: ${REPO_URL}`
3796
+ ].join('\n');
3797
+ }
3798
+ function assetRoots(from = node_path.dirname(fileURLToPath(import.meta.url)), exists = node_fs.existsSync) {
3799
+ const pkg = node_path.resolve(from, '..');
3800
+ const bundled = node_path.join(pkg, 'assets');
3801
+ const fromSource = 'src' === node_path.basename(from);
3802
+ const base = !fromSource && exists(node_path.join(bundled, 'features')) ? bundled : node_path.resolve(pkg, '../..');
3803
+ return {
3804
+ features: node_path.join(base, 'features'),
3805
+ templates: node_path.join(base, 'templates')
3806
+ };
3807
+ }
3808
+ function processIo() {
3809
+ return {
3810
+ stdout: (text)=>process.stdout.write(`${text}\n`),
3811
+ stderr: (text)=>process.stderr.write(`${text}\n`),
3812
+ cwd: process.cwd(),
3813
+ env: process.env,
3814
+ interactive: process.stdin.isTTY && process.stdout.isTTY
3815
+ };
3816
+ }
3817
+ function answer(value) {
3818
+ if (__rspack_external__clack_prompts_d2d5c36d.isCancel(value)) throw new Cancelled();
3819
+ return value;
3820
+ }
3821
+ const clackPrompter = {
3822
+ text: async ({ message, initialValue, validate })=>answer(await __rspack_external__clack_prompts_d2d5c36d.text({
3823
+ message,
3824
+ ...void 0 === initialValue ? {} : {
3825
+ initialValue
3826
+ },
3827
+ ...validate ? {
3828
+ validate: (value)=>validate(value ?? '')
3829
+ } : {}
3830
+ })),
3831
+ multiselect: async ({ message, choices, initialValues })=>answer(await __rspack_external__clack_prompts_d2d5c36d.multiselect({
3832
+ message,
3833
+ options: choices.map((c)=>({
3834
+ value: c.value,
3835
+ label: c.label,
3836
+ ...void 0 === c.hint ? {} : {
3837
+ hint: c.hint
3838
+ }
3839
+ })),
3840
+ initialValues: [
3841
+ ...initialValues
3842
+ ],
3843
+ required: false
3844
+ })),
3845
+ select: async ({ message, choices, initialValue })=>answer(await __rspack_external__clack_prompts_d2d5c36d.select({
3846
+ message,
3847
+ options: choices.map((c)=>({
3848
+ value: c.value,
3849
+ label: c.label,
3850
+ ...void 0 === c.hint ? {} : {
3851
+ hint: c.hint
3852
+ }
3853
+ })),
3854
+ ...void 0 === initialValue ? {} : {
3855
+ initialValue
3856
+ }
3857
+ })),
3858
+ confirm: async ({ message, initialValue })=>answer(await __rspack_external__clack_prompts_d2d5c36d.confirm({
3859
+ message,
3860
+ initialValue: initialValue ?? true
3861
+ }))
3862
+ };
3863
+ const REPO_VARIABLES = [
3864
+ 'GIT_DIR',
3865
+ 'GIT_WORK_TREE',
3866
+ 'GIT_INDEX_FILE',
3867
+ 'GIT_COMMON_DIR',
3868
+ 'GIT_OBJECT_DIRECTORY',
3869
+ 'GIT_ALTERNATE_OBJECT_DIRECTORIES',
3870
+ 'GIT_PREFIX',
3871
+ 'GIT_NAMESPACE'
3872
+ ];
3873
+ function isolatedEnv(env = process.env) {
3874
+ const copy = {
3875
+ ...env
3876
+ };
3877
+ for (const name of REPO_VARIABLES)delete copy[name];
3878
+ return copy;
3879
+ }
3880
+ const run = (command, args, options)=>new Promise((resolve, reject)=>{
3881
+ const child = spawn(command, args, {
3882
+ cwd: options.cwd,
3883
+ env: isolatedEnv(),
3884
+ stdio: [
3885
+ 'ignore',
3886
+ 'pipe',
3887
+ 'pipe'
3888
+ ],
3889
+ shell: 'win32' === process.platform && !node_path.isAbsolute(command)
3890
+ });
3891
+ let output = '';
3892
+ const collect = (chunk)=>{
3893
+ const text = chunk.toString('utf8');
3894
+ output += text;
3895
+ options.onOutput?.(text);
3896
+ };
3897
+ child.stdout.on('data', collect);
3898
+ child.stderr.on('data', collect);
3899
+ child.on('error', reject);
3900
+ child.on('close', (code)=>{
3901
+ resolve({
3902
+ code: code ?? 1,
3903
+ output
3904
+ });
3905
+ });
3906
+ });
3907
+ const TAIL_LINES = 15;
3908
+ function installArgs(packageManager) {
3909
+ return 'pnpm' === packageManager ? [
3910
+ 'install',
3911
+ '--no-frozen-lockfile'
3912
+ ] : [
3913
+ 'install'
3914
+ ];
3915
+ }
3916
+ function createSystem(io, runner = run) {
3917
+ return {
3918
+ async install (root, packageManager, verbose) {
3919
+ let result;
3920
+ try {
3921
+ result = await runner(packageManager, installArgs(packageManager), {
3922
+ cwd: root,
3923
+ ...verbose ? {
3924
+ onOutput: (chunk)=>io.stderr(chunk.trimEnd())
3925
+ } : {}
3926
+ });
3927
+ } catch (error) {
3928
+ throw new Error(`could not start ${packageManager} (${messageOf(error)}); install it, for example with \`corepack enable\``, {
3929
+ cause: error
3930
+ });
3931
+ }
3932
+ if (0 !== result.code) {
3933
+ const tail = result.output.trimEnd().split('\n').slice(-TAIL_LINES).join('\n');
3934
+ throw new Error(`${packageManager} install exited with ${String(result.code)}\n${tail}`);
3935
+ }
3936
+ },
3937
+ async gitInit (root) {
3938
+ try {
3939
+ const inside = await runner('git', [
3940
+ 'rev-parse',
3941
+ '--is-inside-work-tree'
3942
+ ], {
3943
+ cwd: root
3944
+ });
3945
+ if (0 === inside.code) return false;
3946
+ return 0 === (await runner('git', [
3947
+ 'init',
3948
+ '--quiet'
3949
+ ], {
3950
+ cwd: root
3951
+ })).code;
3952
+ } catch {
3953
+ return false;
3954
+ }
3955
+ }
3956
+ };
3957
+ }
3958
+ function realRuntime(io = processIo()) {
3959
+ return {
3960
+ io,
3961
+ prompter: clackPrompter,
3962
+ system: createSystem(io),
3963
+ fetch: (url, init)=>fetch(url, init),
3964
+ roots: assetRoots(),
3965
+ dev: realDevHost()
3966
+ };
3967
+ }
3968
+ const COMMANDS = [
3969
+ {
3970
+ name: 'init',
3971
+ summary: 'create a new project: a shell and remotes',
3972
+ usage: 'mfeflow init [dir] [options]',
3973
+ args: INIT_ARGS,
3974
+ examples: [
3975
+ 'mfeflow init my-app',
3976
+ 'mfeflow init my-app --yes',
3977
+ 'mfeflow init my-app --scope @acme --remotes catalog,cart,checkout --features lint-oxlint,git-hooks',
3978
+ 'mfeflow init my-app --dry-run # print the diff, write nothing',
3979
+ 'mfeflow init my-app --yes --local ~/code/mfeflow # link a local checkout'
3980
+ ],
3981
+ run: runInit
3982
+ },
3983
+ {
3984
+ name: 'add',
3985
+ summary: 'add a remote, a shell, features or a typed env key to this project',
3986
+ usage: 'mfeflow add remote <name> | mfeflow add shell <name> | mfeflow add <feature...> | mfeflow add env-key <KEY> --scope server|runtime --schema <expr>',
3987
+ args: ADD_ARGS,
3988
+ examples: [
3989
+ 'mfeflow add remote checkout',
3990
+ 'mfeflow add remote checkout --port 4320',
3991
+ 'mfeflow add shell admin',
3992
+ 'mfeflow add git-hooks',
3993
+ 'mfeflow add @someone/analytics-segment --yes',
3994
+ 'mfeflow add url-factory --dry-run',
3995
+ 'mfeflow add env-t3 --option env-t3.validator=valibot',
3996
+ 'mfeflow add env-key API_ORIGIN --scope server --schema "z.url()"',
3997
+ 'mfeflow add env-key GTM_ID --scope runtime --schema "z.string().min(1)" --app shell'
3998
+ ],
3999
+ run: runAdd
4000
+ },
4001
+ {
4002
+ name: 'features',
4003
+ summary: 'list the features you can add',
4004
+ usage: 'mfeflow features [search] [options]',
4005
+ args: FEATURES_ARGS,
4006
+ examples: [
4007
+ 'mfeflow features',
4008
+ 'mfeflow features lint',
4009
+ 'mfeflow features --json'
4010
+ ],
4011
+ run: runFeatures
4012
+ },
4013
+ {
4014
+ name: 'doctor',
4015
+ summary: 'check singletons, compatibility, config and drift; --fix repairs the safe ones',
4016
+ usage: 'mfeflow doctor [options]',
4017
+ args: DOCTOR_ARGS,
4018
+ examples: [
4019
+ 'mfeflow doctor',
4020
+ 'mfeflow doctor --fix',
4021
+ 'mfeflow doctor --json',
4022
+ 'mfeflow doctor --fix --dry-run'
4023
+ ],
4024
+ run: runDoctor
4025
+ },
4026
+ {
4027
+ name: 'validate',
4028
+ summary: 'check mfeflow.json, every mfe.json and every registry entry against their schemas',
4029
+ usage: 'mfeflow validate [options]',
4030
+ args: VALIDATE_ARGS,
4031
+ examples: [
4032
+ 'mfeflow validate',
4033
+ 'mfeflow validate --json'
4034
+ ],
4035
+ run: runValidate
4036
+ },
4037
+ {
4038
+ name: 'check-compat',
4039
+ summary: "check each remote's shared ranges against the shells it runs in",
4040
+ usage: 'mfeflow check-compat [options]',
4041
+ args: COMPAT_ARGS,
4042
+ examples: [
4043
+ 'mfeflow check-compat',
4044
+ 'mfeflow check-compat --json'
4045
+ ],
4046
+ run: runCheckCompat
4047
+ },
4048
+ {
4049
+ name: 'budget',
4050
+ summary: 'measure the built JS of the shell and each remote against the cost-model budget',
4051
+ usage: 'mfeflow budget [options]',
4052
+ args: BUDGET_ARGS,
4053
+ examples: [
4054
+ 'mfeflow budget',
4055
+ 'mfeflow budget --json',
4056
+ 'mfeflow budget --dist build'
4057
+ ],
4058
+ run: runBudget
4059
+ },
4060
+ {
4061
+ name: 'dev',
4062
+ summary: 'run the shell and every remote together, with a dev registry that follows the project',
4063
+ usage: 'mfeflow dev [options]',
4064
+ args: DEV_ARGS,
4065
+ examples: [
4066
+ 'mfeflow dev',
4067
+ 'mfeflow dev --only shell,catalog',
4068
+ 'mfeflow dev --remote checkout=http://localhost:3001',
4069
+ 'mfeflow dev --remote checkout=https://dev.example.com --allow-origin https://dev.example.com',
4070
+ 'mfeflow dev --dry-run'
4071
+ ],
4072
+ run: runDev
4073
+ }
4074
+ ];
4075
+ function findCommand(name) {
4076
+ return COMMANDS.find((c)=>c.name === name);
4077
+ }
4078
+ function toCitty(command, capture, rt) {
4079
+ return defineCommand({
4080
+ meta: {
4081
+ name: command.name,
4082
+ description: command.summary
4083
+ },
4084
+ args: command.args,
4085
+ async run ({ args, rawArgs }) {
4086
+ capture(await command.run(rt, args, rawArgs));
4087
+ }
4088
+ });
4089
+ }
4090
+ function failure(error) {
4091
+ return {
4092
+ ok: false,
4093
+ error: reportOf(error),
4094
+ exitCode: exitCodeFor(error)
4095
+ };
4096
+ }
4097
+ function fail(error, rt, json, verbose) {
4098
+ if (error instanceof Cancelled) {
4099
+ rt.io.stderr('Cancelled. Nothing was written.');
4100
+ return EXIT.cancelled;
4101
+ }
4102
+ const mfe = error instanceof Error && 'CLIError' === error.name ? new MfeflowError('E107', stripVTControlCharacters(error.message)) : error;
4103
+ if (isMfeflowError(mfe)) {
4104
+ if (json) rt.io.stdout(JSON.stringify(failure(mfe), null, 2));
4105
+ else rt.io.stderr(mfe.format());
4106
+ if (verbose && void 0 !== mfe.cause) rt.io.stderr(unexpectedText(mfe.cause, true));
4107
+ return exitCodeFor(mfe);
4108
+ }
4109
+ if (json) {
4110
+ const failed = {
4111
+ ok: false,
4112
+ error: unexpectedReport(error),
4113
+ exitCode: EXIT.unexpected
4114
+ };
4115
+ rt.io.stdout(JSON.stringify(failed, null, 2));
4116
+ } else rt.io.stderr(unexpectedText(error, verbose));
4117
+ return EXIT.unexpected;
4118
+ }
4119
+ async function main(argv, rt = realRuntime()) {
4120
+ const json = argv.includes('--json');
4121
+ const verbose = argv.includes('--verbose');
4122
+ try {
4123
+ const [first] = argv;
4124
+ if ('--version' === first || '-v' === first) {
4125
+ rt.io.stdout(`mfeflow ${readPackageJson().version}`);
4126
+ return EXIT.ok;
4127
+ }
4128
+ if (void 0 === first || 'help' === first || '--help' === first || '-h' === first) {
4129
+ rt.io.stdout(rootHelp(COMMANDS));
4130
+ return EXIT.ok;
4131
+ }
4132
+ const command = findCommand(first);
4133
+ if (!command) {
4134
+ const names = COMMANDS.map((c)=>c.name).join(', ');
4135
+ throw new MfeflowError('E107', first.startsWith('-') ? `${first} comes before the command; write mfeflow <command> [options] (commands: ${names})` : `unknown command "${first}" (commands: ${names})`);
4136
+ }
4137
+ const rest = argv.slice(1);
4138
+ if (rest.includes('--help') || rest.includes('-h')) {
4139
+ rt.io.stdout(commandHelp(command));
4140
+ return EXIT.ok;
4141
+ }
4142
+ let result;
4143
+ await runCommand(toCitty(command, (r)=>{
4144
+ result = r;
4145
+ }, rt), {
4146
+ rawArgs: rest
4147
+ });
4148
+ if (json && result) rt.io.stdout(JSON.stringify(result, null, 2));
4149
+ return result ? exitOf(result) : EXIT.ok;
4150
+ } catch (error) {
4151
+ return fail(error, rt, json, verbose);
4152
+ }
4153
+ }
4154
+ export { COMMANDS, EXIT, exitCodeFor, main, realRuntime, validateResult };