Skip to content

Commit 72af96b

Browse files
committed
fix(generate): ignore duplicate component symbol in module declarations
"ng g c X" will no longer insert the component symbol in the NgModule's declaration array should it already exist. Closes angular#4323
1 parent c889dd8 commit 72af96b

File tree

6 files changed

+112
-34
lines changed

6 files changed

+112
-34
lines changed

packages/@angular/cli/blueprints/component/index.ts

Lines changed: 26 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
1-
import {NodeHost} from '../../lib/ast-tools';
1+
import { NodeHost } from '../../lib/ast-tools';
22

3-
const path = require('path');
4-
const fs = require('fs');
5-
const chalk = require('chalk');
3+
import * as fs from 'fs';
4+
import * as path from 'path';
5+
import * as chalk from 'chalk';
66
const Blueprint = require('../../ember-cli/lib/models/blueprint');
77
const dynamicPathParser = require('../../utilities/dynamic-path-parser');
88
const findParentModule = require('../../utilities/find-parent-module').default;
@@ -26,7 +26,7 @@ export default Blueprint.extend({
2626
{ name: 'export', type: Boolean, default: false }
2727
],
2828

29-
beforeInstall: function(options: any) {
29+
beforeInstall: function (options: any) {
3030
if (options.module) {
3131
// Resolve path to module
3232
const modulePath = options.module.endsWith('.ts') ? options.module : `${options.module}.ts`;
@@ -54,13 +54,13 @@ export default Blueprint.extend({
5454

5555
let defaultPrefix = '';
5656
if (this.project.ngConfig &&
57-
this.project.ngConfig.apps[0] &&
58-
this.project.ngConfig.apps[0].prefix) {
57+
this.project.ngConfig.apps[0] &&
58+
this.project.ngConfig.apps[0].prefix) {
5959
defaultPrefix = this.project.ngConfig.apps[0].prefix;
6060
}
6161

6262
let prefix = (this.options.prefix === 'false' || this.options.prefix === '')
63-
? '' : (this.options.prefix || defaultPrefix);
63+
? '' : (this.options.prefix || defaultPrefix);
6464
prefix = prefix && `${prefix}-`;
6565

6666
this.selector = stringUtils.dasherize(prefix + parsedPath.name);
@@ -75,8 +75,8 @@ export default Blueprint.extend({
7575
locals: function (options: any) {
7676
this.styleExt = 'css';
7777
if (this.project.ngConfig &&
78-
this.project.ngConfig.defaults &&
79-
this.project.ngConfig.defaults.styleExt) {
78+
this.project.ngConfig.defaults &&
79+
this.project.ngConfig.defaults.styleExt) {
8080
this.styleExt = this.project.ngConfig.defaults.styleExt;
8181
}
8282

@@ -115,7 +115,7 @@ export default Blueprint.extend({
115115
};
116116
},
117117

118-
files: function() {
118+
files: function () {
119119
let fileList = getFiles.call(this) as Array<string>;
120120

121121
if (this.options && this.options.inlineTemplate) {
@@ -150,7 +150,7 @@ export default Blueprint.extend({
150150
};
151151
},
152152

153-
afterInstall: function(options: any) {
153+
afterInstall: function (options: any) {
154154
if (options.dryRun) {
155155
return;
156156
}
@@ -162,6 +162,8 @@ export default Blueprint.extend({
162162
const importPath = componentDir ? `./${componentDir}/${fileName}` : `./${fileName}`;
163163

164164
if (!options.skipImport) {
165+
const preChange = fs.readFileSync(this.pathToModule, 'utf8');
166+
165167
returns.push(
166168
astUtils.addDeclarationToModule(this.pathToModule, className, importPath)
167169
.then((change: any) => change.apply(NodeHost))
@@ -171,10 +173,19 @@ export default Blueprint.extend({
171173
.then((change: any) => change.apply(NodeHost));
172174
}
173175
return result;
176+
})
177+
.then(() => {
178+
const postChange = fs.readFileSync(this.pathToModule, 'utf8');
179+
let moduleStatus = 'update';
180+
181+
if (postChange === preChange) {
182+
moduleStatus = 'identical';
183+
}
184+
185+
this._writeStatusToUI(chalk.yellow,
186+
moduleStatus,
187+
path.relative(this.project.root, this.pathToModule));
174188
}));
175-
this._writeStatusToUI(chalk.yellow,
176-
'update',
177-
path.relative(this.project.root, this.pathToModule));
178189
}
179190

180191
return Promise.all(returns);

packages/@angular/cli/lib/ast-tools/ast-utils.spec.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -227,6 +227,29 @@ class Module {}`
227227
});
228228
});
229229

230+
it('does not append duplicate declarations', () => {
231+
return addDeclarationToModule('2.ts', 'MyClass', 'MyImportPath')
232+
.then(change => change.apply(NodeHost))
233+
.then(() => addDeclarationToModule('2.ts', 'MyClass', 'MyImportPath'))
234+
.then(change => change.apply(NodeHost))
235+
.then(() => readFile('2.ts', 'utf-8'))
236+
.then(content => {
237+
expect(content).toEqual(
238+
'\n' +
239+
'import {NgModule} from \'@angular/core\';\n' +
240+
'import { MyClass } from \'MyImportPath\';\n' +
241+
'\n' +
242+
'@NgModule({\n' +
243+
' declarations: [\n' +
244+
' Other,\n' +
245+
' MyClass\n' +
246+
' ]\n' +
247+
'})\n' +
248+
'class Module {}'
249+
);
250+
});
251+
});
252+
230253
it('works with array with declarations', () => {
231254
return addDeclarationToModule('2.ts', 'MyClass', 'MyImportPath')
232255
.then(change => change.apply(NodeHost))

packages/@angular/cli/lib/ast-tools/ast-utils.ts

Lines changed: 25 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,16 @@
11
import * as ts from 'typescript';
22
import * as fs from 'fs';
3-
import {Symbols} from '@angular/tsc-wrapped/src/symbols';
3+
import { Symbols } from '@angular/tsc-wrapped/src/symbols';
44
import {
55
isMetadataImportedSymbolReferenceExpression,
66
isMetadataModuleReferenceExpression
77
} from '@angular/tsc-wrapped';
8-
import {Change, InsertChange, NoopChange, MultiChange} from './change';
9-
import {findNodes} from './node';
10-
import {insertImport} from './route-utils';
8+
import { Change, InsertChange, NoopChange, MultiChange } from './change';
9+
import { findNodes } from './node';
10+
import { insertImport } from './route-utils';
1111

12-
import {Observable} from 'rxjs/Observable';
13-
import {ReplaySubject} from 'rxjs/ReplaySubject';
12+
import { Observable } from 'rxjs/Observable';
13+
import { ReplaySubject } from 'rxjs/ReplaySubject';
1414
import 'rxjs/add/observable/empty';
1515
import 'rxjs/add/observable/of';
1616
import 'rxjs/add/operator/do';
@@ -81,7 +81,7 @@ function nodesByPosition(first: ts.Node, second: ts.Node): number {
8181
* @throw Error if toInsert is first occurence but fall back is not set
8282
*/
8383
export function insertAfterLastOccurrence(nodes: ts.Node[], toInsert: string,
84-
file: string, fallbackPos?: number, syntaxKind?: ts.SyntaxKind): Change {
84+
file: string, fallbackPos?: number, syntaxKind?: ts.SyntaxKind): Change {
8585
let lastItem = nodes.sort(nodesByPosition).pop();
8686
if (syntaxKind) {
8787
lastItem = findNodes(lastItem, syntaxKind).sort(nodesByPosition).pop();
@@ -106,13 +106,13 @@ export function getContentOfKeyLiteral(source: ts.SourceFile, node: ts.Node): st
106106

107107

108108
export function getDecoratorMetadata(source: ts.SourceFile, identifier: string,
109-
module: string): Observable<ts.Node> {
109+
module: string): Observable<ts.Node> {
110110
const symbols = new Symbols(source as any);
111111

112112
return getSourceNodes(source)
113113
.filter(node => {
114114
return node.kind == ts.SyntaxKind.Decorator
115-
&& (<ts.Decorator>node).expression.kind == ts.SyntaxKind.CallExpression;
115+
&& (<ts.Decorator>node).expression.kind == ts.SyntaxKind.CallExpression;
116116
})
117117
.map(node => <ts.CallExpression>(<ts.Decorator>node).expression)
118118
.filter(expr => {
@@ -140,13 +140,13 @@ export function getDecoratorMetadata(source: ts.SourceFile, identifier: string,
140140
return false;
141141
})
142142
.filter(expr => expr.arguments[0]
143-
&& expr.arguments[0].kind == ts.SyntaxKind.ObjectLiteralExpression)
143+
&& expr.arguments[0].kind == ts.SyntaxKind.ObjectLiteralExpression)
144144
.map(expr => <ts.ObjectLiteralExpression>expr.arguments[0]);
145145
}
146146

147147

148148
function _addSymbolToNgModuleMetadata(ngModulePath: string, metadataField: string,
149-
symbolName: string, importPath: string) {
149+
symbolName: string, importPath: string) {
150150
const source: ts.SourceFile = getSource(ngModulePath);
151151
let metadata = getDecoratorMetadata(source, 'NgModule', '@angular/core');
152152

@@ -200,10 +200,18 @@ function _addSymbolToNgModuleMetadata(ngModulePath: string, metadataField: strin
200200
})
201201
.then((node: ts.Node) => {
202202
if (!node) {
203-
/* eslint-disable no-console */
204203
console.log('No app module found. Please add your new class to your component.');
205204
return new NoopChange();
206205
}
206+
207+
if (Array.isArray(node)) {
208+
const nodeArray = node as any as Array<ts.Node>;
209+
const symbolsArray = nodeArray.map(node => node.getText());
210+
if (symbolsArray.includes(symbolName)) {
211+
return new NoopChange();
212+
}
213+
}
214+
207215
if (Array.isArray(node)) {
208216
node = node[node.length - 1];
209217
}
@@ -213,7 +221,7 @@ function _addSymbolToNgModuleMetadata(ngModulePath: string, metadataField: strin
213221
if (node.kind == ts.SyntaxKind.ObjectLiteralExpression) {
214222
// We haven't found the field in the metadata declaration. Insert a new
215223
// field.
216-
let expr = <ts.ObjectLiteralExpression>node;
224+
let expr = node as ts.ObjectLiteralExpression;
217225
if (expr.properties.length == 0) {
218226
position = expr.getEnd() - 1;
219227
toInsert = ` ${metadataField}: [${symbolName}]\n`;
@@ -254,7 +262,7 @@ function _addSymbolToNgModuleMetadata(ngModulePath: string, metadataField: strin
254262
* into NgModule declarations. It also imports the component.
255263
*/
256264
export function addDeclarationToModule(modulePath: string, classifiedName: string,
257-
importPath: string): Promise<Change> {
265+
importPath: string): Promise<Change> {
258266

259267
return _addSymbolToNgModuleMetadata(modulePath, 'declarations', classifiedName, importPath);
260268
}
@@ -264,7 +272,7 @@ export function addDeclarationToModule(modulePath: string, classifiedName: strin
264272
* into NgModule declarations. It also imports the component.
265273
*/
266274
export function addImportToModule(modulePath: string, classifiedName: string,
267-
importPath: string): Promise<Change> {
275+
importPath: string): Promise<Change> {
268276

269277
return _addSymbolToNgModuleMetadata(modulePath, 'imports', classifiedName, importPath);
270278
}
@@ -273,15 +281,15 @@ export function addImportToModule(modulePath: string, classifiedName: string,
273281
* Custom function to insert a provider into NgModule. It also imports it.
274282
*/
275283
export function addProviderToModule(modulePath: string, classifiedName: string,
276-
importPath: string): Promise<Change> {
284+
importPath: string): Promise<Change> {
277285
return _addSymbolToNgModuleMetadata(modulePath, 'providers', classifiedName, importPath);
278286
}
279287

280288
/**
281289
* Custom function to insert an export into NgModule. It also imports it.
282290
*/
283291
export function addExportToModule(modulePath: string, classifiedName: string,
284-
importPath: string): Promise<Change> {
292+
importPath: string): Promise<Change> {
285293
return _addSymbolToNgModuleMetadata(modulePath, 'exports', classifiedName, importPath);
286294
}
287295

packages/@angular/cli/tsconfig.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,9 @@
1313
"sourceMap": true,
1414
"sourceRoot": "/",
1515
"target": "es5",
16-
"lib": ["es6"],
16+
"lib": [
17+
"es2016"
18+
],
1719
"skipLibCheck": true,
1820
"typeRoots": [
1921
"../../../node_modules/@types"

tests/acceptance/generate-component.spec.js

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,16 @@ describe('Acceptance: ng generate component', function () {
4242
});
4343
});
4444

45+
it('generating my-comp twice does not add two declarations to module', function () {
46+
const appModule = path.join(root, 'tmp/foo/src/app/app.module.ts');
47+
return ng(['generate', 'component', 'my-comp'])
48+
.then(() => ng(['generate', 'component', 'my-comp']))
49+
.then(() => readFile(appModule, 'utf-8'))
50+
.then(content => {
51+
expect(content).matches(/declarations:\s+\[\r?\n\s+AppComponent,\r?\n\s+MyCompComponent\r?\n\s+\]/m);
52+
});
53+
});
54+
4555
it('test' + path.sep + 'my-comp', function () {
4656
fs.mkdirsSync(path.join(root, 'tmp', 'foo', 'src', 'app', 'test'));
4757
return ng(['generate', 'component', 'test' + path.sep + 'my-comp']).then(() => {
@@ -206,7 +216,7 @@ describe('Acceptance: ng generate component', function () {
206216
});
207217
});
208218

209-
it('my-comp --no-spec', function() {
219+
it('my-comp --no-spec', function () {
210220
return ng(['generate', 'component', 'my-comp', '--no-spec']).then(() => {
211221
var testPath = path.join(root, 'tmp', 'foo', 'src', 'app', 'my-comp', 'my-comp.component.spec.ts');
212222
expect(existsSync(testPath)).to.equal(false);
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import * as path from 'path';
2+
import { ng } from '../../../utils/process';
3+
import { oneLine } from 'common-tags';
4+
5+
export default function () {
6+
return ng('generate', 'component', 'test-component')
7+
.then((output) => {
8+
if (!output.match(/update src[\\|\/]app[\\|\/]app.module.ts/)) {
9+
throw new Error(oneLine`
10+
Expected to match
11+
"update src${path.sep}app${path.sep}app.module.ts"
12+
in ${output}.`);
13+
}
14+
})
15+
.then(() => ng('generate', 'component', 'test-component'))
16+
.then((output) => {
17+
if (!output.match(/identical src[\\|\/]app[\\|\/]app.module.ts/)) {
18+
throw new Error(oneLine`
19+
Expected to match
20+
"identical src${path.sep}app${path.sep}app.module.ts"
21+
in ${output}.`);
22+
}
23+
});
24+
}

0 commit comments

Comments
 (0)