@bleedingdev/modern-js-create 3.2.0-ultramodern.101 → 3.2.0-ultramodern.103
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/dist/index.js +317 -3
- package/dist/types/locale/index.d.ts +117 -2
- package/package.json +3 -3
- package/template/README.md +6 -0
- package/template/config/favicon.svg +5 -0
- package/template/config/public/assets/ultramodern-logo.svg +6 -0
- package/template/config/public/locales/cs/translation.json +5 -0
- package/template/config/public/locales/en/translation.json +5 -0
- package/template/modern.config.ts.handlebars +6 -0
- package/template/scripts/validate-ultramodern.mjs.handlebars +93 -0
- package/template/src/routes/[lang]/page.tsx.handlebars +39 -41
- package/template/src/routes/index.css.handlebars +192 -55
- package/template/tests/ultramodern.contract.test.ts.handlebars +57 -0
- package/template/tsconfig.json +1 -1
package/dist/index.js
CHANGED
|
@@ -566,6 +566,7 @@ const MODULE_FEDERATION_VERSION = '2.5.0';
|
|
|
566
566
|
const ZEPHYR_RSPACK_PLUGIN_VERSION = '1.1.1';
|
|
567
567
|
const ZEPHYR_AGENT_VERSION = '1.1.1';
|
|
568
568
|
const WRANGLER_VERSION = '4.95.0';
|
|
569
|
+
const CLOUDFLARE_COMPATIBILITY_DATE = '2026-06-02';
|
|
569
570
|
const TAILWIND_VERSION = '4.3.0';
|
|
570
571
|
const TAILWIND_POSTCSS_VERSION = '4.3.0';
|
|
571
572
|
const EFFECT_TSGO_VERSION = '0.13.0';
|
|
@@ -900,6 +901,12 @@ function toCamelCase(value) {
|
|
|
900
901
|
function toEnvSegment(value) {
|
|
901
902
|
return toKebabCase(value).replace(/-/g, '_').toUpperCase();
|
|
902
903
|
}
|
|
904
|
+
function createRspackUniqueName(app) {
|
|
905
|
+
return app.mfName;
|
|
906
|
+
}
|
|
907
|
+
function createRspackChunkLoadingGlobal(app) {
|
|
908
|
+
return `__ULTRAMODERN_${toEnvSegment(app.mfName)}_LOADED_CHUNKS__`;
|
|
909
|
+
}
|
|
903
910
|
function ultramodern_workspace_packageName(scope, suffix) {
|
|
904
911
|
return `@${scope}/${suffix}`;
|
|
905
912
|
}
|
|
@@ -1095,17 +1102,105 @@ function createCloudflareProofRoute(app) {
|
|
|
1095
1102
|
} : {}
|
|
1096
1103
|
};
|
|
1097
1104
|
}
|
|
1105
|
+
function createCloudflareSecurityContract() {
|
|
1106
|
+
return {
|
|
1107
|
+
enabled: true,
|
|
1108
|
+
headers: {
|
|
1109
|
+
referrerPolicy: 'strict-origin-when-cross-origin',
|
|
1110
|
+
contentTypeOptions: 'nosniff',
|
|
1111
|
+
permissionsPolicy: 'camera=(), geolocation=(), microphone=(), payment=(), usb=()'
|
|
1112
|
+
},
|
|
1113
|
+
contentSecurityPolicy: {
|
|
1114
|
+
mode: 'report-only',
|
|
1115
|
+
directives: {
|
|
1116
|
+
'base-uri': [
|
|
1117
|
+
"'self'"
|
|
1118
|
+
],
|
|
1119
|
+
'connect-src': [
|
|
1120
|
+
"'self'",
|
|
1121
|
+
'https:',
|
|
1122
|
+
'http:',
|
|
1123
|
+
'wss:',
|
|
1124
|
+
'ws:'
|
|
1125
|
+
],
|
|
1126
|
+
'default-src': [
|
|
1127
|
+
"'self'"
|
|
1128
|
+
],
|
|
1129
|
+
'font-src': [
|
|
1130
|
+
"'self'",
|
|
1131
|
+
'data:',
|
|
1132
|
+
'https:',
|
|
1133
|
+
'http:'
|
|
1134
|
+
],
|
|
1135
|
+
'form-action': [
|
|
1136
|
+
"'self'"
|
|
1137
|
+
],
|
|
1138
|
+
'frame-ancestors': [
|
|
1139
|
+
"'self'"
|
|
1140
|
+
],
|
|
1141
|
+
'img-src': [
|
|
1142
|
+
"'self'",
|
|
1143
|
+
'data:',
|
|
1144
|
+
'blob:',
|
|
1145
|
+
'https:',
|
|
1146
|
+
'http:'
|
|
1147
|
+
],
|
|
1148
|
+
'manifest-src': [
|
|
1149
|
+
"'self'",
|
|
1150
|
+
'https:',
|
|
1151
|
+
'http:'
|
|
1152
|
+
],
|
|
1153
|
+
'object-src': [
|
|
1154
|
+
"'none'"
|
|
1155
|
+
],
|
|
1156
|
+
"script-src": [
|
|
1157
|
+
"'self'",
|
|
1158
|
+
"'unsafe-inline'",
|
|
1159
|
+
"'unsafe-eval'",
|
|
1160
|
+
'https:',
|
|
1161
|
+
'http:',
|
|
1162
|
+
'blob:'
|
|
1163
|
+
],
|
|
1164
|
+
'style-src': [
|
|
1165
|
+
"'self'",
|
|
1166
|
+
"'unsafe-inline'",
|
|
1167
|
+
'https:',
|
|
1168
|
+
'http:'
|
|
1169
|
+
],
|
|
1170
|
+
'worker-src': [
|
|
1171
|
+
"'self'",
|
|
1172
|
+
'blob:'
|
|
1173
|
+
]
|
|
1174
|
+
},
|
|
1175
|
+
reason: "Report-only by default so Cloudflare Module Federation SSR can prove remote script, style, and connect compatibility before enforcement."
|
|
1176
|
+
},
|
|
1177
|
+
noindex: {
|
|
1178
|
+
workersDev: true,
|
|
1179
|
+
localhost: true,
|
|
1180
|
+
previewHostnames: []
|
|
1181
|
+
},
|
|
1182
|
+
cookies: {
|
|
1183
|
+
mutateSetCookie: false,
|
|
1184
|
+
reason: 'Generated Cloudflare worker does not own application Set-Cookie headers.'
|
|
1185
|
+
}
|
|
1186
|
+
};
|
|
1187
|
+
}
|
|
1188
|
+
function formatTsJsonValue(value, indent) {
|
|
1189
|
+
return JSON.stringify(value, null, 2).replaceAll('\n', `\n${' '.repeat(indent)}`);
|
|
1190
|
+
}
|
|
1098
1191
|
function createCloudflareDeployContract(scope, app) {
|
|
1099
1192
|
return {
|
|
1100
1193
|
target: 'cloudflare',
|
|
1101
1194
|
workerName: createCloudflareWorkerName(scope, app),
|
|
1102
1195
|
publicUrlEnv: createCloudflarePublicUrlEnv(app),
|
|
1196
|
+
compatibilityDate: CLOUDFLARE_COMPATIBILITY_DATE,
|
|
1103
1197
|
compatibilityFlags: [
|
|
1104
1198
|
'nodejs_compat',
|
|
1105
1199
|
'global_fetch_strictly_public'
|
|
1106
1200
|
],
|
|
1107
1201
|
assetsBinding: 'ASSETS',
|
|
1108
1202
|
routes: createCloudflareProofRoute(app),
|
|
1203
|
+
security: createCloudflareSecurityContract(),
|
|
1109
1204
|
evidence: {
|
|
1110
1205
|
proofScript: "scripts/proof-cloudflare-version.mjs",
|
|
1111
1206
|
reportDefault: '.codex/reports/cloudflare-version-proof/public-url-proof.json'
|
|
@@ -1379,6 +1474,9 @@ ${bffPluginEntry} moduleFederationPlugin(),
|
|
|
1379
1474
|
overrideBrowserslist: ['defaults'],
|
|
1380
1475
|
},
|
|
1381
1476
|
bundlerChain: chain => {
|
|
1477
|
+
chain.output
|
|
1478
|
+
.uniqueName('${createRspackUniqueName(app)}')
|
|
1479
|
+
.chunkLoadingGlobal('${createRspackChunkLoadingGlobal(app)}');
|
|
1382
1480
|
chain.ignoreWarnings([
|
|
1383
1481
|
{
|
|
1384
1482
|
message: /the request of a dependency is an expression/u,
|
|
@@ -1391,6 +1489,8 @@ ${bffPluginEntry} moduleFederationPlugin(),
|
|
|
1391
1489
|
? {
|
|
1392
1490
|
deploy: {
|
|
1393
1491
|
worker: {
|
|
1492
|
+
compatibilityDate: '${CLOUDFLARE_COMPATIBILITY_DATE}',
|
|
1493
|
+
security: ${formatTsJsonValue(createCloudflareSecurityContract(), 16)},
|
|
1394
1494
|
ssr: true,
|
|
1395
1495
|
},
|
|
1396
1496
|
},
|
|
@@ -1629,12 +1729,18 @@ ${createModuleFederationRemotesConfig(scope, app, remotes)}${createSharedModuleF
|
|
|
1629
1729
|
function appI18nNamespace(app) {
|
|
1630
1730
|
return 'shell' === app.kind ? 'shell' : app.domain ?? app.id;
|
|
1631
1731
|
}
|
|
1732
|
+
const privateAppRoutePublicness = {
|
|
1733
|
+
indexable: false,
|
|
1734
|
+
public: false,
|
|
1735
|
+
publicSurface: 'private-app-screen'
|
|
1736
|
+
};
|
|
1632
1737
|
function createRouteOwnedI18nPaths(app) {
|
|
1633
1738
|
const namespace = appI18nNamespace(app);
|
|
1634
1739
|
const base = {
|
|
1635
1740
|
mfBoundaryId: app.mfName,
|
|
1636
1741
|
namespace,
|
|
1637
|
-
ownerAppId: app.id
|
|
1742
|
+
ownerAppId: app.id,
|
|
1743
|
+
...privateAppRoutePublicness
|
|
1638
1744
|
};
|
|
1639
1745
|
if ('shell' === app.kind) return [
|
|
1640
1746
|
{
|
|
@@ -1807,8 +1913,11 @@ function createRouteOwnedI18nPaths(app) {
|
|
|
1807
1913
|
}
|
|
1808
1914
|
];
|
|
1809
1915
|
}
|
|
1810
|
-
function
|
|
1811
|
-
return
|
|
1916
|
+
function isPublicIndexableRoute(route) {
|
|
1917
|
+
return route.public && route.indexable;
|
|
1918
|
+
}
|
|
1919
|
+
function createLocalisedUrlsMapFromRoutes(routes) {
|
|
1920
|
+
return Object.fromEntries(routes.flatMap((route)=>{
|
|
1812
1921
|
if ('/' === route.canonicalPath) return [];
|
|
1813
1922
|
return Array.from(new Set([
|
|
1814
1923
|
route.canonicalPath,
|
|
@@ -1819,9 +1928,23 @@ function createLocalisedUrlsMap(app) {
|
|
|
1819
1928
|
]);
|
|
1820
1929
|
}));
|
|
1821
1930
|
}
|
|
1931
|
+
function createLocalisedUrlsMap(app) {
|
|
1932
|
+
return createLocalisedUrlsMapFromRoutes(createRouteOwnedI18nPaths(app));
|
|
1933
|
+
}
|
|
1934
|
+
function createPublicRouteMetadata(app) {
|
|
1935
|
+
return createRouteOwnedI18nPaths(app).filter(isPublicIndexableRoute).map((route)=>({
|
|
1936
|
+
canonicalPath: route.canonicalPath,
|
|
1937
|
+
id: route.id,
|
|
1938
|
+
localisedPaths: route.localisedPaths,
|
|
1939
|
+
namespace: route.namespace,
|
|
1940
|
+
ownerAppId: route.ownerAppId,
|
|
1941
|
+
titleKey: route.titleKey
|
|
1942
|
+
}));
|
|
1943
|
+
}
|
|
1822
1944
|
function createRouteMetadataModule(app) {
|
|
1823
1945
|
const routes = sortJsonValue(createRouteOwnedI18nPaths(app));
|
|
1824
1946
|
const localisedUrls = sortJsonValue(createLocalisedUrlsMap(app));
|
|
1947
|
+
const publicRoutes = sortJsonValue(createPublicRouteMetadata(app));
|
|
1825
1948
|
const namespace = appI18nNamespace(app);
|
|
1826
1949
|
return `export const ultramodernRouteNamespace = '${namespace}' as const;
|
|
1827
1950
|
|
|
@@ -1829,9 +1952,12 @@ export const ultramodernRouteMetadata = ${JSON.stringify(routes, null, 2)} as co
|
|
|
1829
1952
|
|
|
1830
1953
|
export const ultramodernLocalisedUrls = ${JSON.stringify(localisedUrls, null, 2)} as const;
|
|
1831
1954
|
|
|
1955
|
+
export const ultramodernPublicRoutes = ${JSON.stringify(publicRoutes, null, 2)} as const;
|
|
1956
|
+
|
|
1832
1957
|
export const ultramodernRouteConfig = {
|
|
1833
1958
|
localisedUrls: ultramodernLocalisedUrls,
|
|
1834
1959
|
namespace: ultramodernRouteNamespace,
|
|
1960
|
+
publicRoutes: ultramodernPublicRoutes,
|
|
1835
1961
|
routes: ultramodernRouteMetadata,
|
|
1836
1962
|
source: 'route-owned',
|
|
1837
1963
|
} as const;
|
|
@@ -4051,6 +4177,12 @@ function createAppConfigContract(app) {
|
|
|
4051
4177
|
disableClientServer: true
|
|
4052
4178
|
}
|
|
4053
4179
|
},
|
|
4180
|
+
rspack: {
|
|
4181
|
+
output: {
|
|
4182
|
+
uniqueName: createRspackUniqueName(app),
|
|
4183
|
+
chunkLoadingGlobal: createRspackChunkLoadingGlobal(app)
|
|
4184
|
+
}
|
|
4185
|
+
},
|
|
4054
4186
|
html: {
|
|
4055
4187
|
outputStructure: 'flat'
|
|
4056
4188
|
},
|
|
@@ -4233,7 +4365,9 @@ function createAppGeneratedContract(scope, app, apps, enableTailwind) {
|
|
|
4233
4365
|
target: 'cloudflare',
|
|
4234
4366
|
cloudflare: createCloudflareDeployContract(scope, app),
|
|
4235
4367
|
worker: {
|
|
4368
|
+
compatibilityDate: CLOUDFLARE_COMPATIBILITY_DATE,
|
|
4236
4369
|
name: createCloudflareWorkerName(scope, app),
|
|
4370
|
+
security: createCloudflareSecurityContract(),
|
|
4237
4371
|
ssr: true
|
|
4238
4372
|
},
|
|
4239
4373
|
output: {
|
|
@@ -4278,6 +4412,9 @@ function createAppGeneratedContract(scope, app, apps, enableTailwind) {
|
|
|
4278
4412
|
metadataExport: './src/routes/ultramodern-route-metadata',
|
|
4279
4413
|
localisedUrls: createLocalisedUrlsMap(app),
|
|
4280
4414
|
owned: createRouteOwnedI18nPaths(app),
|
|
4415
|
+
publicRoutes: createPublicRouteMetadata(app),
|
|
4416
|
+
privateByDefault: true,
|
|
4417
|
+
publicnessDefault: 'private-app-screen',
|
|
4281
4418
|
generatedRouteMap: true,
|
|
4282
4419
|
manualOverrides: []
|
|
4283
4420
|
},
|
|
@@ -4755,6 +4892,7 @@ function createWorkspaceValidationScript(scope, enableTailwind, remotes = []) {
|
|
|
4755
4892
|
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 ultramodern:assert-mf-types' : 'ULTRAMODERN_ZEPHYR=false pnpm --filter "./apps/shell-super-app" run build && pnpm ultramodern:assert-mf-types';
|
|
4756
4893
|
const expectedCloudflareBuildScript = remotes.length > 0 ? 'pnpm -r --filter "./verticals/*" run cloudflare:build && pnpm --filter "./apps/shell-super-app" run cloudflare:build && pnpm ultramodern:assert-mf-types' : 'pnpm --filter "./apps/shell-super-app" run cloudflare:build && pnpm ultramodern:assert-mf-types';
|
|
4757
4894
|
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';
|
|
4895
|
+
const expectedCloudflareSecurity = createCloudflareSecurityContract();
|
|
4758
4896
|
return `import { execFileSync } from 'node:child_process';
|
|
4759
4897
|
import fs from 'node:fs';
|
|
4760
4898
|
import path from 'node:path';
|
|
@@ -4769,6 +4907,7 @@ const oldRemotePaths = ${JSON.stringify(oldRemotePaths, null, 2)};
|
|
|
4769
4907
|
const expectedBuildScript = ${JSON.stringify(expectedBuildScript)};
|
|
4770
4908
|
const expectedCloudflareBuildScript = ${JSON.stringify(expectedCloudflareBuildScript)};
|
|
4771
4909
|
const expectedCloudflareDeployScript = ${JSON.stringify(expectedCloudflareDeployScript)};
|
|
4910
|
+
const expectedCloudflareSecurity = ${JSON.stringify(expectedCloudflareSecurity, null, 2)};
|
|
4772
4911
|
|
|
4773
4912
|
const readText = relativePath => fs.readFileSync(path.join(root, relativePath), 'utf-8');
|
|
4774
4913
|
const readJson = relativePath => JSON.parse(readText(relativePath));
|
|
@@ -4784,6 +4923,12 @@ const assertNotExists = relativePath => {
|
|
|
4784
4923
|
assert(!fs.existsSync(path.join(root, relativePath)), \`Unexpected \${relativePath}\`);
|
|
4785
4924
|
};
|
|
4786
4925
|
const expectedWorkerName = packageSuffix => \`\${packageScope}-\${packageSuffix}\`.slice(0, 63);
|
|
4926
|
+
const expectedChunkLoadingGlobal = mfName =>
|
|
4927
|
+
\`__ULTRAMODERN_\${mfName
|
|
4928
|
+
.replace(/([a-z0-9])([A-Z])/g, '$1_$2')
|
|
4929
|
+
.replace(/[^a-zA-Z0-9]+/g, '_')
|
|
4930
|
+
.replace(/^_+|_+$/g, '')
|
|
4931
|
+
.toUpperCase()}_LOADED_CHUNKS__\`;
|
|
4787
4932
|
const parseSemver = version => {
|
|
4788
4933
|
const match = /^(\\d+)\\.(\\d+)\\.(\\d+)/u.exec(version);
|
|
4789
4934
|
assert(match, \`Unable to parse pnpm version: \${version}\`);
|
|
@@ -4929,6 +5074,7 @@ assert(rootPackage.scripts?.['skills:check'] === 'node ./scripts/bootstrap-agent
|
|
|
4929
5074
|
assert(rootPackage.scripts?.postinstall === "oxfmt . '!repos/**' && node ./scripts/bootstrap-agent-skills.mjs && node ./scripts/setup-agent-reference-repos.mjs", 'Root postinstall must format, bootstrap agent skills, initialize git/hooks, and install reference repositories');
|
|
4930
5075
|
|
|
4931
5076
|
const expectedAppIds = ['shell-super-app', ...fullStackVerticals.map(vertical => vertical.id)];
|
|
5077
|
+
const expectedCloudflareCompatibilityDate = '${CLOUDFLARE_COMPATIBILITY_DATE}';
|
|
4932
5078
|
const expectedCloudflareCompatibilityFlags = ['nodejs_compat', 'global_fetch_strictly_public'];
|
|
4933
5079
|
assert(
|
|
4934
5080
|
JSON.stringify(generatedContract.apps?.map(app => app.id)) === JSON.stringify(expectedAppIds),
|
|
@@ -4959,7 +5105,12 @@ assert(
|
|
|
4959
5105
|
const shellContract = generatedContract.apps?.find(app => app.id === 'shell-super-app');
|
|
4960
5106
|
assert(shellContract?.deploy?.cloudflare?.workerName === expectedWorkerName('shell-super-app'), 'Shell Cloudflare workerName is incorrect');
|
|
4961
5107
|
assert(shellContract?.deploy?.cloudflare?.publicUrlEnv === 'ULTRAMODERN_PUBLIC_URL_SHELL_SUPER_APP', 'Shell Cloudflare public URL env is incorrect');
|
|
5108
|
+
assert(shellContract?.deploy?.cloudflare?.compatibilityDate === expectedCloudflareCompatibilityDate, 'Shell Cloudflare compatibilityDate is incorrect');
|
|
4962
5109
|
assert(JSON.stringify(shellContract?.deploy?.cloudflare?.compatibilityFlags) === JSON.stringify(expectedCloudflareCompatibilityFlags), 'Shell Cloudflare compatibility flags are incorrect');
|
|
5110
|
+
assert(JSON.stringify(shellContract?.deploy?.cloudflare?.security) === JSON.stringify(expectedCloudflareSecurity), 'Shell Cloudflare security contract is incorrect');
|
|
5111
|
+
assert(shellContract?.deploy?.worker?.compatibilityDate === expectedCloudflareCompatibilityDate, 'Shell worker compatibilityDate is incorrect');
|
|
5112
|
+
assert(shellContract?.config?.rspack?.output?.uniqueName === 'shellSuperApp', 'Shell Rspack uniqueName is incorrect');
|
|
5113
|
+
assert(shellContract?.config?.rspack?.output?.chunkLoadingGlobal === expectedChunkLoadingGlobal('shellSuperApp'), 'Shell Rspack chunkLoadingGlobal is incorrect');
|
|
4963
5114
|
assert(topology.shell?.cloudflare?.workerName === expectedWorkerName('shell-super-app'), 'Shell topology Cloudflare workerName is incorrect');
|
|
4964
5115
|
assert(shellContract?.styling?.federation?.owner?.id === 'shell-super-app', 'Shell CSS federation owner is missing');
|
|
4965
5116
|
assert(shellContract?.styling?.federation?.role === 'shell-base-overlay', 'Shell must own base and overlay CSS');
|
|
@@ -4971,6 +5122,13 @@ assert(shellContract?.styling?.federation?.entrypoints?.css?.includes('src/route
|
|
|
4971
5122
|
assert(shellContract?.styling?.federation?.assets?.shared?.some(asset => asset.endsWith('/shared-design-tokens/tokens.css')), 'Shell must import the shared design token CSS asset');
|
|
4972
5123
|
assert(shellContract?.styling?.federation?.dedupe?.duplicateBaseStylesAllowed === false, 'Shell CSS contract must forbid duplicated base styles');
|
|
4973
5124
|
assert(shellContract?.styling?.federation?.ssr?.firstPaintRequired === true, 'Shell CSS must be required for SSR first paint');
|
|
5125
|
+
assert(shellContract?.routes?.privateByDefault === true, 'Shell routes must be private by default');
|
|
5126
|
+
assert(shellContract?.routes?.publicnessDefault === 'private-app-screen', 'Shell route publicness default is incorrect');
|
|
5127
|
+
assert(JSON.stringify(shellContract?.routes?.publicRoutes ?? []) === '[]', 'Shell must not expose generated public routes by default');
|
|
5128
|
+
assert(
|
|
5129
|
+
(shellContract?.routes?.owned ?? []).every(route => route.public === false && route.indexable === false && route.publicSurface === 'private-app-screen'),
|
|
5130
|
+
'Shell owned routes must be non-indexable private app screens by default',
|
|
5131
|
+
);
|
|
4974
5132
|
assert(
|
|
4975
5133
|
topology.shell?.verticalRefs?.join(',') === fullStackVerticals.map(vertical => vertical.id).join(','),
|
|
4976
5134
|
'Topology shell verticalRefs must match generated verticals',
|
|
@@ -5006,8 +5164,13 @@ for (const vertical of fullStackVerticals) {
|
|
|
5006
5164
|
assert(contractEntry?.kind === 'vertical', \`\${vertical.id} generated contract kind is incorrect\`);
|
|
5007
5165
|
assert(contractEntry?.deploy?.cloudflare?.workerName === expectedWorkerName(vertical.id), \`\${vertical.id} Cloudflare workerName is incorrect\`);
|
|
5008
5166
|
assert(contractEntry?.deploy?.cloudflare?.publicUrlEnv === \`ULTRAMODERN_PUBLIC_URL_\${vertical.id.replace(/-/g, '_').toUpperCase()}\`, \`\${vertical.id} Cloudflare public URL env is incorrect\`);
|
|
5167
|
+
assert(contractEntry?.deploy?.cloudflare?.compatibilityDate === expectedCloudflareCompatibilityDate, \`\${vertical.id} Cloudflare compatibilityDate is incorrect\`);
|
|
5009
5168
|
assert(JSON.stringify(contractEntry?.deploy?.cloudflare?.compatibilityFlags) === JSON.stringify(expectedCloudflareCompatibilityFlags), \`\${vertical.id} Cloudflare compatibility flags are incorrect\`);
|
|
5169
|
+
assert(JSON.stringify(contractEntry?.deploy?.cloudflare?.security) === JSON.stringify(expectedCloudflareSecurity), \`\${vertical.id} Cloudflare security contract is incorrect\`);
|
|
5170
|
+
assert(contractEntry?.deploy?.worker?.compatibilityDate === expectedCloudflareCompatibilityDate, \`\${vertical.id} worker compatibilityDate is incorrect\`);
|
|
5010
5171
|
assert(contractEntry?.deploy?.cloudflare?.routes?.effectReadiness === \`\${vertical.apiPrefix}/effect/\${vertical.stem}/readiness\`, \`\${vertical.id} Cloudflare proof readiness route is incorrect\`);
|
|
5172
|
+
assert(contractEntry?.config?.rspack?.output?.uniqueName === vertical.mfName, \`\${vertical.id} Rspack uniqueName is incorrect\`);
|
|
5173
|
+
assert(contractEntry?.config?.rspack?.output?.chunkLoadingGlobal === expectedChunkLoadingGlobal(vertical.mfName), \`\${vertical.id} Rspack chunkLoadingGlobal is incorrect\`);
|
|
5011
5174
|
assert(contractEntry?.moduleFederation?.name === vertical.mfName, \`\${vertical.id} MF name is incorrect\`);
|
|
5012
5175
|
assert(JSON.stringify(contractEntry?.moduleFederation?.exposes) === JSON.stringify(vertical.exposes), \`\${vertical.id} MF exposes are incorrect\`);
|
|
5013
5176
|
assert(contractEntry?.moduleFederation?.dts?.compilerInstance === '--package typescript -- tsc', \`\${vertical.id} must keep mandatory DTS compiler\`);
|
|
@@ -5031,6 +5194,13 @@ for (const vertical of fullStackVerticals) {
|
|
|
5031
5194
|
);
|
|
5032
5195
|
assert(contractEntry?.routes?.source === 'route-owned', \`\${vertical.id} routes must be route-owned\`);
|
|
5033
5196
|
assert(contractEntry?.routes?.metadataExport === './src/routes/ultramodern-route-metadata', \`\${vertical.id} route metadata export is incorrect\`);
|
|
5197
|
+
assert(contractEntry?.routes?.privateByDefault === true, \`\${vertical.id} routes must be private by default\`);
|
|
5198
|
+
assert(contractEntry?.routes?.publicnessDefault === 'private-app-screen', \`\${vertical.id} route publicness default is incorrect\`);
|
|
5199
|
+
assert(JSON.stringify(contractEntry?.routes?.publicRoutes ?? []) === '[]', \`\${vertical.id} must not expose generated public routes by default\`);
|
|
5200
|
+
assert(
|
|
5201
|
+
(contractEntry?.routes?.owned ?? []).every(route => route.public === false && route.indexable === false && route.publicSurface === 'private-app-screen'),
|
|
5202
|
+
\`\${vertical.id} owned routes must be non-indexable private app screens by default\`,
|
|
5203
|
+
);
|
|
5034
5204
|
assert(contractEntry?.styling?.federation?.owner?.id === vertical.id, \`\${vertical.id} CSS federation owner is missing\`);
|
|
5035
5205
|
assert(contractEntry?.styling?.federation?.role === 'vertical-css', \`\${vertical.id} must own only vertical CSS\`);
|
|
5036
5206
|
assert(contractEntry?.styling?.federation?.rootSelector === \`[data-app-id="\${vertical.id}"]\`, \`\${vertical.id} CSS root selector is incorrect\`);
|
|
@@ -5136,8 +5306,14 @@ async function fetchText(url) {
|
|
|
5136
5306
|
ok: response.ok,
|
|
5137
5307
|
status: response.status,
|
|
5138
5308
|
accessControlAllowOrigin: response.headers.get('access-control-allow-origin'),
|
|
5309
|
+
contentSecurityPolicy: response.headers.get('content-security-policy'),
|
|
5310
|
+
contentSecurityPolicyReportOnly: response.headers.get('content-security-policy-report-only'),
|
|
5139
5311
|
contentType: response.headers.get('content-type'),
|
|
5140
5312
|
link: response.headers.get('link'),
|
|
5313
|
+
permissionsPolicy: response.headers.get('permissions-policy'),
|
|
5314
|
+
referrerPolicy: response.headers.get('referrer-policy'),
|
|
5315
|
+
xContentTypeOptions: response.headers.get('x-content-type-options'),
|
|
5316
|
+
xRobotsTag: response.headers.get('x-robots-tag'),
|
|
5141
5317
|
body: await response.text(),
|
|
5142
5318
|
};
|
|
5143
5319
|
}
|
|
@@ -5188,6 +5364,139 @@ function assert(condition, message) {
|
|
|
5188
5364
|
}
|
|
5189
5365
|
}
|
|
5190
5366
|
|
|
5367
|
+
function matchesPreviewHostname(hostname, pattern) {
|
|
5368
|
+
const normalizedHostname = hostname.toLowerCase();
|
|
5369
|
+
const normalizedPattern = String(pattern || '').toLowerCase();
|
|
5370
|
+
|
|
5371
|
+
if (!normalizedPattern) {
|
|
5372
|
+
return false;
|
|
5373
|
+
}
|
|
5374
|
+
|
|
5375
|
+
if (normalizedPattern.startsWith('*.')) {
|
|
5376
|
+
return normalizedHostname.endsWith(normalizedPattern.slice(1));
|
|
5377
|
+
}
|
|
5378
|
+
|
|
5379
|
+
return normalizedHostname === normalizedPattern;
|
|
5380
|
+
}
|
|
5381
|
+
|
|
5382
|
+
function shouldNoindexUrl(publicUrl, noindex) {
|
|
5383
|
+
if (!noindex || noindex === false) {
|
|
5384
|
+
return false;
|
|
5385
|
+
}
|
|
5386
|
+
|
|
5387
|
+
const { hostname } = new URL(publicUrl);
|
|
5388
|
+
const normalizedHostname = hostname.toLowerCase();
|
|
5389
|
+
|
|
5390
|
+
if (
|
|
5391
|
+
noindex.localhost !== false &&
|
|
5392
|
+
(normalizedHostname === 'localhost' ||
|
|
5393
|
+
normalizedHostname === '127.0.0.1' ||
|
|
5394
|
+
normalizedHostname === '[::1]')
|
|
5395
|
+
) {
|
|
5396
|
+
return true;
|
|
5397
|
+
}
|
|
5398
|
+
|
|
5399
|
+
if (
|
|
5400
|
+
noindex.workersDev !== false &&
|
|
5401
|
+
normalizedHostname.endsWith('.workers.dev')
|
|
5402
|
+
) {
|
|
5403
|
+
return true;
|
|
5404
|
+
}
|
|
5405
|
+
|
|
5406
|
+
return (noindex.previewHostnames || []).some(pattern =>
|
|
5407
|
+
matchesPreviewHostname(normalizedHostname, pattern),
|
|
5408
|
+
);
|
|
5409
|
+
}
|
|
5410
|
+
|
|
5411
|
+
function assertHeader(evidence, response, expected, options) {
|
|
5412
|
+
if (expected === false || expected === undefined) {
|
|
5413
|
+
return;
|
|
5414
|
+
}
|
|
5415
|
+
|
|
5416
|
+
const actual = response[options.field];
|
|
5417
|
+
evidence.assertions.push({
|
|
5418
|
+
type: 'security-header',
|
|
5419
|
+
header: options.header,
|
|
5420
|
+
route: options.route,
|
|
5421
|
+
expected,
|
|
5422
|
+
actual,
|
|
5423
|
+
status: actual === expected ? 'pass' : 'fail',
|
|
5424
|
+
});
|
|
5425
|
+
assert(actual === expected, \`\${options.appId} \${options.route} is missing \${options.header}\`);
|
|
5426
|
+
}
|
|
5427
|
+
|
|
5428
|
+
function assertCloudflareSecurity(evidence, app, response, route, publicUrl, options = {}) {
|
|
5429
|
+
const security = app.deploy?.cloudflare?.security;
|
|
5430
|
+
|
|
5431
|
+
if (!security || security.enabled === false) {
|
|
5432
|
+
return;
|
|
5433
|
+
}
|
|
5434
|
+
|
|
5435
|
+
const headers = security.headers || {};
|
|
5436
|
+
assertHeader(evidence, response, headers.referrerPolicy, {
|
|
5437
|
+
appId: app.id,
|
|
5438
|
+
field: 'referrerPolicy',
|
|
5439
|
+
header: 'referrer-policy',
|
|
5440
|
+
route,
|
|
5441
|
+
});
|
|
5442
|
+
assertHeader(evidence, response, headers.contentTypeOptions, {
|
|
5443
|
+
appId: app.id,
|
|
5444
|
+
field: 'xContentTypeOptions',
|
|
5445
|
+
header: 'x-content-type-options',
|
|
5446
|
+
route,
|
|
5447
|
+
});
|
|
5448
|
+
assertHeader(evidence, response, headers.permissionsPolicy, {
|
|
5449
|
+
appId: app.id,
|
|
5450
|
+
field: 'permissionsPolicy',
|
|
5451
|
+
header: 'permissions-policy',
|
|
5452
|
+
route,
|
|
5453
|
+
});
|
|
5454
|
+
|
|
5455
|
+
const csp = security.contentSecurityPolicy;
|
|
5456
|
+
if (options.html && csp?.mode !== 'off') {
|
|
5457
|
+
const header =
|
|
5458
|
+
csp?.mode === 'enforce'
|
|
5459
|
+
? 'content-security-policy'
|
|
5460
|
+
: 'content-security-policy-report-only';
|
|
5461
|
+
const actual =
|
|
5462
|
+
csp?.mode === 'enforce'
|
|
5463
|
+
? response.contentSecurityPolicy
|
|
5464
|
+
: response.contentSecurityPolicyReportOnly;
|
|
5465
|
+
const expectedDirectives = ['script-src', 'style-src', 'connect-src'];
|
|
5466
|
+
const missingDirectives = expectedDirectives.filter(
|
|
5467
|
+
directive => !actual?.includes(directive),
|
|
5468
|
+
);
|
|
5469
|
+
|
|
5470
|
+
evidence.assertions.push({
|
|
5471
|
+
type: 'security-csp',
|
|
5472
|
+
header,
|
|
5473
|
+
route,
|
|
5474
|
+
mode: csp?.mode ?? 'report-only',
|
|
5475
|
+
actual,
|
|
5476
|
+
missingDirectives,
|
|
5477
|
+
status: actual && missingDirectives.length === 0 ? 'pass' : 'fail',
|
|
5478
|
+
});
|
|
5479
|
+
assert(actual, \`\${app.id} \${route} is missing \${header}\`);
|
|
5480
|
+
assert(
|
|
5481
|
+
missingDirectives.length === 0,
|
|
5482
|
+
\`\${app.id} \${route} CSP is missing \${missingDirectives.join(', ')}\`,
|
|
5483
|
+
);
|
|
5484
|
+
}
|
|
5485
|
+
|
|
5486
|
+
if (shouldNoindexUrl(publicUrl, security.noindex)) {
|
|
5487
|
+
evidence.assertions.push({
|
|
5488
|
+
type: 'security-noindex',
|
|
5489
|
+
route,
|
|
5490
|
+
actual: response.xRobotsTag,
|
|
5491
|
+
status: response.xRobotsTag === 'noindex, nofollow' ? 'pass' : 'fail',
|
|
5492
|
+
});
|
|
5493
|
+
assert(
|
|
5494
|
+
response.xRobotsTag === 'noindex, nofollow',
|
|
5495
|
+
\`\${app.id} \${route} is missing noindex X-Robots-Tag\`,
|
|
5496
|
+
);
|
|
5497
|
+
}
|
|
5498
|
+
}
|
|
5499
|
+
|
|
5191
5500
|
async function validateApp(app, publicUrl) {
|
|
5192
5501
|
const cloudflare = app.deploy?.cloudflare;
|
|
5193
5502
|
const routes = cloudflare?.routes ?? {};
|
|
@@ -5208,6 +5517,9 @@ async function validateApp(app, publicUrl) {
|
|
|
5208
5517
|
statusCode: ssr.status,
|
|
5209
5518
|
});
|
|
5210
5519
|
assert(ssr.ok, \`\${app.id} SSR route returned HTTP \${ssr.status}\`);
|
|
5520
|
+
assertCloudflareSecurity(evidence, app, ssr, ssrRoute, publicUrl, {
|
|
5521
|
+
html: true,
|
|
5522
|
+
});
|
|
5211
5523
|
|
|
5212
5524
|
const uiMarker = extractUiMarker(ssr.body);
|
|
5213
5525
|
evidence.assertions.push({
|
|
@@ -5261,6 +5573,7 @@ async function validateApp(app, publicUrl) {
|
|
|
5261
5573
|
manifest.ok,
|
|
5262
5574
|
\`\${app.id} MF manifest returned HTTP \${manifest.status}\`,
|
|
5263
5575
|
);
|
|
5576
|
+
assertCloudflareSecurity(evidence, app, manifest, manifestRoute, publicUrl);
|
|
5264
5577
|
evidence.assertions.push({
|
|
5265
5578
|
type: 'mf-manifest-cors',
|
|
5266
5579
|
route: manifestRoute,
|
|
@@ -5300,6 +5613,7 @@ async function validateApp(app, publicUrl) {
|
|
|
5300
5613
|
statusCode: locale.status,
|
|
5301
5614
|
});
|
|
5302
5615
|
assert(locale.ok, \`\${app.id} locale JSON returned HTTP \${locale.status}\`);
|
|
5616
|
+
assertCloudflareSecurity(evidence, app, locale, localeRoute, publicUrl);
|
|
5303
5617
|
evidence.assertions.push({
|
|
5304
5618
|
type: 'i18n-cors',
|
|
5305
5619
|
route: localeRoute,
|
|
@@ -1,3 +1,118 @@
|
|
|
1
|
-
|
|
2
|
-
declare const
|
|
1
|
+
import { I18n } from '@modern-js/i18n-utils';
|
|
2
|
+
declare const i18n: I18n;
|
|
3
|
+
declare const localeKeys: {
|
|
4
|
+
prompt: {
|
|
5
|
+
projectName: string;
|
|
6
|
+
};
|
|
7
|
+
error: {
|
|
8
|
+
projectNameEmpty: string;
|
|
9
|
+
directoryExists: string;
|
|
10
|
+
invalidRouter: string;
|
|
11
|
+
invalidBffRuntime: string;
|
|
12
|
+
createFailed: string;
|
|
13
|
+
};
|
|
14
|
+
message: {
|
|
15
|
+
welcome: string;
|
|
16
|
+
success: string;
|
|
17
|
+
nextSteps: string;
|
|
18
|
+
step1: string;
|
|
19
|
+
step2: string;
|
|
20
|
+
step3: string;
|
|
21
|
+
};
|
|
22
|
+
help: {
|
|
23
|
+
title: string;
|
|
24
|
+
description: string;
|
|
25
|
+
usage: string;
|
|
26
|
+
usageExample: string;
|
|
27
|
+
options: string;
|
|
28
|
+
optionHelp: string;
|
|
29
|
+
optionVersion: string;
|
|
30
|
+
optionLang: string;
|
|
31
|
+
optionRouter: string;
|
|
32
|
+
optionBff: string;
|
|
33
|
+
optionBffRuntime: string;
|
|
34
|
+
optionTailwind: string;
|
|
35
|
+
optionWorkspace: string;
|
|
36
|
+
optionUltramodernWorkspace: string;
|
|
37
|
+
optionUltramodernPackageSource: string;
|
|
38
|
+
optionUltramodernPackageScope: string;
|
|
39
|
+
optionUltramodernPackageNamePrefix: string;
|
|
40
|
+
optionVertical: string;
|
|
41
|
+
optionSub: string;
|
|
42
|
+
examples: string;
|
|
43
|
+
example1: string;
|
|
44
|
+
example2: string;
|
|
45
|
+
example3: string;
|
|
46
|
+
example4: string;
|
|
47
|
+
example5: string;
|
|
48
|
+
example6: string;
|
|
49
|
+
example7: string;
|
|
50
|
+
example8: string;
|
|
51
|
+
example9: string;
|
|
52
|
+
example10: string;
|
|
53
|
+
example11: string;
|
|
54
|
+
example12: string;
|
|
55
|
+
moreInfo: string;
|
|
56
|
+
};
|
|
57
|
+
version: {
|
|
58
|
+
message: string;
|
|
59
|
+
};
|
|
60
|
+
} | {
|
|
61
|
+
prompt: {
|
|
62
|
+
projectName: string;
|
|
63
|
+
};
|
|
64
|
+
error: {
|
|
65
|
+
projectNameEmpty: string;
|
|
66
|
+
directoryExists: string;
|
|
67
|
+
invalidRouter: string;
|
|
68
|
+
invalidBffRuntime: string;
|
|
69
|
+
createFailed: string;
|
|
70
|
+
};
|
|
71
|
+
message: {
|
|
72
|
+
welcome: string;
|
|
73
|
+
success: string;
|
|
74
|
+
nextSteps: string;
|
|
75
|
+
step1: string;
|
|
76
|
+
step2: string;
|
|
77
|
+
step3: string;
|
|
78
|
+
};
|
|
79
|
+
help: {
|
|
80
|
+
title: string;
|
|
81
|
+
description: string;
|
|
82
|
+
usage: string;
|
|
83
|
+
usageExample: string;
|
|
84
|
+
options: string;
|
|
85
|
+
optionHelp: string;
|
|
86
|
+
optionVersion: string;
|
|
87
|
+
optionLang: string;
|
|
88
|
+
optionRouter: string;
|
|
89
|
+
optionBff: string;
|
|
90
|
+
optionBffRuntime: string;
|
|
91
|
+
optionTailwind: string;
|
|
92
|
+
optionWorkspace: string;
|
|
93
|
+
optionUltramodernWorkspace: string;
|
|
94
|
+
optionUltramodernPackageSource: string;
|
|
95
|
+
optionUltramodernPackageScope: string;
|
|
96
|
+
optionUltramodernPackageNamePrefix: string;
|
|
97
|
+
optionVertical: string;
|
|
98
|
+
optionSub: string;
|
|
99
|
+
examples: string;
|
|
100
|
+
example1: string;
|
|
101
|
+
example2: string;
|
|
102
|
+
example3: string;
|
|
103
|
+
example4: string;
|
|
104
|
+
example5: string;
|
|
105
|
+
example6: string;
|
|
106
|
+
example7: string;
|
|
107
|
+
example8: string;
|
|
108
|
+
example9: string;
|
|
109
|
+
example10: string;
|
|
110
|
+
example11: string;
|
|
111
|
+
example12: string;
|
|
112
|
+
moreInfo: string;
|
|
113
|
+
};
|
|
114
|
+
version: {
|
|
115
|
+
message: string;
|
|
116
|
+
};
|
|
117
|
+
};
|
|
3
118
|
export { i18n, localeKeys };
|
package/package.json
CHANGED
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
"engines": {
|
|
22
22
|
"node": ">=20"
|
|
23
23
|
},
|
|
24
|
-
"version": "3.2.0-ultramodern.
|
|
24
|
+
"version": "3.2.0-ultramodern.103",
|
|
25
25
|
"types": "./dist/types/index.d.ts",
|
|
26
26
|
"main": "./dist/index.js",
|
|
27
27
|
"bin": {
|
|
@@ -41,7 +41,7 @@
|
|
|
41
41
|
"@types/node": "^25.9.1",
|
|
42
42
|
"@typescript/native-preview": "7.0.0-dev.20260527.2",
|
|
43
43
|
"tsx": "^4.22.3",
|
|
44
|
-
"@modern-js/i18n-utils": "npm:@bleedingdev/modern-js-i18n-utils@3.2.0-ultramodern.
|
|
44
|
+
"@modern-js/i18n-utils": "npm:@bleedingdev/modern-js-i18n-utils@3.2.0-ultramodern.103"
|
|
45
45
|
},
|
|
46
46
|
"publishConfig": {
|
|
47
47
|
"registry": "https://registry.npmjs.org/",
|
|
@@ -54,6 +54,6 @@
|
|
|
54
54
|
"start": "node ./dist/index.js"
|
|
55
55
|
},
|
|
56
56
|
"ultramodern": {
|
|
57
|
-
"frameworkVersion": "3.2.0-ultramodern.
|
|
57
|
+
"frameworkVersion": "3.2.0-ultramodern.103"
|
|
58
58
|
}
|
|
59
59
|
}
|
package/template/README.md
CHANGED
|
@@ -49,6 +49,7 @@ The default app is intentionally monolith-friendly:
|
|
|
49
49
|
| --- | --- |
|
|
50
50
|
| App routes | Locale-prefixed pages under `src/routes/[lang]` |
|
|
51
51
|
| Copy | English and Czech resources in `config/public/locales` |
|
|
52
|
+
| Web defaults | Local favicon/logo assets, localized metadata, semantic starter markup |
|
|
52
53
|
| Styling | App-local CSS, with Tailwind files only when selected |
|
|
53
54
|
| Server logic | Optional BFF entrypoints under `api/` |
|
|
54
55
|
| Tests | Rstest smoke coverage in `tests/` |
|
|
@@ -66,6 +67,11 @@ real routes, actions, and API calls. Put user-visible text in
|
|
|
66
67
|
`config/public/locales/<lang>/translation.json`, then render it through
|
|
67
68
|
`react-i18next` or `@modern-js/plugin-i18n/runtime`.
|
|
68
69
|
|
|
70
|
+
The starter keeps favicon and logo assets local in `config/favicon.svg` and
|
|
71
|
+
`config/public/assets/ultramodern-logo.svg`. Replace those files when your app
|
|
72
|
+
has product branding. The localized page title and description live in the same
|
|
73
|
+
translation resources as the visible UI copy.
|
|
74
|
+
|
|
69
75
|
Tune the preset in `modern.config.ts`. Production builds require
|
|
70
76
|
`MODERN_PUBLIC_SITE_URL` so canonical and `hreflang` URLs use your deployed
|
|
71
77
|
origin. The local fallback is `http://localhost:8080`.
|