@bleedingdev/modern-js-create 3.2.0-ultramodern.102 → 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 +292 -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';
|
|
@@ -1101,17 +1102,105 @@ function createCloudflareProofRoute(app) {
|
|
|
1101
1102
|
} : {}
|
|
1102
1103
|
};
|
|
1103
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
|
+
}
|
|
1104
1191
|
function createCloudflareDeployContract(scope, app) {
|
|
1105
1192
|
return {
|
|
1106
1193
|
target: 'cloudflare',
|
|
1107
1194
|
workerName: createCloudflareWorkerName(scope, app),
|
|
1108
1195
|
publicUrlEnv: createCloudflarePublicUrlEnv(app),
|
|
1196
|
+
compatibilityDate: CLOUDFLARE_COMPATIBILITY_DATE,
|
|
1109
1197
|
compatibilityFlags: [
|
|
1110
1198
|
'nodejs_compat',
|
|
1111
1199
|
'global_fetch_strictly_public'
|
|
1112
1200
|
],
|
|
1113
1201
|
assetsBinding: 'ASSETS',
|
|
1114
1202
|
routes: createCloudflareProofRoute(app),
|
|
1203
|
+
security: createCloudflareSecurityContract(),
|
|
1115
1204
|
evidence: {
|
|
1116
1205
|
proofScript: "scripts/proof-cloudflare-version.mjs",
|
|
1117
1206
|
reportDefault: '.codex/reports/cloudflare-version-proof/public-url-proof.json'
|
|
@@ -1400,6 +1489,8 @@ ${bffPluginEntry} moduleFederationPlugin(),
|
|
|
1400
1489
|
? {
|
|
1401
1490
|
deploy: {
|
|
1402
1491
|
worker: {
|
|
1492
|
+
compatibilityDate: '${CLOUDFLARE_COMPATIBILITY_DATE}',
|
|
1493
|
+
security: ${formatTsJsonValue(createCloudflareSecurityContract(), 16)},
|
|
1403
1494
|
ssr: true,
|
|
1404
1495
|
},
|
|
1405
1496
|
},
|
|
@@ -1638,12 +1729,18 @@ ${createModuleFederationRemotesConfig(scope, app, remotes)}${createSharedModuleF
|
|
|
1638
1729
|
function appI18nNamespace(app) {
|
|
1639
1730
|
return 'shell' === app.kind ? 'shell' : app.domain ?? app.id;
|
|
1640
1731
|
}
|
|
1732
|
+
const privateAppRoutePublicness = {
|
|
1733
|
+
indexable: false,
|
|
1734
|
+
public: false,
|
|
1735
|
+
publicSurface: 'private-app-screen'
|
|
1736
|
+
};
|
|
1641
1737
|
function createRouteOwnedI18nPaths(app) {
|
|
1642
1738
|
const namespace = appI18nNamespace(app);
|
|
1643
1739
|
const base = {
|
|
1644
1740
|
mfBoundaryId: app.mfName,
|
|
1645
1741
|
namespace,
|
|
1646
|
-
ownerAppId: app.id
|
|
1742
|
+
ownerAppId: app.id,
|
|
1743
|
+
...privateAppRoutePublicness
|
|
1647
1744
|
};
|
|
1648
1745
|
if ('shell' === app.kind) return [
|
|
1649
1746
|
{
|
|
@@ -1816,8 +1913,11 @@ function createRouteOwnedI18nPaths(app) {
|
|
|
1816
1913
|
}
|
|
1817
1914
|
];
|
|
1818
1915
|
}
|
|
1819
|
-
function
|
|
1820
|
-
return
|
|
1916
|
+
function isPublicIndexableRoute(route) {
|
|
1917
|
+
return route.public && route.indexable;
|
|
1918
|
+
}
|
|
1919
|
+
function createLocalisedUrlsMapFromRoutes(routes) {
|
|
1920
|
+
return Object.fromEntries(routes.flatMap((route)=>{
|
|
1821
1921
|
if ('/' === route.canonicalPath) return [];
|
|
1822
1922
|
return Array.from(new Set([
|
|
1823
1923
|
route.canonicalPath,
|
|
@@ -1828,9 +1928,23 @@ function createLocalisedUrlsMap(app) {
|
|
|
1828
1928
|
]);
|
|
1829
1929
|
}));
|
|
1830
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
|
+
}
|
|
1831
1944
|
function createRouteMetadataModule(app) {
|
|
1832
1945
|
const routes = sortJsonValue(createRouteOwnedI18nPaths(app));
|
|
1833
1946
|
const localisedUrls = sortJsonValue(createLocalisedUrlsMap(app));
|
|
1947
|
+
const publicRoutes = sortJsonValue(createPublicRouteMetadata(app));
|
|
1834
1948
|
const namespace = appI18nNamespace(app);
|
|
1835
1949
|
return `export const ultramodernRouteNamespace = '${namespace}' as const;
|
|
1836
1950
|
|
|
@@ -1838,9 +1952,12 @@ export const ultramodernRouteMetadata = ${JSON.stringify(routes, null, 2)} as co
|
|
|
1838
1952
|
|
|
1839
1953
|
export const ultramodernLocalisedUrls = ${JSON.stringify(localisedUrls, null, 2)} as const;
|
|
1840
1954
|
|
|
1955
|
+
export const ultramodernPublicRoutes = ${JSON.stringify(publicRoutes, null, 2)} as const;
|
|
1956
|
+
|
|
1841
1957
|
export const ultramodernRouteConfig = {
|
|
1842
1958
|
localisedUrls: ultramodernLocalisedUrls,
|
|
1843
1959
|
namespace: ultramodernRouteNamespace,
|
|
1960
|
+
publicRoutes: ultramodernPublicRoutes,
|
|
1844
1961
|
routes: ultramodernRouteMetadata,
|
|
1845
1962
|
source: 'route-owned',
|
|
1846
1963
|
} as const;
|
|
@@ -4248,7 +4365,9 @@ function createAppGeneratedContract(scope, app, apps, enableTailwind) {
|
|
|
4248
4365
|
target: 'cloudflare',
|
|
4249
4366
|
cloudflare: createCloudflareDeployContract(scope, app),
|
|
4250
4367
|
worker: {
|
|
4368
|
+
compatibilityDate: CLOUDFLARE_COMPATIBILITY_DATE,
|
|
4251
4369
|
name: createCloudflareWorkerName(scope, app),
|
|
4370
|
+
security: createCloudflareSecurityContract(),
|
|
4252
4371
|
ssr: true
|
|
4253
4372
|
},
|
|
4254
4373
|
output: {
|
|
@@ -4293,6 +4412,9 @@ function createAppGeneratedContract(scope, app, apps, enableTailwind) {
|
|
|
4293
4412
|
metadataExport: './src/routes/ultramodern-route-metadata',
|
|
4294
4413
|
localisedUrls: createLocalisedUrlsMap(app),
|
|
4295
4414
|
owned: createRouteOwnedI18nPaths(app),
|
|
4415
|
+
publicRoutes: createPublicRouteMetadata(app),
|
|
4416
|
+
privateByDefault: true,
|
|
4417
|
+
publicnessDefault: 'private-app-screen',
|
|
4296
4418
|
generatedRouteMap: true,
|
|
4297
4419
|
manualOverrides: []
|
|
4298
4420
|
},
|
|
@@ -4770,6 +4892,7 @@ function createWorkspaceValidationScript(scope, enableTailwind, remotes = []) {
|
|
|
4770
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';
|
|
4771
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';
|
|
4772
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();
|
|
4773
4896
|
return `import { execFileSync } from 'node:child_process';
|
|
4774
4897
|
import fs from 'node:fs';
|
|
4775
4898
|
import path from 'node:path';
|
|
@@ -4784,6 +4907,7 @@ const oldRemotePaths = ${JSON.stringify(oldRemotePaths, null, 2)};
|
|
|
4784
4907
|
const expectedBuildScript = ${JSON.stringify(expectedBuildScript)};
|
|
4785
4908
|
const expectedCloudflareBuildScript = ${JSON.stringify(expectedCloudflareBuildScript)};
|
|
4786
4909
|
const expectedCloudflareDeployScript = ${JSON.stringify(expectedCloudflareDeployScript)};
|
|
4910
|
+
const expectedCloudflareSecurity = ${JSON.stringify(expectedCloudflareSecurity, null, 2)};
|
|
4787
4911
|
|
|
4788
4912
|
const readText = relativePath => fs.readFileSync(path.join(root, relativePath), 'utf-8');
|
|
4789
4913
|
const readJson = relativePath => JSON.parse(readText(relativePath));
|
|
@@ -4950,6 +5074,7 @@ assert(rootPackage.scripts?.['skills:check'] === 'node ./scripts/bootstrap-agent
|
|
|
4950
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');
|
|
4951
5075
|
|
|
4952
5076
|
const expectedAppIds = ['shell-super-app', ...fullStackVerticals.map(vertical => vertical.id)];
|
|
5077
|
+
const expectedCloudflareCompatibilityDate = '${CLOUDFLARE_COMPATIBILITY_DATE}';
|
|
4953
5078
|
const expectedCloudflareCompatibilityFlags = ['nodejs_compat', 'global_fetch_strictly_public'];
|
|
4954
5079
|
assert(
|
|
4955
5080
|
JSON.stringify(generatedContract.apps?.map(app => app.id)) === JSON.stringify(expectedAppIds),
|
|
@@ -4980,7 +5105,10 @@ assert(
|
|
|
4980
5105
|
const shellContract = generatedContract.apps?.find(app => app.id === 'shell-super-app');
|
|
4981
5106
|
assert(shellContract?.deploy?.cloudflare?.workerName === expectedWorkerName('shell-super-app'), 'Shell Cloudflare workerName is incorrect');
|
|
4982
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');
|
|
4983
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');
|
|
4984
5112
|
assert(shellContract?.config?.rspack?.output?.uniqueName === 'shellSuperApp', 'Shell Rspack uniqueName is incorrect');
|
|
4985
5113
|
assert(shellContract?.config?.rspack?.output?.chunkLoadingGlobal === expectedChunkLoadingGlobal('shellSuperApp'), 'Shell Rspack chunkLoadingGlobal is incorrect');
|
|
4986
5114
|
assert(topology.shell?.cloudflare?.workerName === expectedWorkerName('shell-super-app'), 'Shell topology Cloudflare workerName is incorrect');
|
|
@@ -4994,6 +5122,13 @@ assert(shellContract?.styling?.federation?.entrypoints?.css?.includes('src/route
|
|
|
4994
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');
|
|
4995
5123
|
assert(shellContract?.styling?.federation?.dedupe?.duplicateBaseStylesAllowed === false, 'Shell CSS contract must forbid duplicated base styles');
|
|
4996
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
|
+
);
|
|
4997
5132
|
assert(
|
|
4998
5133
|
topology.shell?.verticalRefs?.join(',') === fullStackVerticals.map(vertical => vertical.id).join(','),
|
|
4999
5134
|
'Topology shell verticalRefs must match generated verticals',
|
|
@@ -5029,7 +5164,10 @@ for (const vertical of fullStackVerticals) {
|
|
|
5029
5164
|
assert(contractEntry?.kind === 'vertical', \`\${vertical.id} generated contract kind is incorrect\`);
|
|
5030
5165
|
assert(contractEntry?.deploy?.cloudflare?.workerName === expectedWorkerName(vertical.id), \`\${vertical.id} Cloudflare workerName is incorrect\`);
|
|
5031
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\`);
|
|
5032
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\`);
|
|
5033
5171
|
assert(contractEntry?.deploy?.cloudflare?.routes?.effectReadiness === \`\${vertical.apiPrefix}/effect/\${vertical.stem}/readiness\`, \`\${vertical.id} Cloudflare proof readiness route is incorrect\`);
|
|
5034
5172
|
assert(contractEntry?.config?.rspack?.output?.uniqueName === vertical.mfName, \`\${vertical.id} Rspack uniqueName is incorrect\`);
|
|
5035
5173
|
assert(contractEntry?.config?.rspack?.output?.chunkLoadingGlobal === expectedChunkLoadingGlobal(vertical.mfName), \`\${vertical.id} Rspack chunkLoadingGlobal is incorrect\`);
|
|
@@ -5056,6 +5194,13 @@ for (const vertical of fullStackVerticals) {
|
|
|
5056
5194
|
);
|
|
5057
5195
|
assert(contractEntry?.routes?.source === 'route-owned', \`\${vertical.id} routes must be route-owned\`);
|
|
5058
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
|
+
);
|
|
5059
5204
|
assert(contractEntry?.styling?.federation?.owner?.id === vertical.id, \`\${vertical.id} CSS federation owner is missing\`);
|
|
5060
5205
|
assert(contractEntry?.styling?.federation?.role === 'vertical-css', \`\${vertical.id} must own only vertical CSS\`);
|
|
5061
5206
|
assert(contractEntry?.styling?.federation?.rootSelector === \`[data-app-id="\${vertical.id}"]\`, \`\${vertical.id} CSS root selector is incorrect\`);
|
|
@@ -5161,8 +5306,14 @@ async function fetchText(url) {
|
|
|
5161
5306
|
ok: response.ok,
|
|
5162
5307
|
status: response.status,
|
|
5163
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'),
|
|
5164
5311
|
contentType: response.headers.get('content-type'),
|
|
5165
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'),
|
|
5166
5317
|
body: await response.text(),
|
|
5167
5318
|
};
|
|
5168
5319
|
}
|
|
@@ -5213,6 +5364,139 @@ function assert(condition, message) {
|
|
|
5213
5364
|
}
|
|
5214
5365
|
}
|
|
5215
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
|
+
|
|
5216
5500
|
async function validateApp(app, publicUrl) {
|
|
5217
5501
|
const cloudflare = app.deploy?.cloudflare;
|
|
5218
5502
|
const routes = cloudflare?.routes ?? {};
|
|
@@ -5233,6 +5517,9 @@ async function validateApp(app, publicUrl) {
|
|
|
5233
5517
|
statusCode: ssr.status,
|
|
5234
5518
|
});
|
|
5235
5519
|
assert(ssr.ok, \`\${app.id} SSR route returned HTTP \${ssr.status}\`);
|
|
5520
|
+
assertCloudflareSecurity(evidence, app, ssr, ssrRoute, publicUrl, {
|
|
5521
|
+
html: true,
|
|
5522
|
+
});
|
|
5236
5523
|
|
|
5237
5524
|
const uiMarker = extractUiMarker(ssr.body);
|
|
5238
5525
|
evidence.assertions.push({
|
|
@@ -5286,6 +5573,7 @@ async function validateApp(app, publicUrl) {
|
|
|
5286
5573
|
manifest.ok,
|
|
5287
5574
|
\`\${app.id} MF manifest returned HTTP \${manifest.status}\`,
|
|
5288
5575
|
);
|
|
5576
|
+
assertCloudflareSecurity(evidence, app, manifest, manifestRoute, publicUrl);
|
|
5289
5577
|
evidence.assertions.push({
|
|
5290
5578
|
type: 'mf-manifest-cors',
|
|
5291
5579
|
route: manifestRoute,
|
|
@@ -5325,6 +5613,7 @@ async function validateApp(app, publicUrl) {
|
|
|
5325
5613
|
statusCode: locale.status,
|
|
5326
5614
|
});
|
|
5327
5615
|
assert(locale.ok, \`\${app.id} locale JSON returned HTTP \${locale.status}\`);
|
|
5616
|
+
assertCloudflareSecurity(evidence, app, locale, localeRoute, publicUrl);
|
|
5328
5617
|
evidence.assertions.push({
|
|
5329
5618
|
type: 'i18n-cors',
|
|
5330
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`.
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" role="img" aria-label="UltraModern.js">
|
|
2
|
+
<rect width="64" height="64" rx="12" fill="#111827"/>
|
|
3
|
+
<path fill="#35d399" d="M14 17h9v21c0 5 3 8 9 8s9-3 9-8V17h9v22c0 11-8 18-18 18s-18-7-18-18V17Z"/>
|
|
4
|
+
<path fill="#ffffff" d="M28 17h8v25h-8z"/>
|
|
5
|
+
</svg>
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 160 160" role="img" aria-label="UltraModern.js logo">
|
|
2
|
+
<rect width="160" height="160" rx="28" fill="#111827"/>
|
|
3
|
+
<path fill="#35d399" d="M42 36h22v56c0 22 14 34 36 34s36-12 36-34V36h22v57c0 35-24 57-58 57S42 128 42 93V36Z"/>
|
|
4
|
+
<path fill="#ffffff" d="M76 36h20v74H76z"/>
|
|
5
|
+
<path fill="#8be8ff" d="M112 36h18v74h-18z"/>
|
|
6
|
+
</svg>
|
|
@@ -33,7 +33,12 @@
|
|
|
33
33
|
"switcher": "Jazyk"
|
|
34
34
|
},
|
|
35
35
|
"logoAlt": "Logo UltraModern.js",
|
|
36
|
+
"meta": {
|
|
37
|
+
"description": "Lokalizovany starter aplikace UltraModern.js se silnymi vychozimi hodnotami pro komplexni produkty.",
|
|
38
|
+
"title": "UltraModern.js Starter"
|
|
39
|
+
},
|
|
36
40
|
"name": "Jednoduchy starter aplikace",
|
|
41
|
+
"skipLink": "Preskocit na obsah",
|
|
37
42
|
"title": "UltraModern.js Starter"
|
|
38
43
|
}
|
|
39
44
|
}
|
|
@@ -33,7 +33,12 @@
|
|
|
33
33
|
"switcher": "Language"
|
|
34
34
|
},
|
|
35
35
|
"logoAlt": "UltraModern.js Logo",
|
|
36
|
+
"meta": {
|
|
37
|
+
"description": "A localized UltraModern.js app starter with strong defaults for complex products.",
|
|
38
|
+
"title": "UltraModern.js Starter"
|
|
39
|
+
},
|
|
36
40
|
"name": "Simple app starter",
|
|
41
|
+
"skipLink": "Skip to content",
|
|
37
42
|
"title": "UltraModern.js Starter"
|
|
38
43
|
}
|
|
39
44
|
}
|
|
@@ -53,6 +53,12 @@ export default defineConfig(
|
|
|
53
53
|
{{/if}}{{#if enableBff}}
|
|
54
54
|
bffPlugin(),
|
|
55
55
|
{{/if}} ],
|
|
56
|
+
html: {
|
|
57
|
+
title: 'UltraModern.js Starter',
|
|
58
|
+
meta: {
|
|
59
|
+
viewport: 'width=device-width, initial-scale=1.0, viewport-fit=cover',
|
|
60
|
+
},
|
|
61
|
+
},
|
|
56
62
|
source: {
|
|
57
63
|
globalVars: {
|
|
58
64
|
ULTRAMODERN_SITE_URL: siteUrl,
|
|
@@ -8,6 +8,9 @@ const packageSourcePath = path.resolve(
|
|
|
8
8
|
process.cwd(),
|
|
9
9
|
'.modernjs/ultramodern-package-source.json',
|
|
10
10
|
);
|
|
11
|
+
const readText = (relativePath) =>
|
|
12
|
+
fs.readFileSync(path.resolve(process.cwd(), relativePath), 'utf-8');
|
|
13
|
+
const readJson = (relativePath) => JSON.parse(readText(relativePath));
|
|
11
14
|
const readPnpmConfig = (key) => {
|
|
12
15
|
const env = Object.fromEntries(
|
|
13
16
|
Object.entries(process.env).filter(
|
|
@@ -109,6 +112,8 @@ const requiredPaths = [
|
|
|
109
112
|
{{/if}}
|
|
110
113
|
'config/public/locales/en/translation.json',
|
|
111
114
|
'config/public/locales/cs/translation.json',
|
|
115
|
+
'config/favicon.svg',
|
|
116
|
+
'config/public/assets/ultramodern-logo.svg',
|
|
112
117
|
'src/modern-app-env.d.ts',
|
|
113
118
|
'src/routes/index.css',
|
|
114
119
|
'src/routes/layout.tsx',
|
|
@@ -241,6 +246,11 @@ const packageJson = JSON.parse(
|
|
|
241
246
|
fs.readFileSync(path.resolve(process.cwd(), 'package.json'), 'utf-8'),
|
|
242
247
|
);
|
|
243
248
|
const packageSource = JSON.parse(fs.readFileSync(packageSourcePath, 'utf-8'));
|
|
249
|
+
const routePage = readText('src/routes/[lang]/page.tsx');
|
|
250
|
+
const routeCss = readText('src/routes/index.css');
|
|
251
|
+
const modernConfig = readText('modern.config.ts');
|
|
252
|
+
const enLocale = readJson('config/public/locales/en/translation.json');
|
|
253
|
+
const csLocale = readJson('config/public/locales/cs/translation.json');
|
|
244
254
|
const unresolvedTemplateMarker = String.fromCodePoint(123, 123);
|
|
245
255
|
if (JSON.stringify(packageJson).includes(unresolvedTemplateMarker)) {
|
|
246
256
|
console.error('package.json contains unresolved template markers');
|
|
@@ -250,6 +260,89 @@ if (JSON.stringify(packageSource).includes(unresolvedTemplateMarker)) {
|
|
|
250
260
|
console.error('package source metadata contains unresolved template markers');
|
|
251
261
|
process.exit(1);
|
|
252
262
|
}
|
|
263
|
+
|
|
264
|
+
for (const [fileName, text] of [
|
|
265
|
+
['src/routes/[lang]/page.tsx', routePage],
|
|
266
|
+
['src/routes/index.css', routeCss],
|
|
267
|
+
['modern.config.ts', modernConfig],
|
|
268
|
+
]) {
|
|
269
|
+
if (text.includes('lf3-static.bytednsdoc.com')) {
|
|
270
|
+
console.error(`${fileName} must not depend on remote starter assets`);
|
|
271
|
+
process.exit(1);
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
for (const requiredSnippet of [
|
|
276
|
+
'<Helmet',
|
|
277
|
+
'htmlAttributes={{',
|
|
278
|
+
'dir: languageDirections[currentLanguage]',
|
|
279
|
+
'lang: currentLanguage',
|
|
280
|
+
'<title>{pageTitle}</title>',
|
|
281
|
+
'<meta name="description" content={pageDescription} />',
|
|
282
|
+
'<main id="starter-main" className="starter-main">',
|
|
283
|
+
'<h1 id="starter-heading" className="title">',
|
|
284
|
+
'src="/assets/ultramodern-logo.svg"',
|
|
285
|
+
'height={96}',
|
|
286
|
+
'width={96}',
|
|
287
|
+
'<span aria-hidden="true" className="arrow-right" />',
|
|
288
|
+
]) {
|
|
289
|
+
if (!routePage.includes(requiredSnippet)) {
|
|
290
|
+
console.error(`Generated route page must retain starter correctness snippet: ${requiredSnippet}`);
|
|
291
|
+
process.exit(1);
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
for (const forbiddenSnippet of ['src="https://', '<div className="title">']) {
|
|
296
|
+
if (routePage.includes(forbiddenSnippet)) {
|
|
297
|
+
console.error(`Generated route page must not contain ${forbiddenSnippet}`);
|
|
298
|
+
process.exit(1);
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
for (const requiredSnippet of [
|
|
303
|
+
'width=device-width, initial-scale=1.0, viewport-fit=cover',
|
|
304
|
+
"title: 'UltraModern.js Starter'",
|
|
305
|
+
]) {
|
|
306
|
+
if (!modernConfig.includes(requiredSnippet)) {
|
|
307
|
+
console.error(`modern.config.ts must retain ${requiredSnippet}`);
|
|
308
|
+
process.exit(1);
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
if (modernConfig.includes('user-scalable=no') || modernConfig.includes('maximum-scale')) {
|
|
312
|
+
console.error('modern.config.ts must not disable user zoom');
|
|
313
|
+
process.exit(1);
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
for (const requiredSnippet of [
|
|
317
|
+
'min-block-size: 100dvh',
|
|
318
|
+
'grid-template-columns: repeat(auto-fit, minmax(min(100%, 17rem), 1fr))',
|
|
319
|
+
':focus-visible',
|
|
320
|
+
'@media (prefers-reduced-motion: reduce)',
|
|
321
|
+
'.skip-link',
|
|
322
|
+
]) {
|
|
323
|
+
if (!routeCss.includes(requiredSnippet)) {
|
|
324
|
+
console.error(`Starter CSS must retain ${requiredSnippet}`);
|
|
325
|
+
process.exit(1);
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
if (routeCss.includes('width: 1100px') || /\.card:focus(?!-visible)/u.test(routeCss)) {
|
|
329
|
+
console.error('Starter CSS must not reintroduce fixed grid width or focus transform styling');
|
|
330
|
+
process.exit(1);
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
for (const [localeName, locale] of [
|
|
334
|
+
['en', enLocale],
|
|
335
|
+
['cs', csLocale],
|
|
336
|
+
]) {
|
|
337
|
+
if (
|
|
338
|
+
typeof locale.home?.meta?.title !== 'string' ||
|
|
339
|
+
typeof locale.home?.meta?.description !== 'string' ||
|
|
340
|
+
typeof locale.home?.skipLink !== 'string'
|
|
341
|
+
) {
|
|
342
|
+
console.error(`${localeName} locale must include starter metadata and skip-link copy`);
|
|
343
|
+
process.exit(1);
|
|
344
|
+
}
|
|
345
|
+
}
|
|
253
346
|
{{#unless isSubproject}}
|
|
254
347
|
const skillsLock = JSON.parse(
|
|
255
348
|
fs.readFileSync(path.resolve(process.cwd(), '.agents/skills-lock.json'), 'utf-8'),
|
|
@@ -13,6 +13,10 @@ import { useTranslation } from 'react-i18next';
|
|
|
13
13
|
const fallbackLanguage = 'en';
|
|
14
14
|
const supportedLanguages = ['en', 'cs'] as const;
|
|
15
15
|
type SupportedLanguage = (typeof supportedLanguages)[number];
|
|
16
|
+
const languageDirections: Record<SupportedLanguage, 'ltr'> = {
|
|
17
|
+
cs: 'ltr',
|
|
18
|
+
en: 'ltr',
|
|
19
|
+
};
|
|
16
20
|
|
|
17
21
|
const isSupportedLanguage = (value: string): value is SupportedLanguage =>
|
|
18
22
|
supportedLanguages.includes(value as SupportedLanguage);
|
|
@@ -52,6 +56,8 @@ const Index = () => {
|
|
|
52
56
|
const { language } = useModernI18n();
|
|
53
57
|
const location = useLocation();
|
|
54
58
|
const currentLanguage = isSupportedLanguage(language) ? language : fallbackLanguage;
|
|
59
|
+
const pageTitle = t('home.meta.title');
|
|
60
|
+
const pageDescription = t('home.meta.description');
|
|
55
61
|
const canonicalPath = localizedPath(location.pathname, currentLanguage);
|
|
56
62
|
const suffix = locationSuffix(location);
|
|
57
63
|
const languageOptions = supportedLanguages.map((code) => ({
|
|
@@ -81,12 +87,9 @@ const Index = () => {
|
|
|
81
87
|
{{/if}}
|
|
82
88
|
return (
|
|
83
89
|
<div className="container-box">
|
|
84
|
-
<Helmet>
|
|
85
|
-
<
|
|
86
|
-
|
|
87
|
-
type="image/x-icon"
|
|
88
|
-
href="https://lf3-static.bytednsdoc.com/obj/eden-cn/uhbfnupenuhf/favicon.ico"
|
|
89
|
-
/>
|
|
90
|
+
<Helmet htmlAttributes={{ dir: languageDirections[currentLanguage], lang: currentLanguage }}>
|
|
91
|
+
<title>{pageTitle}</title>
|
|
92
|
+
<meta name="description" content={pageDescription} />
|
|
90
93
|
<link rel="canonical" href={absoluteUrl(canonicalPath)} />
|
|
91
94
|
{supportedLanguages.map((code) => (
|
|
92
95
|
<link
|
|
@@ -102,7 +105,10 @@ const Index = () => {
|
|
|
102
105
|
rel="alternate"
|
|
103
106
|
/>
|
|
104
107
|
</Helmet>
|
|
105
|
-
<main>
|
|
108
|
+
<a className="skip-link" href="#starter-main">
|
|
109
|
+
{t('home.skipLink')}
|
|
110
|
+
</a>
|
|
111
|
+
<header className="starter-header">
|
|
106
112
|
<nav className="language-switcher" aria-label={t('home.language.switcher')}>
|
|
107
113
|
{languageOptions.map((option) => (
|
|
108
114
|
<a
|
|
@@ -114,24 +120,32 @@ const Index = () => {
|
|
|
114
120
|
</a>
|
|
115
121
|
))}
|
|
116
122
|
</nav>
|
|
117
|
-
|
|
118
|
-
|
|
123
|
+
</header>
|
|
124
|
+
<main id="starter-main" className="starter-main">
|
|
125
|
+
<section className="hero" aria-labelledby="starter-heading">
|
|
119
126
|
<img
|
|
120
127
|
alt={t('home.logoAlt')}
|
|
121
128
|
className="logo"
|
|
122
|
-
|
|
129
|
+
height={96}
|
|
130
|
+
src="/assets/ultramodern-logo.svg"
|
|
131
|
+
width={96}
|
|
123
132
|
/>
|
|
124
|
-
<
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
133
|
+
<div className="hero-copy">
|
|
134
|
+
<p className="name">{t('home.name')}</p>
|
|
135
|
+
<h1 id="starter-heading" className="title">
|
|
136
|
+
{t('home.title')}
|
|
137
|
+
</h1>
|
|
138
|
+
<p className="description{{#if enableTailwind}} text-emerald-700 font-semibold{{/if}}">
|
|
139
|
+
{t('home.description.intro')}{' '}
|
|
140
|
+
<code className="code">presetUltramodern(...)</code>{' '}
|
|
141
|
+
{t('home.description.afterPreset')}{' '}
|
|
142
|
+
<code className="code">modern.config.ts</code>{' '}
|
|
143
|
+
{t('home.description.afterConfig')}{' '}
|
|
144
|
+
<code className="code">pnpm run ultramodern:check</code>{' '}
|
|
145
|
+
{t('home.description.end')}
|
|
146
|
+
</p>
|
|
147
|
+
</div>
|
|
148
|
+
</section>
|
|
135
149
|
{{#if useEffectBff}}
|
|
136
150
|
<p className="description effect-message{{#if enableTailwind}} text-emerald-700 font-semibold{{/if}}">
|
|
137
151
|
{t('home.bff.response')} <code className="code">{effectMessage}</code>
|
|
@@ -146,11 +160,7 @@ const Index = () => {
|
|
|
146
160
|
>
|
|
147
161
|
<h2>
|
|
148
162
|
{t('home.cards.guide.title')}
|
|
149
|
-
<
|
|
150
|
-
alt=""
|
|
151
|
-
className="arrow-right"
|
|
152
|
-
src="https://lf3-static.bytednsdoc.com/obj/eden-cn/zq-uylkvT/ljhwZthlaukjlkulzlp/arrow-right.svg"
|
|
153
|
-
/>
|
|
163
|
+
<span aria-hidden="true" className="arrow-right" />
|
|
154
164
|
</h2>
|
|
155
165
|
<p>{t('home.cards.guide.body')}</p>
|
|
156
166
|
</a>
|
|
@@ -162,11 +172,7 @@ const Index = () => {
|
|
|
162
172
|
>
|
|
163
173
|
<h2>
|
|
164
174
|
{t('home.cards.config.title')}
|
|
165
|
-
<
|
|
166
|
-
alt=""
|
|
167
|
-
className="arrow-right"
|
|
168
|
-
src="https://lf3-static.bytednsdoc.com/obj/eden-cn/zq-uylkvT/ljhwZthlaukjlkulzlp/arrow-right.svg"
|
|
169
|
-
/>
|
|
175
|
+
<span aria-hidden="true" className="arrow-right" />
|
|
170
176
|
</h2>
|
|
171
177
|
<p>{t('home.cards.config.body')}</p>
|
|
172
178
|
</a>
|
|
@@ -178,11 +184,7 @@ const Index = () => {
|
|
|
178
184
|
>
|
|
179
185
|
<h2>
|
|
180
186
|
{t('home.cards.gates.title')}
|
|
181
|
-
<
|
|
182
|
-
alt=""
|
|
183
|
-
className="arrow-right"
|
|
184
|
-
src="https://lf3-static.bytednsdoc.com/obj/eden-cn/zq-uylkvT/ljhwZthlaukjlkulzlp/arrow-right.svg"
|
|
185
|
-
/>
|
|
187
|
+
<span aria-hidden="true" className="arrow-right" />
|
|
186
188
|
</h2>
|
|
187
189
|
<p>{t('home.cards.gates.body')}</p>
|
|
188
190
|
</a>
|
|
@@ -194,11 +196,7 @@ const Index = () => {
|
|
|
194
196
|
>
|
|
195
197
|
<h2>
|
|
196
198
|
{t('home.cards.bff.title')}
|
|
197
|
-
<
|
|
198
|
-
alt=""
|
|
199
|
-
className="arrow-right"
|
|
200
|
-
src="https://lf3-static.bytednsdoc.com/obj/eden-cn/zq-uylkvT/ljhwZthlaukjlkulzlp/arrow-right.svg"
|
|
201
|
-
/>
|
|
199
|
+
<span aria-hidden="true" className="arrow-right" />
|
|
202
200
|
</h2>
|
|
203
201
|
<p>{t('home.cards.bff.body')}</p>
|
|
204
202
|
</a>
|
|
@@ -1,129 +1,266 @@
|
|
|
1
1
|
{{#if enableTailwind}}@import 'tailwindcss';
|
|
2
2
|
|
|
3
|
-
{{/if}}
|
|
3
|
+
{{/if}}:root {
|
|
4
|
+
color-scheme: light;
|
|
5
|
+
--starter-bg: #f3f6f4;
|
|
6
|
+
--starter-surface: #ffffff;
|
|
7
|
+
--starter-text: #14201b;
|
|
8
|
+
--starter-muted: #52625b;
|
|
9
|
+
--starter-border: #cfdad4;
|
|
10
|
+
--starter-accent: #087f5b;
|
|
11
|
+
--starter-accent-strong: #075f48;
|
|
12
|
+
--starter-focus: #0b7285;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
html {
|
|
16
|
+
scroll-behavior: smooth;
|
|
17
|
+
}
|
|
18
|
+
|
|
4
19
|
body {
|
|
5
|
-
padding: 0;
|
|
6
20
|
margin: 0;
|
|
7
21
|
font-family:
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
22
|
+
Inter,
|
|
23
|
+
ui-sans-serif,
|
|
24
|
+
system-ui,
|
|
25
|
+
-apple-system,
|
|
26
|
+
BlinkMacSystemFont,
|
|
27
|
+
Segoe UI,
|
|
11
28
|
Arial,
|
|
12
29
|
sans-serif;
|
|
13
|
-
|
|
30
|
+
color: var(--starter-text);
|
|
31
|
+
background: linear-gradient(180deg, #ffffff 0%, var(--starter-bg) 100%);
|
|
14
32
|
}
|
|
15
33
|
|
|
16
34
|
p {
|
|
17
35
|
margin: 0;
|
|
18
36
|
}
|
|
19
37
|
|
|
20
|
-
|
|
38
|
+
.container-box,
|
|
39
|
+
.container-box *,
|
|
40
|
+
.container-box *::before,
|
|
41
|
+
.container-box *::after {
|
|
21
42
|
-webkit-font-smoothing: antialiased;
|
|
22
43
|
-moz-osx-font-smoothing: grayscale;
|
|
23
44
|
box-sizing: border-box;
|
|
24
45
|
}
|
|
25
46
|
|
|
26
47
|
.container-box {
|
|
27
|
-
min-
|
|
28
|
-
|
|
48
|
+
min-block-size: 100dvh;
|
|
49
|
+
inline-size: 100%;
|
|
29
50
|
display: flex;
|
|
30
51
|
flex-direction: column;
|
|
31
|
-
|
|
32
|
-
align-items: center;
|
|
33
|
-
padding-top: 10px;
|
|
52
|
+
padding: 1.25rem;
|
|
34
53
|
}
|
|
35
54
|
|
|
36
|
-
|
|
37
|
-
|
|
55
|
+
.skip-link {
|
|
56
|
+
position: fixed;
|
|
57
|
+
inset-block-start: 1rem;
|
|
58
|
+
inset-inline-start: 1rem;
|
|
59
|
+
z-index: 1;
|
|
60
|
+
padding: 0.65rem 0.9rem;
|
|
61
|
+
color: #ffffff;
|
|
62
|
+
background: var(--starter-accent-strong);
|
|
63
|
+
border-radius: 6px;
|
|
64
|
+
transform: translateY(-150%);
|
|
65
|
+
transition: transform 0.15s ease;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
.skip-link:focus-visible {
|
|
69
|
+
transform: translateY(0);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
.starter-header {
|
|
73
|
+
inline-size: min(100%, 72rem);
|
|
74
|
+
margin-inline: auto;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
.language-switcher {
|
|
38
78
|
display: flex;
|
|
39
|
-
flex-
|
|
40
|
-
|
|
79
|
+
flex-wrap: wrap;
|
|
80
|
+
gap: 0.5rem;
|
|
81
|
+
justify-content: flex-end;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
.language-switcher a {
|
|
85
|
+
min-block-size: 2.25rem;
|
|
86
|
+
padding: 0.55rem 0.8rem;
|
|
87
|
+
color: var(--starter-muted);
|
|
88
|
+
text-decoration: none;
|
|
89
|
+
border: 1px solid transparent;
|
|
90
|
+
border-radius: 999px;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
.language-switcher a:hover {
|
|
94
|
+
color: var(--starter-text);
|
|
95
|
+
border-color: var(--starter-border);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
.language-switcher a[aria-current='page'] {
|
|
99
|
+
color: var(--starter-accent-strong);
|
|
100
|
+
background: rgb(8 127 91 / 10%);
|
|
101
|
+
border-color: rgb(8 127 91 / 24%);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
.starter-main {
|
|
105
|
+
flex: 1;
|
|
106
|
+
inline-size: min(100%, 72rem);
|
|
107
|
+
margin-inline: auto;
|
|
108
|
+
padding-block: 4rem 2rem;
|
|
109
|
+
display: grid;
|
|
110
|
+
gap: 3rem;
|
|
111
|
+
align-content: center;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
.hero {
|
|
115
|
+
display: grid;
|
|
116
|
+
grid-template-columns: auto minmax(0, 1fr);
|
|
117
|
+
gap: 1.5rem;
|
|
41
118
|
align-items: center;
|
|
42
119
|
}
|
|
43
120
|
|
|
44
121
|
.title {
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
font-
|
|
49
|
-
|
|
122
|
+
margin: 0.35rem 0 0;
|
|
123
|
+
font-size: 3.5rem;
|
|
124
|
+
line-height: 1;
|
|
125
|
+
font-weight: 750;
|
|
126
|
+
letter-spacing: 0;
|
|
127
|
+
text-wrap: balance;
|
|
50
128
|
}
|
|
51
129
|
|
|
52
130
|
.logo {
|
|
53
|
-
|
|
54
|
-
|
|
131
|
+
inline-size: 6rem;
|
|
132
|
+
block-size: 6rem;
|
|
133
|
+
border-radius: 1.25rem;
|
|
134
|
+
filter: drop-shadow(0 1rem 1.5rem rgb(17 24 39 / 18%));
|
|
55
135
|
}
|
|
56
136
|
|
|
57
137
|
.name {
|
|
58
|
-
color:
|
|
138
|
+
color: var(--starter-accent-strong);
|
|
139
|
+
font-size: 0.95rem;
|
|
140
|
+
font-weight: 700;
|
|
141
|
+
line-height: 1.4;
|
|
59
142
|
}
|
|
60
143
|
|
|
61
144
|
.description {
|
|
62
|
-
text-align: center;
|
|
63
145
|
line-height: 1.5;
|
|
64
|
-
font-size: 1.
|
|
65
|
-
color:
|
|
66
|
-
|
|
146
|
+
font-size: 1.15rem;
|
|
147
|
+
color: var(--starter-muted);
|
|
148
|
+
max-inline-size: 54rem;
|
|
149
|
+
margin-block-start: 1rem;
|
|
150
|
+
text-wrap: pretty;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
.effect-message {
|
|
154
|
+
padding: 1rem;
|
|
155
|
+
background: var(--starter-surface);
|
|
156
|
+
border: 1px solid var(--starter-border);
|
|
157
|
+
border-radius: 8px;
|
|
67
158
|
}
|
|
68
159
|
|
|
69
160
|
.code {
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
161
|
+
display: inline-block;
|
|
162
|
+
padding: 0.15rem 0.35rem;
|
|
163
|
+
color: var(--starter-accent-strong);
|
|
164
|
+
background: rgb(8 127 91 / 10%);
|
|
165
|
+
border-radius: 6px;
|
|
166
|
+
font-size: 0.95em;
|
|
74
167
|
font-family:
|
|
168
|
+
ui-monospace,
|
|
169
|
+
SFMono-Regular,
|
|
75
170
|
Menlo,
|
|
76
171
|
Monaco,
|
|
77
|
-
Lucida Console,
|
|
78
|
-
Liberation Mono,
|
|
79
|
-
DejaVu Sans Mono,
|
|
80
|
-
Bitstream Vera Sans Mono,
|
|
81
|
-
Courier New,
|
|
82
172
|
monospace;
|
|
83
173
|
}
|
|
84
174
|
|
|
85
175
|
.container-box .grid {
|
|
86
|
-
display:
|
|
176
|
+
display: grid;
|
|
177
|
+
grid-template-columns: repeat(auto-fit, minmax(min(100%, 17rem), 1fr));
|
|
87
178
|
align-items: center;
|
|
88
|
-
|
|
89
|
-
width: 1100px;
|
|
90
|
-
margin-top: 3rem;
|
|
179
|
+
gap: 1rem;
|
|
91
180
|
}
|
|
92
181
|
|
|
93
182
|
.card {
|
|
94
|
-
padding: 1.5rem;
|
|
95
183
|
display: flex;
|
|
96
184
|
flex-direction: column;
|
|
97
|
-
|
|
98
|
-
|
|
185
|
+
gap: 0.75rem;
|
|
186
|
+
min-block-size: 9rem;
|
|
187
|
+
padding: 1.25rem;
|
|
99
188
|
color: inherit;
|
|
100
189
|
text-decoration: none;
|
|
101
|
-
|
|
102
|
-
|
|
190
|
+
background: var(--starter-surface);
|
|
191
|
+
border: 1px solid var(--starter-border);
|
|
192
|
+
border-radius: 8px;
|
|
193
|
+
transition:
|
|
194
|
+
border-color 0.15s ease,
|
|
195
|
+
transform 0.15s ease;
|
|
103
196
|
}
|
|
104
197
|
|
|
105
|
-
.card:hover
|
|
106
|
-
|
|
107
|
-
transform:
|
|
198
|
+
.card:hover {
|
|
199
|
+
border-color: rgb(8 127 91 / 45%);
|
|
200
|
+
transform: translateY(-0.125rem);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
.skip-link:focus-visible,
|
|
204
|
+
.language-switcher a:focus-visible,
|
|
205
|
+
.card:focus-visible {
|
|
206
|
+
outline: 3px solid var(--starter-focus);
|
|
207
|
+
outline-offset: 3px;
|
|
108
208
|
}
|
|
109
209
|
|
|
110
210
|
.card h2 {
|
|
111
211
|
display: flex;
|
|
112
212
|
align-items: center;
|
|
113
|
-
|
|
213
|
+
gap: 0.55rem;
|
|
214
|
+
font-size: 1.25rem;
|
|
215
|
+
line-height: 1.2;
|
|
114
216
|
margin: 0;
|
|
115
217
|
padding: 0;
|
|
116
218
|
}
|
|
117
219
|
|
|
118
220
|
.card p {
|
|
119
|
-
opacity: 0.
|
|
221
|
+
opacity: 0.7;
|
|
120
222
|
font-size: 0.9rem;
|
|
121
223
|
line-height: 1.5;
|
|
122
|
-
margin-top: 1rem;
|
|
123
224
|
}
|
|
124
225
|
|
|
125
226
|
.arrow-right {
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
227
|
+
flex: none;
|
|
228
|
+
inline-size: 0.65rem;
|
|
229
|
+
block-size: 0.65rem;
|
|
230
|
+
border-block-start: 2px solid currentColor;
|
|
231
|
+
border-inline-end: 2px solid currentColor;
|
|
232
|
+
transform: rotate(45deg);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
@media (max-width: 44rem) {
|
|
236
|
+
.container-box {
|
|
237
|
+
padding: 1rem;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
.starter-main {
|
|
241
|
+
padding-block: 2rem 1rem;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
.hero {
|
|
245
|
+
grid-template-columns: 1fr;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
.title {
|
|
249
|
+
font-size: 2.5rem;
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
@media (prefers-reduced-motion: reduce) {
|
|
254
|
+
html {
|
|
255
|
+
scroll-behavior: auto;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
.skip-link,
|
|
259
|
+
.card {
|
|
260
|
+
transition: none;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
.card:hover {
|
|
264
|
+
transform: none;
|
|
265
|
+
}
|
|
129
266
|
}
|
|
@@ -25,6 +25,63 @@ describe('generated UltraModern contract', () => {
|
|
|
25
25
|
{{/unless}}
|
|
26
26
|
});
|
|
27
27
|
|
|
28
|
+
test('keeps UltraModern starter web correctness defaults', () => {
|
|
29
|
+
const routePage = readText('src/routes/[lang]/page.tsx');
|
|
30
|
+
const routeCss = readText('src/routes/index.css');
|
|
31
|
+
const modernConfig = readText('modern.config.ts');
|
|
32
|
+
const enLocale = readJson<{
|
|
33
|
+
home?: {
|
|
34
|
+
meta?: {
|
|
35
|
+
description?: string;
|
|
36
|
+
title?: string;
|
|
37
|
+
};
|
|
38
|
+
skipLink?: string;
|
|
39
|
+
};
|
|
40
|
+
}>('config/public/locales/en/translation.json');
|
|
41
|
+
|
|
42
|
+
expect(fs.existsSync(path.join(root, 'config/favicon.svg'))).toBe(true);
|
|
43
|
+
expect(
|
|
44
|
+
fs.existsSync(
|
|
45
|
+
path.join(root, 'config/public/assets/ultramodern-logo.svg'),
|
|
46
|
+
),
|
|
47
|
+
).toBe(true);
|
|
48
|
+
expect(routePage).not.toContain('lf3-static.bytednsdoc.com');
|
|
49
|
+
expect(routePage).toContain('<Helmet');
|
|
50
|
+
expect(routePage).toContain('htmlAttributes={{');
|
|
51
|
+
expect(routePage).toContain('dir: languageDirections[currentLanguage]');
|
|
52
|
+
expect(routePage).toContain('lang: currentLanguage');
|
|
53
|
+
expect(routePage).toContain('<title>{pageTitle}</title>');
|
|
54
|
+
expect(routePage).toContain(
|
|
55
|
+
'<meta name="description" content={pageDescription} />',
|
|
56
|
+
);
|
|
57
|
+
expect(routePage).toContain(
|
|
58
|
+
'<main id="starter-main" className="starter-main">',
|
|
59
|
+
);
|
|
60
|
+
expect(routePage).toContain('<h1 id="starter-heading" className="title">');
|
|
61
|
+
expect(routePage).toContain('src="/assets/ultramodern-logo.svg"');
|
|
62
|
+
expect(routePage).toContain('height={96}');
|
|
63
|
+
expect(routePage).toContain('width={96}');
|
|
64
|
+
expect(routePage).toContain(
|
|
65
|
+
'<span aria-hidden="true" className="arrow-right" />',
|
|
66
|
+
);
|
|
67
|
+
expect(routePage).not.toContain('<div className="title">');
|
|
68
|
+
expect(modernConfig).toContain(
|
|
69
|
+
'width=device-width, initial-scale=1.0, viewport-fit=cover',
|
|
70
|
+
);
|
|
71
|
+
expect(modernConfig).not.toContain('user-scalable=no');
|
|
72
|
+
expect(modernConfig).not.toContain('maximum-scale');
|
|
73
|
+
expect(routeCss).toContain('min-block-size: 100dvh');
|
|
74
|
+
expect(routeCss).toContain(
|
|
75
|
+
'grid-template-columns: repeat(auto-fit, minmax(min(100%, 17rem), 1fr))',
|
|
76
|
+
);
|
|
77
|
+
expect(routeCss).toContain(':focus-visible');
|
|
78
|
+
expect(routeCss).toContain('@media (prefers-reduced-motion: reduce)');
|
|
79
|
+
expect(routeCss).not.toContain('width: 1100px');
|
|
80
|
+
expect(enLocale.home?.meta?.title).toBe('UltraModern.js Starter');
|
|
81
|
+
expect(enLocale.home?.meta?.description).toBeTruthy();
|
|
82
|
+
expect(enLocale.home?.skipLink).toBeTruthy();
|
|
83
|
+
});
|
|
84
|
+
|
|
28
85
|
test('retains package-source metadata for generated Modern.js packages', () => {
|
|
29
86
|
const packageJson = readJson<{
|
|
30
87
|
dependencies?: Record<string, string>;
|