File size: 6,028 Bytes
a23aa7d
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
#!/usr/bin/env node

/**
 * @fileoverview Main CLI that is run via the eslint command.
 * @author Nicholas C. Zakas
 */

/* eslint no-console:off -- CLI */

"use strict";

const mod = require("node:module");

// to use V8's code cache to speed up instantiation time
mod.enableCompileCache?.();

// must do this initialization *before* other requires in order to work
if (process.argv.includes("--debug")) {
	require("debug").enable("eslint:*,-eslint:code-path,eslintrc:*");
}

//------------------------------------------------------------------------------
// Helpers
//------------------------------------------------------------------------------

/**
 * Read data from stdin til the end.
 *
 * Note: See
 * - https://github.com/nodejs/node/blob/master/doc/api/process.md#processstdin
 * - https://github.com/nodejs/node/blob/master/doc/api/process.md#a-note-on-process-io
 * - https://lists.gnu.org/archive/html/bug-gnu-emacs/2016-01/msg00419.html
 * - https://github.com/nodejs/node/issues/7439 (historical)
 *
 * On Windows using `fs.readFileSync(STDIN_FILE_DESCRIPTOR, "utf8")` seems
 * to read 4096 bytes before blocking and never drains to read further data.
 *
 * The investigation on the Emacs thread indicates:
 *
 * > Emacs on MS-Windows uses pipes to communicate with subprocesses; a
 * > pipe on Windows has a 4K buffer. So as soon as Emacs writes more than
 * > 4096 bytes to the pipe, the pipe becomes full, and Emacs then waits for
 * > the subprocess to read its end of the pipe, at which time Emacs will
 * > write the rest of the stuff.
 * @returns {Promise<string>} The read text.
 */
function readStdin() {
	return new Promise((resolve, reject) => {
		let content = "";
		let chunk = "";

		process.stdin
			.setEncoding("utf8")
			.on("readable", () => {
				while ((chunk = process.stdin.read()) !== null) {
					content += chunk;
				}
			})
			.on("end", () => resolve(content))
			.on("error", reject);
	});
}

/**
 * Spawns an external command and propagates its exit status.
 * @param {string} command The command to run.
 * @param {string[]} args The command arguments.
 * @throws {Error} If the command cannot be spawned.
 * @returns {void}
 */
function spawnExternalCommand(command, args) {
	const spawn = require("cross-spawn");
	const result = spawn.sync(command, args, {
		encoding: "utf8",
		stdio: "inherit",
	});

	if (result.error) {
		throw result.error;
	}

	if (result.signal) {
		process.kill(process.pid, result.signal);
		return;
	}

	process.exitCode = result.status ?? 0;
}

/**
 * Get the error message of a given value.
 * @param {any} error The value to get.
 * @returns {string} The error message.
 */
function getErrorMessage(error) {
	// Lazy loading because this is used only if an error happened.
	const util = require("node:util");

	// Foolproof -- third-party module might throw non-object.
	if (typeof error !== "object" || error === null) {
		return String(error);
	}

	// Use templates if `error.messageTemplate` is present.
	if (typeof error.messageTemplate === "string") {
		try {
			const template = require(`../messages/${error.messageTemplate}.js`);

			return template(error.messageData || {});
		} catch {
			// Ignore template error then fallback to use `error.stack`.
		}
	}

	// Use the stacktrace if it's an error object.
	if (typeof error.stack === "string") {
		return error.stack;
	}

	// Otherwise, dump the object.
	return util.format("%o", error);
}

/**
 * Tracks error messages that are shown to the user so we only ever show the
 * same message once.
 * @type {Set<string>}
 */
const displayedErrors = new Set();

/**
 * Tracks whether an unexpected error was caught
 * @type {boolean}
 */
let hadFatalError = false;

/**
 * Catch and report unexpected error.
 * @param {any} error The thrown error object.
 * @returns {void}
 */
function onFatalError(error) {
	process.exitCode = 2;
	hadFatalError = true;

	const { version } = require("../package.json");
	const message = `
Oops! Something went wrong! :(

ESLint: ${version}

${getErrorMessage(error)}`;

	if (!displayedErrors.has(message)) {
		console.error(message);
		displayedErrors.add(message);
	}
}

//------------------------------------------------------------------------------
// Execution
//------------------------------------------------------------------------------

(async function main() {
	process.on("uncaughtException", onFatalError);
	process.on("unhandledRejection", onFatalError);

	// Call the config initializer if `--init` is present.
	if (process.argv.includes("--init")) {
		// `eslint --init` has been moved to `@eslint/create-config`
		console.warn(
			"You can also run this command directly using 'npm init @eslint/config@latest'.",
		);

		spawnExternalCommand("npm", ["init", "@eslint/config@latest"]);
		return;
	}

	// start the MCP server if `--mcp` is present
	if (process.argv.includes("--mcp")) {
		console.warn(
			"You can also run this command directly using 'npx @eslint/mcp@latest'.",
		);

		spawnExternalCommand("npx", ["@eslint/mcp@latest"]);
		return;
	}

	// Otherwise, call the CLI.
	const cli = require("../lib/cli");
	const exitCode = await cli.execute(
		process.argv,
		process.argv.includes("--stdin") ? await readStdin() : void 0,
	);

	/*
	 * If an uncaught exception or unhandled rejection was detected in the meantime,
	 * keep the fatal exit code 2 that is already assigned to `process.exitCode`.
	 * Without this condition, exit code 2 (unsuccessful execution) could be overwritten with
	 * 1 (successful execution, lint problems found) or even 0 (successful execution, no lint problems found).
	 * This ensures that unexpected errors that seemingly don't affect the success
	 * of the execution will still cause a non-zero exit code, as it's a common
	 * practice and the default behavior of Node.js to exit with non-zero
	 * in case of an uncaught exception or unhandled rejection.
	 *
	 * Otherwise, assign the exit code returned from CLI.
	 */
	if (!hadFatalError) {
		process.exitCode = exitCode;
	}
})().catch(onFatalError);