UNPKG

sculptui-vscode

Version:

Visually edit your React component code from the browser

283 lines (266 loc) 10 kB
import { workspace, window, WorkspaceConfiguration, QuickPickItem } from 'vscode'; import * as path from 'path'; import { runScriptInOutputWindow, stopScriptInOutputWindow, runCommandInIntegratedTerminal, } from '../utils/run-terminal'; import * as portfinder from 'portfinder'; import { getPorts } from '../utils/ports'; import { logError, log } from '../utils/vscode-output'; import { selectCode } from '../utils/vscode-file'; import { getProjectFolder, isWorkspacesRoot, traverseSubFolders, removeRootFolder } from '../utils/folders'; import resolveFrom from 'resolve-from'; import { detectScriptsVersion } from '@sculpt-ui/server'; import telemetry from '../telemetry'; export enum DevServerStatus { stopped, starting, started, stopping, } const runInTerminal = true; const terminalId = 'SculptUI'; const outputWindowId = 'SculptUI - Dev Server'; const defaultPorts = { port: 3000, highestPort: 3100 }; interface FolderOptions extends QuickPickItem { folder: string; } export default class DevServer { private readonly extensionPath: string; private status: DevServerStatus = DevServerStatus.stopped; private onStatusChanged?: (status: DevServerStatus) => void; private onStatusMessage?: (message: string, duration?: number) => void; private serverPId?: number; private url?: string; private readonly ideServerUrl: string; constructor( extensionPath: string, ideServerUrl: string, onStatusChanged?: (status: DevServerStatus) => void, onStatusMessage?: (message: string, duration?: number) => void, ) { this.ideServerUrl = ideServerUrl; this.extensionPath = extensionPath; this.onStatusChanged = onStatusChanged; this.onStatusMessage = onStatusMessage; this.setStatus(DevServerStatus.stopped); } start = async () => { const config = workspace.getConfiguration('sculpt-ui'); try { const folder = await this.getFolder(config); if (!folder) { return; } this.setStatus(DevServerStatus.starting); this.setStatusMessage('dev server starting...', 3000); const port = await this.getPort(config); const useHttps = config.get<boolean>('https'); telemetry.sendEvent('start', { port, useHttps }); this.startServer(folder, { port, useHttps }); return folder; } catch (err) { this.setStatus(DevServerStatus.stopped); telemetry.sendError(err as Error); this.notifyError(`SculptUI can't start: ${err}`); } }; stop = () => { telemetry.sendEvent('stop'); if (runInTerminal) { runCommandInIntegratedTerminal(terminalId, '\x03', [], undefined); this.setStatus(DevServerStatus.stopped); } else { if (this.serverPId) { this.setStatus(DevServerStatus.stopping); stopScriptInOutputWindow(this.serverPId); } else { // make sure status is notified this.setStatus(DevServerStatus.stopped); } } }; getStatus() { return this.status; } getUrl() { return this.url; } private notifyError(message: string) { logError(message); window.showErrorMessage(message); } private getFolder(config: WorkspaceConfiguration): Promise<string | undefined> { return new Promise((resolve, reject) => { let folder = getProjectFolder(config); if (!folder) { reject(new Error(`No folder is opened in VS Code.`)); return; } const detectedInRoot = detectScriptsVersion(folder); if (detectedInRoot && (detectedInRoot === 'sculpt' || !isWorkspacesRoot(folder))) { resolve(folder); return; } const subFolders: string[] = detectedInRoot && !isWorkspacesRoot(folder) ? [folder] : []; traverseSubFolders( folder, (dir: string) => { if (detectScriptsVersion(dir)) { subFolders.push(dir); } return false; }, 3, ['node_modules', '.vscode', '.git'], ); if (subFolders.length === 0) { reject(new Error(`SculptUI can not start: No project folder is available.`)); } else if (subFolders.length === 1) { resolve(subFolders[0]); } else { const folderTrunc = folder.endsWith(path.sep) ? folder.substring(0, folder.length - 2) : folder; const makeLabel = (subFolder: string) => { return removeRootFolder(subFolder, folderTrunc) || '\\'; }; const folderOptions = subFolders.map<FolderOptions>(subFolder => ({ label: makeLabel(subFolder), folder: subFolder, })); folderOptions.push( ...subFolders.map<FolderOptions>(subFolder => ({ label: makeLabel(subFolder), description: '$(symbol-property) and always use in future', folder: subFolder, })), ); window.showQuickPick(folderOptions).then( value => { if (value && value.description) { this.saveFolder(config, value.folder); } resolve(value && value.folder); }, reason => reject(reason), ); } }); } private saveFolder(config: WorkspaceConfiguration, folder: string) { const projectFolder = getProjectFolder(); if (projectFolder) { folder = path.relative(projectFolder, folder); } config.update('folderForStarting', folder, false); window.showInformationMessage( `Folder '${folder}' has been set as the folder to always start SculptUI in. You can change this anytime in the SculptUI extension's settings.`, ); } private getPort(config: WorkspaceConfiguration): Promise<number | undefined> { return new Promise((resolve, reject) => { try { let ports = getPorts(config.get('port'), undefined); if (!ports) { resolve(undefined); } else { portfinder .getPortPromise(ports || defaultPorts) .then(port => resolve(port)) .catch(reason => reject(`Can't retrieve an avialable port: ${reason}`)); } } catch (error) { reject(`Port is not correctly configured:\n${error}\nPlease check the extension's settings.`); return; } }); } private startServer(folder: string, options?: { port?: number; useHttps?: boolean }) { const command = resolveFrom(this.extensionPath, '@sculpt-ui/server/bin/sculpt.js'); const port = options ? options.port : undefined; const useHttps = options ? options.useHttps : undefined; //start our server if (runInTerminal) { const commandArgs = [command]; commandArgs.push('--sculpt-ide-url', this.ideServerUrl); if (port) { commandArgs.push('--port', port.toString()); } if (useHttps) { commandArgs.push('--https', useHttps.toString()); } runCommandInIntegratedTerminal(terminalId, 'node', commandArgs, folder).then(pid => { this.serverPId = pid; this.setStatus(DevServerStatus.started); }); } else { const env: any = { ...process.env }; if (useHttps) { env.HTTPS = useHttps; } if (port) { env.PORT = port.toString(); } this.serverPId = runScriptInOutputWindow( outputWindowId, command, [], folder, env, message => { if (typeof message === 'object') { if (message.type === 'selectInEditor') { if (!message.file || message.line === undefined || Number.isNaN(Number(message.line))) { logError(`Select in editor message with incorrect parameters ${message}`); } else { log(`Selecting in code editor ${message.file}:${message.line}:${message.column || 0}`); selectCode( message.file, { line: message.endLine ? message.endLine - 1 : 0, character: message.endColumn || 0 }, { line: message.line - 1, character: message.column || 0 }, ); } } else if (message.type === 'devServerURL') { this.setStatusMessage('opening ' + message.url, 3000); this.setStatus(DevServerStatus.started); this.url = message.url; } } }, (code, signal, outputChannel) => { if (signal === 'SIGTERM') { /* window.setStatusBarMessage('SculptUI dev server terminated.', 3000); window.showInformationMessage('Sculpt devserver has been terminated.'); */ outputChannel.appendLine('Successfully stopped server'); outputChannel.appendLine('-----------------------'); outputChannel.appendLine(''); this.serverPId = undefined; this.setStatus(DevServerStatus.stopped); } else { if (code) { window.showErrorMessage( `SculptUI dev server has exited with error code '${code}'. Please check the SculptUI dev server's output for more details.`, ); } outputChannel.appendLine(`SculptUI dev server has exited with error code '${code}'.`); outputChannel.appendLine('-----------------------'); outputChannel.appendLine(''); this.serverPId = undefined; this.setStatus(DevServerStatus.stopped); } }, ); } } private setStatus(status: DevServerStatus) { this.status = status; if (this.onStatusChanged) { this.onStatusChanged(status); } } private setStatusMessage(message: string, duration?: number) { if (this.onStatusMessage) { this.onStatusMessage(message, duration); } } }