Skip to content

Commit 7f44fe6

Browse files
authored
Run prettier instance in worker_threads (#3016)
1 parent 27a6896 commit 7f44fe6

13 files changed

Lines changed: 353 additions & 39 deletions

.eslintignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
/src/worker/*.js

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,10 @@ All notable changes to the "prettier-vscode" extension will be documented in thi
44

55
<!-- Check [Keep a Changelog](https://keepachangelog.com/) for recommendations on how to structure this file. -->
66

7+
## [Unreleased]
8+
9+
- Run Prettier in worker_threads for v3.
10+
711
## [9.14.0]
812

913
- Fixes a bug in Remote SSH that had been occurring since VSCode 1.79.

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@
7272
"lint": "eslint -c .eslintrc.js --ext .ts .",
7373
"pretest": "yarn test-compile && cd test-fixtures/plugins && yarn install && cd ../plugins-pnpm && pnpm i && cd ../outdated && yarn install && cd ../module && yarn install && cd ../specific-version && yarn install && cd ../explicit-dep && yarn install && cd implicit-dep && yarn install && cd ../../v3 && yarn install",
7474
"prettier": "prettier --write '**/*.{ts,json,md,hbs,yml,js}'",
75-
"test-compile": "yarn clean && tsc -p ./ && yarn webpack",
75+
"test-compile": "yarn clean && tsc -p ./ && yarn webpack && cp -r ./src/worker ./out",
7676
"test": "node ./out/test/runTests.js",
7777
"version": "node ./scripts/version.js && git add CHANGELOG.md",
7878
"vscode:prepublish": "webpack --mode production",
@@ -105,6 +105,7 @@
105105
"@typescript-eslint/parser": "^5.45.0",
106106
"@vscode/test-electron": "^2.1.3",
107107
"@vscode/test-web": "^0.0.30",
108+
"copy-webpack-plugin": "^11.0.0",
108109
"eslint": "^8.31.0",
109110
"eslint-config-prettier": "^8.5.0",
110111
"fs-extra": "^11.1.1",

src/ModuleResolver.ts

Lines changed: 14 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import {
2424
PrettierVSCodeConfig,
2525
} from "./types";
2626
import { getConfig, getWorkspaceRelativePath } from "./util";
27+
import { PrettierWorkerInstance } from "./PrettierWorkerInstance";
2728

2829
const minPrettierVersion = "1.13.0";
2930
declare const __webpack_require__: typeof require;
@@ -93,7 +94,8 @@ function globalPathGet(packageManager: PackageManagers): string | undefined {
9394
export class ModuleResolver implements ModuleResolverInterface {
9495
private findPkgCache: Map<string, string>;
9596
private ignorePathCache = new Map<string, string>();
96-
private path2Module = new Map<string, PrettierNodeModule>();
97+
98+
private path2Module = new Map<string, PrettierWorkerInstance>();
9799

98100
constructor(private loggingService: LoggingService) {
99101
this.findPkgCache = new Map();
@@ -109,7 +111,7 @@ export class ModuleResolver implements ModuleResolverInterface {
109111
*/
110112
public async getPrettierInstance(
111113
fileName: string
112-
): Promise<PrettierNodeModule | undefined> {
114+
): Promise<PrettierNodeModule | PrettierWorkerInstance | undefined> {
113115
if (!workspace.isTrusted) {
114116
this.loggingService.logDebug(UNTRUSTED_WORKSPACE_USING_BUNDLED_PRETTIER);
115117
return prettier;
@@ -172,7 +174,8 @@ export class ModuleResolver implements ModuleResolverInterface {
172174
}
173175
}
174176

175-
let moduleInstance: PrettierNodeModule | undefined = undefined;
177+
let moduleInstance: PrettierWorkerInstance | undefined = undefined;
178+
176179
if (modulePath !== undefined) {
177180
this.loggingService.logDebug(
178181
`Local prettier module path: '${modulePath}'`
@@ -183,7 +186,7 @@ export class ModuleResolver implements ModuleResolverInterface {
183186
return moduleInstance;
184187
} else {
185188
try {
186-
moduleInstance = this.loadNodeModule<PrettierNodeModule>(modulePath);
189+
moduleInstance = new PrettierWorkerInstance(modulePath);
187190
if (moduleInstance) {
188191
this.path2Module.set(modulePath, moduleInstance);
189192
}
@@ -202,31 +205,23 @@ export class ModuleResolver implements ModuleResolverInterface {
202205
}
203206

204207
if (moduleInstance) {
205-
// If the instance is missing `format`, it's probably
206-
// not an instance of Prettier
207-
const isPrettierInstance = !!moduleInstance.format;
208-
const isValidVersion =
209-
moduleInstance.version &&
210-
!!moduleInstance.getSupportInfo &&
211-
!!moduleInstance.getFileInfo &&
212-
!!moduleInstance.resolveConfig &&
213-
semver.gte(moduleInstance.version, minPrettierVersion);
214-
215-
if (!isPrettierInstance && prettierPath) {
208+
const version = await moduleInstance.import();
209+
210+
if (!version && prettierPath) {
216211
this.loggingService.logError(INVALID_PRETTIER_PATH_MESSAGE);
217212
return undefined;
218213
}
219214

215+
const isValidVersion = version && semver.gte(version, minPrettierVersion);
216+
220217
if (!isValidVersion) {
221218
this.loggingService.logInfo(
222219
`Attempted to load Prettier module from ${modulePath}`
223220
);
224221
this.loggingService.logError(OUTDATED_PRETTIER_VERSION_MESSAGE);
225222
return undefined;
226223
} else {
227-
this.loggingService.logDebug(
228-
`Using prettier version ${moduleInstance.version}`
229-
);
224+
this.loggingService.logDebug(`Using prettier version ${version}`);
230225
}
231226
return moduleInstance;
232227
} else {
@@ -351,6 +346,7 @@ export class ModuleResolver implements ModuleResolverInterface {
351346
await prettier.clearConfigCache();
352347
this.path2Module.forEach((module) => {
353348
try {
349+
// eslint-disable-next-line @typescript-eslint/no-floating-promises
354350
module.clearConfigCache();
355351
} catch (error) {
356352
this.loggingService.logError("Error clearing module cache.", error);
@@ -365,19 +361,6 @@ export class ModuleResolver implements ModuleResolverInterface {
365361
: require;
366362
}
367363

368-
// Source: https://github.com/microsoft/vscode-eslint/blob/master/server/src/eslintServer.ts
369-
private loadNodeModule<T>(moduleName: string): T | undefined {
370-
try {
371-
return this.nodeModuleLoader(moduleName);
372-
} catch (error) {
373-
this.loggingService.logError(
374-
`Error loading node module '${moduleName}'`,
375-
error
376-
);
377-
}
378-
return undefined;
379-
}
380-
381364
private resolveNodeModule(moduleName: string, options?: { paths: string[] }) {
382365
try {
383366
return this.nodeModuleLoader.resolve(moduleName, options);

src/PrettierEditService.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import {
2525
RangeFormattingOptions,
2626
} from "./types";
2727
import { getConfig } from "./util";
28+
import { PrettierWorkerInstance } from "./PrettierWorkerInstance";
2829

2930
interface ISelectors {
3031
rangeLanguageSelector: ReadonlyArray<DocumentFilter>;
@@ -252,7 +253,7 @@ export default class PrettierEditService implements Disposable {
252253
* Build formatter selectors
253254
*/
254255
private getSelectors = async (
255-
prettierInstance: PrettierModule,
256+
prettierInstance: PrettierModule | PrettierWorkerInstance,
256257
uri?: Uri
257258
): Promise<ISelectors> => {
258259
const { languages } = await prettierInstance.getSupportInfo();
@@ -393,6 +394,7 @@ export default class PrettierEditService implements Disposable {
393394
const prettierInstance = await this.moduleResolver.getPrettierInstance(
394395
fileName
395396
);
397+
this.loggingService.logInfo("PrettierInstance:", prettierInstance);
396398

397399
if (!prettierInstance) {
398400
this.loggingService.logError(

src/PrettierWorkerInstance.ts

Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
import { Worker } from "worker_threads";
2+
import * as url from "url";
3+
import * as path from "path";
4+
import {
5+
PrettierFileInfoOptions,
6+
PrettierFileInfoResult,
7+
PrettierOptions,
8+
PrettierSupportLanguage,
9+
} from "./types";
10+
11+
const worker = new Worker(
12+
url.pathToFileURL(path.join(__dirname, "/worker/prettier-instance-worker.js"))
13+
);
14+
15+
export class PrettierWorkerInstance {
16+
private importResolver: {
17+
resolve: (version: string) => void;
18+
reject: (version: string) => void;
19+
} | null = null;
20+
21+
private callMethodResolvers: Map<
22+
number,
23+
{
24+
resolve: (value: unknown) => void;
25+
reject: (value: unknown) => void;
26+
}
27+
> = new Map();
28+
29+
private currentCallMethodId = 0;
30+
31+
public version: string | null = null;
32+
33+
constructor(private modulePath: string) {
34+
worker.on("message", ({ type, payload }) => {
35+
switch (type) {
36+
case "import": {
37+
this.importResolver?.resolve(payload.version);
38+
this.version = payload.version;
39+
break;
40+
}
41+
case "callMethod": {
42+
const resolver = this.callMethodResolvers.get(payload.id);
43+
this.callMethodResolvers.delete(payload.id);
44+
if (resolver) {
45+
if (payload.isError) {
46+
resolver.reject(payload.result);
47+
} else {
48+
resolver.resolve(payload.result);
49+
}
50+
}
51+
break;
52+
}
53+
}
54+
});
55+
}
56+
57+
public async import(): Promise</* version of imported prettier */ string> {
58+
const promise = new Promise<string>((resolve, reject) => {
59+
this.importResolver = { resolve, reject };
60+
});
61+
worker.postMessage({
62+
type: "import",
63+
payload: { modulePath: this.modulePath },
64+
});
65+
return promise;
66+
}
67+
68+
public async format(
69+
source: string,
70+
options?: PrettierOptions
71+
): Promise<string> {
72+
const result = await this.callMethod("format", [source, options]);
73+
return result;
74+
}
75+
76+
public async getSupportInfo(): Promise<{
77+
languages: PrettierSupportLanguage[];
78+
}> {
79+
const result = await this.callMethod("getSupportInfo", []);
80+
return result;
81+
}
82+
83+
public async clearConfigCache(): Promise<void> {
84+
await this.callMethod("clearConfigCache", []);
85+
}
86+
87+
public async getFileInfo(
88+
filePath: string,
89+
fileInfoOptions?: PrettierFileInfoOptions
90+
): Promise<PrettierFileInfoResult> {
91+
const result = await this.callMethod("getFileInfo", [
92+
filePath,
93+
fileInfoOptions,
94+
]);
95+
return result;
96+
}
97+
98+
private callMethod(methodName: string, methodArgs: unknown[]): Promise<any> {
99+
const callMethodId = this.currentCallMethodId++;
100+
const promise = new Promise((resolve, reject) => {
101+
this.callMethodResolvers.set(callMethodId, { resolve, reject });
102+
});
103+
worker.postMessage({
104+
type: "callMethod",
105+
payload: {
106+
id: callMethodId,
107+
modulePath: this.modulePath,
108+
methodName,
109+
methodArgs,
110+
},
111+
});
112+
return promise;
113+
}
114+
}

src/test/suite/format.test.ts

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,11 @@ export function putBackPrettierRC(done: Done) {
5454
* @param base base URI
5555
* @returns source code and resulting code
5656
*/
57-
export async function format(workspaceFolderName: string, testFile: string) {
57+
export async function format(
58+
workspaceFolderName: string,
59+
testFile: string,
60+
shouldRetry = false
61+
) {
5862
const base = getWorkspaceFolderUri(workspaceFolderName);
5963
const absPath = path.join(base.fsPath, testFile);
6064
const doc = await vscode.workspace.openTextDocument(absPath);
@@ -70,10 +74,23 @@ export async function format(workspaceFolderName: string, testFile: string) {
7074
console.time(testFile);
7175
await vscode.commands.executeCommand("editor.action.formatDocument");
7276

77+
let actual = doc.getText();
78+
79+
if (shouldRetry) {
80+
for (let i = 0; i < 10; i++) {
81+
if (text !== actual) {
82+
break;
83+
}
84+
await wait(150);
85+
await vscode.commands.executeCommand("editor.action.formatDocument");
86+
actual = doc.getText();
87+
}
88+
}
89+
7390
// eslint-disable-next-line no-console
7491
console.timeEnd(testFile);
7592

76-
return { actual: doc.getText(), source: text };
93+
return { actual, source: text };
7794
}
7895
/**
7996
* Compare prettier's output (default settings)

src/test/suite/module.test.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,11 @@ suite("Test module resolution", function () {
1717
});
1818

1919
test("it loads plugin referenced in dependency module", async () => {
20-
const { actual } = await format("module-plugin", "index.js");
20+
const { actual } = await format(
21+
"module-plugin",
22+
"index.js",
23+
/* shouldRetry */ true
24+
);
2125
const expected = await getText("module-plugin", "index.result.js");
2226
assert.equal(actual, expected);
2327
});

src/test/suite/plugins.test.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,11 @@ import { format, getText } from "./format.test";
44
suite("Test plugins", function () {
55
this.timeout(10000);
66
test("it formats with plugins", async () => {
7-
const { actual } = await format("plugins", "index.php");
7+
const { actual } = await format(
8+
"plugins",
9+
"index.php",
10+
/* shouldRetry */ true
11+
);
812
const expected = await getText("plugins", "index.result.php");
913
assert.equal(actual, expected);
1014
});

src/types.d.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import * as prettier from "prettier";
22
import { TextDocument } from "vscode";
3+
import { PrettierWorkerInstance } from "./PrettierWorkerInstance";
34

45
type PrettierSupportLanguage = {
56
vscodeLanguageIds?: string[];
@@ -25,7 +26,9 @@ type PrettierModule = {
2526
};
2627

2728
type ModuleResolverInterface = {
28-
getPrettierInstance(fileName: string): Promise<PrettierModule | undefined>;
29+
getPrettierInstance(
30+
fileName: string
31+
): Promise<PrettierModule | PrettierWorkerInstance | undefined>;
2932
getResolvedIgnorePath(
3033
fileName: string,
3134
ignorePath: string

0 commit comments

Comments
 (0)