Skip to content

Commit c3df47f

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 8c6cb07 commit c3df47f

File tree

5 files changed

+102
-28
lines changed

5 files changed

+102
-28
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`;
@@ -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: 23 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';
@@ -112,26 +112,26 @@ export function getDecoratorMetadata(source: ts.SourceFile, identifier: string,
112112
return getSourceNodes(source)
113113
.filter(node => {
114114
return node.kind == ts.SyntaxKind.Decorator
115-
&& (<ts.Decorator>node).expression.kind == ts.SyntaxKind.CallExpression;
115+
&& (node as ts.Decorator).expression.kind == ts.SyntaxKind.CallExpression;
116116
})
117-
.map(node => <ts.CallExpression>(<ts.Decorator>node).expression)
117+
.map(node => (node as ts.Decorator).expression as ts.CallExpression)
118118
.filter(expr => {
119119
if (expr.expression.kind == ts.SyntaxKind.Identifier) {
120-
const id = <ts.Identifier>expr.expression;
120+
const id = expr.expression as ts.Identifier;
121121
const metaData = symbols.resolve(id.getFullText(source));
122122
if (isMetadataImportedSymbolReferenceExpression(metaData)) {
123123
return metaData.name == identifier && metaData.module == module;
124124
}
125125
} else if (expr.expression.kind == ts.SyntaxKind.PropertyAccessExpression) {
126126
// This covers foo.NgModule when importing * as foo.
127-
const paExpr = <ts.PropertyAccessExpression>expr.expression;
127+
const paExpr = expr.expression as ts.PropertyAccessExpression;
128128
// If the left expression is not an identifier, just give up at that point.
129129
if (paExpr.expression.kind !== ts.SyntaxKind.Identifier) {
130130
return false;
131131
}
132132

133133
const id = paExpr.name;
134-
const moduleId = <ts.Identifier>paExpr.expression;
134+
const moduleId = paExpr.expression as ts.Identifier;
135135
const moduleMetaData = symbols.resolve(moduleId.getFullText(source));
136136
if (isMetadataModuleReferenceExpression(moduleMetaData)) {
137137
return moduleMetaData.module == module && id.getFullText(source) == identifier;
@@ -141,7 +141,7 @@ export function getDecoratorMetadata(source: ts.SourceFile, identifier: string,
141141
})
142142
.filter(expr => expr.arguments[0]
143143
&& expr.arguments[0].kind == ts.SyntaxKind.ObjectLiteralExpression)
144-
.map(expr => <ts.ObjectLiteralExpression>expr.arguments[0]);
144+
.map(expr => expr.arguments[0] as ts.ObjectLiteralExpression);
145145
}
146146

147147

@@ -184,14 +184,14 @@ function _addSymbolToNgModuleMetadata(ngModulePath: string, metadataField: strin
184184
return metadata.toPromise();
185185
}
186186

187-
const assignment = <ts.PropertyAssignment>matchingProperties[0];
187+
const assignment = matchingProperties[0] as ts.PropertyAssignment;
188188

189189
// If it's not an array, nothing we can do really.
190190
if (assignment.initializer.kind !== ts.SyntaxKind.ArrayLiteralExpression) {
191191
return null;
192192
}
193193

194-
const arrLiteral = <ts.ArrayLiteralExpression>assignment.initializer;
194+
const arrLiteral = assignment.initializer as ts.ArrayLiteralExpression;
195195
if (arrLiteral.elements.length == 0) {
196196
// Forward the property.
197197
return arrLiteral;
@@ -200,11 +200,17 @@ 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+
207207
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+
208214
node = node[node.length - 1];
209215
}
210216

@@ -213,7 +219,7 @@ function _addSymbolToNgModuleMetadata(ngModulePath: string, metadataField: strin
213219
if (node.kind == ts.SyntaxKind.ObjectLiteralExpression) {
214220
// We haven't found the field in the metadata declaration. Insert a new
215221
// field.
216-
let expr = <ts.ObjectLiteralExpression>node;
222+
let expr = node as ts.ObjectLiteralExpression;
217223
if (expr.properties.length == 0) {
218224
position = expr.getEnd() - 1;
219225
toInsert = ` ${metadataField}: [${symbolName}]\n`;
@@ -281,7 +287,7 @@ export function addProviderToModule(modulePath: string, classifiedName: string,
281287
* Custom function to insert an export into NgModule. It also imports it.
282288
*/
283289
export function addExportToModule(modulePath: string, classifiedName: string,
284-
importPath: string): Promise<Change> {
290+
importPath: string): Promise<Change> {
285291
return _addSymbolToNgModuleMetadata(modulePath, 'exports', classifiedName, importPath);
286292
}
287293

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)