mfeflow 0.2.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.
- package/assets/features/analytics-clarity/feature.json +64 -0
- package/assets/features/analytics-clarity/files/clarity-test.ts +36 -0
- package/assets/features/analytics-clarity/files/clarity.ts +26 -0
- package/assets/features/analytics-gtm/files/init.ts +2 -2
- package/assets/features/commit-prompt/feature.json +27 -0
- package/assets/features/commit-prompt/files/cz.config.js +27 -0
- package/assets/features/data-react-query/feature.json +89 -0
- package/assets/features/data-react-query/files/keys.ts.eta +8 -0
- package/assets/features/data-react-query/files/remote-Query-test.tsx.eta +39 -0
- package/assets/features/data-react-query/files/remote-Query.tsx +15 -0
- package/assets/features/data-react-query/files/shell-Query-test.tsx +21 -0
- package/assets/features/data-react-query/files/shell-Query.tsx +14 -0
- package/assets/features/data-react-query/files/useHealth.fetch.ts.eta +15 -0
- package/assets/features/data-react-query/files/useHealth.http.ts.eta +11 -0
- package/assets/features/env-t3/feature.json +21 -0
- package/assets/features/forms/feature.json +90 -0
- package/assets/features/forms/files/ContactForm-test.tsx +35 -0
- package/assets/features/forms/files/ContactForm.tsx +50 -0
- package/assets/features/forms/files/schema.arktype.ts +9 -0
- package/assets/features/forms/files/schema.valibot.ts +9 -0
- package/assets/features/forms/files/schema.zod.ts +9 -0
- package/assets/features/i18n/feature.json +139 -0
- package/assets/features/i18n/files/LanguageSwitcher.tsx +28 -0
- package/assets/features/i18n/files/Languages.tsx +12 -0
- package/assets/features/i18n/files/language-switcher-test.tsx +19 -0
- package/assets/features/i18n/files/remote-I18n-test.tsx.eta +43 -0
- package/assets/features/i18n/files/remote-I18n.tsx.eta +36 -0
- package/assets/features/i18n/files/remote-ar.json.eta +3 -0
- package/assets/features/i18n/files/remote-config.ts +9 -0
- package/assets/features/i18n/files/remote-en.json.eta +3 -0
- package/assets/features/i18n/files/remote-load.ts +4 -0
- package/assets/features/i18n/files/shell-config.ts +20 -0
- package/assets/features/i18n/files/shell-init-test.ts +32 -0
- package/assets/features/i18n/files/shell-init.ts +7 -0
- package/assets/features/lint-oxlint/files/oxlintrc.json +1 -1
- package/assets/features/lint-oxlint/files/prettierignore +1 -0
- package/assets/features/storybook/feature.json +119 -0
- package/assets/features/storybook/files/App.stories.tsx.eta +8 -0
- package/assets/features/storybook/files/Home.stories.tsx +9 -0
- package/assets/features/storybook/files/button.stories.tsx +14 -0
- package/assets/features/storybook/files/remote-main.ts +13 -0
- package/assets/features/storybook/files/rsbuild.config.ts +10 -0
- package/assets/features/storybook/files/shell-main.ts +39 -0
- package/assets/features/tailwind/feature.json +87 -0
- package/assets/features/tailwind/files/Scope-test.tsx.eta +17 -0
- package/assets/features/tailwind/files/Scope.tsx.eta +15 -0
- package/assets/features/tailwind/files/postcss.config.mjs +1 -0
- package/assets/features/tailwind/files/remote-styles.css.eta +21 -0
- package/assets/features/tailwind/files/shell-styles.css.eta +22 -0
- package/assets/features/testing/files/vitest.config.ts +11 -2
- package/assets/features/theming/feature.json +103 -0
- package/assets/features/theming/files/Theme.tsx +15 -0
- package/assets/features/theming/files/ThemeToggle.tsx +23 -0
- package/assets/features/theming/files/apply-stored-test.ts +37 -0
- package/assets/features/theming/files/apply-stored.ts +13 -0
- package/assets/features/theming/files/head-script.ts +8 -0
- package/assets/features/theming/files/theme-toggle-test.tsx +18 -0
- package/assets/features/theming/files/theme.css +14 -0
- package/assets/features/theming/files/themes.ts +30 -0
- package/assets/features/theming/files/tokens-test.ts +9 -0
- package/assets/features/theming/files/tokens.ts +9 -0
- package/assets/features/ui-shadcn/feature.json +61 -0
- package/assets/features/ui-shadcn/files/button-test.tsx.eta +32 -0
- package/assets/features/ui-shadcn/files/button.tsx.eta +36 -0
- package/assets/features/ui-shadcn/files/components.json.eta +22 -0
- package/assets/features/ui-shadcn/files/utils-test.ts.eta +17 -0
- package/assets/features/ui-shadcn/files/utils.ts.eta +11 -0
- package/assets/templates/monorepo/files/root/gitignore.eta +1 -0
- package/assets/templates/monorepo/files/root/pnpm-workspace.yaml.eta +3 -1
- package/dist/index.js +45 -0
- package/package.json +4 -4
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "i18n",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Translations with @mfeflow/i18n: the shell starts one i18next instance and sets lang and dir; each remote loads its own messages from its own deploy, so a translation ships with the remote that uses it. Sample messages in English and Arabic (right to left) and a language switcher you own.",
|
|
5
|
+
"requires": ["core", "tsconfig"],
|
|
6
|
+
"files": [
|
|
7
|
+
{
|
|
8
|
+
"to": "src/i18n/config.ts",
|
|
9
|
+
"target": "shell",
|
|
10
|
+
"from": "files/shell-config.ts"
|
|
11
|
+
},
|
|
12
|
+
{
|
|
13
|
+
"to": "src/i18n/init.ts",
|
|
14
|
+
"target": "shell",
|
|
15
|
+
"from": "files/shell-init.ts"
|
|
16
|
+
},
|
|
17
|
+
{
|
|
18
|
+
"to": "src/i18n/Languages.tsx",
|
|
19
|
+
"target": "shell",
|
|
20
|
+
"from": "files/Languages.tsx"
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
"to": "src/i18n/LanguageSwitcher.tsx",
|
|
24
|
+
"target": "shell",
|
|
25
|
+
"from": "files/LanguageSwitcher.tsx"
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
"to": "src/i18n/init.test.ts",
|
|
29
|
+
"target": "shell",
|
|
30
|
+
"from": "files/shell-init-test.ts",
|
|
31
|
+
"when": {
|
|
32
|
+
"feature:testing": true
|
|
33
|
+
}
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
"to": "src/i18n/LanguageSwitcher.test.tsx",
|
|
37
|
+
"target": "shell",
|
|
38
|
+
"from": "files/language-switcher-test.tsx",
|
|
39
|
+
"when": {
|
|
40
|
+
"feature:testing": true
|
|
41
|
+
}
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
"to": "src/i18n/config.ts",
|
|
45
|
+
"target": "remotes",
|
|
46
|
+
"from": "files/remote-config.ts"
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
"to": "src/i18n/load.ts",
|
|
50
|
+
"target": "remotes",
|
|
51
|
+
"from": "files/remote-load.ts"
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
"to": "src/i18n/I18n.tsx",
|
|
55
|
+
"target": "remotes",
|
|
56
|
+
"from": "files/remote-I18n.tsx.eta",
|
|
57
|
+
"template": true
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
"to": "src/i18n/messages/en.json",
|
|
61
|
+
"target": "remotes",
|
|
62
|
+
"from": "files/remote-en.json.eta",
|
|
63
|
+
"template": true
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
"to": "src/i18n/messages/ar.json",
|
|
67
|
+
"target": "remotes",
|
|
68
|
+
"from": "files/remote-ar.json.eta",
|
|
69
|
+
"template": true
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
"to": "src/i18n/I18n.test.tsx",
|
|
73
|
+
"target": "remotes",
|
|
74
|
+
"from": "files/remote-I18n-test.tsx.eta",
|
|
75
|
+
"template": true,
|
|
76
|
+
"when": {
|
|
77
|
+
"feature:testing": true
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
],
|
|
81
|
+
"dependencies": [
|
|
82
|
+
{
|
|
83
|
+
"name": "@mfeflow/i18n",
|
|
84
|
+
"range": "latest",
|
|
85
|
+
"target": "shell"
|
|
86
|
+
},
|
|
87
|
+
{
|
|
88
|
+
"name": "i18next",
|
|
89
|
+
"range": "26.5.12",
|
|
90
|
+
"target": "shell"
|
|
91
|
+
},
|
|
92
|
+
{
|
|
93
|
+
"name": "react-i18next",
|
|
94
|
+
"range": "17.1.9",
|
|
95
|
+
"target": "shell"
|
|
96
|
+
},
|
|
97
|
+
{
|
|
98
|
+
"name": "@mfeflow/i18n",
|
|
99
|
+
"range": "latest",
|
|
100
|
+
"target": "remotes"
|
|
101
|
+
},
|
|
102
|
+
{
|
|
103
|
+
"name": "i18next",
|
|
104
|
+
"range": "26.5.12",
|
|
105
|
+
"target": "remotes"
|
|
106
|
+
},
|
|
107
|
+
{
|
|
108
|
+
"name": "react-i18next",
|
|
109
|
+
"range": "17.1.9",
|
|
110
|
+
"target": "remotes"
|
|
111
|
+
}
|
|
112
|
+
],
|
|
113
|
+
"transforms": [
|
|
114
|
+
{
|
|
115
|
+
"op": "add-boot-step",
|
|
116
|
+
"file": "src/bootstrap.tsx",
|
|
117
|
+
"target": "shell",
|
|
118
|
+
"call": "initShellI18n",
|
|
119
|
+
"from": "./i18n/init",
|
|
120
|
+
"await": true
|
|
121
|
+
},
|
|
122
|
+
{
|
|
123
|
+
"op": "wrap-provider",
|
|
124
|
+
"file": "src/App.tsx",
|
|
125
|
+
"target": "shell",
|
|
126
|
+
"component": "Languages",
|
|
127
|
+
"from": "./i18n/Languages",
|
|
128
|
+
"function": "App"
|
|
129
|
+
},
|
|
130
|
+
{
|
|
131
|
+
"op": "wrap-provider",
|
|
132
|
+
"file": "src/exposed/App.tsx",
|
|
133
|
+
"target": "remotes",
|
|
134
|
+
"component": "I18n",
|
|
135
|
+
"from": "../i18n/I18n",
|
|
136
|
+
"function": "App"
|
|
137
|
+
}
|
|
138
|
+
]
|
|
139
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { useLanguage, useMfeTranslation } from '@mfeflow/i18n/react';
|
|
2
|
+
import { loadShellMessages } from './config';
|
|
3
|
+
|
|
4
|
+
/** Your component: move it into the header or restyle it. Changing the language also sets <html lang dir>. */
|
|
5
|
+
export function LanguageSwitcher() {
|
|
6
|
+
const { t } = useMfeTranslation('shell', loadShellMessages);
|
|
7
|
+
const { language, languages, setLanguage } = useLanguage();
|
|
8
|
+
return (
|
|
9
|
+
<label
|
|
10
|
+
className="language-switcher"
|
|
11
|
+
style={{ position: 'fixed', left: '1rem', bottom: '1rem' }}
|
|
12
|
+
>
|
|
13
|
+
{t('language')}{' '}
|
|
14
|
+
<select
|
|
15
|
+
value={language}
|
|
16
|
+
onChange={(event) => {
|
|
17
|
+
void setLanguage(event.target.value);
|
|
18
|
+
}}
|
|
19
|
+
>
|
|
20
|
+
{languages.map((option) => (
|
|
21
|
+
<option key={option.code} value={option.code}>
|
|
22
|
+
{option.label}
|
|
23
|
+
</option>
|
|
24
|
+
))}
|
|
25
|
+
</select>
|
|
26
|
+
</label>
|
|
27
|
+
);
|
|
28
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { ReactNode } from 'react';
|
|
2
|
+
import { LanguageSwitcher } from './LanguageSwitcher';
|
|
3
|
+
|
|
4
|
+
/** Wraps the shell and shows the language switcher. The instance itself starts in the boot step. */
|
|
5
|
+
export function Languages({ children }: { children: ReactNode }) {
|
|
6
|
+
return (
|
|
7
|
+
<>
|
|
8
|
+
{children}
|
|
9
|
+
<LanguageSwitcher />
|
|
10
|
+
</>
|
|
11
|
+
);
|
|
12
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { fireEvent, screen, waitFor } from '@testing-library/react';
|
|
2
|
+
import { resetPlatform, renderWithPlatform } from '@mfeflow/testing';
|
|
3
|
+
import { afterEach, describe, expect, it } from 'vitest';
|
|
4
|
+
import { initShellI18n } from './init';
|
|
5
|
+
import { LanguageSwitcher } from './LanguageSwitcher';
|
|
6
|
+
|
|
7
|
+
afterEach(resetPlatform);
|
|
8
|
+
|
|
9
|
+
describe('LanguageSwitcher', () => {
|
|
10
|
+
it('switches the language and the page direction', async () => {
|
|
11
|
+
await initShellI18n();
|
|
12
|
+
renderWithPlatform(<LanguageSwitcher />);
|
|
13
|
+
fireEvent.change(screen.getByRole('combobox'), { target: { value: 'ar' } });
|
|
14
|
+
await waitFor(() => {
|
|
15
|
+
expect(document.documentElement.dir).toBe('rtl');
|
|
16
|
+
});
|
|
17
|
+
expect(screen.getByText('اللغة')).toBeDefined();
|
|
18
|
+
});
|
|
19
|
+
});
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { StrictMode } from 'react';
|
|
2
|
+
import { screen } from '@testing-library/react';
|
|
3
|
+
import { getI18n } from '@mfeflow/i18n';
|
|
4
|
+
import { renderWithPlatform, resetPlatform } from '@mfeflow/testing';
|
|
5
|
+
import { afterEach, describe, expect, it } from 'vitest';
|
|
6
|
+
import { I18n } from './I18n';
|
|
7
|
+
|
|
8
|
+
afterEach(resetPlatform);
|
|
9
|
+
|
|
10
|
+
describe('the <%= it.app.name %> messages', () => {
|
|
11
|
+
it('start their own i18n when the remote runs without the shell', async () => {
|
|
12
|
+
renderWithPlatform(
|
|
13
|
+
<I18n>
|
|
14
|
+
<p>content</p>
|
|
15
|
+
</I18n>,
|
|
16
|
+
);
|
|
17
|
+
expect(screen.getByText('content')).toBeDefined();
|
|
18
|
+
expect(await screen.findByText('Hello from <%= it.app.name %>')).toBeDefined();
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it('follow a language change', async () => {
|
|
22
|
+
renderWithPlatform(
|
|
23
|
+
<I18n>
|
|
24
|
+
<p>content</p>
|
|
25
|
+
</I18n>,
|
|
26
|
+
);
|
|
27
|
+
await screen.findByText('Hello from <%= it.app.name %>');
|
|
28
|
+
await getI18n().changeLanguage('ar');
|
|
29
|
+
expect(await screen.findByText('مرحبا من <%= it.app.name %>')).toBeDefined();
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it('starts one instance when StrictMode runs the effect twice', async () => {
|
|
33
|
+
renderWithPlatform(
|
|
34
|
+
<StrictMode>
|
|
35
|
+
<I18n>
|
|
36
|
+
<p>content</p>
|
|
37
|
+
</I18n>
|
|
38
|
+
</StrictMode>,
|
|
39
|
+
);
|
|
40
|
+
expect(await screen.findByText('Hello from <%= it.app.name %>')).toBeDefined();
|
|
41
|
+
expect(screen.getAllByText('Hello from <%= it.app.name %>')).toHaveLength(1);
|
|
42
|
+
});
|
|
43
|
+
});
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { useEffect, useState, type ReactNode } from 'react';
|
|
2
|
+
import { getI18n, initI18n } from '@mfeflow/i18n';
|
|
3
|
+
import { useMfeTranslation } from '@mfeflow/i18n/react';
|
|
4
|
+
import { defaultLanguage, languages } from './config';
|
|
5
|
+
import { loadMessages } from './load';
|
|
6
|
+
|
|
7
|
+
function started(): boolean {
|
|
8
|
+
try {
|
|
9
|
+
getI18n();
|
|
10
|
+
return true;
|
|
11
|
+
} catch {
|
|
12
|
+
return false;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function Hello() {
|
|
17
|
+
const { t } = useMfeTranslation('<%= it.app.name %>', loadMessages);
|
|
18
|
+
return <p data-testid="<%= it.app.name %>-hello">{t('hello')}</p>;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** Wraps this remote's App. Inside the shell the page already has an i18n instance; on its own the remote starts one. */
|
|
22
|
+
export function I18n({ children }: { children: ReactNode }) {
|
|
23
|
+
const [ready, setReady] = useState(started);
|
|
24
|
+
useEffect(() => {
|
|
25
|
+
if (ready) return;
|
|
26
|
+
void initI18n({ languages, defaultLanguage, detect: false }).then(() => {
|
|
27
|
+
setReady(true);
|
|
28
|
+
});
|
|
29
|
+
}, [ready]);
|
|
30
|
+
return (
|
|
31
|
+
<>
|
|
32
|
+
{ready ? <Hello /> : null}
|
|
33
|
+
{children}
|
|
34
|
+
</>
|
|
35
|
+
);
|
|
36
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { LanguageOption } from '@mfeflow/i18n';
|
|
2
|
+
|
|
3
|
+
// Only used when this remote runs on its own (pnpm dev in its folder). Inside the shell, the shell's list wins.
|
|
4
|
+
export const languages: LanguageOption[] = [
|
|
5
|
+
{ code: 'en', label: 'English' },
|
|
6
|
+
{ code: 'ar', label: 'العربية' },
|
|
7
|
+
];
|
|
8
|
+
|
|
9
|
+
export const defaultLanguage = 'en';
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { LanguageOption } from '@mfeflow/i18n';
|
|
2
|
+
|
|
3
|
+
export const languages: LanguageOption[] = [
|
|
4
|
+
{ code: 'en', label: 'English' },
|
|
5
|
+
{ code: 'ar', label: 'العربية' },
|
|
6
|
+
];
|
|
7
|
+
|
|
8
|
+
export const defaultLanguage = 'en';
|
|
9
|
+
|
|
10
|
+
// The shell's own messages ship with the shell. Remotes bring theirs (see their src/i18n folder).
|
|
11
|
+
export const shellMessages: Record<string, { shell: Record<string, string> }> = {
|
|
12
|
+
en: { shell: { language: 'Language' } },
|
|
13
|
+
ar: { shell: { language: 'اللغة' } },
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
/** The messages of one language, or the default language's when this one has none, so the text never goes blank. */
|
|
17
|
+
export function loadShellMessages(language: string): Promise<Record<string, string>> {
|
|
18
|
+
const found = shellMessages[language] ?? shellMessages[defaultLanguage];
|
|
19
|
+
return Promise.resolve(found?.shell ?? {});
|
|
20
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { getI18n, getLanguages } from '@mfeflow/i18n';
|
|
2
|
+
import { resetPlatform } from '@mfeflow/testing';
|
|
3
|
+
import { afterEach, describe, expect, it } from 'vitest';
|
|
4
|
+
import { loadShellMessages } from './config';
|
|
5
|
+
import { initShellI18n } from './init';
|
|
6
|
+
|
|
7
|
+
afterEach(resetPlatform);
|
|
8
|
+
|
|
9
|
+
describe('initShellI18n', () => {
|
|
10
|
+
it('starts the page instance with the shell languages', async () => {
|
|
11
|
+
await initShellI18n();
|
|
12
|
+
expect(getLanguages().map((l) => l.code)).toEqual(['en', 'ar']);
|
|
13
|
+
expect(getI18n().t('shell:language')).toBe('Language');
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
it('sets lang and dir when the language changes', async () => {
|
|
17
|
+
await initShellI18n();
|
|
18
|
+
await getI18n().changeLanguage('ar');
|
|
19
|
+
expect(document.documentElement.lang).toBe('ar');
|
|
20
|
+
expect(document.documentElement.dir).toBe('rtl');
|
|
21
|
+
});
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
describe('loadShellMessages', () => {
|
|
25
|
+
it('gives the messages of the language asked for', async () => {
|
|
26
|
+
expect(await loadShellMessages('ar')).toEqual({ language: 'اللغة' });
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it('falls back to the default language for one with no messages', async () => {
|
|
30
|
+
expect(await loadShellMessages('fr')).toEqual({ language: 'Language' });
|
|
31
|
+
});
|
|
32
|
+
});
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { initI18n } from '@mfeflow/i18n';
|
|
2
|
+
import { defaultLanguage, languages, shellMessages } from './config';
|
|
3
|
+
|
|
4
|
+
/** The boot step: one i18next instance for the page, shared by the shell and every remote. */
|
|
5
|
+
export async function initShellI18n(): Promise<void> {
|
|
6
|
+
await initI18n({ languages, defaultLanguage, resources: shellMessages });
|
|
7
|
+
}
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "storybook",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Storybook 10 on Rsbuild (storybook-react-rsbuild) in every remote, and the shell's Storybook shows them all: it lists each remote's built Storybook as a ref, found from mfeflow.json, so a remote added later appears without an edit. Each remote has its own Storybook config, kept apart from the Module Federation build.",
|
|
5
|
+
"requires": ["core", "tsconfig"],
|
|
6
|
+
"files": [
|
|
7
|
+
{
|
|
8
|
+
"to": ".storybook/main.ts",
|
|
9
|
+
"target": "shell",
|
|
10
|
+
"from": "files/shell-main.ts"
|
|
11
|
+
},
|
|
12
|
+
{
|
|
13
|
+
"to": ".storybook/rsbuild.config.ts",
|
|
14
|
+
"target": "shell",
|
|
15
|
+
"from": "files/rsbuild.config.ts"
|
|
16
|
+
},
|
|
17
|
+
{
|
|
18
|
+
"to": "src/pages/Home.stories.tsx",
|
|
19
|
+
"target": "shell",
|
|
20
|
+
"from": "files/Home.stories.tsx"
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
"to": ".storybook/main.ts",
|
|
24
|
+
"target": "remotes",
|
|
25
|
+
"from": "files/remote-main.ts"
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
"to": ".storybook/rsbuild.config.ts",
|
|
29
|
+
"target": "remotes",
|
|
30
|
+
"from": "files/rsbuild.config.ts"
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
"to": "src/exposed/App.stories.tsx",
|
|
34
|
+
"target": "remotes",
|
|
35
|
+
"from": "files/App.stories.tsx.eta",
|
|
36
|
+
"template": true
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
"to": "src/components/ui/button.stories.tsx",
|
|
40
|
+
"target": "remotes",
|
|
41
|
+
"from": "files/button.stories.tsx",
|
|
42
|
+
"when": {
|
|
43
|
+
"feature:ui-shadcn": true
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
],
|
|
47
|
+
"dependencies": [
|
|
48
|
+
{
|
|
49
|
+
"name": "storybook",
|
|
50
|
+
"range": "10.6.5",
|
|
51
|
+
"target": "shell",
|
|
52
|
+
"dev": true
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
"name": "storybook-react-rsbuild",
|
|
56
|
+
"range": "3.6.1",
|
|
57
|
+
"target": "shell",
|
|
58
|
+
"dev": true
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
"name": "@rsbuild/plugin-react",
|
|
62
|
+
"range": "2.1.1",
|
|
63
|
+
"target": "shell",
|
|
64
|
+
"dev": true
|
|
65
|
+
},
|
|
66
|
+
{
|
|
67
|
+
"name": "storybook",
|
|
68
|
+
"range": "10.6.5",
|
|
69
|
+
"target": "remotes",
|
|
70
|
+
"dev": true
|
|
71
|
+
},
|
|
72
|
+
{
|
|
73
|
+
"name": "storybook-react-rsbuild",
|
|
74
|
+
"range": "3.6.1",
|
|
75
|
+
"target": "remotes",
|
|
76
|
+
"dev": true
|
|
77
|
+
},
|
|
78
|
+
{
|
|
79
|
+
"name": "@rsbuild/plugin-react",
|
|
80
|
+
"range": "2.1.1",
|
|
81
|
+
"target": "remotes",
|
|
82
|
+
"dev": true
|
|
83
|
+
}
|
|
84
|
+
],
|
|
85
|
+
"scripts": [
|
|
86
|
+
{
|
|
87
|
+
"name": "storybook",
|
|
88
|
+
"command": "storybook dev --no-open",
|
|
89
|
+
"target": "shell"
|
|
90
|
+
},
|
|
91
|
+
{
|
|
92
|
+
"name": "storybook:build",
|
|
93
|
+
"command": "storybook build -o storybook-static",
|
|
94
|
+
"target": "shell"
|
|
95
|
+
},
|
|
96
|
+
{
|
|
97
|
+
"name": "storybook",
|
|
98
|
+
"command": "storybook dev --no-open --ci",
|
|
99
|
+
"target": "remotes"
|
|
100
|
+
},
|
|
101
|
+
{
|
|
102
|
+
"name": "storybook:build",
|
|
103
|
+
"command": "storybook build -o storybook-static",
|
|
104
|
+
"target": "remotes"
|
|
105
|
+
},
|
|
106
|
+
{
|
|
107
|
+
"name": "storybook:remotes",
|
|
108
|
+
"command": "pnpm --filter \"./apps/*\" --filter \"!./apps/shell\" run --if-present storybook:build"
|
|
109
|
+
},
|
|
110
|
+
{
|
|
111
|
+
"name": "storybook",
|
|
112
|
+
"command": "pnpm storybook:remotes && pnpm --filter ./apps/shell run storybook"
|
|
113
|
+
},
|
|
114
|
+
{
|
|
115
|
+
"name": "storybook:build",
|
|
116
|
+
"command": "pnpm storybook:remotes && pnpm --filter ./apps/shell run storybook:build"
|
|
117
|
+
}
|
|
118
|
+
]
|
|
119
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from 'storybook-react-rsbuild';
|
|
2
|
+
import { Home } from './Home';
|
|
3
|
+
|
|
4
|
+
const meta = { title: 'shell/Home', component: Home } satisfies Meta<typeof Home>;
|
|
5
|
+
|
|
6
|
+
export default meta;
|
|
7
|
+
|
|
8
|
+
// No registry is loaded in Storybook, so this is the empty state.
|
|
9
|
+
export const NoRemotes: StoryObj<typeof meta> = {};
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from 'storybook-react-rsbuild';
|
|
2
|
+
import { Button } from './button';
|
|
3
|
+
|
|
4
|
+
const meta = {
|
|
5
|
+
title: 'ui/Button',
|
|
6
|
+
component: Button,
|
|
7
|
+
args: { children: 'Save' },
|
|
8
|
+
} satisfies Meta<typeof Button>;
|
|
9
|
+
|
|
10
|
+
export default meta;
|
|
11
|
+
|
|
12
|
+
export const Default: StoryObj<typeof meta> = {};
|
|
13
|
+
export const Secondary: StoryObj<typeof meta> = { args: { variant: 'secondary' } };
|
|
14
|
+
export const Destructive: StoryObj<typeof meta> = { args: { variant: 'destructive' } };
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { StorybookConfig } from 'storybook-react-rsbuild';
|
|
2
|
+
|
|
3
|
+
const config: StorybookConfig = {
|
|
4
|
+
framework: {
|
|
5
|
+
name: 'storybook-react-rsbuild',
|
|
6
|
+
options: { builder: { rsbuildConfigPath: './.storybook/rsbuild.config.ts' } },
|
|
7
|
+
},
|
|
8
|
+
stories: ['../src/**/*.stories.@(ts|tsx)'],
|
|
9
|
+
addons: [],
|
|
10
|
+
core: { disableTelemetry: true, disableWhatsNewNotifications: true },
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export default config;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { defineConfig } from '@rsbuild/core';
|
|
2
|
+
import { pluginReact } from '@rsbuild/plugin-react';
|
|
3
|
+
|
|
4
|
+
// Storybook's own build config. It leaves out the app's rsbuild.config.ts on purpose: that one
|
|
5
|
+
// sets up Module Federation, which a story does not need. Add loaders or aliases your stories need here.
|
|
6
|
+
export default defineConfig({
|
|
7
|
+
plugins: [pluginReact()],
|
|
8
|
+
// One copy of React, also when a package is linked from another folder.
|
|
9
|
+
resolve: { dedupe: ['react', 'react-dom'] },
|
|
10
|
+
});
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import type { StorybookConfig } from 'storybook-react-rsbuild';
|
|
4
|
+
|
|
5
|
+
// The shell's Storybook shows its own stories and every remote's, each remote as a ref. Remotes are read from
|
|
6
|
+
// mfeflow.json, so one added later appears here without an edit. A ref is the remote's Storybook as built by
|
|
7
|
+
// `pnpm storybook:build` (apps/<remote>/storybook-static), served from /<remote>/ inside this one.
|
|
8
|
+
const root = path.resolve(import.meta.dirname, '../../..');
|
|
9
|
+
|
|
10
|
+
function field(value: unknown, key: string): unknown {
|
|
11
|
+
return typeof value === 'object' && value !== null ? Reflect.get(value, key) : undefined;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function remoteStorybooks(): { name: string; built: string }[] {
|
|
15
|
+
const apps = field(JSON.parse(fs.readFileSync(path.join(root, 'mfeflow.json'), 'utf8')), 'apps');
|
|
16
|
+
if (typeof apps !== 'object' || apps === null) return [];
|
|
17
|
+
return Object.entries(apps).flatMap(([name, app]) => {
|
|
18
|
+
const dir = field(app, 'path');
|
|
19
|
+
if (field(app, 'kind') !== 'remote' || typeof dir !== 'string') return [];
|
|
20
|
+
const built = path.join(root, dir, 'storybook-static');
|
|
21
|
+
return fs.existsSync(path.join(built, 'index.json')) ? [{ name, built }] : [];
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const remotes = remoteStorybooks();
|
|
26
|
+
|
|
27
|
+
const config: StorybookConfig = {
|
|
28
|
+
framework: {
|
|
29
|
+
name: 'storybook-react-rsbuild',
|
|
30
|
+
options: { builder: { rsbuildConfigPath: './.storybook/rsbuild.config.ts' } },
|
|
31
|
+
},
|
|
32
|
+
stories: ['../src/**/*.stories.@(ts|tsx)'],
|
|
33
|
+
addons: [],
|
|
34
|
+
core: { disableTelemetry: true, disableWhatsNewNotifications: true },
|
|
35
|
+
staticDirs: remotes.map(({ name, built }) => ({ from: built, to: `/${name}` })),
|
|
36
|
+
refs: Object.fromEntries(remotes.map(({ name }) => [name, { title: name, url: `./${name}` }])),
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
export default config;
|