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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Sanjeev Sharma
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,127 @@
1
- # Temporary Holding Version
1
+ # mfeflow
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Part of [mfeflow](https://github.com/mfeflow/mfeflow), the React micro-frontend boilerplate and platform kit.
4
+
5
+ Pre-1.0: commands and flags may change in minor versions. To run it from a checkout of the repo, see
6
+ [Try it from a checkout](#try-it-from-a-checkout).
7
+
8
+ ```sh
9
+ npm create mfeflow@latest my-app -- --yes
10
+ cd my-app
11
+ pnpm dev # shell on :4300, remotes on :4311, :4312
12
+ ```
13
+
14
+ ## Commands
15
+
16
+ | Command | Does |
17
+ | ------------------------------------- | ------------------------------------------------------------------------------------ |
18
+ | `mfeflow init [dir]` | New project: a shell, remotes (default `catalog`, `cart`), pnpm workspace, Turborepo |
19
+ | `mfeflow add remote <name>` | New remote with its registry entries and a route in every shell |
20
+ | `mfeflow add shell <name>` | New shell that routes to every existing remote |
21
+ | `mfeflow add <feature...>` | Add features, or `@namespace/feature` from a registry in `mfeflow.json` |
22
+ | `mfeflow add env-key <KEY>` | Add a typed env key: `--scope server` (env.ts) or `runtime`, with `--schema <expr>` |
23
+ | `mfeflow features [search]` | List the features you can add |
24
+ | `mfeflow dev` | Run the shell and every remote, one prefixed log each; follows `add remote` |
25
+ | `mfeflow doctor` | Singletons, compatibility, config health, drift; `--fix` repairs the safe ones |
26
+ | `mfeflow validate` | `mfeflow.json`, every `mfe.json` and every registry entry against their schemas |
27
+ | `mfeflow check-compat` | Each remote's shared ranges against the shells it runs in |
28
+ | `mfeflow budget` | Built first-load JS of the shell and each remote against the cost-model budget |
29
+ | `mfeflow --version`, `mfeflow --help` | Version, and help with examples (`mfeflow <command> --help` for one command) |
30
+
31
+ Features that ship with it: `core` and `tsconfig` (always), `lint-oxlint`, `url-factory`, `env-t3` and `testing`
32
+ (default), `ci-github` and `git-hooks` (opt-in). `init` takes `--scope`, `--remotes a,b`, `--features a,b` and
33
+ `--option feature.key=value`; every prompt has a flag.
34
+
35
+ ## dev, doctor and the CI checks
36
+
37
+ `mfeflow dev` starts every app with `pnpm run dev` in its folder and writes the dev registry the shell reads to
38
+ `.mfeflow/dev-registry.json` (apps from `mfeflow.json`, entries from `registry/envs/dev`). When `mfeflow.json` or
39
+ `registry/envs/dev` changes it rewrites the file, the shell restarts itself, and a new remote is started. `--only a,b`
40
+ runs part of the project; `--remote name=url` uses a server you run yourself (the name must be a remote; the URL must be on
41
+ localhost unless `--allow-origin <origin>` names it); Ctrl+C stops every app, process trees included, on Windows too.
42
+ It exits 130 after Ctrl+C.
43
+
44
+ `doctor` runs these checks, each with a code and a docs page (`https://mfeflow.github.io/mfeflow/errors/<code>`). It exits with 0
45
+ when there are no errors, else with the lowest exit code among the errors' ranges (project 3 before build 5).
46
+
47
+ | Check | Code | Fixed by `--fix` |
48
+ | ------------------------------------- | -------------- | ----------------------------- |
49
+ | every app in mfeflow.json is on disk | E112 | |
50
+ | valid `mfe.json`, name matches | E113, E116 | |
51
+ | no shared port | E115 | |
52
+ | no route claimed twice | E116 | |
53
+ | registry entry per remote and env | E114 | writes the missing ones |
54
+ | linked checkout is built | E110 | |
55
+ | one version of each singleton | E301 | |
56
+ | every workspace package has a version | E111 | adds `0.0.0` |
57
+ | remote ranges fit the shell | E302 | |
58
+ | copied files match their hash | E117 (warning) | |
59
+ | runtime plugins import nothing shared | E303 | |
60
+ | `.env.example` matches `env.ts` | E118 (warning) | skipped until `env-t3` exists |
61
+ | runtime-config files match the schema | E119 | |
62
+ | no `PUBLIC_*` variable but constants | E120 (warning) | |
63
+ | server keys listed in `turbo.json` | E121 (warning) | |
64
+
65
+ `validate` runs the schema and registry checks, `check-compat` the compatibility check, `budget` reads the build output
66
+ (`dist/`): each remote's entry against `budget.remoteKb` (10), a route against `mfe.json` `budgets.initialJsKb`, and the
67
+ shell against `budget.baselineKb + budget.shellKb` (32) when a baseline is set. All four take `--json` and exit non-zero on
68
+ errors. LCP is measured by the perf job, not by `budget`.
69
+
70
+ ## Flags on every command
71
+
72
+ | Flag | Does |
73
+ | ---------------------- | --------------------------------------------------------------------------------------------------- |
74
+ | `--yes`, `-y` | Take the defaults, never prompt |
75
+ | `--preset <file\|url>` | Replay options from a JSON file or https URL (`scope`, `remotes`, `features`, `options`); flags win |
76
+ | `--json` | One JSON document on stdout, nothing else; errors come as `{ ok: false, error }` |
77
+ | `--dry-run` | Print the summary and a unified diff, write nothing, run nothing |
78
+ | `--cwd <dir>` | Run as if started in that folder |
79
+ | `--no-install` | Do not run the package manager |
80
+ | `--verbose` | Show the diff, package manager output and stack traces |
81
+ | `--local <path>` | Link `@mfeflow/*` from a built checkout (env `MFEFLOW_LOCAL`) |
82
+
83
+ Every write is a plan first: the summary (and with `--dry-run` the diff) is shown, then the files are
84
+ written all or nothing, and the install runs inside the same transaction. If the install fails, the
85
+ files are put back.
86
+
87
+ ## Errors and exit codes
88
+
89
+ Errors print the code, one line of fix and a docs link:
90
+
91
+ ```
92
+ MFEFLOW_E108 The target folder is not empty: /work/app already has notes.txt
93
+ Fix: Pick a new or empty folder, or move what is in the way.
94
+ https://mfeflow.github.io/mfeflow/errors/E108
95
+ ```
96
+
97
+ | Exit | Meaning |
98
+ | ---- | ----------------------------------------------------------------------- |
99
+ | 0 | success |
100
+ | 1 | unexpected error (`--verbose` prints the stack) |
101
+ | 2 | bad command line: `MFEFLOW_E107`, `MFEFLOW_E215` |
102
+ | 3 | project or config problem: `MFEFLOW_E1xx` |
103
+ | 4 | feature problem: `MFEFLOW_E2xx` |
104
+ | 5 | build or runtime problem: `MFEFLOW_E3xx` |
105
+ | 6 | registry problem: `MFEFLOW_E4xx` |
106
+ | 7 | upgrade problem: `MFEFLOW_E5xx` |
107
+ | 130 | cancelled at a prompt (nothing was written), or Ctrl+C in `mfeflow dev` |
108
+
109
+ ## Try it from a checkout
110
+
111
+ Before `@mfeflow/*` is on npm, `--local` writes the dependencies as `link:` to the packages of a clone.
112
+ Without it, they are `^<the CLI's version>`.
113
+
114
+ ```sh
115
+ git clone https://github.com/mfeflow/mfeflow && cd mfeflow
116
+ pnpm install && pnpm build
117
+
118
+ node packages/cli/bin/mfeflow.js init ../my-app --yes --local .
119
+ cd ../my-app
120
+ pnpm dev # http://localhost:4300 renders the shell with both remotes
121
+ ```
122
+
123
+ `--local` needs the checkout built (`pnpm build`); it says so (`MFEFLOW_E110`) if a package has no `dist/`.
124
+ `MFEFLOW_LOCAL=/path/to/mfeflow` does the same for `init`.
125
+ The checkout is recorded in `mfeflow.json` as `"local"` (a path relative to the project), so later `mfeflow add` commands keep writing `link:` without the flag. `--local none` removes it; dependencies already written stay as they are.
126
+
127
+ MIT licensed.
@@ -0,0 +1,117 @@
1
+ {
2
+ "name": "analytics-gtm",
3
+ "version": "0.1.0",
4
+ "description": "Google Tag Manager with Consent Mode v2 behind @mfeflow/analytics: consent defaults before gtm.js, a consent banner hook, page views on route changes, and a typed tracker per remote. The container id comes from the runtime config.",
5
+ "requires": ["core", "tsconfig"],
6
+ "files": [
7
+ {
8
+ "to": "src/analytics/init.ts",
9
+ "target": "shell",
10
+ "from": "files/init.ts"
11
+ },
12
+ {
13
+ "to": "src/analytics/Analytics.tsx",
14
+ "target": "shell",
15
+ "from": "files/Analytics.tsx"
16
+ },
17
+ {
18
+ "to": "src/analytics/ConsentBanner.tsx",
19
+ "target": "shell",
20
+ "from": "files/ConsentBanner.tsx"
21
+ },
22
+ {
23
+ "to": "src/analytics/init.test.ts",
24
+ "target": "shell",
25
+ "from": "files/shell-init-test.ts",
26
+ "when": {
27
+ "feature:testing": true
28
+ }
29
+ },
30
+ {
31
+ "to": "src/analytics/ConsentBanner.test.tsx",
32
+ "target": "shell",
33
+ "from": "files/consent-banner-test.tsx",
34
+ "when": {
35
+ "feature:testing": true
36
+ }
37
+ },
38
+ {
39
+ "to": "src/analytics.ts",
40
+ "target": "remotes",
41
+ "from": "files/remote-analytics.ts.eta",
42
+ "template": true
43
+ },
44
+ {
45
+ "to": "src/analytics.test.ts",
46
+ "target": "remotes",
47
+ "from": "files/remote-analytics-test.ts.eta",
48
+ "template": true,
49
+ "when": {
50
+ "feature:testing": true
51
+ }
52
+ }
53
+ ],
54
+ "dependencies": [
55
+ {
56
+ "name": "@mfeflow/analytics",
57
+ "range": "latest",
58
+ "target": "shell"
59
+ },
60
+ {
61
+ "name": "@mfeflow/analytics",
62
+ "range": "latest",
63
+ "target": "remotes"
64
+ }
65
+ ],
66
+ "transforms": [
67
+ {
68
+ "op": "add-boot-step",
69
+ "file": "src/bootstrap.tsx",
70
+ "target": "shell",
71
+ "call": "initAnalyticsFromConfig",
72
+ "from": "./analytics/init",
73
+ "await": false
74
+ },
75
+ {
76
+ "op": "wrap-provider",
77
+ "file": "src/App.tsx",
78
+ "target": "shell",
79
+ "component": "Analytics",
80
+ "from": "./analytics/Analytics",
81
+ "function": "App"
82
+ },
83
+ {
84
+ "op": "add-env-key",
85
+ "file": "src/config/schema.ts",
86
+ "target": "shell",
87
+ "key": "gtmId",
88
+ "scope": "runtime",
89
+ "schema": {
90
+ "zod": "z.string().regex(/^(GTM-[A-Z0-9]{4,12})?$/)",
91
+ "valibot": "v.pipe(v.string(), v.regex(/^(GTM-[A-Z0-9]{4,12})?$/))",
92
+ "arktype": "'/^(GTM-[A-Z0-9]{4,12})?$/'"
93
+ },
94
+ "value": "",
95
+ "configFiles": [
96
+ "public/config/runtime-config.dev.json",
97
+ "public/config/runtime-config.staging.json",
98
+ "public/config/runtime-config.prod.json"
99
+ ],
100
+ "when": {
101
+ "feature:env-t3": true
102
+ }
103
+ },
104
+ {
105
+ "op": "add-env-key",
106
+ "file": "public/config/runtime-config.json",
107
+ "target": "shell",
108
+ "key": "gtmId",
109
+ "scope": "runtime",
110
+ "schema": "z.string().regex(/^(GTM-[A-Z0-9]{4,12})?$/)",
111
+ "value": "",
112
+ "when": {
113
+ "feature:env-t3": false
114
+ }
115
+ }
116
+ ]
117
+ }
@@ -0,0 +1,13 @@
1
+ import type { ReactNode } from 'react';
2
+ import { AnalyticsProvider } from '@mfeflow/analytics/react';
3
+ import { ConsentBanner } from './ConsentBanner';
4
+
5
+ /** Wraps the shell. Sends a page_view on every route change and shows the consent banner. */
6
+ export function Analytics({ children }: { children: ReactNode }) {
7
+ return (
8
+ <AnalyticsProvider>
9
+ {children}
10
+ <ConsentBanner />
11
+ </AnalyticsProvider>
12
+ );
13
+ }
@@ -0,0 +1,47 @@
1
+ import { useConsent } from '@mfeflow/analytics/react';
2
+
3
+ // Yours to restyle or replace. Until the visitor chooses, every consent signal stays denied, and
4
+ // each button grants only what its label says.
5
+ export function ConsentBanner() {
6
+ const { decided, update, rejectAll } = useConsent();
7
+ if (decided) return null;
8
+ return (
9
+ <section aria-label="Cookie consent" data-testid="consent-banner">
10
+ <p>
11
+ We use analytics cookies to measure how the site is used. Advertising cookies are optional
12
+ and stay off unless you allow them.
13
+ </p>
14
+ <button
15
+ type="button"
16
+ data-testid="consent-accept-analytics"
17
+ onClick={() => {
18
+ update({
19
+ analytics_storage: 'granted',
20
+ ad_storage: 'denied',
21
+ ad_user_data: 'denied',
22
+ ad_personalization: 'denied',
23
+ });
24
+ }}
25
+ >
26
+ Accept analytics
27
+ </button>
28
+ <button
29
+ type="button"
30
+ data-testid="consent-accept-ads"
31
+ onClick={() => {
32
+ update({
33
+ analytics_storage: 'granted',
34
+ ad_storage: 'granted',
35
+ ad_user_data: 'granted',
36
+ ad_personalization: 'granted',
37
+ });
38
+ }}
39
+ >
40
+ Accept analytics and advertising
41
+ </button>
42
+ <button type="button" data-testid="consent-reject" onClick={rejectAll}>
43
+ Reject
44
+ </button>
45
+ </section>
46
+ );
47
+ }
@@ -0,0 +1,36 @@
1
+ import { getConsent, resetAnalyticsForTests } from '@mfeflow/analytics';
2
+ import { cleanup, fireEvent, render, screen } from '@testing-library/react';
3
+ import { afterEach, describe, expect, it } from 'vitest';
4
+ import { ConsentBanner } from './ConsentBanner';
5
+
6
+ afterEach(() => {
7
+ cleanup();
8
+ localStorage.clear();
9
+ resetAnalyticsForTests();
10
+ });
11
+
12
+ describe('ConsentBanner', () => {
13
+ it('grants analytics only, and denies advertising, on "Accept analytics"', () => {
14
+ render(<ConsentBanner />);
15
+ fireEvent.click(screen.getByText('Accept analytics'));
16
+ expect(getConsent()).toEqual({
17
+ analytics_storage: 'granted',
18
+ ad_storage: 'denied',
19
+ ad_user_data: 'denied',
20
+ ad_personalization: 'denied',
21
+ });
22
+ expect(screen.queryByTestId('consent-banner')).toBeNull();
23
+ });
24
+
25
+ it('grants advertising too only on its own button', () => {
26
+ render(<ConsentBanner />);
27
+ fireEvent.click(screen.getByText('Accept analytics and advertising'));
28
+ expect(getConsent()).toMatchObject({ analytics_storage: 'granted', ad_storage: 'granted' });
29
+ });
30
+
31
+ it('denies everything on Reject', () => {
32
+ render(<ConsentBanner />);
33
+ fireEvent.click(screen.getByText('Reject'));
34
+ expect(Object.values(getConsent())).toEqual(['denied', 'denied', 'denied', 'denied']);
35
+ });
36
+ });
@@ -0,0 +1,26 @@
1
+ import { initAnalytics } from '@mfeflow/analytics';
2
+ import { createGtmProvider } from '@mfeflow/analytics/gtm';
3
+ import { fetchRuntimeConfig } from '@mfeflow/runtime';
4
+
5
+ /**
6
+ * Starts analytics from the runtime config's `gtmId`. With no id, analytics stays off and every
7
+ * tracker call is dropped. Safe to call twice: the first start wins.
8
+ */
9
+ export async function startAnalytics(): Promise<void> {
10
+ const config = await fetchRuntimeConfig().catch((error: unknown) => {
11
+ console.warn('[analytics] the runtime config did not load, so analytics stays off', error);
12
+ return undefined;
13
+ });
14
+ const gtmId = config?.gtmId;
15
+ if (typeof gtmId !== 'string' || gtmId === '') return;
16
+ try {
17
+ initAnalytics(createGtmProvider({ containerId: gtmId }));
18
+ } catch (error) {
19
+ console.warn('[analytics] not started', error);
20
+ }
21
+ }
22
+
23
+ /** The boot step: starts analytics without holding up first render. */
24
+ export function initAnalyticsFromConfig(): void {
25
+ void startAnalytics();
26
+ }
@@ -0,0 +1,18 @@
1
+ import { createRecordingProvider, initAnalytics, resetAnalyticsForTests } from '@mfeflow/analytics';
2
+ import { afterEach, describe, expect, it } from 'vitest';
3
+ import { tracker } from './analytics';
4
+
5
+ afterEach(resetAnalyticsForTests);
6
+
7
+ describe('the <%= it.app.name %> tracker', () => {
8
+ it('stamps its events with the remote name', () => {
9
+ const provider = createRecordingProvider();
10
+ initAnalytics(provider);
11
+ tracker.track('<%= it.app.name %>_viewed');
12
+ expect(provider.calls.at(-1)).toMatchObject({
13
+ type: 'track',
14
+ event: '<%= it.app.name %>_viewed',
15
+ params: { mfe_name: '<%= it.app.name %>', mfe_version: null },
16
+ });
17
+ });
18
+ });
@@ -0,0 +1,13 @@
1
+ import { createTracker } from '@mfeflow/analytics';
2
+
3
+ // Every event this remote sends, with its parameters. The map is merged into the platform's, so
4
+ // tracker.track('<%= it.app.name %>_viewed') is checked here and nowhere else.
5
+ declare module '@mfeflow/analytics' {
6
+ interface AnalyticsEvents {
7
+ <%= it.app.name %>_viewed: undefined;
8
+ }
9
+ }
10
+
11
+ export const events = ['<%= it.app.name %>_viewed'] as const;
12
+
13
+ export const tracker = createTracker('<%= it.app.name %>', { events });
@@ -0,0 +1,42 @@
1
+ import { resetAnalyticsForTests } from '@mfeflow/analytics';
2
+ import { afterEach, describe, expect, it } from 'vitest';
3
+ import { startAnalytics } from './init';
4
+
5
+ function dataLayer(): unknown[] | undefined {
6
+ const layer: unknown = Reflect.get(globalThis, 'dataLayer');
7
+ return Array.isArray(layer) ? layer : undefined;
8
+ }
9
+
10
+ // gtag pushes the `arguments` object, which GTM reads and an array would not be.
11
+ const argumentsOf = (entry: unknown): unknown[] =>
12
+ typeof entry === 'object' && entry !== null ? Object.values(entry) : [];
13
+
14
+ afterEach(() => {
15
+ Reflect.deleteProperty(globalThis, '__MFE_RUNTIME_CONFIG__');
16
+ Reflect.deleteProperty(globalThis, 'dataLayer');
17
+ document.head.innerHTML = '';
18
+ resetAnalyticsForTests();
19
+ });
20
+
21
+ describe('startAnalytics', () => {
22
+ it('stays off without a gtmId', async () => {
23
+ Reflect.set(globalThis, '__MFE_RUNTIME_CONFIG__', { gtmId: '' });
24
+ await startAnalytics();
25
+ expect(dataLayer()).toBeUndefined();
26
+ expect(document.querySelector('script[src*="gtm.js"]')).toBeNull();
27
+ });
28
+
29
+ it('pushes the consent defaults, then loads gtm.js once, however often it runs', async () => {
30
+ Reflect.set(globalThis, '__MFE_RUNTIME_CONFIG__', { gtmId: 'GTM-TEST123' });
31
+ await startAnalytics();
32
+ await startAnalytics();
33
+ expect(argumentsOf(dataLayer()?.[0]).slice(0, 2)).toEqual(['consent', 'default']);
34
+ expect(document.querySelectorAll('script[src*="gtm.js?id=GTM-TEST123"]')).toHaveLength(1);
35
+ });
36
+
37
+ it('stays off when the id is not a container id', async () => {
38
+ Reflect.set(globalThis, '__MFE_RUNTIME_CONFIG__', { gtmId: 'oops' });
39
+ await startAnalytics();
40
+ expect(dataLayer()).toBeUndefined();
41
+ });
42
+ });
@@ -0,0 +1,26 @@
1
+ {
2
+ "name": "ci-github",
3
+ "version": "0.1.0",
4
+ "description": "GitHub Actions workflow for pull requests: lint, typecheck, test, build, doctor, validate, check-compat, budget, runtime-config check and the Playwright smoke test. Opt-in.",
5
+ "requires": ["core"],
6
+ "files": [
7
+ {
8
+ "to": ".github/workflows/ci.yml",
9
+ "target": "root",
10
+ "from": "files/ci.yml"
11
+ },
12
+ {
13
+ "to": "docs/ci.md",
14
+ "target": "root",
15
+ "from": "files/ci.md"
16
+ }
17
+ ],
18
+ "dependencies": [
19
+ {
20
+ "name": "mfeflow",
21
+ "range": "latest",
22
+ "target": "root",
23
+ "dev": true
24
+ }
25
+ ]
26
+ }
@@ -0,0 +1,41 @@
1
+ # CI
2
+
3
+ `.github/workflows/ci.yml` runs on every pull request: lint, formatting, typecheck, tests, build, then
4
+ `mfeflow doctor`, `validate`, `check-compat` and `budget`, the runtime-config check and the Playwright smoke test.
5
+ A step for a feature the project does not have is skipped, so adding `testing` or `env-t3` later needs no edit to the
6
+ workflow. Every action is pinned to a commit SHA, the workflow starts from `permissions: {}`, and the job gets
7
+ `contents: read` only. Dependabot or Renovate can keep the pins current.
8
+
9
+ Make the `check` job a required status check on your default branch (Settings, Rules, Rulesets).
10
+
11
+ ## Preview deploys (not generated yet)
12
+
13
+ mfeflow does not generate a deploy workflow in this release: pushing remotes to a registry comes with the registry
14
+ features. When you add a deploy job, keep to this shape.
15
+
16
+ - Run it on `pull_request` with `contents: read` and `id-token: write`, and no repository secrets.
17
+ - Upload the build of one remote to its own path, for example `preview/<remote>/<pr-number>/`, never to the
18
+ path production reads.
19
+ - Comment the override link on the pull request. `?mfe-override=<remote>@<url of its mf-manifest.json>` on the
20
+ staging shell loads that build instead of the registered one (the shell honours it outside production only).
21
+ - Pull requests from forks get no `id-token`, so they cannot deploy; build them without a preview.
22
+
23
+ ## OIDC instead of stored cloud keys
24
+
25
+ Let the job ask GitHub for a short-lived token and exchange it for cloud credentials; no access key sits in the
26
+ repository settings.
27
+
28
+ ```yaml
29
+ permissions:
30
+ id-token: write
31
+ contents: read
32
+ steps:
33
+ - uses: aws-actions/configure-aws-credentials@<sha>
34
+ with:
35
+ role-to-assume: arn:aws:iam::<account>:role/<team>-deploy
36
+ aws-region: eu-west-1
37
+ ```
38
+
39
+ Give each team a role that can write only its own prefixes (`mfe/<remote>/*` and `preview/<remote>/*`), and restrict the
40
+ role's trust policy to this repository, the `pull_request` or `main` ref, and the workflow file. The same pattern
41
+ works with Google Cloud (`google-github-actions/auth`) and Azure (`azure/login`).
@@ -0,0 +1,63 @@
1
+ name: ci
2
+
3
+ on:
4
+ pull_request:
5
+
6
+ permissions: {}
7
+
8
+ concurrency:
9
+ group: ci-${{ github.ref }}
10
+ cancel-in-progress: true
11
+
12
+ # Steps for a feature you have not added (lint, tests, runtime config, Playwright) are skipped
13
+ # by their `if`, so this file stays the same as the project grows.
14
+ jobs:
15
+ check:
16
+ runs-on: ubuntu-latest
17
+ timeout-minutes: 20
18
+ permissions:
19
+ contents: read
20
+ steps:
21
+ - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
22
+ with:
23
+ persist-credentials: false
24
+ - uses: pnpm/action-setup@ea17c68df8912ef543352723c149a84f56e3d413 # v6.1.0
25
+ - uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0
26
+ with:
27
+ node-version: lts/*
28
+ cache: pnpm
29
+ - run: pnpm install --frozen-lockfile
30
+ - name: Lint
31
+ if: hashFiles('.oxlintrc.json') != ''
32
+ run: pnpm lint
33
+ - name: Check formatting
34
+ if: hashFiles('.prettierrc.json') != ''
35
+ run: pnpm format:check
36
+ - run: pnpm typecheck
37
+ - run: pnpm run --if-present typecheck:tooling
38
+ - name: Test
39
+ run: pnpm run --if-present test
40
+ - run: pnpm build
41
+ - name: mfeflow doctor
42
+ run: pnpm exec mfeflow doctor
43
+ - name: mfeflow validate
44
+ run: pnpm exec mfeflow validate
45
+ - name: mfeflow check-compat
46
+ run: pnpm exec mfeflow check-compat
47
+ - name: mfeflow budget
48
+ run: pnpm exec mfeflow budget
49
+ - name: Check runtime config
50
+ run: pnpm run --if-present check:runtime-config
51
+ - name: Install Playwright browser
52
+ if: hashFiles('playwright.config.ts') != ''
53
+ run: pnpm exec playwright install --with-deps chromium
54
+ - name: Playwright smoke test
55
+ if: hashFiles('playwright.config.ts') != ''
56
+ run: pnpm run test:e2e
57
+ - name: Upload Playwright report
58
+ if: failure() && hashFiles('playwright.config.ts') != ''
59
+ uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
60
+ with:
61
+ name: playwright-report
62
+ path: node_modules/.cache/playwright/report
63
+ retention-days: 7