From f0604a5ea94791b060ac03f37c82058d8b2893e5 Mon Sep 17 00:00:00 2001 From: "Robert (Jamie) Munro" Date: Wed, 6 May 2026 16:32:10 +0200 Subject: [PATCH 01/15] feat: add sofie-code-preset-setup bin script Adds a new `sofie-code-preset-setup` CLI command that automates setting up or updating a project to use this preset. It: - Errors if the project does not use yarn - Sets the `prettier` config key in package.json - Adds/updates lint, lint:fix, lint:eslint, lint:prettier, and license-validate scripts - Adds a prepare script for husky if not already present - Sets up lint-staged config in package.json - Creates eslint.config.mjs if it does not already exist - Copies the .editorconfig from the preset - Creates .husky/pre-commit if it does not already exist Also adds .editorconfig to the published files list. --- bin/setup.mjs | 151 ++++++++++++++++++++++++++++++++++++++++++++++++++ package.json | 2 + yarn.lock | 1 + 3 files changed, 154 insertions(+) create mode 100755 bin/setup.mjs diff --git a/bin/setup.mjs b/bin/setup.mjs new file mode 100755 index 0000000..cbd8232 --- /dev/null +++ b/bin/setup.mjs @@ -0,0 +1,151 @@ +#!/usr/bin/env node +'use strict' + +import { existsSync } from 'fs' +import { copyFile, mkdir, readFile, writeFile } from 'fs/promises' +import path from 'path' +import { fileURLToPath } from 'url' + +const scriptDir = path.dirname(fileURLToPath(import.meta.url)) +const projectDir = process.cwd() + +// ── 1. Find and parse the project's package.json ────────────────────────────── + +const pkgPath = path.join(projectDir, 'package.json') +if (!existsSync(pkgPath)) { + console.error('Error: No package.json found in the current directory.') + process.exit(1) +} + +let pkgText +try { + pkgText = await readFile(pkgPath, 'utf-8') +} catch (e) { + console.error(`Error reading package.json: ${e.message}`) + process.exit(1) +} + +let pkg +try { + pkg = JSON.parse(pkgText) +} catch (e) { + console.error(`Error parsing package.json: ${e.message}`) + process.exit(1) +} + +// ── 2. Require yarn ──────────────────────────────────────────────────────────── + +const pmField = pkg.packageManager ?? '' +if (pmField && !pmField.startsWith('yarn')) { + console.error(`Error: package.json declares packageManager "${pmField}". This tool requires yarn.`) + process.exit(1) +} +if (!pmField) { + if (existsSync(path.join(projectDir, 'package-lock.json'))) { + console.error('Error: Found a package-lock.json. This tool requires yarn.') + process.exit(1) + } + if (existsSync(path.join(projectDir, 'pnpm-lock.yaml'))) { + console.error('Error: Found a pnpm-lock.yaml. This tool requires yarn.') + process.exit(1) + } +} + +// ── 3. Update package.json ──────────────────────────────────────────────────── + +let pkgChanged = false +// Preserve the original indentation style +const indent = pkgText.match(/^\t/m) ? '\t' : ' ' + +function markChanged(label) { + pkgChanged = true + console.log(` \u2714 ${label}`) +} + +// prettier config +const prettierValue = '@sofie-automation/code-standard-preset/prettier.config.mjs' +if (pkg.prettier !== prettierValue) { + pkg.prettier = prettierValue + markChanged('Set prettier config') +} + +// lint scripts — always update preset-owned scripts; add "prepare" only if absent +pkg.scripts ??= {} +const presetScripts = { + 'lint:eslint': 'eslint .', + 'lint:prettier': 'prettier --check .', + lint: 'yarn lint:eslint && yarn lint:prettier', + 'lint:fix': 'yarn lint:eslint --fix && yarn lint:prettier --write', + 'license-validate': 'sofie-licensecheck', +} +for (const [name, cmd] of Object.entries(presetScripts)) { + if (pkg.scripts[name] !== cmd) { + pkg.scripts[name] = cmd + markChanged(`Set script "${name}"`) + } +} +if (!pkg.scripts.prepare) { + pkg.scripts.prepare = 'husky' + markChanged('Set script "prepare" (husky)') +} + +// lint-staged +const targetLintStaged = { + '*.{css,json,md,scss}': ['prettier --write'], + '*.{ts,tsx,js,jsx,mjs,cjs}': ['eslint --fix'], +} +if (JSON.stringify(pkg['lint-staged']) !== JSON.stringify(targetLintStaged)) { + pkg['lint-staged'] = targetLintStaged + markChanged('Set lint-staged config') +} + +if (pkgChanged) { + await writeFile(pkgPath, JSON.stringify(pkg, null, indent) + '\n', 'utf-8') + console.log(' \u2714 Wrote package.json') +} else { + console.log(' - package.json already up to date') +} + +// ── 4. Create eslint.config.mjs if missing ──────────────────────────────────── + +const eslintConfigPath = path.join(projectDir, 'eslint.config.mjs') +if (!existsSync(eslintConfigPath)) { + await writeFile( + eslintConfigPath, + [ + "import { generateEslintConfig } from '@sofie-automation/code-standard-preset/eslint/main.mjs'", + '', + 'export default await generateEslintConfig({})', + '', + ].join('\n'), + 'utf-8' + ) + console.log(' \u2714 Created eslint.config.mjs') +} else { + console.log(' - eslint.config.mjs already exists, skipping') +} + +// ── 5. Copy .editorconfig ───────────────────────────────────────────────────── + +const srcEditorconfig = path.join(scriptDir, '..', '.editorconfig') +const destEditorconfig = path.join(projectDir, '.editorconfig') +await copyFile(srcEditorconfig, destEditorconfig) +console.log(' \u2714 Copied .editorconfig') + +// ── 6. Create .husky/pre-commit if missing ──────────────────────────────────── + +const preCommitPath = path.join(projectDir, '.husky', 'pre-commit') +if (!existsSync(preCommitPath)) { + await mkdir(path.join(projectDir, '.husky'), { recursive: true }) + await writeFile(preCommitPath, 'lint-staged\n', { encoding: 'utf-8', mode: 0o755 }) + console.log(' \u2714 Created .husky/pre-commit') +} else { + console.log(' - .husky/pre-commit already exists, skipping') +} + +// ── Done ────────────────────────────────────────────────────────────────────── + +console.log('\nDone. Next steps:') +console.log(' 1. yarn add --dev eslint typescript husky lint-staged prettier') +console.log(' 2. yarn install (to initialize husky via the prepare script)') +console.log(' 3. Review and commit the changes') diff --git a/package.json b/package.json index 34da14a..149adf7 100644 --- a/package.json +++ b/package.json @@ -28,6 +28,7 @@ "node": ">= 22.12" }, "bin": { + "sofie-code-standard-preset-setup": "./bin/setup.mjs", "sofie-licensecheck": "./bin/checkLicenses.mjs", "sofie-version": "./bin/updateVersion.mjs" }, @@ -47,6 +48,7 @@ "files": [ "/CHANGELOG.md", "/README.md", + "/.editorconfig", "/ts", "/bin", "/eslint", diff --git a/yarn.lock b/yarn.lock index c7b82ff..729b05b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -407,6 +407,7 @@ __metadata: prettier: ^3 typescript: ~6.0 bin: + sofie-code-standard-preset-setup: ./bin/setup.mjs sofie-licensecheck: ./bin/checkLicenses.mjs sofie-version: ./bin/updateVersion.mjs languageName: unknown From 8b05a25f511c05decebb4fcaa46c85dc5cb5b6f9 Mon Sep 17 00:00:00 2001 From: "Robert (Jamie) Munro" Date: Wed, 6 May 2026 17:56:16 +0200 Subject: [PATCH 02/15] feat(setup): don't override existing lint* scripts unless --force --- bin/setup.mjs | 42 ++++++++++++++++++++++++++++++++++++++---- 1 file changed, 38 insertions(+), 4 deletions(-) diff --git a/bin/setup.mjs b/bin/setup.mjs index cbd8232..cd42983 100755 --- a/bin/setup.mjs +++ b/bin/setup.mjs @@ -8,6 +8,7 @@ import { fileURLToPath } from 'url' const scriptDir = path.dirname(fileURLToPath(import.meta.url)) const projectDir = process.cwd() +const force = process.argv.includes('--force') // ── 1. Find and parse the project's package.json ────────────────────────────── @@ -69,21 +70,54 @@ if (pkg.prettier !== prettierValue) { markChanged('Set prettier config') } -// lint scripts — always update preset-owned scripts; add "prepare" only if absent +// lint scripts — add only if absent (or --force for lint* scripts); add "prepare" only if absent pkg.scripts ??= {} -const presetScripts = { + +// These scripts are skipped if already set (unless --force) +const lintSubScripts = { 'lint:eslint': 'eslint .', 'lint:prettier': 'prettier --check .', - lint: 'yarn lint:eslint && yarn lint:prettier', 'lint:fix': 'yarn lint:eslint --fix && yarn lint:prettier --write', +} +for (const [name, cmd] of Object.entries(lintSubScripts)) { + if (pkg.scripts[name] === cmd) { + // already correct, nothing to do + } else if (!pkg.scripts[name] || force) { + pkg.scripts[name] = cmd + markChanged(`Set script "${name}"`) + } else { + console.log(` - Skipping script "${name}" (already set) — use --force to override`) + } +} + +// Only add the "lint" umbrella if both sub-scripts are now at the expected values +const lintUmbrella = 'yarn lint:eslint && yarn lint:prettier' +const eslintReady = pkg.scripts['lint:eslint'] === lintSubScripts['lint:eslint'] +const prettierReady = pkg.scripts['lint:prettier'] === lintSubScripts['lint:prettier'] +if (eslintReady && prettierReady) { + if (pkg.scripts.lint === lintUmbrella) { + // already correct, nothing to do + } else if (!pkg.scripts.lint || force) { + pkg.scripts.lint = lintUmbrella + markChanged('Set script "lint"') + } else { + console.log(` - Skipping script "lint" (already set) — use --force to override`) + } +} else if (pkg.scripts.lint) { + console.log(' - Skipping script "lint" (lint:eslint or lint:prettier not set to expected values)') +} + +// These scripts are always set to the expected value +const alwaysSetScripts = { 'license-validate': 'sofie-licensecheck', } -for (const [name, cmd] of Object.entries(presetScripts)) { +for (const [name, cmd] of Object.entries(alwaysSetScripts)) { if (pkg.scripts[name] !== cmd) { pkg.scripts[name] = cmd markChanged(`Set script "${name}"`) } } + if (!pkg.scripts.prepare) { pkg.scripts.prepare = 'husky' markChanged('Set script "prepare" (husky)') From 0e8d0f88e82312186586132e9416c26db08c8402 Mon Sep 17 00:00:00 2001 From: "Robert (Jamie) Munro" Date: Wed, 6 May 2026 17:58:15 +0200 Subject: [PATCH 03/15] =?UTF-8?q?refactor(setup):=20unify=20all=20script?= =?UTF-8?q?=20handling=20=E2=80=94=20skip=20if=20set,=20--force=20to=20ove?= =?UTF-8?q?rride?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- bin/setup.mjs | 29 +++++++---------------------- 1 file changed, 7 insertions(+), 22 deletions(-) diff --git a/bin/setup.mjs b/bin/setup.mjs index cd42983..8f72b9c 100755 --- a/bin/setup.mjs +++ b/bin/setup.mjs @@ -70,16 +70,17 @@ if (pkg.prettier !== prettierValue) { markChanged('Set prettier config') } -// lint scripts — add only if absent (or --force for lint* scripts); add "prepare" only if absent +// scripts — skip if already set to a different value, unless --force pkg.scripts ??= {} -// These scripts are skipped if already set (unless --force) -const lintSubScripts = { +const presetScripts = { 'lint:eslint': 'eslint .', 'lint:prettier': 'prettier --check .', 'lint:fix': 'yarn lint:eslint --fix && yarn lint:prettier --write', + 'license-validate': 'sofie-licensecheck', + prepare: 'husky', } -for (const [name, cmd] of Object.entries(lintSubScripts)) { +for (const [name, cmd] of Object.entries(presetScripts)) { if (pkg.scripts[name] === cmd) { // already correct, nothing to do } else if (!pkg.scripts[name] || force) { @@ -92,8 +93,8 @@ for (const [name, cmd] of Object.entries(lintSubScripts)) { // Only add the "lint" umbrella if both sub-scripts are now at the expected values const lintUmbrella = 'yarn lint:eslint && yarn lint:prettier' -const eslintReady = pkg.scripts['lint:eslint'] === lintSubScripts['lint:eslint'] -const prettierReady = pkg.scripts['lint:prettier'] === lintSubScripts['lint:prettier'] +const eslintReady = pkg.scripts['lint:eslint'] === presetScripts['lint:eslint'] +const prettierReady = pkg.scripts['lint:prettier'] === presetScripts['lint:prettier'] if (eslintReady && prettierReady) { if (pkg.scripts.lint === lintUmbrella) { // already correct, nothing to do @@ -107,22 +108,6 @@ if (eslintReady && prettierReady) { console.log(' - Skipping script "lint" (lint:eslint or lint:prettier not set to expected values)') } -// These scripts are always set to the expected value -const alwaysSetScripts = { - 'license-validate': 'sofie-licensecheck', -} -for (const [name, cmd] of Object.entries(alwaysSetScripts)) { - if (pkg.scripts[name] !== cmd) { - pkg.scripts[name] = cmd - markChanged(`Set script "${name}"`) - } -} - -if (!pkg.scripts.prepare) { - pkg.scripts.prepare = 'husky' - markChanged('Set script "prepare" (husky)') -} - // lint-staged const targetLintStaged = { '*.{css,json,md,scss}': ['prettier --write'], From c739aeaa696b524a7aeb74292b6a1336ddc0db55 Mon Sep 17 00:00:00 2001 From: "Robert (Jamie) Munro" Date: Wed, 6 May 2026 18:05:44 +0200 Subject: [PATCH 04/15] feat(setup): skip lint-staged if already set unless --force --- bin/setup.mjs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/bin/setup.mjs b/bin/setup.mjs index 8f72b9c..229ab15 100755 --- a/bin/setup.mjs +++ b/bin/setup.mjs @@ -113,9 +113,13 @@ const targetLintStaged = { '*.{css,json,md,scss}': ['prettier --write'], '*.{ts,tsx,js,jsx,mjs,cjs}': ['eslint --fix'], } -if (JSON.stringify(pkg['lint-staged']) !== JSON.stringify(targetLintStaged)) { +if (JSON.stringify(pkg['lint-staged']) === JSON.stringify(targetLintStaged)) { + // already correct, nothing to do +} else if (!pkg['lint-staged'] || force) { pkg['lint-staged'] = targetLintStaged markChanged('Set lint-staged config') +} else { + console.log(' - Skipping lint-staged config (already set) — use --force to override') } if (pkgChanged) { From b9922c5d3c5a9014d9eba35beefa8deaa6ff9a8a Mon Sep 17 00:00:00 2001 From: "Robert (Jamie) Munro" Date: Wed, 6 May 2026 18:06:14 +0200 Subject: [PATCH 05/15] feat(setup): overwrite known old prettier config; warn and require --force for unknown values --- bin/setup.mjs | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/bin/setup.mjs b/bin/setup.mjs index 229ab15..8f7ee5c 100755 --- a/bin/setup.mjs +++ b/bin/setup.mjs @@ -65,9 +65,17 @@ function markChanged(label) { // prettier config const prettierValue = '@sofie-automation/code-standard-preset/prettier.config.mjs' -if (pkg.prettier !== prettierValue) { +if (pkg.prettier === prettierValue) { + // already correct, nothing to do +} else if ( + !pkg.prettier || + (typeof pkg.prettier === 'string' && pkg.prettier.startsWith('@sofie-automation/code-standard-preset/')) || + force +) { pkg.prettier = prettierValue markChanged('Set prettier config') +} else { + console.log(' - Skipping prettier config (already set to an unrecognised value) — use --force to override') } // scripts — skip if already set to a different value, unless --force From 18e6543829e6648e29ab3ccabc894648b1c050aa Mon Sep 17 00:00:00 2001 From: "Robert (Jamie) Munro" Date: Wed, 6 May 2026 18:07:38 +0200 Subject: [PATCH 06/15] feat(setup): add --help flag; document setup CLI in README --- README.md | 31 +++++++++++++++++++++++++++++-- bin/setup.mjs | 26 ++++++++++++++++++++++++++ 2 files changed, 55 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index a77554c..cf2a55c 100644 --- a/README.md +++ b/README.md @@ -22,7 +22,33 @@ This readme assumes you are using yarn v4. For other package managers the steps `yarn add --dev @sofie-automation/code-standard-preset eslint typescript husky lint-staged prettier` -### Packages +### Automated setup + +The easiest way to configure a project is to run the setup CLI: + +```sh +npx @sofie-automation/code-standard-preset/bin/setup.mjs +# or, if already installed as a devDependency: +yarn sofie-code-preset-setup +``` + +This will: + +1. Set the `prettier` field in `package.json` to use the preset's config +2. Add `lint`, `lint:eslint`, `lint:prettier`, `lint:fix`, `license-validate` and `prepare` scripts +3. Set `lint-staged` config +4. Create `eslint.config.mjs` if missing +5. Copy `.editorconfig` from the preset +6. Create `.husky/pre-commit` if missing +7. Install required devDependencies via `yarn add --dev` + +If any of the above items are already configured to a different value, they will be skipped with a warning. Pass `--force` to overwrite existing values. + +After running, review and commit the changes, then follow the manual steps below to add any project-specific configuration. + +### Manual setup + +#### Packages **Add** the following information to your `package.json`: @@ -92,9 +118,10 @@ The parameter to the `generateEslintConfig` contains various option fields that If you need to add additional rules, you can do so by building off the generated config, such as: ```mjs -import { generateEslintConfig } from '@sofie-automation/code-standard-preset/eslint/main.mjs' import pluginYaml from 'eslint-plugin-yml' +import { generateEslintConfig } from '@sofie-automation/code-standard-preset/eslint/main.mjs' + const extendedRules = await generateEslintConfig({ ignores: ['client', 'server'], }) diff --git a/bin/setup.mjs b/bin/setup.mjs index 8f7ee5c..1a9f52c 100755 --- a/bin/setup.mjs +++ b/bin/setup.mjs @@ -9,6 +9,32 @@ import { fileURLToPath } from 'url' const scriptDir = path.dirname(fileURLToPath(import.meta.url)) const projectDir = process.cwd() const force = process.argv.includes('--force') +const help = process.argv.includes('--help') || process.argv.includes('-h') + +if (help) { + console.log(` +Usage: sofie-code-preset-setup [--force] [--help] + +Configures the current project to use @sofie-automation/code-standard-preset. + +Steps performed: + 1. Reads package.json in the current directory + 2. Verifies the project uses yarn + 3. Sets "prettier" to point to the preset's prettier.config.mjs + 4. Adds lint scripts (lint, lint:eslint, lint:prettier, lint:fix) + 5. Adds license-validate and prepare (husky) scripts + 6. Sets lint-staged config + 7. Creates eslint.config.mjs if missing + 8. Copies .editorconfig from the preset + 9. Creates .husky/pre-commit if missing + 10. Installs required devDependencies via yarn add --dev + +Options: + --force Overwrite existing values that would otherwise be skipped + --help Show this help message +`) + process.exit(0) +} // ── 1. Find and parse the project's package.json ────────────────────────────── From 73c825dbc8a2e434d8e62ae09530885643f32d8c Mon Sep 17 00:00:00 2001 From: "Robert (Jamie) Munro" Date: Wed, 6 May 2026 18:09:53 +0200 Subject: [PATCH 07/15] feat(setup): auto-install devDependencies via yarn add --dev --- README.md | 6 ++---- bin/setup.mjs | 17 +++++++++++++---- 2 files changed, 15 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index cf2a55c..3730ea2 100644 --- a/README.md +++ b/README.md @@ -20,15 +20,13 @@ A script for checking compatible licenses is included. This readme assumes you are using yarn v4. For other package managers the steps should be similar but may vary a little from what is written here. -`yarn add --dev @sofie-automation/code-standard-preset eslint typescript husky lint-staged prettier` +`yarn add --dev @sofie-automation/code-standard-preset` ### Automated setup -The easiest way to configure a project is to run the setup CLI: +The easiest way to configure a project is to run the setup CLI, which will install all other required devDependencies and configure the project automatically: ```sh -npx @sofie-automation/code-standard-preset/bin/setup.mjs -# or, if already installed as a devDependency: yarn sofie-code-preset-setup ``` diff --git a/bin/setup.mjs b/bin/setup.mjs index 1a9f52c..3f95b6a 100755 --- a/bin/setup.mjs +++ b/bin/setup.mjs @@ -1,6 +1,7 @@ #!/usr/bin/env node 'use strict' +import { execFileSync } from 'child_process' import { existsSync } from 'fs' import { copyFile, mkdir, readFile, writeFile } from 'fs/promises' import path from 'path' @@ -200,9 +201,17 @@ if (!existsSync(preCommitPath)) { console.log(' - .husky/pre-commit already exists, skipping') } +// ── 7. Install devDependencies ─────────────────────────────────────────────── + +const devDeps = ['eslint', 'typescript', 'husky', 'lint-staged', 'prettier'] +console.log(`\nInstalling devDependencies: ${devDeps.join(', ')} ...`) +try { + execFileSync('yarn', ['add', '--dev', ...devDeps], { stdio: 'inherit', cwd: projectDir }) +} catch (e) { + console.error(`Error installing devDependencies: ${e.message}`) + console.error(` Run manually: yarn add --dev ${devDeps.join(' ')}`) +} + // ── Done ────────────────────────────────────────────────────────────────────── -console.log('\nDone. Next steps:') -console.log(' 1. yarn add --dev eslint typescript husky lint-staged prettier') -console.log(' 2. yarn install (to initialize husky via the prepare script)') -console.log(' 3. Review and commit the changes') +console.log('\nDone. Review and commit the changes.') From 18eb8e61e3be7827cbaa9a01f5684abafbf9a944 Mon Sep 17 00:00:00 2001 From: "Robert (Jamie) Munro" Date: Thu, 7 May 2026 17:45:31 +0200 Subject: [PATCH 08/15] fix: Install peer-compatible versions of eslint/typescript/prettier The setup script was running `yarn add --dev eslint` without a version constraint, picking up whatever the latest major was (e.g. eslint@10) instead of the version range declared in this preset's peerDependencies (eslint@^9). This caused ERR_MODULE_NOT_FOUND for @eslint/js because eslint@10 no longer bundles it. Now the script reads its own peerDependencies and appends the range when installing each peer dep (e.g. eslint@^9, typescript@~6.0, prettier@^3). --- bin/setup.mjs | 23 ++++++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/bin/setup.mjs b/bin/setup.mjs index 3f95b6a..99cd138 100755 --- a/bin/setup.mjs +++ b/bin/setup.mjs @@ -203,7 +203,28 @@ if (!existsSync(preCommitPath)) { // ── 7. Install devDependencies ─────────────────────────────────────────────── -const devDeps = ['eslint', 'typescript', 'husky', 'lint-staged', 'prettier'] +// Read the peer dependency versions from this preset's package.json so that +// we install compatible versions (e.g. eslint@^9 not the latest eslint@^10). +const presetPkgPath = path.join(scriptDir, '..', 'package.json') +let presetPkg = {} +try { + presetPkg = JSON.parse(await readFile(presetPkgPath, 'utf-8')) +} catch { + // Ignore – fall back to unversioned installs +} +const peerDeps = presetPkg.peerDependencies ?? {} + +function depWithVersion(name) { + return peerDeps[name] ? `${name}@${peerDeps[name]}` : name +} + +const devDeps = [ + depWithVersion('eslint'), + depWithVersion('typescript'), + 'husky', + 'lint-staged', + depWithVersion('prettier'), +] console.log(`\nInstalling devDependencies: ${devDeps.join(', ')} ...`) try { execFileSync('yarn', ['add', '--dev', ...devDeps], { stdio: 'inherit', cwd: projectDir }) From 1095417d19e0f64457232760fa3cdfbbce67de17 Mon Sep 17 00:00:00 2001 From: "Robert (Jamie) Munro" Date: Thu, 7 May 2026 19:39:34 +0200 Subject: [PATCH 09/15] fix: Detect and update existing .prettierrc.json when running setup script If the project dir contains a .prettierrc.json pointing to the old .prettierrc.json path (which no longer exists in the preset), update it to prettier.config.mjs. Prettier searches .prettierrc.json before the package.json "prettier" key, so a stale .prettierrc.json silently overrides the correct package.json config. --- bin/setup.mjs | 28 ++++++++++++++++++++++++++-- 1 file changed, 26 insertions(+), 2 deletions(-) diff --git a/bin/setup.mjs b/bin/setup.mjs index 99cd138..0c1b982 100755 --- a/bin/setup.mjs +++ b/bin/setup.mjs @@ -92,8 +92,32 @@ function markChanged(label) { // prettier config const prettierValue = '@sofie-automation/code-standard-preset/prettier.config.mjs' -if (pkg.prettier === prettierValue) { - // already correct, nothing to do + +// If there's a .prettierrc.json file, fix it rather than relying on the package.json key +// (prettier searches .prettierrc.json before the package.json "prettier" key) +const prettierrcPath = path.join(projectDir, '.prettierrc.json') +if (existsSync(prettierrcPath)) { + let existingContent + try { + existingContent = JSON.parse(await readFile(prettierrcPath, 'utf-8')) + } catch { + existingContent = null + } + if (existingContent === prettierValue) { + console.log(' - .prettierrc.json already correct, skipping') + } else if ( + existingContent === null || + (typeof existingContent === 'string' && + existingContent.startsWith('@sofie-automation/code-standard-preset/')) || + force + ) { + await writeFile(prettierrcPath, `"${prettierValue}"\n`, 'utf-8') + console.log(' \u2714 Fixed .prettierrc.json') + } else { + console.log(' - Skipping .prettierrc.json (already set to an unrecognised value) — use --force to override') + } +} else if (pkg.prettier === prettierValue) { + // package.json prettier key already correct, nothing to do } else if ( !pkg.prettier || (typeof pkg.prettier === 'string' && pkg.prettier.startsWith('@sofie-automation/code-standard-preset/')) || From 07ccff1878b5f25bcf6f058019376d1bf9567703 Mon Sep 17 00:00:00 2001 From: "Robert (Jamie) Munro" Date: Thu, 7 May 2026 19:39:43 +0200 Subject: [PATCH 10/15] feat: Add --fix-subpackages flag to clean up monorepo sub-package configs In a monorepo, prettier and ESLint config files in sub-packages shadow the root config. Running setup with --fix-subpackages will: - Remove the "prettier" key from sub-package package.json files (they inherit from the root via config walk-up) - Delete sub-package .prettierrc* files that are just references to the preset (root config handles this) - Delete legacy .eslintrc* files (replaced by flat config at root) - Note flat eslint.config.mjs files for manual review Without the flag, setup reports any issues found and suggests running with --fix-subpackages. --- bin/setup.mjs | 129 ++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 126 insertions(+), 3 deletions(-) diff --git a/bin/setup.mjs b/bin/setup.mjs index 0c1b982..cba4a70 100755 --- a/bin/setup.mjs +++ b/bin/setup.mjs @@ -3,13 +3,14 @@ import { execFileSync } from 'child_process' import { existsSync } from 'fs' -import { copyFile, mkdir, readFile, writeFile } from 'fs/promises' +import { copyFile, mkdir, readdir, readFile, unlink, writeFile } from 'fs/promises' import path from 'path' import { fileURLToPath } from 'url' const scriptDir = path.dirname(fileURLToPath(import.meta.url)) const projectDir = process.cwd() const force = process.argv.includes('--force') +const fixSubpackages = process.argv.includes('--fix-subpackages') const help = process.argv.includes('--help') || process.argv.includes('-h') if (help) { @@ -29,10 +30,13 @@ Steps performed: 8. Copies .editorconfig from the preset 9. Creates .husky/pre-commit if missing 10. Installs required devDependencies via yarn add --dev + 11. (with --fix-subpackages) Removes redundant "prettier" keys from sub-package + package.json files (they inherit from the root package.json via walk-up) Options: - --force Overwrite existing values that would otherwise be skipped - --help Show this help message + --force Overwrite existing values that would otherwise be skipped + --fix-subpackages Remove redundant prettier config from sub-packages + --help Show this help message `) process.exit(0) } @@ -257,6 +261,125 @@ try { console.error(` Run manually: yarn add --dev ${devDeps.join(' ')}`) } +// ── 8. Fix sub-package config files ───────────────────────────────────────── + +// In a monorepo, config files in sub-packages shadow the root config. +// Sub-packages typically don't need their own prettier or legacy eslint configs. +console.log('\n── Sub-package config files ──') + +const prettierConfigFileNames = [ + '.prettierrc', + '.prettierrc.json', + '.prettierrc.js', + '.prettierrc.cjs', + '.prettierrc.mjs', + '.prettierrc.yaml', + '.prettierrc.yml', + '.prettierrc.toml', + 'prettier.config.js', + 'prettier.config.cjs', + 'prettier.config.mjs', +] +const eslintLegacyConfigFileNames = [ + '.eslintrc', + '.eslintrc.js', + '.eslintrc.cjs', + '.eslintrc.json', + '.eslintrc.yaml', + '.eslintrc.yml', +] + +const subPkgWarnings = [] +try { + const entries = await readdir(projectDir, { withFileTypes: true }) + for (const entry of entries) { + if (!entry.isDirectory()) continue + const subDir = path.join(projectDir, entry.name) + + // package.json "prettier" key + const subPkgPath = path.join(subDir, 'package.json') + if (existsSync(subPkgPath)) { + let subPkg, subPkgText + try { + subPkgText = await readFile(subPkgPath, 'utf-8') + subPkg = JSON.parse(subPkgText) + } catch { + subPkg = null + } + if (subPkg?.prettier) { + const rel = path.join(entry.name, 'package.json') + if (fixSubpackages) { + const subIndent = subPkgText.match(/^\t/m) ? '\t' : ' ' + delete subPkg.prettier + await writeFile(subPkgPath, JSON.stringify(subPkg, null, subIndent) + '\n', 'utf-8') + console.log(` ✔ Removed "prettier" key from ${rel}`) + } else { + subPkgWarnings.push(`${rel}: has "prettier" key`) + } + } + } + + // Prettier config files + for (const file of prettierConfigFileNames) { + const filePath = path.join(subDir, file) + if (!existsSync(filePath)) continue + const rel = path.join(entry.name, file) + if (fixSubpackages) { + let content = null + try { + content = await readFile(filePath, 'utf-8') + } catch { + /* ignore */ + } + let parsed + try { + parsed = JSON.parse(content) + } catch { + parsed = content + } + if (typeof parsed === 'string' && parsed.startsWith('@sofie-automation/code-standard-preset/')) { + await unlink(filePath) + console.log(` ✔ Removed ${rel} (preset reference — root config handles this)`) + } else { + console.log(` - Skipping ${rel} (unrecognised content) — review manually`) + } + } else { + subPkgWarnings.push(`${rel}: prettier config file`) + } + } + + // Legacy ESLint config files (conflict with flat config at root) + for (const file of eslintLegacyConfigFileNames) { + const filePath = path.join(subDir, file) + if (!existsSync(filePath)) continue + const rel = path.join(entry.name, file) + if (fixSubpackages) { + await unlink(filePath) + console.log(` ✔ Removed ${rel} (legacy ESLint config — flat config at root handles this)`) + } else { + subPkgWarnings.push(`${rel}: legacy ESLint config`) + } + } + + // New-style flat ESLint config in a sub-package — don't auto-remove, just note it + const subEslintFlat = path.join(subDir, 'eslint.config.mjs') + if (existsSync(subEslintFlat)) { + console.log(` - Note: ${path.join(entry.name, 'eslint.config.mjs')} exists — review if intentional`) + } + } +} catch (e) { + console.error(` Warning: could not scan sub-packages: ${e.message}`) +} + +if (subPkgWarnings.length > 0) { + console.log( + ` - Found ${subPkgWarnings.length} item(s) in sub-packages — run with --fix-subpackages to fix them:\n` + + subPkgWarnings.map((f) => ` ${f}`).join('\n') + ) +} else if (!fixSubpackages) { + console.log(' - No sub-package config issues found') +} + // ── Done ────────────────────────────────────────────────────────────────────── console.log('\nDone. Review and commit the changes.') From 7308afee0ec66f2f785d8c092ca7316b3606d9fa Mon Sep 17 00:00:00 2001 From: "Robert (Jamie) Munro" Date: Thu, 7 May 2026 22:04:28 +0200 Subject: [PATCH 11/15] fix: Use check-only commands in lint-staged config Pre-commit hooks should fail and notify, not silently auto-fix. Use 'prettier --check' and 'eslint' instead of '--write'/'--fix'. --- bin/setup.mjs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/bin/setup.mjs b/bin/setup.mjs index cba4a70..af63003 100755 --- a/bin/setup.mjs +++ b/bin/setup.mjs @@ -172,9 +172,11 @@ if (eslintReady && prettierReady) { } // lint-staged +// Use check-only commands: pre-commit should notify and fail, not silently +// auto-fix (lint-staged doesn't re-add modified files to the commit index) const targetLintStaged = { - '*.{css,json,md,scss}': ['prettier --write'], - '*.{ts,tsx,js,jsx,mjs,cjs}': ['eslint --fix'], + '*.{css,json,md,scss}': ['prettier --check'], + '*.{ts,tsx,js,jsx,mjs,cjs}': ['eslint'], } if (JSON.stringify(pkg['lint-staged']) === JSON.stringify(targetLintStaged)) { // already correct, nothing to do From 8c68658079f45b2a8cc50b182f0cd934884db8cc Mon Sep 17 00:00:00 2001 From: "Robert (Jamie) Munro" Date: Sat, 9 May 2026 11:25:17 +0100 Subject: [PATCH 12/15] feat: Add husky@^9 and lint-staged@^17 to peerDependencies These are installed by the setup script, so they should be declared as peer dependencies with version constraints, matching what's in use across other Sofie projects. --- bin/setup.mjs | 8 +------- package.json | 2 ++ yarn.lock | 2 ++ 3 files changed, 5 insertions(+), 7 deletions(-) diff --git a/bin/setup.mjs b/bin/setup.mjs index af63003..775a678 100755 --- a/bin/setup.mjs +++ b/bin/setup.mjs @@ -248,13 +248,7 @@ function depWithVersion(name) { return peerDeps[name] ? `${name}@${peerDeps[name]}` : name } -const devDeps = [ - depWithVersion('eslint'), - depWithVersion('typescript'), - 'husky', - 'lint-staged', - depWithVersion('prettier'), -] +const devDeps = ['eslint', 'husky', 'lint-staged', 'prettier', 'typescript'].map(depWithVersion) console.log(`\nInstalling devDependencies: ${devDeps.join(', ')} ...`) try { execFileSync('yarn', ['add', '--dev', ...devDeps], { stdio: 'inherit', cwd: projectDir }) diff --git a/package.json b/package.json index 149adf7..1de831c 100644 --- a/package.json +++ b/package.json @@ -78,6 +78,8 @@ }, "peerDependencies": { "eslint": "^9", + "husky": "^9", + "lint-staged": "^17", "prettier": "^3", "typescript": "~6.0" }, diff --git a/yarn.lock b/yarn.lock index 729b05b..c27aa06 100644 --- a/yarn.lock +++ b/yarn.lock @@ -404,6 +404,8 @@ __metadata: typescript-eslint: "npm:8.58.0" peerDependencies: eslint: ^9 + husky: ^9 + lint-staged: ^17 prettier: ^3 typescript: ~6.0 bin: From 234c1cd9238fe05f15a46d2f492e680eb216052d Mon Sep 17 00:00:00 2001 From: "Robert (Jamie) Munro" Date: Wed, 13 May 2026 10:58:10 +0100 Subject: [PATCH 13/15] fix: add @eslint/js to devDependencies installed by setup script --- bin/setup.mjs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/bin/setup.mjs b/bin/setup.mjs index 775a678..d39ec95 100755 --- a/bin/setup.mjs +++ b/bin/setup.mjs @@ -29,7 +29,7 @@ Steps performed: 7. Creates eslint.config.mjs if missing 8. Copies .editorconfig from the preset 9. Creates .husky/pre-commit if missing - 10. Installs required devDependencies via yarn add --dev + 10. Installs required devDependencies via yarn add --dev (@eslint/js, eslint, husky, lint-staged, prettier, typescript) 11. (with --fix-subpackages) Removes redundant "prettier" keys from sub-package package.json files (they inherit from the root package.json via walk-up) @@ -248,7 +248,7 @@ function depWithVersion(name) { return peerDeps[name] ? `${name}@${peerDeps[name]}` : name } -const devDeps = ['eslint', 'husky', 'lint-staged', 'prettier', 'typescript'].map(depWithVersion) +const devDeps = ['@eslint/js', 'eslint', 'husky', 'lint-staged', 'prettier', 'typescript'].map(depWithVersion) console.log(`\nInstalling devDependencies: ${devDeps.join(', ')} ...`) try { execFileSync('yarn', ['add', '--dev', ...devDeps], { stdio: 'inherit', cwd: projectDir }) From 6f7273c8f0d38ee136790a6f8c1bb09cce40f983 Mon Sep 17 00:00:00 2001 From: "Robert (Jamie) Munro" Date: Wed, 13 May 2026 10:58:43 +0100 Subject: [PATCH 14/15] docs: fix bin name and add --force example in README --- README.md | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 3730ea2..3fd2dcd 100644 --- a/README.md +++ b/README.md @@ -27,7 +27,7 @@ This readme assumes you are using yarn v4. For other package managers the steps The easiest way to configure a project is to run the setup CLI, which will install all other required devDependencies and configure the project automatically: ```sh -yarn sofie-code-preset-setup +yarn sofie-code-standard-preset-setup ``` This will: @@ -38,9 +38,13 @@ This will: 4. Create `eslint.config.mjs` if missing 5. Copy `.editorconfig` from the preset 6. Create `.husky/pre-commit` if missing -7. Install required devDependencies via `yarn add --dev` +7. Install required devDependencies via `yarn add --dev` (`@eslint/js`, `eslint`, `husky`, `lint-staged`, `prettier`, `typescript`) -If any of the above items are already configured to a different value, they will be skipped with a warning. Pass `--force` to overwrite existing values. +If any of the above items are already configured to a different value, they will be skipped with a warning. Pass `--force` to overwrite existing values: + +```sh +yarn sofie-code-standard-preset-setup --force +``` After running, review and commit the changes, then follow the manual steps below to add any project-specific configuration. From 398787288648e3b993eb7eb7f2c3d2580d45ccf9 Mon Sep 17 00:00:00 2001 From: "Robert (Jamie) Munro" Date: Wed, 13 May 2026 10:58:59 +0100 Subject: [PATCH 15/15] fix: suppress n/no-unpublished-import for eslint.config.* files MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit eslint.config.* files always import devDependencies (ESLint plugins/presets) by design — they are never part of the published package. However, projects without a "files" field in package.json cause npm to default to publishing everything, making eslint.config.* appear to be a published file. In that case the rule fires on line 1 of every project's eslint.config.mjs: "@sofie-automation/code-standard-preset" is not published n/no-unpublished-import We already suppress n/no-extraneous-import for the same files. Add the same exemption for n/no-unpublished-import. --- eslint/main.mjs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/eslint/main.mjs b/eslint/main.mjs index eed731d..0396b05 100644 --- a/eslint/main.mjs +++ b/eslint/main.mjs @@ -221,7 +221,13 @@ export async function generateEslintConfig(options) { ? { files: ['eslint.config.*'], rules: { + // eslint.config.* files always import devDependencies (ESLint plugins/presets) + // by design — they are never part of the published package. The n/ rules flag + // these imports if the project has no "files" field in package.json (because + // npm then defaults to publishing everything, making eslint.config.* look like + // a published file). Suppress both rules unconditionally for config files. 'n/no-extraneous-import': 'off', + 'n/no-unpublished-import': 'off', }, } : null,