Skip to content

Commit a1b0d86

Browse files
feat: allow prettier to use custom node
Adds a `prettier.runtime` configuration option that, when used, invokes a version of `PrettierWorkerInstance` that uses `fork` instead of worker threads. Fixes: #3017 Fixes: #2857
1 parent 4152e7b commit a1b0d86

14 files changed

+265
-84
lines changed

CHANGELOG.md

+2
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ All notable changes to the "prettier-vscode" extension will be documented in thi
66

77
## [Unreleased]
88

9+
- Adds `prettier.runtime` config value to allow choosing the command to run prettier
10+
911
## [9.19.0]
1012

1113
- Reverts change to `prettierPath` resolution. (#3045)

package.json

+5
Original file line numberDiff line numberDiff line change
@@ -189,6 +189,11 @@
189189
"markdownDescription": "%ext.config.resolveGlobalModules%",
190190
"scope": "resource"
191191
},
192+
"prettier.runtime": {
193+
"type": "string",
194+
"markdownDescription": "%ext.config.runtime%",
195+
"scope": "resource"
196+
},
192197
"prettier.withNodeModules": {
193198
"type": "boolean",
194199
"default": false,

package.nls.json

+1
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
"ext.config.requireConfig": "Require a prettier configuration file to format. See [documentation for valid configuration files](https://prettier.io/docs/en/configuration.html).\n\n> _Note, untitled files will still be formatted using the VS Code prettier settings even when this setting is set._",
2828
"ext.config.requirePragma": "Prettier can restrict itself to only format files that contain a special comment, called a pragma, at the top of the file. This is very useful when gradually transitioning large, unformatted codebases to prettier.",
2929
"ext.config.resolveGlobalModules": "When enabled, this extension will attempt to use global npm or yarn modules if local modules cannot be resolved.\n> _This setting can have a negative performance impact, particularly on Windows when you have attached network drives. Only enable this if you must use global modules._",
30+
"ext.config.runtime": "he location of the node binary to run prettier under.",
3031
"ext.config.withNodeModules": "This extension will process files in `node_modules`.",
3132
"ext.config.semi": "Whether to add a semicolon at the end of every line.",
3233
"ext.config.singleQuote": "Use single instead of double quotes.",

package.nls.zh-cn.json

+1
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
"ext.config.requireConfig": "Prettier 配置文件(如 `.prettierrc`)必须存在。详见 [配置文件的文档说明](https://prettier.io/docs/en/configuration.html)。\n\n_注意:未命名文件仍会使用 `VS Code` 的 `setting.json` 中的配置进行格式化,不受该选项影响。_",
2020
"ext.config.requirePragma": "Prettier 可以限制只对包含特定注释的文件进行格式化,这个特定的注释称为 pragma。这对于那些大型的、尚未采用 Prettier 的代码仓库逐步引入 Prettier 非常有用。",
2121
"ext.config.resolveGlobalModules": "如果在当前项目中找不到 `prettier` 包时尝试使用 npm 或 yarn 全局安装的包。\n>_该设置可能影响性能,特别是在 Windows 中挂载了网络磁盘的时候。只有在你需要使用全局安装的包时再启用。_",
22+
"ext.config.runtime": "TODO TRANSLATE ME",
2223
"ext.config.withNodeModules": "允许 Prettier 格式化 `node_modules` 中的文件。",
2324
"ext.config.semi": "在所有代码语句的末尾添加分号。",
2425
"ext.config.singleQuote": "使用单引号而不是双引号。",

package.nls.zh-tw.json

+1
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
"ext.config.requireConfig": "排版需要 prettier 組態檔。參閱[可用的組態檔文件](https://prettier.io/docs/en/configuration.html).\n\n> _注意,無標題檔案仍會使用 VS Code 的 prettier 設定進行排版,不受這個設定值影響。_",
2525
"ext.config.requirePragma": "Prettier 可以限制它自己只對包含特殊註解的檔案進行排版,這個特殊的註解成為 pragma ,位於檔案的最頂部。這對於想要對那些大型、未經過排版的程式碼緩步採納 prettier 非常有幫助。",
2626
"ext.config.resolveGlobalModules": "這個套件會在區域的模組找不到 prettier 模組時嘗試使用去全域的 npm 或 yarn 模組中尋找。\n> _這個設定會導致效能的負面影響,特別在 Windows 中有掛載網路磁碟機。只有在你必須使用全域模組的情況下再啟用。_",
27+
"ext.config.runtime": "TODO TRANSLATE ME",
2728
"ext.config.withNodeModules": "這個套件會對 node_modules 的檔案進行排版。",
2829
"ext.config.semi": "是否要在每一列的結尾加上分號。",
2930
"ext.config.singleQuote": "會使用單引號而非雙引號。",

src/ChildProcessWorker.ts

+105
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
import { fileURLToPath, URL } from "url";
2+
import { ChildProcess, fork, ForkOptions } from "child_process";
3+
import { EventEmitter } from "events";
4+
5+
export class ChildProcessWorker {
6+
#process: ChildProcess | null = null;
7+
#url: URL;
8+
#processOptions: ForkOptions;
9+
#events: EventEmitter;
10+
#queue: any[];
11+
12+
constructor(url: URL, processOptions: ForkOptions) {
13+
this.#url = url;
14+
this.#processOptions = processOptions;
15+
this.#events = new EventEmitter();
16+
this.#queue = [];
17+
void Promise.resolve().then(() => this.startProcess());
18+
}
19+
20+
startProcess() {
21+
try {
22+
const stderr: Buffer[] = [];
23+
const stdout: Buffer[] = [];
24+
this.#process = fork(fileURLToPath(this.#url), [], {
25+
...this.#processOptions,
26+
stdio: ["pipe", "pipe", "pipe", "ipc"],
27+
});
28+
this.#process.stderr?.on("data", (chunk) => {
29+
stderr.push(chunk);
30+
});
31+
this.#process.stdout?.on("data", (chunk) => {
32+
stdout.push(chunk);
33+
});
34+
this.#process
35+
.on("error", (err) => {
36+
this.#process = null;
37+
this.#events.emit("error", err);
38+
})
39+
.on("exit", (code) => {
40+
this.#process = null;
41+
const stdoutResult = Buffer.concat(stdout).toString("utf8");
42+
const stderrResult = Buffer.concat(stderr).toString("utf8");
43+
if (code !== 0) {
44+
this.#events.emit(
45+
"error",
46+
new Error(
47+
`Process crashed with code ${code}: ${stdoutResult} ${stderrResult}`
48+
)
49+
);
50+
} else {
51+
this.#events.emit(
52+
"error",
53+
new Error(
54+
`Process unexpectedly exit: ${stdoutResult} ${stderrResult}`
55+
)
56+
);
57+
}
58+
})
59+
.on("message", (msg) => {
60+
this.#events.emit("message", msg);
61+
});
62+
this.flushQueue();
63+
} catch (err) {
64+
this.#process = null;
65+
this.#events.emit("error", err);
66+
}
67+
}
68+
69+
on(evt: string, fn: (payload: any) => void) {
70+
if (evt === "message" || evt === "error") {
71+
this.#events.on(evt, fn);
72+
return;
73+
}
74+
throw new Error(`Unsupported event ${evt}.`);
75+
}
76+
77+
flushQueue() {
78+
if (!this.#process) {
79+
return;
80+
}
81+
let items = 0;
82+
for (const entry of this.#queue) {
83+
if (!this.#process.send(entry)) {
84+
break;
85+
}
86+
items++;
87+
}
88+
if (items > 0) {
89+
this.#queue.splice(0, items);
90+
}
91+
}
92+
93+
postMessage(data: any) {
94+
this.flushQueue();
95+
if (this.#process) {
96+
if (this.#process.send(data)) {
97+
return true;
98+
} else {
99+
this.#queue.push(data);
100+
}
101+
}
102+
this.#queue.push(data);
103+
return false;
104+
}
105+
}

src/ModuleResolver.ts

+25-7
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ import {
2525
} from "./types";
2626
import { getConfig, getWorkspaceRelativePath, isAboveV3 } from "./util";
2727
import { PrettierWorkerInstance } from "./PrettierWorkerInstance";
28-
import { PrettierInstance } from "./PrettierInstance";
28+
import { PrettierInstance, PrettierInstanceContext } from "./PrettierInstance";
2929
import { PrettierMainThreadInstance } from "./PrettierMainThreadInstance";
3030
import { loadNodeModule, resolveConfigPlugins } from "./ModuleLoader";
3131

@@ -155,9 +155,8 @@ export class ModuleResolver implements ModuleResolverInterface {
155155
return prettier;
156156
}
157157

158-
const { prettierPath, resolveGlobalModules } = getConfig(
159-
Uri.file(fileName)
160-
);
158+
const config = getConfig(Uri.file(fileName));
159+
const { prettierPath, resolveGlobalModules } = config;
161160

162161
// Look for local module
163162
let modulePath: string | undefined = undefined;
@@ -213,6 +212,10 @@ export class ModuleResolver implements ModuleResolverInterface {
213212
}
214213

215214
let moduleInstance: PrettierInstance | undefined = undefined;
215+
const context: PrettierInstanceContext = {
216+
config,
217+
loggingService: this.loggingService,
218+
};
216219

217220
if (modulePath !== undefined) {
218221
this.loggingService.logDebug(
@@ -227,12 +230,27 @@ export class ModuleResolver implements ModuleResolverInterface {
227230
const prettierVersion =
228231
this.loadPrettierVersionFromPackageJson(modulePath);
229232

233+
this.loggingService.logInfo(
234+
`Detected prettier version: '${prettierVersion}'`
235+
);
236+
230237
const isAboveVersion3 = isAboveV3(prettierVersion);
231238

232-
if (isAboveVersion3) {
233-
moduleInstance = new PrettierWorkerInstance(modulePath);
239+
if (isAboveVersion3 || config.runtime) {
240+
if (config.runtime) {
241+
this.loggingService.logInfo(
242+
`Using node version: ${execSync(
243+
`${config.runtime} --version`
244+
)}.`
245+
);
246+
}
247+
248+
moduleInstance = new PrettierWorkerInstance(modulePath, context);
234249
} else {
235-
moduleInstance = new PrettierMainThreadInstance(modulePath);
250+
moduleInstance = new PrettierMainThreadInstance(
251+
modulePath,
252+
context
253+
);
236254
}
237255
if (moduleInstance) {
238256
this.path2Module.set(modulePath, moduleInstance);

src/PrettierInstance.ts

+8-1
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,9 @@ import {
55
PrettierOptions,
66
PrettierPlugin,
77
PrettierSupportLanguage,
8+
PrettierVSCodeConfig,
89
} from "./types";
10+
import { LoggingService } from "./LoggingService";
911

1012
export interface PrettierInstance {
1113
version: string | null;
@@ -30,6 +32,11 @@ export interface PrettierInstance {
3032
): Promise<PrettierOptions | null>;
3133
}
3234

35+
export interface PrettierInstanceContext {
36+
config: PrettierVSCodeConfig;
37+
loggingService: LoggingService;
38+
}
39+
3340
export interface PrettierInstanceConstructor {
34-
new (modulePath: string): PrettierInstance;
41+
new (modulePath: string, context: PrettierInstanceContext): PrettierInstance;
3542
}

src/PrettierWorkerInstance.ts

+21-6
Original file line numberDiff line numberDiff line change
@@ -8,14 +8,19 @@ import {
88
PrettierPlugin,
99
PrettierSupportLanguage,
1010
} from "./types";
11+
import { ChildProcessWorker } from "./ChildProcessWorker";
1112
import {
1213
PrettierInstance,
1314
PrettierInstanceConstructor,
15+
PrettierInstanceContext,
1416
} from "./PrettierInstance";
1517
import { ResolveConfigOptions, Options } from "prettier";
1618

17-
const worker = new Worker(
18-
url.pathToFileURL(path.join(__dirname, "/worker/prettier-instance-worker.js"))
19+
const processWorker = url.pathToFileURL(
20+
path.join(__dirname, "/worker/prettier-instance-worker-process.js")
21+
);
22+
const threadWorker = url.pathToFileURL(
23+
path.join(__dirname, "/worker/prettier-instance-worker-process-thread.js")
1924
);
2025

2126
export const PrettierWorkerInstance: PrettierInstanceConstructor = class PrettierWorkerInstance
@@ -34,12 +39,19 @@ export const PrettierWorkerInstance: PrettierInstanceConstructor = class Prettie
3439
}
3540
> = new Map();
3641

42+
private worker;
3743
private currentCallMethodId = 0;
3844

3945
public version: string | null = null;
4046

41-
constructor(private modulePath: string) {
42-
worker.on("message", ({ type, payload }) => {
47+
constructor(private modulePath: string, context: PrettierInstanceContext) {
48+
this.worker = context.config.runtime
49+
? new ChildProcessWorker(processWorker, {
50+
execPath: context.config.runtime,
51+
})
52+
: new Worker(threadWorker);
53+
54+
this.worker.on("message", ({ type, payload }) => {
4355
switch (type) {
4456
case "import": {
4557
this.importResolver?.resolve(payload.version);
@@ -60,13 +72,16 @@ export const PrettierWorkerInstance: PrettierInstanceConstructor = class Prettie
6072
}
6173
}
6274
});
75+
this.worker.on("error", (err) => {
76+
context.loggingService.logInfo(`Worker error ${err.message}`, err.stack);
77+
});
6378
}
6479

6580
public async import(): Promise</* version of imported prettier */ string> {
6681
const promise = new Promise<string>((resolve, reject) => {
6782
this.importResolver = { resolve, reject };
6883
});
69-
worker.postMessage({
84+
this.worker.postMessage({
7085
type: "import",
7186
payload: { modulePath: this.modulePath },
7287
});
@@ -127,7 +142,7 @@ export const PrettierWorkerInstance: PrettierInstanceConstructor = class Prettie
127142
const promise = new Promise((resolve, reject) => {
128143
this.callMethodResolvers.set(callMethodId, { resolve, reject });
129144
});
130-
worker.postMessage({
145+
this.worker.postMessage({
131146
type: "callMethod",
132147
payload: {
133148
id: callMethodId,

src/types.d.ts

+4
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,10 @@ interface IExtensionConfig {
102102
* If true, enabled debug logs
103103
*/
104104
enableDebugLogs: boolean;
105+
/**
106+
* If defined, a path to the node runtime.
107+
*/
108+
runtime: string | undefined;
105109
}
106110
/**
107111
* Configuration for prettier-vscode

src/util.ts

+1
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ export function getConfig(uri?: Uri): PrettierVSCodeConfig {
4646
useEditorConfig: false,
4747
withNodeModules: false,
4848
resolveGlobalModules: false,
49+
runtime: undefined,
4950
};
5051
return newConfig;
5152
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
const createWorker = require("./prettier-instance-worker");
2+
3+
const parentPort = {
4+
on: (evt, fn) => {
5+
if (evt === "message") {
6+
process.on(evt, fn);
7+
return;
8+
}
9+
throw new Error(`Unsupported event ${evt}.`);
10+
},
11+
postMessage(msg) {
12+
process.send(msg);
13+
},
14+
};
15+
16+
createWorker(parentPort);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
const { parentPort } = require("worker_threads");
2+
const createWorker = require("./prettier-instance-worker");
3+
4+
createWorker(parentPort);

0 commit comments

Comments
 (0)