@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
|
@@ -60,23 +60,23 @@ const TANSTACK_ROUTER_VERSION = '1.170.15';
|
|
|
60
60
|
const MODULE_FEDERATION_VERSION = '2.5.1';
|
|
61
61
|
const ZEPHYR_RSPACK_PLUGIN_VERSION = '1.1.1';
|
|
62
62
|
const ZEPHYR_AGENT_VERSION = '1.1.1';
|
|
63
|
-
const WRANGLER_VERSION = '4.
|
|
63
|
+
const WRANGLER_VERSION = '4.99.0';
|
|
64
64
|
const CLOUDFLARE_COMPATIBILITY_DATE = '2026-06-02';
|
|
65
65
|
const TAILWIND_VERSION = '4.3.0';
|
|
66
66
|
const TAILWIND_POSTCSS_VERSION = '4.3.0';
|
|
67
67
|
const POSTCSS_VERSION = '8.5.15';
|
|
68
|
-
const EFFECT_TSGO_VERSION = '0.14.
|
|
68
|
+
const EFFECT_TSGO_VERSION = '0.14.3';
|
|
69
69
|
const TYPESCRIPT_VERSION = '6.0.3';
|
|
70
|
-
const TYPESCRIPT_NATIVE_PREVIEW_VERSION = '7.0.0-dev.
|
|
71
|
-
const OXLINT_VERSION = '1.
|
|
72
|
-
const OXFMT_VERSION = '0.
|
|
73
|
-
const ULTRACITE_VERSION = '7.8.
|
|
70
|
+
const TYPESCRIPT_NATIVE_PREVIEW_VERSION = '7.0.0-dev.20260610.1';
|
|
71
|
+
const OXLINT_VERSION = '1.69.0';
|
|
72
|
+
const OXFMT_VERSION = '0.54.0';
|
|
73
|
+
const ULTRACITE_VERSION = '7.8.3';
|
|
74
74
|
const LEFTHOOK_VERSION = '^2.1.9';
|
|
75
75
|
const I18NEXT_VERSION = '26.3.1';
|
|
76
76
|
const REACT_VERSION = '^19.2.7';
|
|
77
77
|
const REACT_DOM_VERSION = '^19.2.7';
|
|
78
78
|
const REACT_ROUTER_DOM_VERSION = '7.17.0';
|
|
79
|
-
const PNPM_VERSION = '11.5.
|
|
79
|
+
const PNPM_VERSION = '11.5.3';
|
|
80
80
|
const GENERATED_CONTRACT_PATH = '.modernjs/ultramodern-generated-contract.json';
|
|
81
81
|
const RSTACK_AGENT_SKILLS_COMMIT = '61c948b42512e223bad44b83af4080eba48b2677';
|
|
82
82
|
const MODULE_FEDERATION_AGENT_SKILLS_COMMIT = '07bb5b6c43ad457609e00c081b72d4c42508ec76';
|
|
@@ -673,9 +673,67 @@ function createCloudflareSecurityContract() {
|
|
|
673
673
|
}
|
|
674
674
|
};
|
|
675
675
|
}
|
|
676
|
+
const PUBLIC_WEBSITE_POLICY = {
|
|
677
|
+
qualityGates: {
|
|
678
|
+
publicRoutes: {
|
|
679
|
+
requireSitemapWhenPresent: true,
|
|
680
|
+
requireRobotsSitemapConsistency: true,
|
|
681
|
+
requireWebManifestWhenPresent: true
|
|
682
|
+
},
|
|
683
|
+
statusCodes: {
|
|
684
|
+
notFoundRoute: '/__ultramodern-smoke-missing',
|
|
685
|
+
unknownRouteStatus: 404
|
|
686
|
+
},
|
|
687
|
+
indexing: {
|
|
688
|
+
previewNoindex: true,
|
|
689
|
+
productionPublicRoutesIndexable: true
|
|
690
|
+
},
|
|
691
|
+
assets: {
|
|
692
|
+
cssPreloadRequired: true,
|
|
693
|
+
cssResponseRequired: true,
|
|
694
|
+
cacheControlRequiredForCss: true,
|
|
695
|
+
sourcemapsPubliclyReferenced: false
|
|
696
|
+
},
|
|
697
|
+
budgets: {
|
|
698
|
+
ssrHtmlMaxBytes: 250000,
|
|
699
|
+
mfManifestMaxBytes: 500000,
|
|
700
|
+
localeJsonMaxBytes: 100000,
|
|
701
|
+
sitemapXmlMaxBytes: 500000,
|
|
702
|
+
cssAssetMaxBytes: 750000
|
|
703
|
+
},
|
|
704
|
+
csp: {
|
|
705
|
+
finalMode: 'report-only-dogfood',
|
|
706
|
+
decision: "Report-only remains the generated final mode until public smoke proof records MF SSR script/style/connect compatibility for the deployed surface."
|
|
707
|
+
}
|
|
708
|
+
},
|
|
709
|
+
publicHead: {
|
|
710
|
+
indexableRobots: 'index, follow',
|
|
711
|
+
privateRouteRobots: 'noindex, nofollow'
|
|
712
|
+
},
|
|
713
|
+
publicSurface: {
|
|
714
|
+
defaultProviderFile: 'route.sitemap.mjs',
|
|
715
|
+
draftPolicy: 'omit-draft-by-default',
|
|
716
|
+
indexablePolicy: 'omit-indexable-false'
|
|
717
|
+
}
|
|
718
|
+
};
|
|
676
719
|
function formatTsJsonValue(value, indent) {
|
|
677
720
|
return JSON.stringify(value, null, 2).replaceAll('\n', `\n${' '.repeat(indent)}`);
|
|
678
721
|
}
|
|
722
|
+
function formatIntegerCodeLiteral(value) {
|
|
723
|
+
return String(value).replace(/\B(?=(\d{3})+(?!\d))/gu, '_');
|
|
724
|
+
}
|
|
725
|
+
function createPublicWebsiteQualityGateContract() {
|
|
726
|
+
return PUBLIC_WEBSITE_POLICY.qualityGates;
|
|
727
|
+
}
|
|
728
|
+
function createPublicWebsiteBudgetFallback(budgetName) {
|
|
729
|
+
return formatIntegerCodeLiteral(PUBLIC_WEBSITE_POLICY.qualityGates.budgets[budgetName]);
|
|
730
|
+
}
|
|
731
|
+
function createPublicHeadRobotsPolicy() {
|
|
732
|
+
return PUBLIC_WEBSITE_POLICY.publicHead;
|
|
733
|
+
}
|
|
734
|
+
function createPublicSurfaceContentExpansionPolicy() {
|
|
735
|
+
return PUBLIC_WEBSITE_POLICY.publicSurface;
|
|
736
|
+
}
|
|
679
737
|
function createCloudflareDeployContract(scope, app) {
|
|
680
738
|
return {
|
|
681
739
|
target: 'cloudflare',
|
|
@@ -689,6 +747,7 @@ function createCloudflareDeployContract(scope, app) {
|
|
|
689
747
|
assetsBinding: 'ASSETS',
|
|
690
748
|
routes: createCloudflareProofRoute(app),
|
|
691
749
|
security: createCloudflareSecurityContract(),
|
|
750
|
+
qualityGates: createPublicWebsiteQualityGateContract(),
|
|
692
751
|
evidence: {
|
|
693
752
|
proofScript: "scripts/proof-cloudflare-version.mjs",
|
|
694
753
|
reportDefault: '.codex/reports/cloudflare-version-proof/public-url-proof.json'
|
|
@@ -754,6 +813,9 @@ function createPackageTsConfig(packageDir, includeApi = false) {
|
|
|
754
813
|
};
|
|
755
814
|
}
|
|
756
815
|
function createAppPackage(scope, app, packageSource, enableTailwind, remotes = []) {
|
|
816
|
+
const publicSurfaceBuildCommand = createPublicSurfaceGenerationCommand(app, 'dist');
|
|
817
|
+
const publicSurfaceCloudflareBuildCommand = createPublicSurfaceGenerationCommand(app, 'dist');
|
|
818
|
+
const publicSurfaceCloudflareOutputCommand = createPublicSurfaceGenerationCommand(app, 'cloudflare');
|
|
757
819
|
const packageExports = Object.fromEntries(Object.entries(app.exposes ?? {}).map(([expose, source])=>[
|
|
758
820
|
expose,
|
|
759
821
|
source
|
|
@@ -764,8 +826,8 @@ function createAppPackage(scope, app, packageSource, enableTailwind, remotes = [
|
|
|
764
826
|
version: '0.1.0',
|
|
765
827
|
scripts: {
|
|
766
828
|
dev: 'modern dev',
|
|
767
|
-
build: app.exposes ? `ULTRAMODERN_ZEPHYR=false modern build && node ${relativeRootFor(app.directory)}/scripts/assert-mf-types.mjs` :
|
|
768
|
-
'cloudflare:build':
|
|
829
|
+
build: app.exposes ? `ULTRAMODERN_ZEPHYR=false modern build && ${publicSurfaceBuildCommand} && node ${relativeRootFor(app.directory)}/scripts/assert-mf-types.mjs` : `ULTRAMODERN_ZEPHYR=false modern build && ${publicSurfaceBuildCommand}`,
|
|
830
|
+
'cloudflare:build': `ULTRAMODERN_ZEPHYR=false MODERNJS_DEPLOY=cloudflare modern build && ${publicSurfaceCloudflareBuildCommand} && ULTRAMODERN_ZEPHYR=false MODERNJS_DEPLOY=cloudflare modern deploy && ${publicSurfaceCloudflareOutputCommand}`,
|
|
769
831
|
'cloudflare:deploy': 'ULTRAMODERN_CLOUDFLARE_REQUIRE_PUBLIC_URLS=true pnpm run cloudflare:build && wrangler deploy --config .output/wrangler.json',
|
|
770
832
|
'cloudflare:preview': 'pnpm run cloudflare:build && wrangler dev --config .output/wrangler.json',
|
|
771
833
|
'cloudflare:proof': `node ${relativeRootFor(app.directory)}/scripts/proof-cloudflare-version.mjs --app ${app.id}`,
|
|
@@ -822,6 +884,40 @@ function createSharedPackage(scope, id, description, packageSource) {
|
|
|
822
884
|
};
|
|
823
885
|
return packageJson;
|
|
824
886
|
}
|
|
887
|
+
function createSharedContractsIndex() {
|
|
888
|
+
return `export type UltramodernPublicSitemapChangeFrequency =
|
|
889
|
+
| 'always'
|
|
890
|
+
| 'hourly'
|
|
891
|
+
| 'daily'
|
|
892
|
+
| 'weekly'
|
|
893
|
+
| 'monthly'
|
|
894
|
+
| 'yearly'
|
|
895
|
+
| 'never';
|
|
896
|
+
|
|
897
|
+
export type UltramodernPublicSitemapEntry = {
|
|
898
|
+
/**
|
|
899
|
+
* Params used to expand every localized route pattern, for example
|
|
900
|
+
* { slug: 'platform-story' } for /talks/:slug.
|
|
901
|
+
*/
|
|
902
|
+
params: Record<string, string | number | boolean>;
|
|
903
|
+
/**
|
|
904
|
+
* Per-locale overrides when translated URLs use translated params.
|
|
905
|
+
*/
|
|
906
|
+
localeParams?: Partial<Record<'en' | 'cs', Record<string, string | number | boolean>>>;
|
|
907
|
+
draft?: boolean;
|
|
908
|
+
indexable?: boolean;
|
|
909
|
+
lastModified?: string;
|
|
910
|
+
changeFrequency?: UltramodernPublicSitemapChangeFrequency;
|
|
911
|
+
priority?: number;
|
|
912
|
+
};
|
|
913
|
+
|
|
914
|
+
export const ultramodernWorkspaceContract = {
|
|
915
|
+
ownership: 'topology/ownership.json',
|
|
916
|
+
preset: 'presetUltramodern',
|
|
917
|
+
topology: 'topology/reference-topology.json',
|
|
918
|
+
} as const;
|
|
919
|
+
`;
|
|
920
|
+
}
|
|
825
921
|
function createAppModernConfig(scope, app) {
|
|
826
922
|
const bffImport = appHasEffectApi(app) ? "import { bffPlugin } from '@modern-js/plugin-bff';\n" : '';
|
|
827
923
|
const bffConfig = appHasEffectApi(app) ? ` bff: {
|
|
@@ -887,11 +983,20 @@ const inferredCloudflareUrl =
|
|
|
887
983
|
cloudflareDeployEnabled && cloudflareWorkersDevSubdomain !== undefined
|
|
888
984
|
? \`https://\${cloudflareWorkerName}.\${cloudflareWorkersDevSubdomain}.workers.dev\`
|
|
889
985
|
: undefined;
|
|
986
|
+
// Site origin (SEO: canonical/hreflang URLs) prefers the site-wide public URL;
|
|
987
|
+
// the per-app deployment URL only fills in when no site origin is configured.
|
|
890
988
|
const siteUrl =
|
|
891
|
-
configuredCloudflareUrl ||
|
|
892
989
|
configuredSiteUrl ||
|
|
990
|
+
configuredCloudflareUrl ||
|
|
893
991
|
inferredCloudflareUrl ||
|
|
894
992
|
\`http://localhost:\${port}\`;
|
|
993
|
+
// Asset origin prefers the per-app deployment URL (each MF app serves its own
|
|
994
|
+
// assets). Without an explicit public URL, assets must stay origin-relative so
|
|
995
|
+
// the app works behind tunnels and proxies (an absolute localhost assetPrefix
|
|
996
|
+
// makes pages served via e.g. ngrok fetch scripts from localhost, which Chrome
|
|
997
|
+
// blocks behind a Local Network Access permission prompt).
|
|
998
|
+
const assetPrefix =
|
|
999
|
+
configuredCloudflareUrl || configuredSiteUrl || inferredCloudflareUrl || '/';
|
|
895
1000
|
|
|
896
1001
|
if (
|
|
897
1002
|
cloudflareDeployEnabled &&
|
|
@@ -920,11 +1025,16 @@ ${bffConfig} ...(cloudflareDeployEnabled
|
|
|
920
1025
|
},
|
|
921
1026
|
}
|
|
922
1027
|
: {}),
|
|
1028
|
+
dev: {
|
|
1029
|
+
// Keep dev assets origin-relative too; the default absolute
|
|
1030
|
+
// http://localhost:<port> prefix breaks pages served through tunnels.
|
|
1031
|
+
assetPrefix: '/',
|
|
1032
|
+
},
|
|
923
1033
|
html: {
|
|
924
1034
|
outputStructure: 'flat',
|
|
925
1035
|
},
|
|
926
1036
|
output: {
|
|
927
|
-
assetPrefix
|
|
1037
|
+
assetPrefix,
|
|
928
1038
|
disableTsChecker: true,
|
|
929
1039
|
distPath: {
|
|
930
1040
|
html: './',
|
|
@@ -1149,7 +1259,7 @@ export default createModuleFederationConfig({
|
|
|
1149
1259
|
dts: {
|
|
1150
1260
|
displayErrorInTerminal: true,
|
|
1151
1261
|
generateTypes: {
|
|
1152
|
-
compilerInstance: '
|
|
1262
|
+
compilerInstance: 'tsgo',
|
|
1153
1263
|
},
|
|
1154
1264
|
},
|
|
1155
1265
|
filename: 'remoteEntry.js',
|
|
@@ -1207,7 +1317,7 @@ export default createModuleFederationConfig({
|
|
|
1207
1317
|
dts: {
|
|
1208
1318
|
displayErrorInTerminal: true,
|
|
1209
1319
|
generateTypes: {
|
|
1210
|
-
compilerInstance: '
|
|
1320
|
+
compilerInstance: 'tsgo',
|
|
1211
1321
|
},
|
|
1212
1322
|
},
|
|
1213
1323
|
exposes: ${exposes},
|
|
@@ -1229,6 +1339,7 @@ const privateAppRoutePublicness = {
|
|
|
1229
1339
|
function createRouteOwnedI18nPaths(app) {
|
|
1230
1340
|
const namespace = appI18nNamespace(app);
|
|
1231
1341
|
const base = {
|
|
1342
|
+
descriptionKey: `${namespace}.seo.description`,
|
|
1232
1343
|
mfBoundaryId: app.mfName,
|
|
1233
1344
|
namespace,
|
|
1234
1345
|
ownerAppId: app.id,
|
|
@@ -1430,6 +1541,7 @@ function createPublicRouteMetadata(app) {
|
|
|
1430
1541
|
localisedPaths: route.localisedPaths,
|
|
1431
1542
|
namespace: route.namespace,
|
|
1432
1543
|
ownerAppId: route.ownerAppId,
|
|
1544
|
+
descriptionKey: route.descriptionKey,
|
|
1433
1545
|
titleKey: route.titleKey
|
|
1434
1546
|
}));
|
|
1435
1547
|
}
|
|
@@ -1438,7 +1550,11 @@ function createRouteMetadataModule(app) {
|
|
|
1438
1550
|
const localisedUrls = sortJsonValue(createLocalisedUrlsMap(app));
|
|
1439
1551
|
const publicRoutes = sortJsonValue(createPublicRouteMetadata(app));
|
|
1440
1552
|
const namespace = appI18nNamespace(app);
|
|
1441
|
-
return
|
|
1553
|
+
return `// @generated by @modern-js/create.
|
|
1554
|
+
// Author route metadata in colocated src/routes/**/route.meta.ts files.
|
|
1555
|
+
// This compatibility manifest is regenerated from route-owned metadata.
|
|
1556
|
+
|
|
1557
|
+
export const ultramodernRouteNamespace = '${namespace}' as const;
|
|
1442
1558
|
|
|
1443
1559
|
export const ultramodernRouteMetadata = ${JSON.stringify(routes, null, 2)} as const;
|
|
1444
1560
|
|
|
@@ -1447,6 +1563,8 @@ export const ultramodernLocalisedUrls = ${JSON.stringify(localisedUrls, null, 2)
|
|
|
1447
1563
|
export const ultramodernPublicRoutes = ${JSON.stringify(publicRoutes, null, 2)} as const;
|
|
1448
1564
|
|
|
1449
1565
|
export const ultramodernRouteConfig = {
|
|
1566
|
+
authoring: 'colocated-route-meta',
|
|
1567
|
+
generatedManifest: true,
|
|
1450
1568
|
localisedUrls: ultramodernLocalisedUrls,
|
|
1451
1569
|
namespace: ultramodernRouteNamespace,
|
|
1452
1570
|
publicRoutes: ultramodernPublicRoutes,
|
|
@@ -1455,20 +1573,52 @@ export const ultramodernRouteConfig = {
|
|
|
1455
1573
|
} as const;
|
|
1456
1574
|
`;
|
|
1457
1575
|
}
|
|
1576
|
+
function createRouteMetaModule(route) {
|
|
1577
|
+
return `const routeMeta = ${JSON.stringify(sortJsonValue(route), null, 2)} as const;
|
|
1578
|
+
|
|
1579
|
+
export default routeMeta;
|
|
1580
|
+
export { routeMeta };
|
|
1581
|
+
`;
|
|
1582
|
+
}
|
|
1583
|
+
function normalisePublicPath(pathname) {
|
|
1584
|
+
const normalised = pathname.trim().replaceAll(/\/+/gu, '/').replace(/\/+$/u, '');
|
|
1585
|
+
return normalised.length > 0 && normalised.startsWith('/') ? normalised : `/${normalised}`;
|
|
1586
|
+
}
|
|
1587
|
+
function splitPublicPathSegments(pathname) {
|
|
1588
|
+
return normalisePublicPath(pathname).split('/').filter(Boolean);
|
|
1589
|
+
}
|
|
1590
|
+
function routePathParamName(segment) {
|
|
1591
|
+
if (segment.startsWith(':')) return segment.slice(1).replace(/[?*+]$/u, '');
|
|
1592
|
+
if (segment.startsWith('[') && segment.endsWith(']')) return segment.slice(1, -1).replace(/^\.\.\./u, '').replace(/\$$/u, '');
|
|
1593
|
+
}
|
|
1594
|
+
function isDynamicPublicPathSegment(segment) {
|
|
1595
|
+
return void 0 !== routePathParamName(segment) || segment.includes('*') || segment.startsWith('[');
|
|
1596
|
+
}
|
|
1597
|
+
function isConcretePublicPath(pathname) {
|
|
1598
|
+
return !splitPublicPathSegments(pathname).some(isDynamicPublicPathSegment);
|
|
1599
|
+
}
|
|
1458
1600
|
function routeSegmentToDirectory(segment) {
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
return segment.endsWith('?') ? `[${name}$]` : `[${name}]`;
|
|
1462
|
-
}
|
|
1601
|
+
const paramName = routePathParamName(segment);
|
|
1602
|
+
if (paramName && segment.startsWith(':')) return segment.endsWith('?') ? `[${paramName}$]` : `[${paramName}]`;
|
|
1463
1603
|
return segment;
|
|
1464
1604
|
}
|
|
1605
|
+
function routePathDirectorySegments(routePath) {
|
|
1606
|
+
return splitPublicPathSegments(routePath).map(routeSegmentToDirectory);
|
|
1607
|
+
}
|
|
1465
1608
|
function createRoutePageFilePath(app, canonicalPath) {
|
|
1466
|
-
const segments = canonicalPath
|
|
1609
|
+
const segments = routePathDirectorySegments(canonicalPath);
|
|
1467
1610
|
return `${app.directory}/src/routes/[lang]/${[
|
|
1468
1611
|
...segments,
|
|
1469
1612
|
'page.tsx'
|
|
1470
1613
|
].join('/')}`;
|
|
1471
1614
|
}
|
|
1615
|
+
function createRouteMetaFilePath(app, canonicalPath) {
|
|
1616
|
+
const segments = routePathDirectorySegments(canonicalPath);
|
|
1617
|
+
return `${app.directory}/src/routes/[lang]/${[
|
|
1618
|
+
...segments,
|
|
1619
|
+
'route.meta.ts'
|
|
1620
|
+
].join('/')}`;
|
|
1621
|
+
}
|
|
1472
1622
|
function createRouteAliasPage(canonicalPath) {
|
|
1473
1623
|
const depth = canonicalPath.split('/').filter(Boolean).length;
|
|
1474
1624
|
const rootPageImport = `${'../'.repeat(depth)}page`;
|
|
@@ -1626,24 +1776,22 @@ export default {} satisfies Config;
|
|
|
1626
1776
|
function createTw(prefix) {
|
|
1627
1777
|
return (classList)=>classList.split(/\s+/u).filter(Boolean).map((candidate)=>`${prefix}:${candidate.replace(/\[&&\]:/gu, '')}`).join(' ');
|
|
1628
1778
|
}
|
|
1629
|
-
const
|
|
1630
|
-
'config/public/robots.txt'
|
|
1631
|
-
];
|
|
1632
|
-
const publicSurfaceOptionalAssetPaths = [
|
|
1779
|
+
const publicSurfaceManagedSourceAssetPaths = [
|
|
1780
|
+
'config/public/robots.txt',
|
|
1633
1781
|
'config/public/sitemap.xml',
|
|
1634
1782
|
'config/public/site.webmanifest'
|
|
1635
1783
|
];
|
|
1636
|
-
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
|
|
1784
|
+
const publicSurfaceBaseOutputFiles = [
|
|
1785
|
+
'robots.txt'
|
|
1786
|
+
];
|
|
1787
|
+
const publicSurfacePublicRouteOutputFiles = [
|
|
1788
|
+
'sitemap.xml',
|
|
1789
|
+
'site.webmanifest'
|
|
1790
|
+
];
|
|
1640
1791
|
function createLocalisedPublicPath(pathname, language) {
|
|
1641
1792
|
const publicPath = normalisePublicPath(pathname);
|
|
1642
1793
|
return '/' === publicPath ? `/${language}` : `/${language}${publicPath}`;
|
|
1643
1794
|
}
|
|
1644
|
-
function isConcretePublicPath(pathname) {
|
|
1645
|
-
return !normalisePublicPath(pathname).split('/').some((segment)=>segment.startsWith(':') || segment.includes('*') || segment.startsWith('['));
|
|
1646
|
-
}
|
|
1647
1795
|
function uniqueSorted(values) {
|
|
1648
1796
|
return Array.from(new Set(values)).sort((left, right)=>left.localeCompare(right));
|
|
1649
1797
|
}
|
|
@@ -1661,94 +1809,45 @@ function createPublicSurfaceRouteEntries(app) {
|
|
|
1661
1809
|
};
|
|
1662
1810
|
}).filter((route)=>void 0 !== route).sort((left, right)=>left.canonicalUrlPath.localeCompare(right.canonicalUrlPath) || left.id.localeCompare(right.id));
|
|
1663
1811
|
}
|
|
1812
|
+
function createPublicSurfaceContentSources(_app) {
|
|
1813
|
+
return [];
|
|
1814
|
+
}
|
|
1664
1815
|
function createPublicSurfaceUrlPaths(app) {
|
|
1665
1816
|
return uniqueSorted(createPublicSurfaceRouteEntries(app).flatMap((route)=>supportedWorkspaceLanguages.map((language)=>route.localeUrlPaths[language])));
|
|
1666
1817
|
}
|
|
1667
|
-
function
|
|
1668
|
-
return
|
|
1669
|
-
|
|
1670
|
-
|
|
1671
|
-
return value.replaceAll('&', '&').replaceAll('<', '<').replaceAll('>', '>');
|
|
1672
|
-
}
|
|
1673
|
-
function escapeXmlAttribute(value) {
|
|
1674
|
-
return escapeXmlText(value).replaceAll('"', '"');
|
|
1675
|
-
}
|
|
1676
|
-
function renderRobotsTxt(app) {
|
|
1677
|
-
const urlPaths = createPublicSurfaceUrlPaths(app);
|
|
1678
|
-
const lines = [
|
|
1679
|
-
'User-agent: *'
|
|
1680
|
-
];
|
|
1681
|
-
if (0 === urlPaths.length) lines.push('Disallow: /');
|
|
1682
|
-
else {
|
|
1683
|
-
for (const urlPath of urlPaths)lines.push(`Allow: ${urlPath}$`);
|
|
1684
|
-
lines.push('Disallow: /');
|
|
1685
|
-
lines.push(`Sitemap: ${createPublicSurfaceOrigin(app)}/sitemap.xml`);
|
|
1686
|
-
}
|
|
1687
|
-
return `${lines.join('\n')}\n`;
|
|
1688
|
-
}
|
|
1689
|
-
function renderSitemapXml(app) {
|
|
1690
|
-
const origin = createPublicSurfaceOrigin(app);
|
|
1691
|
-
const routes = createPublicSurfaceRouteEntries(app);
|
|
1692
|
-
const lines = [
|
|
1693
|
-
'<?xml version="1.0" encoding="UTF-8"?>',
|
|
1694
|
-
'<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9" xmlns:xhtml="http://www.w3.org/1999/xhtml">'
|
|
1818
|
+
function createPublicSurfaceOutputFiles(app) {
|
|
1819
|
+
return [
|
|
1820
|
+
...publicSurfaceBaseOutputFiles,
|
|
1821
|
+
...createPublicRouteMetadata(app).length > 0 ? publicSurfacePublicRouteOutputFiles : []
|
|
1695
1822
|
];
|
|
1696
|
-
for (const route of routes)for (const language of supportedWorkspaceLanguages){
|
|
1697
|
-
lines.push(' <url>');
|
|
1698
|
-
lines.push(` <loc>${escapeXmlText(`${origin}${route.localeUrlPaths[language]}`)}</loc>`);
|
|
1699
|
-
for (const alternateLanguage of supportedWorkspaceLanguages)lines.push(` <xhtml:link rel="alternate" hreflang="${alternateLanguage}" href="${escapeXmlAttribute(`${origin}${route.localeUrlPaths[alternateLanguage]}`)}" />`);
|
|
1700
|
-
lines.push(` <xhtml:link rel="alternate" hreflang="x-default" href="${escapeXmlAttribute(`${origin}${route.localeUrlPaths.en}`)}" />`);
|
|
1701
|
-
lines.push(' </url>');
|
|
1702
|
-
}
|
|
1703
|
-
lines.push('</urlset>');
|
|
1704
|
-
return `${lines.join('\n')}\n`;
|
|
1705
|
-
}
|
|
1706
|
-
function renderWebManifest(app) {
|
|
1707
|
-
const startUrl = createPublicSurfaceUrlPaths(app)[0];
|
|
1708
|
-
const manifest = {
|
|
1709
|
-
name: app.displayName,
|
|
1710
|
-
short_name: app.displayName,
|
|
1711
|
-
display: 'standalone',
|
|
1712
|
-
background_color: '#ffffff',
|
|
1713
|
-
theme_color: '#133225',
|
|
1714
|
-
lang: 'en',
|
|
1715
|
-
categories: [
|
|
1716
|
-
'business',
|
|
1717
|
-
'productivity'
|
|
1718
|
-
],
|
|
1719
|
-
icons: [],
|
|
1720
|
-
...startUrl ? {
|
|
1721
|
-
scope: '/',
|
|
1722
|
-
start_url: startUrl
|
|
1723
|
-
} : {}
|
|
1724
|
-
};
|
|
1725
|
-
return `${JSON.stringify(sortJsonValue(manifest), null, 2)}\n`;
|
|
1726
1823
|
}
|
|
1727
|
-
function
|
|
1728
|
-
|
|
1729
|
-
'config/public/robots.txt': renderRobotsTxt(app)
|
|
1730
|
-
};
|
|
1731
|
-
if (createPublicSurfaceRouteEntries(app).length > 0) {
|
|
1732
|
-
assets['config/public/sitemap.xml'] = renderSitemapXml(app);
|
|
1733
|
-
assets['config/public/site.webmanifest'] = renderWebManifest(app);
|
|
1734
|
-
}
|
|
1735
|
-
return assets;
|
|
1824
|
+
function createPublicSurfaceGenerationCommand(app, target, requirePublicOrigin = false) {
|
|
1825
|
+
return `node ${relativeRootFor(app.directory)}/scripts/generate-public-surface-assets.mjs --app ${app.id} --target ${target}${requirePublicOrigin ? ' --require-public-origin' : ''}`;
|
|
1736
1826
|
}
|
|
1737
1827
|
function workspaceAssetsForApp(app) {
|
|
1738
|
-
return
|
|
1828
|
+
return {};
|
|
1739
1829
|
}
|
|
1740
1830
|
function rewriteWorkspaceAssetsForApp(workspaceRoot, app) {
|
|
1831
|
+
for (const relativePath of publicSurfaceManagedSourceAssetPaths)external_node_fs_default().rmSync(external_node_path_default().join(workspaceRoot, app.directory, relativePath), {
|
|
1832
|
+
force: true
|
|
1833
|
+
});
|
|
1741
1834
|
for (const [relativePath, content] of Object.entries(workspaceAssetsForApp(app)))writeFileReplacing(workspaceRoot, `${app.directory}/${relativePath}`, content);
|
|
1742
1835
|
}
|
|
1743
|
-
function
|
|
1744
|
-
|
|
1836
|
+
function createRouteHeadModule(app) {
|
|
1837
|
+
const robotsPolicy = createPublicHeadRobotsPolicy();
|
|
1838
|
+
return `import { useLocalizedLocation, useModernI18n } from '@modern-js/plugin-i18n/runtime';
|
|
1839
|
+
import { Helmet } from '@modern-js/runtime/head';
|
|
1840
|
+
import {
|
|
1841
|
+
ultramodernRouteMetadata,
|
|
1842
|
+
} from './ultramodern-route-metadata';
|
|
1843
|
+
|
|
1844
|
+
const appName = ${JSON.stringify(app.displayName)};
|
|
1845
|
+
const fallbackLanguage = 'en';
|
|
1745
1846
|
const supportedLanguages = ['en', 'cs'] as const;
|
|
1746
1847
|
type SupportedLanguage = (typeof supportedLanguages)[number];
|
|
1848
|
+
type RouteMetadata = (typeof ultramodernRouteMetadata)[number];
|
|
1747
1849
|
|
|
1748
|
-
const
|
|
1749
|
-
string,
|
|
1750
|
-
Record<SupportedLanguage, string>
|
|
1751
|
-
>;
|
|
1850
|
+
const routeMetadata = ultramodernRouteMetadata as readonly RouteMetadata[];
|
|
1752
1851
|
|
|
1753
1852
|
const isSupportedLanguage = (value: string): value is SupportedLanguage =>
|
|
1754
1853
|
supportedLanguages.includes(value as SupportedLanguage);
|
|
@@ -1799,55 +1898,24 @@ const matchPattern = (pathname: string, pattern: string) => {
|
|
|
1799
1898
|
return params;
|
|
1800
1899
|
};
|
|
1801
1900
|
|
|
1802
|
-
const
|
|
1803
|
-
const path = normalisePath(pattern)
|
|
1804
|
-
.split('/')
|
|
1805
|
-
.filter(Boolean)
|
|
1806
|
-
.map(segment => {
|
|
1807
|
-
if (!segment.startsWith(':')) {
|
|
1808
|
-
return segment;
|
|
1809
|
-
}
|
|
1810
|
-
const value = params[paramName(segment)];
|
|
1811
|
-
return value !== undefined && value.length > 0
|
|
1812
|
-
? encodeURIComponent(value)
|
|
1813
|
-
: '';
|
|
1814
|
-
})
|
|
1815
|
-
.filter(Boolean)
|
|
1816
|
-
.join('/');
|
|
1817
|
-
|
|
1818
|
-
return \`/\${path}\`;
|
|
1819
|
-
};
|
|
1820
|
-
|
|
1821
|
-
const resolveLocalisedPath = (
|
|
1822
|
-
pathname: string,
|
|
1823
|
-
targetLanguage: SupportedLanguage,
|
|
1824
|
-
) => {
|
|
1901
|
+
const resolveRouteMetadata = (pathname: string) => {
|
|
1825
1902
|
const pathWithoutLanguage = stripLanguagePrefix(pathname);
|
|
1826
1903
|
|
|
1827
|
-
for (const
|
|
1828
|
-
const
|
|
1829
|
-
if (
|
|
1830
|
-
|
|
1904
|
+
for (const route of routeMetadata) {
|
|
1905
|
+
const canonicalParams = matchPattern(pathWithoutLanguage, route.canonicalPath);
|
|
1906
|
+
if (canonicalParams !== undefined) {
|
|
1907
|
+
return route;
|
|
1831
1908
|
}
|
|
1832
1909
|
|
|
1833
1910
|
for (const language of supportedLanguages) {
|
|
1834
|
-
const
|
|
1835
|
-
const params =
|
|
1836
|
-
sourcePattern === undefined
|
|
1837
|
-
? undefined
|
|
1838
|
-
: matchPattern(pathWithoutLanguage, sourcePattern);
|
|
1911
|
+
const params = matchPattern(pathWithoutLanguage, route.localisedPaths[language]);
|
|
1839
1912
|
if (params !== undefined) {
|
|
1840
|
-
return
|
|
1913
|
+
return route;
|
|
1841
1914
|
}
|
|
1842
1915
|
}
|
|
1843
1916
|
}
|
|
1844
1917
|
|
|
1845
|
-
return
|
|
1846
|
-
};
|
|
1847
|
-
|
|
1848
|
-
const localizedPath = (pathname: string, language: SupportedLanguage) => {
|
|
1849
|
-
const pathWithoutLanguage = resolveLocalisedPath(pathname, language);
|
|
1850
|
-
return pathWithoutLanguage === '/' ? \`/\${language}\` : \`/\${language}\${pathWithoutLanguage}\`;
|
|
1918
|
+
return routeMetadata[0];
|
|
1851
1919
|
};
|
|
1852
1920
|
|
|
1853
1921
|
const absoluteUrl = (pathname: string) => {
|
|
@@ -1855,26 +1923,68 @@ const absoluteUrl = (pathname: string) => {
|
|
|
1855
1923
|
return \`\${origin}\${pathname}\`;
|
|
1856
1924
|
};
|
|
1857
1925
|
|
|
1858
|
-
const
|
|
1859
|
-
|
|
1860
|
-
|
|
1926
|
+
const sanitiseJsonLd = (value: unknown) =>
|
|
1927
|
+
JSON.stringify(value).replaceAll('<', '\\\\u003c');
|
|
1928
|
+
|
|
1929
|
+
export const UltramodernRouteHead = () => {
|
|
1930
|
+
const { i18nInstance } = useModernI18n();
|
|
1931
|
+
const t = i18nInstance['t'].bind(i18nInstance);
|
|
1932
|
+
const { canonical, alternates } = useLocalizedLocation();
|
|
1933
|
+
const route = resolveRouteMetadata(canonical);
|
|
1934
|
+
const title = route ? t(route.titleKey) : appName;
|
|
1935
|
+
const description = route ? t(route.descriptionKey) : appName;
|
|
1936
|
+
const canonicalUrl = absoluteUrl(alternates[fallbackLanguage] ?? \`/\${fallbackLanguage}\`);
|
|
1937
|
+
const indexable = route?.public === true && route?.indexable === true;
|
|
1938
|
+
const jsonLd = indexable
|
|
1939
|
+
? {
|
|
1940
|
+
'@context': 'https://schema.org',
|
|
1941
|
+
'@type': 'WebPage',
|
|
1942
|
+
description,
|
|
1943
|
+
inLanguage: supportedLanguages.join(','),
|
|
1944
|
+
isPartOf: {
|
|
1945
|
+
'@type': 'WebSite',
|
|
1946
|
+
name: appName,
|
|
1947
|
+
url: absoluteUrl('/'),
|
|
1948
|
+
},
|
|
1949
|
+
name: title,
|
|
1950
|
+
url: canonicalUrl,
|
|
1951
|
+
}
|
|
1952
|
+
: undefined;
|
|
1861
1953
|
|
|
1862
1954
|
return (
|
|
1863
|
-
<Helmet>
|
|
1864
|
-
<
|
|
1865
|
-
{
|
|
1866
|
-
|
|
1867
|
-
|
|
1868
|
-
|
|
1869
|
-
|
|
1870
|
-
|
|
1871
|
-
|
|
1872
|
-
|
|
1873
|
-
|
|
1874
|
-
|
|
1875
|
-
|
|
1876
|
-
|
|
1877
|
-
|
|
1955
|
+
<Helmet htmlAttributes={{ lang: i18nInstance.language ?? fallbackLanguage }}>
|
|
1956
|
+
<title>{title}</title>
|
|
1957
|
+
<meta content={description} name="description" />
|
|
1958
|
+
<meta content={indexable ? '${robotsPolicy.indexableRobots}' : '${robotsPolicy.privateRouteRobots}'} name="robots" />
|
|
1959
|
+
{indexable && (
|
|
1960
|
+
<>
|
|
1961
|
+
<link rel="canonical" href={canonicalUrl} />
|
|
1962
|
+
{supportedLanguages.map(code => (
|
|
1963
|
+
<link
|
|
1964
|
+
href={absoluteUrl(alternates[code] ?? \`/\${code}\`)}
|
|
1965
|
+
hrefLang={code}
|
|
1966
|
+
key={code}
|
|
1967
|
+
rel="alternate"
|
|
1968
|
+
/>
|
|
1969
|
+
))}
|
|
1970
|
+
<link
|
|
1971
|
+
href={absoluteUrl(alternates[fallbackLanguage] ?? \`/\${fallbackLanguage}\`)}
|
|
1972
|
+
hrefLang="x-default"
|
|
1973
|
+
rel="alternate"
|
|
1974
|
+
/>
|
|
1975
|
+
<meta content={title} property="og:title" />
|
|
1976
|
+
<meta content={description} property="og:description" />
|
|
1977
|
+
<meta content={canonicalUrl} property="og:url" />
|
|
1978
|
+
<meta content="website" property="og:type" />
|
|
1979
|
+
<meta content={i18nInstance.language ?? fallbackLanguage} property="og:locale" />
|
|
1980
|
+
<meta content="summary_large_image" name="twitter:card" />
|
|
1981
|
+
<meta content={title} name="twitter:title" />
|
|
1982
|
+
<meta content={description} name="twitter:description" />
|
|
1983
|
+
{jsonLd && (
|
|
1984
|
+
<script type="application/ld+json">{sanitiseJsonLd(jsonLd)}</script>
|
|
1985
|
+
)}
|
|
1986
|
+
</>
|
|
1987
|
+
)}
|
|
1878
1988
|
</Helmet>
|
|
1879
1989
|
);
|
|
1880
1990
|
};
|
|
@@ -1883,31 +1993,28 @@ const LocalizedHead = () => {
|
|
|
1883
1993
|
function createShellPage(remotes = []) {
|
|
1884
1994
|
const tw = createTw(tailwindPrefixForApp(shellApp));
|
|
1885
1995
|
const remoteCount = String(remotes.length);
|
|
1886
|
-
return `import {
|
|
1887
|
-
import { Helmet } from '@modern-js/runtime/head';
|
|
1888
|
-
import { useLocation } from '@modern-js/plugin-tanstack/runtime';
|
|
1996
|
+
return `import { Link, useModernI18n } from '@modern-js/plugin-i18n/runtime';
|
|
1889
1997
|
import ShellFrame from '../shell-frame';
|
|
1998
|
+
import { UltramodernRouteHead } from '../ultramodern-route-head';
|
|
1890
1999
|
import { VerticalShowcase } from '../vertical-components';
|
|
1891
|
-
import { ultramodernLocalisedUrls } from '../ultramodern-route-metadata';
|
|
1892
2000
|
import { ultramodernUiMarker } from '../../ultramodern-build';
|
|
1893
2001
|
|
|
1894
|
-
${createLocalizedHeadComponent()}
|
|
1895
2002
|
export default function ShellHome() {
|
|
1896
2003
|
const { i18nInstance } = useModernI18n();
|
|
1897
2004
|
const t = i18nInstance['t'].bind(i18nInstance);
|
|
1898
2005
|
|
|
1899
2006
|
return (
|
|
1900
2007
|
<ShellFrame>
|
|
1901
|
-
<
|
|
2008
|
+
<UltramodernRouteHead />
|
|
1902
2009
|
<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')}">
|
|
1903
2010
|
<div className="${tw('min-w-0')}">
|
|
1904
2011
|
<p className="${tw('text-xs font-black uppercase tracking-[0.18em] text-emerald-800')}">{t('shell.hero.eyebrow')}</p>
|
|
1905
2012
|
<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>
|
|
1906
2013
|
<p className="${tw('mt-5 max-w-2xl text-lg leading-8 text-stone-600')}">{t('shell.hero.lede')}</p>
|
|
1907
2014
|
<div className="${tw('mt-7 flex flex-wrap gap-3')}">
|
|
1908
|
-
<
|
|
2015
|
+
<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="/">
|
|
1909
2016
|
{t('shell.hero.primary')}
|
|
1910
|
-
</
|
|
2017
|
+
</Link>
|
|
1911
2018
|
<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')}">
|
|
1912
2019
|
{t('shell.hero.secondary')}
|
|
1913
2020
|
</span>
|
|
@@ -1940,145 +2047,18 @@ export default function ShellHome() {
|
|
|
1940
2047
|
}
|
|
1941
2048
|
function createShellFrameComponent() {
|
|
1942
2049
|
const tw = createTw(tailwindPrefixForApp(shellApp));
|
|
1943
|
-
return `import { useModernI18n } from '@modern-js/plugin-i18n/runtime';
|
|
1944
|
-
import { useLocation } from '@modern-js/plugin-tanstack/runtime';
|
|
2050
|
+
return `import { useLocalizedLocation, useModernI18n } from '@modern-js/plugin-i18n/runtime';
|
|
1945
2051
|
import type { ReactNode } from 'react';
|
|
1946
2052
|
import { Header, StatusBadge } from './vertical-components';
|
|
1947
|
-
import { ultramodernLocalisedUrls } from './ultramodern-route-metadata';
|
|
1948
|
-
|
|
1949
|
-
const supportedLanguages = ['en', 'cs'] as const;
|
|
1950
|
-
type SupportedLanguage = (typeof supportedLanguages)[number];
|
|
1951
2053
|
|
|
1952
2054
|
interface ShellFrameProps {
|
|
1953
2055
|
children: ReactNode;
|
|
1954
2056
|
}
|
|
1955
2057
|
|
|
1956
|
-
const localisedUrls = ultramodernLocalisedUrls as Record<
|
|
1957
|
-
string,
|
|
1958
|
-
Record<SupportedLanguage, string>
|
|
1959
|
-
>;
|
|
1960
|
-
|
|
1961
|
-
const isSupportedLanguage = (value: string): value is SupportedLanguage =>
|
|
1962
|
-
supportedLanguages.includes(value as SupportedLanguage);
|
|
1963
|
-
|
|
1964
|
-
const normalisePath = (pathname: string) => {
|
|
1965
|
-
const normalised = pathname.replaceAll(/\\/+/gu, '/').replace(/\\/+$/u, '');
|
|
1966
|
-
return normalised.length > 0 ? normalised : '/';
|
|
1967
|
-
};
|
|
1968
|
-
|
|
1969
|
-
const stripLanguagePrefix = (pathname: string) => {
|
|
1970
|
-
const segments = normalisePath(pathname).split('/').filter(Boolean);
|
|
1971
|
-
if (segments.length > 0 && isSupportedLanguage(segments[0] ?? '')) {
|
|
1972
|
-
segments.shift();
|
|
1973
|
-
}
|
|
1974
|
-
return \`/\${segments.join('/')}\`;
|
|
1975
|
-
};
|
|
1976
|
-
|
|
1977
|
-
const escapeRegExp = (value: string) =>
|
|
1978
|
-
value.replaceAll(/[.*+?^\${}()|[\\]\\\\]/gu, '\\\\$&');
|
|
1979
|
-
|
|
1980
|
-
const paramName = (segment: string) => segment.slice(1).replace(/\\?$/u, '');
|
|
1981
|
-
|
|
1982
|
-
const matchPattern = (pathname: string, pattern: string) => {
|
|
1983
|
-
const names: string[] = [];
|
|
1984
|
-
const source = normalisePath(pattern)
|
|
1985
|
-
.split('/')
|
|
1986
|
-
.filter(Boolean)
|
|
1987
|
-
.map(segment => {
|
|
1988
|
-
if (segment.startsWith(':')) {
|
|
1989
|
-
names.push(paramName(segment));
|
|
1990
|
-
return segment.endsWith('?') ? '(?:/([^/]+))?' : '/([^/]+)';
|
|
1991
|
-
}
|
|
1992
|
-
return \`/\${escapeRegExp(segment)}\`;
|
|
1993
|
-
})
|
|
1994
|
-
.join('');
|
|
1995
|
-
const match = new RegExp(\`^\${source || '/'}$\`, 'u').exec(
|
|
1996
|
-
normalisePath(pathname),
|
|
1997
|
-
);
|
|
1998
|
-
|
|
1999
|
-
if (match === null) {
|
|
2000
|
-
return;
|
|
2001
|
-
}
|
|
2002
|
-
|
|
2003
|
-
const params: Record<string, string> = {};
|
|
2004
|
-
for (const [index, name] of names.entries()) {
|
|
2005
|
-
params[name] = decodeURIComponent(match[index + 1] ?? '');
|
|
2006
|
-
}
|
|
2007
|
-
return params;
|
|
2008
|
-
};
|
|
2009
|
-
|
|
2010
|
-
const buildPath = (pattern: string, params: Record<string, string>) => {
|
|
2011
|
-
const path = normalisePath(pattern)
|
|
2012
|
-
.split('/')
|
|
2013
|
-
.filter(Boolean)
|
|
2014
|
-
.map(segment => {
|
|
2015
|
-
if (!segment.startsWith(':')) {
|
|
2016
|
-
return segment;
|
|
2017
|
-
}
|
|
2018
|
-
const value = params[paramName(segment)];
|
|
2019
|
-
return value !== undefined && value.length > 0
|
|
2020
|
-
? encodeURIComponent(value)
|
|
2021
|
-
: '';
|
|
2022
|
-
})
|
|
2023
|
-
.filter(Boolean)
|
|
2024
|
-
.join('/');
|
|
2025
|
-
|
|
2026
|
-
return \`/\${path}\`;
|
|
2027
|
-
};
|
|
2028
|
-
|
|
2029
|
-
const resolveLocalisedPath = (
|
|
2030
|
-
pathname: string,
|
|
2031
|
-
targetLanguage: SupportedLanguage,
|
|
2032
|
-
) => {
|
|
2033
|
-
const pathWithoutLanguage = stripLanguagePrefix(pathname);
|
|
2034
|
-
|
|
2035
|
-
for (const entry of Object.values(localisedUrls)) {
|
|
2036
|
-
const targetPattern = entry[targetLanguage];
|
|
2037
|
-
if (targetPattern === undefined) {
|
|
2038
|
-
continue;
|
|
2039
|
-
}
|
|
2040
|
-
|
|
2041
|
-
for (const language of supportedLanguages) {
|
|
2042
|
-
const sourcePattern = entry[language];
|
|
2043
|
-
const params =
|
|
2044
|
-
sourcePattern === undefined
|
|
2045
|
-
? undefined
|
|
2046
|
-
: matchPattern(pathWithoutLanguage, sourcePattern);
|
|
2047
|
-
if (params !== undefined) {
|
|
2048
|
-
return buildPath(targetPattern, params);
|
|
2049
|
-
}
|
|
2050
|
-
}
|
|
2051
|
-
}
|
|
2052
|
-
|
|
2053
|
-
return pathWithoutLanguage;
|
|
2054
|
-
};
|
|
2055
|
-
|
|
2056
|
-
const localizedPath = (pathname: string, language: SupportedLanguage) => {
|
|
2057
|
-
const pathWithoutLanguage = resolveLocalisedPath(pathname, language);
|
|
2058
|
-
return pathWithoutLanguage === '/' ? \`/\${language}\` : \`/\${language}\${pathWithoutLanguage}\`;
|
|
2059
|
-
};
|
|
2060
|
-
|
|
2061
|
-
const locationSuffix = (location: {
|
|
2062
|
-
hash?: unknown;
|
|
2063
|
-
search?: unknown;
|
|
2064
|
-
searchStr?: unknown;
|
|
2065
|
-
}) => {
|
|
2066
|
-
let locationSearch = '';
|
|
2067
|
-
if (typeof location.searchStr === 'string') {
|
|
2068
|
-
locationSearch = location.searchStr;
|
|
2069
|
-
} else if (typeof location.search === 'string') {
|
|
2070
|
-
locationSearch = location.search;
|
|
2071
|
-
}
|
|
2072
|
-
const locationHash = typeof location.hash === 'string' ? location.hash : '';
|
|
2073
|
-
|
|
2074
|
-
return \`\${locationSearch}\${locationHash}\`;
|
|
2075
|
-
};
|
|
2076
|
-
|
|
2077
2058
|
export default function ShellFrame({ children }: ShellFrameProps) {
|
|
2078
2059
|
const { i18nInstance, language } = useModernI18n();
|
|
2079
2060
|
const t = i18nInstance['t'].bind(i18nInstance);
|
|
2080
|
-
const
|
|
2081
|
-
const suffix = locationSuffix(location);
|
|
2061
|
+
const { alternates } = useLocalizedLocation();
|
|
2082
2062
|
|
|
2083
2063
|
return (
|
|
2084
2064
|
<main className="${tw('min-h-screen bg-um-canvas px-4 py-5 text-um-foreground sm:px-6 lg:px-12')}">
|
|
@@ -2095,10 +2075,9 @@ export default function ShellFrame({ children }: ShellFrameProps) {
|
|
|
2095
2075
|
name="language"
|
|
2096
2076
|
onChange={event => {
|
|
2097
2077
|
const nextLanguage = event.currentTarget.value;
|
|
2098
|
-
|
|
2099
|
-
|
|
2100
|
-
|
|
2101
|
-
);
|
|
2078
|
+
const targetHref = alternates[nextLanguage];
|
|
2079
|
+
if (targetHref !== undefined) {
|
|
2080
|
+
window.location.assign(targetHref);
|
|
2102
2081
|
}
|
|
2103
2082
|
}}
|
|
2104
2083
|
value={language}
|
|
@@ -2189,7 +2168,7 @@ const createHydratedRemote =
|
|
|
2189
2168
|
return ` <${componentName} key="${remote.id}" />`;
|
|
2190
2169
|
}).join('\n');
|
|
2191
2170
|
const remoteCount = String(widgetRemotes.length);
|
|
2192
|
-
return `${federationImports}import {
|
|
2171
|
+
return `${federationImports}import { Link, useModernI18n } from '@modern-js/plugin-i18n/runtime';
|
|
2193
2172
|
|
|
2194
2173
|
const widgetCount = Number('${remoteCount}');
|
|
2195
2174
|
|
|
@@ -2202,7 +2181,7 @@ const createHydratedRemote =
|
|
|
2202
2181
|
|
|
2203
2182
|
return (
|
|
2204
2183
|
<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">
|
|
2205
|
-
<
|
|
2184
|
+
<Link className="${tw('whitespace-nowrap text-xl font-black tracking-normal text-stone-950 no-underline')}" to="/">{t('shell.title')}</Link>
|
|
2206
2185
|
</header>
|
|
2207
2186
|
);
|
|
2208
2187
|
};
|
|
@@ -2244,17 +2223,16 @@ function createRemotePage(app) {
|
|
|
2244
2223
|
const tw = createTw(tailwindPrefixForApp(app));
|
|
2245
2224
|
const listEffectItems = `list${toPascalCase(effectApiStem(app))}`;
|
|
2246
2225
|
const effectBffImport = appHasEffectApi(app) ? `import { useModernI18n } from '@modern-js/plugin-i18n/runtime';
|
|
2247
|
-
import {
|
|
2248
|
-
import { Link, useLocation } from '@modern-js/plugin-tanstack/runtime';
|
|
2226
|
+
import { Link } from '@modern-js/plugin-tanstack/runtime';
|
|
2249
2227
|
import { useEffect, useState } from 'react';
|
|
2250
2228
|
import {
|
|
2251
2229
|
Effect,
|
|
2252
2230
|
${listEffectItems},
|
|
2253
2231
|
runEffectRequest,
|
|
2254
2232
|
} from '../../effect/${effectApiStem(app)}-client';
|
|
2255
|
-
import {
|
|
2233
|
+
import { UltramodernRouteHead } from '../ultramodern-route-head';
|
|
2256
2234
|
import { ultramodernUiMarker } from '../../ultramodern-build';
|
|
2257
|
-
` : "import { useModernI18n } from '@modern-js/plugin-i18n/runtime';\nimport {
|
|
2235
|
+
` : "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";
|
|
2258
2236
|
const effectBffState = appHasEffectApi(app) ? ` const [effectApiStatus, setEffectApiStatus] = useState('pending');
|
|
2259
2237
|
|
|
2260
2238
|
useEffect(() => {
|
|
@@ -2287,13 +2265,12 @@ import { ultramodernUiMarker } from '../../ultramodern-build';
|
|
|
2287
2265
|
const effectBffMarkup = appHasEffectApi(app) ? ` <p data-testid="effect-bff-status">{effectApiStatus}</p>
|
|
2288
2266
|
` : '';
|
|
2289
2267
|
return `${effectBffImport}
|
|
2290
|
-
${createLocalizedHeadComponent()}
|
|
2291
2268
|
export default function ${toPascalCase(app.id)}Home() {
|
|
2292
2269
|
const { i18nInstance, language } = useModernI18n();
|
|
2293
2270
|
const t = i18nInstance['t'].bind(i18nInstance);
|
|
2294
2271
|
${effectBffState} return (
|
|
2295
2272
|
<main className="${tw('min-h-screen bg-um-canvas px-4 py-6 text-um-foreground sm:px-8')}">
|
|
2296
|
-
<
|
|
2273
|
+
<UltramodernRouteHead />
|
|
2297
2274
|
<nav aria-label={t('${app.domain}.language.switcher')} className="${tw('flex gap-3')}">
|
|
2298
2275
|
{supportedLanguages.map(code => (
|
|
2299
2276
|
<Link
|
|
@@ -2373,7 +2350,7 @@ export default function ${componentName}() {
|
|
|
2373
2350
|
}
|
|
2374
2351
|
function createRemoteExposeComponent(app, expose) {
|
|
2375
2352
|
const tw = createTw(tailwindPrefixForApp(app));
|
|
2376
|
-
if ('workspace' === app.id && './Header' === expose) return `import {
|
|
2353
|
+
if ('workspace' === app.id && './Header' === expose) return `import { Link, useModernI18n } from '@modern-js/plugin-i18n/runtime';
|
|
2377
2354
|
|
|
2378
2355
|
export default function Header() {
|
|
2379
2356
|
const { i18nInstance } = useModernI18n();
|
|
@@ -2381,16 +2358,16 @@ export default function Header() {
|
|
|
2381
2358
|
|
|
2382
2359
|
return (
|
|
2383
2360
|
<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}">
|
|
2384
|
-
<
|
|
2361
|
+
<Link className="${tw('whitespace-nowrap text-xl font-black tracking-normal text-stone-950 no-underline')}" to="/">{t('workspace.header.brand')}</Link>
|
|
2385
2362
|
<nav aria-label={t('workspace.header.navigation')} className="${tw('flex items-center gap-5')}">
|
|
2386
|
-
<
|
|
2387
|
-
<
|
|
2363
|
+
<Link className="${tw('text-sm font-extrabold text-stone-900 no-underline')}" to="/workspaces">{t('workspace.header.workspaces')}</Link>
|
|
2364
|
+
<Link className="${tw('text-sm font-extrabold text-stone-900 no-underline')}" to="/directory">{t('workspace.header.directory')}</Link>
|
|
2388
2365
|
</nav>
|
|
2389
2366
|
</header>
|
|
2390
2367
|
);
|
|
2391
2368
|
}
|
|
2392
2369
|
`;
|
|
2393
|
-
if ('workspace' === app.id && './Highlights' === expose) return `import {
|
|
2370
|
+
if ('workspace' === app.id && './Highlights' === expose) return `import { Link, useModernI18n } from '@modern-js/plugin-i18n/runtime';
|
|
2394
2371
|
|
|
2395
2372
|
const highlights = [
|
|
2396
2373
|
{ badge: 'workspace.highlights.shell', href: '/workspaces', name: 'workspace.highlights.shellTitle' },
|
|
@@ -2407,10 +2384,10 @@ export default function Highlights() {
|
|
|
2407
2384
|
<h2 className="${tw('text-3xl font-black tracking-normal text-stone-950')}">{t('workspace.highlights.title')}</h2>
|
|
2408
2385
|
<div className="${tw('mt-5 grid gap-4 md:grid-cols-3')}">
|
|
2409
2386
|
{highlights.map(highlight => (
|
|
2410
|
-
<
|
|
2387
|
+
<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}>
|
|
2411
2388
|
<span className="${tw('text-xs font-black uppercase tracking-[0.16em] text-emerald-800')}">{t(highlight.badge)}</span>
|
|
2412
2389
|
<strong className="${tw('mt-3 block text-xl font-black leading-tight')}">{t(highlight.name)}</strong>
|
|
2413
|
-
</
|
|
2390
|
+
</Link>
|
|
2414
2391
|
))}
|
|
2415
2392
|
</div>
|
|
2416
2393
|
</section>
|
|
@@ -2487,7 +2464,7 @@ export default function ${componentName}() {
|
|
|
2487
2464
|
);
|
|
2488
2465
|
}
|
|
2489
2466
|
`;
|
|
2490
|
-
if ('actions' === app.id && './StartAction' === expose) return `import {
|
|
2467
|
+
if ('actions' === app.id && './StartAction' === expose) return `import { Link, useModernI18n } from '@modern-js/plugin-i18n/runtime';
|
|
2491
2468
|
import { useActionQueue } from '../action-queue-store';
|
|
2492
2469
|
|
|
2493
2470
|
export default function ${componentName}() {
|
|
@@ -2500,9 +2477,9 @@ export default function ${componentName}() {
|
|
|
2500
2477
|
<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">
|
|
2501
2478
|
{t('actions.controls.start')}
|
|
2502
2479
|
</button>
|
|
2503
|
-
<
|
|
2480
|
+
<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">
|
|
2504
2481
|
{t('actions.controls.viewQueue')}
|
|
2505
|
-
</
|
|
2482
|
+
</Link>
|
|
2506
2483
|
</div>
|
|
2507
2484
|
);
|
|
2508
2485
|
}
|
|
@@ -2667,6 +2644,9 @@ const commonLocaleMessages = {
|
|
|
2667
2644
|
review: 'Revize akce',
|
|
2668
2645
|
unavailable: 'Nedostupné',
|
|
2669
2646
|
workspaces: 'Pracovní prostory'
|
|
2647
|
+
},
|
|
2648
|
+
seo: {
|
|
2649
|
+
description: 'Route-owned UltraModern plocha s lokalizovaným SSR a frameworkem řízenými public metadata.'
|
|
2670
2650
|
}
|
|
2671
2651
|
},
|
|
2672
2652
|
en: {
|
|
@@ -2684,6 +2664,9 @@ const commonLocaleMessages = {
|
|
|
2684
2664
|
review: 'Action review',
|
|
2685
2665
|
unavailable: 'Unavailable',
|
|
2686
2666
|
workspaces: 'Workspaces'
|
|
2667
|
+
},
|
|
2668
|
+
seo: {
|
|
2669
|
+
description: 'Route-owned UltraModern surface with localized SSR and framework-owned public metadata.'
|
|
2687
2670
|
}
|
|
2688
2671
|
}
|
|
2689
2672
|
};
|
|
@@ -2762,6 +2745,9 @@ const generatedLocaleResources = {
|
|
|
2762
2745
|
routes: {
|
|
2763
2746
|
home: commonLocaleMessages.cs.routes.home
|
|
2764
2747
|
},
|
|
2748
|
+
seo: {
|
|
2749
|
+
description: 'UltraModern shell SuperApp s lokalizovaným SSR, Module Federation a frameworkem řízenými public metadata.'
|
|
2750
|
+
},
|
|
2765
2751
|
title: 'UltraModern Workspace'
|
|
2766
2752
|
},
|
|
2767
2753
|
workspace: {
|
|
@@ -2871,6 +2857,9 @@ const generatedLocaleResources = {
|
|
|
2871
2857
|
routes: {
|
|
2872
2858
|
home: commonLocaleMessages.en.routes.home
|
|
2873
2859
|
},
|
|
2860
|
+
seo: {
|
|
2861
|
+
description: 'UltraModern shell SuperApp with localized SSR, Module Federation, and framework-owned public metadata.'
|
|
2862
|
+
},
|
|
2874
2863
|
title: 'UltraModern Workspace'
|
|
2875
2864
|
},
|
|
2876
2865
|
workspace: {
|
|
@@ -3747,15 +3736,17 @@ function createAppConfigContract(app) {
|
|
|
3747
3736
|
'moduleFederationPlugin',
|
|
3748
3737
|
'zephyrRspackPlugin'
|
|
3749
3738
|
],
|
|
3739
|
+
dev: {
|
|
3740
|
+
assetPrefix: '/'
|
|
3741
|
+
},
|
|
3750
3742
|
output: {
|
|
3751
3743
|
assetPrefix: {
|
|
3752
3744
|
envFallbackOrder: [
|
|
3753
3745
|
createCloudflarePublicUrlEnv(app),
|
|
3754
3746
|
'MODERN_PUBLIC_SITE_URL',
|
|
3755
|
-
'ULTRAMODERN_CLOUDFLARE_WORKERS_DEV_SUBDOMAIN'
|
|
3756
|
-
app.portEnv
|
|
3747
|
+
'ULTRAMODERN_CLOUDFLARE_WORKERS_DEV_SUBDOMAIN'
|
|
3757
3748
|
],
|
|
3758
|
-
|
|
3749
|
+
default: '/'
|
|
3759
3750
|
},
|
|
3760
3751
|
disableTsChecker: true,
|
|
3761
3752
|
distPath: {
|
|
@@ -3781,6 +3772,15 @@ function createAppConfigContract(app) {
|
|
|
3781
3772
|
},
|
|
3782
3773
|
source: {
|
|
3783
3774
|
mainEntryName: 'index',
|
|
3775
|
+
siteUrl: {
|
|
3776
|
+
envFallbackOrder: [
|
|
3777
|
+
'MODERN_PUBLIC_SITE_URL',
|
|
3778
|
+
createCloudflarePublicUrlEnv(app),
|
|
3779
|
+
'ULTRAMODERN_CLOUDFLARE_WORKERS_DEV_SUBDOMAIN',
|
|
3780
|
+
app.portEnv
|
|
3781
|
+
],
|
|
3782
|
+
defaultLocalhostPort: app.port
|
|
3783
|
+
},
|
|
3784
3784
|
siteUrlGlobal: 'ULTRAMODERN_SITE_URL'
|
|
3785
3785
|
},
|
|
3786
3786
|
...appHasEffectApi(app) ? {
|
|
@@ -3942,11 +3942,17 @@ function createStylingContract(scope, app, enableTailwind) {
|
|
|
3942
3942
|
};
|
|
3943
3943
|
}
|
|
3944
3944
|
function createPublicSurfaceContract(app) {
|
|
3945
|
-
const files =
|
|
3945
|
+
const files = createPublicSurfaceOutputFiles(app);
|
|
3946
|
+
const contentExpansionPolicy = createPublicSurfaceContentExpansionPolicy();
|
|
3946
3947
|
return {
|
|
3948
|
+
authoring: 'colocated-route-meta',
|
|
3949
|
+
artifactLifecycle: 'build-and-deploy-output',
|
|
3950
|
+
generatedManifest: './src/routes/ultramodern-route-metadata',
|
|
3947
3951
|
source: 'route-owned-public-routes',
|
|
3948
3952
|
metadataExport: './src/routes/ultramodern-route-metadata',
|
|
3949
|
-
|
|
3953
|
+
generator: "scripts/generate-public-surface-assets.mjs",
|
|
3954
|
+
outputRoot: 'dist/public',
|
|
3955
|
+
cloudflareOutputRoot: '.output/public',
|
|
3950
3956
|
privateRoutePolicy: 'omit-from-generated-public-surface',
|
|
3951
3957
|
files,
|
|
3952
3958
|
omittedByDefault: [
|
|
@@ -3954,15 +3960,103 @@ function createPublicSurfaceContract(app) {
|
|
|
3954
3960
|
'llms.txt',
|
|
3955
3961
|
'security.txt'
|
|
3956
3962
|
],
|
|
3963
|
+
languages: [
|
|
3964
|
+
...supportedWorkspaceLanguages
|
|
3965
|
+
],
|
|
3966
|
+
contentExpansion: {
|
|
3967
|
+
authoring: 'route-owned-esm-provider',
|
|
3968
|
+
defaultProviderFile: contentExpansionPolicy.defaultProviderFile,
|
|
3969
|
+
entryExport: 'default-or-entries',
|
|
3970
|
+
paramsSource: 'params-or-localeParams',
|
|
3971
|
+
draftPolicy: contentExpansionPolicy.draftPolicy,
|
|
3972
|
+
indexablePolicy: contentExpansionPolicy.indexablePolicy,
|
|
3973
|
+
lifecycle: 'executed-during-public-surface-generation'
|
|
3974
|
+
},
|
|
3975
|
+
contentSources: createPublicSurfaceContentSources(app),
|
|
3957
3976
|
publicRoutes: createPublicRouteMetadata(app),
|
|
3977
|
+
routeEntries: createPublicSurfaceRouteEntries(app),
|
|
3958
3978
|
concreteUrlPaths: createPublicSurfaceUrlPaths(app)
|
|
3959
3979
|
};
|
|
3960
3980
|
}
|
|
3981
|
+
function createPublicHeadContract() {
|
|
3982
|
+
const robotsPolicy = createPublicHeadRobotsPolicy();
|
|
3983
|
+
return {
|
|
3984
|
+
authoring: 'colocated-route-meta',
|
|
3985
|
+
generator: './src/routes/ultramodern-route-head',
|
|
3986
|
+
renderer: '@modern-js/runtime/head Helmet',
|
|
3987
|
+
ssr: true,
|
|
3988
|
+
title: {
|
|
3989
|
+
required: true,
|
|
3990
|
+
source: 'route.titleKey'
|
|
3991
|
+
},
|
|
3992
|
+
description: {
|
|
3993
|
+
required: true,
|
|
3994
|
+
source: "route.descriptionKey"
|
|
3995
|
+
},
|
|
3996
|
+
canonical: {
|
|
3997
|
+
publicIndexableOnly: true,
|
|
3998
|
+
source: 'localized canonical route URL'
|
|
3999
|
+
},
|
|
4000
|
+
alternates: {
|
|
4001
|
+
hreflang: [
|
|
4002
|
+
...supportedWorkspaceLanguages
|
|
4003
|
+
],
|
|
4004
|
+
xDefault: 'en'
|
|
4005
|
+
},
|
|
4006
|
+
openGraph: {
|
|
4007
|
+
publicIndexableOnly: true,
|
|
4008
|
+
required: [
|
|
4009
|
+
'og:title',
|
|
4010
|
+
"og:description",
|
|
4011
|
+
'og:url',
|
|
4012
|
+
'og:type'
|
|
4013
|
+
]
|
|
4014
|
+
},
|
|
4015
|
+
twitter: {
|
|
4016
|
+
publicIndexableOnly: true,
|
|
4017
|
+
required: [
|
|
4018
|
+
'twitter:card',
|
|
4019
|
+
'twitter:title',
|
|
4020
|
+
"twitter:description"
|
|
4021
|
+
]
|
|
4022
|
+
},
|
|
4023
|
+
structuredData: {
|
|
4024
|
+
publicIndexableOnly: true,
|
|
4025
|
+
type: 'WebPage',
|
|
4026
|
+
sanitizesHtmlOpenBracket: true
|
|
4027
|
+
},
|
|
4028
|
+
privateRouteRobots: robotsPolicy.privateRouteRobots
|
|
4029
|
+
};
|
|
4030
|
+
}
|
|
4031
|
+
function createPublicWebAppArtifacts(app) {
|
|
4032
|
+
const routeMetadata = createRouteOwnedI18nPaths(app);
|
|
4033
|
+
return {
|
|
4034
|
+
routeMetadataFile: {
|
|
4035
|
+
path: `${app.directory}/src/routes/ultramodern-route-metadata.ts`,
|
|
4036
|
+
content: createRouteMetadataModule(app)
|
|
4037
|
+
},
|
|
4038
|
+
routeHeadFile: {
|
|
4039
|
+
path: `${app.directory}/src/routes/ultramodern-route-head.tsx`,
|
|
4040
|
+
content: createRouteHeadModule(app)
|
|
4041
|
+
},
|
|
4042
|
+
routeMetaFiles: routeMetadata.map((route)=>({
|
|
4043
|
+
path: createRouteMetaFilePath(app, route.canonicalPath),
|
|
4044
|
+
content: createRouteMetaModule(route)
|
|
4045
|
+
})),
|
|
4046
|
+
routeAliasFiles: routeMetadata.filter((route)=>'/' !== route.canonicalPath && 'shell' !== app.kind).map((route)=>({
|
|
4047
|
+
path: createRoutePageFilePath(app, route.canonicalPath),
|
|
4048
|
+
content: createRouteAliasPage(route.canonicalPath)
|
|
4049
|
+
})),
|
|
4050
|
+
publicHead: createPublicHeadContract(),
|
|
4051
|
+
publicSurface: createPublicSurfaceContract(app)
|
|
4052
|
+
};
|
|
4053
|
+
}
|
|
3961
4054
|
function createAppGeneratedContract(scope, app, apps, enableTailwind) {
|
|
3962
4055
|
const appWithResolvedRefs = 'shell' === app.kind ? {
|
|
3963
4056
|
...app,
|
|
3964
4057
|
verticalRefs: apps.filter((candidate)=>'shell' !== candidate.kind).map((candidate)=>candidate.id)
|
|
3965
4058
|
} : app;
|
|
4059
|
+
const publicWeb = createPublicWebAppArtifacts(app);
|
|
3966
4060
|
const consumedRemotes = createModuleFederationRemoteContracts(appWithResolvedRefs, apps);
|
|
3967
4061
|
return {
|
|
3968
4062
|
id: app.id,
|
|
@@ -4019,6 +4113,8 @@ function createAppGeneratedContract(scope, app, apps, enableTailwind) {
|
|
|
4019
4113
|
},
|
|
4020
4114
|
routes: {
|
|
4021
4115
|
source: 'route-owned',
|
|
4116
|
+
metadataAuthoring: 'colocated-route-meta',
|
|
4117
|
+
generatedManifest: true,
|
|
4022
4118
|
metadataExport: './src/routes/ultramodern-route-metadata',
|
|
4023
4119
|
localisedUrls: createLocalisedUrlsMap(app),
|
|
4024
4120
|
owned: createRouteOwnedI18nPaths(app),
|
|
@@ -4027,7 +4123,8 @@ function createAppGeneratedContract(scope, app, apps, enableTailwind) {
|
|
|
4027
4123
|
publicnessDefault: 'private-app-screen',
|
|
4028
4124
|
generatedRouteMap: true,
|
|
4029
4125
|
manualOverrides: [],
|
|
4030
|
-
|
|
4126
|
+
publicHead: publicWeb.publicHead,
|
|
4127
|
+
publicSurface: publicWeb.publicSurface
|
|
4031
4128
|
},
|
|
4032
4129
|
moduleFederation: {
|
|
4033
4130
|
name: app.mfName,
|
|
@@ -4038,7 +4135,7 @@ function createAppGeneratedContract(scope, app, apps, enableTailwind) {
|
|
|
4038
4135
|
exposes: Object.keys(app.exposes ?? {}),
|
|
4039
4136
|
dts: {
|
|
4040
4137
|
displayErrorInTerminal: true,
|
|
4041
|
-
compilerInstance:
|
|
4138
|
+
compilerInstance: 'tsgo'
|
|
4042
4139
|
},
|
|
4043
4140
|
browserSafeExposesOnly: true,
|
|
4044
4141
|
zephyrRspackPlugin: ZEPHYR_RSPACK_PLUGIN_VERSION
|
|
@@ -4263,7 +4360,7 @@ for (const appDir of appDirs) {
|
|
|
4263
4360
|
if (
|
|
4264
4361
|
contractEntry &&
|
|
4265
4362
|
contractEntry.moduleFederation?.dts?.compilerInstance !==
|
|
4266
|
-
'
|
|
4363
|
+
'tsgo'
|
|
4267
4364
|
) {
|
|
4268
4365
|
throw new Error(
|
|
4269
4366
|
\`Module Federation DTS must use the workspace TypeScript compiler: \${appDir}\`,
|
|
@@ -4302,53 +4399,588 @@ process.exitCode = runWorkspaceSourceCheck({
|
|
|
4302
4399
|
});
|
|
4303
4400
|
`;
|
|
4304
4401
|
}
|
|
4305
|
-
function
|
|
4306
|
-
const
|
|
4307
|
-
|
|
4308
|
-
domain: remote.domain,
|
|
4309
|
-
stem: remote.effectApi.stem,
|
|
4310
|
-
group: verticalEffectGroupName(remote),
|
|
4311
|
-
path: remote.directory,
|
|
4312
|
-
mfName: remote.mfName,
|
|
4313
|
-
apiPrefix: remote.effectApi.prefix,
|
|
4314
|
-
tailwindPrefix: tailwindPrefixForApp(remote),
|
|
4315
|
-
zephyrAlias: remoteDependencyAlias(remote),
|
|
4316
|
-
packageName: ultramodern_workspace_packageName(scope, remote.packageSuffix),
|
|
4317
|
-
exposes: Object.keys(remote.exposes ?? {}),
|
|
4318
|
-
componentPaths: Object.keys(remote.exposes ?? {}).map((expose)=>remoteComponentOutputPath(remote, expose)).filter((componentPath)=>Boolean(componentPath)),
|
|
4319
|
-
namespace: appI18nNamespace(remote),
|
|
4320
|
-
routePagePaths: createRouteOwnedI18nPaths(remote).filter((route)=>'/' !== route.canonicalPath).map((route)=>createRoutePageFilePath(remote, route.canonicalPath)),
|
|
4321
|
-
localisedUrls: createLocalisedUrlsMap(remote),
|
|
4322
|
-
verticalRefs: remote.verticalRefs ?? []
|
|
4323
|
-
}));
|
|
4324
|
-
const shellNamespace = appI18nNamespace(shellApp);
|
|
4325
|
-
const oldRemotePaths = [
|
|
4326
|
-
'apps/remotes'
|
|
4327
|
-
];
|
|
4328
|
-
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';
|
|
4329
|
-
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';
|
|
4330
|
-
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';
|
|
4331
|
-
const expectedCloudflareSecurity = createCloudflareSecurityContract();
|
|
4332
|
-
return `import { execFileSync } from 'node:child_process';
|
|
4402
|
+
function createPublicSurfaceAssetsScript() {
|
|
4403
|
+
const contentExpansionPolicy = createPublicSurfaceContentExpansionPolicy();
|
|
4404
|
+
return `#!/usr/bin/env node
|
|
4333
4405
|
import fs from 'node:fs';
|
|
4334
4406
|
import path from 'node:path';
|
|
4407
|
+
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
4335
4408
|
|
|
4336
|
-
const
|
|
4337
|
-
|
|
4338
|
-
|
|
4339
|
-
|
|
4340
|
-
const
|
|
4341
|
-
|
|
4342
|
-
|
|
4409
|
+
const workspaceRoot = path.resolve(
|
|
4410
|
+
path.dirname(fileURLToPath(import.meta.url)),
|
|
4411
|
+
'..',
|
|
4412
|
+
);
|
|
4413
|
+
const contractPath = path.join(
|
|
4414
|
+
workspaceRoot,
|
|
4415
|
+
'.modernjs/ultramodern-generated-contract.json',
|
|
4416
|
+
);
|
|
4417
|
+
|
|
4418
|
+
function readJson(filePath) {
|
|
4419
|
+
return JSON.parse(fs.readFileSync(filePath, 'utf-8'));
|
|
4420
|
+
}
|
|
4421
|
+
|
|
4422
|
+
function parseArgs(argv) {
|
|
4423
|
+
const parsed = {
|
|
4424
|
+
appId: undefined,
|
|
4425
|
+
target: 'dist',
|
|
4426
|
+
requirePublicOrigin: false,
|
|
4427
|
+
};
|
|
4428
|
+
|
|
4429
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
4430
|
+
const arg = argv[index];
|
|
4431
|
+
if (arg === '--app') {
|
|
4432
|
+
parsed.appId = argv[index + 1];
|
|
4433
|
+
index += 1;
|
|
4434
|
+
} else if (arg === '--target') {
|
|
4435
|
+
parsed.target = argv[index + 1];
|
|
4436
|
+
index += 1;
|
|
4437
|
+
} else if (arg === '--require-public-origin') {
|
|
4438
|
+
parsed.requirePublicOrigin = true;
|
|
4439
|
+
} else if (arg === '--help' || arg === '-h') {
|
|
4440
|
+
parsed.help = true;
|
|
4441
|
+
} else {
|
|
4442
|
+
throw new Error(\`Unknown argument: \${arg}\`);
|
|
4443
|
+
}
|
|
4444
|
+
}
|
|
4445
|
+
|
|
4446
|
+
if (!parsed.appId && !parsed.help) {
|
|
4447
|
+
throw new Error('Missing required --app argument');
|
|
4448
|
+
}
|
|
4449
|
+
if (!['dist', 'cloudflare'].includes(parsed.target)) {
|
|
4450
|
+
throw new Error(\`Unsupported public surface target: \${parsed.target}\`);
|
|
4451
|
+
}
|
|
4452
|
+
|
|
4453
|
+
return parsed;
|
|
4454
|
+
}
|
|
4455
|
+
|
|
4456
|
+
function printHelp() {
|
|
4457
|
+
process.stdout.write(\`Usage:
|
|
4458
|
+
node scripts/generate-public-surface-assets.mjs --app shell-super-app [--target dist|cloudflare] [--require-public-origin]
|
|
4459
|
+
|
|
4460
|
+
Set each app's production URL using the contract env key, for example:
|
|
4461
|
+
ULTRAMODERN_PUBLIC_URL_SHELL_SUPER_APP=https://example.com
|
|
4462
|
+
|
|
4463
|
+
Dynamic public routes can opt into sitemap expansion by adding a route-owned
|
|
4464
|
+
${contentExpansionPolicy.defaultProviderFile} provider beside route metadata, or by adding an
|
|
4465
|
+
explicit provider to routes.publicSurface.contentSources. Providers should export
|
|
4466
|
+
an entries array, entries() function, or default entries/loader returning
|
|
4467
|
+
UltramodernPublicSitemapEntry[].
|
|
4468
|
+
\`);
|
|
4469
|
+
}
|
|
4470
|
+
|
|
4471
|
+
function normalizeOrigin(value) {
|
|
4472
|
+
if (typeof value !== 'string' || value.trim() === '') {
|
|
4473
|
+
return undefined;
|
|
4474
|
+
}
|
|
4475
|
+
const url = new URL(value);
|
|
4476
|
+
return url.origin;
|
|
4477
|
+
}
|
|
4478
|
+
|
|
4479
|
+
function resolveOrigin(app, requirePublicOrigin) {
|
|
4480
|
+
const cloudflare = app.deploy?.cloudflare ?? {};
|
|
4481
|
+
const publicUrlEnv = cloudflare.publicUrlEnv;
|
|
4482
|
+
const fromAppEnv =
|
|
4483
|
+
typeof publicUrlEnv === 'string' ? normalizeOrigin(process.env[publicUrlEnv]) : undefined;
|
|
4484
|
+
const fromGlobalEnv = normalizeOrigin(process.env.MODERN_PUBLIC_SITE_URL);
|
|
4485
|
+
const workersDevSubdomain = process.env.ULTRAMODERN_CLOUDFLARE_WORKERS_DEV_SUBDOMAIN;
|
|
4486
|
+
const fromWorkersDev =
|
|
4487
|
+
typeof workersDevSubdomain === 'string' && workersDevSubdomain.trim() !== ''
|
|
4488
|
+
? normalizeOrigin(\`https://\${cloudflare.workerName}.\${workersDevSubdomain}.workers.dev\`)
|
|
4489
|
+
: undefined;
|
|
4490
|
+
|
|
4491
|
+
// SEO output (sitemap <loc>, robots Sitemap:) uses the site-wide origin
|
|
4492
|
+
// first; the per-app deployment URL is only a fallback.
|
|
4493
|
+
const configuredOrigin = fromGlobalEnv ?? fromAppEnv ?? fromWorkersDev;
|
|
4494
|
+
if (configuredOrigin) {
|
|
4495
|
+
return configuredOrigin;
|
|
4496
|
+
}
|
|
4497
|
+
if (requirePublicOrigin) {
|
|
4498
|
+
throw new Error(
|
|
4499
|
+
\`\${app.id} has public routes but no production public URL. Set \${publicUrlEnv ?? 'ULTRAMODERN_PUBLIC_URL_<APP>'} or MODERN_PUBLIC_SITE_URL.\`,
|
|
4500
|
+
);
|
|
4501
|
+
}
|
|
4502
|
+
return undefined;
|
|
4503
|
+
}
|
|
4504
|
+
|
|
4505
|
+
function ensureOutputDir(app, target) {
|
|
4506
|
+
const relativeDir =
|
|
4507
|
+
target === 'cloudflare'
|
|
4508
|
+
? app.routes?.publicSurface?.cloudflareOutputRoot
|
|
4509
|
+
: app.routes?.publicSurface?.outputRoot;
|
|
4510
|
+
if (typeof relativeDir !== 'string') {
|
|
4511
|
+
throw new Error(\`\${app.id} public surface contract is missing outputRoot for \${target}\`);
|
|
4512
|
+
}
|
|
4513
|
+
const outputDir = path.resolve(workspaceRoot, app.path, relativeDir);
|
|
4514
|
+
const appRoot = path.resolve(workspaceRoot, app.path);
|
|
4515
|
+
if (!outputDir.startsWith(appRoot + path.sep)) {
|
|
4516
|
+
throw new Error(\`\${app.id} public surface output escaped the app directory\`);
|
|
4517
|
+
}
|
|
4518
|
+
fs.mkdirSync(outputDir, { recursive: true });
|
|
4519
|
+
return outputDir;
|
|
4520
|
+
}
|
|
4521
|
+
|
|
4522
|
+
function resolveAppRelativePath(app, relativePath) {
|
|
4523
|
+
if (
|
|
4524
|
+
typeof relativePath !== 'string' ||
|
|
4525
|
+
relativePath.trim() === '' ||
|
|
4526
|
+
path.isAbsolute(relativePath) ||
|
|
4527
|
+
relativePath.split(/[\\\\/]+/).includes('..')
|
|
4528
|
+
) {
|
|
4529
|
+
throw new Error(app.id + ' public content source has an unsafe module path');
|
|
4530
|
+
}
|
|
4531
|
+
const appRoot = path.resolve(workspaceRoot, app.path);
|
|
4532
|
+
const resolved = path.resolve(appRoot, relativePath);
|
|
4533
|
+
if (resolved !== appRoot && !resolved.startsWith(appRoot + path.sep)) {
|
|
4534
|
+
throw new Error(app.id + ' public content source escaped the app directory');
|
|
4535
|
+
}
|
|
4536
|
+
return resolved;
|
|
4537
|
+
}
|
|
4538
|
+
|
|
4539
|
+
function normalizePublicPath(pathname) {
|
|
4540
|
+
if (typeof pathname !== 'string') {
|
|
4541
|
+
throw new Error('Public route path must be a string');
|
|
4542
|
+
}
|
|
4543
|
+
const normalised = pathname
|
|
4544
|
+
.trim()
|
|
4545
|
+
.replaceAll(/\\/+/gu, '/')
|
|
4546
|
+
.replace(/\\/+$/u, '');
|
|
4547
|
+
return normalised.length > 0 && normalised.startsWith('/')
|
|
4548
|
+
? normalised
|
|
4549
|
+
: '/' + normalised;
|
|
4550
|
+
}
|
|
4551
|
+
|
|
4552
|
+
function createLocalisedPublicPath(pathname, language) {
|
|
4553
|
+
const publicPath = normalizePublicPath(pathname);
|
|
4554
|
+
return publicPath === '/' ? '/' + language : '/' + language + publicPath;
|
|
4555
|
+
}
|
|
4556
|
+
|
|
4557
|
+
function splitPublicPathSegments(pathname) {
|
|
4558
|
+
return normalizePublicPath(pathname).split('/').filter(Boolean);
|
|
4559
|
+
}
|
|
4560
|
+
|
|
4561
|
+
function routePathParamName(segment) {
|
|
4562
|
+
if (segment.startsWith(':')) {
|
|
4563
|
+
return segment.slice(1).replace(/[?*+]$/u, '');
|
|
4564
|
+
}
|
|
4565
|
+
if (segment.startsWith('[') && segment.endsWith(']')) {
|
|
4566
|
+
return segment.slice(1, -1).replace(/^\\.\\.\\./u, '').replace(/\\$$/u, '');
|
|
4567
|
+
}
|
|
4568
|
+
return undefined;
|
|
4569
|
+
}
|
|
4570
|
+
|
|
4571
|
+
function routeSegmentToDirectory(segment) {
|
|
4572
|
+
const paramName = routePathParamName(segment);
|
|
4573
|
+
if (paramName && segment.startsWith(':')) {
|
|
4574
|
+
return segment.endsWith('?') ? '[' + paramName + '$]' : '[' + paramName + ']';
|
|
4575
|
+
}
|
|
4576
|
+
return segment;
|
|
4577
|
+
}
|
|
4578
|
+
|
|
4579
|
+
function assertParamValue(routeId, language, paramName, value) {
|
|
4580
|
+
if (typeof value !== 'string' && typeof value !== 'number' && typeof value !== 'boolean') {
|
|
4581
|
+
throw new Error(routeId + ' ' + language + ' sitemap param ' + paramName + ' must be a string, number, or boolean');
|
|
4582
|
+
}
|
|
4583
|
+
const text = String(value).trim();
|
|
4584
|
+
if (text === '' || text.includes('/')) {
|
|
4585
|
+
throw new Error(routeId + ' ' + language + ' sitemap param ' + paramName + ' must be a non-empty path segment');
|
|
4586
|
+
}
|
|
4587
|
+
return encodeURIComponent(text);
|
|
4588
|
+
}
|
|
4589
|
+
|
|
4590
|
+
function expandPublicPathPattern(routeId, language, pattern, params) {
|
|
4591
|
+
const segments = splitPublicPathSegments(pattern);
|
|
4592
|
+
if (segments.length === 0) {
|
|
4593
|
+
return '/';
|
|
4594
|
+
}
|
|
4595
|
+
const expanded = segments.map(segment => {
|
|
4596
|
+
const paramName = routePathParamName(segment);
|
|
4597
|
+
if (!paramName) {
|
|
4598
|
+
if (segment.includes('*')) {
|
|
4599
|
+
throw new Error(routeId + ' ' + language + ' sitemap expansion does not support wildcard path segment ' + segment);
|
|
4600
|
+
}
|
|
4601
|
+
return segment;
|
|
4602
|
+
}
|
|
4603
|
+
if (!Object.prototype.hasOwnProperty.call(params, paramName)) {
|
|
4604
|
+
throw new Error(routeId + ' ' + language + ' sitemap entry is missing param ' + paramName);
|
|
4605
|
+
}
|
|
4606
|
+
return assertParamValue(routeId, language, paramName, params[paramName]);
|
|
4607
|
+
});
|
|
4608
|
+
return '/' + expanded.join('/');
|
|
4609
|
+
}
|
|
4610
|
+
|
|
4611
|
+
function assertPlainObject(value, label) {
|
|
4612
|
+
if (value === null || typeof value !== 'object' || Array.isArray(value)) {
|
|
4613
|
+
throw new Error(label + ' must be an object');
|
|
4614
|
+
}
|
|
4615
|
+
return value;
|
|
4616
|
+
}
|
|
4617
|
+
|
|
4618
|
+
function normalizeSitemapFields(routeId, entry) {
|
|
4619
|
+
const normalized = {};
|
|
4620
|
+
if (entry.lastModified !== undefined) {
|
|
4621
|
+
const lastModified = String(entry.lastModified).trim();
|
|
4622
|
+
if (lastModified === '' || Number.isNaN(Date.parse(lastModified))) {
|
|
4623
|
+
throw new Error(routeId + ' sitemap entry has invalid lastModified');
|
|
4624
|
+
}
|
|
4625
|
+
normalized.lastModified = lastModified;
|
|
4626
|
+
}
|
|
4627
|
+
if (entry.changeFrequency !== undefined) {
|
|
4628
|
+
const allowed = new Set(['always', 'hourly', 'daily', 'weekly', 'monthly', 'yearly', 'never']);
|
|
4629
|
+
if (!allowed.has(entry.changeFrequency)) {
|
|
4630
|
+
throw new Error(routeId + ' sitemap entry has invalid changeFrequency');
|
|
4631
|
+
}
|
|
4632
|
+
normalized.changeFrequency = entry.changeFrequency;
|
|
4633
|
+
}
|
|
4634
|
+
if (entry.priority !== undefined) {
|
|
4635
|
+
if (typeof entry.priority !== 'number' || entry.priority < 0 || entry.priority > 1) {
|
|
4636
|
+
throw new Error(routeId + ' sitemap entry priority must be a number between 0 and 1');
|
|
4637
|
+
}
|
|
4638
|
+
normalized.priority = entry.priority;
|
|
4639
|
+
}
|
|
4640
|
+
return normalized;
|
|
4641
|
+
}
|
|
4642
|
+
|
|
4643
|
+
function routePathToProviderDirectory(routePath) {
|
|
4644
|
+
const segments = splitPublicPathSegments(routePath);
|
|
4645
|
+
if (segments.length === 0) {
|
|
4646
|
+
return 'src/routes/[lang]';
|
|
4647
|
+
}
|
|
4648
|
+
return path.posix.join(
|
|
4649
|
+
'src/routes/[lang]',
|
|
4650
|
+
...segments.map(routeSegmentToDirectory),
|
|
4651
|
+
);
|
|
4652
|
+
}
|
|
4653
|
+
|
|
4654
|
+
function createDiscoveredContentSources(app, publicSurface) {
|
|
4655
|
+
const explicitRouteIds = new Set(
|
|
4656
|
+
(publicSurface.contentSources ?? []).map(source => source.routeId),
|
|
4657
|
+
);
|
|
4658
|
+
const discovered = [];
|
|
4659
|
+
for (const route of publicSurface.publicRoutes ?? []) {
|
|
4660
|
+
if (
|
|
4661
|
+
explicitRouteIds.has(route.id) ||
|
|
4662
|
+
!Object.values(route.localisedPaths ?? {}).some(routePath =>
|
|
4663
|
+
/(?:^|\\/):[^/]+|\\[[^\\]]+\\]/u.test(routePath),
|
|
4664
|
+
)
|
|
4665
|
+
) {
|
|
4666
|
+
continue;
|
|
4667
|
+
}
|
|
4668
|
+
const providerModule = path.posix.join(
|
|
4669
|
+
routePathToProviderDirectory(route.canonicalPath),
|
|
4670
|
+
'${contentExpansionPolicy.defaultProviderFile}',
|
|
4671
|
+
);
|
|
4672
|
+
if (fs.existsSync(resolveAppRelativePath(app, providerModule))) {
|
|
4673
|
+
discovered.push({
|
|
4674
|
+
entryExport: 'default-or-entries',
|
|
4675
|
+
module: providerModule,
|
|
4676
|
+
routeId: route.id,
|
|
4677
|
+
});
|
|
4678
|
+
}
|
|
4679
|
+
}
|
|
4680
|
+
return discovered;
|
|
4681
|
+
}
|
|
4682
|
+
|
|
4683
|
+
function resolveContentSources(app, publicSurface) {
|
|
4684
|
+
return [
|
|
4685
|
+
...(publicSurface.contentSources ?? []),
|
|
4686
|
+
...createDiscoveredContentSources(app, publicSurface),
|
|
4687
|
+
];
|
|
4688
|
+
}
|
|
4689
|
+
|
|
4690
|
+
async function loadContentSourceEntries(app, contentSource, languages) {
|
|
4691
|
+
if (typeof contentSource?.routeId !== 'string' || contentSource.routeId.trim() === '') {
|
|
4692
|
+
throw new Error(app.id + ' public content source is missing routeId');
|
|
4693
|
+
}
|
|
4694
|
+
const modulePath = resolveAppRelativePath(app, contentSource.module);
|
|
4695
|
+
const moduleExports = await import(pathToFileURL(modulePath).href);
|
|
4696
|
+
const exported = moduleExports.default ?? moduleExports.entries;
|
|
4697
|
+
const rawEntries =
|
|
4698
|
+
typeof exported === 'function'
|
|
4699
|
+
? await exported({
|
|
4700
|
+
appId: app.id,
|
|
4701
|
+
languages,
|
|
4702
|
+
routeId: contentSource.routeId,
|
|
4703
|
+
})
|
|
4704
|
+
: exported;
|
|
4705
|
+
if (!Array.isArray(rawEntries)) {
|
|
4706
|
+
throw new Error(app.id + ' public content source for ' + contentSource.routeId + ' must export an entries array or loader');
|
|
4707
|
+
}
|
|
4708
|
+
return rawEntries;
|
|
4709
|
+
}
|
|
4710
|
+
|
|
4711
|
+
async function expandContentSources(app, publicSurface, languages) {
|
|
4712
|
+
const routesById = new Map(
|
|
4713
|
+
(publicSurface.publicRoutes ?? []).map(route => [route.id, route]),
|
|
4714
|
+
);
|
|
4715
|
+
const expanded = [];
|
|
4716
|
+
for (const contentSource of resolveContentSources(app, publicSurface)) {
|
|
4717
|
+
const route = routesById.get(contentSource.routeId);
|
|
4718
|
+
if (!route) {
|
|
4719
|
+
throw new Error(app.id + ' public content source references unknown route ' + contentSource.routeId);
|
|
4720
|
+
}
|
|
4721
|
+
const rawEntries = await loadContentSourceEntries(app, contentSource, languages);
|
|
4722
|
+
for (const rawEntry of rawEntries) {
|
|
4723
|
+
const entry = assertPlainObject(rawEntry, route.id + ' sitemap entry');
|
|
4724
|
+
if (entry.draft === true || entry.indexable === false) {
|
|
4725
|
+
continue;
|
|
4726
|
+
}
|
|
4727
|
+
const baseParams = assertPlainObject(entry.params, route.id + ' sitemap entry params');
|
|
4728
|
+
const localeParams = entry.localeParams === undefined
|
|
4729
|
+
? {}
|
|
4730
|
+
: assertPlainObject(entry.localeParams, route.id + ' sitemap entry localeParams');
|
|
4731
|
+
const localeUrlPaths = {};
|
|
4732
|
+
for (const language of languages) {
|
|
4733
|
+
const params = {
|
|
4734
|
+
...baseParams,
|
|
4735
|
+
...(localeParams[language] ?? {}),
|
|
4736
|
+
};
|
|
4737
|
+
localeUrlPaths[language] = createLocalisedPublicPath(
|
|
4738
|
+
expandPublicPathPattern(route.id, language, route.localisedPaths[language], params),
|
|
4739
|
+
language,
|
|
4740
|
+
);
|
|
4741
|
+
}
|
|
4742
|
+
expanded.push({
|
|
4743
|
+
...route,
|
|
4744
|
+
...normalizeSitemapFields(route.id, entry),
|
|
4745
|
+
canonicalUrlPath: localeUrlPaths.en,
|
|
4746
|
+
localeUrlPaths,
|
|
4747
|
+
});
|
|
4748
|
+
}
|
|
4749
|
+
}
|
|
4750
|
+
return expanded;
|
|
4751
|
+
}
|
|
4752
|
+
|
|
4753
|
+
function mergeRouteEntries(routeEntries, expandedRouteEntries, languages) {
|
|
4754
|
+
const byKey = new Map();
|
|
4755
|
+
const urlPathOwners = new Map();
|
|
4756
|
+
for (const route of [...routeEntries, ...expandedRouteEntries]) {
|
|
4757
|
+
const key = route.id + ':' + route.canonicalUrlPath;
|
|
4758
|
+
if (byKey.has(key)) {
|
|
4759
|
+
throw new Error('Duplicate public sitemap route entry ' + key);
|
|
4760
|
+
}
|
|
4761
|
+
for (const language of languages) {
|
|
4762
|
+
const urlPath = route.localeUrlPaths?.[language];
|
|
4763
|
+
if (typeof urlPath !== 'string') {
|
|
4764
|
+
throw new Error(route.id + ' public route entry is missing ' + language + ' locale URL path');
|
|
4765
|
+
}
|
|
4766
|
+
const existingOwner = urlPathOwners.get(urlPath);
|
|
4767
|
+
if (existingOwner && existingOwner !== route.id) {
|
|
4768
|
+
throw new Error('Duplicate public sitemap URL path ' + urlPath + ' from ' + existingOwner + ' and ' + route.id);
|
|
4769
|
+
}
|
|
4770
|
+
urlPathOwners.set(urlPath, route.id);
|
|
4771
|
+
}
|
|
4772
|
+
byKey.set(key, route);
|
|
4773
|
+
}
|
|
4774
|
+
return Array.from(byKey.values()).sort(
|
|
4775
|
+
(left, right) =>
|
|
4776
|
+
left.canonicalUrlPath.localeCompare(right.canonicalUrlPath) ||
|
|
4777
|
+
left.id.localeCompare(right.id),
|
|
4778
|
+
);
|
|
4779
|
+
}
|
|
4780
|
+
|
|
4781
|
+
function uniqueSorted(values) {
|
|
4782
|
+
return Array.from(new Set(values)).sort((left, right) =>
|
|
4783
|
+
left.localeCompare(right),
|
|
4784
|
+
);
|
|
4785
|
+
}
|
|
4786
|
+
|
|
4787
|
+
function createConcreteUrlPaths(routeEntries, languages) {
|
|
4788
|
+
return uniqueSorted(
|
|
4789
|
+
routeEntries.flatMap(route => languages.map(language => route.localeUrlPaths[language])),
|
|
4790
|
+
);
|
|
4791
|
+
}
|
|
4792
|
+
|
|
4793
|
+
function escapeXmlText(value) {
|
|
4794
|
+
return value
|
|
4795
|
+
.replaceAll('&', '&')
|
|
4796
|
+
.replaceAll('<', '<')
|
|
4797
|
+
.replaceAll('>', '>');
|
|
4798
|
+
}
|
|
4799
|
+
|
|
4800
|
+
function escapeXmlAttribute(value) {
|
|
4801
|
+
return escapeXmlText(value).replaceAll('"', '"');
|
|
4802
|
+
}
|
|
4803
|
+
|
|
4804
|
+
function renderRobotsTxt(urlPaths, sitemapUrl) {
|
|
4805
|
+
const lines = ['User-agent: *'];
|
|
4806
|
+
if (urlPaths.length === 0) {
|
|
4807
|
+
lines.push('Disallow: /');
|
|
4808
|
+
} else {
|
|
4809
|
+
for (const urlPath of urlPaths) {
|
|
4810
|
+
lines.push(\`Allow: \${urlPath}$\`);
|
|
4811
|
+
}
|
|
4812
|
+
lines.push('Disallow: /');
|
|
4813
|
+
if (sitemapUrl) {
|
|
4814
|
+
lines.push(\`Sitemap: \${sitemapUrl}\`);
|
|
4815
|
+
}
|
|
4816
|
+
}
|
|
4817
|
+
return \`\${lines.join('\\n')}\\n\`;
|
|
4818
|
+
}
|
|
4819
|
+
|
|
4820
|
+
function renderSitemapXml(origin, routeEntries, languages) {
|
|
4821
|
+
const lines = [
|
|
4822
|
+
'<?xml version="1.0" encoding="UTF-8"?>',
|
|
4823
|
+
'<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9" xmlns:xhtml="http://www.w3.org/1999/xhtml">',
|
|
4824
|
+
];
|
|
4825
|
+
|
|
4826
|
+
for (const route of routeEntries) {
|
|
4827
|
+
for (const language of languages) {
|
|
4828
|
+
lines.push(' <url>');
|
|
4829
|
+
lines.push(\` <loc>\${escapeXmlText(\`\${origin}\${route.localeUrlPaths[language]}\`)}</loc>\`);
|
|
4830
|
+
for (const alternateLanguage of languages) {
|
|
4831
|
+
lines.push(
|
|
4832
|
+
\` <xhtml:link rel="alternate" hreflang="\${alternateLanguage}" href="\${escapeXmlAttribute(
|
|
4833
|
+
\`\${origin}\${route.localeUrlPaths[alternateLanguage]}\`,
|
|
4834
|
+
)}" />\`,
|
|
4835
|
+
);
|
|
4836
|
+
}
|
|
4837
|
+
lines.push(
|
|
4838
|
+
\` <xhtml:link rel="alternate" hreflang="x-default" href="\${escapeXmlAttribute(
|
|
4839
|
+
\`\${origin}\${route.localeUrlPaths.en}\`,
|
|
4840
|
+
)}" />\`,
|
|
4841
|
+
);
|
|
4842
|
+
if (route.lastModified) {
|
|
4843
|
+
lines.push(\` <lastmod>\${escapeXmlText(route.lastModified)}</lastmod>\`);
|
|
4844
|
+
}
|
|
4845
|
+
if (route.changeFrequency) {
|
|
4846
|
+
lines.push(\` <changefreq>\${escapeXmlText(route.changeFrequency)}</changefreq>\`);
|
|
4847
|
+
}
|
|
4848
|
+
if (route.priority !== undefined) {
|
|
4849
|
+
lines.push(\` <priority>\${route.priority.toFixed(1).replace(/\\.0$/u, '')}</priority>\`);
|
|
4850
|
+
}
|
|
4851
|
+
lines.push(' </url>');
|
|
4852
|
+
}
|
|
4853
|
+
}
|
|
4854
|
+
|
|
4855
|
+
lines.push('</urlset>');
|
|
4856
|
+
return \`\${lines.join('\\n')}\\n\`;
|
|
4857
|
+
}
|
|
4858
|
+
|
|
4859
|
+
function renderWebManifest(app, urlPaths) {
|
|
4860
|
+
const startUrl = urlPaths[0];
|
|
4861
|
+
const manifest = {
|
|
4862
|
+
background_color: '#ffffff',
|
|
4863
|
+
categories: ['business', 'productivity'],
|
|
4864
|
+
display: 'standalone',
|
|
4865
|
+
icons: [],
|
|
4866
|
+
lang: 'en',
|
|
4867
|
+
name: app.marker?.appId ?? app.id,
|
|
4868
|
+
short_name: app.marker?.appId ?? app.id,
|
|
4869
|
+
theme_color: '#133225',
|
|
4870
|
+
...(startUrl ? { scope: '/', start_url: startUrl } : {}),
|
|
4871
|
+
};
|
|
4872
|
+
return \`\${JSON.stringify(manifest, null, 2)}\\n\`;
|
|
4873
|
+
}
|
|
4874
|
+
|
|
4875
|
+
function removeIfExists(outputDir, fileName) {
|
|
4876
|
+
fs.rmSync(path.join(outputDir, fileName), { force: true });
|
|
4877
|
+
}
|
|
4878
|
+
|
|
4879
|
+
function writeText(outputDir, fileName, content) {
|
|
4880
|
+
fs.writeFileSync(path.join(outputDir, fileName), content);
|
|
4881
|
+
}
|
|
4882
|
+
|
|
4883
|
+
async function generatePublicSurfaceAssets(app, target, requirePublicOrigin) {
|
|
4884
|
+
const publicSurface = app.routes?.publicSurface ?? {};
|
|
4885
|
+
const languages = publicSurface.languages ?? ['en', 'cs'];
|
|
4886
|
+
const outputDir = ensureOutputDir(app, target);
|
|
4887
|
+
const shouldRequirePublicOrigin =
|
|
4888
|
+
requirePublicOrigin ||
|
|
4889
|
+
process.env.ULTRAMODERN_CLOUDFLARE_REQUIRE_PUBLIC_URLS === 'true';
|
|
4890
|
+
const routeEntries = mergeRouteEntries(
|
|
4891
|
+
publicSurface.routeEntries ?? [],
|
|
4892
|
+
await expandContentSources(app, publicSurface, languages),
|
|
4893
|
+
languages,
|
|
4894
|
+
);
|
|
4895
|
+
const urlPaths = createConcreteUrlPaths(routeEntries, languages);
|
|
4896
|
+
|
|
4897
|
+
if (routeEntries.length === 0) {
|
|
4898
|
+
writeText(outputDir, 'robots.txt', renderRobotsTxt([], undefined));
|
|
4899
|
+
removeIfExists(outputDir, 'sitemap.xml');
|
|
4900
|
+
removeIfExists(outputDir, 'site.webmanifest');
|
|
4901
|
+
return;
|
|
4902
|
+
}
|
|
4903
|
+
|
|
4904
|
+
const origin = resolveOrigin(app, shouldRequirePublicOrigin);
|
|
4905
|
+
if (!origin) {
|
|
4906
|
+
writeText(outputDir, 'robots.txt', renderRobotsTxt([], undefined));
|
|
4907
|
+
removeIfExists(outputDir, 'sitemap.xml');
|
|
4908
|
+
removeIfExists(outputDir, 'site.webmanifest');
|
|
4909
|
+
return;
|
|
4910
|
+
}
|
|
4911
|
+
|
|
4912
|
+
writeText(outputDir, 'sitemap.xml', renderSitemapXml(origin, routeEntries, languages));
|
|
4913
|
+
writeText(outputDir, 'site.webmanifest', renderWebManifest(app, urlPaths));
|
|
4914
|
+
writeText(outputDir, 'robots.txt', renderRobotsTxt(urlPaths, \`\${origin}/sitemap.xml\`));
|
|
4915
|
+
}
|
|
4916
|
+
|
|
4917
|
+
try {
|
|
4918
|
+
const args = parseArgs(process.argv.slice(2));
|
|
4919
|
+
if (args.help) {
|
|
4920
|
+
printHelp();
|
|
4921
|
+
process.exit(0);
|
|
4922
|
+
}
|
|
4923
|
+
const contract = readJson(contractPath);
|
|
4924
|
+
const app = contract.apps?.find(candidate => candidate.id === args.appId);
|
|
4925
|
+
if (!app) {
|
|
4926
|
+
throw new Error(\`Unknown app in generated contract: \${args.appId}\`);
|
|
4927
|
+
}
|
|
4928
|
+
await generatePublicSurfaceAssets(app, args.target, args.requirePublicOrigin);
|
|
4929
|
+
} catch (error) {
|
|
4930
|
+
process.stderr.write(\`[public-surface] \${error.message}\\n\`);
|
|
4931
|
+
process.exitCode = 1;
|
|
4932
|
+
}
|
|
4933
|
+
`;
|
|
4934
|
+
}
|
|
4935
|
+
function createWorkspaceValidationScript(scope, enableTailwind, remotes = []) {
|
|
4936
|
+
const verticals = remotes.filter(appHasEffectApi).map((remote)=>({
|
|
4937
|
+
id: remote.id,
|
|
4938
|
+
domain: remote.domain,
|
|
4939
|
+
stem: remote.effectApi.stem,
|
|
4940
|
+
group: verticalEffectGroupName(remote),
|
|
4941
|
+
path: remote.directory,
|
|
4942
|
+
mfName: remote.mfName,
|
|
4943
|
+
apiPrefix: remote.effectApi.prefix,
|
|
4944
|
+
tailwindPrefix: tailwindPrefixForApp(remote),
|
|
4945
|
+
zephyrAlias: remoteDependencyAlias(remote),
|
|
4946
|
+
packageName: ultramodern_workspace_packageName(scope, remote.packageSuffix),
|
|
4947
|
+
exposes: Object.keys(remote.exposes ?? {}),
|
|
4948
|
+
componentPaths: Object.keys(remote.exposes ?? {}).map((expose)=>remoteComponentOutputPath(remote, expose)).filter((componentPath)=>Boolean(componentPath)),
|
|
4949
|
+
namespace: appI18nNamespace(remote),
|
|
4950
|
+
routePagePaths: createRouteOwnedI18nPaths(remote).filter((route)=>'/' !== route.canonicalPath).map((route)=>createRoutePageFilePath(remote, route.canonicalPath)),
|
|
4951
|
+
routeMetaPaths: createRouteOwnedI18nPaths(remote).map((route)=>createRouteMetaFilePath(remote, route.canonicalPath)),
|
|
4952
|
+
localisedUrls: createLocalisedUrlsMap(remote),
|
|
4953
|
+
verticalRefs: remote.verticalRefs ?? []
|
|
4954
|
+
}));
|
|
4955
|
+
const shellRouteMetaPaths = createRouteOwnedI18nPaths(shellApp).map((route)=>createRouteMetaFilePath(shellApp, route.canonicalPath));
|
|
4956
|
+
const shellNamespace = appI18nNamespace(shellApp);
|
|
4957
|
+
const oldRemotePaths = [
|
|
4958
|
+
'apps/remotes'
|
|
4959
|
+
];
|
|
4960
|
+
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';
|
|
4961
|
+
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';
|
|
4962
|
+
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';
|
|
4963
|
+
const expectedCloudflareSecurity = createCloudflareSecurityContract();
|
|
4964
|
+
const contentExpansionPolicy = createPublicSurfaceContentExpansionPolicy();
|
|
4965
|
+
const robotsPolicy = createPublicHeadRobotsPolicy();
|
|
4966
|
+
const qualityGates = createPublicWebsiteQualityGateContract();
|
|
4967
|
+
return `import { execFileSync } from 'node:child_process';
|
|
4968
|
+
import fs from 'node:fs';
|
|
4969
|
+
import path from 'node:path';
|
|
4970
|
+
|
|
4971
|
+
const root = process.cwd();
|
|
4972
|
+
const packageScope = '${scope}';
|
|
4973
|
+
const expectedPnpmVersion = '${PNPM_VERSION}';
|
|
4974
|
+
const tailwindEnabled = ${JSON.stringify(enableTailwind)};
|
|
4975
|
+
const fullStackVerticals = ${JSON.stringify(verticals, null, 2)};
|
|
4976
|
+
const shellNamespace = ${JSON.stringify(shellNamespace)};
|
|
4977
|
+
const oldRemotePaths = ${JSON.stringify(oldRemotePaths, null, 2)};
|
|
4343
4978
|
const expectedBuildScript = ${JSON.stringify(expectedBuildScript)};
|
|
4344
4979
|
const expectedCloudflareBuildScript = ${JSON.stringify(expectedCloudflareBuildScript)};
|
|
4345
4980
|
const expectedCloudflareDeployScript = ${JSON.stringify(expectedCloudflareDeployScript)};
|
|
4346
4981
|
const expectedCloudflareSecurity = ${JSON.stringify(expectedCloudflareSecurity, null, 2)};
|
|
4347
|
-
const
|
|
4348
|
-
...
|
|
4349
|
-
], null, 2)};
|
|
4350
|
-
const publicSurfaceOptionalAssetPaths = ${JSON.stringify([
|
|
4351
|
-
...publicSurfaceOptionalAssetPaths
|
|
4982
|
+
const publicSurfaceManagedSourceAssetPaths = ${JSON.stringify([
|
|
4983
|
+
...publicSurfaceManagedSourceAssetPaths
|
|
4352
4984
|
], null, 2)};
|
|
4353
4985
|
const expectedModernPackageSpecifier = packageName => {
|
|
4354
4986
|
if (packageSource.strategy === 'workspace') {
|
|
@@ -4374,19 +5006,67 @@ const assertNotExists = relativePath => {
|
|
|
4374
5006
|
assert(!fs.existsSync(path.join(root, relativePath)), \`Unexpected \${relativePath}\`);
|
|
4375
5007
|
};
|
|
4376
5008
|
const assertPublicSurfaceAssets = (appPath, publicRoutes) => {
|
|
4377
|
-
const
|
|
4378
|
-
|
|
4379
|
-
|
|
4380
|
-
|
|
4381
|
-
|
|
4382
|
-
|
|
4383
|
-
|
|
5009
|
+
for (const relativePath of publicSurfaceManagedSourceAssetPaths) {
|
|
5010
|
+
assertNotExists(\`\${appPath}/\${relativePath}\`);
|
|
5011
|
+
}
|
|
5012
|
+
void publicRoutes;
|
|
5013
|
+
};
|
|
5014
|
+
const assertPublicSurfaceContract = (appId, publicSurface) => {
|
|
5015
|
+
assert(publicSurface?.artifactLifecycle === 'build-and-deploy-output', \`\${appId} public surface artifacts must be build/deploy outputs\`);
|
|
5016
|
+
assert(publicSurface?.generator === 'scripts/generate-public-surface-assets.mjs', \`\${appId} public surface generator script is incorrect\`);
|
|
5017
|
+
assert(publicSurface?.outputRoot === 'dist/public', \`\${appId} public surface dist outputRoot is incorrect\`);
|
|
5018
|
+
assert(publicSurface?.cloudflareOutputRoot === '.output/public', \`\${appId} public surface Cloudflare outputRoot is incorrect\`);
|
|
5019
|
+
assert(!('staticRoot' in (publicSurface ?? {})), \`\${appId} public surface must not point at source config/public\`);
|
|
5020
|
+
assert((publicSurface?.files ?? []).includes('robots.txt'), \`\${appId} public surface must always emit robots.txt\`);
|
|
5021
|
+
assert(publicSurface?.contentExpansion?.authoring === 'route-owned-esm-provider', \`\${appId} public content expansion authoring is incorrect\`);
|
|
5022
|
+
assert(publicSurface?.contentExpansion?.defaultProviderFile === '${contentExpansionPolicy.defaultProviderFile}', \`\${appId} public content expansion provider file is incorrect\`);
|
|
5023
|
+
assert(publicSurface?.contentExpansion?.draftPolicy === '${contentExpansionPolicy.draftPolicy}', \`\${appId} public content expansion draft policy is incorrect\`);
|
|
5024
|
+
assert(publicSurface?.contentExpansion?.indexablePolicy === '${contentExpansionPolicy.indexablePolicy}', \`\${appId} public content expansion indexable policy is incorrect\`);
|
|
5025
|
+
assert(Array.isArray(publicSurface?.contentSources), \`\${appId} public content sources must be an array\`);
|
|
5026
|
+
if ((publicSurface?.publicRoutes ?? []).length === 0) {
|
|
5027
|
+
assert(!(publicSurface?.files ?? []).includes('sitemap.xml'), \`\${appId} private public surface must omit sitemap.xml\`);
|
|
5028
|
+
assert(!(publicSurface?.files ?? []).includes('site.webmanifest'), \`\${appId} private public surface must omit site.webmanifest\`);
|
|
5029
|
+
} else {
|
|
5030
|
+
assert((publicSurface?.files ?? []).includes('sitemap.xml'), \`\${appId} public surface must emit sitemap.xml when public routes exist\`);
|
|
5031
|
+
assert((publicSurface?.files ?? []).includes('site.webmanifest'), \`\${appId} public surface must emit site.webmanifest when public routes exist\`);
|
|
5032
|
+
}
|
|
5033
|
+
};
|
|
5034
|
+
const assertPublicHeadContract = (appId, publicHead, headModule) => {
|
|
5035
|
+
assert(publicHead?.generator === './src/routes/ultramodern-route-head', \`\${appId} public head generator is incorrect\`);
|
|
5036
|
+
assert(publicHead?.renderer === '@modern-js/runtime/head Helmet', \`\${appId} public head renderer is incorrect\`);
|
|
5037
|
+
assert(publicHead?.ssr === true, \`\${appId} public head must be SSR-rendered\`);
|
|
5038
|
+
assert(publicHead?.title?.source === 'route.titleKey', \`\${appId} public head title must come from route metadata\`);
|
|
5039
|
+
assert(publicHead?.description?.source === 'route.descriptionKey', \`\${appId} public head description must come from route metadata\`);
|
|
5040
|
+
assert(publicHead?.canonical?.publicIndexableOnly === true, \`\${appId} canonical links must be public/indexable only\`);
|
|
5041
|
+
assert(publicHead?.structuredData?.sanitizesHtmlOpenBracket === true, \`\${appId} structured data must sanitize HTML open brackets\`);
|
|
5042
|
+
assert(publicHead?.privateRouteRobots === '${robotsPolicy.privateRouteRobots}', \`\${appId} private route robots policy is incorrect\`);
|
|
5043
|
+
for (const snippet of [
|
|
5044
|
+
"from '@modern-js/runtime/head'",
|
|
5045
|
+
'<title>{title}</title>',
|
|
5046
|
+
'name="description"',
|
|
5047
|
+
'name="robots"',
|
|
5048
|
+
'rel="canonical"',
|
|
5049
|
+
'rel="alternate"',
|
|
5050
|
+
'property="og:title"',
|
|
5051
|
+
'property="og:description"',
|
|
5052
|
+
'name="twitter:card"',
|
|
5053
|
+
'application/ld+json',
|
|
5054
|
+
"replaceAll('<', '\\\\\\\\u003c')",
|
|
5055
|
+
]) {
|
|
5056
|
+
assert(headModule.includes(snippet), \`\${appId} route head module is missing \${snippet}\`);
|
|
4384
5057
|
}
|
|
4385
|
-
|
|
4386
|
-
|
|
4387
|
-
assert(
|
|
4388
|
-
assert(
|
|
4389
|
-
assert(
|
|
5058
|
+
};
|
|
5059
|
+
const assertCloudflareQualityGates = (appId, qualityGates) => {
|
|
5060
|
+
assert(qualityGates?.publicRoutes?.requireSitemapWhenPresent === true, \`\${appId} quality gates must require sitemap for public routes\`);
|
|
5061
|
+
assert(qualityGates?.publicRoutes?.requireRobotsSitemapConsistency === true, \`\${appId} quality gates must require robots/sitemap consistency\`);
|
|
5062
|
+
assert(qualityGates?.statusCodes?.unknownRouteStatus === 404, \`\${appId} quality gates must require 404 unknown routes\`);
|
|
5063
|
+
assert(qualityGates?.indexing?.previewNoindex === true, \`\${appId} quality gates must require preview noindex\`);
|
|
5064
|
+
assert(qualityGates?.indexing?.productionPublicRoutesIndexable === true, \`\${appId} quality gates must require production public routes to be indexable\`);
|
|
5065
|
+
assert(qualityGates?.assets?.cssPreloadRequired === true, \`\${appId} quality gates must require CSS preload evidence\`);
|
|
5066
|
+
assert(qualityGates?.assets?.sourcemapsPubliclyReferenced === false, \`\${appId} quality gates must reject public sourcemap references\`);
|
|
5067
|
+
assert(typeof qualityGates?.budgets?.ssrHtmlMaxBytes === 'number', \`\${appId} quality gates must define SSR HTML byte budget\`);
|
|
5068
|
+
assert(typeof qualityGates?.budgets?.mfManifestMaxBytes === 'number', \`\${appId} quality gates must define MF manifest byte budget\`);
|
|
5069
|
+
assert(qualityGates?.csp?.finalMode === '${qualityGates.csp.finalMode}', \`\${appId} CSP final mode decision is missing\`);
|
|
4390
5070
|
};
|
|
4391
5071
|
const expectedWorkerName = packageSuffix => \`\${packageScope}-\${packageSuffix}\`.slice(0, 63);
|
|
4392
5072
|
const expectedChunkLoadingGlobal = mfName =>
|
|
@@ -4448,6 +5128,7 @@ const requiredPaths = [
|
|
|
4448
5128
|
'scripts/assert-mf-types.mjs',
|
|
4449
5129
|
'scripts/bootstrap-agent-skills.mjs',
|
|
4450
5130
|
'scripts/check-ultramodern-i18n-boundaries.mjs',
|
|
5131
|
+
'scripts/generate-public-surface-assets.mjs',
|
|
4451
5132
|
'scripts/proof-cloudflare-version.mjs',
|
|
4452
5133
|
'scripts/setup-agent-reference-repos.mjs',
|
|
4453
5134
|
'apps/shell-super-app/package.json',
|
|
@@ -4462,11 +5143,10 @@ const requiredPaths = [
|
|
|
4462
5143
|
\`apps/shell-super-app/locales/cs/\${shellNamespace}.json\`,
|
|
4463
5144
|
'apps/shell-super-app/src/routes/index.css',
|
|
4464
5145
|
'apps/shell-super-app/src/routes/layout.tsx',
|
|
5146
|
+
'apps/shell-super-app/src/routes/ultramodern-route-head.tsx',
|
|
4465
5147
|
'apps/shell-super-app/src/routes/ultramodern-route-metadata.ts',
|
|
4466
5148
|
'apps/shell-super-app/src/routes/[lang]/page.tsx',
|
|
4467
|
-
|
|
4468
|
-
relativePath => \`apps/shell-super-app/\${relativePath}\`,
|
|
4469
|
-
),
|
|
5149
|
+
...${JSON.stringify(shellRouteMetaPaths, null, 2)},
|
|
4470
5150
|
'packages/shared-contracts/src/index.ts',
|
|
4471
5151
|
'packages/shared-design-tokens/src/index.ts',
|
|
4472
5152
|
'packages/shared-design-tokens/src/tokens.css',
|
|
@@ -4491,12 +5171,11 @@ for (const vertical of fullStackVerticals) {
|
|
|
4491
5171
|
\`\${vertical.path}/locales/cs/\${vertical.namespace}.json\`,
|
|
4492
5172
|
\`\${vertical.path}/src/routes/index.css\`,
|
|
4493
5173
|
\`\${vertical.path}/src/routes/layout.tsx\`,
|
|
5174
|
+
\`\${vertical.path}/src/routes/ultramodern-route-head.tsx\`,
|
|
4494
5175
|
\`\${vertical.path}/src/routes/ultramodern-route-metadata.ts\`,
|
|
4495
5176
|
\`\${vertical.path}/src/routes/[lang]/page.tsx\`,
|
|
4496
|
-
...publicSurfaceRequiredAssetPaths.map(
|
|
4497
|
-
relativePath => \`\${vertical.path}/\${relativePath}\`,
|
|
4498
|
-
),
|
|
4499
5177
|
...vertical.routePagePaths,
|
|
5178
|
+
...vertical.routeMetaPaths,
|
|
4500
5179
|
);
|
|
4501
5180
|
}
|
|
4502
5181
|
|
|
@@ -4617,6 +5296,10 @@ assert(generatedContract.cssFederation?.sharedDesignTokens?.ssr?.firstPaintRequi
|
|
|
4617
5296
|
|
|
4618
5297
|
const shellPackage = readJson('apps/shell-super-app/package.json');
|
|
4619
5298
|
const shellModernConfig = readText('apps/shell-super-app/modern.config.ts');
|
|
5299
|
+
const shellRouteHead = readText('apps/shell-super-app/src/routes/ultramodern-route-head.tsx');
|
|
5300
|
+
const shellRouteMetadata = readText('apps/shell-super-app/src/routes/ultramodern-route-metadata.ts');
|
|
5301
|
+
assert(shellRouteMetadata.includes('@generated by @modern-js/create'), 'Shell route metadata compatibility manifest must be marked generated');
|
|
5302
|
+
assert(shellRouteMetadata.includes("authoring: 'colocated-route-meta'"), 'Shell route metadata manifest must advertise colocated authoring');
|
|
4620
5303
|
const expectedZephyrDependencies = Object.fromEntries(
|
|
4621
5304
|
fullStackVerticals.map(vertical => [
|
|
4622
5305
|
vertical.zephyrAlias,
|
|
@@ -4639,10 +5322,18 @@ assert(shellContract?.deploy?.cloudflare?.publicUrlEnv === 'ULTRAMODERN_PUBLIC_U
|
|
|
4639
5322
|
assert(shellContract?.deploy?.cloudflare?.compatibilityDate === expectedCloudflareCompatibilityDate, 'Shell Cloudflare compatibilityDate is incorrect');
|
|
4640
5323
|
assert(JSON.stringify(shellContract?.deploy?.cloudflare?.compatibilityFlags) === JSON.stringify(expectedCloudflareCompatibilityFlags), 'Shell Cloudflare compatibility flags are incorrect');
|
|
4641
5324
|
assert(JSON.stringify(shellContract?.deploy?.cloudflare?.security) === JSON.stringify(expectedCloudflareSecurity), 'Shell Cloudflare security contract is incorrect');
|
|
5325
|
+
assertCloudflareQualityGates('shell-super-app', shellContract?.deploy?.cloudflare?.qualityGates);
|
|
4642
5326
|
assert(shellContract?.deploy?.worker?.compatibilityDate === expectedCloudflareCompatibilityDate, 'Shell worker compatibilityDate is incorrect');
|
|
4643
5327
|
assert(shellContract?.deploy?.worker?.name === expectedWorkerName('shell-super-app'), 'Shell worker name is incorrect');
|
|
4644
5328
|
assert(shellModernConfig.includes("const cloudflareWorkerName = '" + expectedWorkerName('shell-super-app') + "'"), 'Shell modern.config.ts must define the Cloudflare worker name');
|
|
4645
5329
|
assert(shellModernConfig.includes('name: cloudflareWorkerName'), 'Shell modern.config.ts must wire deploy.worker.name');
|
|
5330
|
+
assert(shellModernConfig.includes('const assetPrefix ='), 'Shell modern.config.ts must derive a dedicated asset prefix');
|
|
5331
|
+
assert(shellModernConfig.includes("assetPrefix: '/'"), 'Shell modern.config.ts must keep dev assets origin-relative');
|
|
5332
|
+
assert(shellModernConfig.includes('assetPrefix,'), 'Shell modern.config.ts must wire output.assetPrefix to the derived asset prefix');
|
|
5333
|
+
assert(shellContract?.config?.dev?.assetPrefix === '/', 'Shell dev asset prefix must stay origin-relative');
|
|
5334
|
+
assert(shellContract?.config?.output?.assetPrefix?.default === '/', 'Shell asset prefix must default to origin-relative paths');
|
|
5335
|
+
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');
|
|
5336
|
+
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');
|
|
4646
5337
|
assert(shellContract?.config?.rspack?.output?.uniqueName === 'shellSuperApp', 'Shell Rspack uniqueName is incorrect');
|
|
4647
5338
|
assert(shellContract?.config?.rspack?.output?.chunkLoadingGlobal === expectedChunkLoadingGlobal('shellSuperApp'), 'Shell Rspack chunkLoadingGlobal is incorrect');
|
|
4648
5339
|
assert(topology.shell?.cloudflare?.workerName === expectedWorkerName('shell-super-app'), 'Shell topology Cloudflare workerName is incorrect');
|
|
@@ -4657,11 +5348,15 @@ assert(shellContract?.styling?.federation?.assets?.shared?.some(asset => asset.e
|
|
|
4657
5348
|
assert(shellContract?.styling?.federation?.dedupe?.duplicateBaseStylesAllowed === false, 'Shell CSS contract must forbid duplicated base styles');
|
|
4658
5349
|
assert(shellContract?.styling?.federation?.ssr?.firstPaintRequired === true, 'Shell CSS must be required for SSR first paint');
|
|
4659
5350
|
assert(shellContract?.routes?.privateByDefault === true, 'Shell routes must be private by default');
|
|
5351
|
+
assert(shellContract?.routes?.metadataAuthoring === 'colocated-route-meta', 'Shell route metadata authoring mode is incorrect');
|
|
5352
|
+
assert(shellContract?.routes?.generatedManifest === true, 'Shell route metadata manifest must be generated');
|
|
4660
5353
|
assert(shellContract?.routes?.publicnessDefault === 'private-app-screen', 'Shell route publicness default is incorrect');
|
|
4661
5354
|
assert(JSON.stringify(shellContract?.routes?.publicRoutes ?? []) === '[]', 'Shell must not expose generated public routes by default');
|
|
5355
|
+
assertPublicHeadContract('shell-super-app', shellContract?.routes?.publicHead, shellRouteHead);
|
|
5356
|
+
assertPublicSurfaceContract('shell-super-app', shellContract?.routes?.publicSurface);
|
|
4662
5357
|
assert(
|
|
4663
|
-
(shellContract?.routes?.owned ?? []).every(route => route.public === false && route.indexable === false && route.publicSurface === 'private-app-screen'),
|
|
4664
|
-
'Shell owned routes must be non-indexable private app screens by default',
|
|
5358
|
+
(shellContract?.routes?.owned ?? []).every(route => route.public === false && route.indexable === false && route.publicSurface === 'private-app-screen' && typeof route.descriptionKey === 'string'),
|
|
5359
|
+
'Shell owned routes must be non-indexable private app screens by default and include description keys',
|
|
4665
5360
|
);
|
|
4666
5361
|
assertPublicSurfaceAssets('apps/shell-super-app', shellContract?.routes?.publicRoutes ?? []);
|
|
4667
5362
|
assert(
|
|
@@ -4675,6 +5370,10 @@ assert(!('effectServices' in topology), 'Default APIs must be vertical-owned, no
|
|
|
4675
5370
|
for (const vertical of fullStackVerticals) {
|
|
4676
5371
|
const packageJson = readJson(\`\${vertical.path}/package.json\`);
|
|
4677
5372
|
const modernConfig = readText(\`\${vertical.path}/modern.config.ts\`);
|
|
5373
|
+
const routeHead = readText(\`\${vertical.path}/src/routes/ultramodern-route-head.tsx\`);
|
|
5374
|
+
const routeMetadata = readText(\`\${vertical.path}/src/routes/ultramodern-route-metadata.ts\`);
|
|
5375
|
+
assert(routeMetadata.includes('@generated by @modern-js/create'), \`\${vertical.id} route metadata compatibility manifest must be marked generated\`);
|
|
5376
|
+
assert(routeMetadata.includes("authoring: 'colocated-route-meta'"), \`\${vertical.id} route metadata manifest must advertise colocated authoring\`);
|
|
4678
5377
|
assert(packageJson.name === vertical.packageName, \`\${vertical.id} package name is incorrect\`);
|
|
4679
5378
|
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\`);
|
|
4680
5379
|
assert(packageJson.scripts?.['cloudflare:proof']?.includes(\`--app \${vertical.id}\`), \`\${vertical.id} must expose cloudflare:proof\`);
|
|
@@ -4707,16 +5406,23 @@ for (const vertical of fullStackVerticals) {
|
|
|
4707
5406
|
assert(contractEntry?.deploy?.cloudflare?.compatibilityDate === expectedCloudflareCompatibilityDate, \`\${vertical.id} Cloudflare compatibilityDate is incorrect\`);
|
|
4708
5407
|
assert(JSON.stringify(contractEntry?.deploy?.cloudflare?.compatibilityFlags) === JSON.stringify(expectedCloudflareCompatibilityFlags), \`\${vertical.id} Cloudflare compatibility flags are incorrect\`);
|
|
4709
5408
|
assert(JSON.stringify(contractEntry?.deploy?.cloudflare?.security) === JSON.stringify(expectedCloudflareSecurity), \`\${vertical.id} Cloudflare security contract is incorrect\`);
|
|
5409
|
+
assertCloudflareQualityGates(vertical.id, contractEntry?.deploy?.cloudflare?.qualityGates);
|
|
4710
5410
|
assert(contractEntry?.deploy?.worker?.compatibilityDate === expectedCloudflareCompatibilityDate, \`\${vertical.id} worker compatibilityDate is incorrect\`);
|
|
4711
5411
|
assert(contractEntry?.deploy?.worker?.name === expectedWorkerName(vertical.id), \`\${vertical.id} worker name is incorrect\`);
|
|
4712
5412
|
assert(modernConfig.includes("const cloudflareWorkerName = '" + expectedWorkerName(vertical.id) + "'"), \`\${vertical.id} modern.config.ts must define the Cloudflare worker name\`);
|
|
4713
5413
|
assert(modernConfig.includes('name: cloudflareWorkerName'), \`\${vertical.id} modern.config.ts must wire deploy.worker.name\`);
|
|
5414
|
+
assert(modernConfig.includes('const assetPrefix ='), \`\${vertical.id} modern.config.ts must derive a dedicated asset prefix\`);
|
|
5415
|
+
assert(modernConfig.includes("assetPrefix: '/'"), \`\${vertical.id} modern.config.ts must keep dev assets origin-relative\`);
|
|
5416
|
+
assert(modernConfig.includes('assetPrefix,'), \`\${vertical.id} modern.config.ts must wire output.assetPrefix to the derived asset prefix\`);
|
|
5417
|
+
assert(contractEntry?.config?.dev?.assetPrefix === '/', \`\${vertical.id} dev asset prefix must stay origin-relative\`);
|
|
5418
|
+
assert(contractEntry?.config?.output?.assetPrefix?.default === '/', \`\${vertical.id} asset prefix must default to origin-relative paths\`);
|
|
5419
|
+
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\`);
|
|
4714
5420
|
assert(contractEntry?.deploy?.cloudflare?.routes?.effectReadiness === \`\${vertical.apiPrefix}/effect/\${vertical.stem}/readiness\`, \`\${vertical.id} Cloudflare proof readiness route is incorrect\`);
|
|
4715
5421
|
assert(contractEntry?.config?.rspack?.output?.uniqueName === vertical.mfName, \`\${vertical.id} Rspack uniqueName is incorrect\`);
|
|
4716
5422
|
assert(contractEntry?.config?.rspack?.output?.chunkLoadingGlobal === expectedChunkLoadingGlobal(vertical.mfName), \`\${vertical.id} Rspack chunkLoadingGlobal is incorrect\`);
|
|
4717
5423
|
assert(contractEntry?.moduleFederation?.name === vertical.mfName, \`\${vertical.id} MF name is incorrect\`);
|
|
4718
5424
|
assert(JSON.stringify(contractEntry?.moduleFederation?.exposes) === JSON.stringify(vertical.exposes), \`\${vertical.id} MF exposes are incorrect\`);
|
|
4719
|
-
assert(contractEntry?.moduleFederation?.dts?.compilerInstance === '
|
|
5425
|
+
assert(contractEntry?.moduleFederation?.dts?.compilerInstance === 'tsgo', \`\${vertical.id} must keep mandatory DTS compiler\`);
|
|
4720
5426
|
assert(JSON.stringify(contractEntry?.moduleFederation?.verticalRefs ?? []) === JSON.stringify(vertical.verticalRefs), \`\${vertical.id} MF verticalRefs are incorrect\`);
|
|
4721
5427
|
assert(
|
|
4722
5428
|
JSON.stringify((contractEntry?.moduleFederation?.remotes ?? []).map(remote => remote.id)) ===
|
|
@@ -4736,13 +5442,17 @@ for (const vertical of fullStackVerticals) {
|
|
|
4736
5442
|
\`\${vertical.id} localisedUrls must come from route metadata\`,
|
|
4737
5443
|
);
|
|
4738
5444
|
assert(contractEntry?.routes?.source === 'route-owned', \`\${vertical.id} routes must be route-owned\`);
|
|
5445
|
+
assert(contractEntry?.routes?.metadataAuthoring === 'colocated-route-meta', \`\${vertical.id} route metadata authoring mode is incorrect\`);
|
|
5446
|
+
assert(contractEntry?.routes?.generatedManifest === true, \`\${vertical.id} route metadata manifest must be generated\`);
|
|
4739
5447
|
assert(contractEntry?.routes?.metadataExport === './src/routes/ultramodern-route-metadata', \`\${vertical.id} route metadata export is incorrect\`);
|
|
4740
5448
|
assert(contractEntry?.routes?.privateByDefault === true, \`\${vertical.id} routes must be private by default\`);
|
|
4741
5449
|
assert(contractEntry?.routes?.publicnessDefault === 'private-app-screen', \`\${vertical.id} route publicness default is incorrect\`);
|
|
4742
5450
|
assert(JSON.stringify(contractEntry?.routes?.publicRoutes ?? []) === '[]', \`\${vertical.id} must not expose generated public routes by default\`);
|
|
5451
|
+
assertPublicHeadContract(vertical.id, contractEntry?.routes?.publicHead, routeHead);
|
|
5452
|
+
assertPublicSurfaceContract(vertical.id, contractEntry?.routes?.publicSurface);
|
|
4743
5453
|
assert(
|
|
4744
|
-
(contractEntry?.routes?.owned ?? []).every(route => route.public === false && route.indexable === false && route.publicSurface === 'private-app-screen'),
|
|
4745
|
-
\`\${vertical.id} owned routes must be non-indexable private app screens by default\`,
|
|
5454
|
+
(contractEntry?.routes?.owned ?? []).every(route => route.public === false && route.indexable === false && route.publicSurface === 'private-app-screen' && typeof route.descriptionKey === 'string'),
|
|
5455
|
+
\`\${vertical.id} owned routes must be non-indexable private app screens by default and include description keys\`,
|
|
4746
5456
|
);
|
|
4747
5457
|
assertPublicSurfaceAssets(vertical.path, contractEntry?.routes?.publicRoutes ?? []);
|
|
4748
5458
|
assert(contractEntry?.styling?.federation?.owner?.id === vertical.id, \`\${vertical.id} CSS federation owner is missing\`);
|
|
@@ -4759,84 +5469,28 @@ for (const vertical of fullStackVerticals) {
|
|
|
4759
5469
|
const topologyEntry = topology.verticals?.find(verticalEntry => verticalEntry.id === vertical.id);
|
|
4760
5470
|
assert(topologyEntry?.kind === 'vertical', \`\${vertical.id} topology kind is incorrect\`);
|
|
4761
5471
|
assert(topologyEntry?.package === vertical.packageName, \`\${vertical.id} topology package is incorrect\`);
|
|
4762
|
-
assert(topologyEntry?.cloudflare?.workerName === expectedWorkerName(vertical.id), \`\${vertical.id} topology Cloudflare workerName is incorrect\`);
|
|
4763
|
-
assert(topologyEntry?.moduleFederation?.name === vertical.mfName, \`\${vertical.id} topology MF name is incorrect\`);
|
|
4764
|
-
assert(JSON.stringify(topologyEntry?.moduleFederation?.exposes) === JSON.stringify(vertical.exposes), \`\${vertical.id} topology exposes are incorrect\`);
|
|
4765
|
-
assert(JSON.stringify(topologyEntry?.moduleFederation?.verticalRefs ?? []) === JSON.stringify(vertical.verticalRefs), \`\${vertical.id} topology verticalRefs are incorrect\`);
|
|
4766
|
-
assert(topologyEntry?.api?.effect?.bff?.prefix === vertical.apiPrefix, \`\${vertical.id} topology API prefix is incorrect\`);
|
|
4767
|
-
assert(topologyEntry?.api?.effect?.serverEntry === \`\${vertical.path}/api/effect/index.ts\`, \`\${vertical.id} topology server entry is incorrect\`);
|
|
4768
|
-
assert(topologyEntry?.api?.effect?.readiness?.endpoint === \`/effect/\${vertical.stem}/readiness\`, \`\${vertical.id} topology readiness endpoint is incorrect\`);
|
|
4769
|
-
assert(Object.keys(topologyEntry?.api?.effect?.domainOperations ?? {}).length >= 3, \`\${vertical.id} topology domain operations are missing\`);
|
|
4770
|
-
|
|
4771
|
-
assert(ownership.owners?.some(owner => owner.id === vertical.id && owner.path === vertical.path), \`\${vertical.id} ownership entry is missing\`);
|
|
4772
|
-
assert(overlay.ports?.[vertical.id], \`\${vertical.id} development port is missing\`);
|
|
4773
|
-
assert(overlay.manifests?.[vertical.id]?.includes('/mf-manifest.json'), \`\${vertical.id} development manifest is missing\`);
|
|
4774
|
-
assert(overlay.apis?.[vertical.id]?.endsWith(vertical.apiPrefix), \`\${vertical.id} development API URL is missing\`);
|
|
4775
|
-
}
|
|
4776
|
-
|
|
4777
|
-
console.log('UltraModern workspace scaffold validated');
|
|
4778
|
-
`;
|
|
4779
|
-
}
|
|
4780
|
-
function createCloudflareVersionProofScript() {
|
|
4781
|
-
return `#!/usr/bin/env node
|
|
4782
|
-
import fs from 'node:fs';
|
|
4783
|
-
import path from 'node:path';
|
|
4784
|
-
import { fileURLToPath } from 'node:url';
|
|
4785
|
-
|
|
4786
|
-
const workspaceRoot = path.resolve(
|
|
4787
|
-
path.dirname(fileURLToPath(import.meta.url)),
|
|
4788
|
-
'..',
|
|
4789
|
-
);
|
|
4790
|
-
const contractPath = path.join(
|
|
4791
|
-
workspaceRoot,
|
|
4792
|
-
'.modernjs/ultramodern-generated-contract.json',
|
|
4793
|
-
);
|
|
4794
|
-
const defaultOut = path.join(
|
|
4795
|
-
workspaceRoot,
|
|
4796
|
-
'.codex/reports/cloudflare-version-proof/public-url-proof.json',
|
|
4797
|
-
);
|
|
4798
|
-
|
|
4799
|
-
function readJson(filePath) {
|
|
4800
|
-
return JSON.parse(fs.readFileSync(filePath, 'utf8'));
|
|
4801
|
-
}
|
|
4802
|
-
|
|
4803
|
-
function parseArgs(argv) {
|
|
4804
|
-
const parsed = {
|
|
4805
|
-
appId: undefined,
|
|
4806
|
-
out: defaultOut,
|
|
4807
|
-
requirePublicUrls: false,
|
|
4808
|
-
};
|
|
4809
|
-
|
|
4810
|
-
for (let index = 0; index < argv.length; index += 1) {
|
|
4811
|
-
const arg = argv[index];
|
|
4812
|
-
if (arg === '--app') {
|
|
4813
|
-
parsed.appId = argv[index + 1];
|
|
4814
|
-
index += 1;
|
|
4815
|
-
} else if (arg === '--out') {
|
|
4816
|
-
parsed.out = argv[index + 1];
|
|
4817
|
-
index += 1;
|
|
4818
|
-
} else if (arg === '--require-public-urls') {
|
|
4819
|
-
parsed.requirePublicUrls = true;
|
|
4820
|
-
} else if (arg === '--help' || arg === '-h') {
|
|
4821
|
-
parsed.help = true;
|
|
4822
|
-
} else {
|
|
4823
|
-
throw new Error(\`Unknown argument: \${arg}\`);
|
|
4824
|
-
}
|
|
4825
|
-
}
|
|
5472
|
+
assert(topologyEntry?.cloudflare?.workerName === expectedWorkerName(vertical.id), \`\${vertical.id} topology Cloudflare workerName is incorrect\`);
|
|
5473
|
+
assert(topologyEntry?.moduleFederation?.name === vertical.mfName, \`\${vertical.id} topology MF name is incorrect\`);
|
|
5474
|
+
assert(JSON.stringify(topologyEntry?.moduleFederation?.exposes) === JSON.stringify(vertical.exposes), \`\${vertical.id} topology exposes are incorrect\`);
|
|
5475
|
+
assert(JSON.stringify(topologyEntry?.moduleFederation?.verticalRefs ?? []) === JSON.stringify(vertical.verticalRefs), \`\${vertical.id} topology verticalRefs are incorrect\`);
|
|
5476
|
+
assert(topologyEntry?.api?.effect?.bff?.prefix === vertical.apiPrefix, \`\${vertical.id} topology API prefix is incorrect\`);
|
|
5477
|
+
assert(topologyEntry?.api?.effect?.serverEntry === \`\${vertical.path}/api/effect/index.ts\`, \`\${vertical.id} topology server entry is incorrect\`);
|
|
5478
|
+
assert(topologyEntry?.api?.effect?.readiness?.endpoint === \`/effect/\${vertical.stem}/readiness\`, \`\${vertical.id} topology readiness endpoint is incorrect\`);
|
|
5479
|
+
assert(Object.keys(topologyEntry?.api?.effect?.domainOperations ?? {}).length >= 3, \`\${vertical.id} topology domain operations are missing\`);
|
|
4826
5480
|
|
|
4827
|
-
|
|
5481
|
+
assert(ownership.owners?.some(owner => owner.id === vertical.id && owner.path === vertical.path), \`\${vertical.id} ownership entry is missing\`);
|
|
5482
|
+
assert(overlay.ports?.[vertical.id], \`\${vertical.id} development port is missing\`);
|
|
5483
|
+
assert(overlay.manifests?.[vertical.id]?.includes('/mf-manifest.json'), \`\${vertical.id} development manifest is missing\`);
|
|
5484
|
+
assert(overlay.apis?.[vertical.id]?.endsWith(vertical.apiPrefix), \`\${vertical.id} development API URL is missing\`);
|
|
4828
5485
|
}
|
|
4829
5486
|
|
|
4830
|
-
|
|
4831
|
-
|
|
4832
|
-
node scripts/proof-cloudflare-version.mjs [--app workspace] [--out evidence.json] [--require-public-urls]
|
|
4833
|
-
|
|
4834
|
-
Set each app's public URL using the contract env key, for example:
|
|
4835
|
-
ULTRAMODERN_PUBLIC_URL_WORKSPACE=https://workspace.example.workers.dev
|
|
4836
|
-
\`);
|
|
5487
|
+
console.log('UltraModern workspace scaffold validated');
|
|
5488
|
+
`;
|
|
4837
5489
|
}
|
|
4838
|
-
|
|
4839
|
-
|
|
5490
|
+
function createCloudflareProofHelperScript() {
|
|
5491
|
+
const robotsPolicy = createPublicHeadRobotsPolicy();
|
|
5492
|
+
const qualityGates = createPublicWebsiteQualityGateContract();
|
|
5493
|
+
return `function joinUrl(baseUrl, routePath) {
|
|
4840
5494
|
return new URL(routePath, baseUrl.endsWith('/') ? baseUrl : \`\${baseUrl}/\`);
|
|
4841
5495
|
}
|
|
4842
5496
|
|
|
@@ -4850,6 +5504,8 @@ async function fetchText(url) {
|
|
|
4850
5504
|
ok: response.ok,
|
|
4851
5505
|
status: response.status,
|
|
4852
5506
|
accessControlAllowOrigin: response.headers.get('access-control-allow-origin'),
|
|
5507
|
+
cacheControl: response.headers.get('cache-control'),
|
|
5508
|
+
contentLength: response.headers.get('content-length'),
|
|
4853
5509
|
contentSecurityPolicy: response.headers.get('content-security-policy'),
|
|
4854
5510
|
contentSecurityPolicyReportOnly: response.headers.get('content-security-policy-report-only'),
|
|
4855
5511
|
contentType: response.headers.get('content-type'),
|
|
@@ -4908,6 +5564,52 @@ function assert(condition, message) {
|
|
|
4908
5564
|
}
|
|
4909
5565
|
}
|
|
4910
5566
|
|
|
5567
|
+
function responseByteLength(response) {
|
|
5568
|
+
return Buffer.byteLength(response.body, 'utf8');
|
|
5569
|
+
}
|
|
5570
|
+
|
|
5571
|
+
function assertByteBudget(evidence, app, response, options) {
|
|
5572
|
+
const bytes = responseByteLength(response);
|
|
5573
|
+
const passed = bytes <= options.maxBytes;
|
|
5574
|
+
evidence.assertions.push({
|
|
5575
|
+
type: 'byte-budget',
|
|
5576
|
+
label: options.label,
|
|
5577
|
+
route: options.route,
|
|
5578
|
+
actualBytes: bytes,
|
|
5579
|
+
maxBytes: options.maxBytes,
|
|
5580
|
+
status: passed ? 'pass' : 'fail',
|
|
5581
|
+
});
|
|
5582
|
+
assert(
|
|
5583
|
+
passed,
|
|
5584
|
+
app.id + ' ' + options.route + ' exceeds ' + options.label + ' byte budget: ' + bytes + ' > ' + options.maxBytes,
|
|
5585
|
+
);
|
|
5586
|
+
}
|
|
5587
|
+
|
|
5588
|
+
function assertContentType(evidence, app, response, options) {
|
|
5589
|
+
const actual = response.contentType ?? '';
|
|
5590
|
+
const passed = actual.toLowerCase().includes(options.includes);
|
|
5591
|
+
evidence.assertions.push({
|
|
5592
|
+
type: 'content-type',
|
|
5593
|
+
route: options.route,
|
|
5594
|
+
expectedIncludes: options.includes,
|
|
5595
|
+
actual,
|
|
5596
|
+
status: passed ? 'pass' : 'fail',
|
|
5597
|
+
});
|
|
5598
|
+
assert(passed, app.id + ' ' + options.route + ' content-type must include ' + options.includes);
|
|
5599
|
+
}
|
|
5600
|
+
|
|
5601
|
+
function assertCacheControl(evidence, app, response, options) {
|
|
5602
|
+
const actual = response.cacheControl ?? '';
|
|
5603
|
+
const passed = options.required === false || actual.trim() !== '';
|
|
5604
|
+
evidence.assertions.push({
|
|
5605
|
+
type: 'cache-control',
|
|
5606
|
+
route: options.route,
|
|
5607
|
+
actual,
|
|
5608
|
+
status: passed ? 'pass' : 'fail',
|
|
5609
|
+
});
|
|
5610
|
+
assert(passed, app.id + ' ' + options.route + ' is missing cache-control');
|
|
5611
|
+
}
|
|
5612
|
+
|
|
4911
5613
|
function matchesPreviewHostname(hostname, pattern) {
|
|
4912
5614
|
const normalizedHostname = hostname.toLowerCase();
|
|
4913
5615
|
const normalizedPattern = String(pattern || '').toLowerCase();
|
|
@@ -5032,15 +5734,382 @@ function assertCloudflareSecurity(evidence, app, response, route, publicUrl, opt
|
|
|
5032
5734
|
type: 'security-noindex',
|
|
5033
5735
|
route,
|
|
5034
5736
|
actual: response.xRobotsTag,
|
|
5035
|
-
status: response.xRobotsTag === '
|
|
5737
|
+
status: response.xRobotsTag === '${robotsPolicy.privateRouteRobots}' ? 'pass' : 'fail',
|
|
5036
5738
|
});
|
|
5037
5739
|
assert(
|
|
5038
|
-
response.xRobotsTag === '
|
|
5740
|
+
response.xRobotsTag === '${robotsPolicy.privateRouteRobots}',
|
|
5039
5741
|
\`\${app.id} \${route} is missing noindex X-Robots-Tag\`,
|
|
5040
5742
|
);
|
|
5041
5743
|
}
|
|
5042
5744
|
}
|
|
5043
5745
|
|
|
5746
|
+
function collectStringValues(value, results = []) {
|
|
5747
|
+
if (typeof value === 'string') {
|
|
5748
|
+
results.push(value);
|
|
5749
|
+
return results;
|
|
5750
|
+
}
|
|
5751
|
+
if (Array.isArray(value)) {
|
|
5752
|
+
for (const item of value) {
|
|
5753
|
+
collectStringValues(item, results);
|
|
5754
|
+
}
|
|
5755
|
+
return results;
|
|
5756
|
+
}
|
|
5757
|
+
if (value && typeof value === 'object') {
|
|
5758
|
+
for (const item of Object.values(value)) {
|
|
5759
|
+
collectStringValues(item, results);
|
|
5760
|
+
}
|
|
5761
|
+
}
|
|
5762
|
+
return results;
|
|
5763
|
+
}
|
|
5764
|
+
|
|
5765
|
+
function assertNoPublicSourcemapRefs(evidence, app, manifestJson) {
|
|
5766
|
+
const sourcemapRefs = collectStringValues(manifestJson).filter(value =>
|
|
5767
|
+
/\\.map(?:$|[?#])/u.test(value),
|
|
5768
|
+
);
|
|
5769
|
+
evidence.assertions.push({
|
|
5770
|
+
type: 'sourcemap-policy',
|
|
5771
|
+
actual: sourcemapRefs,
|
|
5772
|
+
status: sourcemapRefs.length === 0 ? 'pass' : 'fail',
|
|
5773
|
+
});
|
|
5774
|
+
assert(
|
|
5775
|
+
sourcemapRefs.length === 0,
|
|
5776
|
+
app.id + ' MF manifest must not publicly reference sourcemaps',
|
|
5777
|
+
);
|
|
5778
|
+
}
|
|
5779
|
+
|
|
5780
|
+
function extractPreloadStyleUrls(linkHeader, publicUrl) {
|
|
5781
|
+
const urls = [];
|
|
5782
|
+
for (const match of String(linkHeader || '').matchAll(/<([^>]+)>\\s*;[^,]*rel=preload[^,]*as=style/giu)) {
|
|
5783
|
+
urls.push(String(joinUrl(publicUrl, match[1])));
|
|
5784
|
+
}
|
|
5785
|
+
return urls;
|
|
5786
|
+
}
|
|
5787
|
+
|
|
5788
|
+
function htmlHasRobotsDirective(html, expectedContent) {
|
|
5789
|
+
return htmlHasTagWithAttributes(html, 'meta', {
|
|
5790
|
+
name: 'robots',
|
|
5791
|
+
content: expectedContent,
|
|
5792
|
+
});
|
|
5793
|
+
}
|
|
5794
|
+
|
|
5795
|
+
function escapeRegExp(value) {
|
|
5796
|
+
return String(value).replace(/[.*+?^\${}()|[\\]\\\\]/g, '\\\\$&');
|
|
5797
|
+
}
|
|
5798
|
+
|
|
5799
|
+
function htmlHasTagWithAttributes(html, tagName, attributes) {
|
|
5800
|
+
const tagPattern = new RegExp(\`<\${tagName}\\\\b[^>]*>\`, 'giu');
|
|
5801
|
+
const tags = html.match(tagPattern) || [];
|
|
5802
|
+
return tags.some(tag =>
|
|
5803
|
+
Object.entries(attributes).every(([name, value]) => {
|
|
5804
|
+
const attrPattern = new RegExp(
|
|
5805
|
+
\`\\\\b\${escapeRegExp(name)}=["']\${escapeRegExp(value)}["']\`,
|
|
5806
|
+
'iu',
|
|
5807
|
+
);
|
|
5808
|
+
return attrPattern.test(tag);
|
|
5809
|
+
}),
|
|
5810
|
+
);
|
|
5811
|
+
}
|
|
5812
|
+
|
|
5813
|
+
function assertHeadTag(evidence, html, options) {
|
|
5814
|
+
const found = htmlHasTagWithAttributes(
|
|
5815
|
+
html,
|
|
5816
|
+
options.tag,
|
|
5817
|
+
options.attributes,
|
|
5818
|
+
);
|
|
5819
|
+
evidence.assertions.push({
|
|
5820
|
+
type: 'ssr-head',
|
|
5821
|
+
route: options.route,
|
|
5822
|
+
tag: options.tag,
|
|
5823
|
+
attributes: options.attributes,
|
|
5824
|
+
status: found ? 'pass' : 'fail',
|
|
5825
|
+
});
|
|
5826
|
+
assert(found, \`\${options.appId} \${options.route} SSR head is missing \${options.label}\`);
|
|
5827
|
+
}
|
|
5828
|
+
|
|
5829
|
+
async function validateSsrHead(evidence, app, publicUrl, ssrRoute, ssr) {
|
|
5830
|
+
const titleFound = /<title\\b[^>]*>[^<]+<\\/title>/iu.test(ssr.body);
|
|
5831
|
+
evidence.assertions.push({
|
|
5832
|
+
type: 'ssr-head',
|
|
5833
|
+
route: ssrRoute,
|
|
5834
|
+
tag: 'title',
|
|
5835
|
+
status: titleFound ? 'pass' : 'fail',
|
|
5836
|
+
});
|
|
5837
|
+
assert(titleFound, \`\${app.id} \${ssrRoute} SSR head is missing title\`);
|
|
5838
|
+
assertHeadTag(evidence, ssr.body, {
|
|
5839
|
+
appId: app.id,
|
|
5840
|
+
route: ssrRoute,
|
|
5841
|
+
tag: 'meta',
|
|
5842
|
+
attributes: { name: 'description' },
|
|
5843
|
+
label: 'description meta',
|
|
5844
|
+
});
|
|
5845
|
+
assertHeadTag(evidence, ssr.body, {
|
|
5846
|
+
appId: app.id,
|
|
5847
|
+
route: ssrRoute,
|
|
5848
|
+
tag: 'meta',
|
|
5849
|
+
attributes: { name: 'robots' },
|
|
5850
|
+
label: 'robots meta',
|
|
5851
|
+
});
|
|
5852
|
+
|
|
5853
|
+
const publicSurface = app.routes?.publicSurface ?? {};
|
|
5854
|
+
const routeEntry = (publicSurface.routeEntries ?? [])[0];
|
|
5855
|
+
if (!routeEntry) {
|
|
5856
|
+
const canonicalFound = htmlHasTagWithAttributes(ssr.body, 'link', {
|
|
5857
|
+
rel: 'canonical',
|
|
5858
|
+
});
|
|
5859
|
+
evidence.assertions.push({
|
|
5860
|
+
type: 'ssr-head-private-canonical',
|
|
5861
|
+
route: ssrRoute,
|
|
5862
|
+
status: canonicalFound ? 'fail' : 'pass',
|
|
5863
|
+
});
|
|
5864
|
+
assert(!canonicalFound, \`\${app.id} \${ssrRoute} private SSR head must not emit canonical links\`);
|
|
5865
|
+
return;
|
|
5866
|
+
}
|
|
5867
|
+
|
|
5868
|
+
const publicRoute = routeEntry.localeUrlPaths?.en ?? publicSurface.concreteUrlPaths?.[0];
|
|
5869
|
+
const headRoute = publicRoute || ssrRoute;
|
|
5870
|
+
const headResponse =
|
|
5871
|
+
headRoute === ssrRoute ? ssr : await fetchText(joinUrl(publicUrl, headRoute));
|
|
5872
|
+
if (headRoute !== ssrRoute) {
|
|
5873
|
+
evidence.assertions.push({
|
|
5874
|
+
type: 'ssr-head-route',
|
|
5875
|
+
route: headRoute,
|
|
5876
|
+
status: headResponse.ok ? 'pass' : 'fail',
|
|
5877
|
+
statusCode: headResponse.status,
|
|
5878
|
+
});
|
|
5879
|
+
assert(headResponse.ok, \`\${app.id} public head route returned HTTP \${headResponse.status}\`);
|
|
5880
|
+
assertCloudflareSecurity(evidence, app, headResponse, headRoute, publicUrl, {
|
|
5881
|
+
html: true,
|
|
5882
|
+
});
|
|
5883
|
+
}
|
|
5884
|
+
const isPreview = shouldNoindexUrl(publicUrl, app.deploy?.cloudflare?.security?.noindex);
|
|
5885
|
+
const robotsIndexable = htmlHasRobotsDirective(headResponse.body, '${robotsPolicy.indexableRobots}');
|
|
5886
|
+
evidence.assertions.push({
|
|
5887
|
+
type: 'indexing-policy',
|
|
5888
|
+
route: headRoute,
|
|
5889
|
+
mode: isPreview ? 'preview' : 'production',
|
|
5890
|
+
xRobotsTag: headResponse.xRobotsTag,
|
|
5891
|
+
htmlRobotsIndexable: robotsIndexable,
|
|
5892
|
+
status:
|
|
5893
|
+
isPreview || (headResponse.xRobotsTag !== '${robotsPolicy.privateRouteRobots}' && robotsIndexable)
|
|
5894
|
+
? 'pass'
|
|
5895
|
+
: 'fail',
|
|
5896
|
+
});
|
|
5897
|
+
if (!isPreview) {
|
|
5898
|
+
assert(
|
|
5899
|
+
headResponse.xRobotsTag !== '${robotsPolicy.privateRouteRobots}' && robotsIndexable,
|
|
5900
|
+
\`\${app.id} \${headRoute} production public route must be indexable\`,
|
|
5901
|
+
);
|
|
5902
|
+
}
|
|
5903
|
+
|
|
5904
|
+
const canonicalUrl = String(joinUrl(publicUrl, headRoute));
|
|
5905
|
+
assertHeadTag(evidence, headResponse.body, {
|
|
5906
|
+
appId: app.id,
|
|
5907
|
+
route: headRoute,
|
|
5908
|
+
tag: 'link',
|
|
5909
|
+
attributes: { rel: 'canonical', href: canonicalUrl },
|
|
5910
|
+
label: 'canonical link',
|
|
5911
|
+
});
|
|
5912
|
+
for (const language of app.routes?.publicHead?.alternates?.hreflang ?? []) {
|
|
5913
|
+
const href = String(joinUrl(publicUrl, routeEntry.localeUrlPaths?.[language] ?? headRoute));
|
|
5914
|
+
assertHeadTag(evidence, headResponse.body, {
|
|
5915
|
+
appId: app.id,
|
|
5916
|
+
route: headRoute,
|
|
5917
|
+
tag: 'link',
|
|
5918
|
+
attributes: { rel: 'alternate', hreflang: language, href },
|
|
5919
|
+
label: \`hreflang \${language}\`,
|
|
5920
|
+
});
|
|
5921
|
+
}
|
|
5922
|
+
assertHeadTag(evidence, headResponse.body, {
|
|
5923
|
+
appId: app.id,
|
|
5924
|
+
route: headRoute,
|
|
5925
|
+
tag: 'link',
|
|
5926
|
+
attributes: { rel: 'alternate', hreflang: 'x-default' },
|
|
5927
|
+
label: 'x-default hreflang',
|
|
5928
|
+
});
|
|
5929
|
+
for (const property of ['og:title', 'og:description', 'og:url', 'og:type']) {
|
|
5930
|
+
assertHeadTag(evidence, headResponse.body, {
|
|
5931
|
+
appId: app.id,
|
|
5932
|
+
route: headRoute,
|
|
5933
|
+
tag: 'meta',
|
|
5934
|
+
attributes: { property },
|
|
5935
|
+
label: property,
|
|
5936
|
+
});
|
|
5937
|
+
}
|
|
5938
|
+
for (const name of ['twitter:card', 'twitter:title', 'twitter:description']) {
|
|
5939
|
+
assertHeadTag(evidence, headResponse.body, {
|
|
5940
|
+
appId: app.id,
|
|
5941
|
+
route: headRoute,
|
|
5942
|
+
tag: 'meta',
|
|
5943
|
+
attributes: { name },
|
|
5944
|
+
label: name,
|
|
5945
|
+
});
|
|
5946
|
+
}
|
|
5947
|
+
assertHeadTag(evidence, headResponse.body, {
|
|
5948
|
+
appId: app.id,
|
|
5949
|
+
route: headRoute,
|
|
5950
|
+
tag: 'script',
|
|
5951
|
+
attributes: { type: 'application/ld+json' },
|
|
5952
|
+
label: 'JSON-LD structured data',
|
|
5953
|
+
});
|
|
5954
|
+
}
|
|
5955
|
+
|
|
5956
|
+
async function validateNotFound(evidence, app, publicUrl) {
|
|
5957
|
+
const qualityGates = app.deploy?.cloudflare?.qualityGates ?? {};
|
|
5958
|
+
const notFoundRoute =
|
|
5959
|
+
qualityGates.statusCodes?.notFoundRoute ?? '${qualityGates.statusCodes.notFoundRoute}';
|
|
5960
|
+
const expectedStatus = qualityGates.statusCodes?.unknownRouteStatus ?? ${qualityGates.statusCodes.unknownRouteStatus};
|
|
5961
|
+
const response = await fetchText(joinUrl(publicUrl, notFoundRoute));
|
|
5962
|
+
evidence.assertions.push({
|
|
5963
|
+
type: 'status-code',
|
|
5964
|
+
route: notFoundRoute,
|
|
5965
|
+
expectedStatus,
|
|
5966
|
+
actualStatus: response.status,
|
|
5967
|
+
status: response.status === expectedStatus ? 'pass' : 'fail',
|
|
5968
|
+
});
|
|
5969
|
+
assert(
|
|
5970
|
+
response.status === expectedStatus,
|
|
5971
|
+
\`\${app.id} unknown route must return HTTP \${expectedStatus}, got \${response.status}\`,
|
|
5972
|
+
);
|
|
5973
|
+
assertCloudflareSecurity(evidence, app, response, notFoundRoute, publicUrl, {
|
|
5974
|
+
html: response.contentType?.includes('text/html'),
|
|
5975
|
+
});
|
|
5976
|
+
}
|
|
5977
|
+
|
|
5978
|
+
async function validateCssAsset(evidence, app, publicUrl, ssr) {
|
|
5979
|
+
const qualityGates = app.deploy?.cloudflare?.qualityGates ?? {};
|
|
5980
|
+
const budgets = qualityGates.budgets ?? {};
|
|
5981
|
+
const styleUrls = extractPreloadStyleUrls(ssr.link, publicUrl);
|
|
5982
|
+
evidence.assertions.push({
|
|
5983
|
+
type: 'css-preload-assets',
|
|
5984
|
+
actual: styleUrls,
|
|
5985
|
+
status: styleUrls.length > 0 ? 'pass' : 'fail',
|
|
5986
|
+
});
|
|
5987
|
+
assert(styleUrls.length > 0, \`\${app.id} SSR response did not expose preloadable CSS assets\`);
|
|
5988
|
+
|
|
5989
|
+
const styleUrl = styleUrls[0];
|
|
5990
|
+
const route = new URL(styleUrl).pathname;
|
|
5991
|
+
const css = await fetchText(styleUrl);
|
|
5992
|
+
evidence.assertions.push({
|
|
5993
|
+
type: 'css-asset',
|
|
5994
|
+
route,
|
|
5995
|
+
status: css.ok && css.body.trim() !== '' ? 'pass' : 'fail',
|
|
5996
|
+
statusCode: css.status,
|
|
5997
|
+
});
|
|
5998
|
+
assert(css.ok, \`\${app.id} CSS asset returned HTTP \${css.status}\`);
|
|
5999
|
+
assert(css.body.trim() !== '', \`\${app.id} CSS asset is empty\`);
|
|
6000
|
+
assertContentType(evidence, app, css, {
|
|
6001
|
+
route,
|
|
6002
|
+
includes: 'text/css',
|
|
6003
|
+
});
|
|
6004
|
+
assertCacheControl(evidence, app, css, {
|
|
6005
|
+
route,
|
|
6006
|
+
required: qualityGates.assets?.cacheControlRequiredForCss,
|
|
6007
|
+
});
|
|
6008
|
+
assertByteBudget(evidence, app, css, {
|
|
6009
|
+
label: 'cssAssetMaxBytes',
|
|
6010
|
+
maxBytes: budgets.cssAssetMaxBytes ?? ${createPublicWebsiteBudgetFallback('cssAssetMaxBytes')},
|
|
6011
|
+
route,
|
|
6012
|
+
});
|
|
6013
|
+
}
|
|
6014
|
+
|
|
6015
|
+
async function validatePublicSurface(evidence, app, publicUrl) {
|
|
6016
|
+
const publicSurface = app.routes?.publicSurface ?? {};
|
|
6017
|
+
const qualityGates = app.deploy?.cloudflare?.qualityGates ?? {};
|
|
6018
|
+
const budgets = qualityGates.budgets ?? {};
|
|
6019
|
+
const hasPublicRoutes =
|
|
6020
|
+
(publicSurface.publicRoutes ?? []).length > 0 ||
|
|
6021
|
+
(publicSurface.routeEntries ?? []).length > 0 ||
|
|
6022
|
+
(publicSurface.contentSources ?? []).length > 0;
|
|
6023
|
+
|
|
6024
|
+
const robotsRoute = '/robots.txt';
|
|
6025
|
+
const robots = await fetchText(joinUrl(publicUrl, robotsRoute));
|
|
6026
|
+
evidence.assertions.push({
|
|
6027
|
+
type: 'public-surface-robots',
|
|
6028
|
+
route: robotsRoute,
|
|
6029
|
+
status: robots.ok ? 'pass' : 'fail',
|
|
6030
|
+
statusCode: robots.status,
|
|
6031
|
+
});
|
|
6032
|
+
assert(robots.ok, \`\${app.id} robots.txt returned HTTP \${robots.status}\`);
|
|
6033
|
+
assertContentType(evidence, app, robots, {
|
|
6034
|
+
route: robotsRoute,
|
|
6035
|
+
includes: 'text/plain',
|
|
6036
|
+
});
|
|
6037
|
+
assertCloudflareSecurity(evidence, app, robots, robotsRoute, publicUrl);
|
|
6038
|
+
|
|
6039
|
+
if (!hasPublicRoutes) {
|
|
6040
|
+
const disallowsAll = robots.body.includes('Disallow: /');
|
|
6041
|
+
const referencesSitemap = /\\bSitemap:/iu.test(robots.body);
|
|
6042
|
+
evidence.assertions.push({
|
|
6043
|
+
type: 'public-surface-private-robots',
|
|
6044
|
+
route: robotsRoute,
|
|
6045
|
+
disallowsAll,
|
|
6046
|
+
referencesSitemap,
|
|
6047
|
+
status: disallowsAll && !referencesSitemap ? 'pass' : 'fail',
|
|
6048
|
+
});
|
|
6049
|
+
assert(disallowsAll, \`\${app.id} private public surface robots.txt must disallow crawling\`);
|
|
6050
|
+
assert(!referencesSitemap, \`\${app.id} private public surface robots.txt must not reference sitemap.xml\`);
|
|
6051
|
+
return;
|
|
6052
|
+
}
|
|
6053
|
+
|
|
6054
|
+
const sitemapRoute = '/sitemap.xml';
|
|
6055
|
+
const sitemap = await fetchText(joinUrl(publicUrl, sitemapRoute));
|
|
6056
|
+
evidence.assertions.push({
|
|
6057
|
+
type: 'public-surface-sitemap',
|
|
6058
|
+
route: sitemapRoute,
|
|
6059
|
+
status: sitemap.ok ? 'pass' : 'fail',
|
|
6060
|
+
statusCode: sitemap.status,
|
|
6061
|
+
});
|
|
6062
|
+
assert(sitemap.ok, \`\${app.id} sitemap.xml returned HTTP \${sitemap.status}\`);
|
|
6063
|
+
assertContentType(evidence, app, sitemap, {
|
|
6064
|
+
route: sitemapRoute,
|
|
6065
|
+
includes: 'xml',
|
|
6066
|
+
});
|
|
6067
|
+
assertByteBudget(evidence, app, sitemap, {
|
|
6068
|
+
label: 'sitemapXmlMaxBytes',
|
|
6069
|
+
maxBytes: budgets.sitemapXmlMaxBytes ?? ${createPublicWebsiteBudgetFallback('sitemapXmlMaxBytes')},
|
|
6070
|
+
route: sitemapRoute,
|
|
6071
|
+
});
|
|
6072
|
+
|
|
6073
|
+
const sitemapUrl = String(joinUrl(publicUrl, sitemapRoute));
|
|
6074
|
+
const robotsReferencesSitemap = robots.body.includes(\`Sitemap: \${sitemapUrl}\`);
|
|
6075
|
+
evidence.assertions.push({
|
|
6076
|
+
type: 'robots-sitemap-consistency',
|
|
6077
|
+
route: robotsRoute,
|
|
6078
|
+
sitemapUrl,
|
|
6079
|
+
status: robotsReferencesSitemap ? 'pass' : 'fail',
|
|
6080
|
+
});
|
|
6081
|
+
assert(
|
|
6082
|
+
robotsReferencesSitemap,
|
|
6083
|
+
\`\${app.id} robots.txt must reference generated sitemap.xml\`,
|
|
6084
|
+
);
|
|
6085
|
+
|
|
6086
|
+
for (const urlPath of publicSurface.concreteUrlPaths ?? []) {
|
|
6087
|
+
const loc = \`<loc>\${String(joinUrl(publicUrl, urlPath))}</loc>\`;
|
|
6088
|
+
evidence.assertions.push({
|
|
6089
|
+
type: 'sitemap-route',
|
|
6090
|
+
route: urlPath,
|
|
6091
|
+
status: sitemap.body.includes(loc) ? 'pass' : 'fail',
|
|
6092
|
+
});
|
|
6093
|
+
assert(sitemap.body.includes(loc), \`\${app.id} sitemap.xml is missing \${urlPath}\`);
|
|
6094
|
+
}
|
|
6095
|
+
|
|
6096
|
+
const manifestRoute = '/site.webmanifest';
|
|
6097
|
+
const webManifest = await fetchText(joinUrl(publicUrl, manifestRoute));
|
|
6098
|
+
const webManifestJson = parseMaybeJson(webManifest.body);
|
|
6099
|
+
evidence.assertions.push({
|
|
6100
|
+
type: 'public-surface-webmanifest',
|
|
6101
|
+
route: manifestRoute,
|
|
6102
|
+
status: webManifest.ok && webManifestJson ? 'pass' : 'fail',
|
|
6103
|
+
statusCode: webManifest.status,
|
|
6104
|
+
});
|
|
6105
|
+
assert(webManifest.ok, \`\${app.id} site.webmanifest returned HTTP \${webManifest.status}\`);
|
|
6106
|
+
assert(webManifestJson, \`\${app.id} site.webmanifest must be valid JSON\`);
|
|
6107
|
+
assertContentType(evidence, app, webManifest, {
|
|
6108
|
+
route: manifestRoute,
|
|
6109
|
+
includes: 'manifest',
|
|
6110
|
+
});
|
|
6111
|
+
}
|
|
6112
|
+
|
|
5044
6113
|
async function validateApp(app, publicUrl) {
|
|
5045
6114
|
const cloudflare = app.deploy?.cloudflare;
|
|
5046
6115
|
const routes = cloudflare?.routes ?? {};
|
|
@@ -5054,6 +6123,8 @@ async function validateApp(app, publicUrl) {
|
|
|
5054
6123
|
|
|
5055
6124
|
const ssrRoute = routes.ssr ?? '/en';
|
|
5056
6125
|
const ssr = await fetchText(joinUrl(publicUrl, ssrRoute));
|
|
6126
|
+
const qualityGates = cloudflare?.qualityGates ?? {};
|
|
6127
|
+
const budgets = qualityGates.budgets ?? {};
|
|
5057
6128
|
evidence.assertions.push({
|
|
5058
6129
|
type: 'ssr',
|
|
5059
6130
|
route: ssrRoute,
|
|
@@ -5064,6 +6135,18 @@ async function validateApp(app, publicUrl) {
|
|
|
5064
6135
|
assertCloudflareSecurity(evidence, app, ssr, ssrRoute, publicUrl, {
|
|
5065
6136
|
html: true,
|
|
5066
6137
|
});
|
|
6138
|
+
assertContentType(evidence, app, ssr, {
|
|
6139
|
+
route: ssrRoute,
|
|
6140
|
+
includes: 'text/html',
|
|
6141
|
+
});
|
|
6142
|
+
assertByteBudget(evidence, app, ssr, {
|
|
6143
|
+
label: 'ssrHtmlMaxBytes',
|
|
6144
|
+
maxBytes: budgets.ssrHtmlMaxBytes ?? ${createPublicWebsiteBudgetFallback('ssrHtmlMaxBytes')},
|
|
6145
|
+
route: ssrRoute,
|
|
6146
|
+
});
|
|
6147
|
+
await validateSsrHead(evidence, app, publicUrl, ssrRoute, ssr);
|
|
6148
|
+
await validateNotFound(evidence, app, publicUrl);
|
|
6149
|
+
await validatePublicSurface(evidence, app, publicUrl);
|
|
5067
6150
|
|
|
5068
6151
|
const uiMarker = extractUiMarker(ssr.body);
|
|
5069
6152
|
evidence.assertions.push({
|
|
@@ -5103,6 +6186,7 @@ async function validateApp(app, publicUrl) {
|
|
|
5103
6186
|
cssPreloadLinkHeader.includes('as=style'),
|
|
5104
6187
|
\`\${app.id} SSR response is missing CSS preload Link headers\`,
|
|
5105
6188
|
);
|
|
6189
|
+
await validateCssAsset(evidence, app, publicUrl, ssr);
|
|
5106
6190
|
|
|
5107
6191
|
const manifestRoute = routes.mfManifest ?? '/mf-manifest.json';
|
|
5108
6192
|
const manifest = await fetchText(joinUrl(publicUrl, manifestRoute));
|
|
@@ -5118,6 +6202,16 @@ async function validateApp(app, publicUrl) {
|
|
|
5118
6202
|
\`\${app.id} MF manifest returned HTTP \${manifest.status}\`,
|
|
5119
6203
|
);
|
|
5120
6204
|
assertCloudflareSecurity(evidence, app, manifest, manifestRoute, publicUrl);
|
|
6205
|
+
assertContentType(evidence, app, manifest, {
|
|
6206
|
+
route: manifestRoute,
|
|
6207
|
+
includes: 'json',
|
|
6208
|
+
});
|
|
6209
|
+
assertByteBudget(evidence, app, manifest, {
|
|
6210
|
+
label: 'mfManifestMaxBytes',
|
|
6211
|
+
maxBytes: budgets.mfManifestMaxBytes ?? ${createPublicWebsiteBudgetFallback('mfManifestMaxBytes')},
|
|
6212
|
+
route: manifestRoute,
|
|
6213
|
+
});
|
|
6214
|
+
assertNoPublicSourcemapRefs(evidence, app, manifestJson);
|
|
5121
6215
|
evidence.assertions.push({
|
|
5122
6216
|
type: 'mf-manifest-cors',
|
|
5123
6217
|
route: manifestRoute,
|
|
@@ -5158,6 +6252,15 @@ async function validateApp(app, publicUrl) {
|
|
|
5158
6252
|
});
|
|
5159
6253
|
assert(locale.ok, \`\${app.id} locale JSON returned HTTP \${locale.status}\`);
|
|
5160
6254
|
assertCloudflareSecurity(evidence, app, locale, localeRoute, publicUrl);
|
|
6255
|
+
assertContentType(evidence, app, locale, {
|
|
6256
|
+
route: localeRoute,
|
|
6257
|
+
includes: 'json',
|
|
6258
|
+
});
|
|
6259
|
+
assertByteBudget(evidence, app, locale, {
|
|
6260
|
+
label: 'localeJsonMaxBytes',
|
|
6261
|
+
maxBytes: budgets.localeJsonMaxBytes ?? ${createPublicWebsiteBudgetFallback('localeJsonMaxBytes')},
|
|
6262
|
+
route: localeRoute,
|
|
6263
|
+
});
|
|
5161
6264
|
evidence.assertions.push({
|
|
5162
6265
|
type: 'i18n-cors',
|
|
5163
6266
|
route: localeRoute,
|
|
@@ -5195,6 +6298,75 @@ async function validateApp(app, publicUrl) {
|
|
|
5195
6298
|
return evidence;
|
|
5196
6299
|
}
|
|
5197
6300
|
|
|
6301
|
+
export { validateApp };
|
|
6302
|
+
`;
|
|
6303
|
+
}
|
|
6304
|
+
function createCloudflareVersionProofScript() {
|
|
6305
|
+
return `#!/usr/bin/env node
|
|
6306
|
+
import fs from 'node:fs';
|
|
6307
|
+
import path from 'node:path';
|
|
6308
|
+
import { fileURLToPath } from 'node:url';
|
|
6309
|
+
import { validateApp } from './ultramodern-cloudflare-proof.mjs';
|
|
6310
|
+
|
|
6311
|
+
const workspaceRoot = path.resolve(
|
|
6312
|
+
path.dirname(fileURLToPath(import.meta.url)),
|
|
6313
|
+
'..',
|
|
6314
|
+
);
|
|
6315
|
+
const contractPath = path.join(
|
|
6316
|
+
workspaceRoot,
|
|
6317
|
+
'.modernjs/ultramodern-generated-contract.json',
|
|
6318
|
+
);
|
|
6319
|
+
const defaultOut = path.join(
|
|
6320
|
+
workspaceRoot,
|
|
6321
|
+
'.codex/reports/cloudflare-version-proof/public-url-proof.json',
|
|
6322
|
+
);
|
|
6323
|
+
|
|
6324
|
+
function readJson(filePath) {
|
|
6325
|
+
return JSON.parse(fs.readFileSync(filePath, 'utf8'));
|
|
6326
|
+
}
|
|
6327
|
+
|
|
6328
|
+
function parseArgs(argv) {
|
|
6329
|
+
const parsed = {
|
|
6330
|
+
appId: undefined,
|
|
6331
|
+
out: defaultOut,
|
|
6332
|
+
requirePublicUrls: false,
|
|
6333
|
+
};
|
|
6334
|
+
|
|
6335
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
6336
|
+
const arg = argv[index];
|
|
6337
|
+
if (arg === '--app') {
|
|
6338
|
+
parsed.appId = argv[index + 1];
|
|
6339
|
+
index += 1;
|
|
6340
|
+
} else if (arg === '--out') {
|
|
6341
|
+
parsed.out = argv[index + 1];
|
|
6342
|
+
index += 1;
|
|
6343
|
+
} else if (arg === '--require-public-urls') {
|
|
6344
|
+
parsed.requirePublicUrls = true;
|
|
6345
|
+
} else if (arg === '--help' || arg === '-h') {
|
|
6346
|
+
parsed.help = true;
|
|
6347
|
+
} else {
|
|
6348
|
+
throw new Error(\`Unknown argument: \${arg}\`);
|
|
6349
|
+
}
|
|
6350
|
+
}
|
|
6351
|
+
|
|
6352
|
+
return parsed;
|
|
6353
|
+
}
|
|
6354
|
+
|
|
6355
|
+
function printHelp() {
|
|
6356
|
+
process.stdout.write(\`Usage:
|
|
6357
|
+
node scripts/proof-cloudflare-version.mjs [--app workspace] [--out evidence.json] [--require-public-urls]
|
|
6358
|
+
|
|
6359
|
+
Set each app's public URL using the contract env key, for example:
|
|
6360
|
+
ULTRAMODERN_PUBLIC_URL_WORKSPACE=https://workspace.example.workers.dev
|
|
6361
|
+
\`);
|
|
6362
|
+
}
|
|
6363
|
+
|
|
6364
|
+
function assert(condition, message) {
|
|
6365
|
+
if (!condition) {
|
|
6366
|
+
throw new Error(message);
|
|
6367
|
+
}
|
|
6368
|
+
}
|
|
6369
|
+
|
|
5198
6370
|
async function main(argv = process.argv.slice(2)) {
|
|
5199
6371
|
const args = parseArgs(argv);
|
|
5200
6372
|
if (args.help) {
|
|
@@ -5261,10 +6433,13 @@ function writeGeneratedWorkspaceScripts(targetDir, scope, enableTailwind, remote
|
|
|
5261
6433
|
writeFileReplacing(targetDir, "scripts/assert-mf-types.mjs", createAssertMfTypesScript(remotes));
|
|
5262
6434
|
writeFileReplacing(targetDir, "scripts/validate-ultramodern-workspace.mjs", createWorkspaceValidationScript(scope, enableTailwind, remotes));
|
|
5263
6435
|
writeFileReplacing(targetDir, "scripts/check-ultramodern-i18n-boundaries.mjs", createWorkspaceI18nBoundaryValidationScript());
|
|
6436
|
+
writeFileReplacing(targetDir, "scripts/generate-public-surface-assets.mjs", createPublicSurfaceAssetsScript());
|
|
6437
|
+
writeFileReplacing(targetDir, "scripts/ultramodern-cloudflare-proof.mjs", createCloudflareProofHelperScript());
|
|
5264
6438
|
writeFileReplacing(targetDir, "scripts/proof-cloudflare-version.mjs", createCloudflareVersionProofScript());
|
|
5265
6439
|
}
|
|
5266
6440
|
function writeApp(targetDir, scope, app, packageSource, enableTailwind, remotes = []) {
|
|
5267
6441
|
const resolvedApp = 'shell' === app.kind ? createShellHost(remotes) : app;
|
|
6442
|
+
const publicWeb = createPublicWebAppArtifacts(resolvedApp);
|
|
5268
6443
|
const writeAppFile = (relativePath, content)=>{
|
|
5269
6444
|
writeFile(targetDir, `${resolvedApp.directory}/${relativePath}`, content);
|
|
5270
6445
|
};
|
|
@@ -5272,7 +6447,8 @@ function writeApp(targetDir, scope, app, packageSource, enableTailwind, remotes
|
|
|
5272
6447
|
writeJson(targetDir, `${resolvedApp.directory}/tsconfig.json`, createPackageTsConfig(resolvedApp.directory, appHasEffectApi(resolvedApp)));
|
|
5273
6448
|
writeFile(targetDir, `${resolvedApp.directory}/src/modern-app-env.d.ts`, createAppEnvDts(resolvedApp, remotes));
|
|
5274
6449
|
writeFile(targetDir, `${resolvedApp.directory}/src/ultramodern-build.ts`, createUltramodernBuildModule(scope, resolvedApp));
|
|
5275
|
-
writeFile(targetDir,
|
|
6450
|
+
writeFile(targetDir, publicWeb.routeMetadataFile.path, publicWeb.routeMetadataFile.content);
|
|
6451
|
+
writeFile(targetDir, publicWeb.routeHeadFile.path, publicWeb.routeHeadFile.content);
|
|
5276
6452
|
writeFile(targetDir, `${resolvedApp.directory}/modern.config.ts`, createAppModernConfig(scope, resolvedApp));
|
|
5277
6453
|
writeFile(targetDir, `${resolvedApp.directory}/src/modern.runtime.ts`, createAppRuntimeConfig(resolvedApp, scope, remotes));
|
|
5278
6454
|
writeJson(targetDir, `${resolvedApp.directory}/locales/en/translation.json`, createAppPublicLocaleMessages(resolvedApp, 'en', remotes));
|
|
@@ -5288,7 +6464,8 @@ function writeApp(targetDir, scope, app, packageSource, enableTailwind, remotes
|
|
|
5288
6464
|
writeAppFile('src/routes/layout.tsx', createLayout(resolvedApp.id));
|
|
5289
6465
|
for (const [relativePath, content] of Object.entries(workspaceAssetsForApp(resolvedApp)))writeFile(targetDir, `${resolvedApp.directory}/${relativePath}`, content);
|
|
5290
6466
|
writeAppFile('src/routes/[lang]/page.tsx', 'shell' === resolvedApp.kind ? createShellPage(remotes) : createRemotePage(resolvedApp));
|
|
5291
|
-
for (const
|
|
6467
|
+
for (const generatedFile of publicWeb.routeMetaFiles)writeFile(targetDir, generatedFile.path, generatedFile.content);
|
|
6468
|
+
for (const generatedFile of publicWeb.routeAliasFiles)writeFile(targetDir, generatedFile.path, generatedFile.content);
|
|
5292
6469
|
if ('shell' === resolvedApp.kind) {
|
|
5293
6470
|
writeAppFile('src/routes/vertical-components.tsx', createShellRemoteComponents(scope, remotes));
|
|
5294
6471
|
writeAppFile('src/routes/shell-frame.tsx', createShellFrameComponent());
|
|
@@ -5319,12 +6496,7 @@ function writeSharedPackages(targetDir, scope, packageSource) {
|
|
|
5319
6496
|
]
|
|
5320
6497
|
});
|
|
5321
6498
|
}
|
|
5322
|
-
writeFile(targetDir, 'packages/shared-contracts/src/index.ts',
|
|
5323
|
-
ownership: 'topology/ownership.json',
|
|
5324
|
-
preset: 'presetUltramodern',
|
|
5325
|
-
topology: 'topology/reference-topology.json',
|
|
5326
|
-
} as const;
|
|
5327
|
-
`);
|
|
6499
|
+
writeFile(targetDir, 'packages/shared-contracts/src/index.ts', createSharedContractsIndex());
|
|
5328
6500
|
writeFile(targetDir, 'packages/shared-design-tokens/src/index.ts', `export const sharedDesignTokens = {
|
|
5329
6501
|
color: {
|
|
5330
6502
|
accent: '#2f8f68',
|
|
@@ -5400,9 +6572,12 @@ function updateRootWorkspaceScripts(workspaceRoot, scope, packageSource, remotes
|
|
|
5400
6572
|
}
|
|
5401
6573
|
function rewriteShellAppFiles(workspaceRoot, scope, packageSource, enableTailwind, remotes) {
|
|
5402
6574
|
const shellHost = createShellHost(remotes);
|
|
6575
|
+
const publicWeb = createPublicWebAppArtifacts(shellHost);
|
|
5403
6576
|
writeJsonFile(external_node_path_default().join(workspaceRoot, `${shellApp.directory}/package.json`), createAppPackage(scope, shellHost, packageSource, enableTailwind, remotes));
|
|
5404
6577
|
writeFileReplacing(workspaceRoot, `${shellApp.directory}/src/modern-app-env.d.ts`, createAppEnvDts(shellHost, remotes));
|
|
5405
|
-
writeFileReplacing(workspaceRoot,
|
|
6578
|
+
writeFileReplacing(workspaceRoot, publicWeb.routeMetadataFile.path, publicWeb.routeMetadataFile.content);
|
|
6579
|
+
writeFileReplacing(workspaceRoot, publicWeb.routeHeadFile.path, publicWeb.routeHeadFile.content);
|
|
6580
|
+
for (const generatedFile of publicWeb.routeMetaFiles)writeFileReplacing(workspaceRoot, generatedFile.path, generatedFile.content);
|
|
5406
6581
|
rewriteWorkspaceAssetsForApp(workspaceRoot, shellHost);
|
|
5407
6582
|
writeFileReplacing(workspaceRoot, `${shellApp.directory}/src/modern.runtime.ts`, createAppRuntimeConfig(shellHost, scope, remotes));
|
|
5408
6583
|
writeJsonFile(external_node_path_default().join(workspaceRoot, `${shellApp.directory}/locales/en/translation.json`), createAppPublicLocaleMessages(shellHost, 'en', remotes));
|