UNPKG

eslint_d

Version:

Speed up eslint to accelerate your development workflow

237 lines (214 loc) 6.06 kB
import debug from 'debug'; import net from 'node:net'; import supportsColor from 'supports-color'; import { removeConfig } from '../lib/config.js'; import { LINT_COMMAND, PING_COMMAND } from './commands.js'; import { launchDaemon } from './launcher.js'; /** * @import { Config } from './config.js' * @import { Resolver } from './resolver.js' */ const EXIT_TOKEN_REGEXP = new RegExp(/EXIT([0-9]{3})/); const EXIT_TOKEN_LENGTH = 7; const log = debug('eslint_d:forwarder'); /** * @param {Config | null} config * @returns {Promise<boolean>} */ export async function isAlive(config) { try { await forwardCommandToDaemon(config, PING_COMMAND); return true; } catch { return false; } } /** * @param {Config | null} config * @param {string} command * @returns {Promise<void>} */ export async function forwardCommandToDaemon(config, command) { if (!config) { return Promise.reject(new Error('Config not found')); } const socket = await connectToDaemon(config); log('Request: %o', { token: config.token, command }); socket.write(JSON.stringify([config.token, command])); socket.end(); return new Promise((resolve, reject) => { socket.on('end', resolve).on('error', reject); }); } /** * @param {Resolver} resolver * @param {Config} config */ 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' ); } let socket; try { socket = await createSocket(true); } catch (err) { console.error(`eslint_d: ${err}`); // eslint-disable-next-line require-atomic-updates process.exitCode = 1; return; } const color_level = stdout ? stdout.level : 0; const cwd = process.cwd(); const DEBUG = process.env.DEBUG || ''; log('Request: %o', { token: config.token, command: LINT_COMMAND, color_level, cwd, args: eslint_args, DEBUG }); const args = [ config.token, LINT_COMMAND, color_level, cwd, eslint_args, DEBUG ]; socket.write(JSON.stringify(args)); if (text) { log('Request stdin: %d', text.length); socket.write('\n'); socket.write(text); } socket.end(); let content = ''; socket .setEncoding('utf8') .on('readable', () => { let chunk; while ((chunk = socket.read()) !== null) { content += chunk; if (!fix_to_stdout && content.length > EXIT_TOKEN_LENGTH) { process.stdout.write(flushMessage()); } } }) .on('end', () => { // The remaining 'content' must be the termination code: const match = content.match(EXIT_TOKEN_REGEXP); if (!match) { process.stdout.write(content); console.error('eslint_d: unexpected response'); process.exitCode = 1; return; } if (!fix_to_stdout) { const exit_code = Number(match[1]); log('Exit %d', exit_code); process.exitCode = exit_code; return; } try { const { output } = JSON.parse(flushMessage())[0]; process.stdout.write(output || text); // Exit code from eslint is deliberately ignored in this case log('Exit %d', 0); process.exitCode = 0; } catch (err) { process.stdout.write(text); console.error(`eslint_d: ${err}`); process.exitCode = 1; } }) .on('error', (err) => { console.error(`eslint_d: ${err}`); process.exitCode = 1; }); /** * @param {boolean} retry * @returns {Promise<net.Socket>} */ async function createSocket(retry) { try { return await connectToDaemon(config); } catch (/** @type {Object} */ err) { log('Failed to connect %o', err); if (err['code'] === 'ECONNREFUSED' && retry) { await removeConfig(resolver); // @ts-expect-error we check for nullability in the line below // eslint-disable-next-line require-atomic-updates config = await launchDaemon(resolver, config.hash); if (!config) { throw new Error('Unable to start daemon', err); } return createSocket(false); } if (err['code'] === 'ECONNREFUSED') { await removeConfig(resolver); throw new Error(`${err.message} - removing config`, err); } throw 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; } } /** * @param {Config} config * @returns {Promise<net.Socket>} */ function connectToDaemon(config) { log('Connecting to daemon 127.0.0.1:%d', config.port); return new Promise((resolve, reject) => { const socket = net.connect(config.port, '127.0.0.1'); socket .on('connect', () => { socket.off('error', reject); resolve(socket); }) .once('error', reject); }); } function readStdin() { return new Promise((resolve, reject) => { let content = ''; process.stdin .setEncoding('utf8') .on('readable', () => { let chunk; while ((chunk = process.stdin.read()) !== null) { content += chunk; } }) .on('end', () => resolve(content)) .on('error', reject); }); }