UNPKG

sfdx-hardis

Version:

Swiss-army-knife Toolbox for Salesforce. Allows you to define a complete CD/CD Pipeline. Orchestrate base commands and assist users with interactive wizards

299 lines • 11.1 kB
import c from 'chalk'; import * as util from 'util'; import WebSocket from 'ws'; import { isCI, uxLog } from './utils/index.js'; import { SfError } from '@salesforce/core'; import path from 'path'; import { fileURLToPath } from 'url'; import { CONSTANTS } from '../config/index.js'; const __dirname = path.dirname(fileURLToPath(import.meta.url)); let globalWs; let isWsOpen = false; let userInput = null; const PORT = process.env.SFDX_HARDIS_WEBSOCKET_PORT || 2702; // Define allowed log types and type alias outside the class export const LOG_TYPES = ['log', 'action', 'warning', 'error', 'success', 'table', "other"]; export class WebSocketClient { ws; wsContext; promptResponse; isDead = false; isInitialized = false; constructor(context) { this.wsContext = context; const wsHostPort = context.websocketHostPort ? `ws://${context.websocketHostPort}` : `ws://localhost:${PORT}`; try { this.ws = new WebSocket(wsHostPort); globalWs = this; // eslint-disable-line this.start(); console.log("WS Client started"); } catch (err) { this.isDead = true; uxLog("warning", this, c.yellow('Unable to start WebSocket client on ' + wsHostPort + '. ' + err.message)); } } static async isInitialized() { if (globalWs) { let retries = 40; // Wait up to 10 seconds while (!globalWs.isInitialized && retries > 0 && !globalWs.isDead) { await new Promise((resolve) => setTimeout(resolve, 250)); retries--; } return globalWs.isInitialized; } return false; } static isAlive() { return !isCI && globalWs != null && isWsOpen === true; } static isAliveWithLwcUI() { return this.isAlive() && userInput === 'ui-lwc'; } static sendMessage(data) { if (globalWs) { globalWs.sendMessageToServer(data); } } // Requests open file within VS Code if linked static requestOpenFile(file) { WebSocketClient.sendMessage({ event: 'openFile', file: file.replace(/\\/g, '/') }); } // Send refresh status message static sendRefreshStatusMessage() { WebSocketClient.sendMessage({ event: 'refreshStatus' }); } // Send refresh commands message static sendRefreshCommandsMessage() { WebSocketClient.sendMessage({ event: 'refreshCommands' }); } // Send progress start message static sendProgressStartMessage(title, totalSteps) { WebSocketClient.sendMessage({ event: 'progressStart', title: title || 'Progress', totalSteps: totalSteps || 0 }); } // Send progress step message static sendProgressStepMessage(step, totalSteps) { WebSocketClient.sendMessage({ event: 'progressStep', step: step, totalSteps: totalSteps }); } // Send progress end message static sendProgressEndMessage(totalSteps) { WebSocketClient.sendMessage({ event: 'progressEnd', totalSteps: totalSteps }); } // Send refresh plugins message static sendRefreshPluginsMessage() { WebSocketClient.sendMessage({ event: 'refreshPlugins' }); } // Send command sub-command start message static sendCommandSubCommandStartMessage(command, cwd, options) { WebSocketClient.sendMessage({ event: 'commandSubCommandStart', data: { command: command, cwd: cwd, options: options, }, }); } // Send command sub-command end message static sendCommandSubCommandEndMessage(command, cwd, options, success, result) { WebSocketClient.sendMessage({ event: 'commandSubCommandEnd', data: { command: command, cwd: cwd, options: options, success: success, result: result, }, }); } // Send command log line message static sendCommandLogLineMessage(message, logType, isQuestion) { WebSocketClient.sendMessage({ event: 'commandLogLine', logType: logType, message: message, isQuestion: isQuestion, }); } // Send run SFDX Hardis command message static sendRunSfdxHardisCommandMessage(sfdxHardisCommand) { WebSocketClient.sendMessage({ event: 'runSfdxHardisCommand', sfdxHardisCommand: sfdxHardisCommand, }); } // Sends refresh pipeline message static sendRefreshPipelineMessage() { WebSocketClient.sendMessage({ event: 'refreshPipeline' }); } // Sends info about downloadable report file static sendReportFileMessage(file, title, type) { WebSocketClient.sendMessage({ event: 'reportFile', file: file.replace(/\\/g, '/'), title: title, type: type }); } static sendPrompts(prompts) { if (globalWs) { return globalWs.promptServer(prompts); } throw new SfError('globalWs should be set in sendPrompts'); } // Send close client message with status static sendCloseClientMessage(status, error = null) { const message = { event: 'closeClient', context: globalWs?.wsContext, status: status, }; if (error) { message.error = { type: error.type || 'unknown', message: error.message || 'An error occurred', stack: error.stack || '', }; } WebSocketClient.sendMessage(message); } // Close the WebSocket connection externally static closeClient(status) { if (globalWs) { globalWs.dispose(status); } } getCommandDocUrl() { // Extract command from context to build documentation URL if (this.wsContext?.command) { const command = this.wsContext.command; // Convert command format like "hardis:doc:flow2markdown" to URL path const urlPath = command.replace(/:/g, '/'); return `${CONSTANTS.DOC_URL_ROOT}/${urlPath}/`; } // Return undefined if no specific command return undefined; } async start() { this.ws.on('open', async () => { isWsOpen = true; const commandDocUrl = this.getCommandDocUrl(); const message = { event: 'initClient', context: this.wsContext, }; if (commandDocUrl) { message.commandDocUrl = commandDocUrl; } // Dynamically import command class and send static uiConfig if present if (this.wsContext?.command) { try { // Convert command string to file path, e.g. hardis:cache:clear -> lib/commands/hardis/cache/clear.js const commandParts = this.wsContext.command.split(':'); const commandPath = path.resolve(__dirname, '../../lib/commands', ...commandParts) + '.js'; const fileUrl = 'file://' + commandPath.replace(/\\/g, '/'); const imported = await import(fileUrl); const CommandClass = imported.default; if (process.env.NO_NEW_COMMAND_TAB === "true") { message.uiConfig = { hide: true }; } else if (CommandClass && CommandClass.uiConfig) { message.uiConfig = CommandClass.uiConfig; } } catch (e) { uxLog("warning", this, c.yellow(`Unable to import command class for ${this.wsContext.command}: ${e instanceof Error ? e.message : String(e)}`)); } } // Add link to command log file if (globalThis?.hardisLogFileStream?.path) { const logFilePath = String(globalThis.hardisLogFileStream.path).replace(/\\/g, '/'); message.commandLogFile = logFilePath; } this.ws.send(JSON.stringify(message)); // uxLog("other", this, c.grey('Initialized WebSocket connection with VS Code SFDX Hardis.')); }); this.ws.on('message', (data) => { this.receiveMessage(JSON.parse(data)); }); this.ws.on('error', (err) => { this.ws.terminate(); globalWs = null; isWsOpen = false; this.isDead = true; if (process.env.DEBUG) { console.error(err); } }); } receiveMessage(data) { if (process.env.DEBUG) { console.debug('websocket: received: %s', util.inspect(data)); } if (data.event === 'ping') { // Respond to ping messages to keep the connection alive this.ws.send(JSON.stringify({ event: 'pong' })); } else if (data.event === 'promptsResponse') { this.promptResponse = data.promptsResponse; } else if (data.event === 'userInput') { userInput = data.userInput; this.isInitialized = true; } else if (data.event === 'cancelCommand') { if (this.wsContext?.command === data?.context?.command && this.wsContext.id === data?.context?.id) { uxLog("error", this, c.red('Command cancelled by user.')); process.exit(1); } } } sendMessageToServer(data) { data.context = this.wsContext; this.ws.send(JSON.stringify(data)); } promptServer(prompts) { this.sendMessageToServer({ event: 'prompts', prompts: prompts }); this.promptResponse = null; let ok = false; return new Promise((resolve, reject) => { let interval = null; let timeout = null; interval = setInterval(() => { if (this.promptResponse != null) { clearInterval(interval); clearTimeout(timeout); ok = true; resolve(this.promptResponse); } }, 300); timeout = setTimeout(() => { if (ok === false) { clearInterval(interval); reject('[sfdx-hardis] No response from UI WebSocket Server'); } }, 7200000); // 2h timeout }); } dispose(status, error = null) { WebSocketClient.sendCloseClientMessage(status, error); this.ws.terminate(); this.isDead = true; isWsOpen = false; globalWs = null; // uxLog("other", this,c.grey('Closed WebSocket connection with VS Code SFDX Hardis')); } } //# sourceMappingURL=websocketClient.js.map