@bleedingdev/modern-js-create 3.2.0-ultramodern.31 → 3.2.0-ultramodern.33
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 +98 -1
- package/dist/index.js +1875 -722
- package/package.json +1 -1
- package/template/README.md +95 -0
package/dist/index.js
CHANGED
|
@@ -607,6 +607,9 @@ const modernPackageNames = [
|
|
|
607
607
|
'@modern-js/runtime'
|
|
608
608
|
];
|
|
609
609
|
const ULTRAMODERN_WORKSPACE_FLAG = '--ultramodern-workspace';
|
|
610
|
+
function isRecord(value) {
|
|
611
|
+
return null !== value && 'object' == typeof value && !Array.isArray(value);
|
|
612
|
+
}
|
|
610
613
|
const shellApp = {
|
|
611
614
|
id: 'shell-super-app',
|
|
612
615
|
directory: 'apps/shell-super-app',
|
|
@@ -617,9 +620,9 @@ const shellApp = {
|
|
|
617
620
|
port: 3020,
|
|
618
621
|
mfName: 'shellSuperApp',
|
|
619
622
|
remoteRefs: [
|
|
620
|
-
'remote-
|
|
621
|
-
'remote-
|
|
622
|
-
'remote-
|
|
623
|
+
'remote-explore',
|
|
624
|
+
'remote-decide',
|
|
625
|
+
'remote-checkout'
|
|
623
626
|
],
|
|
624
627
|
ownership: {
|
|
625
628
|
team: 'super-app-platform',
|
|
@@ -638,104 +641,123 @@ const shellApp = {
|
|
|
638
641
|
};
|
|
639
642
|
const remoteApps = [
|
|
640
643
|
{
|
|
641
|
-
id: 'remote-
|
|
642
|
-
directory: 'apps/remotes/remote-
|
|
643
|
-
packageSuffix: 'remote-
|
|
644
|
-
displayName: '
|
|
644
|
+
id: 'remote-explore',
|
|
645
|
+
directory: 'apps/remotes/remote-explore',
|
|
646
|
+
packageSuffix: 'remote-explore',
|
|
647
|
+
displayName: 'Explore Remote',
|
|
645
648
|
kind: 'vertical',
|
|
646
|
-
domain: '
|
|
647
|
-
portEnv: '
|
|
649
|
+
domain: 'explore',
|
|
650
|
+
portEnv: 'REMOTE_EXPLORE_PORT',
|
|
648
651
|
port: 3021,
|
|
649
|
-
mfName: '
|
|
652
|
+
mfName: 'remoteExplore',
|
|
650
653
|
exposes: {
|
|
654
|
+
'./Footer': './src/components/footer.tsx',
|
|
655
|
+
'./Header': './src/components/header.tsx',
|
|
656
|
+
'./Recommendations': './src/components/recommendations.tsx',
|
|
651
657
|
'./Route': './src/remote-entry.tsx',
|
|
652
|
-
'./
|
|
658
|
+
'./StorePicker': './src/components/store-picker.tsx'
|
|
653
659
|
},
|
|
654
660
|
effectApi: {
|
|
655
|
-
stem: '
|
|
656
|
-
prefix: '/
|
|
661
|
+
stem: 'explore',
|
|
662
|
+
prefix: '/explore-api',
|
|
657
663
|
consumedBy: [
|
|
658
664
|
shellApp.id,
|
|
659
|
-
'remote-
|
|
665
|
+
'remote-explore'
|
|
660
666
|
]
|
|
661
667
|
},
|
|
662
668
|
ownership: {
|
|
663
|
-
team: '
|
|
664
|
-
slack: '#
|
|
665
|
-
pagerDuty: 'pd-
|
|
666
|
-
runbookRef: 'runbooks/wave2/remote-
|
|
667
|
-
adrRef: 'docs/super-app-rfc-adr/wave2/reference-topology.md#remote-
|
|
669
|
+
team: 'tractor-explore',
|
|
670
|
+
slack: '#tractor-explore',
|
|
671
|
+
pagerDuty: 'pd-tractor-explore',
|
|
672
|
+
runbookRef: 'runbooks/wave2/remote-explore.md',
|
|
673
|
+
adrRef: 'docs/super-app-rfc-adr/wave2/reference-topology.md#remote-explore',
|
|
668
674
|
blastRadius: {
|
|
669
|
-
tier: 'tier-1-
|
|
675
|
+
tier: 'tier-1-tractor-discovery',
|
|
670
676
|
references: [
|
|
671
|
-
'docs/super-app-rfc-adr/wave2/blast-radius.md#
|
|
672
|
-
'docs/super-app-rfc-adr/wave2/rollback.md#
|
|
677
|
+
'docs/super-app-rfc-adr/wave2/blast-radius.md#explore',
|
|
678
|
+
'docs/super-app-rfc-adr/wave2/rollback.md#explore-lkg'
|
|
673
679
|
]
|
|
674
680
|
}
|
|
675
681
|
}
|
|
676
682
|
},
|
|
677
683
|
{
|
|
678
|
-
id: 'remote-
|
|
679
|
-
directory: 'apps/remotes/remote-
|
|
680
|
-
packageSuffix: 'remote-
|
|
681
|
-
displayName: '
|
|
684
|
+
id: 'remote-decide',
|
|
685
|
+
directory: 'apps/remotes/remote-decide',
|
|
686
|
+
packageSuffix: 'remote-decide',
|
|
687
|
+
displayName: 'Decide Remote',
|
|
682
688
|
kind: 'vertical',
|
|
683
|
-
domain: '
|
|
684
|
-
portEnv: '
|
|
689
|
+
domain: 'decide',
|
|
690
|
+
portEnv: 'REMOTE_DECIDE_PORT',
|
|
685
691
|
port: 3022,
|
|
686
|
-
mfName: '
|
|
692
|
+
mfName: 'remoteDecide',
|
|
693
|
+
remoteRefs: [
|
|
694
|
+
'remote-explore',
|
|
695
|
+
'remote-checkout'
|
|
696
|
+
],
|
|
687
697
|
exposes: {
|
|
688
|
-
'./
|
|
689
|
-
'./
|
|
698
|
+
'./ProductPage': './src/components/product-page.tsx',
|
|
699
|
+
'./Route': './src/remote-entry.tsx'
|
|
690
700
|
},
|
|
691
701
|
effectApi: {
|
|
692
|
-
stem: '
|
|
693
|
-
prefix: '/
|
|
702
|
+
stem: 'decide',
|
|
703
|
+
prefix: '/decide-api',
|
|
694
704
|
consumedBy: [
|
|
695
705
|
shellApp.id,
|
|
696
|
-
'remote-
|
|
706
|
+
'remote-decide'
|
|
697
707
|
]
|
|
698
708
|
},
|
|
699
709
|
ownership: {
|
|
700
|
-
team: '
|
|
701
|
-
slack: '#
|
|
702
|
-
pagerDuty: 'pd-
|
|
703
|
-
runbookRef: 'runbooks/wave2/remote-
|
|
704
|
-
adrRef: 'docs/super-app-rfc-adr/wave2/reference-topology.md#remote-
|
|
710
|
+
team: 'tractor-decide',
|
|
711
|
+
slack: '#tractor-decide',
|
|
712
|
+
pagerDuty: 'pd-tractor-decide',
|
|
713
|
+
runbookRef: 'runbooks/wave2/remote-decide.md',
|
|
714
|
+
adrRef: 'docs/super-app-rfc-adr/wave2/reference-topology.md#remote-decide',
|
|
705
715
|
blastRadius: {
|
|
706
|
-
tier: 'tier-
|
|
716
|
+
tier: 'tier-1-tractor-configuration',
|
|
707
717
|
references: [
|
|
708
|
-
'docs/super-app-rfc-adr/wave2/blast-radius.md#
|
|
709
|
-
'docs/super-app-rfc-adr/wave2/rollback.md#
|
|
718
|
+
'docs/super-app-rfc-adr/wave2/blast-radius.md#decide',
|
|
719
|
+
'docs/super-app-rfc-adr/wave2/rollback.md#decide-lkg'
|
|
710
720
|
]
|
|
711
721
|
}
|
|
712
722
|
}
|
|
713
723
|
},
|
|
714
724
|
{
|
|
715
|
-
id: 'remote-
|
|
716
|
-
directory: 'apps/remotes/remote-
|
|
717
|
-
packageSuffix: 'remote-
|
|
718
|
-
displayName: '
|
|
719
|
-
kind: '
|
|
720
|
-
domain: '
|
|
721
|
-
portEnv: '
|
|
725
|
+
id: 'remote-checkout',
|
|
726
|
+
directory: 'apps/remotes/remote-checkout',
|
|
727
|
+
packageSuffix: 'remote-checkout',
|
|
728
|
+
displayName: 'Checkout Remote',
|
|
729
|
+
kind: 'vertical',
|
|
730
|
+
domain: 'checkout',
|
|
731
|
+
portEnv: 'REMOTE_CHECKOUT_PORT',
|
|
722
732
|
port: 3023,
|
|
723
|
-
mfName: '
|
|
733
|
+
mfName: 'remoteCheckout',
|
|
724
734
|
exposes: {
|
|
725
|
-
'./
|
|
726
|
-
'./
|
|
735
|
+
'./AddToCart': './src/components/add-to-cart.tsx',
|
|
736
|
+
'./CartPage': './src/components/cart-page.tsx',
|
|
737
|
+
'./CheckoutPage': './src/components/checkout-page.tsx',
|
|
738
|
+
'./MiniCart': './src/components/mini-cart.tsx',
|
|
739
|
+
'./Route': './src/remote-entry.tsx',
|
|
740
|
+
'./ThanksPage': './src/components/thanks-page.tsx'
|
|
741
|
+
},
|
|
742
|
+
effectApi: {
|
|
743
|
+
stem: 'checkout',
|
|
744
|
+
prefix: '/checkout-api',
|
|
745
|
+
consumedBy: [
|
|
746
|
+
shellApp.id,
|
|
747
|
+
'remote-checkout'
|
|
748
|
+
]
|
|
727
749
|
},
|
|
728
750
|
ownership: {
|
|
729
|
-
team: '
|
|
730
|
-
slack: '#
|
|
731
|
-
pagerDuty: 'pd-
|
|
732
|
-
runbookRef: 'runbooks/wave2/remote-
|
|
733
|
-
adrRef: 'docs/super-app-rfc-adr/wave2/reference-topology.md#remote-
|
|
751
|
+
team: 'tractor-checkout',
|
|
752
|
+
slack: '#tractor-checkout',
|
|
753
|
+
pagerDuty: 'pd-tractor-checkout',
|
|
754
|
+
runbookRef: 'runbooks/wave2/remote-checkout.md',
|
|
755
|
+
adrRef: 'docs/super-app-rfc-adr/wave2/reference-topology.md#remote-checkout',
|
|
734
756
|
blastRadius: {
|
|
735
|
-
tier: 'tier-
|
|
757
|
+
tier: 'tier-1-tractor-purchase',
|
|
736
758
|
references: [
|
|
737
|
-
'docs/super-app-rfc-adr/wave2/blast-radius.md#
|
|
738
|
-
'docs/super-app-rfc-adr/wave2/rollback.md#
|
|
759
|
+
'docs/super-app-rfc-adr/wave2/blast-radius.md#checkout',
|
|
760
|
+
'docs/super-app-rfc-adr/wave2/rollback.md#checkout-lkg'
|
|
739
761
|
]
|
|
740
762
|
}
|
|
741
763
|
}
|
|
@@ -1100,11 +1122,11 @@ function createRootPackageJson(scope, packageSource) {
|
|
|
1100
1122
|
type: 'module',
|
|
1101
1123
|
packageManager: `pnpm@${PNPM_VERSION}`,
|
|
1102
1124
|
scripts: {
|
|
1103
|
-
dev: `pnpm --parallel --filter ${ultramodern_workspace_packageName(scope, shellApp.packageSuffix)} --filter ${ultramodern_workspace_packageName(scope, 'remote-
|
|
1125
|
+
dev: `pnpm --parallel --filter ${ultramodern_workspace_packageName(scope, shellApp.packageSuffix)} --filter ${ultramodern_workspace_packageName(scope, 'remote-explore')} --filter ${ultramodern_workspace_packageName(scope, 'remote-decide')} --filter ${ultramodern_workspace_packageName(scope, 'remote-checkout')} dev`,
|
|
1104
1126
|
'dev:shell': `pnpm --filter ${ultramodern_workspace_packageName(scope, shellApp.packageSuffix)} dev`,
|
|
1105
|
-
'dev:
|
|
1106
|
-
'dev:
|
|
1107
|
-
'dev:
|
|
1127
|
+
'dev:explore': `pnpm --filter ${ultramodern_workspace_packageName(scope, 'remote-explore')} dev`,
|
|
1128
|
+
'dev:decide': `pnpm --filter ${ultramodern_workspace_packageName(scope, 'remote-decide')} dev`,
|
|
1129
|
+
'dev:checkout': `pnpm --filter ${ultramodern_workspace_packageName(scope, 'remote-checkout')} dev`,
|
|
1108
1130
|
build: 'pnpm -r --filter "./apps/remotes/**" run build && pnpm --filter "./apps/shell-super-app" run build && pnpm ultramodern:assert-mf-types',
|
|
1109
1131
|
format: 'oxfmt .',
|
|
1110
1132
|
'format:check': 'oxfmt --check .',
|
|
@@ -1112,6 +1134,8 @@ function createRootPackageJson(scope, packageSource) {
|
|
|
1112
1134
|
'lint:fix': 'oxlint . --fix',
|
|
1113
1135
|
typecheck: `pnpm -r --filter "@${scope}/*" typecheck`,
|
|
1114
1136
|
'cloudflare:build': 'pnpm -r --filter "./apps/remotes/**" run cloudflare:build && pnpm --filter "./apps/shell-super-app" run cloudflare:build && pnpm ultramodern:assert-mf-types',
|
|
1137
|
+
'cloudflare:deploy': 'pnpm -r --filter "./apps/remotes/**" run cloudflare:deploy && pnpm --filter "./apps/shell-super-app" run cloudflare:deploy',
|
|
1138
|
+
'cloudflare:proof': "node ./scripts/proof-cloudflare-version.mjs --out .codex/reports/cloudflare-version-proof/public-url-proof.json",
|
|
1115
1139
|
'skills:install': "node ./scripts/bootstrap-agent-skills.mjs",
|
|
1116
1140
|
'skills:check': "node ./scripts/bootstrap-agent-skills.mjs --check",
|
|
1117
1141
|
'agents:refs:install': "node ./scripts/setup-agent-reference-repos.mjs",
|
|
@@ -1158,13 +1182,61 @@ function remoteDependencyAlias(remote) {
|
|
|
1158
1182
|
function zephyrRemoteDependency(scope, remote) {
|
|
1159
1183
|
return `${ultramodern_workspace_packageName(scope, remote.packageSuffix)}@workspace:*`;
|
|
1160
1184
|
}
|
|
1185
|
+
function resolveRemoteRefs(app, remotes = remoteApps) {
|
|
1186
|
+
const remoteRefs = app.remoteRefs ?? [];
|
|
1187
|
+
return remoteRefs.map((remoteRef)=>remotes.find((remote)=>remote.id === remoteRef)).filter((remote)=>void 0 !== remote);
|
|
1188
|
+
}
|
|
1189
|
+
function createModuleFederationRemoteContracts(app, remotes = remoteApps) {
|
|
1190
|
+
return resolveRemoteRefs(app, remotes).map((remote)=>({
|
|
1191
|
+
id: remote.id,
|
|
1192
|
+
alias: remoteDependencyAlias(remote),
|
|
1193
|
+
name: remote.mfName,
|
|
1194
|
+
manifestEnv: createRemoteManifestEnv(remote),
|
|
1195
|
+
manifestUrl: `http://localhost:${remote.port}/mf-manifest.json`
|
|
1196
|
+
}));
|
|
1197
|
+
}
|
|
1161
1198
|
function createZephyrDependencies(scope, app, remotes = remoteApps) {
|
|
1162
|
-
if (
|
|
1163
|
-
return Object.fromEntries(remotes.map((remote)=>[
|
|
1199
|
+
if (!app.remoteRefs?.length) return {};
|
|
1200
|
+
return Object.fromEntries(resolveRemoteRefs(app, remotes).map((remote)=>[
|
|
1164
1201
|
remoteDependencyAlias(remote),
|
|
1165
1202
|
zephyrRemoteDependency(scope, remote)
|
|
1166
1203
|
]));
|
|
1167
1204
|
}
|
|
1205
|
+
function createCloudflareWorkerName(scope, app) {
|
|
1206
|
+
return toKebabCase(`${scope}-${app.packageSuffix}`).slice(0, 63);
|
|
1207
|
+
}
|
|
1208
|
+
function createCloudflarePublicUrlEnv(app) {
|
|
1209
|
+
return `ULTRAMODERN_PUBLIC_URL_${toEnvSegment(app.id)}`;
|
|
1210
|
+
}
|
|
1211
|
+
function createCloudflareProofRoute(app) {
|
|
1212
|
+
const languageRoutes = createLocalisedUrlsMap(app);
|
|
1213
|
+
const firstCanonicalPath = Object.keys(languageRoutes)[0];
|
|
1214
|
+
const localizedPath = firstCanonicalPath && isRecord(languageRoutes[firstCanonicalPath]) ? languageRoutes[firstCanonicalPath].en : void 0;
|
|
1215
|
+
return {
|
|
1216
|
+
ssr: localizedPath ?? '/en',
|
|
1217
|
+
mfManifest: '/mf-manifest.json',
|
|
1218
|
+
locale: `/locales/en/${appI18nNamespace(app)}.json`,
|
|
1219
|
+
...appHasEffectApi(app) ? {
|
|
1220
|
+
effectReadiness: `${effectApiPrefix(app)}/effect/${effectApiStem(app)}/readiness`
|
|
1221
|
+
} : {}
|
|
1222
|
+
};
|
|
1223
|
+
}
|
|
1224
|
+
function createCloudflareDeployContract(scope, app) {
|
|
1225
|
+
return {
|
|
1226
|
+
target: 'cloudflare',
|
|
1227
|
+
workerName: createCloudflareWorkerName(scope, app),
|
|
1228
|
+
publicUrlEnv: createCloudflarePublicUrlEnv(app),
|
|
1229
|
+
compatibilityFlags: [
|
|
1230
|
+
'nodejs_compat'
|
|
1231
|
+
],
|
|
1232
|
+
assetsBinding: 'ASSETS',
|
|
1233
|
+
routes: createCloudflareProofRoute(app),
|
|
1234
|
+
evidence: {
|
|
1235
|
+
proofScript: "scripts/proof-cloudflare-version.mjs",
|
|
1236
|
+
reportDefault: '.codex/reports/cloudflare-version-proof/public-url-proof.json'
|
|
1237
|
+
}
|
|
1238
|
+
};
|
|
1239
|
+
}
|
|
1168
1240
|
function createTsConfigBase() {
|
|
1169
1241
|
return {
|
|
1170
1242
|
compilerOptions: {
|
|
@@ -1234,7 +1306,9 @@ function createAppPackage(scope, app, packageSource, enableTailwind) {
|
|
|
1234
1306
|
dev: 'modern dev',
|
|
1235
1307
|
build: app.exposes ? `modern build && node ${relativeRootFor(app.directory)}/scripts/assert-mf-types.mjs` : 'modern build',
|
|
1236
1308
|
'cloudflare:build': 'MODERNJS_DEPLOY=cloudflare modern build && MODERNJS_DEPLOY=cloudflare modern deploy',
|
|
1309
|
+
'cloudflare:deploy': 'MODERNJS_DEPLOY=cloudflare modern deploy',
|
|
1237
1310
|
'cloudflare:preview': 'MODERNJS_DEPLOY=cloudflare modern build && MODERNJS_DEPLOY=cloudflare modern deploy && wrangler dev --config .output/wrangler.json',
|
|
1311
|
+
'cloudflare:proof': `node ${relativeRootFor(app.directory)}/scripts/proof-cloudflare-version.mjs --app ${app.id}`,
|
|
1238
1312
|
serve: 'modern serve',
|
|
1239
1313
|
typecheck: effectTsgoTypecheckCommand
|
|
1240
1314
|
},
|
|
@@ -1255,6 +1329,9 @@ function createAppPackage(scope, app, packageSource, enableTailwind) {
|
|
|
1255
1329
|
'./effect/client': `./src/effect/${app.effectApi.stem}-client.ts`,
|
|
1256
1330
|
'./shared/effect/api': './shared/effect/api.ts'
|
|
1257
1331
|
};
|
|
1332
|
+
else if ('shell' === app.kind) packageJson.exports = {
|
|
1333
|
+
'./effect/clients': './src/effect/recommendations-client.ts'
|
|
1334
|
+
};
|
|
1258
1335
|
return packageJson;
|
|
1259
1336
|
}
|
|
1260
1337
|
function createServicePackage(scope, packageSource, enableTailwind, service = effectService) {
|
|
@@ -1315,12 +1392,16 @@ function createSharedPackage(scope, id, description, packageSource) {
|
|
|
1315
1392
|
"@typescript/native-preview": TYPESCRIPT_NATIVE_PREVIEW_VERSION
|
|
1316
1393
|
}
|
|
1317
1394
|
};
|
|
1395
|
+
if ('shared-design-tokens' === id) packageJson.exports = {
|
|
1396
|
+
...packageJson.exports,
|
|
1397
|
+
'./tokens.css': './src/tokens.css'
|
|
1398
|
+
};
|
|
1318
1399
|
if ('shared-effect-api' === id) packageJson.dependencies = {
|
|
1319
1400
|
'@modern-js/plugin-bff': modernPackageSpecifier('@modern-js/plugin-bff', packageSource)
|
|
1320
1401
|
};
|
|
1321
1402
|
return packageJson;
|
|
1322
1403
|
}
|
|
1323
|
-
function createAppModernConfig(app) {
|
|
1404
|
+
function createAppModernConfig(scope, app) {
|
|
1324
1405
|
const bffImport = appHasEffectApi(app) ? "import { bffPlugin } from '@modern-js/plugin-bff';\n" : '';
|
|
1325
1406
|
const bffConfig = appHasEffectApi(app) ? ` bff: {
|
|
1326
1407
|
effect: {
|
|
@@ -1343,6 +1424,7 @@ ${bffImport}import { i18nPlugin } from '@modern-js/plugin-i18n';
|
|
|
1343
1424
|
import { tanstackRouterPlugin } from '@modern-js/plugin-tanstack';
|
|
1344
1425
|
import { moduleFederationPlugin } from '@module-federation/modern-js-v3';
|
|
1345
1426
|
import { withZephyr as withZephyrRspack } from 'zephyr-rspack-plugin';
|
|
1427
|
+
import { ultramodernLocalisedUrls } from './src/routes/ultramodern-route-metadata';
|
|
1346
1428
|
|
|
1347
1429
|
type ZephyrRspackConfig = Parameters<ReturnType<typeof withZephyrRspack>>[0];
|
|
1348
1430
|
|
|
@@ -1361,22 +1443,12 @@ const zephyrRspackPlugin = () => ({
|
|
|
1361
1443
|
});
|
|
1362
1444
|
|
|
1363
1445
|
const appId = '${app.id}';
|
|
1446
|
+
const cloudflareWorkerName = '${createCloudflareWorkerName(scope, app)}';
|
|
1364
1447
|
const port = Number(process.env['${app.portEnv}'] ?? ${app.port});
|
|
1365
|
-
const
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
process.env['NODE_ENV'] === 'production' || process.argv.includes('build');
|
|
1370
|
-
|
|
1371
|
-
if (isProductionBuild && !hasConfiguredSiteUrl) {
|
|
1372
|
-
throw new Error(
|
|
1373
|
-
'MODERN_PUBLIC_SITE_URL must be set for production builds so canonical and hreflang URLs use the deployed origin.',
|
|
1374
|
-
);
|
|
1375
|
-
}
|
|
1376
|
-
|
|
1377
|
-
const siteUrl = hasConfiguredSiteUrl
|
|
1378
|
-
? configuredSiteUrl
|
|
1379
|
-
: \`http://localhost:\${port}\`;
|
|
1448
|
+
const siteUrl =
|
|
1449
|
+
process.env['MODERN_PUBLIC_SITE_URL'] ??
|
|
1450
|
+
process.env['${createCloudflarePublicUrlEnv(app)}'] ??
|
|
1451
|
+
\`http://localhost:\${port}\`;
|
|
1380
1452
|
|
|
1381
1453
|
export default defineConfig(
|
|
1382
1454
|
presetUltramodern(
|
|
@@ -1403,13 +1475,14 @@ ${bffConfig} output: {
|
|
|
1403
1475
|
tanstackRouterPlugin(),
|
|
1404
1476
|
i18nPlugin({
|
|
1405
1477
|
backend: {
|
|
1406
|
-
enabled:
|
|
1478
|
+
enabled: true,
|
|
1407
1479
|
},
|
|
1408
1480
|
reactI18next: false,
|
|
1409
1481
|
localeDetection: {
|
|
1410
1482
|
fallbackLanguage: 'en',
|
|
1411
1483
|
languages: ['en', 'cs'],
|
|
1412
1484
|
localePathRedirect: true,
|
|
1485
|
+
localisedUrls: ultramodernLocalisedUrls as Record<string, Record<string, string>>,
|
|
1413
1486
|
ignoreRedirectRoutes: [
|
|
1414
1487
|
'/@mf-types',
|
|
1415
1488
|
'/bundles',
|
|
@@ -1442,6 +1515,7 @@ ${bffPluginEntry} moduleFederationPlugin(),
|
|
|
1442
1515
|
deploy: {
|
|
1443
1516
|
target: 'cloudflare',
|
|
1444
1517
|
worker: {
|
|
1518
|
+
name: cloudflareWorkerName,
|
|
1445
1519
|
ssr: true,
|
|
1446
1520
|
},
|
|
1447
1521
|
},
|
|
@@ -1510,13 +1584,24 @@ ${entries.map(([key, entryValue])=>` '${key}': '${entryValue}',`).join('\n')}
|
|
|
1510
1584
|
function createRemoteManifestEnv(remote) {
|
|
1511
1585
|
return `REMOTE_${toEnvSegment(remote.domain ?? remote.id)}_MF_MANIFEST`;
|
|
1512
1586
|
}
|
|
1513
|
-
function
|
|
1514
|
-
const remoteEntries = remotes.map((remote)=>{
|
|
1587
|
+
function createModuleFederationRemotesConfig(app, remotes = remoteApps) {
|
|
1588
|
+
const remoteEntries = resolveRemoteRefs(app, remotes).map((remote)=>{
|
|
1515
1589
|
const key = remoteDependencyAlias(remote);
|
|
1516
1590
|
return ` ${key}:
|
|
1517
1591
|
process.env['${createRemoteManifestEnv(remote)}'] ??
|
|
1518
1592
|
'${remote.mfName}@http://localhost:${remote.port}/mf-manifest.json',`;
|
|
1519
1593
|
}).join('\n');
|
|
1594
|
+
if (!remoteEntries) return '';
|
|
1595
|
+
return ` remotes: {
|
|
1596
|
+
${remoteEntries}
|
|
1597
|
+
},
|
|
1598
|
+
`;
|
|
1599
|
+
}
|
|
1600
|
+
function createShellModuleFederationConfig(remotes = remoteApps) {
|
|
1601
|
+
const shellHost = {
|
|
1602
|
+
...shellApp,
|
|
1603
|
+
remoteRefs: remotes.map((remote)=>remote.id)
|
|
1604
|
+
};
|
|
1520
1605
|
return `// @effect-diagnostics nodeBuiltinImport:off processEnv:off
|
|
1521
1606
|
import { createRequire } from 'node:module';
|
|
1522
1607
|
import { createModuleFederationConfig } from '@module-federation/modern-js-v3';
|
|
@@ -1540,10 +1625,7 @@ export default createModuleFederationConfig({
|
|
|
1540
1625
|
},
|
|
1541
1626
|
filename: 'remoteEntry.js',
|
|
1542
1627
|
name: '${shellApp.mfName}',
|
|
1543
|
-
|
|
1544
|
-
${remoteEntries}
|
|
1545
|
-
},
|
|
1546
|
-
${createSharedModuleFederationConfig()},
|
|
1628
|
+
${createModuleFederationRemotesConfig(shellHost, remotes)}${createSharedModuleFederationConfig()},
|
|
1547
1629
|
});
|
|
1548
1630
|
`;
|
|
1549
1631
|
}
|
|
@@ -1570,7 +1652,7 @@ export const ultramodernApiMarker = {
|
|
|
1570
1652
|
} as const;
|
|
1571
1653
|
`;
|
|
1572
1654
|
}
|
|
1573
|
-
function createRemoteModuleFederationConfig(app) {
|
|
1655
|
+
function createRemoteModuleFederationConfig(app, remotes = remoteApps) {
|
|
1574
1656
|
const exposes = formatTsObjectLiteral(app.exposes ?? {});
|
|
1575
1657
|
return `// @effect-diagnostics nodeBuiltinImport:off
|
|
1576
1658
|
import { createRequire } from 'node:module';
|
|
@@ -1596,12 +1678,248 @@ export default createModuleFederationConfig({
|
|
|
1596
1678
|
exposes: ${exposes},
|
|
1597
1679
|
filename: 'remoteEntry.js',
|
|
1598
1680
|
name: '${app.mfName}',
|
|
1599
|
-
${createSharedModuleFederationConfig()},
|
|
1681
|
+
${createModuleFederationRemotesConfig(app, remotes)}${createSharedModuleFederationConfig()},
|
|
1600
1682
|
});
|
|
1601
1683
|
`;
|
|
1602
1684
|
}
|
|
1603
|
-
function
|
|
1604
|
-
return
|
|
1685
|
+
function appI18nNamespace(app) {
|
|
1686
|
+
return 'shell' === app.kind ? 'shell' : app.domain ?? app.id;
|
|
1687
|
+
}
|
|
1688
|
+
function createRouteOwnedI18nPaths(app) {
|
|
1689
|
+
const namespace = appI18nNamespace(app);
|
|
1690
|
+
const base = {
|
|
1691
|
+
mfBoundaryId: app.mfName,
|
|
1692
|
+
namespace,
|
|
1693
|
+
ownerAppId: app.id
|
|
1694
|
+
};
|
|
1695
|
+
if ('shell' === app.kind) return [
|
|
1696
|
+
{
|
|
1697
|
+
...base,
|
|
1698
|
+
canonicalPath: '/',
|
|
1699
|
+
id: 'shell-home',
|
|
1700
|
+
localisedPaths: {
|
|
1701
|
+
cs: '/',
|
|
1702
|
+
en: '/'
|
|
1703
|
+
},
|
|
1704
|
+
titleKey: 'shell.title'
|
|
1705
|
+
}
|
|
1706
|
+
];
|
|
1707
|
+
if ('explore' === app.domain) return [
|
|
1708
|
+
{
|
|
1709
|
+
...base,
|
|
1710
|
+
canonicalPath: '/',
|
|
1711
|
+
id: 'explore-home',
|
|
1712
|
+
localisedPaths: {
|
|
1713
|
+
cs: '/',
|
|
1714
|
+
en: '/'
|
|
1715
|
+
},
|
|
1716
|
+
titleKey: 'explore.title'
|
|
1717
|
+
},
|
|
1718
|
+
{
|
|
1719
|
+
...base,
|
|
1720
|
+
canonicalPath: '/tractors',
|
|
1721
|
+
id: 'explore-listing',
|
|
1722
|
+
localisedPaths: {
|
|
1723
|
+
cs: '/traktory',
|
|
1724
|
+
en: '/tractors'
|
|
1725
|
+
},
|
|
1726
|
+
titleKey: 'explore.routes.listing'
|
|
1727
|
+
},
|
|
1728
|
+
{
|
|
1729
|
+
...base,
|
|
1730
|
+
canonicalPath: '/stores',
|
|
1731
|
+
id: 'explore-store-picker',
|
|
1732
|
+
localisedPaths: {
|
|
1733
|
+
cs: '/prodejci',
|
|
1734
|
+
en: '/stores'
|
|
1735
|
+
},
|
|
1736
|
+
titleKey: 'explore.routes.storePicker'
|
|
1737
|
+
},
|
|
1738
|
+
{
|
|
1739
|
+
...base,
|
|
1740
|
+
canonicalPath: '/unavailable',
|
|
1741
|
+
id: 'explore-unavailable',
|
|
1742
|
+
localisedPaths: {
|
|
1743
|
+
cs: '/nedostupne',
|
|
1744
|
+
en: '/unavailable'
|
|
1745
|
+
},
|
|
1746
|
+
titleKey: 'explore.routes.unavailable'
|
|
1747
|
+
}
|
|
1748
|
+
];
|
|
1749
|
+
if ('decide' === app.domain) return [
|
|
1750
|
+
{
|
|
1751
|
+
...base,
|
|
1752
|
+
canonicalPath: '/',
|
|
1753
|
+
id: 'decide-home',
|
|
1754
|
+
localisedPaths: {
|
|
1755
|
+
cs: '/',
|
|
1756
|
+
en: '/'
|
|
1757
|
+
},
|
|
1758
|
+
titleKey: 'decide.title'
|
|
1759
|
+
},
|
|
1760
|
+
{
|
|
1761
|
+
...base,
|
|
1762
|
+
canonicalPath: '/tractors',
|
|
1763
|
+
id: 'decide-listing-parent',
|
|
1764
|
+
localisedPaths: {
|
|
1765
|
+
cs: '/traktory',
|
|
1766
|
+
en: '/tractors'
|
|
1767
|
+
},
|
|
1768
|
+
titleKey: 'decide.routes.listing'
|
|
1769
|
+
},
|
|
1770
|
+
{
|
|
1771
|
+
...base,
|
|
1772
|
+
canonicalPath: '/tractors/:slug',
|
|
1773
|
+
id: 'decide-product-detail',
|
|
1774
|
+
localisedPaths: {
|
|
1775
|
+
cs: '/traktory/:slug',
|
|
1776
|
+
en: '/tractors/:slug'
|
|
1777
|
+
},
|
|
1778
|
+
titleKey: 'decide.routes.productDetail'
|
|
1779
|
+
},
|
|
1780
|
+
{
|
|
1781
|
+
...base,
|
|
1782
|
+
canonicalPath: '/unavailable',
|
|
1783
|
+
id: 'decide-unavailable',
|
|
1784
|
+
localisedPaths: {
|
|
1785
|
+
cs: '/nedostupne',
|
|
1786
|
+
en: '/unavailable'
|
|
1787
|
+
},
|
|
1788
|
+
titleKey: 'decide.routes.unavailable'
|
|
1789
|
+
}
|
|
1790
|
+
];
|
|
1791
|
+
if ('checkout' === app.domain) return [
|
|
1792
|
+
{
|
|
1793
|
+
...base,
|
|
1794
|
+
canonicalPath: '/',
|
|
1795
|
+
id: 'checkout-home',
|
|
1796
|
+
localisedPaths: {
|
|
1797
|
+
cs: '/',
|
|
1798
|
+
en: '/'
|
|
1799
|
+
},
|
|
1800
|
+
titleKey: 'checkout.title'
|
|
1801
|
+
},
|
|
1802
|
+
{
|
|
1803
|
+
...base,
|
|
1804
|
+
canonicalPath: '/cart',
|
|
1805
|
+
id: 'checkout-cart',
|
|
1806
|
+
localisedPaths: {
|
|
1807
|
+
cs: '/kosik',
|
|
1808
|
+
en: '/cart'
|
|
1809
|
+
},
|
|
1810
|
+
titleKey: 'checkout.routes.cart'
|
|
1811
|
+
},
|
|
1812
|
+
{
|
|
1813
|
+
...base,
|
|
1814
|
+
canonicalPath: '/checkout',
|
|
1815
|
+
id: 'checkout-start',
|
|
1816
|
+
localisedPaths: {
|
|
1817
|
+
cs: '/pokladna',
|
|
1818
|
+
en: '/checkout'
|
|
1819
|
+
},
|
|
1820
|
+
titleKey: 'checkout.routes.checkout'
|
|
1821
|
+
},
|
|
1822
|
+
{
|
|
1823
|
+
...base,
|
|
1824
|
+
canonicalPath: '/checkout/thank-you',
|
|
1825
|
+
id: 'checkout-thank-you-parent',
|
|
1826
|
+
localisedPaths: {
|
|
1827
|
+
cs: '/pokladna/dekujeme',
|
|
1828
|
+
en: '/checkout/thank-you'
|
|
1829
|
+
},
|
|
1830
|
+
titleKey: 'checkout.routes.thankYou'
|
|
1831
|
+
},
|
|
1832
|
+
{
|
|
1833
|
+
...base,
|
|
1834
|
+
canonicalPath: '/checkout/thank-you/:orderId?',
|
|
1835
|
+
id: 'checkout-thank-you',
|
|
1836
|
+
localisedPaths: {
|
|
1837
|
+
cs: '/pokladna/dekujeme/:orderId?',
|
|
1838
|
+
en: '/checkout/thank-you/:orderId?'
|
|
1839
|
+
},
|
|
1840
|
+
titleKey: 'checkout.routes.thankYou'
|
|
1841
|
+
},
|
|
1842
|
+
{
|
|
1843
|
+
...base,
|
|
1844
|
+
canonicalPath: '/unavailable',
|
|
1845
|
+
id: 'checkout-unavailable',
|
|
1846
|
+
localisedPaths: {
|
|
1847
|
+
cs: '/nedostupne',
|
|
1848
|
+
en: '/unavailable'
|
|
1849
|
+
},
|
|
1850
|
+
titleKey: 'checkout.routes.unavailable'
|
|
1851
|
+
}
|
|
1852
|
+
];
|
|
1853
|
+
return [
|
|
1854
|
+
{
|
|
1855
|
+
...base,
|
|
1856
|
+
canonicalPath: '/',
|
|
1857
|
+
id: `${app.id}-home`,
|
|
1858
|
+
localisedPaths: {
|
|
1859
|
+
cs: '/',
|
|
1860
|
+
en: '/'
|
|
1861
|
+
},
|
|
1862
|
+
titleKey: `${namespace}.title`
|
|
1863
|
+
}
|
|
1864
|
+
];
|
|
1865
|
+
}
|
|
1866
|
+
function createLocalisedUrlsMap(app) {
|
|
1867
|
+
return Object.fromEntries(createRouteOwnedI18nPaths(app).filter((route)=>'/' !== route.canonicalPath).map((route)=>[
|
|
1868
|
+
route.canonicalPath,
|
|
1869
|
+
route.localisedPaths
|
|
1870
|
+
]));
|
|
1871
|
+
}
|
|
1872
|
+
function createRouteMetadataModule(app) {
|
|
1873
|
+
const routes = createRouteOwnedI18nPaths(app);
|
|
1874
|
+
const localisedUrls = createLocalisedUrlsMap(app);
|
|
1875
|
+
const namespace = appI18nNamespace(app);
|
|
1876
|
+
return `export const ultramodernRouteNamespace = '${namespace}' as const;
|
|
1877
|
+
|
|
1878
|
+
export const ultramodernRouteMetadata = ${JSON.stringify(routes, null, 2)} as const;
|
|
1879
|
+
|
|
1880
|
+
export const ultramodernLocalisedUrls = ${JSON.stringify(localisedUrls, null, 2)} as const;
|
|
1881
|
+
|
|
1882
|
+
export const ultramodernRouteConfig = {
|
|
1883
|
+
source: 'route-owned',
|
|
1884
|
+
namespace: ultramodernRouteNamespace,
|
|
1885
|
+
localisedUrls: ultramodernLocalisedUrls,
|
|
1886
|
+
routes: ultramodernRouteMetadata,
|
|
1887
|
+
} as const;
|
|
1888
|
+
`;
|
|
1889
|
+
}
|
|
1890
|
+
function routeSegmentToDirectory(segment) {
|
|
1891
|
+
if (segment.startsWith(':')) {
|
|
1892
|
+
const name = segment.slice(1).replace(/\?$/u, '');
|
|
1893
|
+
return segment.endsWith('?') ? `[${name}$]` : `[${name}]`;
|
|
1894
|
+
}
|
|
1895
|
+
return segment;
|
|
1896
|
+
}
|
|
1897
|
+
function createRoutePageFilePath(app, canonicalPath) {
|
|
1898
|
+
const segments = canonicalPath.split('/').filter(Boolean).map(routeSegmentToDirectory);
|
|
1899
|
+
return `${app.directory}/src/routes/[lang]/${[
|
|
1900
|
+
...segments,
|
|
1901
|
+
'page.tsx'
|
|
1902
|
+
].join('/')}`;
|
|
1903
|
+
}
|
|
1904
|
+
function createRouteAliasPage(canonicalPath) {
|
|
1905
|
+
const depth = canonicalPath.split('/').filter(Boolean).length;
|
|
1906
|
+
const rootPageImport = `${'../'.repeat(depth)}page`;
|
|
1907
|
+
return `export { default } from '${rootPageImport}';
|
|
1908
|
+
`;
|
|
1909
|
+
}
|
|
1910
|
+
function createAppEnvDts(app, remotes = remoteApps) {
|
|
1911
|
+
const remoteModuleDeclarations = resolveRemoteRefs(app, remotes).flatMap((remote)=>Object.keys(remote.exposes ?? {}).filter((expose)=>'./Route' !== expose).map((expose)=>{
|
|
1912
|
+
const moduleName = `${remoteDependencyAlias(remote)}/${expose.replace(/^\.\//u, '')}`;
|
|
1913
|
+
return `declare module '${moduleName}' {
|
|
1914
|
+
const Component: import('react').ComponentType<Record<string, never>>;
|
|
1915
|
+
export default Component;
|
|
1916
|
+
}
|
|
1917
|
+
`;
|
|
1918
|
+
})).join('\n');
|
|
1919
|
+
return `/// <reference types='@modern-js/app-tools/types' />
|
|
1920
|
+
|
|
1921
|
+
declare const ULTRAMODERN_SITE_URL: string;
|
|
1922
|
+
${remoteModuleDeclarations ? `\n${remoteModuleDeclarations}` : ''}`;
|
|
1605
1923
|
}
|
|
1606
1924
|
function createServiceModernConfigFor(service = effectService) {
|
|
1607
1925
|
return `// @effect-diagnostics processEnv:off
|
|
@@ -1639,16 +1957,20 @@ export default defineConfig(
|
|
|
1639
1957
|
`;
|
|
1640
1958
|
}
|
|
1641
1959
|
function createAppRuntimeConfig(app) {
|
|
1960
|
+
const namespace = appI18nNamespace(app);
|
|
1642
1961
|
const resources = {
|
|
1643
1962
|
cs: {
|
|
1963
|
+
[namespace]: createAppLocaleMessages(app, 'cs'),
|
|
1644
1964
|
translation: createAppLocaleMessages(app, 'cs')
|
|
1645
1965
|
},
|
|
1646
1966
|
en: {
|
|
1967
|
+
[namespace]: createAppLocaleMessages(app, 'en'),
|
|
1647
1968
|
translation: createAppLocaleMessages(app, 'en')
|
|
1648
1969
|
}
|
|
1649
1970
|
};
|
|
1650
1971
|
return `import { defineRuntimeConfig } from '@modern-js/runtime';
|
|
1651
1972
|
import { createInstance } from 'i18next';
|
|
1973
|
+
import { ultramodernRouteNamespace } from './routes/ultramodern-route-metadata';
|
|
1652
1974
|
|
|
1653
1975
|
const i18nInstance = createInstance();
|
|
1654
1976
|
|
|
@@ -1656,12 +1978,12 @@ export default defineRuntimeConfig({
|
|
|
1656
1978
|
i18n: {
|
|
1657
1979
|
i18nInstance,
|
|
1658
1980
|
initOptions: {
|
|
1659
|
-
defaultNS:
|
|
1981
|
+
defaultNS: ultramodernRouteNamespace,
|
|
1660
1982
|
fallbackLng: 'en',
|
|
1661
1983
|
interpolation: {
|
|
1662
1984
|
escapeValue: false,
|
|
1663
1985
|
},
|
|
1664
|
-
ns: ['translation'],
|
|
1986
|
+
ns: [ultramodernRouteNamespace, 'translation'],
|
|
1665
1987
|
resources: ${JSON.stringify(resources, null, 8).split('\n').join('\n ')},
|
|
1666
1988
|
supportedLngs: ['en', 'cs'],
|
|
1667
1989
|
},
|
|
@@ -1672,10 +1994,16 @@ export default defineRuntimeConfig({
|
|
|
1672
1994
|
});
|
|
1673
1995
|
`;
|
|
1674
1996
|
}
|
|
1675
|
-
function
|
|
1676
|
-
return
|
|
1677
|
-
|
|
1678
|
-
|
|
1997
|
+
function createCssTokenImport(scope) {
|
|
1998
|
+
return `@import '${ultramodern_workspace_packageName(scope, 'shared-design-tokens')}/tokens.css';\n`;
|
|
1999
|
+
}
|
|
2000
|
+
function createShellStyles(enableTailwind, scope) {
|
|
2001
|
+
return `${enableTailwind ? "@import 'tailwindcss';\n" : ''}${createCssTokenImport(scope)}
|
|
2002
|
+
|
|
2003
|
+
@layer ultramodern-shell-base {
|
|
2004
|
+
:root {
|
|
2005
|
+
color: var(--um-color-foreground);
|
|
2006
|
+
background: var(--um-color-canvas);
|
|
1679
2007
|
font-family:
|
|
1680
2008
|
Geist,
|
|
1681
2009
|
Inter,
|
|
@@ -1703,9 +2031,52 @@ nav {
|
|
|
1703
2031
|
}
|
|
1704
2032
|
|
|
1705
2033
|
a {
|
|
1706
|
-
color:
|
|
2034
|
+
color: var(--um-color-link);
|
|
2035
|
+
}
|
|
2036
|
+
}
|
|
2037
|
+
|
|
2038
|
+
@layer ultramodern-shell-overlay {
|
|
2039
|
+
.boundary-overlay {
|
|
2040
|
+
inset: 0;
|
|
2041
|
+
pointer-events: none;
|
|
2042
|
+
position: fixed;
|
|
2043
|
+
z-index: 70;
|
|
2044
|
+
}
|
|
2045
|
+
|
|
2046
|
+
.boundary-overlay__box {
|
|
2047
|
+
border: 0.0625rem solid var(--boundary-color);
|
|
2048
|
+
border-radius: 0.55rem;
|
|
2049
|
+
box-shadow:
|
|
2050
|
+
0 0 0 0.0625rem rgba(255, 255, 255, 0.72),
|
|
2051
|
+
0 0.35rem 1.25rem color-mix(in srgb, var(--boundary-color) 20%, transparent);
|
|
2052
|
+
position: fixed;
|
|
2053
|
+
}
|
|
2054
|
+
|
|
2055
|
+
.boundary-overlay__label {
|
|
2056
|
+
background: color-mix(in srgb, var(--boundary-color) 88%, white);
|
|
2057
|
+
border-radius: 999px;
|
|
2058
|
+
color: #0b0a08;
|
|
2059
|
+
font-size: 0.7rem;
|
|
2060
|
+
font-weight: 850;
|
|
2061
|
+
line-height: 1;
|
|
2062
|
+
padding: 0.3rem 0.55rem;
|
|
2063
|
+
position: absolute;
|
|
2064
|
+
right: 0.35rem;
|
|
2065
|
+
top: 0.35rem;
|
|
2066
|
+
white-space: nowrap;
|
|
2067
|
+
}
|
|
2068
|
+
|
|
2069
|
+
.boundary-overlay__box[data-label-placement="above"] .boundary-overlay__label {
|
|
2070
|
+
bottom: calc(100% + 0.25rem);
|
|
2071
|
+
top: auto;
|
|
1707
2072
|
}
|
|
2073
|
+
}
|
|
2074
|
+
`;
|
|
2075
|
+
}
|
|
2076
|
+
function createRemoteStyles(enableTailwind, scope, app) {
|
|
2077
|
+
if ('commerce' === app.domain) return `${enableTailwind ? "@import 'tailwindcss';\n" : ''}${createCssTokenImport(scope)}
|
|
1708
2078
|
|
|
2079
|
+
@layer ultramodern-remote-${app.domain} {
|
|
1709
2080
|
.commerce-shell {
|
|
1710
2081
|
background: #f1eadc;
|
|
1711
2082
|
color: #0b0a08;
|
|
@@ -1933,41 +2304,6 @@ a {
|
|
|
1933
2304
|
width: 1rem;
|
|
1934
2305
|
}
|
|
1935
2306
|
|
|
1936
|
-
.boundary-overlay {
|
|
1937
|
-
inset: 0;
|
|
1938
|
-
pointer-events: none;
|
|
1939
|
-
position: fixed;
|
|
1940
|
-
z-index: 70;
|
|
1941
|
-
}
|
|
1942
|
-
|
|
1943
|
-
.boundary-overlay__box {
|
|
1944
|
-
border: 0.0625rem solid var(--boundary-color);
|
|
1945
|
-
border-radius: 0.55rem;
|
|
1946
|
-
box-shadow:
|
|
1947
|
-
0 0 0 0.0625rem rgba(255, 255, 255, 0.72),
|
|
1948
|
-
0 0.35rem 1.25rem color-mix(in srgb, var(--boundary-color) 20%, transparent);
|
|
1949
|
-
position: fixed;
|
|
1950
|
-
}
|
|
1951
|
-
|
|
1952
|
-
.boundary-overlay__label {
|
|
1953
|
-
background: color-mix(in srgb, var(--boundary-color) 88%, white);
|
|
1954
|
-
border-radius: 999px;
|
|
1955
|
-
color: #0b0a08;
|
|
1956
|
-
font-size: 0.7rem;
|
|
1957
|
-
font-weight: 850;
|
|
1958
|
-
line-height: 1;
|
|
1959
|
-
padding: 0.3rem 0.55rem;
|
|
1960
|
-
position: absolute;
|
|
1961
|
-
right: 0.35rem;
|
|
1962
|
-
top: 0.35rem;
|
|
1963
|
-
white-space: nowrap;
|
|
1964
|
-
}
|
|
1965
|
-
|
|
1966
|
-
.boundary-overlay__box[data-label-placement="above"] .boundary-overlay__label {
|
|
1967
|
-
bottom: calc(100% + 0.25rem);
|
|
1968
|
-
top: auto;
|
|
1969
|
-
}
|
|
1970
|
-
|
|
1971
2307
|
@media (max-width: 860px) {
|
|
1972
2308
|
.commerce-header,
|
|
1973
2309
|
.commerce-footer,
|
|
@@ -1987,8 +2323,78 @@ a {
|
|
|
1987
2323
|
min-height: 20rem;
|
|
1988
2324
|
}
|
|
1989
2325
|
}
|
|
2326
|
+
}
|
|
2327
|
+
`;
|
|
2328
|
+
return `${enableTailwind ? "@import 'tailwindcss';\n" : ''}${createCssTokenImport(scope)}
|
|
2329
|
+
|
|
2330
|
+
@layer ultramodern-remote-${app.domain ?? app.id} {
|
|
2331
|
+
[data-app-id="${app.id}"] {
|
|
2332
|
+
color: var(--um-color-foreground);
|
|
2333
|
+
background: var(--um-color-surface);
|
|
2334
|
+
font-family:
|
|
2335
|
+
Geist,
|
|
2336
|
+
Inter,
|
|
2337
|
+
ui-sans-serif,
|
|
2338
|
+
system-ui,
|
|
2339
|
+
-apple-system,
|
|
2340
|
+
BlinkMacSystemFont,
|
|
2341
|
+
"Segoe UI",
|
|
2342
|
+
sans-serif;
|
|
2343
|
+
min-height: 100vh;
|
|
2344
|
+
}
|
|
2345
|
+
|
|
2346
|
+
[data-app-id="${app.id}"] main {
|
|
2347
|
+
min-height: 100vh;
|
|
2348
|
+
padding: 2rem;
|
|
2349
|
+
}
|
|
2350
|
+
|
|
2351
|
+
[data-app-id="${app.id}"] nav {
|
|
2352
|
+
display: flex;
|
|
2353
|
+
gap: 0.75rem;
|
|
2354
|
+
margin-bottom: 2rem;
|
|
2355
|
+
}
|
|
2356
|
+
|
|
2357
|
+
[data-app-id="${app.id}"] a {
|
|
2358
|
+
color: var(--um-color-link);
|
|
2359
|
+
}
|
|
2360
|
+
|
|
2361
|
+
[data-mf-remote="${app.id}"] {
|
|
2362
|
+
border: 0.0625rem solid color-mix(in srgb, var(--um-color-accent) 30%, transparent);
|
|
2363
|
+
border-radius: 0.5rem;
|
|
2364
|
+
padding: 1rem;
|
|
2365
|
+
}
|
|
2366
|
+
}
|
|
2367
|
+
`;
|
|
2368
|
+
}
|
|
2369
|
+
function createServiceStyles(enableTailwind, scope, service) {
|
|
2370
|
+
return `${enableTailwind ? "@import 'tailwindcss';\n" : ''}${createCssTokenImport(scope)}
|
|
2371
|
+
|
|
2372
|
+
@layer ultramodern-effect-service {
|
|
2373
|
+
[data-app-id="${service.id}"] {
|
|
2374
|
+
color: var(--um-color-foreground);
|
|
2375
|
+
background: var(--um-color-surface);
|
|
2376
|
+
font-family:
|
|
2377
|
+
Geist,
|
|
2378
|
+
Inter,
|
|
2379
|
+
ui-sans-serif,
|
|
2380
|
+
system-ui,
|
|
2381
|
+
-apple-system,
|
|
2382
|
+
BlinkMacSystemFont,
|
|
2383
|
+
"Segoe UI",
|
|
2384
|
+
sans-serif;
|
|
2385
|
+
min-height: 100vh;
|
|
2386
|
+
}
|
|
2387
|
+
|
|
2388
|
+
[data-app-id="${service.id}"] main {
|
|
2389
|
+
min-height: 100vh;
|
|
2390
|
+
padding: 2rem;
|
|
2391
|
+
}
|
|
2392
|
+
}
|
|
1990
2393
|
`;
|
|
1991
2394
|
}
|
|
2395
|
+
function createAppStyles(enableTailwind, scope, app) {
|
|
2396
|
+
return 'shell' === app.kind ? createShellStyles(enableTailwind, scope) : createRemoteStyles(enableTailwind, scope, app);
|
|
2397
|
+
}
|
|
1992
2398
|
function createPostcssConfig() {
|
|
1993
2399
|
return `export default {
|
|
1994
2400
|
plugins: {
|
|
@@ -2010,57 +2416,177 @@ function createLocalizedHeadComponent() {
|
|
|
2010
2416
|
const supportedLanguages = ['en', 'cs'] as const;
|
|
2011
2417
|
type SupportedLanguage = (typeof supportedLanguages)[number];
|
|
2012
2418
|
|
|
2013
|
-
const
|
|
2419
|
+
const localisedUrls = ultramodernLocalisedUrls as Record<
|
|
2420
|
+
string,
|
|
2421
|
+
Record<SupportedLanguage, string>
|
|
2422
|
+
>;
|
|
2014
2423
|
|
|
2015
|
-
const
|
|
2016
|
-
|
|
2017
|
-
|
|
2424
|
+
const isSupportedLanguage = (value: string): value is SupportedLanguage =>
|
|
2425
|
+
supportedLanguages.includes(value as SupportedLanguage);
|
|
2426
|
+
|
|
2427
|
+
const normalisePath = (pathname: string) => {
|
|
2428
|
+
const normalised = pathname.replace(/\\/+$/u, '').replace(/\\/+/gu, '/');
|
|
2429
|
+
return normalised.length > 0 ? normalised : '/';
|
|
2018
2430
|
};
|
|
2019
|
-
const LocalizedHead = () => {
|
|
2020
|
-
const canonicalPath = localizedPath(fallbackLanguage);
|
|
2021
2431
|
|
|
2022
|
-
|
|
2023
|
-
|
|
2024
|
-
|
|
2025
|
-
|
|
2026
|
-
|
|
2027
|
-
|
|
2028
|
-
hrefLang={code}
|
|
2029
|
-
key={code}
|
|
2030
|
-
rel="alternate"
|
|
2031
|
-
/>
|
|
2032
|
-
))}
|
|
2033
|
-
<link
|
|
2034
|
-
href={absoluteUrl(localizedPath(fallbackLanguage))}
|
|
2035
|
-
hrefLang="x-default"
|
|
2036
|
-
rel="alternate"
|
|
2037
|
-
/>
|
|
2038
|
-
</>
|
|
2039
|
-
);
|
|
2432
|
+
const stripLanguagePrefix = (pathname: string) => {
|
|
2433
|
+
const segments = normalisePath(pathname).split('/').filter(Boolean);
|
|
2434
|
+
if (segments.length > 0 && isSupportedLanguage(segments[0] ?? '')) {
|
|
2435
|
+
segments.shift();
|
|
2436
|
+
}
|
|
2437
|
+
return \`/\${segments.join('/')}\`;
|
|
2040
2438
|
};
|
|
2041
|
-
`;
|
|
2042
|
-
}
|
|
2043
|
-
function createShellPage() {
|
|
2044
|
-
return `import { useModernI18n } from '@modern-js/plugin-i18n/runtime';
|
|
2045
|
-
import { ultramodernUiMarker } from '../../ultramodern-build';
|
|
2046
2439
|
|
|
2047
|
-
const
|
|
2440
|
+
const escapeRegExp = (value: string) =>
|
|
2441
|
+
value.replace(/[.*+?^\${}()|[\\]\\\\]/g, '\\\\$&');
|
|
2048
2442
|
|
|
2049
|
-
const
|
|
2443
|
+
const paramName = (segment: string) => segment.slice(1).replace(/\\?$/u, '');
|
|
2050
2444
|
|
|
2051
|
-
|
|
2052
|
-
|
|
2053
|
-
const
|
|
2054
|
-
|
|
2445
|
+
const matchPattern = (pathname: string, pattern: string) => {
|
|
2446
|
+
const names: string[] = [];
|
|
2447
|
+
const source = normalisePath(pattern)
|
|
2448
|
+
.split('/')
|
|
2449
|
+
.filter(Boolean)
|
|
2450
|
+
.map(segment => {
|
|
2451
|
+
if (segment.startsWith(':')) {
|
|
2452
|
+
names.push(paramName(segment));
|
|
2453
|
+
return segment.endsWith('?') ? '(?:/([^/]+))?' : '/([^/]+)';
|
|
2454
|
+
}
|
|
2455
|
+
return \`/\${escapeRegExp(segment)}\`;
|
|
2456
|
+
})
|
|
2457
|
+
.join('');
|
|
2458
|
+
const match = new RegExp(\`^\${source || '/'}$\`).exec(normalisePath(pathname));
|
|
2055
2459
|
|
|
2056
|
-
|
|
2057
|
-
|
|
2460
|
+
if (!match) {
|
|
2461
|
+
return undefined;
|
|
2462
|
+
}
|
|
2463
|
+
|
|
2464
|
+
return names.reduce<Record<string, string>>((params, name, index) => {
|
|
2465
|
+
params[name] = decodeURIComponent(match[index + 1] ?? '');
|
|
2466
|
+
return params;
|
|
2467
|
+
}, {});
|
|
2468
|
+
};
|
|
2469
|
+
|
|
2470
|
+
const buildPath = (pattern: string, params: Record<string, string>) => {
|
|
2471
|
+
const path = normalisePath(pattern)
|
|
2472
|
+
.split('/')
|
|
2473
|
+
.filter(Boolean)
|
|
2474
|
+
.map(segment => {
|
|
2475
|
+
if (!segment.startsWith(':')) {
|
|
2476
|
+
return segment;
|
|
2477
|
+
}
|
|
2478
|
+
const value = params[paramName(segment)];
|
|
2479
|
+
return value ? encodeURIComponent(value) : '';
|
|
2480
|
+
})
|
|
2481
|
+
.filter(Boolean)
|
|
2482
|
+
.join('/');
|
|
2483
|
+
|
|
2484
|
+
return \`/\${path}\`;
|
|
2485
|
+
};
|
|
2486
|
+
|
|
2487
|
+
const resolveLocalisedPath = (
|
|
2488
|
+
pathname: string,
|
|
2489
|
+
targetLanguage: SupportedLanguage,
|
|
2490
|
+
) => {
|
|
2491
|
+
const pathWithoutLanguage = stripLanguagePrefix(pathname);
|
|
2492
|
+
|
|
2493
|
+
for (const entry of Object.values(localisedUrls)) {
|
|
2494
|
+
const targetPattern = entry[targetLanguage];
|
|
2495
|
+
if (!targetPattern) {
|
|
2496
|
+
continue;
|
|
2497
|
+
}
|
|
2498
|
+
|
|
2499
|
+
for (const language of supportedLanguages) {
|
|
2500
|
+
const sourcePattern = entry[language];
|
|
2501
|
+
const params = sourcePattern
|
|
2502
|
+
? matchPattern(pathWithoutLanguage, sourcePattern)
|
|
2503
|
+
: undefined;
|
|
2504
|
+
if (params) {
|
|
2505
|
+
return buildPath(targetPattern, params);
|
|
2506
|
+
}
|
|
2507
|
+
}
|
|
2508
|
+
}
|
|
2509
|
+
|
|
2510
|
+
return pathWithoutLanguage;
|
|
2511
|
+
};
|
|
2512
|
+
|
|
2513
|
+
const localizedPath = (pathname: string, language: SupportedLanguage) => {
|
|
2514
|
+
const pathWithoutLanguage = resolveLocalisedPath(pathname, language);
|
|
2515
|
+
return pathWithoutLanguage === '/' ? \`/\${language}\` : \`/\${language}\${pathWithoutLanguage}\`;
|
|
2516
|
+
};
|
|
2517
|
+
|
|
2518
|
+
const absoluteUrl = (pathname: string) => {
|
|
2519
|
+
const origin = ULTRAMODERN_SITE_URL.replace(/\\/+$/u, '');
|
|
2520
|
+
return \`\${origin}\${pathname}\`;
|
|
2521
|
+
};
|
|
2522
|
+
|
|
2523
|
+
const locationSuffix = (location: {
|
|
2524
|
+
hash?: unknown;
|
|
2525
|
+
search?: unknown;
|
|
2526
|
+
searchStr?: unknown;
|
|
2527
|
+
}) => {
|
|
2528
|
+
const locationSearch =
|
|
2529
|
+
typeof location.searchStr === 'string'
|
|
2530
|
+
? location.searchStr
|
|
2531
|
+
: typeof location.search === 'string'
|
|
2532
|
+
? location.search
|
|
2533
|
+
: '';
|
|
2534
|
+
const locationHash = typeof location.hash === 'string' ? location.hash : '';
|
|
2535
|
+
|
|
2536
|
+
return \`\${locationSearch}\${locationHash}\`;
|
|
2537
|
+
};
|
|
2538
|
+
|
|
2539
|
+
const LocalizedHead = () => {
|
|
2540
|
+
const location = useLocation();
|
|
2541
|
+
const canonicalPath = localizedPath(location.pathname, fallbackLanguage);
|
|
2542
|
+
|
|
2543
|
+
return (
|
|
2544
|
+
<Helmet>
|
|
2545
|
+
<link rel="canonical" href={absoluteUrl(canonicalPath)} />
|
|
2546
|
+
{supportedLanguages.map(code => (
|
|
2547
|
+
<link
|
|
2548
|
+
href={absoluteUrl(localizedPath(location.pathname, code))}
|
|
2549
|
+
hrefLang={code}
|
|
2550
|
+
key={code}
|
|
2551
|
+
rel="alternate"
|
|
2552
|
+
/>
|
|
2553
|
+
))}
|
|
2554
|
+
<link
|
|
2555
|
+
href={absoluteUrl(localizedPath(location.pathname, fallbackLanguage))}
|
|
2556
|
+
hrefLang="x-default"
|
|
2557
|
+
rel="alternate"
|
|
2558
|
+
/>
|
|
2559
|
+
</Helmet>
|
|
2560
|
+
);
|
|
2561
|
+
};
|
|
2562
|
+
`;
|
|
2563
|
+
}
|
|
2564
|
+
function createShellPage() {
|
|
2565
|
+
return `import { useModernI18n } from '@modern-js/plugin-i18n/runtime';
|
|
2566
|
+
import { Helmet } from '@modern-js/runtime/head';
|
|
2567
|
+
import { useLocation } from '@modern-js/plugin-tanstack/runtime';
|
|
2568
|
+
import { ultramodernLocalisedUrls } from '../ultramodern-route-metadata';
|
|
2569
|
+
import { ultramodernUiMarker } from '../../ultramodern-build';
|
|
2570
|
+
|
|
2571
|
+
const languageCodes = ['en', 'cs'] as const;
|
|
2572
|
+
|
|
2573
|
+
const remoteKeys = ['explore', 'decide', 'checkout'] as const;
|
|
2574
|
+
|
|
2575
|
+
${createLocalizedHeadComponent()}
|
|
2576
|
+
export default function ShellHome() {
|
|
2577
|
+
const { i18nInstance, language } = useModernI18n();
|
|
2578
|
+
const t = i18nInstance.t.bind(i18nInstance);
|
|
2579
|
+
const location = useLocation();
|
|
2580
|
+
const suffix = locationSuffix(location);
|
|
2581
|
+
|
|
2582
|
+
return (
|
|
2583
|
+
<main>
|
|
2058
2584
|
<LocalizedHead />
|
|
2059
2585
|
<nav aria-label={t('shell.language.switcher')}>
|
|
2060
2586
|
{languageCodes.map(code => (
|
|
2061
2587
|
<a
|
|
2062
2588
|
aria-current={language === code ? 'page' : undefined}
|
|
2063
|
-
href={
|
|
2589
|
+
href={\`\${localizedPath(location.pathname, code)}\${suffix}\`}
|
|
2064
2590
|
key={code}
|
|
2065
2591
|
>
|
|
2066
2592
|
{t(\`shell.language.\${code}\`)}
|
|
@@ -2083,11 +2609,13 @@ export default function ShellHome() {
|
|
|
2083
2609
|
`;
|
|
2084
2610
|
}
|
|
2085
2611
|
function createRemotePage(app) {
|
|
2086
|
-
if ('remote-commerce' === app.id) return createCommerceRemotePage(app);
|
|
2087
2612
|
const effectBffImport = appHasEffectApi(app) ? `import { useModernI18n } from '@modern-js/plugin-i18n/runtime';
|
|
2613
|
+
import { Helmet } from '@modern-js/runtime/head';
|
|
2614
|
+
import { useLocation } from '@modern-js/plugin-tanstack/runtime';
|
|
2088
2615
|
import { useEffect, useState } from 'react';
|
|
2616
|
+
import { ultramodernLocalisedUrls } from '../ultramodern-route-metadata';
|
|
2089
2617
|
import { ultramodernUiMarker } from '../../ultramodern-build';
|
|
2090
|
-
` : "import { useModernI18n } from '@modern-js/plugin-i18n/runtime';\nimport { ultramodernUiMarker } from '../../ultramodern-build';\n";
|
|
2618
|
+
` : "import { useModernI18n } from '@modern-js/plugin-i18n/runtime';\nimport { Helmet } from '@modern-js/runtime/head';\nimport { useLocation } from '@modern-js/plugin-tanstack/runtime';\nimport { ultramodernLocalisedUrls } from '../ultramodern-route-metadata';\nimport { ultramodernUiMarker } from '../../ultramodern-build';\n";
|
|
2091
2619
|
const effectBffState = appHasEffectApi(app) ? ` const [effectApiStatus, setEffectApiStatus] = useState('pending');
|
|
2092
2620
|
|
|
2093
2621
|
useEffect(() => {
|
|
@@ -2119,6 +2647,8 @@ ${createLocalizedHeadComponent()}
|
|
|
2119
2647
|
export default function ${toPascalCase(app.id)}Home() {
|
|
2120
2648
|
const { i18nInstance, language } = useModernI18n();
|
|
2121
2649
|
const t = i18nInstance.t.bind(i18nInstance);
|
|
2650
|
+
const location = useLocation();
|
|
2651
|
+
const suffix = locationSuffix(location);
|
|
2122
2652
|
${effectBffState} return (
|
|
2123
2653
|
<main>
|
|
2124
2654
|
<LocalizedHead />
|
|
@@ -2126,7 +2656,7 @@ ${effectBffState} return (
|
|
|
2126
2656
|
{supportedLanguages.map(code => (
|
|
2127
2657
|
<a
|
|
2128
2658
|
aria-current={language === code ? 'page' : undefined}
|
|
2129
|
-
href={
|
|
2659
|
+
href={\`\${localizedPath(location.pathname, code)}\${suffix}\`}
|
|
2130
2660
|
key={code}
|
|
2131
2661
|
>
|
|
2132
2662
|
{t(\`${app.domain}.language.\${code}\`)}
|
|
@@ -2143,399 +2673,6 @@ ${effectBffMarkup} </main>
|
|
|
2143
2673
|
}
|
|
2144
2674
|
`;
|
|
2145
2675
|
}
|
|
2146
|
-
function createCommerceRemotePage(app) {
|
|
2147
|
-
return `import { useModernI18n } from '@modern-js/plugin-i18n/runtime';
|
|
2148
|
-
import { useEffect, useState, type CSSProperties } from 'react';
|
|
2149
|
-
import { ultramodernUiMarker } from '../../ultramodern-build';
|
|
2150
|
-
|
|
2151
|
-
const languageCodes = ['en', 'cs'] as const;
|
|
2152
|
-
|
|
2153
|
-
const boundaryDefinitions = [
|
|
2154
|
-
{
|
|
2155
|
-
color: '#ff5a57',
|
|
2156
|
-
id: 'explore',
|
|
2157
|
-
labelKey: 'commerce.boundaries.explore',
|
|
2158
|
-
},
|
|
2159
|
-
{
|
|
2160
|
-
color: '#24d671',
|
|
2161
|
-
id: 'decide',
|
|
2162
|
-
labelKey: 'commerce.boundaries.decide',
|
|
2163
|
-
},
|
|
2164
|
-
{
|
|
2165
|
-
color: '#f4d044',
|
|
2166
|
-
id: 'checkout',
|
|
2167
|
-
labelKey: 'commerce.boundaries.checkout',
|
|
2168
|
-
},
|
|
2169
|
-
] as const;
|
|
2170
|
-
|
|
2171
|
-
type BoundaryId = (typeof boundaryDefinitions)[number]['id'];
|
|
2172
|
-
type BoundaryDefinition = (typeof boundaryDefinitions)[number];
|
|
2173
|
-
|
|
2174
|
-
const boundaryMetadata: Record<BoundaryId, BoundaryDefinition> = {
|
|
2175
|
-
checkout: boundaryDefinitions[2],
|
|
2176
|
-
decide: boundaryDefinitions[1],
|
|
2177
|
-
explore: boundaryDefinitions[0],
|
|
2178
|
-
};
|
|
2179
|
-
|
|
2180
|
-
const products = [
|
|
2181
|
-
{
|
|
2182
|
-
id: 'field-loader-112',
|
|
2183
|
-
titleKey: 'commerce.products.fieldLoader.title',
|
|
2184
|
-
descriptionKey: 'commerce.products.fieldLoader.description',
|
|
2185
|
-
priceKey: 'commerce.products.fieldLoader.price',
|
|
2186
|
-
powerKey: 'commerce.products.fieldLoader.power',
|
|
2187
|
-
availabilityKey: 'commerce.products.fieldLoader.availability',
|
|
2188
|
-
},
|
|
2189
|
-
{
|
|
2190
|
-
id: 'orchard-tractor',
|
|
2191
|
-
titleKey: 'commerce.products.orchard.title',
|
|
2192
|
-
badgeKey: 'commerce.products.orchard.badge',
|
|
2193
|
-
},
|
|
2194
|
-
{
|
|
2195
|
-
id: 'autonomy-kit',
|
|
2196
|
-
titleKey: 'commerce.products.autonomy.title',
|
|
2197
|
-
badgeKey: 'commerce.products.autonomy.badge',
|
|
2198
|
-
},
|
|
2199
|
-
] as const;
|
|
2200
|
-
|
|
2201
|
-
type ProductId = (typeof products)[number]['id'];
|
|
2202
|
-
type CartState = Partial<Record<ProductId, number>>;
|
|
2203
|
-
|
|
2204
|
-
type BoundaryLabels = Record<BoundaryId, string>;
|
|
2205
|
-
type BoundaryBox = {
|
|
2206
|
-
color: string;
|
|
2207
|
-
height: number;
|
|
2208
|
-
id: BoundaryId;
|
|
2209
|
-
label: string;
|
|
2210
|
-
labelPlacement: 'above' | 'inside';
|
|
2211
|
-
left: number;
|
|
2212
|
-
top: number;
|
|
2213
|
-
width: number;
|
|
2214
|
-
};
|
|
2215
|
-
|
|
2216
|
-
const featuredProduct = products[0];
|
|
2217
|
-
const recommendations = [products[1], products[2]] as const;
|
|
2218
|
-
|
|
2219
|
-
${createLocalizedHeadComponent()}
|
|
2220
|
-
const isBoundaryId = (value: string): value is BoundaryId =>
|
|
2221
|
-
Object.prototype.hasOwnProperty.call(boundaryMetadata, value);
|
|
2222
|
-
|
|
2223
|
-
function collectBoundaryBoxes(labels: BoundaryLabels): BoundaryBox[] {
|
|
2224
|
-
return Array.from(
|
|
2225
|
-
document.querySelectorAll<HTMLElement>('[data-boundary], [data-boundary-page]'),
|
|
2226
|
-
)
|
|
2227
|
-
.map(element => {
|
|
2228
|
-
const id = element.dataset.boundary ?? element.dataset.boundaryPage;
|
|
2229
|
-
|
|
2230
|
-
if (id === undefined || !isBoundaryId(id)) {
|
|
2231
|
-
return undefined;
|
|
2232
|
-
}
|
|
2233
|
-
|
|
2234
|
-
const rect = element.getBoundingClientRect();
|
|
2235
|
-
|
|
2236
|
-
if (rect.width <= 0 || rect.height <= 0) {
|
|
2237
|
-
return undefined;
|
|
2238
|
-
}
|
|
2239
|
-
|
|
2240
|
-
return {
|
|
2241
|
-
color: boundaryMetadata[id].color,
|
|
2242
|
-
height: rect.height,
|
|
2243
|
-
id,
|
|
2244
|
-
label: labels[id],
|
|
2245
|
-
labelPlacement: rect.top > 28 ? 'above' : 'inside',
|
|
2246
|
-
left: rect.left,
|
|
2247
|
-
top: rect.top,
|
|
2248
|
-
width: rect.width,
|
|
2249
|
-
};
|
|
2250
|
-
})
|
|
2251
|
-
.filter((box): box is BoundaryBox => box !== undefined);
|
|
2252
|
-
}
|
|
2253
|
-
|
|
2254
|
-
function BoundaryOverlay({
|
|
2255
|
-
labels,
|
|
2256
|
-
visible,
|
|
2257
|
-
}: {
|
|
2258
|
-
labels: BoundaryLabels;
|
|
2259
|
-
visible: boolean;
|
|
2260
|
-
}) {
|
|
2261
|
-
const [boxes, setBoxes] = useState<BoundaryBox[]>([]);
|
|
2262
|
-
|
|
2263
|
-
useEffect(() => {
|
|
2264
|
-
if (!visible) {
|
|
2265
|
-
setBoxes([]);
|
|
2266
|
-
return;
|
|
2267
|
-
}
|
|
2268
|
-
|
|
2269
|
-
let animationFrame = 0;
|
|
2270
|
-
const update = () => {
|
|
2271
|
-
cancelAnimationFrame(animationFrame);
|
|
2272
|
-
animationFrame = requestAnimationFrame(() => {
|
|
2273
|
-
setBoxes(collectBoundaryBoxes(labels));
|
|
2274
|
-
});
|
|
2275
|
-
};
|
|
2276
|
-
const observer = new ResizeObserver(update);
|
|
2277
|
-
|
|
2278
|
-
observer.observe(document.body);
|
|
2279
|
-
update();
|
|
2280
|
-
window.addEventListener('resize', update);
|
|
2281
|
-
window.addEventListener('scroll', update, true);
|
|
2282
|
-
|
|
2283
|
-
return () => {
|
|
2284
|
-
cancelAnimationFrame(animationFrame);
|
|
2285
|
-
observer.disconnect();
|
|
2286
|
-
window.removeEventListener('resize', update);
|
|
2287
|
-
window.removeEventListener('scroll', update, true);
|
|
2288
|
-
};
|
|
2289
|
-
}, [labels, visible]);
|
|
2290
|
-
|
|
2291
|
-
if (!visible) {
|
|
2292
|
-
return null;
|
|
2293
|
-
}
|
|
2294
|
-
|
|
2295
|
-
return (
|
|
2296
|
-
<div aria-hidden="true" className="boundary-overlay">
|
|
2297
|
-
{boxes.map((box, index) => (
|
|
2298
|
-
<div
|
|
2299
|
-
className="boundary-overlay__box"
|
|
2300
|
-
data-boundary-id={box.id}
|
|
2301
|
-
data-label-placement={box.labelPlacement}
|
|
2302
|
-
key={\`\${box.id}-\${index}\`}
|
|
2303
|
-
style={{
|
|
2304
|
-
'--boundary-color': box.color,
|
|
2305
|
-
height: box.height,
|
|
2306
|
-
left: box.left,
|
|
2307
|
-
top: box.top,
|
|
2308
|
-
width: box.width,
|
|
2309
|
-
} as CSSProperties}
|
|
2310
|
-
>
|
|
2311
|
-
<span className="boundary-overlay__label">{box.label}</span>
|
|
2312
|
-
</div>
|
|
2313
|
-
))}
|
|
2314
|
-
</div>
|
|
2315
|
-
);
|
|
2316
|
-
}
|
|
2317
|
-
|
|
2318
|
-
export default function ${toPascalCase(app.id)}Home() {
|
|
2319
|
-
const { i18nInstance, language } = useModernI18n();
|
|
2320
|
-
const t = i18nInstance.t.bind(i18nInstance);
|
|
2321
|
-
const [cart, setCart] = useState<CartState>({});
|
|
2322
|
-
const [showBoundaries, setShowBoundaries] = useState(false);
|
|
2323
|
-
const [effectApiStatus, setEffectApiStatus] = useState('pending');
|
|
2324
|
-
const boundaryLabels = {
|
|
2325
|
-
checkout: t('commerce.boundaries.checkout'),
|
|
2326
|
-
decide: t('commerce.boundaries.decide'),
|
|
2327
|
-
explore: t('commerce.boundaries.explore'),
|
|
2328
|
-
} satisfies BoundaryLabels;
|
|
2329
|
-
const cartLines = products
|
|
2330
|
-
.map(product => ({
|
|
2331
|
-
product,
|
|
2332
|
-
quantity: cart[product.id] ?? 0,
|
|
2333
|
-
}))
|
|
2334
|
-
.filter(line => line.quantity > 0);
|
|
2335
|
-
const cartCount = cartLines.reduce((total, line) => total + line.quantity, 0);
|
|
2336
|
-
|
|
2337
|
-
useEffect(() => {
|
|
2338
|
-
void fetch('${effectApiPrefix(app)}/effect/${effectApiStem(app)}?limit=1', {
|
|
2339
|
-
headers: {
|
|
2340
|
-
accept: 'application/json',
|
|
2341
|
-
},
|
|
2342
|
-
})
|
|
2343
|
-
.then(response => {
|
|
2344
|
-
if (!response.ok) {
|
|
2345
|
-
throw new Error(\`Effect BFF request failed: \${response.status}\`);
|
|
2346
|
-
}
|
|
2347
|
-
|
|
2348
|
-
return response.json() as Promise<{ items?: Array<{ title?: string }> }>;
|
|
2349
|
-
})
|
|
2350
|
-
.then(data => {
|
|
2351
|
-
setEffectApiStatus(data.items[0]?.title ?? 'empty');
|
|
2352
|
-
})
|
|
2353
|
-
.catch(() => {
|
|
2354
|
-
setEffectApiStatus('unavailable');
|
|
2355
|
-
});
|
|
2356
|
-
}, []);
|
|
2357
|
-
|
|
2358
|
-
const addToCart = (id: ProductId) => {
|
|
2359
|
-
setCart(current => ({
|
|
2360
|
-
...current,
|
|
2361
|
-
[id]: (current[id] ?? 0) + 1,
|
|
2362
|
-
}));
|
|
2363
|
-
};
|
|
2364
|
-
|
|
2365
|
-
const reduceQuantity = (id: ProductId) => {
|
|
2366
|
-
setCart(current => {
|
|
2367
|
-
const quantity = current[id] ?? 0;
|
|
2368
|
-
const next = { ...current };
|
|
2369
|
-
|
|
2370
|
-
if (quantity <= 1) {
|
|
2371
|
-
delete next[id];
|
|
2372
|
-
} else {
|
|
2373
|
-
next[id] = quantity - 1;
|
|
2374
|
-
}
|
|
2375
|
-
|
|
2376
|
-
return next;
|
|
2377
|
-
});
|
|
2378
|
-
};
|
|
2379
|
-
|
|
2380
|
-
const removeFromCart = (id: ProductId) => {
|
|
2381
|
-
setCart(current => {
|
|
2382
|
-
const next = { ...current };
|
|
2383
|
-
|
|
2384
|
-
delete next[id];
|
|
2385
|
-
return next;
|
|
2386
|
-
});
|
|
2387
|
-
};
|
|
2388
|
-
|
|
2389
|
-
return (
|
|
2390
|
-
<main className="commerce-shell">
|
|
2391
|
-
<LocalizedHead />
|
|
2392
|
-
<BoundaryOverlay labels={boundaryLabels} visible={showBoundaries} />
|
|
2393
|
-
<header className="commerce-header" data-boundary="explore">
|
|
2394
|
-
<strong className="commerce-logo">{t('commerce.brand')}</strong>
|
|
2395
|
-
<nav aria-label={t('commerce.navigation.primary')} className="commerce-nav">
|
|
2396
|
-
<a className="commerce-pill" href="#machines">
|
|
2397
|
-
{t('commerce.navigation.machines')}
|
|
2398
|
-
</a>
|
|
2399
|
-
<a className="commerce-pill" href="#checkout">
|
|
2400
|
-
{t('commerce.navigation.checkout')}
|
|
2401
|
-
</a>
|
|
2402
|
-
</nav>
|
|
2403
|
-
<div className="commerce-actions">
|
|
2404
|
-
<a className="commerce-cart-button" data-boundary="checkout" href="#cart">
|
|
2405
|
-
{t('commerce.cart.button', { count: cartCount })}
|
|
2406
|
-
</a>
|
|
2407
|
-
<nav aria-label={t('commerce.language.switcher')} className="commerce-language">
|
|
2408
|
-
{languageCodes.map(code => (
|
|
2409
|
-
<a
|
|
2410
|
-
aria-current={language === code ? 'page' : undefined}
|
|
2411
|
-
className="commerce-pill"
|
|
2412
|
-
href={\`/\${code}\`}
|
|
2413
|
-
key={code}
|
|
2414
|
-
>
|
|
2415
|
-
{t(\`commerce.language.\${code}\`)}
|
|
2416
|
-
</a>
|
|
2417
|
-
))}
|
|
2418
|
-
</nav>
|
|
2419
|
-
</div>
|
|
2420
|
-
</header>
|
|
2421
|
-
|
|
2422
|
-
<div className="commerce-page">
|
|
2423
|
-
<section className="commerce-product" data-boundary-page="decide" id="machines">
|
|
2424
|
-
<div
|
|
2425
|
-
aria-label={t('commerce.products.fieldLoader.imageAlt')}
|
|
2426
|
-
className="commerce-product-media"
|
|
2427
|
-
role="img"
|
|
2428
|
-
/>
|
|
2429
|
-
<div>
|
|
2430
|
-
<p className="commerce-eyebrow">{t('commerce.detail.eyebrow')}</p>
|
|
2431
|
-
<h1 className="commerce-title">{t(featuredProduct.titleKey)}</h1>
|
|
2432
|
-
<p className="commerce-lede">{t(featuredProduct.descriptionKey)}</p>
|
|
2433
|
-
<div className="commerce-facts">
|
|
2434
|
-
<div className="commerce-fact">
|
|
2435
|
-
<span>{t('commerce.detail.price')}</span>
|
|
2436
|
-
<strong>{t(featuredProduct.priceKey)}</strong>
|
|
2437
|
-
</div>
|
|
2438
|
-
<div className="commerce-fact">
|
|
2439
|
-
<span>{t('commerce.detail.power')}</span>
|
|
2440
|
-
<strong>{t(featuredProduct.powerKey)}</strong>
|
|
2441
|
-
</div>
|
|
2442
|
-
<div className="commerce-fact">
|
|
2443
|
-
<span>{t('commerce.detail.availability')}</span>
|
|
2444
|
-
<strong>{t(featuredProduct.availabilityKey)}</strong>
|
|
2445
|
-
</div>
|
|
2446
|
-
</div>
|
|
2447
|
-
<div className="commerce-checkout" data-boundary="checkout" id="checkout">
|
|
2448
|
-
<button
|
|
2449
|
-
className="commerce-button"
|
|
2450
|
-
onClick={() => addToCart(featuredProduct.id)}
|
|
2451
|
-
type="button"
|
|
2452
|
-
>
|
|
2453
|
-
{t('commerce.cart.add')}
|
|
2454
|
-
</button>
|
|
2455
|
-
<a className="commerce-link-button" href="#cart">
|
|
2456
|
-
{t('commerce.cart.view')}
|
|
2457
|
-
</a>
|
|
2458
|
-
</div>
|
|
2459
|
-
</div>
|
|
2460
|
-
</section>
|
|
2461
|
-
|
|
2462
|
-
<section data-boundary="explore">
|
|
2463
|
-
<h2 className="commerce-section-title">{t('commerce.recommendations.title')}</h2>
|
|
2464
|
-
<div className="commerce-grid">
|
|
2465
|
-
{recommendations.map(product => (
|
|
2466
|
-
<article className="commerce-card" key={product.id}>
|
|
2467
|
-
<span>{t(product.badgeKey)}</span>
|
|
2468
|
-
<strong>{t(product.titleKey)}</strong>
|
|
2469
|
-
</article>
|
|
2470
|
-
))}
|
|
2471
|
-
</div>
|
|
2472
|
-
</section>
|
|
2473
|
-
|
|
2474
|
-
<section className="commerce-cart-panel" data-boundary="checkout" id="cart">
|
|
2475
|
-
<h2>{t('commerce.cart.title')}</h2>
|
|
2476
|
-
{cartLines.length === 0 ? (
|
|
2477
|
-
<p>{t('commerce.cart.empty')}</p>
|
|
2478
|
-
) : (
|
|
2479
|
-
cartLines.map(line => (
|
|
2480
|
-
<div className="commerce-cart-line" key={line.product.id}>
|
|
2481
|
-
<strong>{t(line.product.titleKey)}</strong>
|
|
2482
|
-
<div className="commerce-quantity">
|
|
2483
|
-
<button
|
|
2484
|
-
aria-label={t('commerce.cart.decrease', {
|
|
2485
|
-
name: t(line.product.titleKey),
|
|
2486
|
-
})}
|
|
2487
|
-
className="commerce-quantity-button"
|
|
2488
|
-
onClick={() => reduceQuantity(line.product.id)}
|
|
2489
|
-
type="button"
|
|
2490
|
-
>
|
|
2491
|
-
-
|
|
2492
|
-
</button>
|
|
2493
|
-
<span>{line.quantity}</span>
|
|
2494
|
-
<button
|
|
2495
|
-
aria-label={t('commerce.cart.increase', {
|
|
2496
|
-
name: t(line.product.titleKey),
|
|
2497
|
-
})}
|
|
2498
|
-
className="commerce-quantity-button"
|
|
2499
|
-
onClick={() => addToCart(line.product.id)}
|
|
2500
|
-
type="button"
|
|
2501
|
-
>
|
|
2502
|
-
+
|
|
2503
|
-
</button>
|
|
2504
|
-
<button
|
|
2505
|
-
className="commerce-link-button"
|
|
2506
|
-
onClick={() => removeFromCart(line.product.id)}
|
|
2507
|
-
type="button"
|
|
2508
|
-
>
|
|
2509
|
-
{t('commerce.cart.remove')}
|
|
2510
|
-
</button>
|
|
2511
|
-
</div>
|
|
2512
|
-
</div>
|
|
2513
|
-
))
|
|
2514
|
-
)}
|
|
2515
|
-
</section>
|
|
2516
|
-
</div>
|
|
2517
|
-
|
|
2518
|
-
<footer className="commerce-footer" data-boundary="explore">
|
|
2519
|
-
<span>{t('commerce.footer.stack')}</span>
|
|
2520
|
-
<span data-testid="effect-bff-status">{effectApiStatus}</span>
|
|
2521
|
-
<span data-build-marker={ultramodernUiMarker.build} data-testid="ultramodern-ui-marker">
|
|
2522
|
-
{ultramodernUiMarker.appId}:{ultramodernUiMarker.version}
|
|
2523
|
-
</span>
|
|
2524
|
-
</footer>
|
|
2525
|
-
|
|
2526
|
-
<label className="commerce-boundary-toggle">
|
|
2527
|
-
<input
|
|
2528
|
-
checked={showBoundaries}
|
|
2529
|
-
onChange={event => setShowBoundaries(event.currentTarget.checked)}
|
|
2530
|
-
type="checkbox"
|
|
2531
|
-
/>
|
|
2532
|
-
{t('commerce.boundaries.toggle')}
|
|
2533
|
-
</label>
|
|
2534
|
-
</main>
|
|
2535
|
-
);
|
|
2536
|
-
}
|
|
2537
|
-
`;
|
|
2538
|
-
}
|
|
2539
2676
|
function createLayout(appId) {
|
|
2540
2677
|
return `import { Outlet } from '@modern-js/plugin-tanstack/runtime';
|
|
2541
2678
|
import './index.css';
|
|
@@ -2550,7 +2687,18 @@ export default function Layout() {
|
|
|
2550
2687
|
`;
|
|
2551
2688
|
}
|
|
2552
2689
|
function createRemoteEntry(app) {
|
|
2553
|
-
return `export { default } from './components
|
|
2690
|
+
if (app.exposes?.['./ProductPage']) return `export { default } from './components/product-page';
|
|
2691
|
+
`;
|
|
2692
|
+
if (app.exposes?.['./CartPage']) return `export { default } from './components/cart-page';
|
|
2693
|
+
`;
|
|
2694
|
+
return `export default function ${toPascalCase(app.domain ?? app.id)}Route() {
|
|
2695
|
+
return (
|
|
2696
|
+
<section data-mf-remote="${app.id}" data-mf-expose="./Route">
|
|
2697
|
+
<h2>${app.displayName}</h2>
|
|
2698
|
+
<p>Route surface for ${app.domain ?? app.id}.</p>
|
|
2699
|
+
</section>
|
|
2700
|
+
);
|
|
2701
|
+
}
|
|
2554
2702
|
`;
|
|
2555
2703
|
}
|
|
2556
2704
|
function createRemoteWidget(app) {
|
|
@@ -2566,16 +2714,57 @@ function createRemoteWidget(app) {
|
|
|
2566
2714
|
}
|
|
2567
2715
|
`;
|
|
2568
2716
|
}
|
|
2717
|
+
function createRemoteExposeComponent(app, expose) {
|
|
2718
|
+
if ('./Widget' === expose) return createRemoteWidget(app);
|
|
2719
|
+
const componentName = `${toPascalCase(app.domain ?? app.id)}${toPascalCase(expose.replace(/^\.\//u, ''))}`;
|
|
2720
|
+
if ('remote-decide' === app.id && './ProductPage' === expose) return `import AddToCart from 'checkout/AddToCart';
|
|
2721
|
+
import Recommendations from 'explore/Recommendations';
|
|
2722
|
+
|
|
2723
|
+
export default function ${componentName}() {
|
|
2724
|
+
return (
|
|
2725
|
+
<section data-mf-remote="${app.id}" data-mf-expose="${expose}">
|
|
2726
|
+
<p>Decide owns tractor product selection.</p>
|
|
2727
|
+
<h2>Field Loader 112</h2>
|
|
2728
|
+
<p>Hydraulic-ready compact tractor with guided implement matching.</p>
|
|
2729
|
+
<AddToCart />
|
|
2730
|
+
<Recommendations />
|
|
2731
|
+
</section>
|
|
2732
|
+
);
|
|
2733
|
+
}
|
|
2734
|
+
`;
|
|
2735
|
+
return `export default function ${componentName}() {
|
|
2736
|
+
return (
|
|
2737
|
+
<section data-mf-remote="${app.id}" data-mf-expose="${expose}">
|
|
2738
|
+
<h2>${app.displayName} ${expose.replace(/^\.\//u, '')}</h2>
|
|
2739
|
+
<p>Module Federation surface owned by ${app.ownership.team}.</p>
|
|
2740
|
+
</section>
|
|
2741
|
+
);
|
|
2742
|
+
}
|
|
2743
|
+
`;
|
|
2744
|
+
}
|
|
2745
|
+
function remoteComponentOutputPath(app, expose) {
|
|
2746
|
+
const exposePath = app.exposes?.[expose];
|
|
2747
|
+
if (!exposePath?.startsWith('./src/components/')) return;
|
|
2748
|
+
return `${app.directory}/${exposePath.replace(/^\.\//u, '')}`;
|
|
2749
|
+
}
|
|
2569
2750
|
function createAppLocaleMessages(app, language) {
|
|
2570
2751
|
const czechLabels = {
|
|
2571
|
-
|
|
2572
|
-
role: '
|
|
2573
|
-
title: '
|
|
2752
|
+
checkout: {
|
|
2753
|
+
role: 'pokladna',
|
|
2754
|
+
title: 'Pokladní remote'
|
|
2755
|
+
},
|
|
2756
|
+
decide: {
|
|
2757
|
+
role: 'rozhodování',
|
|
2758
|
+
title: 'Rozhodovací remote'
|
|
2574
2759
|
},
|
|
2575
2760
|
'design-system': {
|
|
2576
2761
|
role: 'design system',
|
|
2577
2762
|
title: 'Design system remote'
|
|
2578
2763
|
},
|
|
2764
|
+
explore: {
|
|
2765
|
+
role: 'procházení',
|
|
2766
|
+
title: 'Průzkumný remote'
|
|
2767
|
+
},
|
|
2579
2768
|
identity: {
|
|
2580
2769
|
role: 'identita',
|
|
2581
2770
|
title: 'Identitní remote'
|
|
@@ -2589,9 +2778,12 @@ function createAppLocaleMessages(app, language) {
|
|
|
2589
2778
|
switcher: 'en' === language ? 'Language' : 'Jazyk'
|
|
2590
2779
|
},
|
|
2591
2780
|
remotes: {
|
|
2592
|
-
|
|
2593
|
-
|
|
2594
|
-
|
|
2781
|
+
checkout: 'en' === language ? 'Checkout Remote' : 'Checkout remote',
|
|
2782
|
+
decide: 'en' === language ? 'Decide Remote' : 'Decide remote',
|
|
2783
|
+
explore: 'en' === language ? 'Explore Remote' : 'Explore remote'
|
|
2784
|
+
},
|
|
2785
|
+
routes: {
|
|
2786
|
+
home: 'en' === language ? 'Home' : 'Domů'
|
|
2595
2787
|
},
|
|
2596
2788
|
title: 'en' === language ? 'UltraModern SuperApp Shell' : 'UltraModern SuperApp shell'
|
|
2597
2789
|
}
|
|
@@ -2601,69 +2793,6 @@ function createAppLocaleMessages(app, language) {
|
|
|
2601
2793
|
role: domain,
|
|
2602
2794
|
title: `${app.displayName} CZ`
|
|
2603
2795
|
};
|
|
2604
|
-
if ('commerce' === domain) return {
|
|
2605
|
-
commerce: {
|
|
2606
|
-
boundaries: {
|
|
2607
|
-
checkout: 'en' === language ? 'checkout' : 'pokladna',
|
|
2608
|
-
decide: 'en' === language ? 'decide' : 'rozhodování',
|
|
2609
|
-
explore: 'en' === language ? 'explore' : 'procházení',
|
|
2610
|
-
toggle: 'en' === language ? 'show team boundaries' : 'zobrazit hranice týmů'
|
|
2611
|
-
},
|
|
2612
|
-
brand: 'Acre & Iron',
|
|
2613
|
-
cart: {
|
|
2614
|
-
add: 'en' === language ? 'Add to cart' : 'Přidat do košíku',
|
|
2615
|
-
button: 'en' === language ? 'Your cart ({{count}})' : 'Košík ({{count}})',
|
|
2616
|
-
decrease: 'en' === language ? 'Decrease {{name}} quantity' : 'Snížit množství položky {{name}}',
|
|
2617
|
-
empty: 'en' === language ? 'Your cart is empty.' : 'Košík je prázdný.',
|
|
2618
|
-
increase: 'en' === language ? 'Increase {{name}} quantity' : 'Zvýšit množství položky {{name}}',
|
|
2619
|
-
remove: 'en' === language ? 'Remove' : 'Odebrat',
|
|
2620
|
-
title: 'en' === language ? 'Cart' : 'Košík',
|
|
2621
|
-
view: 'en' === language ? 'View cart' : 'Zobrazit košík'
|
|
2622
|
-
},
|
|
2623
|
-
detail: {
|
|
2624
|
-
availability: 'en' === language ? 'Availability' : 'Dostupnost',
|
|
2625
|
-
eyebrow: 'en' === language ? 'Machine detail' : 'Detail stroje',
|
|
2626
|
-
power: 'en' === language ? 'Power' : 'Výkon',
|
|
2627
|
-
price: 'en' === language ? 'Price' : 'Cena'
|
|
2628
|
-
},
|
|
2629
|
-
footer: {
|
|
2630
|
-
stack: 'en' === language ? 'SPA, SSR-ready Module Federation, React, Effect BFF' : 'SPA, SSR-ready Module Federation, React, Effect BFF'
|
|
2631
|
-
},
|
|
2632
|
-
language: {
|
|
2633
|
-
cs: 'en' === language ? 'Czech' : 'Čeština',
|
|
2634
|
-
en: 'en' === language ? 'English' : 'Angličtina',
|
|
2635
|
-
switcher: 'en' === language ? 'Language' : 'Jazyk'
|
|
2636
|
-
},
|
|
2637
|
-
navigation: {
|
|
2638
|
-
checkout: 'en' === language ? 'Checkout' : 'Pokladna',
|
|
2639
|
-
machines: 'en' === language ? 'Machines' : 'Stroje',
|
|
2640
|
-
primary: 'en' === language ? 'Primary commerce navigation' : 'Hlavní navigace obchodu'
|
|
2641
|
-
},
|
|
2642
|
-
products: {
|
|
2643
|
-
autonomy: {
|
|
2644
|
-
badge: 'en' === language ? 'AI-first option' : 'AI varianta',
|
|
2645
|
-
title: 'en' === language ? 'Autonomy Retrofit Kit' : 'Sada pro autonomní řízení'
|
|
2646
|
-
},
|
|
2647
|
-
fieldLoader: {
|
|
2648
|
-
availability: 'en' === language ? 'In stock' : 'Skladem',
|
|
2649
|
-
description: 'en' === language ? 'A loader-ready tractor for feed, hay, gravel, and winter road work.' : 'Traktor připravený na nakladač pro krmivo, seno, štěrk i zimní údržbu cest.',
|
|
2650
|
-
imageAlt: 'en' === language ? 'Field Loader 112 tractor working on a bright farm lane' : 'Traktor Field Loader 112 pracuje na světlé polní cestě',
|
|
2651
|
-
power: '112 hp',
|
|
2652
|
-
price: 'EUR 42,500',
|
|
2653
|
-
title: 'Field Loader 112'
|
|
2654
|
-
},
|
|
2655
|
-
orchard: {
|
|
2656
|
-
badge: 'en' === language ? 'Best for tight rows' : 'Nejlepší do úzkých řádků',
|
|
2657
|
-
title: 'en' === language ? 'Narrow Orchard Tractor' : 'Úzký sadový traktor'
|
|
2658
|
-
}
|
|
2659
|
-
},
|
|
2660
|
-
recommendations: {
|
|
2661
|
-
title: 'en' === language ? 'Compare alternatives' : 'Porovnat alternativy'
|
|
2662
|
-
},
|
|
2663
|
-
role: 'en' === language ? 'commerce' : 'obchod',
|
|
2664
|
-
title: 'en' === language ? app.displayName : czechLabel.title
|
|
2665
|
-
}
|
|
2666
|
-
};
|
|
2667
2796
|
return {
|
|
2668
2797
|
[domain]: {
|
|
2669
2798
|
language: {
|
|
@@ -2672,6 +2801,16 @@ function createAppLocaleMessages(app, language) {
|
|
|
2672
2801
|
switcher: 'en' === language ? 'Language' : 'Jazyk'
|
|
2673
2802
|
},
|
|
2674
2803
|
role: 'en' === language ? app.domain ?? app.kind : czechLabel.role,
|
|
2804
|
+
routes: {
|
|
2805
|
+
cart: 'en' === language ? 'Cart' : 'Košík',
|
|
2806
|
+
checkout: 'en' === language ? 'Checkout' : 'Pokladna',
|
|
2807
|
+
home: 'en' === language ? 'Home' : 'Domů',
|
|
2808
|
+
listing: 'en' === language ? 'Tractors' : 'Traktory',
|
|
2809
|
+
productDetail: 'en' === language ? 'Tractor detail' : 'Detail traktoru',
|
|
2810
|
+
storePicker: 'en' === language ? 'Store picker' : 'Výběr prodejce',
|
|
2811
|
+
thankYou: 'en' === language ? 'Order confirmation' : 'Potvrzení objednávky',
|
|
2812
|
+
unavailable: 'en' === language ? 'Unavailable' : 'Nedostupné'
|
|
2813
|
+
},
|
|
2675
2814
|
title: 'en' === language ? app.displayName : czechLabel.title
|
|
2676
2815
|
}
|
|
2677
2816
|
};
|
|
@@ -2706,6 +2845,18 @@ function createDesignTokens() {
|
|
|
2706
2845
|
} as const;
|
|
2707
2846
|
`;
|
|
2708
2847
|
}
|
|
2848
|
+
function createSharedDesignTokensCss() {
|
|
2849
|
+
return `@layer ultramodern-shared-tokens {
|
|
2850
|
+
:root {
|
|
2851
|
+
--um-color-accent: #2f8f68;
|
|
2852
|
+
--um-color-canvas: #f1eadc;
|
|
2853
|
+
--um-color-foreground: #133225;
|
|
2854
|
+
--um-color-link: #166b4b;
|
|
2855
|
+
--um-color-surface: #f6fbf7;
|
|
2856
|
+
}
|
|
2857
|
+
}
|
|
2858
|
+
`;
|
|
2859
|
+
}
|
|
2709
2860
|
function serviceEffectApiExport(service = effectService) {
|
|
2710
2861
|
return `${toCamelCase(effectApiStem(service))}EffectApi`;
|
|
2711
2862
|
}
|
|
@@ -2718,6 +2869,12 @@ function serviceEffectApiName(service = effectService) {
|
|
|
2718
2869
|
function serviceEffectSchemaExport(service = effectService) {
|
|
2719
2870
|
return `${toCamelCase(effectApiStem(service))}ItemSchema`;
|
|
2720
2871
|
}
|
|
2872
|
+
function serviceEffectMarkerSchemaExport(service = effectService) {
|
|
2873
|
+
return `${toCamelCase(effectApiStem(service))}MarkerSchema`;
|
|
2874
|
+
}
|
|
2875
|
+
function serviceEffectReadinessSchemaExport(service = effectService) {
|
|
2876
|
+
return `${toCamelCase(effectApiStem(service))}ReadinessSchema`;
|
|
2877
|
+
}
|
|
2721
2878
|
function serviceEffectErrorStem(service = effectService) {
|
|
2722
2879
|
const stem = effectApiStem(service);
|
|
2723
2880
|
return 'recommendations' === stem ? 'recommendation' : stem;
|
|
@@ -2743,6 +2900,8 @@ function createEffectSharedApiImports() {
|
|
|
2743
2900
|
}
|
|
2744
2901
|
function createEffectSharedApiContract(service = effectService) {
|
|
2745
2902
|
const schemaExport = serviceEffectSchemaExport(service);
|
|
2903
|
+
const markerSchemaExport = serviceEffectMarkerSchemaExport(service);
|
|
2904
|
+
const readinessSchemaExport = serviceEffectReadinessSchemaExport(service);
|
|
2746
2905
|
const createPayloadSchemaExport = serviceEffectCreatePayloadSchemaExport(service);
|
|
2747
2906
|
const notFoundErrorExport = serviceEffectNotFoundErrorExport(service);
|
|
2748
2907
|
const notFoundSchemaExport = serviceEffectNotFoundSchemaExport(service);
|
|
@@ -2751,19 +2910,33 @@ function createEffectSharedApiContract(service = effectService) {
|
|
|
2751
2910
|
const groupName = serviceEffectGroupName(service);
|
|
2752
2911
|
const stem = effectApiStem(service);
|
|
2753
2912
|
const servicePrefix = effectApiPrefix(service);
|
|
2754
|
-
return `export const ${
|
|
2913
|
+
return `export const ${markerSchemaExport} = Schema.Struct({
|
|
2914
|
+
appId: Schema.String,
|
|
2915
|
+
packageName: Schema.String,
|
|
2916
|
+
version: Schema.String,
|
|
2917
|
+
build: Schema.String,
|
|
2918
|
+
deployProfile: Schema.String,
|
|
2919
|
+
surface: Schema.String,
|
|
2920
|
+
});
|
|
2921
|
+
|
|
2922
|
+
export const ${schemaExport} = Schema.Struct({
|
|
2755
2923
|
id: Schema.String,
|
|
2756
|
-
marker:
|
|
2757
|
-
appId: Schema.String,
|
|
2758
|
-
packageName: Schema.String,
|
|
2759
|
-
version: Schema.String,
|
|
2760
|
-
build: Schema.String,
|
|
2761
|
-
deployProfile: Schema.String,
|
|
2762
|
-
surface: Schema.String,
|
|
2763
|
-
}),
|
|
2924
|
+
marker: ${markerSchemaExport},
|
|
2764
2925
|
title: Schema.String,
|
|
2765
2926
|
});
|
|
2766
2927
|
|
|
2928
|
+
export const ${readinessSchemaExport} = Schema.Struct({
|
|
2929
|
+
checks: Schema.Struct({
|
|
2930
|
+
effectBff: Schema.Literal('ready'),
|
|
2931
|
+
moduleFederation: Schema.Literal('ready'),
|
|
2932
|
+
ssr: Schema.Literal('ready'),
|
|
2933
|
+
translations: Schema.Literal('ready'),
|
|
2934
|
+
}),
|
|
2935
|
+
marker: ${markerSchemaExport},
|
|
2936
|
+
status: Schema.Literal('ready'),
|
|
2937
|
+
versionSkew: Schema.Literal('none'),
|
|
2938
|
+
});
|
|
2939
|
+
|
|
2767
2940
|
export const ${createPayloadSchemaExport} = Schema.Struct({
|
|
2768
2941
|
title: Schema.String,
|
|
2769
2942
|
});
|
|
@@ -2799,6 +2972,11 @@ export const ${apiExport} = HttpApi.make('${apiName}').add(
|
|
|
2799
2972
|
}),
|
|
2800
2973
|
}),
|
|
2801
2974
|
)
|
|
2975
|
+
.add(
|
|
2976
|
+
HttpApiEndpoint.get('readiness', '/effect/${stem}/readiness', {
|
|
2977
|
+
success: ${readinessSchemaExport},
|
|
2978
|
+
}),
|
|
2979
|
+
)
|
|
2802
2980
|
.add(
|
|
2803
2981
|
HttpApiEndpoint.get('get', '/effect/${stem}/:id', {
|
|
2804
2982
|
params: {
|
|
@@ -2825,6 +3003,12 @@ export const ${groupName}OperationContexts = {
|
|
|
2825
3003
|
method: 'GET',
|
|
2826
3004
|
source: 'generated-client',
|
|
2827
3005
|
},
|
|
3006
|
+
readiness: {
|
|
3007
|
+
operationId: '${apiName}:${groupName}:readiness',
|
|
3008
|
+
routePath: '/effect/${stem}/readiness',
|
|
3009
|
+
method: 'GET',
|
|
3010
|
+
source: 'generated-client',
|
|
3011
|
+
},
|
|
2828
3012
|
get: {
|
|
2829
3013
|
operationId: '${apiName}:${groupName}:get',
|
|
2830
3014
|
routePath: '/effect/${stem}/:id',
|
|
@@ -2843,6 +3027,7 @@ export const ${groupName}ApiContract = {
|
|
|
2843
3027
|
basePath: '${servicePrefix}/effect/${stem}',
|
|
2844
3028
|
ownerId: '${service.id}',
|
|
2845
3029
|
servicePrefix: '${servicePrefix}',
|
|
3030
|
+
readinessPath: '${servicePrefix}/effect/${stem}/readiness',
|
|
2846
3031
|
} as const;
|
|
2847
3032
|
`;
|
|
2848
3033
|
}
|
|
@@ -2911,6 +3096,24 @@ const ${groupName}Layer = HttpApiBuilder.group(
|
|
|
2911
3096
|
}),
|
|
2912
3097
|
),
|
|
2913
3098
|
)
|
|
3099
|
+
.handle('readiness', () =>
|
|
3100
|
+
Effect.succeed({
|
|
3101
|
+
checks: {
|
|
3102
|
+
effectBff: 'ready' as const,
|
|
3103
|
+
moduleFederation: 'ready' as const,
|
|
3104
|
+
ssr: 'ready' as const,
|
|
3105
|
+
translations: 'ready' as const,
|
|
3106
|
+
},
|
|
3107
|
+
marker: ultramodernApiMarker,
|
|
3108
|
+
status: 'ready' as const,
|
|
3109
|
+
versionSkew: 'none' as const,
|
|
3110
|
+
}).pipe(
|
|
3111
|
+
Effect.withSpan('ultramodern.effect.${groupName}.readiness', {
|
|
3112
|
+
attributes: operationAttributes(${groupName}OperationContexts.readiness),
|
|
3113
|
+
kind: 'server',
|
|
3114
|
+
}),
|
|
3115
|
+
),
|
|
3116
|
+
)
|
|
2914
3117
|
.handle('get', ({ params }) => {
|
|
2915
3118
|
const item = ${groupName}Items.find(item => item.id === params.id);
|
|
2916
3119
|
return (item !== undefined
|
|
@@ -2960,6 +3163,7 @@ function createEffectClient(service, contractImportPath) {
|
|
|
2960
3163
|
const clientOptionsName = `${toPascalCase(stem)}ClientOptions`;
|
|
2961
3164
|
const createClientName = `create${toPascalCase(stem)}Client`;
|
|
2962
3165
|
const listName = `list${toPascalCase(stem)}`;
|
|
3166
|
+
const readinessName = `get${toPascalCase(stem)}Readiness`;
|
|
2963
3167
|
const getName = `get${toPascalCase(singular)}`;
|
|
2964
3168
|
const createName = `create${toPascalCase(singular)}`;
|
|
2965
3169
|
return `import {
|
|
@@ -3004,6 +3208,20 @@ export function ${listName}(
|
|
|
3004
3208
|
);
|
|
3005
3209
|
}
|
|
3006
3210
|
|
|
3211
|
+
export function ${readinessName}(
|
|
3212
|
+
options: ${clientOptionsName} = {},
|
|
3213
|
+
) {
|
|
3214
|
+
return runEffectRequest(
|
|
3215
|
+
${createClientName}({
|
|
3216
|
+
...options,
|
|
3217
|
+
operationContext:
|
|
3218
|
+
options.operationContext ?? ${groupName}OperationContexts.readiness,
|
|
3219
|
+
}),
|
|
3220
|
+
).then(client =>
|
|
3221
|
+
runEffectRequest(client.${groupName}.readiness({})),
|
|
3222
|
+
);
|
|
3223
|
+
}
|
|
3224
|
+
|
|
3007
3225
|
export function ${getName}(
|
|
3008
3226
|
id: string,
|
|
3009
3227
|
options: ${clientOptionsName} = {},
|
|
@@ -3039,17 +3257,141 @@ export function ${createName}(
|
|
|
3039
3257
|
}
|
|
3040
3258
|
function createShellEffectClient(scope) {
|
|
3041
3259
|
return `export {
|
|
3042
|
-
|
|
3043
|
-
|
|
3044
|
-
|
|
3045
|
-
|
|
3046
|
-
|
|
3047
|
-
|
|
3260
|
+
createCheckout,
|
|
3261
|
+
createCheckoutClient,
|
|
3262
|
+
getCheckout,
|
|
3263
|
+
getCheckoutReadiness,
|
|
3264
|
+
listCheckout,
|
|
3265
|
+
type CheckoutClientOptions,
|
|
3266
|
+
} from '${ultramodern_workspace_packageName(scope, 'remote-checkout')}/effect/client';
|
|
3267
|
+
|
|
3268
|
+
export {
|
|
3269
|
+
createDecide,
|
|
3270
|
+
createDecideClient,
|
|
3271
|
+
getDecide,
|
|
3272
|
+
getDecideReadiness,
|
|
3273
|
+
listDecide,
|
|
3274
|
+
type DecideClientOptions,
|
|
3275
|
+
} from '${ultramodern_workspace_packageName(scope, 'remote-decide')}/effect/client';
|
|
3276
|
+
|
|
3277
|
+
export {
|
|
3278
|
+
createExplore,
|
|
3279
|
+
createExploreClient,
|
|
3280
|
+
getExplore,
|
|
3281
|
+
getExploreReadiness,
|
|
3282
|
+
listExplore,
|
|
3283
|
+
type ExploreClientOptions,
|
|
3284
|
+
} from '${ultramodern_workspace_packageName(scope, 'remote-explore')}/effect/client';
|
|
3048
3285
|
`;
|
|
3049
3286
|
}
|
|
3050
3287
|
function toPascalCase(value) {
|
|
3051
3288
|
return value.split(/[-_]+/).filter(Boolean).map((part)=>`${part.charAt(0).toUpperCase()}${part.slice(1)}`).join('');
|
|
3052
3289
|
}
|
|
3290
|
+
function createEffectReadinessContract(app) {
|
|
3291
|
+
const stem = effectApiStem(app);
|
|
3292
|
+
return {
|
|
3293
|
+
endpoint: `/effect/${stem}/readiness`,
|
|
3294
|
+
marker: {
|
|
3295
|
+
ui: 'ultramodernUiMarker',
|
|
3296
|
+
api: 'ultramodernApiMarker',
|
|
3297
|
+
skew: 'none'
|
|
3298
|
+
},
|
|
3299
|
+
checks: [
|
|
3300
|
+
'moduleFederation',
|
|
3301
|
+
'ssr',
|
|
3302
|
+
'translations',
|
|
3303
|
+
'effectBff'
|
|
3304
|
+
]
|
|
3305
|
+
};
|
|
3306
|
+
}
|
|
3307
|
+
function createEffectRequestContextContract() {
|
|
3308
|
+
return {
|
|
3309
|
+
propagatedHeaders: [
|
|
3310
|
+
'accept-language',
|
|
3311
|
+
'authorization',
|
|
3312
|
+
'traceparent',
|
|
3313
|
+
'x-correlation-id',
|
|
3314
|
+
'x-tenant-id',
|
|
3315
|
+
'x-ultramodern-env',
|
|
3316
|
+
'x-vertical-version-id'
|
|
3317
|
+
],
|
|
3318
|
+
source: 'shell-to-vertical-effect-client'
|
|
3319
|
+
};
|
|
3320
|
+
}
|
|
3321
|
+
function createEffectDomainOperations(app) {
|
|
3322
|
+
const stem = effectApiStem(app);
|
|
3323
|
+
const group = serviceEffectGroupName(app);
|
|
3324
|
+
const basePath = `/effect/${stem}`;
|
|
3325
|
+
if ('checkout' === stem) return {
|
|
3326
|
+
cartSnapshot: {
|
|
3327
|
+
client: 'listCheckout',
|
|
3328
|
+
method: 'GET',
|
|
3329
|
+
path: basePath,
|
|
3330
|
+
resource: 'cart',
|
|
3331
|
+
owner: app.id
|
|
3332
|
+
},
|
|
3333
|
+
cartMutation: {
|
|
3334
|
+
client: 'createCheckout',
|
|
3335
|
+
method: 'POST',
|
|
3336
|
+
path: basePath,
|
|
3337
|
+
resource: 'cart-line',
|
|
3338
|
+
owner: app.id
|
|
3339
|
+
},
|
|
3340
|
+
orderConfirmation: {
|
|
3341
|
+
client: 'getCheckout',
|
|
3342
|
+
method: 'GET',
|
|
3343
|
+
path: `${basePath}/:id`,
|
|
3344
|
+
resource: 'order',
|
|
3345
|
+
owner: app.id
|
|
3346
|
+
}
|
|
3347
|
+
};
|
|
3348
|
+
if ('decide' === stem) return {
|
|
3349
|
+
productDetail: {
|
|
3350
|
+
client: 'getDecide',
|
|
3351
|
+
method: 'GET',
|
|
3352
|
+
path: `${basePath}/:id`,
|
|
3353
|
+
resource: 'product-detail',
|
|
3354
|
+
owner: app.id
|
|
3355
|
+
},
|
|
3356
|
+
configurationDraft: {
|
|
3357
|
+
client: 'createDecide',
|
|
3358
|
+
method: 'POST',
|
|
3359
|
+
path: basePath,
|
|
3360
|
+
resource: 'configuration',
|
|
3361
|
+
owner: app.id
|
|
3362
|
+
},
|
|
3363
|
+
productList: {
|
|
3364
|
+
client: 'listDecide',
|
|
3365
|
+
method: 'GET',
|
|
3366
|
+
path: basePath,
|
|
3367
|
+
resource: 'products',
|
|
3368
|
+
owner: app.id
|
|
3369
|
+
}
|
|
3370
|
+
};
|
|
3371
|
+
return {
|
|
3372
|
+
recommendationFeed: {
|
|
3373
|
+
client: `list${toPascalCase(stem)}`,
|
|
3374
|
+
method: 'GET',
|
|
3375
|
+
path: basePath,
|
|
3376
|
+
resource: 'recommendations',
|
|
3377
|
+
owner: app.id
|
|
3378
|
+
},
|
|
3379
|
+
recommendationDetail: {
|
|
3380
|
+
client: `get${toPascalCase(serviceEffectErrorStem(app))}`,
|
|
3381
|
+
method: 'GET',
|
|
3382
|
+
path: `${basePath}/:id`,
|
|
3383
|
+
resource: 'recommendation',
|
|
3384
|
+
owner: app.id
|
|
3385
|
+
},
|
|
3386
|
+
recommendationCreate: {
|
|
3387
|
+
client: `create${toPascalCase(serviceEffectErrorStem(app))}`,
|
|
3388
|
+
method: 'POST',
|
|
3389
|
+
path: basePath,
|
|
3390
|
+
resource: group,
|
|
3391
|
+
owner: app.id
|
|
3392
|
+
}
|
|
3393
|
+
};
|
|
3394
|
+
}
|
|
3053
3395
|
function effectApiTopologyMetadata(app) {
|
|
3054
3396
|
if (!appHasEffectApi(app)) return;
|
|
3055
3397
|
return {
|
|
@@ -3069,7 +3411,10 @@ function effectApiTopologyMetadata(app) {
|
|
|
3069
3411
|
},
|
|
3070
3412
|
serverEntry: `${app.directory}/api/effect/index.ts`,
|
|
3071
3413
|
basePath: `${app.effectApi.prefix}/effect/${app.effectApi.stem}`,
|
|
3072
|
-
consumedBy: app.effectApi.consumedBy
|
|
3414
|
+
consumedBy: app.effectApi.consumedBy,
|
|
3415
|
+
readiness: createEffectReadinessContract(app),
|
|
3416
|
+
requestContext: createEffectRequestContextContract(),
|
|
3417
|
+
domainOperations: createEffectDomainOperations(app)
|
|
3073
3418
|
}
|
|
3074
3419
|
};
|
|
3075
3420
|
}
|
|
@@ -3088,14 +3433,11 @@ function createTopology(scope) {
|
|
|
3088
3433
|
moduleFederation: {
|
|
3089
3434
|
role: 'host',
|
|
3090
3435
|
name: shellApp.mfName,
|
|
3091
|
-
remotes:
|
|
3092
|
-
id: remote.id,
|
|
3093
|
-
name: remote.mfName,
|
|
3094
|
-
manifestUrl: `http://localhost:${remote.port}/mf-manifest.json`
|
|
3095
|
-
})),
|
|
3436
|
+
remotes: createModuleFederationRemoteContracts(shellApp),
|
|
3096
3437
|
ssr: true,
|
|
3097
3438
|
sharedContractVersion: 'mf-ssr-contract-v1'
|
|
3098
3439
|
},
|
|
3440
|
+
cloudflare: createCloudflareDeployContract(scope, shellApp),
|
|
3099
3441
|
ownership: shellApp.ownership
|
|
3100
3442
|
},
|
|
3101
3443
|
remotes: remoteApps.map((remote)=>({
|
|
@@ -3108,6 +3450,10 @@ function createTopology(scope) {
|
|
|
3108
3450
|
name: remote.mfName,
|
|
3109
3451
|
manifestUrl: `http://localhost:${remote.port}/mf-manifest.json`,
|
|
3110
3452
|
exposes: Object.keys(remote.exposes ?? {}),
|
|
3453
|
+
...remote.remoteRefs?.length ? {
|
|
3454
|
+
remoteRefs: remote.remoteRefs,
|
|
3455
|
+
remotes: createModuleFederationRemoteContracts(remote)
|
|
3456
|
+
} : {},
|
|
3111
3457
|
ssr: true,
|
|
3112
3458
|
fallbackTelemetryEvent: 'modernjs:mv-runtime-parity',
|
|
3113
3459
|
sharedContractVersion: 'mf-ssr-contract-v1'
|
|
@@ -3115,6 +3461,7 @@ function createTopology(scope) {
|
|
|
3115
3461
|
...effectApiTopologyMetadata(remote) ? {
|
|
3116
3462
|
api: effectApiTopologyMetadata(remote)
|
|
3117
3463
|
} : {},
|
|
3464
|
+
cloudflare: createCloudflareDeployContract(scope, remote),
|
|
3118
3465
|
ownership: remote.ownership
|
|
3119
3466
|
})),
|
|
3120
3467
|
effectServices: [],
|
|
@@ -3225,6 +3572,11 @@ function createEffectOperationContract(target) {
|
|
|
3225
3572
|
path: `/effect/${stem}`,
|
|
3226
3573
|
source: 'generated-client'
|
|
3227
3574
|
},
|
|
3575
|
+
readiness: {
|
|
3576
|
+
method: 'GET',
|
|
3577
|
+
path: `/effect/${stem}/readiness`,
|
|
3578
|
+
source: 'generated-client'
|
|
3579
|
+
},
|
|
3228
3580
|
get: {
|
|
3229
3581
|
method: 'GET',
|
|
3230
3582
|
path: `/effect/${stem}/:id`,
|
|
@@ -3281,7 +3633,143 @@ function createAppConfigContract(app) {
|
|
|
3281
3633
|
} : {}
|
|
3282
3634
|
};
|
|
3283
3635
|
}
|
|
3284
|
-
function
|
|
3636
|
+
function cssLayerName(app) {
|
|
3637
|
+
if ('shell' === app.kind) return 'ultramodern-shell-base';
|
|
3638
|
+
return `ultramodern-remote-${app.domain ?? app.id}`;
|
|
3639
|
+
}
|
|
3640
|
+
function cssRole(app) {
|
|
3641
|
+
if ('shell' === app.kind) return 'shell-base-overlay';
|
|
3642
|
+
return 'horizontal-remote' === app.kind ? 'horizontal-remote-css' : 'vertical-remote-css';
|
|
3643
|
+
}
|
|
3644
|
+
function cssClassPrefix(app) {
|
|
3645
|
+
if ('shell' === app.kind) return 'shell-';
|
|
3646
|
+
return `${app.domain ?? app.id.replace(/^remote-/, '')}-`;
|
|
3647
|
+
}
|
|
3648
|
+
function createCssDedupeContract(scope) {
|
|
3649
|
+
return {
|
|
3650
|
+
strategy: 'shared-token-package-plus-css-content-hash',
|
|
3651
|
+
sharedPackage: ultramodern_workspace_packageName(scope, 'shared-design-tokens'),
|
|
3652
|
+
sharedLayers: [
|
|
3653
|
+
'ultramodern-shared-tokens'
|
|
3654
|
+
],
|
|
3655
|
+
runtimeLoad: 'once-per-content-hash',
|
|
3656
|
+
duplicateBaseStylesAllowed: false
|
|
3657
|
+
};
|
|
3658
|
+
}
|
|
3659
|
+
function createCssSsrContract(app) {
|
|
3660
|
+
return {
|
|
3661
|
+
cloudflare: true,
|
|
3662
|
+
firstPaintRequired: true,
|
|
3663
|
+
linkEmission: 'modern-ssr-css-assets',
|
|
3664
|
+
remoteCss: 'shell' === app.kind ? 'host-preloads-shell-and-shared-css' : 'remote-manifest-owned-css'
|
|
3665
|
+
};
|
|
3666
|
+
}
|
|
3667
|
+
function createAppCssFederationContract(scope, app) {
|
|
3668
|
+
const ownedLayers = 'shell' === app.kind ? [
|
|
3669
|
+
'ultramodern-shell-base',
|
|
3670
|
+
'ultramodern-shell-overlay'
|
|
3671
|
+
] : [
|
|
3672
|
+
cssLayerName(app)
|
|
3673
|
+
];
|
|
3674
|
+
return {
|
|
3675
|
+
owner: {
|
|
3676
|
+
id: app.id,
|
|
3677
|
+
package: ultramodern_workspace_packageName(scope, app.packageSuffix),
|
|
3678
|
+
team: app.ownership.team
|
|
3679
|
+
},
|
|
3680
|
+
role: cssRole(app),
|
|
3681
|
+
rootSelector: `[data-app-id="${app.id}"]`,
|
|
3682
|
+
classPrefix: cssClassPrefix(app),
|
|
3683
|
+
layers: {
|
|
3684
|
+
shared: [
|
|
3685
|
+
'ultramodern-shared-tokens'
|
|
3686
|
+
],
|
|
3687
|
+
owned: ownedLayers,
|
|
3688
|
+
imports: 'shell' === app.kind ? [
|
|
3689
|
+
'ultramodern-shared-tokens'
|
|
3690
|
+
] : [
|
|
3691
|
+
'ultramodern-shared-tokens'
|
|
3692
|
+
]
|
|
3693
|
+
},
|
|
3694
|
+
entrypoints: {
|
|
3695
|
+
layoutImport: 'src/routes/layout.tsx',
|
|
3696
|
+
css: [
|
|
3697
|
+
'src/routes/index.css'
|
|
3698
|
+
],
|
|
3699
|
+
...'shell' !== app.kind ? {
|
|
3700
|
+
remoteEntry: 'src/remote-entry.tsx'
|
|
3701
|
+
} : {}
|
|
3702
|
+
},
|
|
3703
|
+
assets: {
|
|
3704
|
+
shared: [
|
|
3705
|
+
`${ultramodern_workspace_packageName(scope, 'shared-design-tokens')}/tokens.css`
|
|
3706
|
+
],
|
|
3707
|
+
owned: [
|
|
3708
|
+
'src/routes/index.css'
|
|
3709
|
+
],
|
|
3710
|
+
emittedBy: 'modern-rspack-css-extraction',
|
|
3711
|
+
contentHash: true
|
|
3712
|
+
},
|
|
3713
|
+
dedupe: createCssDedupeContract(scope),
|
|
3714
|
+
ssr: createCssSsrContract(app)
|
|
3715
|
+
};
|
|
3716
|
+
}
|
|
3717
|
+
function createCssFederationContract(scope) {
|
|
3718
|
+
return {
|
|
3719
|
+
schemaVersion: 1,
|
|
3720
|
+
sharedDesignTokens: {
|
|
3721
|
+
owner: {
|
|
3722
|
+
id: 'shared-design-tokens',
|
|
3723
|
+
package: ultramodern_workspace_packageName(scope, 'shared-design-tokens'),
|
|
3724
|
+
team: 'super-app-platform'
|
|
3725
|
+
},
|
|
3726
|
+
role: 'shared-design-tokens',
|
|
3727
|
+
rootSelector: ':root',
|
|
3728
|
+
classPrefix: '--um-',
|
|
3729
|
+
layers: {
|
|
3730
|
+
owned: [
|
|
3731
|
+
'ultramodern-shared-tokens'
|
|
3732
|
+
]
|
|
3733
|
+
},
|
|
3734
|
+
entrypoints: {
|
|
3735
|
+
css: [
|
|
3736
|
+
'packages/shared-design-tokens/src/tokens.css'
|
|
3737
|
+
],
|
|
3738
|
+
typescript: [
|
|
3739
|
+
'packages/shared-design-tokens/src/index.ts'
|
|
3740
|
+
]
|
|
3741
|
+
},
|
|
3742
|
+
assets: {
|
|
3743
|
+
exports: [
|
|
3744
|
+
'./tokens.css'
|
|
3745
|
+
],
|
|
3746
|
+
css: [
|
|
3747
|
+
'packages/shared-design-tokens/src/tokens.css'
|
|
3748
|
+
]
|
|
3749
|
+
},
|
|
3750
|
+
dedupe: createCssDedupeContract(scope),
|
|
3751
|
+
ssr: {
|
|
3752
|
+
cloudflare: true,
|
|
3753
|
+
firstPaintRequired: true,
|
|
3754
|
+
importedByApps: true
|
|
3755
|
+
}
|
|
3756
|
+
},
|
|
3757
|
+
ownershipRules: {
|
|
3758
|
+
shell: [
|
|
3759
|
+
'base',
|
|
3760
|
+
'overlay'
|
|
3761
|
+
],
|
|
3762
|
+
remotes: [
|
|
3763
|
+
'vertical-css'
|
|
3764
|
+
],
|
|
3765
|
+
forbiddenRemoteLayers: [
|
|
3766
|
+
'ultramodern-shell-base',
|
|
3767
|
+
'ultramodern-shell-overlay'
|
|
3768
|
+
]
|
|
3769
|
+
}
|
|
3770
|
+
};
|
|
3771
|
+
}
|
|
3772
|
+
function createStylingContract(scope, app, enableTailwind) {
|
|
3285
3773
|
return {
|
|
3286
3774
|
tailwind: enableTailwind,
|
|
3287
3775
|
...enableTailwind ? {
|
|
@@ -3291,21 +3779,28 @@ function createStylingContract(enableTailwind) {
|
|
|
3291
3779
|
contentGlobs: [
|
|
3292
3780
|
'./src/**/*.{js,jsx,ts,tsx}'
|
|
3293
3781
|
]
|
|
3294
|
-
} : {}
|
|
3782
|
+
} : {},
|
|
3783
|
+
federation: createAppCssFederationContract(scope, app)
|
|
3295
3784
|
};
|
|
3296
3785
|
}
|
|
3297
3786
|
function createAppGeneratedContract(scope, app, apps, enableTailwind) {
|
|
3298
|
-
const
|
|
3787
|
+
const appWithResolvedRefs = 'shell' === app.kind ? {
|
|
3788
|
+
...app,
|
|
3789
|
+
remoteRefs: apps.filter((candidate)=>'shell' !== candidate.kind).map((candidate)=>candidate.id)
|
|
3790
|
+
} : app;
|
|
3791
|
+
const consumedRemotes = createModuleFederationRemoteContracts(appWithResolvedRefs, apps);
|
|
3299
3792
|
return {
|
|
3300
3793
|
id: app.id,
|
|
3301
3794
|
package: ultramodern_workspace_packageName(scope, app.packageSuffix),
|
|
3302
3795
|
path: app.directory,
|
|
3303
3796
|
kind: app.kind,
|
|
3304
3797
|
config: createAppConfigContract(app),
|
|
3305
|
-
styling: createStylingContract(enableTailwind),
|
|
3798
|
+
styling: createStylingContract(scope, app, enableTailwind),
|
|
3306
3799
|
deploy: {
|
|
3307
3800
|
target: 'cloudflare',
|
|
3801
|
+
cloudflare: createCloudflareDeployContract(scope, app),
|
|
3308
3802
|
worker: {
|
|
3803
|
+
name: createCloudflareWorkerName(scope, app),
|
|
3309
3804
|
ssr: true
|
|
3310
3805
|
},
|
|
3311
3806
|
output: {
|
|
@@ -3319,25 +3814,42 @@ function createAppGeneratedContract(scope, app, apps, enableTailwind) {
|
|
|
3319
3814
|
},
|
|
3320
3815
|
i18n: {
|
|
3321
3816
|
plugin: '@modern-js/plugin-i18n',
|
|
3322
|
-
backend:
|
|
3817
|
+
backend: {
|
|
3818
|
+
enabled: true,
|
|
3819
|
+
loadPath: '/locales/{{lng}}/{{ns}}.json'
|
|
3820
|
+
},
|
|
3323
3821
|
reactI18next: false,
|
|
3324
3822
|
languages: [
|
|
3325
3823
|
'en',
|
|
3326
3824
|
'cs'
|
|
3327
3825
|
],
|
|
3328
3826
|
fallbackLanguage: 'en',
|
|
3329
|
-
|
|
3827
|
+
namespace: appI18nNamespace(app),
|
|
3828
|
+
namespaces: [
|
|
3829
|
+
appI18nNamespace(app),
|
|
3830
|
+
'translation'
|
|
3831
|
+
],
|
|
3832
|
+
publicDir: './locales',
|
|
3833
|
+
localisedUrls: createLocalisedUrlsMap(app),
|
|
3834
|
+
resourceOwnership: {
|
|
3835
|
+
ownerAppId: app.id,
|
|
3836
|
+
source: 'route-owned',
|
|
3837
|
+
staticJson: `./locales/{lng}/${appI18nNamespace(app)}.json`
|
|
3838
|
+
}
|
|
3839
|
+
},
|
|
3840
|
+
routes: {
|
|
3841
|
+
source: 'route-owned',
|
|
3842
|
+
metadataExport: './src/routes/ultramodern-route-metadata',
|
|
3843
|
+
localisedUrls: createLocalisedUrlsMap(app),
|
|
3844
|
+
owned: createRouteOwnedI18nPaths(app),
|
|
3845
|
+
generatedRouteMap: true,
|
|
3846
|
+
manualOverrides: []
|
|
3330
3847
|
},
|
|
3331
3848
|
moduleFederation: {
|
|
3332
3849
|
name: app.mfName,
|
|
3333
|
-
...
|
|
3334
|
-
|
|
3335
|
-
|
|
3336
|
-
alias: remoteDependencyAlias(remote),
|
|
3337
|
-
name: remote.mfName,
|
|
3338
|
-
manifestEnv: createRemoteManifestEnv(remote),
|
|
3339
|
-
manifestUrl: `http://localhost:${remote.port}/mf-manifest.json`
|
|
3340
|
-
}))
|
|
3850
|
+
...appWithResolvedRefs.remoteRefs?.length ? {
|
|
3851
|
+
remoteRefs: appWithResolvedRefs.remoteRefs,
|
|
3852
|
+
remotes: consumedRemotes
|
|
3341
3853
|
} : {},
|
|
3342
3854
|
exposes: Object.keys(app.exposes ?? {}),
|
|
3343
3855
|
dts: {
|
|
@@ -3358,48 +3870,6 @@ function createAppGeneratedContract(scope, app, apps, enableTailwind) {
|
|
|
3358
3870
|
apiSurface: 'effect-bff'
|
|
3359
3871
|
} : {}
|
|
3360
3872
|
},
|
|
3361
|
-
...'commerce' === app.domain ? {
|
|
3362
|
-
boundaryVisualization: {
|
|
3363
|
-
mode: 'overlay',
|
|
3364
|
-
layoutAffecting: false,
|
|
3365
|
-
toggle: 'user-controlled',
|
|
3366
|
-
boundaries: [
|
|
3367
|
-
{
|
|
3368
|
-
id: 'explore',
|
|
3369
|
-
labelKey: 'commerce.boundaries.explore',
|
|
3370
|
-
owner: 'team-explore',
|
|
3371
|
-
color: '#ff5a57',
|
|
3372
|
-
owns: [
|
|
3373
|
-
'header',
|
|
3374
|
-
'footer',
|
|
3375
|
-
'recommendations',
|
|
3376
|
-
'catalog'
|
|
3377
|
-
]
|
|
3378
|
-
},
|
|
3379
|
-
{
|
|
3380
|
-
id: 'decide',
|
|
3381
|
-
labelKey: 'commerce.boundaries.decide',
|
|
3382
|
-
owner: 'team-decide',
|
|
3383
|
-
color: '#24d671',
|
|
3384
|
-
owns: [
|
|
3385
|
-
'product-detail',
|
|
3386
|
-
'variant-selection'
|
|
3387
|
-
]
|
|
3388
|
-
},
|
|
3389
|
-
{
|
|
3390
|
-
id: 'checkout',
|
|
3391
|
-
labelKey: 'commerce.boundaries.checkout',
|
|
3392
|
-
owner: 'team-checkout',
|
|
3393
|
-
color: '#f4d044',
|
|
3394
|
-
owns: [
|
|
3395
|
-
'add-to-cart',
|
|
3396
|
-
'cart-link',
|
|
3397
|
-
'cart-lines'
|
|
3398
|
-
]
|
|
3399
|
-
}
|
|
3400
|
-
]
|
|
3401
|
-
}
|
|
3402
|
-
} : {},
|
|
3403
3873
|
...appHasEffectApi(app) ? {
|
|
3404
3874
|
effect: {
|
|
3405
3875
|
runtime: 'effect',
|
|
@@ -3409,6 +3879,9 @@ function createAppGeneratedContract(scope, app, apps, enableTailwind) {
|
|
|
3409
3879
|
workerEntry: 'worker/__modern_bff_effect.js',
|
|
3410
3880
|
contract: './shared/effect/api',
|
|
3411
3881
|
client: './effect/client',
|
|
3882
|
+
readiness: createEffectReadinessContract(app),
|
|
3883
|
+
requestContext: createEffectRequestContextContract(),
|
|
3884
|
+
domainOperations: createEffectDomainOperations(app),
|
|
3412
3885
|
...createEffectOperationContract(app)
|
|
3413
3886
|
}
|
|
3414
3887
|
} : {}
|
|
@@ -3437,6 +3910,7 @@ function createGeneratedContract(scope, apps = [
|
|
|
3437
3910
|
zephyrAgent: ZEPHYR_AGENT_VERSION,
|
|
3438
3911
|
wrangler: WRANGLER_VERSION
|
|
3439
3912
|
},
|
|
3913
|
+
cssFederation: createCssFederationContract(scope),
|
|
3440
3914
|
apps: apps.map((app)=>createAppGeneratedContract(scope, app, apps, enableTailwind))
|
|
3441
3915
|
};
|
|
3442
3916
|
}
|
|
@@ -3559,16 +4033,670 @@ function createTemplateManifest(modernVersion, packageSource) {
|
|
|
3559
4033
|
}
|
|
3560
4034
|
};
|
|
3561
4035
|
}
|
|
4036
|
+
function createAssertMfTypesScript(remotes = remoteApps) {
|
|
4037
|
+
return `import fs from 'node:fs';
|
|
4038
|
+
import path from 'node:path';
|
|
4039
|
+
|
|
4040
|
+
const root = process.cwd();
|
|
4041
|
+
const generatedContractPath = path.join(
|
|
4042
|
+
root,
|
|
4043
|
+
'.modernjs/ultramodern-generated-contract.json',
|
|
4044
|
+
);
|
|
4045
|
+
const generatedContract = fs.existsSync(generatedContractPath)
|
|
4046
|
+
? JSON.parse(fs.readFileSync(generatedContractPath, 'utf-8'))
|
|
4047
|
+
: undefined;
|
|
4048
|
+
const defaultAppDirs = ${JSON.stringify(remotes.map((remote)=>remote.directory), null, 2)};
|
|
4049
|
+
|
|
4050
|
+
const candidateDirs = process.argv.slice(2);
|
|
4051
|
+
const appDirs = candidateDirs.length
|
|
4052
|
+
? candidateDirs
|
|
4053
|
+
: fs.existsSync(path.join(root, 'module-federation.config.ts'))
|
|
4054
|
+
? ['.']
|
|
4055
|
+
: defaultAppDirs;
|
|
4056
|
+
|
|
4057
|
+
for (const appDir of appDirs) {
|
|
4058
|
+
const configPath = path.join(root, appDir, 'module-federation.config.ts');
|
|
4059
|
+
if (!fs.existsSync(configPath)) {
|
|
4060
|
+
throw new Error(
|
|
4061
|
+
\`Missing Module Federation config: \${path.relative(root, configPath)}\`,
|
|
4062
|
+
);
|
|
4063
|
+
}
|
|
4064
|
+
|
|
4065
|
+
const contractEntry = generatedContract?.apps?.find(
|
|
4066
|
+
app => app.path === appDir.replace(/\\\\/g, '/'),
|
|
4067
|
+
);
|
|
4068
|
+
if (
|
|
4069
|
+
contractEntry &&
|
|
4070
|
+
contractEntry.moduleFederation?.dts?.compilerInstance !==
|
|
4071
|
+
'--package typescript -- tsc'
|
|
4072
|
+
) {
|
|
4073
|
+
throw new Error(
|
|
4074
|
+
\`Module Federation DTS must use the workspace TypeScript compiler: \${appDir}\`,
|
|
4075
|
+
);
|
|
4076
|
+
}
|
|
4077
|
+
|
|
4078
|
+
if (contractEntry && contractEntry.moduleFederation?.exposes?.length === 0) {
|
|
4079
|
+
continue;
|
|
4080
|
+
}
|
|
4081
|
+
|
|
4082
|
+
const typesArchivePath = path.join(root, appDir, 'dist/@mf-types.zip');
|
|
4083
|
+
if (!fs.existsSync(typesArchivePath)) {
|
|
4084
|
+
throw new Error(
|
|
4085
|
+
\`Missing Module Federation DTS archive: \${path.relative(root, typesArchivePath)}\`,
|
|
4086
|
+
);
|
|
4087
|
+
}
|
|
4088
|
+
|
|
4089
|
+
const stats = fs.statSync(typesArchivePath);
|
|
4090
|
+
if (stats.size === 0) {
|
|
4091
|
+
throw new Error(
|
|
4092
|
+
\`Empty Module Federation DTS archive: \${path.relative(root, typesArchivePath)}\`,
|
|
4093
|
+
);
|
|
4094
|
+
}
|
|
4095
|
+
}
|
|
4096
|
+
`;
|
|
4097
|
+
}
|
|
4098
|
+
function createWorkspaceValidationScript(scope, enableTailwind, remotes = remoteApps) {
|
|
4099
|
+
const verticals = remotes.filter(appHasEffectApi).map((remote)=>({
|
|
4100
|
+
id: remote.id,
|
|
4101
|
+
domain: remote.domain,
|
|
4102
|
+
stem: remote.effectApi.stem,
|
|
4103
|
+
group: serviceEffectGroupName(remote),
|
|
4104
|
+
path: remote.directory,
|
|
4105
|
+
mfName: remote.mfName,
|
|
4106
|
+
apiPrefix: remote.effectApi.prefix,
|
|
4107
|
+
packageName: ultramodern_workspace_packageName(scope, remote.packageSuffix),
|
|
4108
|
+
exposes: Object.keys(remote.exposes ?? {}),
|
|
4109
|
+
componentPaths: Object.keys(remote.exposes ?? {}).map((expose)=>remoteComponentOutputPath(remote, expose)).filter((componentPath)=>Boolean(componentPath)),
|
|
4110
|
+
namespace: appI18nNamespace(remote),
|
|
4111
|
+
routePagePaths: createRouteOwnedI18nPaths(remote).filter((route)=>'/' !== route.canonicalPath).map((route)=>createRoutePageFilePath(remote, route.canonicalPath)),
|
|
4112
|
+
localisedUrls: createLocalisedUrlsMap(remote),
|
|
4113
|
+
remoteRefs: remote.remoteRefs ?? []
|
|
4114
|
+
}));
|
|
4115
|
+
const shellNamespace = appI18nNamespace(shellApp);
|
|
4116
|
+
const oldRemotePaths = [
|
|
4117
|
+
'apps/remotes/remote-commerce',
|
|
4118
|
+
'apps/remotes/remote-identity',
|
|
4119
|
+
'apps/remotes/remote-design-system'
|
|
4120
|
+
];
|
|
4121
|
+
return `import { execFileSync } from 'node:child_process';
|
|
4122
|
+
import fs from 'node:fs';
|
|
4123
|
+
import path from 'node:path';
|
|
4124
|
+
|
|
4125
|
+
const root = process.cwd();
|
|
4126
|
+
const packageScope = '${scope}';
|
|
4127
|
+
const expectedPnpmVersion = '${PNPM_VERSION}';
|
|
4128
|
+
const tailwindEnabled = ${JSON.stringify(enableTailwind)};
|
|
4129
|
+
const fullStackVerticals = ${JSON.stringify(verticals, null, 2)};
|
|
4130
|
+
const shellNamespace = ${JSON.stringify(shellNamespace)};
|
|
4131
|
+
const oldRemotePaths = ${JSON.stringify(oldRemotePaths, null, 2)};
|
|
4132
|
+
|
|
4133
|
+
const readText = relativePath => fs.readFileSync(path.join(root, relativePath), 'utf-8');
|
|
4134
|
+
const readJson = relativePath => JSON.parse(readText(relativePath));
|
|
4135
|
+
const assert = (condition, message) => {
|
|
4136
|
+
if (!condition) {
|
|
4137
|
+
throw new Error(message);
|
|
4138
|
+
}
|
|
4139
|
+
};
|
|
4140
|
+
const assertExists = relativePath => {
|
|
4141
|
+
assert(fs.existsSync(path.join(root, relativePath)), \`Missing \${relativePath}\`);
|
|
4142
|
+
};
|
|
4143
|
+
const assertNotExists = relativePath => {
|
|
4144
|
+
assert(!fs.existsSync(path.join(root, relativePath)), \`Unexpected \${relativePath}\`);
|
|
4145
|
+
};
|
|
4146
|
+
const expectedWorkerName = packageSuffix => \`\${packageScope}-\${packageSuffix}\`.slice(0, 63);
|
|
4147
|
+
|
|
4148
|
+
const activePnpmVersion = execFileSync('pnpm', ['--version'], {
|
|
4149
|
+
cwd: root,
|
|
4150
|
+
encoding: 'utf-8',
|
|
4151
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
4152
|
+
}).trim();
|
|
4153
|
+
|
|
4154
|
+
assert(
|
|
4155
|
+
activePnpmVersion === expectedPnpmVersion,
|
|
4156
|
+
\`Generated workspace requires pnpm \${expectedPnpmVersion}; active pnpm is \${activePnpmVersion}. Run mise install, then rerun through mise exec -- pnpm ...\`,
|
|
4157
|
+
);
|
|
4158
|
+
|
|
4159
|
+
const requiredPaths = [
|
|
4160
|
+
'AGENTS.md',
|
|
4161
|
+
'.gitignore',
|
|
4162
|
+
'package.json',
|
|
4163
|
+
'pnpm-workspace.yaml',
|
|
4164
|
+
'tsconfig.base.json',
|
|
4165
|
+
'oxlint.config.ts',
|
|
4166
|
+
'oxfmt.config.ts',
|
|
4167
|
+
'.github/renovate.json',
|
|
4168
|
+
'.github/workflows/ultramodern-workspace-gates.yml',
|
|
4169
|
+
'.agents/skills-lock.json',
|
|
4170
|
+
'.agents/agent-reference-repos.json',
|
|
4171
|
+
'.agents/rstackjs-agent-skills-LICENSE',
|
|
4172
|
+
'topology/reference-topology.json',
|
|
4173
|
+
'topology/ownership.json',
|
|
4174
|
+
'topology/local-overlays/development.json',
|
|
4175
|
+
'.modernjs/ultramodern-workspace-template-manifest.json',
|
|
4176
|
+
'.modernjs/ultramodern-package-source.json',
|
|
4177
|
+
'.modernjs/ultramodern-generated-contract.json',
|
|
4178
|
+
'scripts/assert-mf-types.mjs',
|
|
4179
|
+
'scripts/bootstrap-agent-skills.mjs',
|
|
4180
|
+
'scripts/proof-cloudflare-version.mjs',
|
|
4181
|
+
'scripts/setup-agent-reference-repos.mjs',
|
|
4182
|
+
'apps/shell-super-app/package.json',
|
|
4183
|
+
'apps/shell-super-app/modern.config.ts',
|
|
4184
|
+
'apps/shell-super-app/module-federation.config.ts',
|
|
4185
|
+
'apps/shell-super-app/src/modern-app-env.d.ts',
|
|
4186
|
+
'apps/shell-super-app/src/modern.runtime.ts',
|
|
4187
|
+
'apps/shell-super-app/src/effect/recommendations-client.ts',
|
|
4188
|
+
'apps/shell-super-app/locales/en/translation.json',
|
|
4189
|
+
\`apps/shell-super-app/locales/en/\${shellNamespace}.json\`,
|
|
4190
|
+
'apps/shell-super-app/locales/cs/translation.json',
|
|
4191
|
+
\`apps/shell-super-app/locales/cs/\${shellNamespace}.json\`,
|
|
4192
|
+
'apps/shell-super-app/src/routes/index.css',
|
|
4193
|
+
'apps/shell-super-app/src/routes/layout.tsx',
|
|
4194
|
+
'apps/shell-super-app/src/routes/ultramodern-route-metadata.ts',
|
|
4195
|
+
'apps/shell-super-app/src/routes/[lang]/page.tsx',
|
|
4196
|
+
'packages/shared-contracts/src/index.ts',
|
|
4197
|
+
'packages/shared-design-tokens/src/index.ts',
|
|
4198
|
+
'packages/shared-design-tokens/src/tokens.css',
|
|
4199
|
+
'packages/shared-effect-api/src/index.ts',
|
|
4200
|
+
];
|
|
4201
|
+
|
|
4202
|
+
for (const vertical of fullStackVerticals) {
|
|
4203
|
+
requiredPaths.push(
|
|
4204
|
+
\`\${vertical.path}/package.json\`,
|
|
4205
|
+
\`\${vertical.path}/modern.config.ts\`,
|
|
4206
|
+
\`\${vertical.path}/module-federation.config.ts\`,
|
|
4207
|
+
\`\${vertical.path}/api/effect/index.ts\`,
|
|
4208
|
+
\`\${vertical.path}/shared/effect/api.ts\`,
|
|
4209
|
+
\`\${vertical.path}/src/effect/\${vertical.stem}-client.ts\`,
|
|
4210
|
+
\`\${vertical.path}/src/modern-app-env.d.ts\`,
|
|
4211
|
+
\`\${vertical.path}/src/modern.runtime.ts\`,
|
|
4212
|
+
\`\${vertical.path}/src/remote-entry.tsx\`,
|
|
4213
|
+
...vertical.componentPaths,
|
|
4214
|
+
\`\${vertical.path}/locales/en/translation.json\`,
|
|
4215
|
+
\`\${vertical.path}/locales/en/\${vertical.namespace}.json\`,
|
|
4216
|
+
\`\${vertical.path}/locales/cs/translation.json\`,
|
|
4217
|
+
\`\${vertical.path}/locales/cs/\${vertical.namespace}.json\`,
|
|
4218
|
+
\`\${vertical.path}/src/routes/index.css\`,
|
|
4219
|
+
\`\${vertical.path}/src/routes/layout.tsx\`,
|
|
4220
|
+
\`\${vertical.path}/src/routes/ultramodern-route-metadata.ts\`,
|
|
4221
|
+
\`\${vertical.path}/src/routes/[lang]/page.tsx\`,
|
|
4222
|
+
...vertical.routePagePaths,
|
|
4223
|
+
);
|
|
4224
|
+
}
|
|
4225
|
+
|
|
4226
|
+
if (tailwindEnabled) {
|
|
4227
|
+
requiredPaths.push(
|
|
4228
|
+
'apps/shell-super-app/postcss.config.mjs',
|
|
4229
|
+
'apps/shell-super-app/tailwind.config.ts',
|
|
4230
|
+
...fullStackVerticals.flatMap(vertical => [
|
|
4231
|
+
\`\${vertical.path}/postcss.config.mjs\`,
|
|
4232
|
+
\`\${vertical.path}/tailwind.config.ts\`,
|
|
4233
|
+
]),
|
|
4234
|
+
);
|
|
4235
|
+
}
|
|
4236
|
+
|
|
4237
|
+
for (const requiredPath of requiredPaths) {
|
|
4238
|
+
assertExists(requiredPath);
|
|
4239
|
+
}
|
|
4240
|
+
for (const oldRemotePath of oldRemotePaths) {
|
|
4241
|
+
assertNotExists(oldRemotePath);
|
|
4242
|
+
}
|
|
4243
|
+
assertNotExists('services/service-recommendations-effect');
|
|
4244
|
+
|
|
4245
|
+
const rootPackage = readJson('package.json');
|
|
4246
|
+
const packageSource = readJson('.modernjs/ultramodern-package-source.json');
|
|
4247
|
+
const generatedContract = readJson('.modernjs/ultramodern-generated-contract.json');
|
|
4248
|
+
const topology = readJson('topology/reference-topology.json');
|
|
4249
|
+
const ownership = readJson('topology/ownership.json');
|
|
4250
|
+
const overlay = readJson('topology/local-overlays/development.json');
|
|
4251
|
+
|
|
4252
|
+
assert(rootPackage.private === true, 'Root package must be private');
|
|
4253
|
+
assert(rootPackage.packageManager === \`pnpm@\${expectedPnpmVersion}\`, 'Root must pin pnpm');
|
|
4254
|
+
assert(rootPackage.modernjs?.preset === 'presetUltramodern', 'Root must declare presetUltramodern');
|
|
4255
|
+
assert(rootPackage.modernjs?.packageSource?.config === './.modernjs/ultramodern-package-source.json', 'Root must point at package source metadata');
|
|
4256
|
+
assert(rootPackage.modernjs?.packageSource?.strategy === packageSource.strategy, 'Root package source strategy must match metadata');
|
|
4257
|
+
assert(packageSource.strategy === 'workspace' || packageSource.strategy === 'install', 'Package source strategy must be workspace or install');
|
|
4258
|
+
assert(packageSource.generatedWorkspacePackages?.specifier === 'workspace:*', 'Generated workspace packages must keep workspace:* links');
|
|
4259
|
+
assert(
|
|
4260
|
+
rootPackage.scripts?.build ===
|
|
4261
|
+
'pnpm -r --filter "./apps/remotes/**" run build && pnpm --filter "./apps/shell-super-app" run build && pnpm ultramodern:assert-mf-types',
|
|
4262
|
+
'Root build script must build remotes before shell',
|
|
4263
|
+
);
|
|
4264
|
+
assert(rootPackage.scripts?.['ultramodern:check'] === 'node ./scripts/validate-ultramodern-workspace.mjs', 'Root must expose ultramodern:check');
|
|
4265
|
+
assert(rootPackage.scripts?.['ultramodern:assert-mf-types'] === 'node ./scripts/assert-mf-types.mjs', 'Root must expose ultramodern:assert-mf-types');
|
|
4266
|
+
assert(rootPackage.scripts?.['cloudflare:deploy']?.includes('run cloudflare:deploy'), 'Root must expose cloudflare:deploy');
|
|
4267
|
+
assert(rootPackage.scripts?.['cloudflare:proof'] === 'node ./scripts/proof-cloudflare-version.mjs --out .codex/reports/cloudflare-version-proof/public-url-proof.json', 'Root must expose cloudflare:proof');
|
|
4268
|
+
|
|
4269
|
+
const expectedAppIds = ['shell-super-app', ...fullStackVerticals.map(vertical => vertical.id)];
|
|
4270
|
+
assert(
|
|
4271
|
+
JSON.stringify(generatedContract.apps?.map(app => app.id)) === JSON.stringify(expectedAppIds),
|
|
4272
|
+
'Generated contract must contain shell plus the Tractor full-stack remotes',
|
|
4273
|
+
);
|
|
4274
|
+
assert(generatedContract.cssFederation?.sharedDesignTokens?.owner?.id === 'shared-design-tokens', 'CSS federation must declare shared design token ownership');
|
|
4275
|
+
assert(generatedContract.cssFederation?.sharedDesignTokens?.role === 'shared-design-tokens', 'CSS federation must mark shared-design-tokens as token owner');
|
|
4276
|
+
assert(generatedContract.cssFederation?.sharedDesignTokens?.rootSelector === ':root', 'Shared design tokens must declare their root selector');
|
|
4277
|
+
assert(generatedContract.cssFederation?.sharedDesignTokens?.classPrefix === '--um-', 'Shared design tokens must declare their CSS custom property prefix');
|
|
4278
|
+
assert(generatedContract.cssFederation?.sharedDesignTokens?.layers?.owned?.includes('ultramodern-shared-tokens'), 'Shared design tokens must own the shared token CSS layer');
|
|
4279
|
+
assert(generatedContract.cssFederation?.sharedDesignTokens?.entrypoints?.css?.includes('packages/shared-design-tokens/src/tokens.css'), 'Shared design tokens must declare their CSS entrypoint');
|
|
4280
|
+
assert(generatedContract.cssFederation?.sharedDesignTokens?.assets?.exports?.includes('./tokens.css'), 'Shared design tokens must export their CSS asset');
|
|
4281
|
+
assert(generatedContract.cssFederation?.sharedDesignTokens?.dedupe?.duplicateBaseStylesAllowed === false, 'Shared design token CSS must be deduplicated');
|
|
4282
|
+
assert(generatedContract.cssFederation?.sharedDesignTokens?.ssr?.firstPaintRequired === true, 'Shared design token CSS must be required for SSR first paint');
|
|
4283
|
+
|
|
4284
|
+
const shellPackage = readJson('apps/shell-super-app/package.json');
|
|
4285
|
+
const expectedZephyrDependencies = Object.fromEntries(
|
|
4286
|
+
fullStackVerticals.map(vertical => [
|
|
4287
|
+
vertical.domain,
|
|
4288
|
+
\`\${vertical.packageName}@workspace:*\`,
|
|
4289
|
+
]),
|
|
4290
|
+
);
|
|
4291
|
+
assert(
|
|
4292
|
+
JSON.stringify(shellPackage['zephyr:dependencies']) ===
|
|
4293
|
+
JSON.stringify(expectedZephyrDependencies),
|
|
4294
|
+
'Shell Zephyr dependencies must reference every Tractor remote package',
|
|
4295
|
+
);
|
|
4296
|
+
const shellContract = generatedContract.apps?.find(app => app.id === 'shell-super-app');
|
|
4297
|
+
assert(shellContract?.deploy?.cloudflare?.workerName === expectedWorkerName('shell-super-app'), 'Shell Cloudflare workerName is incorrect');
|
|
4298
|
+
assert(shellContract?.deploy?.cloudflare?.publicUrlEnv === 'ULTRAMODERN_PUBLIC_URL_SHELL_SUPER_APP', 'Shell Cloudflare public URL env is incorrect');
|
|
4299
|
+
assert(topology.shell?.cloudflare?.workerName === expectedWorkerName('shell-super-app'), 'Shell topology Cloudflare workerName is incorrect');
|
|
4300
|
+
assert(shellContract?.styling?.federation?.owner?.id === 'shell-super-app', 'Shell CSS federation owner is missing');
|
|
4301
|
+
assert(shellContract?.styling?.federation?.role === 'shell-base-overlay', 'Shell must own base and overlay CSS');
|
|
4302
|
+
assert(shellContract?.styling?.federation?.rootSelector === '[data-app-id="shell-super-app"]', 'Shell CSS root selector is incorrect');
|
|
4303
|
+
assert(shellContract?.styling?.federation?.classPrefix === 'shell-', 'Shell CSS class prefix is incorrect');
|
|
4304
|
+
assert(shellContract?.styling?.federation?.layers?.owned?.includes('ultramodern-shell-base'), 'Shell must own the base CSS layer');
|
|
4305
|
+
assert(shellContract?.styling?.federation?.layers?.owned?.includes('ultramodern-shell-overlay'), 'Shell must own the overlay CSS layer');
|
|
4306
|
+
assert(shellContract?.styling?.federation?.entrypoints?.css?.includes('src/routes/index.css'), 'Shell CSS entrypoint is missing');
|
|
4307
|
+
assert(shellContract?.styling?.federation?.assets?.shared?.some(asset => asset.endsWith('/shared-design-tokens/tokens.css')), 'Shell must import the shared design token CSS asset');
|
|
4308
|
+
assert(shellContract?.styling?.federation?.dedupe?.duplicateBaseStylesAllowed === false, 'Shell CSS contract must forbid duplicated base styles');
|
|
4309
|
+
assert(shellContract?.styling?.federation?.ssr?.firstPaintRequired === true, 'Shell CSS must be required for SSR first paint');
|
|
4310
|
+
assert(
|
|
4311
|
+
topology.shell?.remoteRefs?.join(',') === fullStackVerticals.map(vertical => vertical.id).join(','),
|
|
4312
|
+
'Topology shell remoteRefs must match Tractor remotes',
|
|
4313
|
+
);
|
|
4314
|
+
assert(topology.remotes?.length === fullStackVerticals.length, 'Topology must contain only Tractor remotes');
|
|
4315
|
+
assert((topology.effectServices ?? []).length === 0, 'Default APIs must be vertical-owned, not effectServices');
|
|
4316
|
+
|
|
4317
|
+
for (const vertical of fullStackVerticals) {
|
|
4318
|
+
const packageJson = readJson(\`\${vertical.path}/package.json\`);
|
|
4319
|
+
assert(packageJson.name === vertical.packageName, \`\${vertical.id} package name is incorrect\`);
|
|
4320
|
+
assert(packageJson.scripts?.['cloudflare:deploy'] === 'MODERNJS_DEPLOY=cloudflare modern deploy', \`\${vertical.id} must expose cloudflare:deploy\`);
|
|
4321
|
+
assert(packageJson.scripts?.['cloudflare:proof']?.includes(\`--app \${vertical.id}\`), \`\${vertical.id} must expose cloudflare:proof\`);
|
|
4322
|
+
assert(packageJson.dependencies?.['@modern-js/plugin-bff'], \`\${vertical.id} must depend on plugin-bff\`);
|
|
4323
|
+
assert(packageJson.exports?.['./effect/client'] === \`./src/effect/\${vertical.stem}-client.ts\`, \`\${vertical.id} must export its Effect client\`);
|
|
4324
|
+
assert(packageJson.exports?.['./shared/effect/api'] === './shared/effect/api.ts', \`\${vertical.id} must export its Effect API contract\`);
|
|
4325
|
+
const expectedVerticalZephyrDependencies = Object.fromEntries(
|
|
4326
|
+
fullStackVerticals
|
|
4327
|
+
.filter(candidate => vertical.remoteRefs.includes(candidate.id))
|
|
4328
|
+
.map(candidate => [
|
|
4329
|
+
candidate.domain,
|
|
4330
|
+
\`\${candidate.packageName}@workspace:*\`,
|
|
4331
|
+
]),
|
|
4332
|
+
);
|
|
4333
|
+
assert(
|
|
4334
|
+
JSON.stringify(packageJson['zephyr:dependencies']) ===
|
|
4335
|
+
JSON.stringify(expectedVerticalZephyrDependencies),
|
|
4336
|
+
\`\${vertical.id} Zephyr dependencies must match declared MF remote refs\`,
|
|
4337
|
+
);
|
|
4338
|
+
|
|
4339
|
+
const contractEntry = generatedContract.apps?.find(app => app.id === vertical.id);
|
|
4340
|
+
assert(contractEntry?.path === vertical.path, \`\${vertical.id} generated contract path is incorrect\`);
|
|
4341
|
+
assert(contractEntry?.kind === 'vertical', \`\${vertical.id} generated contract kind is incorrect\`);
|
|
4342
|
+
assert(contractEntry?.deploy?.cloudflare?.workerName === expectedWorkerName(vertical.id), \`\${vertical.id} Cloudflare workerName is incorrect\`);
|
|
4343
|
+
assert(contractEntry?.deploy?.cloudflare?.publicUrlEnv === \`ULTRAMODERN_PUBLIC_URL_\${vertical.id.replace(/-/g, '_').toUpperCase()}\`, \`\${vertical.id} Cloudflare public URL env is incorrect\`);
|
|
4344
|
+
assert(contractEntry?.deploy?.cloudflare?.routes?.effectReadiness === \`\${vertical.apiPrefix}/effect/\${vertical.stem}/readiness\`, \`\${vertical.id} Cloudflare proof readiness route is incorrect\`);
|
|
4345
|
+
assert(contractEntry?.moduleFederation?.name === vertical.mfName, \`\${vertical.id} MF name is incorrect\`);
|
|
4346
|
+
assert(JSON.stringify(contractEntry?.moduleFederation?.exposes) === JSON.stringify(vertical.exposes), \`\${vertical.id} MF exposes are incorrect\`);
|
|
4347
|
+
assert(contractEntry?.moduleFederation?.dts?.compilerInstance === '--package typescript -- tsc', \`\${vertical.id} must keep mandatory DTS compiler\`);
|
|
4348
|
+
assert(JSON.stringify(contractEntry?.moduleFederation?.remoteRefs ?? []) === JSON.stringify(vertical.remoteRefs), \`\${vertical.id} MF remoteRefs are incorrect\`);
|
|
4349
|
+
assert(
|
|
4350
|
+
JSON.stringify((contractEntry?.moduleFederation?.remotes ?? []).map(remote => remote.id)) ===
|
|
4351
|
+
JSON.stringify(vertical.remoteRefs),
|
|
4352
|
+
\`\${vertical.id} MF consumed remotes are incorrect\`,
|
|
4353
|
+
);
|
|
4354
|
+
assert(contractEntry?.effect?.prefix === vertical.apiPrefix, \`\${vertical.id} Effect API prefix is incorrect\`);
|
|
4355
|
+
assert(contractEntry?.effect?.group === vertical.group, \`\${vertical.id} Effect group is incorrect\`);
|
|
4356
|
+
assert(contractEntry?.effect?.readiness?.endpoint === \`/effect/\${vertical.stem}/readiness\`, \`\${vertical.id} readiness endpoint is incorrect\`);
|
|
4357
|
+
assert(contractEntry?.effect?.operations?.readiness?.path === \`/effect/\${vertical.stem}/readiness\`, \`\${vertical.id} readiness operation is missing\`);
|
|
4358
|
+
assert(contractEntry?.effect?.requestContext?.propagatedHeaders?.includes('traceparent'), \`\${vertical.id} trace context propagation is missing\`);
|
|
4359
|
+
assert(Object.keys(contractEntry?.effect?.domainOperations ?? {}).length >= 3, \`\${vertical.id} domain operations are missing\`);
|
|
4360
|
+
assert(contractEntry?.i18n?.languages?.includes('en') && contractEntry?.i18n?.languages?.includes('cs'), \`\${vertical.id} must declare i18n languages\`);
|
|
4361
|
+
assert(contractEntry?.i18n?.namespace === vertical.namespace, \`\${vertical.id} i18n namespace is incorrect\`);
|
|
4362
|
+
assert(
|
|
4363
|
+
JSON.stringify(contractEntry?.i18n?.localisedUrls) === JSON.stringify(vertical.localisedUrls),
|
|
4364
|
+
\`\${vertical.id} localisedUrls must come from route metadata\`,
|
|
4365
|
+
);
|
|
4366
|
+
assert(contractEntry?.routes?.source === 'route-owned', \`\${vertical.id} routes must be route-owned\`);
|
|
4367
|
+
assert(contractEntry?.routes?.metadataExport === './src/routes/ultramodern-route-metadata', \`\${vertical.id} route metadata export is incorrect\`);
|
|
4368
|
+
assert(contractEntry?.styling?.federation?.owner?.id === vertical.id, \`\${vertical.id} CSS federation owner is missing\`);
|
|
4369
|
+
assert(contractEntry?.styling?.federation?.role === 'vertical-remote-css', \`\${vertical.id} must own only vertical CSS\`);
|
|
4370
|
+
assert(contractEntry?.styling?.federation?.rootSelector === \`[data-app-id="\${vertical.id}"]\`, \`\${vertical.id} CSS root selector is incorrect\`);
|
|
4371
|
+
assert(contractEntry?.styling?.federation?.classPrefix === \`\${vertical.domain}-\`, \`\${vertical.id} CSS class prefix is incorrect\`);
|
|
4372
|
+
assert(contractEntry?.styling?.federation?.layers?.owned?.includes(\`ultramodern-remote-\${vertical.domain}\`), \`\${vertical.id} remote CSS layer is missing\`);
|
|
4373
|
+
assert(!contractEntry?.styling?.federation?.layers?.owned?.includes('ultramodern-shell-base'), \`\${vertical.id} must not own shell base CSS\`);
|
|
4374
|
+
assert(contractEntry?.styling?.federation?.entrypoints?.remoteEntry === 'src/remote-entry.tsx', \`\${vertical.id} remote CSS contract must include remote entry\`);
|
|
4375
|
+
assert(contractEntry?.styling?.federation?.assets?.shared?.some(asset => asset.endsWith('/shared-design-tokens/tokens.css')), \`\${vertical.id} must import shared design token CSS\`);
|
|
4376
|
+
assert(contractEntry?.styling?.federation?.dedupe?.runtimeLoad === 'once-per-content-hash', \`\${vertical.id} CSS dedupe strategy is incorrect\`);
|
|
4377
|
+
assert(contractEntry?.styling?.federation?.ssr?.remoteCss === 'remote-manifest-owned-css', \`\${vertical.id} SSR CSS loading contract is incorrect\`);
|
|
4378
|
+
|
|
4379
|
+
const topologyEntry = topology.remotes?.find(remote => remote.id === vertical.id);
|
|
4380
|
+
assert(topologyEntry?.kind === 'vertical', \`\${vertical.id} topology kind is incorrect\`);
|
|
4381
|
+
assert(topologyEntry?.package === vertical.packageName, \`\${vertical.id} topology package is incorrect\`);
|
|
4382
|
+
assert(topologyEntry?.cloudflare?.workerName === expectedWorkerName(vertical.id), \`\${vertical.id} topology Cloudflare workerName is incorrect\`);
|
|
4383
|
+
assert(topologyEntry?.moduleFederation?.name === vertical.mfName, \`\${vertical.id} topology MF name is incorrect\`);
|
|
4384
|
+
assert(JSON.stringify(topologyEntry?.moduleFederation?.exposes) === JSON.stringify(vertical.exposes), \`\${vertical.id} topology exposes are incorrect\`);
|
|
4385
|
+
assert(JSON.stringify(topologyEntry?.moduleFederation?.remoteRefs ?? []) === JSON.stringify(vertical.remoteRefs), \`\${vertical.id} topology remoteRefs are incorrect\`);
|
|
4386
|
+
assert(topologyEntry?.api?.effect?.bff?.prefix === vertical.apiPrefix, \`\${vertical.id} topology API prefix is incorrect\`);
|
|
4387
|
+
assert(topologyEntry?.api?.effect?.serverEntry === \`\${vertical.path}/api/effect/index.ts\`, \`\${vertical.id} topology server entry is incorrect\`);
|
|
4388
|
+
assert(topologyEntry?.api?.effect?.readiness?.endpoint === \`/effect/\${vertical.stem}/readiness\`, \`\${vertical.id} topology readiness endpoint is incorrect\`);
|
|
4389
|
+
assert(Object.keys(topologyEntry?.api?.effect?.domainOperations ?? {}).length >= 3, \`\${vertical.id} topology domain operations are missing\`);
|
|
4390
|
+
|
|
4391
|
+
assert(ownership.owners?.some(owner => owner.id === vertical.id && owner.path === vertical.path), \`\${vertical.id} ownership entry is missing\`);
|
|
4392
|
+
assert(overlay.ports?.[vertical.id], \`\${vertical.id} development port is missing\`);
|
|
4393
|
+
assert(overlay.manifests?.[vertical.id]?.includes('/mf-manifest.json'), \`\${vertical.id} development manifest is missing\`);
|
|
4394
|
+
assert(overlay.apis?.[vertical.id]?.endsWith(vertical.apiPrefix), \`\${vertical.id} development API URL is missing\`);
|
|
4395
|
+
}
|
|
4396
|
+
|
|
4397
|
+
console.log('UltraModern workspace scaffold validated');
|
|
4398
|
+
`;
|
|
4399
|
+
}
|
|
4400
|
+
function createCloudflareVersionProofScript() {
|
|
4401
|
+
return `#!/usr/bin/env node
|
|
4402
|
+
import fs from 'node:fs';
|
|
4403
|
+
import path from 'node:path';
|
|
4404
|
+
import { fileURLToPath } from 'node:url';
|
|
4405
|
+
|
|
4406
|
+
const workspaceRoot = path.resolve(
|
|
4407
|
+
path.dirname(fileURLToPath(import.meta.url)),
|
|
4408
|
+
'..',
|
|
4409
|
+
);
|
|
4410
|
+
const contractPath = path.join(
|
|
4411
|
+
workspaceRoot,
|
|
4412
|
+
'.modernjs/ultramodern-generated-contract.json',
|
|
4413
|
+
);
|
|
4414
|
+
const defaultOut = path.join(
|
|
4415
|
+
workspaceRoot,
|
|
4416
|
+
'.codex/reports/cloudflare-version-proof/public-url-proof.json',
|
|
4417
|
+
);
|
|
4418
|
+
|
|
4419
|
+
function readJson(filePath) {
|
|
4420
|
+
return JSON.parse(fs.readFileSync(filePath, 'utf8'));
|
|
4421
|
+
}
|
|
4422
|
+
|
|
4423
|
+
function parseArgs(argv) {
|
|
4424
|
+
const parsed = {
|
|
4425
|
+
appId: undefined,
|
|
4426
|
+
out: defaultOut,
|
|
4427
|
+
requirePublicUrls: false,
|
|
4428
|
+
};
|
|
4429
|
+
|
|
4430
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
4431
|
+
const arg = argv[index];
|
|
4432
|
+
if (arg === '--app') {
|
|
4433
|
+
parsed.appId = argv[index + 1];
|
|
4434
|
+
index += 1;
|
|
4435
|
+
} else if (arg === '--out') {
|
|
4436
|
+
parsed.out = argv[index + 1];
|
|
4437
|
+
index += 1;
|
|
4438
|
+
} else if (arg === '--require-public-urls') {
|
|
4439
|
+
parsed.requirePublicUrls = true;
|
|
4440
|
+
} else if (arg === '--help' || arg === '-h') {
|
|
4441
|
+
parsed.help = true;
|
|
4442
|
+
} else {
|
|
4443
|
+
throw new Error(\`Unknown argument: \${arg}\`);
|
|
4444
|
+
}
|
|
4445
|
+
}
|
|
4446
|
+
|
|
4447
|
+
return parsed;
|
|
4448
|
+
}
|
|
4449
|
+
|
|
4450
|
+
function printHelp() {
|
|
4451
|
+
process.stdout.write(\`Usage:
|
|
4452
|
+
node scripts/proof-cloudflare-version.mjs [--app remote-explore] [--out evidence.json] [--require-public-urls]
|
|
4453
|
+
|
|
4454
|
+
Set each app's public URL using the contract env key, for example:
|
|
4455
|
+
ULTRAMODERN_PUBLIC_URL_REMOTE_EXPLORE=https://remote-explore.example.workers.dev
|
|
4456
|
+
\`);
|
|
4457
|
+
}
|
|
4458
|
+
|
|
4459
|
+
function joinUrl(baseUrl, routePath) {
|
|
4460
|
+
return new URL(routePath, baseUrl.endsWith('/') ? baseUrl : \`\${baseUrl}/\`);
|
|
4461
|
+
}
|
|
4462
|
+
|
|
4463
|
+
async function fetchText(url) {
|
|
4464
|
+
const response = await fetch(url);
|
|
4465
|
+
return {
|
|
4466
|
+
ok: response.ok,
|
|
4467
|
+
status: response.status,
|
|
4468
|
+
contentType: response.headers.get('content-type'),
|
|
4469
|
+
body: await response.text(),
|
|
4470
|
+
};
|
|
4471
|
+
}
|
|
4472
|
+
|
|
4473
|
+
function parseMaybeJson(body) {
|
|
4474
|
+
try {
|
|
4475
|
+
return JSON.parse(body);
|
|
4476
|
+
} catch {
|
|
4477
|
+
return undefined;
|
|
4478
|
+
}
|
|
4479
|
+
}
|
|
4480
|
+
|
|
4481
|
+
function markerFromJson(value) {
|
|
4482
|
+
if (!value || typeof value !== 'object') {
|
|
4483
|
+
return undefined;
|
|
4484
|
+
}
|
|
4485
|
+
if (value.marker && typeof value.marker.build === 'string') {
|
|
4486
|
+
return value.marker.build;
|
|
4487
|
+
}
|
|
4488
|
+
if (typeof value.build === 'string') {
|
|
4489
|
+
return value.build;
|
|
4490
|
+
}
|
|
4491
|
+
for (const nested of Object.values(value)) {
|
|
4492
|
+
if (Array.isArray(nested)) {
|
|
4493
|
+
for (const item of nested) {
|
|
4494
|
+
const marker = markerFromJson(item);
|
|
4495
|
+
if (marker) {
|
|
4496
|
+
return marker;
|
|
4497
|
+
}
|
|
4498
|
+
}
|
|
4499
|
+
} else {
|
|
4500
|
+
const marker = markerFromJson(nested);
|
|
4501
|
+
if (marker) {
|
|
4502
|
+
return marker;
|
|
4503
|
+
}
|
|
4504
|
+
}
|
|
4505
|
+
}
|
|
4506
|
+
return undefined;
|
|
4507
|
+
}
|
|
4508
|
+
|
|
4509
|
+
function extractUiMarker(html) {
|
|
4510
|
+
return html.match(/data-build-marker=["']([^"']+)["']/u)?.[1];
|
|
4511
|
+
}
|
|
4512
|
+
|
|
4513
|
+
function assert(condition, message) {
|
|
4514
|
+
if (!condition) {
|
|
4515
|
+
throw new Error(message);
|
|
4516
|
+
}
|
|
4517
|
+
}
|
|
4518
|
+
|
|
4519
|
+
async function validateApp(app, publicUrl) {
|
|
4520
|
+
const cloudflare = app.deploy?.cloudflare;
|
|
4521
|
+
const routes = cloudflare?.routes ?? {};
|
|
4522
|
+
const evidence = {
|
|
4523
|
+
appId: app.id,
|
|
4524
|
+
publicUrl,
|
|
4525
|
+
workerName: cloudflare?.workerName,
|
|
4526
|
+
publicUrlEnv: cloudflare?.publicUrlEnv,
|
|
4527
|
+
assertions: [],
|
|
4528
|
+
};
|
|
4529
|
+
|
|
4530
|
+
const ssrRoute = routes.ssr ?? '/en';
|
|
4531
|
+
const ssr = await fetchText(joinUrl(publicUrl, ssrRoute));
|
|
4532
|
+
evidence.assertions.push({
|
|
4533
|
+
type: 'ssr',
|
|
4534
|
+
route: ssrRoute,
|
|
4535
|
+
status: ssr.ok ? 'pass' : 'fail',
|
|
4536
|
+
statusCode: ssr.status,
|
|
4537
|
+
});
|
|
4538
|
+
assert(ssr.ok, \`\${app.id} SSR route returned HTTP \${ssr.status}\`);
|
|
4539
|
+
|
|
4540
|
+
const uiMarker = extractUiMarker(ssr.body);
|
|
4541
|
+
evidence.assertions.push({
|
|
4542
|
+
type: 'ui-marker',
|
|
4543
|
+
expected: app.marker?.build,
|
|
4544
|
+
actual: uiMarker,
|
|
4545
|
+
status: uiMarker === app.marker?.build ? 'pass' : 'fail',
|
|
4546
|
+
});
|
|
4547
|
+
assert(uiMarker === app.marker?.build, \`\${app.id} UI marker mismatch\`);
|
|
4548
|
+
|
|
4549
|
+
const cssRootSelector = app.styling?.federation?.rootSelector;
|
|
4550
|
+
const expectedAppId = cssRootSelector?.match(/data-app-id="([^"]+)"/u)?.[1];
|
|
4551
|
+
evidence.assertions.push({
|
|
4552
|
+
type: 'css-root-marker',
|
|
4553
|
+
expected: cssRootSelector,
|
|
4554
|
+
status:
|
|
4555
|
+
expectedAppId && ssr.body.includes(\`data-app-id="\${expectedAppId}"\`)
|
|
4556
|
+
? 'pass'
|
|
4557
|
+
: 'fail',
|
|
4558
|
+
});
|
|
4559
|
+
assert(
|
|
4560
|
+
expectedAppId && ssr.body.includes(\`data-app-id="\${expectedAppId}"\`),
|
|
4561
|
+
\`\${app.id} SSR response is missing CSS root marker \${cssRootSelector}\`,
|
|
4562
|
+
);
|
|
4563
|
+
|
|
4564
|
+
const manifestRoute = routes.mfManifest ?? '/mf-manifest.json';
|
|
4565
|
+
const manifest = await fetchText(joinUrl(publicUrl, manifestRoute));
|
|
4566
|
+
evidence.assertions.push({
|
|
4567
|
+
type: 'mf-manifest',
|
|
4568
|
+
route: manifestRoute,
|
|
4569
|
+
status: manifest.ok ? 'pass' : 'fail',
|
|
4570
|
+
statusCode: manifest.status,
|
|
4571
|
+
});
|
|
4572
|
+
assert(
|
|
4573
|
+
manifest.ok,
|
|
4574
|
+
\`\${app.id} MF manifest returned HTTP \${manifest.status}\`,
|
|
4575
|
+
);
|
|
4576
|
+
|
|
4577
|
+
const localeRoute = routes.locale ?? \`/locales/en/\${app.i18n?.namespace}.json\`;
|
|
4578
|
+
const locale = await fetchText(joinUrl(publicUrl, localeRoute));
|
|
4579
|
+
const localeJson = parseMaybeJson(locale.body);
|
|
4580
|
+
evidence.assertions.push({
|
|
4581
|
+
type: 'i18n-marker',
|
|
4582
|
+
namespace: app.i18n?.namespace,
|
|
4583
|
+
route: localeRoute,
|
|
4584
|
+
status:
|
|
4585
|
+
locale.ok &&
|
|
4586
|
+
localeJson &&
|
|
4587
|
+
Object.hasOwn(localeJson, app.i18n?.namespace)
|
|
4588
|
+
? 'pass'
|
|
4589
|
+
: 'fail',
|
|
4590
|
+
statusCode: locale.status,
|
|
4591
|
+
});
|
|
4592
|
+
assert(locale.ok, \`\${app.id} locale JSON returned HTTP \${locale.status}\`);
|
|
4593
|
+
assert(
|
|
4594
|
+
localeJson && Object.hasOwn(localeJson, app.i18n?.namespace),
|
|
4595
|
+
\`\${app.id} locale JSON is missing namespace \${app.i18n?.namespace}\`,
|
|
4596
|
+
);
|
|
4597
|
+
|
|
4598
|
+
if (routes.effectReadiness) {
|
|
4599
|
+
const readiness = await fetchText(joinUrl(publicUrl, routes.effectReadiness));
|
|
4600
|
+
const readinessJson = parseMaybeJson(readiness.body);
|
|
4601
|
+
const apiMarker = markerFromJson(readinessJson);
|
|
4602
|
+
evidence.assertions.push({
|
|
4603
|
+
type: 'api-marker',
|
|
4604
|
+
route: routes.effectReadiness,
|
|
4605
|
+
expected: app.marker?.build,
|
|
4606
|
+
actual: apiMarker,
|
|
4607
|
+
status: readiness.ok && apiMarker === app.marker?.build ? 'pass' : 'fail',
|
|
4608
|
+
statusCode: readiness.status,
|
|
4609
|
+
});
|
|
4610
|
+
assert(
|
|
4611
|
+
readiness.ok,
|
|
4612
|
+
\`\${app.id} Effect readiness returned HTTP \${readiness.status}\`,
|
|
4613
|
+
);
|
|
4614
|
+
assert(apiMarker === app.marker?.build, \`\${app.id} API marker mismatch\`);
|
|
4615
|
+
}
|
|
4616
|
+
|
|
4617
|
+
return evidence;
|
|
4618
|
+
}
|
|
4619
|
+
|
|
4620
|
+
async function main(argv = process.argv.slice(2)) {
|
|
4621
|
+
const args = parseArgs(argv);
|
|
4622
|
+
if (args.help) {
|
|
4623
|
+
printHelp();
|
|
4624
|
+
return 0;
|
|
4625
|
+
}
|
|
4626
|
+
|
|
4627
|
+
const contract = readJson(contractPath);
|
|
4628
|
+
const apps = args.appId
|
|
4629
|
+
? contract.apps.filter(app => app.id === args.appId)
|
|
4630
|
+
: contract.apps;
|
|
4631
|
+
assert(apps.length > 0, \`No generated app matched \${args.appId}\`);
|
|
4632
|
+
|
|
4633
|
+
const results = [];
|
|
4634
|
+
const skipped = [];
|
|
4635
|
+
for (const app of apps) {
|
|
4636
|
+
const publicUrlEnv = app.deploy?.cloudflare?.publicUrlEnv;
|
|
4637
|
+
const publicUrl = publicUrlEnv && process.env[publicUrlEnv];
|
|
4638
|
+
if (!publicUrl) {
|
|
4639
|
+
const skippedEntry = {
|
|
4640
|
+
appId: app.id,
|
|
4641
|
+
status: args.requirePublicUrls ? 'fail' : 'skipped',
|
|
4642
|
+
publicUrlEnv,
|
|
4643
|
+
reason: 'public URL environment variable is not set',
|
|
4644
|
+
};
|
|
4645
|
+
skipped.push(skippedEntry);
|
|
4646
|
+
if (args.requirePublicUrls) {
|
|
4647
|
+
throw new Error(\`\${app.id} requires \${publicUrlEnv}\`);
|
|
4648
|
+
}
|
|
4649
|
+
continue;
|
|
4650
|
+
}
|
|
4651
|
+
results.push(await validateApp(app, publicUrl));
|
|
4652
|
+
}
|
|
4653
|
+
|
|
4654
|
+
const report = {
|
|
4655
|
+
schemaVersion: 1,
|
|
4656
|
+
generatedAt: new Date().toISOString(),
|
|
4657
|
+
status: results.length > 0 ? 'pass' : 'skipped',
|
|
4658
|
+
contractPath,
|
|
4659
|
+
results,
|
|
4660
|
+
skipped,
|
|
4661
|
+
};
|
|
4662
|
+
|
|
4663
|
+
fs.mkdirSync(path.dirname(args.out), { recursive: true });
|
|
4664
|
+
fs.writeFileSync(args.out, \`\${JSON.stringify(report, null, 2)}\\n\`);
|
|
4665
|
+
process.stdout.write(
|
|
4666
|
+
\`[cloudflare-version-proof] \${report.status}: \${args.out}\\n\`,
|
|
4667
|
+
);
|
|
4668
|
+
return 0;
|
|
4669
|
+
}
|
|
4670
|
+
|
|
4671
|
+
main().then(
|
|
4672
|
+
exitCode => {
|
|
4673
|
+
process.exitCode = exitCode;
|
|
4674
|
+
},
|
|
4675
|
+
error => {
|
|
4676
|
+
process.stderr.write(\`[cloudflare-version-proof] \${error.message}\\n\`);
|
|
4677
|
+
process.exitCode = 1;
|
|
4678
|
+
},
|
|
4679
|
+
);
|
|
4680
|
+
`;
|
|
4681
|
+
}
|
|
4682
|
+
function writeGeneratedWorkspaceScripts(targetDir, scope, enableTailwind) {
|
|
4683
|
+
writeFileReplacing(targetDir, "scripts/assert-mf-types.mjs", createAssertMfTypesScript());
|
|
4684
|
+
writeFileReplacing(targetDir, "scripts/validate-ultramodern-workspace.mjs", createWorkspaceValidationScript(scope, enableTailwind));
|
|
4685
|
+
writeFileReplacing(targetDir, "scripts/proof-cloudflare-version.mjs", createCloudflareVersionProofScript());
|
|
4686
|
+
}
|
|
3562
4687
|
function writeApp(targetDir, scope, app, packageSource, enableTailwind) {
|
|
3563
4688
|
writeJson(targetDir, `${app.directory}/package.json`, createAppPackage(scope, app, packageSource, enableTailwind));
|
|
3564
4689
|
writeJson(targetDir, `${app.directory}/tsconfig.json`, createPackageTsConfig(app.directory, appHasEffectApi(app)));
|
|
3565
|
-
writeFile(targetDir, `${app.directory}/src/modern-app-env.d.ts`,
|
|
4690
|
+
writeFile(targetDir, `${app.directory}/src/modern-app-env.d.ts`, createAppEnvDts(app));
|
|
3566
4691
|
writeFile(targetDir, `${app.directory}/src/ultramodern-build.ts`, createUltramodernBuildModule(scope, app));
|
|
3567
|
-
writeFile(targetDir, `${app.directory}/
|
|
4692
|
+
writeFile(targetDir, `${app.directory}/src/routes/ultramodern-route-metadata.ts`, createRouteMetadataModule(app));
|
|
4693
|
+
writeFile(targetDir, `${app.directory}/modern.config.ts`, createAppModernConfig(scope, app));
|
|
3568
4694
|
writeFile(targetDir, `${app.directory}/src/modern.runtime.ts`, createAppRuntimeConfig(app));
|
|
3569
4695
|
writeJson(targetDir, `${app.directory}/locales/en/translation.json`, createAppLocaleMessages(app, 'en'));
|
|
4696
|
+
writeJson(targetDir, `${app.directory}/locales/en/${appI18nNamespace(app)}.json`, createAppLocaleMessages(app, 'en'));
|
|
3570
4697
|
writeJson(targetDir, `${app.directory}/locales/cs/translation.json`, createAppLocaleMessages(app, 'cs'));
|
|
3571
|
-
|
|
4698
|
+
writeJson(targetDir, `${app.directory}/locales/cs/${appI18nNamespace(app)}.json`, createAppLocaleMessages(app, 'cs'));
|
|
4699
|
+
writeFile(targetDir, `${app.directory}/src/routes/index.css`, createAppStyles(enableTailwind, scope, app));
|
|
3572
4700
|
if (enableTailwind) {
|
|
3573
4701
|
writeFile(targetDir, `${app.directory}/postcss.config.mjs`, createPostcssConfig());
|
|
3574
4702
|
writeFile(targetDir, `${app.directory}/tailwind.config.ts`, createTailwindConfig());
|
|
@@ -3576,6 +4704,7 @@ function writeApp(targetDir, scope, app, packageSource, enableTailwind) {
|
|
|
3576
4704
|
writeFile(targetDir, `${app.directory}/module-federation.config.ts`, 'shell' === app.kind ? createShellModuleFederationConfig() : createRemoteModuleFederationConfig(app));
|
|
3577
4705
|
writeFile(targetDir, `${app.directory}/src/routes/layout.tsx`, createLayout(app.id));
|
|
3578
4706
|
writeFile(targetDir, `${app.directory}/src/routes/[lang]/page.tsx`, 'shell' === app.kind ? createShellPage() : createRemotePage(app));
|
|
4707
|
+
for (const route of createRouteOwnedI18nPaths(app))if ('/' !== route.canonicalPath) writeFile(targetDir, createRoutePageFilePath(app, route.canonicalPath), createRouteAliasPage(route.canonicalPath));
|
|
3579
4708
|
if ('shell' === app.kind) writeFile(targetDir, `${app.directory}/src/effect/recommendations-client.ts`, createShellEffectClient(scope));
|
|
3580
4709
|
if (appHasEffectApi(app)) {
|
|
3581
4710
|
writeFile(targetDir, `${app.directory}/shared/effect/api.ts`, createEffectSharedApi(app));
|
|
@@ -3584,7 +4713,10 @@ function writeApp(targetDir, scope, app, packageSource, enableTailwind) {
|
|
|
3584
4713
|
}
|
|
3585
4714
|
if ('vertical' === app.kind || 'horizontal-remote' === app.kind) {
|
|
3586
4715
|
writeFile(targetDir, `${app.directory}/src/remote-entry.tsx`, createRemoteEntry(app));
|
|
3587
|
-
|
|
4716
|
+
for (const expose of Object.keys(app.exposes ?? {})){
|
|
4717
|
+
const outputPath = remoteComponentOutputPath(app, expose);
|
|
4718
|
+
if (outputPath) writeFile(targetDir, outputPath, createRemoteExposeComponent(app, expose));
|
|
4719
|
+
}
|
|
3588
4720
|
}
|
|
3589
4721
|
if ('horizontal-design-system' === app.kind) {
|
|
3590
4722
|
writeFile(targetDir, `${app.directory}/src/components/button.tsx`, createDesignButton());
|
|
@@ -3601,7 +4733,7 @@ function writeEffectService(targetDir, scope, packageSource, enableTailwind, ser
|
|
|
3601
4733
|
return <main>${service.id} Effect service</main>;
|
|
3602
4734
|
}
|
|
3603
4735
|
`);
|
|
3604
|
-
writeFile(targetDir, `${service.directory}/src/routes/index.css`,
|
|
4736
|
+
writeFile(targetDir, `${service.directory}/src/routes/index.css`, createServiceStyles(enableTailwind, scope, service));
|
|
3605
4737
|
if (enableTailwind) {
|
|
3606
4738
|
writeFile(targetDir, `${service.directory}/postcss.config.mjs`, createPostcssConfig());
|
|
3607
4739
|
writeFile(targetDir, `${service.directory}/tailwind.config.ts`, createTailwindConfig());
|
|
@@ -3619,6 +4751,7 @@ function writeGenericSharedPackage(targetDir, scope, packageSource, sharedPackag
|
|
|
3619
4751
|
});
|
|
3620
4752
|
writeFile(targetDir, `${sharedPackage.directory}/src/index.ts`, `export const packageId = '${sharedPackage.id}';
|
|
3621
4753
|
`);
|
|
4754
|
+
if ('shared-design-tokens' === sharedPackage.id) writeFile(targetDir, `${sharedPackage.directory}/src/tokens.css`, createSharedDesignTokensCss());
|
|
3622
4755
|
}
|
|
3623
4756
|
function writeSharedPackages(targetDir, scope, packageSource) {
|
|
3624
4757
|
for (const sharedPackage of sharedPackages){
|
|
@@ -3644,6 +4777,7 @@ function writeSharedPackages(targetDir, scope, packageSource) {
|
|
|
3644
4777
|
},
|
|
3645
4778
|
} as const;
|
|
3646
4779
|
`);
|
|
4780
|
+
writeFile(targetDir, 'packages/shared-design-tokens/src/tokens.css', createSharedDesignTokensCss());
|
|
3647
4781
|
writeFile(targetDir, 'packages/shared-effect-api/src/index.ts', createEffectSharedApi());
|
|
3648
4782
|
}
|
|
3649
4783
|
function readJsonFile(filePath) {
|
|
@@ -3742,6 +4876,10 @@ function remoteTopologyEntry(scope, remote) {
|
|
|
3742
4876
|
name: remote.mfName,
|
|
3743
4877
|
manifestUrl: `http://localhost:${remote.port}/mf-manifest.json`,
|
|
3744
4878
|
exposes: Object.keys(remote.exposes ?? {}),
|
|
4879
|
+
...remote.remoteRefs?.length ? {
|
|
4880
|
+
remoteRefs: remote.remoteRefs,
|
|
4881
|
+
remotes: createModuleFederationRemoteContracts(remote)
|
|
4882
|
+
} : {},
|
|
3745
4883
|
ssr: true,
|
|
3746
4884
|
fallbackTelemetryEvent: 'modernjs:mv-runtime-parity',
|
|
3747
4885
|
sharedContractVersion: 'mf-ssr-contract-v1'
|
|
@@ -3780,6 +4918,17 @@ function remotesFromTopology(topology, ports) {
|
|
|
3780
4918
|
portEnv: '',
|
|
3781
4919
|
port: 'number' == typeof ports[remote.id] ? ports[remote.id] : 0,
|
|
3782
4920
|
mfName: remote.moduleFederation?.name ?? `remote${toPascalCase(remote.id)}`,
|
|
4921
|
+
...Array.isArray(remote.moduleFederation?.exposes) ? {
|
|
4922
|
+
exposes: Object.fromEntries(remote.moduleFederation.exposes.map((expose)=>[
|
|
4923
|
+
expose,
|
|
4924
|
+
''
|
|
4925
|
+
]))
|
|
4926
|
+
} : {},
|
|
4927
|
+
...Array.isArray(remote.moduleFederation?.remoteRefs) ? {
|
|
4928
|
+
remoteRefs: remote.moduleFederation.remoteRefs
|
|
4929
|
+
} : Array.isArray(remote.moduleFederation?.remotes) ? {
|
|
4930
|
+
remoteRefs: remote.moduleFederation.remotes.map((entry)=>entry.id).filter((id)=>'string' == typeof id)
|
|
4931
|
+
} : {},
|
|
3783
4932
|
...effectApi ? {
|
|
3784
4933
|
effectApi
|
|
3785
4934
|
} : {},
|
|
@@ -3837,7 +4986,10 @@ function addUltramodernMicroVertical(options) {
|
|
|
3837
4986
|
writeJsonFile(ownershipPath, ownership);
|
|
3838
4987
|
writeJsonFile(overlayPath, overlay);
|
|
3839
4988
|
writeJsonFile(node_path.join(options.workspaceRoot, GENERATED_CONTRACT_PATH), createGeneratedContract(scope, [
|
|
3840
|
-
|
|
4989
|
+
{
|
|
4990
|
+
...shellApp,
|
|
4991
|
+
remoteRefs: remotesFromTopology(topology, overlay.ports).map((remote)=>remote.id)
|
|
4992
|
+
},
|
|
3841
4993
|
...remotesFromTopology(topology, overlay.ports)
|
|
3842
4994
|
], enableTailwind));
|
|
3843
4995
|
const shellConfigPath = node_path.join(options.workspaceRoot, `${shellApp.directory}/module-federation.config.ts`);
|
|
@@ -3940,6 +5092,7 @@ function generateUltramodernWorkspace(options) {
|
|
|
3940
5092
|
writeApp(options.targetDir, scope, shellApp, packageSource, enableTailwind);
|
|
3941
5093
|
for (const remote of remoteApps)writeApp(options.targetDir, scope, remote, packageSource, enableTailwind);
|
|
3942
5094
|
writeSharedPackages(options.targetDir, scope, packageSource);
|
|
5095
|
+
writeGeneratedWorkspaceScripts(options.targetDir, scope, enableTailwind);
|
|
3943
5096
|
}
|
|
3944
5097
|
const src_dirname = node_path.dirname(fileURLToPath(import.meta.url));
|
|
3945
5098
|
const templateDir = node_path.resolve(src_dirname, '..', 'template');
|