@bleedingdev/modern-js-create 3.2.0-ultramodern.12 → 3.2.0-ultramodern.121
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/README.md +167 -72
- package/bin/run.js +0 -0
- package/dist/cjs/create-package-root.cjs +63 -0
- package/dist/cjs/index.cjs +528 -0
- package/dist/cjs/locale/en.cjs +93 -0
- package/dist/cjs/locale/index.cjs +50 -0
- package/dist/cjs/locale/zh.cjs +93 -0
- package/dist/cjs/ultramodern-package-source.cjs +135 -0
- package/dist/cjs/ultramodern-workspace/add-vertical.cjs +337 -0
- package/dist/cjs/ultramodern-workspace/app-files.cjs +223 -0
- package/dist/cjs/ultramodern-workspace/contracts.cjs +836 -0
- package/dist/cjs/ultramodern-workspace/demo-components.cjs +422 -0
- package/dist/cjs/ultramodern-workspace/descriptors.cjs +222 -0
- package/dist/cjs/ultramodern-workspace/effect-api.cjs +952 -0
- package/dist/cjs/ultramodern-workspace/fs-io.cjs +191 -0
- package/dist/cjs/ultramodern-workspace/index.cjs +48 -0
- package/dist/cjs/ultramodern-workspace/locales.cjs +173 -0
- package/dist/cjs/ultramodern-workspace/module-federation.cjs +487 -0
- package/dist/cjs/ultramodern-workspace/naming.cjs +161 -0
- package/dist/cjs/ultramodern-workspace/package-json.cjs +406 -0
- package/dist/cjs/ultramodern-workspace/package-source.cjs +59 -0
- package/dist/cjs/ultramodern-workspace/policy.cjs +248 -0
- package/dist/cjs/ultramodern-workspace/public-surface.cjs +268 -0
- package/dist/cjs/ultramodern-workspace/routes.cjs +375 -0
- package/dist/cjs/ultramodern-workspace/types.cjs +61 -0
- package/dist/cjs/ultramodern-workspace/versions.cjs +153 -0
- package/dist/cjs/ultramodern-workspace/workspace-scripts.cjs +153 -0
- package/dist/cjs/ultramodern-workspace/write-workspace.cjs +175 -0
- package/dist/esm/create-package-root.js +14 -0
- package/dist/esm/index.js +491 -0
- package/dist/esm/locale/en.js +55 -0
- package/dist/esm/locale/index.js +9 -0
- package/dist/esm/locale/zh.js +55 -0
- package/dist/esm/ultramodern-package-source.js +63 -0
- package/dist/esm/ultramodern-workspace/add-vertical.js +252 -0
- package/dist/esm/ultramodern-workspace/app-files.js +149 -0
- package/dist/esm/ultramodern-workspace/contracts.js +741 -0
- package/dist/esm/ultramodern-workspace/demo-components.js +363 -0
- package/dist/esm/ultramodern-workspace/descriptors.js +133 -0
- package/dist/esm/ultramodern-workspace/effect-api.js +854 -0
- package/dist/esm/ultramodern-workspace/fs-io.js +90 -0
- package/dist/esm/ultramodern-workspace/index.js +3 -0
- package/dist/esm/ultramodern-workspace/locales.js +122 -0
- package/dist/esm/ultramodern-workspace/module-federation.js +415 -0
- package/dist/esm/ultramodern-workspace/naming.js +71 -0
- package/dist/esm/ultramodern-workspace/package-json.js +338 -0
- package/dist/esm/ultramodern-workspace/package-source.js +21 -0
- package/dist/esm/ultramodern-workspace/policy.js +183 -0
- package/dist/esm/ultramodern-workspace/public-surface.js +183 -0
- package/dist/esm/ultramodern-workspace/routes.js +280 -0
- package/dist/esm/ultramodern-workspace/types.js +16 -0
- package/dist/esm/ultramodern-workspace/versions.js +34 -0
- package/dist/esm/ultramodern-workspace/workspace-scripts.js +91 -0
- package/dist/esm/ultramodern-workspace/write-workspace.js +121 -0
- package/dist/esm-node/create-package-root.js +15 -0
- package/dist/esm-node/index.js +492 -0
- package/dist/esm-node/locale/en.js +56 -0
- package/dist/esm-node/locale/index.js +10 -0
- package/dist/esm-node/locale/zh.js +56 -0
- package/dist/esm-node/ultramodern-package-source.js +64 -0
- package/dist/esm-node/ultramodern-workspace/add-vertical.js +253 -0
- package/dist/esm-node/ultramodern-workspace/app-files.js +150 -0
- package/dist/esm-node/ultramodern-workspace/contracts.js +742 -0
- package/dist/esm-node/ultramodern-workspace/demo-components.js +364 -0
- package/dist/esm-node/ultramodern-workspace/descriptors.js +134 -0
- package/dist/esm-node/ultramodern-workspace/effect-api.js +855 -0
- package/dist/esm-node/ultramodern-workspace/fs-io.js +91 -0
- package/dist/esm-node/ultramodern-workspace/index.js +4 -0
- package/dist/esm-node/ultramodern-workspace/locales.js +123 -0
- package/dist/esm-node/ultramodern-workspace/module-federation.js +416 -0
- package/dist/esm-node/ultramodern-workspace/naming.js +72 -0
- package/dist/esm-node/ultramodern-workspace/package-json.js +339 -0
- package/dist/esm-node/ultramodern-workspace/package-source.js +22 -0
- package/dist/esm-node/ultramodern-workspace/policy.js +184 -0
- package/dist/esm-node/ultramodern-workspace/public-surface.js +184 -0
- package/dist/esm-node/ultramodern-workspace/routes.js +281 -0
- package/dist/esm-node/ultramodern-workspace/types.js +17 -0
- package/dist/esm-node/ultramodern-workspace/versions.js +35 -0
- package/dist/esm-node/ultramodern-workspace/workspace-scripts.js +92 -0
- package/dist/esm-node/ultramodern-workspace/write-workspace.js +122 -0
- package/dist/types/create-package-root.d.ts +1 -0
- package/dist/types/locale/en.d.ts +8 -9
- package/dist/types/locale/index.d.ts +109 -2
- package/dist/types/locale/zh.d.ts +8 -9
- package/dist/types/ultramodern-package-source.d.ts +28 -0
- package/dist/types/ultramodern-workspace/add-vertical.d.ts +19 -0
- package/dist/types/ultramodern-workspace/app-files.d.ts +14 -0
- package/dist/types/ultramodern-workspace/contracts.d.ts +21 -0
- package/dist/types/ultramodern-workspace/demo-components.d.ts +9 -0
- package/dist/types/ultramodern-workspace/descriptors.d.ts +39 -0
- package/dist/types/ultramodern-workspace/effect-api.d.ts +73 -0
- package/dist/types/ultramodern-workspace/fs-io.d.ts +18 -0
- package/dist/types/ultramodern-workspace/index.d.ts +4 -0
- package/dist/types/ultramodern-workspace/locales.d.ts +183 -0
- package/dist/types/ultramodern-workspace/module-federation.d.ts +16 -0
- package/dist/types/ultramodern-workspace/naming.d.ts +16 -0
- package/dist/types/ultramodern-workspace/package-json.d.ts +12 -0
- package/dist/types/ultramodern-workspace/package-source.d.ts +2 -0
- package/dist/types/ultramodern-workspace/policy.d.ts +60 -0
- package/dist/types/ultramodern-workspace/public-surface.d.ts +37 -0
- package/dist/types/ultramodern-workspace/routes.d.ts +25 -0
- package/dist/types/ultramodern-workspace/types.d.ts +95 -0
- package/dist/types/ultramodern-workspace/versions.d.ts +38 -0
- package/dist/types/ultramodern-workspace/workspace-scripts.d.ts +10 -0
- package/dist/types/ultramodern-workspace/write-workspace.d.ts +4 -0
- package/package.json +34 -15
- package/template-workspace/.agents/agent-reference-repos.json +24 -0
- package/template-workspace/.agents/skills-lock.json +19 -0
- package/template-workspace/.codex/hooks.json +16 -0
- package/template-workspace/.github/renovate.json +29 -0
- package/template-workspace/.github/workflows/ultramodern-workspace-gates.yml.handlebars +67 -0
- package/template-workspace/.gitignore.handlebars +5 -0
- package/template-workspace/.mise.toml.handlebars +3 -0
- package/template-workspace/AGENTS.md.handlebars +87 -0
- package/template-workspace/README.md.handlebars +132 -11
- package/template-workspace/lefthook.yml +24 -0
- package/template-workspace/oxfmt.config.ts +1 -0
- package/template-workspace/oxlint.config.ts +1 -0
- package/template-workspace/pnpm-workspace.yaml.handlebars +40 -0
- package/template-workspace/scripts/bootstrap-agent-skills.mjs +184 -21
- package/template-workspace/scripts/setup-agent-reference-repos.mjs +370 -0
- package/templates/app/shell-frame.tsx +49 -0
- package/templates/app/ultramodern-route-head.tsx.handlebars +142 -0
- package/templates/packages/shared-contracts-index.ts +466 -0
- package/templates/workspace-scripts/assert-mf-types.mjs.handlebars +69 -0
- package/templates/workspace-scripts/check-ultramodern-i18n-boundaries.mjs +9 -0
- package/templates/workspace-scripts/generate-public-surface-assets.mjs +529 -0
- package/templates/workspace-scripts/proof-cloudflare-version.mjs +125 -0
- package/templates/workspace-scripts/ultramodern-cloudflare-proof.mjs +851 -0
- package/templates/workspace-scripts/ultramodern-performance-readiness.config.mjs +7 -0
- package/templates/workspace-scripts/ultramodern-performance-readiness.mjs +223 -0
- package/templates/workspace-scripts/validate-ultramodern-workspace.mjs.handlebars +593 -0
- package/dist/index.js +0 -2626
- package/dist/types/ultramodern-workspace.d.ts +0 -20
- package/template/.agents/skills-lock.json +0 -34
- package/template/.browserslistrc +0 -4
- package/template/.github/workflows/ultramodern-gates.yml.handlebars +0 -30
- package/template/.gitignore.handlebars +0 -30
- package/template/.nvmrc +0 -2
- package/template/AGENTS.md +0 -25
- package/template/README.md +0 -79
- package/template/api/effect/index.ts.handlebars +0 -23
- package/template/api/lambda/hello.ts.handlebars +0 -6
- package/template/config/public/locales/cs/translation.json +0 -39
- package/template/config/public/locales/en/translation.json +0 -39
- package/template/modern.config.ts.handlebars +0 -53
- package/template/oxfmt.config.ts +0 -8
- package/template/oxlint.config.ts +0 -12
- package/template/package.json.handlebars +0 -67
- package/template/postcss.config.mjs.handlebars +0 -6
- package/template/scripts/bootstrap-agent-skills.mjs +0 -95
- package/template/scripts/check-i18n-strings.mjs +0 -83
- package/template/scripts/validate-ultramodern.mjs.handlebars +0 -178
- package/template/shared/effect/api.ts.handlebars +0 -17
- package/template/src/modern-app-env.d.ts +0 -1
- package/template/src/modern.runtime.ts.handlebars +0 -23
- package/template/src/routes/index.css.handlebars +0 -129
- package/template/src/routes/layout.tsx.handlebars +0 -9
- package/template/src/routes/page.tsx.handlebars +0 -155
- package/template/tailwind.config.ts.handlebars +0 -10
- package/template/tsconfig.json +0 -120
- package/template-workspace/AGENTS.md +0 -50
- package/template-workspace/pnpm-workspace.yaml +0 -17
- package/template-workspace/scripts/check-i18n-strings.mjs +0 -83
- package/template-workspace/scripts/validate-ultramodern-workspace.mjs.handlebars +0 -433
|
@@ -0,0 +1,363 @@
|
|
|
1
|
+
import { appHasEffectApi, effectApiStem, remoteDependencyAlias, shellApp } from "./descriptors.js";
|
|
2
|
+
import { createTw, packageName, tailwindPrefixForApp, toPascalCase } from "./naming.js";
|
|
3
|
+
function createShellPage(remotes = []) {
|
|
4
|
+
const tw = createTw(tailwindPrefixForApp(shellApp));
|
|
5
|
+
const remoteCount = String(remotes.length);
|
|
6
|
+
return `import { Link, useModernI18n } from '@modern-js/plugin-i18n/runtime';
|
|
7
|
+
import ShellFrame from '../shell-frame';
|
|
8
|
+
import { UltramodernRouteHead } from '../ultramodern-route-head';
|
|
9
|
+
import { VerticalShowcase } from '../vertical-components';
|
|
10
|
+
import { ultramodernUiMarker } from '../../ultramodern-build';
|
|
11
|
+
|
|
12
|
+
export default function ShellHome() {
|
|
13
|
+
const { i18nInstance } = useModernI18n();
|
|
14
|
+
const t = i18nInstance['t'].bind(i18nInstance);
|
|
15
|
+
|
|
16
|
+
return (
|
|
17
|
+
<ShellFrame>
|
|
18
|
+
<UltramodernRouteHead />
|
|
19
|
+
<section className="${tw('mx-auto grid max-w-7xl items-center gap-8 py-8 md:grid-cols-[0.9fr_1.1fr] lg:gap-14')}">
|
|
20
|
+
<div className="${tw('min-w-0')}">
|
|
21
|
+
<p className="${tw('text-xs font-black uppercase tracking-[0.18em] text-emerald-800')}">{t('shell.hero.eyebrow')}</p>
|
|
22
|
+
<h1 className="${tw('mt-3 max-w-3xl text-5xl font-black leading-none tracking-normal text-stone-950 md:text-7xl')}">{t('shell.title')}</h1>
|
|
23
|
+
<p className="${tw('mt-5 max-w-2xl text-lg leading-8 text-stone-600')}">{t('shell.hero.lede')}</p>
|
|
24
|
+
<div className="${tw('mt-7 flex flex-wrap gap-3')}">
|
|
25
|
+
<Link className="${tw('inline-flex min-h-11 items-center justify-center rounded-full bg-emerald-800 px-5 font-bold text-white shadow-lg shadow-stone-900/10')}" to="/">
|
|
26
|
+
{t('shell.hero.primary')}
|
|
27
|
+
</Link>
|
|
28
|
+
<span className="${tw('inline-flex min-h-11 items-center justify-center rounded-full border border-stone-900/15 bg-white/90 px-5 font-bold text-stone-950 shadow-lg shadow-stone-900/10')}">
|
|
29
|
+
{t('shell.hero.secondary')}
|
|
30
|
+
</span>
|
|
31
|
+
</div>
|
|
32
|
+
</div>
|
|
33
|
+
<div className="${tw('rounded-3xl bg-white/90 p-6 shadow-2xl shadow-stone-900/15')}">
|
|
34
|
+
<div className="${tw('grid gap-4 sm:grid-cols-2')}">
|
|
35
|
+
<article className="${tw('rounded-2xl bg-emerald-50 p-5')}">
|
|
36
|
+
<span className="${tw('text-sm font-black uppercase tracking-[0.16em] text-emerald-800')}">{t('shell.hero.cardOneKicker')}</span>
|
|
37
|
+
<strong className="${tw('mt-3 block text-3xl font-black text-stone-950')}">${remoteCount}</strong>
|
|
38
|
+
<p className="${tw('mt-2 text-sm font-semibold text-stone-600')}">{t('shell.hero.cardOne')}</p>
|
|
39
|
+
</article>
|
|
40
|
+
<article className="${tw('rounded-2xl bg-amber-50 p-5')}">
|
|
41
|
+
<span className="${tw('text-sm font-black uppercase tracking-[0.16em] text-amber-800')}">{t('shell.hero.cardTwoKicker')}</span>
|
|
42
|
+
<strong className="${tw('mt-3 block text-3xl font-black text-stone-950')}">SSR</strong>
|
|
43
|
+
<p className="${tw('mt-2 text-sm font-semibold text-stone-600')}">{t('shell.hero.cardTwo')}</p>
|
|
44
|
+
</article>
|
|
45
|
+
</div>
|
|
46
|
+
</div>
|
|
47
|
+
</section>
|
|
48
|
+
<VerticalShowcase />
|
|
49
|
+
<p className="${tw('sr-only')}" data-testid="ultramodern-preset">presetUltramodern workspace</p>
|
|
50
|
+
<p className="${tw('sr-only')}" data-build-marker={ultramodernUiMarker.build} data-testid="ultramodern-ui-marker">
|
|
51
|
+
{ultramodernUiMarker.appId}:{ultramodernUiMarker.version}
|
|
52
|
+
</p>
|
|
53
|
+
</ShellFrame>
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
`;
|
|
57
|
+
}
|
|
58
|
+
function createShellRemoteComponents(scope, remotes = []) {
|
|
59
|
+
const tw = createTw(tailwindPrefixForApp(shellApp));
|
|
60
|
+
const widgetRemotes = remotes.filter((remote)=>Object.hasOwn(remote.exposes ?? {}, './Widget'));
|
|
61
|
+
const serverImports = widgetRemotes.map((remote)=>`import ${toPascalCase(remote.id)}WidgetServer from '${packageName(scope, remote.packageSuffix)}/Widget';`).join('\n');
|
|
62
|
+
const hydratedExports = widgetRemotes.map((remote)=>{
|
|
63
|
+
const componentName = `${toPascalCase(remote.id)}Widget`;
|
|
64
|
+
return `const ${componentName} = createHydratedRemote(${componentName}Server, '${remoteDependencyAlias(remote)}/Widget');`;
|
|
65
|
+
}).join('\n');
|
|
66
|
+
const federationImports = widgetRemotes.length > 0 ? `import {
|
|
67
|
+
classifyModuleFederationFallback,
|
|
68
|
+
createModuleFederationFallbackTelemetry,
|
|
69
|
+
emitModuleFederationFallbackTelemetry,
|
|
70
|
+
toModuleFederationFallbackAttributes,
|
|
71
|
+
} from '@modern-js/runtime/module-federation';
|
|
72
|
+
import { createLazyComponent } from '@module-federation/bridge-react';
|
|
73
|
+
import { getInstance, loadRemote } from '@module-federation/modern-js-v3/runtime';
|
|
74
|
+
import { Suspense, useEffect, useMemo, useState } from 'react';
|
|
75
|
+
import type { ComponentType } from 'react';
|
|
76
|
+
${serverImports}
|
|
77
|
+
` : '';
|
|
78
|
+
const federationHelpers = widgetRemotes.length > 0 ? `interface RemoteComponentModule {
|
|
79
|
+
default: ComponentType;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const loadRemoteComponent = (specifier: string) =>
|
|
83
|
+
loadRemote<RemoteComponentModule>(specifier) as Promise<RemoteComponentModule>;
|
|
84
|
+
|
|
85
|
+
const createRemoteFallback = (specifier: string) =>
|
|
86
|
+
({ error }: { error: Error }) => {
|
|
87
|
+
const { i18nInstance } = useModernI18n();
|
|
88
|
+
const t = i18nInstance['t'].bind(i18nInstance);
|
|
89
|
+
const classification = classifyModuleFederationFallback(error);
|
|
90
|
+
const telemetry = createModuleFederationFallbackTelemetry({
|
|
91
|
+
appName: '${shellApp.id}',
|
|
92
|
+
classification,
|
|
93
|
+
entry: typeof window === 'undefined' ? undefined : window.location.href,
|
|
94
|
+
error,
|
|
95
|
+
eventName: 'mf.client.remote.fallback',
|
|
96
|
+
exportName: 'default',
|
|
97
|
+
phase: 'load',
|
|
98
|
+
remote: specifier,
|
|
99
|
+
status: 'degraded',
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
useEffect(() => {
|
|
103
|
+
void emitModuleFederationFallbackTelemetry({
|
|
104
|
+
appName: telemetry.appName,
|
|
105
|
+
classification,
|
|
106
|
+
entry: telemetry.entry,
|
|
107
|
+
error,
|
|
108
|
+
eventName: telemetry.eventName,
|
|
109
|
+
exportName: 'default',
|
|
110
|
+
metadata: telemetry.metadata,
|
|
111
|
+
phase: telemetry.phase,
|
|
112
|
+
remote: specifier,
|
|
113
|
+
status: 'degraded',
|
|
114
|
+
});
|
|
115
|
+
}, [classification, error, specifier, telemetry]);
|
|
116
|
+
|
|
117
|
+
return <div className="${tw('rounded-xl border border-red-900/20 bg-red-50 px-4 py-3 text-sm font-semibold text-red-900')}" data-remote-error={error.name} {...toModuleFederationFallbackAttributes(telemetry)}>{t('shell.remoteUnavailable')}</div>;
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
const createHydratedRemote =
|
|
121
|
+
(ServerComponent: ComponentType, specifier: string) =>
|
|
122
|
+
function HydratedRemote() {
|
|
123
|
+
const [hydrated, setHydrated] = useState(false);
|
|
124
|
+
|
|
125
|
+
useEffect(() => {
|
|
126
|
+
setHydrated(true);
|
|
127
|
+
}, []);
|
|
128
|
+
|
|
129
|
+
const FederatedComponent = useMemo(() => {
|
|
130
|
+
if (!hydrated) {
|
|
131
|
+
return null;
|
|
132
|
+
}
|
|
133
|
+
const instance = getInstance();
|
|
134
|
+
if (instance === null || instance === undefined) {
|
|
135
|
+
return null;
|
|
136
|
+
}
|
|
137
|
+
return createLazyComponent({
|
|
138
|
+
export: 'default',
|
|
139
|
+
fallback: createRemoteFallback(specifier),
|
|
140
|
+
instance,
|
|
141
|
+
loader: () => loadRemoteComponent(specifier),
|
|
142
|
+
loading: <ServerComponent />,
|
|
143
|
+
});
|
|
144
|
+
}, [hydrated]);
|
|
145
|
+
|
|
146
|
+
if (FederatedComponent === null) {
|
|
147
|
+
return <ServerComponent />;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
return (
|
|
151
|
+
<Suspense fallback={<ServerComponent />}>
|
|
152
|
+
<FederatedComponent />
|
|
153
|
+
</Suspense>
|
|
154
|
+
);
|
|
155
|
+
};
|
|
156
|
+
` : '';
|
|
157
|
+
const showcaseItems = widgetRemotes.map((remote)=>{
|
|
158
|
+
const componentName = `${toPascalCase(remote.id)}Widget`;
|
|
159
|
+
return ` <${componentName} key="${remote.id}" />`;
|
|
160
|
+
}).join('\n');
|
|
161
|
+
const remoteCount = String(widgetRemotes.length);
|
|
162
|
+
return `${federationImports}import { Link, useModernI18n } from '@modern-js/plugin-i18n/runtime';
|
|
163
|
+
|
|
164
|
+
const widgetCount = Number('${remoteCount}');
|
|
165
|
+
|
|
166
|
+
${federationHelpers}
|
|
167
|
+
${hydratedExports}
|
|
168
|
+
|
|
169
|
+
export const Header = () => {
|
|
170
|
+
const { i18nInstance } = useModernI18n();
|
|
171
|
+
const t = i18nInstance['t'].bind(i18nInstance);
|
|
172
|
+
|
|
173
|
+
return (
|
|
174
|
+
<header className="${tw('flex min-w-0 flex-wrap items-center gap-x-8 gap-y-2 md:flex-1')}" data-modern-boundary-id="${shellApp.mfName}" data-modern-mf-expose="shell/Header">
|
|
175
|
+
<Link className="${tw('whitespace-nowrap text-xl font-black tracking-normal text-stone-950 no-underline')}" to="/">{t('shell.title')}</Link>
|
|
176
|
+
</header>
|
|
177
|
+
);
|
|
178
|
+
};
|
|
179
|
+
|
|
180
|
+
export const StatusBadge = () => {
|
|
181
|
+
const { i18nInstance } = useModernI18n();
|
|
182
|
+
const t = i18nInstance['t'].bind(i18nInstance);
|
|
183
|
+
|
|
184
|
+
return (
|
|
185
|
+
<span className="${tw('inline-flex h-10 shrink-0 items-center justify-center rounded-full border border-stone-900/15 bg-white px-4 text-sm font-extrabold text-stone-950 shadow-lg shadow-stone-900/5')}">
|
|
186
|
+
{widgetCount} {t('shell.hero.cardOneKicker')}
|
|
187
|
+
</span>
|
|
188
|
+
);
|
|
189
|
+
};
|
|
190
|
+
|
|
191
|
+
export const VerticalShowcase = () => {
|
|
192
|
+
const { i18nInstance } = useModernI18n();
|
|
193
|
+
const t = i18nInstance['t'].bind(i18nInstance);
|
|
194
|
+
|
|
195
|
+
if (widgetCount === 0) {
|
|
196
|
+
return (
|
|
197
|
+
<section className="${tw('mx-auto mt-12 max-w-7xl rounded-2xl bg-white/90 p-6 shadow-xl shadow-stone-900/10')}">
|
|
198
|
+
<p className="${tw('text-lg font-bold text-stone-700')}">{t('shell.hero.empty')}</p>
|
|
199
|
+
</section>
|
|
200
|
+
);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
return (
|
|
204
|
+
<section className="${tw('mx-auto mt-12 max-w-7xl')}" data-modern-boundary-id="${shellApp.mfName}">
|
|
205
|
+
<div className="${tw('grid gap-4 md:grid-cols-2')}">
|
|
206
|
+
${showcaseItems}
|
|
207
|
+
</div>
|
|
208
|
+
</section>
|
|
209
|
+
);
|
|
210
|
+
};
|
|
211
|
+
`;
|
|
212
|
+
}
|
|
213
|
+
function createRemotePage(app) {
|
|
214
|
+
const tw = createTw(tailwindPrefixForApp(app));
|
|
215
|
+
const listEffectItems = `list${toPascalCase(effectApiStem(app))}`;
|
|
216
|
+
const effectBffImport = appHasEffectApi(app) ? `import { useModernI18n } from '@modern-js/plugin-i18n/runtime';
|
|
217
|
+
import { Link } from '@modern-js/plugin-tanstack/runtime';
|
|
218
|
+
import { useEffect, useState } from 'react';
|
|
219
|
+
import {
|
|
220
|
+
Effect,
|
|
221
|
+
${listEffectItems},
|
|
222
|
+
runEffectRequest,
|
|
223
|
+
} from '../../effect/${effectApiStem(app)}-client';
|
|
224
|
+
import { UltramodernRouteHead } from '../ultramodern-route-head';
|
|
225
|
+
import { ultramodernUiMarker } from '../../ultramodern-build';
|
|
226
|
+
` : "import { useModernI18n } from '@modern-js/plugin-i18n/runtime';\nimport { Link } from '@modern-js/plugin-tanstack/runtime';\nimport { UltramodernRouteHead } from '../ultramodern-route-head';\nimport { ultramodernUiMarker } from '../../ultramodern-build';\n";
|
|
227
|
+
const effectBffState = appHasEffectApi(app) ? ` const [effectApiStatus, setEffectApiStatus] = useState('pending');
|
|
228
|
+
|
|
229
|
+
useEffect(() => {
|
|
230
|
+
let cancelled = false;
|
|
231
|
+
void runEffectRequest(
|
|
232
|
+
${listEffectItems}({ limit: 1 }).pipe(
|
|
233
|
+
Effect.match({
|
|
234
|
+
onFailure: () => {
|
|
235
|
+
if (cancelled) {
|
|
236
|
+
return;
|
|
237
|
+
}
|
|
238
|
+
setEffectApiStatus('unavailable');
|
|
239
|
+
},
|
|
240
|
+
onSuccess: data => {
|
|
241
|
+
if (cancelled) {
|
|
242
|
+
return;
|
|
243
|
+
}
|
|
244
|
+
setEffectApiStatus(data.items.at(0)?.title ?? 'empty');
|
|
245
|
+
},
|
|
246
|
+
}),
|
|
247
|
+
),
|
|
248
|
+
);
|
|
249
|
+
|
|
250
|
+
return () => {
|
|
251
|
+
cancelled = true;
|
|
252
|
+
};
|
|
253
|
+
}, []);
|
|
254
|
+
|
|
255
|
+
` : '';
|
|
256
|
+
const effectBffMarkup = appHasEffectApi(app) ? ` <p data-testid="effect-bff-status">{effectApiStatus}</p>
|
|
257
|
+
` : '';
|
|
258
|
+
return `${effectBffImport}
|
|
259
|
+
export default function ${toPascalCase(app.id)}Home() {
|
|
260
|
+
const { i18nInstance, language, supportedLanguages } = useModernI18n();
|
|
261
|
+
const t = i18nInstance['t'].bind(i18nInstance);
|
|
262
|
+
${effectBffState} return (
|
|
263
|
+
<main className="${tw('min-h-screen bg-um-canvas px-4 py-6 text-um-foreground sm:px-8')}">
|
|
264
|
+
<UltramodernRouteHead />
|
|
265
|
+
<nav aria-label={t('${app.domain}.language.switcher')} className="${tw('flex gap-3')}">
|
|
266
|
+
{supportedLanguages.map(code => (
|
|
267
|
+
<Link
|
|
268
|
+
aria-current={language === code ? 'page' : undefined}
|
|
269
|
+
className="${tw('rounded-full border border-stone-900/15 bg-white px-4 py-2 text-sm font-bold text-stone-950 no-underline')}"
|
|
270
|
+
key={code}
|
|
271
|
+
params={{ lang: code }}
|
|
272
|
+
to="/$lang"
|
|
273
|
+
>
|
|
274
|
+
{t(\`${app.domain}.language.\${code}\`)}
|
|
275
|
+
</Link>
|
|
276
|
+
))}
|
|
277
|
+
</nav>
|
|
278
|
+
<h1 className="${tw('mt-10 text-5xl font-black')}">{t('${app.domain}.title')}</h1>
|
|
279
|
+
<p className="${tw('mt-3 text-lg text-stone-600')}" data-modern-mf-role="${app.kind}">{t('${app.domain}.role')}</p>
|
|
280
|
+
<p className="${tw('sr-only')}" data-build-marker={ultramodernUiMarker.build} data-testid="ultramodern-ui-marker">
|
|
281
|
+
{ultramodernUiMarker.appId}:{ultramodernUiMarker.version}
|
|
282
|
+
</p>
|
|
283
|
+
${effectBffMarkup} </main>
|
|
284
|
+
);
|
|
285
|
+
}
|
|
286
|
+
`;
|
|
287
|
+
}
|
|
288
|
+
function createLayout(appId) {
|
|
289
|
+
return `import { Outlet } from '@modern-js/plugin-tanstack/runtime';
|
|
290
|
+
import './index.css';
|
|
291
|
+
|
|
292
|
+
export default function Layout() {
|
|
293
|
+
return (
|
|
294
|
+
<div data-app-id="${appId}">
|
|
295
|
+
<Outlet />
|
|
296
|
+
</div>
|
|
297
|
+
);
|
|
298
|
+
}
|
|
299
|
+
`;
|
|
300
|
+
}
|
|
301
|
+
function createRemoteEntry(app) {
|
|
302
|
+
const tw = createTw(tailwindPrefixForApp(app));
|
|
303
|
+
const domain = app.domain ?? app.id;
|
|
304
|
+
return `import { useModernI18n } from '@modern-js/plugin-i18n/runtime';
|
|
305
|
+
|
|
306
|
+
export default function ${toPascalCase(domain)}Route() {
|
|
307
|
+
const { i18nInstance } = useModernI18n();
|
|
308
|
+
const t = i18nInstance['t'].bind(i18nInstance);
|
|
309
|
+
|
|
310
|
+
return (
|
|
311
|
+
<section className="${tw('rounded-2xl bg-white/90 p-5 shadow-xl shadow-stone-900/10')}" data-modern-boundary-id="${app.mfName}" data-modern-mf-expose="./Route">
|
|
312
|
+
<h2 className="${tw('text-2xl font-black')}">{t('${domain}.title')}</h2>
|
|
313
|
+
<p className="${tw('mt-2 text-stone-600')}">{t('${domain}.routeSurface')}</p>
|
|
314
|
+
</section>
|
|
315
|
+
);
|
|
316
|
+
}
|
|
317
|
+
`;
|
|
318
|
+
}
|
|
319
|
+
function createRemoteWidget(app) {
|
|
320
|
+
const tw = createTw(tailwindPrefixForApp(app));
|
|
321
|
+
const domain = app.domain ?? app.id;
|
|
322
|
+
const componentName = `${toPascalCase(domain)}Widget`;
|
|
323
|
+
return `import { useModernI18n } from '@modern-js/plugin-i18n/runtime';
|
|
324
|
+
|
|
325
|
+
export default function ${componentName}() {
|
|
326
|
+
const { i18nInstance } = useModernI18n();
|
|
327
|
+
const t = i18nInstance['t'].bind(i18nInstance);
|
|
328
|
+
|
|
329
|
+
return (
|
|
330
|
+
<section className="${tw('rounded-2xl bg-white/90 p-5 shadow-xl shadow-stone-900/10')}" data-modern-boundary-id="${app.mfName}" data-modern-mf-expose="./Widget">
|
|
331
|
+
<h2 className="${tw('text-2xl font-black')}">{t('${domain}.title')}</h2>
|
|
332
|
+
<p className="${tw('mt-2 text-stone-600')}">{t('${domain}.widgetBody')}</p>
|
|
333
|
+
</section>
|
|
334
|
+
);
|
|
335
|
+
}
|
|
336
|
+
`;
|
|
337
|
+
}
|
|
338
|
+
function createRemoteExposeComponent(app, expose) {
|
|
339
|
+
const tw = createTw(tailwindPrefixForApp(app));
|
|
340
|
+
if ('./Widget' === expose) return createRemoteWidget(app);
|
|
341
|
+
const componentName = `${toPascalCase(app.domain ?? app.id)}${toPascalCase(expose.replace(/^\.\//u, ''))}`;
|
|
342
|
+
const domain = app.domain ?? app.id;
|
|
343
|
+
return `import { useModernI18n } from '@modern-js/plugin-i18n/runtime';
|
|
344
|
+
|
|
345
|
+
export default function ${componentName}() {
|
|
346
|
+
const { i18nInstance } = useModernI18n();
|
|
347
|
+
const t = i18nInstance['t'].bind(i18nInstance);
|
|
348
|
+
|
|
349
|
+
return (
|
|
350
|
+
<section className="${tw('rounded-2xl bg-white/90 p-5 shadow-xl shadow-stone-900/10')}" data-modern-boundary-id="${app.mfName}" data-modern-mf-expose="${expose}">
|
|
351
|
+
<h2 className="${tw('text-2xl font-black')}">{t('${domain}.title')}</h2>
|
|
352
|
+
<p className="${tw('mt-2 text-stone-600')}">{t('${domain}.federatedSurface')}</p>
|
|
353
|
+
</section>
|
|
354
|
+
);
|
|
355
|
+
}
|
|
356
|
+
`;
|
|
357
|
+
}
|
|
358
|
+
function remoteComponentOutputPath(app, expose) {
|
|
359
|
+
const exposePath = app.exposes?.[expose];
|
|
360
|
+
if (!exposePath?.startsWith('./src/components/')) return;
|
|
361
|
+
return `${app.directory}/${exposePath.replace(/^\.\//u, '')}`;
|
|
362
|
+
}
|
|
363
|
+
export { createLayout, createRemoteEntry, createRemoteExposeComponent, createRemotePage, createRemoteWidget, createShellPage, createShellRemoteComponents, remoteComponentOutputPath };
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import { packageName, toCamelCase, toEnvSegment, toKebabCase, toPascalCase } from "./naming.js";
|
|
2
|
+
const GENERATED_CONTRACT_PATH = '.modernjs/ultramodern-generated-contract.json';
|
|
3
|
+
const shellApp = {
|
|
4
|
+
id: 'shell-super-app',
|
|
5
|
+
directory: 'apps/shell-super-app',
|
|
6
|
+
packageSuffix: 'shell-super-app',
|
|
7
|
+
displayName: 'Shell Super App',
|
|
8
|
+
kind: 'shell',
|
|
9
|
+
portEnv: 'SHELL_SUPER_APP_PORT',
|
|
10
|
+
port: 3020,
|
|
11
|
+
mfName: 'shellSuperApp',
|
|
12
|
+
verticalRefs: [],
|
|
13
|
+
ownership: {
|
|
14
|
+
team: 'super-app-platform',
|
|
15
|
+
slack: '#super-app-platform',
|
|
16
|
+
pagerDuty: 'pd-super-app-platform',
|
|
17
|
+
runbookRef: 'runbooks/wave2/shell-super-app.md',
|
|
18
|
+
adrRef: 'docs/super-app-rfc-adr/wave2/reference-topology.md#shell-super-app',
|
|
19
|
+
blastRadius: {
|
|
20
|
+
tier: 'tier-0-shell',
|
|
21
|
+
references: [
|
|
22
|
+
'docs/super-app-rfc-adr/wave2/blast-radius.md#shell',
|
|
23
|
+
'docs/super-app-rfc-adr/wave2/rollback.md#shell-lkg'
|
|
24
|
+
]
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
};
|
|
28
|
+
function createShellHost(remotes = []) {
|
|
29
|
+
return {
|
|
30
|
+
...shellApp,
|
|
31
|
+
verticalRefs: remotes.map((remote)=>remote.id)
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
const sharedPackages = [
|
|
35
|
+
{
|
|
36
|
+
id: 'shared-contracts',
|
|
37
|
+
directory: 'packages/shared-contracts',
|
|
38
|
+
description: 'Route, ownership, and topology contract placeholders.'
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
id: 'shared-design-tokens',
|
|
42
|
+
directory: 'packages/shared-design-tokens',
|
|
43
|
+
description: 'Design token placeholders consumed by shell and verticals.'
|
|
44
|
+
}
|
|
45
|
+
];
|
|
46
|
+
function createNeutralOwnership(id, tier = 'tier-2-vertical') {
|
|
47
|
+
return {
|
|
48
|
+
team: 'super-app-platform',
|
|
49
|
+
slack: '#super-app-platform',
|
|
50
|
+
pagerDuty: 'pd-super-app-platform',
|
|
51
|
+
runbookRef: `runbooks/verticals/${id}.md`,
|
|
52
|
+
adrRef: `docs/super-app-rfc-adr/verticals.md#${id}`,
|
|
53
|
+
blastRadius: {
|
|
54
|
+
tier,
|
|
55
|
+
references: [
|
|
56
|
+
`docs/super-app-rfc-adr/blast-radius.md#${id}`
|
|
57
|
+
]
|
|
58
|
+
}
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
function createVerticalDescriptor(name, port) {
|
|
62
|
+
const domain = toKebabCase(name);
|
|
63
|
+
const id = domain;
|
|
64
|
+
const displayPrefix = toPascalCase(domain).replace(/([a-z])([A-Z])/g, '$1 $2');
|
|
65
|
+
return {
|
|
66
|
+
id,
|
|
67
|
+
directory: `verticals/${domain}`,
|
|
68
|
+
packageSuffix: domain,
|
|
69
|
+
displayName: `${displayPrefix} Vertical`,
|
|
70
|
+
kind: 'vertical',
|
|
71
|
+
domain,
|
|
72
|
+
portEnv: `VERTICAL_${toEnvSegment(domain)}_PORT`,
|
|
73
|
+
port,
|
|
74
|
+
mfName: `vertical${toPascalCase(domain)}`,
|
|
75
|
+
exposes: {
|
|
76
|
+
'./Route': './src/federation-entry.tsx',
|
|
77
|
+
'./Widget': `./src/components/${domain}-widget.tsx`
|
|
78
|
+
},
|
|
79
|
+
effectApi: {
|
|
80
|
+
stem: domain,
|
|
81
|
+
prefix: `/${domain}-api`,
|
|
82
|
+
consumedBy: [
|
|
83
|
+
shellApp.id,
|
|
84
|
+
id
|
|
85
|
+
]
|
|
86
|
+
},
|
|
87
|
+
ownership: createNeutralOwnership(id)
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
function appHasEffectApi(app) {
|
|
91
|
+
return void 0 !== app.effectApi;
|
|
92
|
+
}
|
|
93
|
+
function effectApiPrefix(target) {
|
|
94
|
+
return target.effectApi?.prefix ?? `/${toKebabCase(target.id)}-api`;
|
|
95
|
+
}
|
|
96
|
+
function effectApiStem(target) {
|
|
97
|
+
return target.effectApi?.stem ?? toKebabCase(target.id).replace(/-api$/, '');
|
|
98
|
+
}
|
|
99
|
+
function verticalEffectApps(remotes = []) {
|
|
100
|
+
return remotes.filter(appHasEffectApi);
|
|
101
|
+
}
|
|
102
|
+
function remoteDependencyAlias(remote) {
|
|
103
|
+
return toCamelCase(remote.domain ?? remote.id.replace(/^remote-/, ''));
|
|
104
|
+
}
|
|
105
|
+
function zephyrRemoteDependency(scope, remote) {
|
|
106
|
+
return `${packageName(scope, remote.packageSuffix)}@workspace:*`;
|
|
107
|
+
}
|
|
108
|
+
function resolveRemoteRefs(app, remotes = []) {
|
|
109
|
+
const verticalRefs = app.verticalRefs ?? [];
|
|
110
|
+
return verticalRefs.map((remoteRef)=>remotes.find((remote)=>remote.id === remoteRef)).filter((remote)=>void 0 !== remote);
|
|
111
|
+
}
|
|
112
|
+
function createRemoteManifestEnv(remote) {
|
|
113
|
+
return `VERTICAL_${toEnvSegment(remote.domain ?? remote.id)}_MF_MANIFEST`;
|
|
114
|
+
}
|
|
115
|
+
function createModuleFederationRemoteContracts(app, remotes = []) {
|
|
116
|
+
return resolveRemoteRefs(app, remotes).map((remote)=>({
|
|
117
|
+
id: remote.id,
|
|
118
|
+
alias: remoteDependencyAlias(remote),
|
|
119
|
+
name: remote.mfName,
|
|
120
|
+
manifestEnv: createRemoteManifestEnv(remote),
|
|
121
|
+
manifestUrl: `http://localhost:${remote.port}/mf-manifest.json`
|
|
122
|
+
}));
|
|
123
|
+
}
|
|
124
|
+
function createCloudflareWorkerName(scope, app) {
|
|
125
|
+
return toKebabCase(`${scope}-${app.packageSuffix}`).slice(0, 63);
|
|
126
|
+
}
|
|
127
|
+
function createCloudflarePublicUrlEnv(app) {
|
|
128
|
+
return `ULTRAMODERN_PUBLIC_URL_${toEnvSegment(app.id)}`;
|
|
129
|
+
}
|
|
130
|
+
function appI18nNamespace(app) {
|
|
131
|
+
return 'shell' === app.kind ? 'shell' : app.domain ?? app.id;
|
|
132
|
+
}
|
|
133
|
+
export { GENERATED_CONTRACT_PATH, appHasEffectApi, appI18nNamespace, createCloudflarePublicUrlEnv, createCloudflareWorkerName, createModuleFederationRemoteContracts, createNeutralOwnership, createRemoteManifestEnv, createShellHost, createVerticalDescriptor, effectApiPrefix, effectApiStem, remoteDependencyAlias, resolveRemoteRefs, sharedPackages, shellApp, verticalEffectApps, zephyrRemoteDependency };
|