// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for details. import assert = require("assert"); import * as vscode from "vscode"; import { EntryPointHandler } from "../../../common/entryPointHandler"; import { ErrorHelper } from "../../../common/error/errorHelper"; import { InternalError } from "../../../common/error/internalError"; import { InternalErrorCode } from "../../../common/error/internalErrorCode"; import { AppLauncher } from "../../appLauncher"; import { PlatformType } from "../../launchArgs"; import { OutputChannelLogger } from "../../log/OutputChannelLogger"; import { TelemetryGenerator } from "../../../common/telemetryGenerators"; import { selectProject } from "."; export abstract class Command { private static instances = new Map(); static formInstance(this: T): T["prototype"] { // 'any' because TypeScript is wrong // workaround from https://github.com/microsoft/TypeScript/issues/5863 const that = this as any; const result = Command.instances.get(that) || new that(); Command.instances.set(that, result); return result; } abstract readonly codeName: string; abstract readonly label: string; abstract readonly error: InternalError; get platform(): string { return ( [ PlatformType.Android, PlatformType.iOS, PlatformType.Exponent, PlatformType.macOS, PlatformType.Windows, ].find(it => this.codeName.toLowerCase().includes(it.toLowerCase())) || "" ); } private entryPointHandler?: EntryPointHandler; /** Initialize project property before executing command */ protected requiresProject = true; /** Throw an Error if workspace is not trusted before executing command */ protected requiresTrust = true; protected project?: AppLauncher; protected constructor() {} protected createHandler(fn = this.baseFn.bind(this)): (...args: ArgT) => Promise { return async (...args: ArgT): Promise => { assert(this.entryPointHandler, "this.entryPointHandler is not defined"); const resultFn = async (generator: TelemetryGenerator) => { try { await this.onBeforeExecute(...args); await fn.bind(this)(...args); } catch (error) { if ( error instanceof InternalError && error.errorCode === InternalErrorCode.CommandCanceled ) { generator.addError(error); return; } throw error; } }; OutputChannelLogger.getMainChannel().debug(`Run command: ${this.codeName}`); await this.entryPointHandler.runFunctionWExtProps( `commandPalette.${this.codeName}`, { platform: { value: this.platform, isPii: false, }, }, this.error, resultFn.bind(this), ); }; } // eslint-disable-next-line @typescript-eslint/no-unused-vars protected async onBeforeExecute(...args: ArgT): Promise { if (this.requiresProject) { this.project = await this.selectProject(); } if (this.requiresTrust && !isWorkspaceTrusted()) { throw ErrorHelper.getInternalError( InternalErrorCode.WorkspaceIsNotTrusted, this.project?.getPackager().getProjectPath() || undefined, this.label, ); } function isWorkspaceTrusted() { // Remove after updating supported VS Code engine version to 1.57.0 if (typeof (vscode.workspace as any).isTrusted === "boolean") { return (vscode.workspace as any).isTrusted; } return true; } } protected async selectProject(): Promise { try { return await selectProject(); } catch (error) { if ( error instanceof InternalError && error.errorCode === InternalErrorCode.UserInputCanceled ) { throw ErrorHelper.getNestedError( error, InternalErrorCode.CommandCanceled, this.label, ); } throw error; } } abstract baseFn(...args: ArgT): Promise; /** Execute base command without telemetry */ async executeLocally(...args: ArgT): Promise { await this.onBeforeExecute(...args); await this.baseFn(...args); } public register = (() => { let isCalled = false; return (entryPointHandler: EntryPointHandler) => { this.entryPointHandler = entryPointHandler; assert(!isCalled, "Command can only be registered once"); isCalled = true; return vscode.commands.registerCommand( `reactNative.${this.codeName}`, this.createHandler(), ); }; })(); }