@bleedingdev/modern-js-create 3.2.0-ultramodern.12 → 3.2.0-ultramodern.14

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
@@ -1139,6 +1139,18 @@ import { moduleFederationPlugin } from '@module-federation/modern-js-v3';
1139
1139
 
1140
1140
  const appId = '${app.id}';
1141
1141
  const port = Number(process.env['${app.portEnv}'] ?? ${app.port});
1142
+ const configuredSiteUrl = process.env['MODERN_PUBLIC_SITE_URL'];
1143
+ const hasConfiguredSiteUrl = typeof configuredSiteUrl === 'string' && configuredSiteUrl.length > 0;
1144
+ const isProductionBuild =
1145
+ process.env['NODE_ENV'] === 'production' || process.argv.includes('build');
1146
+
1147
+ if (isProductionBuild && !hasConfiguredSiteUrl) {
1148
+ throw new Error(
1149
+ 'MODERN_PUBLIC_SITE_URL must be set for production builds so canonical and hreflang URLs use the deployed origin.',
1150
+ );
1151
+ }
1152
+
1153
+ const siteUrl = hasConfiguredSiteUrl ? configuredSiteUrl : \`http://localhost:\${port}\`;
1142
1154
 
1143
1155
  export default defineConfig(
1144
1156
  presetUltramodern(
@@ -1154,6 +1166,7 @@ export default defineConfig(
1154
1166
  localeDetection: {
1155
1167
  fallbackLanguage: 'en',
1156
1168
  languages: ['en', 'cs'],
1169
+ localePathRedirect: true,
1157
1170
  },
1158
1171
  }),
1159
1172
  tanstackRouterPlugin(),
@@ -1166,6 +1179,11 @@ export default defineConfig(
1166
1179
  moduleFederationAppSSR: true,
1167
1180
  },
1168
1181
  },
1182
+ source: {
1183
+ globalVars: {
1184
+ ULTRAMODERN_SITE_URL: siteUrl,
1185
+ },
1186
+ },
1169
1187
  },
1170
1188
  {
1171
1189
  appId,
@@ -1330,32 +1348,103 @@ export default defineRuntimeConfig({
1330
1348
  });
1331
1349
  `;
1332
1350
  }
1351
+ function createLocalizedHeadComponent(includeLocationSuffix = false) {
1352
+ return `const fallbackLanguage = 'en';
1353
+ const supportedLanguages = ['en', 'cs'] as const;
1354
+ type SupportedLanguage = (typeof supportedLanguages)[number];
1355
+
1356
+ const isSupportedLanguage = (value: string): value is SupportedLanguage =>
1357
+ supportedLanguages.includes(value as SupportedLanguage);
1358
+
1359
+ const stripLanguagePrefix = (pathname: string) => {
1360
+ const segments = pathname.split('/').filter(Boolean);
1361
+ if (segments.length > 0 && isSupportedLanguage(segments[0] ?? '')) {
1362
+ segments.shift();
1363
+ }
1364
+ return \`/\${segments.join('/')}\`;
1365
+ };
1366
+
1367
+ const localizedPath = (pathname: string, language: SupportedLanguage) => {
1368
+ const pathWithoutLanguage = stripLanguagePrefix(pathname);
1369
+ return pathWithoutLanguage === '/' ? \`/\${language}\` : \`/\${language}\${pathWithoutLanguage}\`;
1370
+ };
1371
+
1372
+ const absoluteUrl = (pathname: string) => {
1373
+ const origin = ULTRAMODERN_SITE_URL.replace(/\\/+$/u, '');
1374
+ return \`\${origin}\${pathname}\`;
1375
+ };
1376
+ ${includeLocationSuffix ? `
1377
+ const locationSuffix = (location: { hash?: unknown; search?: unknown; searchStr?: unknown }) => {
1378
+ const { hash, search, searchStr } = location;
1379
+ let locationSearch = '';
1380
+ if (typeof searchStr === 'string') {
1381
+ locationSearch = searchStr;
1382
+ } else if (typeof search === 'string') {
1383
+ locationSearch = search;
1384
+ }
1385
+ const locationHash = typeof hash === 'string' ? hash : '';
1386
+ return \`\${locationSearch}\${locationHash}\`;
1387
+ };
1388
+ ` : ''}
1389
+ const LocalizedHead = () => {
1390
+ const { language } = useModernI18n();
1391
+ const location = useLocation();
1392
+ const currentLanguage = isSupportedLanguage(language) ? language : fallbackLanguage;
1393
+ const canonicalPath = localizedPath(location.pathname, currentLanguage);
1394
+
1395
+ return (
1396
+ <Helmet>
1397
+ <link rel="canonical" href={absoluteUrl(canonicalPath)} />
1398
+ {supportedLanguages.map((code) => (
1399
+ <link
1400
+ href={absoluteUrl(localizedPath(location.pathname, code))}
1401
+ hrefLang={code}
1402
+ key={code}
1403
+ rel="alternate"
1404
+ />
1405
+ ))}
1406
+ <link
1407
+ href={absoluteUrl(localizedPath(location.pathname, fallbackLanguage))}
1408
+ hrefLang="x-default"
1409
+ rel="alternate"
1410
+ />
1411
+ </Helmet>
1412
+ );
1413
+ };
1414
+ `;
1415
+ }
1333
1416
  function createShellPage() {
1334
- return `import { useModernI18n } from '@modern-js/plugin-i18n/runtime';
1417
+ return `import { Helmet } from '@modern-js/runtime/head';
1418
+ import { useModernI18n } from '@modern-js/plugin-i18n/runtime';
1419
+ import { useLocation } from '@modern-js/plugin-tanstack/runtime';
1335
1420
  import { useTranslation } from 'react-i18next';
1336
1421
 
1337
1422
  const remotes = ['remote-commerce', 'remote-identity', 'remote-design-system'];
1338
1423
 
1424
+ ${createLocalizedHeadComponent(true)}
1339
1425
  export default function ShellHome() {
1340
1426
  const { t } = useTranslation();
1341
- const { changeLanguage, language } = useModernI18n();
1342
- const languageOptions = [
1343
- { code: 'en', label: t('language.en') },
1344
- { code: 'cs', label: t('language.cs') },
1345
- ];
1346
-
1427
+ const { language } = useModernI18n();
1428
+ const location = useLocation();
1429
+ const currentLanguage = isSupportedLanguage(language) ? language : fallbackLanguage;
1430
+ const suffix = locationSuffix(location);
1431
+ const languageOptions = supportedLanguages.map((code) => ({
1432
+ code,
1433
+ href: \`\${localizedPath(location.pathname, code)}\${suffix}\`,
1434
+ label: t(\`language.\${code}\`),
1435
+ }));
1347
1436
  return (
1348
1437
  <main>
1438
+ <LocalizedHead />
1349
1439
  <nav aria-label={t('language.switcher')}>
1350
1440
  {languageOptions.map((option) => (
1351
- <button
1352
- disabled={language === option.code}
1441
+ <a
1442
+ aria-current={currentLanguage === option.code ? 'page' : undefined}
1443
+ href={option.href}
1353
1444
  key={option.code}
1354
- onClick={() => void changeLanguage(option.code)}
1355
- type="button"
1356
1445
  >
1357
1446
  {option.label}
1358
- </button>
1447
+ </a>
1359
1448
  ))}
1360
1449
  </nav>
1361
1450
  <h1>{t('shell.title')}</h1>
@@ -1371,13 +1460,18 @@ export default function ShellHome() {
1371
1460
  `;
1372
1461
  }
1373
1462
  function createRemotePage(app) {
1374
- return `import { useTranslation } from 'react-i18next';
1463
+ return `import { Helmet } from '@modern-js/runtime/head';
1464
+ import { useModernI18n } from '@modern-js/plugin-i18n/runtime';
1465
+ import { useLocation } from '@modern-js/plugin-tanstack/runtime';
1466
+ import { useTranslation } from 'react-i18next';
1375
1467
 
1468
+ ${createLocalizedHeadComponent()}
1376
1469
  export default function ${toPascalCase(app.id)}Home() {
1377
1470
  const { t } = useTranslation();
1378
1471
 
1379
1472
  return (
1380
1473
  <main>
1474
+ <LocalizedHead />
1381
1475
  <h1>{t('remote.title')}</h1>
1382
1476
  <p data-mf-role="${app.kind}">{t('remote.domain')}</p>
1383
1477
  </main>
@@ -1837,14 +1931,14 @@ function createTemplateManifest(modernVersion, packageSource) {
1837
1931
  function writeApp(targetDir, scope, app, packageSource) {
1838
1932
  writeJson(targetDir, `${app.directory}/package.json`, createAppPackage(scope, app, packageSource));
1839
1933
  writeJson(targetDir, `${app.directory}/tsconfig.json`, createPackageTsConfig(app.directory));
1840
- writeFile(targetDir, `${app.directory}/src/modern-app-env.d.ts`, "/// <reference types='@modern-js/app-tools/types' />\n");
1934
+ writeFile(targetDir, `${app.directory}/src/modern-app-env.d.ts`, "/// <reference types='@modern-js/app-tools/types' />\n\ndeclare const ULTRAMODERN_SITE_URL: string;\n");
1841
1935
  writeFile(targetDir, `${app.directory}/modern.config.ts`, createAppModernConfig(app));
1842
1936
  writeFile(targetDir, `${app.directory}/src/modern.runtime.ts`, createAppRuntimeConfig());
1843
1937
  writeJson(targetDir, `${app.directory}/config/public/locales/en/translation.json`, createEnglishTranslations(app));
1844
1938
  writeJson(targetDir, `${app.directory}/config/public/locales/cs/translation.json`, createCzechTranslations(app));
1845
1939
  writeFile(targetDir, `${app.directory}/module-federation.config.ts`, 'shell' === app.kind ? createShellModuleFederationConfig() : createRemoteModuleFederationConfig(app));
1846
1940
  writeFile(targetDir, `${app.directory}/src/routes/layout.tsx`, createLayout(app.id));
1847
- writeFile(targetDir, `${app.directory}/src/routes/page.tsx`, 'shell' === app.kind ? createShellPage() : createRemotePage(app));
1941
+ writeFile(targetDir, `${app.directory}/src/routes/[lang]/page.tsx`, 'shell' === app.kind ? createShellPage() : createRemotePage(app));
1848
1942
  if ('vertical' === app.kind) {
1849
1943
  writeFile(targetDir, `${app.directory}/src/remote-entry.tsx`, createRemoteEntry(app));
1850
1944
  const widgetFile = 'remote-identity' === app.id ? 'identity-widget.tsx' : 'commerce-widget.tsx';
@@ -1934,7 +2028,6 @@ const templateIdPattern = /^[a-z0-9][a-z0-9._-]*$/;
1934
2028
  const packageNamePattern = /^(?:@[a-z0-9._-]+\/)?[a-z0-9._-]+$/;
1935
2029
  const requiredDeniedPaths = [
1936
2030
  '.git/**',
1937
- '.github/**',
1938
2031
  '.npmrc',
1939
2032
  '.yarnrc',
1940
2033
  '.env',
@@ -2117,6 +2210,7 @@ function createBuiltinTemplateManifest(version) {
2117
2210
  allowedPaths: [
2118
2211
  '.agents/**',
2119
2212
  '.browserslistrc',
2213
+ '.github/**',
2120
2214
  '.gitignore',
2121
2215
  '.modernjs/**',
2122
2216
  '.nvmrc',
@@ -2129,10 +2223,12 @@ function createBuiltinTemplateManifest(version) {
2129
2223
  'oxlint.config.ts',
2130
2224
  'package.json',
2131
2225
  'postcss.config.mjs',
2226
+ 'rstest.config.mts',
2132
2227
  "scripts/**",
2133
2228
  'shared/**',
2134
2229
  'src/**',
2135
2230
  'tailwind.config.ts',
2231
+ 'tests/**',
2136
2232
  'tsconfig.json'
2137
2233
  ],
2138
2234
  deniedPaths: requiredDeniedPaths,
@@ -2161,10 +2257,13 @@ function createBuiltinTemplateManifest(version) {
2161
2257
  postMaterializationValidation: [
2162
2258
  'ultramodern-contract-check',
2163
2259
  'dependency-install-with-lifecycle-deny',
2260
+ 'package-source-retained',
2261
+ 'rstest-smoke-tests',
2164
2262
  'template-manifest-retained'
2165
2263
  ],
2166
2264
  expectedCommands: [
2167
2265
  "pnpm install --ignore-scripts",
2266
+ 'pnpm test',
2168
2267
  'pnpm run ultramodern:check'
2169
2268
  ]
2170
2269
  }
@@ -2393,6 +2492,45 @@ function singleAppModernPackageSpecifier(packageName, packageSource, useWorkspac
2393
2492
  if ('install' !== packageSource.strategy || !packageSource.aliasScope) return packageSource.modernPackageVersion;
2394
2493
  return `npm:${src_modernAliasPackageName(packageName, packageSource)}@${packageSource.modernPackageVersion}`;
2395
2494
  }
2495
+ const singleAppModernPackages = [
2496
+ '@modern-js/runtime',
2497
+ '@modern-js/app-tools',
2498
+ '@modern-js/tsconfig',
2499
+ '@modern-js/plugin-i18n',
2500
+ '@modern-js/plugin-tanstack',
2501
+ '@modern-js/plugin-bff',
2502
+ '@modern-js/adapter-rstest'
2503
+ ];
2504
+ function createSingleAppPackageSourceEvidence(packageSource, useWorkspaceProtocol) {
2505
+ const strategy = useWorkspaceProtocol ? 'workspace' : 'install';
2506
+ const specifier = useWorkspaceProtocol ? 'workspace:*' : packageSource.modernPackageVersion;
2507
+ const aliases = 'install' === strategy && packageSource.aliasScope ? Object.fromEntries(singleAppModernPackages.map((packageName)=>[
2508
+ packageName,
2509
+ src_modernAliasPackageName(packageName, packageSource)
2510
+ ])) : void 0;
2511
+ return {
2512
+ schemaVersion: 1,
2513
+ preset: 'presetUltramodern',
2514
+ strategy,
2515
+ modernPackages: {
2516
+ specifier,
2517
+ packages: singleAppModernPackages,
2518
+ ...packageSource.registry ? {
2519
+ registry: packageSource.registry
2520
+ } : {},
2521
+ ...aliases ? {
2522
+ aliases
2523
+ } : {}
2524
+ }
2525
+ };
2526
+ }
2527
+ function writeSingleAppPackageSourceEvidence(targetDir, packageSource, useWorkspaceProtocol) {
2528
+ const evidencePath = node_path.join(targetDir, '.modernjs', 'ultramodern-package-source.json');
2529
+ node_fs.mkdirSync(node_path.dirname(evidencePath), {
2530
+ recursive: true
2531
+ });
2532
+ node_fs.writeFileSync(evidencePath, `${JSON.stringify(createSingleAppPackageSourceEvidence(packageSource, useWorkspaceProtocol), null, 2)}\n`);
2533
+ }
2396
2534
  function isDirectoryEmpty(dirPath) {
2397
2535
  if (!node_fs.existsSync(dirPath)) return false;
2398
2536
  try {
@@ -2515,6 +2653,7 @@ async function main() {
2515
2653
  version: useWorkspaceProtocol ? 'workspace:*' : packageSource.modernPackageVersion,
2516
2654
  runtimeVersion: singleAppModernPackageSpecifier('@modern-js/runtime', packageSource, useWorkspaceProtocol),
2517
2655
  appToolsVersion: singleAppModernPackageSpecifier('@modern-js/app-tools', packageSource, useWorkspaceProtocol),
2656
+ adapterRstestVersion: singleAppModernPackageSpecifier('@modern-js/adapter-rstest', packageSource, useWorkspaceProtocol),
2518
2657
  tsconfigVersion: singleAppModernPackageSpecifier('@modern-js/tsconfig', packageSource, useWorkspaceProtocol),
2519
2658
  pluginTanstackVersion: singleAppModernPackageSpecifier('@modern-js/plugin-tanstack', packageSource, useWorkspaceProtocol),
2520
2659
  pluginBffVersion: singleAppModernPackageSpecifier('@modern-js/plugin-bff', packageSource, useWorkspaceProtocol),
@@ -2528,6 +2667,14 @@ async function main() {
2528
2667
  const targetPackageJson = node_path.join(targetDir, 'package.json');
2529
2668
  const packageJson = JSON.parse(node_fs.readFileSync(targetPackageJson, 'utf-8'));
2530
2669
  packageJson.name = generatedPackageName;
2670
+ packageJson.modernjs = {
2671
+ ...packageJson.modernjs ?? {},
2672
+ preset: 'presetUltramodern',
2673
+ packageSource: {
2674
+ strategy: useWorkspaceProtocol ? 'workspace' : 'install',
2675
+ config: './.modernjs/ultramodern-package-source.json'
2676
+ }
2677
+ };
2531
2678
  if (isSubproject) {
2532
2679
  delete packageJson['lint-staged'];
2533
2680
  delete packageJson['simple-git-hooks'];
@@ -2550,6 +2697,7 @@ async function main() {
2550
2697
  }
2551
2698
  node_fs.writeFileSync(targetPackageJson, `${JSON.stringify(packageJson, null, 2)}\n`);
2552
2699
  writeTemplateManifestEvidence(targetDir, templateManifest);
2700
+ writeSingleAppPackageSourceEvidence(targetDir, packageSource, useWorkspaceProtocol);
2553
2701
  const dim = '\x1b[2m\x1b[3m';
2554
2702
  const reset = '\x1b[0m';
2555
2703
  console.log(`${i18n.t(localeKeys.message.success)}\n`);
@@ -2566,6 +2714,7 @@ function copyTemplate(src, dest, options) {
2566
2714
  });
2567
2715
  const excludeInSubproject = [
2568
2716
  '.agents',
2717
+ '.github',
2569
2718
  '.gitignore.handlebars',
2570
2719
  'AGENTS.md',
2571
2720
  '.npmrc',
@@ -2595,6 +2744,7 @@ function copyTemplate(src, dest, options) {
2595
2744
  version: options.version,
2596
2745
  runtimeVersion: options.runtimeVersion,
2597
2746
  appToolsVersion: options.appToolsVersion,
2747
+ adapterRstestVersion: options.adapterRstestVersion,
2598
2748
  tsconfigVersion: options.tsconfigVersion,
2599
2749
  pluginTanstackVersion: options.pluginTanstackVersion,
2600
2750
  pluginBffVersion: options.pluginBffVersion,
@@ -2606,7 +2756,7 @@ function copyTemplate(src, dest, options) {
2606
2756
  useHonoBff: 'hono' === options.bffRuntime,
2607
2757
  bffRuntime: options.bffRuntime,
2608
2758
  enableTailwind: options.enableTailwind,
2609
- routerImportPath: 'tanstack' === options.routerFramework ? 'tanstack-router' : 'router'
2759
+ routerRuntimeImport: 'tanstack' === options.routerFramework ? '@modern-js/plugin-tanstack/runtime' : '@modern-js/runtime/router'
2610
2760
  });
2611
2761
  if (0 === rendered.trim().length) continue;
2612
2762
  destPath = destPath.replace(/\.handlebars$/, '');
package/package.json CHANGED
@@ -21,7 +21,7 @@
21
21
  "engines": {
22
22
  "node": ">=20"
23
23
  },
24
- "version": "3.2.0-ultramodern.12",
24
+ "version": "3.2.0-ultramodern.14",
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.8.0",
42
42
  "@typescript/native-preview": "7.0.0-dev.20260516.1",
43
43
  "tsx": "^4.22.0",
44
- "@modern-js/i18n-utils": "npm:@bleedingdev/modern-js-i18n-utils@3.2.0-ultramodern.12"
44
+ "@modern-js/i18n-utils": "npm:@bleedingdev/modern-js-i18n-utils@3.2.0-ultramodern.14"
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.12"
57
+ "frameworkVersion": "3.2.0-ultramodern.14"
58
58
  }
59
59
  }
@@ -21,7 +21,7 @@ jobs:
21
21
  cache: pnpm
22
22
 
23
23
  - name: Install Dependencies
24
- run: pnpm install
24
+ run: pnpm install --ignore-scripts
25
25
 
26
26
  - name: Validate Ultramodern Contract
27
27
  run: pnpm run ultramodern:check
@@ -14,6 +14,8 @@ This project is generated for Codex-first UltraModern.js work.
14
14
 
15
15
  Runtime i18n is enabled by default. Agents must put user-visible UI copy in `config/public/locales/<lang>/translation.json` and render it through `react-i18next` or `@modern-js/plugin-i18n/runtime`. Do not add hardcoded JSX text, `aria-label`, `title`, `alt`, or `placeholder` strings unless the value is a non-translatable technical token.
16
16
 
17
+ Routes are locale-prefixed by default through `localePathRedirect: true`. Keep localized pages under `src/routes/[lang]`, use links for language switching, and preserve canonical plus `hreflang` metadata. Production builds fail unless `MODERN_PUBLIC_SITE_URL` is set, so deployed canonical URLs always use the production origin.
18
+
17
19
  ## Private Skills
18
20
 
19
21
  Private orchestration skills are not vendored into this template. If you are authorized for `TechsioCZ/skills`, run:
@@ -12,6 +12,18 @@ const enableTelemetryExporters =
12
12
  process.env['MODERN_BASELINE_ENABLE_TELEMETRY_EXPORTERS'] !== 'false';
13
13
  const telemetryFailLoudStartup = process.env['MODERN_TELEMETRY_FAIL_LOUD_STARTUP'] !== 'false';
14
14
  const otlpEndpoint = process.env['MODERN_TELEMETRY_OTLP_ENDPOINT'];
15
+ const configuredSiteUrl = process.env['MODERN_PUBLIC_SITE_URL'];
16
+ const hasConfiguredSiteUrl = typeof configuredSiteUrl === 'string' && configuredSiteUrl.length > 0;
17
+ const isProductionBuild =
18
+ process.env['NODE_ENV'] === 'production' || process.argv.includes('build');
19
+
20
+ if (isProductionBuild && !hasConfiguredSiteUrl) {
21
+ throw new Error(
22
+ 'MODERN_PUBLIC_SITE_URL must be set for production builds so canonical and hreflang URLs use the deployed origin.',
23
+ );
24
+ }
25
+
26
+ const siteUrl = hasConfiguredSiteUrl ? configuredSiteUrl : 'http://localhost:8080';
15
27
  const victoriaMetricsEndpoint = process.env['MODERN_TELEMETRY_VICTORIA_ENDPOINT'];
16
28
 
17
29
  // https://bleedingdev.github.io/ultramodern.js/configure/app/usage.html
@@ -32,6 +44,7 @@ export default defineConfig(
32
44
  localeDetection: {
33
45
  fallbackLanguage: 'en',
34
46
  languages: ['en', 'cs'],
47
+ localePathRedirect: true,
35
48
  },
36
49
  }),
37
50
  {{#if isTanstackRouter}}
@@ -39,6 +52,11 @@ export default defineConfig(
39
52
  {{/if}}{{#if enableBff}}
40
53
  bffPlugin(),
41
54
  {{/if}} ],
55
+ source: {
56
+ globalVars: {
57
+ ULTRAMODERN_SITE_URL: siteUrl,
58
+ },
59
+ },
42
60
  },
43
61
  {
44
62
  appId,
@@ -1,17 +1,22 @@
1
1
  {
2
2
  "name": "{{packageName}}",
3
3
  "version": "0.1.0",
4
+ "private": true,
4
5
  "type": "module",
6
+ "packageManager": "pnpm@11.1.2",
5
7
  "scripts": {
6
8
  "reset": "npx rimraf node_modules ./**/node_modules",
7
9
  "dev": "modern dev",
8
10
  "build": "modern build",
9
11
  "serve": "modern serve",
12
+ "test": "rstest run",
10
13
  "typecheck": "node -e \"const fs = require('node:fs'); const { execFileSync, spawnSync } = require('node:child_process'); const bin = execFileSync('effect-tsgo', ['get-exe-path'], { encoding: 'utf8' }).trim(); if (process.platform !== 'win32') fs.chmodSync(bin, 0o755); const result = spawnSync(bin, ['--noEmit', '-p', 'tsconfig.json'], { stdio: 'inherit' }); process.exit(result.status ?? 1);\"",
11
14
  "i18n:check": "node ./scripts/check-i18n-strings.mjs",
15
+ {{#unless isSubproject}}
12
16
  "skills:install": "node ./scripts/bootstrap-agent-skills.mjs",
13
17
  "skills:check": "node ./scripts/bootstrap-agent-skills.mjs --check",
14
- "ultramodern:check": "{{#unless isSubproject}}pnpm format:check && pnpm lint && {{/unless}}pnpm typecheck && pnpm i18n:check{{#unless isSubproject}} && pnpm skills:check{{/unless}} && node ./scripts/validate-ultramodern.mjs"{{#unless isSubproject}},
18
+ {{/unless}}
19
+ "ultramodern:check": "{{#unless isSubproject}}pnpm format:check && pnpm lint && {{/unless}}pnpm typecheck && pnpm i18n:check && pnpm test{{#unless isSubproject}} && pnpm skills:check{{/unless}} && node ./scripts/validate-ultramodern.mjs"{{#unless isSubproject}},
15
20
  "format": "oxfmt .",
16
21
  "format:check": "oxfmt --check .",
17
22
  "lint": "oxlint .",
@@ -32,16 +37,20 @@
32
37
  },
33
38
  "devDependencies": {
34
39
  "@effect/tsgo": "0.7.3",
40
+ "@modern-js/adapter-rstest": "{{adapterRstestVersion}}",
35
41
  "@modern-js/app-tools": "{{appToolsVersion}}",
36
42
  {{#if enableBff}} "@modern-js/plugin-bff": "{{pluginBffVersion}}",
37
43
  {{/if}} "@modern-js/tsconfig": "{{tsconfigVersion}}",
44
+ "@rstest/core": "0.10.0",
38
45
  {{#if enableTailwind}}
39
46
  "@tailwindcss/postcss": "^4.1.18",
40
47
  {{/if}}
48
+ "@testing-library/jest-dom": "^6.9.1",
41
49
  "@types/node": "^20",
42
50
  "@types/react": "^19.1.8",
43
51
  "@types/react-dom": "^19.1.6",
44
52
  "@typescript/native-preview": "7.0.0-dev.20260518.1",
53
+ "happy-dom": "^20.8.9",
45
54
  {{#unless isSubproject}}
46
55
  "lint-staged": "~15.4.0",
47
56
  "oxfmt": "0.50.0",
@@ -62,6 +71,15 @@
62
71
  ]
63
72
  }{{/unless}},
64
73
  "engines": {
65
- "node": ">=20"
74
+ "node": ">=20",
75
+ "pnpm": ">=11.0.0"
76
+ },
77
+ "pnpm": {
78
+ "onlyBuiltDependencies": [
79
+ "@swc/core",
80
+ "core-js",
81
+ "esbuild",
82
+ "msgpackr-extract"
83
+ ]
66
84
  }
67
85
  }
@@ -0,0 +1,8 @@
1
+ import { withModernConfig } from '@modern-js/adapter-rstest';
2
+ import { defineConfig } from '@rstest/core';
3
+
4
+ export default defineConfig({
5
+ extends: withModernConfig(),
6
+ testEnvironment: 'happy-dom',
7
+ setupFiles: ['./tests/rstest.setup.ts'],
8
+ });
@@ -3,6 +3,11 @@ import path from 'node:path';
3
3
 
4
4
  const configPath = path.resolve(process.cwd(), 'modern.config.ts');
5
5
  const templateManifestPath = path.resolve(process.cwd(), '.modernjs/mv-template-manifest.json');
6
+ const packageSourcePath = path.resolve(
7
+ process.cwd(),
8
+ '.modernjs/ultramodern-package-source.json',
9
+ );
10
+ const isSubproject = {{isSubproject}};
6
11
 
7
12
  if (!fs.existsSync(configPath)) {
8
13
  console.error('modern.config.ts not found');
@@ -17,6 +22,10 @@ const requiredTokens = [
17
22
  'enableBffRequestId',
18
23
  'enableTelemetryExporters',
19
24
  'i18nPlugin(',
25
+ 'localePathRedirect: true',
26
+ 'ULTRAMODERN_SITE_URL',
27
+ 'MODERN_PUBLIC_SITE_URL must be set for production builds',
28
+ 'globalVars',
20
29
  ];
21
30
  const missing = requiredTokens.filter((token) => !content.includes(token));
22
31
 
@@ -33,7 +42,6 @@ if (!fs.existsSync(templateManifestPath)) {
33
42
  const templateManifest = JSON.parse(fs.readFileSync(templateManifestPath, 'utf-8'));
34
43
  const requiredDeniedPaths = [
35
44
  '.git/**',
36
- '.github/**',
37
45
  '.npmrc',
38
46
  '.yarnrc',
39
47
  '.env',
@@ -44,17 +52,28 @@ const requiredDeniedPaths = [
44
52
  const requiredPostMaterialization = [
45
53
  'ultramodern-contract-check',
46
54
  'dependency-install-with-lifecycle-deny',
55
+ 'package-source-retained',
56
+ 'rstest-smoke-tests',
47
57
  'template-manifest-retained',
48
58
  ];
49
59
  const requiredPaths = [
60
+ {{#unless isSubproject}}
50
61
  'AGENTS.md',
51
62
  '.agents/skills-lock.json',
63
+ '.github/workflows/ultramodern-gates.yml',
52
64
  'oxlint.config.ts',
53
65
  'oxfmt.config.ts',
54
66
  'scripts/bootstrap-agent-skills.mjs',
67
+ {{/unless}}
68
+ '.modernjs/ultramodern-package-source.json',
69
+ 'rstest.config.mts',
55
70
  'scripts/check-i18n-strings.mjs',
56
71
  'config/public/locales/en/translation.json',
57
72
  'config/public/locales/cs/translation.json',
73
+ 'src/modern-app-env.d.ts',
74
+ 'src/routes/[lang]/page.tsx',
75
+ 'tests/rstest.setup.ts',
76
+ 'tests/ultramodern.contract.test.ts',
58
77
  ];
59
78
  const manifestErrors = [];
60
79
 
@@ -65,6 +84,38 @@ for (const requiredPath of requiredPaths) {
65
84
  }
66
85
  }
67
86
 
87
+ if (fs.existsSync(path.resolve(process.cwd(), 'src/routes/page.tsx'))) {
88
+ console.error('src/routes/page.tsx must move under src/routes/[lang]/page.tsx');
89
+ process.exit(1);
90
+ }
91
+
92
+ const pageContent = fs.readFileSync(
93
+ path.resolve(process.cwd(), 'src/routes/[lang]/page.tsx'),
94
+ 'utf-8',
95
+ );
96
+ for (const token of [
97
+ 'rel="canonical"',
98
+ 'rel="alternate"',
99
+ 'hrefLang="x-default"',
100
+ 'localizedPath(',
101
+ '<a',
102
+ {{#if isTanstackRouter}}
103
+ "@modern-js/plugin-tanstack/runtime",
104
+ {{/if}}
105
+ ]) {
106
+ if (!pageContent.includes(token)) {
107
+ console.error(`Localized route is missing ${token}`);
108
+ process.exit(1);
109
+ }
110
+ }
111
+ {{#if isTanstackRouter}}
112
+ const deprecatedTanstackRuntime = '@modern-js/runtime/' + 'tanstack-router';
113
+ if (pageContent.includes(deprecatedTanstackRuntime)) {
114
+ console.error('Localized route must import TanStack runtime from @modern-js/plugin-tanstack/runtime');
115
+ process.exit(1);
116
+ }
117
+ {{/if}}
118
+
68
119
  if (templateManifest.schemaVersion !== 1) {
69
120
  manifestErrors.push('schemaVersion');
70
121
  }
@@ -111,22 +162,32 @@ if (manifestErrors.length > 0) {
111
162
  const packageJson = JSON.parse(
112
163
  fs.readFileSync(path.resolve(process.cwd(), 'package.json'), 'utf-8'),
113
164
  );
165
+ const packageSource = JSON.parse(fs.readFileSync(packageSourcePath, 'utf-8'));
114
166
  const unresolvedTemplateMarker = String.fromCodePoint(123, 123);
115
167
  if (JSON.stringify(packageJson).includes(unresolvedTemplateMarker)) {
116
168
  console.error('package.json contains unresolved template markers');
117
169
  process.exit(1);
118
170
  }
171
+ if (JSON.stringify(packageSource).includes(unresolvedTemplateMarker)) {
172
+ console.error('package source metadata contains unresolved template markers');
173
+ process.exit(1);
174
+ }
175
+ {{#unless isSubproject}}
119
176
  const skillsLock = JSON.parse(
120
177
  fs.readFileSync(path.resolve(process.cwd(), '.agents/skills-lock.json'), 'utf-8'),
121
178
  );
179
+ {{/unless}}
122
180
  const requiredScripts = {
181
+ 'i18n:check': 'node ./scripts/check-i18n-strings.mjs',
182
+ test: 'rstest run',
183
+ {{#unless isSubproject}}
123
184
  format: 'oxfmt .',
124
185
  'format:check': 'oxfmt --check .',
125
- 'i18n:check': 'node ./scripts/check-i18n-strings.mjs',
126
186
  lint: 'oxlint .',
127
187
  'lint:fix': 'oxlint . --fix',
128
188
  'skills:check': 'node ./scripts/bootstrap-agent-skills.mjs --check',
129
189
  'skills:install': 'node ./scripts/bootstrap-agent-skills.mjs',
190
+ {{/unless}}
130
191
  };
131
192
 
132
193
  for (const [scriptName, scriptCommand] of Object.entries(requiredScripts)) {
@@ -144,6 +205,112 @@ if (
144
205
  process.exit(1);
145
206
  }
146
207
 
208
+ if (!packageJson.scripts?.['ultramodern:check']?.includes('pnpm test')) {
209
+ console.error('ultramodern:check must run the generated Rstest suite');
210
+ process.exit(1);
211
+ }
212
+
213
+ if (packageJson.private !== true) {
214
+ console.error('Generated app package must be private by default');
215
+ process.exit(1);
216
+ }
217
+
218
+ if (packageJson.packageManager !== 'pnpm@11.1.2') {
219
+ console.error('Generated app package must pin pnpm@11.1.2');
220
+ process.exit(1);
221
+ }
222
+
223
+ if (packageJson.engines?.pnpm !== '>=11.0.0') {
224
+ console.error('Generated app package must require pnpm >=11.0.0');
225
+ process.exit(1);
226
+ }
227
+
228
+ if (packageJson.modernjs?.preset !== 'presetUltramodern') {
229
+ console.error('package.json must declare presetUltramodern metadata');
230
+ process.exit(1);
231
+ }
232
+
233
+ if (
234
+ packageJson.modernjs?.packageSource?.config !== './.modernjs/ultramodern-package-source.json'
235
+ ) {
236
+ console.error('package.json must retain package source metadata location');
237
+ process.exit(1);
238
+ }
239
+
240
+ if (packageSource.schemaVersion !== 1) {
241
+ console.error('Package source metadata must use schemaVersion 1');
242
+ process.exit(1);
243
+ }
244
+
245
+ if (packageSource.preset !== 'presetUltramodern') {
246
+ console.error('Package source metadata must declare presetUltramodern');
247
+ process.exit(1);
248
+ }
249
+
250
+ if (packageSource.strategy !== 'workspace' && packageSource.strategy !== 'install') {
251
+ console.error('Package source strategy must be workspace or install');
252
+ process.exit(1);
253
+ }
254
+
255
+ const expectedModernPackages = [
256
+ '@modern-js/runtime',
257
+ '@modern-js/app-tools',
258
+ '@modern-js/tsconfig',
259
+ '@modern-js/plugin-i18n',
260
+ '@modern-js/plugin-tanstack',
261
+ '@modern-js/plugin-bff',
262
+ '@modern-js/adapter-rstest',
263
+ ];
264
+
265
+ for (const packageName of expectedModernPackages) {
266
+ if (!packageSource.modernPackages?.packages?.includes(packageName)) {
267
+ console.error(`Package source metadata must include ${packageName}`);
268
+ process.exit(1);
269
+ }
270
+ }
271
+
272
+ const expectedModernSpecifier = packageSource.modernPackages?.specifier;
273
+ if (typeof expectedModernSpecifier !== 'string' || expectedModernSpecifier.length === 0) {
274
+ console.error('Package source metadata must provide a Modern package specifier');
275
+ process.exit(1);
276
+ }
277
+
278
+ const expectedModernDependency = (packageName) => {
279
+ const alias = packageSource.modernPackages?.aliases?.[packageName];
280
+ return typeof alias === 'string'
281
+ ? `npm:${alias}@${expectedModernSpecifier}`
282
+ : expectedModernSpecifier;
283
+ };
284
+
285
+ for (const packageName of [
286
+ '@modern-js/runtime',
287
+ '@modern-js/plugin-i18n',
288
+ '@modern-js/plugin-tanstack',
289
+ ]) {
290
+ if (
291
+ packageJson.dependencies?.[packageName] &&
292
+ packageJson.dependencies[packageName] !== expectedModernDependency(packageName)
293
+ ) {
294
+ console.error(`Dependency ${packageName} must match package source metadata`);
295
+ process.exit(1);
296
+ }
297
+ }
298
+
299
+ for (const packageName of [
300
+ '@modern-js/app-tools',
301
+ '@modern-js/adapter-rstest',
302
+ '@modern-js/tsconfig',
303
+ '@modern-js/plugin-bff',
304
+ ]) {
305
+ if (
306
+ packageJson.devDependencies?.[packageName] &&
307
+ packageJson.devDependencies[packageName] !== expectedModernDependency(packageName)
308
+ ) {
309
+ console.error(`Dev dependency ${packageName} must match package source metadata`);
310
+ process.exit(1);
311
+ }
312
+ }
313
+
147
314
  for (const dependency of ['@modern-js/plugin-i18n', 'i18next', 'react-i18next']) {
148
315
  if (!packageJson.dependencies?.[dependency]) {
149
316
  console.error(`Missing dependency: ${dependency}`);
@@ -153,10 +320,16 @@ for (const dependency of ['@modern-js/plugin-i18n', 'i18next', 'react-i18next'])
153
320
 
154
321
  for (const dependency of [
155
322
  '@effect/tsgo',
323
+ '@modern-js/adapter-rstest',
324
+ '@rstest/core',
325
+ '@testing-library/jest-dom',
156
326
  '@typescript/native-preview',
327
+ 'happy-dom',
328
+ {{#unless isSubproject}}
157
329
  'oxlint',
158
330
  'oxfmt',
159
331
  'ultracite',
332
+ {{/unless}}
160
333
  ]) {
161
334
  if (!packageJson.devDependencies?.[dependency]) {
162
335
  console.error(`Missing devDependency: ${dependency}`);
@@ -164,6 +337,7 @@ for (const dependency of [
164
337
  }
165
338
  }
166
339
 
340
+ {{#unless isSubproject}}
167
341
  const privateSource = skillsLock.sources?.find(
168
342
  (source) => source.repository === 'https://github.com/TechsioCZ/skills',
169
343
  );
@@ -174,5 +348,6 @@ for (const skillName of ['plan-graph', 'dag', 'subagent-graph', 'helm', 'debugge
174
348
  process.exit(1);
175
349
  }
176
350
  }
351
+ {{/unless}}
177
352
 
178
353
  console.log('Ultramodern contract check passed.');
@@ -1 +1,3 @@
1
1
  /// <reference types='@modern-js/app-tools/types' />
2
+
3
+ declare const ULTRAMODERN_SITE_URL: string;
@@ -1,19 +1,62 @@
1
1
  import { Helmet } from '@modern-js/runtime/head';
2
2
  import { useModernI18n } from '@modern-js/plugin-i18n/runtime';
3
+ import { useLocation } from '{{routerRuntimeImport}}';
3
4
  {{#if useEffectBff}}import effectBff from '@api/effect/index';
4
5
  import { Effect } from '@modern-js/plugin-bff/effect-client';
5
6
  import { useEffect, useState } from 'react';
6
7
  {{/if}}
7
8
  import { useTranslation } from 'react-i18next';
8
- import './index.css';
9
+ import '../index.css';
10
+
11
+ const fallbackLanguage = 'en';
12
+ const supportedLanguages = ['en', 'cs'] as const;
13
+ type SupportedLanguage = (typeof supportedLanguages)[number];
14
+
15
+ const isSupportedLanguage = (value: string): value is SupportedLanguage =>
16
+ supportedLanguages.includes(value as SupportedLanguage);
17
+
18
+ const stripLanguagePrefix = (pathname: string) => {
19
+ const segments = pathname.split('/').filter(Boolean);
20
+ if (segments.length > 0 && isSupportedLanguage(segments[0] ?? '')) {
21
+ segments.shift();
22
+ }
23
+ return `/${segments.join('/')}`;
24
+ };
25
+
26
+ const localizedPath = (pathname: string, language: SupportedLanguage) => {
27
+ const pathWithoutLanguage = stripLanguagePrefix(pathname);
28
+ return pathWithoutLanguage === '/' ? `/${language}` : `/${language}${pathWithoutLanguage}`;
29
+ };
30
+
31
+ const absoluteUrl = (pathname: string) => {
32
+ const origin = ULTRAMODERN_SITE_URL.replace(/\/+$/u, '');
33
+ return `${origin}${pathname}`;
34
+ };
35
+
36
+ const locationSuffix = (location: { hash?: unknown; search?: unknown; searchStr?: unknown }) => {
37
+ const { hash, search, searchStr } = location;
38
+ let locationSearch = '';
39
+ if (typeof searchStr === 'string') {
40
+ locationSearch = searchStr;
41
+ } else if (typeof search === 'string') {
42
+ locationSearch = search;
43
+ }
44
+ const locationHash = typeof hash === 'string' ? hash : '';
45
+ return `${locationSearch}${locationHash}`;
46
+ };
9
47
 
10
48
  const Index = () => {
11
49
  const { t } = useTranslation();
12
- const { changeLanguage, language } = useModernI18n();
13
- const languageOptions = [
14
- { code: 'en', label: t('home.language.en') },
15
- { code: 'cs', label: t('home.language.cs') },
16
- ];
50
+ const { language } = useModernI18n();
51
+ const location = useLocation();
52
+ const currentLanguage = isSupportedLanguage(language) ? language : fallbackLanguage;
53
+ const canonicalPath = localizedPath(location.pathname, currentLanguage);
54
+ const suffix = locationSuffix(location);
55
+ const languageOptions = supportedLanguages.map((code) => ({
56
+ code,
57
+ href: `${localizedPath(location.pathname, code)}${suffix}`,
58
+ label: t(`home.language.${code}`),
59
+ }));
17
60
  {{#if useEffectBff}} const [effectMessage, setEffectMessage] = useState('loading...');
18
61
 
19
62
  useEffect(() => {
@@ -42,18 +85,31 @@ const Index = () => {
42
85
  type="image/x-icon"
43
86
  href="https://lf3-static.bytednsdoc.com/obj/eden-cn/uhbfnupenuhf/favicon.ico"
44
87
  />
88
+ <link rel="canonical" href={absoluteUrl(canonicalPath)} />
89
+ {supportedLanguages.map((code) => (
90
+ <link
91
+ href={absoluteUrl(localizedPath(location.pathname, code))}
92
+ hrefLang={code}
93
+ key={code}
94
+ rel="alternate"
95
+ />
96
+ ))}
97
+ <link
98
+ href={absoluteUrl(localizedPath(location.pathname, fallbackLanguage))}
99
+ hrefLang="x-default"
100
+ rel="alternate"
101
+ />
45
102
  </Helmet>
46
103
  <main>
47
104
  <nav className="language-switcher" aria-label={t('home.language.switcher')}>
48
105
  {languageOptions.map((option) => (
49
- <button
50
- disabled={language === option.code}
106
+ <a
107
+ aria-current={currentLanguage === option.code ? 'page' : undefined}
108
+ href={option.href}
51
109
  key={option.code}
52
- onClick={() => void changeLanguage(option.code)}
53
- type="button"
54
110
  >
55
111
  {option.label}
56
- </button>
112
+ </a>
57
113
  ))}
58
114
  </nav>
59
115
  <div className="title">
@@ -1,4 +1,4 @@
1
- import { Outlet } from '@modern-js/runtime/{{routerImportPath}}';
1
+ import { Outlet } from '{{routerRuntimeImport}}';
2
2
 
3
3
  export default function Layout() {
4
4
  return (
@@ -0,0 +1,4 @@
1
+ import { expect } from '@rstest/core';
2
+ import * as jestDomMatchers from '@testing-library/jest-dom/matchers';
3
+
4
+ expect.extend(jestDomMatchers);
@@ -0,0 +1,7 @@
1
+ {
2
+ "extends": "../tsconfig.json",
3
+ "compilerOptions": {
4
+ "types": ["@testing-library/jest-dom", "@rstest/core/globals"]
5
+ },
6
+ "include": ["./"]
7
+ }
@@ -0,0 +1,67 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { describe, expect, test } from '@rstest/core';
4
+
5
+ const root = process.cwd();
6
+ const readText = (relativePath: string) =>
7
+ fs.readFileSync(path.join(root, relativePath), 'utf-8');
8
+ const readJson = <T>(relativePath: string): T =>
9
+ JSON.parse(readText(relativePath)) as T;
10
+
11
+ describe('generated UltraModern contract', () => {
12
+ test('keeps localized route metadata and Rstest wiring', () => {
13
+ expect(fs.existsSync(path.join(root, 'src/routes/[lang]/page.tsx'))).toBe(
14
+ true,
15
+ );
16
+ expect(fs.existsSync(path.join(root, 'src/routes/page.tsx'))).toBe(false);
17
+
18
+ const page = readText('src/routes/[lang]/page.tsx');
19
+ expect(page).toContain('rel="canonical"');
20
+ expect(page).toContain('rel="alternate"');
21
+ expect(page).toContain('hrefLang="x-default"');
22
+ expect(page).toContain('localizedPath(');
23
+
24
+ const config = readText('rstest.config.mts');
25
+ expect(config).toContain("withModernConfig()");
26
+ expect(config).toContain("testEnvironment: 'happy-dom'");
27
+ });
28
+
29
+ test('retains package-source metadata for generated Modern.js packages', () => {
30
+ const packageJson = readJson<{
31
+ dependencies?: Record<string, string>;
32
+ devDependencies?: Record<string, string>;
33
+ modernjs?: {
34
+ packageSource?: {
35
+ config?: string;
36
+ };
37
+ preset?: string;
38
+ };
39
+ }>('package.json');
40
+ const packageSource = readJson<{
41
+ modernPackages?: {
42
+ packages?: string[];
43
+ specifier?: string;
44
+ };
45
+ strategy?: string;
46
+ }>('.modernjs/ultramodern-package-source.json');
47
+
48
+ expect(packageJson.modernjs?.preset).toBe('presetUltramodern');
49
+ expect(packageJson.modernjs?.packageSource?.config).toBe(
50
+ './.modernjs/ultramodern-package-source.json',
51
+ );
52
+ expect(packageSource.strategy).toMatch(/^(workspace|install)$/u);
53
+ expect(packageSource.modernPackages?.packages).toContain(
54
+ '@modern-js/runtime',
55
+ );
56
+ expect(packageSource.modernPackages?.packages).toContain(
57
+ '@modern-js/app-tools',
58
+ );
59
+ expect(packageSource.modernPackages?.packages).toContain(
60
+ '@modern-js/adapter-rstest',
61
+ );
62
+ expect(packageSource.modernPackages?.specifier).toBeTruthy();
63
+ expect(
64
+ packageJson.devDependencies?.['@modern-js/adapter-rstest'],
65
+ ).toBeTruthy();
66
+ });
67
+ });
@@ -14,6 +14,8 @@ This workspace is generated as an agent-ready UltraModern.js SuperApp. Agents sh
14
14
 
15
15
  Runtime i18n is enabled by default for generated apps. Agents must put user-visible UI copy in each app's `config/public/locales/<lang>/translation.json` and render it through `react-i18next` or `@modern-js/plugin-i18n/runtime`. Do not add hardcoded JSX text, `aria-label`, `title`, `alt`, or `placeholder` strings unless the value is a non-translatable technical token.
16
16
 
17
+ Routes are locale-prefixed by default through `localePathRedirect: true`. Keep localized app pages under `src/routes/[lang]`, use links for language switching, and preserve canonical plus `hreflang` metadata. Production builds fail unless `MODERN_PUBLIC_SITE_URL` is set per deployed app, so canonical URLs always use the production origin.
18
+
17
19
  ## Required Skill Baseline
18
20
 
19
21
  Use these skills when the task touches the matching subsystem:
@@ -65,25 +65,33 @@ const requiredPaths = [
65
65
  'apps/shell-super-app/config/public/locales/cs/translation.json',
66
66
  'apps/shell-super-app/modern.config.ts',
67
67
  'apps/shell-super-app/module-federation.config.ts',
68
+ 'apps/shell-super-app/src/modern-app-env.d.ts',
68
69
  'apps/shell-super-app/src/modern.runtime.ts',
70
+ 'apps/shell-super-app/src/routes/[lang]/page.tsx',
69
71
  'apps/remotes/remote-commerce/package.json',
70
72
  'apps/remotes/remote-commerce/config/public/locales/en/translation.json',
71
73
  'apps/remotes/remote-commerce/config/public/locales/cs/translation.json',
72
74
  'apps/remotes/remote-commerce/modern.config.ts',
73
75
  'apps/remotes/remote-commerce/module-federation.config.ts',
76
+ 'apps/remotes/remote-commerce/src/modern-app-env.d.ts',
74
77
  'apps/remotes/remote-commerce/src/modern.runtime.ts',
78
+ 'apps/remotes/remote-commerce/src/routes/[lang]/page.tsx',
75
79
  'apps/remotes/remote-identity/package.json',
76
80
  'apps/remotes/remote-identity/config/public/locales/en/translation.json',
77
81
  'apps/remotes/remote-identity/config/public/locales/cs/translation.json',
78
82
  'apps/remotes/remote-identity/modern.config.ts',
79
83
  'apps/remotes/remote-identity/module-federation.config.ts',
84
+ 'apps/remotes/remote-identity/src/modern-app-env.d.ts',
80
85
  'apps/remotes/remote-identity/src/modern.runtime.ts',
86
+ 'apps/remotes/remote-identity/src/routes/[lang]/page.tsx',
81
87
  'apps/remotes/remote-design-system/package.json',
82
88
  'apps/remotes/remote-design-system/config/public/locales/en/translation.json',
83
89
  'apps/remotes/remote-design-system/config/public/locales/cs/translation.json',
84
90
  'apps/remotes/remote-design-system/modern.config.ts',
85
91
  'apps/remotes/remote-design-system/module-federation.config.ts',
92
+ 'apps/remotes/remote-design-system/src/modern-app-env.d.ts',
86
93
  'apps/remotes/remote-design-system/src/modern.runtime.ts',
94
+ 'apps/remotes/remote-design-system/src/routes/[lang]/page.tsx',
87
95
  'services/service-recommendations-effect/package.json',
88
96
  'services/service-recommendations-effect/modern.config.ts',
89
97
  'services/service-recommendations-effect/api/effect/index.ts',
@@ -97,9 +105,23 @@ for (const requiredPath of requiredPaths) {
97
105
  assertExists(requiredPath);
98
106
  }
99
107
 
108
+ for (const appDirectory of [
109
+ 'apps/shell-super-app',
110
+ 'apps/remotes/remote-commerce',
111
+ 'apps/remotes/remote-identity',
112
+ 'apps/remotes/remote-design-system',
113
+ ]) {
114
+ assert(
115
+ !fs.existsSync(path.join(root, appDirectory, 'src/routes/page.tsx')),
116
+ `${appDirectory} must use src/routes/[lang]/page.tsx`,
117
+ );
118
+ }
119
+
100
120
  const rootPackage = readJson('package.json');
101
121
  const packageSource = readJson('.modernjs/ultramodern-package-source.json');
102
122
  const skillsLock = readJson('.agents/skills-lock.json');
123
+ const pnpmWorkspace = readText('pnpm-workspace.yaml');
124
+ const deprecatedTanstackRuntime = '@modern-js/runtime/' + 'tanstack-router';
103
125
  const expectedModernSpecifier =
104
126
  packageSource.strategy === 'install' ? packageSource.modernPackages?.specifier : 'workspace:*';
105
127
 
@@ -118,6 +140,16 @@ assert(rootPackage.private === true, 'Root package must be private');
118
140
  assert(rootPackage.modernjs?.preset === 'presetUltramodern', 'Root must declare presetUltramodern');
119
141
  assert(rootPackage.packageManager === 'pnpm@11.1.2', 'Root must pin pnpm 11.1.2');
120
142
  assert(rootPackage.engines?.pnpm === '>=11.0.0', 'Root must require pnpm >=11');
143
+ for (const requiredSnippet of [
144
+ 'packages:\n - apps/*\n - apps/remotes/*\n - services/*\n - packages/*',
145
+ "allowBuilds:\n '@swc/core': true\n core-js: true\n esbuild: true\n msgpackr-extract: true",
146
+ "onlyBuiltDependencies:\n - '@swc/core'\n - core-js\n - esbuild\n - msgpackr-extract",
147
+ ]) {
148
+ assert(
149
+ pnpmWorkspace.includes(requiredSnippet),
150
+ `pnpm-workspace.yaml must retain ${requiredSnippet.split('\n')[0]}`,
151
+ );
152
+ }
121
153
  assert(
122
154
  rootPackage.modernjs?.packageSource?.config === './.modernjs/ultramodern-package-source.json',
123
155
  'Root must point to the UltraModern package source metadata',
@@ -286,6 +318,12 @@ for (const configPath of [
286
318
  const config = readText(configPath);
287
319
  assert(config.includes('presetUltramodern('), `${configPath} must use presetUltramodern`);
288
320
  assert(config.includes('i18nPlugin('), `${configPath} must enable plugin-i18n`);
321
+ assert(config.includes('localePathRedirect: true'), `${configPath} must prefix localized URLs`);
322
+ assert(config.includes('ULTRAMODERN_SITE_URL'), `${configPath} must expose site URL metadata`);
323
+ assert(
324
+ config.includes('MODERN_PUBLIC_SITE_URL must be set for production builds'),
325
+ `${configPath} must require MODERN_PUBLIC_SITE_URL for production builds`,
326
+ );
289
327
  assert(config.includes('tanstackRouterPlugin()'), `${configPath} must enable plugin-tanstack`);
290
328
  assert(
291
329
  config.includes('moduleFederationPlugin()'),
@@ -293,6 +331,27 @@ for (const configPath of [
293
331
  );
294
332
  }
295
333
 
334
+ for (const routePath of [
335
+ 'apps/shell-super-app/src/routes/[lang]/page.tsx',
336
+ 'apps/remotes/remote-commerce/src/routes/[lang]/page.tsx',
337
+ 'apps/remotes/remote-identity/src/routes/[lang]/page.tsx',
338
+ 'apps/remotes/remote-design-system/src/routes/[lang]/page.tsx',
339
+ ]) {
340
+ const route = readText(routePath);
341
+ assert(route.includes('rel="canonical"'), `${routePath} must emit canonical metadata`);
342
+ assert(route.includes('rel="alternate"'), `${routePath} must emit alternate locale metadata`);
343
+ assert(route.includes('hrefLang="x-default"'), `${routePath} must emit x-default metadata`);
344
+ assert(route.includes('localizedPath('), `${routePath} must build localized URLs`);
345
+ assert(
346
+ route.includes('@modern-js/plugin-tanstack/runtime'),
347
+ `${routePath} must import TanStack runtime from @modern-js/plugin-tanstack/runtime`,
348
+ );
349
+ assert(
350
+ !route.includes(deprecatedTanstackRuntime),
351
+ `${routePath} must not import deprecated TanStack runtime path`,
352
+ );
353
+ }
354
+
296
355
  const shellMf = readText('apps/shell-super-app/module-federation.config.ts');
297
356
  assert(shellMf.includes("name: 'shellSuperApp'"), 'Shell MF config must name the shell');
298
357
  assert(