@bleedingdev/modern-js-create 3.2.0-ultramodern.2 → 3.2.0-ultramodern.21

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.
Files changed (63) hide show
  1. package/README.md +12 -0
  2. package/dist/index.js +676 -198
  3. package/package.json +5 -2
  4. package/template/.agents/skills-lock.json +34 -0
  5. package/template/.github/renovate.json +53 -0
  6. package/template/.github/workflows/ultramodern-gates.yml.handlebars +26 -4
  7. package/template/AGENTS.md +27 -0
  8. package/template/README.md +7 -3
  9. package/template/api/effect/index.ts.handlebars +7 -45
  10. package/template/config/public/locales/cs/translation.json +39 -0
  11. package/template/config/public/locales/en/translation.json +39 -0
  12. package/template/modern.config.ts.handlebars +44 -23
  13. package/template/oxfmt.config.ts +8 -0
  14. package/template/oxlint.config.ts +12 -0
  15. package/template/package.json.handlebars +56 -27
  16. package/template/pnpm-workspace.yaml +24 -0
  17. package/template/rstest.config.mts +7 -0
  18. package/template/scripts/bootstrap-agent-skills.mjs +95 -0
  19. package/template/scripts/check-i18n-strings.mjs +83 -0
  20. package/template/scripts/validate-ultramodern.mjs.handlebars +350 -16
  21. package/template/shared/effect/api.ts.handlebars +1 -2
  22. package/template/src/modern-app-env.d.ts +2 -0
  23. package/template/src/modern.runtime.ts.handlebars +17 -3
  24. package/template/src/routes/[lang]/page.tsx.handlebars +211 -0
  25. package/template/src/routes/index.css.handlebars +14 -3
  26. package/template/src/routes/layout.tsx.handlebars +1 -1
  27. package/template/tests/tsconfig.json +7 -0
  28. package/template/tests/ultramodern.contract.test.ts +67 -0
  29. package/template/tsconfig.json +106 -2
  30. package/template-workspace/.agents/agent-reference-repos.json +24 -0
  31. package/template-workspace/.agents/rstackjs-agent-skills-LICENSE +21 -0
  32. package/template-workspace/.agents/skills/rsbuild-best-practices/SKILL.md +57 -0
  33. package/template-workspace/.agents/skills/rsdoctor-analysis/SKILL.md +96 -0
  34. package/template-workspace/.agents/skills/rsdoctor-analysis/references/command-map.md +113 -0
  35. package/template-workspace/.agents/skills/rsdoctor-analysis/references/common-analysis-patterns.md +190 -0
  36. package/template-workspace/.agents/skills/rsdoctor-analysis/references/install-rsdoctor-common.md +88 -0
  37. package/template-workspace/.agents/skills/rsdoctor-analysis/references/install-rsdoctor-rspack.md +138 -0
  38. package/template-workspace/.agents/skills/rsdoctor-analysis/references/install-rsdoctor-webpack.md +71 -0
  39. package/template-workspace/.agents/skills/rsdoctor-analysis/references/install-rsdoctor.md +39 -0
  40. package/template-workspace/.agents/skills/rsdoctor-analysis/references/rsdoctor-data-types.md +103 -0
  41. package/template-workspace/.agents/skills/rslib-best-practices/SKILL.md +58 -0
  42. package/template-workspace/.agents/skills/rslib-modern-package/SKILL.md +173 -0
  43. package/template-workspace/.agents/skills/rspack-best-practices/SKILL.md +70 -0
  44. package/template-workspace/.agents/skills/rspack-tracing/SKILL.md +75 -0
  45. package/template-workspace/.agents/skills/rspack-tracing/references/bottlenecks.md +47 -0
  46. package/template-workspace/.agents/skills/rspack-tracing/references/tracing-guide.md +38 -0
  47. package/template-workspace/.agents/skills/rspack-tracing/scripts/analyze_trace.js +184 -0
  48. package/template-workspace/.agents/skills/rstest-best-practices/SKILL.md +133 -0
  49. package/template-workspace/.agents/skills-lock.json +95 -0
  50. package/template-workspace/.github/renovate.json +29 -0
  51. package/template-workspace/.github/workflows/ultramodern-workspace-gates.yml.handlebars +52 -0
  52. package/template-workspace/.gitignore.handlebars +5 -0
  53. package/template-workspace/AGENTS.md +61 -0
  54. package/template-workspace/README.md.handlebars +11 -1
  55. package/template-workspace/oxfmt.config.ts +16 -0
  56. package/template-workspace/oxlint.config.ts +19 -0
  57. package/template-workspace/pnpm-workspace.yaml +24 -6
  58. package/template-workspace/scripts/bootstrap-agent-skills.mjs +95 -0
  59. package/template-workspace/scripts/check-i18n-strings.mjs +83 -0
  60. package/template-workspace/scripts/setup-agent-reference-repos.mjs +364 -0
  61. package/template-workspace/scripts/validate-ultramodern-workspace.mjs.handlebars +405 -59
  62. package/template/biome.json +0 -41
  63. package/template/src/routes/page.tsx.handlebars +0 -119
@@ -0,0 +1,95 @@
1
+ import { execFileSync } from 'node:child_process';
2
+ import fs from 'node:fs';
3
+ import os from 'node:os';
4
+ import path from 'node:path';
5
+
6
+ const root = process.cwd();
7
+ const lockPath = path.join(root, '.agents/skills-lock.json');
8
+ const checkOnly = process.argv.includes('--check');
9
+ const force = process.argv.includes('--force');
10
+
11
+ const readJson = (filePath) => JSON.parse(fs.readFileSync(filePath, 'utf-8'));
12
+
13
+ const run = (command, args, options = {}) =>
14
+ execFileSync(command, args, {
15
+ cwd: options.cwd ?? root,
16
+ encoding: 'utf-8',
17
+ stdio: options.stdio ?? ['ignore', 'pipe', 'pipe'],
18
+ });
19
+
20
+ const cloneSource = (source, targetDir) => {
21
+ const repo = source.repository.replace(/^https:\/\/github.com\//u, '');
22
+ try {
23
+ run('gh', ['repo', 'clone', repo, targetDir, '--', '--depth', '1'], {
24
+ stdio: 'inherit',
25
+ });
26
+ } catch {
27
+ run('git', ['clone', '--depth', '1', source.repository, targetDir], {
28
+ stdio: 'inherit',
29
+ });
30
+ }
31
+ };
32
+
33
+ const resolveSkillDir = (sourceRoot, skillName) => {
34
+ const candidates = [
35
+ path.join(sourceRoot, skillName),
36
+ path.join(sourceRoot, 'skills', skillName),
37
+ path.join(sourceRoot, 'skills', 'engineering', skillName),
38
+ path.join(sourceRoot, 'skills', 'productivity', skillName),
39
+ ];
40
+ return candidates.find((candidate) => fs.existsSync(path.join(candidate, 'SKILL.md')));
41
+ };
42
+
43
+ if (!fs.existsSync(lockPath)) {
44
+ console.error('Missing .agents/skills-lock.json');
45
+ process.exit(1);
46
+ }
47
+
48
+ const lock = readJson(lockPath);
49
+ const installDir = path.join(root, lock.installDir ?? '.agents/skills');
50
+ const privateSources = (lock.sources ?? []).filter(
51
+ (source) => source.install === 'clone-if-authorized',
52
+ );
53
+
54
+ if (checkOnly) {
55
+ const missing = privateSources.flatMap((source) =>
56
+ (source.baseline ?? [])
57
+ .map((skill) => skill.name)
58
+ .filter((skillName) => !fs.existsSync(path.join(installDir, skillName, 'SKILL.md'))),
59
+ );
60
+ if (missing.length > 0) {
61
+ console.warn(
62
+ `Private skills not installed: ${missing.join(', ')}. Run pnpm skills:install if you have access.`,
63
+ );
64
+ } else {
65
+ console.log('Agent skills are installed.');
66
+ }
67
+ process.exit(0);
68
+ }
69
+
70
+ fs.mkdirSync(installDir, { recursive: true });
71
+
72
+ for (const source of privateSources) {
73
+ const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'ultramodern-skills-'));
74
+ try {
75
+ cloneSource(source, tempDir);
76
+ for (const skill of source.baseline ?? []) {
77
+ const sourceSkillDir = resolveSkillDir(tempDir, skill.name);
78
+ if (!sourceSkillDir) {
79
+ throw new Error(`Skill ${skill.name} not found in ${source.repository}`);
80
+ }
81
+ const targetSkillDir = path.join(installDir, skill.name);
82
+ if (fs.existsSync(targetSkillDir)) {
83
+ if (!force) {
84
+ console.log(`Skipping existing ${skill.name}`);
85
+ continue;
86
+ }
87
+ fs.rmSync(targetSkillDir, { force: true, recursive: true });
88
+ }
89
+ fs.cpSync(sourceSkillDir, targetSkillDir, { recursive: true });
90
+ console.log(`Installed ${skill.name}`);
91
+ }
92
+ } finally {
93
+ fs.rmSync(tempDir, { force: true, recursive: true });
94
+ }
95
+ }
@@ -0,0 +1,83 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+
4
+ const root = process.cwd();
5
+ const scanRoots = ['src'].map((scanRoot) => path.join(root, scanRoot));
6
+ const ignoredDirectories = new Set(['.modern', '.modernjs', 'dist', 'node_modules']);
7
+ const visibleAttributePattern =
8
+ /\s(?:aria-label|alt|placeholder|title)=["']([^"']*[A-Za-z][^"']*)["']/gu;
9
+ const jsxTextPattern = />([^<>{}]*[A-Za-z][^<>{}]*)</gu;
10
+
11
+ const collectFiles = (directory) => {
12
+ if (!fs.existsSync(directory)) {
13
+ return [];
14
+ }
15
+
16
+ const files = [];
17
+ for (const entry of fs.readdirSync(directory, { withFileTypes: true })) {
18
+ if (entry.isDirectory()) {
19
+ if (!ignoredDirectories.has(entry.name)) {
20
+ files.push(...collectFiles(path.join(directory, entry.name)));
21
+ }
22
+ continue;
23
+ }
24
+
25
+ if (entry.isFile() && /\.(jsx|tsx)$/u.test(entry.name) && !entry.name.endsWith('.d.ts')) {
26
+ files.push(path.join(directory, entry.name));
27
+ }
28
+ }
29
+ return files;
30
+ };
31
+
32
+ const lineNumberForIndex = (content, index) => content.slice(0, index).split('\n').length;
33
+ const isIgnoredLine = (content, index) => {
34
+ const lineStart = content.lastIndexOf('\n', index) + 1;
35
+ const lineEnd = content.indexOf('\n', index);
36
+ const currentLineEnd = lineEnd === -1 ? content.length : lineEnd;
37
+ const previousLineStart = content.lastIndexOf('\n', Math.max(0, lineStart - 2)) + 1;
38
+ const nextLineEnd = content.indexOf('\n', currentLineEnd + 1);
39
+ const context = content.slice(
40
+ previousLineStart,
41
+ nextLineEnd === -1 ? content.length : nextLineEnd,
42
+ );
43
+ return /i18n-ignore/u.test(context);
44
+ };
45
+
46
+ const violations = [];
47
+ for (const filePath of scanRoots.flatMap(collectFiles)) {
48
+ const content = fs.readFileSync(filePath, 'utf-8');
49
+ for (const match of content.matchAll(visibleAttributePattern)) {
50
+ if (!isIgnoredLine(content, match.index ?? 0)) {
51
+ violations.push({
52
+ filePath,
53
+ line: lineNumberForIndex(content, match.index ?? 0),
54
+ text: match[1].trim(),
55
+ });
56
+ }
57
+ }
58
+
59
+ for (const match of content.matchAll(jsxTextPattern)) {
60
+ const text = match[1].replaceAll(/\s+/gu, ' ').trim();
61
+ if (text && !isIgnoredLine(content, match.index ?? 0)) {
62
+ violations.push({
63
+ filePath,
64
+ line: lineNumberForIndex(content, match.index ?? 0),
65
+ text,
66
+ });
67
+ }
68
+ }
69
+ }
70
+
71
+ if (violations.length > 0) {
72
+ console.error('Hardcoded user-visible JSX strings found. Move copy to locale JSON files.');
73
+ for (const violation of violations) {
74
+ console.error(
75
+ `${path.relative(root, violation.filePath)}:${violation.line} ${JSON.stringify(
76
+ violation.text,
77
+ )}`,
78
+ );
79
+ }
80
+ process.exit(1);
81
+ }
82
+
83
+ console.log('No hardcoded user-visible JSX strings found.');
@@ -2,30 +2,35 @@ import fs from 'node:fs';
2
2
  import path from 'node:path';
3
3
 
4
4
  const configPath = path.resolve(process.cwd(), 'modern.config.ts');
5
- const templateManifestPath = path.resolve(
5
+ const templateManifestPath = path.resolve(process.cwd(), '.modernjs/mv-template-manifest.json');
6
+ const packageSourcePath = path.resolve(
6
7
  process.cwd(),
7
- '.modernjs/mv-template-manifest.json',
8
+ '.modernjs/ultramodern-package-source.json',
8
9
  );
10
+ const isSubproject = {{isSubproject}};
9
11
 
10
12
  if (!fs.existsSync(configPath)) {
11
13
  console.error('modern.config.ts not found');
12
14
  process.exit(1);
13
15
  }
14
16
 
15
- const content = fs.readFileSync(configPath, 'utf8');
17
+ const content = fs.readFileSync(configPath, 'utf-8');
16
18
  const requiredTokens = [
17
19
  'presetUltramodern(',
18
20
  'appTools()',
19
21
  'enableModuleFederationSSR',
20
22
  'enableBffRequestId',
21
23
  'enableTelemetryExporters',
24
+ 'i18nPlugin(',
25
+ 'localePathRedirect: true',
26
+ 'ULTRAMODERN_SITE_URL',
27
+ 'MODERN_PUBLIC_SITE_URL must be set for production builds',
28
+ 'globalVars',
22
29
  ];
23
- const missing = requiredTokens.filter(token => !content.includes(token));
30
+ const missing = requiredTokens.filter((token) => !content.includes(token));
24
31
 
25
32
  if (missing.length > 0) {
26
- console.error(
27
- `Ultramodern contract check failed. Missing tokens: ${missing.join(', ')}`,
28
- );
33
+ console.error(`Ultramodern contract check failed. Missing tokens: ${missing.join(', ')}`);
29
34
  process.exit(1);
30
35
  }
31
36
 
@@ -34,12 +39,9 @@ if (!fs.existsSync(templateManifestPath)) {
34
39
  process.exit(1);
35
40
  }
36
41
 
37
- const templateManifest = JSON.parse(
38
- fs.readFileSync(templateManifestPath, 'utf8'),
39
- );
42
+ const templateManifest = JSON.parse(fs.readFileSync(templateManifestPath, 'utf-8'));
40
43
  const requiredDeniedPaths = [
41
44
  '.git/**',
42
- '.github/**',
43
45
  '.npmrc',
44
46
  '.yarnrc',
45
47
  '.env',
@@ -50,10 +52,120 @@ const requiredDeniedPaths = [
50
52
  const requiredPostMaterialization = [
51
53
  'ultramodern-contract-check',
52
54
  'dependency-install-with-lifecycle-deny',
55
+ 'github-workflow-security-enforced',
56
+ 'package-source-retained',
57
+ 'pnpm-11-policy-enforced',
58
+ 'rstest-smoke-tests',
53
59
  'template-manifest-retained',
54
60
  ];
61
+ const requiredPaths = [
62
+ {{#unless isSubproject}}
63
+ 'AGENTS.md',
64
+ '.agents/skills-lock.json',
65
+ '.github/renovate.json',
66
+ '.github/workflows/ultramodern-gates.yml',
67
+ 'oxlint.config.ts',
68
+ 'oxfmt.config.ts',
69
+ 'scripts/bootstrap-agent-skills.mjs',
70
+ {{/unless}}
71
+ '.modernjs/ultramodern-package-source.json',
72
+ 'pnpm-workspace.yaml',
73
+ 'rstest.config.mts',
74
+ 'scripts/check-i18n-strings.mjs',
75
+ 'config/public/locales/en/translation.json',
76
+ 'config/public/locales/cs/translation.json',
77
+ 'src/modern-app-env.d.ts',
78
+ 'src/routes/[lang]/page.tsx',
79
+ 'tests/ultramodern.contract.test.ts',
80
+ ];
55
81
  const manifestErrors = [];
56
82
 
83
+ for (const requiredPath of requiredPaths) {
84
+ if (!fs.existsSync(path.resolve(process.cwd(), requiredPath))) {
85
+ console.error(`${requiredPath} not found`);
86
+ process.exit(1);
87
+ }
88
+ }
89
+
90
+ if (fs.existsSync(path.resolve(process.cwd(), 'src/routes/page.tsx'))) {
91
+ console.error('src/routes/page.tsx must move under src/routes/[lang]/page.tsx');
92
+ process.exit(1);
93
+ }
94
+
95
+ {{#unless isSubproject}}
96
+ const workflowContent = fs.readFileSync(
97
+ path.resolve(process.cwd(), '.github/workflows/ultramodern-gates.yml'),
98
+ 'utf-8',
99
+ );
100
+ const renovateConfig = JSON.parse(
101
+ fs.readFileSync(path.resolve(process.cwd(), '.github/renovate.json'), 'utf-8'),
102
+ );
103
+ for (const requiredSnippet of [
104
+ 'permissions:\n contents: read',
105
+ 'pull_request:',
106
+ 'persist-credentials: false',
107
+ 'pnpm install --frozen-lockfile',
108
+ 'MODERN_PUBLIC_SITE_URL: http://localhost:8080',
109
+ 'timeout-minutes:',
110
+ 'egress-policy: audit',
111
+ ]) {
112
+ if (!workflowContent.includes(requiredSnippet)) {
113
+ console.error(`Generated workflow must retain ${requiredSnippet.split('\n')[0]}`);
114
+ process.exit(1);
115
+ }
116
+ }
117
+ if (workflowContent.includes('pull_request_target')) {
118
+ console.error('Generated workflow must not use pull_request_target');
119
+ process.exit(1);
120
+ }
121
+ for (const match of workflowContent.matchAll(/^\s*uses:\s*([^@\s]+)@([^\s#]+)/gmu)) {
122
+ const [, actionName, actionRef] = match;
123
+ if (!/^[a-f0-9]{40}$/u.test(actionRef)) {
124
+ console.error(`Generated workflow must pin ${actionName}@${actionRef} to a commit SHA`);
125
+ process.exit(1);
126
+ }
127
+ }
128
+ if (
129
+ renovateConfig.dependencyDashboard !== true ||
130
+ renovateConfig.minimumReleaseAge !== '1 day' ||
131
+ !renovateConfig.extends?.includes('helpers:pinGitHubActionDigests') ||
132
+ !renovateConfig.packageRules?.some(
133
+ (rule) =>
134
+ rule.dependencyDashboardApproval === true && rule.matchUpdateTypes?.includes('major'),
135
+ )
136
+ ) {
137
+ console.error('Generated Renovate config must retain dashboard, release-age, action pinning, and major-approval policy');
138
+ process.exit(1);
139
+ }
140
+ {{/unless}}
141
+
142
+ const pageContent = fs.readFileSync(
143
+ path.resolve(process.cwd(), 'src/routes/[lang]/page.tsx'),
144
+ 'utf-8',
145
+ );
146
+ for (const token of [
147
+ 'rel="canonical"',
148
+ 'rel="alternate"',
149
+ 'hrefLang="x-default"',
150
+ 'localizedPath(',
151
+ '<a',
152
+ {{#if isTanstackRouter}}
153
+ "@modern-js/plugin-tanstack/runtime",
154
+ {{/if}}
155
+ ]) {
156
+ if (!pageContent.includes(token)) {
157
+ console.error(`Localized route is missing ${token}`);
158
+ process.exit(1);
159
+ }
160
+ }
161
+ {{#if isTanstackRouter}}
162
+ const deprecatedTanstackRuntime = '@modern-js/runtime/' + 'tanstack-router';
163
+ if (pageContent.includes(deprecatedTanstackRuntime)) {
164
+ console.error('Localized route must import TanStack runtime from @modern-js/plugin-tanstack/runtime');
165
+ process.exit(1);
166
+ }
167
+ {{/if}}
168
+
57
169
  if (templateManifest.schemaVersion !== 1) {
58
170
  manifestErrors.push('schemaVersion');
59
171
  }
@@ -65,10 +177,10 @@ if (templateManifest.source?.type !== 'builtin') {
65
177
  if (
66
178
  !Array.isArray(templateManifest.integrity?.checksums) ||
67
179
  !templateManifest.integrity.checksums.some(
68
- checksum =>
180
+ (checksum) =>
69
181
  checksum.algorithm === 'sha256' &&
70
182
  checksum.scope === 'source-tree' &&
71
- /^[0-9a-f]{64}$/.test(checksum.value),
183
+ /^[0-9a-f]{64}$/u.test(checksum.value),
72
184
  )
73
185
  ) {
74
186
  manifestErrors.push('integrity.checksums[source-tree]');
@@ -92,11 +204,233 @@ for (const token of requiredPostMaterialization) {
92
204
 
93
205
  if (manifestErrors.length > 0) {
94
206
  console.error(
95
- `Ultramodern template manifest check failed. Invalid fields: ${manifestErrors.join(
96
- ', ',
97
- )}`,
207
+ `Ultramodern template manifest check failed. Invalid fields: ${manifestErrors.join(', ')}`,
98
208
  );
99
209
  process.exit(1);
100
210
  }
101
211
 
212
+ const packageJson = JSON.parse(
213
+ fs.readFileSync(path.resolve(process.cwd(), 'package.json'), 'utf-8'),
214
+ );
215
+ const pnpmWorkspace = fs.readFileSync(
216
+ path.resolve(process.cwd(), 'pnpm-workspace.yaml'),
217
+ 'utf-8',
218
+ );
219
+ const packageSource = JSON.parse(fs.readFileSync(packageSourcePath, 'utf-8'));
220
+ const unresolvedTemplateMarker = String.fromCodePoint(123, 123);
221
+ if (JSON.stringify(packageJson).includes(unresolvedTemplateMarker)) {
222
+ console.error('package.json contains unresolved template markers');
223
+ process.exit(1);
224
+ }
225
+ if (JSON.stringify(packageSource).includes(unresolvedTemplateMarker)) {
226
+ console.error('package source metadata contains unresolved template markers');
227
+ process.exit(1);
228
+ }
229
+ {{#unless isSubproject}}
230
+ const skillsLock = JSON.parse(
231
+ fs.readFileSync(path.resolve(process.cwd(), '.agents/skills-lock.json'), 'utf-8'),
232
+ );
233
+ {{/unless}}
234
+ const requiredScripts = {
235
+ 'i18n:check': 'node ./scripts/check-i18n-strings.mjs',
236
+ test: 'rstest run',
237
+ {{#unless isSubproject}}
238
+ format: 'oxfmt .',
239
+ 'format:check': 'oxfmt --check .',
240
+ lint: 'oxlint .',
241
+ 'lint:fix': 'oxlint . --fix',
242
+ 'skills:check': 'node ./scripts/bootstrap-agent-skills.mjs --check',
243
+ 'skills:install': 'node ./scripts/bootstrap-agent-skills.mjs',
244
+ {{/unless}}
245
+ };
246
+
247
+ for (const [scriptName, scriptCommand] of Object.entries(requiredScripts)) {
248
+ if (packageJson.scripts?.[scriptName] !== scriptCommand) {
249
+ console.error(`Missing or invalid package script: ${scriptName}`);
250
+ process.exit(1);
251
+ }
252
+ }
253
+
254
+ if (
255
+ !packageJson.scripts?.typecheck?.includes('effect-tsgo') ||
256
+ !packageJson.scripts.typecheck.includes('get-exe-path')
257
+ ) {
258
+ console.error('typecheck must use effect-tsgo as the TypeScript checker');
259
+ process.exit(1);
260
+ }
261
+
262
+ if (!packageJson.scripts?.['ultramodern:check']?.includes('pnpm test')) {
263
+ console.error('ultramodern:check must run the generated Rstest suite');
264
+ process.exit(1);
265
+ }
266
+
267
+ if (packageJson.private !== true) {
268
+ console.error('Generated app package must be private by default');
269
+ process.exit(1);
270
+ }
271
+
272
+ if (packageJson.packageManager !== 'pnpm@11.1.2') {
273
+ console.error('Generated app package must pin pnpm@11.1.2');
274
+ process.exit(1);
275
+ }
276
+
277
+ if (packageJson.engines?.pnpm !== '>=11.0.0') {
278
+ console.error('Generated app package must require pnpm >=11.0.0');
279
+ process.exit(1);
280
+ }
281
+
282
+ if (packageJson.pnpm !== undefined) {
283
+ console.error('Generated app must keep pnpm policy in pnpm-workspace.yaml, not package.json');
284
+ process.exit(1);
285
+ }
286
+
287
+ for (const requiredSnippet of [
288
+ 'minimumReleaseAge: 1440',
289
+ 'minimumReleaseAgeStrict: true',
290
+ 'minimumReleaseAgeIgnoreMissingTime: false',
291
+ "minimumReleaseAgeExclude:\n - '@modern-js/*'\n - '@bleedingdev/*'\n - '@effect/tsgo'\n - '@effect/tsgo-*'\n - '@typescript/native-preview'\n - '@typescript/native-preview-*'",
292
+ 'trustPolicy: no-downgrade',
293
+ 'trustPolicyIgnoreAfter: 1440',
294
+ 'blockExoticSubdeps: true',
295
+ 'engineStrict: true',
296
+ 'pmOnFail: error',
297
+ 'verifyDepsBeforeRun: error',
298
+ 'strictDepBuilds: true',
299
+ "allowBuilds:\n '@swc/core': true\n core-js: true\n esbuild: true\n msgpackr-extract: true\n simple-git-hooks: true",
300
+ ]) {
301
+ if (!pnpmWorkspace.includes(requiredSnippet)) {
302
+ console.error(`pnpm-workspace.yaml must retain ${requiredSnippet.split('\n')[0]}`);
303
+ process.exit(1);
304
+ }
305
+ }
306
+
307
+ if (pnpmWorkspace.includes('onlyBuiltDependencies')) {
308
+ console.error('pnpm-workspace.yaml must use pnpm 11 allowBuilds instead of onlyBuiltDependencies');
309
+ process.exit(1);
310
+ }
311
+
312
+ if (packageJson.modernjs?.preset !== 'presetUltramodern') {
313
+ console.error('package.json must declare presetUltramodern metadata');
314
+ process.exit(1);
315
+ }
316
+
317
+ if (
318
+ packageJson.modernjs?.packageSource?.config !== './.modernjs/ultramodern-package-source.json'
319
+ ) {
320
+ console.error('package.json must retain package source metadata location');
321
+ process.exit(1);
322
+ }
323
+
324
+ if (packageSource.schemaVersion !== 1) {
325
+ console.error('Package source metadata must use schemaVersion 1');
326
+ process.exit(1);
327
+ }
328
+
329
+ if (packageSource.preset !== 'presetUltramodern') {
330
+ console.error('Package source metadata must declare presetUltramodern');
331
+ process.exit(1);
332
+ }
333
+
334
+ if (packageSource.strategy !== 'workspace' && packageSource.strategy !== 'install') {
335
+ console.error('Package source strategy must be workspace or install');
336
+ process.exit(1);
337
+ }
338
+
339
+ const expectedModernPackages = [
340
+ '@modern-js/runtime',
341
+ '@modern-js/app-tools',
342
+ '@modern-js/tsconfig',
343
+ '@modern-js/plugin-i18n',
344
+ '@modern-js/plugin-tanstack',
345
+ '@modern-js/plugin-bff',
346
+ '@modern-js/adapter-rstest',
347
+ ];
348
+
349
+ for (const packageName of expectedModernPackages) {
350
+ if (!packageSource.modernPackages?.packages?.includes(packageName)) {
351
+ console.error(`Package source metadata must include ${packageName}`);
352
+ process.exit(1);
353
+ }
354
+ }
355
+
356
+ const expectedModernSpecifier = packageSource.modernPackages?.specifier;
357
+ if (typeof expectedModernSpecifier !== 'string' || expectedModernSpecifier.length === 0) {
358
+ console.error('Package source metadata must provide a Modern package specifier');
359
+ process.exit(1);
360
+ }
361
+
362
+ const expectedModernDependency = (packageName) => {
363
+ const alias = packageSource.modernPackages?.aliases?.[packageName];
364
+ return typeof alias === 'string'
365
+ ? `npm:${alias}@${expectedModernSpecifier}`
366
+ : expectedModernSpecifier;
367
+ };
368
+
369
+ for (const packageName of [
370
+ '@modern-js/runtime',
371
+ '@modern-js/plugin-i18n',
372
+ '@modern-js/plugin-tanstack',
373
+ ]) {
374
+ if (
375
+ packageJson.dependencies?.[packageName] &&
376
+ packageJson.dependencies[packageName] !== expectedModernDependency(packageName)
377
+ ) {
378
+ console.error(`Dependency ${packageName} must match package source metadata`);
379
+ process.exit(1);
380
+ }
381
+ }
382
+
383
+ for (const packageName of [
384
+ '@modern-js/app-tools',
385
+ '@modern-js/adapter-rstest',
386
+ '@modern-js/tsconfig',
387
+ '@modern-js/plugin-bff',
388
+ ]) {
389
+ if (
390
+ packageJson.devDependencies?.[packageName] &&
391
+ packageJson.devDependencies[packageName] !== expectedModernDependency(packageName)
392
+ ) {
393
+ console.error(`Dev dependency ${packageName} must match package source metadata`);
394
+ process.exit(1);
395
+ }
396
+ }
397
+
398
+ for (const dependency of ['@modern-js/plugin-i18n', 'i18next', 'react-i18next']) {
399
+ if (!packageJson.dependencies?.[dependency]) {
400
+ console.error(`Missing dependency: ${dependency}`);
401
+ process.exit(1);
402
+ }
403
+ }
404
+
405
+ for (const dependency of [
406
+ '@effect/tsgo',
407
+ '@modern-js/adapter-rstest',
408
+ '@rstest/core',
409
+ '@typescript/native-preview',
410
+ 'happy-dom',
411
+ {{#unless isSubproject}}
412
+ 'oxlint',
413
+ 'oxfmt',
414
+ 'ultracite',
415
+ {{/unless}}
416
+ ]) {
417
+ if (!packageJson.devDependencies?.[dependency]) {
418
+ console.error(`Missing devDependency: ${dependency}`);
419
+ process.exit(1);
420
+ }
421
+ }
422
+
423
+ {{#unless isSubproject}}
424
+ const privateSource = skillsLock.sources?.find(
425
+ (source) => source.repository === 'https://github.com/TechsioCZ/skills',
426
+ );
427
+ const privateSkills = new Set(privateSource?.baseline?.map((skill) => skill.name));
428
+ for (const skillName of ['plan-graph', 'dag', 'subagent-graph', 'helm', 'debugger-mode']) {
429
+ if (!privateSkills.has(skillName)) {
430
+ console.error(`Missing private skill allowlist entry: ${skillName}`);
431
+ process.exit(1);
432
+ }
433
+ }
434
+ {{/unless}}
435
+
102
436
  console.log('Ultramodern contract check passed.');
@@ -14,5 +14,4 @@ export const bffEffectApi = HttpApi.make('BffApi').add(
14
14
  }),
15
15
  }),
16
16
  ),
17
- );
18
- {{/if}}
17
+ );{{/if}}
@@ -1 +1,3 @@
1
1
  /// <reference types='@modern-js/app-tools/types' />
2
+
3
+ declare const ULTRAMODERN_SITE_URL: string;
@@ -1,9 +1,23 @@
1
1
  import { defineRuntimeConfig } from '@modern-js/runtime';
2
+ import { createInstance } from 'i18next';
3
+
4
+ const i18nInstance = createInstance();
2
5
 
3
6
  export default defineRuntimeConfig({
4
- {{#if isTanstackRouter}}
5
- router: {
7
+ i18n: {
8
+ i18nInstance,
9
+ initOptions: {
10
+ defaultNS: 'translation',
11
+ fallbackLng: 'en',
12
+ interpolation: {
13
+ escapeValue: false,
14
+ },
15
+ ns: ['translation'],
16
+ supportedLngs: ['en', 'cs'],
17
+ },
18
+ },
19
+ {{#if isTanstackRouter}} router: {
6
20
  framework: 'tanstack',
7
21
  },
8
- {{/if}}
22
+ {{/if~}}
9
23
  });