Skip to content

Commit

Permalink
BREAKING: Make Promise based
Browse files Browse the repository at this point in the history
Fixes #249

Changes the callback based API:s to instead be async and return Promises. Also removes the sync versions of the API:s, to allow for eg. async `resolveEslintConfig`
  • Loading branch information
voxpelli committed Oct 3, 2021
1 parent 72dd732 commit 4207670
Show file tree
Hide file tree
Showing 4 changed files with 105 additions and 158 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -295,7 +295,7 @@ module.exports = {

This function is called with the current ESLint config (the options passed to [ESLint's `CLIEngine`](http://eslint.org/docs/developer-guide/nodejs-api#cliengine)), the options object (`opts`), any options extracted from the project's `package.json` (`packageOpts`), and the directory that contained that `package.json` file (`rootDir`, equivalent to `opts.cwd` if no file was found).

Modify and return `eslintConfig`, or return a new object with the eslint config to be used.
Modify and return `eslintConfig`, or return a new object with the eslint config to be used, either directly or wrapped in a `Promise`.

## API Usage

Expand Down
172 changes: 81 additions & 91 deletions bin/cmd.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,9 @@ const getStdin = require('get-stdin')

/**
* @param {Omit<import('../').LinterOptions, 'cmd'> & StandardCliOptions} rawOpts
* @returns {Promise<void>}
*/
function cli (rawOpts) {
async function cli (rawOpts) {
const opts = {
cmd: 'standard-engine',
tagline: 'JavaScript Custom Style',
Expand Down Expand Up @@ -99,90 +100,35 @@ Flags (advanced):
parser: argv.parser
}

/** @type {string} */
let stdinText

if (argv.stdin) {
getStdin().then(function (text) {
stdinText = text
standard.lintText(text, lintOpts, onResult)
})
} else {
standard.lintFiles(argv._, lintOpts, onResult)
}

/** @type {import('../').LinterCallback} */
function onResult (err, result) {
if (err) return onError(err)
if (!result) throw new Error('expected a result')

if (argv.stdin && argv.fix) {
if (result.results[0] && result.results[0].output) {
// Code contained fixable errors, so print the fixed code
process.stdout.write(result.results[0].output)
} else {
// Code did not contain fixable errors, so print original code
process.stdout.write(stdinText)
}
}
const outputFixed = argv.stdin && argv.fix

if (!result.errorCount && !result.warningCount) {
process.exitCode = 0
return
/**
* Print lint errors to stdout -- this is expected output from `standard-engine`.
* Note: When fixing code from stdin (`standard --stdin --fix`), the transformed
* code is printed to stdout, so print lint errors to stderr in this case.
* @type {typeof console.log}
*/
const log = (...args) => {
if (outputFixed) {
args[0] = opts.cmd + ': ' + args[0]
console.error.apply(console, args)
} else {
console.log.apply(console, args)
}
}

console.error('%s: %s (%s)', opts.cmd, opts.tagline, opts.homepage)

// Are any warnings present?
const isSomeWarnings = result.results.some(function (result) {
return result.messages.some(function (message) {
return message.severity === 1
})
})

if (isSomeWarnings) {
const homepage = opts.homepage != null ? ` (${opts.homepage})` : ''
console.error(
'%s: %s',
opts.cmd,
`Some warnings are present which will be errors in the next version${homepage}`
)
}
/** @type {string} */
const stdinText = argv.stdin ? await getStdin() : ''
/** @type {import('eslint').CLIEngine.LintReport} */
let result

// Are any fixable rules present?
const isSomeFixable = result.results.some(function (result) {
return result.messages.some(function (message) {
return !!message.fix
})
})

if (isSomeFixable) {
console.error(
'%s: %s',
opts.cmd,
'Run `' + opts.cmd + ' --fix` to automatically fix some problems.'
)
try {
if (argv.stdin) {
result = await standard.lintText(stdinText, lintOpts)
} else {
result = await standard.lintFiles(argv._, lintOpts)
}

result.results.forEach(function (result) {
result.messages.forEach(function (message) {
log(
' %s:%d:%d: %s%s%s',
result.filePath,
message.line || 0,
message.column || 0,
message.message,
' (' + message.ruleId + ')',
message.severity === 1 ? ' (warning)' : ''
)
})
})

process.exitCode = result.errorCount ? 1 : 0
}

/** @param {Error|unknown} err */
function onError (err) {
} catch (err) {
console.error(opts.cmd + ': Unexpected linter output:\n')
if (err instanceof Error) {
console.error(err.stack || err.message)
Expand All @@ -195,22 +141,66 @@ Flags (advanced):
opts.bugs
)
process.exitCode = 1
return
}

/**
* Print lint errors to stdout -- this is expected output from `standard-engine`.
* Note: When fixing code from stdin (`standard --stdin --fix`), the transformed
* code is printed to stdout, so print lint errors to stderr in this case.
* @type {typeof console.log}
*/
function log (...args) {
if (argv.stdin && argv.fix) {
args[0] = opts.cmd + ': ' + args[0]
console.error.apply(console, args)
if (!result) throw new Error('expected a result')

if (outputFixed) {
if (result.results[0] && result.results[0].output) {
// Code contained fixable errors, so print the fixed code
process.stdout.write(result.results[0].output)
} else {
console.log.apply(console, args)
// Code did not contain fixable errors, so print original code
process.stdout.write(stdinText)
}
}

if (!result.errorCount && !result.warningCount) {
process.exitCode = 0
return
}

console.error('%s: %s (%s)', opts.cmd, opts.tagline, opts.homepage)

// Are any warnings present?
const isSomeWarnings = result.results.some(item => item.messages.some(message => message.severity === 1))

if (isSomeWarnings) {
const homepage = opts.homepage != null ? ` (${opts.homepage})` : ''
console.error(
'%s: %s',
opts.cmd,
`Some warnings are present which will be errors in the next version${homepage}`
)
}

// Are any fixable rules present?
const isSomeFixable = result.results.some(item => item.messages.some(message => !!message.fix))

if (isSomeFixable) {
console.error(
'%s: %s',
opts.cmd,
'Run `' + opts.cmd + ' --fix` to automatically fix some problems.'
)
}

for (const item of result.results) {
for (const message of item.messages) {
log(
' %s:%d:%d: %s%s%s',
item.filePath,
message.line || 0,
message.column || 0,
message.message,
' (' + message.ruleId + ')',
message.severity === 1 ? ' (warning)' : ''
)
}
}

process.exitCode = result.errorCount ? 1 : 0
}

module.exports = cli
47 changes: 8 additions & 39 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ const { resolveEslintConfig } = require('./lib/resolve-eslint-config')

/** @typedef {ConstructorParameters<typeof import('eslint').CLIEngine>[0]} CLIEngineOptions */
/** @typedef {Omit<import('./lib/resolve-eslint-config').ResolveOptions, 'cmd'|'cwd'>} BaseLintOptions */
/** @typedef {(err: Error|unknown, result?: import('eslint').CLIEngine.LintReport) => void} LinterCallback */

/**
* @typedef LinterOptions
Expand Down Expand Up @@ -69,64 +68,34 @@ class Linter {
*
* @param {string} text file text to lint
* @param {Omit<BaseLintOptions, 'ignore'|'noDefaultIgnore'> & { filename?: string }} [opts] base options + path of file containing the text being linted
* @returns {import('eslint').CLIEngine.LintReport}
* @returns {Promise<import('eslint').CLIEngine.LintReport>}
*/
lintTextSync (text, opts) {
const eslintConfig = this.resolveEslintConfig(opts)
async lintText (text, opts) {
const eslintConfig = await this.resolveEslintConfig(opts)
const engine = new this.eslint.CLIEngine(eslintConfig)
return engine.executeOnText(text, (opts || {}).filename)
}

/**
* Lint text to enforce JavaScript Style.
*
* @param {string} text file text to lint
* @param {Omit<BaseLintOptions, 'ignore'|'noDefaultIgnore'> & { filename?: string }} [opts] base options + path of file containing the text being linted
* @param {LinterCallback} [cb]
* @returns {void}
*/
lintText (text, opts, cb) {
if (typeof opts === 'function') return this.lintText(text, undefined, opts)
if (!cb) throw new Error('callback is required')

let result
try {
result = this.lintTextSync(text, opts)
} catch (err) {
return process.nextTick(cb, err)
}
process.nextTick(cb, null, result)
}

/**
* Lint files to enforce JavaScript Style.
*
* @param {Array.<string>} files file globs to lint
* @param {Array<string>} 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 {LinterCallback} [cb]
* @returns {void}
* @returns {Promise<import('eslint').CLIEngine.LintReport>}
*/
lintFiles (files, opts, cb) {
if (typeof opts === 'function') { return this.lintFiles(files, undefined, opts) }
if (!cb) throw new Error('callback is required')

async lintFiles (files, opts) {
const eslintConfig = this.resolveEslintConfig(opts)

if (typeof files === 'string') files = [files]
if (files.length === 0) files = ['.']

let result
try {
result = new this.eslint.CLIEngine(eslintConfig).executeOnFiles(files)
} catch (err) {
return cb(err)
}
const result = new this.eslint.CLIEngine(eslintConfig).executeOnFiles(files)

if (eslintConfig.fix) {
this.eslint.CLIEngine.outputFixes(result)
}

return cb(null, result)
return result
}

/**
Expand Down
42 changes: 15 additions & 27 deletions test/api.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,54 +4,42 @@ const test = require('tape')

const { Linter } = require('../')

function getStandard () {
async function getStandard () {
return new Linter({
cmd: 'pocketlint',
version: '0.0.0',
eslint,
eslintConfig: require('../tmp/standard/options').eslintConfig
eslintConfig: (await import('../tmp/standard/options.js')).default.eslintConfig
})
}

test('api: lintFiles', function (t) {
t.plan(3)
const standard = getStandard()
standard.lintFiles([], { cwd: path.join(__dirname, '../bin') }, function (err, result) {
t.error(err, 'no error while linting')
t.equal(typeof result, 'object', 'result is an object')
t.equal(result.errorCount, 0)
})
})

test('api: lintText', function (t) {
t.plan(3)
const standard = getStandard()
standard.lintText('console.log("hi there")\n', function (err, result) {
t.error(err, 'no error while linting')
t.equal(typeof result, 'object', 'result is an object')
t.equal(result.errorCount, 1, 'should have used single quotes')
})
test('api: lintFiles', async function (t) {
t.plan(2)
const standard = await getStandard()
const result = await standard.lintFiles([], { cwd: path.join(__dirname, '../bin') })
t.equal(typeof result, 'object', 'result is an object')
t.equal(result.errorCount, 0)
})

test('api: lintTextSync', function (t) {
test('api: lintText', async function (t) {
t.plan(2)
const standard = getStandard()
const result = standard.lintTextSync('console.log("hi there")\n')
const standard = await getStandard()
const result = await standard.lintText('console.log("hi there")\n')
t.equal(typeof result, 'object', 'result is an object')
t.equal(result.errorCount, 1, 'should have used single quotes')
})

test('api: resolveEslintConfig -- avoid this.eslintConfig parser mutation', function (t) {
test('api: resolveEslintConfig -- avoid this.eslintConfig parser mutation', async function (t) {
t.plan(2)
const standard = getStandard()
const standard = await getStandard()
const opts = standard.resolveEslintConfig({ parser: 'blah' })
t.equal(opts.parser, 'blah')
t.equal(standard.eslintConfig.parser, undefined)
})

test('api: resolveEslintConfig -- avoid this.eslintConfig global mutation', function (t) {
test('api: resolveEslintConfig -- avoid this.eslintConfig global mutation', async function (t) {
t.plan(2)
const standard = getStandard()
const standard = await getStandard()
const opts = standard.resolveEslintConfig({ globals: ['what'] })
t.deepEqual(opts.globals, ['what'])
t.deepEqual(standard.eslintConfig.globals, [])
Expand Down

0 comments on commit 4207670

Please sign in to comment.