Skip to content

Commit 882b254

Browse files
feat(schematics): firebase function deployment for angular universal (#2305)
1. Refactoring of the `ng-add` schematics. It decomposes the function to two separate ones responsible for static file deployments and SSR. Unfortunately, I wasn't able to get rid of the extra schematic from `collection.json` since currently our APIs do not allow manually persisting the `Tree` on the disk. 2. Minor refactoring of the `deploy` builder to incorporate the functionality for server-side rendering enabled deployments. 3. Refactoring of the tests to reflect the updated structure of `ng-add` and the deploy action. 4. Implementation of deployment to Firebase functions. This implementation supports Angular Universal version 9 and above. Originally I was thinking of checking the dependency versions manually with `semver` during `ng deploy`/`ng add`, but then decided that the peer dependency check that `@angular/fire` does might be sufficient. Co-authored-by: NothingEverHappens <[email protected]>
1 parent fb4159d commit 882b254

21 files changed

+1624
-480
lines changed

angular.json

+43-39
Original file line numberDiff line numberDiff line change
@@ -1,42 +1,46 @@
11
{
2-
"$schema": "./node_modules/@angular/cli/lib/config/schema.json",
3-
"version": 1,
4-
"newProjectRoot": ".",
5-
"projects": {
6-
"angularfire": {
7-
"projectType": "library",
8-
"root": "src",
9-
"sourceRoot": "src",
10-
"prefix": "angularfire",
11-
"architect": {
12-
"build": {
13-
"builder": "@angular-devkit/build-ng-packagr:build",
14-
"options": {
15-
"tsConfig": "tsconfig.json",
16-
"project": "src/package.json"
17-
}
18-
},
19-
"test": {
20-
"builder": "@angular-devkit/build-angular:karma",
21-
"options": {
22-
"main": "src/test.ts",
23-
"tsConfig": "tsconfig.spec.json",
24-
"karmaConfig": "karma.conf.js"
25-
}
26-
},
27-
"lint": {
28-
"builder": "@angular-devkit/build-angular:tslint",
29-
"options": {
30-
"tsConfig": [
31-
"tsconfig.json",
32-
"tsconfig.spec.json"
33-
],
34-
"exclude": [
35-
"**/node_modules/**"
36-
]
37-
}
2+
"$schema": "./node_modules/@angular/cli/lib/config/schema.json",
3+
"version": 1,
4+
"newProjectRoot": ".",
5+
"projects": {
6+
"angularfire": {
7+
"projectType": "library",
8+
"root": "src",
9+
"sourceRoot": "src",
10+
"prefix": "angularfire",
11+
"architect": {
12+
"build": {
13+
"builder": "@angular-devkit/build-ng-packagr:build",
14+
"options": {
15+
"tsConfig": "tsconfig.json",
16+
"project": "src/package.json"
17+
}
18+
},
19+
"test": {
20+
"builder": "@angular-devkit/build-angular:karma",
21+
"options": {
22+
"main": "src/test.ts",
23+
"tsConfig": "tsconfig.spec.json",
24+
"karmaConfig": "karma.conf.js"
25+
}
26+
},
27+
"lint": {
28+
"builder": "@angular-devkit/build-angular:tslint",
29+
"options": {
30+
"tsConfig": [
31+
"tsconfig.json",
32+
"tsconfig.spec.json"
33+
],
34+
"exclude": [
35+
"**/node_modules/**"
36+
]
3837
}
3938
}
40-
}},
41-
"defaultProject": "angularfire"
42-
}
39+
}
40+
}
41+
},
42+
"defaultProject": "angularfire",
43+
"cli": {
44+
"analytics": "86795b8f-9036-4a53-929c-a7303453d677"
45+
}
46+
}

docs/deploy/getting-started.md

+30-6
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
1-
# Deploy your application on Firebase Hosting
1+
# Deploy your application on Firebase Hosting & Functions
22

3-
In this guide, we'll look at how to use `@angular/fire` to automatically deploy an Angular application to Firebase hosting by using the Angular CLI.
3+
In this guide, we'll look at how to use `@angular/fire` to automatically deploy an Angular application to Firebase hosting or functions by using the Angular CLI.
4+
5+
`@angular/fire` uses Firebase functions to deploy your Angular universal projects, with server-side rendering enabled.
6+
7+
**Angular Universal deployments work with `@nguniversal/*` version 9.0.0 and above**.
48

59
## Step 1: add `@angular/fire` to your project
610

@@ -12,7 +16,9 @@ ng add @angular/fire
1216

1317
*Note that the command above assumes you have global Angular CLI installed. To install Angular CLI globally run `npm i -g @angular/cli`.*
1418

15-
The command above will trigger the `@angular/fire` `ng-add` schematics. The schematics will open a web browser and guide you through the Firebase authentication flow (if you're not signed in already). After you authenticate, you'll see a prompt to select a Firebase hosting project.
19+
First, the command above will check if you have an Angular universal project. It'll do so by looking at your `angular.json` project, looking for a `server` target for the specified project. If it finds one, it'll ask you if you want to deploy the project in a firebase function.
20+
21+
After that it will trigger the `@angular/fire` `ng-add` schematics. The schematics will open a web browser and guide you through the Firebase authentication flow (if you're not signed in already). After you authenticate, you'll see a prompt to select a Firebase hosting project.
1622

1723
The schematics will do the following:
1824

@@ -22,7 +28,7 @@ The schematics will do the following:
2228

2329
In the end, your `angular.json` project will look like below:
2430

25-
```json
31+
```js
2632
{
2733
"$schema": "./node_modules/@angular/cli/lib/config/schema.json",
2834
"version": 1,
@@ -32,7 +38,9 @@ In the end, your `angular.json` project will look like below:
3238
// ...
3339
"deploy": {
3440
"builder": "@angular/fire:deploy",
35-
"options": {}
41+
"options": {} // Here you may find an "ssr": true option if you've
42+
// selected that you want to deploy your Angular universal project
43+
// as a firebase function.
3644
}
3745
}
3846
}
@@ -53,14 +61,30 @@ ng add @angular/fire --project=[PROJECT_NAME]
5361
As the second step, to deploy your project run:
5462

5563
```
56-
ng run [ANGULAR_PROJECT_NAME]:deploy
64+
ng deploy --project=[PROJECT_NAME]
5765
```
5866

67+
*The `--project` option is optional. Learn more [here](https://angular.io/cli/deploy).*
68+
5969
The command above will trigger:
6070

6171
1. Production build of your application
6272
2. Deployment of the produced assets to the firebase hosting project you selected during `ng add`
6373

74+
If you've specified that you want a server-side rendering enabled deployment in a firebase function, the command will also:
75+
76+
1. Create a firebase function in `dist`, which directly consumes `main.js` from your server output directory.
77+
2. Create `package.json` for the firebase function with the required dependencies.
78+
3. Deploy the static assets to firebase hosting and your universal server as a Firebase function.
79+
80+
If you want to preview your Angular Universal project before we deploy it as a Firebase Function you can run:
81+
82+
```
83+
ng deploy --preview
84+
```
85+
86+
We'll create the function and a `package.json` in your project output directory. This way, you can later run `firebase serve` in your project root so you can test everything before deploying.
87+
6488
## Step 3: customization
6589

6690
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).

package.json

+6-1
Original file line numberDiff line numberDiff line change
@@ -44,12 +44,16 @@
4444
"@angular/platform-browser-dynamic": "^9.0.0-0 || ^9.0.0 || ^10.0.0-0",
4545
"@angular/router": "^9.0.0-0 || ^9.0.0 || ^10.0.0-0",
4646
"firebase": "^7.8.0",
47+
"firebase-admin": "^8.9.2",
48+
"firebase-functions": "^3.3.0",
4749
"firebase-tools": "^7.12.1",
50+
"fs-extra": "^8.0.1",
4851
"fuzzy": "^0.1.3",
4952
"inquirer": "^6.2.2",
5053
"inquirer-autocomplete-prompt": "^1.0.1",
5154
"rxfire": "^3.9.7",
5255
"rxjs": "^6.5.3",
56+
"semver": "^7.1.3",
5357
"tslib": "^1.10.0",
5458
"ws": "^7.2.1",
5559
"xhr2": "^0.1.4",
@@ -72,10 +76,11 @@
7276
"@types/jasmine": "^3.3.13",
7377
"@types/node": "^12.6.2",
7478
"@types/request": "0.0.30",
79+
"@types/semver": "^7.1.0",
7580
"codelyzer": "^5.0.0",
7681
"concurrently": "^2.2.0",
7782
"conventional-changelog-cli": "^1.2.0",
78-
"fs-extra": "^8.0.1",
83+
"firebase-functions-test": "^0.1.7",
7984
"gzip-size": "^5.1.1",
8085
"jasmine": "^3.4.0",
8186
"jasmine-core": "^3.4.0",

src/core/collection.json

+2-2
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,9 @@
55
"description": "Add firebase deploy schematic",
66
"factory": "./schematics/public_api#ngAdd"
77
},
8-
"ng-add-setup-firebase-deploy": {
8+
"ng-add-setup-project": {
99
"description": "Setup ng deploy",
10-
"factory": "./schematics/public_api#setupNgDeploy"
10+
"factory": "./schematics/public_api#ngAddSetupProject"
1111
}
1212
}
1313
}

src/schematics/deploy/actions.jasmine.ts

+88-16
Original file line numberDiff line numberDiff line change
@@ -1,76 +1,148 @@
1-
import { JsonObject, logging } from '@angular-devkit/core';
1+
import {experimental, JsonObject, logging} from '@angular-devkit/core';
22
import { BuilderContext, BuilderRun, ScheduleOptions, Target, } from '@angular-devkit/architect';
3-
import { FirebaseTools, FirebaseDeployConfig } from '../interfaces';
4-
import deploy from './actions';
3+
import {FirebaseTools, FirebaseDeployConfig, BuildTarget, FSHost} from '../interfaces';
4+
import deploy, {deployToFunction} from './actions';
55

66

77
let context: BuilderContext;
88
let firebaseMock: FirebaseTools;
9+
let fsHost: FSHost;
910

1011
const FIREBASE_PROJECT = 'ikachu-aa3ef';
1112
const PROJECT = 'pirojok-project';
12-
const BUILD_TARGET = `${PROJECT}:build:production`;
13+
const BUILD_TARGET: BuildTarget = {
14+
name: `${PROJECT}:build:production`
15+
};
16+
17+
const projectTargets: experimental.workspace.WorkspaceTool = {
18+
build: {
19+
options: {
20+
outputPath: 'dist/browser'
21+
}
22+
},
23+
server: {
24+
options: {
25+
outputPath: 'dist/server'
26+
}
27+
}
28+
};
1329

1430
describe('Deploy Angular apps', () => {
1531
beforeEach(() => initMocks());
1632

1733
it('should call login', async () => {
1834
const spy = spyOn(firebaseMock, 'login');
19-
await deploy(firebaseMock, context, 'host', BUILD_TARGET, FIREBASE_PROJECT);
35+
await deploy(firebaseMock, context, projectTargets, [BUILD_TARGET], FIREBASE_PROJECT, false, false);
2036
expect(spy).toHaveBeenCalled();
2137
});
2238

2339
it('should invoke the builder', async () => {
2440
const spy = spyOn(context, 'scheduleTarget').and.callThrough();
25-
await deploy(firebaseMock, context, 'host', BUILD_TARGET, FIREBASE_PROJECT);
41+
await deploy(firebaseMock, context, projectTargets, [BUILD_TARGET], FIREBASE_PROJECT, false, false);
2642
expect(spy).toHaveBeenCalled();
2743
expect(spy).toHaveBeenCalledWith({
2844
target: 'build',
2945
configuration: 'production',
3046
project: PROJECT
31-
});
47+
}, undefined);
3248
});
3349

3450
it('should allow the buildTarget to be specified', async () => {
35-
const buildTarget = `${PROJECT}:prerender`;
51+
const buildTarget = {
52+
name: `${PROJECT}:prerender`,
53+
options: {}
54+
};
3655
const spy = spyOn(context, 'scheduleTarget').and.callThrough();
37-
await deploy(firebaseMock, context, 'host', buildTarget, FIREBASE_PROJECT);
56+
await deploy(firebaseMock, context, projectTargets, [buildTarget], FIREBASE_PROJECT, false, false);
3857
expect(spy).toHaveBeenCalled();
39-
expect(spy).toHaveBeenCalledWith({ target: 'prerender', project: PROJECT });
58+
expect(spy).toHaveBeenCalledWith({ target: 'prerender', project: PROJECT }, {});
4059
});
4160

4261
it('should invoke firebase.deploy', async () => {
4362
const spy = spyOn(firebaseMock, 'deploy').and.callThrough();
44-
await deploy(firebaseMock, context, 'host', BUILD_TARGET, FIREBASE_PROJECT);
63+
await deploy(firebaseMock, context, projectTargets, [BUILD_TARGET], FIREBASE_PROJECT, false, false);
4564
expect(spy).toHaveBeenCalled();
4665
expect(spy).toHaveBeenCalledWith({
47-
cwd: 'host', only: 'hosting:' + PROJECT
66+
cwd: 'cwd',
67+
only: 'hosting:' + PROJECT
4868
});
4969
});
5070

5171
describe('error handling', () => {
5272
it('throws if there is no firebase project', async () => {
5373
try {
54-
await deploy(firebaseMock, context, 'host', BUILD_TARGET)
55-
fail();
74+
await deploy(firebaseMock, context, projectTargets, [BUILD_TARGET], undefined, false, false);
5675
} catch (e) {
76+
console.log(e);
5777
expect(e.message).toMatch(/Cannot find firebase project/);
5878
}
5979
});
6080

6181
it('throws if there is no target project', async () => {
6282
context.target = undefined;
6383
try {
64-
await deploy(firebaseMock, context, 'host', BUILD_TARGET, FIREBASE_PROJECT)
65-
fail();
84+
await deploy(firebaseMock, context, projectTargets, [BUILD_TARGET], FIREBASE_PROJECT, false, false)
6685
} catch (e) {
6786
expect(e.message).toMatch(/Cannot execute the build target/);
6887
}
6988
});
7089
});
7190
});
7291

92+
describe('universal deployment', () => {
93+
beforeEach(() => initMocks());
94+
95+
it('should create a firebase function', async () => {
96+
const spy = spyOn(fsHost, 'writeFileSync');
97+
await deployToFunction(firebaseMock, context, '/home/user', projectTargets, false, fsHost);
98+
99+
expect(spy).toHaveBeenCalledTimes(2);
100+
101+
const packageArgs = spy.calls.argsFor(0);
102+
const functionArgs = spy.calls.argsFor(1);
103+
104+
expect(packageArgs[0]).toBe('dist/package.json');
105+
expect(functionArgs[0]).toBe('dist/index.js');
106+
});
107+
108+
it('should rename the index.html file in the nested dist', async () => {
109+
const spy = spyOn(fsHost, 'renameSync');
110+
await deployToFunction(firebaseMock, context, '/home/user', projectTargets, false, fsHost);
111+
112+
expect(spy).toHaveBeenCalledTimes(1);
113+
114+
const packageArgs = spy.calls.argsFor(0);
115+
116+
expect(packageArgs).toEqual([
117+
'dist/dist/browser/index.html',
118+
'dist/dist/browser/index.original.html'
119+
]);
120+
});
121+
122+
it('should invoke firebase.deploy', async () => {
123+
const spy = spyOn(firebaseMock, 'deploy');
124+
await deployToFunction(firebaseMock, context, '/home/user', projectTargets, false, fsHost);
125+
126+
expect(spy).toHaveBeenCalledTimes(1);
127+
});
128+
129+
it('should not deploy if the command is invoked with --preview', async () => {
130+
const spy = spyOn(firebaseMock, 'deploy');
131+
await deployToFunction(firebaseMock, context, '/home/user', projectTargets, true, fsHost);
132+
expect(spy).not.toHaveBeenCalled();
133+
});
134+
});
135+
73136
const initMocks = () => {
137+
fsHost = {
138+
moveSync(_: string, __: string) {
139+
},
140+
renameSync(_: string, __: string) {
141+
},
142+
writeFileSync(_: string, __: string) {
143+
}
144+
};
145+
74146
firebaseMock = {
75147
login: () => Promise.resolve(),
76148
projects: {

0 commit comments

Comments
 (0)