From 9ca0ecc264212dabe0930ddc108591466dedefb1 Mon Sep 17 00:00:00 2001 From: Ali Ariff Date: Sat, 31 Aug 2024 14:06:19 +0200 Subject: [PATCH] chore: Refactor HtmlBeautifier class and improve error handling (#73) * chore: Refactor HtmlBeautifier class and improve error handling * refactor: Update HtmlBeautifier class to handle error cases and improve code readability --- src/formatter/htmlbeautifier.ts | 231 ++++++++++++++++++-------------- 1 file changed, 131 insertions(+), 100 deletions(-) diff --git a/src/formatter/htmlbeautifier.ts b/src/formatter/htmlbeautifier.ts index cda7c21..33acd52 100644 --- a/src/formatter/htmlbeautifier.ts +++ b/src/formatter/htmlbeautifier.ts @@ -3,34 +3,39 @@ import * as cp from "child_process"; const isWsl = require("is-wsl"); export default class HtmlBeautifier { + private logChannel: vscode.LogOutputChannel; + + constructor() { + this.logChannel = vscode.window.createOutputChannel("ERB Beautifier", { + log: true, + }); + } + /** - * Formats the given input using HTML Beautifier - * @param {string} input - The input to be formatted - * @returns {Promise} The formatted input + * Formats the input string using HTML Beautifier. + * @param input The input string to be formatted. + * @returns A promise that resolves to the formatted string. */ public async format(input: string): Promise { try { - const cmd = `${this.exe} ${this.cliOptions.join( - " " - )} with custom env ${JSON.stringify(this.customEnvVars)}`; - console.log(`Formatting ERB with command: ${cmd}`); - console.time(cmd); - + const startTime = Date.now(); const result = await this.executeCommand(input); - - console.timeEnd(cmd); + const duration = Date.now() - startTime; + this.logChannel.info( + `Formatting completed successfully in ${duration}ms.` + ); return result; } catch (error) { - console.error(error); - const errorMessage = - error instanceof Error ? error.message : "Unknown error occurred"; - vscode.window.showErrorMessage( - `Error occurred while formatting: ${errorMessage}` - ); + this.handleError(error, "Error occurred while formatting"); throw error; } } + /** + * Executes the formatting command with the provided input. + * @param input The input to format. + * @returns A promise that resolves to the formatted output. + */ private executeCommand(input: string): Promise { return new Promise((resolve, reject) => { // Handle spawn EINVAL error on Windows. See https://github.com/nodejs/node/issues/52554 @@ -44,53 +49,37 @@ export default class HtmlBeautifier { ...shellOptions, }); - if (htmlbeautifier.stdin === null || htmlbeautifier.stdout === null) { - const msg = "Couldn't initialize STDIN or STDOUT"; - console.warn(msg); - vscode.window.showErrorMessage(msg); - reject(new Error(msg)); - return; - } - - let formattedResult = ""; - let errorMessage = ""; - let stdoutChunks: Buffer[] = []; - let stderrChunks: Buffer[] = []; + const fullCommand = `${this.exe} ${this.cliOptions.join(" ")} (cwd: ${ + vscode.workspace.rootPath || __dirname + }) with custom env: ${JSON.stringify(this.customEnvVars)}`; + this.logChannel.info(`Formatting ERB with command: ${fullCommand}`); - htmlbeautifier.on("error", (err) => { - console.warn(err); - vscode.window.showErrorMessage( - `Couldn't run ${this.exe} '${err.message}'` + if (!htmlbeautifier.stdin || !htmlbeautifier.stdout) { + return this.handleSpawnError( + reject, + "Couldn't initialize STDIN or STDOUT" ); - reject(err); - }); - - htmlbeautifier.stdout.on("data", (chunk) => { - stdoutChunks.push(chunk); - }); + } - htmlbeautifier.stdout.on("end", () => { - let result = Buffer.concat(stdoutChunks).toString(); - formattedResult = this.handleFinalNewline(input, result); - }); + const stdoutChunks: Buffer[] = []; + const stderrChunks: Buffer[] = []; - htmlbeautifier.stderr.on("data", (chunk) => { - stderrChunks.push(chunk); - }); + htmlbeautifier.stdout.on("data", (chunk) => stdoutChunks.push(chunk)); + htmlbeautifier.stderr.on("data", (chunk) => stderrChunks.push(chunk)); - htmlbeautifier.stderr.on("end", () => { - errorMessage = Buffer.concat(stderrChunks).toString(); - }); + htmlbeautifier.on("error", (err) => + this.handleSpawnError( + reject, + `Couldn't run ${this.exe}: ${err.message}`, + err + ) + ); htmlbeautifier.on("exit", (code) => { - if (code) { - vscode.window.showErrorMessage( - `Failed with exit code: ${code}. '${errorMessage}'` - ); - reject(new Error(`Command failed with exit code ${code}`)); - } else { - resolve(formattedResult); - } + const formattedResult = Buffer.concat(stdoutChunks).toString(); + const finalResult = this.handleFinalNewline(input, formattedResult); + const errorMessage = Buffer.concat(stderrChunks).toString(); + this.handleExit(code, finalResult, errorMessage, resolve, reject); }); htmlbeautifier.stdin.write(input); @@ -99,8 +88,64 @@ export default class HtmlBeautifier { } /** - * Returns the executable path for HTML Beautifier - * @returns {string} The executable path + * Handles errors during process spawning. + * @param reject The promise reject function. + * @param message The error message to log and show to the user. + * @param err Optional error object. + */ + private handleSpawnError( + reject: (reason?: any) => void, + message: string, + err?: Error + ): void { + this.logChannel.warn(message); + vscode.window.showErrorMessage(message); + if (err) { + this.logChannel.warn(err.message); + } + reject(err || new Error(message)); + } + + /** + * Handles the process exit event and resolves or rejects the promise. + * @param code The process exit code. + * @param result The formatted result. + * @param errorMessage The error message, if any. + * @param resolve The promise resolve function. + * @param reject The promise reject function. + */ + private handleExit( + code: number | null, + result: string, + errorMessage: string, + resolve: (value: string | PromiseLike) => void, + reject: (reason?: any) => void + ): void { + if (code && code !== 0) { + const error = `Failed with exit code: ${code}. ${errorMessage}`; + this.logChannel.error(error); + vscode.window.showErrorMessage(error); + reject(new Error(error)); + } else { + resolve(result); + } + } + + /** + * Handles errors by logging and displaying a message to the user. + * @param error The error object or message. + * @param userMessage The message to display to the user. + */ + private handleError(error: any, userMessage: string): void { + const errorMessage = + error instanceof Error ? error.message : "Unknown error occurred"; + this.logChannel.error(errorMessage); + vscode.window.showErrorMessage(`${userMessage}: ${errorMessage}`); + } + + /** + * Gets the executable path for HTML Beautifier based on the configuration. + * @returns The path to the executable. */ private get exe(): string { const config = vscode.workspace.getConfiguration("vscode-erb-beautify"); @@ -112,26 +157,26 @@ export default class HtmlBeautifier { } /** - * Determines if the current platform is Windows (excluding WSL) - * @returns {boolean} True if the platform is Windows, false otherwise + * Checks if the current platform is Windows (excluding WSL). + * @returns True if the platform is Windows; false otherwise. */ private isWindows(): boolean { return process.platform === "win32" && !isWsl; } /** - * Returns the command-line options for HTML Beautifier - * @returns {string[]} The command-line options + * Retrieves the command-line options for HTML Beautifier from the configuration. + * @returns An array of command-line options. */ private get cliOptions(): string[] { const config = vscode.workspace.getConfiguration("vscode-erb-beautify"); - const acc: string[] = []; + const options: string[] = []; if (config.get("useBundler")) { - acc.push("exec", "htmlbeautifier"); + options.push("exec", "htmlbeautifier"); } - return Object.keys(config).reduce(function (acc, key) { + return Object.keys(config).reduce((acc, key) => { switch (key) { case "indentBy": acc.push("--indent-by", config[key]); @@ -154,27 +199,23 @@ export default class HtmlBeautifier { break; } return acc; - }, acc); + }, options); } /** - * Retrieves the custom environment variables from the configuration - * @returns {Record} The custom environment variables + * Retrieves custom environment variables from the configuration. + * @returns A record of custom environment variables. */ private get customEnvVars(): Record { const config = vscode.workspace.getConfiguration("vscode-erb-beautify"); - const customEnvVar = config.get("customEnvVar", {}) as Record< - string, - string - >; - return customEnvVar; + return config.get("customEnvVar", {}) as Record; } /** - * Adjusts the final newline of the result string based on the VS Code configuration and the input string. - * @param {string} input - The original input string. - * @param {string} result - The result string to be processed. - * @returns {string} The processed result string. + * Adjusts the final newline of the result string based on VS Code configuration. + * @param input The original input string. + * @param result The formatted result string. + * @returns The adjusted result string. */ private handleFinalNewline(input: string, result: string): string { // Get the 'insertFinalNewline' setting from VS Code configuration @@ -182,7 +223,7 @@ export default class HtmlBeautifier { .getConfiguration() .get("files.insertFinalNewline"); - // Check if the result string ends with a newline + // Determine if the result ends with a newline const resultEndsWithNewline = result.endsWith("\n") || result.endsWith("\r\n"); @@ -191,11 +232,8 @@ export default class HtmlBeautifier { // Get the 'files.eol' setting from VS Code configuration const eol = vscode.workspace.getConfiguration().get("files.eol"); - // Set the newline character(s) based on the 'files.eol' setting and the platform - let newline = eol; - if (eol === "auto") { - newline = this.isWindows() ? "\r\n" : "\n"; - } + // Determine newline character(s) based on the 'files.eol' setting and the platform + const newline = eol === "auto" ? (this.isWindows() ? "\r\n" : "\n") : eol; // Append the newline to the result result += newline; @@ -208,11 +246,8 @@ export default class HtmlBeautifier { // If the input and result use different newline character(s) if (inputNewline !== resultNewline) { - // Remove the newline from the end of the result - result = result.slice(0, -resultNewline.length); - - // Append the newline from the input to the result - result += inputNewline; + // Remove the newline from the end of the result and append the input's newline + result = result.slice(0, -resultNewline.length) + inputNewline; } } @@ -221,20 +256,16 @@ export default class HtmlBeautifier { } /** - * Determines the type of newline used in the input string. - * @param {string} input - The input string. - * @returns {string} The newline character(s) used in the input string, or an empty string if the input does not end with a newline. + * Determines the newline character(s) used in the input string. + * @param input The input string. + * @returns The newline character(s) used, or an empty string if none. */ private getNewline(input: string): string { - // If the input ends with a Windows-style newline, return '\r\n' if (input.endsWith("\r\n")) { - return "\r\n"; - } - // If the input ends with a Unix-style newline, return '\n' - else if (input.endsWith("\n")) { - return "\n"; + return "\r\n"; // Return Windows-style newline + } else if (input.endsWith("\n")) { + return "\n"; // Return Unix-style newline } - // If the input does not end with a newline, return an empty string - return ""; + return ""; // Return empty if no newline found } }