From 10e7c5800fef43fafacc0d043e6d712ab054be5b Mon Sep 17 00:00:00 2001 From: L4XB Date: Thu, 22 Oct 2026 23:51:32 +0200 Subject: [PATCH] fix(hooks): recognize Prettier's TypeScript and JSON5 config files Prettier's config searcher also reads .prettierrc.json5, .prettierrc.ts/.mts/.cts and prettier.config.ts/.mts/.cts. resolve-formatter knew none of them, so a project configured only through prettier.config.ts was never formatted by post-edit-format, quality-gate or the Stop batch. config-protection's rc pattern stopped at .yaml/.toml, so an existing .prettierrc.ts or .prettierrc.json5 could be edited freely. --- scripts/hooks/config-protection.js | 5 +++-- scripts/lib/resolve-formatter.js | 11 ++++++++++- tests/hooks/config-protection.test.js | 19 +++++++++++++++++++ tests/lib/resolve-formatter.test.js | 15 +++++++++++++++ 4 files changed, 47 insertions(+), 3 deletions(-) diff --git a/scripts/hooks/config-protection.js b/scripts/hooks/config-protection.js index 75a7a0781..8dce8b2ea 100644 --- a/scripts/hooks/config-protection.js +++ b/scripts/hooks/config-protection.js @@ -117,8 +117,9 @@ const PROTECTED_FILES = new Set([ const PROTECTED_PATTERNS = [ // eslint.config.base.mjs, prettier.config.shared.cjs, stylelint.config.local.js ... /^(eslint|prettier|stylelint|commitlint|oxlint)\.config(\.[A-Za-z0-9_-]+)*\.(js|mjs|cjs|ts|mts|cts)$/i, - // .eslintrc.base.json, .prettierrc.shared.yml ... - /^\.(eslintrc|prettierrc|stylelintrc|markdownlintrc)(\.[A-Za-z0-9_-]+)*\.(js|cjs|mjs|json|jsonc|yml|yaml|toml)$/i, + // .eslintrc.base.json, .prettierrc.shared.yml ... Prettier also reads + // `.prettierrc.ts`, `.mts`, `.cts` and `.json5`. + /^\.(eslintrc|prettierrc|stylelintrc|markdownlintrc)(\.[A-Za-z0-9_-]+)*\.(js|cjs|mjs|ts|mts|cts|json|jsonc|json5|yml|yaml|toml)$/i, ]; function isProtectedName(basename) { diff --git a/scripts/lib/resolve-formatter.js b/scripts/lib/resolve-formatter.js index 3fec591c9..251264e78 100644 --- a/scripts/lib/resolve-formatter.js +++ b/scripts/lib/resolve-formatter.js @@ -32,7 +32,16 @@ const PRETTIER_CONFIGS = [ '.prettierrc.toml', 'prettier.config.js', 'prettier.config.cjs', - 'prettier.config.mjs' + 'prettier.config.mjs', + // The rest of the names Prettier's config searcher looks for + // (src/config/prettier-config/config-searcher.js). + '.prettierrc.json5', + '.prettierrc.ts', + '.prettierrc.mts', + '.prettierrc.cts', + 'prettier.config.ts', + 'prettier.config.mts', + 'prettier.config.cts' ]; const PROJECT_ROOT_MARKERS = ['package.json', ...BIOME_CONFIGS, ...PRETTIER_CONFIGS]; diff --git a/tests/hooks/config-protection.test.js b/tests/hooks/config-protection.test.js index b0b88ae54..9ab7d844f 100644 --- a/tests/hooks/config-protection.test.js +++ b/tests/hooks/config-protection.test.js @@ -447,6 +447,25 @@ function runTests() { }) ); + results.push( + test("blocks Prettier's TypeScript and JSON5 rc files", () => { + return withOwnedDirectory(fs.mkdtempSync(path.join(os.tmpdir(), 'ecc-config-protect-prettier-')), tmpDir => { + for (const name of ['.prettierrc.ts', '.prettierrc.mts', '.prettierrc.cts', '.prettierrc.json5', '.prettierrc.shared.ts']) { + const absPath = path.join(tmpDir, name); + fs.writeFileSync(absPath, 'export default {};\n'); + + const result = runHook({ tool_name: 'Edit', tool_input: { file_path: absPath } }); + + assert.strictEqual(result.code, 2, 'Expected ' + name + ' to be blocked'); + assert.ok( + result.stderr.includes('BLOCKED: Modifying ' + name + ' is not allowed.'), + 'Expected block message for ' + name + ', got: ' + result.stderr + ); + } + }); + }) + ); + results.push( test('does not block build or test tooling configs', () => { // Pins the boundary: this hook guards LINTER configs. A future widening diff --git a/tests/lib/resolve-formatter.test.js b/tests/lib/resolve-formatter.test.js index dc478fc84..42185725f 100644 --- a/tests/lib/resolve-formatter.test.js +++ b/tests/lib/resolve-formatter.test.js @@ -155,6 +155,21 @@ function runTests() { assert.strictEqual(detectFormatter(root), 'prettier'); }); + run("detectFormatter: detects Prettier's TypeScript and JSON5 config files", () => { + const names = ['.prettierrc.json5', '.prettierrc.ts', '.prettierrc.mts', '.prettierrc.cts', + 'prettier.config.ts', 'prettier.config.mts', 'prettier.config.cts']; + for (const name of names) { + const root = makeTmpDir(); + fs.writeFileSync(path.join(root, 'package.json'), JSON.stringify({ name: 'test' })); + fs.writeFileSync(path.join(root, name), 'export default {};\n'); + assert.strictEqual(detectFormatter(root), 'prettier', name); + const nested = path.join(root, 'src'); + fs.mkdirSync(nested); + fs.rmSync(path.join(root, 'package.json')); + assert.strictEqual(findProjectRoot(nested), root, `${name} marks the project root`); + } + }); + run('detectFormatter: detects prettier key in package.json', () => { const root = makeTmpDir(); fs.writeFileSync(path.join(root, 'package.json'), JSON.stringify({ name: 'test', prettier: { singleQuote: true } }));