Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: allow prettier to use custom node #3061

Closed
wants to merge 2 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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",
Copy link

@dmaskasky dmaskasky Mar 12, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

待办事项 翻译我

"ext.config.withNodeModules": "這個套件會對 node_modules 的檔案進行排版。",
"ext.config.semi": "是否要在每一列的結尾加上分號。",
"ext.config.singleQuote": "會使用單引號而非雙引號。",
Expand Down
147 changes: 147 additions & 0 deletions src/ChildProcessWorker.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
import { fileURLToPath, URL } from "url";
import { ChildProcess, fork, ForkOptions } from "child_process";
import { EventEmitter } from "events";

const alive = (child: ChildProcess): boolean => {
try {
return !!process.kill(child.pid!, 0);
} catch (e: any) {
if (e.code === "EPERM" || e.code === "ESRCH") {
return false;
}
throw e;
}
};

const kill = async (child: ChildProcess) => {
if (!alive(child)) {
return;
}
return new Promise((resolve) => {
let timeout: NodeJS.Timeout | null = null;
child.once("exit", () => {
if (timeout) {
clearTimeout(timeout);
}
resolve(undefined);
});
child.kill("SIGINT");
timeout = setTimeout(() => {
child.kill("SIGTERM");
timeout = null;
}, 3000);
});
};

export class ChildProcessWorker {
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This class mimics Worker (with on("message", ...) and postMessage(...)) except it uses fork and node's IPC instead.

#process: ChildProcess | null = null;
#url: URL;
#processOptions: ForkOptions;
#events: EventEmitter;
#queue: any[];
#exitCode: number | null = null;

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.#exitCode = -1;
this.#events.emit("error", err);
})
.on("exit", (code) => {
this.#process = null;
this.#exitCode = code;
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}.`);
}

async terminate(): Promise<number> {
if (!this.#process) {
return this.#exitCode ?? -1;
}
await kill(this.#process);
return this.#exitCode ?? -1;
}

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;
}
46 changes: 41 additions & 5 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,11 +39,25 @@ export const PrettierWorkerInstance: PrettierInstanceConstructor = class Prettie
}
> = new Map();

private worker: ChildProcessWorker | Worker | null = null;
private currentCallMethodId = 0;

public version: string | null = null;

constructor(private modulePath: string) {
constructor(
private modulePath: string,
private context: PrettierInstanceContext
) {
this.createWorker();
}

private createWorker() {
const worker = this.context.config.runtime
? new ChildProcessWorker(processWorker, {
execPath: this.context.config.runtime,
})
: new Worker(threadWorker);

worker.on("message", ({ type, payload }) => {
switch (type) {
case "import": {
Expand All @@ -60,13 +79,27 @@ export const PrettierWorkerInstance: PrettierInstanceConstructor = class Prettie
}
}
});
worker.on("error", async (err) => {
this.worker = null;
this.context.loggingService.logInfo(
`Worker error ${err.message}`,
err.stack
);
await worker.terminate();
this.createWorker();
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TODO: Consider throttling this.

});

this.worker = worker;
}

public async import(): Promise</* version of imported prettier */ string> {
if (!this.worker) {
throw new Error("Worker not available.");
}
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 @@ -123,11 +156,14 @@ export const PrettierWorkerInstance: PrettierInstanceConstructor = class Prettie
}

private callMethod(methodName: string, methodArgs: unknown[]): Promise<any> {
if (!this.worker) {
throw new Error("Worker not available.");
}
const callMethodId = this.currentCallMethodId++;
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
Loading