Skip to content

Commit 624c6c4

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. fixes angular#4323
1 parent c889dd8 commit 624c6c4

File tree

5 files changed

+100
-18
lines changed

5 files changed

+100
-18
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: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -204,6 +204,14 @@ function _addSymbolToNgModuleMetadata(ngModulePath: string, metadataField: strin
204204
console.log('No app module found. Please add your new class to your component.');
205205
return new NoopChange();
206206
}
207+
208+
let shouldInsertChange = true;
209+
if (Array.isArray(node)) {
210+
const nodeArray = node as any as Array<ts.Node>;
211+
const symbolsArray = nodeArray.map(node => node.getText());
212+
shouldInsertChange = !(symbolsArray.indexOf(symbolName) > -1);
213+
}
214+
207215
if (Array.isArray(node)) {
208216
node = node[node.length - 1];
209217
}
@@ -242,10 +250,16 @@ function _addSymbolToNgModuleMetadata(ngModulePath: string, metadataField: strin
242250
}
243251
}
244252

245-
const insert = new InsertChange(ngModulePath, position, toInsert);
246253
const importInsert: Change = insertImport(
247254
ngModulePath, symbolName.replace(/\..*$/, ''), importPath);
248-
return new MultiChange([insert, importInsert]);
255+
const changes = [importInsert];
256+
257+
if (shouldInsertChange) {
258+
const insert = new InsertChange(ngModulePath, position, toInsert);
259+
changes.push(insert);
260+
}
261+
262+
return new MultiChange(changes);
249263
});
250264
}
251265

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)