diff --git a/README.md b/README.md index 698be99..1830e2c 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,7 @@ ## Overview -Wrap your own eslint rules in a easy-to-use command line tool and/or a JS module. +Wrap your own eslint and prettier rules in a easy-to-use command line tool and/or a JS module. ## Install @@ -61,6 +61,7 @@ require('standard-engine').cli(opts) ```js const eslint = require('eslint') +const prettier = require('prettier') const path = require('path') const pkg = require('./package.json') @@ -70,10 +71,14 @@ module.exports = { homepage: pkg.homepage, bugs: pkg.bugs.url, eslint: eslint, // pass any version of eslint >= 1.0.0 + prettier: prettier, // pass any version of prettier >= 2.0.0 cmd: 'pocketlint', // should match the "bin" key in your package.json tagline: 'Live by your own standards!', // displayed in output --help eslintConfig: { - configFile: path.join(__dirname, 'eslintrc.json') + configFile: path.join(__dirname, '.eslintrc.json') + }, + prettierConfig: { + configFile: path.join(__dirname, '.prettierrc.json') }, cwd: '' // current working directory, passed to eslint } @@ -81,9 +86,9 @@ module.exports = { Additionally an optional `resolveEslintConfig()` function can be provided. See below for details. -### `eslintrc.json` +### `.eslintrc.json` -Put all your .eslintrc rules in this file. A good practice is to create an [ESLint Shareable Config](http://eslint.org/docs/developer-guide/shareable-configs) and extend it, but its not required: +Put all your .eslintrc rules in this file. A good practice is to create an [ESLint Shareable Config](http://eslint.org/docs/developer-guide/shareable-configs) and extend it, but its not required: ```js { @@ -165,12 +170,7 @@ a `ignore` property to `package.json`: Some files are ignored by default: ```js -const DEFAULT_IGNORE = [ - '*.min.js', - 'coverage/', - 'node_modules/', - 'vendor/' -] +const DEFAULT_IGNORE = ['*.min.js', 'coverage/', 'node_modules/', 'vendor/'] ``` You can disable these default ignores by setting the `noDefaultIgnore` option to `true`. @@ -272,6 +272,7 @@ You can provide a `resolveEslintConfig()` function in the `options.js` exports: ```js const eslint = require('eslint') +const prettier = require('prettier') const path = require('path') const pkg = require('./package.json') @@ -281,10 +282,14 @@ module.exports = { homepage: pkg.homepage, bugs: pkg.bugs.url, eslint: eslint, // pass any version of eslint >= 1.0.0 + prettier: prettier, // pass any version of prettier >= 2.0.0 cmd: 'pocketlint', // should match the "bin" key in your package.json tagline: 'Live by your own standards!', // displayed in output --help eslintConfig: { - configFile: path.join(__dirname, 'eslintrc.json') + configFile: path.join(__dirname, '.eslintrc.json') + }, + prettierConfig: { + configFile: path.join(__dirname, '.prettierrc.json') }, resolveEslintConfig: function (eslintConfig, opts, packageOpts, rootDir) { // provide implementation here, then return the eslintConfig object (or a new one) @@ -312,6 +317,7 @@ be provided: // common to lintText and lintFiles cwd: '', // current working directory (default: process.cwd()) fix: false, // automatically fix problems + format: false, // aggressively format code for consistency extensions: [], // file extensions to lint (has sane defaults) globals: [], // custom global variables to declare plugins: [], // custom eslint plugins @@ -335,9 +341,7 @@ const results = { results: [ { filePath: '', - messages: [ - { ruleId: '', message: '', line: 0, column: 0 } - ], + messages: [{ ruleId: '', message: '', line: 0, column: 0 }], errorCount: 0, warningCount: 0, output: '' // fixed source code (only present with {fix: true} option) @@ -365,6 +369,7 @@ Lint the provided `files` globs. An `opts` object may be provided: // common to lintText and lintFiles cwd: '', // current working directory (default: process.cwd()) fix: false, // automatically fix problems + format: false, // aggressively format code for consistency extensions: [], // file extensions to lint (has sane defaults) globals: [], // custom global variables to declare plugins: [], // custom eslint plugins @@ -394,6 +399,7 @@ This is the full set of options accepted by the above APIs. Not all options make filename: '', // path of the file containing the text being linted (optional) fix: false, // automatically fix problems globals: [], // custom global variables to declare + format: false, // aggressively format code for consistency plugins: [], // custom eslint plugins envs: [], // custom eslint environment parser: '' // custom js parser (e.g. babel-eslint) diff --git a/bin/cmd.js b/bin/cmd.js index 5f0d4c6..08d8495 100755 --- a/bin/cmd.js +++ b/bin/cmd.js @@ -34,6 +34,7 @@ function cli (rawOpts) { }, boolean: [ 'fix', + 'format', 'help', 'stdin', 'version' @@ -69,6 +70,7 @@ Usage: Flags: --fix Automatically fix problems + --format Aggressively format code for consistency --version Show current version -h, --help Show usage information @@ -92,6 +94,7 @@ Flags (advanced): const lintOpts = { fix: argv.fix, + format: argv.format, extensions: argv.ext, globals: argv.global, plugins: argv.plugin, diff --git a/index.js b/index.js index 14ee6c3..89707f2 100644 --- a/index.js +++ b/index.js @@ -1,7 +1,9 @@ /*! standard-engine. MIT License. Feross Aboukhadijeh */ const os = require('os') +const fs = require('fs') const path = require('path') +const globby = require('globby') const CACHE_HOME = require('xdg-basedir').cache || os.tmpdir() @@ -11,12 +13,19 @@ const { resolveEslintConfig } = require('./lib/resolve-eslint-config') /** @typedef {Omit} BaseLintOptions */ /** @typedef {(err: Error|unknown, result?: import('eslint').CLIEngine.LintReport) => void} LinterCallback */ +/** + * @typedef PrettierOptions + * @property {string | null} [configFile] + */ + /** * @typedef LinterOptions * @property {string} cmd * @property {import('eslint')} eslint + * @property {import('prettier')} prettier * @property {string} [cwd] * @property {CLIEngineOptions} [eslintConfig] + * @property {PrettierOptions} [prettierConfig] * @property {import('./lib/resolve-eslint-config').CustomEslintConfigResolver} [resolveEslintConfig] * @property {string} [version] */ @@ -28,11 +37,14 @@ class Linter { constructor (opts) { if (!opts || !opts.cmd) throw new Error('opts.cmd option is required') if (!opts.eslint) throw new Error('opts.eslint option is required') + if (!opts.prettier) throw new Error('opts.prettier option is required') /** @type {string} */ this.cmd = opts.cmd /** @type {import('eslint')} */ this.eslint = opts.eslint + /** @type {import('prettier')} */ + this.prettier = opts.prettier /** @type {string} */ this.cwd = opts.cwd || process.cwd() this.customEslintConfigResolver = opts.resolveEslintConfig @@ -57,6 +69,11 @@ class Linter { ...(opts.eslintConfig || {}) } + /** @type {PrettierOptions} */ + this.prettierConfig = { + configFile: opts.prettierConfig != null ? opts.prettierConfig.configFile : null + } + if (this.eslintConfig.configFile != null) { this.eslintConfig.resolvePluginsRelativeTo = path.dirname( this.eslintConfig.configFile @@ -102,19 +119,54 @@ class Linter { * Lint files to enforce JavaScript Style. * * @param {Array.} files file globs to lint - * @param {BaseLintOptions & { cwd?: string }} [opts] base options + file globs to ignore (has sane defaults) + current working directory (default: process.cwd()) + * @param {BaseLintOptions & { cwd?: string, format?: boolean }} [opts] base options + file globs to ignore (has sane defaults) + current working directory (default: process.cwd()) * @param {LinterCallback} [cb] * @returns {void} */ lintFiles (files, opts, cb) { if (typeof opts === 'function') { return this.lintFiles(files, undefined, opts) } if (!cb) throw new Error('callback is required') + const format = opts == null ? false : opts.format const eslintConfig = this.resolveEslintConfig(opts) if (typeof files === 'string') files = [files] if (files.length === 0) files = ['.'] + if (format) { + /** @type {string[]} */ + const extensionsArray = [] + this.prettier.getSupportInfo().languages.forEach((language) => { + if (language != null && language.extensions != null) { + extensionsArray.push(...language.extensions) + } + }) + const extensions = extensionsArray.join(',') + const prettierOptions = this.prettier.resolveConfig.sync( + this.prettierConfig.configFile == null ? path.join(__dirname, '.prettierrc.json') : this.prettierConfig.configFile + ) || {} + const ignore = typeof eslintConfig.ignorePattern === 'string' + ? [eslintConfig.ignorePattern] + : eslintConfig.ignorePattern == null + ? [] + : eslintConfig.ignorePattern + const files = globby.sync([`**/*{${extensions}}`], { + ignore, + cwd: opts == null ? undefined : opts.cwd + }) + for (const file of files) { + const filePath = path.join(process.cwd(), file) + const fileInfo = this.prettier.getFileInfo.sync(filePath, {}) + prettierOptions.parser = fileInfo.inferredParser == null ? 'babel' : fileInfo.inferredParser + const input = fs.readFileSync(filePath, 'utf8') + const output = this.prettier.format(input, prettierOptions) + const formatted = input !== output + if (formatted) { + fs.writeFileSync(filePath, output, { encoding: 'utf8' }) + } + } + } + let result try { result = new this.eslint.CLIEngine(eslintConfig).executeOnFiles(files) @@ -122,7 +174,7 @@ class Linter { return cb(err) } - if (eslintConfig.fix) { + if (eslintConfig.fix || format) { this.eslint.CLIEngine.outputFixes(result) } diff --git a/package.json b/package.json index 3d0e73c..2c78ac6 100644 --- a/package.json +++ b/package.json @@ -39,6 +39,7 @@ }, "dependencies": { "get-stdin": "^8.0.0", + "globby": "^11.0.4", "minimist": "^1.2.5", "pkg-conf": "^3.1.0", "xdg-basedir": "^4.0.0" @@ -48,6 +49,7 @@ "@types/eslint": "^7.28.0", "@types/minimist": "^1.2.2", "@types/node": "~12.20.0", + "@types/prettier": "^2.3.2", "cross-spawn": "^7.0.3", "eslint": "^7.12.1", "eslint-config-standard": "^16.0.0", @@ -56,6 +58,7 @@ "eslint-plugin-node": "^11.1.0", "eslint-plugin-promise": "^5.1.0", "eslint-plugin-react": "^7.21.5", + "prettier": "^2.4.0", "standard": "*", "tape": "^5.0.1", "typescript": "~4.4.3" diff --git a/test/api.js b/test/api.js index dd51d2b..9e5e084 100644 --- a/test/api.js +++ b/test/api.js @@ -1,4 +1,5 @@ const eslint = require('eslint') +const prettier = require('prettier') const path = require('path') const test = require('tape') @@ -9,6 +10,7 @@ function getStandard () { cmd: 'pocketlint', version: '0.0.0', eslint, + prettier, eslintConfig: require('../tmp/standard/options').eslintConfig }) } diff --git a/test/lib/standard-cmd.js b/test/lib/standard-cmd.js index 2f898f9..f7f144e 100755 --- a/test/lib/standard-cmd.js +++ b/test/lib/standard-cmd.js @@ -1,11 +1,13 @@ #!/usr/bin/env node const path = require('path') const eslint = require('eslint') +const prettier = require('prettier') const opts = { cmd: 'pocketlint', version: '0.0.0', eslint, + prettier, eslintConfig: { configFile: path.join(__dirname, 'standard.json'), useEslintrc: false