diff --git a/firebase-vscode/src/cli.ts b/firebase-vscode/src/cli.ts index c320971a505..8b99e300753 100644 --- a/firebase-vscode/src/cli.ts +++ b/firebase-vscode/src/cli.ts @@ -20,11 +20,15 @@ import { currentOptions, getCommandOptions } from "./options"; import { setInquirerOptions } from "./stubs/inquirer-stub"; import { ServiceAccount } from "../common/types"; import { listChannels } from "../../src/hosting/api"; -import { ChannelWithId } from "../common/messaging/types"; +import { EmulatorUiSelections, ChannelWithId } from "../common/messaging/types"; import { pluginLogger } from "./logger-wrapper"; import { Config } from "../../src/config"; import { currentUser } from "./workflow"; import { setAccessToken } from "../../src/apiv2"; +import { startAll as startAllEmulators, cleanShutdown as stopAllEmulators } from "../../src/emulator/controller"; +import { EmulatorRegistry } from "../../src/emulator/registry"; +import { EmulatorInfo, Emulators } from "../../src/emulator/types"; +import * as commandUtils from "../../src/emulator/commandUtils"; /** * Try to get a service account by calling requireAuth() without @@ -289,3 +293,29 @@ export async function deployToHosting( } return { success: true, hostingUrl: "", consoleUrl: "" }; } + +export async function emulatorsStart(emulatorUiSelections: EmulatorUiSelections) { + const commandOptions = await getCommandOptions(undefined, { + ...currentOptions, + project: emulatorUiSelections.projectId, + exportOnExit: emulatorUiSelections.exportStateOnExit, + import: emulatorUiSelections.importStateFolderPath, + only: emulatorUiSelections.mode === "hosting" ? "hosting" : "" + }); + // Adjusts some options, export on exit can be a boolean or a path. + commandUtils.setExportOnExitOptions(commandOptions as commandUtils.ExportOnExitOptions); + return startAllEmulators(commandOptions, /*showUi=*/ true); +} + +export async function stopEmulators() { + await stopAllEmulators(); +} + +export function listRunningEmulators(): EmulatorInfo[] { + return EmulatorRegistry.listRunningWithInfo(); +} + +export function getEmulatorUiUrl(): string | undefined { + const url: URL = EmulatorRegistry.url(Emulators.UI); + return url.hostname === "unknown" ? undefined : url.toString(); +} diff --git a/firebase-vscode/src/extension.ts b/firebase-vscode/src/extension.ts index 4bd7409689d..8c9daaf7fd6 100644 --- a/firebase-vscode/src/extension.ts +++ b/firebase-vscode/src/extension.ts @@ -10,6 +10,7 @@ import { } from "../common/messaging/protocol"; import { setupWorkflow } from "./workflow"; import { pluginLogger } from "./logger-wrapper"; +import { onShutdown } from "./workflow"; import { registerWebview } from "./webview"; const broker = createBroker< @@ -32,3 +33,9 @@ export function activate(context: vscode.ExtensionContext) { }) ); } + +// This method is called when the extension is deactivated +export async function deactivate() { + // This await is optimistic but it might wait for a moment longer while we run cleanup activities + await onShutdown(); +} diff --git a/firebase-vscode/src/workflow.ts b/firebase-vscode/src/workflow.ts index 68dde058ff7..918cd8cb84b 100644 --- a/firebase-vscode/src/workflow.ts +++ b/firebase-vscode/src/workflow.ts @@ -11,6 +11,10 @@ import { listProjects, login, logoutUser, + stopEmulators, + listRunningEmulators, + getEmulatorUiUrl, + emulatorsStart } from "./cli"; import { User } from "../../src/types/auth"; import { currentOptions } from "./options"; @@ -229,6 +233,43 @@ export async function setupWorkflow( broker.send("notifyPreviewChannelResponse", { id: response }); }); + broker.on( + "launchEmulators", + async ({ emulatorUiSelections }) => { + await emulatorsStart(emulatorUiSelections); + broker.send("notifyRunningEmulatorInfo", { uiUrl: getEmulatorUiUrl(), displayInfo: listRunningEmulators() }); + } + ); + + broker.on( + "stopEmulators", + async () => { + await stopEmulators(); + // Update the UI + broker.send("notifyEmulatorsStopped"); + } + ); + + broker.on( + "selectEmulatorImportFolder", + async () => { + const options: vscode.OpenDialogOptions = { + canSelectMany: false, + openLabel: `Pick an import folder`, + title: `Pick an import folder`, + canSelectFiles: false, + canSelectFolders: true, + }; + const fileUri = await vscode.window.showOpenDialog(options); + // Update the UI of the selection + if (!fileUri || fileUri.length < 1) { + vscode.window.showErrorMessage("Invalid import folder selected."); + return; + } + broker.send("notifyEmulatorImportFolder", { folder: fileUri[0].fsPath }); + } + ); + context.subscriptions.push( setupFirebaseJsonAndRcFileSystemWatcher(broker, context) ); @@ -399,3 +440,10 @@ export async function setupWorkflow( } } } + +/** + * Cleans up any open resources before shutting down. + */ +export async function onShutdown() { + await stopEmulators(); +} \ No newline at end of file diff --git a/firebase-vscode/webpack.common.js b/firebase-vscode/webpack.common.js index 8b7c5786494..dabc9b30fc9 100644 --- a/firebase-vscode/webpack.common.js +++ b/firebase-vscode/webpack.common.js @@ -111,6 +111,28 @@ const extensionConfig = { ], }, }, + { + test: /\.js$/, + loader: "string-replace-loader", + options: { + multiple: [ + // firebase-tools/node_modules/superstatic/lib/utils/patterns.js + // Stub out the optional RE2 dependency + // TODO: copy the dependency into dist instead of removing them via search/replace. + { + search: 'RE2 = require("re2");', + replace: 'RE2 = null;' + }, + // firebase-tools/node_modules/superstatic/lib/middleware/index.js + // Stub out these runtime requirements + // TODO: copy the dependencies into dist instead of removing them via search/replace. + { + search: 'const mware = require("./" + _.kebabCase(name))(spec, config);', + replace: 'return "";' + } + ], + } + } ], }, plugins: [ diff --git a/firebase-vscode/webviews/SidebarApp.tsx b/firebase-vscode/webviews/SidebarApp.tsx index 2fd8d936034..e7d4dd8a5eb 100644 --- a/firebase-vscode/webviews/SidebarApp.tsx +++ b/firebase-vscode/webviews/SidebarApp.tsx @@ -9,6 +9,7 @@ import { ServiceAccountUser } from "../common/types"; import { DeployPanel } from "./components/DeployPanel"; import { HostingInitState, DeployState } from "./webview-types"; import { ChannelWithId } from "./messaging/types"; +import { EmulatorPanel } from "./components/EmulatorPanel"; import { webLogger } from "./globals/web-logger"; import { InitFirebasePanel } from "./components/InitPanel"; @@ -161,7 +162,14 @@ export function SidebarApp() { setHostingInitState={setHostingInitState} /> )} - + { + // Only load the emulator panel if we have a user, firebase.json and this isn't Monospace + // The user login requirement can be removed in the future but the panel will have to + // be restricted to full-offline emulation only. + !!user && !!firebaseJson && !env?.isMonospace && ( + + ) + } { // Only load quickstart panel if this isn't a Monospace workspace !env?.isMonospace && (