mfeflow 0.1.0 → 0.3.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 (101) hide show
  1. package/assets/features/analytics-clarity/feature.json +64 -0
  2. package/assets/features/analytics-clarity/files/clarity-test.ts +36 -0
  3. package/assets/features/analytics-clarity/files/clarity.ts +26 -0
  4. package/assets/features/analytics-gtm/files/init.ts +2 -2
  5. package/assets/features/ci-github/files/ci.md +1 -1
  6. package/assets/features/ci-github/files/ci.yml +4 -4
  7. package/assets/features/commit-prompt/feature.json +27 -0
  8. package/assets/features/commit-prompt/files/cz.config.js +27 -0
  9. package/assets/features/data-react-query/feature.json +89 -0
  10. package/assets/features/data-react-query/files/keys.ts.eta +8 -0
  11. package/assets/features/data-react-query/files/remote-Query-test.tsx.eta +39 -0
  12. package/assets/features/data-react-query/files/remote-Query.tsx +15 -0
  13. package/assets/features/data-react-query/files/shell-Query-test.tsx +21 -0
  14. package/assets/features/data-react-query/files/shell-Query.tsx +14 -0
  15. package/assets/features/data-react-query/files/useHealth.fetch.ts.eta +15 -0
  16. package/assets/features/data-react-query/files/useHealth.http.ts.eta +11 -0
  17. package/assets/features/env-t3/feature.json +42 -0
  18. package/assets/features/env-t3/files/env.arktype.ts +1 -1
  19. package/assets/features/env-t3/files/env.valibot.ts +1 -1
  20. package/assets/features/env-t3/files/env.zod.ts +1 -1
  21. package/assets/features/env-t3/files/schema.arktype.ts +1 -1
  22. package/assets/features/env-t3/files/schema.valibot.ts +1 -1
  23. package/assets/features/env-t3/files/schema.zod.ts +1 -1
  24. package/assets/features/event-bus/feature.json +100 -0
  25. package/assets/features/event-bus/files/announce.tsx.eta +10 -0
  26. package/assets/features/event-bus/files/contracts-events.ts +11 -0
  27. package/assets/features/event-bus/files/contracts-package.json.eta +10 -0
  28. package/assets/features/event-bus/files/contracts-readme.md +10 -0
  29. package/assets/features/event-bus/files/events-test.ts.eta +18 -0
  30. package/assets/features/event-bus/files/events.ts.eta +20 -0
  31. package/assets/features/event-bus/files/shell-events-test.ts.eta +22 -0
  32. package/assets/features/event-bus/files/shell-events.ts.eta +19 -0
  33. package/assets/features/forms/feature.json +90 -0
  34. package/assets/features/forms/files/ContactForm-test.tsx +35 -0
  35. package/assets/features/forms/files/ContactForm.tsx +50 -0
  36. package/assets/features/forms/files/schema.arktype.ts +9 -0
  37. package/assets/features/forms/files/schema.valibot.ts +9 -0
  38. package/assets/features/forms/files/schema.zod.ts +9 -0
  39. package/assets/features/http/feature.json +81 -0
  40. package/assets/features/http/files/client-test.ts +19 -0
  41. package/assets/features/http/files/client.ts +20 -0
  42. package/assets/features/i18n/feature.json +139 -0
  43. package/assets/features/i18n/files/LanguageSwitcher.tsx +28 -0
  44. package/assets/features/i18n/files/Languages.tsx +12 -0
  45. package/assets/features/i18n/files/language-switcher-test.tsx +19 -0
  46. package/assets/features/i18n/files/remote-I18n-test.tsx.eta +43 -0
  47. package/assets/features/i18n/files/remote-I18n.tsx.eta +36 -0
  48. package/assets/features/i18n/files/remote-ar.json.eta +3 -0
  49. package/assets/features/i18n/files/remote-config.ts +9 -0
  50. package/assets/features/i18n/files/remote-en.json.eta +3 -0
  51. package/assets/features/i18n/files/remote-load.ts +4 -0
  52. package/assets/features/i18n/files/shell-config.ts +20 -0
  53. package/assets/features/i18n/files/shell-init-test.ts +32 -0
  54. package/assets/features/i18n/files/shell-init.ts +7 -0
  55. package/assets/features/lint-oxlint/files/oxlintrc.json +1 -1
  56. package/assets/features/lint-oxlint/files/prettierignore +1 -0
  57. package/assets/features/route-factory/feature.json +60 -0
  58. package/assets/features/route-factory/files/contract.ts.eta +7 -0
  59. package/assets/features/route-factory/files/routes-contract.ts.eta +11 -0
  60. package/assets/features/route-factory/files/routes-test.ts.eta +9 -0
  61. package/assets/features/route-factory/files/routes.ts.eta +11 -0
  62. package/assets/features/storybook/feature.json +119 -0
  63. package/assets/features/storybook/files/App.stories.tsx.eta +8 -0
  64. package/assets/features/storybook/files/Home.stories.tsx +9 -0
  65. package/assets/features/storybook/files/button.stories.tsx +14 -0
  66. package/assets/features/storybook/files/remote-main.ts +13 -0
  67. package/assets/features/storybook/files/rsbuild.config.ts +10 -0
  68. package/assets/features/storybook/files/shell-main.ts +39 -0
  69. package/assets/features/tailwind/feature.json +87 -0
  70. package/assets/features/tailwind/files/Scope-test.tsx.eta +17 -0
  71. package/assets/features/tailwind/files/Scope.tsx.eta +15 -0
  72. package/assets/features/tailwind/files/postcss.config.mjs +1 -0
  73. package/assets/features/tailwind/files/remote-styles.css.eta +21 -0
  74. package/assets/features/tailwind/files/shell-styles.css.eta +22 -0
  75. package/assets/features/testing/files/vitest.config.ts +11 -2
  76. package/assets/features/theming/feature.json +103 -0
  77. package/assets/features/theming/files/Theme.tsx +15 -0
  78. package/assets/features/theming/files/ThemeToggle.tsx +23 -0
  79. package/assets/features/theming/files/apply-stored-test.ts +37 -0
  80. package/assets/features/theming/files/apply-stored.ts +13 -0
  81. package/assets/features/theming/files/head-script.ts +8 -0
  82. package/assets/features/theming/files/theme-toggle-test.tsx +18 -0
  83. package/assets/features/theming/files/theme.css +14 -0
  84. package/assets/features/theming/files/themes.ts +30 -0
  85. package/assets/features/theming/files/tokens-test.ts +9 -0
  86. package/assets/features/theming/files/tokens.ts +9 -0
  87. package/assets/features/ui-shadcn/feature.json +61 -0
  88. package/assets/features/ui-shadcn/files/button-test.tsx.eta +32 -0
  89. package/assets/features/ui-shadcn/files/button.tsx.eta +36 -0
  90. package/assets/features/ui-shadcn/files/components.json.eta +22 -0
  91. package/assets/features/ui-shadcn/files/utils-test.ts.eta +17 -0
  92. package/assets/features/ui-shadcn/files/utils.ts.eta +11 -0
  93. package/assets/features/url-factory/files/endpoints.ts +13 -2
  94. package/assets/templates/monorepo/feature.json +26 -0
  95. package/assets/templates/monorepo/files/root/AGENTS.md.eta +3 -3
  96. package/assets/templates/monorepo/files/root/README.md.eta +11 -11
  97. package/assets/templates/monorepo/files/root/gitignore.eta +1 -0
  98. package/assets/templates/monorepo/files/root/pnpm-workspace.yaml.eta +3 -1
  99. package/assets/templates/monorepo/files/shell/src/pages/Home.tsx.eta +3 -1
  100. package/dist/index.js +62 -0
  101. package/package.json +4 -4
@@ -0,0 +1,64 @@
1
+ {
2
+ "name": "analytics-clarity",
3
+ "version": "0.1.0",
4
+ "description": "Microsoft Clarity behind @mfeflow/analytics, next to Google Tag Manager. Clarity loads only after the visitor grants analytics consent in the banner analytics-gtm adds, and stops again if they withdraw it. The project id is the runtime config's clarityId.",
5
+ "requires": ["core", "tsconfig", "analytics-gtm"],
6
+ "files": [
7
+ {
8
+ "to": "src/analytics/clarity.ts",
9
+ "target": "shell",
10
+ "from": "files/clarity.ts"
11
+ },
12
+ {
13
+ "to": "src/analytics/clarity.test.ts",
14
+ "target": "shell",
15
+ "from": "files/clarity-test.ts",
16
+ "when": {
17
+ "feature:testing": true
18
+ }
19
+ }
20
+ ],
21
+ "transforms": [
22
+ {
23
+ "op": "add-boot-step",
24
+ "file": "src/bootstrap.tsx",
25
+ "target": "shell",
26
+ "call": "initClarityFromConfig",
27
+ "from": "./analytics/clarity",
28
+ "await": false
29
+ },
30
+ {
31
+ "op": "add-env-key",
32
+ "file": "src/config/schema.ts",
33
+ "target": "shell",
34
+ "key": "clarityId",
35
+ "scope": "runtime",
36
+ "schema": {
37
+ "zod": "z.string().regex(/^([a-z0-9]{8,12})?$/)",
38
+ "valibot": "v.pipe(v.string(), v.regex(/^([a-z0-9]{8,12})?$/))",
39
+ "arktype": "'/^([a-z0-9]{8,12})?$/'"
40
+ },
41
+ "value": "",
42
+ "configFiles": [
43
+ "public/config/runtime-config.dev.json",
44
+ "public/config/runtime-config.staging.json",
45
+ "public/config/runtime-config.prod.json"
46
+ ],
47
+ "when": {
48
+ "feature:env-t3": true
49
+ }
50
+ },
51
+ {
52
+ "op": "add-env-key",
53
+ "file": "public/config/runtime-config.json",
54
+ "target": "shell",
55
+ "key": "clarityId",
56
+ "scope": "runtime",
57
+ "schema": "z.string().regex(/^([a-z0-9]{8,12})?$/)",
58
+ "value": "",
59
+ "when": {
60
+ "feature:env-t3": false
61
+ }
62
+ }
63
+ ]
64
+ }
@@ -0,0 +1,36 @@
1
+ import { providerName, resetAnalyticsForTests, updateConsent } from '@mfeflow/analytics';
2
+ import { afterEach, describe, expect, it } from 'vitest';
3
+ import { startClarity } from './clarity';
4
+
5
+ const clarityScript = () => document.querySelector('script[src*="clarity.ms/tag/"]');
6
+
7
+ afterEach(() => {
8
+ Reflect.deleteProperty(globalThis, '__MFE_RUNTIME_CONFIG__');
9
+ Reflect.deleteProperty(globalThis, 'clarity');
10
+ document.head.innerHTML = '';
11
+ resetAnalyticsForTests();
12
+ });
13
+
14
+ describe('startClarity', () => {
15
+ it('stays off without a clarityId', async () => {
16
+ Reflect.set(globalThis, '__MFE_RUNTIME_CONFIG__', { clarityId: '' });
17
+ await startClarity();
18
+ expect(providerName()).toBe('noop');
19
+ });
20
+
21
+ it('stays off when the id is not a Clarity project id', async () => {
22
+ Reflect.set(globalThis, '__MFE_RUNTIME_CONFIG__', { clarityId: 'GTM-ABC123' });
23
+ await startClarity();
24
+ expect(providerName()).toBe('noop');
25
+ });
26
+
27
+ it('waits for consent, then loads the script once', async () => {
28
+ Reflect.set(globalThis, '__MFE_RUNTIME_CONFIG__', { clarityId: 'k3x9m2abcd' });
29
+ await startClarity();
30
+ await startClarity();
31
+ expect(providerName()).toBe('clarity');
32
+ expect(clarityScript()).toBeNull();
33
+ updateConsent({ analytics_storage: 'granted' });
34
+ expect(document.querySelectorAll('script[src*="clarity.ms/tag/k3x9m2abcd"]')).toHaveLength(1);
35
+ });
36
+ });
@@ -0,0 +1,26 @@
1
+ import { addProvider } from '@mfeflow/analytics/providers';
2
+ import { createClarityProvider } from '@mfeflow/analytics/clarity';
3
+ import { fetchRuntimeConfig } from '@mfeflow/runtime';
4
+
5
+ /**
6
+ * Adds Clarity next to the other analytics providers, from the runtime config's `clarityId`. With no id it
7
+ * does nothing. Even with one, Clarity requests nothing until the visitor grants analytics consent.
8
+ */
9
+ export async function startClarity(): Promise<void> {
10
+ const config = await fetchRuntimeConfig().catch((error: unknown) => {
11
+ console.warn('[analytics] the runtime config did not load, so Clarity stays off', error);
12
+ return undefined;
13
+ });
14
+ const clarityId = config?.clarityId;
15
+ if (typeof clarityId !== 'string' || clarityId === '') return;
16
+ try {
17
+ addProvider(createClarityProvider({ projectId: clarityId }));
18
+ } catch (error) {
19
+ console.warn('[analytics] Clarity not started', error);
20
+ }
21
+ }
22
+
23
+ /** The boot step: never holds up the first render. */
24
+ export function initClarityFromConfig(): void {
25
+ void startClarity();
26
+ }
@@ -1,4 +1,4 @@
1
- import { initAnalytics } from '@mfeflow/analytics';
1
+ import { addProvider } from '@mfeflow/analytics/providers';
2
2
  import { createGtmProvider } from '@mfeflow/analytics/gtm';
3
3
  import { fetchRuntimeConfig } from '@mfeflow/runtime';
4
4
 
@@ -14,7 +14,7 @@ export async function startAnalytics(): Promise<void> {
14
14
  const gtmId = config?.gtmId;
15
15
  if (typeof gtmId !== 'string' || gtmId === '') return;
16
16
  try {
17
- initAnalytics(createGtmProvider({ containerId: gtmId }));
17
+ addProvider(createGtmProvider({ containerId: gtmId }));
18
18
  } catch (error) {
19
19
  console.warn('[analytics] not started', error);
20
20
  }
@@ -1,7 +1,7 @@
1
1
  # CI
2
2
 
3
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.
4
+ `pnpm mfeflow doctor`, `validate`, `check-compat` and `budget`, the runtime-config check and the Playwright smoke test.
5
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
6
  workflow. Every action is pinned to a commit SHA, the workflow starts from `permissions: {}`, and the job gets
7
7
  `contents: read` only. Dependabot or Renovate can keep the pins current.
@@ -39,13 +39,13 @@ jobs:
39
39
  run: pnpm run --if-present test
40
40
  - run: pnpm build
41
41
  - name: mfeflow doctor
42
- run: pnpm exec mfeflow doctor
42
+ run: pnpm mfeflow doctor
43
43
  - name: mfeflow validate
44
- run: pnpm exec mfeflow validate
44
+ run: pnpm mfeflow validate
45
45
  - name: mfeflow check-compat
46
- run: pnpm exec mfeflow check-compat
46
+ run: pnpm mfeflow check-compat
47
47
  - name: mfeflow budget
48
- run: pnpm exec mfeflow budget
48
+ run: pnpm mfeflow budget
49
49
  - name: Check runtime config
50
50
  run: pnpm run --if-present check:runtime-config
51
51
  - name: Install Playwright browser
@@ -0,0 +1,27 @@
1
+ {
2
+ "name": "commit-prompt",
3
+ "version": "0.1.0",
4
+ "description": "An interactive Conventional Commits prompt (czg): pnpm commit asks for the type, scope and message, then runs git commit. No emoji. Works with git-hooks, whose commitlint hook checks the same message.",
5
+ "requires": ["core"],
6
+ "files": [
7
+ {
8
+ "to": "cz.config.js",
9
+ "target": "root",
10
+ "from": "files/cz.config.js"
11
+ }
12
+ ],
13
+ "dependencies": [
14
+ {
15
+ "name": "czg",
16
+ "range": "1.14.0",
17
+ "target": "root",
18
+ "dev": true
19
+ }
20
+ ],
21
+ "scripts": [
22
+ {
23
+ "name": "commit",
24
+ "command": "czg"
25
+ }
26
+ ]
27
+ }
@@ -0,0 +1,27 @@
1
+ // `pnpm commit` asks for each part of a Conventional Commit, then runs `git commit`.
2
+ // The types are the ones commitlint's conventional config accepts, so the git-hooks check passes.
3
+ export default {
4
+ useEmoji: false,
5
+ // commitlint's conventional config rejects a capitalised subject, so the prompt lowercases it.
6
+ upperCaseSubject: false,
7
+ allowCustomScopes: true,
8
+ allowEmptyScopes: true,
9
+ allowBreakingChanges: ['feat', 'fix'],
10
+ maxHeaderLength: 100,
11
+ // Offered as a list; anything else can be typed. Add your apps and areas.
12
+ scopes: ['deps', 'ci'],
13
+ skipQuestions: ['footerPrefix'],
14
+ types: [
15
+ { value: 'feat', name: 'feat: A new feature' },
16
+ { value: 'fix', name: 'fix: A bug fix' },
17
+ { value: 'docs', name: 'docs: Documentation only' },
18
+ { value: 'style', name: 'style: Formatting, no change to behaviour' },
19
+ { value: 'refactor', name: 'refactor: Neither a fix nor a feature' },
20
+ { value: 'perf', name: 'perf: Makes it faster' },
21
+ { value: 'test', name: 'test: Add or fix tests' },
22
+ { value: 'build', name: 'build: Build system or dependencies' },
23
+ { value: 'ci', name: 'ci: CI configuration' },
24
+ { value: 'chore', name: 'chore: Anything else that is not in src or tests' },
25
+ { value: 'revert', name: 'revert: Revert an earlier commit' },
26
+ ],
27
+ };
@@ -0,0 +1,89 @@
1
+ {
2
+ "name": "data-react-query",
3
+ "version": "0.1.0",
4
+ "description": "Server state with TanStack Query as a shared singleton: the shell owns the QueryClient and its provider, every remote calls useQuery against the same cache, and each remote prefixes its query keys with its own name. The example query uses the http client when http is installed.",
5
+ "requires": ["core", "tsconfig"],
6
+ "files": [
7
+ {
8
+ "to": "src/query/Query.tsx",
9
+ "target": "shell",
10
+ "from": "files/shell-Query.tsx"
11
+ },
12
+ {
13
+ "to": "src/query/Query.test.tsx",
14
+ "target": "shell",
15
+ "from": "files/shell-Query-test.tsx",
16
+ "when": {
17
+ "feature:testing": true
18
+ }
19
+ },
20
+ {
21
+ "to": "src/query/Query.tsx",
22
+ "target": "remotes",
23
+ "from": "files/remote-Query.tsx"
24
+ },
25
+ {
26
+ "to": "src/query/keys.ts",
27
+ "target": "remotes",
28
+ "from": "files/keys.ts.eta",
29
+ "template": true
30
+ },
31
+ {
32
+ "to": "src/query/useHealth.ts",
33
+ "target": "remotes",
34
+ "from": "files/useHealth.http.ts.eta",
35
+ "template": true,
36
+ "when": {
37
+ "feature:http": true
38
+ }
39
+ },
40
+ {
41
+ "to": "src/query/useHealth.ts",
42
+ "target": "remotes",
43
+ "from": "files/useHealth.fetch.ts.eta",
44
+ "template": true,
45
+ "when": {
46
+ "feature:http": false
47
+ }
48
+ },
49
+ {
50
+ "to": "src/query/Query.test.tsx",
51
+ "target": "remotes",
52
+ "from": "files/remote-Query-test.tsx.eta",
53
+ "template": true,
54
+ "when": {
55
+ "feature:testing": true
56
+ }
57
+ }
58
+ ],
59
+ "dependencies": [
60
+ {
61
+ "name": "@tanstack/react-query",
62
+ "range": "5.105.0",
63
+ "target": "shell"
64
+ },
65
+ {
66
+ "name": "@tanstack/react-query",
67
+ "range": "5.105.0",
68
+ "target": "remotes"
69
+ }
70
+ ],
71
+ "transforms": [
72
+ {
73
+ "op": "wrap-provider",
74
+ "file": "src/App.tsx",
75
+ "target": "shell",
76
+ "component": "Query",
77
+ "from": "./query/Query",
78
+ "function": "App"
79
+ },
80
+ {
81
+ "op": "wrap-provider",
82
+ "file": "src/exposed/App.tsx",
83
+ "target": "remotes",
84
+ "component": "Query",
85
+ "from": "../query/Query",
86
+ "function": "App"
87
+ }
88
+ ]
89
+ }
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Every query key of this remote starts with its name, so it never meets another remote's key in the shared cache,
3
+ * and `queryClient.invalidateQueries({ queryKey: queryKeys.all })` refreshes just this remote.
4
+ */
5
+ export const queryKeys = {
6
+ all: ['<%= it.app.name %>'] as const,
7
+ health: () => [...queryKeys.all, 'health'] as const,
8
+ };
@@ -0,0 +1,39 @@
1
+ import { render, screen } from '@testing-library/react';
2
+ import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
3
+ import { describe, expect, it } from 'vitest';
4
+ import { queryKeys } from './keys';
5
+ import { Query } from './Query';
6
+ import { useHealth } from './useHealth';
7
+
8
+ function Health() {
9
+ const { data } = useHealth();
10
+ return <p>{data ? `ok: ${String(data.ok)}` : 'loading'}</p>;
11
+ }
12
+
13
+ describe('the <%= it.app.name %> queries', () => {
14
+ it('start with the remote name', () => {
15
+ expect(queryKeys.health()[0]).toBe('<%= it.app.name %>');
16
+ });
17
+
18
+ it('run on their own, without the shell', async () => {
19
+ render(
20
+ <Query>
21
+ <Health />
22
+ </Query>,
23
+ );
24
+ expect(await screen.findByText('ok: true')).toBeDefined();
25
+ });
26
+
27
+ it('use the shell client when there is one', async () => {
28
+ const shell = new QueryClient();
29
+ render(
30
+ <QueryClientProvider client={shell}>
31
+ <Query>
32
+ <Health />
33
+ </Query>
34
+ </QueryClientProvider>,
35
+ );
36
+ await screen.findByText('ok: true');
37
+ expect(shell.getQueryData(queryKeys.health())).toEqual({ ok: true });
38
+ });
39
+ });
@@ -0,0 +1,15 @@
1
+ import { useContext, type ReactNode } from 'react';
2
+ import { QueryClient, QueryClientContext, QueryClientProvider } from '@tanstack/react-query';
3
+
4
+ let standalone: QueryClient | undefined;
5
+
6
+ /**
7
+ * Inside the shell this does nothing: the shell's client is already in context. When the remote runs on its own
8
+ * (`pnpm dev` in its folder) there is no shell, so it provides a client of its own.
9
+ */
10
+ export function Query({ children }: { children: ReactNode }) {
11
+ const inShell = useContext(QueryClientContext) !== undefined;
12
+ if (inShell) return children;
13
+ standalone ??= new QueryClient();
14
+ return <QueryClientProvider client={standalone}>{children}</QueryClientProvider>;
15
+ }
@@ -0,0 +1,21 @@
1
+ import { render, screen } from '@testing-library/react';
2
+ import { useQuery } from '@tanstack/react-query';
3
+ import { describe, expect, it } from 'vitest';
4
+ import { Query, queryClient } from './Query';
5
+
6
+ function Value() {
7
+ const { data } = useQuery({ queryKey: ['shell', 'answer'], queryFn: () => Promise.resolve(42) });
8
+ return <p>{data ?? 'loading'}</p>;
9
+ }
10
+
11
+ describe('Query', () => {
12
+ it('gives the app one cache', async () => {
13
+ render(
14
+ <Query>
15
+ <Value />
16
+ </Query>,
17
+ );
18
+ expect(await screen.findByText('42')).toBeDefined();
19
+ expect(queryClient.getQueryData(['shell', 'answer'])).toBe(42);
20
+ });
21
+ });
@@ -0,0 +1,14 @@
1
+ import type { ReactNode } from 'react';
2
+ import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
3
+
4
+ /**
5
+ * The page's one cache. Remotes call `useQuery` against it because `@tanstack/react-query` is a shared
6
+ * singleton, so they find this client through context. Tune the defaults here.
7
+ */
8
+ export const queryClient = new QueryClient({
9
+ defaultOptions: { queries: { staleTime: 30_000, retry: 1, refetchOnWindowFocus: false } },
10
+ });
11
+
12
+ export function Query({ children }: { children: ReactNode }) {
13
+ return <QueryClientProvider client={queryClient}>{children}</QueryClientProvider>;
14
+ }
@@ -0,0 +1,15 @@
1
+ import { useQuery } from '@tanstack/react-query';
2
+ import { queryKeys } from './keys';
3
+
4
+ /** An example query with plain fetch. With the `http` feature this file uses its client instead. */
5
+ export function useHealth() {
6
+ return useQuery({
7
+ queryKey: queryKeys.health(),
8
+ queryFn: async ({ signal }) => {
9
+ const response = await fetch(new URL('/api/health', location.origin), { signal });
10
+ if (!response.ok) throw new Error(`health check failed (${String(response.status)})`);
11
+ const body: unknown = await response.json();
12
+ return { ok: typeof body === 'object' && body !== null && Reflect.get(body, 'ok') === true };
13
+ },
14
+ });
15
+ }
@@ -0,0 +1,11 @@
1
+ import { useQuery } from '@tanstack/react-query';
2
+ import { api } from '../api/client';
3
+ import { queryKeys } from './keys';
4
+
5
+ /** An example query: the API's health check through the http client, cancelled when the component unmounts. */
6
+ export function useHealth() {
7
+ return useQuery({
8
+ queryKey: queryKeys.health(),
9
+ queryFn: ({ signal }) => api.get('health', { signal }),
10
+ });
11
+ }
@@ -373,6 +373,48 @@
373
373
  "when": {
374
374
  "feature:telemetry-otel": true
375
375
  }
376
+ },
377
+ {
378
+ "op": "add-env-key",
379
+ "file": "src/config/schema.ts",
380
+ "target": "shell",
381
+ "key": "apiUrl",
382
+ "scope": "runtime",
383
+ "schema": {
384
+ "zod": "z.string().min(1)",
385
+ "valibot": "v.pipe(v.string(), v.minLength(1))",
386
+ "arktype": "'string > 0'"
387
+ },
388
+ "value": "/api",
389
+ "configFiles": [
390
+ "public/config/runtime-config.dev.json",
391
+ "public/config/runtime-config.staging.json",
392
+ "public/config/runtime-config.prod.json"
393
+ ],
394
+ "when": {
395
+ "feature:http": true
396
+ }
397
+ },
398
+ {
399
+ "op": "add-env-key",
400
+ "file": "src/config/schema.ts",
401
+ "target": "shell",
402
+ "key": "clarityId",
403
+ "scope": "runtime",
404
+ "schema": {
405
+ "zod": "z.string().regex(/^([a-z0-9]{8,12})?$/)",
406
+ "valibot": "v.pipe(v.string(), v.regex(/^([a-z0-9]{8,12})?$/))",
407
+ "arktype": "'/^([a-z0-9]{8,12})?$/'"
408
+ },
409
+ "value": "",
410
+ "configFiles": [
411
+ "public/config/runtime-config.dev.json",
412
+ "public/config/runtime-config.staging.json",
413
+ "public/config/runtime-config.prod.json"
414
+ ],
415
+ "when": {
416
+ "feature:analytics-clarity": true
417
+ }
376
418
  }
377
419
  ]
378
420
  }
@@ -2,7 +2,7 @@ import { createEnv } from '@t3-oss/env-core';
2
2
  import { type } from 'arktype';
3
3
 
4
4
  // Server and build-time variables, checked when a process that imports this file starts.
5
- // Add one: mfeflow add env-key API_ORIGIN --scope server --schema "type('string.url')"
5
+ // Add one: pnpm mfeflow add env-key API_ORIGIN --scope server --schema "type('string.url')"
6
6
  export const env = createEnv({
7
7
  server: {
8
8
  NODE_ENV: type("'development' | 'production' | 'test' | undefined"),
@@ -2,7 +2,7 @@ import { createEnv } from '@t3-oss/env-core';
2
2
  import * as v from 'valibot';
3
3
 
4
4
  // Server and build-time variables, checked when a process that imports this file starts.
5
- // Add one: mfeflow add env-key API_ORIGIN --scope server --schema "v.pipe(v.string(), v.url())"
5
+ // Add one: pnpm mfeflow add env-key API_ORIGIN --scope server --schema "v.pipe(v.string(), v.url())"
6
6
  export const env = createEnv({
7
7
  server: {
8
8
  NODE_ENV: v.optional(v.picklist(['development', 'production', 'test'])),
@@ -2,7 +2,7 @@ import { createEnv } from '@t3-oss/env-core';
2
2
  import { z } from 'zod';
3
3
 
4
4
  // Server and build-time variables, checked when a process that imports this file starts.
5
- // Add one: mfeflow add env-key API_ORIGIN --scope server --schema "z.url()"
5
+ // Add one: pnpm mfeflow add env-key API_ORIGIN --scope server --schema "z.url()"
6
6
  export const env = createEnv({
7
7
  server: {
8
8
  NODE_ENV: z.enum(['development', 'production', 'test']).optional(),
@@ -2,7 +2,7 @@ import { type } from 'arktype';
2
2
 
3
3
  // The shape of public/config/runtime-config.<env>.json. The browser only imports this file's type
4
4
  // (see ./runtime), so the validator stays out of the bundle; `pnpm check:runtime-config` validates the files.
5
- // Add a key: mfeflow add env-key GTM_ID --scope runtime --schema "type('string > 0')"
5
+ // Add a key: pnpm mfeflow add env-key GTM_ID --scope runtime --schema "type('string > 0')"
6
6
  export const runtimeConfigSchema = type({
7
7
  environment: "'dev' | 'staging' | 'prod'",
8
8
  });
@@ -2,7 +2,7 @@ import * as v from 'valibot';
2
2
 
3
3
  // The shape of public/config/runtime-config.<env>.json. The browser only imports this file's type
4
4
  // (see ./runtime), so the validator stays out of the bundle; `pnpm check:runtime-config` validates the files.
5
- // Add a key: mfeflow add env-key GTM_ID --scope runtime --schema "v.pipe(v.string(), v.minLength(1))"
5
+ // Add a key: pnpm mfeflow add env-key GTM_ID --scope runtime --schema "v.pipe(v.string(), v.minLength(1))"
6
6
  export const runtimeConfigSchema = v.object({
7
7
  environment: v.picklist(['dev', 'staging', 'prod']),
8
8
  });
@@ -2,7 +2,7 @@ import { z } from 'zod';
2
2
 
3
3
  // The shape of public/config/runtime-config.<env>.json. The browser only imports this file's type
4
4
  // (see ./runtime), so the validator stays out of the bundle; `pnpm check:runtime-config` validates the files.
5
- // Add a key: mfeflow add env-key GTM_ID --scope runtime --schema "z.string().min(1)"
5
+ // Add a key: pnpm mfeflow add env-key GTM_ID --scope runtime --schema "z.string().min(1)"
6
6
  export const runtimeConfigSchema = z.object({
7
7
  environment: z.enum(['dev', 'staging', 'prod']),
8
8
  });
@@ -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
+ }