diff --git a/README.md b/README.md index 5a0bb48..952c836 100644 --- a/README.md +++ b/README.md @@ -2,10 +2,10 @@ This project: -* Defines and explains the coding style and workflow for the S3 project. See +- Defines and explains the coding style and workflow for the S3 project. See [CONTRIBUTING.md](CONTRIBUTING.md) for details. -* Provides an `eslint-config-scality` package that can be added as a dependency +- Provides an `eslint-config-scality` package that can be added as a dependency in other projects. That way, coding style can automatically be checked using eslint. @@ -22,29 +22,30 @@ To use it in a project: 1. Install Prettier in the project: - ```sh - yarn add --dev prettier - # or - npm install --save-dev prettier - ``` + ```sh + yarn add --dev prettier + # or + npm install --save-dev prettier + ``` 2. Copy or extend the configuration: - ```js - // prettier.config.cjs - module.exports = require('eslint-config-scality/prettier.config.cjs'); - ``` + ```js + // prettier.config.cjs + module.exports = require('eslint-config-scality/prettier.config.cjs'); + ``` 3. Add convenience scripts: - ```json - { - "scripts": { - "format": "prettier --write .", - "format:check": "prettier --check ." - } - } - ``` + ```json + { + "scripts": { + "format": "prettier --write .", + "format:check": "prettier --check .", + "prettier:diff": "scality-prettier-diff" + } + } + ``` Projects may override options locally (for example `tabWidth` or `printWidth`) if they have strong legacy constraints, but this configuration @@ -53,6 +54,19 @@ Node.js codebases. YAML/JSON/Markdown files default to a 2-space indentation and Markdown prose is left un-reflowed by default (`proseWrap: 'preserve'`), to avoid noisy diffs in existing documentation. +For pull request checks or local review of changed files, this package exposes +the `scality-prettier-diff` script. It runs Prettier on files returned by +`git diff --name-only --diff-filter=ACMRT`, defaults to `--check`, and forwards +remaining arguments to `git diff`. + +Examples: + +```sh +yarn run prettier:diff +yarn run prettier:diff --check HEAD~1..HEAD +yarn run prettier:diff --write --cached +``` + ## Contributing - See `CONTRIBUTING.md` for contribution guidelines and coding standards. diff --git a/bin/prettier-diff.js b/bin/prettier-diff.js new file mode 100755 index 0000000..1334670 --- /dev/null +++ b/bin/prettier-diff.js @@ -0,0 +1,107 @@ +#!/usr/bin/env node + +'use strict'; + +const { spawnSync } = require('child_process'); + +const supportedFilePattern = /\.(js|cjs|mjs|ts|tsx|json|ya?ml|md)$/; + +function parseArgs(args) { + const prettierArgs = []; + const diffArgs = []; + let hasMode = false; + + args.forEach(arg => { + if (arg === '--format') { + prettierArgs.push('--write'); + hasMode = true; + return; + } + if (arg === '--check' || arg === '--write') { + prettierArgs.push(arg); + hasMode = true; + return; + } + diffArgs.push(arg); + }); + + if (!hasMode) { + prettierArgs.unshift('--check'); + } + + return { prettierArgs, diffArgs }; +} + +function filterSupportedFiles(files) { + return files.filter(file => supportedFilePattern.test(file)); +} + +function run(command, args, options = {}) { + return spawnSync(command, args, { + encoding: 'utf8', + stdio: ['ignore', 'pipe', 'pipe'], + ...options, + }); +} + +function exitFromResult(result) { + if (result.error) { + process.stderr.write(`${result.error.message}\n`); + process.exit(1); + } + if (result.signal) { + process.stderr.write(`Command terminated by signal ${result.signal}\n`); + process.exit(1); + } + process.exit(result.status); +} + +function getGitRoot() { + const result = run('git', ['rev-parse', '--show-toplevel']); + if (result.status !== 0) { + process.stderr.write(result.stderr); + exitFromResult(result); + } + return result.stdout.trim(); +} + +function getChangedFiles(diffArgs, cwd) { + const result = run('git', ['diff', '--name-only', '--diff-filter=ACMRT', ...diffArgs], { cwd }); + if (result.status !== 0) { + process.stderr.write(result.stderr); + exitFromResult(result); + } + return result.stdout.split('\n').filter(Boolean); +} + +function main() { + const { prettierArgs, diffArgs } = parseArgs(process.argv.slice(2)); + const rootDir = getGitRoot(); + const changedFiles = filterSupportedFiles(getChangedFiles(diffArgs, rootDir)); + + if (changedFiles.length === 0) { + process.stdout.write('No supported files changed; skipping Prettier.\n'); + return; + } + + process.stdout.write(`Running Prettier on ${changedFiles.length} file(s):\n`); + changedFiles.forEach(file => process.stdout.write(` - ${file}\n`)); + + const result = spawnSync( + 'yarn', + ['run', '--silent', 'prettier', ...changedFiles, ...prettierArgs], + { cwd: rootDir, stdio: 'inherit' }, + ); + exitFromResult(result); +} + +if (require.main === module) { + main(); +} + +module.exports = { + exitFromResult, + filterSupportedFiles, + main, + parseArgs, +}; diff --git a/package.json b/package.json index 1657095..06a1134 100644 --- a/package.json +++ b/package.json @@ -1,15 +1,18 @@ { "name": "eslint-config-scality", - "version": "8.3.1", + "version": "8.3.2", "description": "ESLint config for Scality's Node.js coding guidelines", "bin": { - "mdlint": "./bin/mdlint.js" + "mdlint": "./bin/mdlint.js", + "scality-prettier-diff": "./bin/prettier-diff.js" }, "main": "index.js", "scripts": { - "test": "yarn run --silent lint && yarn run --silent lint_md", + "test": "yarn run --silent lint && yarn run --silent lint_md && yarn run --silent prettier:diff", "lint": "eslint -c eslint.config.cjs $(git ls-files '*.js')", - "lint_md": "node bin/mdlint.js $(git ls-files '*.md')" + "lint_md": "node bin/mdlint.js $(git ls-files '*.md')", + "prettier": "prettier", + "prettier:diff": "node tests/prettier-diff.js" }, "repository": { "type": "git", @@ -20,7 +23,8 @@ "markdownlint": "0.31.1" }, "devDependencies": { - "eslint": "^9.9.1" + "eslint": "^9.9.1", + "prettier": "^3.4.2" }, "keywords": [ "eslint", diff --git a/tests/prettier-diff.js b/tests/prettier-diff.js new file mode 100644 index 0000000..fa050c2 --- /dev/null +++ b/tests/prettier-diff.js @@ -0,0 +1,60 @@ +'use strict'; + +const assert = require('assert'); +const { exitFromResult, filterSupportedFiles, parseArgs } = require('../bin/prettier-diff'); + +assert.deepStrictEqual(parseArgs([]), { + prettierArgs: ['--check'], + diffArgs: [], +}); + +assert.deepStrictEqual(parseArgs(['--format', 'HEAD~1..HEAD']), { + prettierArgs: ['--write'], + diffArgs: ['HEAD~1..HEAD'], +}); + +assert.deepStrictEqual(parseArgs(['--write', '--cached']), { + prettierArgs: ['--write'], + diffArgs: ['--cached'], +}); + +assert.deepStrictEqual( + filterSupportedFiles([ + 'index.js', + 'lib/config.cjs', + 'src/main.ts', + 'workflow.yaml', + 'README.md', + 'package-lock.json', + 'image.png', + 'script.sh', + ]), + ['index.js', 'lib/config.cjs', 'src/main.ts', 'workflow.yaml', 'README.md', 'package-lock.json'], +); + +const originalExit = process.exit; +const originalStderrWrite = process.stderr.write; + +let exitCode; +let stderr = ''; + +process.exit = code => { + exitCode = code; + throw new Error('process.exit'); +}; +process.stderr.write = message => { + stderr += message; +}; + +assert.throws( + () => + exitFromResult({ + error: new Error('spawn yarn ENOENT'), + }), + /process\.exit/, +); +assert.strictEqual(exitCode, 1); +assert.match(stderr, /spawn yarn ENOENT/); + +process.exit = originalExit; +process.stderr.write = originalStderrWrite; diff --git a/yarn.lock b/yarn.lock index 947b403..c69d6af 100644 --- a/yarn.lock +++ b/yarn.lock @@ -543,6 +543,11 @@ prelude-ls@^1.2.1: resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.2.1.tgz#debc6489d7a6e6b0e7611888cec880337d316396" integrity sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g== +prettier@^3.4.2: + version "3.8.3" + resolved "https://registry.yarnpkg.com/prettier/-/prettier-3.8.3.tgz#560f2de55bf01b4c0503bc629d5df99b9a1d09b0" + integrity sha512-7igPTM53cGHMW8xWuVTydi2KO233VFiTNyF5hLJqpilHfmn8C8gPf+PS7dUT64YcXFbiMGZxS9pCSxL/Dxm/Jw== + punycode@^2.1.0: version "2.1.1" resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.1.1.tgz#b58b010ac40c22c5657616c8d2c2c02c7bf479ec"