Skip to content
Open
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]

Copy link

Copilot AI Nov 27, 2025

Choose a reason for hiding this comment

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

[nitpick] The CHANGELOG entry should include more context about the Prettier version upgrade from 2.8.8 to 3.6.2, as this is a major version change that could affect users. Consider adding a separate entry like:

- Upgrade Prettier from 2.8.8 to 3.6.2
- Add support for global Prettier plugins via `prettier.plugins` setting

This makes the breaking changes more visible to users reviewing the changelog.

Suggested change
- Upgrade Prettier from 2.8.8 to 3.6.2

Copilot uses AI. Check for mistakes.
- Add support for global Prettier plugins via `prettier.plugins` setting

## [11.0.0]

- [BREAKING CHANGE] Prevent `.editorconfig` from satisfying the `requireConfig` setting (#2708) - Thanks to [@redoPop](https://github.com/redoPop)
Expand Down
7 changes: 6 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -127,7 +127,7 @@
},
"dependencies": {
"find-up": "5.0.0",
"prettier": "^2.8.8",
"prettier": "^3.6.2",
"resolve": "^1.22.8",
"semver": "^7.6.3",
"vscode-nls": "^5.2.0"
Expand All @@ -154,6 +154,11 @@
"type": "object",
"title": "%ext.config.title%",
"properties": {
"prettier.plugins": {
"type": "array",
"default": [],
Copy link

Copilot AI Nov 27, 2025

Choose a reason for hiding this comment

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

The prettier.plugins configuration should include a "items" schema definition to specify that array elements should be strings. This provides better type checking and IDE autocomplete support.

Add:

"items": {
  "type": "string"
}
Suggested change
"default": [],
"default": [],
"items": {
"type": "string"
},

Copilot uses AI. Check for mistakes.
"markdownDescription": "%ext.config.plugins%"
Copy link

Copilot AI Nov 27, 2025

Choose a reason for hiding this comment

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

[nitpick] Consider adding a "scope" property to the prettier.plugins configuration. Based on other settings in this file, global plugin configuration should likely use "scope": "window" to apply at the workspace level rather than per-resource, similar to prettier.disableLanguages and prettier.documentSelectors.

Suggested change
"markdownDescription": "%ext.config.plugins%"
"markdownDescription": "%ext.config.plugins%",
"scope": "window"

Copilot uses AI. Check for mistakes.
},
"prettier.disableLanguages": {
"type": "array",
"items": {
Expand Down
1 change: 1 addition & 0 deletions package.nls.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
{
"ext.command.createConfigFile.title": "Prettier: Create Configuration File",
"ext.command.forceFormatDocument.title": "Format Document (Forced)",
"ext.config.plugins": "A list of plugins to automatically install and load globally.",
"ext.config.arrowParens": "Include parentheses around a sole arrow function parameter.",
"ext.config.bracketSpacing": "Controls the printing of spaces inside object literals.",
"ext.config.configPath": "Path to the prettier configuration file.",
Expand Down
1 change: 1 addition & 0 deletions package.nls.zh-cn.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
{
"ext.command.createConfigFile.title": "Prettier:选择配置文件",
"ext.command.forceFormatDocument.title": "格式化文件(强制)",
"ext.config.plugins": "一组插件,用于自动全局安装和加载。",
"ext.config.arrowParens": "箭头函数仅有一个参数时,参数是否添加括号。",
"ext.config.bracketSpacing": "在对象字面量的花括号内侧使用空格作为间隔。",
"ext.config.configPath": "指定 Prettier 配置文件的路径。",
Expand Down
1 change: 1 addition & 0 deletions package.nls.zh-tw.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
{
"ext.command.createConfigFile.title": "Prettier: 建立組態檔",
"ext.command.forceFormatDocument.title": "排版文件(強制)",
"ext.config.plugins": "一組插件,用於自動全域安裝和加載。",
"ext.config.arrowParens": "箭頭函式中只有一個參數也加上括號。",
"ext.config.bracketSpacing": "控制物件字面值中兩側的留白。",
"ext.config.configPath": "Prettier 組態檔的路徑。",
Expand Down
31 changes: 20 additions & 11 deletions src/BrowserModuleResolver.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import {
ModuleResolverInterface,
PrettierFileInfoOptions,
PrettierFileInfoResult,
PrettierSupportLanguage,
PrettierModule,
PrettierOptions,
ModuleResolverInterface,
PrettierSupportLanguage,
PrettierVSCodeConfig,
} from "./types";

Expand All @@ -26,10 +26,10 @@ import * as yamlPlugin from "prettier/parser-yaml";
//import * as flowPlugin from "prettier/parser-flow";
//import * as postcssPlugin from "prettier/parser-postcss";

import { Options, Plugin, ResolveConfigOptions } from "prettier";
import { TextDocument, Uri } from "vscode";
import { LoggingService } from "./LoggingService";
import { getWorkspaceRelativePath } from "./util";
import { ResolveConfigOptions, Options } from "prettier";

const plugins = [
angularPlugin,
Expand All @@ -41,21 +41,21 @@ const plugins = [
meriyahPlugin,
typescriptPlugin,
yamlPlugin,
];
] as unknown as Plugin[];

export class ModuleResolver implements ModuleResolverInterface {
constructor(private loggingService: LoggingService) {}

public async getPrettierInstance(
// eslint-disable-next-line @typescript-eslint/no-unused-vars
_fileName: string
_fileName: string,
): Promise<PrettierModule | undefined> {
return this.getGlobalPrettierInstance();
}

public async getResolvedIgnorePath(
fileName: string,
ignorePath: string
ignorePath: string,
): Promise<string | undefined> {
return getWorkspaceRelativePath(fileName, ignorePath);
}
Expand All @@ -66,7 +66,9 @@ export class ModuleResolver implements ModuleResolverInterface {
format: (source: string, options: PrettierOptions) => {
return prettierStandalone.format(source, { ...options, plugins });
},
getSupportInfo: (): { languages: PrettierSupportLanguage[] } => {
getSupportInfo: async (): Promise<{
languages: PrettierSupportLanguage[];
}> => {
return {
languages: [
{
Expand Down Expand Up @@ -167,7 +169,7 @@ export class ModuleResolver implements ModuleResolverInterface {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
filePath: string,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
options?: PrettierFileInfoOptions
options?: PrettierFileInfoOptions,
): Promise<PrettierFileInfoResult> => {
// TODO: implement ignore file reading
return { ignored: false, inferredParser: null };
Expand All @@ -181,15 +183,15 @@ export class ModuleResolver implements ModuleResolverInterface {
resolveConfigFile(filePath?: string | undefined): Promise<string | null>;
resolveConfig(
fileName: string,
options?: ResolveConfigOptions | undefined
options?: ResolveConfigOptions | undefined,
): Promise<Options | null>;
},
// eslint-disable-next-line @typescript-eslint/no-unused-vars
uri: Uri,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
fileName: string,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
vscodeConfig: PrettierVSCodeConfig
vscodeConfig: PrettierVSCodeConfig,
): Promise<Options | "error" | "disabled" | null> {
return null;
}
Expand All @@ -198,11 +200,18 @@ export class ModuleResolver implements ModuleResolverInterface {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
_doc: TextDocument,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
_vscodeConfig: PrettierVSCodeConfig
_vscodeConfig: PrettierVSCodeConfig,
): Promise<"error" | "disabled" | PrettierOptions | null> {
return null;
}

resolvePluginsGlobally(
// eslint-disable-next-line @typescript-eslint/no-unused-vars
plugins: string[],
): string[] {
return [];
}

dispose() {
// nothing to do
}
Expand Down
70 changes: 62 additions & 8 deletions src/ModuleResolver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,18 +16,23 @@ import {
UNTRUSTED_WORKSPACE_USING_BUNDLED_PRETTIER,
USING_BUNDLED_PRETTIER,
} from "./message";
import { loadNodeModule, resolveConfigPlugins } from "./ModuleLoader";
import { PrettierInstance } from "./PrettierInstance";
import { PrettierMainThreadInstance } from "./PrettierMainThreadInstance";
import { PrettierWorkerInstance } from "./PrettierWorkerInstance";
import {
ModuleResolverInterface,
PackageManagers,
PrettierOptions,
PrettierResolveConfigOptions,
PrettierVSCodeConfig,
} from "./types";
import { getConfig, getWorkspaceRelativePath, isAboveV3 } from "./util";
import { PrettierWorkerInstance } from "./PrettierWorkerInstance";
import { PrettierInstance } from "./PrettierInstance";
import { PrettierMainThreadInstance } from "./PrettierMainThreadInstance";
import { loadNodeModule, resolveConfigPlugins } from "./ModuleLoader";
import {
getConfig,
getPackageInfo,
getWorkspaceRelativePath,
isAboveV3,
} from "./util";

const minPrettierVersion = "1.13.0";

Expand Down Expand Up @@ -57,6 +62,8 @@ const fsStatSyncWorkaround = (
// @ts-expect-error Workaround for https://github.com/prettier/prettier-vscode/issues/3020
fs.statSync = fsStatSyncWorkaround;

declare const __non_webpack_require__: NodeRequire;

const globalPaths: {
[key: string]: { cache: string | undefined; get(): string | undefined };
} = {
Expand Down Expand Up @@ -95,6 +102,7 @@ function globalPathGet(packageManager: PackageManagers): string | undefined {
export class ModuleResolver implements ModuleResolverInterface {
private findPkgCache: Map<string, string>;
private ignorePathCache = new Map<string, string>();
private pluginsCache = new Map<string, string[]>();

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

Expand All @@ -121,7 +129,7 @@ export class ModuleResolver implements ModuleResolverInterface {
return pkgFilePath;
}
},
{ cwd }
{ cwd },
);

if (!packageJsonPath) {
Expand Down Expand Up @@ -344,6 +352,52 @@ export class ModuleResolver implements ModuleResolverInterface {
return fileName;
}

public resolvePluginsGlobally(plugins: string[]): string[] {
if (plugins.length === 0) {
return [];
}

const cacheKey = plugins.sort().join(",");
Copy link

Copilot AI Nov 27, 2025

Choose a reason for hiding this comment

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

The plugins.sort() call mutates the original input array. This is problematic because:

  1. It modifies the caller's array, which is an unexpected side effect
  2. If the same array reference is passed multiple times, the caching logic won't work correctly since the array is already sorted

Consider using [...plugins].sort() or plugins.slice().sort() to create a sorted copy instead of mutating the original array.

Suggested change
const cacheKey = plugins.sort().join(",");
const cacheKey = [...plugins].sort().join(",");

Copilot uses AI. Check for mistakes.

if (this.pluginsCache.has(cacheKey)) {
return this.pluginsCache.get(cacheKey)!;
}

const pluginsDirectory = path.join(__dirname, "..", "plugins");
Copy link

Copilot AI Nov 27, 2025

Choose a reason for hiding this comment

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

The pluginsDirectory path uses __dirname which refers to the compiled output location. In a webpack-bundled extension, this could be unpredictable. Consider using the extension's global storage path via VS Code's ExtensionContext.globalStorageUri instead, which is the recommended location for extension-managed data.

This would require passing the storage path through the constructor, but would make the extension more robust and follow VS Code best practices.

Copilot uses AI. Check for mistakes.

if (!fs.existsSync(pluginsDirectory)) {
fs.mkdirSync(pluginsDirectory, { recursive: true });
}

try {
this.loggingService.logInfo(
`Installing ${plugins.length} plugins at ${pluginsDirectory}`,
);

if (!fs.existsSync(path.join(pluginsDirectory, "package.json"))) {
execSync("npm init -y", { cwd: pluginsDirectory });
}

execSync(`npm install ${plugins.join(" ")}`, { cwd: pluginsDirectory });
Comment on lines +377 to +381
Copy link

Copilot AI Nov 27, 2025

Choose a reason for hiding this comment

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

The plugin installation happens synchronously on every call when the cache is empty, which will block the extension thread. This could cause VS Code to become unresponsive, especially if:

  1. Multiple plugins need to be installed
  2. The npm registry is slow
  3. Large plugins are being downloaded

Consider making this operation asynchronous or showing a progress indicator to the user. The function signature should change to return Promise<string[]> and use async/await or spawn the npm process asynchronously.

Copilot uses AI. Check for mistakes.
Copy link

Copilot AI Nov 27, 2025

Choose a reason for hiding this comment

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

The npm install command is vulnerable to command injection. Plugin names from user configuration are directly interpolated into a shell command without sanitization. A malicious plugin name like ; rm -rf / or $(malicious-command) could execute arbitrary commands.

Consider using execSync with an array of arguments via the shell: false option, or properly escape/validate the plugin names before passing them to the shell. For example:

const args = ['install', ...plugins];
execSync(`npm ${args.map(arg => JSON.stringify(arg)).join(' ')}`, { cwd: pluginsDirectory });

Or better yet, use a programmatic API if available, or validate that each plugin name matches a safe pattern (e.g., /^[@a-z0-9-_/]+$/i).

Suggested change
execSync(`npm install ${plugins.join(" ")}`, { cwd: pluginsDirectory });
execSync('npm', ['install', ...plugins], { cwd: pluginsDirectory });

Copilot uses AI. Check for mistakes.

Comment on lines +381 to +382
Copy link

Copilot AI Nov 27, 2025

Choose a reason for hiding this comment

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

The plugin installation logic doesn't check if the plugins are already installed before running npm install. This means every time VS Code starts or the configuration changes, the extension will re-run npm install even if the plugins are already present.

Consider checking if the plugins are already installed in node_modules before running npm install. This would significantly speed up repeated calls and reduce unnecessary network requests.

Suggested change
execSync(`npm install ${plugins.join(" ")}`, { cwd: pluginsDirectory });
// Check which plugins are missing from node_modules
const missingPlugins = plugins.filter((plugin) => {
const pluginPackageName = getPackageInfo(plugin).name;
const pluginPath = path.join(pluginsDirectory, "node_modules", pluginPackageName);
return !fs.existsSync(pluginPath);
});
if (missingPlugins.length > 0) {
this.loggingService.logInfo(
`Installing missing plugins: ${missingPlugins.join(", ")}`
);
execSync(`npm install ${missingPlugins.join(" ")}`, { cwd: pluginsDirectory });
} else {
this.loggingService.logInfo("All plugins already installed, skipping npm install.");
}

Copilot uses AI. Check for mistakes.
const resolvedPlugins = plugins.map((plugin) =>
__non_webpack_require__.resolve(getPackageInfo(plugin).name, {
paths: [path.join(pluginsDirectory, "node_modules")],
}),
);

this.loggingService.logInfo(`Plugins installed successfully.`);

this.pluginsCache.clear();
Copy link

Copilot AI Nov 27, 2025

Choose a reason for hiding this comment

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

The pluginsCache.clear() call before setting the new cache entry defeats the purpose of caching. If a user has multiple plugin configurations (e.g., ["plugin-a"] and ["plugin-b"]), installing one set will clear the cache for all others, forcing reinstallation.

Remove the clear() call and just set the specific cache key:

this.pluginsCache.set(cacheKey, resolvedPlugins);

The cache should only be cleared when the extension is disposed or reloaded.

Suggested change
this.pluginsCache.clear();

Copilot uses AI. Check for mistakes.
this.pluginsCache.set(cacheKey, resolvedPlugins);

return resolvedPlugins;
} catch (error) {
this.loggingService.logError(`Failed to install plugins.`, error);
return [];
}
}

public async resolveConfig(
prettierInstance: {
version: string | null;
Expand Down Expand Up @@ -382,8 +436,8 @@ export class ModuleResolver implements ModuleResolverInterface {
config: isVirtual
? undefined
: vscodeConfig.configPath
? getWorkspaceRelativePath(fileName, vscodeConfig.configPath)
: configPath,
? getWorkspaceRelativePath(fileName, vscodeConfig.configPath)
: configPath,
editorconfig: isVirtual ? undefined : vscodeConfig.useEditorConfig,
};

Expand Down
Loading