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