Skip to content
Draft
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
6 changes: 6 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -225,6 +225,12 @@
"markdownDescription": "%ext.config.enableDebugLogs%",
"scope": "resource"
},
"prettier.enableErrorNotifications": {
"type": "boolean",
"default": true,
"markdownDescription": "%ext.config.enableErrorNotifications%",
"scope": "resource"
},
"prettier.printWidth": {
"type": "integer",
"default": 80,
Expand Down
1 change: 1 addition & 0 deletions package.nls.json
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@
"ext.config.embeddedLanguageFormatting": "Control whether Prettier formats quoted code embedded in the file.",
"ext.config.enable": "Controls whether Prettier is enabled or not. Reload required.",
"ext.config.enableDebugLogs": "Enable debug logs for troubleshooting.",
"ext.config.enableErrorNotifications": "Show error notifications when Prettier encounters fatal errors (e.g., module loading failures, invalid configuration files). When disabled, errors will only be logged to the output channel.",
"ext.config.experimentalTernaries": "Try prettier's [new ternary formatting](https://github.com/prettier/prettier/pull/13183) before it becomes the default behavior.",
"ext.config.objectWrap": "Controls how object literals are wrapped.\nValid options:\n- `preserve` - Preserve the original wrapping of object literals.\n- `collapse` - Collapse object literals to fit on one line when possible.",
"ext.config.experimentalOperatorPosition": "Controls where to break lines around binary operators.\nValid options:\n- `end` - Break lines after operators.\n- `start` - Break lines before operators.",
Expand Down
14 changes: 13 additions & 1 deletion src/ModuleResolverNode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
} from "./utils/global-node-paths.js";
import { findUp, pathExists, FIND_UP_STOP } from "./utils/find-up.js";
import { LoggingService } from "./LoggingService.js";
import { NotificationService } from "./NotificationService.js";
import {
FAILED_TO_LOAD_MODULE_MESSAGE,
INVALID_PRETTIER_CONFIG,
Expand Down Expand Up @@ -85,7 +86,10 @@ async function globalPathGet(
export class ModuleResolver implements ModuleResolverInterface {
private path2Module = new Map<string, PrettierInstance>();

constructor(private loggingService: LoggingService) {}
constructor(
private loggingService: LoggingService,
private notificationService: NotificationService,
) {}

public async getGlobalPrettierInstance(): Promise<PrettierNodeModule> {
return getBundledPrettier();
Expand Down Expand Up @@ -152,6 +156,8 @@ export class ModuleResolver implements ModuleResolverInterface {
`${FAILED_TO_LOAD_MODULE_MESSAGE}: ${modulePath}`,
error,
);
// Show user-facing notification
void this.notificationService.showPrettierLoadFailedError(fileName);
return undefined;
}

Expand All @@ -176,6 +182,8 @@ export class ModuleResolver implements ModuleResolverInterface {
}

this.loggingService.logError(INVALID_PRETTIER_PATH_MESSAGE);
// Show user-facing notification for invalid prettier path
void this.notificationService.showInvalidPrettierPathError();
return undefined;
}

Expand Down Expand Up @@ -399,6 +407,8 @@ export class ModuleResolver implements ModuleResolverInterface {
`Failed to resolve config file for ${fileName}`,
error,
);
// Show user-facing notification for config resolution error
void this.notificationService.showConfigResolutionError();
return "error";
}

Expand Down Expand Up @@ -437,6 +447,8 @@ export class ModuleResolver implements ModuleResolverInterface {
);
} catch (error) {
this.loggingService.logError(INVALID_PRETTIER_CONFIG, error);
// Show user-facing notification for invalid config
void this.notificationService.showInvalidConfigError();
return "error";
}

Expand Down
8 changes: 7 additions & 1 deletion src/ModuleResolverWeb.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
} from "./types.js";
import { TextDocument } from "vscode";
import { LoggingService } from "./LoggingService.js";
import { NotificationService } from "./NotificationService.js";
import { getWorkspaceRelativePath } from "./utils/workspace.js";
import type { ResolveConfigOptions, Options, Plugin } from "prettier";

Expand Down Expand Up @@ -48,7 +49,12 @@ const plugins: Plugin[] = [
];

export class ModuleResolver implements ModuleResolverInterface {
constructor(private loggingService: LoggingService) {}
constructor(
private loggingService: LoggingService,
// NotificationService is not used in web mode (always uses bundled prettier)
// but is required for interface consistency
_notificationService: NotificationService,
) {}

public async getPrettierInstance(
_fileName: string,
Expand Down
183 changes: 183 additions & 0 deletions src/NotificationService.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,183 @@
import * as fs from "fs";
import * as path from "path";
import { commands, Uri, window, workspace } from "vscode";
import { pathExists } from "./utils/find-up.js";

/**
* Service for showing user-facing notifications for fatal errors
*/
export class NotificationService {
// Track which errors we've already shown to avoid spam
private shownErrors = new Set<string>();

/**
* Check if error notifications are enabled in settings
*/
private isNotificationEnabled(): boolean {
const config = workspace.getConfiguration("prettier");
return config.get<boolean>("enableErrorNotifications", true);
}

/**
* Check if prettier is listed in package.json dependencies
*/
private async isPrettierInPackageJson(
fileName: string,
): Promise<boolean | undefined> {
try {
const workspaceFolder = workspace.getWorkspaceFolder(Uri.file(fileName));
if (!workspaceFolder) {
return undefined;
}

const packageJsonPath = path.join(
workspaceFolder.uri.fsPath,
"package.json",
);

if (!(await pathExists(packageJsonPath))) {
return undefined;
}

const content = await fs.promises.readFile(packageJsonPath, "utf8");
const packageJson = JSON.parse(content);

return !!(
packageJson.dependencies?.prettier ||
packageJson.devDependencies?.prettier ||
packageJson.peerDependencies?.prettier
);
} catch {
return undefined;
}
}

/**
* Helper to show an error message with optional action buttons
*/
private async showErrorWithActions(
message: string,
...actions: string[]
): Promise<string | undefined> {
return window.showErrorMessage(message, ...actions);
}

/**
* Show error notification when prettier module cannot be loaded
*/
public async showPrettierLoadFailedError(fileName: string): Promise<void> {
if (!this.isNotificationEnabled()) {
return;
}

const errorKey = `load-failed:${fileName}`;
if (this.shownErrors.has(errorKey)) {
return;
}
this.shownErrors.add(errorKey);

const isInPackageJson = await this.isPrettierInPackageJson(fileName);

const message = isInPackageJson
? "Prettier: Failed to load module. Prettier is listed in package.json but could not be loaded. Please run npm install (or yarn/pnpm install)."
: "Prettier: Failed to load module. See output for more details.";

const selection = await this.showErrorWithActions(
message,
"View Output",
"Dismiss",
);

if (selection === "View Output") {
void commands.executeCommand("prettier.openOutput");
}
}

/**
* Show error notification for invalid prettier path configuration
*/
public async showInvalidPrettierPathError(): Promise<void> {
if (!this.isNotificationEnabled()) {
return;
}

const errorKey = "invalid-prettier-path";
if (this.shownErrors.has(errorKey)) {
return;
}
this.shownErrors.add(errorKey);

const selection = await this.showErrorWithActions(
"Prettier: The 'prettierPath' setting does not reference a valid Prettier installation. Please check your settings.",
"Open Settings",
"View Output",
"Dismiss",
);

if (selection === "Open Settings") {
void commands.executeCommand(
"workbench.action.openSettings",
"prettier.prettierPath",
);
} else if (selection === "View Output") {
void commands.executeCommand("prettier.openOutput");
}
}

/**
* Show error notification for invalid prettier configuration file
*/
public async showInvalidConfigError(): Promise<void> {
if (!this.isNotificationEnabled()) {
return;
}

const errorKey = "invalid-config";
if (this.shownErrors.has(errorKey)) {
return;
}
this.shownErrors.add(errorKey);

const selection = await this.showErrorWithActions(
"Prettier: Invalid configuration file detected. Please check your Prettier config files (.prettierrc, prettier.config.js, etc.).",
"View Output",
"Dismiss",
);

if (selection === "View Output") {
void commands.executeCommand("prettier.openOutput");
}
}

/**
* Show error notification for config resolution failures
*/
public async showConfigResolutionError(): Promise<void> {
if (!this.isNotificationEnabled()) {
return;
}

const errorKey = "config-resolution";
if (this.shownErrors.has(errorKey)) {
return;
}
this.shownErrors.add(errorKey);

const selection = await this.showErrorWithActions(
"Prettier: Failed to resolve configuration file. See output for details.",
"View Output",
"Dismiss",
);

if (selection === "View Output") {
void commands.executeCommand("prettier.openOutput");
}
}

/**
* Reset notification tracking (useful for testing or when configuration changes)
*/
public reset(): void {
this.shownErrors.clear();
}
}
7 changes: 6 additions & 1 deletion src/extension.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { commands, ExtensionContext, workspace } from "vscode";
import { createConfigFile } from "./commands.js";
import { LoggingService } from "./LoggingService.js";
import { NotificationService } from "./NotificationService.js";
import { ModuleResolver } from "./ModuleResolverNode.js";
import PrettierEditService from "./PrettierEditService.js";
import { StatusBar } from "./StatusBar.js";
Expand All @@ -14,6 +15,7 @@ const extensionVersion = process.env.EXTENSION_VERSION || "0.0.0";

export async function activate(context: ExtensionContext) {
const loggingService = new LoggingService();
const notificationService = new NotificationService();

loggingService.logInfo(`Extension Name: ${extensionName}.`);
loggingService.logInfo(`Extension Version: ${extensionVersion}.`);
Expand All @@ -36,7 +38,10 @@ export async function activate(context: ExtensionContext) {
return;
}

const moduleResolver = new ModuleResolver(loggingService);
const moduleResolver = new ModuleResolver(
loggingService,
notificationService,
);

// Get the global prettier instance promise - needed for TemplateService
// and editService registration
Expand Down
4 changes: 4 additions & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,10 @@ export interface IExtensionConfig {
* If true, enabled debug logs
*/
enableDebugLogs: boolean;
/**
* If true, show error notifications for fatal errors
*/
enableErrorNotifications: boolean;
}
/**
* Configuration for prettier-vscode
Expand Down