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,22 @@
1
+ <%_ const prefix = it.app.tailwindPrefix _%>
2
+ /* The shell is the only app that loads Tailwind's reset (preflight). */
3
+ @layer theme, base, components, utilities;
4
+ @import 'tailwindcss/theme.css' layer(theme) prefix(<%= prefix %>);
5
+ @import 'tailwindcss/preflight.css' layer(base);
6
+ @import 'tailwindcss/utilities.css' layer(utilities) prefix(<%= prefix %>);
7
+
8
+ /* Tokens read the theme variables (--mf-*). The second value is used when there is no theme. */
9
+ @theme inline {
10
+ --color-background: var(--mf-color-bg, #ffffff);
11
+ --color-foreground: var(--mf-color-fg, #111827);
12
+ --color-surface: var(--mf-color-surface, #f3f4f6);
13
+ --color-primary: var(--mf-color-primary, #2563eb);
14
+ --color-primary-foreground: var(--mf-color-primary-fg, #ffffff);
15
+ --color-secondary: var(--mf-color-surface, #f3f4f6);
16
+ --color-secondary-foreground: var(--mf-color-fg, #111827);
17
+ --color-accent: var(--mf-color-surface, #f3f4f6);
18
+ --color-accent-foreground: var(--mf-color-fg, #111827);
19
+ --color-destructive: #dc2626;
20
+ --color-border: var(--mf-color-border, #e5e7eb);
21
+ --color-ring: var(--mf-color-primary, #2563eb);
22
+ }
@@ -1,8 +1,17 @@
1
1
  import { defineConfig } from 'vitest/config';
2
2
 
3
3
  export default defineConfig({
4
- // One copy of React and Testing Library, also when a package like @mfeflow/testing is linked from another folder.
5
- resolve: { dedupe: ['react', 'react-dom', '@testing-library/react', '@testing-library/dom'] },
4
+ // One copy of React, i18next and Testing Library, also when a package like @mfeflow/testing is linked from another folder.
5
+ resolve: {
6
+ dedupe: [
7
+ 'react',
8
+ 'react-dom',
9
+ 'react-i18next',
10
+ 'i18next',
11
+ '@testing-library/react',
12
+ '@testing-library/dom',
13
+ ],
14
+ },
6
15
  test: {
7
16
  environment: 'jsdom',
8
17
  include: ['src/**/*.test.{ts,tsx}'],
@@ -0,0 +1,103 @@
1
+ {
2
+ "name": "theming",
3
+ "version": "0.1.0",
4
+ "description": "Light and dark themes as CSS variables (@mfeflow/theme): the shell owns the provider and the persisted choice, every remote reads the same --mf-* tokens with no theme code of its own. The stored theme is set by an inline script in the shell's <head>, before any bundle loads, so a reload never paints the wrong colours.",
5
+ "requires": ["core", "tsconfig"],
6
+ "files": [
7
+ {
8
+ "to": "src/theme/themes.ts",
9
+ "target": "shell",
10
+ "from": "files/themes.ts"
11
+ },
12
+ {
13
+ "to": "src/theme/Theme.tsx",
14
+ "target": "shell",
15
+ "from": "files/Theme.tsx"
16
+ },
17
+ {
18
+ "to": "src/theme/ThemeToggle.tsx",
19
+ "target": "shell",
20
+ "from": "files/ThemeToggle.tsx"
21
+ },
22
+ {
23
+ "to": "src/theme/theme.css",
24
+ "target": "shell",
25
+ "from": "files/theme.css"
26
+ },
27
+ {
28
+ "to": "src/theme/apply-stored.ts",
29
+ "target": "shell",
30
+ "from": "files/apply-stored.ts"
31
+ },
32
+ {
33
+ "to": "src/theme/head-script.ts",
34
+ "target": "shell",
35
+ "from": "files/head-script.ts"
36
+ },
37
+ {
38
+ "to": "src/theme/apply-stored.test.ts",
39
+ "target": "shell",
40
+ "from": "files/apply-stored-test.ts",
41
+ "when": {
42
+ "feature:testing": true
43
+ }
44
+ },
45
+ {
46
+ "to": "src/theme/ThemeToggle.test.tsx",
47
+ "target": "shell",
48
+ "from": "files/theme-toggle-test.tsx",
49
+ "when": {
50
+ "feature:testing": true
51
+ }
52
+ },
53
+ {
54
+ "to": "src/theme/tokens.ts",
55
+ "target": "remotes",
56
+ "from": "files/tokens.ts"
57
+ },
58
+ {
59
+ "to": "src/theme/tokens.test.ts",
60
+ "target": "remotes",
61
+ "from": "files/tokens-test.ts",
62
+ "when": {
63
+ "feature:testing": true
64
+ }
65
+ }
66
+ ],
67
+ "dependencies": [
68
+ {
69
+ "name": "@mfeflow/theme",
70
+ "range": "latest",
71
+ "target": "shell"
72
+ },
73
+ {
74
+ "name": "@mfeflow/theme",
75
+ "range": "latest",
76
+ "target": "remotes"
77
+ }
78
+ ],
79
+ "transforms": [
80
+ {
81
+ "op": "add-import",
82
+ "file": "rsbuild.config.ts",
83
+ "target": "shell",
84
+ "from": "./src/theme/head-script"
85
+ },
86
+ {
87
+ "op": "add-boot-step",
88
+ "file": "src/bootstrap.tsx",
89
+ "target": "shell",
90
+ "call": "applyStoredTheme",
91
+ "from": "./theme/apply-stored",
92
+ "await": false
93
+ },
94
+ {
95
+ "op": "wrap-provider",
96
+ "file": "src/App.tsx",
97
+ "target": "shell",
98
+ "component": "Theme",
99
+ "from": "./theme/Theme",
100
+ "function": "App"
101
+ }
102
+ ]
103
+ }
@@ -0,0 +1,15 @@
1
+ import type { ReactNode } from 'react';
2
+ import { ThemeProvider } from '@mfeflow/theme/react';
3
+ import { ThemeToggle } from './ThemeToggle';
4
+ import { defaultTheme, followSystem, themes } from './themes';
5
+ import './theme.css';
6
+
7
+ /** Wraps the shell. The choice is stored under the shell's storage, so every tab and reload agrees. */
8
+ export function Theme({ children }: { children: ReactNode }) {
9
+ return (
10
+ <ThemeProvider themes={themes} defaultTheme={defaultTheme} followSystem={followSystem}>
11
+ {children}
12
+ <ThemeToggle />
13
+ </ThemeProvider>
14
+ );
15
+ }
@@ -0,0 +1,23 @@
1
+ import { useTheme } from '@mfeflow/theme/react';
2
+
3
+ /** Your component: move it into the header, restyle it, or swap it for a menu. */
4
+ export function ThemeToggle() {
5
+ const { theme, themes, setTheme } = useTheme();
6
+ return (
7
+ <label className="theme-toggle">
8
+ Theme{' '}
9
+ <select
10
+ value={theme.name}
11
+ onChange={(event) => {
12
+ setTheme(event.target.value);
13
+ }}
14
+ >
15
+ {themes.map((name) => (
16
+ <option key={name} value={name}>
17
+ {name}
18
+ </option>
19
+ ))}
20
+ </select>
21
+ </label>
22
+ );
23
+ }
@@ -0,0 +1,37 @@
1
+ import { afterEach, describe, expect, it } from 'vitest';
2
+ import { applyStoredTheme, currentStartingTheme } from './apply-stored';
3
+
4
+ afterEach(() => {
5
+ localStorage.clear();
6
+ document.documentElement.removeAttribute('data-theme');
7
+ });
8
+
9
+ describe('applyStoredTheme', () => {
10
+ it('uses the default theme when nothing is stored', () => {
11
+ expect(currentStartingTheme().name).toBe('light');
12
+ });
13
+
14
+ it('puts the stored theme on <html> with its variables', () => {
15
+ localStorage.setItem('shell:theme', JSON.stringify('dark'));
16
+ applyStoredTheme();
17
+ expect(document.documentElement.dataset.theme).toBe('dark');
18
+ expect(document.documentElement.style.getPropertyValue('--mf-color-bg')).not.toBe('');
19
+ });
20
+
21
+ it('ignores a stored value that is not a theme', () => {
22
+ localStorage.setItem('shell:theme', '{broken');
23
+ expect(currentStartingTheme().name).toBe('light');
24
+ localStorage.setItem('shell:theme', JSON.stringify('purple'));
25
+ expect(currentStartingTheme().name).toBe('light');
26
+ });
27
+
28
+ it.each(['constructor', '__proto__', 'toString'])(
29
+ 'ignores a stored name that only exists on every object: %s',
30
+ (name) => {
31
+ localStorage.setItem('shell:theme', JSON.stringify(name));
32
+ expect(currentStartingTheme().name).toBe('light');
33
+ applyStoredTheme();
34
+ expect(document.documentElement.dataset.theme).toBe('light');
35
+ },
36
+ );
37
+ });
@@ -0,0 +1,13 @@
1
+ import { applyTheme, type Theme } from '@mfeflow/theme';
2
+ import { startingTheme } from '@mfeflow/theme/starting';
3
+ import { defaultTheme, followSystem, themes } from './themes';
4
+
5
+ /** The theme to show before React renders: the stored choice, else the system preference (if followSystem), else the default. */
6
+ export function currentStartingTheme(): Theme {
7
+ return startingTheme(themes, { defaultTheme, followSystem });
8
+ }
9
+
10
+ /** The first boot step: the variables are on <html> before the first render, also when the head script was blocked. */
11
+ export function applyStoredTheme(): void {
12
+ applyTheme(currentStartingTheme());
13
+ }
@@ -0,0 +1,8 @@
1
+ import { registerHeadScript } from '@mfeflow/rsbuild';
2
+ import { noFlashScript } from '@mfeflow/theme';
3
+ import { defaultTheme, followSystem, themes } from './themes';
4
+
5
+ // Imported by rsbuild.config.ts. The script goes first in the shell's <head>, so the stored theme is on <html>
6
+ // before any bundle has loaded. It needs the CSP to allow an inline script (a nonce or 'unsafe-inline');
7
+ // without that, apply-stored.ts still sets the theme before the first render.
8
+ registerHeadScript('theme', noFlashScript(themes, { defaultTheme, followSystem }));
@@ -0,0 +1,18 @@
1
+ import { fireEvent, screen } from '@testing-library/react';
2
+ import { renderWithPlatform } from '@mfeflow/testing';
3
+ import { describe, expect, it } from 'vitest';
4
+ import { Theme } from './Theme';
5
+
6
+ describe('Theme', () => {
7
+ it('switches the theme on <html> and remembers it', () => {
8
+ renderWithPlatform(
9
+ <Theme>
10
+ <p>content</p>
11
+ </Theme>,
12
+ );
13
+ expect(document.documentElement.dataset.theme).toBe('light');
14
+ fireEvent.change(screen.getByRole('combobox'), { target: { value: 'dark' } });
15
+ expect(document.documentElement.dataset.theme).toBe('dark');
16
+ expect(localStorage.getItem('shell:theme')).toBe('"dark"');
17
+ });
18
+ });
@@ -0,0 +1,14 @@
1
+ html {
2
+ background: var(--mf-color-bg, #ffffff);
3
+ color: var(--mf-color-fg, #111827);
4
+ }
5
+
6
+ .theme-toggle {
7
+ position: fixed;
8
+ right: 1rem;
9
+ bottom: 1rem;
10
+ padding: 0.25rem 0.5rem;
11
+ border-radius: 0.5rem;
12
+ background: var(--mf-color-surface, #f3f4f6);
13
+ color: var(--mf-color-fg, #111827);
14
+ }
@@ -0,0 +1,30 @@
1
+ import { defineTheme } from '@mfeflow/theme';
2
+
3
+ // Every token becomes a --mf-<token> variable on <html>. Remotes read them with var(--mf-color-fg).
4
+ export const themes = [
5
+ defineTheme('light', {
6
+ 'color-bg': '#ffffff',
7
+ 'color-fg': '#111827',
8
+ 'color-surface': '#f3f4f6',
9
+ 'color-primary': '#2563eb',
10
+ 'color-primary-fg': '#ffffff',
11
+ 'color-border': '#e5e7eb',
12
+ }),
13
+ defineTheme(
14
+ 'dark',
15
+ {
16
+ 'color-bg': '#0b1020',
17
+ 'color-fg': '#e5e7eb',
18
+ 'color-surface': '#1f2937',
19
+ 'color-primary': '#60a5fa',
20
+ 'color-primary-fg': '#0b1020',
21
+ 'color-border': '#374151',
22
+ },
23
+ { colorScheme: 'dark' },
24
+ ),
25
+ ];
26
+
27
+ export const defaultTheme = 'light';
28
+
29
+ // Use the system dark-mode setting when no theme is stored. The provider, the head script and the boot step all read this.
30
+ export const followSystem = true;
@@ -0,0 +1,9 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { tokens } from './tokens';
3
+
4
+ describe('tokens', () => {
5
+ it('are the shell variables, with a fallback for standalone runs', () => {
6
+ expect(tokens.primary).toBe('var(--mf-color-primary, #2563eb)');
7
+ expect(tokens.fg).toContain('--mf-color-fg');
8
+ });
9
+ });
@@ -0,0 +1,9 @@
1
+ import { cssVar } from '@mfeflow/theme';
2
+
3
+ // The shell sets these on <html>. The second argument is what a standalone run (no shell) falls back to.
4
+ export const tokens = {
5
+ bg: cssVar('color-bg', '#ffffff'),
6
+ fg: cssVar('color-fg', '#111827'),
7
+ surface: cssVar('color-surface', '#f3f4f6'),
8
+ primary: cssVar('color-primary', '#2563eb'),
9
+ };
@@ -0,0 +1,61 @@
1
+ {
2
+ "name": "ui-shadcn",
3
+ "version": "0.1.0",
4
+ "description": "shadcn/ui in every remote, on top of tailwind: components.json, the cn helper and a Button, copied into the remote so you own them. The classes carry the remote's Tailwind prefix and the colours come from the theme. Nothing is shared between remotes.",
5
+ "requires": ["core", "tsconfig", "tailwind"],
6
+ "files": [
7
+ {
8
+ "to": "components.json",
9
+ "target": "remotes",
10
+ "from": "files/components.json.eta",
11
+ "template": true
12
+ },
13
+ {
14
+ "to": "src/lib/utils.ts",
15
+ "target": "remotes",
16
+ "from": "files/utils.ts.eta",
17
+ "template": true
18
+ },
19
+ {
20
+ "to": "src/components/ui/button.tsx",
21
+ "target": "remotes",
22
+ "from": "files/button.tsx.eta",
23
+ "template": true
24
+ },
25
+ {
26
+ "to": "src/lib/utils.test.ts",
27
+ "target": "remotes",
28
+ "from": "files/utils-test.ts.eta",
29
+ "template": true,
30
+ "when": {
31
+ "feature:testing": true
32
+ }
33
+ },
34
+ {
35
+ "to": "src/components/ui/button.test.tsx",
36
+ "target": "remotes",
37
+ "from": "files/button-test.tsx.eta",
38
+ "template": true,
39
+ "when": {
40
+ "feature:testing": true
41
+ }
42
+ }
43
+ ],
44
+ "dependencies": [
45
+ {
46
+ "name": "class-variance-authority",
47
+ "range": "0.7.1",
48
+ "target": "remotes"
49
+ },
50
+ {
51
+ "name": "clsx",
52
+ "range": "2.1.1",
53
+ "target": "remotes"
54
+ },
55
+ {
56
+ "name": "tailwind-merge",
57
+ "range": "3.7.0",
58
+ "target": "remotes"
59
+ }
60
+ ]
61
+ }
@@ -0,0 +1,32 @@
1
+ <%_ const prefix = it.app.tailwindPrefix _%>
2
+ import { fireEvent, render, screen } from '@testing-library/react';
3
+ import { describe, expect, it, vi } from 'vitest';
4
+ import { Button } from './button';
5
+
6
+ const prefix = '<%= prefix %>';
7
+
8
+ describe('Button', () => {
9
+ it('is a button that does not submit a form unless asked', () => {
10
+ render(<Button>Save</Button>);
11
+ expect(screen.getByRole('button', { name: 'Save' }).getAttribute('type')).toBe('button');
12
+ });
13
+
14
+ it('carries the remote prefix on its classes, and lets a caller override one', () => {
15
+ render(<Button className={`${prefix}:px-8`}>Save</Button>);
16
+ const { className } = screen.getByRole('button');
17
+ expect(className).toContain(`${prefix}:bg-primary`);
18
+ expect(className).toContain(`${prefix}:px-8`);
19
+ expect(className).not.toContain(`${prefix}:px-4`);
20
+ });
21
+
22
+ it('does not click while disabled', () => {
23
+ const onClick = vi.fn();
24
+ render(
25
+ <Button disabled onClick={onClick}>
26
+ Save
27
+ </Button>,
28
+ );
29
+ fireEvent.click(screen.getByRole('button'));
30
+ expect(onClick).not.toHaveBeenCalled();
31
+ });
32
+ });
@@ -0,0 +1,36 @@
1
+ <%_ const prefix = it.app.tailwindPrefix _%>
2
+ import { cva, type VariantProps } from 'class-variance-authority';
3
+ import type { ButtonHTMLAttributes } from 'react';
4
+ import { cn } from '../../lib/utils';
5
+
6
+ // Every class starts with <%= prefix %>: (this remote's Tailwind prefix). Colours are the theme tokens from src/styles.css.
7
+ // prettier-ignore
8
+ const buttonVariants = cva(
9
+ '<%= prefix %>:inline-flex <%= prefix %>:items-center <%= prefix %>:justify-center <%= prefix %>:gap-2 <%= prefix %>:rounded-md <%= prefix %>:text-sm <%= prefix %>:font-medium <%= prefix %>:transition-colors <%= prefix %>:disabled:pointer-events-none <%= prefix %>:disabled:opacity-50 <%= prefix %>:focus-visible:outline-2 <%= prefix %>:focus-visible:outline-offset-2 <%= prefix %>:focus-visible:outline-ring',
10
+ {
11
+ variants: {
12
+ variant: {
13
+ default: '<%= prefix %>:bg-primary <%= prefix %>:text-primary-foreground <%= prefix %>:hover:opacity-90',
14
+ secondary: '<%= prefix %>:bg-secondary <%= prefix %>:text-secondary-foreground <%= prefix %>:hover:opacity-80',
15
+ outline: '<%= prefix %>:border <%= prefix %>:border-border <%= prefix %>:bg-background <%= prefix %>:hover:bg-accent <%= prefix %>:hover:text-accent-foreground',
16
+ destructive: '<%= prefix %>:bg-destructive <%= prefix %>:text-white <%= prefix %>:hover:opacity-90',
17
+ ghost: '<%= prefix %>:hover:bg-accent <%= prefix %>:hover:text-accent-foreground',
18
+ },
19
+ size: {
20
+ default: '<%= prefix %>:h-9 <%= prefix %>:px-4 <%= prefix %>:py-2',
21
+ sm: '<%= prefix %>:h-8 <%= prefix %>:px-3',
22
+ lg: '<%= prefix %>:h-10 <%= prefix %>:px-6',
23
+ },
24
+ },
25
+ defaultVariants: { variant: 'default', size: 'default' },
26
+ },
27
+ );
28
+
29
+ export interface ButtonProps
30
+ extends ButtonHTMLAttributes<HTMLButtonElement>, VariantProps<typeof buttonVariants> {}
31
+
32
+ export function Button({ className, variant, size, type = 'button', ...props }: ButtonProps) {
33
+ return (
34
+ <button type={type} className={cn(buttonVariants({ variant, size }), className)} {...props} />
35
+ );
36
+ }
@@ -0,0 +1,22 @@
1
+ <%_ const prefix = it.app.tailwindPrefix _%>
2
+ {
3
+ "$schema": "https://ui.shadcn.com/schema.json",
4
+ "style": "new-york",
5
+ "rsc": false,
6
+ "tsx": true,
7
+ "tailwind": {
8
+ "config": "",
9
+ "css": "src/styles.css",
10
+ "baseColor": "neutral",
11
+ "cssVariables": false,
12
+ "prefix": "<%= prefix %>"
13
+ },
14
+ "aliases": {
15
+ "components": "@/components",
16
+ "ui": "@/components/ui",
17
+ "lib": "@/lib",
18
+ "utils": "@/lib/utils",
19
+ "hooks": "@/hooks"
20
+ },
21
+ "iconLibrary": "lucide"
22
+ }
@@ -0,0 +1,17 @@
1
+ <%_ const prefix = it.app.tailwindPrefix _%>
2
+ import { describe, expect, it } from 'vitest';
3
+ import { cn } from './utils';
4
+
5
+ const prefix = '<%= prefix %>';
6
+
7
+ describe('cn', () => {
8
+ it("keeps the last of two conflicting classes with this remote's prefix", () => {
9
+ expect(cn(`${prefix}:p-2`, `${prefix}:p-4`)).toBe(`${prefix}:p-4`);
10
+ });
11
+
12
+ it('skips false values and keeps classes that do not conflict', () => {
13
+ expect(cn(`${prefix}:flex`, false, undefined, `${prefix}:gap-2`)).toBe(
14
+ `${prefix}:flex ${prefix}:gap-2`,
15
+ );
16
+ });
17
+ });
@@ -0,0 +1,11 @@
1
+ <%_ const prefix = it.app.tailwindPrefix _%>
2
+ import { clsx, type ClassValue } from 'clsx';
3
+ import { extendTailwindMerge } from 'tailwind-merge';
4
+
5
+ // Knows this remote's prefix, so cn('<%= prefix %>:p-2', '<%= prefix %>:p-4') keeps only the last one.
6
+ const merge = extendTailwindMerge({ prefix: '<%= prefix %>' });
7
+
8
+ /** Joins class names and drops the ones a later class overrides. */
9
+ export function cn(...inputs: ClassValue[]): string {
10
+ return merge(clsx(inputs));
11
+ }
@@ -4,9 +4,20 @@ export const endpoints = defineEndpoints({
4
4
  health: { service: 'api', path: '/health' },
5
5
  });
6
6
 
7
- const services: Record<string, string> = { api: '/api' };
7
+ const defaults: Record<string, string> = { api: '/api' };
8
+
9
+ // A service's base is `<service>Url` in the runtime config (apiUrl for api) when the shell loaded
10
+ // one, so a build runs in every environment; the default above is for tests and standalone runs.
11
+ function configured(service: string): string | undefined {
12
+ const config: unknown = Reflect.get(globalThis, '__MFE_RUNTIME_CONFIG__');
13
+ const value: unknown =
14
+ typeof config === 'object' && config !== null
15
+ ? Reflect.get(config, `${service}Url`)
16
+ : undefined;
17
+ return typeof value === 'string' && value !== '' ? value : undefined;
18
+ }
8
19
 
9
20
  export const apiUrl = createUrlFactory(endpoints, (service) => {
10
- const base = services[service];
21
+ const base = configured(service) ?? defaults[service];
11
22
  return base === undefined ? undefined : new URL(base, location.origin).toString();
12
23
  });
@@ -164,6 +164,12 @@
164
164
  "target": "root",
165
165
  "dev": true
166
166
  },
167
+ {
168
+ "name": "mfeflow",
169
+ "range": "latest",
170
+ "target": "root",
171
+ "dev": true
172
+ },
167
173
  {
168
174
  "name": "@mfeflow/runtime",
169
175
  "range": "latest",
@@ -271,6 +277,26 @@
271
277
  "command": "turbo run build",
272
278
  "target": "root"
273
279
  },
280
+ {
281
+ "name": "doctor",
282
+ "command": "mfeflow doctor",
283
+ "target": "root"
284
+ },
285
+ {
286
+ "name": "validate",
287
+ "command": "mfeflow validate",
288
+ "target": "root"
289
+ },
290
+ {
291
+ "name": "check-compat",
292
+ "command": "mfeflow check-compat",
293
+ "target": "root"
294
+ },
295
+ {
296
+ "name": "budget",
297
+ "command": "mfeflow budget",
298
+ "target": "root"
299
+ },
274
300
  {
275
301
  "name": "dev",
276
302
  "command": "rsbuild dev",
@@ -4,14 +4,14 @@ Guidance for coding agents working in this repo.
4
4
 
5
5
  - Package manager: pnpm (via corepack). Never npm or yarn. Versions live in the `catalog` of
6
6
  `pnpm-workspace.yaml`; add dependencies there and reference them as `catalog:`.
7
- - `pnpm dev` runs the shell and every remote. `pnpm build` builds all of them. `mfeflow dev` does the same with
7
+ - `pnpm dev` runs the shell and every remote. `pnpm build` builds all of them. `pnpm mfeflow dev` does the same with
8
8
  per-app logs and picks up new remotes without a restart.
9
- - Before finishing: `mfeflow doctor` (and `mfeflow validate`, `mfeflow check-compat`, `mfeflow budget` after a build) must exit 0.
9
+ - Before finishing: `pnpm doctor` (and `pnpm validate`, `pnpm check-compat`, `pnpm budget` after a build) must exit 0.
10
10
  - TypeScript 7, strict. Relative imports have no file extension (`./plan`, not `./plan.js`).
11
11
  - Each remote is deployed on its own. A remote must not import from another remote or from
12
12
  the shell; talk through the registry, events or URLs.
13
13
  - A remote's public surface is `src/exposed/`. Everything else is private to it.
14
- - Add apps and features with the `mfeflow` CLI (`mfeflow add remote <name>`, `mfeflow features`).
14
+ - Add apps and features with the `mfeflow` CLI, installed as a root dev dependency (`pnpm mfeflow add remote <name>`, `pnpm mfeflow features`).
15
15
  `mfeflow.json` is the CLI's record of the project; do not edit the file hashes by hand.
16
16
  - Shared singletons (react, react-dom, `@mfeflow/runtime`) are provided by the shell. Do not
17
17
  bundle your own copy.
@@ -7,7 +7,7 @@ one shell and independently deployable remotes, loaded at runtime through Module
7
7
  apps/shell the host: routes, layout, registry loading
8
8
  apps/<remote> one micro-frontend each, exposing ./App
9
9
  registry/envs which build of each remote every environment serves
10
- mfeflow.json what the CLI generated; `mfeflow add` reads and updates it
10
+ mfeflow.json what the CLI generated; `pnpm mfeflow add` reads and updates it
11
11
  ```
12
12
 
13
13
  ## Run it
@@ -20,28 +20,28 @@ pnpm dev
20
20
  `pnpm dev` starts the shell on http://localhost:4300 and every remote on its own port
21
21
  (see `registry/envs/dev`). The shell reads that dev registry and renders the remotes.
22
22
 
23
- `mfeflow dev` does the same with a prefixed, coloured log per app, and it follows the project:
24
- add a remote with `mfeflow add remote checkout` and it starts, and the shell lists it, without a restart.
25
- Use a remote you already run elsewhere with `mfeflow dev --remote checkout=http://localhost:3001`,
23
+ `pnpm mfeflow dev` does the same with a prefixed, coloured log per app, and it follows the project:
24
+ add a remote with `pnpm mfeflow add remote checkout` and it starts, and the shell lists it, without a restart.
25
+ Use a remote you already run elsewhere with `pnpm mfeflow dev --remote checkout=http://localhost:3001`,
26
26
  or run a few apps with `--only shell,catalog`. Ctrl+C stops everything.
27
27
 
28
28
  ## Change it
29
29
 
30
30
  ```sh
31
- mfeflow add remote checkout # a new remote, its registry files and its shell route
32
- mfeflow add <feature> # see `mfeflow features`
31
+ pnpm mfeflow add remote checkout # a new remote, its registry files and its shell route
32
+ pnpm mfeflow add <feature> # see `pnpm mfeflow features`
33
33
  ```
34
34
 
35
35
  ## Check it
36
36
 
37
37
  ```sh
38
- mfeflow doctor # singletons, compatibility, config, drift; --fix repairs the safe ones
39
- mfeflow validate # mfeflow.json, every mfe.json and every registry entry
40
- mfeflow check-compat # each remote's shared ranges against its shell
41
- mfeflow budget # after `pnpm build`: first-load JS per shell and remote
38
+ pnpm doctor # singletons, compatibility, config, drift; `pnpm doctor --fix` repairs the safe ones
39
+ pnpm validate # mfeflow.json, every mfe.json and every registry entry
40
+ pnpm check-compat # each remote's shared ranges against its shell
41
+ pnpm budget # after `pnpm build`: first-load JS per shell and remote
42
42
  ```
43
43
 
44
- All of them take `--json` and exit non-zero on errors, so they can run in CI. `mfeflow budget` checks each remote's entry
44
+ Each is a root script for the `mfeflow` CLI in your devDependencies (`pnpm mfeflow <command>` runs it directly). All of them take `--json` and exit non-zero on errors, so they can run in CI. `pnpm budget` checks each remote's entry
45
45
  against the 10 KB a remote may add; set `budget.baselineKb` in `mfeflow.json` (your pages built as one app) to check
46
46
  the shell against baseline + 32 KB too.
47
47