Skip to content

Commit

Permalink
feat: allow prettier to use custom node
Browse files Browse the repository at this point in the history
Adds a `prettier.runtime` configuration option that, when used, invokes a version of `PrettierWorkerInstance` that uses `fork` instead of worker threads.

Fixes: prettier#3017
Fixes: prettier#2857
  • Loading branch information
izaakschroeder committed Jul 3, 2023
1 parent 4152e7b commit a1b0d86
Show file tree
Hide file tree
Showing 14 changed files with 265 additions and 84 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ All notable changes to the "prettier-vscode" extension will be documented in thi

## [Unreleased]

- Adds `prettier.runtime` config value to allow choosing the command to run prettier

## [9.19.0]

- Reverts change to `prettierPath` resolution. (#3045)
Expand Down
5 changes: 5 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -189,6 +189,11 @@
"markdownDescription": "%ext.config.resolveGlobalModules%",
"scope": "resource"
},
"prettier.runtime": {
"type": "string",
"markdownDescription": "%ext.config.runtime%",
"scope": "resource"
},
"prettier.withNodeModules": {
"type": "boolean",
"default": false,
Expand Down
1 change: 1 addition & 0 deletions package.nls.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
"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._",
"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.",
"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._",
"ext.config.runtime": "he location of the node binary to run prettier under.",
"ext.config.withNodeModules": "This extension will process files in `node_modules`.",
"ext.config.semi": "Whether to add a semicolon at the end of every line.",
"ext.config.singleQuote": "Use single instead of double quotes.",
Expand Down
1 change: 1 addition & 0 deletions package.nls.zh-cn.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
"ext.config.requireConfig": "Prettier 配置文件(如 `.prettierrc`)必须存在。详见 [配置文件的文档说明](https://prettier.io/docs/en/configuration.html)。\n\n_注意:未命名文件仍会使用 `VS Code` 的 `setting.json` 中的配置进行格式化,不受该选项影响。_",
"ext.config.requirePragma": "Prettier 可以限制只对包含特定注释的文件进行格式化,这个特定的注释称为 pragma。这对于那些大型的、尚未采用 Prettier 的代码仓库逐步引入 Prettier 非常有用。",
"ext.config.resolveGlobalModules": "如果在当前项目中找不到 `prettier` 包时尝试使用 npm 或 yarn 全局安装的包。\n>_该设置可能影响性能,特别是在 Windows 中挂载了网络磁盘的时候。只有在你需要使用全局安装的包时再启用。_",
"ext.config.runtime": "TODO TRANSLATE ME",
"ext.config.withNodeModules": "允许 Prettier 格式化 `node_modules` 中的文件。",
"ext.config.semi": "在所有代码语句的末尾添加分号。",
"ext.config.singleQuote": "使用单引号而不是双引号。",
Expand Down
1 change: 1 addition & 0 deletions package.nls.zh-tw.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
"ext.config.requireConfig": "排版需要 prettier 組態檔。參閱[可用的組態檔文件](https://prettier.io/docs/en/configuration.html).\n\n> _注意,無標題檔案仍會使用 VS Code 的 prettier 設定進行排版,不受這個設定值影響。_",
"ext.config.requirePragma": "Prettier 可以限制它自己只對包含特殊註解的檔案進行排版,這個特殊的註解成為 pragma ,位於檔案的最頂部。這對於想要對那些大型、未經過排版的程式碼緩步採納 prettier 非常有幫助。",
"ext.config.resolveGlobalModules": "這個套件會在區域的模組找不到 prettier 模組時嘗試使用去全域的 npm 或 yarn 模組中尋找。\n> _這個設定會導致效能的負面影響,特別在 Windows 中有掛載網路磁碟機。只有在你必須使用全域模組的情況下再啟用。_",
"ext.config.runtime": "TODO TRANSLATE ME",
"ext.config.withNodeModules": "這個套件會對 node_modules 的檔案進行排版。",
"ext.config.semi": "是否要在每一列的結尾加上分號。",
"ext.config.singleQuote": "會使用單引號而非雙引號。",
Expand Down
105 changes: 105 additions & 0 deletions src/ChildProcessWorker.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
import { fileURLToPath, URL } from "url";
import { ChildProcess, fork, ForkOptions } from "child_process";
import { EventEmitter } from "events";

export class ChildProcessWorker {
#process: ChildProcess | null = null;
#url: URL;
#processOptions: ForkOptions;
#events: EventEmitter;
#queue: any[];

constructor(url: URL, processOptions: ForkOptions) {
this.#url = url;
this.#processOptions = processOptions;
this.#events = new EventEmitter();
this.#queue = [];
void Promise.resolve().then(() => this.startProcess());
}

startProcess() {
try {
const stderr: Buffer[] = [];
const stdout: Buffer[] = [];
this.#process = fork(fileURLToPath(this.#url), [], {
...this.#processOptions,
stdio: ["pipe", "pipe", "pipe", "ipc"],
});
this.#process.stderr?.on("data", (chunk) => {
stderr.push(chunk);
});
this.#process.stdout?.on("data", (chunk) => {
stdout.push(chunk);
});
this.#process
.on("error", (err) => {
this.#process = null;
this.#events.emit("error", err);
})
.on("exit", (code) => {
this.#process = null;
const stdoutResult = Buffer.concat(stdout).toString("utf8");
const stderrResult = Buffer.concat(stderr).toString("utf8");
if (code !== 0) {
this.#events.emit(
"error",
new Error(
`Process crashed with code ${code}: ${stdoutResult} ${stderrResult}`
)
);
} else {
this.#events.emit(
"error",
new Error(
`Process unexpectedly exit: ${stdoutResult} ${stderrResult}`
)
);
}
})
.on("message", (msg) => {
this.#events.emit("message", msg);
});
this.flushQueue();
} catch (err) {
this.#process = null;
this.#events.emit("error", err);
}
}

on(evt: string, fn: (payload: any) => void) {
if (evt === "message" || evt === "error") {
this.#events.on(evt, fn);
return;
}
throw new Error(`Unsupported event ${evt}.`);
}

flushQueue() {
if (!this.#process) {
return;
}
let items = 0;
for (const entry of this.#queue) {
if (!this.#process.send(entry)) {
break;
}
items++;
}
if (items > 0) {
this.#queue.splice(0, items);
}
}

postMessage(data: any) {
this.flushQueue();
if (this.#process) {
if (this.#process.send(data)) {
return true;
} else {
this.#queue.push(data);
}
}
this.#queue.push(data);
return false;
}
}
32 changes: 25 additions & 7 deletions src/ModuleResolver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ import {
} from "./types";
import { getConfig, getWorkspaceRelativePath, isAboveV3 } from "./util";
import { PrettierWorkerInstance } from "./PrettierWorkerInstance";
import { PrettierInstance } from "./PrettierInstance";
import { PrettierInstance, PrettierInstanceContext } from "./PrettierInstance";
import { PrettierMainThreadInstance } from "./PrettierMainThreadInstance";
import { loadNodeModule, resolveConfigPlugins } from "./ModuleLoader";

Expand Down Expand Up @@ -155,9 +155,8 @@ export class ModuleResolver implements ModuleResolverInterface {
return prettier;
}

const { prettierPath, resolveGlobalModules } = getConfig(
Uri.file(fileName)
);
const config = getConfig(Uri.file(fileName));
const { prettierPath, resolveGlobalModules } = config;

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

let moduleInstance: PrettierInstance | undefined = undefined;
const context: PrettierInstanceContext = {
config,
loggingService: this.loggingService,
};

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

this.loggingService.logInfo(
`Detected prettier version: '${prettierVersion}'`
);

const isAboveVersion3 = isAboveV3(prettierVersion);

if (isAboveVersion3) {
moduleInstance = new PrettierWorkerInstance(modulePath);
if (isAboveVersion3 || config.runtime) {
if (config.runtime) {
this.loggingService.logInfo(
`Using node version: ${execSync(
`${config.runtime} --version`
)}.`
);
}

moduleInstance = new PrettierWorkerInstance(modulePath, context);
} else {
moduleInstance = new PrettierMainThreadInstance(modulePath);
moduleInstance = new PrettierMainThreadInstance(
modulePath,
context
);
}
if (moduleInstance) {
this.path2Module.set(modulePath, moduleInstance);
Expand Down
9 changes: 8 additions & 1 deletion src/PrettierInstance.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@ import {
PrettierOptions,
PrettierPlugin,
PrettierSupportLanguage,
PrettierVSCodeConfig,
} from "./types";
import { LoggingService } from "./LoggingService";

export interface PrettierInstance {
version: string | null;
Expand All @@ -30,6 +32,11 @@ export interface PrettierInstance {
): Promise<PrettierOptions | null>;
}

export interface PrettierInstanceContext {
config: PrettierVSCodeConfig;
loggingService: LoggingService;
}

export interface PrettierInstanceConstructor {
new (modulePath: string): PrettierInstance;
new (modulePath: string, context: PrettierInstanceContext): PrettierInstance;
}
27 changes: 21 additions & 6 deletions src/PrettierWorkerInstance.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,19 @@ import {
PrettierPlugin,
PrettierSupportLanguage,
} from "./types";
import { ChildProcessWorker } from "./ChildProcessWorker";
import {
PrettierInstance,
PrettierInstanceConstructor,
PrettierInstanceContext,
} from "./PrettierInstance";
import { ResolveConfigOptions, Options } from "prettier";

const worker = new Worker(
url.pathToFileURL(path.join(__dirname, "/worker/prettier-instance-worker.js"))
const processWorker = url.pathToFileURL(
path.join(__dirname, "/worker/prettier-instance-worker-process.js")
);
const threadWorker = url.pathToFileURL(
path.join(__dirname, "/worker/prettier-instance-worker-process-thread.js")
);

export const PrettierWorkerInstance: PrettierInstanceConstructor = class PrettierWorkerInstance
Expand All @@ -34,12 +39,19 @@ export const PrettierWorkerInstance: PrettierInstanceConstructor = class Prettie
}
> = new Map();

private worker;
private currentCallMethodId = 0;

public version: string | null = null;

constructor(private modulePath: string) {
worker.on("message", ({ type, payload }) => {
constructor(private modulePath: string, context: PrettierInstanceContext) {
this.worker = context.config.runtime
? new ChildProcessWorker(processWorker, {
execPath: context.config.runtime,
})
: new Worker(threadWorker);

this.worker.on("message", ({ type, payload }) => {
switch (type) {
case "import": {
this.importResolver?.resolve(payload.version);
Expand All @@ -60,13 +72,16 @@ export const PrettierWorkerInstance: PrettierInstanceConstructor = class Prettie
}
}
});
this.worker.on("error", (err) => {
context.loggingService.logInfo(`Worker error ${err.message}`, err.stack);
});
}

public async import(): Promise</* version of imported prettier */ string> {
const promise = new Promise<string>((resolve, reject) => {
this.importResolver = { resolve, reject };
});
worker.postMessage({
this.worker.postMessage({
type: "import",
payload: { modulePath: this.modulePath },
});
Expand Down Expand Up @@ -127,7 +142,7 @@ export const PrettierWorkerInstance: PrettierInstanceConstructor = class Prettie
const promise = new Promise((resolve, reject) => {
this.callMethodResolvers.set(callMethodId, { resolve, reject });
});
worker.postMessage({
this.worker.postMessage({
type: "callMethod",
payload: {
id: callMethodId,
Expand Down
4 changes: 4 additions & 0 deletions src/types.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,10 @@ interface IExtensionConfig {
* If true, enabled debug logs
*/
enableDebugLogs: boolean;
/**
* If defined, a path to the node runtime.
*/
runtime: string | undefined;
}
/**
* Configuration for prettier-vscode
Expand Down
1 change: 1 addition & 0 deletions src/util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ export function getConfig(uri?: Uri): PrettierVSCodeConfig {
useEditorConfig: false,
withNodeModules: false,
resolveGlobalModules: false,
runtime: undefined,
};
return newConfig;
}
Expand Down
16 changes: 16 additions & 0 deletions src/worker/prettier-instance-worker-process.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
const createWorker = require("./prettier-instance-worker");

const parentPort = {
on: (evt, fn) => {
if (evt === "message") {
process.on(evt, fn);
return;
}
throw new Error(`Unsupported event ${evt}.`);
},
postMessage(msg) {
process.send(msg);
},
};

createWorker(parentPort);
4 changes: 4 additions & 0 deletions src/worker/prettier-instance-worker-thread.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
const { parentPort } = require("worker_threads");
const createWorker = require("./prettier-instance-worker");

createWorker(parentPort);
Loading

0 comments on commit a1b0d86

Please sign in to comment.