mfeflow 0.6.0 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (28) hide show
  1. package/assets/features/deploy-cloudflare/feature.json +85 -0
  2. package/assets/features/deploy-cloudflare/files/cloudflare.json.eta +13 -0
  3. package/assets/features/deploy-cloudflare/files/cloudflare.md.eta +104 -0
  4. package/assets/features/deploy-cloudflare/files/deploy-cloudflare.ts +318 -0
  5. package/assets/features/deploy-cloudflare/files/deploy-cloudflare.yml +111 -0
  6. package/assets/features/deploy-cloudflare/files/headers.remote +14 -0
  7. package/assets/features/deploy-cloudflare/files/headers.shell.eta +18 -0
  8. package/assets/features/deploy-cloudflare/files/r2-cors.json +10 -0
  9. package/assets/features/deploy-cloudflare/files/registry-proxy.ts +80 -0
  10. package/assets/features/deploy-cloudflare/files/wrangler.remote.jsonc.eta +21 -0
  11. package/assets/features/deploy-cloudflare/files/wrangler.shell.jsonc.eta +40 -0
  12. package/assets/features/deploy-docker/feature.json +22 -0
  13. package/assets/features/deploy-docker/files/40-mfe-config.sh +76 -0
  14. package/assets/features/deploy-docker/files/Dockerfile +48 -0
  15. package/assets/features/deploy-docker/files/compose-registry.json.eta +19 -0
  16. package/assets/features/deploy-docker/files/compose.yaml.eta +50 -0
  17. package/assets/features/deploy-docker/files/docker.md +52 -0
  18. package/assets/features/deploy-docker/files/dockerignore +17 -0
  19. package/assets/features/deploy-docker/files/k8s.yaml +114 -0
  20. package/assets/features/deploy-docker/files/nginx.remote.conf +73 -0
  21. package/assets/features/deploy-docker/files/nginx.shell.conf +84 -0
  22. package/assets/templates/monorepo/feature.json +1 -1
  23. package/assets/templates/monorepo/files/root/pnpm-workspace.yaml.eta +3 -2
  24. package/dist/commands/doctor.d.ts +5 -0
  25. package/dist/index.js +80 -21
  26. package/dist/output.d.ts +1 -1
  27. package/dist/server-client.d.ts +8 -0
  28. package/package.json +5 -5
@@ -0,0 +1,84 @@
1
+ # The shell. Every response says how long it may be cached: revalidate by default, a year only for
2
+ # files whose name changes when their content does (/static/). nginx drops the headers of the
3
+ # level above as soon as a location sets its own, so each location repeats Cache-Control.
4
+
5
+ # The stock mime.types has no .mjs. This block sits at http level, where it adds to the stock
6
+ # list; inside a server block a types block would replace the whole list.
7
+ types {
8
+ text/javascript mjs;
9
+ }
10
+
11
+ # add_header is not inherited by a location that sets its own, so every location repeats the
12
+ # security headers beside Cache-Control.
13
+ gzip on;
14
+ gzip_vary on;
15
+ gzip_min_length 256;
16
+ gzip_types text/css text/javascript application/javascript application/json image/svg+xml;
17
+ # This image has no brotli module. Let a CDN or ingress do brotli if you want it.
18
+
19
+ server {
20
+ listen 8080;
21
+ server_name _;
22
+ server_tokens off;
23
+ root /usr/share/nginx/html;
24
+
25
+ location = /healthz {
26
+ access_log off;
27
+ default_type text/plain;
28
+ return 200 "ok\n";
29
+ }
30
+
31
+ # Dotfiles (.env, .git) are never served. .well-known stays reachable.
32
+ location ~ /\.(?!well-known) { return 404; }
33
+
34
+ # Written by the start script (or mounted as a ConfigMap) so the image holds no environment.
35
+ location = /config/runtime-config.json {
36
+ alias /tmp/mfe-runtime/runtime-config.json;
37
+ default_type application/json;
38
+ add_header Cache-Control "no-cache" always;
39
+ add_header X-Content-Type-Options "nosniff" always;
40
+ add_header Referrer-Policy "strict-origin-when-cross-origin" always;
41
+ add_header X-Frame-Options "SAMEORIGIN" always;
42
+ }
43
+
44
+ # Mounted, never baked in: a ConfigMap in Kubernetes, a file in Docker, or leave it out and point
45
+ # registryUrls in the runtime config at a registry you host elsewhere.
46
+ location = /registry/index.json {
47
+ alias /tmp/mfe-registry/index.json;
48
+ default_type application/json;
49
+ add_header Cache-Control "no-cache" always;
50
+ add_header X-Content-Type-Options "nosniff" always;
51
+ add_header Referrer-Policy "strict-origin-when-cross-origin" always;
52
+ add_header X-Frame-Options "SAMEORIGIN" always;
53
+ }
54
+
55
+ location ^~ /config/ { return 404; }
56
+ location ^~ /registry/ { return 404; }
57
+
58
+ # Hashed build output.
59
+ location ^~ /static/ {
60
+ try_files $uri =404;
61
+ add_header Cache-Control "public, max-age=31536000, immutable";
62
+ add_header X-Content-Type-Options "nosniff" always;
63
+ add_header Referrer-Policy "strict-origin-when-cross-origin" always;
64
+ add_header X-Frame-Options "SAMEORIGIN" always;
65
+ }
66
+
67
+ # Any other file (favicon, mf-manifest.json): a missing one is a real 404, never index.html.
68
+ location ~ \.[A-Za-z0-9]+$ {
69
+ try_files $uri =404;
70
+ add_header Cache-Control "no-cache" always;
71
+ add_header X-Content-Type-Options "nosniff" always;
72
+ add_header Referrer-Policy "strict-origin-when-cross-origin" always;
73
+ add_header X-Frame-Options "SAMEORIGIN" always;
74
+ }
75
+
76
+ # Routes. The fallback to index.html lives here and nowhere else.
77
+ location / {
78
+ try_files $uri /index.html;
79
+ add_header Cache-Control "no-cache" always;
80
+ add_header X-Content-Type-Options "nosniff" always;
81
+ add_header Referrer-Policy "strict-origin-when-cross-origin" always;
82
+ add_header X-Frame-Options "SAMEORIGIN" always;
83
+ }
84
+ }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "core",
3
- "version": "0.5.0",
3
+ "version": "0.5.1",
4
4
  "description": "The shell, the remotes, the registry and the workspace around them.",
5
5
  "files": [
6
6
  {
@@ -2,8 +2,9 @@ packages:
2
2
  - apps/*
3
3
  - packages/*
4
4
 
5
- # Dependencies allowed to run install scripts. esbuild (pulled in by Storybook) works without its
6
- # script; listing it as false keeps pnpm from stopping the install to ask.
5
+ # Dependencies allowed to run install scripts. esbuild (pulled in by Storybook) and workerd (pulled in
6
+ # by wrangler) work without their script; listing them as false keeps pnpm from stopping the install to ask.
7
7
  allowBuilds:
8
8
  lefthook: true
9
9
  esbuild: false
10
+ workerd: false
@@ -43,6 +43,11 @@ export declare const DOCTOR_ARGS: {
43
43
  readonly alias: 'h';
44
44
  readonly description: 'Show this help';
45
45
  };
46
+ readonly against: {
47
+ readonly type: 'string';
48
+ readonly description: 'Check against the shell a registry index or a deployed mf-manifest.json describes (file or https URL)';
49
+ readonly valueHint: 'url|file';
50
+ };
46
51
  readonly fix: {
47
52
  readonly type: 'boolean';
48
53
  readonly description: 'Repair what is safe: missing workspace versions and registry entries';
package/dist/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import { stripVTControlCharacters } from "node:util";
2
- import { COST_MODEL, DOCS_URL, MFE_NAME, REGISTRY_NAMESPACE, ROUTE_OVERLAP, isLoopbackHttp, isRegistryProtocol, isSafeRelativePath, judgeCost, judgeRemoteInitial, primaryExpose, redactUrl, redactUrlsIn, registryIndexUrl, registryTemplateProblem, remoteDistFiles, round1, routesOverlap, validateMfeManifest, validateRegistryEntry, validateRegistryIndex } from "@mfeflow/contracts";
3
- import { APP_FILE, LEGACY_APP_FILE, MfeflowError, PROJECT_FILE, PlanBuilder, TRUST_ENV, addTrusted, applyPlan, buildRegistry, checkIntegrity, configDirOf, detectFederation, dotenvKeys, fetchFeatureIndex, findDiverged, findProjectRoot, getHttps, hasErrno, inOrder, isMfeflowError, legacyAppFileNotice, loadFeature, loadProject, messageOf, parseFeature, parseTrustEnv, planAdoption, planUpgrade, readAppFile, readCappedText, readTrusted, realFs, recordPlan, removeTrusted, renderDiff, renderTemplate, resolveFeature, runtimeConfigFiles, saveProject, schemaKeys, serializeProject, summarize, symlinkedPrefix, trustedNamespaces, writeFileAtomic } from "@mfeflow/core";
2
+ import { COST_MODEL, DOCS_URL, ENV_KEY_NAME, MFE_NAME, REGISTRY_NAMESPACE, ROUTE_OVERLAP, isLoopbackHttp, isRegistryProtocol, isSafeRelativePath, judgeCost, judgeRemoteInitial, primaryExpose, redactUrl, redactUrlsIn, registryIndexUrl, registryTemplateProblem, remoteDistFiles, round1, routesOverlap, validateMfeManifest, validateRegistryEntry, validateRegistryIndex } from "@mfeflow/contracts";
3
+ import { APP_FILE, LEGACY_APP_FILE, MfeflowError, PROJECT_FILE, PlanBuilder, TRUST_ENV, addTrusted, applyPlan, buildRegistry, checkIntegrity, configDirOf, detectFederation, dotenvKeys, fetchFeatureIndex, findDiverged, findProjectRoot, getHttps, hasErrno, inOrder, isMfeflowError, legacyAppFileNotice, loadFeature, loadProject, messageOf, parseFeature, parseTrustEnv, planAdoption, planUpgrade, readAppFile, readCappedText, readTrusted, realFs, recordEnvKey, recordPlan, removeTrusted, renderDiff, renderTemplate, resolveFeature, runtimeConfigFiles, saveProject, schemaKeys, serializeProject, summarize, symlinkedPrefix, trustedNamespaces, writeFileAtomic } from "@mfeflow/core";
4
4
  import { defineCommand, runCommand } from "citty";
5
5
  import node_path from "node:path";
6
6
  import promises from "node:fs/promises";
@@ -242,6 +242,16 @@ const FIRST_PARTY = [
242
242
  name: 'registry-server',
243
243
  always: false,
244
244
  default: false
245
+ },
246
+ {
247
+ name: 'deploy-cloudflare',
248
+ always: false,
249
+ default: false
250
+ },
251
+ {
252
+ name: 'deploy-docker',
253
+ always: false,
254
+ default: false
245
255
  }
246
256
  ];
247
257
  function isFirstParty(name) {
@@ -1379,7 +1389,6 @@ async function writePlan(request) {
1379
1389
  installed: installing
1380
1390
  };
1381
1391
  }
1382
- const KEY = /^[A-Za-z_][A-Za-z0-9_]*$/;
1383
1392
  const GROUPS = [
1384
1393
  'root',
1385
1394
  'shell',
@@ -1460,7 +1469,7 @@ function envKeyOp(input) {
1460
1469
  async function addEnvKey(rt, globals, args, key) {
1461
1470
  const scope = args_text(args, 'scope');
1462
1471
  const schema = args_text(args, 'schema');
1463
- 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>');
1472
+ if (void 0 === key || !ENV_KEY_NAME.test(key)) throw new MfeflowError('E107', 'add env-key needs a key like API_ORIGIN: mfeflow add env-key <KEY>');
1464
1473
  if ('server' !== scope && 'runtime' !== scope) throw new MfeflowError('E107', '--scope must be server or runtime');
1465
1474
  if (void 0 === schema) throw new MfeflowError('E107', '--schema is required, for example --schema "z.url()"');
1466
1475
  const root = await findProjectRoot(globals.cwd);
@@ -1501,6 +1510,7 @@ async function addEnvKey(rt, globals, args, key) {
1501
1510
  ...builder.build(),
1502
1511
  features: []
1503
1512
  };
1513
+ const recorded = recordEnvKey(project, plan.changes.map((change)=>change.path), key);
1504
1514
  const outcome = await writePlan({
1505
1515
  rt,
1506
1516
  globals: {
@@ -1509,7 +1519,7 @@ async function addEnvKey(rt, globals, args, key) {
1509
1519
  },
1510
1520
  root,
1511
1521
  plan,
1512
- project
1522
+ project: recorded
1513
1523
  });
1514
1524
  const result = {
1515
1525
  ok: true,
@@ -3457,7 +3467,7 @@ const singletonVersions = {
3457
3467
  }
3458
3468
  };
3459
3469
  const ABSENT_SHELL_WARNING = 'shell is not in this repo, pass --against <url|file>';
3460
- const ABSENT_UNDER_DOCTOR = 'shell is not in this repo, run `mfeflow check-compat --against <url|file>`';
3470
+ const ABSENT_UNDER_DOCTOR = 'shell is not in this repo, run `mfeflow doctor --against <url|file>`';
3461
3471
  async function versionsOf(app, names, root) {
3462
3472
  const found = await Promise.all(names.map(async (name)=>[
3463
3473
  name,
@@ -4069,22 +4079,26 @@ const DOCTOR_CHECKS = [
4069
4079
  runtimePlugins,
4070
4080
  ...envChecks
4071
4081
  ];
4082
+ const AGAINST_ARG = {
4083
+ against: {
4084
+ type: 'string',
4085
+ description: 'Check against the shell a registry index or a deployed mf-manifest.json describes (file or https URL)',
4086
+ valueHint: 'url|file'
4087
+ }
4088
+ };
4072
4089
  const DOCTOR_ARGS = {
4073
4090
  fix: {
4074
4091
  type: 'boolean',
4075
4092
  description: 'Repair what is safe: missing workspace versions and registry entries'
4076
4093
  },
4094
+ ...AGAINST_ARG,
4077
4095
  ...GLOBAL_ARGS
4078
4096
  };
4079
4097
  const VALIDATE_ARGS = {
4080
4098
  ...GLOBAL_ARGS
4081
4099
  };
4082
4100
  const COMPAT_ARGS = {
4083
- against: {
4084
- type: 'string',
4085
- description: 'Check against the shell a registry index or a deployed mf-manifest.json describes (file or https URL)',
4086
- valueHint: 'url|file'
4087
- },
4101
+ ...AGAINST_ARG,
4088
4102
  ...GLOBAL_ARGS
4089
4103
  };
4090
4104
  const DEFAULT_VERSION = '0.0.0';
@@ -4199,7 +4213,17 @@ async function runGroup(rt, args, raw, defs, command, checks, fix) {
4199
4213
  };
4200
4214
  }
4201
4215
  function runDoctor(rt, args, raw) {
4202
- return runGroup(rt, args, raw, DOCTOR_ARGS, 'doctor', DOCTOR_CHECKS, true === args.fix);
4216
+ const against = 'string' == typeof args.against ? args.against : void 0;
4217
+ return runGroup(rt, args, raw, DOCTOR_ARGS, 'doctor', void 0 === against ? DOCTOR_CHECKS : async (globals)=>{
4218
+ const shell = await loadShell(against, {
4219
+ cwd: globals.cwd,
4220
+ fetch: rt.fetch
4221
+ });
4222
+ return DOCTOR_CHECKS.map((check)=>check.id === compat.id ? compatCheck(shell, 'doctor') : check);
4223
+ }, true === args.fix).then((result)=>void 0 === against ? result : {
4224
+ ...result,
4225
+ against: redactUrl(against)
4226
+ });
4203
4227
  }
4204
4228
  function runValidate(rt, args, raw) {
4205
4229
  return runGroup(rt, args, raw, VALIDATE_ARGS, 'validate', VALIDATE_CHECKS, false);
@@ -4893,6 +4917,40 @@ class ServerClient {
4893
4917
  scrubAll(value) {
4894
4918
  return JSON.parse(this.scrub(JSON.stringify(value)));
4895
4919
  }
4920
+ scrubError(error, seen = new WeakSet()) {
4921
+ if ('string' == typeof error) return this.scrub(error);
4922
+ if ('object' != typeof error || null === error) return error;
4923
+ if (seen.has(error)) return error;
4924
+ seen.add(error);
4925
+ if (!(error instanceof Error)) try {
4926
+ return this.scrubAll(error);
4927
+ } catch {
4928
+ return this.scrub(Object.prototype.toString.call(error));
4929
+ }
4930
+ error.message = this.scrub(error.message);
4931
+ if (void 0 !== error.stack) error.stack = this.scrub(error.stack);
4932
+ if (error instanceof MfeflowError) error.fix = this.scrub(error.fix);
4933
+ if (void 0 !== error.cause) error.cause = this.scrubError(error.cause, seen);
4934
+ if (error instanceof AggregateError) error.errors = error.errors.map((inner)=>this.scrubError(inner, seen));
4935
+ return error;
4936
+ }
4937
+ async guarded(rt, work) {
4938
+ const prompter = {
4939
+ ...rt.prompter,
4940
+ confirm: (options)=>rt.prompter.confirm({
4941
+ ...options,
4942
+ message: this.scrub(options.message)
4943
+ })
4944
+ };
4945
+ try {
4946
+ return this.scrubAll(await work({
4947
+ ...rt,
4948
+ prompter
4949
+ }));
4950
+ } catch (error) {
4951
+ throw this.scrubError(error);
4952
+ }
4953
+ }
4896
4954
  get hasToken() {
4897
4955
  return void 0 !== this.token;
4898
4956
  }
@@ -5651,24 +5709,24 @@ async function killInner(rt, args, raw, restore, client) {
5651
5709
  }
5652
5710
  const rate = (c)=>`${String(c.loads)} loads, ${String(c.errors)} errors`;
5653
5711
  async function deployViaServer(rt, args, raw, defs, client) {
5654
- return client.scrubAll(await deployInner(rt, args, raw, defs, client));
5712
+ return client.guarded(rt, (guarded)=>deployInner(guarded, args, raw, defs, client));
5655
5713
  }
5656
5714
  async function promoteViaServer(rt, args, raw, defs, client) {
5657
- return client.scrubAll(await promoteInner(rt, args, raw, defs, client));
5715
+ return client.guarded(rt, (guarded)=>promoteInner(guarded, args, raw, defs, client));
5658
5716
  }
5659
5717
  async function rollbackViaServer(rt, args, raw, defs, client) {
5660
- return client.scrubAll(await rollbackInner(rt, args, raw, defs, client));
5718
+ return client.guarded(rt, (guarded)=>rollbackInner(guarded, args, raw, defs, client));
5661
5719
  }
5662
5720
  async function statusViaServer(rt, args, raw, defs, client) {
5663
- return client.scrubAll(await statusInner(rt, args, raw, defs, client));
5721
+ return client.guarded(rt, (guarded)=>statusInner(guarded, args, raw, defs, client));
5664
5722
  }
5665
5723
  async function runRolloutCommand(rt, args, raw, client) {
5666
- const result = await rolloutInner(rt, args, raw, client);
5667
- return void 0 === client ? result : client.scrubAll(result);
5724
+ if (void 0 === client) return rolloutInner(rt, args, raw, client);
5725
+ return client.guarded(rt, (guarded)=>rolloutInner(guarded, args, raw, client));
5668
5726
  }
5669
5727
  async function runKillCommand(rt, args, raw, restore, client) {
5670
- const result = await killInner(rt, args, raw, restore, client);
5671
- return void 0 === client ? result : client.scrubAll(result);
5728
+ if (void 0 === client) return killInner(rt, args, raw, restore, client);
5729
+ return client.guarded(rt, (guarded)=>killInner(guarded, args, raw, restore, client));
5672
5730
  }
5673
5731
  function formatServerAction(result) {
5674
5732
  if ('rollout' === result.command) {
@@ -8108,7 +8166,8 @@ const COMMANDS = [
8108
8166
  'mfeflow doctor',
8109
8167
  'mfeflow doctor --fix',
8110
8168
  'mfeflow doctor --json',
8111
- 'mfeflow doctor --fix --dry-run'
8169
+ 'mfeflow doctor --fix --dry-run',
8170
+ 'mfeflow doctor --against https://cdn.example.com/envs/prod/index.json'
8112
8171
  ],
8113
8172
  run: runDoctor
8114
8173
  },
package/dist/output.d.ts CHANGED
@@ -195,7 +195,7 @@ export interface ReportResult {
195
195
  summary: Summary;
196
196
  checks: CheckReport[];
197
197
  findings: Finding[];
198
- /** `check-compat --against`: where the shell's versions came from. */
198
+ /** `doctor` and `check-compat --against`: where the shell's versions came from. */
199
199
  against?: string;
200
200
  }
201
201
  export interface ShellBudget {
@@ -43,6 +43,14 @@ export declare class ServerClient {
43
43
  scrub(words: string): string;
44
44
  /** A result document with every string scrubbed. The server's words reach the output through it. */
45
45
  scrubAll<T>(value: T): T;
46
+ /**
47
+ * Whatever was thrown, with the token taken out for errors raised outside `run`: message, stack,
48
+ * `fix`, the `cause` chain and the errors of an AggregateError. Strings and plain objects are
49
+ * scrubbed too.
50
+ */
51
+ scrubError(error: unknown, seen?: WeakSet<object>): unknown;
52
+ /** Runs a whole server-mode command: whatever it prints, prompts or throws is scrubbed. */
53
+ guarded<T>(rt: Runtime, work: (rt: Runtime) => Promise<T>): Promise<T>;
46
54
  get hasToken(): boolean;
47
55
  /** Throws the catalogue error for anything that is not a 2xx reply; `gates_failed` is thrown as a ServerError. */
48
56
  get(route: string, needsToken?: boolean): Promise<unknown>;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mfeflow",
3
- "version": "0.6.0",
3
+ "version": "0.7.0",
4
4
  "description": "Create and grow a React micro-frontend platform: mfeflow init, add remote, add <feature>.",
5
5
  "keywords": [
6
6
  "micro-frontend",
@@ -49,15 +49,15 @@
49
49
  },
50
50
  "dependencies": {
51
51
  "@clack/prompts": "1.8.1",
52
- "@mfeflow/contracts": "0.6.0",
53
- "@mfeflow/core": "0.6.0",
54
- "@mfeflow/registry": "0.6.0",
52
+ "@mfeflow/contracts": "0.7.0",
53
+ "@mfeflow/core": "0.7.0",
54
+ "@mfeflow/registry": "0.7.0",
55
55
  "citty": "0.2.2",
56
56
  "semver": "7.8.5",
57
57
  "yaml": "2.9.1"
58
58
  },
59
59
  "devDependencies": {
60
- "@mfeflow/rsbuild": "0.6.0",
60
+ "@mfeflow/rsbuild": "0.7.0",
61
61
  "@rslib/core": "1.0.5",
62
62
  "@types/node": "26.6.6",
63
63
  "@types/semver": "7.8.0",