Skip to content

Commit 1bbd3e4

Browse files
feat(deploy): More deploy options (#2647)
* Adding `functionsNodeVersion` option to allow configuration of Cloud Functions Node.js version * Adding `functionsRuntimeOptions` option to allow configuration of memory, timeout, VPC connectors, etc. * Adding `firebaseProject` option to allow finer grain deployments * Fixing NPM version parsing for external dependencies * Fixing the bundle all warning Co-authored-by: George <[email protected]>
1 parent 5cdb8ce commit 1bbd3e4

File tree

9 files changed

+159
-52
lines changed

9 files changed

+159
-52
lines changed

docs/deploy/getting-started.md

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,3 +88,56 @@ We'll create the function and a `package.json` in your project output directory.
8888
## Step 3: customization
8989

9090
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).
91+
92+
### Configuring Cloud Functions
93+
94+
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.
95+
96+
```json
97+
"deploy": {
98+
"builder": "@angular/fire:deploy",
99+
"options": {
100+
"functionsNodeVersion": 12,
101+
"functionsRuntimeOptions": {
102+
"memory": "2GB",
103+
"timeoutSeconds": 10,
104+
"vpcConnector": "my-vpc-connector",
105+
"vpcConnectorEgressSettings": "PRIVATE_RANGES_ONLY"
106+
}
107+
}
108+
}
109+
```
110+
111+
### Working with multiple Firebase Projects
112+
113+
If you have multiple build targets and deploy targets, it is possible to specify them in your `angular.json` or `workspace.json`.
114+
115+
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`.
116+
117+
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.
118+
119+
You may specify a `buildTarget` and `firebaseProject` in your `options` as follows:
120+
121+
```json
122+
"deploy": {
123+
"builder": "@angular/fire:deploy",
124+
"options": {
125+
"buildTarget": "projectName:build",
126+
"firebaseProject": "developmentProject"
127+
},
128+
"configurations": {
129+
"production": {
130+
"buildTarget": "projectName:build:production",
131+
"firebaseProject": "productionProject"
132+
}
133+
}
134+
}
135+
```
136+
137+
The above configuration specifies the following:
138+
139+
1. `ng deploy` will deploy the default project with default configuration.
140+
2. `ng deploy projectName` will deploy the specified project with default configuration.
141+
3. `ng deploy projectName --prod` or `ng deploy projectName --configuration='production'` will deploy `projectName` with production build settings to your production environment.
142+
143+
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.

sample/angular.json

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -194,7 +194,11 @@
194194
"deploy": {
195195
"builder": "@angular/fire:deploy",
196196
"options": {
197-
"ssr": true
197+
"ssr": true,
198+
"functionsNodeVersion": 12,
199+
"functionsRuntimeOptions": {
200+
"memory": "1GB"
201+
}
198202
}
199203
}
200204
}

src/schematics/deploy/actions.jasmine.ts

Lines changed: 37 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -82,19 +82,19 @@ describe('Deploy Angular apps', () => {
8282

8383
it('should call login', async () => {
8484
const spy = spyOn(firebaseMock, 'login');
85-
await deploy(firebaseMock, context, STATIC_BUILD_TARGET, undefined, FIREBASE_PROJECT, false);
85+
await deploy(firebaseMock, context, STATIC_BUILD_TARGET, undefined, FIREBASE_PROJECT, { preview: false });
8686
expect(spy).toHaveBeenCalled();
8787
});
8888

8989
it('should not call login', async () => {
9090
const spy = spyOn(firebaseMock, 'login');
91-
await deploy(firebaseMock, context, STATIC_BUILD_TARGET, undefined, FIREBASE_PROJECT, false, FIREBASE_TOKEN);
91+
await deploy(firebaseMock, context, STATIC_BUILD_TARGET, undefined, FIREBASE_PROJECT, { preview: false }, FIREBASE_TOKEN);
9292
expect(spy).not.toHaveBeenCalled();
9393
});
9494

9595
it('should invoke the builder', async () => {
9696
const spy = spyOn(context, 'scheduleTarget').and.callThrough();
97-
await deploy(firebaseMock, context, STATIC_BUILD_TARGET, undefined, FIREBASE_PROJECT, false);
97+
await deploy(firebaseMock, context, STATIC_BUILD_TARGET, undefined, FIREBASE_PROJECT, { preview: false });
9898
expect(spy).toHaveBeenCalled();
9999
expect(spy).toHaveBeenCalledWith({
100100
target: 'build',
@@ -109,14 +109,14 @@ describe('Deploy Angular apps', () => {
109109
options: {}
110110
};
111111
const spy = spyOn(context, 'scheduleTarget').and.callThrough();
112-
await deploy(firebaseMock, context, buildTarget, undefined, FIREBASE_PROJECT, false);
112+
await deploy(firebaseMock, context, buildTarget, undefined, FIREBASE_PROJECT, { preview: false });
113113
expect(spy).toHaveBeenCalled();
114114
expect(spy).toHaveBeenCalledWith({ target: 'prerender', project: PROJECT }, {});
115115
});
116116

117117
it('should invoke firebase.deploy', async () => {
118118
const spy = spyOn(firebaseMock, 'deploy').and.callThrough();
119-
await deploy(firebaseMock, context, STATIC_BUILD_TARGET, undefined, FIREBASE_PROJECT, false, FIREBASE_TOKEN);
119+
await deploy(firebaseMock, context, STATIC_BUILD_TARGET, undefined, FIREBASE_PROJECT, { preview: false }, FIREBASE_TOKEN);
120120
expect(spy).toHaveBeenCalled();
121121
expect(spy).toHaveBeenCalledWith({
122122
cwd: 'cwd',
@@ -128,7 +128,7 @@ describe('Deploy Angular apps', () => {
128128
describe('error handling', () => {
129129
it('throws if there is no firebase project', async () => {
130130
try {
131-
await deploy(firebaseMock, context, STATIC_BUILD_TARGET, undefined, undefined, false);
131+
await deploy(firebaseMock, context, STATIC_BUILD_TARGET, undefined, undefined, { preview: false });
132132
} catch (e) {
133133
console.log(e);
134134
expect(e.message).toMatch(/Cannot find firebase project/);
@@ -138,7 +138,7 @@ describe('Deploy Angular apps', () => {
138138
it('throws if there is no target project', async () => {
139139
context.target = undefined;
140140
try {
141-
await deploy(firebaseMock, context, STATIC_BUILD_TARGET, undefined, FIREBASE_PROJECT, false);
141+
await deploy(firebaseMock, context, STATIC_BUILD_TARGET, undefined, FIREBASE_PROJECT, { preview: false });
142142
} catch (e) {
143143
expect(e.message).toMatch(/Cannot execute the build target/);
144144
}
@@ -151,7 +151,16 @@ describe('universal deployment', () => {
151151

152152
it('should create a firebase function', async () => {
153153
const spy = spyOn(fsHost, 'writeFileSync');
154-
await deployToFunction(firebaseMock, context, '/home/user', STATIC_BUILD_TARGET, SERVER_BUILD_TARGET, false, undefined, fsHost);
154+
await deployToFunction(
155+
firebaseMock,
156+
context,
157+
'/home/user',
158+
STATIC_BUILD_TARGET,
159+
SERVER_BUILD_TARGET,
160+
{ preview: false },
161+
undefined,
162+
fsHost
163+
);
155164

156165
expect(spy).toHaveBeenCalledTimes(2);
157166

@@ -164,7 +173,16 @@ describe('universal deployment', () => {
164173

165174
it('should rename the index.html file in the nested dist', async () => {
166175
const spy = spyOn(fsHost, 'renameSync');
167-
await deployToFunction(firebaseMock, context, '/home/user', STATIC_BUILD_TARGET, SERVER_BUILD_TARGET, false, undefined, fsHost);
176+
await deployToFunction(
177+
firebaseMock,
178+
context,
179+
'/home/user',
180+
STATIC_BUILD_TARGET,
181+
SERVER_BUILD_TARGET,
182+
{ preview: false },
183+
undefined,
184+
fsHost
185+
);
168186

169187
expect(spy).toHaveBeenCalledTimes(1);
170188

@@ -178,7 +196,16 @@ describe('universal deployment', () => {
178196

179197
it('should invoke firebase.deploy', async () => {
180198
const spy = spyOn(firebaseMock, 'deploy');
181-
await deployToFunction(firebaseMock, context, '/home/user', STATIC_BUILD_TARGET, SERVER_BUILD_TARGET, false, undefined, fsHost);
199+
await deployToFunction(
200+
firebaseMock,
201+
context,
202+
'/home/user',
203+
STATIC_BUILD_TARGET,
204+
SERVER_BUILD_TARGET,
205+
{ preview: false },
206+
undefined,
207+
fsHost
208+
);
182209

183210
expect(spy).toHaveBeenCalledTimes(1);
184211
});

src/schematics/deploy/actions.ts

Lines changed: 26 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,15 @@
11
import { BuilderContext, targetFromTargetString } from '@angular-devkit/architect';
2-
import { BuildTarget, FirebaseTools, FSHost } from '../interfaces';
2+
import { BuildTarget, DeployBuilderSchema, FirebaseTools, FSHost } from '../interfaces';
33
import { existsSync, readFileSync, renameSync, writeFileSync } from 'fs';
44
import { copySync, removeSync } from 'fs-extra';
55
import { dirname, join } from 'path';
66
import { execSync } from 'child_process';
7-
import { defaultFunction, defaultPackage, NODE_VERSION } from './functions-templates';
7+
import { defaultFunction, defaultPackage } from './functions-templates';
88
import { satisfies } from 'semver';
99
import * as open from 'open';
1010

11+
export type DeployBuilderOptions = DeployBuilderSchema | Record<string, string>;
12+
1113
const escapeRegExp = (str: string) => str.replace(/[\-\[\]\/{}()*+?.\\^$|]/g, '\\$&');
1214

1315
const moveSync = (src: string, dest: string) => {
@@ -19,11 +21,11 @@ const deployToHosting = (
1921
firebaseTools: FirebaseTools,
2022
context: BuilderContext,
2123
workspaceRoot: string,
22-
preview: boolean,
24+
options: DeployBuilderOptions,
2325
firebaseToken?: string,
2426
) => {
2527

26-
if (preview) {
28+
if (options.preview) {
2729
const port = 5000; // TODO make this configurable
2830

2931
setTimeout(() => {
@@ -71,10 +73,10 @@ const getVersionRange = (v: number) => `^${v}.0.0`;
7173

7274
const findPackageVersion = (name: string) => {
7375
const match = execSync(`npm list ${name}`).toString().match(` ${escapeRegExp(name)}@.+\\w`);
74-
return match ? match[0].split(`${name}@`)[1] : null;
76+
return match ? match[0].split(`${name}@`)[1].split(/\s/)[0] : null;
7577
};
7678

77-
const getPackageJson = (context: BuilderContext, workspaceRoot: string) => {
79+
const getPackageJson = (context: BuilderContext, workspaceRoot: string, options: DeployBuilderOptions) => {
7880
const dependencies = {
7981
'firebase-admin': 'latest',
8082
'firebase-functions': 'latest'
@@ -111,7 +113,7 @@ const getPackageJson = (context: BuilderContext, workspaceRoot: string) => {
111113
});
112114
}
113115
} // TODO should we throw?
114-
return defaultPackage(dependencies, devDependencies);
116+
return defaultPackage(dependencies, devDependencies, options);
115117
};
116118

117119
export const deployToFunction = async (
@@ -120,15 +122,10 @@ export const deployToFunction = async (
120122
workspaceRoot: string,
121123
staticBuildTarget: BuildTarget,
122124
serverBuildTarget: BuildTarget,
123-
preview: boolean,
125+
options: DeployBuilderOptions,
124126
firebaseToken?: string,
125-
fsHost: FSHost = defaultFsHost,
127+
fsHost: FSHost = defaultFsHost
126128
) => {
127-
if (!satisfies(process.versions.node, getVersionRange(NODE_VERSION))) {
128-
context.logger.warn(
129-
`⚠️ Your Node.js version (${process.versions.node}) does not match the Firebase Functions runtime (${NODE_VERSION}).`
130-
);
131-
}
132129

133130
const staticBuildOptions = await context.getTargetOptions(targetFromTargetString(staticBuildTarget.name));
134131
if (!staticBuildOptions.outputPath || typeof staticBuildOptions.outputPath !== 'string') {
@@ -156,22 +153,31 @@ export const deployToFunction = async (
156153
fsHost.moveSync(staticOut, newClientPath);
157154
fsHost.moveSync(serverOut, newServerPath);
158155

156+
const packageJson = getPackageJson(context, workspaceRoot, options);
157+
const nodeVersion = JSON.parse(packageJson).engines.node;
158+
159+
if (!satisfies(process.versions.node, getVersionRange(nodeVersion))) {
160+
context.logger.warn(
161+
`⚠️ Your Node.js version (${process.versions.node}) does not match the Firebase Functions runtime (${nodeVersion}).`
162+
);
163+
}
164+
159165
fsHost.writeFileSync(
160166
join(dirname(serverOut), 'package.json'),
161-
getPackageJson(context, workspaceRoot)
167+
packageJson
162168
);
163169

164170
fsHost.writeFileSync(
165171
join(dirname(serverOut), 'index.js'),
166-
defaultFunction(serverOut)
172+
defaultFunction(serverOut, options)
167173
);
168174

169175
fsHost.renameSync(
170176
join(newClientPath, 'index.html'),
171177
join(newClientPath, 'index.original.html')
172178
);
173179

174-
if (preview) {
180+
if (options.preview) {
175181
const port = 5000; // TODO make this configurable
176182

177183
setTimeout(() => {
@@ -211,7 +217,7 @@ export default async function deploy(
211217
staticBuildTarget: BuildTarget,
212218
serverBuildTarget: BuildTarget | undefined,
213219
firebaseProject: string,
214-
preview: boolean,
220+
options: DeployBuilderOptions,
215221
firebaseToken?: string,
216222
) {
217223
if (!firebaseToken) {
@@ -266,15 +272,15 @@ export default async function deploy(
266272
context.workspaceRoot,
267273
staticBuildTarget,
268274
serverBuildTarget,
269-
preview,
275+
options,
270276
firebaseToken,
271277
);
272278
} else {
273279
await deployToHosting(
274280
firebaseTools,
275281
context,
276282
context.workspaceRoot,
277-
preview,
283+
options,
278284
firebaseToken,
279285
);
280286
}

src/schematics/deploy/builder.ts

Lines changed: 5 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,8 @@
11
import { BuilderContext, BuilderOutput, createBuilder } from '@angular-devkit/architect';
2-
import deploy from './actions';
3-
import { BuildTarget, DeployBuilderSchema } from '../interfaces';
2+
import deploy, { DeployBuilderOptions } from './actions';
3+
import { BuildTarget } from '../interfaces';
44
import { getFirebaseProjectName } from '../utils';
55

6-
type DeployBuilderOptions = DeployBuilderSchema & Record<string, string>;
76

87
// Call the createBuilder() function to create a builder. This mirrors
98
// createJobHandler() but add typings specific to Architect Builders.
@@ -13,7 +12,7 @@ export default createBuilder(
1312
throw new Error('Cannot deploy the application without a target');
1413
}
1514

16-
const firebaseProject = getFirebaseProjectName(
15+
const firebaseProject = options.firebaseProject || getFirebaseProjectName(
1716
context.workspaceRoot,
1817
context.target.project
1918
);
@@ -27,10 +26,7 @@ export default createBuilder(
2726
let serverBuildTarget: BuildTarget | undefined;
2827
if (options.ssr) {
2928
serverBuildTarget = {
30-
name: options.universalBuildTarget || `${context.target.project}:server:production`,
31-
options: {
32-
bundleDependencies: 'all'
33-
}
29+
name: options.universalBuildTarget || `${context.target.project}:server:production`
3430
};
3531
}
3632

@@ -41,7 +37,7 @@ export default createBuilder(
4137
staticBuildTarget,
4238
serverBuildTarget,
4339
firebaseProject,
44-
!!options.preview,
40+
options,
4541
process.env.FIREBASE_TOKEN,
4642
);
4743
} catch (e) {

0 commit comments

Comments
 (0)