// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for details.
import * as vscode from "vscode";
import * as Q from "q";
import * as XDL from "./exponent/xdlInterface";
import {SettingsHelper} from "./settingsHelper";
import {OutputChannelLogger} from "./log/OutputChannelLogger";
import {Packager} from "../common/packager";
import {TargetType, GeneralMobilePlatform} from "./generalMobilePlatform";
import {AndroidPlatform} from "./android/androidPlatform";
import {IOSPlatform} from "./ios/iOSPlatform";
import {ReactNativeProjectHelper} from "../common/reactNativeProjectHelper";
import {TargetPlatformHelper} from "../common/targetPlatformHelper";
import {TelemetryHelper} from "../common/telemetryHelper";
import {ExponentHelper} from "./exponent/exponentHelper";
import {ReactDirManager} from "./reactDirManager";
import {ExtensionServer} from "./extensionServer";
import {IAndroidRunOptions, IIOSRunOptions} from "./launchArgs";
import {ExponentPlatform} from "./exponent/exponentPlatform";
import {spawn, ChildProcess} from "child_process";
import {HostPlatform} from "../common/hostPlatform";
import {CommandExecutor} from "../common/commandExecutor";
import * as nls from "vscode-nls";
import {ErrorHelper} from "../common/error/errorHelper";
import {InternalErrorCode} from "../common/error/internalErrorCode";
const localize = nls.loadMessageBundle();
interface IReactNativeStuff {
packager: Packager;
exponentHelper: ExponentHelper;
reactDirManager: ReactDirManager;
extensionServer: ExtensionServer;
}
interface IReactNativeProject extends IReactNativeStuff {
workspaceFolder: vscode.WorkspaceFolder;
reactNativeVersion?: string;
}
export class CommandPaletteHandler {
public static elementInspector: ChildProcess | null;
private static projectsCache: {[key: string]: IReactNativeProject} = {};
private static logger: OutputChannelLogger = OutputChannelLogger.getMainChannel();
public static addFolder(workspaceFolder: vscode.WorkspaceFolder, stuff: IReactNativeStuff): void {
this.logger.debug(`Command palette: added folder ${workspaceFolder.uri.fsPath}`);
this.projectsCache[workspaceFolder.uri.fsPath] = {
...stuff,
workspaceFolder,
};
}
public static getFolder(workspaceFolder: vscode.WorkspaceFolder): IReactNativeProject {
return this.projectsCache[workspaceFolder.uri.fsPath];
}
public static delFolder(workspaceFolder: vscode.WorkspaceFolder): void {
delete this.projectsCache[workspaceFolder.uri.fsPath];
}
/**
* Starts the React Native packager
*/
public static startPackager(): Q.Promise<void> {
return this.selectProject()
.then((project: IReactNativeProject) => {
return ReactNativeProjectHelper.getReactNativePackageVersionFromNodeModules(project.workspaceFolder.uri.fsPath)
.then(version => {
return this.executeCommandInContext("startPackager", project.workspaceFolder, () => {
return project.packager.isRunning()
.then((running) => {
return running ? project.packager.stop() : Q.resolve(void 0);
});
})
.then(() => project.packager.start());
});
});
}
/**
* Kills the React Native packager invoked by the extension's packager
*/
public static stopPackager(): Q.Promise<void> {
return this.selectProject()
.then((project: IReactNativeProject) => {
return this.executeCommandInContext("stopPackager", project.workspaceFolder, () => project.packager.stop());
});
}
public static stopAllPackagers(): Q.Promise<void> {
let keys = Object.keys(this.projectsCache);
let promises: Q.Promise<void>[] = [];
keys.forEach((key) => {
let project = this.projectsCache[key];
promises.push(this.executeCommandInContext("stopPackager", project.workspaceFolder, () => project.packager.stop()));
});
return Q.all(promises).then(() => {});
}
/**
* Restarts the React Native packager
*/
public static restartPackager(): Q.Promise<void> {
return this.selectProject()
.then((project: IReactNativeProject) => {
return ReactNativeProjectHelper.getReactNativePackageVersionFromNodeModules(project.workspaceFolder.uri.fsPath)
.then(version => {
return this.executeCommandInContext("restartPackager", project.workspaceFolder, () =>
this.runRestartPackagerCommandAndUpdateStatus(project));
});
});
}
/**
* Execute command to publish to exponent host.
*/
public static publishToExpHost(): Q.Promise<void> {
return this.selectProject()
.then((project: IReactNativeProject) => {
return this.executeCommandInContext("publishToExpHost", project.workspaceFolder, () => {
return this.executePublishToExpHost(project).then((didPublish) => {
if (!didPublish) {
CommandPaletteHandler.logger.warning(localize("ExponentPublishingWasUnsuccessfulMakeSureYoureLoggedInToExpo", "Publishing was unsuccessful. Please make sure you are logged in Expo and your project is a valid Expo project"));
}
});
});
});
}
/**
* Executes the 'react-native run-android' command
*/
public static runAndroid(target: TargetType = "simulator"): Q.Promise<void> {
return this.selectProject()
.then((project: IReactNativeProject) => {
TargetPlatformHelper.checkTargetPlatformSupport("android");
return ReactNativeProjectHelper.getReactNativePackageVersionFromNodeModules(project.workspaceFolder.uri.fsPath)
.then(version => {
project.reactNativeVersion = version;
return this.executeCommandInContext("runAndroid", project.workspaceFolder, () => {
const platform = <AndroidPlatform>this.createPlatform(project, "android", AndroidPlatform, target);
return platform.beforeStartPackager()
.then(() => {
return platform.startPackager();
})
.then(() => {
return platform.runApp(/*shouldLaunchInAllDevices*/true);
})
.then(() => {
return platform.disableJSDebuggingMode();
});
});
});
});
}
/**
* Executes the 'react-native run-ios' command
*/
public static runIos(target: TargetType = "simulator"): Q.Promise<void> {
return this.selectProject()
.then((project: IReactNativeProject) => {
return ReactNativeProjectHelper.getReactNativePackageVersionFromNodeModules(project.workspaceFolder.uri.fsPath)
.then(version => {
project.reactNativeVersion = version;
TargetPlatformHelper.checkTargetPlatformSupport("ios");
return this.executeCommandInContext("runIos", project.workspaceFolder, () => {
const platform = <IOSPlatform>this.createPlatform(project, "ios", IOSPlatform, target);
return platform.beforeStartPackager()
.then(() => {
return platform.startPackager();
})
.then(() => {
// Set the Debugging setting to disabled, because in iOS it's persisted across runs of the app
return platform.disableJSDebuggingMode();
})
.catch(() => { }) // If setting the debugging mode fails, we ignore the error and we run the run ios command anyways
.then(() => {
return platform.runApp();
});
});
});
});
}
/**
* Starts the Exponent packager
*/
public static runExponent(): Q.Promise<void> {
return this.selectProject()
.then((project: IReactNativeProject) => {
return ReactNativeProjectHelper.getReactNativePackageVersionFromNodeModules(project.workspaceFolder.uri.fsPath)
.then(version => {
return this.loginToExponent(project)
.then(() => {
return this.executeCommandInContext("runExponent", project.workspaceFolder, () => {
project.reactNativeVersion = version;
const platform = <ExponentPlatform>this.createPlatform(project, "exponent", ExponentPlatform);
return platform.beforeStartPackager()
.then(() => {
return platform.startPackager();
})
.then(() => {
return platform.runApp();
});
});
});
});
});
}
public static showDevMenu(): Q.Promise<void> {
return this.selectProject()
.then((project: IReactNativeProject) => {
const androidPlatform = <AndroidPlatform>this.createPlatform(project, "android", AndroidPlatform);
androidPlatform.showDevMenu()
.catch(() => { }); // Ignore any errors
if (process.platform === "darwin") {
const iosPlatform = <IOSPlatform>this.createPlatform(project, "ios", IOSPlatform);
iosPlatform.showDevMenu()
.catch(() => { }); // Ignore any errors
}
return Q.resolve(void 0);
});
}
public static reloadApp(): Q.Promise<void> {
return this.selectProject()
.then((project: IReactNativeProject) => {
const androidPlatform = <AndroidPlatform>this.createPlatform(project, "android", AndroidPlatform);
androidPlatform.reloadApp()
.catch(() => { }); // Ignore any errors
if (process.platform === "darwin") {
const iosPlatform = <IOSPlatform>this.createPlatform(project, "ios", IOSPlatform);
iosPlatform.reloadApp()
.catch(() => { }); // Ignore any errors
}
return Q.resolve(void 0);
});
}
public static runElementInspector(): Q.Promise<void> {
if (!CommandPaletteHandler.elementInspector) {
// Remove the following env variables to prevent running electron app in node mode.
// https://github.com/Microsoft/vscode/issues/3011#issuecomment-184577502
let env = Object.assign({}, process.env);
delete env.ATOM_SHELL_INTERNAL_RUN_AS_NODE;
delete env.ELECTRON_RUN_AS_NODE;
let command = HostPlatform.getNpmCliCommand("react-devtools");
CommandPaletteHandler.elementInspector = spawn(command, [], {
env,
});
if (!CommandPaletteHandler.elementInspector.pid) {
CommandPaletteHandler.elementInspector = null;
return Q.reject(ErrorHelper.getInternalError(InternalErrorCode.ReactDevtoolsIsNotInstalled));
}
CommandPaletteHandler.elementInspector.stdout.on("data", (data: string) => {
this.logger.info(data);
});
CommandPaletteHandler.elementInspector.stderr.on("data", (data: string) => {
this.logger.error(data);
});
CommandPaletteHandler.elementInspector.once("exit", () => {
CommandPaletteHandler.elementInspector = null;
});
} else {
this.logger.info(localize("AnotherElementInspectorAlreadyRun", "Another element inspector already run"));
}
return Q(void 0);
}
public static stopElementInspector(): void {
return CommandPaletteHandler.elementInspector ? CommandPaletteHandler.elementInspector.kill() : void 0;
}
public static getPlatformByCommandName(commandName: string): string {
commandName = commandName.toLocaleLowerCase();
if (commandName.indexOf("android") > -1) {
return "android";
}
if (commandName.indexOf("ios") > -1) {
return "ios";
}
if (commandName.indexOf("exponent") > -1) {
return "exponent";
}
return "";
}
private static createPlatform(project: IReactNativeProject, platform: "ios" | "android" | "exponent", platformClass: typeof GeneralMobilePlatform, target?: TargetType): GeneralMobilePlatform {
const runOptions = CommandPaletteHandler.getRunOptions(project, platform, target);
return new platformClass(runOptions, {
packager: project.packager,
});
}
private static runRestartPackagerCommandAndUpdateStatus(project: IReactNativeProject): Q.Promise<void> {
return project.packager.restart(SettingsHelper.getPackagerPort(project.workspaceFolder.uri.fsPath));
}
/**
* Ensures that we are in a React Native project and then executes the operation
* Otherwise, displays an error message banner
* {operation} - a function that performs the expected operation
*/
private static executeCommandInContext(rnCommand: string, workspaceFolder: vscode.WorkspaceFolder, operation: () => Q.Promise<void>): Q.Promise<void> {
const extProps = {
platform: {
value: CommandPaletteHandler.getPlatformByCommandName(rnCommand),
isPii: false,
},
};
return TelemetryHelper.generate("RNCommand", extProps, (generator) => {
generator.add("command", rnCommand, false);
const projectRoot = SettingsHelper.getReactNativeProjectRoot(workspaceFolder.uri.fsPath);
this.logger.debug(`Command palette: run project ${projectRoot} in context`);
return ReactNativeProjectHelper.isReactNativeProject(projectRoot)
.then(isRNProject => {
generator.add("isRNProject", isRNProject, false);
if (isRNProject) {
// Bring the log channel to focus
this.logger.setFocusOnLogChannel();
// Execute the operation
return operation();
} else {
vscode.window.showErrorMessage(`${projectRoot} workspace is not a React Native project.`);
return;
}
});
});
}
/**
* Publish project to exponent server. In order to do this we need to make sure the user is logged in exponent and the packager is running.
*/
private static executePublishToExpHost(project: IReactNativeProject): Q.Promise<boolean> {
CommandPaletteHandler.logger.info(localize("PublishingAppToExponentServer", "Publishing app to Expo server. This might take a moment."));
return this.loginToExponent(project)
.then(user => {
CommandPaletteHandler.logger.debug(`Publishing as ${user.username}...`);
return this.runExponent()
.then(() =>
XDL.publish(project.workspaceFolder.uri.fsPath))
.then(response => {
if (response.err || !response.url) {
return false;
}
const publishedOutput = localize("ExpoAppSuccessfullyPublishedTo", "Expo app successfully published to {0}", response.url);
CommandPaletteHandler.logger.info(publishedOutput);
vscode.window.showInformationMessage(publishedOutput);
return true;
});
});
}
private static loginToExponent(project: IReactNativeProject): Q.Promise<XDL.IUser> {
return project.exponentHelper.loginToExponent(
(message, password) => {
return Q.Promise((resolve, reject) => {
vscode.window.showInputBox({ placeHolder: message, password: password })
.then(login => {
resolve(login || "");
}, reject);
});
},
(message) => {
return Q.Promise((resolve, reject) => {
vscode.window.showInformationMessage(message)
.then(password => {
resolve(password || "");
}, reject);
});
}
)
.catch((err) => {
CommandPaletteHandler.logger.warning(localize("ExpoErrorOccuredMakeSureYouAreLoggedIn", "An error has occured. Please make sure you are logged in to Expo, your project is setup correctly for publishing and your packager is running as Expo."));
throw err;
});
}
private static selectProject(): Q.Promise<IReactNativeProject> {
let keys = Object.keys(this.projectsCache);
if (keys.length > 1) {
return Q.Promise((resolve, reject) => {
vscode.window.showQuickPick(keys)
.then((selected) => {
if (selected) {
this.logger.debug(`Command palette: selected project ${selected}`);
resolve(this.projectsCache[selected]);
}
}, reject);
});
} else if (keys.length === 1) {
this.logger.debug(`Command palette: once project ${keys[0]}`);
return Q.resolve(this.projectsCache[keys[0]]);
} else {
return Q.reject(ErrorHelper.getInternalError(InternalErrorCode.WorkspaceNotFound, "Current workspace does not contain React Native projects."));
}
}
private static getRunOptions(project: IReactNativeProject, platform: "ios" | "android" | "exponent", target: TargetType = "simulator"): IAndroidRunOptions | IIOSRunOptions {
const packagerPort = SettingsHelper.getPackagerPort(project.workspaceFolder.uri.fsPath);
const runArgs = SettingsHelper.getRunArgs(platform, target, project.workspaceFolder.uri);
const envArgs = SettingsHelper.getEnvArgs(platform, target, project.workspaceFolder.uri);
const envFile = SettingsHelper.getEnvFile(platform, target, project.workspaceFolder.uri);
const projectRoot = SettingsHelper.getReactNativeProjectRoot(project.workspaceFolder.uri.fsPath);
const runOptions: IAndroidRunOptions | IIOSRunOptions = {
platform: platform,
workspaceRoot: project.workspaceFolder.uri.fsPath,
projectRoot: projectRoot,
packagerPort: packagerPort,
runArguments: runArgs,
env: envArgs,
envFile: envFile,
reactNativeVersion: project.reactNativeVersion || "",
};
CommandExecutor.ReactNativeCommand = SettingsHelper.getReactNativeGlobalCommandName(project.workspaceFolder.uri);
return runOptions;
}
}microsoft/vscode-react-native
Publicmirrored fromhttps://github.com/microsoft/vscode-react-nativeAvailable
src/extension/commandPaletteHandler.ts
432lines · modepreview