@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 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 createLocalisedUrlsMap(app) {
1820
- return Object.fromEntries(createRouteOwnedI18nPaths(app).flatMap((route)=>{
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
- declare const i18n: any;
2
- declare const localeKeys: any;
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.102",
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.102"
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.102"
57
+ "frameworkVersion": "3.2.0-ultramodern.103"
58
58
  }
59
59
  }
@@ -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
- <link
86
- rel="icon"
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
- <div className="title">
118
- {t('home.title')}
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
- src="https://lf3-static.bytednsdoc.com/obj/eden-cn/zq-uylkvT/ljhwZthlaukjlkulzlp/modern-js-logo.svg"
129
+ height={96}
130
+ src="/assets/ultramodern-logo.svg"
131
+ width={96}
123
132
  />
124
- <p className="name">{t('home.name')}</p>
125
- </div>
126
- <p className="description{{#if enableTailwind}} text-emerald-700 font-semibold{{/if}}">
127
- {t('home.description.intro')}{' '}
128
- <code className="code">presetUltramodern(...)</code>{' '}
129
- {t('home.description.afterPreset')}{' '}
130
- <code className="code">modern.config.ts</code>{' '}
131
- {t('home.description.afterConfig')}{' '}
132
- <code className="code">pnpm run ultramodern:check</code>{' '}
133
- {t('home.description.end')}
134
- </p>
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
- <img
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
- <img
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
- <img
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
- <img
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}}html,
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
- PingFang SC,
9
- Hiragino Sans GB,
10
- Microsoft YaHei,
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
- background: linear-gradient(to bottom, transparent, #fff) #eceeef;
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-height: 100vh;
28
- max-width: 100%;
48
+ min-block-size: 100dvh;
49
+ inline-size: 100%;
29
50
  display: flex;
30
51
  flex-direction: column;
31
- justify-content: center;
32
- align-items: center;
33
- padding-top: 10px;
52
+ padding: 1.25rem;
34
53
  }
35
54
 
36
- main {
37
- flex: 1;
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-direction: column;
40
- justify-content: center;
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
- display: flex;
46
- margin: 4rem 0 4rem;
47
- align-items: center;
48
- font-size: 4rem;
49
- font-weight: 600;
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
- width: 6rem;
54
- margin: 7px 0 0 1rem;
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: #4ecaff;
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.3rem;
65
- color: #1b3a42;
66
- margin-bottom: 5rem;
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
- background: #fafafa;
71
- border-radius: 12px;
72
- padding: 0.6rem 0.9rem;
73
- font-size: 1.05rem;
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: flex;
176
+ display: grid;
177
+ grid-template-columns: repeat(auto-fit, minmax(min(100%, 17rem), 1fr));
87
178
  align-items: center;
88
- justify-content: center;
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
- justify-content: center;
98
- height: 100px;
185
+ gap: 0.75rem;
186
+ min-block-size: 9rem;
187
+ padding: 1.25rem;
99
188
  color: inherit;
100
189
  text-decoration: none;
101
- transition: 0.15s ease;
102
- width: 45%;
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
- .card:focus {
107
- transform: scale(1.05);
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
- font-size: 1.5rem;
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.6;
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
- width: 1.3rem;
127
- margin-left: 0.5rem;
128
- margin-top: 3px;
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>;
@@ -116,6 +116,6 @@
116
116
  }
117
117
  ]
118
118
  },
119
- "include": ["src", "api", "shared", "config", "modern.config.ts"],
119
+ "include": ["src", "api", "shared", "config", "modern.config.ts", "server"],
120
120
  "exclude": ["**/node_modules"]
121
121
  }