Skip to content

Commit 9c25f74

Browse files
delastevehansl
authored andcommitted
fix(@angular/cli): generate command now ignores duplicate component symbol (angular#4446)
"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 9d29cbc commit 9c25f74

File tree

5 files changed

+100
-27
lines changed

5 files changed

+100
-27
lines changed

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

Lines changed: 21 additions & 10 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`;
@@ -119,7 +119,7 @@ export default Blueprint.extend({
119119
};
120120
},
121121

122-
files: function() {
122+
files: function () {
123123
let fileList = getFiles.call(this) as Array<string>;
124124

125125
if (this.options && this.options.inlineTemplate) {
@@ -154,7 +154,7 @@ export default Blueprint.extend({
154154
};
155155
},
156156

157-
afterInstall: function(options: any) {
157+
afterInstall: function (options: any) {
158158
if (options.dryRun) {
159159
return;
160160
}
@@ -166,6 +166,8 @@ export default Blueprint.extend({
166166
const importPath = componentDir ? `./${componentDir}/${fileName}` : `./${fileName}`;
167167

168168
if (!options.skipImport) {
169+
const preChange = fs.readFileSync(this.pathToModule, 'utf8');
170+
169171
returns.push(
170172
astUtils.addDeclarationToModule(this.pathToModule, className, importPath)
171173
.then((change: any) => change.apply(NodeHost))
@@ -175,10 +177,19 @@ export default Blueprint.extend({
175177
.then((change: any) => change.apply(NodeHost));
176178
}
177179
return result;
180+
})
181+
.then(() => {
182+
const postChange = fs.readFileSync(this.pathToModule, 'utf8');
183+
let moduleStatus = 'update';
184+
185+
if (postChange === preChange) {
186+
moduleStatus = 'identical';
187+
}
188+
189+
this._writeStatusToUI(chalk.yellow,
190+
moduleStatus,
191+
path.relative(this.project.root, this.pathToModule));
178192
}));
179-
this._writeStatusToUI(chalk.yellow,
180-
'update',
181-
path.relative(this.project.root, this.pathToModule));
182193
}
183194

184195
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: 21 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,10 @@
11
import * as ts from 'typescript';
22
import * as fs from 'fs';
3-
import {Change, InsertChange, NoopChange, MultiChange} from './change';
4-
import {findNodes} from './node';
5-
import {insertImport} from './route-utils';
6-
7-
import {Observable} from 'rxjs/Observable';
8-
import {ReplaySubject} from 'rxjs/ReplaySubject';
3+
import { Change, InsertChange, NoopChange, MultiChange } from './change';
4+
import { findNodes } from './node';
5+
import { insertImport } from './route-utils';
6+
import { Observable } from 'rxjs/Observable';
7+
import { ReplaySubject } from 'rxjs/ReplaySubject';
98
import 'rxjs/add/observable/empty';
109
import 'rxjs/add/observable/of';
1110
import 'rxjs/add/operator/do';
@@ -162,17 +161,17 @@ export function getDecoratorMetadata(source: ts.SourceFile, identifier: string,
162161
return getSourceNodes(source)
163162
.filter(node => {
164163
return node.kind == ts.SyntaxKind.Decorator
165-
&& (<ts.Decorator>node).expression.kind == ts.SyntaxKind.CallExpression;
164+
&& (node as ts.Decorator).expression.kind == ts.SyntaxKind.CallExpression;
166165
})
167-
.map(node => <ts.CallExpression>(<ts.Decorator>node).expression)
166+
.map(node => (node as ts.Decorator).expression as ts.CallExpression)
168167
.filter(expr => {
169168
if (expr.expression.kind == ts.SyntaxKind.Identifier) {
170-
const id = <ts.Identifier>expr.expression;
169+
const id = expr.expression as ts.Identifier;
171170
return id.getFullText(source) == identifier
172171
&& angularImports[id.getFullText(source)] === module;
173172
} else if (expr.expression.kind == ts.SyntaxKind.PropertyAccessExpression) {
174173
// This covers foo.NgModule when importing * as foo.
175-
const paExpr = <ts.PropertyAccessExpression>expr.expression;
174+
const paExpr = expr.expression as ts.PropertyAccessExpression;
176175
// If the left expression is not an identifier, just give up at that point.
177176
if (paExpr.expression.kind !== ts.SyntaxKind.Identifier) {
178177
return false;
@@ -186,7 +185,7 @@ export function getDecoratorMetadata(source: ts.SourceFile, identifier: string,
186185
})
187186
.filter(expr => expr.arguments[0]
188187
&& expr.arguments[0].kind == ts.SyntaxKind.ObjectLiteralExpression)
189-
.map(expr => <ts.ObjectLiteralExpression>expr.arguments[0]);
188+
.map(expr => expr.arguments[0] as ts.ObjectLiteralExpression);
190189
}
191190

192191

@@ -229,14 +228,14 @@ function _addSymbolToNgModuleMetadata(ngModulePath: string, metadataField: strin
229228
return metadata.toPromise();
230229
}
231230

232-
const assignment = <ts.PropertyAssignment>matchingProperties[0];
231+
const assignment = matchingProperties[0] as ts.PropertyAssignment;
233232

234233
// If it's not an array, nothing we can do really.
235234
if (assignment.initializer.kind !== ts.SyntaxKind.ArrayLiteralExpression) {
236235
return null;
237236
}
238237

239-
const arrLiteral = <ts.ArrayLiteralExpression>assignment.initializer;
238+
const arrLiteral = assignment.initializer as ts.ArrayLiteralExpression;
240239
if (arrLiteral.elements.length == 0) {
241240
// Forward the property.
242241
return arrLiteral;
@@ -245,11 +244,17 @@ function _addSymbolToNgModuleMetadata(ngModulePath: string, metadataField: strin
245244
})
246245
.then((node: ts.Node) => {
247246
if (!node) {
248-
/* eslint-disable no-console */
249247
console.log('No app module found. Please add your new class to your component.');
250248
return new NoopChange();
251249
}
250+
252251
if (Array.isArray(node)) {
252+
const nodeArray = node as any as Array<ts.Node>;
253+
const symbolsArray = nodeArray.map(node => node.getText());
254+
if (symbolsArray.includes(symbolName)) {
255+
return new NoopChange();
256+
}
257+
253258
node = node[node.length - 1];
254259
}
255260

@@ -258,7 +263,7 @@ function _addSymbolToNgModuleMetadata(ngModulePath: string, metadataField: strin
258263
if (node.kind == ts.SyntaxKind.ObjectLiteralExpression) {
259264
// We haven't found the field in the metadata declaration. Insert a new
260265
// field.
261-
let expr = <ts.ObjectLiteralExpression>node;
266+
let expr = node as ts.ObjectLiteralExpression;
262267
if (expr.properties.length == 0) {
263268
position = expr.getEnd() - 1;
264269
toInsert = ` ${metadataField}: [${symbolName}]\n`;
@@ -326,7 +331,7 @@ export function addProviderToModule(modulePath: string, classifiedName: string,
326331
* Custom function to insert an export into NgModule. It also imports it.
327332
*/
328333
export function addExportToModule(modulePath: string, classifiedName: string,
329-
importPath: string): Promise<Change> {
334+
importPath: string): Promise<Change> {
330335
return _addSymbolToNgModuleMetadata(modulePath, 'exports', classifiedName, importPath);
331336
}
332337

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)