Skip to content

Commit

Permalink
feat(schematics): adding multi-user support. (#2958)
Browse files Browse the repository at this point in the history
Utilize firebase-tools multi user support to allow an account picker on `ng add`
  • Loading branch information
jamesdaniels authored Sep 14, 2021
1 parent 73bde38 commit ca6eac2
Show file tree
Hide file tree
Showing 5 changed files with 72 additions and 40 deletions.
7 changes: 5 additions & 2 deletions src/schematics/deploy/actions.jasmine.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand Down Expand Up @@ -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 }
Expand Down Expand Up @@ -149,6 +151,7 @@ describe('Deploy Angular apps', () => {
only: 'hosting:' + PROJECT,
token: FIREBASE_TOKEN,
nonInteractive: true,
projectRoot: 'cwd',
});
});

Expand Down
18 changes: 13 additions & 5 deletions src/schematics/deploy/actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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({
Expand All @@ -97,6 +98,7 @@ const deployToHosting = async (
cwd: workspaceRoot,
token: firebaseToken,
nonInteractive: true,
projectRoot: workspaceRoot,
});

};
Expand Down Expand Up @@ -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({
Expand All @@ -245,6 +248,7 @@ export const deployToFunction = async (
cwd: workspaceRoot,
token: firebaseToken,
nonInteractive: true,
projectRoot: workspaceRoot,
});

};
Expand Down Expand Up @@ -352,6 +356,7 @@ export const deployToCloudRun = async (
cwd: workspaceRoot,
token: firebaseToken,
nonInteractive: true,
projectRoot: workspaceRoot,
});
};

Expand All @@ -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) {
Expand Down Expand Up @@ -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}'`);
}
Expand Down
4 changes: 3 additions & 1 deletion src/schematics/interfaces.ts
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,9 @@ export interface FirebaseTools {

login: {
list(): Promise<{user: Record<string, any>}[]>;
} & (() => Promise<void>);
add(): Promise<Record<string, any>>;
use(email: string, options?: {}): Promise<string>;
} & ((options?: {}) => Promise<Record<string, any>>);

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

Expand Down
24 changes: 16 additions & 8 deletions src/schematics/setup/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,16 @@ 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 {
FirebaseApp, FirebaseHostingSite, FirebaseProject, DeployOptions, NgAddNormalizedOptions,
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 & {
Expand Down Expand Up @@ -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;
Expand All @@ -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;
Expand All @@ -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;

}
Expand Down
59 changes: 35 additions & 24 deletions src/schematics/setup/prompts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -118,9 +118,35 @@ export const featuresPrompt = async (): Promise<FEATURES[]> => {
return features;
};

export const projectPrompt = async (defaultProject?: string) => {
export const userPrompt = async (options: {}): Promise<Record<string, any>> => {
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',
Expand All @@ -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',
Expand All @@ -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 [{
Expand All @@ -199,24 +222,12 @@ export const sitePrompt = async ({ projectId: project }: FirebaseProject) => {
name: 'subdomain',
message: 'Please provide an unique, URL-friendly id for the site (<id>.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;
Expand Down

0 comments on commit ca6eac2

Please sign in to comment.