sculptui-vscode
Version:
Visually edit your React component code from the browser
283 lines (266 loc) • 10 kB
text/typescript
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);
}
}
}