diff --git a/firebase-vscode/common/messaging/protocol.ts b/firebase-vscode/common/messaging/protocol.ts index 837e81db46b..c6cd978cb67 100644 --- a/firebase-vscode/common/messaging/protocol.ts +++ b/firebase-vscode/common/messaging/protocol.ts @@ -113,6 +113,9 @@ export interface WebviewToExtensionParamsMap { // Initialize "result" tab. getDataConnectResults: void; + + // execute terminal tasks + executeLogin: void; } export interface DataConnectResults { diff --git a/firebase-vscode/package.json b/firebase-vscode/package.json index 6376d3b52cb..c8ccf231122 100644 --- a/firebase-vscode/package.json +++ b/firebase-vscode/package.json @@ -55,12 +55,12 @@ "properties": { "firebase.debug": { "type": "boolean", - "default": false, + "default": true, "description": "Enable writing debug-level messages to the file provided in firebase.debugLogPath (requires restart)" }, "firebase.debugLogPath": { "type": "string", - "default": "", + "default": "/tmp/firebase-plugin.log", "description": "If firebase.debug is true, appends debug-level messages to the provided file (requires restart)" }, "firebase.npmPath": { diff --git a/firebase-vscode/scripts/swap-pkg.js b/firebase-vscode/scripts/swap-pkg.js index 8a2ad521664..4c428799793 100644 --- a/firebase-vscode/scripts/swap-pkg.js +++ b/firebase-vscode/scripts/swap-pkg.js @@ -2,42 +2,10 @@ const { writeFileSync } = require("fs"); const path = require("path"); const pkg = require(path.join(__dirname, "../package.json")); -// Swaps package.json config as appropriate for packaging for -// Monospace or VSCE marketplace. -// TODO(chholland): Don't overwrite the real package.json file and -// create a generated one in dist/ - redo .vscodeignore to package -// dist/ - -let target = "vsce"; - -process.argv.forEach((arg) => { - if (arg === "vsce" || arg === "monospace") { - target = arg; - } -}); - -if (target === "vsce") { - delete pkg.extensionDependencies; - console.log( - "Removing google.monospace extensionDependency for VSCE packaging." - ); - pkg.contributes.configuration.properties["firebase.debug"].default = false; - pkg.contributes.configuration.properties["firebase.debugLogPath"].default = - ""; - console.log("Setting default debug log settings to off for VSCE packaging."); -} else if (target === "monospace") { - pkg.extensionDependencies = ["google.monospace"]; - console.log( - "Adding google.monospace extensionDependency for Monospace packaging." - ); - pkg.contributes.configuration.properties["firebase.debug"].default = true; - pkg.contributes.configuration.properties["firebase.debugLogPath"].default = - "/tmp/firebase-plugin.log"; - console.log( - "Setting default debug log settings to on for Monospace packaging." - ); -} +pkg.contributes.configuration.properties["firebase.debug"].default = true; +pkg.contributes.configuration.properties["firebase.debugLogPath"].default = + "/tmp/firebase-plugin.log"; writeFileSync( path.join(__dirname, "../package.json"), diff --git a/firebase-vscode/src/cli.ts b/firebase-vscode/src/cli.ts index 3a008fed6cc..433c4f64c15 100644 --- a/firebase-vscode/src/cli.ts +++ b/firebase-vscode/src/cli.ts @@ -100,9 +100,11 @@ async function getServiceAccount() { */ async function requireAuthWrapper(showError: boolean = true): Promise { // Try to get global default from configstore. For some reason this is - // often overwritten when restarting the extension. pluginLogger.debug("requireAuthWrapper"); let account = getGlobalDefaultAccount(); + // often overwritten when restarting the extension. + console.log("HAROLD", account); + if (!account) { // If nothing in configstore top level, grab the first "additionalAccount" const accounts = getAllAccounts(); @@ -136,6 +138,7 @@ async function requireAuthWrapper(showError: boolean = true): Promise { // user // Priority 3: Google login account exists and there is no selected user // Clear service account access token from memory in apiv2. + console.log("HAROLD2", account); setAccessToken(); await requireAuth({ ...commandOptions, ...account }); return true; @@ -153,7 +156,7 @@ async function requireAuthWrapper(showError: boolean = true): Promise { // "error". Usually set on user-triggered actions such as // init hosting and deploy. pluginLogger.error( - `requireAuth error: ${e.original?.message || e.message}` + `requireAuth error: ${e.original?.message || e.message}`, ); vscode.window.showErrorMessage("Not logged in", { modal: true, @@ -164,7 +167,7 @@ async function requireAuthWrapper(showError: boolean = true): Promise { // but we should log it for debugging purposes. pluginLogger.debug( "requireAuth error output: ", - e.original?.message || e.message + e.original?.message || e.message, ); } return false; diff --git a/firebase-vscode/src/core/project.ts b/firebase-vscode/src/core/project.ts index d14c5fe7294..535975ac102 100644 --- a/firebase-vscode/src/core/project.ts +++ b/firebase-vscode/src/core/project.ts @@ -6,14 +6,13 @@ import { FirebaseProjectMetadata } from "../types/project"; import { currentUser, isServiceAccount } from "./user"; import { listProjects } from "../cli"; import { pluginLogger } from "../logger-wrapper"; -import { selectProjectInMonospace } from "../../../src/monospace"; import { currentOptions } from "../options"; import { globalSignal } from "../utils/globals"; import { firstWhereDefined } from "../utils/signal"; /** Available projects */ export const projects = globalSignal>( - {} + {}, ); /** Currently selected project ID */ @@ -22,7 +21,7 @@ export const currentProjectId = globalSignal(""); const userScopedProjects = computed( () => { return projects.value[currentUser.value?.email ?? ""]; - } + }, ); /** Gets the currently selected project, fallback to first default project in RC file */ @@ -41,7 +40,7 @@ export const currentProject = computed( } return userScopedProjects.value?.find((p) => p.projectId === wantProjectId); - } + }, ); export function registerProject(broker: ExtensionBrokerImpl): Disposable { @@ -90,28 +89,17 @@ export function registerProject(broker: ExtensionBrokerImpl): Disposable { if (process.env.MONOSPACE_ENV) { pluginLogger.debug( "selectProject: found MONOSPACE_ENV, " + - "prompting user using external flow" + "prompting user using CLI", ); - /** - * Monospace case: use Monospace flow - */ - const monospaceExtension = - vscode.extensions.getExtension("google.monospace"); - process.env.MONOSPACE_DAEMON_PORT = - monospaceExtension.exports.getMonospaceDaemonPort(); - try { - const projectId = await selectProjectInMonospace({ - projectRoot: currentOptions.value.cwd, - project: undefined, - isVSCE: true, - }); - - if (projectId) { - currentProjectId.value = projectId; - } - } catch (e) { - pluginLogger.error(e); - } + try { + const projects = firstWhereDefined(userScopedProjects); + + currentProjectId.value = + (await _promptUserForProject(projects)) ?? currentProjectId.value; + } catch (e) { + vscode.window.showErrorMessage(e.message); + } + } else if (isServiceAccount.value) { return; } else { @@ -124,11 +112,11 @@ export function registerProject(broker: ExtensionBrokerImpl): Disposable { vscode.window.showErrorMessage(e.message); } } - } + }, ); const sub6 = broker.on("selectProject", () => - vscode.commands.executeCommand("firebase.selectProject") + vscode.commands.executeCommand("firebase.selectProject"), ); return vscode.Disposable.from( @@ -138,7 +126,7 @@ export function registerProject(broker: ExtensionBrokerImpl): Disposable { { dispose: sub3 }, { dispose: sub4 }, { dispose: sub5 }, - { dispose: sub6 } + { dispose: sub6 }, ); } @@ -149,7 +137,7 @@ export function registerProject(broker: ExtensionBrokerImpl): Disposable { */ export async function _promptUserForProject( projects: Thenable, - token?: vscode.CancellationToken + token?: vscode.CancellationToken, ): Promise { const items = projects.then((projects) => { return projects.map((p) => ({ diff --git a/firebase-vscode/src/core/user.ts b/firebase-vscode/src/core/user.ts index ad4c1773981..4c5f2349e07 100644 --- a/firebase-vscode/src/core/user.ts +++ b/firebase-vscode/src/core/user.ts @@ -23,7 +23,16 @@ export const isServiceAccount = computed(() => { return (currentUser.value as ServiceAccountUser)?.type === "service_account"; }); +export async function checkLogin() { + const accounts = await getAccounts(); + users.value = accounts.reduce( + (cumm, curr) => ({ ...cumm, [curr.user.email]: curr.user }), + {} + ); +} + export function registerUser(broker: ExtensionBrokerImpl): Disposable { + const sub1 = effect(() => { broker.send("notifyUsers", { users: Object.values(users.value) }); }); @@ -33,11 +42,7 @@ export function registerUser(broker: ExtensionBrokerImpl): Disposable { }); const sub3 = broker.on("getInitialData", async () => { - const accounts = await getAccounts(); - users.value = accounts.reduce( - (cumm, curr) => ({ ...cumm, [curr.user.email]: curr.user }), - {} - ); + checkLogin(); }); const sub4 = broker.on("addUser", async () => { diff --git a/firebase-vscode/src/data-connect/index.ts b/firebase-vscode/src/data-connect/index.ts index b8755760806..c664a757677 100644 --- a/firebase-vscode/src/data-connect/index.ts +++ b/firebase-vscode/src/data-connect/index.ts @@ -28,6 +28,7 @@ import { runDataConnectCompiler } from "./core-compiler"; import { Result } from "../result"; import { runEmulatorIssuesStream } from "./emulator-stream"; import { LanguageClient } from "vscode-languageclient/node"; +import { registerTerminalTasks } from "./terminal"; class CodeActionsProvider implements vscode.CodeActionProvider { constructor( @@ -221,6 +222,7 @@ export function registerFdc( registerAdHoc(), registerConnectors(context, broker, fdcService), registerFdcDeploy(broker), + registerTerminalTasks(broker), operationCodeLensProvider, vscode.languages.registerCodeLensProvider( // **Hack**: For testing purposes, enable code lenses on all graphql files diff --git a/firebase-vscode/src/data-connect/terminal.ts b/firebase-vscode/src/data-connect/terminal.ts index 2411042d2af..b45adcc7d8b 100644 --- a/firebase-vscode/src/data-connect/terminal.ts +++ b/firebase-vscode/src/data-connect/terminal.ts @@ -1,6 +1,7 @@ import { TerminalOptions } from "vscode"; -import * as vscode from "vscode"; - +import { ExtensionBrokerImpl } from "../extension-broker"; +import vscode, { Disposable } from "vscode"; +import { checkLogin } from "../core/user"; const environmentVariables = {}; const terminalOptions: TerminalOptions = { @@ -17,3 +18,52 @@ export function runCommand(command: string) { terminal.show(); terminal.sendText(command); } + +export function runTerminalTask( + taskName: string, + command: string, +): Promise { + const type = "firebase-" + Date.now(); + return new Promise(async (resolve, reject) => { + vscode.tasks.onDidEndTaskProcess(async (e) => { + if (e.execution.task.definition.type === type) { + e.execution.terminate(); + + if (e.exitCode === 0) { + resolve(`Successfully executed ${taskName} with command: ${command}`); + } else { + reject( + new Error(`Failed to execute ${taskName} with command: ${command}`), + ); + } + } + }); + vscode.tasks.executeTask( + new vscode.Task( + { type }, + vscode.TaskScope.Workspace, + taskName, + "firebase", + new vscode.ShellExecution(command), + ), + ); + }); +} + +export function registerTerminalTasks(broker: ExtensionBrokerImpl): Disposable { + const loginTaskBroker = broker.on("executeLogin", () => { + runTerminalTask("firebase login", "firebase login --no-localhost").then(() => { + checkLogin(); + }); + }); + + return Disposable.from( + { dispose: loginTaskBroker }, + vscode.commands.registerCommand( + "firebase.dataConnect.runTerminalTask", + (taskName, command) => { + runTerminalTask(taskName, command); + }, + ), + ); +} diff --git a/firebase-vscode/webviews/SidebarApp.tsx b/firebase-vscode/webviews/SidebarApp.tsx index 55704eb5a6c..33e9dfe91b8 100644 --- a/firebase-vscode/webviews/SidebarApp.tsx +++ b/firebase-vscode/webviews/SidebarApp.tsx @@ -160,7 +160,8 @@ function SidebarContent(props: { {!!user && ( )} - {hostingInitState === "success" && + { // TODO: disable hosting completely + /* {hostingInitState === "success" && !!user && !!projectId && env?.isMonospace && ( @@ -184,7 +185,7 @@ function SidebarContent(props: { hostingInitState={hostingInitState} setHostingInitState={setHostingInitState} /> - )} + )} */} { // disable emulator panel for now, as we have an individual emulator panel in the FDC section } diff --git a/firebase-vscode/webviews/components/AccountSection.tsx b/firebase-vscode/webviews/components/AccountSection.tsx index 79726e1061a..82f798d7ba0 100644 --- a/firebase-vscode/webviews/components/AccountSection.tsx +++ b/firebase-vscode/webviews/components/AccountSection.tsx @@ -34,9 +34,11 @@ export function AccountSection({ if (usersLoaded && (!allUsers.length || !user)) { // Users loaded but no user was found if (isMonospace) { - // Monospace: this is an error, should have found a workspace - // service account - currentUserElement = TEXT.MONOSPACE_LOGIN_FAIL; + currentUserElement = ( + broker.send("executeLogin")}> + {TEXT.GOOGLE_SIGN_IN} + + ); } else { // VS Code: prompt user to log in with Google account currentUserElement = ( @@ -49,7 +51,11 @@ export function AccountSection({ // Users loaded, at least one user was found if (user.type === "service_account") { if (isMonospace) { - currentUserElement = TEXT.MONOSPACE_LOGGED_IN; + currentUserElement = ( + broker.send("executeLogin")}> + {TEXT.GOOGLE_SIGN_IN} + + ); } else { currentUserElement = TEXT.VSCE_SERVICE_ACCOUNT_LOGGED_IN; } @@ -63,13 +69,6 @@ export function AccountSection({ {currentUserElement} ); - if (user?.type === "service_account" && isMonospace) { - userBoxElement = ( - - ); - } return (
{userBoxElement} diff --git a/src/monospace/index.ts b/src/monospace/index.ts deleted file mode 100644 index 0b033f4679b..00000000000 --- a/src/monospace/index.ts +++ /dev/null @@ -1,158 +0,0 @@ -import fetch from "node-fetch"; - -import { FirebaseError } from "../error"; -import { logger } from "../logger"; -import { loadRC } from "../rc"; - -import type { - GetInitFirebaseResponse, - InitFirebaseResponse, - SetupMonospaceOptions, -} from "./interfaces"; - -const POLL_USER_RESPONSE_MILLIS = 2000; - -/** - * Integrate Firebase Plugin with Monospace’s service Account Authentication - * - * @return null if no project was authorized - * @return string if a project was authorized and isVSCE is true - * @return void if a project was authorized and isVSCE is falsy, creating - * `.firebaserc` with authorized project using the default alias - */ -export async function selectProjectInMonospace({ - projectRoot, - project, - isVSCE, -}: SetupMonospaceOptions): Promise { - const initFirebaseResponse = await initFirebase(project); - - if (initFirebaseResponse.success === false) { - throw new Error(String(initFirebaseResponse.error)); - } - - const { rid } = initFirebaseResponse; - - const authorizedProject = await pollAuthorizedProject(rid); - - if (!authorizedProject) return null; - - if (isVSCE) return authorizedProject; - - if (projectRoot) createFirebaseRc(projectRoot, authorizedProject); -} - -/** - * Since `initFirebase` pops up a dialog and waits for user response, it might - * take some time for the response to become available. Here we poll for user's - * response. - */ -async function pollAuthorizedProject(rid: string): Promise { - const getInitFirebaseRes = await getInitFirebaseResponse(rid); - - // If the user authorizes a project, `userResponse` will be available - if ("userResponse" in getInitFirebaseRes) { - if (getInitFirebaseRes.userResponse.success) { - return getInitFirebaseRes.userResponse.projectId; - } - - return null; - } - - const { error } = getInitFirebaseRes; - - // Wait response: User hasn’t finished the interaction yet - if (error === "WAITING_FOR_RESPONSE") { - // wait and call back - await new Promise((res) => setTimeout(res, POLL_USER_RESPONSE_MILLIS)); - - // TODO: decide how long to ultimately wait before declaring - // that the user is never going to respond. - - return pollAuthorizedProject(rid); - } - - // TODO: Review this. It's not being reached as the process exits before a new - // call is made - - // Error response: User canceled without authorizing any project - if (error === "USER_CANCELED") { - // The user hasn’t authorized any project. - // Display appropriate error message. - throw new FirebaseError("User canceled without authorizing any project"); - } - - throw new FirebaseError(`Unhandled /get-init-firebase-response error`, { - original: new Error(error), - }); -} - -/** - * Make call to init Firebase, get request id (rid) or error - */ -async function initFirebase(project?: string): Promise { - const port = getMonospaceDaemonPort(); - if (!port) throw new FirebaseError("Undefined MONOSPACE_DAEMON_PORT"); - - const initFirebaseURL = new URL(`http://localhost:${port}/init-firebase`); - - if (project) { - initFirebaseURL.searchParams.set("known_project", project); - } - - const initFirebaseRes = await fetch(initFirebaseURL.toString(), { - method: "POST", - headers: { - "Content-Type": "application/json", - }, - }); - - const initFirebaseResponse = (await initFirebaseRes.json()) as InitFirebaseResponse; - - return initFirebaseResponse; -} - -/** - * Get response from the user - authorized project or error - */ -async function getInitFirebaseResponse(rid: string): Promise { - const port = getMonospaceDaemonPort(); - if (!port) throw new FirebaseError("Undefined MONOSPACE_DAEMON_PORT"); - - const getInitFirebaseRes = await fetch( - `http://localhost:${port}/get-init-firebase-response?rid=${rid}`, - ); - - const getInitFirebaseJson = (await getInitFirebaseRes.json()) as GetInitFirebaseResponse; - - logger.debug(`/get-init-firebase-response?rid=${rid} response:`); - logger.debug(getInitFirebaseJson); - - return getInitFirebaseJson; -} - -/** - * Create a .firebaserc in the project's root with the authorized project - * as the default project - */ -function createFirebaseRc(projectDir: string, authorizedProject: string): boolean { - const firebaseRc = loadRC({ cwd: projectDir }); - - firebaseRc.addProjectAlias("default", authorizedProject); - - return firebaseRc.save(); -} - -/** - * Whether this is a Monospace environment - */ -export function isMonospaceEnv(): boolean { - return getMonospaceDaemonPort() !== undefined; -} - -/** - * @return process.env.MONOSPACE_DAEMON_PORT - */ -function getMonospaceDaemonPort(): string | undefined { - return process.env.MONOSPACE_DAEMON_PORT; -} diff --git a/src/monospace/interfaces.ts b/src/monospace/interfaces.ts deleted file mode 100644 index c57bb4079e6..00000000000 --- a/src/monospace/interfaces.ts +++ /dev/null @@ -1,33 +0,0 @@ -import type { Options } from "../options"; - -export type SetupMonospaceOptions = { - projectRoot: Options["projectRoot"]; - project: Options["project"]; - isVSCE: Options["isVSCE"]; -}; - -export type GetInitFirebaseResponse = - | { - success: true; - userResponse: { - success: true; - projectId: string; - }; - } - | { - success: true; - userResponse: { - success: false; - }; - } - | { - success: false; - error: "WAITING_FOR_RESPONSE" | "USER_CANCELED" | string; // TODO: define all errors - }; - -export type InitFirebaseResponse = - | { - success: true; - rid: string; - } - | { success: false; error: "NOT_INITIALIZED" | unknown }; // TODO: define all errors diff --git a/src/requireAuth.ts b/src/requireAuth.ts index 93757e1f3a0..293d6924821 100644 --- a/src/requireAuth.ts +++ b/src/requireAuth.ts @@ -9,7 +9,6 @@ import * as utils from "./utils"; import * as scopes from "./scopes"; import { Tokens, User } from "./types/auth"; import { setRefreshToken, setActiveAccount } from "./auth"; -import { selectProjectInMonospace, isMonospaceEnv } from "./monospace"; import type { Options } from "./options"; const AUTH_ERROR_MESSAGE = `Command requires authentication, please run ${clc.bold( @@ -51,14 +50,6 @@ async function autoAuth(options: Options, authScopes: string[]): Promise