mfeflow 0.4.0 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (82) hide show
  1. package/README.md +14 -5
  2. package/assets/features/analytics-gtm/feature.json +1 -1
  3. package/assets/features/ci-github/feature.json +1 -1
  4. package/assets/features/env-t3/feature.json +1 -1
  5. package/assets/features/lint-oxlint/feature.json +1 -1
  6. package/assets/features/registry-server/feature.json +128 -0
  7. package/assets/features/registry-server/files/deploy-server.yml +69 -0
  8. package/assets/features/registry-server/files/registry-server.md.eta +52 -0
  9. package/assets/features/registry-server/files/registry-sources.ts +12 -0
  10. package/assets/features/registry-static/feature.json +61 -0
  11. package/assets/features/registry-static/files/deploy.yml +61 -0
  12. package/assets/features/registry-static/files/registry.md.eta +49 -0
  13. package/assets/features/release/feature.json +43 -0
  14. package/assets/features/release/files/changeset-readme.md +7 -0
  15. package/assets/features/release/files/config.json +10 -0
  16. package/assets/features/release/files/releasing.md +21 -0
  17. package/assets/features/testing/feature.json +1 -1
  18. package/assets/features/tsconfig/feature.json +1 -1
  19. package/assets/features/url-factory/feature.json +1 -1
  20. package/assets/templates/monorepo/feature.json +13 -6
  21. package/assets/templates/monorepo/files/remote/{mfe.json.eta → mfeflow.app.json.eta} +1 -0
  22. package/assets/templates/monorepo/files/root/README.md.eta +16 -1
  23. package/assets/templates/monorepo/files/shell/{mfe.json.eta → mfeflow.app.json.eta} +1 -0
  24. package/assets/templates/monorepo/files/shell/rsbuild.config.ts.eta +42 -6
  25. package/assets/templates/monorepo/files/shell/src/assert.ts.eta +37 -0
  26. package/assets/templates/monorepo/files/shell/src/bootstrap.tsx.eta +12 -2
  27. package/assets/templates/monorepo/migrations/0.4.0-app-file.mjs +58 -0
  28. package/assets/templates/polyrepo/feature.json +305 -0
  29. package/assets/templates/polyrepo/files/AGENTS.md.eta +15 -0
  30. package/assets/templates/polyrepo/files/changeset/README.md +5 -0
  31. package/assets/templates/polyrepo/files/changeset/config.json +9 -0
  32. package/assets/templates/polyrepo/files/packages/contracts/package.json.eta +23 -0
  33. package/assets/templates/polyrepo/files/packages/contracts/src/index.ts +11 -0
  34. package/assets/templates/polyrepo/files/packages/contracts/tsconfig.json.eta +5 -0
  35. package/assets/templates/polyrepo/files/packages/tokens/package.json.eta +23 -0
  36. package/assets/templates/polyrepo/files/packages/tokens/src/index.ts +13 -0
  37. package/assets/templates/polyrepo/files/packages/tokens/tsconfig.json.eta +5 -0
  38. package/assets/templates/polyrepo/files/platform/README.md.eta +43 -0
  39. package/assets/templates/polyrepo/files/platform/default.json.eta +27 -0
  40. package/assets/templates/polyrepo/files/remote/README.md.eta +23 -0
  41. package/assets/templates/polyrepo/files/renovate.json.eta +4 -0
  42. package/assets/templates/polyrepo/files/shell/README.md.eta +32 -0
  43. package/assets/templates/polyrepo/files/tsconfig.base.json.eta +13 -0
  44. package/assets/templates/polyrepo/files/workflows/platform-ci.yml +29 -0
  45. package/assets/templates/polyrepo/files/workflows/release.yml +40 -0
  46. package/assets/templates/polyrepo/files/workflows/reusable-ci.yml.eta +50 -0
  47. package/assets/templates/polyrepo/files/workflows/team-ci.yml.eta +12 -0
  48. package/dist/against.d.ts +16 -0
  49. package/dist/catalog.d.ts +8 -0
  50. package/dist/checks/compat.d.ts +12 -0
  51. package/dist/checks/index.d.ts +0 -1
  52. package/dist/checks/singletons.d.ts +2 -5
  53. package/dist/commands/add.d.ts +4 -2
  54. package/dist/commands/adopt.d.ts +54 -0
  55. package/dist/commands/deploy.d.ts +262 -0
  56. package/dist/commands/doctor.d.ts +5 -0
  57. package/dist/commands/init.d.ts +9 -1
  58. package/dist/commands/server-mode.d.ts +210 -0
  59. package/dist/commands/status.d.ts +63 -0
  60. package/dist/commands/team-repo.d.ts +9 -0
  61. package/dist/commands/upgrade.d.ts +66 -0
  62. package/dist/deploy-testing.d.ts +41 -0
  63. package/dist/devregistry.d.ts +4 -5
  64. package/dist/devremotes.d.ts +15 -0
  65. package/dist/exit.d.ts +2 -1
  66. package/dist/glue.d.ts +6 -0
  67. package/dist/hosts.d.ts +25 -0
  68. package/dist/index.d.ts +1 -1
  69. package/dist/index.js +5477 -1643
  70. package/dist/inspect.d.ts +14 -2
  71. package/dist/output.d.ts +214 -7
  72. package/dist/pipeline.d.ts +13 -2
  73. package/dist/polyrepo.d.ts +69 -0
  74. package/dist/publish.d.ts +98 -0
  75. package/dist/runtime.d.ts +10 -1
  76. package/dist/server-client.d.ts +61 -0
  77. package/dist/system.d.ts +2 -0
  78. package/dist/testing.d.ts +82 -0
  79. package/dist/trust.d.ts +4 -0
  80. package/dist/upgrade.d.ts +25 -0
  81. package/dist/write.d.ts +8 -0
  82. package/package.json +5 -4
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "core",
3
- "version": "0.1.0",
3
+ "version": "0.5.0",
4
4
  "description": "The shell, the remotes, the registry and the workspace around them.",
5
5
  "files": [
6
6
  {
@@ -67,9 +67,9 @@
67
67
  "template": true
68
68
  },
69
69
  {
70
- "to": "mfe.json",
70
+ "to": "mfeflow.app.json",
71
71
  "target": "shell",
72
- "from": "files/shell/mfe.json.eta",
72
+ "from": "files/shell/mfeflow.app.json.eta",
73
73
  "template": true
74
74
  },
75
75
  {
@@ -96,6 +96,12 @@
96
96
  "from": "files/shell/src/bootstrap.tsx.eta",
97
97
  "template": true
98
98
  },
99
+ {
100
+ "to": "src/assert.ts",
101
+ "target": "shell",
102
+ "from": "files/shell/src/assert.ts.eta",
103
+ "template": true
104
+ },
99
105
  {
100
106
  "to": "src/App.tsx",
101
107
  "target": "shell",
@@ -115,9 +121,9 @@
115
121
  "template": true
116
122
  },
117
123
  {
118
- "to": "mfe.json",
124
+ "to": "mfeflow.app.json",
119
125
  "target": "remotes",
120
- "from": "files/remote/mfe.json.eta",
126
+ "from": "files/remote/mfeflow.app.json.eta",
121
127
  "template": true
122
128
  },
123
129
  {
@@ -317,5 +323,6 @@
317
323
  "command": "rsbuild build",
318
324
  "target": "remotes"
319
325
  }
320
- ]
326
+ ],
327
+ "migrations": [{ "from": "0.3.0", "to": "0.4.0", "module": "migrations/0.4.0-app-file.mjs" }]
321
328
  }
@@ -1,4 +1,5 @@
1
1
  {
2
+ "$schema": "https://mfeflow.github.io/mfeflow/schema/mfeflow-app.json",
2
3
  "name": "<%= it.app.name %>",
3
4
  "owner": "team-<%= it.app.name %>",
4
5
  "shells": ["shell"],
@@ -36,7 +36,7 @@ pnpm mfeflow add <feature> # see `pnpm mfeflow features`
36
36
 
37
37
  ```sh
38
38
  pnpm doctor # singletons, compatibility, config, drift; `pnpm doctor --fix` repairs the safe ones
39
- pnpm validate # mfeflow.json, every mfe.json and every registry entry
39
+ pnpm validate # mfeflow.json, every mfeflow.app.json and every registry entry
40
40
  pnpm check-compat # each remote's shared ranges against its shell
41
41
  pnpm budget # after `pnpm build`: first-load JS per shell and remote
42
42
  ```
@@ -45,6 +45,21 @@ Each is a root script for the `mfeflow` CLI in your devDependencies (`pnpm mfefl
45
45
  against the 10 KB a remote may add; set `budget.baselineKb` in `mfeflow.json` (your pages built as one app) to check
46
46
  the shell against baseline + 32 KB too.
47
47
 
48
+ ## Deploy it
49
+
50
+ ```sh
51
+ pnpm build
52
+ pnpm mfeflow deploy catalog --env staging --host file:./site --base-url https://cdn.example.com
53
+ pnpm mfeflow promote catalog --from staging --to prod --host file:./site
54
+ pnpm mfeflow rollback catalog --env prod --host file:./site
55
+ pnpm mfeflow status --host file:./site
56
+ ```
57
+
58
+ `deploy` uploads a remote's `dist` and points an environment at it, `rollback` points it back without a rebuild, and
59
+ `status` shows what is served. Put `host` and `baseUrl` under `registry` in `mfeflow.json` to drop the flags, or use
60
+ `s3://<bucket>` with the `MFEFLOW_S3_*` and `AWS_*` variables. There are no root scripts for these, because the package manager has a deploy command of its own, so
61
+ run them through `pnpm mfeflow`.
62
+
48
63
  ## Before you deploy
49
64
 
50
65
  `registry/envs/staging` and `registry/envs/prod` point at `example.com`. Replace the hosts
@@ -1,4 +1,5 @@
1
1
  {
2
+ "$schema": "https://mfeflow.github.io/mfeflow/schema/mfeflow-app.json",
2
3
  "name": "<%= it.app.name %>",
3
4
  "owner": "team-<%= it.app.name %>",
4
5
  "shells": [],
@@ -3,7 +3,40 @@ import path from 'node:path';
3
3
  import { defineConfig, mergeRsbuildConfig } from '@rsbuild/core';
4
4
  import { createShellConfig } from '@mfeflow/rsbuild';
5
5
 
6
- interface DevEntry {
6
+ <% if (it.topology === 'polyrepo') { %>interface DevRemote {
7
+ name: string;
8
+ entry: string;
9
+ version: string;
10
+ }
11
+
12
+ function readDevRemotes(file: string): DevRemote[] {
13
+ let value: unknown;
14
+ try {
15
+ value = JSON.parse(fs.readFileSync(file, 'utf8'));
16
+ } catch (error) {
17
+ // A shell with no dev config yet has no remotes to list.
18
+ if (error instanceof Error && 'code' in error && error.code === 'ENOENT') return [];
19
+ throw error;
20
+ }
21
+ const remotes =
22
+ typeof value === 'object' && value !== null && 'devRemotes' in value ? value.devRemotes : {};
23
+ if (typeof remotes !== 'object' || remotes === null) {
24
+ throw new Error(`${file}: devRemotes must be an object of name: manifest URL`);
25
+ }
26
+ return Object.entries(remotes)
27
+ .toSorted(([a], [b]) => (a < b ? -1 : 1))
28
+ .map(([name, entry]) => {
29
+ if (typeof entry !== 'string') throw new Error(`${file}: devRemotes.${name} must be a URL`);
30
+ return { name, entry, version: 'dev' };
31
+ });
32
+ }
33
+
34
+ // The shell finds its remotes through the registry. The remotes live in other repos, so in dev
35
+ // their manifest URLs come from "devRemotes" in public/config/runtime-config.dev.json, inlined so
36
+ // the shell needs no registry server. `mfeflow dev` writes the merged registry (that file, plus
37
+ // --remote overrides) to MFEFLOW_DEV_REGISTRY; plain `pnpm dev` reads the file.
38
+ const devConfig = path.resolve(process.cwd(), 'public/config/runtime-config.dev.json');
39
+ <% } else { %>interface DevEntry {
7
40
  manifestUrl: string;
8
41
  version: string;
9
42
  }
@@ -34,7 +67,7 @@ function readNames(dir: string): string[] {
34
67
  // `mfeflow dev` writes the merged registry (apps from mfeflow.json, --remote overrides) to
35
68
  // MFEFLOW_DEV_REGISTRY and rewrites it when the project changes; plain `pnpm dev` reads the folder.
36
69
  const registryDir = path.resolve(process.cwd(), '../../registry/envs/dev');
37
- const generated = process.env.MFEFLOW_DEV_REGISTRY;
70
+ <% } %>const generated = process.env.MFEFLOW_DEV_REGISTRY;
38
71
 
39
72
  function readGenerated(file: string): unknown {
40
73
  return JSON.parse(fs.readFileSync(file, 'utf8'));
@@ -44,7 +77,9 @@ function devRegistry(): string {
44
77
  const remotes = generated
45
78
  ? readGenerated(generated)
46
79
  : {
47
- remotes: readNames(registryDir)
80
+ <% if (it.topology === 'polyrepo') { %> remotes: readDevRemotes(devConfig),
81
+ };
82
+ <% } else { %> remotes: readNames(registryDir)
48
83
  .filter((file) => file.endsWith('.json'))
49
84
  .toSorted()
50
85
  .map((file) => {
@@ -52,7 +87,7 @@ function devRegistry(): string {
52
87
  return { name: file.slice(0, -'.json'.length), entry: manifestUrl, version };
53
88
  }),
54
89
  };
55
- return `window.__MFE_REGISTRY__=${JSON.stringify(remotes).replaceAll('<', '\\u003c')}`;
90
+ <% } %> return `window.__MFE_REGISTRY__=${JSON.stringify(remotes).replaceAll('<', '\\u003c')}`;
56
91
  }
57
92
 
58
93
  export default defineConfig(({ command }) =>
@@ -61,8 +96,9 @@ export default defineConfig(({ command }) =>
61
96
  createShellConfig({ name: '<%= it.app.name %>', port: <%= it.app.port %>, snapshot: true }),
62
97
  defineConfig({
63
98
  // A new or changed remote restarts the dev server, which reads the registry again.
64
- dev: { watchFiles: { paths: generated ?? registryDir, type: 'restart' } },
65
- html: {
99
+ <% if (it.topology === 'polyrepo') { %> dev: { watchFiles: { paths: generated ?? devConfig, type: 'restart' } },
100
+ <% } else { %> dev: { watchFiles: { paths: generated ?? registryDir, type: 'restart' } },
101
+ <% } %> html: {
66
102
  title: '<%= it.app.name %>',
67
103
  tags:
68
104
  command === 'dev'
@@ -0,0 +1,37 @@
1
+ /* oxlint-disable eslint/no-underscore-dangle -- __MFEFLOW_SINGLETONS__ is the name the rsbuild preset defines */
2
+ import { checkSingletons, reportRemoteError } from '@mfeflow/runtime';
3
+
4
+ // The shell's rsbuild preset defines the packages it shares; an older preset does not, so fall back.
5
+ declare const __MFEFLOW_SINGLETONS__: readonly string[] | undefined;
6
+ const SINGLETONS =
7
+ typeof __MFEFLOW_SINGLETONS__ === 'undefined' ? ['react', 'react-dom'] : __MFEFLOW_SINGLETONS__;
8
+
9
+ // Remotes register their shared packages as they load, so the check repeats for a while.
10
+ const CHECK_EVERY_MS = 3000;
11
+ const STOP_AFTER_MS = 60_000;
12
+
13
+ declare global {
14
+ var mfeflowCheckSingletons: (() => ReturnType<typeof checkSingletons>) | undefined;
15
+ }
16
+
17
+ const reported = new Set<string>();
18
+
19
+ function check(): ReturnType<typeof checkSingletons> {
20
+ const result = checkSingletons(SINGLETONS);
21
+ // `missing` is not a problem: a shared package nothing has loaded yet is not in the share scope.
22
+ for (const name of result.duplicated) {
23
+ const problem = `${name} is loaded in more than one version`;
24
+ if (reported.has(problem)) continue;
25
+ reported.add(problem);
26
+ reportRemoteError('<%= it.app.name %>', 'singletons', new Error(problem));
27
+ }
28
+ return result;
29
+ }
30
+
31
+ // Loaded only in development: it reads every loaded remote's share scope.
32
+ globalThis.mfeflowCheckSingletons = check;
33
+ check();
34
+ const timer = setInterval(check, CHECK_EVERY_MS);
35
+ setTimeout(() => {
36
+ clearInterval(timer);
37
+ }, STOP_AFTER_MS);
@@ -3,6 +3,7 @@ import {
3
3
  applyDevOverrides,
4
4
  loadRegistryIndex,
5
5
  registerRemotesFromIndex,
6
+ registrySources,
6
7
  reportRemoteError,
7
8
  } from '@mfeflow/runtime';
8
9
  import { App } from './App';
@@ -12,13 +13,22 @@ const DEV = process.env.NODE_ENV !== 'production';
12
13
  export async function main(): Promise<void> {
13
14
  try {
14
15
  // Dev only: ?mfe-override=cart@http://localhost:4312/mf-manifest.json points a remote at your build.
15
- const index = applyDevOverrides(await loadRegistryIndex(), { enabled: DEV });
16
+ // runtime-config.json can set registryUrls (primary, then fallback) and registryOrigins for a CDN-hosted registry;
17
+ // it never holds up boot for more than 1.5 s, and is not fetched at all when the index is inlined.
18
+ const { urls, allowedOrigins: cdnOrigins } = await registrySources();
19
+ const index = applyDevOverrides(await loadRegistryIndex(urls, { allowedOrigins: cdnOrigins }), {
20
+ enabled: DEV,
21
+ });
16
22
  // In dev the registry points at the remotes' dev servers; elsewhere remotes share the shell's origin.
17
23
  const devOrigins = DEV ? index.remotes.map((remote) => new URL(remote.entry).origin) : [];
18
- registerRemotesFromIndex(index, { allowedOrigins: [location.origin, ...devOrigins] });
24
+ registerRemotesFromIndex(index, {
25
+ allowedOrigins: [location.origin, ...cdnOrigins, ...devOrigins],
26
+ });
19
27
  } catch (error) {
20
28
  reportRemoteError('<%= it.app.name %>', 'registry', error);
21
29
  }
22
30
  const el = document.getElementById('root');
23
31
  if (el) createRoot(el).render(<App />);
32
+ // Development only, so production builds carry nothing for it.
33
+ if (DEV) void import('./assert');
24
34
  }
@@ -0,0 +1,58 @@
1
+ // core 0.4.0: each app's ownership file is now mfeflow.app.json, not mfe.json.
2
+ // Moves the content of every app's mfe.json to mfeflow.app.json, with the schema URL updated,
3
+ // and deletes mfe.json. An app that already has both files is left alone with a warning (E122).
4
+ // Self-contained on purpose: `mfeflow upgrade` imports it from the feature folder.
5
+
6
+ const OLD = 'mfe.json';
7
+ const NEW = 'mfeflow.app.json';
8
+ const OLD_SCHEMA = 'https://mfeflow.github.io/mfeflow/schema/mfe.json';
9
+ const NEW_SCHEMA = 'https://mfeflow.github.io/mfeflow/schema/mfeflow-app.json';
10
+
11
+ const inApp = (dir, file) => (dir === '' ? file : `${dir}/${file}`);
12
+
13
+ /** The file text with the new schema URL: the old URL replaced, or a `$schema` line added first. */
14
+ export function withNewSchema(text) {
15
+ if (text.includes(OLD_SCHEMA)) return text.replaceAll(OLD_SCHEMA, NEW_SCHEMA);
16
+ if (text.includes('"$schema"')) return text;
17
+ const open = /^\{(\r?\n)/.exec(text);
18
+ if (!open) return text;
19
+ return `{${open[1]} "$schema": "${NEW_SCHEMA}",${open[1]}${text.slice(open[0].length)}`;
20
+ }
21
+
22
+ /**
23
+ * @param {import('@mfeflow/core').PlanBuilder} builder
24
+ * @param {import('@mfeflow/core').MfeflowProject} project
25
+ */
26
+ export default async function migrate(builder, project) {
27
+ const dirs = Object.values(project.apps)
28
+ .map((app) => app.path)
29
+ .toSorted();
30
+ for (const dir of dirs) {
31
+ const from = inApp(dir, OLD);
32
+ const to = inApp(dir, NEW);
33
+ // oxlint-disable-next-line no-await-in-loop -- apps in order keep the plan deterministic
34
+ const text = await builder.content(from);
35
+ if (text === undefined) continue;
36
+ // oxlint-disable-next-line no-await-in-loop -- see above
37
+ if (await builder.existed(to)) {
38
+ builder.warn(
39
+ `${dir || '.'} has both ${OLD} and ${NEW}; keep ${NEW} and delete ${OLD} (E122)`,
40
+ );
41
+ continue;
42
+ }
43
+ const moved = withNewSchema(text);
44
+ // The upgrade may already plan a fresh NEW file from the template. When the old file was
45
+ // never edited the two match and that tracked file stays. Otherwise your content wins, and
46
+ // it is not tracked, so a later upgrade shows a diff instead of replacing it.
47
+ // oxlint-disable-next-line no-await-in-loop -- see above
48
+ const planned = await builder.content(to);
49
+ if (planned !== moved) {
50
+ // oxlint-disable-next-line no-await-in-loop -- see above
51
+ if (planned !== undefined) await builder.delete(to);
52
+ // oxlint-disable-next-line no-await-in-loop -- see above
53
+ await builder.create(to, moved, false);
54
+ }
55
+ // oxlint-disable-next-line no-await-in-loop -- see above
56
+ await builder.delete(from);
57
+ }
58
+ }
@@ -0,0 +1,305 @@
1
+ {
2
+ "name": "polyrepo",
3
+ "version": "0.2.0",
4
+ "description": "What a polyrepo repo has beyond the monorepo template: the platform packages, the reusable CI and Renovate preset, and a package.json, README and CI caller for each repo.",
5
+ "prompts": [
6
+ {
7
+ "name": "repo",
8
+ "type": "select",
9
+ "message": "Which repo of the polyrepo this is",
10
+ "choices": ["platform", "shell", "remote"],
11
+ "default": "platform"
12
+ },
13
+ {
14
+ "name": "name",
15
+ "type": "string",
16
+ "message": "The remote's name (repo remote only)",
17
+ "default": ""
18
+ },
19
+ {
20
+ "name": "org",
21
+ "type": "string",
22
+ "message": "The GitHub owner the repos will live under (default: the scope without @)",
23
+ "default": ""
24
+ }
25
+ ],
26
+ "files": [
27
+ {
28
+ "to": "AGENTS.md",
29
+ "target": "root",
30
+ "from": "files/AGENTS.md.eta",
31
+ "template": true
32
+ },
33
+ {
34
+ "to": "renovate.json",
35
+ "target": "root",
36
+ "from": "files/renovate.json.eta",
37
+ "template": true
38
+ },
39
+ {
40
+ "to": "README.md",
41
+ "target": "root",
42
+ "from": "files/platform/README.md.eta",
43
+ "template": true,
44
+ "when": {
45
+ "repo": "platform"
46
+ }
47
+ },
48
+ {
49
+ "to": "README.md",
50
+ "target": "root",
51
+ "from": "files/shell/README.md.eta",
52
+ "template": true,
53
+ "when": {
54
+ "repo": "shell"
55
+ }
56
+ },
57
+ {
58
+ "to": "README.md",
59
+ "target": "root",
60
+ "from": "files/remote/README.md.eta",
61
+ "template": true,
62
+ "when": {
63
+ "repo": "remote"
64
+ }
65
+ },
66
+ {
67
+ "to": "default.json",
68
+ "target": "root",
69
+ "from": "files/platform/default.json.eta",
70
+ "template": true,
71
+ "when": {
72
+ "repo": "platform"
73
+ }
74
+ },
75
+ {
76
+ "to": "tsconfig.base.json",
77
+ "target": "root",
78
+ "from": "files/tsconfig.base.json.eta",
79
+ "template": true,
80
+ "when": {
81
+ "repo": "platform"
82
+ }
83
+ },
84
+ {
85
+ "to": "packages/contracts/package.json",
86
+ "target": "root",
87
+ "from": "files/packages/contracts/package.json.eta",
88
+ "template": true,
89
+ "when": {
90
+ "repo": "platform"
91
+ }
92
+ },
93
+ {
94
+ "to": "packages/contracts/tsconfig.json",
95
+ "target": "root",
96
+ "from": "files/packages/contracts/tsconfig.json.eta",
97
+ "template": true,
98
+ "when": {
99
+ "repo": "platform"
100
+ }
101
+ },
102
+ {
103
+ "to": "packages/contracts/src/index.ts",
104
+ "target": "root",
105
+ "from": "files/packages/contracts/src/index.ts",
106
+ "when": {
107
+ "repo": "platform"
108
+ }
109
+ },
110
+ {
111
+ "to": "packages/tokens/package.json",
112
+ "target": "root",
113
+ "from": "files/packages/tokens/package.json.eta",
114
+ "template": true,
115
+ "when": {
116
+ "repo": "platform"
117
+ }
118
+ },
119
+ {
120
+ "to": "packages/tokens/tsconfig.json",
121
+ "target": "root",
122
+ "from": "files/packages/tokens/tsconfig.json.eta",
123
+ "template": true,
124
+ "when": {
125
+ "repo": "platform"
126
+ }
127
+ },
128
+ {
129
+ "to": "packages/tokens/src/index.ts",
130
+ "target": "root",
131
+ "from": "files/packages/tokens/src/index.ts",
132
+ "when": {
133
+ "repo": "platform"
134
+ }
135
+ },
136
+ {
137
+ "to": ".changeset/config.json",
138
+ "target": "root",
139
+ "from": "files/changeset/config.json",
140
+ "when": {
141
+ "repo": "platform"
142
+ }
143
+ },
144
+ {
145
+ "to": ".changeset/README.md",
146
+ "target": "root",
147
+ "from": "files/changeset/README.md",
148
+ "when": {
149
+ "repo": "platform"
150
+ }
151
+ },
152
+ {
153
+ "to": ".github/workflows/ci.yml",
154
+ "target": "root",
155
+ "from": "files/workflows/platform-ci.yml",
156
+ "when": {
157
+ "repo": "platform"
158
+ }
159
+ },
160
+ {
161
+ "to": ".github/workflows/reusable-ci.yml",
162
+ "target": "root",
163
+ "from": "files/workflows/reusable-ci.yml.eta",
164
+ "template": true,
165
+ "when": {
166
+ "repo": "platform"
167
+ }
168
+ },
169
+ {
170
+ "to": ".github/workflows/release.yml",
171
+ "target": "root",
172
+ "from": "files/workflows/release.yml",
173
+ "when": {
174
+ "repo": "platform"
175
+ }
176
+ },
177
+ {
178
+ "to": ".github/workflows/ci.yml",
179
+ "target": "root",
180
+ "from": "files/workflows/team-ci.yml.eta",
181
+ "template": true,
182
+ "when": {
183
+ "repo": "shell"
184
+ }
185
+ },
186
+ {
187
+ "to": ".github/workflows/ci.yml",
188
+ "target": "root",
189
+ "from": "files/workflows/team-ci.yml.eta",
190
+ "template": true,
191
+ "when": {
192
+ "repo": "remote"
193
+ }
194
+ },
195
+ {
196
+ "to": "public/config/runtime-config.staging.json",
197
+ "target": "shell",
198
+ "content": "{\n \"environment\": \"staging\"\n}\n"
199
+ },
200
+ {
201
+ "to": "public/config/runtime-config.prod.json",
202
+ "target": "shell",
203
+ "content": "{\n \"environment\": \"prod\"\n}\n"
204
+ }
205
+ ],
206
+ "dependencies": [
207
+ {
208
+ "name": "@changesets/cli",
209
+ "range": "3.0.3",
210
+ "target": "root",
211
+ "dev": true,
212
+ "when": {
213
+ "repo": "platform"
214
+ }
215
+ },
216
+ {
217
+ "name": "typescript",
218
+ "range": "7.0.2",
219
+ "target": "root",
220
+ "dev": true,
221
+ "when": {
222
+ "repo": "platform"
223
+ }
224
+ },
225
+ {
226
+ "name": "mfeflow",
227
+ "range": "latest",
228
+ "target": "root",
229
+ "dev": true,
230
+ "when": {
231
+ "repo": "platform"
232
+ }
233
+ },
234
+ {
235
+ "name": "{{scope}}/contracts",
236
+ "range": "latest",
237
+ "target": "shell"
238
+ },
239
+ {
240
+ "name": "{{scope}}/tokens",
241
+ "range": "latest",
242
+ "target": "shell"
243
+ },
244
+ {
245
+ "name": "{{scope}}/contracts",
246
+ "range": "latest",
247
+ "target": "remotes"
248
+ },
249
+ {
250
+ "name": "{{scope}}/tokens",
251
+ "range": "latest",
252
+ "target": "remotes"
253
+ }
254
+ ],
255
+ "scripts": [
256
+ {
257
+ "name": "build",
258
+ "command": "pnpm -r --if-present run build",
259
+ "target": "root",
260
+ "when": {
261
+ "repo": "platform"
262
+ }
263
+ },
264
+ {
265
+ "name": "typecheck",
266
+ "command": "pnpm -r --if-present run typecheck",
267
+ "target": "root",
268
+ "when": {
269
+ "repo": "platform"
270
+ }
271
+ },
272
+ {
273
+ "name": "validate",
274
+ "command": "mfeflow validate",
275
+ "target": "root",
276
+ "when": {
277
+ "repo": "platform"
278
+ }
279
+ },
280
+ {
281
+ "name": "changeset",
282
+ "command": "changeset",
283
+ "target": "root",
284
+ "when": {
285
+ "repo": "platform"
286
+ }
287
+ },
288
+ {
289
+ "name": "version",
290
+ "command": "changeset version",
291
+ "target": "root",
292
+ "when": {
293
+ "repo": "platform"
294
+ }
295
+ },
296
+ {
297
+ "name": "release",
298
+ "command": "pnpm build && changeset publish",
299
+ "target": "root",
300
+ "when": {
301
+ "repo": "platform"
302
+ }
303
+ }
304
+ ]
305
+ }
@@ -0,0 +1,15 @@
1
+ # AGENTS.md
2
+
3
+ Guidance for coding agents working in this repo.
4
+
5
+ - This is the <%= it.options.repo %> repo of a polyrepo. The other repos are its siblings: `../platform`, `../shell` and one per team.
6
+ - Package manager: pnpm (via corepack). Never npm or yarn. Versions live in the `catalog` of
7
+ `pnpm-workspace.yaml`; add dependencies there and reference them as `catalog:`.
8
+ <% if (it.options.repo === 'platform') { %>- Every change to a package in `packages/` needs a changeset (`pnpm changeset`).
9
+ - Never import from a sibling repo by path. Other repos depend on what is published from here.
10
+ <% } else { %>- `<%= it.scope %>/*` packages come from `../platform` (`link:` paths until they are published). Build the platform repo first.
11
+ - TypeScript 7, strict. Relative imports have no file extension (`./plan`, not `./plan.js`).
12
+ - Add features with the `mfeflow` CLI (`pnpm mfeflow add <feature>`). `mfeflow.json` is its record of this repo.
13
+ <% } %><% if (it.options.repo === 'remote') { %>- A remote's public surface is `src/exposed/`. It must not import from the shell or another remote.
14
+ <% } %><% if (it.options.repo === 'shell') { %>- A new remote gets a repo with `pnpm mfeflow add team-repo <name>`. Do not add remotes to this repo by hand.
15
+ <% } %>EOF
@@ -0,0 +1,5 @@
1
+ # Changesets
2
+
3
+ Run `pnpm changeset` in any pull request that changes a package here. Pick the bump and describe
4
+ the change for the teams that use it. When the pull request merges, the release workflow opens a
5
+ "chore: version packages" pull request. Merging that one publishes the packages.
@@ -0,0 +1,9 @@
1
+ {
2
+ "$schema": "https://unpkg.com/@changesets/config/schema.json",
3
+ "changelog": "@changesets/cli/changelog",
4
+ "commit": false,
5
+ "access": "public",
6
+ "baseBranch": "main",
7
+ "updateInternalDependencies": "patch",
8
+ "ignore": []
9
+ }
@@ -0,0 +1,23 @@
1
+ {
2
+ "name": "<%= it.scope %>/contracts",
3
+ "version": "0.1.0",
4
+ "description": "Event names and types the shell and every remote agree on.",
5
+ "type": "module",
6
+ "files": ["dist"],
7
+ "exports": {
8
+ ".": {
9
+ "types": "./dist/index.d.ts",
10
+ "import": "./dist/index.js"
11
+ }
12
+ },
13
+ "publishConfig": {
14
+ "access": "public"
15
+ },
16
+ "scripts": {
17
+ "build": "tsc",
18
+ "typecheck": "tsc --noEmit"
19
+ },
20
+ "devDependencies": {
21
+ "typescript": "catalog:"
22
+ }
23
+ }