From fc3bfeabae29b65f99b6911a989b0b41d3d1128e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Iiro=20J=C3=A4ppinen?= Date: Mon, 21 Aug 2023 12:41:48 +0300 Subject: [PATCH] fix: fix reading config from stdin, introduced in v14.0.0 (#1317) --- bin/lint-staged.js | 13 ++--- lib/messages.js | 2 +- lib/readStdin.js | 19 +++++++ package-lock.json | 7 +++ package.json | 1 + test/e2e/__utils__/getLintStagedExecutor.js | 6 +-- test/e2e/stdin-config.test.js | 55 +++++++++++++++++++++ test/unit/readStdin.spec.js | 16 ++++++ 8 files changed, 107 insertions(+), 12 deletions(-) create mode 100644 lib/readStdin.js create mode 100644 test/e2e/stdin-config.test.js create mode 100644 test/unit/readStdin.spec.js diff --git a/bin/lint-staged.js b/bin/lint-staged.js index 4f4bff5bd..f1ae6aba1 100755 --- a/bin/lint-staged.js +++ b/bin/lint-staged.js @@ -8,6 +8,7 @@ import debug from 'debug' import lintStaged from '../lib/index.js' import { CONFIG_STDIN_ERROR } from '../lib/messages.js' +import { readStdin } from '../lib/readStdin.js' // Force colors for packages that depend on https://www.npmjs.com/package/supports-color if (supportsColor) { @@ -110,17 +111,13 @@ debugLog('Options parsed from command-line:', options) if (options.configPath === '-') { delete options.configPath try { - options.config = await fs.readFile(process.stdin.fd, 'utf8').toString().trim() - } catch { + debugLog('Reading config from stdin') + options.config = JSON.parse(await readStdin()) + } catch (error) { + debugLog(CONFIG_STDIN_ERROR, error) console.error(CONFIG_STDIN_ERROR) process.exit(1) } - - try { - options.config = JSON.parse(options.config) - } catch { - // Let config parsing complain if it's not JSON - } } try { diff --git a/lib/messages.js b/lib/messages.js index 9308d209d..06afd98cc 100644 --- a/lib/messages.js +++ b/lib/messages.js @@ -71,4 +71,4 @@ export const RESTORE_STASH_EXAMPLE = ` Any lost modifications can be restored f > git stash apply --index stash@{0} ` -export const CONFIG_STDIN_ERROR = 'Error: Could not read config from stdin.' +export const CONFIG_STDIN_ERROR = chalk.redBright(`${error} Failed to read config from stdin.`) diff --git a/lib/readStdin.js b/lib/readStdin.js new file mode 100644 index 000000000..3ff5a6a04 --- /dev/null +++ b/lib/readStdin.js @@ -0,0 +1,19 @@ +import { createInterface } from 'node:readline' + +/** + * Returns a promise resolving to the first line written to stdin after invoking. + * @warn will never resolve if called after writing to stdin + * + * @returns {Promise} + */ +export const readStdin = () => { + const readline = createInterface({ input: process.stdin }) + + return new Promise((resolve) => { + readline.prompt() + readline.on('line', (line) => { + readline.close() + resolve(line) + }) + }) +} diff --git a/package-lock.json b/package-lock.json index 5cb2f55b0..dcf35c9ca 100644 --- a/package-lock.json +++ b/package-lock.json @@ -38,6 +38,7 @@ "husky": "8.0.3", "jest": "29.6.2", "jest-snapshot-serializer-ansi": "2.1.0", + "mock-stdin": "1.0.0", "prettier": "3.0.1" }, "engines": { @@ -7300,6 +7301,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/mock-stdin": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/mock-stdin/-/mock-stdin-1.0.0.tgz", + "integrity": "sha512-tukRdb9Beu27t6dN+XztSRHq9J0B/CoAOySGzHfn8UTfmqipA5yNT/sDUEyYdAV3Hpka6Wx6kOMxuObdOex60Q==", + "dev": true + }, "node_modules/ms": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", diff --git a/package.json b/package.json index 45d80054f..513d7b712 100644 --- a/package.json +++ b/package.json @@ -59,6 +59,7 @@ "husky": "8.0.3", "jest": "29.6.2", "jest-snapshot-serializer-ansi": "2.1.0", + "mock-stdin": "1.0.0", "prettier": "3.0.1" }, "keywords": [ diff --git a/test/e2e/__utils__/getLintStagedExecutor.js b/test/e2e/__utils__/getLintStagedExecutor.js index d1e94afe3..cbbff6245 100644 --- a/test/e2e/__utils__/getLintStagedExecutor.js +++ b/test/e2e/__utils__/getLintStagedExecutor.js @@ -2,7 +2,7 @@ import { resolve } from 'node:path' import { execaCommand } from 'execa' -let lintStagedBin = resolve(__dirname, '../../../bin/lint-staged.js') +const lintStagedBin = resolve(__dirname, '../../../bin/lint-staged.js') /** * @param {string} cwd @@ -10,5 +10,5 @@ let lintStagedBin = resolve(__dirname, '../../../bin/lint-staged.js') */ export const getLintStagedExecutor = (cwd) => - async (params = '') => - await execaCommand(`${lintStagedBin} --cwd=${cwd} ${params}`) + (params = '', options) => + execaCommand(`${lintStagedBin} --cwd=${cwd} ${params}`, options) diff --git a/test/e2e/stdin-config.test.js b/test/e2e/stdin-config.test.js new file mode 100644 index 000000000..9c002fdc4 --- /dev/null +++ b/test/e2e/stdin-config.test.js @@ -0,0 +1,55 @@ +import '../integration/__mocks__/resolveConfig.js' + +import { jest } from '@jest/globals' + +import { withGitIntegration } from '../integration/__utils__/withGitIntegration.js' +import * as fileFixtures from '../integration/__fixtures__/files.js' +import * as configFixtures from '../integration/__fixtures__/configs.js' + +import { getLintStagedExecutor } from './__utils__/getLintStagedExecutor.js' + +jest.setTimeout(20000) +jest.retryTimes(2) + +describe('lint-staged', () => { + test( + 'reads config from stdin', + withGitIntegration(async ({ cwd, execGit, readFile, writeFile }) => { + const lintStaged = getLintStagedExecutor(cwd) + + // Stage ugly file + await writeFile('test file.js', fileFixtures.uglyJS) + await execGit(['add', 'test file.js']) + + // Run lint-staged with config from stdin + await lintStaged('-c -', { + input: JSON.stringify(configFixtures.prettierWrite), + }) + + // Nothing was wrong so file was prettified + expect(await readFile('test file.js')).toEqual(fileFixtures.prettyJS) + }) + ) + + test( + 'fails when stdin config is not valid', + withGitIntegration(async ({ cwd, execGit, readFile, writeFile }) => { + const lintStaged = getLintStagedExecutor(cwd) + + // Stage ugly file + await writeFile('test file.js', fileFixtures.uglyJS) + await execGit(['add', 'test file.js']) + + // Break JSON by removing } from the end + const brokenJSONConfig = JSON.stringify(configFixtures.prettierWrite).replace('"}', '"') + + // Run lint-staged with broken config from stdin + await expect(lintStaged('-c -', { input: brokenJSONConfig })).rejects.toThrowError( + 'Failed to read config from stdin' + ) + + // File was not edited + expect(await readFile('test file.js')).toEqual(fileFixtures.uglyJS) + }) + ) +}) diff --git a/test/unit/readStdin.spec.js b/test/unit/readStdin.spec.js new file mode 100644 index 000000000..2cea3550c --- /dev/null +++ b/test/unit/readStdin.spec.js @@ -0,0 +1,16 @@ +import { stdin } from 'mock-stdin' + +import { readStdin } from '../../lib/readStdin.js' + +const mockStdin = stdin() + +describe('readStdin', () => { + it('should return stdin', async () => { + const stdinPromise = readStdin() + + mockStdin.send('Hello, world!') + mockStdin.end() + + expect(await stdinPromise).toEqual('Hello, world!') + }) +})