From ca6eac2a89651b5691ec742fddd67b85756fdb10 Mon Sep 17 00:00:00 2001 From: James Daniels Date: Tue, 14 Sep 2021 19:42:34 -0400 Subject: [PATCH] feat(schematics): adding multi-user support. (#2958) Utilize firebase-tools multi user support to allow an account picker on `ng add` --- src/schematics/deploy/actions.jasmine.ts | 7 ++- src/schematics/deploy/actions.ts | 18 ++++++-- src/schematics/interfaces.ts | 4 +- src/schematics/setup/index.ts | 24 ++++++---- src/schematics/setup/prompts.ts | 59 ++++++++++++++---------- 5 files changed, 72 insertions(+), 40 deletions(-) diff --git a/src/schematics/deploy/actions.jasmine.ts b/src/schematics/deploy/actions.jasmine.ts index c228d58c3..80a88fee9 100644 --- a/src/schematics/deploy/actions.jasmine.ts +++ b/src/schematics/deploy/actions.jasmine.ts @@ -21,8 +21,10 @@ const SERVER_BUILD_TARGET: BuildTarget = { name: `${PROJECT}:server:production` }; -const login = () => Promise.resolve(); +const login = () => Promise.resolve({ user: { email: 'foo@bar.baz' }}); login.list = () => Promise.resolve([{ user: { email: 'foo@bar.baz' }}]); +login.add = () => Promise.resolve([{ user: { email: 'foo@bar.baz' }}]); +login.use = () => Promise.resolve('foo@bar.baz'); const initMocks = () => { fsHost = { @@ -104,7 +106,7 @@ describe('Deploy Angular apps', () => { beforeEach(() => initMocks()); it('should call login', async () => { - const spy = spyOn(firebaseMock, 'login'); + const spy = spyOn(firebaseMock, 'login').and.resolveTo({ email: 'foo@bar.baz' }); await deploy( firebaseMock, context, STATIC_BUILD_TARGET, undefined, undefined, undefined, { projectId: FIREBASE_PROJECT, preview: false } @@ -149,6 +151,7 @@ describe('Deploy Angular apps', () => { only: 'hosting:' + PROJECT, token: FIREBASE_TOKEN, nonInteractive: true, + projectRoot: 'cwd', }); }); diff --git a/src/schematics/deploy/actions.ts b/src/schematics/deploy/actions.ts index 31294da46..8e814dfc3 100644 --- a/src/schematics/deploy/actions.ts +++ b/src/schematics/deploy/actions.ts @@ -78,7 +78,8 @@ const deployToHosting = async ( host: DEFAULT_EMULATOR_HOST, // tslint:disable-next-line:no-non-null-assertion targets: [`hosting:${context.target!.project}`], - nonInteractive: true + nonInteractive: true, + projectRoot: workspaceRoot, }); const { deployProject } = await inquirer.prompt({ @@ -97,6 +98,7 @@ const deployToHosting = async ( cwd: workspaceRoot, token: firebaseToken, nonInteractive: true, + projectRoot: workspaceRoot, }); }; @@ -228,7 +230,8 @@ export const deployToFunction = async ( port: DEFAULT_EMULATOR_PORT, host: DEFAULT_EMULATOR_HOST, targets: [`hosting:${project}`, `functions:${functionName}`], - nonInteractive: true + nonInteractive: true, + projectRoot: workspaceRoot, }); const { deployProject} = await inquirer.prompt({ @@ -245,6 +248,7 @@ export const deployToFunction = async ( cwd: workspaceRoot, token: firebaseToken, nonInteractive: true, + projectRoot: workspaceRoot, }); }; @@ -352,6 +356,7 @@ export const deployToCloudRun = async ( cwd: workspaceRoot, token: firebaseToken, nonInteractive: true, + projectRoot: workspaceRoot, }); }; @@ -367,8 +372,8 @@ export default async function deploy( ) { if (!firebaseToken) { await firebaseTools.login(); - const users = await firebaseTools.login.list(); - console.log(`Logged into Firebase as ${users.map(it => it.user.email).join(', ')}.`); + const user = await firebaseTools.login({ projectRoot: context.workspaceRoot }); + console.log(`Logged into Firebase as ${user.email}.`); } if (prerenderBuildTarget) { @@ -405,7 +410,10 @@ export default async function deploy( } try { - await firebaseTools.use(firebaseProject, { project: firebaseProject }); + await firebaseTools.use(firebaseProject, { + project: firebaseProject, + projectRoot: context.workspaceRoot, + }); } catch (e) { throw new Error(`Cannot select firebase project '${firebaseProject}'`); } diff --git a/src/schematics/interfaces.ts b/src/schematics/interfaces.ts index 9ec5014b3..1778fd351 100644 --- a/src/schematics/interfaces.ts +++ b/src/schematics/interfaces.ts @@ -120,7 +120,9 @@ export interface FirebaseTools { login: { list(): Promise<{user: Record}[]>; - } & (() => Promise); + add(): Promise>; + use(email: string, options?: {}): Promise; + } & ((options?: {}) => Promise>); deploy(config: FirebaseDeployConfig): Promise; diff --git a/src/schematics/setup/index.ts b/src/schematics/setup/index.ts index 4b3c5c6c9..c9061d0cb 100644 --- a/src/schematics/setup/index.ts +++ b/src/schematics/setup/index.ts @@ -3,7 +3,7 @@ import { getWorkspace, getProject, getFirebaseProjectNameFromHost, addEnvironmentEntry, addToNgModule, addIgnoreFiles, addFixesToServer } from '../utils'; -import { projectTypePrompt, appPrompt, sitePrompt, projectPrompt, featuresPrompt } from './prompts'; +import { projectTypePrompt, appPrompt, sitePrompt, projectPrompt, featuresPrompt, userPrompt } from './prompts'; import { setupUniversalDeployment } from './ssr'; import { setupStaticDeployment } from './static'; import { @@ -11,6 +11,8 @@ import { FEATURES, PROJECT_TYPE } from '../interfaces'; import { getFirebaseTools } from '../firebaseTools'; +import { writeFileSync } from 'fs'; +import { join } from 'path'; export const setupProject = async (tree: Tree, context: SchematicContext, features: FEATURES[], config: DeployOptions & { @@ -109,21 +111,27 @@ ${Object.entries(config.sdkConfig).reduce( export const ngAddSetupProject = ( options: DeployOptions ) => async (host: Tree, context: SchematicContext) => { + + // TODO is there a public API for this? + const projectRoot: string = (host as any)._backend._root; + const features = await featuresPrompt(); if (features.length > 0) { const firebaseTools = await getFirebaseTools(); - await firebaseTools.login(); - const users = await firebaseTools.login.list(); - console.log(`Logged into Firebase as ${users.map(it => it.user.email).join(', ')}.`); + // Add the firebase files if they don't exist already so login.use works + if (!host.exists('/firebase.json')) { writeFileSync(join(projectRoot, 'firebase.json'), '{}'); } + + const user = await userPrompt({ projectRoot }); + await firebaseTools.login.use(user.email, { projectRoot }); const { project: ngProject, projectName: ngProjectName } = getProject(options, host); const [ defaultProjectName ] = getFirebaseProjectNameFromHost(host, ngProjectName); - const firebaseProject = await projectPrompt(defaultProjectName); + const firebaseProject = await projectPrompt(defaultProjectName, { projectRoot }); let hosting = { projectType: PROJECT_TYPE.Static, prerender: false }; let firebaseHostingSite: FirebaseHostingSite|undefined; @@ -132,7 +140,7 @@ export const ngAddSetupProject = ( // TODO read existing settings from angular.json, if available const results = await projectTypePrompt(ngProject, ngProjectName); hosting = { ...hosting, ...results }; - firebaseHostingSite = await sitePrompt(firebaseProject); + firebaseHostingSite = await sitePrompt(firebaseProject, { projectRoot }); } let firebaseApp: FirebaseApp|undefined; @@ -141,9 +149,9 @@ export const ngAddSetupProject = ( if (features.find(it => it !== FEATURES.Hosting)) { const defaultAppId = firebaseHostingSite?.appId; - firebaseApp = await appPrompt(firebaseProject, defaultAppId); + firebaseApp = await appPrompt(firebaseProject, defaultAppId, { projectRoot }); - const result = await firebaseTools.apps.sdkconfig('web', firebaseApp.appId, { nonInteractive: true }); + const result = await firebaseTools.apps.sdkconfig('web', firebaseApp.appId, { nonInteractive: true, projectRoot }); sdkConfig = result.sdkConfig; } diff --git a/src/schematics/setup/prompts.ts b/src/schematics/setup/prompts.ts index 8e993bc0d..ee7c34046 100644 --- a/src/schematics/setup/prompts.ts +++ b/src/schematics/setup/prompts.ts @@ -118,9 +118,35 @@ export const featuresPrompt = async (): Promise => { return features; }; -export const projectPrompt = async (defaultProject?: string) => { +export const userPrompt = async (options: {}): Promise> => { const firebaseTools = await getFirebaseTools(); - const projects = firebaseTools.projects.list({}); + const users = await firebaseTools.login.list(); + if (!users || users.length === 0) { + await firebaseTools.login(); // first login isn't returning anything of value + const user = await firebaseTools.login(options); + return user; + } else { + const defaultUser = await firebaseTools.login(options); + const choices = users.map(({user}) => ({ name: user.email, value: user })); + const newChoice = { name: '[Login in with another account]', value: NEW_OPTION }; + const { user } = await inquirer.prompt({ + type: 'list', + name: 'user', + choices: [newChoice].concat(choices as any), // TODO types + message: 'Which Firebase account would you like to use?', + default: choices.find(it => it.value.email === defaultUser.email)?.value, + }); + if (user === NEW_OPTION) { + const { user } = await firebaseTools.login.add(); + return user; + } + return user; + } +}; + +export const projectPrompt = async (defaultProject: string|undefined, options: {}) => { + const firebaseTools = await getFirebaseTools(); + const projects = firebaseTools.projects.list(options); const { projectId } = await autocomplete({ type: 'autocomplete', name: 'projectId', @@ -140,15 +166,15 @@ export const projectPrompt = async (defaultProject?: string) => { message: 'What would you like to call your project?', default: projectId, }); - return await firebaseTools.projects.create(projectId, { displayName, nonInteractive: true }); + return await firebaseTools.projects.create(projectId, { ...options, displayName, nonInteractive: true }); } // tslint:disable-next-line:no-non-null-assertion return (await projects).find(it => it.projectId === projectId)!; }; -export const appPrompt = async ({ projectId: project }: FirebaseProject, defaultAppId: string|undefined) => { +export const appPrompt = async ({ projectId: project }: FirebaseProject, defaultAppId: string|undefined, options: {}) => { const firebaseTools = await getFirebaseTools(); - const apps = firebaseTools.apps.list('web', { project }); + const apps = firebaseTools.apps.list('web', { ...options, project }); const { appId } = await autocomplete({ type: 'autocomplete', name: 'appId', @@ -162,18 +188,15 @@ export const appPrompt = async ({ projectId: project }: FirebaseProject, default name: 'displayName', message: 'What would you like to call your app?', }); - return await firebaseTools.apps.create('web', displayName, { nonInteractive: true, project }); + return await firebaseTools.apps.create('web', displayName, { ...options, nonInteractive: true, project }); } // tslint:disable-next-line:no-non-null-assertion return (await apps).find(it => shortAppId(it) === appId)!; }; -export const sitePrompt = async ({ projectId: project }: FirebaseProject) => { +export const sitePrompt = async ({ projectId: project }: FirebaseProject, options: {}) => { const firebaseTools = await getFirebaseTools(); - if (!firebaseTools.hosting.sites) { - return undefined; - } - const sites = firebaseTools.hosting.sites.list({ project }).then(it => { + const sites = firebaseTools.hosting.sites.list({ ...options, project }).then(it => { if (it.sites.length === 0) { // newly created projects don't return their default site, stub one return [{ @@ -199,24 +222,12 @@ export const sitePrompt = async ({ projectId: project }: FirebaseProject) => { name: 'subdomain', message: 'Please provide an unique, URL-friendly id for the site (.web.app):', }); - return await firebaseTools.hosting.sites.create(subdomain, { nonInteractive: true, project }); + return await firebaseTools.hosting.sites.create(subdomain, { ...options, nonInteractive: true, project }); } // tslint:disable-next-line:no-non-null-assertion return (await sites).find(it => shortSiteName(it) === siteName)!; }; -export const prerenderPrompt = (project: WorkspaceProject, prerender: boolean): Promise<{ projectType: PROJECT_TYPE }> => { - if (isUniversalApp(project)) { - return inquirer.prompt({ - type: 'prompt', - name: 'prerender', - message: 'We detected an Angular Universal project. How would you like to render server-side content?', - default: true - }); - } - return Promise.resolve({ projectType: PROJECT_TYPE.Static }); -}; - export const projectTypePrompt = async (project: WorkspaceProject, name: string) => { let prerender = false; let nodeVersion: string|undefined;