@bleedingdev/modern-js-create 3.2.0-ultramodern.119 → 3.2.0-ultramodern.120
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 +50 -11
- package/dist/cjs/ultramodern-workspace.cjs +1651 -476
- package/dist/esm/ultramodern-workspace.js +1651 -476
- package/dist/esm-node/ultramodern-workspace.js +1651 -476
- package/package.json +7 -7
- package/template-workspace/AGENTS.md +5 -3
- package/template-workspace/README.md.handlebars +45 -9
|
@@ -11,23 +11,23 @@ const TANSTACK_ROUTER_VERSION = '1.170.15';
|
|
|
11
11
|
const MODULE_FEDERATION_VERSION = '2.5.1';
|
|
12
12
|
const ZEPHYR_RSPACK_PLUGIN_VERSION = '1.1.1';
|
|
13
13
|
const ZEPHYR_AGENT_VERSION = '1.1.1';
|
|
14
|
-
const WRANGLER_VERSION = '4.
|
|
14
|
+
const WRANGLER_VERSION = '4.99.0';
|
|
15
15
|
const CLOUDFLARE_COMPATIBILITY_DATE = '2026-06-02';
|
|
16
16
|
const TAILWIND_VERSION = '4.3.0';
|
|
17
17
|
const TAILWIND_POSTCSS_VERSION = '4.3.0';
|
|
18
18
|
const POSTCSS_VERSION = '8.5.15';
|
|
19
|
-
const EFFECT_TSGO_VERSION = '0.14.
|
|
19
|
+
const EFFECT_TSGO_VERSION = '0.14.3';
|
|
20
20
|
const TYPESCRIPT_VERSION = '6.0.3';
|
|
21
|
-
const TYPESCRIPT_NATIVE_PREVIEW_VERSION = '7.0.0-dev.
|
|
22
|
-
const OXLINT_VERSION = '1.
|
|
23
|
-
const OXFMT_VERSION = '0.
|
|
24
|
-
const ULTRACITE_VERSION = '7.8.
|
|
21
|
+
const TYPESCRIPT_NATIVE_PREVIEW_VERSION = '7.0.0-dev.20260610.1';
|
|
22
|
+
const OXLINT_VERSION = '1.69.0';
|
|
23
|
+
const OXFMT_VERSION = '0.54.0';
|
|
24
|
+
const ULTRACITE_VERSION = '7.8.3';
|
|
25
25
|
const LEFTHOOK_VERSION = '^2.1.9';
|
|
26
26
|
const I18NEXT_VERSION = '26.3.1';
|
|
27
27
|
const REACT_VERSION = '^19.2.7';
|
|
28
28
|
const REACT_DOM_VERSION = '^19.2.7';
|
|
29
29
|
const REACT_ROUTER_DOM_VERSION = '7.17.0';
|
|
30
|
-
const PNPM_VERSION = '11.5.
|
|
30
|
+
const PNPM_VERSION = '11.5.3';
|
|
31
31
|
const GENERATED_CONTRACT_PATH = '.modernjs/ultramodern-generated-contract.json';
|
|
32
32
|
const RSTACK_AGENT_SKILLS_COMMIT = '61c948b42512e223bad44b83af4080eba48b2677';
|
|
33
33
|
const MODULE_FEDERATION_AGENT_SKILLS_COMMIT = '07bb5b6c43ad457609e00c081b72d4c42508ec76';
|
|
@@ -624,9 +624,67 @@ function createCloudflareSecurityContract() {
|
|
|
624
624
|
}
|
|
625
625
|
};
|
|
626
626
|
}
|
|
627
|
+
const PUBLIC_WEBSITE_POLICY = {
|
|
628
|
+
qualityGates: {
|
|
629
|
+
publicRoutes: {
|
|
630
|
+
requireSitemapWhenPresent: true,
|
|
631
|
+
requireRobotsSitemapConsistency: true,
|
|
632
|
+
requireWebManifestWhenPresent: true
|
|
633
|
+
},
|
|
634
|
+
statusCodes: {
|
|
635
|
+
notFoundRoute: '/__ultramodern-smoke-missing',
|
|
636
|
+
unknownRouteStatus: 404
|
|
637
|
+
},
|
|
638
|
+
indexing: {
|
|
639
|
+
previewNoindex: true,
|
|
640
|
+
productionPublicRoutesIndexable: true
|
|
641
|
+
},
|
|
642
|
+
assets: {
|
|
643
|
+
cssPreloadRequired: true,
|
|
644
|
+
cssResponseRequired: true,
|
|
645
|
+
cacheControlRequiredForCss: true,
|
|
646
|
+
sourcemapsPubliclyReferenced: false
|
|
647
|
+
},
|
|
648
|
+
budgets: {
|
|
649
|
+
ssrHtmlMaxBytes: 250000,
|
|
650
|
+
mfManifestMaxBytes: 500000,
|
|
651
|
+
localeJsonMaxBytes: 100000,
|
|
652
|
+
sitemapXmlMaxBytes: 500000,
|
|
653
|
+
cssAssetMaxBytes: 750000
|
|
654
|
+
},
|
|
655
|
+
csp: {
|
|
656
|
+
finalMode: 'report-only-dogfood',
|
|
657
|
+
decision: "Report-only remains the generated final mode until public smoke proof records MF SSR script/style/connect compatibility for the deployed surface."
|
|
658
|
+
}
|
|
659
|
+
},
|
|
660
|
+
publicHead: {
|
|
661
|
+
indexableRobots: 'index, follow',
|
|
662
|
+
privateRouteRobots: 'noindex, nofollow'
|
|
663
|
+
},
|
|
664
|
+
publicSurface: {
|
|
665
|
+
defaultProviderFile: 'route.sitemap.mjs',
|
|
666
|
+
draftPolicy: 'omit-draft-by-default',
|
|
667
|
+
indexablePolicy: 'omit-indexable-false'
|
|
668
|
+
}
|
|
669
|
+
};
|
|
627
670
|
function formatTsJsonValue(value, indent) {
|
|
628
671
|
return JSON.stringify(value, null, 2).replaceAll('\n', `\n${' '.repeat(indent)}`);
|
|
629
672
|
}
|
|
673
|
+
function formatIntegerCodeLiteral(value) {
|
|
674
|
+
return String(value).replace(/\B(?=(\d{3})+(?!\d))/gu, '_');
|
|
675
|
+
}
|
|
676
|
+
function createPublicWebsiteQualityGateContract() {
|
|
677
|
+
return PUBLIC_WEBSITE_POLICY.qualityGates;
|
|
678
|
+
}
|
|
679
|
+
function createPublicWebsiteBudgetFallback(budgetName) {
|
|
680
|
+
return formatIntegerCodeLiteral(PUBLIC_WEBSITE_POLICY.qualityGates.budgets[budgetName]);
|
|
681
|
+
}
|
|
682
|
+
function createPublicHeadRobotsPolicy() {
|
|
683
|
+
return PUBLIC_WEBSITE_POLICY.publicHead;
|
|
684
|
+
}
|
|
685
|
+
function createPublicSurfaceContentExpansionPolicy() {
|
|
686
|
+
return PUBLIC_WEBSITE_POLICY.publicSurface;
|
|
687
|
+
}
|
|
630
688
|
function createCloudflareDeployContract(scope, app) {
|
|
631
689
|
return {
|
|
632
690
|
target: 'cloudflare',
|
|
@@ -640,6 +698,7 @@ function createCloudflareDeployContract(scope, app) {
|
|
|
640
698
|
assetsBinding: 'ASSETS',
|
|
641
699
|
routes: createCloudflareProofRoute(app),
|
|
642
700
|
security: createCloudflareSecurityContract(),
|
|
701
|
+
qualityGates: createPublicWebsiteQualityGateContract(),
|
|
643
702
|
evidence: {
|
|
644
703
|
proofScript: "scripts/proof-cloudflare-version.mjs",
|
|
645
704
|
reportDefault: '.codex/reports/cloudflare-version-proof/public-url-proof.json'
|
|
@@ -705,6 +764,9 @@ function createPackageTsConfig(packageDir, includeApi = false) {
|
|
|
705
764
|
};
|
|
706
765
|
}
|
|
707
766
|
function createAppPackage(scope, app, packageSource, enableTailwind, remotes = []) {
|
|
767
|
+
const publicSurfaceBuildCommand = createPublicSurfaceGenerationCommand(app, 'dist');
|
|
768
|
+
const publicSurfaceCloudflareBuildCommand = createPublicSurfaceGenerationCommand(app, 'dist');
|
|
769
|
+
const publicSurfaceCloudflareOutputCommand = createPublicSurfaceGenerationCommand(app, 'cloudflare');
|
|
708
770
|
const packageExports = Object.fromEntries(Object.entries(app.exposes ?? {}).map(([expose, source])=>[
|
|
709
771
|
expose,
|
|
710
772
|
source
|
|
@@ -715,8 +777,8 @@ function createAppPackage(scope, app, packageSource, enableTailwind, remotes = [
|
|
|
715
777
|
version: '0.1.0',
|
|
716
778
|
scripts: {
|
|
717
779
|
dev: 'modern dev',
|
|
718
|
-
build: app.exposes ? `ULTRAMODERN_ZEPHYR=false modern build && node ${relativeRootFor(app.directory)}/scripts/assert-mf-types.mjs` :
|
|
719
|
-
'cloudflare:build':
|
|
780
|
+
build: app.exposes ? `ULTRAMODERN_ZEPHYR=false modern build && ${publicSurfaceBuildCommand} && node ${relativeRootFor(app.directory)}/scripts/assert-mf-types.mjs` : `ULTRAMODERN_ZEPHYR=false modern build && ${publicSurfaceBuildCommand}`,
|
|
781
|
+
'cloudflare:build': `ULTRAMODERN_ZEPHYR=false MODERNJS_DEPLOY=cloudflare modern build && ${publicSurfaceCloudflareBuildCommand} && ULTRAMODERN_ZEPHYR=false MODERNJS_DEPLOY=cloudflare modern deploy && ${publicSurfaceCloudflareOutputCommand}`,
|
|
720
782
|
'cloudflare:deploy': 'ULTRAMODERN_CLOUDFLARE_REQUIRE_PUBLIC_URLS=true pnpm run cloudflare:build && wrangler deploy --config .output/wrangler.json',
|
|
721
783
|
'cloudflare:preview': 'pnpm run cloudflare:build && wrangler dev --config .output/wrangler.json',
|
|
722
784
|
'cloudflare:proof': `node ${relativeRootFor(app.directory)}/scripts/proof-cloudflare-version.mjs --app ${app.id}`,
|
|
@@ -773,6 +835,40 @@ function createSharedPackage(scope, id, description, packageSource) {
|
|
|
773
835
|
};
|
|
774
836
|
return packageJson;
|
|
775
837
|
}
|
|
838
|
+
function createSharedContractsIndex() {
|
|
839
|
+
return `export type UltramodernPublicSitemapChangeFrequency =
|
|
840
|
+
| 'always'
|
|
841
|
+
| 'hourly'
|
|
842
|
+
| 'daily'
|
|
843
|
+
| 'weekly'
|
|
844
|
+
| 'monthly'
|
|
845
|
+
| 'yearly'
|
|
846
|
+
| 'never';
|
|
847
|
+
|
|
848
|
+
export type UltramodernPublicSitemapEntry = {
|
|
849
|
+
/**
|
|
850
|
+
* Params used to expand every localized route pattern, for example
|
|
851
|
+
* { slug: 'platform-story' } for /talks/:slug.
|
|
852
|
+
*/
|
|
853
|
+
params: Record<string, string | number | boolean>;
|
|
854
|
+
/**
|
|
855
|
+
* Per-locale overrides when translated URLs use translated params.
|
|
856
|
+
*/
|
|
857
|
+
localeParams?: Partial<Record<'en' | 'cs', Record<string, string | number | boolean>>>;
|
|
858
|
+
draft?: boolean;
|
|
859
|
+
indexable?: boolean;
|
|
860
|
+
lastModified?: string;
|
|
861
|
+
changeFrequency?: UltramodernPublicSitemapChangeFrequency;
|
|
862
|
+
priority?: number;
|
|
863
|
+
};
|
|
864
|
+
|
|
865
|
+
export const ultramodernWorkspaceContract = {
|
|
866
|
+
ownership: 'topology/ownership.json',
|
|
867
|
+
preset: 'presetUltramodern',
|
|
868
|
+
topology: 'topology/reference-topology.json',
|
|
869
|
+
} as const;
|
|
870
|
+
`;
|
|
871
|
+
}
|
|
776
872
|
function createAppModernConfig(scope, app) {
|
|
777
873
|
const bffImport = appHasEffectApi(app) ? "import { bffPlugin } from '@modern-js/plugin-bff';\n" : '';
|
|
778
874
|
const bffConfig = appHasEffectApi(app) ? ` bff: {
|
|
@@ -838,11 +934,20 @@ const inferredCloudflareUrl =
|
|
|
838
934
|
cloudflareDeployEnabled && cloudflareWorkersDevSubdomain !== undefined
|
|
839
935
|
? \`https://\${cloudflareWorkerName}.\${cloudflareWorkersDevSubdomain}.workers.dev\`
|
|
840
936
|
: undefined;
|
|
937
|
+
// Site origin (SEO: canonical/hreflang URLs) prefers the site-wide public URL;
|
|
938
|
+
// the per-app deployment URL only fills in when no site origin is configured.
|
|
841
939
|
const siteUrl =
|
|
842
|
-
configuredCloudflareUrl ||
|
|
843
940
|
configuredSiteUrl ||
|
|
941
|
+
configuredCloudflareUrl ||
|
|
844
942
|
inferredCloudflareUrl ||
|
|
845
943
|
\`http://localhost:\${port}\`;
|
|
944
|
+
// Asset origin prefers the per-app deployment URL (each MF app serves its own
|
|
945
|
+
// assets). Without an explicit public URL, assets must stay origin-relative so
|
|
946
|
+
// the app works behind tunnels and proxies (an absolute localhost assetPrefix
|
|
947
|
+
// makes pages served via e.g. ngrok fetch scripts from localhost, which Chrome
|
|
948
|
+
// blocks behind a Local Network Access permission prompt).
|
|
949
|
+
const assetPrefix =
|
|
950
|
+
configuredCloudflareUrl || configuredSiteUrl || inferredCloudflareUrl || '/';
|
|
846
951
|
|
|
847
952
|
if (
|
|
848
953
|
cloudflareDeployEnabled &&
|
|
@@ -871,11 +976,16 @@ ${bffConfig} ...(cloudflareDeployEnabled
|
|
|
871
976
|
},
|
|
872
977
|
}
|
|
873
978
|
: {}),
|
|
979
|
+
dev: {
|
|
980
|
+
// Keep dev assets origin-relative too; the default absolute
|
|
981
|
+
// http://localhost:<port> prefix breaks pages served through tunnels.
|
|
982
|
+
assetPrefix: '/',
|
|
983
|
+
},
|
|
874
984
|
html: {
|
|
875
985
|
outputStructure: 'flat',
|
|
876
986
|
},
|
|
877
987
|
output: {
|
|
878
|
-
assetPrefix
|
|
988
|
+
assetPrefix,
|
|
879
989
|
disableTsChecker: true,
|
|
880
990
|
distPath: {
|
|
881
991
|
html: './',
|
|
@@ -1100,7 +1210,7 @@ export default createModuleFederationConfig({
|
|
|
1100
1210
|
dts: {
|
|
1101
1211
|
displayErrorInTerminal: true,
|
|
1102
1212
|
generateTypes: {
|
|
1103
|
-
compilerInstance: '
|
|
1213
|
+
compilerInstance: 'tsgo',
|
|
1104
1214
|
},
|
|
1105
1215
|
},
|
|
1106
1216
|
filename: 'remoteEntry.js',
|
|
@@ -1158,7 +1268,7 @@ export default createModuleFederationConfig({
|
|
|
1158
1268
|
dts: {
|
|
1159
1269
|
displayErrorInTerminal: true,
|
|
1160
1270
|
generateTypes: {
|
|
1161
|
-
compilerInstance: '
|
|
1271
|
+
compilerInstance: 'tsgo',
|
|
1162
1272
|
},
|
|
1163
1273
|
},
|
|
1164
1274
|
exposes: ${exposes},
|
|
@@ -1180,6 +1290,7 @@ const privateAppRoutePublicness = {
|
|
|
1180
1290
|
function createRouteOwnedI18nPaths(app) {
|
|
1181
1291
|
const namespace = appI18nNamespace(app);
|
|
1182
1292
|
const base = {
|
|
1293
|
+
descriptionKey: `${namespace}.seo.description`,
|
|
1183
1294
|
mfBoundaryId: app.mfName,
|
|
1184
1295
|
namespace,
|
|
1185
1296
|
ownerAppId: app.id,
|
|
@@ -1381,6 +1492,7 @@ function createPublicRouteMetadata(app) {
|
|
|
1381
1492
|
localisedPaths: route.localisedPaths,
|
|
1382
1493
|
namespace: route.namespace,
|
|
1383
1494
|
ownerAppId: route.ownerAppId,
|
|
1495
|
+
descriptionKey: route.descriptionKey,
|
|
1384
1496
|
titleKey: route.titleKey
|
|
1385
1497
|
}));
|
|
1386
1498
|
}
|
|
@@ -1389,7 +1501,11 @@ function createRouteMetadataModule(app) {
|
|
|
1389
1501
|
const localisedUrls = sortJsonValue(createLocalisedUrlsMap(app));
|
|
1390
1502
|
const publicRoutes = sortJsonValue(createPublicRouteMetadata(app));
|
|
1391
1503
|
const namespace = appI18nNamespace(app);
|
|
1392
|
-
return
|
|
1504
|
+
return `// @generated by @modern-js/create.
|
|
1505
|
+
// Author route metadata in colocated src/routes/**/route.meta.ts files.
|
|
1506
|
+
// This compatibility manifest is regenerated from route-owned metadata.
|
|
1507
|
+
|
|
1508
|
+
export const ultramodernRouteNamespace = '${namespace}' as const;
|
|
1393
1509
|
|
|
1394
1510
|
export const ultramodernRouteMetadata = ${JSON.stringify(routes, null, 2)} as const;
|
|
1395
1511
|
|
|
@@ -1398,6 +1514,8 @@ export const ultramodernLocalisedUrls = ${JSON.stringify(localisedUrls, null, 2)
|
|
|
1398
1514
|
export const ultramodernPublicRoutes = ${JSON.stringify(publicRoutes, null, 2)} as const;
|
|
1399
1515
|
|
|
1400
1516
|
export const ultramodernRouteConfig = {
|
|
1517
|
+
authoring: 'colocated-route-meta',
|
|
1518
|
+
generatedManifest: true,
|
|
1401
1519
|
localisedUrls: ultramodernLocalisedUrls,
|
|
1402
1520
|
namespace: ultramodernRouteNamespace,
|
|
1403
1521
|
publicRoutes: ultramodernPublicRoutes,
|
|
@@ -1406,20 +1524,52 @@ export const ultramodernRouteConfig = {
|
|
|
1406
1524
|
} as const;
|
|
1407
1525
|
`;
|
|
1408
1526
|
}
|
|
1527
|
+
function createRouteMetaModule(route) {
|
|
1528
|
+
return `const routeMeta = ${JSON.stringify(sortJsonValue(route), null, 2)} as const;
|
|
1529
|
+
|
|
1530
|
+
export default routeMeta;
|
|
1531
|
+
export { routeMeta };
|
|
1532
|
+
`;
|
|
1533
|
+
}
|
|
1534
|
+
function normalisePublicPath(pathname) {
|
|
1535
|
+
const normalised = pathname.trim().replaceAll(/\/+/gu, '/').replace(/\/+$/u, '');
|
|
1536
|
+
return normalised.length > 0 && normalised.startsWith('/') ? normalised : `/${normalised}`;
|
|
1537
|
+
}
|
|
1538
|
+
function splitPublicPathSegments(pathname) {
|
|
1539
|
+
return normalisePublicPath(pathname).split('/').filter(Boolean);
|
|
1540
|
+
}
|
|
1541
|
+
function routePathParamName(segment) {
|
|
1542
|
+
if (segment.startsWith(':')) return segment.slice(1).replace(/[?*+]$/u, '');
|
|
1543
|
+
if (segment.startsWith('[') && segment.endsWith(']')) return segment.slice(1, -1).replace(/^\.\.\./u, '').replace(/\$$/u, '');
|
|
1544
|
+
}
|
|
1545
|
+
function isDynamicPublicPathSegment(segment) {
|
|
1546
|
+
return void 0 !== routePathParamName(segment) || segment.includes('*') || segment.startsWith('[');
|
|
1547
|
+
}
|
|
1548
|
+
function isConcretePublicPath(pathname) {
|
|
1549
|
+
return !splitPublicPathSegments(pathname).some(isDynamicPublicPathSegment);
|
|
1550
|
+
}
|
|
1409
1551
|
function routeSegmentToDirectory(segment) {
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
return segment.endsWith('?') ? `[${name}$]` : `[${name}]`;
|
|
1413
|
-
}
|
|
1552
|
+
const paramName = routePathParamName(segment);
|
|
1553
|
+
if (paramName && segment.startsWith(':')) return segment.endsWith('?') ? `[${paramName}$]` : `[${paramName}]`;
|
|
1414
1554
|
return segment;
|
|
1415
1555
|
}
|
|
1556
|
+
function routePathDirectorySegments(routePath) {
|
|
1557
|
+
return splitPublicPathSegments(routePath).map(routeSegmentToDirectory);
|
|
1558
|
+
}
|
|
1416
1559
|
function createRoutePageFilePath(app, canonicalPath) {
|
|
1417
|
-
const segments = canonicalPath
|
|
1560
|
+
const segments = routePathDirectorySegments(canonicalPath);
|
|
1418
1561
|
return `${app.directory}/src/routes/[lang]/${[
|
|
1419
1562
|
...segments,
|
|
1420
1563
|
'page.tsx'
|
|
1421
1564
|
].join('/')}`;
|
|
1422
1565
|
}
|
|
1566
|
+
function createRouteMetaFilePath(app, canonicalPath) {
|
|
1567
|
+
const segments = routePathDirectorySegments(canonicalPath);
|
|
1568
|
+
return `${app.directory}/src/routes/[lang]/${[
|
|
1569
|
+
...segments,
|
|
1570
|
+
'route.meta.ts'
|
|
1571
|
+
].join('/')}`;
|
|
1572
|
+
}
|
|
1423
1573
|
function createRouteAliasPage(canonicalPath) {
|
|
1424
1574
|
const depth = canonicalPath.split('/').filter(Boolean).length;
|
|
1425
1575
|
const rootPageImport = `${'../'.repeat(depth)}page`;
|
|
@@ -1577,24 +1727,22 @@ export default {} satisfies Config;
|
|
|
1577
1727
|
function createTw(prefix) {
|
|
1578
1728
|
return (classList)=>classList.split(/\s+/u).filter(Boolean).map((candidate)=>`${prefix}:${candidate.replace(/\[&&\]:/gu, '')}`).join(' ');
|
|
1579
1729
|
}
|
|
1580
|
-
const
|
|
1581
|
-
'config/public/robots.txt'
|
|
1582
|
-
];
|
|
1583
|
-
const publicSurfaceOptionalAssetPaths = [
|
|
1730
|
+
const publicSurfaceManagedSourceAssetPaths = [
|
|
1731
|
+
'config/public/robots.txt',
|
|
1584
1732
|
'config/public/sitemap.xml',
|
|
1585
1733
|
'config/public/site.webmanifest'
|
|
1586
1734
|
];
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
|
|
1735
|
+
const publicSurfaceBaseOutputFiles = [
|
|
1736
|
+
'robots.txt'
|
|
1737
|
+
];
|
|
1738
|
+
const publicSurfacePublicRouteOutputFiles = [
|
|
1739
|
+
'sitemap.xml',
|
|
1740
|
+
'site.webmanifest'
|
|
1741
|
+
];
|
|
1591
1742
|
function createLocalisedPublicPath(pathname, language) {
|
|
1592
1743
|
const publicPath = normalisePublicPath(pathname);
|
|
1593
1744
|
return '/' === publicPath ? `/${language}` : `/${language}${publicPath}`;
|
|
1594
1745
|
}
|
|
1595
|
-
function isConcretePublicPath(pathname) {
|
|
1596
|
-
return !normalisePublicPath(pathname).split('/').some((segment)=>segment.startsWith(':') || segment.includes('*') || segment.startsWith('['));
|
|
1597
|
-
}
|
|
1598
1746
|
function uniqueSorted(values) {
|
|
1599
1747
|
return Array.from(new Set(values)).sort((left, right)=>left.localeCompare(right));
|
|
1600
1748
|
}
|
|
@@ -1612,94 +1760,45 @@ function createPublicSurfaceRouteEntries(app) {
|
|
|
1612
1760
|
};
|
|
1613
1761
|
}).filter((route)=>void 0 !== route).sort((left, right)=>left.canonicalUrlPath.localeCompare(right.canonicalUrlPath) || left.id.localeCompare(right.id));
|
|
1614
1762
|
}
|
|
1763
|
+
function createPublicSurfaceContentSources(_app) {
|
|
1764
|
+
return [];
|
|
1765
|
+
}
|
|
1615
1766
|
function createPublicSurfaceUrlPaths(app) {
|
|
1616
1767
|
return uniqueSorted(createPublicSurfaceRouteEntries(app).flatMap((route)=>supportedWorkspaceLanguages.map((language)=>route.localeUrlPaths[language])));
|
|
1617
1768
|
}
|
|
1618
|
-
function
|
|
1619
|
-
return
|
|
1620
|
-
|
|
1621
|
-
|
|
1622
|
-
return value.replaceAll('&', '&').replaceAll('<', '<').replaceAll('>', '>');
|
|
1623
|
-
}
|
|
1624
|
-
function escapeXmlAttribute(value) {
|
|
1625
|
-
return escapeXmlText(value).replaceAll('"', '"');
|
|
1626
|
-
}
|
|
1627
|
-
function renderRobotsTxt(app) {
|
|
1628
|
-
const urlPaths = createPublicSurfaceUrlPaths(app);
|
|
1629
|
-
const lines = [
|
|
1630
|
-
'User-agent: *'
|
|
1631
|
-
];
|
|
1632
|
-
if (0 === urlPaths.length) lines.push('Disallow: /');
|
|
1633
|
-
else {
|
|
1634
|
-
for (const urlPath of urlPaths)lines.push(`Allow: ${urlPath}$`);
|
|
1635
|
-
lines.push('Disallow: /');
|
|
1636
|
-
lines.push(`Sitemap: ${createPublicSurfaceOrigin(app)}/sitemap.xml`);
|
|
1637
|
-
}
|
|
1638
|
-
return `${lines.join('\n')}\n`;
|
|
1639
|
-
}
|
|
1640
|
-
function renderSitemapXml(app) {
|
|
1641
|
-
const origin = createPublicSurfaceOrigin(app);
|
|
1642
|
-
const routes = createPublicSurfaceRouteEntries(app);
|
|
1643
|
-
const lines = [
|
|
1644
|
-
'<?xml version="1.0" encoding="UTF-8"?>',
|
|
1645
|
-
'<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9" xmlns:xhtml="http://www.w3.org/1999/xhtml">'
|
|
1769
|
+
function createPublicSurfaceOutputFiles(app) {
|
|
1770
|
+
return [
|
|
1771
|
+
...publicSurfaceBaseOutputFiles,
|
|
1772
|
+
...createPublicRouteMetadata(app).length > 0 ? publicSurfacePublicRouteOutputFiles : []
|
|
1646
1773
|
];
|
|
1647
|
-
for (const route of routes)for (const language of supportedWorkspaceLanguages){
|
|
1648
|
-
lines.push(' <url>');
|
|
1649
|
-
lines.push(` <loc>${escapeXmlText(`${origin}${route.localeUrlPaths[language]}`)}</loc>`);
|
|
1650
|
-
for (const alternateLanguage of supportedWorkspaceLanguages)lines.push(` <xhtml:link rel="alternate" hreflang="${alternateLanguage}" href="${escapeXmlAttribute(`${origin}${route.localeUrlPaths[alternateLanguage]}`)}" />`);
|
|
1651
|
-
lines.push(` <xhtml:link rel="alternate" hreflang="x-default" href="${escapeXmlAttribute(`${origin}${route.localeUrlPaths.en}`)}" />`);
|
|
1652
|
-
lines.push(' </url>');
|
|
1653
|
-
}
|
|
1654
|
-
lines.push('</urlset>');
|
|
1655
|
-
return `${lines.join('\n')}\n`;
|
|
1656
|
-
}
|
|
1657
|
-
function renderWebManifest(app) {
|
|
1658
|
-
const startUrl = createPublicSurfaceUrlPaths(app)[0];
|
|
1659
|
-
const manifest = {
|
|
1660
|
-
name: app.displayName,
|
|
1661
|
-
short_name: app.displayName,
|
|
1662
|
-
display: 'standalone',
|
|
1663
|
-
background_color: '#ffffff',
|
|
1664
|
-
theme_color: '#133225',
|
|
1665
|
-
lang: 'en',
|
|
1666
|
-
categories: [
|
|
1667
|
-
'business',
|
|
1668
|
-
'productivity'
|
|
1669
|
-
],
|
|
1670
|
-
icons: [],
|
|
1671
|
-
...startUrl ? {
|
|
1672
|
-
scope: '/',
|
|
1673
|
-
start_url: startUrl
|
|
1674
|
-
} : {}
|
|
1675
|
-
};
|
|
1676
|
-
return `${JSON.stringify(sortJsonValue(manifest), null, 2)}\n`;
|
|
1677
1774
|
}
|
|
1678
|
-
function
|
|
1679
|
-
|
|
1680
|
-
'config/public/robots.txt': renderRobotsTxt(app)
|
|
1681
|
-
};
|
|
1682
|
-
if (createPublicSurfaceRouteEntries(app).length > 0) {
|
|
1683
|
-
assets['config/public/sitemap.xml'] = renderSitemapXml(app);
|
|
1684
|
-
assets['config/public/site.webmanifest'] = renderWebManifest(app);
|
|
1685
|
-
}
|
|
1686
|
-
return assets;
|
|
1775
|
+
function createPublicSurfaceGenerationCommand(app, target, requirePublicOrigin = false) {
|
|
1776
|
+
return `node ${relativeRootFor(app.directory)}/scripts/generate-public-surface-assets.mjs --app ${app.id} --target ${target}${requirePublicOrigin ? ' --require-public-origin' : ''}`;
|
|
1687
1777
|
}
|
|
1688
1778
|
function workspaceAssetsForApp(app) {
|
|
1689
|
-
return
|
|
1779
|
+
return {};
|
|
1690
1780
|
}
|
|
1691
1781
|
function rewriteWorkspaceAssetsForApp(workspaceRoot, app) {
|
|
1782
|
+
for (const relativePath of publicSurfaceManagedSourceAssetPaths)node_fs.rmSync(node_path.join(workspaceRoot, app.directory, relativePath), {
|
|
1783
|
+
force: true
|
|
1784
|
+
});
|
|
1692
1785
|
for (const [relativePath, content] of Object.entries(workspaceAssetsForApp(app)))writeFileReplacing(workspaceRoot, `${app.directory}/${relativePath}`, content);
|
|
1693
1786
|
}
|
|
1694
|
-
function
|
|
1695
|
-
|
|
1787
|
+
function createRouteHeadModule(app) {
|
|
1788
|
+
const robotsPolicy = createPublicHeadRobotsPolicy();
|
|
1789
|
+
return `import { useLocalizedLocation, useModernI18n } from '@modern-js/plugin-i18n/runtime';
|
|
1790
|
+
import { Helmet } from '@modern-js/runtime/head';
|
|
1791
|
+
import {
|
|
1792
|
+
ultramodernRouteMetadata,
|
|
1793
|
+
} from './ultramodern-route-metadata';
|
|
1794
|
+
|
|
1795
|
+
const appName = ${JSON.stringify(app.displayName)};
|
|
1796
|
+
const fallbackLanguage = 'en';
|
|
1696
1797
|
const supportedLanguages = ['en', 'cs'] as const;
|
|
1697
1798
|
type SupportedLanguage = (typeof supportedLanguages)[number];
|
|
1799
|
+
type RouteMetadata = (typeof ultramodernRouteMetadata)[number];
|
|
1698
1800
|
|
|
1699
|
-
const
|
|
1700
|
-
string,
|
|
1701
|
-
Record<SupportedLanguage, string>
|
|
1702
|
-
>;
|
|
1801
|
+
const routeMetadata = ultramodernRouteMetadata as readonly RouteMetadata[];
|
|
1703
1802
|
|
|
1704
1803
|
const isSupportedLanguage = (value: string): value is SupportedLanguage =>
|
|
1705
1804
|
supportedLanguages.includes(value as SupportedLanguage);
|
|
@@ -1750,55 +1849,24 @@ const matchPattern = (pathname: string, pattern: string) => {
|
|
|
1750
1849
|
return params;
|
|
1751
1850
|
};
|
|
1752
1851
|
|
|
1753
|
-
const
|
|
1754
|
-
const path = normalisePath(pattern)
|
|
1755
|
-
.split('/')
|
|
1756
|
-
.filter(Boolean)
|
|
1757
|
-
.map(segment => {
|
|
1758
|
-
if (!segment.startsWith(':')) {
|
|
1759
|
-
return segment;
|
|
1760
|
-
}
|
|
1761
|
-
const value = params[paramName(segment)];
|
|
1762
|
-
return value !== undefined && value.length > 0
|
|
1763
|
-
? encodeURIComponent(value)
|
|
1764
|
-
: '';
|
|
1765
|
-
})
|
|
1766
|
-
.filter(Boolean)
|
|
1767
|
-
.join('/');
|
|
1768
|
-
|
|
1769
|
-
return \`/\${path}\`;
|
|
1770
|
-
};
|
|
1771
|
-
|
|
1772
|
-
const resolveLocalisedPath = (
|
|
1773
|
-
pathname: string,
|
|
1774
|
-
targetLanguage: SupportedLanguage,
|
|
1775
|
-
) => {
|
|
1852
|
+
const resolveRouteMetadata = (pathname: string) => {
|
|
1776
1853
|
const pathWithoutLanguage = stripLanguagePrefix(pathname);
|
|
1777
1854
|
|
|
1778
|
-
for (const
|
|
1779
|
-
const
|
|
1780
|
-
if (
|
|
1781
|
-
|
|
1855
|
+
for (const route of routeMetadata) {
|
|
1856
|
+
const canonicalParams = matchPattern(pathWithoutLanguage, route.canonicalPath);
|
|
1857
|
+
if (canonicalParams !== undefined) {
|
|
1858
|
+
return route;
|
|
1782
1859
|
}
|
|
1783
1860
|
|
|
1784
1861
|
for (const language of supportedLanguages) {
|
|
1785
|
-
const
|
|
1786
|
-
const params =
|
|
1787
|
-
sourcePattern === undefined
|
|
1788
|
-
? undefined
|
|
1789
|
-
: matchPattern(pathWithoutLanguage, sourcePattern);
|
|
1862
|
+
const params = matchPattern(pathWithoutLanguage, route.localisedPaths[language]);
|
|
1790
1863
|
if (params !== undefined) {
|
|
1791
|
-
return
|
|
1864
|
+
return route;
|
|
1792
1865
|
}
|
|
1793
1866
|
}
|
|
1794
1867
|
}
|
|
1795
1868
|
|
|
1796
|
-
return
|
|
1797
|
-
};
|
|
1798
|
-
|
|
1799
|
-
const localizedPath = (pathname: string, language: SupportedLanguage) => {
|
|
1800
|
-
const pathWithoutLanguage = resolveLocalisedPath(pathname, language);
|
|
1801
|
-
return pathWithoutLanguage === '/' ? \`/\${language}\` : \`/\${language}\${pathWithoutLanguage}\`;
|
|
1869
|
+
return routeMetadata[0];
|
|
1802
1870
|
};
|
|
1803
1871
|
|
|
1804
1872
|
const absoluteUrl = (pathname: string) => {
|
|
@@ -1806,26 +1874,68 @@ const absoluteUrl = (pathname: string) => {
|
|
|
1806
1874
|
return \`\${origin}\${pathname}\`;
|
|
1807
1875
|
};
|
|
1808
1876
|
|
|
1809
|
-
const
|
|
1810
|
-
|
|
1811
|
-
|
|
1877
|
+
const sanitiseJsonLd = (value: unknown) =>
|
|
1878
|
+
JSON.stringify(value).replaceAll('<', '\\\\u003c');
|
|
1879
|
+
|
|
1880
|
+
export const UltramodernRouteHead = () => {
|
|
1881
|
+
const { i18nInstance } = useModernI18n();
|
|
1882
|
+
const t = i18nInstance['t'].bind(i18nInstance);
|
|
1883
|
+
const { canonical, alternates } = useLocalizedLocation();
|
|
1884
|
+
const route = resolveRouteMetadata(canonical);
|
|
1885
|
+
const title = route ? t(route.titleKey) : appName;
|
|
1886
|
+
const description = route ? t(route.descriptionKey) : appName;
|
|
1887
|
+
const canonicalUrl = absoluteUrl(alternates[fallbackLanguage] ?? \`/\${fallbackLanguage}\`);
|
|
1888
|
+
const indexable = route?.public === true && route?.indexable === true;
|
|
1889
|
+
const jsonLd = indexable
|
|
1890
|
+
? {
|
|
1891
|
+
'@context': 'https://schema.org',
|
|
1892
|
+
'@type': 'WebPage',
|
|
1893
|
+
description,
|
|
1894
|
+
inLanguage: supportedLanguages.join(','),
|
|
1895
|
+
isPartOf: {
|
|
1896
|
+
'@type': 'WebSite',
|
|
1897
|
+
name: appName,
|
|
1898
|
+
url: absoluteUrl('/'),
|
|
1899
|
+
},
|
|
1900
|
+
name: title,
|
|
1901
|
+
url: canonicalUrl,
|
|
1902
|
+
}
|
|
1903
|
+
: undefined;
|
|
1812
1904
|
|
|
1813
1905
|
return (
|
|
1814
|
-
<Helmet>
|
|
1815
|
-
<
|
|
1816
|
-
{
|
|
1817
|
-
|
|
1818
|
-
|
|
1819
|
-
|
|
1820
|
-
|
|
1821
|
-
|
|
1822
|
-
|
|
1823
|
-
|
|
1824
|
-
|
|
1825
|
-
|
|
1826
|
-
|
|
1827
|
-
|
|
1828
|
-
|
|
1906
|
+
<Helmet htmlAttributes={{ lang: i18nInstance.language ?? fallbackLanguage }}>
|
|
1907
|
+
<title>{title}</title>
|
|
1908
|
+
<meta content={description} name="description" />
|
|
1909
|
+
<meta content={indexable ? '${robotsPolicy.indexableRobots}' : '${robotsPolicy.privateRouteRobots}'} name="robots" />
|
|
1910
|
+
{indexable && (
|
|
1911
|
+
<>
|
|
1912
|
+
<link rel="canonical" href={canonicalUrl} />
|
|
1913
|
+
{supportedLanguages.map(code => (
|
|
1914
|
+
<link
|
|
1915
|
+
href={absoluteUrl(alternates[code] ?? \`/\${code}\`)}
|
|
1916
|
+
hrefLang={code}
|
|
1917
|
+
key={code}
|
|
1918
|
+
rel="alternate"
|
|
1919
|
+
/>
|
|
1920
|
+
))}
|
|
1921
|
+
<link
|
|
1922
|
+
href={absoluteUrl(alternates[fallbackLanguage] ?? \`/\${fallbackLanguage}\`)}
|
|
1923
|
+
hrefLang="x-default"
|
|
1924
|
+
rel="alternate"
|
|
1925
|
+
/>
|
|
1926
|
+
<meta content={title} property="og:title" />
|
|
1927
|
+
<meta content={description} property="og:description" />
|
|
1928
|
+
<meta content={canonicalUrl} property="og:url" />
|
|
1929
|
+
<meta content="website" property="og:type" />
|
|
1930
|
+
<meta content={i18nInstance.language ?? fallbackLanguage} property="og:locale" />
|
|
1931
|
+
<meta content="summary_large_image" name="twitter:card" />
|
|
1932
|
+
<meta content={title} name="twitter:title" />
|
|
1933
|
+
<meta content={description} name="twitter:description" />
|
|
1934
|
+
{jsonLd && (
|
|
1935
|
+
<script type="application/ld+json">{sanitiseJsonLd(jsonLd)}</script>
|
|
1936
|
+
)}
|
|
1937
|
+
</>
|
|
1938
|
+
)}
|
|
1829
1939
|
</Helmet>
|
|
1830
1940
|
);
|
|
1831
1941
|
};
|
|
@@ -1834,31 +1944,28 @@ const LocalizedHead = () => {
|
|
|
1834
1944
|
function createShellPage(remotes = []) {
|
|
1835
1945
|
const tw = createTw(tailwindPrefixForApp(shellApp));
|
|
1836
1946
|
const remoteCount = String(remotes.length);
|
|
1837
|
-
return `import {
|
|
1838
|
-
import { Helmet } from '@modern-js/runtime/head';
|
|
1839
|
-
import { useLocation } from '@modern-js/plugin-tanstack/runtime';
|
|
1947
|
+
return `import { Link, useModernI18n } from '@modern-js/plugin-i18n/runtime';
|
|
1840
1948
|
import ShellFrame from '../shell-frame';
|
|
1949
|
+
import { UltramodernRouteHead } from '../ultramodern-route-head';
|
|
1841
1950
|
import { VerticalShowcase } from '../vertical-components';
|
|
1842
|
-
import { ultramodernLocalisedUrls } from '../ultramodern-route-metadata';
|
|
1843
1951
|
import { ultramodernUiMarker } from '../../ultramodern-build';
|
|
1844
1952
|
|
|
1845
|
-
${createLocalizedHeadComponent()}
|
|
1846
1953
|
export default function ShellHome() {
|
|
1847
1954
|
const { i18nInstance } = useModernI18n();
|
|
1848
1955
|
const t = i18nInstance['t'].bind(i18nInstance);
|
|
1849
1956
|
|
|
1850
1957
|
return (
|
|
1851
1958
|
<ShellFrame>
|
|
1852
|
-
<
|
|
1959
|
+
<UltramodernRouteHead />
|
|
1853
1960
|
<section className="${tw('mx-auto grid max-w-7xl items-center gap-8 py-8 md:grid-cols-[0.9fr_1.1fr] lg:gap-14')}">
|
|
1854
1961
|
<div className="${tw('min-w-0')}">
|
|
1855
1962
|
<p className="${tw('text-xs font-black uppercase tracking-[0.18em] text-emerald-800')}">{t('shell.hero.eyebrow')}</p>
|
|
1856
1963
|
<h1 className="${tw('mt-3 max-w-3xl text-5xl font-black leading-none tracking-normal text-stone-950 md:text-7xl')}">{t('shell.title')}</h1>
|
|
1857
1964
|
<p className="${tw('mt-5 max-w-2xl text-lg leading-8 text-stone-600')}">{t('shell.hero.lede')}</p>
|
|
1858
1965
|
<div className="${tw('mt-7 flex flex-wrap gap-3')}">
|
|
1859
|
-
<
|
|
1966
|
+
<Link className="${tw('inline-flex min-h-11 items-center justify-center rounded-full bg-emerald-800 px-5 font-bold text-white shadow-lg shadow-stone-900/10')}" to="/">
|
|
1860
1967
|
{t('shell.hero.primary')}
|
|
1861
|
-
</
|
|
1968
|
+
</Link>
|
|
1862
1969
|
<span className="${tw('inline-flex min-h-11 items-center justify-center rounded-full border border-stone-900/15 bg-white/90 px-5 font-bold text-stone-950 shadow-lg shadow-stone-900/10')}">
|
|
1863
1970
|
{t('shell.hero.secondary')}
|
|
1864
1971
|
</span>
|
|
@@ -1891,145 +1998,18 @@ export default function ShellHome() {
|
|
|
1891
1998
|
}
|
|
1892
1999
|
function createShellFrameComponent() {
|
|
1893
2000
|
const tw = createTw(tailwindPrefixForApp(shellApp));
|
|
1894
|
-
return `import { useModernI18n } from '@modern-js/plugin-i18n/runtime';
|
|
1895
|
-
import { useLocation } from '@modern-js/plugin-tanstack/runtime';
|
|
2001
|
+
return `import { useLocalizedLocation, useModernI18n } from '@modern-js/plugin-i18n/runtime';
|
|
1896
2002
|
import type { ReactNode } from 'react';
|
|
1897
2003
|
import { Header, StatusBadge } from './vertical-components';
|
|
1898
|
-
import { ultramodernLocalisedUrls } from './ultramodern-route-metadata';
|
|
1899
|
-
|
|
1900
|
-
const supportedLanguages = ['en', 'cs'] as const;
|
|
1901
|
-
type SupportedLanguage = (typeof supportedLanguages)[number];
|
|
1902
2004
|
|
|
1903
2005
|
interface ShellFrameProps {
|
|
1904
2006
|
children: ReactNode;
|
|
1905
2007
|
}
|
|
1906
2008
|
|
|
1907
|
-
const localisedUrls = ultramodernLocalisedUrls as Record<
|
|
1908
|
-
string,
|
|
1909
|
-
Record<SupportedLanguage, string>
|
|
1910
|
-
>;
|
|
1911
|
-
|
|
1912
|
-
const isSupportedLanguage = (value: string): value is SupportedLanguage =>
|
|
1913
|
-
supportedLanguages.includes(value as SupportedLanguage);
|
|
1914
|
-
|
|
1915
|
-
const normalisePath = (pathname: string) => {
|
|
1916
|
-
const normalised = pathname.replaceAll(/\\/+/gu, '/').replace(/\\/+$/u, '');
|
|
1917
|
-
return normalised.length > 0 ? normalised : '/';
|
|
1918
|
-
};
|
|
1919
|
-
|
|
1920
|
-
const stripLanguagePrefix = (pathname: string) => {
|
|
1921
|
-
const segments = normalisePath(pathname).split('/').filter(Boolean);
|
|
1922
|
-
if (segments.length > 0 && isSupportedLanguage(segments[0] ?? '')) {
|
|
1923
|
-
segments.shift();
|
|
1924
|
-
}
|
|
1925
|
-
return \`/\${segments.join('/')}\`;
|
|
1926
|
-
};
|
|
1927
|
-
|
|
1928
|
-
const escapeRegExp = (value: string) =>
|
|
1929
|
-
value.replaceAll(/[.*+?^\${}()|[\\]\\\\]/gu, '\\\\$&');
|
|
1930
|
-
|
|
1931
|
-
const paramName = (segment: string) => segment.slice(1).replace(/\\?$/u, '');
|
|
1932
|
-
|
|
1933
|
-
const matchPattern = (pathname: string, pattern: string) => {
|
|
1934
|
-
const names: string[] = [];
|
|
1935
|
-
const source = normalisePath(pattern)
|
|
1936
|
-
.split('/')
|
|
1937
|
-
.filter(Boolean)
|
|
1938
|
-
.map(segment => {
|
|
1939
|
-
if (segment.startsWith(':')) {
|
|
1940
|
-
names.push(paramName(segment));
|
|
1941
|
-
return segment.endsWith('?') ? '(?:/([^/]+))?' : '/([^/]+)';
|
|
1942
|
-
}
|
|
1943
|
-
return \`/\${escapeRegExp(segment)}\`;
|
|
1944
|
-
})
|
|
1945
|
-
.join('');
|
|
1946
|
-
const match = new RegExp(\`^\${source || '/'}$\`, 'u').exec(
|
|
1947
|
-
normalisePath(pathname),
|
|
1948
|
-
);
|
|
1949
|
-
|
|
1950
|
-
if (match === null) {
|
|
1951
|
-
return;
|
|
1952
|
-
}
|
|
1953
|
-
|
|
1954
|
-
const params: Record<string, string> = {};
|
|
1955
|
-
for (const [index, name] of names.entries()) {
|
|
1956
|
-
params[name] = decodeURIComponent(match[index + 1] ?? '');
|
|
1957
|
-
}
|
|
1958
|
-
return params;
|
|
1959
|
-
};
|
|
1960
|
-
|
|
1961
|
-
const buildPath = (pattern: string, params: Record<string, string>) => {
|
|
1962
|
-
const path = normalisePath(pattern)
|
|
1963
|
-
.split('/')
|
|
1964
|
-
.filter(Boolean)
|
|
1965
|
-
.map(segment => {
|
|
1966
|
-
if (!segment.startsWith(':')) {
|
|
1967
|
-
return segment;
|
|
1968
|
-
}
|
|
1969
|
-
const value = params[paramName(segment)];
|
|
1970
|
-
return value !== undefined && value.length > 0
|
|
1971
|
-
? encodeURIComponent(value)
|
|
1972
|
-
: '';
|
|
1973
|
-
})
|
|
1974
|
-
.filter(Boolean)
|
|
1975
|
-
.join('/');
|
|
1976
|
-
|
|
1977
|
-
return \`/\${path}\`;
|
|
1978
|
-
};
|
|
1979
|
-
|
|
1980
|
-
const resolveLocalisedPath = (
|
|
1981
|
-
pathname: string,
|
|
1982
|
-
targetLanguage: SupportedLanguage,
|
|
1983
|
-
) => {
|
|
1984
|
-
const pathWithoutLanguage = stripLanguagePrefix(pathname);
|
|
1985
|
-
|
|
1986
|
-
for (const entry of Object.values(localisedUrls)) {
|
|
1987
|
-
const targetPattern = entry[targetLanguage];
|
|
1988
|
-
if (targetPattern === undefined) {
|
|
1989
|
-
continue;
|
|
1990
|
-
}
|
|
1991
|
-
|
|
1992
|
-
for (const language of supportedLanguages) {
|
|
1993
|
-
const sourcePattern = entry[language];
|
|
1994
|
-
const params =
|
|
1995
|
-
sourcePattern === undefined
|
|
1996
|
-
? undefined
|
|
1997
|
-
: matchPattern(pathWithoutLanguage, sourcePattern);
|
|
1998
|
-
if (params !== undefined) {
|
|
1999
|
-
return buildPath(targetPattern, params);
|
|
2000
|
-
}
|
|
2001
|
-
}
|
|
2002
|
-
}
|
|
2003
|
-
|
|
2004
|
-
return pathWithoutLanguage;
|
|
2005
|
-
};
|
|
2006
|
-
|
|
2007
|
-
const localizedPath = (pathname: string, language: SupportedLanguage) => {
|
|
2008
|
-
const pathWithoutLanguage = resolveLocalisedPath(pathname, language);
|
|
2009
|
-
return pathWithoutLanguage === '/' ? \`/\${language}\` : \`/\${language}\${pathWithoutLanguage}\`;
|
|
2010
|
-
};
|
|
2011
|
-
|
|
2012
|
-
const locationSuffix = (location: {
|
|
2013
|
-
hash?: unknown;
|
|
2014
|
-
search?: unknown;
|
|
2015
|
-
searchStr?: unknown;
|
|
2016
|
-
}) => {
|
|
2017
|
-
let locationSearch = '';
|
|
2018
|
-
if (typeof location.searchStr === 'string') {
|
|
2019
|
-
locationSearch = location.searchStr;
|
|
2020
|
-
} else if (typeof location.search === 'string') {
|
|
2021
|
-
locationSearch = location.search;
|
|
2022
|
-
}
|
|
2023
|
-
const locationHash = typeof location.hash === 'string' ? location.hash : '';
|
|
2024
|
-
|
|
2025
|
-
return \`\${locationSearch}\${locationHash}\`;
|
|
2026
|
-
};
|
|
2027
|
-
|
|
2028
2009
|
export default function ShellFrame({ children }: ShellFrameProps) {
|
|
2029
2010
|
const { i18nInstance, language } = useModernI18n();
|
|
2030
2011
|
const t = i18nInstance['t'].bind(i18nInstance);
|
|
2031
|
-
const
|
|
2032
|
-
const suffix = locationSuffix(location);
|
|
2012
|
+
const { alternates } = useLocalizedLocation();
|
|
2033
2013
|
|
|
2034
2014
|
return (
|
|
2035
2015
|
<main className="${tw('min-h-screen bg-um-canvas px-4 py-5 text-um-foreground sm:px-6 lg:px-12')}">
|
|
@@ -2046,10 +2026,9 @@ export default function ShellFrame({ children }: ShellFrameProps) {
|
|
|
2046
2026
|
name="language"
|
|
2047
2027
|
onChange={event => {
|
|
2048
2028
|
const nextLanguage = event.currentTarget.value;
|
|
2049
|
-
|
|
2050
|
-
|
|
2051
|
-
|
|
2052
|
-
);
|
|
2029
|
+
const targetHref = alternates[nextLanguage];
|
|
2030
|
+
if (targetHref !== undefined) {
|
|
2031
|
+
window.location.assign(targetHref);
|
|
2053
2032
|
}
|
|
2054
2033
|
}}
|
|
2055
2034
|
value={language}
|
|
@@ -2140,7 +2119,7 @@ const createHydratedRemote =
|
|
|
2140
2119
|
return ` <${componentName} key="${remote.id}" />`;
|
|
2141
2120
|
}).join('\n');
|
|
2142
2121
|
const remoteCount = String(widgetRemotes.length);
|
|
2143
|
-
return `${federationImports}import {
|
|
2122
|
+
return `${federationImports}import { Link, useModernI18n } from '@modern-js/plugin-i18n/runtime';
|
|
2144
2123
|
|
|
2145
2124
|
const widgetCount = Number('${remoteCount}');
|
|
2146
2125
|
|
|
@@ -2153,7 +2132,7 @@ const createHydratedRemote =
|
|
|
2153
2132
|
|
|
2154
2133
|
return (
|
|
2155
2134
|
<header className="${tw('flex min-w-0 flex-wrap items-center gap-x-8 gap-y-2 md:flex-1')}" data-modern-boundary-id="${shellApp.mfName}" data-modern-mf-expose="shell/Header">
|
|
2156
|
-
<
|
|
2135
|
+
<Link className="${tw('whitespace-nowrap text-xl font-black tracking-normal text-stone-950 no-underline')}" to="/">{t('shell.title')}</Link>
|
|
2157
2136
|
</header>
|
|
2158
2137
|
);
|
|
2159
2138
|
};
|
|
@@ -2195,17 +2174,16 @@ function createRemotePage(app) {
|
|
|
2195
2174
|
const tw = createTw(tailwindPrefixForApp(app));
|
|
2196
2175
|
const listEffectItems = `list${toPascalCase(effectApiStem(app))}`;
|
|
2197
2176
|
const effectBffImport = appHasEffectApi(app) ? `import { useModernI18n } from '@modern-js/plugin-i18n/runtime';
|
|
2198
|
-
import {
|
|
2199
|
-
import { Link, useLocation } from '@modern-js/plugin-tanstack/runtime';
|
|
2177
|
+
import { Link } from '@modern-js/plugin-tanstack/runtime';
|
|
2200
2178
|
import { useEffect, useState } from 'react';
|
|
2201
2179
|
import {
|
|
2202
2180
|
Effect,
|
|
2203
2181
|
${listEffectItems},
|
|
2204
2182
|
runEffectRequest,
|
|
2205
2183
|
} from '../../effect/${effectApiStem(app)}-client';
|
|
2206
|
-
import {
|
|
2184
|
+
import { UltramodernRouteHead } from '../ultramodern-route-head';
|
|
2207
2185
|
import { ultramodernUiMarker } from '../../ultramodern-build';
|
|
2208
|
-
` : "import { useModernI18n } from '@modern-js/plugin-i18n/runtime';\nimport {
|
|
2186
|
+
` : "import { useModernI18n } from '@modern-js/plugin-i18n/runtime';\nimport { Link } from '@modern-js/plugin-tanstack/runtime';\nimport { UltramodernRouteHead } from '../ultramodern-route-head';\nimport { ultramodernUiMarker } from '../../ultramodern-build';\n";
|
|
2209
2187
|
const effectBffState = appHasEffectApi(app) ? ` const [effectApiStatus, setEffectApiStatus] = useState('pending');
|
|
2210
2188
|
|
|
2211
2189
|
useEffect(() => {
|
|
@@ -2238,13 +2216,12 @@ import { ultramodernUiMarker } from '../../ultramodern-build';
|
|
|
2238
2216
|
const effectBffMarkup = appHasEffectApi(app) ? ` <p data-testid="effect-bff-status">{effectApiStatus}</p>
|
|
2239
2217
|
` : '';
|
|
2240
2218
|
return `${effectBffImport}
|
|
2241
|
-
${createLocalizedHeadComponent()}
|
|
2242
2219
|
export default function ${toPascalCase(app.id)}Home() {
|
|
2243
2220
|
const { i18nInstance, language } = useModernI18n();
|
|
2244
2221
|
const t = i18nInstance['t'].bind(i18nInstance);
|
|
2245
2222
|
${effectBffState} return (
|
|
2246
2223
|
<main className="${tw('min-h-screen bg-um-canvas px-4 py-6 text-um-foreground sm:px-8')}">
|
|
2247
|
-
<
|
|
2224
|
+
<UltramodernRouteHead />
|
|
2248
2225
|
<nav aria-label={t('${app.domain}.language.switcher')} className="${tw('flex gap-3')}">
|
|
2249
2226
|
{supportedLanguages.map(code => (
|
|
2250
2227
|
<Link
|
|
@@ -2324,7 +2301,7 @@ export default function ${componentName}() {
|
|
|
2324
2301
|
}
|
|
2325
2302
|
function createRemoteExposeComponent(app, expose) {
|
|
2326
2303
|
const tw = createTw(tailwindPrefixForApp(app));
|
|
2327
|
-
if ('workspace' === app.id && './Header' === expose) return `import {
|
|
2304
|
+
if ('workspace' === app.id && './Header' === expose) return `import { Link, useModernI18n } from '@modern-js/plugin-i18n/runtime';
|
|
2328
2305
|
|
|
2329
2306
|
export default function Header() {
|
|
2330
2307
|
const { i18nInstance } = useModernI18n();
|
|
@@ -2332,16 +2309,16 @@ export default function Header() {
|
|
|
2332
2309
|
|
|
2333
2310
|
return (
|
|
2334
2311
|
<header className="${tw('flex min-w-0 flex-wrap items-center gap-x-8 gap-y-2 md:flex-1')}" data-modern-boundary-id="${app.mfName}" data-modern-mf-expose="${expose}">
|
|
2335
|
-
<
|
|
2312
|
+
<Link className="${tw('whitespace-nowrap text-xl font-black tracking-normal text-stone-950 no-underline')}" to="/">{t('workspace.header.brand')}</Link>
|
|
2336
2313
|
<nav aria-label={t('workspace.header.navigation')} className="${tw('flex items-center gap-5')}">
|
|
2337
|
-
<
|
|
2338
|
-
<
|
|
2314
|
+
<Link className="${tw('text-sm font-extrabold text-stone-900 no-underline')}" to="/workspaces">{t('workspace.header.workspaces')}</Link>
|
|
2315
|
+
<Link className="${tw('text-sm font-extrabold text-stone-900 no-underline')}" to="/directory">{t('workspace.header.directory')}</Link>
|
|
2339
2316
|
</nav>
|
|
2340
2317
|
</header>
|
|
2341
2318
|
);
|
|
2342
2319
|
}
|
|
2343
2320
|
`;
|
|
2344
|
-
if ('workspace' === app.id && './Highlights' === expose) return `import {
|
|
2321
|
+
if ('workspace' === app.id && './Highlights' === expose) return `import { Link, useModernI18n } from '@modern-js/plugin-i18n/runtime';
|
|
2345
2322
|
|
|
2346
2323
|
const highlights = [
|
|
2347
2324
|
{ badge: 'workspace.highlights.shell', href: '/workspaces', name: 'workspace.highlights.shellTitle' },
|
|
@@ -2358,10 +2335,10 @@ export default function Highlights() {
|
|
|
2358
2335
|
<h2 className="${tw('text-3xl font-black tracking-normal text-stone-950')}">{t('workspace.highlights.title')}</h2>
|
|
2359
2336
|
<div className="${tw('mt-5 grid gap-4 md:grid-cols-3')}">
|
|
2360
2337
|
{highlights.map(highlight => (
|
|
2361
|
-
<
|
|
2338
|
+
<Link className="${tw('block rounded-2xl bg-white/90 p-5 text-stone-950 no-underline shadow-xl shadow-stone-900/10 transition hover:-translate-y-0.5 hover:shadow-2xl')}" key={highlight.href} to={highlight.href}>
|
|
2362
2339
|
<span className="${tw('text-xs font-black uppercase tracking-[0.16em] text-emerald-800')}">{t(highlight.badge)}</span>
|
|
2363
2340
|
<strong className="${tw('mt-3 block text-xl font-black leading-tight')}">{t(highlight.name)}</strong>
|
|
2364
|
-
</
|
|
2341
|
+
</Link>
|
|
2365
2342
|
))}
|
|
2366
2343
|
</div>
|
|
2367
2344
|
</section>
|
|
@@ -2438,7 +2415,7 @@ export default function ${componentName}() {
|
|
|
2438
2415
|
);
|
|
2439
2416
|
}
|
|
2440
2417
|
`;
|
|
2441
|
-
if ('actions' === app.id && './StartAction' === expose) return `import {
|
|
2418
|
+
if ('actions' === app.id && './StartAction' === expose) return `import { Link, useModernI18n } from '@modern-js/plugin-i18n/runtime';
|
|
2442
2419
|
import { useActionQueue } from '../action-queue-store';
|
|
2443
2420
|
|
|
2444
2421
|
export default function ${componentName}() {
|
|
@@ -2451,9 +2428,9 @@ export default function ${componentName}() {
|
|
|
2451
2428
|
<button className="${tw('inline-flex min-h-11 items-center justify-center rounded-full bg-emerald-800 px-5 font-bold text-white shadow-lg shadow-stone-900/10')}" onClick={queue.addStarterAction} type="button">
|
|
2452
2429
|
{t('actions.controls.start')}
|
|
2453
2430
|
</button>
|
|
2454
|
-
<
|
|
2431
|
+
<Link className="${tw('inline-flex min-h-11 items-center justify-center rounded-full border border-stone-900/15 bg-white/90 px-5 font-bold text-stone-950 shadow-lg shadow-stone-900/10')}" to="/actions">
|
|
2455
2432
|
{t('actions.controls.viewQueue')}
|
|
2456
|
-
</
|
|
2433
|
+
</Link>
|
|
2457
2434
|
</div>
|
|
2458
2435
|
);
|
|
2459
2436
|
}
|
|
@@ -2618,6 +2595,9 @@ const commonLocaleMessages = {
|
|
|
2618
2595
|
review: 'Revize akce',
|
|
2619
2596
|
unavailable: 'Nedostupné',
|
|
2620
2597
|
workspaces: 'Pracovní prostory'
|
|
2598
|
+
},
|
|
2599
|
+
seo: {
|
|
2600
|
+
description: 'Route-owned UltraModern plocha s lokalizovaným SSR a frameworkem řízenými public metadata.'
|
|
2621
2601
|
}
|
|
2622
2602
|
},
|
|
2623
2603
|
en: {
|
|
@@ -2635,6 +2615,9 @@ const commonLocaleMessages = {
|
|
|
2635
2615
|
review: 'Action review',
|
|
2636
2616
|
unavailable: 'Unavailable',
|
|
2637
2617
|
workspaces: 'Workspaces'
|
|
2618
|
+
},
|
|
2619
|
+
seo: {
|
|
2620
|
+
description: 'Route-owned UltraModern surface with localized SSR and framework-owned public metadata.'
|
|
2638
2621
|
}
|
|
2639
2622
|
}
|
|
2640
2623
|
};
|
|
@@ -2713,6 +2696,9 @@ const generatedLocaleResources = {
|
|
|
2713
2696
|
routes: {
|
|
2714
2697
|
home: commonLocaleMessages.cs.routes.home
|
|
2715
2698
|
},
|
|
2699
|
+
seo: {
|
|
2700
|
+
description: 'UltraModern shell SuperApp s lokalizovaným SSR, Module Federation a frameworkem řízenými public metadata.'
|
|
2701
|
+
},
|
|
2716
2702
|
title: 'UltraModern Workspace'
|
|
2717
2703
|
},
|
|
2718
2704
|
workspace: {
|
|
@@ -2822,6 +2808,9 @@ const generatedLocaleResources = {
|
|
|
2822
2808
|
routes: {
|
|
2823
2809
|
home: commonLocaleMessages.en.routes.home
|
|
2824
2810
|
},
|
|
2811
|
+
seo: {
|
|
2812
|
+
description: 'UltraModern shell SuperApp with localized SSR, Module Federation, and framework-owned public metadata.'
|
|
2813
|
+
},
|
|
2825
2814
|
title: 'UltraModern Workspace'
|
|
2826
2815
|
},
|
|
2827
2816
|
workspace: {
|
|
@@ -3698,15 +3687,17 @@ function createAppConfigContract(app) {
|
|
|
3698
3687
|
'moduleFederationPlugin',
|
|
3699
3688
|
'zephyrRspackPlugin'
|
|
3700
3689
|
],
|
|
3690
|
+
dev: {
|
|
3691
|
+
assetPrefix: '/'
|
|
3692
|
+
},
|
|
3701
3693
|
output: {
|
|
3702
3694
|
assetPrefix: {
|
|
3703
3695
|
envFallbackOrder: [
|
|
3704
3696
|
createCloudflarePublicUrlEnv(app),
|
|
3705
3697
|
'MODERN_PUBLIC_SITE_URL',
|
|
3706
|
-
'ULTRAMODERN_CLOUDFLARE_WORKERS_DEV_SUBDOMAIN'
|
|
3707
|
-
app.portEnv
|
|
3698
|
+
'ULTRAMODERN_CLOUDFLARE_WORKERS_DEV_SUBDOMAIN'
|
|
3708
3699
|
],
|
|
3709
|
-
|
|
3700
|
+
default: '/'
|
|
3710
3701
|
},
|
|
3711
3702
|
disableTsChecker: true,
|
|
3712
3703
|
distPath: {
|
|
@@ -3732,6 +3723,15 @@ function createAppConfigContract(app) {
|
|
|
3732
3723
|
},
|
|
3733
3724
|
source: {
|
|
3734
3725
|
mainEntryName: 'index',
|
|
3726
|
+
siteUrl: {
|
|
3727
|
+
envFallbackOrder: [
|
|
3728
|
+
'MODERN_PUBLIC_SITE_URL',
|
|
3729
|
+
createCloudflarePublicUrlEnv(app),
|
|
3730
|
+
'ULTRAMODERN_CLOUDFLARE_WORKERS_DEV_SUBDOMAIN',
|
|
3731
|
+
app.portEnv
|
|
3732
|
+
],
|
|
3733
|
+
defaultLocalhostPort: app.port
|
|
3734
|
+
},
|
|
3735
3735
|
siteUrlGlobal: 'ULTRAMODERN_SITE_URL'
|
|
3736
3736
|
},
|
|
3737
3737
|
...appHasEffectApi(app) ? {
|
|
@@ -3893,11 +3893,17 @@ function createStylingContract(scope, app, enableTailwind) {
|
|
|
3893
3893
|
};
|
|
3894
3894
|
}
|
|
3895
3895
|
function createPublicSurfaceContract(app) {
|
|
3896
|
-
const files =
|
|
3896
|
+
const files = createPublicSurfaceOutputFiles(app);
|
|
3897
|
+
const contentExpansionPolicy = createPublicSurfaceContentExpansionPolicy();
|
|
3897
3898
|
return {
|
|
3899
|
+
authoring: 'colocated-route-meta',
|
|
3900
|
+
artifactLifecycle: 'build-and-deploy-output',
|
|
3901
|
+
generatedManifest: './src/routes/ultramodern-route-metadata',
|
|
3898
3902
|
source: 'route-owned-public-routes',
|
|
3899
3903
|
metadataExport: './src/routes/ultramodern-route-metadata',
|
|
3900
|
-
|
|
3904
|
+
generator: "scripts/generate-public-surface-assets.mjs",
|
|
3905
|
+
outputRoot: 'dist/public',
|
|
3906
|
+
cloudflareOutputRoot: '.output/public',
|
|
3901
3907
|
privateRoutePolicy: 'omit-from-generated-public-surface',
|
|
3902
3908
|
files,
|
|
3903
3909
|
omittedByDefault: [
|
|
@@ -3905,15 +3911,103 @@ function createPublicSurfaceContract(app) {
|
|
|
3905
3911
|
'llms.txt',
|
|
3906
3912
|
'security.txt'
|
|
3907
3913
|
],
|
|
3914
|
+
languages: [
|
|
3915
|
+
...supportedWorkspaceLanguages
|
|
3916
|
+
],
|
|
3917
|
+
contentExpansion: {
|
|
3918
|
+
authoring: 'route-owned-esm-provider',
|
|
3919
|
+
defaultProviderFile: contentExpansionPolicy.defaultProviderFile,
|
|
3920
|
+
entryExport: 'default-or-entries',
|
|
3921
|
+
paramsSource: 'params-or-localeParams',
|
|
3922
|
+
draftPolicy: contentExpansionPolicy.draftPolicy,
|
|
3923
|
+
indexablePolicy: contentExpansionPolicy.indexablePolicy,
|
|
3924
|
+
lifecycle: 'executed-during-public-surface-generation'
|
|
3925
|
+
},
|
|
3926
|
+
contentSources: createPublicSurfaceContentSources(app),
|
|
3908
3927
|
publicRoutes: createPublicRouteMetadata(app),
|
|
3928
|
+
routeEntries: createPublicSurfaceRouteEntries(app),
|
|
3909
3929
|
concreteUrlPaths: createPublicSurfaceUrlPaths(app)
|
|
3910
3930
|
};
|
|
3911
3931
|
}
|
|
3932
|
+
function createPublicHeadContract() {
|
|
3933
|
+
const robotsPolicy = createPublicHeadRobotsPolicy();
|
|
3934
|
+
return {
|
|
3935
|
+
authoring: 'colocated-route-meta',
|
|
3936
|
+
generator: './src/routes/ultramodern-route-head',
|
|
3937
|
+
renderer: '@modern-js/runtime/head Helmet',
|
|
3938
|
+
ssr: true,
|
|
3939
|
+
title: {
|
|
3940
|
+
required: true,
|
|
3941
|
+
source: 'route.titleKey'
|
|
3942
|
+
},
|
|
3943
|
+
description: {
|
|
3944
|
+
required: true,
|
|
3945
|
+
source: "route.descriptionKey"
|
|
3946
|
+
},
|
|
3947
|
+
canonical: {
|
|
3948
|
+
publicIndexableOnly: true,
|
|
3949
|
+
source: 'localized canonical route URL'
|
|
3950
|
+
},
|
|
3951
|
+
alternates: {
|
|
3952
|
+
hreflang: [
|
|
3953
|
+
...supportedWorkspaceLanguages
|
|
3954
|
+
],
|
|
3955
|
+
xDefault: 'en'
|
|
3956
|
+
},
|
|
3957
|
+
openGraph: {
|
|
3958
|
+
publicIndexableOnly: true,
|
|
3959
|
+
required: [
|
|
3960
|
+
'og:title',
|
|
3961
|
+
"og:description",
|
|
3962
|
+
'og:url',
|
|
3963
|
+
'og:type'
|
|
3964
|
+
]
|
|
3965
|
+
},
|
|
3966
|
+
twitter: {
|
|
3967
|
+
publicIndexableOnly: true,
|
|
3968
|
+
required: [
|
|
3969
|
+
'twitter:card',
|
|
3970
|
+
'twitter:title',
|
|
3971
|
+
"twitter:description"
|
|
3972
|
+
]
|
|
3973
|
+
},
|
|
3974
|
+
structuredData: {
|
|
3975
|
+
publicIndexableOnly: true,
|
|
3976
|
+
type: 'WebPage',
|
|
3977
|
+
sanitizesHtmlOpenBracket: true
|
|
3978
|
+
},
|
|
3979
|
+
privateRouteRobots: robotsPolicy.privateRouteRobots
|
|
3980
|
+
};
|
|
3981
|
+
}
|
|
3982
|
+
function createPublicWebAppArtifacts(app) {
|
|
3983
|
+
const routeMetadata = createRouteOwnedI18nPaths(app);
|
|
3984
|
+
return {
|
|
3985
|
+
routeMetadataFile: {
|
|
3986
|
+
path: `${app.directory}/src/routes/ultramodern-route-metadata.ts`,
|
|
3987
|
+
content: createRouteMetadataModule(app)
|
|
3988
|
+
},
|
|
3989
|
+
routeHeadFile: {
|
|
3990
|
+
path: `${app.directory}/src/routes/ultramodern-route-head.tsx`,
|
|
3991
|
+
content: createRouteHeadModule(app)
|
|
3992
|
+
},
|
|
3993
|
+
routeMetaFiles: routeMetadata.map((route)=>({
|
|
3994
|
+
path: createRouteMetaFilePath(app, route.canonicalPath),
|
|
3995
|
+
content: createRouteMetaModule(route)
|
|
3996
|
+
})),
|
|
3997
|
+
routeAliasFiles: routeMetadata.filter((route)=>'/' !== route.canonicalPath && 'shell' !== app.kind).map((route)=>({
|
|
3998
|
+
path: createRoutePageFilePath(app, route.canonicalPath),
|
|
3999
|
+
content: createRouteAliasPage(route.canonicalPath)
|
|
4000
|
+
})),
|
|
4001
|
+
publicHead: createPublicHeadContract(),
|
|
4002
|
+
publicSurface: createPublicSurfaceContract(app)
|
|
4003
|
+
};
|
|
4004
|
+
}
|
|
3912
4005
|
function createAppGeneratedContract(scope, app, apps, enableTailwind) {
|
|
3913
4006
|
const appWithResolvedRefs = 'shell' === app.kind ? {
|
|
3914
4007
|
...app,
|
|
3915
4008
|
verticalRefs: apps.filter((candidate)=>'shell' !== candidate.kind).map((candidate)=>candidate.id)
|
|
3916
4009
|
} : app;
|
|
4010
|
+
const publicWeb = createPublicWebAppArtifacts(app);
|
|
3917
4011
|
const consumedRemotes = createModuleFederationRemoteContracts(appWithResolvedRefs, apps);
|
|
3918
4012
|
return {
|
|
3919
4013
|
id: app.id,
|
|
@@ -3970,6 +4064,8 @@ function createAppGeneratedContract(scope, app, apps, enableTailwind) {
|
|
|
3970
4064
|
},
|
|
3971
4065
|
routes: {
|
|
3972
4066
|
source: 'route-owned',
|
|
4067
|
+
metadataAuthoring: 'colocated-route-meta',
|
|
4068
|
+
generatedManifest: true,
|
|
3973
4069
|
metadataExport: './src/routes/ultramodern-route-metadata',
|
|
3974
4070
|
localisedUrls: createLocalisedUrlsMap(app),
|
|
3975
4071
|
owned: createRouteOwnedI18nPaths(app),
|
|
@@ -3978,7 +4074,8 @@ function createAppGeneratedContract(scope, app, apps, enableTailwind) {
|
|
|
3978
4074
|
publicnessDefault: 'private-app-screen',
|
|
3979
4075
|
generatedRouteMap: true,
|
|
3980
4076
|
manualOverrides: [],
|
|
3981
|
-
|
|
4077
|
+
publicHead: publicWeb.publicHead,
|
|
4078
|
+
publicSurface: publicWeb.publicSurface
|
|
3982
4079
|
},
|
|
3983
4080
|
moduleFederation: {
|
|
3984
4081
|
name: app.mfName,
|
|
@@ -3989,7 +4086,7 @@ function createAppGeneratedContract(scope, app, apps, enableTailwind) {
|
|
|
3989
4086
|
exposes: Object.keys(app.exposes ?? {}),
|
|
3990
4087
|
dts: {
|
|
3991
4088
|
displayErrorInTerminal: true,
|
|
3992
|
-
compilerInstance:
|
|
4089
|
+
compilerInstance: 'tsgo'
|
|
3993
4090
|
},
|
|
3994
4091
|
browserSafeExposesOnly: true,
|
|
3995
4092
|
zephyrRspackPlugin: ZEPHYR_RSPACK_PLUGIN_VERSION
|
|
@@ -4214,7 +4311,7 @@ for (const appDir of appDirs) {
|
|
|
4214
4311
|
if (
|
|
4215
4312
|
contractEntry &&
|
|
4216
4313
|
contractEntry.moduleFederation?.dts?.compilerInstance !==
|
|
4217
|
-
'
|
|
4314
|
+
'tsgo'
|
|
4218
4315
|
) {
|
|
4219
4316
|
throw new Error(
|
|
4220
4317
|
\`Module Federation DTS must use the workspace TypeScript compiler: \${appDir}\`,
|
|
@@ -4253,53 +4350,588 @@ process.exitCode = runWorkspaceSourceCheck({
|
|
|
4253
4350
|
});
|
|
4254
4351
|
`;
|
|
4255
4352
|
}
|
|
4256
|
-
function
|
|
4257
|
-
const
|
|
4258
|
-
|
|
4259
|
-
domain: remote.domain,
|
|
4260
|
-
stem: remote.effectApi.stem,
|
|
4261
|
-
group: verticalEffectGroupName(remote),
|
|
4262
|
-
path: remote.directory,
|
|
4263
|
-
mfName: remote.mfName,
|
|
4264
|
-
apiPrefix: remote.effectApi.prefix,
|
|
4265
|
-
tailwindPrefix: tailwindPrefixForApp(remote),
|
|
4266
|
-
zephyrAlias: remoteDependencyAlias(remote),
|
|
4267
|
-
packageName: ultramodern_workspace_packageName(scope, remote.packageSuffix),
|
|
4268
|
-
exposes: Object.keys(remote.exposes ?? {}),
|
|
4269
|
-
componentPaths: Object.keys(remote.exposes ?? {}).map((expose)=>remoteComponentOutputPath(remote, expose)).filter((componentPath)=>Boolean(componentPath)),
|
|
4270
|
-
namespace: appI18nNamespace(remote),
|
|
4271
|
-
routePagePaths: createRouteOwnedI18nPaths(remote).filter((route)=>'/' !== route.canonicalPath).map((route)=>createRoutePageFilePath(remote, route.canonicalPath)),
|
|
4272
|
-
localisedUrls: createLocalisedUrlsMap(remote),
|
|
4273
|
-
verticalRefs: remote.verticalRefs ?? []
|
|
4274
|
-
}));
|
|
4275
|
-
const shellNamespace = appI18nNamespace(shellApp);
|
|
4276
|
-
const oldRemotePaths = [
|
|
4277
|
-
'apps/remotes'
|
|
4278
|
-
];
|
|
4279
|
-
const expectedBuildScript = remotes.length > 0 ? 'ULTRAMODERN_ZEPHYR=false pnpm -r --filter "./verticals/*" run build && ULTRAMODERN_ZEPHYR=false pnpm --filter "./apps/shell-super-app" run build && pnpm mf:types' : 'ULTRAMODERN_ZEPHYR=false pnpm --filter "./apps/shell-super-app" run build && pnpm mf:types';
|
|
4280
|
-
const expectedCloudflareBuildScript = remotes.length > 0 ? 'pnpm -r --filter "./verticals/*" run cloudflare:build && pnpm --filter "./apps/shell-super-app" run cloudflare:build && pnpm mf:types' : 'pnpm --filter "./apps/shell-super-app" run cloudflare:build && pnpm mf:types';
|
|
4281
|
-
const expectedCloudflareDeployScript = remotes.length > 0 ? 'pnpm -r --filter "./verticals/*" run cloudflare:deploy && pnpm --filter "./apps/shell-super-app" run cloudflare:deploy' : 'pnpm --filter "./apps/shell-super-app" run cloudflare:deploy';
|
|
4282
|
-
const expectedCloudflareSecurity = createCloudflareSecurityContract();
|
|
4283
|
-
return `import { execFileSync } from 'node:child_process';
|
|
4353
|
+
function createPublicSurfaceAssetsScript() {
|
|
4354
|
+
const contentExpansionPolicy = createPublicSurfaceContentExpansionPolicy();
|
|
4355
|
+
return `#!/usr/bin/env node
|
|
4284
4356
|
import fs from 'node:fs';
|
|
4285
4357
|
import path from 'node:path';
|
|
4358
|
+
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
4286
4359
|
|
|
4287
|
-
const
|
|
4288
|
-
|
|
4289
|
-
|
|
4290
|
-
|
|
4291
|
-
const
|
|
4292
|
-
|
|
4293
|
-
|
|
4360
|
+
const workspaceRoot = path.resolve(
|
|
4361
|
+
path.dirname(fileURLToPath(import.meta.url)),
|
|
4362
|
+
'..',
|
|
4363
|
+
);
|
|
4364
|
+
const contractPath = path.join(
|
|
4365
|
+
workspaceRoot,
|
|
4366
|
+
'.modernjs/ultramodern-generated-contract.json',
|
|
4367
|
+
);
|
|
4368
|
+
|
|
4369
|
+
function readJson(filePath) {
|
|
4370
|
+
return JSON.parse(fs.readFileSync(filePath, 'utf-8'));
|
|
4371
|
+
}
|
|
4372
|
+
|
|
4373
|
+
function parseArgs(argv) {
|
|
4374
|
+
const parsed = {
|
|
4375
|
+
appId: undefined,
|
|
4376
|
+
target: 'dist',
|
|
4377
|
+
requirePublicOrigin: false,
|
|
4378
|
+
};
|
|
4379
|
+
|
|
4380
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
4381
|
+
const arg = argv[index];
|
|
4382
|
+
if (arg === '--app') {
|
|
4383
|
+
parsed.appId = argv[index + 1];
|
|
4384
|
+
index += 1;
|
|
4385
|
+
} else if (arg === '--target') {
|
|
4386
|
+
parsed.target = argv[index + 1];
|
|
4387
|
+
index += 1;
|
|
4388
|
+
} else if (arg === '--require-public-origin') {
|
|
4389
|
+
parsed.requirePublicOrigin = true;
|
|
4390
|
+
} else if (arg === '--help' || arg === '-h') {
|
|
4391
|
+
parsed.help = true;
|
|
4392
|
+
} else {
|
|
4393
|
+
throw new Error(\`Unknown argument: \${arg}\`);
|
|
4394
|
+
}
|
|
4395
|
+
}
|
|
4396
|
+
|
|
4397
|
+
if (!parsed.appId && !parsed.help) {
|
|
4398
|
+
throw new Error('Missing required --app argument');
|
|
4399
|
+
}
|
|
4400
|
+
if (!['dist', 'cloudflare'].includes(parsed.target)) {
|
|
4401
|
+
throw new Error(\`Unsupported public surface target: \${parsed.target}\`);
|
|
4402
|
+
}
|
|
4403
|
+
|
|
4404
|
+
return parsed;
|
|
4405
|
+
}
|
|
4406
|
+
|
|
4407
|
+
function printHelp() {
|
|
4408
|
+
process.stdout.write(\`Usage:
|
|
4409
|
+
node scripts/generate-public-surface-assets.mjs --app shell-super-app [--target dist|cloudflare] [--require-public-origin]
|
|
4410
|
+
|
|
4411
|
+
Set each app's production URL using the contract env key, for example:
|
|
4412
|
+
ULTRAMODERN_PUBLIC_URL_SHELL_SUPER_APP=https://example.com
|
|
4413
|
+
|
|
4414
|
+
Dynamic public routes can opt into sitemap expansion by adding a route-owned
|
|
4415
|
+
${contentExpansionPolicy.defaultProviderFile} provider beside route metadata, or by adding an
|
|
4416
|
+
explicit provider to routes.publicSurface.contentSources. Providers should export
|
|
4417
|
+
an entries array, entries() function, or default entries/loader returning
|
|
4418
|
+
UltramodernPublicSitemapEntry[].
|
|
4419
|
+
\`);
|
|
4420
|
+
}
|
|
4421
|
+
|
|
4422
|
+
function normalizeOrigin(value) {
|
|
4423
|
+
if (typeof value !== 'string' || value.trim() === '') {
|
|
4424
|
+
return undefined;
|
|
4425
|
+
}
|
|
4426
|
+
const url = new URL(value);
|
|
4427
|
+
return url.origin;
|
|
4428
|
+
}
|
|
4429
|
+
|
|
4430
|
+
function resolveOrigin(app, requirePublicOrigin) {
|
|
4431
|
+
const cloudflare = app.deploy?.cloudflare ?? {};
|
|
4432
|
+
const publicUrlEnv = cloudflare.publicUrlEnv;
|
|
4433
|
+
const fromAppEnv =
|
|
4434
|
+
typeof publicUrlEnv === 'string' ? normalizeOrigin(process.env[publicUrlEnv]) : undefined;
|
|
4435
|
+
const fromGlobalEnv = normalizeOrigin(process.env.MODERN_PUBLIC_SITE_URL);
|
|
4436
|
+
const workersDevSubdomain = process.env.ULTRAMODERN_CLOUDFLARE_WORKERS_DEV_SUBDOMAIN;
|
|
4437
|
+
const fromWorkersDev =
|
|
4438
|
+
typeof workersDevSubdomain === 'string' && workersDevSubdomain.trim() !== ''
|
|
4439
|
+
? normalizeOrigin(\`https://\${cloudflare.workerName}.\${workersDevSubdomain}.workers.dev\`)
|
|
4440
|
+
: undefined;
|
|
4441
|
+
|
|
4442
|
+
// SEO output (sitemap <loc>, robots Sitemap:) uses the site-wide origin
|
|
4443
|
+
// first; the per-app deployment URL is only a fallback.
|
|
4444
|
+
const configuredOrigin = fromGlobalEnv ?? fromAppEnv ?? fromWorkersDev;
|
|
4445
|
+
if (configuredOrigin) {
|
|
4446
|
+
return configuredOrigin;
|
|
4447
|
+
}
|
|
4448
|
+
if (requirePublicOrigin) {
|
|
4449
|
+
throw new Error(
|
|
4450
|
+
\`\${app.id} has public routes but no production public URL. Set \${publicUrlEnv ?? 'ULTRAMODERN_PUBLIC_URL_<APP>'} or MODERN_PUBLIC_SITE_URL.\`,
|
|
4451
|
+
);
|
|
4452
|
+
}
|
|
4453
|
+
return undefined;
|
|
4454
|
+
}
|
|
4455
|
+
|
|
4456
|
+
function ensureOutputDir(app, target) {
|
|
4457
|
+
const relativeDir =
|
|
4458
|
+
target === 'cloudflare'
|
|
4459
|
+
? app.routes?.publicSurface?.cloudflareOutputRoot
|
|
4460
|
+
: app.routes?.publicSurface?.outputRoot;
|
|
4461
|
+
if (typeof relativeDir !== 'string') {
|
|
4462
|
+
throw new Error(\`\${app.id} public surface contract is missing outputRoot for \${target}\`);
|
|
4463
|
+
}
|
|
4464
|
+
const outputDir = path.resolve(workspaceRoot, app.path, relativeDir);
|
|
4465
|
+
const appRoot = path.resolve(workspaceRoot, app.path);
|
|
4466
|
+
if (!outputDir.startsWith(appRoot + path.sep)) {
|
|
4467
|
+
throw new Error(\`\${app.id} public surface output escaped the app directory\`);
|
|
4468
|
+
}
|
|
4469
|
+
fs.mkdirSync(outputDir, { recursive: true });
|
|
4470
|
+
return outputDir;
|
|
4471
|
+
}
|
|
4472
|
+
|
|
4473
|
+
function resolveAppRelativePath(app, relativePath) {
|
|
4474
|
+
if (
|
|
4475
|
+
typeof relativePath !== 'string' ||
|
|
4476
|
+
relativePath.trim() === '' ||
|
|
4477
|
+
path.isAbsolute(relativePath) ||
|
|
4478
|
+
relativePath.split(/[\\\\/]+/).includes('..')
|
|
4479
|
+
) {
|
|
4480
|
+
throw new Error(app.id + ' public content source has an unsafe module path');
|
|
4481
|
+
}
|
|
4482
|
+
const appRoot = path.resolve(workspaceRoot, app.path);
|
|
4483
|
+
const resolved = path.resolve(appRoot, relativePath);
|
|
4484
|
+
if (resolved !== appRoot && !resolved.startsWith(appRoot + path.sep)) {
|
|
4485
|
+
throw new Error(app.id + ' public content source escaped the app directory');
|
|
4486
|
+
}
|
|
4487
|
+
return resolved;
|
|
4488
|
+
}
|
|
4489
|
+
|
|
4490
|
+
function normalizePublicPath(pathname) {
|
|
4491
|
+
if (typeof pathname !== 'string') {
|
|
4492
|
+
throw new Error('Public route path must be a string');
|
|
4493
|
+
}
|
|
4494
|
+
const normalised = pathname
|
|
4495
|
+
.trim()
|
|
4496
|
+
.replaceAll(/\\/+/gu, '/')
|
|
4497
|
+
.replace(/\\/+$/u, '');
|
|
4498
|
+
return normalised.length > 0 && normalised.startsWith('/')
|
|
4499
|
+
? normalised
|
|
4500
|
+
: '/' + normalised;
|
|
4501
|
+
}
|
|
4502
|
+
|
|
4503
|
+
function createLocalisedPublicPath(pathname, language) {
|
|
4504
|
+
const publicPath = normalizePublicPath(pathname);
|
|
4505
|
+
return publicPath === '/' ? '/' + language : '/' + language + publicPath;
|
|
4506
|
+
}
|
|
4507
|
+
|
|
4508
|
+
function splitPublicPathSegments(pathname) {
|
|
4509
|
+
return normalizePublicPath(pathname).split('/').filter(Boolean);
|
|
4510
|
+
}
|
|
4511
|
+
|
|
4512
|
+
function routePathParamName(segment) {
|
|
4513
|
+
if (segment.startsWith(':')) {
|
|
4514
|
+
return segment.slice(1).replace(/[?*+]$/u, '');
|
|
4515
|
+
}
|
|
4516
|
+
if (segment.startsWith('[') && segment.endsWith(']')) {
|
|
4517
|
+
return segment.slice(1, -1).replace(/^\\.\\.\\./u, '').replace(/\\$$/u, '');
|
|
4518
|
+
}
|
|
4519
|
+
return undefined;
|
|
4520
|
+
}
|
|
4521
|
+
|
|
4522
|
+
function routeSegmentToDirectory(segment) {
|
|
4523
|
+
const paramName = routePathParamName(segment);
|
|
4524
|
+
if (paramName && segment.startsWith(':')) {
|
|
4525
|
+
return segment.endsWith('?') ? '[' + paramName + '$]' : '[' + paramName + ']';
|
|
4526
|
+
}
|
|
4527
|
+
return segment;
|
|
4528
|
+
}
|
|
4529
|
+
|
|
4530
|
+
function assertParamValue(routeId, language, paramName, value) {
|
|
4531
|
+
if (typeof value !== 'string' && typeof value !== 'number' && typeof value !== 'boolean') {
|
|
4532
|
+
throw new Error(routeId + ' ' + language + ' sitemap param ' + paramName + ' must be a string, number, or boolean');
|
|
4533
|
+
}
|
|
4534
|
+
const text = String(value).trim();
|
|
4535
|
+
if (text === '' || text.includes('/')) {
|
|
4536
|
+
throw new Error(routeId + ' ' + language + ' sitemap param ' + paramName + ' must be a non-empty path segment');
|
|
4537
|
+
}
|
|
4538
|
+
return encodeURIComponent(text);
|
|
4539
|
+
}
|
|
4540
|
+
|
|
4541
|
+
function expandPublicPathPattern(routeId, language, pattern, params) {
|
|
4542
|
+
const segments = splitPublicPathSegments(pattern);
|
|
4543
|
+
if (segments.length === 0) {
|
|
4544
|
+
return '/';
|
|
4545
|
+
}
|
|
4546
|
+
const expanded = segments.map(segment => {
|
|
4547
|
+
const paramName = routePathParamName(segment);
|
|
4548
|
+
if (!paramName) {
|
|
4549
|
+
if (segment.includes('*')) {
|
|
4550
|
+
throw new Error(routeId + ' ' + language + ' sitemap expansion does not support wildcard path segment ' + segment);
|
|
4551
|
+
}
|
|
4552
|
+
return segment;
|
|
4553
|
+
}
|
|
4554
|
+
if (!Object.prototype.hasOwnProperty.call(params, paramName)) {
|
|
4555
|
+
throw new Error(routeId + ' ' + language + ' sitemap entry is missing param ' + paramName);
|
|
4556
|
+
}
|
|
4557
|
+
return assertParamValue(routeId, language, paramName, params[paramName]);
|
|
4558
|
+
});
|
|
4559
|
+
return '/' + expanded.join('/');
|
|
4560
|
+
}
|
|
4561
|
+
|
|
4562
|
+
function assertPlainObject(value, label) {
|
|
4563
|
+
if (value === null || typeof value !== 'object' || Array.isArray(value)) {
|
|
4564
|
+
throw new Error(label + ' must be an object');
|
|
4565
|
+
}
|
|
4566
|
+
return value;
|
|
4567
|
+
}
|
|
4568
|
+
|
|
4569
|
+
function normalizeSitemapFields(routeId, entry) {
|
|
4570
|
+
const normalized = {};
|
|
4571
|
+
if (entry.lastModified !== undefined) {
|
|
4572
|
+
const lastModified = String(entry.lastModified).trim();
|
|
4573
|
+
if (lastModified === '' || Number.isNaN(Date.parse(lastModified))) {
|
|
4574
|
+
throw new Error(routeId + ' sitemap entry has invalid lastModified');
|
|
4575
|
+
}
|
|
4576
|
+
normalized.lastModified = lastModified;
|
|
4577
|
+
}
|
|
4578
|
+
if (entry.changeFrequency !== undefined) {
|
|
4579
|
+
const allowed = new Set(['always', 'hourly', 'daily', 'weekly', 'monthly', 'yearly', 'never']);
|
|
4580
|
+
if (!allowed.has(entry.changeFrequency)) {
|
|
4581
|
+
throw new Error(routeId + ' sitemap entry has invalid changeFrequency');
|
|
4582
|
+
}
|
|
4583
|
+
normalized.changeFrequency = entry.changeFrequency;
|
|
4584
|
+
}
|
|
4585
|
+
if (entry.priority !== undefined) {
|
|
4586
|
+
if (typeof entry.priority !== 'number' || entry.priority < 0 || entry.priority > 1) {
|
|
4587
|
+
throw new Error(routeId + ' sitemap entry priority must be a number between 0 and 1');
|
|
4588
|
+
}
|
|
4589
|
+
normalized.priority = entry.priority;
|
|
4590
|
+
}
|
|
4591
|
+
return normalized;
|
|
4592
|
+
}
|
|
4593
|
+
|
|
4594
|
+
function routePathToProviderDirectory(routePath) {
|
|
4595
|
+
const segments = splitPublicPathSegments(routePath);
|
|
4596
|
+
if (segments.length === 0) {
|
|
4597
|
+
return 'src/routes/[lang]';
|
|
4598
|
+
}
|
|
4599
|
+
return path.posix.join(
|
|
4600
|
+
'src/routes/[lang]',
|
|
4601
|
+
...segments.map(routeSegmentToDirectory),
|
|
4602
|
+
);
|
|
4603
|
+
}
|
|
4604
|
+
|
|
4605
|
+
function createDiscoveredContentSources(app, publicSurface) {
|
|
4606
|
+
const explicitRouteIds = new Set(
|
|
4607
|
+
(publicSurface.contentSources ?? []).map(source => source.routeId),
|
|
4608
|
+
);
|
|
4609
|
+
const discovered = [];
|
|
4610
|
+
for (const route of publicSurface.publicRoutes ?? []) {
|
|
4611
|
+
if (
|
|
4612
|
+
explicitRouteIds.has(route.id) ||
|
|
4613
|
+
!Object.values(route.localisedPaths ?? {}).some(routePath =>
|
|
4614
|
+
/(?:^|\\/):[^/]+|\\[[^\\]]+\\]/u.test(routePath),
|
|
4615
|
+
)
|
|
4616
|
+
) {
|
|
4617
|
+
continue;
|
|
4618
|
+
}
|
|
4619
|
+
const providerModule = path.posix.join(
|
|
4620
|
+
routePathToProviderDirectory(route.canonicalPath),
|
|
4621
|
+
'${contentExpansionPolicy.defaultProviderFile}',
|
|
4622
|
+
);
|
|
4623
|
+
if (fs.existsSync(resolveAppRelativePath(app, providerModule))) {
|
|
4624
|
+
discovered.push({
|
|
4625
|
+
entryExport: 'default-or-entries',
|
|
4626
|
+
module: providerModule,
|
|
4627
|
+
routeId: route.id,
|
|
4628
|
+
});
|
|
4629
|
+
}
|
|
4630
|
+
}
|
|
4631
|
+
return discovered;
|
|
4632
|
+
}
|
|
4633
|
+
|
|
4634
|
+
function resolveContentSources(app, publicSurface) {
|
|
4635
|
+
return [
|
|
4636
|
+
...(publicSurface.contentSources ?? []),
|
|
4637
|
+
...createDiscoveredContentSources(app, publicSurface),
|
|
4638
|
+
];
|
|
4639
|
+
}
|
|
4640
|
+
|
|
4641
|
+
async function loadContentSourceEntries(app, contentSource, languages) {
|
|
4642
|
+
if (typeof contentSource?.routeId !== 'string' || contentSource.routeId.trim() === '') {
|
|
4643
|
+
throw new Error(app.id + ' public content source is missing routeId');
|
|
4644
|
+
}
|
|
4645
|
+
const modulePath = resolveAppRelativePath(app, contentSource.module);
|
|
4646
|
+
const moduleExports = await import(pathToFileURL(modulePath).href);
|
|
4647
|
+
const exported = moduleExports.default ?? moduleExports.entries;
|
|
4648
|
+
const rawEntries =
|
|
4649
|
+
typeof exported === 'function'
|
|
4650
|
+
? await exported({
|
|
4651
|
+
appId: app.id,
|
|
4652
|
+
languages,
|
|
4653
|
+
routeId: contentSource.routeId,
|
|
4654
|
+
})
|
|
4655
|
+
: exported;
|
|
4656
|
+
if (!Array.isArray(rawEntries)) {
|
|
4657
|
+
throw new Error(app.id + ' public content source for ' + contentSource.routeId + ' must export an entries array or loader');
|
|
4658
|
+
}
|
|
4659
|
+
return rawEntries;
|
|
4660
|
+
}
|
|
4661
|
+
|
|
4662
|
+
async function expandContentSources(app, publicSurface, languages) {
|
|
4663
|
+
const routesById = new Map(
|
|
4664
|
+
(publicSurface.publicRoutes ?? []).map(route => [route.id, route]),
|
|
4665
|
+
);
|
|
4666
|
+
const expanded = [];
|
|
4667
|
+
for (const contentSource of resolveContentSources(app, publicSurface)) {
|
|
4668
|
+
const route = routesById.get(contentSource.routeId);
|
|
4669
|
+
if (!route) {
|
|
4670
|
+
throw new Error(app.id + ' public content source references unknown route ' + contentSource.routeId);
|
|
4671
|
+
}
|
|
4672
|
+
const rawEntries = await loadContentSourceEntries(app, contentSource, languages);
|
|
4673
|
+
for (const rawEntry of rawEntries) {
|
|
4674
|
+
const entry = assertPlainObject(rawEntry, route.id + ' sitemap entry');
|
|
4675
|
+
if (entry.draft === true || entry.indexable === false) {
|
|
4676
|
+
continue;
|
|
4677
|
+
}
|
|
4678
|
+
const baseParams = assertPlainObject(entry.params, route.id + ' sitemap entry params');
|
|
4679
|
+
const localeParams = entry.localeParams === undefined
|
|
4680
|
+
? {}
|
|
4681
|
+
: assertPlainObject(entry.localeParams, route.id + ' sitemap entry localeParams');
|
|
4682
|
+
const localeUrlPaths = {};
|
|
4683
|
+
for (const language of languages) {
|
|
4684
|
+
const params = {
|
|
4685
|
+
...baseParams,
|
|
4686
|
+
...(localeParams[language] ?? {}),
|
|
4687
|
+
};
|
|
4688
|
+
localeUrlPaths[language] = createLocalisedPublicPath(
|
|
4689
|
+
expandPublicPathPattern(route.id, language, route.localisedPaths[language], params),
|
|
4690
|
+
language,
|
|
4691
|
+
);
|
|
4692
|
+
}
|
|
4693
|
+
expanded.push({
|
|
4694
|
+
...route,
|
|
4695
|
+
...normalizeSitemapFields(route.id, entry),
|
|
4696
|
+
canonicalUrlPath: localeUrlPaths.en,
|
|
4697
|
+
localeUrlPaths,
|
|
4698
|
+
});
|
|
4699
|
+
}
|
|
4700
|
+
}
|
|
4701
|
+
return expanded;
|
|
4702
|
+
}
|
|
4703
|
+
|
|
4704
|
+
function mergeRouteEntries(routeEntries, expandedRouteEntries, languages) {
|
|
4705
|
+
const byKey = new Map();
|
|
4706
|
+
const urlPathOwners = new Map();
|
|
4707
|
+
for (const route of [...routeEntries, ...expandedRouteEntries]) {
|
|
4708
|
+
const key = route.id + ':' + route.canonicalUrlPath;
|
|
4709
|
+
if (byKey.has(key)) {
|
|
4710
|
+
throw new Error('Duplicate public sitemap route entry ' + key);
|
|
4711
|
+
}
|
|
4712
|
+
for (const language of languages) {
|
|
4713
|
+
const urlPath = route.localeUrlPaths?.[language];
|
|
4714
|
+
if (typeof urlPath !== 'string') {
|
|
4715
|
+
throw new Error(route.id + ' public route entry is missing ' + language + ' locale URL path');
|
|
4716
|
+
}
|
|
4717
|
+
const existingOwner = urlPathOwners.get(urlPath);
|
|
4718
|
+
if (existingOwner && existingOwner !== route.id) {
|
|
4719
|
+
throw new Error('Duplicate public sitemap URL path ' + urlPath + ' from ' + existingOwner + ' and ' + route.id);
|
|
4720
|
+
}
|
|
4721
|
+
urlPathOwners.set(urlPath, route.id);
|
|
4722
|
+
}
|
|
4723
|
+
byKey.set(key, route);
|
|
4724
|
+
}
|
|
4725
|
+
return Array.from(byKey.values()).sort(
|
|
4726
|
+
(left, right) =>
|
|
4727
|
+
left.canonicalUrlPath.localeCompare(right.canonicalUrlPath) ||
|
|
4728
|
+
left.id.localeCompare(right.id),
|
|
4729
|
+
);
|
|
4730
|
+
}
|
|
4731
|
+
|
|
4732
|
+
function uniqueSorted(values) {
|
|
4733
|
+
return Array.from(new Set(values)).sort((left, right) =>
|
|
4734
|
+
left.localeCompare(right),
|
|
4735
|
+
);
|
|
4736
|
+
}
|
|
4737
|
+
|
|
4738
|
+
function createConcreteUrlPaths(routeEntries, languages) {
|
|
4739
|
+
return uniqueSorted(
|
|
4740
|
+
routeEntries.flatMap(route => languages.map(language => route.localeUrlPaths[language])),
|
|
4741
|
+
);
|
|
4742
|
+
}
|
|
4743
|
+
|
|
4744
|
+
function escapeXmlText(value) {
|
|
4745
|
+
return value
|
|
4746
|
+
.replaceAll('&', '&')
|
|
4747
|
+
.replaceAll('<', '<')
|
|
4748
|
+
.replaceAll('>', '>');
|
|
4749
|
+
}
|
|
4750
|
+
|
|
4751
|
+
function escapeXmlAttribute(value) {
|
|
4752
|
+
return escapeXmlText(value).replaceAll('"', '"');
|
|
4753
|
+
}
|
|
4754
|
+
|
|
4755
|
+
function renderRobotsTxt(urlPaths, sitemapUrl) {
|
|
4756
|
+
const lines = ['User-agent: *'];
|
|
4757
|
+
if (urlPaths.length === 0) {
|
|
4758
|
+
lines.push('Disallow: /');
|
|
4759
|
+
} else {
|
|
4760
|
+
for (const urlPath of urlPaths) {
|
|
4761
|
+
lines.push(\`Allow: \${urlPath}$\`);
|
|
4762
|
+
}
|
|
4763
|
+
lines.push('Disallow: /');
|
|
4764
|
+
if (sitemapUrl) {
|
|
4765
|
+
lines.push(\`Sitemap: \${sitemapUrl}\`);
|
|
4766
|
+
}
|
|
4767
|
+
}
|
|
4768
|
+
return \`\${lines.join('\\n')}\\n\`;
|
|
4769
|
+
}
|
|
4770
|
+
|
|
4771
|
+
function renderSitemapXml(origin, routeEntries, languages) {
|
|
4772
|
+
const lines = [
|
|
4773
|
+
'<?xml version="1.0" encoding="UTF-8"?>',
|
|
4774
|
+
'<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9" xmlns:xhtml="http://www.w3.org/1999/xhtml">',
|
|
4775
|
+
];
|
|
4776
|
+
|
|
4777
|
+
for (const route of routeEntries) {
|
|
4778
|
+
for (const language of languages) {
|
|
4779
|
+
lines.push(' <url>');
|
|
4780
|
+
lines.push(\` <loc>\${escapeXmlText(\`\${origin}\${route.localeUrlPaths[language]}\`)}</loc>\`);
|
|
4781
|
+
for (const alternateLanguage of languages) {
|
|
4782
|
+
lines.push(
|
|
4783
|
+
\` <xhtml:link rel="alternate" hreflang="\${alternateLanguage}" href="\${escapeXmlAttribute(
|
|
4784
|
+
\`\${origin}\${route.localeUrlPaths[alternateLanguage]}\`,
|
|
4785
|
+
)}" />\`,
|
|
4786
|
+
);
|
|
4787
|
+
}
|
|
4788
|
+
lines.push(
|
|
4789
|
+
\` <xhtml:link rel="alternate" hreflang="x-default" href="\${escapeXmlAttribute(
|
|
4790
|
+
\`\${origin}\${route.localeUrlPaths.en}\`,
|
|
4791
|
+
)}" />\`,
|
|
4792
|
+
);
|
|
4793
|
+
if (route.lastModified) {
|
|
4794
|
+
lines.push(\` <lastmod>\${escapeXmlText(route.lastModified)}</lastmod>\`);
|
|
4795
|
+
}
|
|
4796
|
+
if (route.changeFrequency) {
|
|
4797
|
+
lines.push(\` <changefreq>\${escapeXmlText(route.changeFrequency)}</changefreq>\`);
|
|
4798
|
+
}
|
|
4799
|
+
if (route.priority !== undefined) {
|
|
4800
|
+
lines.push(\` <priority>\${route.priority.toFixed(1).replace(/\\.0$/u, '')}</priority>\`);
|
|
4801
|
+
}
|
|
4802
|
+
lines.push(' </url>');
|
|
4803
|
+
}
|
|
4804
|
+
}
|
|
4805
|
+
|
|
4806
|
+
lines.push('</urlset>');
|
|
4807
|
+
return \`\${lines.join('\\n')}\\n\`;
|
|
4808
|
+
}
|
|
4809
|
+
|
|
4810
|
+
function renderWebManifest(app, urlPaths) {
|
|
4811
|
+
const startUrl = urlPaths[0];
|
|
4812
|
+
const manifest = {
|
|
4813
|
+
background_color: '#ffffff',
|
|
4814
|
+
categories: ['business', 'productivity'],
|
|
4815
|
+
display: 'standalone',
|
|
4816
|
+
icons: [],
|
|
4817
|
+
lang: 'en',
|
|
4818
|
+
name: app.marker?.appId ?? app.id,
|
|
4819
|
+
short_name: app.marker?.appId ?? app.id,
|
|
4820
|
+
theme_color: '#133225',
|
|
4821
|
+
...(startUrl ? { scope: '/', start_url: startUrl } : {}),
|
|
4822
|
+
};
|
|
4823
|
+
return \`\${JSON.stringify(manifest, null, 2)}\\n\`;
|
|
4824
|
+
}
|
|
4825
|
+
|
|
4826
|
+
function removeIfExists(outputDir, fileName) {
|
|
4827
|
+
fs.rmSync(path.join(outputDir, fileName), { force: true });
|
|
4828
|
+
}
|
|
4829
|
+
|
|
4830
|
+
function writeText(outputDir, fileName, content) {
|
|
4831
|
+
fs.writeFileSync(path.join(outputDir, fileName), content);
|
|
4832
|
+
}
|
|
4833
|
+
|
|
4834
|
+
async function generatePublicSurfaceAssets(app, target, requirePublicOrigin) {
|
|
4835
|
+
const publicSurface = app.routes?.publicSurface ?? {};
|
|
4836
|
+
const languages = publicSurface.languages ?? ['en', 'cs'];
|
|
4837
|
+
const outputDir = ensureOutputDir(app, target);
|
|
4838
|
+
const shouldRequirePublicOrigin =
|
|
4839
|
+
requirePublicOrigin ||
|
|
4840
|
+
process.env.ULTRAMODERN_CLOUDFLARE_REQUIRE_PUBLIC_URLS === 'true';
|
|
4841
|
+
const routeEntries = mergeRouteEntries(
|
|
4842
|
+
publicSurface.routeEntries ?? [],
|
|
4843
|
+
await expandContentSources(app, publicSurface, languages),
|
|
4844
|
+
languages,
|
|
4845
|
+
);
|
|
4846
|
+
const urlPaths = createConcreteUrlPaths(routeEntries, languages);
|
|
4847
|
+
|
|
4848
|
+
if (routeEntries.length === 0) {
|
|
4849
|
+
writeText(outputDir, 'robots.txt', renderRobotsTxt([], undefined));
|
|
4850
|
+
removeIfExists(outputDir, 'sitemap.xml');
|
|
4851
|
+
removeIfExists(outputDir, 'site.webmanifest');
|
|
4852
|
+
return;
|
|
4853
|
+
}
|
|
4854
|
+
|
|
4855
|
+
const origin = resolveOrigin(app, shouldRequirePublicOrigin);
|
|
4856
|
+
if (!origin) {
|
|
4857
|
+
writeText(outputDir, 'robots.txt', renderRobotsTxt([], undefined));
|
|
4858
|
+
removeIfExists(outputDir, 'sitemap.xml');
|
|
4859
|
+
removeIfExists(outputDir, 'site.webmanifest');
|
|
4860
|
+
return;
|
|
4861
|
+
}
|
|
4862
|
+
|
|
4863
|
+
writeText(outputDir, 'sitemap.xml', renderSitemapXml(origin, routeEntries, languages));
|
|
4864
|
+
writeText(outputDir, 'site.webmanifest', renderWebManifest(app, urlPaths));
|
|
4865
|
+
writeText(outputDir, 'robots.txt', renderRobotsTxt(urlPaths, \`\${origin}/sitemap.xml\`));
|
|
4866
|
+
}
|
|
4867
|
+
|
|
4868
|
+
try {
|
|
4869
|
+
const args = parseArgs(process.argv.slice(2));
|
|
4870
|
+
if (args.help) {
|
|
4871
|
+
printHelp();
|
|
4872
|
+
process.exit(0);
|
|
4873
|
+
}
|
|
4874
|
+
const contract = readJson(contractPath);
|
|
4875
|
+
const app = contract.apps?.find(candidate => candidate.id === args.appId);
|
|
4876
|
+
if (!app) {
|
|
4877
|
+
throw new Error(\`Unknown app in generated contract: \${args.appId}\`);
|
|
4878
|
+
}
|
|
4879
|
+
await generatePublicSurfaceAssets(app, args.target, args.requirePublicOrigin);
|
|
4880
|
+
} catch (error) {
|
|
4881
|
+
process.stderr.write(\`[public-surface] \${error.message}\\n\`);
|
|
4882
|
+
process.exitCode = 1;
|
|
4883
|
+
}
|
|
4884
|
+
`;
|
|
4885
|
+
}
|
|
4886
|
+
function createWorkspaceValidationScript(scope, enableTailwind, remotes = []) {
|
|
4887
|
+
const verticals = remotes.filter(appHasEffectApi).map((remote)=>({
|
|
4888
|
+
id: remote.id,
|
|
4889
|
+
domain: remote.domain,
|
|
4890
|
+
stem: remote.effectApi.stem,
|
|
4891
|
+
group: verticalEffectGroupName(remote),
|
|
4892
|
+
path: remote.directory,
|
|
4893
|
+
mfName: remote.mfName,
|
|
4894
|
+
apiPrefix: remote.effectApi.prefix,
|
|
4895
|
+
tailwindPrefix: tailwindPrefixForApp(remote),
|
|
4896
|
+
zephyrAlias: remoteDependencyAlias(remote),
|
|
4897
|
+
packageName: ultramodern_workspace_packageName(scope, remote.packageSuffix),
|
|
4898
|
+
exposes: Object.keys(remote.exposes ?? {}),
|
|
4899
|
+
componentPaths: Object.keys(remote.exposes ?? {}).map((expose)=>remoteComponentOutputPath(remote, expose)).filter((componentPath)=>Boolean(componentPath)),
|
|
4900
|
+
namespace: appI18nNamespace(remote),
|
|
4901
|
+
routePagePaths: createRouteOwnedI18nPaths(remote).filter((route)=>'/' !== route.canonicalPath).map((route)=>createRoutePageFilePath(remote, route.canonicalPath)),
|
|
4902
|
+
routeMetaPaths: createRouteOwnedI18nPaths(remote).map((route)=>createRouteMetaFilePath(remote, route.canonicalPath)),
|
|
4903
|
+
localisedUrls: createLocalisedUrlsMap(remote),
|
|
4904
|
+
verticalRefs: remote.verticalRefs ?? []
|
|
4905
|
+
}));
|
|
4906
|
+
const shellRouteMetaPaths = createRouteOwnedI18nPaths(shellApp).map((route)=>createRouteMetaFilePath(shellApp, route.canonicalPath));
|
|
4907
|
+
const shellNamespace = appI18nNamespace(shellApp);
|
|
4908
|
+
const oldRemotePaths = [
|
|
4909
|
+
'apps/remotes'
|
|
4910
|
+
];
|
|
4911
|
+
const expectedBuildScript = remotes.length > 0 ? 'ULTRAMODERN_ZEPHYR=false pnpm -r --filter "./verticals/*" run build && ULTRAMODERN_ZEPHYR=false pnpm --filter "./apps/shell-super-app" run build && pnpm mf:types' : 'ULTRAMODERN_ZEPHYR=false pnpm --filter "./apps/shell-super-app" run build && pnpm mf:types';
|
|
4912
|
+
const expectedCloudflareBuildScript = remotes.length > 0 ? 'pnpm -r --filter "./verticals/*" run cloudflare:build && pnpm --filter "./apps/shell-super-app" run cloudflare:build && pnpm mf:types' : 'pnpm --filter "./apps/shell-super-app" run cloudflare:build && pnpm mf:types';
|
|
4913
|
+
const expectedCloudflareDeployScript = remotes.length > 0 ? 'pnpm -r --filter "./verticals/*" run cloudflare:deploy && pnpm --filter "./apps/shell-super-app" run cloudflare:deploy' : 'pnpm --filter "./apps/shell-super-app" run cloudflare:deploy';
|
|
4914
|
+
const expectedCloudflareSecurity = createCloudflareSecurityContract();
|
|
4915
|
+
const contentExpansionPolicy = createPublicSurfaceContentExpansionPolicy();
|
|
4916
|
+
const robotsPolicy = createPublicHeadRobotsPolicy();
|
|
4917
|
+
const qualityGates = createPublicWebsiteQualityGateContract();
|
|
4918
|
+
return `import { execFileSync } from 'node:child_process';
|
|
4919
|
+
import fs from 'node:fs';
|
|
4920
|
+
import path from 'node:path';
|
|
4921
|
+
|
|
4922
|
+
const root = process.cwd();
|
|
4923
|
+
const packageScope = '${scope}';
|
|
4924
|
+
const expectedPnpmVersion = '${PNPM_VERSION}';
|
|
4925
|
+
const tailwindEnabled = ${JSON.stringify(enableTailwind)};
|
|
4926
|
+
const fullStackVerticals = ${JSON.stringify(verticals, null, 2)};
|
|
4927
|
+
const shellNamespace = ${JSON.stringify(shellNamespace)};
|
|
4928
|
+
const oldRemotePaths = ${JSON.stringify(oldRemotePaths, null, 2)};
|
|
4294
4929
|
const expectedBuildScript = ${JSON.stringify(expectedBuildScript)};
|
|
4295
4930
|
const expectedCloudflareBuildScript = ${JSON.stringify(expectedCloudflareBuildScript)};
|
|
4296
4931
|
const expectedCloudflareDeployScript = ${JSON.stringify(expectedCloudflareDeployScript)};
|
|
4297
4932
|
const expectedCloudflareSecurity = ${JSON.stringify(expectedCloudflareSecurity, null, 2)};
|
|
4298
|
-
const
|
|
4299
|
-
...
|
|
4300
|
-
], null, 2)};
|
|
4301
|
-
const publicSurfaceOptionalAssetPaths = ${JSON.stringify([
|
|
4302
|
-
...publicSurfaceOptionalAssetPaths
|
|
4933
|
+
const publicSurfaceManagedSourceAssetPaths = ${JSON.stringify([
|
|
4934
|
+
...publicSurfaceManagedSourceAssetPaths
|
|
4303
4935
|
], null, 2)};
|
|
4304
4936
|
const expectedModernPackageSpecifier = packageName => {
|
|
4305
4937
|
if (packageSource.strategy === 'workspace') {
|
|
@@ -4325,19 +4957,67 @@ const assertNotExists = relativePath => {
|
|
|
4325
4957
|
assert(!fs.existsSync(path.join(root, relativePath)), \`Unexpected \${relativePath}\`);
|
|
4326
4958
|
};
|
|
4327
4959
|
const assertPublicSurfaceAssets = (appPath, publicRoutes) => {
|
|
4328
|
-
const
|
|
4329
|
-
|
|
4330
|
-
|
|
4331
|
-
|
|
4332
|
-
|
|
4333
|
-
|
|
4334
|
-
|
|
4960
|
+
for (const relativePath of publicSurfaceManagedSourceAssetPaths) {
|
|
4961
|
+
assertNotExists(\`\${appPath}/\${relativePath}\`);
|
|
4962
|
+
}
|
|
4963
|
+
void publicRoutes;
|
|
4964
|
+
};
|
|
4965
|
+
const assertPublicSurfaceContract = (appId, publicSurface) => {
|
|
4966
|
+
assert(publicSurface?.artifactLifecycle === 'build-and-deploy-output', \`\${appId} public surface artifacts must be build/deploy outputs\`);
|
|
4967
|
+
assert(publicSurface?.generator === 'scripts/generate-public-surface-assets.mjs', \`\${appId} public surface generator script is incorrect\`);
|
|
4968
|
+
assert(publicSurface?.outputRoot === 'dist/public', \`\${appId} public surface dist outputRoot is incorrect\`);
|
|
4969
|
+
assert(publicSurface?.cloudflareOutputRoot === '.output/public', \`\${appId} public surface Cloudflare outputRoot is incorrect\`);
|
|
4970
|
+
assert(!('staticRoot' in (publicSurface ?? {})), \`\${appId} public surface must not point at source config/public\`);
|
|
4971
|
+
assert((publicSurface?.files ?? []).includes('robots.txt'), \`\${appId} public surface must always emit robots.txt\`);
|
|
4972
|
+
assert(publicSurface?.contentExpansion?.authoring === 'route-owned-esm-provider', \`\${appId} public content expansion authoring is incorrect\`);
|
|
4973
|
+
assert(publicSurface?.contentExpansion?.defaultProviderFile === '${contentExpansionPolicy.defaultProviderFile}', \`\${appId} public content expansion provider file is incorrect\`);
|
|
4974
|
+
assert(publicSurface?.contentExpansion?.draftPolicy === '${contentExpansionPolicy.draftPolicy}', \`\${appId} public content expansion draft policy is incorrect\`);
|
|
4975
|
+
assert(publicSurface?.contentExpansion?.indexablePolicy === '${contentExpansionPolicy.indexablePolicy}', \`\${appId} public content expansion indexable policy is incorrect\`);
|
|
4976
|
+
assert(Array.isArray(publicSurface?.contentSources), \`\${appId} public content sources must be an array\`);
|
|
4977
|
+
if ((publicSurface?.publicRoutes ?? []).length === 0) {
|
|
4978
|
+
assert(!(publicSurface?.files ?? []).includes('sitemap.xml'), \`\${appId} private public surface must omit sitemap.xml\`);
|
|
4979
|
+
assert(!(publicSurface?.files ?? []).includes('site.webmanifest'), \`\${appId} private public surface must omit site.webmanifest\`);
|
|
4980
|
+
} else {
|
|
4981
|
+
assert((publicSurface?.files ?? []).includes('sitemap.xml'), \`\${appId} public surface must emit sitemap.xml when public routes exist\`);
|
|
4982
|
+
assert((publicSurface?.files ?? []).includes('site.webmanifest'), \`\${appId} public surface must emit site.webmanifest when public routes exist\`);
|
|
4983
|
+
}
|
|
4984
|
+
};
|
|
4985
|
+
const assertPublicHeadContract = (appId, publicHead, headModule) => {
|
|
4986
|
+
assert(publicHead?.generator === './src/routes/ultramodern-route-head', \`\${appId} public head generator is incorrect\`);
|
|
4987
|
+
assert(publicHead?.renderer === '@modern-js/runtime/head Helmet', \`\${appId} public head renderer is incorrect\`);
|
|
4988
|
+
assert(publicHead?.ssr === true, \`\${appId} public head must be SSR-rendered\`);
|
|
4989
|
+
assert(publicHead?.title?.source === 'route.titleKey', \`\${appId} public head title must come from route metadata\`);
|
|
4990
|
+
assert(publicHead?.description?.source === 'route.descriptionKey', \`\${appId} public head description must come from route metadata\`);
|
|
4991
|
+
assert(publicHead?.canonical?.publicIndexableOnly === true, \`\${appId} canonical links must be public/indexable only\`);
|
|
4992
|
+
assert(publicHead?.structuredData?.sanitizesHtmlOpenBracket === true, \`\${appId} structured data must sanitize HTML open brackets\`);
|
|
4993
|
+
assert(publicHead?.privateRouteRobots === '${robotsPolicy.privateRouteRobots}', \`\${appId} private route robots policy is incorrect\`);
|
|
4994
|
+
for (const snippet of [
|
|
4995
|
+
"from '@modern-js/runtime/head'",
|
|
4996
|
+
'<title>{title}</title>',
|
|
4997
|
+
'name="description"',
|
|
4998
|
+
'name="robots"',
|
|
4999
|
+
'rel="canonical"',
|
|
5000
|
+
'rel="alternate"',
|
|
5001
|
+
'property="og:title"',
|
|
5002
|
+
'property="og:description"',
|
|
5003
|
+
'name="twitter:card"',
|
|
5004
|
+
'application/ld+json',
|
|
5005
|
+
"replaceAll('<', '\\\\\\\\u003c')",
|
|
5006
|
+
]) {
|
|
5007
|
+
assert(headModule.includes(snippet), \`\${appId} route head module is missing \${snippet}\`);
|
|
4335
5008
|
}
|
|
4336
|
-
|
|
4337
|
-
|
|
4338
|
-
assert(
|
|
4339
|
-
assert(
|
|
4340
|
-
assert(
|
|
5009
|
+
};
|
|
5010
|
+
const assertCloudflareQualityGates = (appId, qualityGates) => {
|
|
5011
|
+
assert(qualityGates?.publicRoutes?.requireSitemapWhenPresent === true, \`\${appId} quality gates must require sitemap for public routes\`);
|
|
5012
|
+
assert(qualityGates?.publicRoutes?.requireRobotsSitemapConsistency === true, \`\${appId} quality gates must require robots/sitemap consistency\`);
|
|
5013
|
+
assert(qualityGates?.statusCodes?.unknownRouteStatus === 404, \`\${appId} quality gates must require 404 unknown routes\`);
|
|
5014
|
+
assert(qualityGates?.indexing?.previewNoindex === true, \`\${appId} quality gates must require preview noindex\`);
|
|
5015
|
+
assert(qualityGates?.indexing?.productionPublicRoutesIndexable === true, \`\${appId} quality gates must require production public routes to be indexable\`);
|
|
5016
|
+
assert(qualityGates?.assets?.cssPreloadRequired === true, \`\${appId} quality gates must require CSS preload evidence\`);
|
|
5017
|
+
assert(qualityGates?.assets?.sourcemapsPubliclyReferenced === false, \`\${appId} quality gates must reject public sourcemap references\`);
|
|
5018
|
+
assert(typeof qualityGates?.budgets?.ssrHtmlMaxBytes === 'number', \`\${appId} quality gates must define SSR HTML byte budget\`);
|
|
5019
|
+
assert(typeof qualityGates?.budgets?.mfManifestMaxBytes === 'number', \`\${appId} quality gates must define MF manifest byte budget\`);
|
|
5020
|
+
assert(qualityGates?.csp?.finalMode === '${qualityGates.csp.finalMode}', \`\${appId} CSP final mode decision is missing\`);
|
|
4341
5021
|
};
|
|
4342
5022
|
const expectedWorkerName = packageSuffix => \`\${packageScope}-\${packageSuffix}\`.slice(0, 63);
|
|
4343
5023
|
const expectedChunkLoadingGlobal = mfName =>
|
|
@@ -4399,6 +5079,7 @@ const requiredPaths = [
|
|
|
4399
5079
|
'scripts/assert-mf-types.mjs',
|
|
4400
5080
|
'scripts/bootstrap-agent-skills.mjs',
|
|
4401
5081
|
'scripts/check-ultramodern-i18n-boundaries.mjs',
|
|
5082
|
+
'scripts/generate-public-surface-assets.mjs',
|
|
4402
5083
|
'scripts/proof-cloudflare-version.mjs',
|
|
4403
5084
|
'scripts/setup-agent-reference-repos.mjs',
|
|
4404
5085
|
'apps/shell-super-app/package.json',
|
|
@@ -4413,11 +5094,10 @@ const requiredPaths = [
|
|
|
4413
5094
|
\`apps/shell-super-app/locales/cs/\${shellNamespace}.json\`,
|
|
4414
5095
|
'apps/shell-super-app/src/routes/index.css',
|
|
4415
5096
|
'apps/shell-super-app/src/routes/layout.tsx',
|
|
5097
|
+
'apps/shell-super-app/src/routes/ultramodern-route-head.tsx',
|
|
4416
5098
|
'apps/shell-super-app/src/routes/ultramodern-route-metadata.ts',
|
|
4417
5099
|
'apps/shell-super-app/src/routes/[lang]/page.tsx',
|
|
4418
|
-
|
|
4419
|
-
relativePath => \`apps/shell-super-app/\${relativePath}\`,
|
|
4420
|
-
),
|
|
5100
|
+
...${JSON.stringify(shellRouteMetaPaths, null, 2)},
|
|
4421
5101
|
'packages/shared-contracts/src/index.ts',
|
|
4422
5102
|
'packages/shared-design-tokens/src/index.ts',
|
|
4423
5103
|
'packages/shared-design-tokens/src/tokens.css',
|
|
@@ -4442,12 +5122,11 @@ for (const vertical of fullStackVerticals) {
|
|
|
4442
5122
|
\`\${vertical.path}/locales/cs/\${vertical.namespace}.json\`,
|
|
4443
5123
|
\`\${vertical.path}/src/routes/index.css\`,
|
|
4444
5124
|
\`\${vertical.path}/src/routes/layout.tsx\`,
|
|
5125
|
+
\`\${vertical.path}/src/routes/ultramodern-route-head.tsx\`,
|
|
4445
5126
|
\`\${vertical.path}/src/routes/ultramodern-route-metadata.ts\`,
|
|
4446
5127
|
\`\${vertical.path}/src/routes/[lang]/page.tsx\`,
|
|
4447
|
-
...publicSurfaceRequiredAssetPaths.map(
|
|
4448
|
-
relativePath => \`\${vertical.path}/\${relativePath}\`,
|
|
4449
|
-
),
|
|
4450
5128
|
...vertical.routePagePaths,
|
|
5129
|
+
...vertical.routeMetaPaths,
|
|
4451
5130
|
);
|
|
4452
5131
|
}
|
|
4453
5132
|
|
|
@@ -4568,6 +5247,10 @@ assert(generatedContract.cssFederation?.sharedDesignTokens?.ssr?.firstPaintRequi
|
|
|
4568
5247
|
|
|
4569
5248
|
const shellPackage = readJson('apps/shell-super-app/package.json');
|
|
4570
5249
|
const shellModernConfig = readText('apps/shell-super-app/modern.config.ts');
|
|
5250
|
+
const shellRouteHead = readText('apps/shell-super-app/src/routes/ultramodern-route-head.tsx');
|
|
5251
|
+
const shellRouteMetadata = readText('apps/shell-super-app/src/routes/ultramodern-route-metadata.ts');
|
|
5252
|
+
assert(shellRouteMetadata.includes('@generated by @modern-js/create'), 'Shell route metadata compatibility manifest must be marked generated');
|
|
5253
|
+
assert(shellRouteMetadata.includes("authoring: 'colocated-route-meta'"), 'Shell route metadata manifest must advertise colocated authoring');
|
|
4571
5254
|
const expectedZephyrDependencies = Object.fromEntries(
|
|
4572
5255
|
fullStackVerticals.map(vertical => [
|
|
4573
5256
|
vertical.zephyrAlias,
|
|
@@ -4590,10 +5273,18 @@ assert(shellContract?.deploy?.cloudflare?.publicUrlEnv === 'ULTRAMODERN_PUBLIC_U
|
|
|
4590
5273
|
assert(shellContract?.deploy?.cloudflare?.compatibilityDate === expectedCloudflareCompatibilityDate, 'Shell Cloudflare compatibilityDate is incorrect');
|
|
4591
5274
|
assert(JSON.stringify(shellContract?.deploy?.cloudflare?.compatibilityFlags) === JSON.stringify(expectedCloudflareCompatibilityFlags), 'Shell Cloudflare compatibility flags are incorrect');
|
|
4592
5275
|
assert(JSON.stringify(shellContract?.deploy?.cloudflare?.security) === JSON.stringify(expectedCloudflareSecurity), 'Shell Cloudflare security contract is incorrect');
|
|
5276
|
+
assertCloudflareQualityGates('shell-super-app', shellContract?.deploy?.cloudflare?.qualityGates);
|
|
4593
5277
|
assert(shellContract?.deploy?.worker?.compatibilityDate === expectedCloudflareCompatibilityDate, 'Shell worker compatibilityDate is incorrect');
|
|
4594
5278
|
assert(shellContract?.deploy?.worker?.name === expectedWorkerName('shell-super-app'), 'Shell worker name is incorrect');
|
|
4595
5279
|
assert(shellModernConfig.includes("const cloudflareWorkerName = '" + expectedWorkerName('shell-super-app') + "'"), 'Shell modern.config.ts must define the Cloudflare worker name');
|
|
4596
5280
|
assert(shellModernConfig.includes('name: cloudflareWorkerName'), 'Shell modern.config.ts must wire deploy.worker.name');
|
|
5281
|
+
assert(shellModernConfig.includes('const assetPrefix ='), 'Shell modern.config.ts must derive a dedicated asset prefix');
|
|
5282
|
+
assert(shellModernConfig.includes("assetPrefix: '/'"), 'Shell modern.config.ts must keep dev assets origin-relative');
|
|
5283
|
+
assert(shellModernConfig.includes('assetPrefix,'), 'Shell modern.config.ts must wire output.assetPrefix to the derived asset prefix');
|
|
5284
|
+
assert(shellContract?.config?.dev?.assetPrefix === '/', 'Shell dev asset prefix must stay origin-relative');
|
|
5285
|
+
assert(shellContract?.config?.output?.assetPrefix?.default === '/', 'Shell asset prefix must default to origin-relative paths');
|
|
5286
|
+
assert(JSON.stringify(shellContract?.config?.output?.assetPrefix?.envFallbackOrder) === JSON.stringify(['ULTRAMODERN_PUBLIC_URL_SHELL_SUPER_APP', 'MODERN_PUBLIC_SITE_URL', 'ULTRAMODERN_CLOUDFLARE_WORKERS_DEV_SUBDOMAIN']), 'Shell asset prefix env fallback order is incorrect');
|
|
5287
|
+
assert(JSON.stringify(shellContract?.config?.source?.siteUrl?.envFallbackOrder) === JSON.stringify(['MODERN_PUBLIC_SITE_URL', 'ULTRAMODERN_PUBLIC_URL_SHELL_SUPER_APP', 'ULTRAMODERN_CLOUDFLARE_WORKERS_DEV_SUBDOMAIN', 'SHELL_SUPER_APP_PORT']), 'Shell site URL env fallback order is incorrect');
|
|
4597
5288
|
assert(shellContract?.config?.rspack?.output?.uniqueName === 'shellSuperApp', 'Shell Rspack uniqueName is incorrect');
|
|
4598
5289
|
assert(shellContract?.config?.rspack?.output?.chunkLoadingGlobal === expectedChunkLoadingGlobal('shellSuperApp'), 'Shell Rspack chunkLoadingGlobal is incorrect');
|
|
4599
5290
|
assert(topology.shell?.cloudflare?.workerName === expectedWorkerName('shell-super-app'), 'Shell topology Cloudflare workerName is incorrect');
|
|
@@ -4608,11 +5299,15 @@ assert(shellContract?.styling?.federation?.assets?.shared?.some(asset => asset.e
|
|
|
4608
5299
|
assert(shellContract?.styling?.federation?.dedupe?.duplicateBaseStylesAllowed === false, 'Shell CSS contract must forbid duplicated base styles');
|
|
4609
5300
|
assert(shellContract?.styling?.federation?.ssr?.firstPaintRequired === true, 'Shell CSS must be required for SSR first paint');
|
|
4610
5301
|
assert(shellContract?.routes?.privateByDefault === true, 'Shell routes must be private by default');
|
|
5302
|
+
assert(shellContract?.routes?.metadataAuthoring === 'colocated-route-meta', 'Shell route metadata authoring mode is incorrect');
|
|
5303
|
+
assert(shellContract?.routes?.generatedManifest === true, 'Shell route metadata manifest must be generated');
|
|
4611
5304
|
assert(shellContract?.routes?.publicnessDefault === 'private-app-screen', 'Shell route publicness default is incorrect');
|
|
4612
5305
|
assert(JSON.stringify(shellContract?.routes?.publicRoutes ?? []) === '[]', 'Shell must not expose generated public routes by default');
|
|
5306
|
+
assertPublicHeadContract('shell-super-app', shellContract?.routes?.publicHead, shellRouteHead);
|
|
5307
|
+
assertPublicSurfaceContract('shell-super-app', shellContract?.routes?.publicSurface);
|
|
4613
5308
|
assert(
|
|
4614
|
-
(shellContract?.routes?.owned ?? []).every(route => route.public === false && route.indexable === false && route.publicSurface === 'private-app-screen'),
|
|
4615
|
-
'Shell owned routes must be non-indexable private app screens by default',
|
|
5309
|
+
(shellContract?.routes?.owned ?? []).every(route => route.public === false && route.indexable === false && route.publicSurface === 'private-app-screen' && typeof route.descriptionKey === 'string'),
|
|
5310
|
+
'Shell owned routes must be non-indexable private app screens by default and include description keys',
|
|
4616
5311
|
);
|
|
4617
5312
|
assertPublicSurfaceAssets('apps/shell-super-app', shellContract?.routes?.publicRoutes ?? []);
|
|
4618
5313
|
assert(
|
|
@@ -4626,6 +5321,10 @@ assert(!('effectServices' in topology), 'Default APIs must be vertical-owned, no
|
|
|
4626
5321
|
for (const vertical of fullStackVerticals) {
|
|
4627
5322
|
const packageJson = readJson(\`\${vertical.path}/package.json\`);
|
|
4628
5323
|
const modernConfig = readText(\`\${vertical.path}/modern.config.ts\`);
|
|
5324
|
+
const routeHead = readText(\`\${vertical.path}/src/routes/ultramodern-route-head.tsx\`);
|
|
5325
|
+
const routeMetadata = readText(\`\${vertical.path}/src/routes/ultramodern-route-metadata.ts\`);
|
|
5326
|
+
assert(routeMetadata.includes('@generated by @modern-js/create'), \`\${vertical.id} route metadata compatibility manifest must be marked generated\`);
|
|
5327
|
+
assert(routeMetadata.includes("authoring: 'colocated-route-meta'"), \`\${vertical.id} route metadata manifest must advertise colocated authoring\`);
|
|
4629
5328
|
assert(packageJson.name === vertical.packageName, \`\${vertical.id} package name is incorrect\`);
|
|
4630
5329
|
assert(packageJson.scripts?.['cloudflare:deploy'] === 'ULTRAMODERN_CLOUDFLARE_REQUIRE_PUBLIC_URLS=true pnpm run cloudflare:build && wrangler deploy --config .output/wrangler.json', \`\${vertical.id} must expose cloudflare:deploy\`);
|
|
4631
5330
|
assert(packageJson.scripts?.['cloudflare:proof']?.includes(\`--app \${vertical.id}\`), \`\${vertical.id} must expose cloudflare:proof\`);
|
|
@@ -4658,16 +5357,23 @@ for (const vertical of fullStackVerticals) {
|
|
|
4658
5357
|
assert(contractEntry?.deploy?.cloudflare?.compatibilityDate === expectedCloudflareCompatibilityDate, \`\${vertical.id} Cloudflare compatibilityDate is incorrect\`);
|
|
4659
5358
|
assert(JSON.stringify(contractEntry?.deploy?.cloudflare?.compatibilityFlags) === JSON.stringify(expectedCloudflareCompatibilityFlags), \`\${vertical.id} Cloudflare compatibility flags are incorrect\`);
|
|
4660
5359
|
assert(JSON.stringify(contractEntry?.deploy?.cloudflare?.security) === JSON.stringify(expectedCloudflareSecurity), \`\${vertical.id} Cloudflare security contract is incorrect\`);
|
|
5360
|
+
assertCloudflareQualityGates(vertical.id, contractEntry?.deploy?.cloudflare?.qualityGates);
|
|
4661
5361
|
assert(contractEntry?.deploy?.worker?.compatibilityDate === expectedCloudflareCompatibilityDate, \`\${vertical.id} worker compatibilityDate is incorrect\`);
|
|
4662
5362
|
assert(contractEntry?.deploy?.worker?.name === expectedWorkerName(vertical.id), \`\${vertical.id} worker name is incorrect\`);
|
|
4663
5363
|
assert(modernConfig.includes("const cloudflareWorkerName = '" + expectedWorkerName(vertical.id) + "'"), \`\${vertical.id} modern.config.ts must define the Cloudflare worker name\`);
|
|
4664
5364
|
assert(modernConfig.includes('name: cloudflareWorkerName'), \`\${vertical.id} modern.config.ts must wire deploy.worker.name\`);
|
|
5365
|
+
assert(modernConfig.includes('const assetPrefix ='), \`\${vertical.id} modern.config.ts must derive a dedicated asset prefix\`);
|
|
5366
|
+
assert(modernConfig.includes("assetPrefix: '/'"), \`\${vertical.id} modern.config.ts must keep dev assets origin-relative\`);
|
|
5367
|
+
assert(modernConfig.includes('assetPrefix,'), \`\${vertical.id} modern.config.ts must wire output.assetPrefix to the derived asset prefix\`);
|
|
5368
|
+
assert(contractEntry?.config?.dev?.assetPrefix === '/', \`\${vertical.id} dev asset prefix must stay origin-relative\`);
|
|
5369
|
+
assert(contractEntry?.config?.output?.assetPrefix?.default === '/', \`\${vertical.id} asset prefix must default to origin-relative paths\`);
|
|
5370
|
+
assert(JSON.stringify(contractEntry?.config?.output?.assetPrefix?.envFallbackOrder) === JSON.stringify([\`ULTRAMODERN_PUBLIC_URL_\${vertical.id.replace(/-/g, '_').toUpperCase()}\`, 'MODERN_PUBLIC_SITE_URL', 'ULTRAMODERN_CLOUDFLARE_WORKERS_DEV_SUBDOMAIN']), \`\${vertical.id} asset prefix env fallback order is incorrect\`);
|
|
4665
5371
|
assert(contractEntry?.deploy?.cloudflare?.routes?.effectReadiness === \`\${vertical.apiPrefix}/effect/\${vertical.stem}/readiness\`, \`\${vertical.id} Cloudflare proof readiness route is incorrect\`);
|
|
4666
5372
|
assert(contractEntry?.config?.rspack?.output?.uniqueName === vertical.mfName, \`\${vertical.id} Rspack uniqueName is incorrect\`);
|
|
4667
5373
|
assert(contractEntry?.config?.rspack?.output?.chunkLoadingGlobal === expectedChunkLoadingGlobal(vertical.mfName), \`\${vertical.id} Rspack chunkLoadingGlobal is incorrect\`);
|
|
4668
5374
|
assert(contractEntry?.moduleFederation?.name === vertical.mfName, \`\${vertical.id} MF name is incorrect\`);
|
|
4669
5375
|
assert(JSON.stringify(contractEntry?.moduleFederation?.exposes) === JSON.stringify(vertical.exposes), \`\${vertical.id} MF exposes are incorrect\`);
|
|
4670
|
-
assert(contractEntry?.moduleFederation?.dts?.compilerInstance === '
|
|
5376
|
+
assert(contractEntry?.moduleFederation?.dts?.compilerInstance === 'tsgo', \`\${vertical.id} must keep mandatory DTS compiler\`);
|
|
4671
5377
|
assert(JSON.stringify(contractEntry?.moduleFederation?.verticalRefs ?? []) === JSON.stringify(vertical.verticalRefs), \`\${vertical.id} MF verticalRefs are incorrect\`);
|
|
4672
5378
|
assert(
|
|
4673
5379
|
JSON.stringify((contractEntry?.moduleFederation?.remotes ?? []).map(remote => remote.id)) ===
|
|
@@ -4687,13 +5393,17 @@ for (const vertical of fullStackVerticals) {
|
|
|
4687
5393
|
\`\${vertical.id} localisedUrls must come from route metadata\`,
|
|
4688
5394
|
);
|
|
4689
5395
|
assert(contractEntry?.routes?.source === 'route-owned', \`\${vertical.id} routes must be route-owned\`);
|
|
5396
|
+
assert(contractEntry?.routes?.metadataAuthoring === 'colocated-route-meta', \`\${vertical.id} route metadata authoring mode is incorrect\`);
|
|
5397
|
+
assert(contractEntry?.routes?.generatedManifest === true, \`\${vertical.id} route metadata manifest must be generated\`);
|
|
4690
5398
|
assert(contractEntry?.routes?.metadataExport === './src/routes/ultramodern-route-metadata', \`\${vertical.id} route metadata export is incorrect\`);
|
|
4691
5399
|
assert(contractEntry?.routes?.privateByDefault === true, \`\${vertical.id} routes must be private by default\`);
|
|
4692
5400
|
assert(contractEntry?.routes?.publicnessDefault === 'private-app-screen', \`\${vertical.id} route publicness default is incorrect\`);
|
|
4693
5401
|
assert(JSON.stringify(contractEntry?.routes?.publicRoutes ?? []) === '[]', \`\${vertical.id} must not expose generated public routes by default\`);
|
|
5402
|
+
assertPublicHeadContract(vertical.id, contractEntry?.routes?.publicHead, routeHead);
|
|
5403
|
+
assertPublicSurfaceContract(vertical.id, contractEntry?.routes?.publicSurface);
|
|
4694
5404
|
assert(
|
|
4695
|
-
(contractEntry?.routes?.owned ?? []).every(route => route.public === false && route.indexable === false && route.publicSurface === 'private-app-screen'),
|
|
4696
|
-
\`\${vertical.id} owned routes must be non-indexable private app screens by default\`,
|
|
5405
|
+
(contractEntry?.routes?.owned ?? []).every(route => route.public === false && route.indexable === false && route.publicSurface === 'private-app-screen' && typeof route.descriptionKey === 'string'),
|
|
5406
|
+
\`\${vertical.id} owned routes must be non-indexable private app screens by default and include description keys\`,
|
|
4697
5407
|
);
|
|
4698
5408
|
assertPublicSurfaceAssets(vertical.path, contractEntry?.routes?.publicRoutes ?? []);
|
|
4699
5409
|
assert(contractEntry?.styling?.federation?.owner?.id === vertical.id, \`\${vertical.id} CSS federation owner is missing\`);
|
|
@@ -4710,84 +5420,28 @@ for (const vertical of fullStackVerticals) {
|
|
|
4710
5420
|
const topologyEntry = topology.verticals?.find(verticalEntry => verticalEntry.id === vertical.id);
|
|
4711
5421
|
assert(topologyEntry?.kind === 'vertical', \`\${vertical.id} topology kind is incorrect\`);
|
|
4712
5422
|
assert(topologyEntry?.package === vertical.packageName, \`\${vertical.id} topology package is incorrect\`);
|
|
4713
|
-
assert(topologyEntry?.cloudflare?.workerName === expectedWorkerName(vertical.id), \`\${vertical.id} topology Cloudflare workerName is incorrect\`);
|
|
4714
|
-
assert(topologyEntry?.moduleFederation?.name === vertical.mfName, \`\${vertical.id} topology MF name is incorrect\`);
|
|
4715
|
-
assert(JSON.stringify(topologyEntry?.moduleFederation?.exposes) === JSON.stringify(vertical.exposes), \`\${vertical.id} topology exposes are incorrect\`);
|
|
4716
|
-
assert(JSON.stringify(topologyEntry?.moduleFederation?.verticalRefs ?? []) === JSON.stringify(vertical.verticalRefs), \`\${vertical.id} topology verticalRefs are incorrect\`);
|
|
4717
|
-
assert(topologyEntry?.api?.effect?.bff?.prefix === vertical.apiPrefix, \`\${vertical.id} topology API prefix is incorrect\`);
|
|
4718
|
-
assert(topologyEntry?.api?.effect?.serverEntry === \`\${vertical.path}/api/effect/index.ts\`, \`\${vertical.id} topology server entry is incorrect\`);
|
|
4719
|
-
assert(topologyEntry?.api?.effect?.readiness?.endpoint === \`/effect/\${vertical.stem}/readiness\`, \`\${vertical.id} topology readiness endpoint is incorrect\`);
|
|
4720
|
-
assert(Object.keys(topologyEntry?.api?.effect?.domainOperations ?? {}).length >= 3, \`\${vertical.id} topology domain operations are missing\`);
|
|
4721
|
-
|
|
4722
|
-
assert(ownership.owners?.some(owner => owner.id === vertical.id && owner.path === vertical.path), \`\${vertical.id} ownership entry is missing\`);
|
|
4723
|
-
assert(overlay.ports?.[vertical.id], \`\${vertical.id} development port is missing\`);
|
|
4724
|
-
assert(overlay.manifests?.[vertical.id]?.includes('/mf-manifest.json'), \`\${vertical.id} development manifest is missing\`);
|
|
4725
|
-
assert(overlay.apis?.[vertical.id]?.endsWith(vertical.apiPrefix), \`\${vertical.id} development API URL is missing\`);
|
|
4726
|
-
}
|
|
4727
|
-
|
|
4728
|
-
console.log('UltraModern workspace scaffold validated');
|
|
4729
|
-
`;
|
|
4730
|
-
}
|
|
4731
|
-
function createCloudflareVersionProofScript() {
|
|
4732
|
-
return `#!/usr/bin/env node
|
|
4733
|
-
import fs from 'node:fs';
|
|
4734
|
-
import path from 'node:path';
|
|
4735
|
-
import { fileURLToPath } from 'node:url';
|
|
4736
|
-
|
|
4737
|
-
const workspaceRoot = path.resolve(
|
|
4738
|
-
path.dirname(fileURLToPath(import.meta.url)),
|
|
4739
|
-
'..',
|
|
4740
|
-
);
|
|
4741
|
-
const contractPath = path.join(
|
|
4742
|
-
workspaceRoot,
|
|
4743
|
-
'.modernjs/ultramodern-generated-contract.json',
|
|
4744
|
-
);
|
|
4745
|
-
const defaultOut = path.join(
|
|
4746
|
-
workspaceRoot,
|
|
4747
|
-
'.codex/reports/cloudflare-version-proof/public-url-proof.json',
|
|
4748
|
-
);
|
|
4749
|
-
|
|
4750
|
-
function readJson(filePath) {
|
|
4751
|
-
return JSON.parse(fs.readFileSync(filePath, 'utf8'));
|
|
4752
|
-
}
|
|
4753
|
-
|
|
4754
|
-
function parseArgs(argv) {
|
|
4755
|
-
const parsed = {
|
|
4756
|
-
appId: undefined,
|
|
4757
|
-
out: defaultOut,
|
|
4758
|
-
requirePublicUrls: false,
|
|
4759
|
-
};
|
|
4760
|
-
|
|
4761
|
-
for (let index = 0; index < argv.length; index += 1) {
|
|
4762
|
-
const arg = argv[index];
|
|
4763
|
-
if (arg === '--app') {
|
|
4764
|
-
parsed.appId = argv[index + 1];
|
|
4765
|
-
index += 1;
|
|
4766
|
-
} else if (arg === '--out') {
|
|
4767
|
-
parsed.out = argv[index + 1];
|
|
4768
|
-
index += 1;
|
|
4769
|
-
} else if (arg === '--require-public-urls') {
|
|
4770
|
-
parsed.requirePublicUrls = true;
|
|
4771
|
-
} else if (arg === '--help' || arg === '-h') {
|
|
4772
|
-
parsed.help = true;
|
|
4773
|
-
} else {
|
|
4774
|
-
throw new Error(\`Unknown argument: \${arg}\`);
|
|
4775
|
-
}
|
|
4776
|
-
}
|
|
5423
|
+
assert(topologyEntry?.cloudflare?.workerName === expectedWorkerName(vertical.id), \`\${vertical.id} topology Cloudflare workerName is incorrect\`);
|
|
5424
|
+
assert(topologyEntry?.moduleFederation?.name === vertical.mfName, \`\${vertical.id} topology MF name is incorrect\`);
|
|
5425
|
+
assert(JSON.stringify(topologyEntry?.moduleFederation?.exposes) === JSON.stringify(vertical.exposes), \`\${vertical.id} topology exposes are incorrect\`);
|
|
5426
|
+
assert(JSON.stringify(topologyEntry?.moduleFederation?.verticalRefs ?? []) === JSON.stringify(vertical.verticalRefs), \`\${vertical.id} topology verticalRefs are incorrect\`);
|
|
5427
|
+
assert(topologyEntry?.api?.effect?.bff?.prefix === vertical.apiPrefix, \`\${vertical.id} topology API prefix is incorrect\`);
|
|
5428
|
+
assert(topologyEntry?.api?.effect?.serverEntry === \`\${vertical.path}/api/effect/index.ts\`, \`\${vertical.id} topology server entry is incorrect\`);
|
|
5429
|
+
assert(topologyEntry?.api?.effect?.readiness?.endpoint === \`/effect/\${vertical.stem}/readiness\`, \`\${vertical.id} topology readiness endpoint is incorrect\`);
|
|
5430
|
+
assert(Object.keys(topologyEntry?.api?.effect?.domainOperations ?? {}).length >= 3, \`\${vertical.id} topology domain operations are missing\`);
|
|
4777
5431
|
|
|
4778
|
-
|
|
5432
|
+
assert(ownership.owners?.some(owner => owner.id === vertical.id && owner.path === vertical.path), \`\${vertical.id} ownership entry is missing\`);
|
|
5433
|
+
assert(overlay.ports?.[vertical.id], \`\${vertical.id} development port is missing\`);
|
|
5434
|
+
assert(overlay.manifests?.[vertical.id]?.includes('/mf-manifest.json'), \`\${vertical.id} development manifest is missing\`);
|
|
5435
|
+
assert(overlay.apis?.[vertical.id]?.endsWith(vertical.apiPrefix), \`\${vertical.id} development API URL is missing\`);
|
|
4779
5436
|
}
|
|
4780
5437
|
|
|
4781
|
-
|
|
4782
|
-
|
|
4783
|
-
node scripts/proof-cloudflare-version.mjs [--app workspace] [--out evidence.json] [--require-public-urls]
|
|
4784
|
-
|
|
4785
|
-
Set each app's public URL using the contract env key, for example:
|
|
4786
|
-
ULTRAMODERN_PUBLIC_URL_WORKSPACE=https://workspace.example.workers.dev
|
|
4787
|
-
\`);
|
|
5438
|
+
console.log('UltraModern workspace scaffold validated');
|
|
5439
|
+
`;
|
|
4788
5440
|
}
|
|
4789
|
-
|
|
4790
|
-
|
|
5441
|
+
function createCloudflareProofHelperScript() {
|
|
5442
|
+
const robotsPolicy = createPublicHeadRobotsPolicy();
|
|
5443
|
+
const qualityGates = createPublicWebsiteQualityGateContract();
|
|
5444
|
+
return `function joinUrl(baseUrl, routePath) {
|
|
4791
5445
|
return new URL(routePath, baseUrl.endsWith('/') ? baseUrl : \`\${baseUrl}/\`);
|
|
4792
5446
|
}
|
|
4793
5447
|
|
|
@@ -4801,6 +5455,8 @@ async function fetchText(url) {
|
|
|
4801
5455
|
ok: response.ok,
|
|
4802
5456
|
status: response.status,
|
|
4803
5457
|
accessControlAllowOrigin: response.headers.get('access-control-allow-origin'),
|
|
5458
|
+
cacheControl: response.headers.get('cache-control'),
|
|
5459
|
+
contentLength: response.headers.get('content-length'),
|
|
4804
5460
|
contentSecurityPolicy: response.headers.get('content-security-policy'),
|
|
4805
5461
|
contentSecurityPolicyReportOnly: response.headers.get('content-security-policy-report-only'),
|
|
4806
5462
|
contentType: response.headers.get('content-type'),
|
|
@@ -4859,6 +5515,52 @@ function assert(condition, message) {
|
|
|
4859
5515
|
}
|
|
4860
5516
|
}
|
|
4861
5517
|
|
|
5518
|
+
function responseByteLength(response) {
|
|
5519
|
+
return Buffer.byteLength(response.body, 'utf8');
|
|
5520
|
+
}
|
|
5521
|
+
|
|
5522
|
+
function assertByteBudget(evidence, app, response, options) {
|
|
5523
|
+
const bytes = responseByteLength(response);
|
|
5524
|
+
const passed = bytes <= options.maxBytes;
|
|
5525
|
+
evidence.assertions.push({
|
|
5526
|
+
type: 'byte-budget',
|
|
5527
|
+
label: options.label,
|
|
5528
|
+
route: options.route,
|
|
5529
|
+
actualBytes: bytes,
|
|
5530
|
+
maxBytes: options.maxBytes,
|
|
5531
|
+
status: passed ? 'pass' : 'fail',
|
|
5532
|
+
});
|
|
5533
|
+
assert(
|
|
5534
|
+
passed,
|
|
5535
|
+
app.id + ' ' + options.route + ' exceeds ' + options.label + ' byte budget: ' + bytes + ' > ' + options.maxBytes,
|
|
5536
|
+
);
|
|
5537
|
+
}
|
|
5538
|
+
|
|
5539
|
+
function assertContentType(evidence, app, response, options) {
|
|
5540
|
+
const actual = response.contentType ?? '';
|
|
5541
|
+
const passed = actual.toLowerCase().includes(options.includes);
|
|
5542
|
+
evidence.assertions.push({
|
|
5543
|
+
type: 'content-type',
|
|
5544
|
+
route: options.route,
|
|
5545
|
+
expectedIncludes: options.includes,
|
|
5546
|
+
actual,
|
|
5547
|
+
status: passed ? 'pass' : 'fail',
|
|
5548
|
+
});
|
|
5549
|
+
assert(passed, app.id + ' ' + options.route + ' content-type must include ' + options.includes);
|
|
5550
|
+
}
|
|
5551
|
+
|
|
5552
|
+
function assertCacheControl(evidence, app, response, options) {
|
|
5553
|
+
const actual = response.cacheControl ?? '';
|
|
5554
|
+
const passed = options.required === false || actual.trim() !== '';
|
|
5555
|
+
evidence.assertions.push({
|
|
5556
|
+
type: 'cache-control',
|
|
5557
|
+
route: options.route,
|
|
5558
|
+
actual,
|
|
5559
|
+
status: passed ? 'pass' : 'fail',
|
|
5560
|
+
});
|
|
5561
|
+
assert(passed, app.id + ' ' + options.route + ' is missing cache-control');
|
|
5562
|
+
}
|
|
5563
|
+
|
|
4862
5564
|
function matchesPreviewHostname(hostname, pattern) {
|
|
4863
5565
|
const normalizedHostname = hostname.toLowerCase();
|
|
4864
5566
|
const normalizedPattern = String(pattern || '').toLowerCase();
|
|
@@ -4983,15 +5685,382 @@ function assertCloudflareSecurity(evidence, app, response, route, publicUrl, opt
|
|
|
4983
5685
|
type: 'security-noindex',
|
|
4984
5686
|
route,
|
|
4985
5687
|
actual: response.xRobotsTag,
|
|
4986
|
-
status: response.xRobotsTag === '
|
|
5688
|
+
status: response.xRobotsTag === '${robotsPolicy.privateRouteRobots}' ? 'pass' : 'fail',
|
|
4987
5689
|
});
|
|
4988
5690
|
assert(
|
|
4989
|
-
response.xRobotsTag === '
|
|
5691
|
+
response.xRobotsTag === '${robotsPolicy.privateRouteRobots}',
|
|
4990
5692
|
\`\${app.id} \${route} is missing noindex X-Robots-Tag\`,
|
|
4991
5693
|
);
|
|
4992
5694
|
}
|
|
4993
5695
|
}
|
|
4994
5696
|
|
|
5697
|
+
function collectStringValues(value, results = []) {
|
|
5698
|
+
if (typeof value === 'string') {
|
|
5699
|
+
results.push(value);
|
|
5700
|
+
return results;
|
|
5701
|
+
}
|
|
5702
|
+
if (Array.isArray(value)) {
|
|
5703
|
+
for (const item of value) {
|
|
5704
|
+
collectStringValues(item, results);
|
|
5705
|
+
}
|
|
5706
|
+
return results;
|
|
5707
|
+
}
|
|
5708
|
+
if (value && typeof value === 'object') {
|
|
5709
|
+
for (const item of Object.values(value)) {
|
|
5710
|
+
collectStringValues(item, results);
|
|
5711
|
+
}
|
|
5712
|
+
}
|
|
5713
|
+
return results;
|
|
5714
|
+
}
|
|
5715
|
+
|
|
5716
|
+
function assertNoPublicSourcemapRefs(evidence, app, manifestJson) {
|
|
5717
|
+
const sourcemapRefs = collectStringValues(manifestJson).filter(value =>
|
|
5718
|
+
/\\.map(?:$|[?#])/u.test(value),
|
|
5719
|
+
);
|
|
5720
|
+
evidence.assertions.push({
|
|
5721
|
+
type: 'sourcemap-policy',
|
|
5722
|
+
actual: sourcemapRefs,
|
|
5723
|
+
status: sourcemapRefs.length === 0 ? 'pass' : 'fail',
|
|
5724
|
+
});
|
|
5725
|
+
assert(
|
|
5726
|
+
sourcemapRefs.length === 0,
|
|
5727
|
+
app.id + ' MF manifest must not publicly reference sourcemaps',
|
|
5728
|
+
);
|
|
5729
|
+
}
|
|
5730
|
+
|
|
5731
|
+
function extractPreloadStyleUrls(linkHeader, publicUrl) {
|
|
5732
|
+
const urls = [];
|
|
5733
|
+
for (const match of String(linkHeader || '').matchAll(/<([^>]+)>\\s*;[^,]*rel=preload[^,]*as=style/giu)) {
|
|
5734
|
+
urls.push(String(joinUrl(publicUrl, match[1])));
|
|
5735
|
+
}
|
|
5736
|
+
return urls;
|
|
5737
|
+
}
|
|
5738
|
+
|
|
5739
|
+
function htmlHasRobotsDirective(html, expectedContent) {
|
|
5740
|
+
return htmlHasTagWithAttributes(html, 'meta', {
|
|
5741
|
+
name: 'robots',
|
|
5742
|
+
content: expectedContent,
|
|
5743
|
+
});
|
|
5744
|
+
}
|
|
5745
|
+
|
|
5746
|
+
function escapeRegExp(value) {
|
|
5747
|
+
return String(value).replace(/[.*+?^\${}()|[\\]\\\\]/g, '\\\\$&');
|
|
5748
|
+
}
|
|
5749
|
+
|
|
5750
|
+
function htmlHasTagWithAttributes(html, tagName, attributes) {
|
|
5751
|
+
const tagPattern = new RegExp(\`<\${tagName}\\\\b[^>]*>\`, 'giu');
|
|
5752
|
+
const tags = html.match(tagPattern) || [];
|
|
5753
|
+
return tags.some(tag =>
|
|
5754
|
+
Object.entries(attributes).every(([name, value]) => {
|
|
5755
|
+
const attrPattern = new RegExp(
|
|
5756
|
+
\`\\\\b\${escapeRegExp(name)}=["']\${escapeRegExp(value)}["']\`,
|
|
5757
|
+
'iu',
|
|
5758
|
+
);
|
|
5759
|
+
return attrPattern.test(tag);
|
|
5760
|
+
}),
|
|
5761
|
+
);
|
|
5762
|
+
}
|
|
5763
|
+
|
|
5764
|
+
function assertHeadTag(evidence, html, options) {
|
|
5765
|
+
const found = htmlHasTagWithAttributes(
|
|
5766
|
+
html,
|
|
5767
|
+
options.tag,
|
|
5768
|
+
options.attributes,
|
|
5769
|
+
);
|
|
5770
|
+
evidence.assertions.push({
|
|
5771
|
+
type: 'ssr-head',
|
|
5772
|
+
route: options.route,
|
|
5773
|
+
tag: options.tag,
|
|
5774
|
+
attributes: options.attributes,
|
|
5775
|
+
status: found ? 'pass' : 'fail',
|
|
5776
|
+
});
|
|
5777
|
+
assert(found, \`\${options.appId} \${options.route} SSR head is missing \${options.label}\`);
|
|
5778
|
+
}
|
|
5779
|
+
|
|
5780
|
+
async function validateSsrHead(evidence, app, publicUrl, ssrRoute, ssr) {
|
|
5781
|
+
const titleFound = /<title\\b[^>]*>[^<]+<\\/title>/iu.test(ssr.body);
|
|
5782
|
+
evidence.assertions.push({
|
|
5783
|
+
type: 'ssr-head',
|
|
5784
|
+
route: ssrRoute,
|
|
5785
|
+
tag: 'title',
|
|
5786
|
+
status: titleFound ? 'pass' : 'fail',
|
|
5787
|
+
});
|
|
5788
|
+
assert(titleFound, \`\${app.id} \${ssrRoute} SSR head is missing title\`);
|
|
5789
|
+
assertHeadTag(evidence, ssr.body, {
|
|
5790
|
+
appId: app.id,
|
|
5791
|
+
route: ssrRoute,
|
|
5792
|
+
tag: 'meta',
|
|
5793
|
+
attributes: { name: 'description' },
|
|
5794
|
+
label: 'description meta',
|
|
5795
|
+
});
|
|
5796
|
+
assertHeadTag(evidence, ssr.body, {
|
|
5797
|
+
appId: app.id,
|
|
5798
|
+
route: ssrRoute,
|
|
5799
|
+
tag: 'meta',
|
|
5800
|
+
attributes: { name: 'robots' },
|
|
5801
|
+
label: 'robots meta',
|
|
5802
|
+
});
|
|
5803
|
+
|
|
5804
|
+
const publicSurface = app.routes?.publicSurface ?? {};
|
|
5805
|
+
const routeEntry = (publicSurface.routeEntries ?? [])[0];
|
|
5806
|
+
if (!routeEntry) {
|
|
5807
|
+
const canonicalFound = htmlHasTagWithAttributes(ssr.body, 'link', {
|
|
5808
|
+
rel: 'canonical',
|
|
5809
|
+
});
|
|
5810
|
+
evidence.assertions.push({
|
|
5811
|
+
type: 'ssr-head-private-canonical',
|
|
5812
|
+
route: ssrRoute,
|
|
5813
|
+
status: canonicalFound ? 'fail' : 'pass',
|
|
5814
|
+
});
|
|
5815
|
+
assert(!canonicalFound, \`\${app.id} \${ssrRoute} private SSR head must not emit canonical links\`);
|
|
5816
|
+
return;
|
|
5817
|
+
}
|
|
5818
|
+
|
|
5819
|
+
const publicRoute = routeEntry.localeUrlPaths?.en ?? publicSurface.concreteUrlPaths?.[0];
|
|
5820
|
+
const headRoute = publicRoute || ssrRoute;
|
|
5821
|
+
const headResponse =
|
|
5822
|
+
headRoute === ssrRoute ? ssr : await fetchText(joinUrl(publicUrl, headRoute));
|
|
5823
|
+
if (headRoute !== ssrRoute) {
|
|
5824
|
+
evidence.assertions.push({
|
|
5825
|
+
type: 'ssr-head-route',
|
|
5826
|
+
route: headRoute,
|
|
5827
|
+
status: headResponse.ok ? 'pass' : 'fail',
|
|
5828
|
+
statusCode: headResponse.status,
|
|
5829
|
+
});
|
|
5830
|
+
assert(headResponse.ok, \`\${app.id} public head route returned HTTP \${headResponse.status}\`);
|
|
5831
|
+
assertCloudflareSecurity(evidence, app, headResponse, headRoute, publicUrl, {
|
|
5832
|
+
html: true,
|
|
5833
|
+
});
|
|
5834
|
+
}
|
|
5835
|
+
const isPreview = shouldNoindexUrl(publicUrl, app.deploy?.cloudflare?.security?.noindex);
|
|
5836
|
+
const robotsIndexable = htmlHasRobotsDirective(headResponse.body, '${robotsPolicy.indexableRobots}');
|
|
5837
|
+
evidence.assertions.push({
|
|
5838
|
+
type: 'indexing-policy',
|
|
5839
|
+
route: headRoute,
|
|
5840
|
+
mode: isPreview ? 'preview' : 'production',
|
|
5841
|
+
xRobotsTag: headResponse.xRobotsTag,
|
|
5842
|
+
htmlRobotsIndexable: robotsIndexable,
|
|
5843
|
+
status:
|
|
5844
|
+
isPreview || (headResponse.xRobotsTag !== '${robotsPolicy.privateRouteRobots}' && robotsIndexable)
|
|
5845
|
+
? 'pass'
|
|
5846
|
+
: 'fail',
|
|
5847
|
+
});
|
|
5848
|
+
if (!isPreview) {
|
|
5849
|
+
assert(
|
|
5850
|
+
headResponse.xRobotsTag !== '${robotsPolicy.privateRouteRobots}' && robotsIndexable,
|
|
5851
|
+
\`\${app.id} \${headRoute} production public route must be indexable\`,
|
|
5852
|
+
);
|
|
5853
|
+
}
|
|
5854
|
+
|
|
5855
|
+
const canonicalUrl = String(joinUrl(publicUrl, headRoute));
|
|
5856
|
+
assertHeadTag(evidence, headResponse.body, {
|
|
5857
|
+
appId: app.id,
|
|
5858
|
+
route: headRoute,
|
|
5859
|
+
tag: 'link',
|
|
5860
|
+
attributes: { rel: 'canonical', href: canonicalUrl },
|
|
5861
|
+
label: 'canonical link',
|
|
5862
|
+
});
|
|
5863
|
+
for (const language of app.routes?.publicHead?.alternates?.hreflang ?? []) {
|
|
5864
|
+
const href = String(joinUrl(publicUrl, routeEntry.localeUrlPaths?.[language] ?? headRoute));
|
|
5865
|
+
assertHeadTag(evidence, headResponse.body, {
|
|
5866
|
+
appId: app.id,
|
|
5867
|
+
route: headRoute,
|
|
5868
|
+
tag: 'link',
|
|
5869
|
+
attributes: { rel: 'alternate', hreflang: language, href },
|
|
5870
|
+
label: \`hreflang \${language}\`,
|
|
5871
|
+
});
|
|
5872
|
+
}
|
|
5873
|
+
assertHeadTag(evidence, headResponse.body, {
|
|
5874
|
+
appId: app.id,
|
|
5875
|
+
route: headRoute,
|
|
5876
|
+
tag: 'link',
|
|
5877
|
+
attributes: { rel: 'alternate', hreflang: 'x-default' },
|
|
5878
|
+
label: 'x-default hreflang',
|
|
5879
|
+
});
|
|
5880
|
+
for (const property of ['og:title', 'og:description', 'og:url', 'og:type']) {
|
|
5881
|
+
assertHeadTag(evidence, headResponse.body, {
|
|
5882
|
+
appId: app.id,
|
|
5883
|
+
route: headRoute,
|
|
5884
|
+
tag: 'meta',
|
|
5885
|
+
attributes: { property },
|
|
5886
|
+
label: property,
|
|
5887
|
+
});
|
|
5888
|
+
}
|
|
5889
|
+
for (const name of ['twitter:card', 'twitter:title', 'twitter:description']) {
|
|
5890
|
+
assertHeadTag(evidence, headResponse.body, {
|
|
5891
|
+
appId: app.id,
|
|
5892
|
+
route: headRoute,
|
|
5893
|
+
tag: 'meta',
|
|
5894
|
+
attributes: { name },
|
|
5895
|
+
label: name,
|
|
5896
|
+
});
|
|
5897
|
+
}
|
|
5898
|
+
assertHeadTag(evidence, headResponse.body, {
|
|
5899
|
+
appId: app.id,
|
|
5900
|
+
route: headRoute,
|
|
5901
|
+
tag: 'script',
|
|
5902
|
+
attributes: { type: 'application/ld+json' },
|
|
5903
|
+
label: 'JSON-LD structured data',
|
|
5904
|
+
});
|
|
5905
|
+
}
|
|
5906
|
+
|
|
5907
|
+
async function validateNotFound(evidence, app, publicUrl) {
|
|
5908
|
+
const qualityGates = app.deploy?.cloudflare?.qualityGates ?? {};
|
|
5909
|
+
const notFoundRoute =
|
|
5910
|
+
qualityGates.statusCodes?.notFoundRoute ?? '${qualityGates.statusCodes.notFoundRoute}';
|
|
5911
|
+
const expectedStatus = qualityGates.statusCodes?.unknownRouteStatus ?? ${qualityGates.statusCodes.unknownRouteStatus};
|
|
5912
|
+
const response = await fetchText(joinUrl(publicUrl, notFoundRoute));
|
|
5913
|
+
evidence.assertions.push({
|
|
5914
|
+
type: 'status-code',
|
|
5915
|
+
route: notFoundRoute,
|
|
5916
|
+
expectedStatus,
|
|
5917
|
+
actualStatus: response.status,
|
|
5918
|
+
status: response.status === expectedStatus ? 'pass' : 'fail',
|
|
5919
|
+
});
|
|
5920
|
+
assert(
|
|
5921
|
+
response.status === expectedStatus,
|
|
5922
|
+
\`\${app.id} unknown route must return HTTP \${expectedStatus}, got \${response.status}\`,
|
|
5923
|
+
);
|
|
5924
|
+
assertCloudflareSecurity(evidence, app, response, notFoundRoute, publicUrl, {
|
|
5925
|
+
html: response.contentType?.includes('text/html'),
|
|
5926
|
+
});
|
|
5927
|
+
}
|
|
5928
|
+
|
|
5929
|
+
async function validateCssAsset(evidence, app, publicUrl, ssr) {
|
|
5930
|
+
const qualityGates = app.deploy?.cloudflare?.qualityGates ?? {};
|
|
5931
|
+
const budgets = qualityGates.budgets ?? {};
|
|
5932
|
+
const styleUrls = extractPreloadStyleUrls(ssr.link, publicUrl);
|
|
5933
|
+
evidence.assertions.push({
|
|
5934
|
+
type: 'css-preload-assets',
|
|
5935
|
+
actual: styleUrls,
|
|
5936
|
+
status: styleUrls.length > 0 ? 'pass' : 'fail',
|
|
5937
|
+
});
|
|
5938
|
+
assert(styleUrls.length > 0, \`\${app.id} SSR response did not expose preloadable CSS assets\`);
|
|
5939
|
+
|
|
5940
|
+
const styleUrl = styleUrls[0];
|
|
5941
|
+
const route = new URL(styleUrl).pathname;
|
|
5942
|
+
const css = await fetchText(styleUrl);
|
|
5943
|
+
evidence.assertions.push({
|
|
5944
|
+
type: 'css-asset',
|
|
5945
|
+
route,
|
|
5946
|
+
status: css.ok && css.body.trim() !== '' ? 'pass' : 'fail',
|
|
5947
|
+
statusCode: css.status,
|
|
5948
|
+
});
|
|
5949
|
+
assert(css.ok, \`\${app.id} CSS asset returned HTTP \${css.status}\`);
|
|
5950
|
+
assert(css.body.trim() !== '', \`\${app.id} CSS asset is empty\`);
|
|
5951
|
+
assertContentType(evidence, app, css, {
|
|
5952
|
+
route,
|
|
5953
|
+
includes: 'text/css',
|
|
5954
|
+
});
|
|
5955
|
+
assertCacheControl(evidence, app, css, {
|
|
5956
|
+
route,
|
|
5957
|
+
required: qualityGates.assets?.cacheControlRequiredForCss,
|
|
5958
|
+
});
|
|
5959
|
+
assertByteBudget(evidence, app, css, {
|
|
5960
|
+
label: 'cssAssetMaxBytes',
|
|
5961
|
+
maxBytes: budgets.cssAssetMaxBytes ?? ${createPublicWebsiteBudgetFallback('cssAssetMaxBytes')},
|
|
5962
|
+
route,
|
|
5963
|
+
});
|
|
5964
|
+
}
|
|
5965
|
+
|
|
5966
|
+
async function validatePublicSurface(evidence, app, publicUrl) {
|
|
5967
|
+
const publicSurface = app.routes?.publicSurface ?? {};
|
|
5968
|
+
const qualityGates = app.deploy?.cloudflare?.qualityGates ?? {};
|
|
5969
|
+
const budgets = qualityGates.budgets ?? {};
|
|
5970
|
+
const hasPublicRoutes =
|
|
5971
|
+
(publicSurface.publicRoutes ?? []).length > 0 ||
|
|
5972
|
+
(publicSurface.routeEntries ?? []).length > 0 ||
|
|
5973
|
+
(publicSurface.contentSources ?? []).length > 0;
|
|
5974
|
+
|
|
5975
|
+
const robotsRoute = '/robots.txt';
|
|
5976
|
+
const robots = await fetchText(joinUrl(publicUrl, robotsRoute));
|
|
5977
|
+
evidence.assertions.push({
|
|
5978
|
+
type: 'public-surface-robots',
|
|
5979
|
+
route: robotsRoute,
|
|
5980
|
+
status: robots.ok ? 'pass' : 'fail',
|
|
5981
|
+
statusCode: robots.status,
|
|
5982
|
+
});
|
|
5983
|
+
assert(robots.ok, \`\${app.id} robots.txt returned HTTP \${robots.status}\`);
|
|
5984
|
+
assertContentType(evidence, app, robots, {
|
|
5985
|
+
route: robotsRoute,
|
|
5986
|
+
includes: 'text/plain',
|
|
5987
|
+
});
|
|
5988
|
+
assertCloudflareSecurity(evidence, app, robots, robotsRoute, publicUrl);
|
|
5989
|
+
|
|
5990
|
+
if (!hasPublicRoutes) {
|
|
5991
|
+
const disallowsAll = robots.body.includes('Disallow: /');
|
|
5992
|
+
const referencesSitemap = /\\bSitemap:/iu.test(robots.body);
|
|
5993
|
+
evidence.assertions.push({
|
|
5994
|
+
type: 'public-surface-private-robots',
|
|
5995
|
+
route: robotsRoute,
|
|
5996
|
+
disallowsAll,
|
|
5997
|
+
referencesSitemap,
|
|
5998
|
+
status: disallowsAll && !referencesSitemap ? 'pass' : 'fail',
|
|
5999
|
+
});
|
|
6000
|
+
assert(disallowsAll, \`\${app.id} private public surface robots.txt must disallow crawling\`);
|
|
6001
|
+
assert(!referencesSitemap, \`\${app.id} private public surface robots.txt must not reference sitemap.xml\`);
|
|
6002
|
+
return;
|
|
6003
|
+
}
|
|
6004
|
+
|
|
6005
|
+
const sitemapRoute = '/sitemap.xml';
|
|
6006
|
+
const sitemap = await fetchText(joinUrl(publicUrl, sitemapRoute));
|
|
6007
|
+
evidence.assertions.push({
|
|
6008
|
+
type: 'public-surface-sitemap',
|
|
6009
|
+
route: sitemapRoute,
|
|
6010
|
+
status: sitemap.ok ? 'pass' : 'fail',
|
|
6011
|
+
statusCode: sitemap.status,
|
|
6012
|
+
});
|
|
6013
|
+
assert(sitemap.ok, \`\${app.id} sitemap.xml returned HTTP \${sitemap.status}\`);
|
|
6014
|
+
assertContentType(evidence, app, sitemap, {
|
|
6015
|
+
route: sitemapRoute,
|
|
6016
|
+
includes: 'xml',
|
|
6017
|
+
});
|
|
6018
|
+
assertByteBudget(evidence, app, sitemap, {
|
|
6019
|
+
label: 'sitemapXmlMaxBytes',
|
|
6020
|
+
maxBytes: budgets.sitemapXmlMaxBytes ?? ${createPublicWebsiteBudgetFallback('sitemapXmlMaxBytes')},
|
|
6021
|
+
route: sitemapRoute,
|
|
6022
|
+
});
|
|
6023
|
+
|
|
6024
|
+
const sitemapUrl = String(joinUrl(publicUrl, sitemapRoute));
|
|
6025
|
+
const robotsReferencesSitemap = robots.body.includes(\`Sitemap: \${sitemapUrl}\`);
|
|
6026
|
+
evidence.assertions.push({
|
|
6027
|
+
type: 'robots-sitemap-consistency',
|
|
6028
|
+
route: robotsRoute,
|
|
6029
|
+
sitemapUrl,
|
|
6030
|
+
status: robotsReferencesSitemap ? 'pass' : 'fail',
|
|
6031
|
+
});
|
|
6032
|
+
assert(
|
|
6033
|
+
robotsReferencesSitemap,
|
|
6034
|
+
\`\${app.id} robots.txt must reference generated sitemap.xml\`,
|
|
6035
|
+
);
|
|
6036
|
+
|
|
6037
|
+
for (const urlPath of publicSurface.concreteUrlPaths ?? []) {
|
|
6038
|
+
const loc = \`<loc>\${String(joinUrl(publicUrl, urlPath))}</loc>\`;
|
|
6039
|
+
evidence.assertions.push({
|
|
6040
|
+
type: 'sitemap-route',
|
|
6041
|
+
route: urlPath,
|
|
6042
|
+
status: sitemap.body.includes(loc) ? 'pass' : 'fail',
|
|
6043
|
+
});
|
|
6044
|
+
assert(sitemap.body.includes(loc), \`\${app.id} sitemap.xml is missing \${urlPath}\`);
|
|
6045
|
+
}
|
|
6046
|
+
|
|
6047
|
+
const manifestRoute = '/site.webmanifest';
|
|
6048
|
+
const webManifest = await fetchText(joinUrl(publicUrl, manifestRoute));
|
|
6049
|
+
const webManifestJson = parseMaybeJson(webManifest.body);
|
|
6050
|
+
evidence.assertions.push({
|
|
6051
|
+
type: 'public-surface-webmanifest',
|
|
6052
|
+
route: manifestRoute,
|
|
6053
|
+
status: webManifest.ok && webManifestJson ? 'pass' : 'fail',
|
|
6054
|
+
statusCode: webManifest.status,
|
|
6055
|
+
});
|
|
6056
|
+
assert(webManifest.ok, \`\${app.id} site.webmanifest returned HTTP \${webManifest.status}\`);
|
|
6057
|
+
assert(webManifestJson, \`\${app.id} site.webmanifest must be valid JSON\`);
|
|
6058
|
+
assertContentType(evidence, app, webManifest, {
|
|
6059
|
+
route: manifestRoute,
|
|
6060
|
+
includes: 'manifest',
|
|
6061
|
+
});
|
|
6062
|
+
}
|
|
6063
|
+
|
|
4995
6064
|
async function validateApp(app, publicUrl) {
|
|
4996
6065
|
const cloudflare = app.deploy?.cloudflare;
|
|
4997
6066
|
const routes = cloudflare?.routes ?? {};
|
|
@@ -5005,6 +6074,8 @@ async function validateApp(app, publicUrl) {
|
|
|
5005
6074
|
|
|
5006
6075
|
const ssrRoute = routes.ssr ?? '/en';
|
|
5007
6076
|
const ssr = await fetchText(joinUrl(publicUrl, ssrRoute));
|
|
6077
|
+
const qualityGates = cloudflare?.qualityGates ?? {};
|
|
6078
|
+
const budgets = qualityGates.budgets ?? {};
|
|
5008
6079
|
evidence.assertions.push({
|
|
5009
6080
|
type: 'ssr',
|
|
5010
6081
|
route: ssrRoute,
|
|
@@ -5015,6 +6086,18 @@ async function validateApp(app, publicUrl) {
|
|
|
5015
6086
|
assertCloudflareSecurity(evidence, app, ssr, ssrRoute, publicUrl, {
|
|
5016
6087
|
html: true,
|
|
5017
6088
|
});
|
|
6089
|
+
assertContentType(evidence, app, ssr, {
|
|
6090
|
+
route: ssrRoute,
|
|
6091
|
+
includes: 'text/html',
|
|
6092
|
+
});
|
|
6093
|
+
assertByteBudget(evidence, app, ssr, {
|
|
6094
|
+
label: 'ssrHtmlMaxBytes',
|
|
6095
|
+
maxBytes: budgets.ssrHtmlMaxBytes ?? ${createPublicWebsiteBudgetFallback('ssrHtmlMaxBytes')},
|
|
6096
|
+
route: ssrRoute,
|
|
6097
|
+
});
|
|
6098
|
+
await validateSsrHead(evidence, app, publicUrl, ssrRoute, ssr);
|
|
6099
|
+
await validateNotFound(evidence, app, publicUrl);
|
|
6100
|
+
await validatePublicSurface(evidence, app, publicUrl);
|
|
5018
6101
|
|
|
5019
6102
|
const uiMarker = extractUiMarker(ssr.body);
|
|
5020
6103
|
evidence.assertions.push({
|
|
@@ -5054,6 +6137,7 @@ async function validateApp(app, publicUrl) {
|
|
|
5054
6137
|
cssPreloadLinkHeader.includes('as=style'),
|
|
5055
6138
|
\`\${app.id} SSR response is missing CSS preload Link headers\`,
|
|
5056
6139
|
);
|
|
6140
|
+
await validateCssAsset(evidence, app, publicUrl, ssr);
|
|
5057
6141
|
|
|
5058
6142
|
const manifestRoute = routes.mfManifest ?? '/mf-manifest.json';
|
|
5059
6143
|
const manifest = await fetchText(joinUrl(publicUrl, manifestRoute));
|
|
@@ -5069,6 +6153,16 @@ async function validateApp(app, publicUrl) {
|
|
|
5069
6153
|
\`\${app.id} MF manifest returned HTTP \${manifest.status}\`,
|
|
5070
6154
|
);
|
|
5071
6155
|
assertCloudflareSecurity(evidence, app, manifest, manifestRoute, publicUrl);
|
|
6156
|
+
assertContentType(evidence, app, manifest, {
|
|
6157
|
+
route: manifestRoute,
|
|
6158
|
+
includes: 'json',
|
|
6159
|
+
});
|
|
6160
|
+
assertByteBudget(evidence, app, manifest, {
|
|
6161
|
+
label: 'mfManifestMaxBytes',
|
|
6162
|
+
maxBytes: budgets.mfManifestMaxBytes ?? ${createPublicWebsiteBudgetFallback('mfManifestMaxBytes')},
|
|
6163
|
+
route: manifestRoute,
|
|
6164
|
+
});
|
|
6165
|
+
assertNoPublicSourcemapRefs(evidence, app, manifestJson);
|
|
5072
6166
|
evidence.assertions.push({
|
|
5073
6167
|
type: 'mf-manifest-cors',
|
|
5074
6168
|
route: manifestRoute,
|
|
@@ -5109,6 +6203,15 @@ async function validateApp(app, publicUrl) {
|
|
|
5109
6203
|
});
|
|
5110
6204
|
assert(locale.ok, \`\${app.id} locale JSON returned HTTP \${locale.status}\`);
|
|
5111
6205
|
assertCloudflareSecurity(evidence, app, locale, localeRoute, publicUrl);
|
|
6206
|
+
assertContentType(evidence, app, locale, {
|
|
6207
|
+
route: localeRoute,
|
|
6208
|
+
includes: 'json',
|
|
6209
|
+
});
|
|
6210
|
+
assertByteBudget(evidence, app, locale, {
|
|
6211
|
+
label: 'localeJsonMaxBytes',
|
|
6212
|
+
maxBytes: budgets.localeJsonMaxBytes ?? ${createPublicWebsiteBudgetFallback('localeJsonMaxBytes')},
|
|
6213
|
+
route: localeRoute,
|
|
6214
|
+
});
|
|
5112
6215
|
evidence.assertions.push({
|
|
5113
6216
|
type: 'i18n-cors',
|
|
5114
6217
|
route: localeRoute,
|
|
@@ -5146,6 +6249,75 @@ async function validateApp(app, publicUrl) {
|
|
|
5146
6249
|
return evidence;
|
|
5147
6250
|
}
|
|
5148
6251
|
|
|
6252
|
+
export { validateApp };
|
|
6253
|
+
`;
|
|
6254
|
+
}
|
|
6255
|
+
function createCloudflareVersionProofScript() {
|
|
6256
|
+
return `#!/usr/bin/env node
|
|
6257
|
+
import fs from 'node:fs';
|
|
6258
|
+
import path from 'node:path';
|
|
6259
|
+
import { fileURLToPath } from 'node:url';
|
|
6260
|
+
import { validateApp } from './ultramodern-cloudflare-proof.mjs';
|
|
6261
|
+
|
|
6262
|
+
const workspaceRoot = path.resolve(
|
|
6263
|
+
path.dirname(fileURLToPath(import.meta.url)),
|
|
6264
|
+
'..',
|
|
6265
|
+
);
|
|
6266
|
+
const contractPath = path.join(
|
|
6267
|
+
workspaceRoot,
|
|
6268
|
+
'.modernjs/ultramodern-generated-contract.json',
|
|
6269
|
+
);
|
|
6270
|
+
const defaultOut = path.join(
|
|
6271
|
+
workspaceRoot,
|
|
6272
|
+
'.codex/reports/cloudflare-version-proof/public-url-proof.json',
|
|
6273
|
+
);
|
|
6274
|
+
|
|
6275
|
+
function readJson(filePath) {
|
|
6276
|
+
return JSON.parse(fs.readFileSync(filePath, 'utf8'));
|
|
6277
|
+
}
|
|
6278
|
+
|
|
6279
|
+
function parseArgs(argv) {
|
|
6280
|
+
const parsed = {
|
|
6281
|
+
appId: undefined,
|
|
6282
|
+
out: defaultOut,
|
|
6283
|
+
requirePublicUrls: false,
|
|
6284
|
+
};
|
|
6285
|
+
|
|
6286
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
6287
|
+
const arg = argv[index];
|
|
6288
|
+
if (arg === '--app') {
|
|
6289
|
+
parsed.appId = argv[index + 1];
|
|
6290
|
+
index += 1;
|
|
6291
|
+
} else if (arg === '--out') {
|
|
6292
|
+
parsed.out = argv[index + 1];
|
|
6293
|
+
index += 1;
|
|
6294
|
+
} else if (arg === '--require-public-urls') {
|
|
6295
|
+
parsed.requirePublicUrls = true;
|
|
6296
|
+
} else if (arg === '--help' || arg === '-h') {
|
|
6297
|
+
parsed.help = true;
|
|
6298
|
+
} else {
|
|
6299
|
+
throw new Error(\`Unknown argument: \${arg}\`);
|
|
6300
|
+
}
|
|
6301
|
+
}
|
|
6302
|
+
|
|
6303
|
+
return parsed;
|
|
6304
|
+
}
|
|
6305
|
+
|
|
6306
|
+
function printHelp() {
|
|
6307
|
+
process.stdout.write(\`Usage:
|
|
6308
|
+
node scripts/proof-cloudflare-version.mjs [--app workspace] [--out evidence.json] [--require-public-urls]
|
|
6309
|
+
|
|
6310
|
+
Set each app's public URL using the contract env key, for example:
|
|
6311
|
+
ULTRAMODERN_PUBLIC_URL_WORKSPACE=https://workspace.example.workers.dev
|
|
6312
|
+
\`);
|
|
6313
|
+
}
|
|
6314
|
+
|
|
6315
|
+
function assert(condition, message) {
|
|
6316
|
+
if (!condition) {
|
|
6317
|
+
throw new Error(message);
|
|
6318
|
+
}
|
|
6319
|
+
}
|
|
6320
|
+
|
|
5149
6321
|
async function main(argv = process.argv.slice(2)) {
|
|
5150
6322
|
const args = parseArgs(argv);
|
|
5151
6323
|
if (args.help) {
|
|
@@ -5212,10 +6384,13 @@ function writeGeneratedWorkspaceScripts(targetDir, scope, enableTailwind, remote
|
|
|
5212
6384
|
writeFileReplacing(targetDir, "scripts/assert-mf-types.mjs", createAssertMfTypesScript(remotes));
|
|
5213
6385
|
writeFileReplacing(targetDir, "scripts/validate-ultramodern-workspace.mjs", createWorkspaceValidationScript(scope, enableTailwind, remotes));
|
|
5214
6386
|
writeFileReplacing(targetDir, "scripts/check-ultramodern-i18n-boundaries.mjs", createWorkspaceI18nBoundaryValidationScript());
|
|
6387
|
+
writeFileReplacing(targetDir, "scripts/generate-public-surface-assets.mjs", createPublicSurfaceAssetsScript());
|
|
6388
|
+
writeFileReplacing(targetDir, "scripts/ultramodern-cloudflare-proof.mjs", createCloudflareProofHelperScript());
|
|
5215
6389
|
writeFileReplacing(targetDir, "scripts/proof-cloudflare-version.mjs", createCloudflareVersionProofScript());
|
|
5216
6390
|
}
|
|
5217
6391
|
function writeApp(targetDir, scope, app, packageSource, enableTailwind, remotes = []) {
|
|
5218
6392
|
const resolvedApp = 'shell' === app.kind ? createShellHost(remotes) : app;
|
|
6393
|
+
const publicWeb = createPublicWebAppArtifacts(resolvedApp);
|
|
5219
6394
|
const writeAppFile = (relativePath, content)=>{
|
|
5220
6395
|
writeFile(targetDir, `${resolvedApp.directory}/${relativePath}`, content);
|
|
5221
6396
|
};
|
|
@@ -5223,7 +6398,8 @@ function writeApp(targetDir, scope, app, packageSource, enableTailwind, remotes
|
|
|
5223
6398
|
writeJson(targetDir, `${resolvedApp.directory}/tsconfig.json`, createPackageTsConfig(resolvedApp.directory, appHasEffectApi(resolvedApp)));
|
|
5224
6399
|
writeFile(targetDir, `${resolvedApp.directory}/src/modern-app-env.d.ts`, createAppEnvDts(resolvedApp, remotes));
|
|
5225
6400
|
writeFile(targetDir, `${resolvedApp.directory}/src/ultramodern-build.ts`, createUltramodernBuildModule(scope, resolvedApp));
|
|
5226
|
-
writeFile(targetDir,
|
|
6401
|
+
writeFile(targetDir, publicWeb.routeMetadataFile.path, publicWeb.routeMetadataFile.content);
|
|
6402
|
+
writeFile(targetDir, publicWeb.routeHeadFile.path, publicWeb.routeHeadFile.content);
|
|
5227
6403
|
writeFile(targetDir, `${resolvedApp.directory}/modern.config.ts`, createAppModernConfig(scope, resolvedApp));
|
|
5228
6404
|
writeFile(targetDir, `${resolvedApp.directory}/src/modern.runtime.ts`, createAppRuntimeConfig(resolvedApp, scope, remotes));
|
|
5229
6405
|
writeJson(targetDir, `${resolvedApp.directory}/locales/en/translation.json`, createAppPublicLocaleMessages(resolvedApp, 'en', remotes));
|
|
@@ -5239,7 +6415,8 @@ function writeApp(targetDir, scope, app, packageSource, enableTailwind, remotes
|
|
|
5239
6415
|
writeAppFile('src/routes/layout.tsx', createLayout(resolvedApp.id));
|
|
5240
6416
|
for (const [relativePath, content] of Object.entries(workspaceAssetsForApp(resolvedApp)))writeFile(targetDir, `${resolvedApp.directory}/${relativePath}`, content);
|
|
5241
6417
|
writeAppFile('src/routes/[lang]/page.tsx', 'shell' === resolvedApp.kind ? createShellPage(remotes) : createRemotePage(resolvedApp));
|
|
5242
|
-
for (const
|
|
6418
|
+
for (const generatedFile of publicWeb.routeMetaFiles)writeFile(targetDir, generatedFile.path, generatedFile.content);
|
|
6419
|
+
for (const generatedFile of publicWeb.routeAliasFiles)writeFile(targetDir, generatedFile.path, generatedFile.content);
|
|
5243
6420
|
if ('shell' === resolvedApp.kind) {
|
|
5244
6421
|
writeAppFile('src/routes/vertical-components.tsx', createShellRemoteComponents(scope, remotes));
|
|
5245
6422
|
writeAppFile('src/routes/shell-frame.tsx', createShellFrameComponent());
|
|
@@ -5270,12 +6447,7 @@ function writeSharedPackages(targetDir, scope, packageSource) {
|
|
|
5270
6447
|
]
|
|
5271
6448
|
});
|
|
5272
6449
|
}
|
|
5273
|
-
writeFile(targetDir, 'packages/shared-contracts/src/index.ts',
|
|
5274
|
-
ownership: 'topology/ownership.json',
|
|
5275
|
-
preset: 'presetUltramodern',
|
|
5276
|
-
topology: 'topology/reference-topology.json',
|
|
5277
|
-
} as const;
|
|
5278
|
-
`);
|
|
6450
|
+
writeFile(targetDir, 'packages/shared-contracts/src/index.ts', createSharedContractsIndex());
|
|
5279
6451
|
writeFile(targetDir, 'packages/shared-design-tokens/src/index.ts', `export const sharedDesignTokens = {
|
|
5280
6452
|
color: {
|
|
5281
6453
|
accent: '#2f8f68',
|
|
@@ -5351,9 +6523,12 @@ function updateRootWorkspaceScripts(workspaceRoot, scope, packageSource, remotes
|
|
|
5351
6523
|
}
|
|
5352
6524
|
function rewriteShellAppFiles(workspaceRoot, scope, packageSource, enableTailwind, remotes) {
|
|
5353
6525
|
const shellHost = createShellHost(remotes);
|
|
6526
|
+
const publicWeb = createPublicWebAppArtifacts(shellHost);
|
|
5354
6527
|
writeJsonFile(node_path.join(workspaceRoot, `${shellApp.directory}/package.json`), createAppPackage(scope, shellHost, packageSource, enableTailwind, remotes));
|
|
5355
6528
|
writeFileReplacing(workspaceRoot, `${shellApp.directory}/src/modern-app-env.d.ts`, createAppEnvDts(shellHost, remotes));
|
|
5356
|
-
writeFileReplacing(workspaceRoot,
|
|
6529
|
+
writeFileReplacing(workspaceRoot, publicWeb.routeMetadataFile.path, publicWeb.routeMetadataFile.content);
|
|
6530
|
+
writeFileReplacing(workspaceRoot, publicWeb.routeHeadFile.path, publicWeb.routeHeadFile.content);
|
|
6531
|
+
for (const generatedFile of publicWeb.routeMetaFiles)writeFileReplacing(workspaceRoot, generatedFile.path, generatedFile.content);
|
|
5357
6532
|
rewriteWorkspaceAssetsForApp(workspaceRoot, shellHost);
|
|
5358
6533
|
writeFileReplacing(workspaceRoot, `${shellApp.directory}/src/modern.runtime.ts`, createAppRuntimeConfig(shellHost, scope, remotes));
|
|
5359
6534
|
writeJsonFile(node_path.join(workspaceRoot, `${shellApp.directory}/locales/en/translation.json`), createAppPublicLocaleMessages(shellHost, 'en', remotes));
|