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

Fix for Prettier v2 and plugin-tailwindcss problem #3038

Merged
merged 8 commits into from
Jun 22, 2023
Merged
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
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@ All notable changes to the "prettier-vscode" extension will be documented in thi

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

## [Unreleased]

- Run only Prettier v3 in worker_threads. Run v2 in main thread.

## [9.15.0]

- Run Prettier in worker_threads for v3.
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@
"scripts": {
"clean": "node ./scripts/clean.js",
"lint": "eslint -c .eslintrc.js --ext .ts .",
"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",
"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 && cd ../plugin-tailwindcss && npm i",
"prettier": "prettier --write '**/*.{ts,json,md,hbs,yml,js}'",
"test-compile": "yarn clean && tsc -p ./ && yarn webpack && cp -r ./src/worker ./out",
"test": "node ./out/test/runTests.js",
Expand Down
17 changes: 17 additions & 0 deletions src/ModuleLoader.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
declare const __webpack_require__: typeof require;
declare const __non_webpack_require__: typeof require;

export function nodeModuleLoader() {
return typeof __webpack_require__ === "function"
? __non_webpack_require__
: require;
}

// Source: https://github.com/microsoft/vscode-eslint/blob/master/server/src/eslintServer.ts
export function loadNodeModule<T>(moduleName: string): T | undefined {
try {
return nodeModuleLoader()(moduleName);
} catch (error) {
throw new Error(`Error loading node module '${moduleName}'`);
}
}
75 changes: 62 additions & 13 deletions src/ModuleResolver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,13 +25,22 @@ import {
} from "./types";
import { getConfig, getWorkspaceRelativePath } from "./util";
import { PrettierWorkerInstance } from "./PrettierWorkerInstance";
import { PrettierInstance } from "./PrettierInstance";
import { PrettierMainThreadInstance } from "./PrettierMainThreadInstance";
import { loadNodeModule, nodeModuleLoader } from "./ModuleLoader";

const minPrettierVersion = "1.13.0";
declare const __webpack_require__: typeof require;
declare const __non_webpack_require__: typeof require;

export type PrettierNodeModule = typeof prettier;

function isAboveV3(version: string | null): boolean {
const parsedVersion = semver.parse(version);
if (!parsedVersion) {
throw new Error("Invalid version");
}
return parsedVersion.major >= 3;
}

const origFsStatSync = fs.statSync;
const fsStatSyncWorkaround = (
path: fs.PathLike,
Expand Down Expand Up @@ -95,7 +104,7 @@ export class ModuleResolver implements ModuleResolverInterface {
private findPkgCache: Map<string, string>;
private ignorePathCache = new Map<string, string>();

private path2Module = new Map<string, PrettierWorkerInstance>();
private path2Module = new Map<string, PrettierInstance>();

constructor(private loggingService: LoggingService) {
this.findPkgCache = new Map();
Expand All @@ -105,13 +114,50 @@ export class ModuleResolver implements ModuleResolverInterface {
return prettier;
}

private loadPrettierVersionFromPackageJson(modulePath: string): string {
const packageJsonPath = findUp.sync(
(dir) => {
const pkgFilePath = path.join(dir, "package.json");
if (fs.existsSync(pkgFilePath)) {
return pkgFilePath;
}
},
{ cwd: path.dirname(modulePath) }
);

if (!packageJsonPath) {
throw new Error("Cannot find Prettier package.json");
}

const prettierPkgJson = loadNodeModule(packageJsonPath);

let version: string | null = null;

if (
typeof prettierPkgJson === "object" &&
prettierPkgJson !== null &&
"version" in prettierPkgJson &&
// @ts-expect-error checked
typeof prettierPkgJson.version === "string"
) {
// @ts-expect-error checked
version = prettierPkgJson.version;
}

if (!version) {
throw new Error("Cannot load Prettier version from package.json");
}

return version;
}

/**
* Returns an instance of the prettier module.
* @param fileName The path of the file to use as the starting point. If none provided, the bundled prettier will be used.
*/
public async getPrettierInstance(
fileName: string
): Promise<PrettierNodeModule | PrettierWorkerInstance | undefined> {
): Promise<PrettierNodeModule | PrettierInstance | undefined> {
if (!workspace.isTrusted) {
this.loggingService.logDebug(UNTRUSTED_WORKSPACE_USING_BUNDLED_PRETTIER);
return prettier;
Expand Down Expand Up @@ -174,7 +220,7 @@ export class ModuleResolver implements ModuleResolverInterface {
}
}

let moduleInstance: PrettierWorkerInstance | undefined = undefined;
let moduleInstance: PrettierInstance | undefined = undefined;

if (modulePath !== undefined) {
this.loggingService.logDebug(
Expand All @@ -186,7 +232,16 @@ export class ModuleResolver implements ModuleResolverInterface {
return moduleInstance;
} else {
try {
moduleInstance = new PrettierWorkerInstance(modulePath);
const prettierVersion =
this.loadPrettierVersionFromPackageJson(modulePath);

const isAboveVersion3 = isAboveV3(prettierVersion);

if (isAboveVersion3) {
moduleInstance = new PrettierWorkerInstance(modulePath);
} else {
moduleInstance = new PrettierMainThreadInstance(modulePath);
}
if (moduleInstance) {
this.path2Module.set(modulePath, moduleInstance);
}
Expand Down Expand Up @@ -355,15 +410,9 @@ export class ModuleResolver implements ModuleResolverInterface {
this.path2Module.clear();
}

private get nodeModuleLoader() {
return typeof __webpack_require__ === "function"
? __non_webpack_require__
: require;
}

private resolveNodeModule(moduleName: string, options?: { paths: string[] }) {
try {
return this.nodeModuleLoader.resolve(moduleName, options);
return nodeModuleLoader().resolve(moduleName, options);
} catch (error) {
this.loggingService.logError(
`Error resolve node module '${moduleName}'`,
Expand Down
4 changes: 2 additions & 2 deletions src/PrettierEditService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ import {
RangeFormattingOptions,
} from "./types";
import { getConfig } from "./util";
import { PrettierWorkerInstance } from "./PrettierWorkerInstance";
import { PrettierInstance } from "./PrettierInstance";

interface ISelectors {
rangeLanguageSelector: ReadonlyArray<DocumentFilter>;
Expand Down Expand Up @@ -253,7 +253,7 @@ export default class PrettierEditService implements Disposable {
* Build formatter selectors
*/
private getSelectors = async (
prettierInstance: PrettierModule | PrettierWorkerInstance,
prettierInstance: PrettierModule | PrettierInstance,
uri?: Uri
): Promise<ISelectors> => {
const { languages } = await prettierInstance.getSupportInfo();
Expand Down
24 changes: 24 additions & 0 deletions src/PrettierInstance.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import {
PrettierFileInfoOptions,
PrettierFileInfoResult,
PrettierOptions,
PrettierSupportLanguage,
} from "./types";

export interface PrettierInstance {
version: string | null;
import(): Promise<string>;
format(source: string, options?: PrettierOptions): Promise<string>;
getFileInfo(
filePath: string,
fileInfoOptions?: PrettierFileInfoOptions
): Promise<PrettierFileInfoResult>;
getSupportInfo(): Promise<{
languages: PrettierSupportLanguage[];
}>;
clearConfigCache(): Promise<void>;
}

export interface PrettierInstanceConstructor {
new (modulePath: string): PrettierInstance;
}
62 changes: 62 additions & 0 deletions src/PrettierMainThreadInstance.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import { FileInfoOptions, Options } from "prettier";
import {
PrettierInstance,
PrettierInstanceConstructor,
} from "./PrettierInstance";
import { PrettierFileInfoResult, PrettierSupportLanguage } from "./types";
import { PrettierNodeModule } from "./ModuleResolver";
import { loadNodeModule } from "./ModuleLoader";

export const PrettierMainThreadInstance: PrettierInstanceConstructor = class PrettierMainThreadInstance
implements PrettierInstance
{
public version: string | null = null;
private prettierModule: PrettierNodeModule | undefined;

constructor(private modulePath: string) {}

public async import(): Promise</* version of imported prettier */ string> {
this.prettierModule = loadNodeModule(this.modulePath);
this.version = this.prettierModule?.version ?? null;
if (this.version == null) {
throw new Error(`Failed to load Prettier instance: ${this.modulePath}`);
}
return this.version;
}

public async format(
source: string,
options?: Options | undefined
): Promise<string> {
if (!this.prettierModule) {
await this.import();
}
return this.prettierModule!.format(source, options);
}

public async getFileInfo(
filePath: string,
fileInfoOptions?: FileInfoOptions | undefined
): Promise<PrettierFileInfoResult> {
if (!this.prettierModule) {
await this.import();
}
return this.prettierModule!.getFileInfo(filePath, fileInfoOptions);
}

public async getSupportInfo(): Promise<{
languages: PrettierSupportLanguage[];
}> {
if (!this.prettierModule) {
await this.import();
}
return this.prettierModule!.getSupportInfo();
}

public async clearConfigCache(): Promise<void> {
if (!this.prettierModule) {
await this.import();
}
return this.prettierModule!.clearConfigCache();
}
};
10 changes: 8 additions & 2 deletions src/PrettierWorkerInstance.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,18 @@ import {
PrettierOptions,
PrettierSupportLanguage,
} from "./types";
import {
PrettierInstance,
PrettierInstanceConstructor,
} from "./PrettierInstance";

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

export class PrettierWorkerInstance {
export const PrettierWorkerInstance: PrettierInstanceConstructor = class PrettierWorkerInstance
implements PrettierInstance
{
private importResolver: {
resolve: (version: string) => void;
reject: (version: string) => void;
Expand Down Expand Up @@ -111,4 +117,4 @@ export class PrettierWorkerInstance {
});
return promise;
}
}
};
15 changes: 15 additions & 0 deletions src/test/suite/plugin-tailwindcss.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import * as assert from "assert";
import { format, getText } from "./format.test";

suite("Test plugin-tailwindcss", function () {
this.timeout(10000);
test("it formats with prettier-plugin-tailwindcss", async () => {
const { actual } = await format(
"plugin-tailwindcss",
"index.js",
/* shouldRetry */ true
);
const expected = await getText("plugin-tailwindcss", "index.result.js");
assert.equal(actual, expected);
});
});
2 changes: 1 addition & 1 deletion src/test/suite/v3.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { format, getText } from "./format.test";
suite("Tests for Prettier v3", function () {
this.timeout(10000);
test("it formats by Prettier v3", async () => {
const { actual } = await format("v3", "index.ts");
const { actual } = await format("v3", "index.ts", /* shouldRetry */ true);
const expected = await getText("v3", "index.result.ts");
assert.equal(actual, expected);
});
Expand Down
4 changes: 2 additions & 2 deletions src/types.d.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import * as prettier from "prettier";
import { TextDocument } from "vscode";
import { PrettierWorkerInstance } from "./PrettierWorkerInstance";
import { PrettierInstance } from "./PrettierInstance";

type PrettierSupportLanguage = {
vscodeLanguageIds?: string[];
Expand Down Expand Up @@ -28,7 +28,7 @@ type PrettierModule = {
type ModuleResolverInterface = {
getPrettierInstance(
fileName: string
): Promise<PrettierModule | PrettierWorkerInstance | undefined>;
): Promise<PrettierModule | PrettierInstance | undefined>;
getResolvedIgnorePath(
fileName: string,
ignorePath: string
Expand Down
6 changes: 6 additions & 0 deletions test-fixtures/plugin-tailwindcss/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
function App() {
return (

<div class="pt-2 p-4"></div>
)
}
3 changes: 3 additions & 0 deletions test-fixtures/plugin-tailwindcss/index.result.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
function App() {
return <div class="p-4 pt-2"></div>;
}
Loading