@bleedingdev/modern-js-create 3.2.0-ultramodern.9 → 3.2.0-ultramodern.91

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 (51) hide show
  1. package/README.md +152 -35
  2. package/dist/index.js +4700 -608
  3. package/dist/types/locale/en.d.ts +3 -0
  4. package/dist/types/locale/zh.d.ts +3 -0
  5. package/dist/types/ultramodern-workspace.d.ts +11 -0
  6. package/package.json +6 -6
  7. package/template/.codex/hooks.json +16 -0
  8. package/template/.github/renovate.json +53 -0
  9. package/template/.github/workflows/ultramodern-gates.yml.handlebars +34 -10
  10. package/template/.mise.toml.handlebars +2 -0
  11. package/template/AGENTS.md +9 -6
  12. package/template/README.md +60 -34
  13. package/template/api/effect/index.ts.handlebars +8 -3
  14. package/template/config/public/locales/cs/translation.json +39 -0
  15. package/template/config/public/locales/en/translation.json +39 -0
  16. package/template/lefthook.yml +10 -0
  17. package/template/modern.config.ts.handlebars +39 -24
  18. package/template/oxfmt.config.ts +11 -3
  19. package/template/oxlint.config.ts +11 -4
  20. package/template/package.json.handlebars +43 -34
  21. package/template/pnpm-workspace.yaml +29 -0
  22. package/template/rstest.config.mts +5 -0
  23. package/template/scripts/bootstrap-agent-skills.mjs +160 -35
  24. package/template/scripts/check-i18n-strings.mjs +94 -0
  25. package/template/scripts/validate-ultramodern.mjs.handlebars +387 -35
  26. package/template/shared/effect/api.ts.handlebars +1 -2
  27. package/template/src/modern-app-env.d.ts +2 -0
  28. package/template/src/modern.runtime.ts.handlebars +17 -3
  29. package/template/src/routes/[lang]/page.tsx.handlebars +211 -0
  30. package/template/src/routes/index.css.handlebars +14 -3
  31. package/template/src/routes/layout.tsx.handlebars +2 -1
  32. package/template/tailwind.config.ts.handlebars +1 -1
  33. package/template/tests/tsconfig.json +7 -0
  34. package/template/tests/ultramodern.contract.test.ts.handlebars +78 -0
  35. package/template-workspace/.agents/agent-reference-repos.json +24 -0
  36. package/template-workspace/.agents/skills-lock.json +19 -0
  37. package/template-workspace/.codex/hooks.json +16 -0
  38. package/template-workspace/.github/renovate.json +29 -0
  39. package/template-workspace/.github/workflows/ultramodern-workspace-gates.yml.handlebars +54 -0
  40. package/template-workspace/.gitignore.handlebars +5 -0
  41. package/template-workspace/.mise.toml.handlebars +2 -0
  42. package/template-workspace/AGENTS.md +36 -5
  43. package/template-workspace/README.md.handlebars +61 -11
  44. package/template-workspace/lefthook.yml +10 -0
  45. package/template-workspace/oxfmt.config.ts +13 -3
  46. package/template-workspace/oxlint.config.ts +12 -4
  47. package/template-workspace/pnpm-workspace.yaml +26 -8
  48. package/template-workspace/scripts/bootstrap-agent-skills.mjs +184 -26
  49. package/template-workspace/scripts/setup-agent-reference-repos.mjs +368 -0
  50. package/template/src/routes/page.tsx.handlebars +0 -119
  51. package/template-workspace/scripts/validate-ultramodern-workspace.mjs.handlebars +0 -403
@@ -1,57 +1,66 @@
1
1
  {
2
2
  "name": "{{packageName}}",
3
3
  "version": "0.1.0",
4
+ "private": true,
5
+ "type": "module",
6
+ "packageManager": "pnpm@{{pnpmVersion}}",
4
7
  "scripts": {
5
8
  "reset": "npx rimraf node_modules ./**/node_modules",
6
9
  "dev": "modern dev",
7
10
  "build": "modern build",
8
11
  "serve": "modern serve",
12
+ "test": "rstest run",
9
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);\"",
14
+ "i18n:check": "node ./scripts/check-i18n-strings.mjs",
15
+ {{#unless isSubproject}}
10
16
  "skills:install": "node ./scripts/bootstrap-agent-skills.mjs",
11
17
  "skills:check": "node ./scripts/bootstrap-agent-skills.mjs --check",
12
- "ultramodern:check": "{{#unless isSubproject}}pnpm format:check && pnpm lint && {{/unless}}pnpm typecheck{{#unless isSubproject}} && pnpm skills:check{{/unless}} && node ./scripts/validate-ultramodern.mjs"{{#unless isSubproject}},
18
+ "postinstall": "oxfmt . && node ./scripts/bootstrap-agent-skills.mjs",
19
+ {{/unless}}
20
+ "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}},
13
21
  "format": "oxfmt .",
14
22
  "format:check": "oxfmt --check .",
15
23
  "lint": "oxlint .",
16
- "lint:fix": "oxlint . --fix",
17
- "prepare": "simple-git-hooks"{{/unless}}
24
+ "lint:fix": "oxlint . --fix"{{/unless}}
18
25
  },
19
- "engines": {
20
- "node": ">=20"
21
- }{{#unless isSubproject}},
22
- "lint-staged": {
23
- "*.{js,ts,cjs,mjs,d.cts,d.mts,jsx,tsx,json,jsonc}": [
24
- "oxfmt --write",
25
- "oxlint --fix"
26
- ]
27
- },
28
- "simple-git-hooks": {
29
- "pre-commit": "npx lint-staged"
30
- }{{/unless}},
31
26
  "dependencies": {
32
- "@modern-js/runtime": "{{runtimeVersion}}"{{#if isTanstackRouter}},
33
- "@modern-js/plugin-tanstack": "{{pluginTanstackVersion}}",
34
- "@tanstack/react-router": "1.170.1"{{/if}},
35
- "react": "^19.2.3",
36
- "react-dom": "^19.2.0"
27
+ "@modern-js/plugin-i18n": "{{pluginI18nVersion}}",
28
+ {{#if isTanstackRouter}} "@modern-js/plugin-tanstack": "{{pluginTanstackVersion}}",
29
+ {{/if}}
30
+ "@modern-js/runtime": "{{runtimeVersion}}",
31
+ {{#if isTanstackRouter}} "@tanstack/react-router": "{{tanstackRouterVersion}}",
32
+ {{/if}}
33
+ "i18next": "26.2.0",
34
+ "react": "^19.2.6",
35
+ "react-dom": "^19.2.6",
36
+ "react-i18next": "17.0.8"
37
37
  },
38
38
  "devDependencies": {
39
+ "@effect/tsgo": "0.13.0",
40
+ "@modern-js/adapter-rstest": "{{adapterRstestVersion}}",
39
41
  "@modern-js/app-tools": "{{appToolsVersion}}",
40
- "@modern-js/tsconfig": "{{tsconfigVersion}}"{{#if enableBff}},
41
- "@modern-js/plugin-bff": "{{pluginBffVersion}}"{{/if}}{{#if enableTailwind}},
42
- "@tailwindcss/postcss": "^4.1.18",
43
- "postcss": "^8.5.6",
44
- "tailwindcss": "^4.1.18"{{/if}},
45
- "@effect/tsgo": "0.7.3",
46
- "@typescript/native-preview": "7.0.0-dev.20260518.1",
42
+ {{#if enableBff}} "@modern-js/plugin-bff": "{{pluginBffVersion}}",
43
+ {{/if}} "@modern-js/tsconfig": "{{tsconfigVersion}}",
44
+ "@rstest/core": "0.10.3",
45
+ {{#if enableTailwind}}
46
+ "@tailwindcss/postcss": "^{{tailwindPostcssVersion}}",
47
+ {{/if}}
47
48
  "@types/node": "^20",
48
49
  "@types/react": "^19.1.8",
49
- "@types/react-dom": "^19.1.6"{{#unless isSubproject}},
50
- "oxlint": "1.65.0",
51
- "oxfmt": "0.50.0",
52
- "ultracite": "7.7.0",
53
- "lint-staged": "~15.4.0",
54
- "simple-git-hooks": "^2.11.1"{{/unless}},
55
- "rimraf": "^6.0.1"
50
+ "@types/react-dom": "^19.1.6",
51
+ "@typescript/native-preview": "7.0.0-dev.20260527.2",
52
+ "happy-dom": "^20.9.0",
53
+ {{#unless isSubproject}}
54
+ "lefthook": "^2.1.9",
55
+ "oxfmt": "0.51.0",
56
+ "oxlint": "1.66.0",
57
+ {{/unless}}{{#if enableTailwind}} "postcss": "^8.5.6",
58
+ {{/if}} "rimraf": "^6.1.3"{{#if enableTailwind}},
59
+ "tailwindcss": "^{{tailwindVersion}}"{{/if}}{{#unless isSubproject}},
60
+ "ultracite": "7.7.0"{{/unless}}
61
+ },
62
+ "engines": {
63
+ "node": ">=20",
64
+ "pnpm": ">={{pnpmVersion}} <11.6.0"
56
65
  }
57
66
  }
@@ -0,0 +1,29 @@
1
+ minimumReleaseAge: 1440
2
+ minimumReleaseAgeStrict: true
3
+ minimumReleaseAgeIgnoreMissingTime: false
4
+ minimumReleaseAgeExclude:
5
+ - '@bleedingdev/modern-js-*'
6
+ trustPolicy: no-downgrade
7
+ trustPolicyIgnoreAfter: 1440
8
+ blockExoticSubdeps: true
9
+ engineStrict: true
10
+ pmOnFail: error
11
+ verifyDepsBeforeRun: error
12
+ strictDepBuilds: true
13
+
14
+ allowBuilds:
15
+ '@swc/core': true
16
+ core-js: true
17
+ esbuild: true
18
+ lefthook: true
19
+ msgpackr-extract: true
20
+ sharp: true
21
+ workerd: true
22
+ onlyBuiltDependencies:
23
+ - '@swc/core'
24
+ - core-js
25
+ - esbuild
26
+ - lefthook
27
+ - msgpackr-extract
28
+ - sharp
29
+ - workerd
@@ -0,0 +1,5 @@
1
+ import { defineConfig } from '@rstest/core';
2
+
3
+ export default defineConfig({
4
+ testEnvironment: 'node',
5
+ });
@@ -8,43 +8,134 @@ const lockPath = path.join(root, '.agents/skills-lock.json');
8
8
  const checkOnly = process.argv.includes('--check');
9
9
  const force = process.argv.includes('--force');
10
10
 
11
- function readJson(filePath) {
12
- return JSON.parse(fs.readFileSync(filePath, 'utf8'));
13
- }
11
+ const readJson = (filePath) => JSON.parse(fs.readFileSync(filePath, 'utf-8'));
14
12
 
15
- function run(command, args, options = {}) {
16
- return execFileSync(command, args, {
13
+ const run = (command, args, options = {}) =>
14
+ execFileSync(command, args, {
17
15
  cwd: options.cwd ?? root,
18
- encoding: 'utf8',
16
+ encoding: 'utf-8',
19
17
  stdio: options.stdio ?? ['ignore', 'pipe', 'pipe'],
20
18
  });
21
- }
22
19
 
23
- function cloneSource(source, targetDir) {
24
- const repo = source.repository.replace(/^https:\/\/github.com\//, '');
20
+ const commandExists = (command) => {
25
21
  try {
26
- run('gh', ['repo', 'clone', repo, targetDir, '--', '--depth', '1'], {
27
- stdio: 'inherit',
28
- });
22
+ run(command, ['--version'], { stdio: 'ignore' });
23
+ return true;
24
+ } catch {
25
+ return false;
26
+ }
27
+ };
28
+
29
+ const runShell = (script) =>
30
+ run('sh', ['-lc', script], {
31
+ stdio: 'inherit',
32
+ });
33
+
34
+ const installGit = () => {
35
+ if (commandExists('git')) {
29
36
  return;
37
+ }
38
+
39
+ if (commandExists('brew')) {
40
+ run('brew', ['install', 'git'], { stdio: 'inherit' });
41
+ } else if (process.platform === 'linux' && commandExists('apt-get')) {
42
+ const sudo = typeof process.getuid === 'function' && process.getuid() === 0 ? '' : 'sudo ';
43
+ runShell(`${sudo}apt-get update && ${sudo}apt-get install -y git`);
44
+ } else if (process.platform === 'linux' && commandExists('dnf')) {
45
+ const sudo = typeof process.getuid === 'function' && process.getuid() === 0 ? '' : 'sudo ';
46
+ runShell(`${sudo}dnf install -y git`);
47
+ } else if (process.platform === 'linux' && commandExists('yum')) {
48
+ const sudo = typeof process.getuid === 'function' && process.getuid() === 0 ? '' : 'sudo ';
49
+ runShell(`${sudo}yum install -y git`);
50
+ } else if (process.platform === 'linux' && commandExists('apk')) {
51
+ runShell('apk add --no-cache git');
52
+ }
53
+
54
+ if (!commandExists('git')) {
55
+ throw new Error(
56
+ 'Git is required for UltraModern setup. Install git and run pnpm skills:install again.',
57
+ );
58
+ }
59
+ };
60
+
61
+ const isInsideGitWorkTree = () => {
62
+ try {
63
+ return run('git', ['rev-parse', '--is-inside-work-tree']).trim() === 'true';
64
+ } catch {
65
+ return false;
66
+ }
67
+ };
68
+
69
+ const initializeGitRepository = () => {
70
+ if (isInsideGitWorkTree()) {
71
+ return;
72
+ }
73
+
74
+ try {
75
+ run('git', ['init', '-b', 'main'], { stdio: 'inherit' });
30
76
  } catch {
31
- run('git', ['clone', '--depth', '1', source.repository, targetDir], {
32
- stdio: 'inherit',
77
+ run('git', ['init'], { stdio: 'inherit' });
78
+ run('git', ['branch', '-M', 'main'], { stdio: 'inherit' });
79
+ }
80
+ };
81
+
82
+ const installLefthook = () => {
83
+ try {
84
+ run('lefthook', ['install'], { stdio: 'inherit' });
85
+ } catch (error) {
86
+ console.warn(`Unable to install lefthook hooks: ${error.message}`);
87
+ }
88
+ };
89
+
90
+ const removeTree = (dir) =>
91
+ fs.rmSync(dir, {
92
+ force: true,
93
+ maxRetries: 5,
94
+ recursive: true,
95
+ retryDelay: 100,
96
+ });
97
+
98
+ const cloneSource = (source, targetDir) => {
99
+ if (source.commit) {
100
+ run('git', ['init', targetDir]);
101
+ run('git', ['remote', 'add', 'origin', source.repository], {
102
+ cwd: targetDir,
103
+ });
104
+ run('git', ['fetch', '--depth', '1', '--quiet', 'origin', source.commit], {
105
+ cwd: targetDir,
33
106
  });
107
+ run(
108
+ 'git',
109
+ [
110
+ '-c',
111
+ 'advice.detachedHead=false',
112
+ 'checkout',
113
+ '--detach',
114
+ '--quiet',
115
+ 'FETCH_HEAD',
116
+ ],
117
+ { cwd: targetDir },
118
+ );
119
+ return;
34
120
  }
35
- }
36
121
 
37
- function resolveSkillDir(sourceRoot, skillName) {
122
+ const repo = source.repository.replace(/^https:\/\/github.com\//u, '');
123
+ try {
124
+ run('gh', ['repo', 'clone', repo, targetDir, '--', '--depth', '1', '--quiet']);
125
+ } catch {
126
+ run('git', ['clone', '--depth', '1', '--quiet', source.repository, targetDir]);
127
+ }
128
+ };
129
+
130
+ const resolveSkillDir = (sourceRoot, skillName) => {
38
131
  const candidates = [
39
132
  path.join(sourceRoot, skillName),
40
133
  path.join(sourceRoot, 'skills', skillName),
41
134
  path.join(sourceRoot, 'skills', 'engineering', skillName),
42
135
  path.join(sourceRoot, 'skills', 'productivity', skillName),
43
136
  ];
44
- return candidates.find(candidate =>
45
- fs.existsSync(path.join(candidate, 'SKILL.md')),
46
- );
47
- }
137
+ return candidates.find((candidate) => fs.existsSync(path.join(candidate, 'SKILL.md')));
138
+ };
48
139
 
49
140
  if (!fs.existsSync(lockPath)) {
50
141
  console.error('Missing .agents/skills-lock.json');
@@ -53,38 +144,69 @@ if (!fs.existsSync(lockPath)) {
53
144
 
54
145
  const lock = readJson(lockPath);
55
146
  const installDir = path.join(root, lock.installDir ?? '.agents/skills');
56
- const privateSources = (lock.sources ?? []).filter(
57
- source => source.install === 'clone-if-authorized',
147
+ const sources = lock.sources ?? [];
148
+ const requiredCloneSources = sources.filter((source) => source.install === 'clone');
149
+ const optionalCloneSources = sources.filter(
150
+ (source) => source.install === 'clone-if-authorized',
151
+ );
152
+ const requiredSkills = [
153
+ ...(lock.baseline ?? []),
154
+ ...requiredCloneSources.flatMap((source) => source.baseline ?? []),
155
+ ].filter(
156
+ (skill, index, skills) =>
157
+ skills.findIndex((candidate) => candidate.name === skill.name) === index,
58
158
  );
59
159
 
60
160
  if (checkOnly) {
61
- const missing = privateSources.flatMap(source =>
161
+ const missingRequired = requiredSkills
162
+ .map((skill) => skill.name)
163
+ .filter((skillName) => !fs.existsSync(path.join(installDir, skillName, 'SKILL.md')));
164
+ const missingOptional = optionalCloneSources.flatMap((source) =>
62
165
  (source.baseline ?? [])
63
- .map(skill => skill.name)
64
- .filter(skillName => !fs.existsSync(path.join(installDir, skillName, 'SKILL.md'))),
166
+ .map((skill) => skill.name)
167
+ .filter((skillName) => !fs.existsSync(path.join(installDir, skillName, 'SKILL.md'))),
65
168
  );
66
- if (missing.length > 0) {
169
+
170
+ if (missingRequired.length > 0) {
171
+ console.error(
172
+ `Required agent skills not installed: ${missingRequired.join(', ')}. Run pnpm skills:install.`,
173
+ );
174
+ process.exit(1);
175
+ }
176
+
177
+ if (missingOptional.length > 0) {
67
178
  console.warn(
68
- `Private skills not installed: ${missing.join(', ')}. Run pnpm skills:install if you have access.`,
179
+ `Private skills not installed: ${missingOptional.join(', ')}. Run pnpm skills:install if you have access.`,
69
180
  );
70
181
  } else {
71
- console.log('Agent skills are installed.');
182
+ console.log('Required and private agent skills are installed.');
183
+ process.exit(0);
72
184
  }
185
+ console.log('Required agent skills are installed.');
73
186
  process.exit(0);
74
187
  }
75
188
 
76
189
  fs.mkdirSync(installDir, { recursive: true });
190
+ installGit();
77
191
 
78
- for (const source of privateSources) {
192
+ for (const source of [...requiredCloneSources, ...optionalCloneSources]) {
79
193
  const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'ultramodern-skills-'));
80
194
  try {
81
- cloneSource(source, tempDir);
195
+ try {
196
+ cloneSource(source, tempDir);
197
+ } catch (error) {
198
+ if (source.install === 'clone-if-authorized') {
199
+ console.warn(
200
+ `Skipping ${source.repository}; current developer may not have access.`,
201
+ );
202
+ continue;
203
+ }
204
+ throw error;
205
+ }
82
206
  for (const skill of source.baseline ?? []) {
83
207
  const sourceSkillDir = resolveSkillDir(tempDir, skill.name);
84
208
  if (!sourceSkillDir) {
85
- throw new Error(
86
- `Skill ${skill.name} not found in ${source.repository}`,
87
- );
209
+ throw new Error(`Skill ${skill.name} not found in ${source.repository}`);
88
210
  }
89
211
  const targetSkillDir = path.join(installDir, skill.name);
90
212
  if (fs.existsSync(targetSkillDir)) {
@@ -92,12 +214,15 @@ for (const source of privateSources) {
92
214
  console.log(`Skipping existing ${skill.name}`);
93
215
  continue;
94
216
  }
95
- fs.rmSync(targetSkillDir, { recursive: true, force: true });
217
+ removeTree(targetSkillDir);
96
218
  }
97
219
  fs.cpSync(sourceSkillDir, targetSkillDir, { recursive: true });
98
220
  console.log(`Installed ${skill.name}`);
99
221
  }
100
222
  } finally {
101
- fs.rmSync(tempDir, { recursive: true, force: true });
223
+ removeTree(tempDir);
102
224
  }
103
225
  }
226
+
227
+ initializeGitRepository();
228
+ installLefthook();
@@ -0,0 +1,94 @@
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 isCodeElementText = (content, index) => {
34
+ const tagStart = content.lastIndexOf('<', index);
35
+ if (tagStart === -1) {
36
+ return false;
37
+ }
38
+ return /^<code(?:\s|>)/u.test(content.slice(tagStart, index));
39
+ };
40
+ const isIgnoredLine = (content, index) => {
41
+ const lineStart = content.lastIndexOf('\n', index) + 1;
42
+ const lineEnd = content.indexOf('\n', index);
43
+ const currentLineEnd = lineEnd === -1 ? content.length : lineEnd;
44
+ const previousLineStart = content.lastIndexOf('\n', Math.max(0, lineStart - 2)) + 1;
45
+ const nextLineEnd = content.indexOf('\n', currentLineEnd + 1);
46
+ const context = content.slice(
47
+ previousLineStart,
48
+ nextLineEnd === -1 ? content.length : nextLineEnd,
49
+ );
50
+ return /i18n-ignore/u.test(context);
51
+ };
52
+
53
+ const violations = [];
54
+ for (const filePath of scanRoots.flatMap(collectFiles)) {
55
+ const content = fs.readFileSync(filePath, 'utf-8');
56
+ for (const match of content.matchAll(visibleAttributePattern)) {
57
+ if (!isIgnoredLine(content, match.index ?? 0)) {
58
+ violations.push({
59
+ filePath,
60
+ line: lineNumberForIndex(content, match.index ?? 0),
61
+ text: match[1].trim(),
62
+ });
63
+ }
64
+ }
65
+
66
+ for (const match of content.matchAll(jsxTextPattern)) {
67
+ const text = match[1].replaceAll(/\s+/gu, ' ').trim();
68
+ if (
69
+ text &&
70
+ !isIgnoredLine(content, match.index ?? 0) &&
71
+ !isCodeElementText(content, match.index ?? 0)
72
+ ) {
73
+ violations.push({
74
+ filePath,
75
+ line: lineNumberForIndex(content, match.index ?? 0),
76
+ text,
77
+ });
78
+ }
79
+ }
80
+ }
81
+
82
+ if (violations.length > 0) {
83
+ console.error('Hardcoded user-visible JSX strings found. Move copy to locale JSON files.');
84
+ for (const violation of violations) {
85
+ console.error(
86
+ `${path.relative(root, violation.filePath)}:${violation.line} ${JSON.stringify(
87
+ violation.text,
88
+ )}`,
89
+ );
90
+ }
91
+ process.exit(1);
92
+ }
93
+
94
+ console.log('No hardcoded user-visible JSX strings found.');