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.
- package/LICENSE +21 -0
- package/README.md +126 -2
- package/assets/features/analytics-gtm/feature.json +117 -0
- package/assets/features/analytics-gtm/files/Analytics.tsx +13 -0
- package/assets/features/analytics-gtm/files/ConsentBanner.tsx +47 -0
- package/assets/features/analytics-gtm/files/consent-banner-test.tsx +36 -0
- package/assets/features/analytics-gtm/files/init.ts +26 -0
- package/assets/features/analytics-gtm/files/remote-analytics-test.ts.eta +18 -0
- package/assets/features/analytics-gtm/files/remote-analytics.ts.eta +13 -0
- package/assets/features/analytics-gtm/files/shell-init-test.ts +42 -0
- package/assets/features/ci-github/feature.json +26 -0
- package/assets/features/ci-github/files/ci.md +41 -0
- package/assets/features/ci-github/files/ci.yml +63 -0
- package/assets/features/env-t3/feature.json +378 -0
- package/assets/features/env-t3/files/check-runtime-config.ts +105 -0
- package/assets/features/env-t3/files/env.arktype.ts +12 -0
- package/assets/features/env-t3/files/env.example +1 -0
- package/assets/features/env-t3/files/env.valibot.ts +12 -0
- package/assets/features/env-t3/files/env.zod.ts +12 -0
- package/assets/features/env-t3/files/runtime.ts.eta +30 -0
- package/assets/features/env-t3/files/schema.arktype.ts +8 -0
- package/assets/features/env-t3/files/schema.valibot.ts +8 -0
- package/assets/features/env-t3/files/schema.zod.ts +8 -0
- package/assets/features/git-hooks/feature.json +42 -0
- package/assets/features/git-hooks/files/commitlint.config.js +1 -0
- package/assets/features/git-hooks/files/lefthook.yml +9 -0
- package/assets/features/lint-oxlint/feature.json +60 -0
- package/assets/features/lint-oxlint/files/oxlintrc.json +5 -0
- package/assets/features/lint-oxlint/files/prettierignore +5 -0
- package/assets/features/lint-oxlint/files/prettierrc.json +5 -0
- package/assets/features/telemetry-otel/feature.json +159 -0
- package/assets/features/telemetry-otel/files/init.ts.eta +53 -0
- package/assets/features/telemetry-otel/files/remote-telemetry-test.ts.eta +13 -0
- package/assets/features/telemetry-otel/files/remote-telemetry.ts.eta +11 -0
- package/assets/features/telemetry-otel/files/shell-init-test.ts.eta +36 -0
- package/assets/features/testing/feature.json +202 -0
- package/assets/features/testing/files/handlers.ts +5 -0
- package/assets/features/testing/files/playwright.config.ts +30 -0
- package/assets/features/testing/files/root-vitest.config.ts +6 -0
- package/assets/features/testing/files/sample-handlers-test.ts +16 -0
- package/assets/features/testing/files/sample-remote-test.tsx +12 -0
- package/assets/features/testing/files/sample-shell-test.tsx +11 -0
- package/assets/features/testing/files/sample-smoke-e2e.ts +53 -0
- package/assets/features/testing/files/server.ts +4 -0
- package/assets/features/testing/files/setup.ts +18 -0
- package/assets/features/testing/files/vitest.config.ts +11 -0
- package/assets/features/tsconfig/feature.json +78 -0
- package/assets/features/tsconfig/files/root.tsconfig.json +4 -0
- package/assets/features/tsconfig/files/tsconfig.json +5 -0
- package/assets/features/url-factory/feature.json +30 -0
- package/assets/features/url-factory/files/endpoints.ts +12 -0
- package/assets/templates/monorepo/feature.json +295 -0
- package/assets/templates/monorepo/files/remote/mfe.json.eta +7 -0
- package/assets/templates/monorepo/files/remote/package.json.eta +6 -0
- package/assets/templates/monorepo/files/remote/rsbuild.config.ts.eta +7 -0
- package/assets/templates/monorepo/files/remote/src/bootstrap.tsx.eta +6 -0
- package/assets/templates/monorepo/files/remote/src/env.d.ts.eta +1 -0
- package/assets/templates/monorepo/files/remote/src/exposed/App.tsx.eta +18 -0
- package/assets/templates/monorepo/files/remote/src/index.ts.eta +2 -0
- package/assets/templates/monorepo/files/root/AGENTS.md.eta +17 -0
- package/assets/templates/monorepo/files/root/README.md.eta +52 -0
- package/assets/templates/monorepo/files/root/gitignore.eta +11 -0
- package/assets/templates/monorepo/files/root/nvmrc.eta +1 -0
- package/assets/templates/monorepo/files/root/package.json.eta +9 -0
- package/assets/templates/monorepo/files/root/pnpm-workspace.yaml.eta +7 -0
- package/assets/templates/monorepo/files/root/turbo.json.eta +16 -0
- package/assets/templates/monorepo/files/shell/mfe.json.eta +7 -0
- package/assets/templates/monorepo/files/shell/package.json.eta +6 -0
- package/assets/templates/monorepo/files/shell/rsbuild.config.ts.eta +74 -0
- package/assets/templates/monorepo/files/shell/src/App.tsx.eta +46 -0
- package/assets/templates/monorepo/files/shell/src/bootstrap.tsx.eta +24 -0
- package/assets/templates/monorepo/files/shell/src/env.d.ts.eta +1 -0
- package/assets/templates/monorepo/files/shell/src/index.ts.eta +4 -0
- package/assets/templates/monorepo/files/shell/src/pages/Home.tsx.eta +12 -0
- package/assets/templates/monorepo/files/shell/src/remotes/route-table.tsx.eta +12 -0
- package/assets/templates/monorepo/glue/registry-entry.json.eta +5 -0
- package/assets/templates/monorepo/glue/route.tsx.eta +5 -0
- package/bin/mfeflow.js +4 -0
- package/dist/args.d.ts +68 -0
- package/dist/assets.d.ts +13 -0
- package/dist/catalog.d.ts +24 -0
- package/dist/checks/compat.d.ts +2 -0
- package/dist/checks/config.d.ts +8 -0
- package/dist/checks/env.d.ts +6 -0
- package/dist/checks/index.d.ts +6 -0
- package/dist/checks/plugins.d.ts +4 -0
- package/dist/checks/singletons.d.ts +9 -0
- package/dist/checks/workspace.d.ts +2 -0
- package/dist/cli.d.ts +11 -0
- package/dist/commands/add.d.ts +84 -0
- package/dist/commands/budget.d.ts +56 -0
- package/dist/commands/dev.d.ts +62 -0
- package/dist/commands/doctor.d.ts +141 -0
- package/dist/commands/env-key.d.ts +4 -0
- package/dist/commands/features.d.ts +53 -0
- package/dist/commands/init.d.ts +102 -0
- package/dist/devregistry.d.ts +19 -0
- package/dist/exit.d.ts +31 -0
- package/dist/findings.d.ts +35 -0
- package/dist/glue.d.ts +41 -0
- package/dist/help.d.ts +12 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.js +4154 -0
- package/dist/inspect.d.ts +57 -0
- package/dist/io.d.ts +10 -0
- package/dist/local.d.ts +28 -0
- package/dist/options.d.ts +7 -0
- package/dist/output.d.ts +157 -0
- package/dist/pipeline.d.ts +7 -0
- package/dist/preset.d.ts +19 -0
- package/dist/proc.d.ts +79 -0
- package/dist/prompter.d.ts +28 -0
- package/dist/report.d.ts +26 -0
- package/dist/runtime.d.ts +17 -0
- package/dist/singletons.d.ts +11 -0
- package/dist/sizes.d.ts +17 -0
- package/dist/system.d.ts +20 -0
- package/dist/version.d.ts +8 -0
- package/dist/write.d.ts +26 -0
- 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
|
-
#
|
|
1
|
+
# mfeflow
|
|
2
2
|
|
|
3
|
-
|
|
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
|