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.
- 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/ci-github/files/ci.md +1 -1
- package/assets/features/ci-github/files/ci.yml +4 -4
- 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 +42 -0
- package/assets/features/env-t3/files/env.arktype.ts +1 -1
- package/assets/features/env-t3/files/env.valibot.ts +1 -1
- package/assets/features/env-t3/files/env.zod.ts +1 -1
- package/assets/features/env-t3/files/schema.arktype.ts +1 -1
- package/assets/features/env-t3/files/schema.valibot.ts +1 -1
- package/assets/features/env-t3/files/schema.zod.ts +1 -1
- 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/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/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/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/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/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/features/url-factory/files/endpoints.ts +13 -2
- package/assets/templates/monorepo/feature.json +26 -0
- package/assets/templates/monorepo/files/root/AGENTS.md.eta +3 -3
- package/assets/templates/monorepo/files/root/README.md.eta +11 -11
- package/assets/templates/monorepo/files/root/gitignore.eta +1 -0
- package/assets/templates/monorepo/files/root/pnpm-workspace.yaml.eta +3 -1
- package/assets/templates/monorepo/files/shell/src/pages/Home.tsx.eta +3 -1
- package/dist/index.js +62 -0
- package/package.json +4 -4
|
@@ -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,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 });
|
|
@@ -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;
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "tailwind",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Tailwind CSS 4 through PostCSS. The shell loads Tailwind's reset (preflight) once; every app gets its own class prefix (cart:flex), so two remotes using the same utility with different tokens never collide. The colour tokens read the --mf-* theme variables, with fallbacks, so they follow theming whichever way round the two are added.",
|
|
5
|
+
"requires": ["core", "tsconfig"],
|
|
6
|
+
"files": [
|
|
7
|
+
{
|
|
8
|
+
"to": "postcss.config.mjs",
|
|
9
|
+
"target": "shell",
|
|
10
|
+
"from": "files/postcss.config.mjs"
|
|
11
|
+
},
|
|
12
|
+
{
|
|
13
|
+
"to": "src/styles.css",
|
|
14
|
+
"target": "shell",
|
|
15
|
+
"from": "files/shell-styles.css.eta",
|
|
16
|
+
"template": true
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
"to": "postcss.config.mjs",
|
|
20
|
+
"target": "remotes",
|
|
21
|
+
"from": "files/postcss.config.mjs"
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
"to": "src/styles.css",
|
|
25
|
+
"target": "remotes",
|
|
26
|
+
"from": "files/remote-styles.css.eta",
|
|
27
|
+
"template": true
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
"to": "src/tailwind/Scope.tsx",
|
|
31
|
+
"target": "remotes",
|
|
32
|
+
"from": "files/Scope.tsx.eta",
|
|
33
|
+
"template": true
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
"to": "src/tailwind/Scope.test.tsx",
|
|
37
|
+
"target": "remotes",
|
|
38
|
+
"from": "files/Scope-test.tsx.eta",
|
|
39
|
+
"template": true,
|
|
40
|
+
"when": {
|
|
41
|
+
"feature:testing": true
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
],
|
|
45
|
+
"dependencies": [
|
|
46
|
+
{
|
|
47
|
+
"name": "tailwindcss",
|
|
48
|
+
"range": "4.3.3",
|
|
49
|
+
"target": "shell",
|
|
50
|
+
"dev": true
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
"name": "@tailwindcss/postcss",
|
|
54
|
+
"range": "4.3.3",
|
|
55
|
+
"target": "shell",
|
|
56
|
+
"dev": true
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
"name": "tailwindcss",
|
|
60
|
+
"range": "4.3.3",
|
|
61
|
+
"target": "remotes",
|
|
62
|
+
"dev": true
|
|
63
|
+
},
|
|
64
|
+
{
|
|
65
|
+
"name": "@tailwindcss/postcss",
|
|
66
|
+
"range": "4.3.3",
|
|
67
|
+
"target": "remotes",
|
|
68
|
+
"dev": true
|
|
69
|
+
}
|
|
70
|
+
],
|
|
71
|
+
"transforms": [
|
|
72
|
+
{
|
|
73
|
+
"op": "add-import",
|
|
74
|
+
"file": "src/bootstrap.tsx",
|
|
75
|
+
"target": "shell",
|
|
76
|
+
"from": "./styles.css"
|
|
77
|
+
},
|
|
78
|
+
{
|
|
79
|
+
"op": "wrap-provider",
|
|
80
|
+
"file": "src/exposed/App.tsx",
|
|
81
|
+
"target": "remotes",
|
|
82
|
+
"component": "Scope",
|
|
83
|
+
"from": "../tailwind/Scope",
|
|
84
|
+
"function": "App"
|
|
85
|
+
}
|
|
86
|
+
]
|
|
87
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { render, screen } from '@testing-library/react';
|
|
2
|
+
import { describe, expect, it } from 'vitest';
|
|
3
|
+
import { Scope } from './Scope';
|
|
4
|
+
|
|
5
|
+
describe('the <%= it.app.name %> Tailwind scope', () => {
|
|
6
|
+
it('prefixes every class with this remote', () => {
|
|
7
|
+
render(
|
|
8
|
+
<Scope>
|
|
9
|
+
<p>content</p>
|
|
10
|
+
</Scope>,
|
|
11
|
+
);
|
|
12
|
+
const root = screen.getByText('content').parentElement;
|
|
13
|
+
for (const name of (root?.className ?? '').split(' ')) {
|
|
14
|
+
expect(name.includes(':'), name).toBe(true);
|
|
15
|
+
}
|
|
16
|
+
});
|
|
17
|
+
});
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
<%_ const prefix = it.app.tailwindPrefix _%>
|
|
2
|
+
import type { ReactNode } from 'react';
|
|
3
|
+
import '../styles.css';
|
|
4
|
+
|
|
5
|
+
// prettier-ignore
|
|
6
|
+
const classes = '<%= prefix %>:p-2 <%= prefix %>:outline-2 <%= prefix %>:outline-primary';
|
|
7
|
+
|
|
8
|
+
/** Wraps this remote's App and loads its styles. The outline shows the primary token; delete it when you have your own markup. */
|
|
9
|
+
export function Scope({ children }: { children: ReactNode }) {
|
|
10
|
+
return (
|
|
11
|
+
<div data-tailwind="<%= it.app.name %>" className={classes}>
|
|
12
|
+
{children}
|
|
13
|
+
</div>
|
|
14
|
+
);
|
|
15
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export default { plugins: { '@tailwindcss/postcss': {} } };
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
<%_ const prefix = it.app.tailwindPrefix _%>
|
|
2
|
+
/* No reset here: the shell has it. Classes are written <%= prefix %>:flex, and the theme variables are --<%= prefix %>-*. */
|
|
3
|
+
@layer theme, base, components, utilities;
|
|
4
|
+
@import 'tailwindcss/theme.css' layer(theme) prefix(<%= prefix %>);
|
|
5
|
+
@import 'tailwindcss/utilities.css' layer(utilities) prefix(<%= prefix %>);
|
|
6
|
+
|
|
7
|
+
/* Tokens read the theme variables (--mf-*). The second value is used when there is no theme. */
|
|
8
|
+
@theme inline {
|
|
9
|
+
--color-background: var(--mf-color-bg, #ffffff);
|
|
10
|
+
--color-foreground: var(--mf-color-fg, #111827);
|
|
11
|
+
--color-surface: var(--mf-color-surface, #f3f4f6);
|
|
12
|
+
--color-primary: var(--mf-color-primary, #2563eb);
|
|
13
|
+
--color-primary-foreground: var(--mf-color-primary-fg, #ffffff);
|
|
14
|
+
--color-secondary: var(--mf-color-surface, #f3f4f6);
|
|
15
|
+
--color-secondary-foreground: var(--mf-color-fg, #111827);
|
|
16
|
+
--color-accent: var(--mf-color-surface, #f3f4f6);
|
|
17
|
+
--color-accent-foreground: var(--mf-color-fg, #111827);
|
|
18
|
+
--color-destructive: #dc2626;
|
|
19
|
+
--color-border: var(--mf-color-border, #e5e7eb);
|
|
20
|
+
--color-ring: var(--mf-color-primary, #2563eb);
|
|
21
|
+
}
|