mfeflow 0.0.0-stage → 0.2.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 +399 -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/event-bus/feature.json +100 -0
- package/assets/features/event-bus/files/announce.tsx.eta +10 -0
- package/assets/features/event-bus/files/contracts-events.ts +11 -0
- package/assets/features/event-bus/files/contracts-package.json.eta +10 -0
- package/assets/features/event-bus/files/contracts-readme.md +10 -0
- package/assets/features/event-bus/files/events-test.ts.eta +18 -0
- package/assets/features/event-bus/files/events.ts.eta +20 -0
- package/assets/features/event-bus/files/shell-events-test.ts.eta +22 -0
- package/assets/features/event-bus/files/shell-events.ts.eta +19 -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/http/feature.json +81 -0
- package/assets/features/http/files/client-test.ts +19 -0
- package/assets/features/http/files/client.ts +20 -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/route-factory/feature.json +60 -0
- package/assets/features/route-factory/files/contract.ts.eta +7 -0
- package/assets/features/route-factory/files/routes-contract.ts.eta +11 -0
- package/assets/features/route-factory/files/routes-test.ts.eta +9 -0
- package/assets/features/route-factory/files/routes.ts.eta +11 -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 +23 -0
- package/assets/templates/monorepo/feature.json +321 -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 +14 -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 +4171 -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
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "event-bus",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Typed events between remotes (@mfeflow/event-bus) and the contracts package that holds their types: packages/contracts/src/events, merged into the bus in every app, with a sample event.",
|
|
5
|
+
"requires": ["core", "tsconfig"],
|
|
6
|
+
"files": [
|
|
7
|
+
{
|
|
8
|
+
"to": "packages/contracts/package.json",
|
|
9
|
+
"target": "root",
|
|
10
|
+
"from": "files/contracts-package.json.eta",
|
|
11
|
+
"template": true
|
|
12
|
+
},
|
|
13
|
+
{
|
|
14
|
+
"to": "packages/contracts/src/events/index.ts",
|
|
15
|
+
"target": "root",
|
|
16
|
+
"from": "files/contracts-events.ts"
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
"to": "packages/contracts/README.md",
|
|
20
|
+
"target": "root",
|
|
21
|
+
"from": "files/contracts-readme.md"
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
"to": "src/events.ts",
|
|
25
|
+
"target": "shell",
|
|
26
|
+
"from": "files/shell-events.ts.eta",
|
|
27
|
+
"template": true
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
"to": "src/events.test.ts",
|
|
31
|
+
"target": "shell",
|
|
32
|
+
"from": "files/shell-events-test.ts.eta",
|
|
33
|
+
"template": true,
|
|
34
|
+
"when": {
|
|
35
|
+
"feature:testing": true
|
|
36
|
+
}
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
"to": "src/events.ts",
|
|
40
|
+
"target": "remotes",
|
|
41
|
+
"from": "files/events.ts.eta",
|
|
42
|
+
"template": true
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
"to": "src/events.test.ts",
|
|
46
|
+
"target": "remotes",
|
|
47
|
+
"from": "files/events-test.ts.eta",
|
|
48
|
+
"template": true,
|
|
49
|
+
"when": {
|
|
50
|
+
"feature:testing": true
|
|
51
|
+
}
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
"to": "src/Announce.tsx",
|
|
55
|
+
"target": "remotes",
|
|
56
|
+
"from": "files/announce.tsx.eta",
|
|
57
|
+
"template": true
|
|
58
|
+
}
|
|
59
|
+
],
|
|
60
|
+
"dependencies": [
|
|
61
|
+
{
|
|
62
|
+
"name": "@mfeflow/event-bus",
|
|
63
|
+
"range": "latest",
|
|
64
|
+
"target": "shell"
|
|
65
|
+
},
|
|
66
|
+
{
|
|
67
|
+
"name": "@mfeflow/event-bus",
|
|
68
|
+
"range": "latest",
|
|
69
|
+
"target": "remotes"
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
"name": "{{scope}}/contracts",
|
|
73
|
+
"range": "workspace:*",
|
|
74
|
+
"target": "shell"
|
|
75
|
+
},
|
|
76
|
+
{
|
|
77
|
+
"name": "{{scope}}/contracts",
|
|
78
|
+
"range": "workspace:*",
|
|
79
|
+
"target": "remotes"
|
|
80
|
+
}
|
|
81
|
+
],
|
|
82
|
+
"transforms": [
|
|
83
|
+
{
|
|
84
|
+
"op": "add-boot-step",
|
|
85
|
+
"file": "src/bootstrap.tsx",
|
|
86
|
+
"target": "shell",
|
|
87
|
+
"call": "configureShellEvents",
|
|
88
|
+
"from": "./events",
|
|
89
|
+
"await": false
|
|
90
|
+
},
|
|
91
|
+
{
|
|
92
|
+
"op": "wrap-provider",
|
|
93
|
+
"file": "src/exposed/App.tsx",
|
|
94
|
+
"target": "remotes",
|
|
95
|
+
"component": "Announce",
|
|
96
|
+
"from": "../Announce",
|
|
97
|
+
"function": "App"
|
|
98
|
+
}
|
|
99
|
+
]
|
|
100
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { useEffect, type ReactNode } from 'react';
|
|
2
|
+
import { announceReady } from './events';
|
|
3
|
+
|
|
4
|
+
/** Tells the other remotes that <%= it.app.name %> is on the page, each time it mounts. */
|
|
5
|
+
export function Announce({ children }: { children: ReactNode }) {
|
|
6
|
+
useEffect(() => {
|
|
7
|
+
announceReady();
|
|
8
|
+
}, []);
|
|
9
|
+
return <>{children}</>;
|
|
10
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Every event the platform can carry, by versioned name: <domain>.<event>.v<major>, like
|
|
3
|
+
* cart.item-added.v1. Types only: nothing here runs, so any remote can read it.
|
|
4
|
+
*
|
|
5
|
+
* Add an event by adding a line. Change a payload in a way old listeners cannot read by adding
|
|
6
|
+
* a new version (cart.item-added.v2) and publishing both until every listener has moved.
|
|
7
|
+
*/
|
|
8
|
+
export interface Events {
|
|
9
|
+
/** A remote is on the page. The sample event: every remote announces itself, every other one can listen. */
|
|
10
|
+
'mfe.ready.v1': { mfe: string };
|
|
11
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
# contracts
|
|
2
|
+
|
|
3
|
+
What the micro-frontends agree on, and nothing else. A remote never imports another remote; it
|
|
4
|
+
imports this.
|
|
5
|
+
|
|
6
|
+
- `src/events`: the events remotes publish and listen to, as types.
|
|
7
|
+
- `src/routes`: each remote's public paths, as plain data (added by `route-factory`).
|
|
8
|
+
|
|
9
|
+
Keep it to types and plain data with no imports, so reading it costs a remote nothing at run time
|
|
10
|
+
and it never pulls in another remote's code.
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { publish, resetEventBusForTests } from '@mfeflow/event-bus';
|
|
2
|
+
import { afterEach, describe, expect, it, vi } from 'vitest';
|
|
3
|
+
import { announceReady, onOtherRemoteReady } from './events';
|
|
4
|
+
|
|
5
|
+
afterEach(resetEventBusForTests);
|
|
6
|
+
|
|
7
|
+
describe('the <%= it.app.name %> events', () => {
|
|
8
|
+
it('hears another remote and not itself', () => {
|
|
9
|
+
const heard = vi.fn();
|
|
10
|
+
const stop = onOtherRemoteReady(heard);
|
|
11
|
+
announceReady();
|
|
12
|
+
publish('mfe.ready.v1', { mfe: 'somewhere-else' });
|
|
13
|
+
expect(heard).toHaveBeenCalledExactlyOnceWith('somewhere-else');
|
|
14
|
+
stop();
|
|
15
|
+
publish('mfe.ready.v1', { mfe: 'later' });
|
|
16
|
+
expect(heard).toHaveBeenCalledOnce();
|
|
17
|
+
});
|
|
18
|
+
});
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { publish, subscribe } from '@mfeflow/event-bus';
|
|
2
|
+
import type { Events } from '<%= it.scope %>/contracts/events';
|
|
3
|
+
|
|
4
|
+
// The platform's event map is the contracts package's. This one declaration merges it in.
|
|
5
|
+
declare module '@mfeflow/event-bus' {
|
|
6
|
+
// oxlint-disable-next-line typescript/no-empty-interface -- merges the contracts into the platform map
|
|
7
|
+
interface EventMap extends Events {}
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/** Tells the other remotes that <%= it.app.name %> is on the page. */
|
|
11
|
+
export function announceReady(): void {
|
|
12
|
+
publish('mfe.ready.v1', { mfe: '<%= it.app.name %>' });
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/** Calls `handler` with the name of each other remote that announces itself. Returns the function that stops it. */
|
|
16
|
+
export function onOtherRemoteReady(handler: (mfe: string) => void): () => void {
|
|
17
|
+
return subscribe('mfe.ready.v1', ({ mfe }) => {
|
|
18
|
+
if (mfe !== '<%= it.app.name %>') handler(mfe);
|
|
19
|
+
});
|
|
20
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { publish, resetEventBusForTests, subscribe } from '@mfeflow/event-bus';
|
|
2
|
+
import { afterEach, describe, expect, it, vi } from 'vitest';
|
|
3
|
+
import { configureShellEvents } from './events';
|
|
4
|
+
|
|
5
|
+
afterEach(resetEventBusForTests);
|
|
6
|
+
|
|
7
|
+
describe('the shell event bus', () => {
|
|
8
|
+
it('carries the contracts events between remotes', () => {
|
|
9
|
+
const heard = vi.fn();
|
|
10
|
+
subscribe('mfe.ready.v1', heard);
|
|
11
|
+
publish('mfe.ready.v1', { mfe: 'cart' });
|
|
12
|
+
expect(heard).toHaveBeenCalledExactlyOnceWith({ mfe: 'cart' });
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
it('replays the latest announcement to a remote that mounts late', () => {
|
|
16
|
+
configureShellEvents();
|
|
17
|
+
publish('mfe.ready.v1', { mfe: 'cart' });
|
|
18
|
+
const late = vi.fn();
|
|
19
|
+
subscribe('mfe.ready.v1', late);
|
|
20
|
+
expect(late).toHaveBeenCalledExactlyOnceWith({ mfe: 'cart' });
|
|
21
|
+
});
|
|
22
|
+
});
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { configureEvent } from '@mfeflow/event-bus';
|
|
2
|
+
import type { Events } from '<%= it.scope %>/contracts/events';
|
|
3
|
+
|
|
4
|
+
// The platform's event map is the contracts package's. This one declaration merges it in.
|
|
5
|
+
declare module '@mfeflow/event-bus' {
|
|
6
|
+
// oxlint-disable-next-line typescript/no-empty-interface -- merges the contracts into the platform map
|
|
7
|
+
interface EventMap extends Events {}
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* How the platform's events behave, set once at boot. The shell imports the bus here, which makes
|
|
12
|
+
* it the one that provides the copy every remote shares.
|
|
13
|
+
*/
|
|
14
|
+
export function configureShellEvents(): void {
|
|
15
|
+
// "This remote is up" is a fact about the page, not a moment: a remote that mounts later still
|
|
16
|
+
// hears the latest announcement. Only the last one is kept. Leave replay off for events that
|
|
17
|
+
// are things that happened, like cart.item-added.v1.
|
|
18
|
+
configureEvent('mfe.ready.v1', { replay: true });
|
|
19
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "git-hooks",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "lefthook with commitlint: Conventional Commit messages, typecheck before push. Opt-in.",
|
|
5
|
+
"requires": ["core"],
|
|
6
|
+
"files": [
|
|
7
|
+
{
|
|
8
|
+
"to": "lefthook.yml",
|
|
9
|
+
"from": "files/lefthook.yml"
|
|
10
|
+
},
|
|
11
|
+
{
|
|
12
|
+
"to": "commitlint.config.js",
|
|
13
|
+
"from": "files/commitlint.config.js"
|
|
14
|
+
}
|
|
15
|
+
],
|
|
16
|
+
"dependencies": [
|
|
17
|
+
{
|
|
18
|
+
"name": "lefthook",
|
|
19
|
+
"range": "2.2.2",
|
|
20
|
+
"target": "root",
|
|
21
|
+
"dev": true
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
"name": "@commitlint/cli",
|
|
25
|
+
"range": "21.2.4",
|
|
26
|
+
"target": "root",
|
|
27
|
+
"dev": true
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
"name": "@commitlint/config-conventional",
|
|
31
|
+
"range": "21.2.4",
|
|
32
|
+
"target": "root",
|
|
33
|
+
"dev": true
|
|
34
|
+
}
|
|
35
|
+
],
|
|
36
|
+
"scripts": [
|
|
37
|
+
{
|
|
38
|
+
"name": "prepare",
|
|
39
|
+
"command": "lefthook install"
|
|
40
|
+
}
|
|
41
|
+
]
|
|
42
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export default { extends: ['@commitlint/config-conventional'] };
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "http",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "A fetch client over your url-factory endpoints (@mfeflow/http): src/api/client.ts in every app, with retries for idempotent requests, timeouts and typed errors. The API base URL is the runtime config's apiUrl.",
|
|
5
|
+
"requires": ["core", "tsconfig", "url-factory"],
|
|
6
|
+
"files": [
|
|
7
|
+
{
|
|
8
|
+
"to": "src/api/client.ts",
|
|
9
|
+
"target": "shell",
|
|
10
|
+
"from": "files/client.ts"
|
|
11
|
+
},
|
|
12
|
+
{
|
|
13
|
+
"to": "src/api/client.ts",
|
|
14
|
+
"target": "remotes",
|
|
15
|
+
"from": "files/client.ts"
|
|
16
|
+
},
|
|
17
|
+
{
|
|
18
|
+
"to": "src/api/client.test.ts",
|
|
19
|
+
"target": "shell",
|
|
20
|
+
"from": "files/client-test.ts",
|
|
21
|
+
"when": {
|
|
22
|
+
"feature:testing": true
|
|
23
|
+
}
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
"to": "src/api/client.test.ts",
|
|
27
|
+
"target": "remotes",
|
|
28
|
+
"from": "files/client-test.ts",
|
|
29
|
+
"when": {
|
|
30
|
+
"feature:testing": true
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
],
|
|
34
|
+
"dependencies": [
|
|
35
|
+
{
|
|
36
|
+
"name": "@mfeflow/http",
|
|
37
|
+
"range": "latest",
|
|
38
|
+
"target": "shell"
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
"name": "@mfeflow/http",
|
|
42
|
+
"range": "latest",
|
|
43
|
+
"target": "remotes"
|
|
44
|
+
}
|
|
45
|
+
],
|
|
46
|
+
"transforms": [
|
|
47
|
+
{
|
|
48
|
+
"op": "add-env-key",
|
|
49
|
+
"file": "src/config/schema.ts",
|
|
50
|
+
"target": "shell",
|
|
51
|
+
"key": "apiUrl",
|
|
52
|
+
"scope": "runtime",
|
|
53
|
+
"schema": {
|
|
54
|
+
"zod": "z.string().min(1)",
|
|
55
|
+
"valibot": "v.pipe(v.string(), v.minLength(1))",
|
|
56
|
+
"arktype": "'string > 0'"
|
|
57
|
+
},
|
|
58
|
+
"value": "/api",
|
|
59
|
+
"configFiles": [
|
|
60
|
+
"public/config/runtime-config.dev.json",
|
|
61
|
+
"public/config/runtime-config.staging.json",
|
|
62
|
+
"public/config/runtime-config.prod.json"
|
|
63
|
+
],
|
|
64
|
+
"when": {
|
|
65
|
+
"feature:env-t3": true
|
|
66
|
+
}
|
|
67
|
+
},
|
|
68
|
+
{
|
|
69
|
+
"op": "add-env-key",
|
|
70
|
+
"file": "public/config/runtime-config.json",
|
|
71
|
+
"target": "shell",
|
|
72
|
+
"key": "apiUrl",
|
|
73
|
+
"scope": "runtime",
|
|
74
|
+
"schema": "z.string().min(1)",
|
|
75
|
+
"value": "/api",
|
|
76
|
+
"when": {
|
|
77
|
+
"feature:env-t3": false
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
]
|
|
81
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { isHttpError } from '@mfeflow/http';
|
|
2
|
+
import { http, HttpResponse } from 'msw';
|
|
3
|
+
import { describe, expect, it } from 'vitest';
|
|
4
|
+
import { server } from '../mocks/server';
|
|
5
|
+
import { api } from './client';
|
|
6
|
+
|
|
7
|
+
describe('the api client', () => {
|
|
8
|
+
it('reads an endpoint through the mock server', async () => {
|
|
9
|
+
expect(await api.get('health')).toEqual({ ok: true });
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
it('turns a failure into an HttpError with the status', async () => {
|
|
13
|
+
server.use(
|
|
14
|
+
http.get('*/api/health', () => HttpResponse.json({ error: 'down' }, { status: 404 })),
|
|
15
|
+
);
|
|
16
|
+
const error = await api.get('health').catch((e: unknown) => e);
|
|
17
|
+
expect(isHttpError(error) && error.status).toBe(404);
|
|
18
|
+
});
|
|
19
|
+
});
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { createHttpClient } from '@mfeflow/http';
|
|
2
|
+
import { apiUrl, type endpoints } from './endpoints';
|
|
3
|
+
|
|
4
|
+
// What each endpoint answers with, by the name in ./endpoints. Add a line per endpoint you call.
|
|
5
|
+
interface Responses {
|
|
6
|
+
health: { ok: boolean };
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* One client per app over the endpoints in ./endpoints. The base URL is the runtime config's
|
|
11
|
+
* `apiUrl`. Idempotent requests retry on a network error, a timeout and a 502, 503 or 504; a
|
|
12
|
+
* failure is an HttpError with the status and body, and no URL in its message.
|
|
13
|
+
*
|
|
14
|
+
* const { ok } = await api.get('health');
|
|
15
|
+
*/
|
|
16
|
+
export const api = createHttpClient<typeof endpoints, Responses>({
|
|
17
|
+
url: apiUrl,
|
|
18
|
+
// Return the signed-in user's token; no header is sent while this returns undefined.
|
|
19
|
+
// getToken: () => session.accessToken(),
|
|
20
|
+
});
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "lint-oxlint",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "oxlint with type-aware rules on TypeScript 7 (@mfeflow/oxlint-config), and Prettier.",
|
|
5
|
+
"requires": ["core", "tsconfig"],
|
|
6
|
+
"files": [
|
|
7
|
+
{
|
|
8
|
+
"to": ".oxlintrc.json",
|
|
9
|
+
"from": "files/oxlintrc.json"
|
|
10
|
+
},
|
|
11
|
+
{
|
|
12
|
+
"to": ".prettierrc.json",
|
|
13
|
+
"from": "files/prettierrc.json"
|
|
14
|
+
},
|
|
15
|
+
{
|
|
16
|
+
"to": ".prettierignore",
|
|
17
|
+
"from": "files/prettierignore"
|
|
18
|
+
}
|
|
19
|
+
],
|
|
20
|
+
"dependencies": [
|
|
21
|
+
{
|
|
22
|
+
"name": "@mfeflow/oxlint-config",
|
|
23
|
+
"range": "latest",
|
|
24
|
+
"target": "root",
|
|
25
|
+
"dev": true
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
"name": "oxlint",
|
|
29
|
+
"range": "1.90.0",
|
|
30
|
+
"target": "root",
|
|
31
|
+
"dev": true
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
"name": "oxlint-tsgolint",
|
|
35
|
+
"range": "7.0.2005",
|
|
36
|
+
"target": "root",
|
|
37
|
+
"dev": true
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
"name": "prettier",
|
|
41
|
+
"range": "3.9.10",
|
|
42
|
+
"target": "root",
|
|
43
|
+
"dev": true
|
|
44
|
+
}
|
|
45
|
+
],
|
|
46
|
+
"scripts": [
|
|
47
|
+
{
|
|
48
|
+
"name": "lint",
|
|
49
|
+
"command": "oxlint --type-aware --deny-warnings"
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
"name": "format",
|
|
53
|
+
"command": "prettier --write ."
|
|
54
|
+
},
|
|
55
|
+
{
|
|
56
|
+
"name": "format:check",
|
|
57
|
+
"command": "prettier --check ."
|
|
58
|
+
}
|
|
59
|
+
]
|
|
60
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "route-factory",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Typed links between remotes (@mfeflow/route-factory): src/routes.ts in every remote. With event-bus the paths live in the contracts package, so remotes link to each other without importing each other.",
|
|
5
|
+
"requires": ["core", "tsconfig"],
|
|
6
|
+
"files": [
|
|
7
|
+
{
|
|
8
|
+
"to": "src/routes.ts",
|
|
9
|
+
"target": "remotes",
|
|
10
|
+
"from": "files/routes.ts.eta",
|
|
11
|
+
"template": true,
|
|
12
|
+
"when": {
|
|
13
|
+
"feature:event-bus": false
|
|
14
|
+
}
|
|
15
|
+
},
|
|
16
|
+
{
|
|
17
|
+
"to": "src/routes.ts",
|
|
18
|
+
"target": "remotes",
|
|
19
|
+
"from": "files/routes-contract.ts.eta",
|
|
20
|
+
"template": true,
|
|
21
|
+
"when": {
|
|
22
|
+
"feature:event-bus": true
|
|
23
|
+
}
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
"to": "packages/contracts/src/routes/{{app.name}}.ts",
|
|
27
|
+
"target": "remotes",
|
|
28
|
+
"atRoot": true,
|
|
29
|
+
"from": "files/contract.ts.eta",
|
|
30
|
+
"template": true,
|
|
31
|
+
"when": {
|
|
32
|
+
"feature:event-bus": true
|
|
33
|
+
}
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
"to": "src/routes.test.ts",
|
|
37
|
+
"target": "remotes",
|
|
38
|
+
"from": "files/routes-test.ts.eta",
|
|
39
|
+
"template": true,
|
|
40
|
+
"when": {
|
|
41
|
+
"feature:testing": true
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
],
|
|
45
|
+
"dependencies": [
|
|
46
|
+
{
|
|
47
|
+
"name": "@mfeflow/route-factory",
|
|
48
|
+
"range": "latest",
|
|
49
|
+
"target": "remotes"
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
"name": "{{scope}}/contracts",
|
|
53
|
+
"range": "workspace:*",
|
|
54
|
+
"target": "remotes",
|
|
55
|
+
"when": {
|
|
56
|
+
"feature:event-bus": true
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
]
|
|
60
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { createLinks, defineRoutes } from '@mfeflow/route-factory';
|
|
2
|
+
import { routes as contract } from '<%= it.scope %>/contracts/routes/<%= it.app.name %>';
|
|
3
|
+
|
|
4
|
+
// The paths are in the contracts package, so another remote can link here without importing this
|
|
5
|
+
// one. `/<%= it.app.name %>` is where the shell serves this remote.
|
|
6
|
+
export const routes = defineRoutes('/<%= it.app.name %>', contract);
|
|
7
|
+
|
|
8
|
+
// Links to this remote's routes. Another remote links here with
|
|
9
|
+
// import { routes as <%= it.app.name %> } from '<%= it.scope %>/contracts/routes/<%= it.app.name %>'
|
|
10
|
+
// and createLinks({ <%= it.app.name %>: defineRoutes('/<%= it.app.name %>', <%= it.app.name %>) }).
|
|
11
|
+
export const links = createLinks({ <%= it.app.name %>: routes });
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { links, routes } from './routes';
|
|
3
|
+
|
|
4
|
+
describe('the <%= it.app.name %> routes', () => {
|
|
5
|
+
it('are served under /<%= it.app.name %>', () => {
|
|
6
|
+
expect(routes.mount).toBe('/<%= it.app.name %>');
|
|
7
|
+
expect(links.href('<%= it.app.name %>.home')).toBe('/<%= it.app.name %>');
|
|
8
|
+
});
|
|
9
|
+
});
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { createLinks, defineRoutes } from '@mfeflow/route-factory';
|
|
2
|
+
|
|
3
|
+
// <%= it.app.name %>'s public paths. `/<%= it.app.name %>` is where the shell serves this remote.
|
|
4
|
+
// A route is a path template: `:id` is a path param, and `?q&page?` after the path names search
|
|
5
|
+
// params (q is required, page is optional). Links to a route are checked against this table.
|
|
6
|
+
export const routes = defineRoutes('/<%= it.app.name %>', {
|
|
7
|
+
home: '/',
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
// Links to this remote's routes. Another remote links here by adding `{ <%= it.app.name %>: routes }` to its own createLinks.
|
|
11
|
+
export const links = createLinks({ <%= it.app.name %>: routes });
|