Skip to content

Commit 1650f30

Browse files
committed
feat(schematics): ng-add schematics
A possibility to install and configure the package with `ng add angularfire2` command closes #1676
1 parent 26f7613 commit 1650f30

13 files changed

+318
-1
lines changed

package.json

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,10 @@
1010
"karma": "karma start",
1111
"test:universal": "npm run build && cp -R dist/packages-dist test/universal-test/node_modules/angularfire2 && cd test/universal-test && npm run prerender",
1212
"delayed_karma": "sleep 10 && karma start",
13-
"build": "rm -rf dist && node tools/build.js",
13+
"build": "rm -rf dist && node tools/build.js && npm run schematics",
14+
"schematics": "npm run schematics:build && npm run schematics:copy",
15+
"schematics:build": "tsc -p ./schematics/tsconfig.json",
16+
"schematics:copy": "ncp schematics/src dist/packages-dist/schematics",
1417
"changelog": "conventional-changelog -p angular -i CHANGELOG.md -s -r 1"
1518
},
1619
"keywords": [
@@ -29,11 +32,14 @@
2932
},
3033
"homepage": "https://github.com/angular/angularfire2#readme",
3134
"dependencies": {
35+
"@angular-devkit/core": "^0.7.3",
36+
"@angular-devkit/schematics": "^0.7.3",
3237
"@angular/common": "^6.0.0",
3338
"@angular/compiler": "^6.0.0",
3439
"@angular/core": "^6.0.0",
3540
"@angular/platform-browser": "^6.0.0",
3641
"@angular/platform-browser-dynamic": "^6.0.0",
42+
"@schematics/angular": "^0.7.3",
3743
"bufferutil": "^3.0.3",
3844
"firebase": "^5.0.3",
3945
"rxjs": "^6.0.0",

schematics/.editorconfig

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
[*]
2+
indent_size = 2

schematics/.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
*.d.ts
2+
*.js
3+
*.js.map

schematics/src/collection.json

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
{
2+
"$schema": "../node_modules/@angular-devkit/schematics/collection-schema.json",
3+
"schematics": {
4+
"ng-add": {
5+
"description": "Adds AngularFire to an application",
6+
"factory": "./ng-add",
7+
"schema": "./ng-add/schema.json"
8+
}
9+
}
10+
}

schematics/src/ng-add/index.ts

Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
import { chain, Rule, SchematicContext, SchematicsException, Tree } from '@angular-devkit/schematics';
2+
import * as ts from 'typescript';
3+
import { addPackageToPackageJson } from '../utils/package';
4+
import { NodePackageInstallTask } from '@angular-devkit/schematics/tasks';
5+
import { angularfire2Version, firebaseVersion } from '../utils/libs-version';
6+
import { addEnvironmentEntry } from '../utils/environment';
7+
import { parseName } from '@schematics/angular/utility/parse-name';
8+
import { getProjectPath } from '../utils/project';
9+
import { Schema } from './schema';
10+
import { addImportToModule, insertImport } from '@schematics/angular/utility/ast-utils';
11+
import { InsertChange } from '@schematics/angular/utility/change';
12+
import { buildRelativePath, findModuleFromOptions } from '@schematics/angular/utility/find-module';
13+
14+
export default function add(options: Schema): Rule {
15+
return chain([
16+
install(),
17+
addConfig(),
18+
addToNgModule(options),
19+
])
20+
}
21+
22+
export function install(): Rule {
23+
return (host: Tree, context: SchematicContext) => {
24+
addPackageToPackageJson(host, 'dependencies', 'firebase', firebaseVersion);
25+
addPackageToPackageJson(host, 'dependencies', 'angularfire2', angularfire2Version);
26+
context.addTask(new NodePackageInstallTask());
27+
return host;
28+
};
29+
}
30+
31+
function addConfig(): Rule {
32+
return (host: Tree) => {
33+
const firebaseConfig = `
34+
firebase: {
35+
apiKey: '<your-key>',
36+
authDomain: '<your-project-authdomain>',
37+
databaseURL: '<your-database-URL>',
38+
projectId: '<your-project-id>',
39+
storageBucket: '<your-storage-bucket>',
40+
messagingSenderId: '<your-messaging-sender-id>'
41+
},`;
42+
addEnvironmentEntry(host, 'environment.ts', firebaseConfig);
43+
return host;
44+
}
45+
}
46+
47+
function addToNgModule(options: Schema): Rule {
48+
return (host: Tree) => {
49+
options.path = parseName(getProjectPath(host, options), '').path;
50+
51+
if (options.module) {
52+
options.module = findModuleFromOptions(host, {
53+
name: '',
54+
module: options.module,
55+
path: options.path,
56+
});
57+
}
58+
59+
const modulePath = options.module;
60+
61+
if (!modulePath) {
62+
return host;
63+
}
64+
65+
if (!host.exists(modulePath)) {
66+
throw new Error(`Specified module path ${modulePath} does not exist`);
67+
}
68+
69+
const text = host.read(modulePath);
70+
if (text === null) {
71+
throw new SchematicsException(`File ${modulePath} does not exist.`);
72+
}
73+
const sourceText = text.toString('utf-8');
74+
75+
const source = ts.createSourceFile(
76+
modulePath,
77+
sourceText,
78+
ts.ScriptTarget.Latest,
79+
true
80+
);
81+
82+
const environmentsPath = buildRelativePath(
83+
modulePath,
84+
`${options.path}/environments/environment`
85+
);
86+
87+
const AngularFireNgModuleImport = addImportToModule(
88+
source,
89+
modulePath,
90+
options.firebaseApp
91+
? `AngularFireModule.initializeApp(environment.firebase, '${options.firebaseApp}')`
92+
: `AngularFireModule.initializeApp(environment.firebase)`,
93+
''
94+
).shift();
95+
96+
const coreImports = [
97+
insertImport(source, modulePath, 'AngularFireModule', 'angularfire2'),
98+
insertImport(source, modulePath, 'environment', environmentsPath),
99+
AngularFireNgModuleImport,
100+
];
101+
102+
const individualImports = options.all
103+
? [
104+
insertImport(source, modulePath, 'AngularFirestoreModule', 'angularfire2/firestore'),
105+
insertImport(source, modulePath, 'AngularFireStorageModule', 'angularfire2/storage'),
106+
insertImport(source, modulePath, 'AngularFireAuthModule', 'angularfire2/auth'),
107+
addImportToModule(source, modulePath, 'AngularFirestoreModule', 'angularfire2/firestore').shift(),
108+
addImportToModule(source, modulePath, 'AngularFireStorageModule', '').shift(),
109+
addImportToModule(source, modulePath, 'AngularFireAuthModule', '').shift(),
110+
]
111+
: [];
112+
113+
const changes = [...coreImports, ...individualImports];
114+
115+
const recorder = host.beginUpdate(modulePath);
116+
for (const change of changes) {
117+
if (change instanceof InsertChange) {
118+
recorder.insertLeft(change.pos, change.toAdd);
119+
}
120+
}
121+
host.commitUpdate(recorder);
122+
123+
return host;
124+
}
125+
}

schematics/src/ng-add/schema.json

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
{
2+
"$schema": "http://json-schema.org/schema",
3+
"id": "SchematicsAngularFire2",
4+
"title": "Adds AngularFire to a project",
5+
"type": "object",
6+
"properties": {
7+
"module": {
8+
"type": "string",
9+
"default": "app",
10+
"description": "Allows specification of the declaring module.",
11+
"alias": "m",
12+
"subtype": "filepath"
13+
},
14+
"firebaseApp": {
15+
"type": "string",
16+
"description": "A custom FirebaseApp name.",
17+
"alias": "f"
18+
},
19+
"all": {
20+
"type": "boolean",
21+
"description": "Add all AngularFire modules (auth, db, storage).",
22+
"alias": "a",
23+
"default": false
24+
},
25+
"path": {
26+
"type": "string",
27+
"format": "path",
28+
"description": "The path to the project.",
29+
"visible": false
30+
}
31+
},
32+
"required": []
33+
}

schematics/src/ng-add/schema.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
export interface Schema {
2+
module?: string;
3+
path?: string;
4+
firebaseApp?: string;
5+
all?: boolean;
6+
}

schematics/src/utils/environment.ts

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import * as ts from 'typescript';
2+
import { SchematicsException, Tree } from '@angular-devkit/schematics';
3+
import { findNode } from '@schematics/angular/utility/ast-utils';
4+
import { InsertChange } from '@schematics/angular/utility/change';
5+
6+
/**
7+
* Adds a package to the package.json
8+
*/
9+
export function addEnvironmentEntry(
10+
host: Tree,
11+
filename: string,
12+
data: string,
13+
): Tree {
14+
const filePath = `src/environments/${filename}`;
15+
16+
if (host.exists(filePath)) {
17+
let sourceText = host.read(filePath)!.toString('utf-8');
18+
let sourceFile = ts.createSourceFile(filePath, sourceText, ts.ScriptTarget.Latest, true);
19+
20+
const envIdentifier = findNode(sourceFile, ts.SyntaxKind.Identifier, 'environment');
21+
if (!envIdentifier) {
22+
throw new SchematicsException(`Cannot find 'environment' identifier in ${filename}`);
23+
}
24+
25+
const envObjectLiteral = envIdentifier.parent!.getChildren().find(({ kind }) => kind === ts.SyntaxKind.ObjectLiteralExpression)!;
26+
const openBracketToken = envObjectLiteral.getChildren().find(({ kind }) => kind === ts.SyntaxKind.OpenBraceToken)!;
27+
28+
const recorder = host.beginUpdate(filePath);
29+
const change = new InsertChange(filePath, openBracketToken.end, data);
30+
recorder.insertLeft(change.pos, change.toAdd);
31+
host.commitUpdate(recorder);
32+
} else {
33+
throw new SchematicsException(`File ${filename} does not exist`);
34+
}
35+
36+
return host;
37+
}

schematics/src/utils/libs-version.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
export const angularfire2Version = '5.0.0-rc.11';
2+
export const firebaseVersion = '^5.4.1';

schematics/src/utils/package.ts

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import { Tree } from '@angular-devkit/schematics';
2+
3+
/**
4+
* Adds a package to the package.json
5+
*/
6+
export function addPackageToPackageJson(
7+
host: Tree,
8+
type: 'dependencies' | 'devDependencies',
9+
pkg: string,
10+
version: string
11+
): Tree {
12+
if (host.exists('package.json')) {
13+
const sourceText = host.read('package.json')!.toString('utf-8');
14+
const json = JSON.parse(sourceText);
15+
if (!json[type]) {
16+
json[type] = {};
17+
}
18+
19+
if (!json[type][pkg]) {
20+
json[type][pkg] = version;
21+
}
22+
23+
host.overwrite('package.json', JSON.stringify(json, null, 2));
24+
}
25+
26+
return host;
27+
}

schematics/src/utils/project.ts

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import { Tree } from '@angular-devkit/schematics';
2+
import { getWorkspace, WorkspaceProject } from '@schematics/angular/utility/config';
3+
4+
export function getProjectPath(
5+
host: Tree,
6+
options: { project?: string | undefined; path?: string | undefined }
7+
) {
8+
const project = getProject(host, options);
9+
10+
if (project.root.substr(-1) === '/') {
11+
project.root = project.root.substr(0, project.root.length - 1);
12+
}
13+
14+
if (options.path === undefined) {
15+
const projectDirName =
16+
project.projectType === 'application' ? 'app' : 'lib';
17+
18+
return `${project.root ? `/${project.root}` : ''}/src/${projectDirName}`;
19+
}
20+
21+
return options.path;
22+
}
23+
24+
export function getProject(host: Tree, options: { project?: string | undefined; path?: string | undefined }): WorkspaceProject {
25+
const workspace = getWorkspace(host);
26+
27+
if (!options.project) {
28+
options.project = Object.keys(workspace.projects)[0];
29+
}
30+
31+
return workspace.projects[options.project];
32+
}

schematics/tsconfig.json

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
{
2+
"compilerOptions": {
3+
"baseUrl": "tsconfig",
4+
"lib": [
5+
"es2017",
6+
"dom"
7+
],
8+
"declaration": true,
9+
"module": "commonjs",
10+
"moduleResolution": "node",
11+
"noEmitOnError": true,
12+
"noFallthroughCasesInSwitch": true,
13+
"noImplicitAny": true,
14+
"noImplicitThis": true,
15+
"noUnusedParameters": true,
16+
"noUnusedLocals": true,
17+
"rootDir": "src/",
18+
"skipDefaultLibCheck": true,
19+
"skipLibCheck": true,
20+
"sourceMap": true,
21+
"strictNullChecks": true,
22+
"target": "es6",
23+
"types": [
24+
"node"
25+
]
26+
},
27+
"include": [
28+
"src/**/*"
29+
],
30+
"exclude": [
31+
"src/*/files/**/*"
32+
]
33+
}

src/core/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,5 +25,6 @@
2525
"rxjs": "RXJS_VERSION",
2626
"zone.js": "ZONEJS_VERSION"
2727
},
28+
"schematics": "./schematics/collection.json",
2829
"typings": "index.d.ts"
2930
}

0 commit comments

Comments
 (0)