diff --git a/package.json b/package.json index 074b8d77e..8e3418c3b 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,8 @@ "description": "The official library of Firebase and Angular.", "private": true, "scripts": { - "test": "npm run build && karma start --single-run", + "test": "npm run build && karma start --single-run && npm run test:node", + "test:node": "jasmine 'dist/packages-dist/schematics/**/*[sS]pec.js'", "test:watch": "concurrently \"npm run build:watch\" \"npm run delayed_karma\"", "test:debug": "npm run build && karma start", "karma": "karma start", diff --git a/src/root.spec.js b/src/root.spec.js index 20c8f5cf1..27cfec26b 100644 --- a/src/root.spec.js +++ b/src/root.spec.js @@ -13,9 +13,6 @@ export * from './packages-dist/database/list/snapshot-changes.spec'; export * from './packages-dist/database/list/state-changes.spec'; export * from './packages-dist/database/list/audit-trail.spec'; export * from './packages-dist/storage/storage.spec'; -export * from './packages-dist/schematics/ng-add.spec'; -export * from './packages-dist/schematics/deploy/actions.spec'; -export * from './packages-dist/schematics/deploy/builder.spec'; //export * from './packages-dist/messaging/messaging.spec'; // // Since this a deprecated API, we run on it on manual tests only diff --git a/src/schematics/deploy/actions.spec.ts b/src/schematics/deploy/actions.spec.ts index c2ff7797e..24be1aace 100644 --- a/src/schematics/deploy/actions.spec.ts +++ b/src/schematics/deploy/actions.spec.ts @@ -1,5 +1,106 @@ -describe('ng deploy:firebase', () => { - it('adds', () => { - expect(1).toBe(1); +import { JsonObject, logging } from '@angular-devkit/core'; +import { BuilderContext, BuilderRun, ScheduleOptions, Target, } from '@angular-devkit/architect/src/index2'; +import { FirebaseTools, FirebaseDeployConfig } from 'schematics/interfaces'; +import deploy from './actions'; + + +let context: BuilderContext; +let firebaseMock: FirebaseTools; + +const FIREBASE_PROJECT = 'ikachu-aa3ef'; +const PROJECT = 'pirojok-project'; + +describe('Deploy Angular apps', () => { + beforeEach(() => initMocks()); + + it('should check if the user is authenticated by invoking list', async () => { + const spy = spyOn(firebaseMock, 'list'); + const spyLogin = spyOn(firebaseMock, 'login'); + await deploy(firebaseMock, context, 'host', FIREBASE_PROJECT); + expect(spy).toHaveBeenCalled(); + expect(spyLogin).not.toHaveBeenCalled(); + }); + + it('should invoke login if list rejects', async () => { + firebaseMock.list = () => Promise.reject(); + const spy = spyOn(firebaseMock, 'list').and.callThrough(); + const spyLogin = spyOn(firebaseMock, 'login'); + await deploy(firebaseMock, context, 'host', FIREBASE_PROJECT); + expect(spy).toHaveBeenCalled(); + expect(spyLogin).toHaveBeenCalled(); + }); + + it('should invoke the builder', async () => { + const spy = spyOn(context, 'scheduleTarget').and.callThrough(); + await deploy(firebaseMock, context, 'host', FIREBASE_PROJECT); + expect(spy).toHaveBeenCalled(); + expect(spy).toHaveBeenCalledWith({ + target: 'build', + configuration: 'production', + project: PROJECT }); + }); + + it('should invoke firebase.deploy', async () => { + const spy = spyOn(firebaseMock, 'deploy').and.callThrough(); + await deploy(firebaseMock, context, 'host', FIREBASE_PROJECT); + expect(spy).toHaveBeenCalled(); + expect(spy).toHaveBeenCalledWith({ + cwd: 'host', only: 'hosting:' + PROJECT + }); + }); + + describe('error handling', () => { + it('throws if there is no firebase project', async () => { + try { + await deploy(firebaseMock, context, 'host') + fail(); + } catch (e) { + expect(e.message).toMatch(/Cannot find firebase project/); + } + }); + + it('throws if there is no target project', async () => { + context.target = undefined; + try { + await deploy(firebaseMock, context, 'host', FIREBASE_PROJECT) + fail(); + } catch (e) { + expect(e.message).toMatch(/Cannot execute the build target/); + } + }); + }); }); + +const initMocks = () => { + firebaseMock = { + login: () => Promise.resolve(), + list: () => Promise.resolve([]), + deploy: (_: FirebaseDeployConfig) => Promise.resolve(), + use: () => Promise.resolve() + }; + + context = { + target: { + configuration: 'production', + project: PROJECT, + target: 'foo' + }, + builder: { + builderName: 'mock', + description: 'mock', + optionSchema: false + }, + currentDirectory: 'cwd', + id: 1, + logger: new logging.NullLogger() as any, + workspaceRoot: 'cwd', + getTargetOptions: (_: Target) => Promise.resolve({}), + reportProgress: (_: number, __?: number, ___?: string) => { + }, + reportStatus: (_: string) => {}, + reportRunning: () => {}, + scheduleBuilder: (_: string, __?: JsonObject, ___?: ScheduleOptions) => Promise.resolve({} as BuilderRun), + scheduleTarget: (_: Target, __?: JsonObject, ___?: ScheduleOptions) => Promise.resolve({} as BuilderRun) + }; +}; \ No newline at end of file diff --git a/src/schematics/deploy/actions.ts b/src/schematics/deploy/actions.ts index cc6956ab8..58246690b 100644 --- a/src/schematics/deploy/actions.ts +++ b/src/schematics/deploy/actions.ts @@ -1,41 +1,54 @@ -import { BuilderContext } from '@angular-devkit/architect/src/index2'; -import { FirebaseTools } from '../interfaces'; +import { BuilderContext } from "@angular-devkit/architect/src/index2"; +import { FirebaseTools } from "../interfaces"; -export default async function deploy(firebaseTools: FirebaseTools, context: BuilderContext, projectRoot: string, firebaseProject?: string) { - if (!firebaseProject) { - throw new Error('Cannot find firebase project for your app in .firebaserc'); - } +export default async function deploy( + firebaseTools: FirebaseTools, + context: BuilderContext, + projectRoot: string, + firebaseProject?: string +) { + if (!firebaseProject) { + throw new Error("Cannot find firebase project for your app in .firebaserc"); + } - try { - await firebaseTools.list(); - } catch (e) { - context.logger.warn("🚨 You're not logged into Firebase. Logging you in..."); - await firebaseTools.login(); - } - if (!context.target) { - throw new Error('Cannot execute the build target'); - } + try { + await firebaseTools.list(); + } catch (e) { + context.logger.warn( + "🚨 You're not logged into Firebase. Logging you in..." + ); + await firebaseTools.login(); + } + if (!context.target) { + throw new Error("Cannot execute the build target"); + } - context.logger.info(`📦 Building "${context.target.project}"`); + context.logger.info(`📦 Building "${context.target.project}"`); - const run = await context.scheduleTarget({ - target: 'build', - project: context.target.project, - configuration: 'production' - }); - await run.result; - - try { - await firebaseTools.use(firebaseProject, {project: firebaseProject}); - } catch (e) { - throw new Error(`Cannot select firebase project '${firebaseProject}'`); - } + const run = await context.scheduleTarget({ + target: "build", + project: context.target.project, + configuration: "production" + }); + await run.result; + try { + await firebaseTools.use(firebaseProject, { project: firebaseProject }); + } catch (e) { + throw new Error(`Cannot select firebase project '${firebaseProject}'`); + } - try { - const success = await firebaseTools.deploy({only: 'hosting:' + context.target.project, cwd: projectRoot}); - context.logger.info(`🚀 Your application is now available at https://${success.hosting.split('/')[1]}.firebaseapp.com/`); - } catch (e) { - context.logger.error(e); - } + try { + const success = await firebaseTools.deploy({ + only: "hosting:" + context.target.project, + cwd: projectRoot + }); + context.logger.info( + `🚀 Your application is now available at https://${ + success.hosting.split("/")[1] + }.firebaseapp.com/` + ); + } catch (e) { + context.logger.error(e); + } } diff --git a/src/schematics/deploy/builder.spec.ts b/src/schematics/deploy/builder.spec.ts deleted file mode 100644 index d1379b78c..000000000 --- a/src/schematics/deploy/builder.spec.ts +++ /dev/null @@ -1,5 +0,0 @@ -describe('ng deploy:firebase createBuilder', () => { - it('adds', () => { - expect(1).toBe(1); - }); -}); diff --git a/src/schematics/deploy/builder.ts b/src/schematics/deploy/builder.ts index e9c1a7083..7e96123b3 100644 --- a/src/schematics/deploy/builder.ts +++ b/src/schematics/deploy/builder.ts @@ -1,34 +1,51 @@ -import { BuilderContext, BuilderOutput, createBuilder } from '@angular-devkit/architect/src/index2'; -import { NodeJsSyncHost } from '@angular-devkit/core/node'; -import deploy from './actions'; -import { experimental, join, normalize } from '@angular-devkit/core'; -import { getFirebaseProjectName } from '../utils'; +import { + BuilderContext, + BuilderOutput, + createBuilder +} from "@angular-devkit/architect/src/index2"; +import { NodeJsSyncHost } from "@angular-devkit/core/node"; +import deploy from "./actions"; +import { experimental, join, normalize } from "@angular-devkit/core"; +import { getFirebaseProjectName } from "../utils"; // Call the createBuilder() function to create a builder. This mirrors // createJobHandler() but add typings specific to Architect Builders. export default createBuilder( - async (_: any, context: BuilderContext): Promise => { - // The project root is added to a BuilderContext. - const root = normalize(context.workspaceRoot); - const workspace = new experimental.workspace.Workspace(root, new NodeJsSyncHost()); - await workspace.loadWorkspaceFromHost(normalize('angular.json')).toPromise(); + async (_: any, context: BuilderContext): Promise => { + // The project root is added to a BuilderContext. + const root = normalize(context.workspaceRoot); + const workspace = new experimental.workspace.Workspace( + root, + new NodeJsSyncHost() + ); + await workspace + .loadWorkspaceFromHost(normalize("angular.json")) + .toPromise(); - if (!context.target) { - throw new Error('Cannot deploy the application without a target'); - } - - const project = workspace.getProject(context.target.project); + if (!context.target) { + throw new Error("Cannot deploy the application without a target"); + } - const firebaseProject = getFirebaseProjectName(workspace.root, context.target.project); + const project = workspace.getProject(context.target.project); - try { - await deploy(require('firebase-tools'), context, join(workspace.root, project.root), firebaseProject); - } catch (e) { - console.error('Error when trying to deploy: '); - console.error(e.message); - return {success: false} - } + const firebaseProject = getFirebaseProjectName( + workspace.root, + context.target.project + ); - return {success: true} + try { + await deploy( + require("firebase-tools"), + context, + join(workspace.root, project.root), + firebaseProject + ); + } catch (e) { + console.error("Error when trying to deploy: "); + console.error(e.message); + return { success: false }; } + + return { success: true }; + } ); diff --git a/src/schematics/index.spec.ts b/src/schematics/index.spec.ts index 0bb778f32..a88cfd94b 100644 --- a/src/schematics/index.spec.ts +++ b/src/schematics/index.spec.ts @@ -1,3 +1,2 @@ import './ng-add.spec'; -import './deploy/builder.spec'; -import './deploy/actions.spec'; \ No newline at end of file +import './deploy/actions.spec'; diff --git a/src/schematics/index.ts b/src/schematics/index.ts index 9a85f300e..4ef9ceca4 100644 --- a/src/schematics/index.ts +++ b/src/schematics/index.ts @@ -1 +1 @@ -export * from './public_api'; \ No newline at end of file +export * from "./public_api"; diff --git a/src/schematics/interfaces.ts b/src/schematics/interfaces.ts index 3c9d22df5..0d483364f 100644 --- a/src/schematics/interfaces.ts +++ b/src/schematics/interfaces.ts @@ -1,45 +1,44 @@ export interface Project { - name: string; - id: string; - permission: 'edit' | 'view' | 'own'; + name: string; + id: string; + permission: "edit" | "view" | "own"; } export interface FirebaseDeployConfig { - cwd: string; - only?: string; + cwd: string; + only?: string; } export interface FirebaseTools { - login(): Promise; + login(): Promise; - list(): Promise; + list(): Promise; - deploy(config: FirebaseDeployConfig): Promise; + deploy(config: FirebaseDeployConfig): Promise; - use(options: any, lol: any): Promise; + use(options: any, lol: any): Promise; } export interface FirebaseHostingRewrite { - source: string; - destination: string; - + source: string; + destination: string; } export interface FirebaseHostingConfig { - public: string; - ignore: string[]; - target: string; - rewrites: FirebaseHostingRewrite[]; + public: string; + ignore: string[]; + target: string; + rewrites: FirebaseHostingRewrite[]; } export interface FirebaseJSON { - hosting: FirebaseHostingConfig[] + hosting: FirebaseHostingConfig[]; } export interface FirebaseRcTarget { - hosting: Record + hosting: Record; } export interface FirebaseRc { - targets: Record + targets: Record; } diff --git a/src/schematics/ng-add.spec.ts b/src/schematics/ng-add.spec.ts index 0ead257e2..eb213bc2c 100644 --- a/src/schematics/ng-add.spec.ts +++ b/src/schematics/ng-add.spec.ts @@ -1,5 +1,443 @@ -describe('ng add', () => { - it('adds', () => { - expect(1).toBe(1); +import { Tree } from "@angular-devkit/schematics"; +import { ngAdd } from "./ng-add"; + +const PROJECT_NAME = "pie-ka-chu"; +const PROJECT_ROOT = "pirojok"; +const FIREBASE_PROJECT = "pirojok-111e3"; + +const OTHER_PROJECT_NAME = "pi-catch-you"; +const OTHER_FIREBASE_PROJECT_NAME = "bi-catch-you-77e7e"; + +describe("ng-add", () => { + describe("generating files", () => { + let tree: Tree; + + beforeEach(() => { + tree = Tree.empty(); + tree.create("angular.json", JSON.stringify(generateAngularJson())); }); + + it('generates new files if starting from scratch', async () => { + const result = ngAdd(tree, {firebaseProject: FIREBASE_PROJECT, project: PROJECT_NAME}); + expect(result.read('firebase.json')!.toString()).toEqual(initialFirebaseJson); + expect(result.read('.firebaserc')!.toString()).toEqual(initialFirebaserc); + expect(result.read('angular.json')!.toString()).toEqual(initialAngularJson); + }); + + it('uses default project', async () => { + const result = ngAdd(tree, {firebaseProject: FIREBASE_PROJECT}); + expect(result.read('firebase.json')!.toString()).toEqual(overwriteFirebaseJson); + expect(result.read('.firebaserc')!.toString()).toEqual(overwriteFirebaserc); + expect(result.read('angular.json')!.toString()).toEqual(overwriteAngularJson); + }); + + it('overrides existing files', async () => { + const tempTree = ngAdd(tree, {firebaseProject: FIREBASE_PROJECT, project: PROJECT_NAME}); + const result = ngAdd(tempTree, {firebaseProject: OTHER_FIREBASE_PROJECT_NAME, project: OTHER_PROJECT_NAME}); + expect(result.read('firebase.json')!.toString()).toEqual(projectFirebaseJson); + expect(result.read('.firebaserc')!.toString()).toEqual(projectFirebaserc); + expect(result.read('angular.json')!.toString()).toEqual(projectAngularJson); + }); + }); + + describe("error handling", () => { + it("fails if project not defined", () => { + const tree = Tree.empty(); + const angularJSON = generateAngularJson(); + delete angularJSON.defaultProject; + tree.create("angular.json", JSON.stringify(angularJSON)); + expect(() => + ngAdd(tree, { + firebaseProject: FIREBASE_PROJECT, + project: "" + }) + ).toThrowError( + /No project selected and no default project in the workspace/ + ); + }); + + it("Should throw if angular.json not found", async () => { + expect(() => + ngAdd(Tree.empty(), { + firebaseProject: FIREBASE_PROJECT, + project: PROJECT_NAME + }) + ).toThrowError(/Could not find angular.json/); + }); + + it("Should throw if angular.json can not be parsed", async () => { + const tree = Tree.empty(); + tree.create("angular.json", "hi"); + expect(() => + ngAdd(tree, { + firebaseProject: FIREBASE_PROJECT, + project: PROJECT_NAME + }) + ).toThrowError(/Could not parse angular.json/); + }); + + it("Should throw if specified project does not exist ", async () => { + const tree = Tree.empty(); + tree.create("angular.json", JSON.stringify({ projects: {} })); + expect(() => + ngAdd(tree, { + firebaseProject: FIREBASE_PROJECT, + project: PROJECT_NAME + }) + ).toThrowError(/Project is not defined in this workspace/); + }); + + it("Should throw if specified project is not application", async () => { + const tree = Tree.empty(); + tree.create( + "angular.json", + JSON.stringify({ + projects: { [PROJECT_NAME]: { projectType: "pokemon" } } + }) + ); + expect(() => + ngAdd(tree, { + firebaseProject: FIREBASE_PROJECT, + project: PROJECT_NAME + }) + ).toThrowError(/Deploy requires a project type of "application"/); + }); + + it("Should throw if app does not have architect configured", async () => { + const tree = Tree.empty(); + tree.create( + "angular.json", + JSON.stringify({ + projects: { [PROJECT_NAME]: { projectType: "application" } } + }) + ); + expect(() => + ngAdd(tree, { + firebaseProject: FIREBASE_PROJECT, + project: PROJECT_NAME + }) + ).toThrowError(/Cannot read the output path/); + }); + + it("Should throw if firebase.json has the project already", async () => { + const tree = Tree.empty(); + tree.create("angular.json", JSON.stringify(generateAngularJson())); + const tempTree = ngAdd(tree, { + firebaseProject: FIREBASE_PROJECT, + project: PROJECT_NAME + }); + + expect(() => + ngAdd(tempTree, { + firebaseProject: FIREBASE_PROJECT, + project: PROJECT_NAME + }) + ).toThrowError(/already exists in firebase.json/); + }); + + it("Should throw if firebase.json is broken", async () => { + const tree = Tree.empty(); + tree.create("angular.json", JSON.stringify(generateAngularJson())); + tree.create("firebase.json", "I'm broken 😔"); + expect(() => + ngAdd(tree, { + firebaseProject: FIREBASE_PROJECT, + project: PROJECT_NAME + }) + ).toThrowError(/firebase.json: Unexpected token/); + }); + + it("Should throw if .firebaserc is broken", async () => { + const tree = Tree.empty(); + tree.create("angular.json", JSON.stringify(generateAngularJson())); + tree.create(".firebaserc", "I'm broken 😔"); + expect(() => + ngAdd(tree, { + firebaseProject: FIREBASE_PROJECT, + project: PROJECT_NAME + }) + ).toThrowError(/.firebaserc: Unexpected token/); + }); + + it("Should throw if firebase.json has the project already", async () => { + const tree = Tree.empty(); + tree.create("angular.json", JSON.stringify(generateAngularJson())); + const tempTree = ngAdd(tree, { + firebaseProject: FIREBASE_PROJECT, + project: PROJECT_NAME + }); + + expect(() => + ngAdd(tempTree, { + firebaseProject: FIREBASE_PROJECT, + project: OTHER_PROJECT_NAME + }) + ).toThrowError(/ already defined in .firebaserc/); + }); + + it("Should throw if firebase.json is broken", async () => { + const tree = Tree.empty(); + tree.create("angular.json", JSON.stringify(generateAngularJson())); + + const tempTree = ngAdd(tree, { + firebaseProject: FIREBASE_PROJECT, + project: PROJECT_NAME + }); + + expect(() => + ngAdd(tempTree, { + firebaseProject: FIREBASE_PROJECT, + project: OTHER_PROJECT_NAME + }) + ).toThrowError(/ already defined in .firebaserc/); + }); + }); }); + +function generateAngularJson() { + return { + defaultProject: PROJECT_NAME, + projects: { + [PROJECT_NAME]: { + projectType: "application", + root: PROJECT_ROOT, + architect: { + build: { + options: { + outputPath: "dist/ikachu" + } + } + } + }, + [OTHER_PROJECT_NAME]: { + projectType: "application", + root: PROJECT_ROOT, + architect: { + build: { + options: { + outputPath: "dist/ikachu" + } + } + } + } + } + }; +} + +const initialFirebaseJson = `{ + \"hosting\": [ + { + \"target\": \"pie-ka-chu\", + \"public\": \"dist/ikachu\", + \"ignore\": [ + \"firebase.json\", + \"**/.*\", + \"**/node_modules/**\" + ], + \"rewrites\": [ + { + \"source\": \"**\", + \"destination\": \"/index.html\" + } + ] + } + ] +}`; + +const initialFirebaserc = `{ + \"targets\": { + \"pirojok-111e3\": { + \"hosting\": { + \"pie-ka-chu\": [ + \"pirojok-111e3\" + ] + } + } + } +}`; + +const initialAngularJson = `{ + \"defaultProject\": \"pie-ka-chu\", + \"projects\": { + \"pie-ka-chu\": { + \"projectType\": \"application\", + \"root\": \"pirojok\", + \"architect\": { + \"build\": { + \"options\": { + \"outputPath\": \"dist/ikachu\" + } + }, + \"deploy\": { + \"builder\": \"@angular/fire:deploy\", + \"options\": {} + } + } + }, + \"pi-catch-you\": { + \"projectType\": \"application\", + \"root\": \"pirojok\", + \"architect\": { + \"build\": { + \"options\": { + \"outputPath\": \"dist/ikachu\" + } + } + } + } + } +}`; + +const overwriteFirebaseJson = `{ + \"hosting\": [ + { + \"target\": \"pie-ka-chu\", + \"public\": \"dist/ikachu\", + \"ignore\": [ + \"firebase.json\", + \"**/.*\", + \"**/node_modules/**\" + ], + \"rewrites\": [ + { + \"source\": \"**\", + \"destination\": \"/index.html\" + } + ] + } + ] +}`; + +const overwriteFirebaserc = `{ + \"targets\": { + \"pirojok-111e3\": { + \"hosting\": { + \"pie-ka-chu\": [ + \"pirojok-111e3\" + ] + } + } + } +}`; + +const overwriteAngularJson = `{ + \"defaultProject\": \"pie-ka-chu\", + \"projects\": { + \"pie-ka-chu\": { + \"projectType\": \"application\", + \"root\": \"pirojok\", + \"architect\": { + \"build\": { + \"options\": { + \"outputPath\": \"dist/ikachu\" + } + }, + \"deploy\": { + \"builder\": \"@angular/fire:deploy\", + \"options\": {} + } + } + }, + \"pi-catch-you\": { + \"projectType\": \"application\", + \"root\": \"pirojok\", + \"architect\": { + \"build\": { + \"options\": { + \"outputPath\": \"dist/ikachu\" + } + } + } + } + } +}`; + +const projectFirebaseJson = `{ + \"hosting\": [ + { + \"target\": \"pie-ka-chu\", + \"public\": \"dist/ikachu\", + \"ignore\": [ + \"firebase.json\", + \"**/.*\", + \"**/node_modules/**\" + ], + \"rewrites\": [ + { + \"source\": \"**\", + \"destination\": \"/index.html\" + } + ] + }, + { + \"target\": \"pi-catch-you\", + \"public\": \"dist/ikachu\", + \"ignore\": [ + \"firebase.json\", + \"**/.*\", + \"**/node_modules/**\" + ], + \"rewrites\": [ + { + \"source\": \"**\", + \"destination\": \"/index.html\" + } + ] + } + ] +}`; + +const projectFirebaserc = `{ + \"targets\": { + \"pirojok-111e3\": { + \"hosting\": { + \"pie-ka-chu\": [ + \"pirojok-111e3\" + ] + } + }, + \"bi-catch-you-77e7e\": { + \"hosting\": { + \"pi-catch-you\": [ + \"bi-catch-you-77e7e\" + ] + } + } + } +}`; + +const projectAngularJson = `{ + \"defaultProject\": \"pie-ka-chu\", + \"projects\": { + \"pie-ka-chu\": { + \"projectType\": \"application\", + \"root\": \"pirojok\", + \"architect\": { + \"build\": { + \"options\": { + \"outputPath\": \"dist/ikachu\" + } + }, + \"deploy\": { + \"builder\": \"@angular/fire:deploy\", + \"options\": {} + } + } + }, + \"pi-catch-you\": { + \"projectType\": \"application\", + \"root\": \"pirojok\", + \"architect\": { + \"build\": { + \"options\": { + \"outputPath\": \"dist/ikachu\" + } + }, + \"deploy\": { + \"builder\": \"@angular/fire:deploy\", + \"options\": {} + } + } + } + } +}`; diff --git a/src/schematics/ng-add.ts b/src/schematics/ng-add.ts index efb8c309f..104c05003 100644 --- a/src/schematics/ng-add.ts +++ b/src/schematics/ng-add.ts @@ -1,122 +1,139 @@ -import { SchematicsException, Tree } from '@angular-devkit/schematics'; -import { FirebaseJSON, FirebaseRc } from './interfaces'; -import { experimental, JsonParseMode, parseJson } from '@angular-devkit/core'; -import { from } from 'rxjs'; -import { map, switchMap } from 'rxjs/operators'; -import { Project } from './interfaces'; -import { listProjects, projectPrompt } from './utils'; +import { SchematicsException, Tree } from "@angular-devkit/schematics"; +import { FirebaseJSON, FirebaseRc } from "./interfaces"; +import { experimental, JsonParseMode, parseJson } from "@angular-devkit/core"; +import { from } from "rxjs"; +import { map, switchMap } from "rxjs/operators"; +import { Project } from "./interfaces"; +import { listProjects, projectPrompt } from "./utils"; const stringifyFormatted = (obj: any) => JSON.stringify(obj, null, 2); function emptyFirebaseJson() { - return { - hosting: [] - } + return { + hosting: [] + }; } function emptyFirebaseRc() { - return { - targets: {} - }; + return { + targets: {} + }; } - function generateHostingConfig(project: string, dist: string) { - return { - target: project, - public: dist, - ignore: ['firebase.json', '**/.*', '**/node_modules/**'], - rewrites: [ - { - source: '**', - destination: '/index.html' - } - ] - } + return { + target: project, + public: dist, + ignore: ["firebase.json", "**/.*", "**/node_modules/**"], + rewrites: [ + { + source: "**", + destination: "/index.html" + } + ] + }; } function safeReadJSON(path: string, tree: Tree) { - try { - return JSON.parse(tree.read(path)!.toString()) - } catch (e) { - throw new SchematicsException(`Error when parsing ${path}: ${e.message}`); - } + try { + return JSON.parse(tree.read(path)!.toString()); + } catch (e) { + throw new SchematicsException(`Error when parsing ${path}: ${e.message}`); + } } -function generateFirebaseJson(tree: Tree, path: string, project: string, dist: string) { - let firebaseJson: FirebaseJSON = tree.exists(path) ? safeReadJSON(path, tree) : emptyFirebaseJson(); - - if (firebaseJson.hosting.find(config => config.target === project)) { - throw new SchematicsException(`Target ${project} already exists in firebase.json`); - } - - firebaseJson.hosting.push(generateHostingConfig(project, dist)); - - overwriteIfExists(tree, path, stringifyFormatted(firebaseJson)); +function generateFirebaseJson( + tree: Tree, + path: string, + project: string, + dist: string +) { + let firebaseJson: FirebaseJSON = tree.exists(path) + ? safeReadJSON(path, tree) + : emptyFirebaseJson(); + + if (firebaseJson.hosting.find(config => config.target === project)) { + throw new SchematicsException( + `Target ${project} already exists in firebase.json` + ); + } + + firebaseJson.hosting.push(generateHostingConfig(project, dist)); + + overwriteIfExists(tree, path, stringifyFormatted(firebaseJson)); } - function generateFirebaseRcTarget(firebaseProject: string, project: string) { - return { - "hosting": { - [project]: [ - // TODO(kirjs): Generally site name is consistent with the project name, but there are edge cases. - firebaseProject - ] - } - }; -} - -function generateFirebaseRc(tree: Tree, path: string, firebaseProject: string, project: string) { - const firebaseRc: FirebaseRc = tree.exists(path) ? safeReadJSON(path, tree) : emptyFirebaseRc(); - - - if (firebaseProject in firebaseRc.targets) { - throw new SchematicsException(`Firebase project ${firebaseProject} already defined in .firebaserc`); + return { + hosting: { + [project]: [ + // TODO(kirjs): Generally site name is consistent with the project name, but there are edge cases. + firebaseProject + ] } + }; +} - firebaseRc.targets[firebaseProject] = generateFirebaseRcTarget(firebaseProject, project); - - overwriteIfExists(tree, path, stringifyFormatted(firebaseRc)); +function generateFirebaseRc( + tree: Tree, + path: string, + firebaseProject: string, + project: string +) { + const firebaseRc: FirebaseRc = tree.exists(path) + ? safeReadJSON(path, tree) + : emptyFirebaseRc(); + + if (firebaseProject in firebaseRc.targets) { + throw new SchematicsException( + `Firebase project ${firebaseProject} already defined in .firebaserc` + ); + } + + firebaseRc.targets[firebaseProject] = generateFirebaseRcTarget( + firebaseProject, + project + ); + + overwriteIfExists(tree, path, stringifyFormatted(firebaseRc)); } const overwriteIfExists = (tree: Tree, path: string, content: string) => { - if (tree.exists(path)) tree.overwrite(path, content); - else tree.create(path, content); + if (tree.exists(path)) tree.overwrite(path, content); + else tree.create(path, content); }; function getWorkspace( - host: Tree, -): { path: string, workspace: experimental.workspace.WorkspaceSchema } { - const possibleFiles = ['/angular.json', '/.angular.json']; - const path = possibleFiles.filter(path => host.exists(path))[0]; - - const configBuffer = host.read(path); - if (configBuffer === null) { - throw new SchematicsException(`Could not find angular.json`); - } - const content = configBuffer.toString(); - - let workspace: experimental.workspace.WorkspaceSchema; - try { - workspace = parseJson( - content, - JsonParseMode.Loose, - ) as {} as experimental.workspace.WorkspaceSchema; - } catch (e) { - throw new SchematicsException(`Could not parse angular.json: ` + e.message); - } - - return { - path, - workspace, - }; + host: Tree +): { path: string; workspace: experimental.workspace.WorkspaceSchema } { + const possibleFiles = ["/angular.json", "/.angular.json"]; + const path = possibleFiles.filter(path => host.exists(path))[0]; + + const configBuffer = host.read(path); + if (configBuffer === null) { + throw new SchematicsException(`Could not find angular.json`); + } + const content = configBuffer.toString(); + + let workspace: experimental.workspace.WorkspaceSchema; + try { + workspace = (parseJson( + content, + JsonParseMode.Loose + ) as {}) as experimental.workspace.WorkspaceSchema; + } catch (e) { + throw new SchematicsException(`Could not parse angular.json: ` + e.message); + } + + return { + path, + workspace + }; } - interface NgAddOptions { - firebaseProject: string; - project?: string; + firebaseProject: string; + project?: string; } interface DeployOptions { @@ -125,44 +142,63 @@ interface DeployOptions { // You don't have to export the function as default. You can also have more than one rule factory // per file. -export const ngDeploy = ({project}: DeployOptions) => (host: Tree) => from(listProjects()).pipe( - switchMap((projects: Project[]) => projectPrompt(projects)), - map(({firebaseProject}: any) => ngAdd(host, {firebaseProject, project})) -); +export const ngDeploy = ({ project }: DeployOptions) => (host: Tree) => + from(listProjects()).pipe( + switchMap((projects: Project[]) => projectPrompt(projects)), + map(({ firebaseProject }: any) => ngAdd(host, { firebaseProject, project })) + ); export function ngAdd(tree: Tree, options: NgAddOptions) { - const {path: workspacePath, workspace} = getWorkspace(tree); - - if (!options.project) { - if (workspace.defaultProject) { - options.project = workspace.defaultProject; - } else { - throw new SchematicsException('No project selected and no default project in the workspace'); - } - } - - const project = workspace.projects[options.project]; - if (!project) { - throw new SchematicsException('Project is not defined in this workspace'); + const { path: workspacePath, workspace } = getWorkspace(tree); + + if (!options.project) { + if (workspace.defaultProject) { + options.project = workspace.defaultProject; + } else { + throw new SchematicsException( + "No project selected and no default project in the workspace" + ); } - - if (project.projectType !== 'application') { - throw new SchematicsException(`Deploy requires a project type of "application" in angular.json`); - } - - if (!project.architect || !project.architect.build || !project.architect.build.options || !project.architect.build.options.outputPath) { - throw new SchematicsException(`Cannot read the output path (architect.build.options.outputPath) of project "${options.project}" in angular.json`); - } - - const outputPath = project.architect.build.options.outputPath; - - project.architect['deploy'] = { - builder: '@angular/fire:deploy', - options: {} - }; - - tree.overwrite(workspacePath, JSON.stringify(workspace, null, 2)); - generateFirebaseJson(tree, 'firebase.json', options.project, outputPath); - generateFirebaseRc(tree, '.firebaserc', options.firebaseProject, options.project); - return tree; + } + + const project = workspace.projects[options.project]; + if (!project) { + throw new SchematicsException("Project is not defined in this workspace"); + } + + if (project.projectType !== "application") { + throw new SchematicsException( + `Deploy requires a project type of "application" in angular.json` + ); + } + + if ( + !project.architect || + !project.architect.build || + !project.architect.build.options || + !project.architect.build.options.outputPath + ) { + throw new SchematicsException( + `Cannot read the output path (architect.build.options.outputPath) of project "${ + options.project + }" in angular.json` + ); + } + + const outputPath = project.architect.build.options.outputPath; + + project.architect["deploy"] = { + builder: "@angular/fire:deploy", + options: {} + }; + + tree.overwrite(workspacePath, JSON.stringify(workspace, null, 2)); + generateFirebaseJson(tree, "firebase.json", options.project, outputPath); + generateFirebaseRc( + tree, + ".firebaserc", + options.firebaseProject, + options.project + ); + return tree; } diff --git a/src/schematics/public_api.ts b/src/schematics/public_api.ts index ad655b255..a0c0eeb20 100644 --- a/src/schematics/public_api.ts +++ b/src/schematics/public_api.ts @@ -1,3 +1,3 @@ -export * from './ng-add'; -export * from './deploy/actions'; -export * from './deploy/builder'; +export * from "./ng-add"; +export * from "./deploy/actions"; +export * from "./deploy/builder"; diff --git a/src/schematics/tsconfig-build.json b/src/schematics/tsconfig-build.json index eda5caa88..ee6afe822 100644 --- a/src/schematics/tsconfig-build.json +++ b/src/schematics/tsconfig-build.json @@ -13,14 +13,17 @@ "declaration": false, "removeComments": true, "strictNullChecks": true, - "lib": ["es2015", "dom", "es2015.promise", "es2015.collection", "es2015.iterable"], + "lib": [ + "es2015", + "dom", + "es2015.promise", + "es2015.collection", + "es2015.iterable" + ], "skipLibCheck": true, "moduleResolution": "node" }, - "files": [ - "index.ts", - "../../node_modules/zone.js/dist/zone.js.d.ts" - ], + "files": ["index.ts", "../../node_modules/zone.js/dist/zone.js.d.ts"], "angularCompilerOptions": { "skipTemplateCodegen": true, "strictMetadataEmit": true, diff --git a/src/schematics/tsconfig-esm.json b/src/schematics/tsconfig-esm.json index b8c37b904..174bca217 100644 --- a/src/schematics/tsconfig-esm.json +++ b/src/schematics/tsconfig-esm.json @@ -1,21 +1,17 @@ { - "extends": "./tsconfig-build.json", - "compilerOptions": { - "target": "es6", - "module": "commonjs", - "outDir": "../../dist/packages-dist/schematics", - "declaration": true - }, - "files": [ - "public_api.ts", - "../../node_modules/zone.js/dist/zone.js.d.ts" - ], - "angularCompilerOptions": { - "skipTemplateCodegen": true, - "strictMetadataEmit": true, - "enableSummariesForJit": false, - "flatModuleOutFile": "index.js", - "flatModuleId": "@angular/fire/schematics" - } + "extends": "./tsconfig-build.json", + "compilerOptions": { + "target": "es6", + "module": "commonjs", + "outDir": "../../dist/packages-dist/schematics", + "declaration": true + }, + "files": ["public_api.ts", "../../node_modules/zone.js/dist/zone.js.d.ts"], + "angularCompilerOptions": { + "skipTemplateCodegen": true, + "strictMetadataEmit": true, + "enableSummariesForJit": false, + "flatModuleOutFile": "index.js", + "flatModuleId": "@angular/fire/schematics" } - \ No newline at end of file +} diff --git a/src/schematics/tsconfig-test.json b/src/schematics/tsconfig-test.json index 48a0cbe35..874330b3e 100644 --- a/src/schematics/tsconfig-test.json +++ b/src/schematics/tsconfig-test.json @@ -2,10 +2,7 @@ "extends": "./tsconfig-esm.json", "compilerOptions": { "baseUrl": ".", - "paths": { } + "paths": {} }, - "files": [ - "index.spec.ts", - "../../node_modules/zone.js/dist/zone.js.d.ts" - ] -} \ No newline at end of file + "files": ["index.spec.ts", "../../node_modules/zone.js/dist/zone.js.d.ts"] +} diff --git a/src/schematics/utils.ts b/src/schematics/utils.ts index f50714e97..aa46aca6a 100644 --- a/src/schematics/utils.ts +++ b/src/schematics/utils.ts @@ -1,61 +1,74 @@ -import { readFileSync } from 'fs'; -import * as inquirer from 'inquirer'; -import { FirebaseRc, Project } from './interfaces'; -import { join } from 'path'; +import { readFileSync } from "fs"; +import * as inquirer from "inquirer"; +import { FirebaseRc, Project } from "./interfaces"; +import { join } from "path"; -const firebase = require('firebase-tools'); +const firebase = require("firebase-tools"); -const fuzzy = require('fuzzy'); +const fuzzy = require("fuzzy"); export function listProjects() { - return firebase.list().catch( - /* If list failed, then login and try again. */ - () => firebase.login().then(() => firebase.list())); + return firebase.list().catch( + /* If list failed, then login and try again. */ + () => firebase.login().then(() => firebase.list()) + ); } - -inquirer.registerPrompt('autocomplete', require('inquirer-autocomplete-prompt')); +inquirer.registerPrompt( + "autocomplete", + require("inquirer-autocomplete-prompt") +); // `fuzzy` passes either the original list of projects or an internal object // which contains the project as a property. const isProject = (elem: Project | { original: Project }): elem is Project => { - return (<{ original: Project }>elem).original === undefined; + return (<{ original: Project }>elem).original === undefined; }; const searchProjects = (projects: Project[]) => { - return (_: any, input: string) => { - return Promise.resolve( - fuzzy - .filter(input, projects, { - extract(el: Project) { - return `${el.id} ${el.name} ${el.permission}`; - } - }) - .map((result: Project | { original: Project }) => { - let original: Project; - if (isProject(result)) { - original = result; - } else { - original = result.original; - } - return {name: `${original.id} (${original.name})`, title: original.name, value: original.id}; - }) - ); - }; + return (_: any, input: string) => { + return Promise.resolve( + fuzzy + .filter(input, projects, { + extract(el: Project) { + return `${el.id} ${el.name} ${el.permission}`; + } + }) + .map((result: Project | { original: Project }) => { + let original: Project; + if (isProject(result)) { + original = result; + } else { + original = result.original; + } + return { + name: `${original.id} (${original.name})`, + title: original.name, + value: original.id + }; + }) + ); + }; }; export const projectPrompt = (projects: Project[]) => { - return (inquirer as any).prompt({ - type: 'autocomplete', - name: 'firebaseProject', - source: searchProjects(projects), - message: 'Please select a project:' - }); + return (inquirer as any).prompt({ + type: "autocomplete", + name: "firebaseProject", + source: searchProjects(projects), + message: "Please select a project:" + }); }; -export function getFirebaseProjectName(projectRoot: string, target: string): string|undefined { - const {targets}: FirebaseRc = JSON.parse(readFileSync(join(projectRoot, '.firebaserc'), 'UTF-8')); - const projects = Object.keys(targets); - return projects.find(project => !!Object.keys(targets[project].hosting).find(t => t === target)); +export function getFirebaseProjectName( + projectRoot: string, + target: string +): string | undefined { + const { targets }: FirebaseRc = JSON.parse( + readFileSync(join(projectRoot, ".firebaserc"), "UTF-8") + ); + const projects = Object.keys(targets); + return projects.find( + project => !!Object.keys(targets[project].hosting).find(t => t === target) + ); } -