Skip to content
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
122 changes: 119 additions & 3 deletions src/ModuleResolverNode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import {
FAILED_TO_LOAD_MODULE_MESSAGE,
INVALID_PRETTIER_CONFIG,
INVALID_PRETTIER_PATH_MESSAGE,
INVALID_PRETTIER_PATH_BINARY_MESSAGE,
OUTDATED_PRETTIER_VERSION_MESSAGE,
UNTRUSTED_WORKSPACE_USING_BUNDLED_PRETTIER,
USING_BUNDLED_PRETTIER,
Expand Down Expand Up @@ -171,11 +172,126 @@ export class ModuleResolver implements ModuleResolverInterface {
prettierPath,
);

if (await pathExists(absolutePrettierPath)) {
return absolutePrettierPath;
if (!(await pathExists(absolutePrettierPath))) {
this.loggingService.logError(
`${INVALID_PRETTIER_PATH_MESSAGE}: Path does not exist: ${absolutePrettierPath}`,
);
return undefined;
}

// Check if the path points to a file (binary/executable) instead of a directory
try {
const stat = await fs.promises.stat(absolutePrettierPath);
if (stat.isFile()) {
// This might be a binary executable or symlink
// Try to resolve to the actual module directory
const resolvedPath =
await this.resolveBinaryToModule(absolutePrettierPath);
if (resolvedPath) {
this.loggingService.logInfo(
`Resolved binary path ${absolutePrettierPath} to module directory ${resolvedPath}`,
);
return resolvedPath;
}
this.loggingService.logError(
`${INVALID_PRETTIER_PATH_MESSAGE}: ${INVALID_PRETTIER_PATH_BINARY_MESSAGE}`,
);
return undefined;
}
} catch (error) {
this.loggingService.logError(
`Failed to stat prettier path: ${absolutePrettierPath}`,
error,
);
return undefined;
}

return absolutePrettierPath;
}

/**
* Helper method to check if a directory contains a prettier package.json
*/
private async isPrettierModule(
moduleDir: string,
): Promise<boolean> {
const packageJsonPath = path.join(moduleDir, "package.json");
if (!(await pathExists(packageJsonPath))) {
return false;
}

try {
const rawPkgJson = await fs.promises.readFile(packageJsonPath, {
encoding: "utf8",
});
const pkgJson = JSON.parse(rawPkgJson) as { name?: string };
// Only check the name property to avoid prototype pollution issues
// Use hasOwnProperty to ensure we're not checking inherited properties
return (
Object.prototype.hasOwnProperty.call(pkgJson, "name") &&
pkgJson.name === "prettier"
);
} catch (error) {
// Invalid or unreadable package.json
this.loggingService.logDebug(
`Failed to read or parse package.json at ${packageJsonPath}`,
error,
);
return false;
}
}

/**
* Attempts to resolve a binary/executable path to the prettier module directory.
* Handles symlinks and binary files in bin directories.
*/
private async resolveBinaryToModule(
binaryPath: string,
): Promise<string | undefined> {
try {
// First, try to resolve symlink
const realPath = await fs.promises.realpath(binaryPath);

// Security: Validate that the resolved path contains "node_modules" or "prettier"
// to prevent potential symlink attacks that could resolve to arbitrary locations
if (
!realPath.includes("node_modules") &&
!realPath.includes("prettier")
) {
this.loggingService.logDebug(
`Resolved path does not appear to be a valid prettier installation: ${realPath}`,
);
return undefined;
}

// Check if this is in a bin directory (e.g., node_modules/.bin/prettier or node_modules/prettier/bin/prettier.cjs)
const dirname = path.dirname(realPath);
const basename = path.basename(dirname);

// If in a 'bin' directory, parent directory might be the module
if (basename === "bin") {
const moduleDir = path.dirname(dirname);
if (await this.isPrettierModule(moduleDir)) {
return moduleDir;
}
}

// Also check if it's in .bin directory (global or local node_modules/.bin)
if (basename === ".bin") {
// Look for prettier in ../prettier
const possibleModuleDir = path.join(path.dirname(dirname), "prettier");
if (await this.isPrettierModule(possibleModuleDir)) {
return possibleModuleDir;
}
}
} catch (error) {
// If we can't resolve, return undefined and let the caller handle it
this.loggingService.logDebug(
`Failed to resolve binary to module: ${binaryPath}`,
error,
);
}

this.loggingService.logError(INVALID_PRETTIER_PATH_MESSAGE);
return undefined;
}

Expand Down
4 changes: 4 additions & 0 deletions src/message.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@ export const OUTDATED_PRETTIER_VERSION_MESSAGE =
"Your project is configured to use an outdated version of prettier that cannot be used by this extension. Upgrade to the latest version of prettier.";
export const INVALID_PRETTIER_PATH_MESSAGE =
"`prettierPath` option does not reference a valid instance of Prettier. Please ensure you are passing a path to the prettier module, not the binary. Falling back to bundled version of prettier.";
export const INVALID_PRETTIER_PATH_BINARY_MESSAGE =
"Path points to a file instead of a module directory. " +
"Please provide the path to the prettier module directory " +
"(e.g., /path/to/node_modules/prettier) instead of the binary executable.";
export const FAILED_TO_LOAD_MODULE_MESSAGE =
"Failed to load module. If you have prettier or plugins referenced in package.json, ensure you have run `npm install`";
export const INVALID_PRETTIER_CONFIG =
Expand Down