Comparing version 14.0.1 to 14.0.2
@@ -20,4 +20,9 @@ import fs from 'node:fs/promises'; | ||
export async function loadConfig(resolver) { | ||
const filename = configFile(resolver); | ||
try { | ||
const raw = await fs.readFile(configFile(resolver), 'utf8'); | ||
let raw = await fs.readFile(filename, 'utf8'); | ||
if (!raw) { | ||
await new Promise((resolve) => setTimeout(resolve, 50)); | ||
raw = await fs.readFile(filename, 'utf8'); | ||
} | ||
const [token, port, pid, hash] = raw.split(' '); | ||
@@ -24,0 +29,0 @@ return { token, port: Number(port), pid: Number(pid), hash }; |
@@ -42,2 +42,25 @@ import fs from 'node:fs/promises'; | ||
}); | ||
it('retries reading the file if content was empty', async () => { | ||
const clock = sinon.useFakeTimers(); | ||
const contents = ['', 'token 123 456 hash']; | ||
sinon.replace( | ||
fs, | ||
'readFile', | ||
sinon.fake(() => Promise.resolve(contents.shift())) | ||
); | ||
const promise = loadConfig(resolver); | ||
await Promise.resolve(); | ||
assert.calledOnce(fs.readFile); | ||
clock.tick(50); | ||
await assert.resolves(promise, { | ||
token: 'token', | ||
port: 123, | ||
pid: 456, | ||
hash: 'hash' | ||
}); | ||
assert.calledTwice(fs.readFile); | ||
}); | ||
}); | ||
@@ -44,0 +67,0 @@ |
@@ -10,2 +10,5 @@ import net from 'node:net'; | ||
const EXIT_TOKEN_REGEXP = new RegExp(/EXIT([0-9]{3})/); | ||
const EXIT_TOKEN_LENGTH = 7; | ||
/** | ||
@@ -16,5 +19,25 @@ * @param {Resolver} resolver | ||
export async function forwardToDaemon(resolver, config) { | ||
const eslint_args = process.argv.slice(); | ||
const text = process.argv.includes('--stdin') ? await readStdin() : null; | ||
const { stdout } = supportsColor; | ||
const fix_to_stdout_index = eslint_args.indexOf('--fix-to-stdout'); | ||
const fix_to_stdout = fix_to_stdout_index !== -1; | ||
if (fix_to_stdout) { | ||
if (!eslint_args.includes('--stdin')) { | ||
console.error('--fix-to-stdout requires passing --stdin as well'); | ||
// eslint-disable-next-line require-atomic-updates | ||
process.exitCode = 1; | ||
return; | ||
} | ||
eslint_args.splice( | ||
fix_to_stdout_index, | ||
1, | ||
'--fix-dry-run', | ||
'--format', | ||
'json' | ||
); | ||
} | ||
const socket = net.connect(config.port, '127.0.0.1'); | ||
@@ -24,6 +47,6 @@ const args = [ | ||
stdout ? stdout.level : 0, | ||
JSON.stringify(process.cwd()), | ||
JSON.stringify(process.argv) | ||
process.cwd(), | ||
eslint_args | ||
]; | ||
socket.write(args.join(' ')); | ||
socket.write(JSON.stringify(args)); | ||
if (text) { | ||
@@ -42,5 +65,4 @@ socket.write('\n'); | ||
content += chunk; | ||
if (content.length > 5) { | ||
process.stdout.write(content.substring(0, content.length - 5)); | ||
content = content.substring(content.length - 5); | ||
if (!fix_to_stdout && content.length > EXIT_TOKEN_LENGTH) { | ||
process.stdout.write(flushMessage()); | ||
} | ||
@@ -50,9 +72,24 @@ } | ||
.on('end', () => { | ||
if (content.startsWith('EXIT')) { | ||
process.exitCode = Number(content.slice(4)); | ||
} else { | ||
process.stdout.write(content); | ||
console.error('eslint_d: unexpected response'); | ||
process.exitCode = 1; | ||
if (fix_to_stdout) { | ||
try { | ||
const { output } = JSON.parse(flushMessage())[0]; | ||
process.stdout.write(output || text); | ||
} catch (err) { | ||
process.stdout.write(text); | ||
console.error(`eslint_d: ${err}`); | ||
process.exitCode = 1; | ||
return; | ||
} | ||
} | ||
// The remaining 'content' must be the termination code: | ||
const match = content.match(EXIT_TOKEN_REGEXP); | ||
if (match) { | ||
process.exitCode = Number(match[1]); | ||
return; | ||
} | ||
process.stdout.write(content); | ||
console.error('eslint_d: unexpected response'); | ||
process.exitCode = 1; | ||
}) | ||
@@ -68,2 +105,14 @@ .on('error', async (err) => { | ||
}); | ||
/** | ||
* @returns {string} | ||
*/ | ||
function flushMessage() { | ||
const message_length = content.length - EXIT_TOKEN_LENGTH; | ||
// Extract everything we are sure doesn't contain the termination code: | ||
const message = content.substring(0, message_length); | ||
// Keep only what we haven't written yet: | ||
content = content.substring(message_length); | ||
return message; | ||
} | ||
} | ||
@@ -70,0 +119,0 @@ |
@@ -15,2 +15,3 @@ import net from 'node:net'; | ||
const config = { token: 'token', port: 123, pid: 456, hash: 'hash' }; | ||
const color_level = supportsColor.stdout?.['level'] || 0; | ||
let socket; | ||
@@ -24,2 +25,8 @@ let argv; | ||
function fakeStdin(text) { | ||
const stdin = new PassThrough(); | ||
stdin.end(text); | ||
sinon.replaceGetter(process, 'stdin', () => stdin); | ||
} | ||
beforeEach(() => { | ||
@@ -50,3 +57,3 @@ socket = new PassThrough(); | ||
socket.write, | ||
`token ${supportsColor.stdout?.['level'] || 0} "the/cwd" ["node","eslint_d"]` | ||
`["token",${color_level},"the/cwd",["node","eslint_d"]]` | ||
); | ||
@@ -77,9 +84,6 @@ assert.calledOnce(socket.end); | ||
it('writes text from stdin to socket', async () => { | ||
const text = 'text from stdin'; | ||
const stdin = new PassThrough(); | ||
fakeStdin('text from stdin'); | ||
argv.push('--stdin'); | ||
sinon.replaceGetter(process, 'stdin', () => stdin); | ||
forwardToDaemon(resolver, config); | ||
stdin.end(text); | ||
await new Promise(setImmediate); | ||
@@ -89,6 +93,6 @@ | ||
assert.calledWith(socket.write, '\n'); | ||
assert.calledWith(socket.write, text); | ||
assert.calledWith(socket.write, 'text from stdin'); | ||
}); | ||
it('forwards socket response to stdout, except for the last 5 characters', () => { | ||
it('forwards socket response to stdout', () => { | ||
const chunks = ['response ', 'from daemon']; | ||
@@ -104,10 +108,12 @@ sinon.replace( | ||
socket.on.firstCall.callback(); // readable | ||
socket.on.secondCall.callback(); // end | ||
assert.calledTwice(process.stdout.write); | ||
assert.calledWith(process.stdout.write, 'resp'); | ||
assert.calledWith(process.stdout.write, 'onse from d'); | ||
assert.calledThrice(process.stdout.write); | ||
assert.calledWith(process.stdout.write, 're'); | ||
assert.calledWith(process.stdout.write, 'sponse from'); | ||
assert.calledWith(process.stdout.write, ' daemon'); | ||
}); | ||
it('handles EXIT0 from response', () => { | ||
const chunks = ['response from daemonEXIT0']; | ||
it('handles "EXIT000" from response', () => { | ||
const chunks = ['response from daemonEXIT000']; | ||
sinon.replace( | ||
@@ -124,3 +130,3 @@ socket, | ||
assert.calledOnceWith(process.stdout.write, 'response from daemon'); | ||
assert.calledWith(process.stdout.write, 'response from daemon'); | ||
assert.equals(process.exitCode, 0); | ||
@@ -130,4 +136,4 @@ refute.called(console.error); | ||
it('handles EXIT1 from response', () => { | ||
const chunks = ['response from daemonEXIT1']; | ||
it('handles "EXIT001" from response', () => { | ||
const chunks = ['response from daemonEXIT001']; | ||
sinon.replace( | ||
@@ -149,2 +155,39 @@ socket, | ||
it('handles "EXIT123" from response', () => { | ||
const chunks = ['response from daemonEXIT123']; | ||
sinon.replace( | ||
socket, | ||
'read', | ||
sinon.fake(() => (chunks.length ? chunks.shift() : null)) | ||
); | ||
sinon.replace(process.stdout, 'write', sinon.fake()); | ||
forwardToDaemon(resolver, config); | ||
socket.on.firstCall.callback(); // readable | ||
socket.on.secondCall.callback(); // end | ||
assert.calledWith(process.stdout.write, 'response from daemon'); | ||
assert.equals(process.exitCode, 123); | ||
refute.called(console.error); | ||
}); | ||
it('handles "EXIT001" inside response', () => { | ||
const chunks = ['response EXIT001', ' from daemonEXIT002']; | ||
sinon.replace( | ||
socket, | ||
'read', | ||
sinon.fake(() => (chunks.length ? chunks.shift() : null)) | ||
); | ||
sinon.replace(process.stdout, 'write', sinon.fake()); | ||
forwardToDaemon(resolver, config); | ||
socket.on.firstCall.callback(); // readable | ||
socket.on.secondCall.callback(); // end | ||
assert.calledWith(process.stdout.write, 'response '); | ||
assert.calledWith(process.stdout.write, 'EXIT001 from daemon'); | ||
assert.equals(process.exitCode, 2); | ||
refute.called(console.error); | ||
}); | ||
it('logs error and sets exitCode to 1 if response does not end with EXIT marker', () => { | ||
@@ -163,4 +206,4 @@ const chunks = ['response from daemon']; | ||
assert.calledWith(process.stdout.write, 'response from d'); | ||
assert.calledWith(process.stdout.write, 'aemon'); | ||
assert.calledWith(process.stdout.write, 'response from'); | ||
assert.calledWith(process.stdout.write, ' daemon'); | ||
assert.equals(process.exitCode, 1); | ||
@@ -196,3 +239,104 @@ assert.calledOnceWith(console.error, 'eslint_d: unexpected response'); | ||
}); | ||
context('--fix-to-stdout', () => { | ||
beforeEach(() => { | ||
sinon.replace(process, 'cwd', sinon.fake.returns('cwd')); | ||
fakeStdin('text from stdin'); | ||
}); | ||
it('throws if --stdin is absent', async () => { | ||
argv.push('--fix-to-stdout'); | ||
await forwardToDaemon(resolver, config); | ||
assert.equals(process.exitCode, 1); | ||
assert.calledOnceWith( | ||
console.error, | ||
'--fix-to-stdout requires passing --stdin as well' | ||
); | ||
}); | ||
it('replaces the option with --fix-dry-run --format json', async () => { | ||
argv.push('--stdin', '--fix-to-stdout', '--other', '--options'); | ||
forwardToDaemon(resolver, config); | ||
await new Promise(setImmediate); | ||
assert.calledThrice(socket.write); | ||
assert.calledWith( | ||
socket.write, | ||
`["token",${color_level},"cwd",["node","eslint_d","--stdin","--fix-dry-run","--format","json","--other","--options"]]` | ||
); | ||
assert.calledWith(socket.write, '\n'); | ||
assert.calledWith(socket.write, 'text from stdin'); | ||
assert.calledOnce(socket.end); | ||
}); | ||
it('prints fixed output to stdout', async () => { | ||
argv.push('--stdin', '--fix-to-stdout'); | ||
const chunks = ['[{"output":"response from daemon"}]EXIT001']; | ||
sinon.replace( | ||
socket, | ||
'read', | ||
sinon.fake(() => (chunks.length ? chunks.shift() : null)) | ||
); | ||
sinon.replace(process.stdout, 'write', sinon.fake()); | ||
forwardToDaemon(resolver, config); | ||
await new Promise(setImmediate); | ||
socket.on.firstCall.callback(); // readable | ||
socket.on.secondCall.callback(); // end | ||
assert.calledWith(process.stdout.write, 'response from daemon'); | ||
assert.equals(process.exitCode, 1); | ||
refute.called(console.error); | ||
}); | ||
it('prints original input to stdout if no output', async () => { | ||
argv.push('--stdin', '--fix-to-stdout'); | ||
const chunks = ['[{}]EXIT000']; | ||
sinon.replace( | ||
socket, | ||
'read', | ||
sinon.fake(() => (chunks.length ? chunks.shift() : null)) | ||
); | ||
sinon.replace(process.stdout, 'write', sinon.fake()); | ||
forwardToDaemon(resolver, config); | ||
await new Promise(setImmediate); | ||
socket.on.firstCall.callback(); // readable | ||
socket.on.secondCall.callback(); // end | ||
assert.calledWith(process.stdout.write, 'text from stdin'); | ||
assert.equals(process.exitCode, 0); | ||
refute.called(console.error); | ||
}); | ||
it('prints error to stderr and original input to stdout if output cannot be parsed', async () => { | ||
argv.push('--stdin', '--fix-to-stdout'); | ||
const chunks = ['NotJSON!EXIT000']; | ||
sinon.replace( | ||
socket, | ||
'read', | ||
sinon.fake(() => (chunks.length ? chunks.shift() : null)) | ||
); | ||
sinon.replace(process.stdout, 'write', sinon.fake()); | ||
forwardToDaemon(resolver, config); | ||
await new Promise(setImmediate); | ||
socket.on.firstCall.callback(); // readable | ||
socket.on.secondCall.callback(); // end | ||
assert.calledWith(process.stdout.write, 'text from stdin'); | ||
assert.equals(process.exitCode, 1); | ||
let error; | ||
try { | ||
JSON.parse('NotJSON!'); | ||
} catch (err) { | ||
error = err; | ||
} | ||
assert.calledOnceWith(console.error, `eslint_d: ${error}`); | ||
}); | ||
}); | ||
}); | ||
}); |
@@ -10,2 +10,3 @@ export function help() { | ||
status Show daemon status, process id and resolved eslint version | ||
--fix-to-stdout Print fixed file to stdout (requires --stdin) | ||
--help, -h Show this help | ||
@@ -12,0 +13,0 @@ --version, -v Show version number of eslint_d and bundled eslint |
import { createRequire } from 'node:module'; | ||
import { dirname } from 'node:path'; | ||
@@ -19,3 +20,3 @@ /** | ||
try { | ||
path = require.resolve('eslint', { paths: [process.cwd()] }); | ||
path = require.resolve('eslint/package.json', { paths: [process.cwd()] }); | ||
} catch (err) { | ||
@@ -30,7 +31,7 @@ if (local === 'ignore') { | ||
// Fallback to bundled eslint | ||
path = require.resolve('eslint'); | ||
path = require.resolve('eslint/package.json'); | ||
bundled = true; | ||
} | ||
return { | ||
base: path.substring(0, path.lastIndexOf('/eslint/') + 7), | ||
base: dirname(path), | ||
bundled, | ||
@@ -37,0 +38,0 @@ require |
@@ -67,3 +67,3 @@ import { resolve } from 'node:path'; | ||
match( | ||
"eslint_d: Failed to resolve eslint - Error: Cannot find module 'eslint'" | ||
"eslint_d: Failed to resolve eslint - Error: Cannot find module 'eslint/package.json'" | ||
) | ||
@@ -70,0 +70,0 @@ ); |
@@ -42,3 +42,3 @@ import { createRequire } from 'node:module'; | ||
} | ||
const [request_token, color_level, cwd, argv] = content.split(' '); | ||
const [request_token, color_level, cwd, argv] = JSON.parse(content); | ||
if (request_token !== token) { | ||
@@ -50,3 +50,3 @@ con.end(); | ||
chalk.level = color_level; | ||
process.chdir(JSON.parse(cwd)); | ||
process.chdir(cwd); | ||
@@ -57,3 +57,3 @@ process.stdout.write = (chunk) => con.write(chunk); | ||
try { | ||
code = await eslint.execute(JSON.parse(argv), text, true); | ||
code = await eslint.execute(argv, text, true); | ||
} catch (e) { | ||
@@ -66,3 +66,3 @@ con.write(String(e)); | ||
/* eslint-enable require-atomic-updates */ | ||
con.end(`EXIT${code}`); | ||
con.end(`EXIT${String(code).padStart(3, '0')}`); | ||
} | ||
@@ -69,0 +69,0 @@ }) |
@@ -47,4 +47,4 @@ import { Socket } from 'node:net'; | ||
const chunks = [ | ||
`${request_token} ${color_level} `, | ||
`${JSON.stringify(cwd)} ${JSON.stringify(argv)}` | ||
`["${request_token}",${color_level},`, | ||
`${JSON.stringify(cwd)},${JSON.stringify(argv)}]` | ||
]; | ||
@@ -81,3 +81,3 @@ if (text !== undefined) { | ||
await eslint_promise.resolve(0); | ||
assert.equals(chalk.level, '3'); | ||
assert.equals(chalk.level, 3); | ||
assert.calledOnceWith(process.chdir, '/'); | ||
@@ -116,3 +116,3 @@ }); | ||
it('ends connection with "EXIT0" if eslint returns 0', async () => { | ||
it('ends connection with "EXIT000" if eslint returns 0', async () => { | ||
send(token, '3', '/', []); | ||
@@ -122,6 +122,6 @@ | ||
refute.called(con.write); | ||
assert.calledOnceWith(con.end, 'EXIT0'); | ||
assert.calledOnceWith(con.end, 'EXIT000'); | ||
}); | ||
it('ends connection with "EXIT1" if eslint returns 1', async () => { | ||
it('ends connection with "EXIT001" if eslint returns 1', async () => { | ||
send(token, '3', '/', []); | ||
@@ -131,6 +131,6 @@ | ||
refute.called(con.write); | ||
assert.calledOnceWith(con.end, 'EXIT1'); | ||
assert.calledOnceWith(con.end, 'EXIT001'); | ||
}); | ||
it('ends connection with "EXIT2" if eslint returns 2', async () => { | ||
it('ends connection with "EXIT002" if eslint returns 2', async () => { | ||
send(token, '3', '/', []); | ||
@@ -140,13 +140,21 @@ | ||
refute.called(con.write); | ||
assert.calledOnceWith(con.end, 'EXIT2'); | ||
assert.calledOnceWith(con.end, 'EXIT002'); | ||
}); | ||
it('ends connection with "EXIT1" if eslint throws', async () => { | ||
it('ends connection with "EXIT123" if eslint returns 123', async () => { | ||
send(token, '3', '/', []); | ||
await eslint_promise.resolve(123); | ||
refute.called(con.write); | ||
assert.calledOnceWith(con.end, 'EXIT123'); | ||
}); | ||
it('ends connection with "EXIT001" if eslint throws', async () => { | ||
send(token, '3', '/', []); | ||
await eslint_promise.reject(new Error('Ouch!')); | ||
assert.calledOnceWith(con.write, 'Error: Ouch!'); | ||
assert.calledOnceWith(con.end, 'EXIT1'); | ||
assert.calledOnceWith(con.end, 'EXIT001'); | ||
}); | ||
}); | ||
}); |
{ | ||
"name": "eslint_d", | ||
"version": "14.0.1", | ||
"version": "14.0.2", | ||
"description": "Speed up eslint to accelerate your development workflow", | ||
@@ -5,0 +5,0 @@ "type": "module", |
@@ -111,2 +111,3 @@ <h1 align="center"> | ||
--version, -v Show version number of eslint_d and bundled eslint | ||
--fix-to-stdout Print fixed file to stdout (requires --stdin) | ||
``` | ||
@@ -125,2 +126,25 @@ | ||
## Automatic fixing | ||
`eslint_d` has an additional option that `eslint` does not have, | ||
`--fix-to-stdout` which prints the fixed file to stdout. This allows editors to | ||
add before save hooks to automatically fix a file prior to saving. It must be | ||
used with `--stdin`. | ||
### Vim | ||
Add this to your `.vimrc` to lint the current buffer or visual selection on | ||
`<leader>f`: | ||
```vim | ||
" Autofix entire buffer with eslint_d: | ||
nnoremap <leader>f mF:%!eslint_d --stdin --fix-to-stdout --stdin-filename %<CR>`F | ||
" Autofix visual selection with eslint_d: | ||
vnoremap <leader>f :!eslint_d --stdin --fix-to-stdout<CR>gv | ||
``` | ||
### Emacs | ||
See [eslintd-fix](https://github.com/aaronjensen/eslintd-fix) | ||
## How does this work? | ||
@@ -157,3 +181,3 @@ | ||
- `14.0.0`: eslint 4 - 8, node 18 - 22 (ships with eslint 9) | ||
- `14.0.0`: eslint 4 - 9, node 18 - 22 (ships with eslint 9) (see [^1]) | ||
- `13.0.0`: eslint 4 - 8, node 12 - 20 (ships with eslint 8) | ||
@@ -183,1 +207,3 @@ - `12.0.0`: eslint 4 - 8, node 12 - 16 (ships with eslint 8) | ||
[SublimeLinter-eslint]: https://github.com/SublimeLinter/SublimeLinter-eslint | ||
[^1]: The support for `--fix-to-stdout` is only provided with eslint 5 and beyond. |
69263
1768
206