baldart 4.51.0 → 4.52.0
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/CHANGELOG.md +29 -0
- package/README.md +10 -3
- package/VERSION +1 -1
- package/framework/.claude/agents/REGISTRY.md +1 -0
- package/framework/.claude/agents/code-reviewer.md +16 -0
- package/framework/.claude/agents/coder.md +6 -0
- package/framework/.claude/agents/doc-reviewer.md +24 -0
- package/framework/.claude/agents/i18n-translator.md +56 -0
- package/framework/.claude/agents/qa-sentinel.md +10 -0
- package/framework/.claude/skills/i18n/SKILL.md +109 -0
- package/framework/.claude/skills/i18n-adopt/SKILL.md +105 -0
- package/framework/.claude/skills/new/references/implement.md +14 -1
- package/framework/.claude/skills/prd/SKILL.md +12 -2
- package/framework/.claude/workflows/new-final-review.js +1 -1
- package/framework/.claude/workflows/new2.js +3 -0
- package/framework/AGENTS.md +4 -0
- package/framework/agents/i18n-protocol.md +120 -0
- package/framework/agents/index.md +2 -0
- package/framework/docs/I18N-LAYER.md +89 -0
- package/framework/routines/i18n-align.routine.yml +61 -0
- package/framework/routines/index.yml +11 -0
- package/framework/templates/baldart.config.template.yml +59 -0
- package/package.json +1 -1
- package/src/commands/configure.js +103 -0
- package/src/commands/doctor.js +58 -0
- package/src/commands/update.js +6 -0
- package/src/utils/i18n-gate.js +195 -0
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const { execSync } = require('child_process');
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* i18n anti-hardcoded gate — BALDART-owned standalone ESLint config (since v4.52.0).
|
|
7
|
+
*
|
|
8
|
+
* The "no hardcoded user-facing strings" rule must be enforced DETERMINISTICALLY,
|
|
9
|
+
* regardless of the consumer's lint setup. The most-adopted rule
|
|
10
|
+
* (`eslint-plugin-i18next` `no-literal-string`) is framework-agnostic (it flags
|
|
11
|
+
* literal JSX text/attributes for next-intl / i18next / vue / any stack) BUT it is
|
|
12
|
+
* an ESLint plugin — a Biome-only toolchain (BALDART's default) does not run it,
|
|
13
|
+
* and wiring it into the user's own ESLint config is fragile.
|
|
14
|
+
*
|
|
15
|
+
* So BALDART writes a SEPARATE, self-contained config it owns and runs ESLint
|
|
16
|
+
* against it with an explicit `--config`. This never touches the user's main lint
|
|
17
|
+
* config, runs the i18n rule deterministically, and works on Biome projects (a
|
|
18
|
+
* dedicated ESLint invocation just for this gate).
|
|
19
|
+
*
|
|
20
|
+
* ESLint era matters: ESLint 9 uses FLAT config (`eslint.i18n.config.mjs`); ESLint
|
|
21
|
+
* 8 uses LEGACY rc config (`.eslintrc.i18n.json` + `--no-eslintrc`). We detect the
|
|
22
|
+
* consumer's installed/declared ESLint major and emit the matching pair so the
|
|
23
|
+
* gate command actually runs instead of erroring on every file. `mode: 'jsx-only'`
|
|
24
|
+
* is the default — it flags JSX text AND user-facing attributes (title /
|
|
25
|
+
* placeholder / alt / aria-label), matching the prose contract; non-JSX
|
|
26
|
+
* imperative strings (e.g. `toast("Saved")`) remain the code-reviewer backstop's
|
|
27
|
+
* domain. Verified against ESLint 9 + eslint-plugin-i18next: Bad.tsx (3 findings)
|
|
28
|
+
* fails, Good.tsx (t()) passes.
|
|
29
|
+
*/
|
|
30
|
+
const FLAT_CONFIG = 'eslint.i18n.config.mjs';
|
|
31
|
+
const LEGACY_CONFIG = '.eslintrc.i18n.json';
|
|
32
|
+
const MODE = 'jsx-only';
|
|
33
|
+
|
|
34
|
+
const FLAT_COMMAND = `npx eslint --config ${FLAT_CONFIG} .`;
|
|
35
|
+
const LEGACY_COMMAND = `npx eslint --no-eslintrc -c ${LEGACY_CONFIG} --ext .js,.jsx,.ts,.tsx .`;
|
|
36
|
+
// Default advertised command (flat / ESLint 9). `resolveLintCommand` picks the
|
|
37
|
+
// real one based on which config file is on disk.
|
|
38
|
+
const LINT_COMMAND = FLAT_COMMAND;
|
|
39
|
+
|
|
40
|
+
const FLAT_DEPS = ['eslint', 'eslint-plugin-i18next', 'typescript-eslint'];
|
|
41
|
+
const LEGACY_DEPS = ['eslint', 'eslint-plugin-i18next', '@typescript-eslint/parser'];
|
|
42
|
+
// Advertised set (flat). `setup` installs the era-correct one.
|
|
43
|
+
const DEV_DEPS = FLAT_DEPS;
|
|
44
|
+
|
|
45
|
+
const FLAT_BODY = `// BALDART-owned i18n anti-hardcoded gate (generated by \`baldart configure\` /
|
|
46
|
+
// \`baldart doctor\`, since v4.52.0). STANDALONE — this file does NOT affect your
|
|
47
|
+
// main ESLint setup. ESLint 9 flat config. Run via: ${FLAT_COMMAND}
|
|
48
|
+
//
|
|
49
|
+
// Fails when a user-facing string is hardcoded instead of routed through your i18n
|
|
50
|
+
// translation function. \`mode: '${MODE}'\` flags JSX text AND user-facing
|
|
51
|
+
// attributes (title/placeholder/alt/aria-label). Broaden to 'all' for non-JSX
|
|
52
|
+
// strings too, or add to \`ignores\` / the rule allowlist — BALDART writes this file
|
|
53
|
+
// only when absent and never overwrites your edits.
|
|
54
|
+
import i18next from 'eslint-plugin-i18next';
|
|
55
|
+
import tseslint from 'typescript-eslint';
|
|
56
|
+
|
|
57
|
+
export default [
|
|
58
|
+
{
|
|
59
|
+
files: ['**/*.{js,jsx,ts,tsx,mjs,cjs}'],
|
|
60
|
+
ignores: [
|
|
61
|
+
'**/node_modules/**', '**/dist/**', '**/build/**', '**/.next/**',
|
|
62
|
+
'**/out/**', '**/coverage/**', '**/*.test.*', '**/*.spec.*',
|
|
63
|
+
],
|
|
64
|
+
languageOptions: {
|
|
65
|
+
parser: tseslint.parser,
|
|
66
|
+
parserOptions: { ecmaFeatures: { jsx: true }, sourceType: 'module' },
|
|
67
|
+
},
|
|
68
|
+
plugins: { i18next },
|
|
69
|
+
rules: {
|
|
70
|
+
'i18next/no-literal-string': ['error', { mode: '${MODE}' }],
|
|
71
|
+
},
|
|
72
|
+
},
|
|
73
|
+
];
|
|
74
|
+
`;
|
|
75
|
+
|
|
76
|
+
const LEGACY_BODY = JSON.stringify({
|
|
77
|
+
root: true,
|
|
78
|
+
parser: '@typescript-eslint/parser',
|
|
79
|
+
parserOptions: { ecmaFeatures: { jsx: true }, sourceType: 'module' },
|
|
80
|
+
plugins: ['i18next'],
|
|
81
|
+
rules: { 'i18next/no-literal-string': ['error', { mode: MODE }] },
|
|
82
|
+
ignorePatterns: ['node_modules/', 'dist/', 'build/', '.next/', 'out/', 'coverage/', '*.test.*', '*.spec.*'],
|
|
83
|
+
}, null, 2) + '\n';
|
|
84
|
+
|
|
85
|
+
function hasPackageJson(cwd = process.cwd()) {
|
|
86
|
+
return fs.existsSync(path.join(cwd, 'package.json'));
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function readPkgDeps(cwd = process.cwd()) {
|
|
90
|
+
try {
|
|
91
|
+
const pkg = JSON.parse(fs.readFileSync(path.join(cwd, 'package.json'), 'utf8'));
|
|
92
|
+
return { ...(pkg.dependencies || {}), ...(pkg.devDependencies || {}) };
|
|
93
|
+
} catch { return {}; }
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Detect the consumer's ESLint major. Prefers the installed version, then the
|
|
98
|
+
* declared range, then null (absent → a fresh install gets ESLint 9 → flat).
|
|
99
|
+
*/
|
|
100
|
+
function eslintMajor(cwd = process.cwd()) {
|
|
101
|
+
try {
|
|
102
|
+
const p = JSON.parse(fs.readFileSync(path.join(cwd, 'node_modules', 'eslint', 'package.json'), 'utf8'));
|
|
103
|
+
const m = parseInt(String(p.version).split('.')[0], 10);
|
|
104
|
+
if (!Number.isNaN(m)) return m;
|
|
105
|
+
} catch { /* not installed */ }
|
|
106
|
+
const deps = readPkgDeps(cwd);
|
|
107
|
+
if (deps.eslint) {
|
|
108
|
+
const m = String(deps.eslint).match(/(\d+)/);
|
|
109
|
+
if (m) return parseInt(m[1], 10);
|
|
110
|
+
}
|
|
111
|
+
return null;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/** Legacy when an EXISTING ESLint 8 is detected; flat otherwise (incl. fresh). */
|
|
115
|
+
function useLegacy(cwd = process.cwd()) {
|
|
116
|
+
return eslintMajor(cwd) === 8;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function configFilename(cwd = process.cwd()) {
|
|
120
|
+
return useLegacy(cwd) ? LEGACY_CONFIG : FLAT_CONFIG;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function depsFor(cwd = process.cwd()) {
|
|
124
|
+
return useLegacy(cwd) ? LEGACY_DEPS : FLAT_DEPS;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/** True when EITHER era's config file is on disk. */
|
|
128
|
+
function isConfigPresent(cwd = process.cwd()) {
|
|
129
|
+
return fs.existsSync(path.join(cwd, FLAT_CONFIG)) || fs.existsSync(path.join(cwd, LEGACY_CONFIG));
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/** Write the era-correct standalone config — only when absent. Never overwrites. */
|
|
133
|
+
function writeConfig(cwd = process.cwd()) {
|
|
134
|
+
const legacy = useLegacy(cwd);
|
|
135
|
+
const file = legacy ? LEGACY_CONFIG : FLAT_CONFIG;
|
|
136
|
+
const p = path.join(cwd, file);
|
|
137
|
+
if (isConfigPresent(cwd)) return { status: 'exists', path: p, file };
|
|
138
|
+
fs.writeFileSync(p, legacy ? LEGACY_BODY : FLAT_BODY, 'utf8');
|
|
139
|
+
return { status: 'written', path: p, file };
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/** True when every era-correct gate dependency is declared in package.json. */
|
|
143
|
+
function depsInstalled(cwd = process.cwd()) {
|
|
144
|
+
const deps = readPkgDeps(cwd);
|
|
145
|
+
return depsFor(cwd).every((d) => d in deps);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/** Install the era-correct gate dev dependencies. Throws on failure. */
|
|
149
|
+
function installDeps(cwd = process.cwd()) {
|
|
150
|
+
execSync(`npm install -D ${depsFor(cwd).join(' ')}`, { cwd, stdio: 'ignore' });
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Full setup: install deps (if a package.json exists) + write the era-correct
|
|
155
|
+
* config. Returns a structured result; never throws (errors are reported).
|
|
156
|
+
*/
|
|
157
|
+
function setup(cwd = process.cwd()) {
|
|
158
|
+
const result = { installed: false, config: null, error: null, era: useLegacy(cwd) ? 'legacy' : 'flat' };
|
|
159
|
+
try {
|
|
160
|
+
if (hasPackageJson(cwd) && !depsInstalled(cwd)) {
|
|
161
|
+
installDeps(cwd);
|
|
162
|
+
result.installed = true;
|
|
163
|
+
}
|
|
164
|
+
result.config = writeConfig(cwd);
|
|
165
|
+
} catch (e) {
|
|
166
|
+
result.error = e.message;
|
|
167
|
+
}
|
|
168
|
+
return result;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Resolve the command a gate runner should execute. Order: explicit
|
|
173
|
+
* `i18n.lint_command` from config → the on-disk standalone config (flat or
|
|
174
|
+
* legacy) by convention → null (gate SKIPs, never a silent pass).
|
|
175
|
+
*/
|
|
176
|
+
function resolveLintCommand(cwd = process.cwd(), config = null) {
|
|
177
|
+
const explicit = config && config.i18n && config.i18n.lint_command;
|
|
178
|
+
if (explicit) return explicit;
|
|
179
|
+
if (fs.existsSync(path.join(cwd, FLAT_CONFIG))) return FLAT_COMMAND;
|
|
180
|
+
if (fs.existsSync(path.join(cwd, LEGACY_CONFIG))) return LEGACY_COMMAND;
|
|
181
|
+
return null;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/** The command configure should record for this project's ESLint era. */
|
|
185
|
+
function commandForCwd(cwd = process.cwd()) {
|
|
186
|
+
return useLegacy(cwd) ? LEGACY_COMMAND : FLAT_COMMAND;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
module.exports = {
|
|
190
|
+
FLAT_CONFIG, LEGACY_CONFIG, MODE, FLAT_COMMAND, LEGACY_COMMAND, LINT_COMMAND,
|
|
191
|
+
FLAT_DEPS, LEGACY_DEPS, DEV_DEPS,
|
|
192
|
+
hasPackageJson, eslintMajor, useLegacy, configFilename, depsFor,
|
|
193
|
+
isConfigPresent, writeConfig, depsInstalled, installDeps, setup,
|
|
194
|
+
resolveLintCommand, commandForCwd,
|
|
195
|
+
};
|