Skip to content

Commit ca6eac2

Browse files
authored
feat(schematics): adding multi-user support. (#2958)
Utilize firebase-tools multi user support to allow an account picker on `ng add`
1 parent 73bde38 commit ca6eac2

File tree

5 files changed

+72
-40
lines changed

5 files changed

+72
-40
lines changed

src/schematics/deploy/actions.jasmine.ts

+5-2
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,10 @@ const SERVER_BUILD_TARGET: BuildTarget = {
2121
name: `${PROJECT}:server:production`
2222
};
2323

24-
const login = () => Promise.resolve();
24+
const login = () => Promise.resolve({ user: { email: '[email protected]' }});
2525
login.list = () => Promise.resolve([{ user: { email: '[email protected]' }}]);
26+
login.add = () => Promise.resolve([{ user: { email: '[email protected]' }}]);
27+
login.use = () => Promise.resolve('[email protected]');
2628

2729
const initMocks = () => {
2830
fsHost = {
@@ -104,7 +106,7 @@ describe('Deploy Angular apps', () => {
104106
beforeEach(() => initMocks());
105107

106108
it('should call login', async () => {
107-
const spy = spyOn(firebaseMock, 'login');
109+
const spy = spyOn(firebaseMock, 'login').and.resolveTo({ email: '[email protected]' });
108110
await deploy(
109111
firebaseMock, context, STATIC_BUILD_TARGET, undefined,
110112
undefined, undefined, { projectId: FIREBASE_PROJECT, preview: false }
@@ -149,6 +151,7 @@ describe('Deploy Angular apps', () => {
149151
only: 'hosting:' + PROJECT,
150152
token: FIREBASE_TOKEN,
151153
nonInteractive: true,
154+
projectRoot: 'cwd',
152155
});
153156
});
154157

src/schematics/deploy/actions.ts

+13-5
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,8 @@ const deployToHosting = async (
7878
host: DEFAULT_EMULATOR_HOST,
7979
// tslint:disable-next-line:no-non-null-assertion
8080
targets: [`hosting:${context.target!.project}`],
81-
nonInteractive: true
81+
nonInteractive: true,
82+
projectRoot: workspaceRoot,
8283
});
8384

8485
const { deployProject } = await inquirer.prompt({
@@ -97,6 +98,7 @@ const deployToHosting = async (
9798
cwd: workspaceRoot,
9899
token: firebaseToken,
99100
nonInteractive: true,
101+
projectRoot: workspaceRoot,
100102
});
101103

102104
};
@@ -228,7 +230,8 @@ export const deployToFunction = async (
228230
port: DEFAULT_EMULATOR_PORT,
229231
host: DEFAULT_EMULATOR_HOST,
230232
targets: [`hosting:${project}`, `functions:${functionName}`],
231-
nonInteractive: true
233+
nonInteractive: true,
234+
projectRoot: workspaceRoot,
232235
});
233236

234237
const { deployProject} = await inquirer.prompt({
@@ -245,6 +248,7 @@ export const deployToFunction = async (
245248
cwd: workspaceRoot,
246249
token: firebaseToken,
247250
nonInteractive: true,
251+
projectRoot: workspaceRoot,
248252
});
249253

250254
};
@@ -352,6 +356,7 @@ export const deployToCloudRun = async (
352356
cwd: workspaceRoot,
353357
token: firebaseToken,
354358
nonInteractive: true,
359+
projectRoot: workspaceRoot,
355360
});
356361
};
357362

@@ -367,8 +372,8 @@ export default async function deploy(
367372
) {
368373
if (!firebaseToken) {
369374
await firebaseTools.login();
370-
const users = await firebaseTools.login.list();
371-
console.log(`Logged into Firebase as ${users.map(it => it.user.email).join(', ')}.`);
375+
const user = await firebaseTools.login({ projectRoot: context.workspaceRoot });
376+
console.log(`Logged into Firebase as ${user.email}.`);
372377
}
373378

374379
if (prerenderBuildTarget) {
@@ -405,7 +410,10 @@ export default async function deploy(
405410
}
406411

407412
try {
408-
await firebaseTools.use(firebaseProject, { project: firebaseProject });
413+
await firebaseTools.use(firebaseProject, {
414+
project: firebaseProject,
415+
projectRoot: context.workspaceRoot,
416+
});
409417
} catch (e) {
410418
throw new Error(`Cannot select firebase project '${firebaseProject}'`);
411419
}

src/schematics/interfaces.ts

+3-1
Original file line numberDiff line numberDiff line change
@@ -120,7 +120,9 @@ export interface FirebaseTools {
120120

121121
login: {
122122
list(): Promise<{user: Record<string, any>}[]>;
123-
} & (() => Promise<void>);
123+
add(): Promise<Record<string, any>>;
124+
use(email: string, options?: {}): Promise<string>;
125+
} & ((options?: {}) => Promise<Record<string, any>>);
124126

125127
deploy(config: FirebaseDeployConfig): Promise<any>;
126128

src/schematics/setup/index.ts

+16-8
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,16 @@ import {
33
getWorkspace, getProject, getFirebaseProjectNameFromHost, addEnvironmentEntry,
44
addToNgModule, addIgnoreFiles, addFixesToServer
55
} from '../utils';
6-
import { projectTypePrompt, appPrompt, sitePrompt, projectPrompt, featuresPrompt } from './prompts';
6+
import { projectTypePrompt, appPrompt, sitePrompt, projectPrompt, featuresPrompt, userPrompt } from './prompts';
77
import { setupUniversalDeployment } from './ssr';
88
import { setupStaticDeployment } from './static';
99
import {
1010
FirebaseApp, FirebaseHostingSite, FirebaseProject, DeployOptions, NgAddNormalizedOptions,
1111
FEATURES, PROJECT_TYPE
1212
} from '../interfaces';
1313
import { getFirebaseTools } from '../firebaseTools';
14+
import { writeFileSync } from 'fs';
15+
import { join } from 'path';
1416

1517
export const setupProject =
1618
async (tree: Tree, context: SchematicContext, features: FEATURES[], config: DeployOptions & {
@@ -109,21 +111,27 @@ ${Object.entries(config.sdkConfig).reduce(
109111
export const ngAddSetupProject = (
110112
options: DeployOptions
111113
) => async (host: Tree, context: SchematicContext) => {
114+
115+
// TODO is there a public API for this?
116+
const projectRoot: string = (host as any)._backend._root;
117+
112118
const features = await featuresPrompt();
113119

114120
if (features.length > 0) {
115121

116122
const firebaseTools = await getFirebaseTools();
117123

118-
await firebaseTools.login();
119-
const users = await firebaseTools.login.list();
120-
console.log(`Logged into Firebase as ${users.map(it => it.user.email).join(', ')}.`);
124+
// Add the firebase files if they don't exist already so login.use works
125+
if (!host.exists('/firebase.json')) { writeFileSync(join(projectRoot, 'firebase.json'), '{}'); }
126+
127+
const user = await userPrompt({ projectRoot });
128+
await firebaseTools.login.use(user.email, { projectRoot });
121129

122130
const { project: ngProject, projectName: ngProjectName } = getProject(options, host);
123131

124132
const [ defaultProjectName ] = getFirebaseProjectNameFromHost(host, ngProjectName);
125133

126-
const firebaseProject = await projectPrompt(defaultProjectName);
134+
const firebaseProject = await projectPrompt(defaultProjectName, { projectRoot });
127135

128136
let hosting = { projectType: PROJECT_TYPE.Static, prerender: false };
129137
let firebaseHostingSite: FirebaseHostingSite|undefined;
@@ -132,7 +140,7 @@ export const ngAddSetupProject = (
132140
// TODO read existing settings from angular.json, if available
133141
const results = await projectTypePrompt(ngProject, ngProjectName);
134142
hosting = { ...hosting, ...results };
135-
firebaseHostingSite = await sitePrompt(firebaseProject);
143+
firebaseHostingSite = await sitePrompt(firebaseProject, { projectRoot });
136144
}
137145

138146
let firebaseApp: FirebaseApp|undefined;
@@ -141,9 +149,9 @@ export const ngAddSetupProject = (
141149
if (features.find(it => it !== FEATURES.Hosting)) {
142150

143151
const defaultAppId = firebaseHostingSite?.appId;
144-
firebaseApp = await appPrompt(firebaseProject, defaultAppId);
152+
firebaseApp = await appPrompt(firebaseProject, defaultAppId, { projectRoot });
145153

146-
const result = await firebaseTools.apps.sdkconfig('web', firebaseApp.appId, { nonInteractive: true });
154+
const result = await firebaseTools.apps.sdkconfig('web', firebaseApp.appId, { nonInteractive: true, projectRoot });
147155
sdkConfig = result.sdkConfig;
148156

149157
}

src/schematics/setup/prompts.ts

+35-24
Original file line numberDiff line numberDiff line change
@@ -118,9 +118,35 @@ export const featuresPrompt = async (): Promise<FEATURES[]> => {
118118
return features;
119119
};
120120

121-
export const projectPrompt = async (defaultProject?: string) => {
121+
export const userPrompt = async (options: {}): Promise<Record<string, any>> => {
122122
const firebaseTools = await getFirebaseTools();
123-
const projects = firebaseTools.projects.list({});
123+
const users = await firebaseTools.login.list();
124+
if (!users || users.length === 0) {
125+
await firebaseTools.login(); // first login isn't returning anything of value
126+
const user = await firebaseTools.login(options);
127+
return user;
128+
} else {
129+
const defaultUser = await firebaseTools.login(options);
130+
const choices = users.map(({user}) => ({ name: user.email, value: user }));
131+
const newChoice = { name: '[Login in with another account]', value: NEW_OPTION };
132+
const { user } = await inquirer.prompt({
133+
type: 'list',
134+
name: 'user',
135+
choices: [newChoice].concat(choices as any), // TODO types
136+
message: 'Which Firebase account would you like to use?',
137+
default: choices.find(it => it.value.email === defaultUser.email)?.value,
138+
});
139+
if (user === NEW_OPTION) {
140+
const { user } = await firebaseTools.login.add();
141+
return user;
142+
}
143+
return user;
144+
}
145+
};
146+
147+
export const projectPrompt = async (defaultProject: string|undefined, options: {}) => {
148+
const firebaseTools = await getFirebaseTools();
149+
const projects = firebaseTools.projects.list(options);
124150
const { projectId } = await autocomplete({
125151
type: 'autocomplete',
126152
name: 'projectId',
@@ -140,15 +166,15 @@ export const projectPrompt = async (defaultProject?: string) => {
140166
message: 'What would you like to call your project?',
141167
default: projectId,
142168
});
143-
return await firebaseTools.projects.create(projectId, { displayName, nonInteractive: true });
169+
return await firebaseTools.projects.create(projectId, { ...options, displayName, nonInteractive: true });
144170
}
145171
// tslint:disable-next-line:no-non-null-assertion
146172
return (await projects).find(it => it.projectId === projectId)!;
147173
};
148174

149-
export const appPrompt = async ({ projectId: project }: FirebaseProject, defaultAppId: string|undefined) => {
175+
export const appPrompt = async ({ projectId: project }: FirebaseProject, defaultAppId: string|undefined, options: {}) => {
150176
const firebaseTools = await getFirebaseTools();
151-
const apps = firebaseTools.apps.list('web', { project });
177+
const apps = firebaseTools.apps.list('web', { ...options, project });
152178
const { appId } = await autocomplete({
153179
type: 'autocomplete',
154180
name: 'appId',
@@ -162,18 +188,15 @@ export const appPrompt = async ({ projectId: project }: FirebaseProject, default
162188
name: 'displayName',
163189
message: 'What would you like to call your app?',
164190
});
165-
return await firebaseTools.apps.create('web', displayName, { nonInteractive: true, project });
191+
return await firebaseTools.apps.create('web', displayName, { ...options, nonInteractive: true, project });
166192
}
167193
// tslint:disable-next-line:no-non-null-assertion
168194
return (await apps).find(it => shortAppId(it) === appId)!;
169195
};
170196

171-
export const sitePrompt = async ({ projectId: project }: FirebaseProject) => {
197+
export const sitePrompt = async ({ projectId: project }: FirebaseProject, options: {}) => {
172198
const firebaseTools = await getFirebaseTools();
173-
if (!firebaseTools.hosting.sites) {
174-
return undefined;
175-
}
176-
const sites = firebaseTools.hosting.sites.list({ project }).then(it => {
199+
const sites = firebaseTools.hosting.sites.list({ ...options, project }).then(it => {
177200
if (it.sites.length === 0) {
178201
// newly created projects don't return their default site, stub one
179202
return [{
@@ -199,24 +222,12 @@ export const sitePrompt = async ({ projectId: project }: FirebaseProject) => {
199222
name: 'subdomain',
200223
message: 'Please provide an unique, URL-friendly id for the site (<id>.web.app):',
201224
});
202-
return await firebaseTools.hosting.sites.create(subdomain, { nonInteractive: true, project });
225+
return await firebaseTools.hosting.sites.create(subdomain, { ...options, nonInteractive: true, project });
203226
}
204227
// tslint:disable-next-line:no-non-null-assertion
205228
return (await sites).find(it => shortSiteName(it) === siteName)!;
206229
};
207230

208-
export const prerenderPrompt = (project: WorkspaceProject, prerender: boolean): Promise<{ projectType: PROJECT_TYPE }> => {
209-
if (isUniversalApp(project)) {
210-
return inquirer.prompt({
211-
type: 'prompt',
212-
name: 'prerender',
213-
message: 'We detected an Angular Universal project. How would you like to render server-side content?',
214-
default: true
215-
});
216-
}
217-
return Promise.resolve({ projectType: PROJECT_TYPE.Static });
218-
};
219-
220231
export const projectTypePrompt = async (project: WorkspaceProject, name: string) => {
221232
let prerender = false;
222233
let nodeVersion: string|undefined;

0 commit comments

Comments
 (0)