From 468e198ee64a59f72764745e9606d98ced81f717 Mon Sep 17 00:00:00 2001 From: James Daniels Date: Wed, 11 Nov 2020 16:25:51 -0500 Subject: [PATCH 1/3] feat(deploy): More deploy options --- src/schematics/deploy/actions.ts | 42 +++++++++++-------- src/schematics/deploy/builder.ts | 14 +++---- src/schematics/deploy/functions-templates.ts | 24 ++++++----- src/schematics/deploy/schema.json | 12 ++++++ src/schematics/interfaces.ts | 5 +++ .../ng9/src/app/app.component.spec.ts | 4 +- 6 files changed, 62 insertions(+), 39 deletions(-) diff --git a/src/schematics/deploy/actions.ts b/src/schematics/deploy/actions.ts index 3d81e4bf2..6569c5044 100644 --- a/src/schematics/deploy/actions.ts +++ b/src/schematics/deploy/actions.ts @@ -1,13 +1,15 @@ import { BuilderContext, targetFromTargetString } from '@angular-devkit/architect'; -import { BuildTarget, FirebaseTools, FSHost } from '../interfaces'; +import { BuildTarget, DeployBuilderSchema, FirebaseTools, FSHost } from '../interfaces'; import { existsSync, readFileSync, renameSync, writeFileSync } from 'fs'; import { copySync, removeSync } from 'fs-extra'; import { dirname, join } from 'path'; import { execSync } from 'child_process'; -import { defaultFunction, defaultPackage, NODE_VERSION } from './functions-templates'; +import { defaultFunction, defaultPackage } from './functions-templates'; import { satisfies } from 'semver'; import * as open from 'open'; +export type DeployBuilderOptions = DeployBuilderSchema & Record; + const escapeRegExp = (str: string) => str.replace(/[\-\[\]\/{}()*+?.\\^$|]/g, '\\$&'); const moveSync = (src: string, dest: string) => { @@ -19,10 +21,10 @@ const deployToHosting = ( firebaseTools: FirebaseTools, context: BuilderContext, workspaceRoot: string, - preview: boolean + options: DeployBuilderOptions ) => { - if (preview) { + if (options.preview) { const port = 5000; // TODO make this configurable setTimeout(() => { @@ -71,7 +73,7 @@ const findPackageVersion = (name: string) => { return match ? match[0].split(`${name}@`)[1] : null; }; -const getPackageJson = (context: BuilderContext, workspaceRoot: string) => { +const getPackageJson = (context: BuilderContext, workspaceRoot: string, options: DeployBuilderOptions) => { const dependencies = { 'firebase-admin': 'latest', 'firebase-functions': 'latest' @@ -108,7 +110,7 @@ const getPackageJson = (context: BuilderContext, workspaceRoot: string) => { }); } } // TODO should we throw? - return defaultPackage(dependencies, devDependencies); + return defaultPackage(dependencies, devDependencies, options); }; export const deployToFunction = async ( @@ -117,14 +119,9 @@ export const deployToFunction = async ( workspaceRoot: string, staticBuildTarget: BuildTarget, serverBuildTarget: BuildTarget, - preview: boolean, + options: DeployBuilderOptions, fsHost: FSHost = defaultFsHost ) => { - if (!satisfies(process.versions.node, getVersionRange(NODE_VERSION))) { - context.logger.warn( - `⚠️ Your Node.js version (${process.versions.node}) does not match the Firebase Functions runtime (${NODE_VERSION}).` - ); - } const staticBuildOptions = await context.getTargetOptions(targetFromTargetString(staticBuildTarget.name)); if (!staticBuildOptions.outputPath || typeof staticBuildOptions.outputPath !== 'string') { @@ -152,14 +149,23 @@ export const deployToFunction = async ( fsHost.moveSync(staticOut, newClientPath); fsHost.moveSync(serverOut, newServerPath); + const packageJson = getPackageJson(context, workspaceRoot, options); + const nodeVersion = JSON.parse(packageJson).engines.node; + + if (!satisfies(process.versions.node, getVersionRange(nodeVersion))) { + context.logger.warn( + `⚠️ Your Node.js version (${process.versions.node}) does not match the Firebase Functions runtime (${nodeVersion}).` + ); + } + fsHost.writeFileSync( join(dirname(serverOut), 'package.json'), - getPackageJson(context, workspaceRoot) + packageJson ); fsHost.writeFileSync( join(dirname(serverOut), 'index.js'), - defaultFunction(serverOut) + defaultFunction(serverOut, options) ); fsHost.renameSync( @@ -167,7 +173,7 @@ export const deployToFunction = async ( join(newClientPath, 'index.original.html') ); - if (preview) { + if (options.preview) { const port = 5000; // TODO make this configurable setTimeout(() => { @@ -206,7 +212,7 @@ export default async function deploy( staticBuildTarget: BuildTarget, serverBuildTarget: BuildTarget | undefined, firebaseProject: string, - preview: boolean + options: DeployBuilderOptions ) { await firebaseTools.login(); @@ -258,14 +264,14 @@ export default async function deploy( context.workspaceRoot, staticBuildTarget, serverBuildTarget, - preview + options ); } else { await deployToHosting( firebaseTools, context, context.workspaceRoot, - preview + options ); } diff --git a/src/schematics/deploy/builder.ts b/src/schematics/deploy/builder.ts index 0242b0e9d..41eaac10c 100644 --- a/src/schematics/deploy/builder.ts +++ b/src/schematics/deploy/builder.ts @@ -1,9 +1,8 @@ import { BuilderContext, BuilderOutput, createBuilder } from '@angular-devkit/architect'; -import deploy from './actions'; -import { BuildTarget, DeployBuilderSchema } from '../interfaces'; +import deploy, { DeployBuilderOptions } from './actions'; +import { BuildTarget } from '../interfaces'; import { getFirebaseProjectName } from '../utils'; -type DeployBuilderOptions = DeployBuilderSchema & Record; // Call the createBuilder() function to create a builder. This mirrors // createJobHandler() but add typings specific to Architect Builders. @@ -13,7 +12,7 @@ export default createBuilder( throw new Error('Cannot deploy the application without a target'); } - const firebaseProject = getFirebaseProjectName( + const firebaseProject = options.firebaseProject || getFirebaseProjectName( context.workspaceRoot, context.target.project ); @@ -27,10 +26,7 @@ export default createBuilder( let serverBuildTarget: BuildTarget | undefined; if (options.ssr) { serverBuildTarget = { - name: options.universalBuildTarget || `${context.target.project}:server:production`, - options: { - bundleDependencies: 'all' - } + name: options.universalBuildTarget || `${context.target.project}:server:production` }; } @@ -41,7 +37,7 @@ export default createBuilder( staticBuildTarget, serverBuildTarget, firebaseProject, - !!options.preview + options, ); } catch (e) { console.error('Error when trying to deploy: '); diff --git a/src/schematics/deploy/functions-templates.ts b/src/schematics/deploy/functions-templates.ts index ad67a3b8e..d28342232 100644 --- a/src/schematics/deploy/functions-templates.ts +++ b/src/schematics/deploy/functions-templates.ts @@ -1,15 +1,18 @@ +import { DeployBuilderOptions } from './actions'; + // TODO allow these to be configured -export const NODE_VERSION = 10; -const FUNCTION_NAME = 'ssr'; -const FUNCTION_REGION = 'us-central1'; -const RUNTIME_OPTIONS = { +const DEFAULT_NODE_VERSION = 10; +const DEFAULT_FUNCTION_NAME = 'ssr'; +const DEFAULT_FUNCTION_REGION = 'us-central1'; +const DEFAULT_RUNTIME_OPTIONS = { timeoutSeconds: 60, memory: '1GB' }; export const defaultPackage = ( dependencies: {[key: string]: string}, - devDependencies: {[key: string]: string} + devDependencies: {[key: string]: string}, + options: DeployBuilderOptions, ) => `{ "name": "functions", "description": "Angular Universal Application", @@ -22,7 +25,7 @@ export const defaultPackage = ( "logs": "firebase functions:log" }, "engines": { - "node": "${NODE_VERSION}" + "node": "${options.functionsNodeVersion || DEFAULT_NODE_VERSION}" }, "dependencies": ${JSON.stringify(dependencies, null, 4)}, "devDependencies": ${JSON.stringify(devDependencies, null, 4)}, @@ -31,7 +34,8 @@ export const defaultPackage = ( `; export const defaultFunction = ( - path: string + path: string, + options: DeployBuilderOptions, ) => `const functions = require('firebase-functions'); // Increase readability in Cloud Logging @@ -39,9 +43,9 @@ require("firebase-functions/lib/logger/compat"); const expressApp = require('./${path}/main').app(); -exports.${FUNCTION_NAME} = functions - .region('${FUNCTION_REGION}') - .runWith(${JSON.stringify(RUNTIME_OPTIONS)}) +exports.${DEFAULT_FUNCTION_NAME} = functions + .region('${DEFAULT_FUNCTION_REGION}') + .runWith(${JSON.stringify(options.functionsRuntimeOptions || DEFAULT_RUNTIME_OPTIONS)}) .https .onRequest(expressApp); `; diff --git a/src/schematics/deploy/schema.json b/src/schematics/deploy/schema.json index a48deed07..df687f668 100644 --- a/src/schematics/deploy/schema.json +++ b/src/schematics/deploy/schema.json @@ -9,6 +9,18 @@ "description": "Target to build.", "pattern": "^[^:\\s]+:[^:\\s]+(:[^\\s]+)?$" }, + "firebaseProject": { + "type": "string", + "description": "The Firebase project name or project alias to use when deploying" + }, + "functionsNodeVersion": { + "type": "number", + "description": "Version of Node.js to run Cloud Functions on" + }, + "functionsRuntimeOptions": { + "type": "object", + "description": "Runtime options for Cloud Functions" + }, "preview": { "type": "boolean", "description": "Do not deploy the application, just set up the Firebase Function in the project output directory. Can be used for testing the Firebase Function with `firebase serve`." diff --git a/src/schematics/interfaces.ts b/src/schematics/interfaces.ts index d4c64874e..7d22c6516 100644 --- a/src/schematics/interfaces.ts +++ b/src/schematics/interfaces.ts @@ -1,3 +1,5 @@ +import { RuntimeOptions } from 'firebase-functions'; + export interface Project { projectId: string; projectNumber: string; @@ -59,9 +61,12 @@ export interface FirebaseRc { export interface DeployBuilderSchema { buildTarget?: string; + firebaseProject?: string; preview?: boolean; universalBuildTarget?: string; ssr?: boolean; + functionsNodeVersion?: number; + functionsRuntimeOptions?: RuntimeOptions; } export interface BuildTarget { diff --git a/test/ng-build/ng9/src/app/app.component.spec.ts b/test/ng-build/ng9/src/app/app.component.spec.ts index dd8cd7669..15ecd03d5 100644 --- a/test/ng-build/ng9/src/app/app.component.spec.ts +++ b/test/ng-build/ng9/src/app/app.component.spec.ts @@ -1,8 +1,8 @@ -import { TestBed, async } from '@angular/core/testing'; +import { TestBed, waitForAsync } from '@angular/core/testing'; import { AppComponent } from './app.component'; describe('AppComponent', () => { - beforeEach(async(() => { + beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ declarations: [ AppComponent From 46cf2d633b7afbc40088af9ca4094c296850efe8 Mon Sep 17 00:00:00 2001 From: James Daniels Date: Wed, 11 Nov 2020 17:37:18 -0500 Subject: [PATCH 2/3] Configuring Cloud Functions --- docs/deploy/getting-started.md | 53 ++++++++++++++++++++++++++++++++++ 1 file changed, 53 insertions(+) diff --git a/docs/deploy/getting-started.md b/docs/deploy/getting-started.md index b9fb66276..c2f4a7e59 100644 --- a/docs/deploy/getting-started.md +++ b/docs/deploy/getting-started.md @@ -88,3 +88,56 @@ We'll create the function and a `package.json` in your project output directory. ## Step 3: customization To customize the deployment flow, you can use the configuration files you're already familiar with from `firebase-tools`. You can find more in the [firebase documentation](https://firebase.google.com/docs/hosting/full-config). + +### Configuring Cloud Functions + +Setting `functionsNodeVersion` and `functionsRuntimeOptions` in your `angular.json` allow you to custimze the version of Node.js Cloud Functions is running and run-time settings like timeout, VPC connectors, and memory. + +```json +"deploy": { + "builder": "@angular/fire:deploy", + "options": { + "functionsNodeVersion": 12, + "functionsRuntimeOptions": { + "memory": "2GB", + "timeoutSeconds": 10, + "vpcConnector": "my-vpc-connector", + "vpcConnectorEgressSettings": "PRIVATE_RANGES_ONLY" + } + } +} +``` + +### Working with multiple Firebase Projects + +If you have multiple build targets and deploy targets, it is possible to specify them in your `angular.json` or `workspace.json`. + +It is possible to use either your project name or project alias in `firebaseProject`. The setting provided here is equivalent to passing a project name or alias to `firebase deploy --project projectNameOrAlias`. + +The `buildTarget` simply points to an existing build configuration for your project. Most projects have a default configuration and a production configuration (commonly activated by using the `--prod` flag) but it is possible to specify as many build configurations as needed. + +You may specify a `buildTarget` and `firebaseProject` in your `options` as follows: + +```json +"deploy": { + "builder": "@angular/fire:deploy", + "options": { + "buildTarget": "projectName:build", + "firebaseProject": "developmentProject" + }, + "configurations": { + "production": { + "buildTarget": "projectName:build:production", + "firebaseProject": "productionProject" + } + } +} +``` + +The above configuration specifies the following: + +1. `ng deploy` will deploy the default project with default configuration. +2. `ng deploy projectName` will deploy the specified project with default configuration. +3. `ng deploy projectName --prod` or `ng deploy projectName --configuration='production'` will deploy `projectName` with production build settings to your production environment. + +All of the options are optional. If you do not specify a `buildTarget`, it defaults to a production build (`projectName:build:production`). If you do not specify a `firebaseProject`, it defaults to the first matching deploy target found in your `.firebaserc` (where your projectName is the same as your Firebase deploy target name). The `configurations` section is also optional. \ No newline at end of file From ed215dc6da71ff0c10d7cf53cb948a40fd6e0af5 Mon Sep 17 00:00:00 2001 From: James Daniels Date: Wed, 11 Nov 2020 17:49:18 -0500 Subject: [PATCH 3/3] Drop the deduped stuff --- sample/angular.json | 6 +++++- src/schematics/deploy/actions.ts | 2 +- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/sample/angular.json b/sample/angular.json index 169743b1d..efc85ec64 100644 --- a/sample/angular.json +++ b/sample/angular.json @@ -194,7 +194,11 @@ "deploy": { "builder": "@angular/fire:deploy", "options": { - "ssr": true + "ssr": true, + "functionsNodeVersion": 12, + "functionsRuntimeOptions": { + "memory": "1GB" + } } } } diff --git a/src/schematics/deploy/actions.ts b/src/schematics/deploy/actions.ts index 45eaf6fe2..aeabc0642 100644 --- a/src/schematics/deploy/actions.ts +++ b/src/schematics/deploy/actions.ts @@ -73,7 +73,7 @@ const getVersionRange = (v: number) => `^${v}.0.0`; const findPackageVersion = (name: string) => { const match = execSync(`npm list ${name}`).toString().match(` ${escapeRegExp(name)}@.+\\w`); - return match ? match[0].split(`${name}@`)[1] : null; + return match ? match[0].split(`${name}@`)[1].split(/\s/)[0] : null; }; const getPackageJson = (context: BuilderContext, workspaceRoot: string, options: DeployBuilderOptions) => {