Stephen Cavaliere 9c25f746d2 fix(@angular/cli): generate command now ignores duplicate component symbol (#4446)
"ng g c X" will no longer insert the component symbol in the NgModule's declaration array should it already exist.

Closes #4323
2017-02-16 17:20:32 -08:00

324 lines
9.8 KiB
TypeScript

import denodeify = require('denodeify');
import mockFs = require('mock-fs');
import ts = require('typescript');
import fs = require('fs');
import {InsertChange, NodeHost, RemoveChange} from './change';
import {insertAfterLastOccurrence, addDeclarationToModule} from './ast-utils';
import {findNodes} from './node';
import {it} from './spec-utils';
const readFile = <any>denodeify(fs.readFile);
describe('ast-utils: findNodes', () => {
const sourceFile = 'tmp/tmp.ts';
beforeEach(() => {
let mockDrive = {
'tmp': {
'tmp.ts': `import * as myTest from 'tests' \n` +
'hello.'
}
};
mockFs(mockDrive);
});
afterEach(() => {
mockFs.restore();
});
it('finds no imports', () => {
let editedFile = new RemoveChange(sourceFile, 0, `import * as myTest from 'tests' \n`);
return editedFile
.apply(NodeHost)
.then(() => {
let rootNode = getRootNode(sourceFile);
let nodes = findNodes(rootNode, ts.SyntaxKind.ImportDeclaration);
expect(nodes).toEqual([]);
});
});
it('finds one import', () => {
let rootNode = getRootNode(sourceFile);
let nodes = findNodes(rootNode, ts.SyntaxKind.ImportDeclaration);
expect(nodes.length).toEqual(1);
});
it('finds two imports from inline declarations', () => {
// remove new line and add an inline import
let editedFile = new RemoveChange(sourceFile, 32, '\n');
return editedFile
.apply(NodeHost)
.then(() => {
let insert = new InsertChange(sourceFile, 32, `import {Routes} from '@angular/routes'`);
return insert.apply(NodeHost);
})
.then(() => {
let rootNode = getRootNode(sourceFile);
let nodes = findNodes(rootNode, ts.SyntaxKind.ImportDeclaration);
expect(nodes.length).toEqual(2);
});
});
it('finds two imports from new line separated declarations', () => {
let editedFile = new InsertChange(sourceFile, 33, `import {Routes} from '@angular/routes'`);
return editedFile
.apply(NodeHost)
.then(() => {
let rootNode = getRootNode(sourceFile);
let nodes = findNodes(rootNode, ts.SyntaxKind.ImportDeclaration);
expect(nodes.length).toEqual(2);
});
});
});
describe('ast-utils: insertAfterLastOccurrence', () => {
const sourceFile = 'tmp/tmp.ts';
beforeEach(() => {
let mockDrive = {
'tmp': {
'tmp.ts': ''
}
};
mockFs(mockDrive);
});
afterEach(() => {
mockFs.restore();
});
it('inserts at beginning of file', () => {
let imports = getNodesOfKind(ts.SyntaxKind.ImportDeclaration, sourceFile);
return insertAfterLastOccurrence(imports, `\nimport { Router } from '@angular/router';`,
sourceFile, 0)
.apply(NodeHost)
.then(() => {
return readFile(sourceFile, 'utf8');
}).then((content) => {
let expected = '\nimport { Router } from \'@angular/router\';';
expect(content).toEqual(expected);
});
});
it('throws an error if first occurence with no fallback position', () => {
let imports = getNodesOfKind(ts.SyntaxKind.ImportDeclaration, sourceFile);
expect(() => insertAfterLastOccurrence(imports, `import { Router } from '@angular/router';`,
sourceFile)).toThrowError();
});
it('inserts after last import', () => {
let content = `import { foo, bar } from 'fizz';`;
let editedFile = new InsertChange(sourceFile, 0, content);
return editedFile
.apply(NodeHost)
.then(() => {
let imports = getNodesOfKind(ts.SyntaxKind.ImportDeclaration, sourceFile);
return insertAfterLastOccurrence(imports, ', baz', sourceFile,
0, ts.SyntaxKind.Identifier)
.apply(NodeHost);
})
.then(() => {
return readFile(sourceFile, 'utf8');
})
.then(newContent => expect(newContent).toEqual(`import { foo, bar, baz } from 'fizz';`));
});
it('inserts after last import declaration', () => {
let content = `import * from 'foo' \n import { bar } from 'baz'`;
let editedFile = new InsertChange(sourceFile, 0, content);
return editedFile
.apply(NodeHost)
.then(() => {
let imports = getNodesOfKind(ts.SyntaxKind.ImportDeclaration, sourceFile);
return insertAfterLastOccurrence(imports, `\nimport Router from '@angular/router'`,
sourceFile)
.apply(NodeHost);
})
.then(() => {
return readFile(sourceFile, 'utf8');
})
.then(newContent => {
let expected = `import * from 'foo' \n import { bar } from 'baz'` +
`\nimport Router from '@angular/router'`;
expect(newContent).toEqual(expected);
});
});
it('inserts correctly if no imports', () => {
let content = `import {} from 'foo'`;
let editedFile = new InsertChange(sourceFile, 0, content);
return editedFile
.apply(NodeHost)
.then(() => {
let imports = getNodesOfKind(ts.SyntaxKind.ImportDeclaration, sourceFile);
return insertAfterLastOccurrence(imports, ', bar', sourceFile, undefined,
ts.SyntaxKind.Identifier)
.apply(NodeHost);
})
.catch(() => {
return readFile(sourceFile, 'utf8');
})
.then(newContent => {
expect(newContent).toEqual(content);
// use a fallback position for safety
let imports = getNodesOfKind(ts.SyntaxKind.ImportDeclaration, sourceFile);
let pos = findNodes(imports.sort((a, b) => a.pos - b.pos).pop(),
ts.SyntaxKind.CloseBraceToken).pop().pos;
return insertAfterLastOccurrence(imports, ' bar ',
sourceFile, pos, ts.SyntaxKind.Identifier)
.apply(NodeHost);
})
.then(() => {
return readFile(sourceFile, 'utf8');
})
.then(newContent => {
expect(newContent).toEqual(`import { bar } from 'foo'`);
});
});
});
describe('addDeclarationToModule', () => {
beforeEach(() => {
mockFs({
'1.ts': `
import {NgModule} from '@angular/core';
@NgModule({
declarations: []
})
class Module {}`,
'2.ts': `
import {NgModule} from '@angular/core';
@NgModule({
declarations: [
Other
]
})
class Module {}`,
'3.ts': `
import {NgModule} from '@angular/core';
@NgModule({
})
class Module {}`,
'4.ts': `
import {NgModule} from '@angular/core';
@NgModule({
field1: [],
field2: {}
})
class Module {}`
});
});
afterEach(() => mockFs.restore());
it('works with empty array', () => {
return addDeclarationToModule('1.ts', 'MyClass', 'MyImportPath')
.then(change => change.apply(NodeHost))
.then(() => readFile('1.ts', 'utf-8'))
.then(content => {
expect(content).toEqual(
'\n' +
'import {NgModule} from \'@angular/core\';\n' +
'import { MyClass } from \'MyImportPath\';\n' +
'\n' +
'@NgModule({\n' +
' declarations: [MyClass]\n' +
'})\n' +
'class Module {}'
);
});
});
it('does not append duplicate declarations', () => {
return addDeclarationToModule('2.ts', 'MyClass', 'MyImportPath')
.then(change => change.apply(NodeHost))
.then(() => addDeclarationToModule('2.ts', 'MyClass', 'MyImportPath'))
.then(change => change.apply(NodeHost))
.then(() => readFile('2.ts', 'utf-8'))
.then(content => {
expect(content).toEqual(
'\n' +
'import {NgModule} from \'@angular/core\';\n' +
'import { MyClass } from \'MyImportPath\';\n' +
'\n' +
'@NgModule({\n' +
' declarations: [\n' +
' Other,\n' +
' MyClass\n' +
' ]\n' +
'})\n' +
'class Module {}'
);
});
});
it('works with array with declarations', () => {
return addDeclarationToModule('2.ts', 'MyClass', 'MyImportPath')
.then(change => change.apply(NodeHost))
.then(() => readFile('2.ts', 'utf-8'))
.then(content => {
expect(content).toEqual(
'\n' +
'import {NgModule} from \'@angular/core\';\n' +
'import { MyClass } from \'MyImportPath\';\n' +
'\n' +
'@NgModule({\n' +
' declarations: [\n' +
' Other,\n' +
' MyClass\n' +
' ]\n' +
'})\n' +
'class Module {}'
);
});
});
it('works without any declarations', () => {
return addDeclarationToModule('3.ts', 'MyClass', 'MyImportPath')
.then(change => change.apply(NodeHost))
.then(() => readFile('3.ts', 'utf-8'))
.then(content => {
expect(content).toEqual(
'\n' +
'import {NgModule} from \'@angular/core\';\n' +
'import { MyClass } from \'MyImportPath\';\n' +
'\n' +
'@NgModule({\n' +
' declarations: [MyClass]\n' +
'})\n' +
'class Module {}'
);
});
});
it('works without a declaration field', () => {
return addDeclarationToModule('4.ts', 'MyClass', 'MyImportPath')
.then(change => change.apply(NodeHost))
.then(() => readFile('4.ts', 'utf-8'))
.then(content => {
expect(content).toEqual(
'\n' +
'import {NgModule} from \'@angular/core\';\n' +
'import { MyClass } from \'MyImportPath\';\n' +
'\n' +
'@NgModule({\n' +
' field1: [],\n' +
' field2: {},\n' +
' declarations: [MyClass]\n' +
'})\n' +
'class Module {}'
);
});
});
});
/**
* Gets node of kind kind from sourceFile
*/
function getNodesOfKind(kind: ts.SyntaxKind, sourceFile: string) {
return findNodes(getRootNode(sourceFile), kind);
}
function getRootNode(sourceFile: string) {
return ts.createSourceFile(sourceFile, fs.readFileSync(sourceFile).toString(),
ts.ScriptTarget.Latest, true);
}