From efcf1dcd95a8780eea39683ca7a32236d43c383c Mon Sep 17 00:00:00 2001 From: Filipe Silva Date: Wed, 25 Oct 2017 13:39:24 +0100 Subject: [PATCH 1/4] refactor(@angular/cli): remove AST utils They aren't being used anymore --- .../cli/lib/ast-tools/ast-utils.spec.ts | 323 --------- .../@angular/cli/lib/ast-tools/ast-utils.ts | 337 ---------- .../@angular/cli/lib/ast-tools/change.spec.ts | 126 ---- packages/@angular/cli/lib/ast-tools/change.ts | 172 ----- packages/@angular/cli/lib/ast-tools/index.ts | 4 - packages/@angular/cli/lib/ast-tools/node.ts | 47 -- .../cli/lib/ast-tools/route-utils.spec.ts | 625 ------------------ .../@angular/cli/lib/ast-tools/route-utils.ts | 557 ---------------- .../@angular/cli/lib/ast-tools/spec-utils.ts | 26 - tests/e2e/tests/build/chunk-hash.ts | 26 +- tests/e2e/tests/misc/common-async.ts | 12 +- tests/e2e/tests/misc/lazy-module.ts | 13 +- tests/e2e/utils/ast.ts | 16 - 13 files changed, 30 insertions(+), 2254 deletions(-) delete mode 100644 packages/@angular/cli/lib/ast-tools/ast-utils.spec.ts delete mode 100644 packages/@angular/cli/lib/ast-tools/ast-utils.ts delete mode 100644 packages/@angular/cli/lib/ast-tools/change.spec.ts delete mode 100644 packages/@angular/cli/lib/ast-tools/change.ts delete mode 100644 packages/@angular/cli/lib/ast-tools/index.ts delete mode 100644 packages/@angular/cli/lib/ast-tools/node.ts delete mode 100644 packages/@angular/cli/lib/ast-tools/route-utils.spec.ts delete mode 100644 packages/@angular/cli/lib/ast-tools/route-utils.ts delete mode 100644 packages/@angular/cli/lib/ast-tools/spec-utils.ts delete mode 100644 tests/e2e/utils/ast.ts diff --git a/packages/@angular/cli/lib/ast-tools/ast-utils.spec.ts b/packages/@angular/cli/lib/ast-tools/ast-utils.spec.ts deleted file mode 100644 index 50b8a5e0e520..000000000000 --- a/packages/@angular/cli/lib/ast-tools/ast-utils.spec.ts +++ /dev/null @@ -1,323 +0,0 @@ -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 = 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); -} diff --git a/packages/@angular/cli/lib/ast-tools/ast-utils.ts b/packages/@angular/cli/lib/ast-tools/ast-utils.ts deleted file mode 100644 index 370bb79b3d14..000000000000 --- a/packages/@angular/cli/lib/ast-tools/ast-utils.ts +++ /dev/null @@ -1,337 +0,0 @@ -import * as ts from 'typescript'; -import * as fs from 'fs'; -import { Change, InsertChange, NoopChange, MultiChange } from './change'; -import { findNodes } from './node'; -import { insertImport } from './route-utils'; -import { Observable } from 'rxjs/Observable'; -import { ReplaySubject } from 'rxjs/ReplaySubject'; -import 'rxjs/add/observable/empty'; -import 'rxjs/add/observable/of'; -import 'rxjs/add/operator/do'; -import 'rxjs/add/operator/filter'; -import 'rxjs/add/operator/last'; -import 'rxjs/add/operator/map'; -import 'rxjs/add/operator/mergeMap'; -import 'rxjs/add/operator/toArray'; -import 'rxjs/add/operator/toPromise'; - - -/** -* Get TS source file based on path. -* @param filePath -* @return source file of ts.SourceFile kind -*/ -export function getSource(filePath: string): ts.SourceFile { - return ts.createSourceFile(filePath, fs.readFileSync(filePath).toString(), - ts.ScriptTarget.Latest, true); -} - - -/** - * Get all the nodes from a source, as an observable. - * @param sourceFile The source file object. - * @returns {Observable} An observable of all the nodes in the source. - */ -export function getSourceNodes(sourceFile: ts.SourceFile): Observable { - const subject = new ReplaySubject(); - let nodes: ts.Node[] = [sourceFile]; - - while (nodes.length > 0) { - const node = nodes.shift(); - - if (node) { - subject.next(node); - if (node.getChildCount(sourceFile) >= 0) { - nodes.unshift(...node.getChildren()); - } - } - } - - subject.complete(); - return subject.asObservable(); -} - - -/** - * Helper for sorting nodes. - * @return function to sort nodes in increasing order of position in sourceFile - */ -function nodesByPosition(first: ts.Node, second: ts.Node): number { - return first.pos - second.pos; -} - - -/** - * Insert `toInsert` after the last occurence of `ts.SyntaxKind[nodes[i].kind]` - * or after the last of occurence of `syntaxKind` if the last occurence is a sub child - * of ts.SyntaxKind[nodes[i].kind] and save the changes in file. - * - * @param nodes insert after the last occurence of nodes - * @param toInsert string to insert - * @param file file to insert changes into - * @param fallbackPos position to insert if toInsert happens to be the first occurence - * @param syntaxKind the ts.SyntaxKind of the subchildren to insert after - * @return Change instance - * @throw Error if toInsert is first occurence but fall back is not set - */ -export function insertAfterLastOccurrence(nodes: ts.Node[], toInsert: string, - file: string, fallbackPos?: number, syntaxKind?: ts.SyntaxKind): Change { - let lastItem = nodes.sort(nodesByPosition).pop(); - if (syntaxKind) { - lastItem = findNodes(lastItem, syntaxKind).sort(nodesByPosition).pop(); - } - if (!lastItem && fallbackPos == undefined) { - throw new Error(`tried to insert ${toInsert} as first occurence with no fallback position`); - } - let lastItemPosition: number = lastItem ? lastItem.end : fallbackPos; - return new InsertChange(file, lastItemPosition, toInsert); -} - - -export function getContentOfKeyLiteral(_source: ts.SourceFile, node: ts.Node): string { - if (node.kind == ts.SyntaxKind.Identifier) { - return (node as ts.Identifier).text; - } else if (node.kind == ts.SyntaxKind.StringLiteral) { - return (node as ts.StringLiteral).text; - } else { - return null; - } -} - - - -function _angularImportsFromNode(node: ts.ImportDeclaration, - _sourceFile: ts.SourceFile): {[name: string]: string} { - const ms = node.moduleSpecifier; - let modulePath: string | null = null; - switch (ms.kind) { - case ts.SyntaxKind.StringLiteral: - modulePath = (ms as ts.StringLiteral).text; - break; - default: - return {}; - } - - if (!modulePath.startsWith('@angular/')) { - return {}; - } - - if (node.importClause) { - if (node.importClause.name) { - // This is of the form `import Name from 'path'`. Ignore. - return {}; - } else if (node.importClause.namedBindings) { - const nb = node.importClause.namedBindings; - if (nb.kind == ts.SyntaxKind.NamespaceImport) { - // This is of the form `import * as name from 'path'`. Return `name.`. - return { - [(nb as ts.NamespaceImport).name.text + '.']: modulePath - }; - } else { - // This is of the form `import {a,b,c} from 'path'` - const namedImports = nb as ts.NamedImports; - - return namedImports.elements - .map((is: ts.ImportSpecifier) => is.propertyName ? is.propertyName.text : is.name.text) - .reduce((acc: {[name: string]: string}, curr: string) => { - acc[curr] = modulePath; - return acc; - }, {}); - } - } - } else { - // This is of the form `import 'path';`. Nothing to do. - return {}; - } -} - - -export function getDecoratorMetadata(source: ts.SourceFile, identifier: string, - module: string): Observable { - const angularImports: {[name: string]: string} - = findNodes(source, ts.SyntaxKind.ImportDeclaration) - .map((node: ts.ImportDeclaration) => _angularImportsFromNode(node, source)) - .reduce((acc: {[name: string]: string}, current: {[name: string]: string}) => { - for (const key of Object.keys(current)) { - acc[key] = current[key]; - } - return acc; - }, {}); - - return getSourceNodes(source) - .filter(node => { - return node.kind == ts.SyntaxKind.Decorator - && (node as ts.Decorator).expression.kind == ts.SyntaxKind.CallExpression; - }) - .map(node => (node as ts.Decorator).expression as ts.CallExpression) - .filter(expr => { - if (expr.expression.kind == ts.SyntaxKind.Identifier) { - const id = expr.expression as ts.Identifier; - return id.getFullText(source) == identifier - && angularImports[id.getFullText(source)] === module; - } else if (expr.expression.kind == ts.SyntaxKind.PropertyAccessExpression) { - // This covers foo.NgModule when importing * as foo. - const paExpr = expr.expression as ts.PropertyAccessExpression; - // If the left expression is not an identifier, just give up at that point. - if (paExpr.expression.kind !== ts.SyntaxKind.Identifier) { - return false; - } - - const id = paExpr.name.text; - const moduleId = (paExpr.expression).getText(source); - return id === identifier && (angularImports[moduleId + '.'] === module); - } - return false; - }) - .filter(expr => expr.arguments[0] - && expr.arguments[0].kind == ts.SyntaxKind.ObjectLiteralExpression) - .map(expr => expr.arguments[0] as ts.ObjectLiteralExpression); -} - - -function _addSymbolToNgModuleMetadata(ngModulePath: string, metadataField: string, - symbolName: string, importPath: string) { - const source: ts.SourceFile = getSource(ngModulePath); - let metadata = getDecoratorMetadata(source, 'NgModule', '@angular/core'); - - // Find the decorator declaration. - return metadata - .toPromise() - .then((node: ts.ObjectLiteralExpression) => { - if (!node) { - return null; - } - - // Get all the children property assignment of object literals. - return node.properties - .filter(prop => prop.kind == ts.SyntaxKind.PropertyAssignment) - // Filter out every fields that's not "metadataField". Also handles string literals - // (but not expressions). - .filter((prop: ts.PropertyAssignment) => { - const name = prop.name; - switch (name.kind) { - case ts.SyntaxKind.Identifier: - return (name as ts.Identifier).getText(source) == metadataField; - case ts.SyntaxKind.StringLiteral: - return (name as ts.StringLiteral).text == metadataField; - } - - return false; - }); - }) - // Get the last node of the array literal. - .then((matchingProperties: ts.ObjectLiteralElement[]): any => { - if (!matchingProperties) { - return null; - } - if (matchingProperties.length == 0) { - return metadata.toPromise(); - } - - const assignment = matchingProperties[0] as ts.PropertyAssignment; - - // If it's not an array, nothing we can do really. - if (assignment.initializer.kind !== ts.SyntaxKind.ArrayLiteralExpression) { - return null; - } - - const arrLiteral = assignment.initializer as ts.ArrayLiteralExpression; - if (arrLiteral.elements.length == 0) { - // Forward the property. - return arrLiteral; - } - return arrLiteral.elements; - }) - .then((node: ts.Node) => { - if (!node) { - console.log('No app module found. Please add your new class to your component.'); - return new NoopChange(); - } - - if (Array.isArray(node)) { - const nodeArray = node as any as Array; - const symbolsArray = nodeArray.map(node => node.getText()); - if (symbolsArray.includes(symbolName)) { - return new NoopChange(); - } - - node = node[node.length - 1]; - } - - let toInsert: string; - let position = node.getEnd(); - if (node.kind == ts.SyntaxKind.ObjectLiteralExpression) { - // We haven't found the field in the metadata declaration. Insert a new - // field. - let expr = node as ts.ObjectLiteralExpression; - if (expr.properties.length == 0) { - position = expr.getEnd() - 1; - toInsert = ` ${metadataField}: [${symbolName}]\n`; - } else { - node = expr.properties[expr.properties.length - 1]; - position = node.getEnd(); - // Get the indentation of the last element, if any. - const text = node.getFullText(source); - if (text.match('^\r?\r?\n')) { - toInsert = `,${text.match(/^\r?\n\s+/)[0]}${metadataField}: [${symbolName}]`; - } else { - toInsert = `, ${metadataField}: [${symbolName}]`; - } - } - } else if (node.kind == ts.SyntaxKind.ArrayLiteralExpression) { - // We found the field but it's empty. Insert it just before the `]`. - position--; - toInsert = `${symbolName}`; - } else { - // Get the indentation of the last element, if any. - const text = node.getFullText(source); - if (text.match(/^\r?\n/)) { - toInsert = `,${text.match(/^\r?\n(\r?)\s+/)[0]}${symbolName}`; - } else { - toInsert = `, ${symbolName}`; - } - } - - const insert = new InsertChange(ngModulePath, position, toInsert); - const importInsert: Change = insertImport( - ngModulePath, symbolName.replace(/\..*$/, ''), importPath); - return new MultiChange([insert, importInsert]); - }); -} - -/** -* Custom function to insert a declaration (component, pipe, directive) -* into NgModule declarations. It also imports the component. -*/ -export function addDeclarationToModule(modulePath: string, classifiedName: string, - importPath: string): Promise { - - return _addSymbolToNgModuleMetadata(modulePath, 'declarations', classifiedName, importPath); -} - -/** - * Custom function to insert a declaration (component, pipe, directive) - * into NgModule declarations. It also imports the component. - */ -export function addImportToModule(modulePath: string, classifiedName: string, - importPath: string): Promise { - - return _addSymbolToNgModuleMetadata(modulePath, 'imports', classifiedName, importPath); -} - -/** - * Custom function to insert a provider into NgModule. It also imports it. - */ -export function addProviderToModule(modulePath: string, classifiedName: string, - importPath: string): Promise { - return _addSymbolToNgModuleMetadata(modulePath, 'providers', classifiedName, importPath); -} - -/** - * Custom function to insert an export into NgModule. It also imports it. - */ -export function addExportToModule(modulePath: string, classifiedName: string, - importPath: string): Promise { - return _addSymbolToNgModuleMetadata(modulePath, 'exports', classifiedName, importPath); -} - diff --git a/packages/@angular/cli/lib/ast-tools/change.spec.ts b/packages/@angular/cli/lib/ast-tools/change.spec.ts deleted file mode 100644 index 52363caf10ab..000000000000 --- a/packages/@angular/cli/lib/ast-tools/change.spec.ts +++ /dev/null @@ -1,126 +0,0 @@ -// This needs to be first so fs module can be mocked correctly. -import mockFs = require('mock-fs'); - -import { it } from './spec-utils'; -import { InsertChange, NodeHost, RemoveChange, ReplaceChange } from './change'; -import { readFile } from 'fs-extra'; -import * as path from 'path'; - - -describe('Change', () => { - let sourcePath = 'src/app/my-component'; - - beforeEach(() => { - let mockDrive = { - 'src/app/my-component': { - 'add-file.txt': 'hello', - 'remove-replace-file.txt': 'import * as foo from "./bar"', - 'replace-file.txt': 'import { FooComponent } from "./baz"' - } - }; - mockFs(mockDrive); - }); - afterEach(() => { - mockFs.restore(); - }); - - describe('InsertChange', () => { - let sourceFile = path.join(sourcePath, 'add-file.txt'); - - it('adds text to the source code', () => { - let changeInstance = new InsertChange(sourceFile, 6, ' world!'); - return changeInstance - .apply(NodeHost) - .then(() => readFile(sourceFile, 'utf8')) - .then(contents => { - expect(contents).toEqual('hello world!'); - }); - }); - it('fails for negative position', () => { - expect(() => new InsertChange(sourceFile, -6, ' world!')).toThrowError(); - }); - it('adds nothing in the source code if empty string is inserted', () => { - let changeInstance = new InsertChange(sourceFile, 6, ''); - return changeInstance - .apply(NodeHost) - .then(() => readFile(sourceFile, 'utf8')) - .then(contents => { - expect(contents).toEqual('hello'); - }); - }); - }); - - describe('RemoveChange', () => { - let sourceFile = path.join(sourcePath, 'remove-replace-file.txt'); - - it('removes given text from the source code', () => { - let changeInstance = new RemoveChange(sourceFile, 9, 'as foo'); - return changeInstance - .apply(NodeHost) - .then(() => readFile(sourceFile, 'utf8')) - .then(contents => { - expect(contents).toEqual('import * from "./bar"'); - }); - }); - it('fails for negative position', () => { - expect(() => new RemoveChange(sourceFile, -6, ' world!')).toThrow(); - }); - it('does not change the file if told to remove empty string', () => { - let changeInstance = new RemoveChange(sourceFile, 9, ''); - return changeInstance - .apply(NodeHost) - .then(() => readFile(sourceFile, 'utf8')) - .then(contents => { - expect(contents).toEqual('import * as foo from "./bar"'); - }); - }); - }); - - describe('ReplaceChange', () => { - it('replaces the given text in the source code', () => { - let sourceFile = path.join(sourcePath, 'remove-replace-file.txt'); - let changeInstance = new ReplaceChange(sourceFile, 7, '* as foo', '{ fooComponent }'); - return changeInstance - .apply(NodeHost) - .then(() => readFile(sourceFile, 'utf8')) - .then(contents => { - expect(contents).toEqual('import { fooComponent } from "./bar"'); - }); - }); - it('fails for negative position', () => { - let sourceFile = path.join(sourcePath, 'remove-replace-file.txt'); - expect(() => new ReplaceChange(sourceFile, -6, 'hello', ' world!')).toThrow(); - }); - it('fails for invalid replacement', () => { - let sourceFile = path.join(sourcePath, 'replace-file.txt'); - let changeInstance = new ReplaceChange(sourceFile, 0, 'foobar', ''); - return changeInstance - .apply(NodeHost) - .then(() => expect(false).toBe(true), err => { - // Check that the message contains the string to replace and the string from the file. - expect(err.message).toContain('foobar'); - expect(err.message).toContain('import'); - }); - }); - it('adds string to the position of an empty string', () => { - let sourceFile = path.join(sourcePath, 'replace-file.txt'); - let changeInstance = new ReplaceChange(sourceFile, 9, '', 'BarComponent, '); - return changeInstance - .apply(NodeHost) - .then(() => readFile(sourceFile, 'utf8')) - .then(contents => { - expect(contents).toEqual('import { BarComponent, FooComponent } from "./baz"'); - }); - }); - it('removes the given string only if an empty string to add is given', () => { - let sourceFile = path.join(sourcePath, 'remove-replace-file.txt'); - let changeInstance = new ReplaceChange(sourceFile, 8, ' as foo', ''); - return changeInstance - .apply(NodeHost) - .then(() => readFile(sourceFile, 'utf8')) - .then(contents => { - expect(contents).toEqual('import * from "./bar"'); - }); - }); - }); -}); diff --git a/packages/@angular/cli/lib/ast-tools/change.ts b/packages/@angular/cli/lib/ast-tools/change.ts deleted file mode 100644 index 073956ad5b02..000000000000 --- a/packages/@angular/cli/lib/ast-tools/change.ts +++ /dev/null @@ -1,172 +0,0 @@ -import fs = require('fs'); -import denodeify = require('denodeify'); - -const readFile = (denodeify(fs.readFile) as (...args: any[]) => Promise); -const writeFile = (denodeify(fs.writeFile) as (...args: any[]) => Promise); - -export interface Host { - write(path: string, content: string): Promise; - read(path: string): Promise; -} - -export const NodeHost: Host = { - write: (path: string, content: string) => writeFile(path, content, 'utf8'), - read: (path: string) => readFile(path, 'utf8') -}; - - -export interface Change { - apply(host: Host): Promise; - - // The file this change should be applied to. Some changes might not apply to - // a file (maybe the config). - readonly path: string | null; - - // The order this change should be applied. Normally the position inside the file. - // Changes are applied from the bottom of a file to the top. - readonly order: number; - - // The description of this change. This will be outputted in a dry or verbose run. - readonly description: string; -} - - -/** - * An operation that does nothing. - */ -export class NoopChange implements Change { - description = 'No operation.'; - order = Infinity; - path: string = null; - apply() { return Promise.resolve(); } -} - -/** - * An operation that mixes two or more changes, and merge them (in order). - * Can only apply to a single file. Use a ChangeManager to apply changes to multiple - * files. - */ -export class MultiChange implements Change { - private _path: string; - private _changes: Change[]; - - constructor(...changes: (Change[] | Change)[]) { - this._changes = []; - [].concat(...changes).forEach(change => this.appendChange(change)); - } - - appendChange(change: Change) { - // Do not append Noop changes. - if (change instanceof NoopChange) { - return; - } - // Validate that the path is the same for everyone of those. - if (this._path === undefined) { - this._path = change.path; - } else if (change.path !== this._path) { - throw new Error('Cannot apply a change to a different path.'); - } - this._changes.push(change); - } - - get description() { - return `Changes:\n ${this._changes.map(x => x.description).join('\n ')}`; - } - // Always apply as early as the highest change. - get order() { return Math.max(...this._changes.map(c => c.order)); } - get path() { return this._path; } - - apply(host: Host) { - return this._changes - .sort((a: Change, b: Change) => b.order - a.order) - .reduce((promise, change) => { - return promise.then(() => change.apply(host)); - }, Promise.resolve()); - } -} - - -/** - * Will add text to the source code. - */ -export class InsertChange implements Change { - - order: number; - description: string; - - constructor(public path: string, private pos: number, private toAdd: string) { - if (pos < 0) { - throw new Error('Negative positions are invalid'); - } - this.description = `Inserted ${toAdd} into position ${pos} of ${path}`; - this.order = pos; - } - - /** - * This method does not insert spaces if there is none in the original string. - */ - apply(host: Host): Promise { - return host.read(this.path).then(content => { - let prefix = content.substring(0, this.pos); - let suffix = content.substring(this.pos); - return host.write(this.path, `${prefix}${this.toAdd}${suffix}`); - }); - } -} - -/** - * Will remove text from the source code. - */ -export class RemoveChange implements Change { - - order: number; - description: string; - - constructor(public path: string, private pos: number, private toRemove: string) { - if (pos < 0) { - throw new Error('Negative positions are invalid'); - } - this.description = `Removed ${toRemove} into position ${pos} of ${path}`; - this.order = pos; - } - - apply(host: Host): Promise { - return host.read(this.path).then(content => { - let prefix = content.substring(0, this.pos); - let suffix = content.substring(this.pos + this.toRemove.length); - // TODO: throw error if toRemove doesn't match removed string. - return host.write(this.path, `${prefix}${suffix}`); - }); - } -} - -/** - * Will replace text from the source code. - */ -export class ReplaceChange implements Change { - order: number; - description: string; - - constructor(public path: string, private pos: number, private oldText: string, - private newText: string) { - if (pos < 0) { - throw new Error('Negative positions are invalid'); - } - this.description = `Replaced ${oldText} into position ${pos} of ${path} with ${newText}`; - this.order = pos; - } - - apply(host: Host): Promise { - return host.read(this.path).then(content => { - const prefix = content.substring(0, this.pos); - const suffix = content.substring(this.pos + this.oldText.length); - const text = content.substring(this.pos, this.pos + this.oldText.length); - - if (text !== this.oldText) { - return Promise.reject(new Error(`Invalid replace: "${text}" != "${this.oldText}".`)); - } - // TODO: throw error if oldText doesn't match removed string. - return host.write(this.path, `${prefix}${this.newText}${suffix}`); - }); - } -} diff --git a/packages/@angular/cli/lib/ast-tools/index.ts b/packages/@angular/cli/lib/ast-tools/index.ts deleted file mode 100644 index d76151999687..000000000000 --- a/packages/@angular/cli/lib/ast-tools/index.ts +++ /dev/null @@ -1,4 +0,0 @@ -export * from './ast-utils'; -export * from './change'; -export * from './node'; -export * from './route-utils'; diff --git a/packages/@angular/cli/lib/ast-tools/node.ts b/packages/@angular/cli/lib/ast-tools/node.ts deleted file mode 100644 index be0635f0fc6c..000000000000 --- a/packages/@angular/cli/lib/ast-tools/node.ts +++ /dev/null @@ -1,47 +0,0 @@ -import ts = require('typescript'); -import {RemoveChange, Change} from './change'; - - -/** - * Find all nodes from the AST in the subtree of node of SyntaxKind kind. - * @param node - * @param kind - * @param max The maximum number of items to return. - * @return all nodes of kind, or [] if none is found - */ -export function findNodes(node: ts.Node, kind: ts.SyntaxKind, max = Infinity): ts.Node[] { - if (!node || max == 0) { - return []; - } - - let arr: ts.Node[] = []; - if (node.kind === kind) { - arr.push(node); - max--; - } - if (max > 0) { - for (const child of node.getChildren()) { - findNodes(child, kind, max).forEach(node => { - if (max > 0) { - arr.push(node); - } - max--; - }); - - if (max <= 0) { - break; - } - } - } - return arr; -} - - -export function removeAstNode(node: ts.Node): Change { - const source = node.getSourceFile(); - return new RemoveChange( - (source as any).path, - node.getStart(source), - node.getFullText(source) - ); -} diff --git a/packages/@angular/cli/lib/ast-tools/route-utils.spec.ts b/packages/@angular/cli/lib/ast-tools/route-utils.spec.ts deleted file mode 100644 index 3b7f32a75e1a..000000000000 --- a/packages/@angular/cli/lib/ast-tools/route-utils.spec.ts +++ /dev/null @@ -1,625 +0,0 @@ -import * as mockFs from 'mock-fs'; -import * as fs from 'fs'; -import * as nru from './route-utils'; -import * as path from 'path'; -import { NodeHost, InsertChange, RemoveChange } from './change'; -import denodeify = require('denodeify'); -import * as _ from 'lodash'; -import {it} from './spec-utils'; - -const readFile = (denodeify(fs.readFile) as (...args: any[]) => Promise); - - -describe('route utils', () => { - describe('insertImport', () => { - const sourceFile = 'tmp/tmp.ts'; - beforeEach(() => { - let mockDrive = { - 'tmp': { - 'tmp.ts': '' - } - }; - mockFs(mockDrive); - }); - - afterEach(() => { - mockFs.restore(); - }); - - it('inserts as last import if not present', () => { - let content = `'use strict'\n import {foo} from 'bar'\n import * as fz from 'fizz';`; - let editedFile = new InsertChange(sourceFile, 0, content); - return editedFile.apply(NodeHost) - .then(() => nru.insertImport(sourceFile, 'Router', '@angular/router').apply(NodeHost)) - .then(() => readFile(sourceFile, 'utf8')) - .then(newContent => { - expect(newContent).toEqual(content + `\nimport { Router } from '@angular/router';`); - }); - }); - it('does not insert if present', () => { - let content = `'use strict'\n import {Router} from '@angular/router'`; - let editedFile = new InsertChange(sourceFile, 0, content); - return editedFile.apply(NodeHost) - .then(() => nru.insertImport(sourceFile, 'Router', '@angular/router')) - .then(() => readFile(sourceFile, 'utf8')) - .then(newContent => { - expect(newContent).toEqual(content); - }); - }); - it('inserts into existing import clause if import file is already cited', () => { - let content = `'use strict'\n import { foo, bar } from 'fizz'`; - let editedFile = new InsertChange(sourceFile, 0, content); - return editedFile.apply(NodeHost) - .then(() => nru.insertImport(sourceFile, 'baz', 'fizz').apply(NodeHost)) - .then(() => readFile(sourceFile, 'utf8')) - .then(newContent => { - expect(newContent).toEqual(`'use strict'\n import { foo, bar, baz } from 'fizz'`); - }); - }); - it('understands * imports', () => { - let content = `\nimport * as myTest from 'tests' \n`; - let editedFile = new InsertChange(sourceFile, 0, content); - return editedFile.apply(NodeHost) - .then(() => nru.insertImport(sourceFile, 'Test', 'tests')) - .then(() => readFile(sourceFile, 'utf8')) - .then(newContent => { - expect(newContent).toEqual(content); - }); - }); - it('inserts after use-strict', () => { - let content = `'use strict';\n hello`; - let editedFile = new InsertChange(sourceFile, 0, content); - return editedFile.apply(NodeHost) - .then(() => nru.insertImport(sourceFile, 'Router', '@angular/router').apply(NodeHost)) - .then(() => readFile(sourceFile, 'utf8')) - .then(newContent => { - expect(newContent).toEqual( - `'use strict';\nimport { Router } from '@angular/router';\n hello`); - }); - }); - it('inserts inserts at beginning of file if no imports exist', () => { - return nru.insertImport(sourceFile, 'Router', '@angular/router').apply(NodeHost) - .then(() => readFile(sourceFile, 'utf8')) - .then(newContent => { - expect(newContent).toEqual(`import { Router } from '@angular/router';\n`); - }); - }); - it('inserts subcomponent in win32 environment', () => { - let content = './level1\\level2/level2.component'; - return nru.insertImport(sourceFile, 'level2', content).apply(NodeHost) - .then(() => readFile(sourceFile, 'utf8')) - .then(newContent => { - if (process.platform.startsWith('win')) { - expect(newContent).toEqual( - `import { level2 } from './level1/level2/level2.component';\n`); - } else { - expect(newContent).toEqual( - `import { level2 } from './level1\\level2/level2.component';\n`); - } - }); - }); - }); - - describe('bootstrapItem', () => { - const mainFile = 'tmp/main.ts'; - const prefix = `import {bootstrap} from '@angular/platform-browser-dynamic'; \n` + - `import { AppComponent } from './app/';\n`; - const routes = {'provideRouter': ['@angular/router'], 'routes': ['./routes', true]}; - const toBootstrap = 'provideRouter(routes)'; - const routerImport = `import routes from './routes';\n` + - `import { provideRouter } from '@angular/router'; \n`; - beforeEach(() => { - let mockDrive = { - 'tmp': { - 'main.ts': `import {bootstrap} from '@angular/platform-browser-dynamic'; \n` + - `import { AppComponent } from './app/'; \n` + - 'bootstrap(AppComponent);' - } - }; - mockFs(mockDrive); - }); - - afterEach(() => { - mockFs.restore(); - }); - - it('adds a provideRouter import if not there already', () => { - return nru.applyChanges(nru.bootstrapItem(mainFile, routes, toBootstrap)) - .then(() => readFile(mainFile, 'utf8')) - .then(content => { - expect(content).toEqual(prefix + routerImport + - 'bootstrap(AppComponent, [ provideRouter(routes) ]);'); - }); - }); - xit('does not add a provideRouter import if it exits already', () => { - return nru.insertImport(mainFile, 'provideRouter', '@angular/router').apply(NodeHost) - .then(() => nru.applyChanges(nru.bootstrapItem(mainFile, routes, toBootstrap))) - .then(() => readFile(mainFile, 'utf8')) - .then(content => { - expect(content).toEqual( - `import routes from './routes'; - import { provideRouter } from '@angular/router'; - bootstrap(AppComponent, [ provideRouter(routes) ]);`); - }); - }); - xit('does not duplicate import to route.ts ', () => { - let editedFile = new InsertChange(mainFile, 100, `\nimport routes from './routes';`); - return editedFile - .apply(NodeHost) - .then(() => nru.applyChanges(nru.bootstrapItem(mainFile, routes, toBootstrap))) - .then(() => readFile(mainFile, 'utf8')) - .then(content => { - expect(content).toEqual(prefix + routerImport + - 'bootstrap(AppComponent, [ provideRouter(routes) ]);'); - }); - }); - it('adds provideRouter to bootstrap if absent and no providers array', () => { - return nru.applyChanges(nru.bootstrapItem(mainFile, routes, toBootstrap)) - .then(() => readFile(mainFile, 'utf8')) - .then(content => { - expect(content).toEqual(prefix + routerImport + - 'bootstrap(AppComponent, [ provideRouter(routes) ]);'); - }); - }); - it('adds provideRouter to bootstrap if absent and empty providers array', () => { - let editFile = new InsertChange(mainFile, 124, ', []'); - return editFile.apply(NodeHost) - .then(() => nru.applyChanges(nru.bootstrapItem(mainFile, routes, toBootstrap))) - .then(() => readFile(mainFile, 'utf8')) - .then(content => { - expect(content).toEqual(prefix + routerImport + - 'bootstrap(AppComponent, [provideRouter(routes)]);'); - }); - }); - it('adds provideRouter to bootstrap if absent and non-empty providers array', () => { - let editedFile = new InsertChange(mainFile, 124, ', [ HTTP_PROVIDERS ]'); - return editedFile.apply(NodeHost) - .then(() => nru.applyChanges(nru.bootstrapItem(mainFile, routes, toBootstrap))) - .then(() => readFile(mainFile, 'utf8')) - .then(content => { - expect(content).toEqual(prefix + routerImport + - 'bootstrap(AppComponent, [ HTTP_PROVIDERS, provideRouter(routes) ]);'); - }); - }); - it('does not add provideRouter to bootstrap if present', () => { - let editedFile = new InsertChange(mainFile, - 124, - ', [ HTTP_PROVIDERS, provideRouter(routes) ]'); - return editedFile.apply(NodeHost) - .then(() => nru.applyChanges(nru.bootstrapItem(mainFile, routes, toBootstrap))) - .then(() => readFile(mainFile, 'utf8')) - .then(content => { - expect(content).toEqual(prefix + routerImport + - 'bootstrap(AppComponent, [ HTTP_PROVIDERS, provideRouter(routes) ]);'); - }); - }); - it('inserts into the correct array', () => { - let editedFile = new InsertChange(mainFile, 124, ', [ HTTP_PROVIDERS, {provide: [BAR]}]'); - return editedFile.apply(NodeHost) - .then(() => nru.applyChanges(nru.bootstrapItem(mainFile, routes, toBootstrap))) - .then(() => readFile(mainFile, 'utf8')) - .then(content => { - expect(content).toEqual(prefix + routerImport + - 'bootstrap(AppComponent, [ HTTP_PROVIDERS, {provide: [BAR]}, provideRouter(routes)]);'); - }); - }); - it('throws an error if there is no or multiple bootstrap expressions', () => { - let editedFile = new InsertChange(mainFile, 126, '\n bootstrap(moreStuff);'); - return editedFile.apply(NodeHost) - .then(() => nru.bootstrapItem(mainFile, routes, toBootstrap)) - .catch(e => - expect(e.message).toEqual('Did not bootstrap provideRouter in' + - ' tmp/main.ts because of multiple or no bootstrap calls') - ); - }); - it('configures correctly if bootstrap or provide router is not at top level', () => { - let editedFile = new InsertChange(mainFile, 126, '\n if(e){bootstrap, provideRouter});'); - return editedFile.apply(NodeHost) - .then(() => nru.applyChanges(nru.bootstrapItem(mainFile, routes, toBootstrap))) - .then(() => readFile(mainFile, 'utf8')) - .then(content => { - expect(content).toEqual(prefix + routerImport + - 'bootstrap(AppComponent, [ provideRouter(routes) ]);\n if(e){bootstrap, provideRouter});'); // tslint:disable-line - }); - }); - }); - - describe('addPathToRoutes', () => { - const routesFile = 'src/routes.ts'; - let options = {dir: 'src/app', appRoot: 'src/app', routesFile: routesFile, - component: 'NewRouteComponent', dasherizedName: 'new-route'}; - const nestedRoutes = `\n { path: 'home', component: HomeComponent, - children: [ - { path: 'about', component: AboutComponent, - children: [ - { path: 'more', component: MoreComponent } - ] - } - ] - }\n`; - beforeEach(() => { - let mockDrive = { - 'src': { - 'routes.ts' : 'export default [];' - } - }; - mockFs(mockDrive); - }); - - afterEach(() => { - mockFs.restore(); - }); - - it('adds import to new route component if absent', () => { - return nru.applyChanges(nru.addPathToRoutes(routesFile, - _.merge({route: 'new-route'}, options))) - .then(() => readFile(routesFile, 'utf8')) - .then(content => { - expect(content).toEqual( - `import { NewRouteComponent } from './app/new-route/new-route.component'; -export default [\n { path: 'new-route', component: NewRouteComponent }\n];`); - }); - }); - it('throws error if multiple export defaults exist', () => { - let editedFile = new InsertChange(routesFile, 20, 'export default {}'); - return editedFile.apply(NodeHost).then(() => { - return nru.addPathToRoutes(routesFile, _.merge({route: 'new-route'}, options)); - }).catch(e => { - expect(e.message).toEqual('Did not insert path in routes.ts because ' - + `there were multiple or no 'export default' statements`); - }); - }); - it('throws error if no export defaults exists', () => { - let editedFile = new RemoveChange(routesFile, 0, 'export default []'); - return editedFile.apply(NodeHost).then(() => { - return nru.addPathToRoutes(routesFile, _.merge({route: 'new-route'}, options)); - }).catch(e => { - expect(e.message).toEqual('Did not insert path in routes.ts because ' - + `there were multiple or no 'export default' statements`); - }); - }); - it('treats positional params correctly', () => { - let editedFile = new InsertChange(routesFile, 16, - `\n { path: 'home', component: HomeComponent }\n`); - return editedFile.apply(NodeHost).then(() => { - options.dasherizedName = 'about'; - options.component = 'AboutComponent'; - return nru.applyChanges( - nru.addPathToRoutes(routesFile, _.merge({route: 'home/about/:id'}, options))); }) - .then(() => readFile(routesFile, 'utf8')) - .then(content => { - expect(content).toEqual( - `import { AboutComponent } from './app/home/about/about.component';` + - `\nexport default [\n` + - ` { path: 'home', component: HomeComponent,\n` + - ` children: [\n` + - ` { path: 'about/:id', component: AboutComponent }` + - `\n ]\n }\n];`); - }); - }); - it('inserts under parent, mid', () => { - let editedFile = new InsertChange(routesFile, 16, nestedRoutes); - return editedFile.apply(NodeHost).then(() => { - options.dasherizedName = 'details'; - options.component = 'DetailsComponent'; - return nru.applyChanges( - nru.addPathToRoutes(routesFile, _.merge({route: 'home/about/details'}, options))); }) - .then(() => readFile(routesFile, 'utf8')) - .then(content => { - // tslint:disable-next-line - let expected = `import { DetailsComponent } from './app/home/about/details/details.component'; -export default [ - { path: 'home', component: HomeComponent, - children: [ - { path: 'about', component: AboutComponent, - children: [ - { path: 'details', component: DetailsComponent }, - { path: 'more', component: MoreComponent } - ] - } - ] - }\n];`; - expect(content).toEqual(expected); - }); - }); - it('inserts under parent, deep', () => { - let editedFile = new InsertChange(routesFile, 16, nestedRoutes); - return editedFile.apply(NodeHost).then(() => { - options.dasherizedName = 'sections'; - options.component = 'SectionsComponent'; - return nru.applyChanges( - nru.addPathToRoutes(routesFile, - _.merge({route: 'home/about/more/sections'}, options))); }) - .then(() => readFile(routesFile, 'utf8')) - .then(content => { - // tslint:disable-next-line - let expected = `import { SectionsComponent } from './app/home/about/more/sections/sections.component'; -export default [ - { path: 'home', component: HomeComponent, - children: [ - { path: 'about', component: AboutComponent, - children: [ - { path: 'more', component: MoreComponent, - children: [ - { path: 'sections', component: SectionsComponent } - ] - } - ] - } - ] - } -];`; - expect(content).toEqual(expected); - }); - }); - it('works well with multiple routes in a level', () => { - let paths = `\n { path: 'main', component: MainComponent } - { path: 'home', component: HomeComponent, - children: [ - { path: 'about', component: AboutComponent } - ] - }\n`; - let editedFile = new InsertChange(routesFile, 16, paths); - return editedFile.apply(NodeHost).then(() => { - options.dasherizedName = 'about'; - options.component = 'AboutComponent_1'; - return nru.applyChanges( - nru.addPathToRoutes(routesFile, _.merge({route: 'home/about/:id'}, options))); }) - .then(() => readFile(routesFile, 'utf8')) - .then(content => { - // tslint:disable-next-line - expect(content).toEqual(`import { AboutComponent_1 } from './app/home/about/about.component'; -export default [ - { path: 'main', component: MainComponent } - { path: 'home', component: HomeComponent, - children: [ - { path: 'about/:id', component: AboutComponent_1 }, - { path: 'about', component: AboutComponent } - ] - } -];` - ); - }); - }); - it('throws error if repeating child, shallow', () => { - let editedFile = new InsertChange(routesFile, 16, nestedRoutes); - return editedFile.apply(NodeHost).then(() => { - options.dasherizedName = 'home'; - options.component = 'HomeComponent'; - return nru.addPathToRoutes(routesFile, _.merge({route: '/home'}, options)); - }).catch(e => { - expect(e.message).toEqual('Route was not added since it is a duplicate'); - }); - }); - it('throws error if repeating child, mid', () => { - let editedFile = new InsertChange(routesFile, 16, nestedRoutes); - return editedFile.apply(NodeHost).then(() => { - options.dasherizedName = 'about'; - options.component = 'AboutComponent'; - return nru.addPathToRoutes(routesFile, _.merge({route: 'home/about/'}, options)); - }).catch(e => { - expect(e.message).toEqual('Route was not added since it is a duplicate'); - }); - }); - it('throws error if repeating child, deep', () => { - let editedFile = new InsertChange(routesFile, 16, nestedRoutes); - return editedFile.apply(NodeHost).then(() => { - options.dasherizedName = 'more'; - options.component = 'MoreComponent'; - return nru.addPathToRoutes(routesFile, _.merge({route: 'home/about/more'}, options)); - }).catch(e => { - expect(e.message).toEqual('Route was not added since it is a duplicate'); - }); - }); - it('does not report false repeat', () => { - let editedFile = new InsertChange(routesFile, 16, nestedRoutes); - return editedFile.apply(NodeHost).then(() => { - options.dasherizedName = 'more'; - options.component = 'MoreComponent'; - return nru.applyChanges(nru.addPathToRoutes(routesFile, _.merge({route: 'more'}, options))); - }) - .then(() => readFile(routesFile, 'utf8')) - .then(content => { - let expected = `import { MoreComponent } from './app/more/more.component'; -export default [ - { path: 'more', component: MoreComponent }, - { path: 'home', component: HomeComponent, - children: [ - { path: 'about', component: AboutComponent, - children: [ - { path: 'more', component: MoreComponent } - ] - } - ] - }\n];`; - expect(content).toEqual(expected); - }); - }); - it('does not report false repeat: multiple paths on a level', () => { - - let routes = `\n { path: 'home', component: HomeComponent, - children: [ - { path: 'about', component: AboutComponent, - children: [ - { path: 'more', component: MoreComponent } - ] - } - ] - },\n { path: 'trap-queen', component: TrapQueenComponent}\n`; - - let editedFile = new InsertChange(routesFile, 16, routes); - return editedFile.apply(NodeHost).then(() => { - options.dasherizedName = 'trap-queen'; - options.component = 'TrapQueenComponent'; - return nru.applyChanges( - nru.addPathToRoutes(routesFile, _.merge({route: 'home/trap-queen'}, options))); - }) - .then(() => readFile(routesFile, 'utf8')) - .then(content => { - // tslint:disable-next-line - let expected = `import { TrapQueenComponent } from './app/home/trap-queen/trap-queen.component'; -export default [ - { path: 'home', component: HomeComponent, - children: [ - { path: 'trap-queen', component: TrapQueenComponent }, - { path: 'about', component: AboutComponent, - children: [ - { path: 'more', component: MoreComponent } - ] - } - ] - },\n { path: 'trap-queen', component: TrapQueenComponent}\n];`; - expect(content).toEqual(expected); - }); - }); - it('resolves imports correctly', () => { - let editedFile = new InsertChange(routesFile, 16, - `\n { path: 'home', component: HomeComponent }\n`); - return editedFile.apply(NodeHost).then(() => { - let editedFile = new InsertChange(routesFile, 0, - `import { HomeComponent } from './app/home/home.component';\n`); - return editedFile.apply(NodeHost); - }) - .then(() => { - options.dasherizedName = 'home'; - options.component = 'HomeComponent'; - return nru.applyChanges( - nru.addPathToRoutes(routesFile, _.merge({route: 'home/home'}, options))); }) - .then(() => readFile(routesFile, 'utf8')) - .then(content => { - let expected = `import { HomeComponent } from './app/home/home.component'; -import { HomeComponent as HomeComponent_1 } from './app/home/home/home.component'; -export default [ - { path: 'home', component: HomeComponent, - children: [ - { path: 'home', component: HomeComponent_1 } - ] - } -];`; - expect(content).toEqual(expected); - }); - }); - it('throws error if components collide and there is repitition', () => { - let editedFile = new InsertChange(routesFile, 16, - `\n { path: 'about', component: AboutComponent, - children: [ - { path: 'details/:id', component: DetailsComponent_1 }, - { path: 'details', component: DetailsComponent } - ] - }`); - return editedFile.apply(NodeHost).then(() => { - let editedFile = new InsertChange(routesFile, 0, - `import { AboutComponent } from './app/about/about.component'; -import { DetailsComponent } from './app/about/details/details.component'; -import { DetailsComponent as DetailsComponent_1 } from './app/about/description/details.component;\n`); // tslint:disable-line - return editedFile.apply(NodeHost); - }).then(() => { - options.dasherizedName = 'details'; - options.component = 'DetailsComponent'; - expect(() => nru.addPathToRoutes(routesFile, _.merge({route: 'about/details'}, options))) - .toThrowError(); - }); - }); - - it('adds guard to parent route: addItemsToRouteProperties', () => { - let path = `\n { path: 'home', component: HomeComponent }\n`; - let editedFile = new InsertChange(routesFile, 16, path); - return editedFile.apply(NodeHost).then(() => { - let toInsert = {'home': ['canActivate', '[ MyGuard ]'] }; - return nru.applyChanges(nru.addItemsToRouteProperties(routesFile, toInsert)); - }) - .then(() => readFile(routesFile, 'utf8')) - .then(content => { - expect(content).toEqual(`export default [ - { path: 'home', component: HomeComponent, canActivate: [ MyGuard ] } -];` - ); - }); - }); - it('adds guard to child route: addItemsToRouteProperties', () => { - let path = `\n { path: 'home', component: HomeComponent }\n`; - let editedFile = new InsertChange(routesFile, 16, path); - return editedFile.apply(NodeHost).then(() => { - options.dasherizedName = 'more'; - options.component = 'MoreComponent'; - return nru.applyChanges( - nru.addPathToRoutes(routesFile, _.merge({route: 'home/more'}, options))); }) - .then(() => { - return nru.applyChanges(nru.addItemsToRouteProperties(routesFile, - { 'home/more': ['canDeactivate', '[ MyGuard ]'] })); }) - .then(() => { - return nru.applyChanges(nru.addItemsToRouteProperties( - routesFile, { 'home/more': ['useAsDefault', 'true'] })); }) - .then(() => readFile(routesFile, 'utf8')) - .then(content => { - expect(content).toEqual( - `import { MoreComponent } from './app/home/more/more.component'; -export default [ - { path: 'home', component: HomeComponent, - children: [ - { path: 'more', component: MoreComponent, canDeactivate: [ MyGuard ], useAsDefault: true } - ] - } -];` - ); - }); - }); - }); - - describe('validators', () => { - const projectRoot = process.cwd(); - const componentFile = path.join(projectRoot, 'src/app/about/about.component.ts'); - beforeEach(() => { - let mockDrive = { - 'src': { - 'app': { - 'about': { - 'about.component.ts' : 'export class AboutComponent { }' - } - } - } - }; - mockFs(mockDrive); - }); - - afterEach(() => { - mockFs.restore(); - }); - - it('accepts component name without \'component\' suffix: resolveComponentPath', () => { - let fileName = nru.resolveComponentPath(projectRoot, 'src/app', 'about'); - expect(fileName).toEqual(componentFile); - }); - it('accepts component name with \'component\' suffix: resolveComponentPath', () => { - let fileName = nru.resolveComponentPath(projectRoot, 'src/app', 'about.component'); - expect(fileName).toEqual(componentFile); - }); - it('accepts path absolute from project root: resolveComponentPath', () => { - let fileName = nru.resolveComponentPath(projectRoot, '', `${path.sep}about`); - expect(fileName).toEqual(componentFile); - }); - it('accept component with directory name: resolveComponentPath', () => { - let fileName = nru.resolveComponentPath(projectRoot, 'src/app', 'about/about.component'); - expect(fileName).toEqual(componentFile); - }); - - it('finds component name: confirmComponentExport', () => { - let exportExists = nru.confirmComponentExport(componentFile, 'AboutComponent'); - expect(exportExists).toBeTruthy(); - }); - it('finds component in the presence of decorators: confirmComponentExport', () => { - let editedFile = new InsertChange(componentFile, 0, '@Component{}\n'); - return editedFile.apply(NodeHost).then(() => { - let exportExists = nru.confirmComponentExport(componentFile, 'AboutComponent'); - expect(exportExists).toBeTruthy(); - }); - }); - it('report absence of component name: confirmComponentExport', () => { - let editedFile = new RemoveChange(componentFile, 21, 'onent'); - return editedFile.apply(NodeHost).then(() => { - let exportExists = nru.confirmComponentExport(componentFile, 'AboutComponent'); - expect(exportExists).not.toBeTruthy(); - }); - }); - }); -}); diff --git a/packages/@angular/cli/lib/ast-tools/route-utils.ts b/packages/@angular/cli/lib/ast-tools/route-utils.ts deleted file mode 100644 index 5d6571968681..000000000000 --- a/packages/@angular/cli/lib/ast-tools/route-utils.ts +++ /dev/null @@ -1,557 +0,0 @@ -import * as ts from 'typescript'; -import * as fs from 'fs'; -import * as path from 'path'; -import {Change, InsertChange, NoopChange} from './change'; -import {findNodes} from './node'; -import {insertAfterLastOccurrence} from './ast-utils'; -import {NodeHost, Host} from './change'; - -/** - * Adds imports to mainFile and adds toBootstrap to the array of providers - * in bootstrap, if not present - * @param mainFile main.ts - * @param imports Object { importedClass: ['path/to/import/from', defaultStyleImport?] } - * @param toBootstrap - */ -export function bootstrapItem( - mainFile: string, - imports: {[key: string]: (string | boolean)[]}, - toBootstrap: string -) { - let changes = Object.keys(imports).map(importedClass => { - let defaultStyleImport = imports[importedClass].length === 2 && !!imports[importedClass][1]; - return insertImport( - mainFile, - importedClass, - imports[importedClass][0].toString(), - defaultStyleImport - ); - }); - let rootNode = getRootNode(mainFile); - // get ExpressionStatements from the top level syntaxList of the sourceFile - let bootstrapNodes = rootNode.getChildAt(0).getChildren().filter(node => { - // get bootstrap expressions - return node.kind === ts.SyntaxKind.ExpressionStatement && - (node.getChildAt(0).getChildAt(0) as ts.Identifier).text.toLowerCase() === 'bootstrap'; - }); - if (bootstrapNodes.length !== 1) { - throw new Error(`Did not bootstrap provideRouter in ${mainFile}` + - ' because of multiple or no bootstrap calls'); - } - let bootstrapNode = bootstrapNodes[0].getChildAt(0); - let isBootstraped = findNodes(bootstrapNode, ts.SyntaxKind.SyntaxList) // get bootstrapped items - .reduce((a, b) => a.concat(b.getChildren().map(n => n.getText())), []) - .filter(n => n !== ',') - .indexOf(toBootstrap) !== -1; - if (isBootstraped) { - return changes; - } - // if bracket exitst already, add configuration template, - // otherwise, insert into bootstrap parens - let fallBackPos: number, configurePathsTemplate: string, separator: string; - let syntaxListNodes: any; - let bootstrapProviders = bootstrapNode.getChildAt(2).getChildAt(2); // array of providers - - if ( bootstrapProviders ) { - syntaxListNodes = bootstrapProviders.getChildAt(1).getChildren(); - fallBackPos = bootstrapProviders.getChildAt(2).pos; // closeBracketLiteral - separator = syntaxListNodes.length === 0 ? '' : ', '; - configurePathsTemplate = `${separator}${toBootstrap}`; - } else { - fallBackPos = bootstrapNode.getChildAt(3).pos; // closeParenLiteral - syntaxListNodes = bootstrapNode.getChildAt(2).getChildren(); - configurePathsTemplate = `, [ ${toBootstrap} ]`; - } - - changes.push(insertAfterLastOccurrence(syntaxListNodes, configurePathsTemplate, - mainFile, fallBackPos)); - return changes; -} - -/** -* Add Import `import { symbolName } from fileName` if the import doesn't exit -* already. Assumes fileToEdit can be resolved and accessed. -* @param fileToEdit (file we want to add import to) -* @param symbolName (item to import) -* @param fileName (path to the file) -* @param isDefault (if true, import follows style for importing default exports) -* @return Change -*/ - -export function insertImport(fileToEdit: string, symbolName: string, - fileName: string, isDefault = false): Change { - if (process.platform.startsWith('win')) { - fileName = fileName.replace(/\\/g, '/'); // correction in windows - } - let rootNode = getRootNode(fileToEdit); - let allImports = findNodes(rootNode, ts.SyntaxKind.ImportDeclaration); - - // get nodes that map to import statements from the file fileName - let relevantImports = allImports.filter(node => { - // StringLiteral of the ImportDeclaration is the import file (fileName in this case). - let importFiles = node.getChildren().filter(child => child.kind === ts.SyntaxKind.StringLiteral) - .map(n => (n).text); - return importFiles.filter(file => file === fileName).length === 1; - }); - - if (relevantImports.length > 0) { - - let importsAsterisk = false; - // imports from import file - let imports: ts.Node[] = []; - relevantImports.forEach(n => { - Array.prototype.push.apply(imports, findNodes(n, ts.SyntaxKind.Identifier)); - if (findNodes(n, ts.SyntaxKind.AsteriskToken).length > 0) { - importsAsterisk = true; - } - }); - - // if imports * from fileName, don't add symbolName - if (importsAsterisk) { - return; - } - - let importTextNodes = imports.filter(n => (n).text === symbolName); - - // insert import if it's not there - if (importTextNodes.length === 0) { - let fallbackPos = findNodes(relevantImports[0], ts.SyntaxKind.CloseBraceToken)[0].pos || - findNodes(relevantImports[0], ts.SyntaxKind.FromKeyword)[0].pos; - return insertAfterLastOccurrence(imports, `, ${symbolName}`, fileToEdit, fallbackPos); - } - return new NoopChange(); - } - - // no such import declaration exists - let useStrict = findNodes(rootNode, ts.SyntaxKind.StringLiteral) - .filter((n: ts.StringLiteral) => n.text === 'use strict'); - let fallbackPos = 0; - if (useStrict.length > 0) { - fallbackPos = useStrict[0].end; - } - let open = isDefault ? '' : '{ '; - let close = isDefault ? '' : ' }'; - // if there are no imports or 'use strict' statement, insert import at beginning of file - let insertAtBeginning = allImports.length === 0 && useStrict.length === 0; - let separator = insertAtBeginning ? '' : ';\n'; - let toInsert = `${separator}import ${open}${symbolName}${close}` + - ` from '${fileName}'${insertAtBeginning ? ';\n' : ''}`; - return insertAfterLastOccurrence( - allImports, - toInsert, - fileToEdit, - fallbackPos, - ts.SyntaxKind.StringLiteral - ); -} - -/** - * Inserts a path to the new route into src/routes.ts if it doesn't exist - * @param routesFile - * @param pathOptions - * @return Change[] - * @throws Error if routesFile has multiple export default or none. - */ -export function addPathToRoutes(routesFile: string, pathOptions: any): Change[] { - let route = pathOptions.route.split('/') - .filter((n: string) => n !== '').join('/'); // change say `/about/:id/` to `about/:id` - let isDefault = pathOptions.isDefault ? ', useAsDefault: true' : ''; - let outlet = pathOptions.outlet ? `, outlet: '${pathOptions.outlet}'` : ''; - - // create route path and resolve component import - let positionalRoutes = /\/:[^/]*/g; - let routePath = route.replace(positionalRoutes, ''); - routePath = `./app/${routePath}/${pathOptions.dasherizedName}.component`; - let originalComponent = pathOptions.component; - pathOptions.component = resolveImportName( - pathOptions.component, - routePath, - pathOptions.routesFile - ); - - let content = `{ path: '${route}', component: ${pathOptions.component}${isDefault}${outlet} }`; - let rootNode = getRootNode(routesFile); - let routesNode = rootNode.getChildAt(0).getChildren().filter(n => { - // get export statement - return n.kind === ts.SyntaxKind.ExportAssignment && - n.getFullText().indexOf('export default') !== -1; - }); - if (routesNode.length !== 1) { - throw new Error('Did not insert path in routes.ts because ' + - `there were multiple or no 'export default' statements`); - } - let pos = routesNode[0].getChildAt(2).getChildAt(0).end; // openBracketLiteral - // all routes in export route array - let routesArray = routesNode[0].getChildAt(2).getChildAt(1) - .getChildren() - .filter(n => n.kind === ts.SyntaxKind.ObjectLiteralExpression); - - if (pathExists(routesArray, route, pathOptions.component)) { - // don't duplicate routes - throw new Error('Route was not added since it is a duplicate'); - } - let isChild = false; - // get parent to insert under - let parent: ts.Node; - if (pathOptions.parent) { - // append '_' to route to find the actual parent (not parent of the parent) - parent = getParent(routesArray, `${pathOptions.parent}/_`); - if (!parent) { - throw new Error( - `You specified parent '${pathOptions.parent}'' which was not found in routes.ts` - ); - } - if (route.indexOf(pathOptions.parent) === 0) { - route = route.substring(pathOptions.parent.length); - } - } else { - parent = getParent(routesArray, route); - } - - if (parent) { - let childrenInfo = addChildPath(parent, pathOptions, route); - if (!childrenInfo) { - // path exists already - throw new Error('Route was not added since it is a duplicate'); - } - content = childrenInfo.newContent; - pos = childrenInfo.pos; - isChild = true; - } - - let isFirstElement = routesArray.length === 0; - if (!isChild) { - let separator = isFirstElement ? '\n' : ','; - content = `\n ${content}${separator}`; - } - let changes: Change[] = [new InsertChange(routesFile, pos, content)]; - let component = originalComponent === pathOptions.component ? originalComponent : - `${originalComponent} as ${pathOptions.component}`; - routePath = routePath.replace(/\\/, '/'); // correction in windows - changes.push(insertImport(routesFile, component, routePath)); - return changes; -} - - -/** - * Add more properties to the route object in routes.ts - * @param routesFile routes.ts - * @param routes Object {route: [key, value]} - */ -export function addItemsToRouteProperties(routesFile: string, routes: {[key: string]: string[]}) { - let rootNode = getRootNode(routesFile); - let routesNode = rootNode.getChildAt(0).getChildren().filter(n => { - // get export statement - return n.kind === ts.SyntaxKind.ExportAssignment && - n.getFullText().indexOf('export default') !== -1; - }); - if (routesNode.length !== 1) { - throw new Error('Did not insert path in routes.ts because ' + - `there were multiple or no 'export default' statements`); - } - let routesArray = routesNode[0].getChildAt(2).getChildAt(1) - .getChildren() - .filter(n => n.kind === ts.SyntaxKind.ObjectLiteralExpression); - let changes: Change[] = Object.keys(routes).reduce((result, route) => { - // let route = routes[guardName][0]; - let itemKey = routes[route][0]; - let itemValue = routes[route][1]; - let currRouteNode = getParent(routesArray, `${route}/_`); - if (!currRouteNode) { - throw new Error(`Could not find '${route}' in routes.ts`); - } - let fallBackPos = findNodes(currRouteNode, ts.SyntaxKind.CloseBraceToken).pop().pos; - let pathPropertiesNodes = currRouteNode.getChildAt(1).getChildren() - .filter(n => n.kind === ts.SyntaxKind.PropertyAssignment); - return result.concat([insertAfterLastOccurrence(pathPropertiesNodes, - `, ${itemKey}: ${itemValue}`, routesFile, fallBackPos)]); - }, []); - return changes; -} - -/** - * Verifies that a component file exports a class of the component - * @param file - * @param componentName - * @return whether file exports componentName - */ -export function confirmComponentExport (file: string, componentName: string): boolean { - const rootNode = getRootNode(file); - let exportNodes = rootNode.getChildAt(0).getChildren().filter(n => { - return n.kind === ts.SyntaxKind.ClassDeclaration && - (n.getChildren().filter((p: ts.Identifier) => p.text === componentName).length !== 0); - }); - return exportNodes.length > 0; -} - -/** - * Ensures there is no collision between import names. If a collision occurs, resolve by adding - * underscore number to the name - * @param importName - * @param importPath path to import component from - * @param fileName (file to add import to) - * @return resolved importName - */ -function resolveImportName (importName: string, importPath: string, fileName: string): string { - const rootNode = getRootNode(fileName); - // get all the import names - let importNodes = rootNode.getChildAt(0).getChildren() - .filter(n => n.kind === ts.SyntaxKind.ImportDeclaration); - // check if imported file is same as current one before updating component name - let importNames = importNodes - .reduce((a, b) => { - let importFrom = findNodes(b, ts.SyntaxKind.StringLiteral); // there's only one - if ((importFrom.pop() as ts.StringLiteral).text !== importPath) { - // importing from different file, add to imported components to inspect - // if only one identifier { FooComponent }, if two { FooComponent as FooComponent_1 } - // choose last element of identifier array in both cases - return a.concat([findNodes(b, ts.SyntaxKind.Identifier).pop()]); - } - return a; - }, []) - .map(n => n.text); - - const index = importNames.indexOf(importName); - if (index === -1) { - return importName; - } - const baseName = importNames[index].split('_')[0]; - let newName = baseName; - let resolutionNumber = 1; - while (importNames.indexOf(newName) !== -1) { - newName = `${baseName}_${resolutionNumber}`; - resolutionNumber++; - } - return newName; -} - -/** - * Resolve a path to a component file. If the path begins with path.sep, it is treated to be - * absolute from the app/ directory. Otherwise, it is relative to currDir - * @param projectRoot - * @param currentDir - * @param filePath componentName or path to componentName - * @return component file name - * @throw Error if component file referenced by path is not found - */ -export function resolveComponentPath(projectRoot: string, currentDir: string, filePath: string) { - - let parsedPath = path.parse(filePath); - let componentName = parsedPath.base.split('.')[0]; - let componentDir = path.parse(parsedPath.dir).base; - - // correction for a case where path is /**/componentName/componentName(.component.ts) - if ( componentName === componentDir) { - filePath = parsedPath.dir; - } - if (parsedPath.dir === '') { - // only component file name is given - filePath = componentName; - } - let directory = filePath[0] === path.sep ? - path.resolve(path.join(projectRoot, 'src', 'app', filePath)) : - path.resolve(currentDir, filePath); - - if (!fs.existsSync(directory)) { - throw new Error(`path '${filePath}' must be relative to current directory` + - ` or absolute from project root`); - } - if (directory.indexOf('src' + path.sep + 'app') === -1) { - throw new Error('Route must be within app'); - } - let componentFile = path.join(directory, `${componentName}.component.ts`); - if (!fs.existsSync(componentFile)) { - throw new Error(`could not find component file referenced by ${filePath}`); - } - return componentFile; -} - -/** - * Sort changes in decreasing order and apply them. - * @param changes - * @param host - * @return Promise - */ -export function applyChanges(changes: Change[], host: Host = NodeHost): Promise { - return changes - .filter(change => !!change) - .sort((curr, next) => next.order - curr.order) - .reduce((newChange, change) => newChange.then(() => change.apply(host)), Promise.resolve()); -} -/** - * Helper for addPathToRoutes. Adds child array to the appropriate position in the routes.ts file - * @return Object (pos, newContent) - */ -function addChildPath (parentObject: ts.Node, pathOptions: any, route: string) { - if (!parentObject) { - return; - } - let pos: number; - let newContent: string; - - // get object with 'children' property - let childrenNode = parentObject.getChildAt(1).getChildren() - .filter(n => - n.kind === ts.SyntaxKind.PropertyAssignment - && ((n as ts.PropertyAssignment).name as ts.Identifier).text === 'children'); - // find number of spaces to pad nested paths - let nestingLevel = 1; // for indenting route object in the `children` array - let n = parentObject; - while (n.parent) { - if (n.kind === ts.SyntaxKind.ObjectLiteralExpression - || n.kind === ts.SyntaxKind.ArrayLiteralExpression) { - nestingLevel ++; - } - n = n.parent; - } - - // strip parent route - let parentRoute = (parentObject.getChildAt(1).getChildAt(0).getChildAt(2) as ts.Identifier).text; - let childRoute = route.substring(route.indexOf(parentRoute) + parentRoute.length + 1); - - let isDefault = pathOptions.isDefault ? ', useAsDefault: true' : ''; - let outlet = pathOptions.outlet ? `, outlet: '${pathOptions.outlet}'` : ''; - let content = `{ path: '${childRoute}', component: ${pathOptions.component}` + - `${isDefault}${outlet} }`; - let spaces = Array(2 * nestingLevel + 1).join(' '); - - if (childrenNode.length !== 0) { - // add to beginning of children array - pos = childrenNode[0].getChildAt(2).getChildAt(1).pos; // open bracket - newContent = `\n${spaces}${content},`; - } else { - // no children array, add one - pos = parentObject.getChildAt(2).pos; // close brace - newContent = `,\n${spaces.substring(2)}children: [\n${spaces}${content}` + - `\n${spaces.substring(2)}]\n${spaces.substring(5)}`; - } - return {newContent: newContent, pos: pos}; -} - -/** - * Helper for addPathToRoutes. - * @return parentNode which contains the children array to add a new path to or - * undefined if none or the entire route was matched. - */ -function getParent(routesArray: ts.Node[], route: string, parent?: ts.Node): ts.Node { - if (routesArray.length === 0 && !parent) { - return; // no children array and no parent found - } - if (route.length === 0) { - return; // route has been completely matched - } - let splitRoute = route.split('/'); - // don't treat positional parameters separately - if (splitRoute.length > 1 && splitRoute[1].indexOf(':') !== -1) { - let actualRoute = splitRoute.shift(); - splitRoute[0] = `${actualRoute}/${splitRoute[0]}`; - } - let potentialParents: ts.Node[] = routesArray // route nodes with same path as current route - .filter(n => getValueForKey(n, 'path') === splitRoute[0]); - if (potentialParents.length !== 0) { - splitRoute.shift(); // matched current parent, move on - route = splitRoute.join('/'); - } - // get all children paths - let newRouteArray = getChildrenArray(routesArray); - if (route && parent && potentialParents.length === 0) { - return parent; // final route is not matched. assign parent from here - } - parent = potentialParents.sort((a, b) => a.pos - b.pos).shift(); - return getParent(newRouteArray, route, parent); -} - -/** - * Helper for addPathToRoutes. - * @return whether path with same route and component exists - */ -function pathExists( - routesArray: ts.Node[], - route: string, - component: string, - fullRoute?: string -): boolean { - if (routesArray.length === 0) { - return false; - } - fullRoute = fullRoute ? fullRoute : route; - let sameRoute = false; - let splitRoute = route.split('/'); - // don't treat positional parameters separately - if (splitRoute.length > 1 && splitRoute[1].indexOf(':') !== -1) { - let actualRoute = splitRoute.shift(); - splitRoute[0] = `${actualRoute}/${splitRoute[0]}`; - } - let repeatedRoutes: ts.Node[] = routesArray.filter(n => { - let currentRoute = getValueForKey(n, 'path'); - let sameComponent = getValueForKey(n, 'component') === component; - - sameRoute = currentRoute === splitRoute[0]; - // Confirm that it's parents are the same - if (sameRoute && sameComponent) { - let path = currentRoute; - let objExp = n.parent; - while (objExp) { - if (objExp.kind === ts.SyntaxKind.ObjectLiteralExpression) { - let currentParentPath = getValueForKey(objExp, 'path'); - path = currentParentPath ? `${currentParentPath}/${path}` : path; - } - objExp = objExp.parent; - } - return path === fullRoute; - } - return false; - }); - - if (sameRoute) { - splitRoute.shift(); // matched current parent, move on - route = splitRoute.join('/'); - } - if (repeatedRoutes.length !== 0) { - return true; // new path will be repeating if inserted. report that path already exists - } - - // all children paths - let newRouteArray = getChildrenArray(routesArray); - return pathExists(newRouteArray, route, component, fullRoute); -} - -/** - * Helper for getParent and pathExists - * @return array with all nodes holding children array under routes - * in routesArray - */ -function getChildrenArray(routesArray: ts.Node[]): ts.Node[] { - return routesArray.reduce((allRoutes, currRoute) => allRoutes.concat( - currRoute.getChildAt(1).getChildren() - .filter(n => n.kind === ts.SyntaxKind.PropertyAssignment - && ((n as ts.PropertyAssignment).name as ts.Identifier).text === 'children') - .map(n => n.getChildAt(2).getChildAt(1)) // syntaxList containing chilren paths - .reduce((childrenArray, currChild) => childrenArray.concat(currChild.getChildren() - .filter(p => p.kind === ts.SyntaxKind.ObjectLiteralExpression) - ), []) - ), []); -} - -/** - * Helper method to get the path text or component - * @param objectLiteralNode - * @param key 'path' or 'component' - */ -function getValueForKey(objectLiteralNode: ts.Node, key: string) { - let currentNode = key === 'component' ? objectLiteralNode.getChildAt(1).getChildAt(2) : - objectLiteralNode.getChildAt(1).getChildAt(0); - return currentNode - && currentNode.getChildAt(0) - && (currentNode.getChildAt(0) as ts.Identifier).text === key - && currentNode.getChildAt(2) - && (currentNode.getChildAt(2) as ts.Identifier).text; -} - -/** - * Helper method to get AST from file - * @param file - */ -function getRootNode(file: string) { - return ts.createSourceFile(file, fs.readFileSync(file).toString(), ts.ScriptTarget.Latest, true); -} diff --git a/packages/@angular/cli/lib/ast-tools/spec-utils.ts b/packages/@angular/cli/lib/ast-tools/spec-utils.ts deleted file mode 100644 index 4db51445a032..000000000000 --- a/packages/@angular/cli/lib/ast-tools/spec-utils.ts +++ /dev/null @@ -1,26 +0,0 @@ -// This file exports a version of the Jasmine `it` that understands promises. -// To use this, simply `import {it} from './spec-utils`. -// TODO(hansl): move this to its own Jasmine-TypeScript package. - -function async(fn: () => PromiseLike | void) { - return (done: DoneFn) => { - let result: PromiseLike | void = null; - - try { - result = fn(); - - if (result && 'then' in result) { - (result as Promise).then(done, done.fail); - } else { - done(); - } - } catch (err) { - done.fail(err); - } - }; -} - - -export function it(description: string, fn: () => PromiseLike | void) { - return (global as any)['it'](description, async(fn)); -} diff --git a/tests/e2e/tests/build/chunk-hash.ts b/tests/e2e/tests/build/chunk-hash.ts index a3306410cc92..13e5a2cb8298 100644 --- a/tests/e2e/tests/build/chunk-hash.ts +++ b/tests/e2e/tests/build/chunk-hash.ts @@ -1,9 +1,7 @@ -import {oneLine} from 'common-tags'; import * as fs from 'fs'; import {ng} from '../../utils/process'; -import {writeFile} from '../../utils/fs'; -import {addImportToModule} from '../../utils/ast'; +import {writeFile, prependToFile, replaceInFile} from '../../utils/fs'; import {getGlobalVariable} from '../../utils/env'; const OUTPUT_RE = /(main|polyfills|vendor|inline|styles|\d+)\.[a-z0-9]+\.(chunk|bundle)\.(js|css)$/; @@ -56,11 +54,14 @@ export default function() { // First, collect the hashes. return Promise.resolve() .then(() => ng('generate', 'module', 'lazy', '--routing')) - .then(() => addImportToModule('src/app/app.module.ts', oneLine` - RouterModule.forRoot([{ path: "lazy", loadChildren: "./lazy/lazy.module#LazyModule" }]) - `, '@angular/router')) - .then(() => addImportToModule( - 'src/app/app.module.ts', 'ReactiveFormsModule', '@angular/forms')) + .then(() => prependToFile('src/app/app.module.ts', ` + import { RouterModule } from '@angular/router'; + import { ReactiveFormsModule } from '@angular/forms'; + `)) + .then(() => replaceInFile('src/app/app.module.ts', 'imports: [', `imports: [ + RouterModule.forRoot([{ path: "lazy", loadChildren: "./lazy/lazy.module#LazyModule" }]), + ReactiveFormsModule, + `)) .then(() => ng('build', '--output-hashing=all')) .then(() => { oldHashes = generateFileHashMap(); @@ -91,8 +92,13 @@ export default function() { validateHashes(oldHashes, newHashes, ['inline', 'main']); oldHashes = newHashes; }) - .then(() => addImportToModule( - 'src/app/lazy/lazy.module.ts', 'ReactiveFormsModule', '@angular/forms')) + .then(() => prependToFile('src/app/lazy/lazy.module.ts', ` + import { ReactiveFormsModule } from '@angular/forms'; + `)) + .then(() => replaceInFile('src/app/lazy/lazy.module.ts', 'imports: [', ` + imports: [ + ReactiveFormsModule, + `)) .then(() => ng('build', '--output-hashing=all')) .then(() => { newHashes = generateFileHashMap(); diff --git a/tests/e2e/tests/misc/common-async.ts b/tests/e2e/tests/misc/common-async.ts index 17a1cbbd3368..92542f5180cc 100644 --- a/tests/e2e/tests/misc/common-async.ts +++ b/tests/e2e/tests/misc/common-async.ts @@ -2,8 +2,7 @@ import {readdirSync} from 'fs'; import {oneLine} from 'common-tags'; import {ng, silentNpm} from '../../utils/process'; -import {addImportToModule} from '../../utils/ast'; -import {appendToFile, expectFileToExist} from '../../utils/fs'; +import {appendToFile, expectFileToExist, prependToFile, replaceInFile} from '../../utils/fs'; import {expectToFail} from '../../utils/utils'; @@ -14,10 +13,13 @@ export default function() { .then(() => oldNumberOfFiles = readdirSync('dist').length) .then(() => ng('generate', 'module', 'lazyA', '--routing')) .then(() => ng('generate', 'module', 'lazyB', '--routing')) - .then(() => addImportToModule('src/app/app.module.ts', oneLine` + .then(() => prependToFile('src/app/app.module.ts', ` + import { RouterModule } from '@angular/router'; + `)) + .then(() => replaceInFile('src/app/app.module.ts', 'imports: [', `imports: [ RouterModule.forRoot([{ path: "lazyA", loadChildren: "./lazy-a/lazy-a.module#LazyAModule" }]), - RouterModule.forRoot([{ path: "lazyB", loadChildren: "./lazy-b/lazy-b.module#LazyBModule" }]) - `, '@angular/router')) + RouterModule.forRoot([{ path: "lazyB", loadChildren: "./lazy-b/lazy-b.module#LazyBModule" }]), + `)) .then(() => ng('build')) .then(() => readdirSync('dist').length) .then(currentNumberOfDistFiles => { diff --git a/tests/e2e/tests/misc/lazy-module.ts b/tests/e2e/tests/misc/lazy-module.ts index 46399fa9ec77..11495f9e70a0 100644 --- a/tests/e2e/tests/misc/lazy-module.ts +++ b/tests/e2e/tests/misc/lazy-module.ts @@ -1,9 +1,7 @@ import {readdirSync} from 'fs'; -import {oneLine} from 'common-tags'; import {ng, silentNpm} from '../../utils/process'; -import {addImportToModule} from '../../utils/ast'; -import {appendToFile, writeFile} from '../../utils/fs'; +import {appendToFile, writeFile, prependToFile, replaceInFile} from '../../utils/fs'; export default function() { @@ -13,11 +11,14 @@ export default function() { .then(() => oldNumberOfFiles = readdirSync('dist').length) .then(() => ng('generate', 'module', 'lazy', '--routing')) .then(() => ng('generate', 'module', 'too/lazy', '--routing')) - .then(() => addImportToModule('src/app/app.module.ts', oneLine` + .then(() => prependToFile('src/app/app.module.ts', ` + import { RouterModule } from '@angular/router'; + `)) + .then(() => replaceInFile('src/app/app.module.ts', 'imports: [', `imports: [ RouterModule.forRoot([{ path: "lazy", loadChildren: "app/lazy/lazy.module#LazyModule" }]), RouterModule.forRoot([{ path: "lazy1", loadChildren: "./lazy/lazy.module#LazyModule" }]), - RouterModule.forRoot([{ path: "lazy2", loadChildren: "./too/lazy/lazy.module#LazyModule" }]) - `, '@angular/router')) + RouterModule.forRoot([{ path: "lazy2", loadChildren: "./too/lazy/lazy.module#LazyModule" }]), + `)) .then(() => ng('build', '--named-chunks')) .then(() => readdirSync('dist')) .then((distFiles) => { diff --git a/tests/e2e/utils/ast.ts b/tests/e2e/utils/ast.ts deleted file mode 100644 index c3ac718b8a7e..000000000000 --- a/tests/e2e/utils/ast.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { - insertImport as _insertImport, - addImportToModule as _addImportToModule, - NodeHost -} from '../../../packages/@angular/cli/lib/ast-tools'; - - -export function insertImport(file: string, symbol: string, module: string) { - return _insertImport(file, symbol, module) - .then(change => change.apply(NodeHost)); -} - -export function addImportToModule(file: string, symbol: string, module: string) { - return _addImportToModule(file, symbol, module) - .then(change => change.apply(NodeHost)); -} From 4a9f426a9cbe2eb29099e07b05999d3e2de84195 Mon Sep 17 00:00:00 2001 From: Filipe Silva Date: Wed, 25 Oct 2017 14:15:57 +0100 Subject: [PATCH 2/4] fix(@angular/cli): remove typescript dependency --- packages/@angular/cli/models/config/config.ts | 16 +++++++++++++--- .../cli/models/webpack-configs/browser.ts | 8 +++++--- .../cli/models/webpack-configs/production.ts | 8 +++++--- .../cli/models/webpack-configs/server.ts | 9 ++++++--- packages/@angular/cli/package.json | 1 - packages/@angular/cli/tasks/eject.ts | 6 +++--- packages/@angular/cli/tasks/lint.ts | 2 ++ packages/@angular/cli/utilities/read-tsconfig.ts | 6 ++++-- packages/@angular/cli/utilities/strip-bom.ts | 5 +++++ 9 files changed, 43 insertions(+), 18 deletions(-) create mode 100644 packages/@angular/cli/utilities/strip-bom.ts diff --git a/packages/@angular/cli/models/config/config.ts b/packages/@angular/cli/models/config/config.ts index 22d8cf313ea1..6c988040853a 100644 --- a/packages/@angular/cli/models/config/config.ts +++ b/packages/@angular/cli/models/config/config.ts @@ -1,10 +1,11 @@ import * as fs from 'fs'; import * as path from 'path'; -import * as ts from 'typescript'; import { stripIndent } from 'common-tags'; import {SchemaClass, SchemaClassFactory} from '@ngtools/json-schema'; +import { stripBom } from '../../utilities/strip-bom'; + const DEFAULT_CONFIG_SCHEMA_PATH = path.join(__dirname, '../../lib/config/schema.json'); @@ -80,13 +81,22 @@ export class CliConfig { } static fromConfigPath(configPath: string, otherPath: string[] = []): CliConfig { - const configContent = ts.sys.readFile(configPath) || '{}'; const schemaContent = fs.readFileSync(DEFAULT_CONFIG_SCHEMA_PATH, 'utf-8'); + let configContent = '{}'; + if (fs.existsSync(configPath)) { + configContent = stripBom(fs.readFileSync(configPath, 'utf-8') || '{}'); + } + let otherContents = new Array(); if (configPath !== otherPath[0]) { otherContents = otherPath - .map(path => ts.sys.readFile(path)) + .map(path => { + if (fs.existsSync(path)) { + return stripBom(fs.readFileSync(path, 'utf-8')); + } + return undefined; + }) .filter(content => !!content); } diff --git a/packages/@angular/cli/models/webpack-configs/browser.ts b/packages/@angular/cli/models/webpack-configs/browser.ts index f3229f5f1dcc..5be79694ac31 100644 --- a/packages/@angular/cli/models/webpack-configs/browser.ts +++ b/packages/@angular/cli/models/webpack-configs/browser.ts @@ -1,7 +1,6 @@ import * as fs from 'fs'; import * as webpack from 'webpack'; import * as path from 'path'; -import * as ts from 'typescript'; const HtmlWebpackPlugin = require('html-webpack-plugin'); const SubresourceIntegrityPlugin = require('webpack-subresource-integrity'); @@ -9,11 +8,14 @@ import { packageChunkSort } from '../../utilities/package-chunk-sort'; import { BaseHrefWebpackPlugin } from '../../lib/base-href-webpack'; import { extraEntryParser, lazyChunksFilter } from './utils'; import { WebpackConfigOptions } from '../webpack-config'; +import { requireProjectModule } from '../../utilities/require-project-module'; export function getBrowserConfig(wco: WebpackConfigOptions) { const { projectRoot, buildOptions, appConfig } = wco; + const projectTs = requireProjectModule(projectRoot, 'typescript'); + const appRoot = path.resolve(projectRoot, appConfig.root); let extraPlugins: any[] = []; @@ -78,8 +80,8 @@ export function getBrowserConfig(wco: WebpackConfigOptions) { })); } - const supportES2015 = wco.tsConfig.options.target !== ts.ScriptTarget.ES3 - && wco.tsConfig.options.target !== ts.ScriptTarget.ES5; + const supportES2015 = wco.tsConfig.options.target !== projectTs.ScriptTarget.ES3 + && wco.tsConfig.options.target !== projectTs.ScriptTarget.ES5; return { resolve: { diff --git a/packages/@angular/cli/models/webpack-configs/production.ts b/packages/@angular/cli/models/webpack-configs/production.ts index 9e3b9d0f7ecf..7666b9813911 100644 --- a/packages/@angular/cli/models/webpack-configs/production.ts +++ b/packages/@angular/cli/models/webpack-configs/production.ts @@ -2,7 +2,6 @@ import * as path from 'path'; import * as webpack from 'webpack'; import * as fs from 'fs'; import * as semver from 'semver'; -import * as ts from 'typescript'; import { stripIndent } from 'common-tags'; import { LicenseWebpackPlugin } from 'license-webpack-plugin'; import { PurifyPlugin } from '@angular-devkit/build-optimizer'; @@ -10,6 +9,7 @@ import { StaticAssetPlugin } from '../../plugins/static-asset'; import { GlobCopyWebpackPlugin } from '../../plugins/glob-copy-webpack-plugin'; import { WebpackConfigOptions } from '../webpack-config'; import { readTsconfig } from '../../utilities/read-tsconfig'; +import { requireProjectModule } from '../../utilities/require-project-module'; const UglifyJSPlugin = require('uglifyjs-webpack-plugin'); @@ -24,6 +24,8 @@ const UglifyJSPlugin = require('uglifyjs-webpack-plugin'); export function getProdConfig(wco: WebpackConfigOptions) { const { projectRoot, buildOptions, appConfig } = wco; + const projectTs = requireProjectModule(projectRoot, 'typescript'); + let extraPlugins: any[] = []; let entryPoints: { [key: string]: string[] } = {}; @@ -124,8 +126,8 @@ export function getProdConfig(wco: WebpackConfigOptions) { // Read the tsconfig to determine if we should apply ES6 uglify. const tsconfigPath = path.resolve(projectRoot, appConfig.root, appConfig.tsconfig); const tsConfig = readTsconfig(tsconfigPath); - const supportES2015 = tsConfig.options.target !== ts.ScriptTarget.ES3 - && tsConfig.options.target !== ts.ScriptTarget.ES5; + const supportES2015 = tsConfig.options.target !== projectTs.ScriptTarget.ES3 + && tsConfig.options.target !== projectTs.ScriptTarget.ES5; return { entry: entryPoints, diff --git a/packages/@angular/cli/models/webpack-configs/server.ts b/packages/@angular/cli/models/webpack-configs/server.ts index 48a40281ffe1..72b8f35bb59d 100644 --- a/packages/@angular/cli/models/webpack-configs/server.ts +++ b/packages/@angular/cli/models/webpack-configs/server.ts @@ -1,13 +1,16 @@ import { WebpackConfigOptions } from '../webpack-config'; -import * as ts from 'typescript'; +import { requireProjectModule } from '../../utilities/require-project-module'; /** * Returns a partial specific to creating a bundle for node * @param wco Options which are include the build options and app config */ export function getServerConfig(wco: WebpackConfigOptions) { - const supportES2015 = wco.tsConfig.options.target !== ts.ScriptTarget.ES3 - && wco.tsConfig.options.target !== ts.ScriptTarget.ES5; + + const projectTs = requireProjectModule(wco.projectRoot, 'typescript'); + + const supportES2015 = wco.tsConfig.options.target !== projectTs.ScriptTarget.ES3 + && wco.tsConfig.options.target !== projectTs.ScriptTarget.ES5; const config: any = { resolve: { diff --git a/packages/@angular/cli/package.json b/packages/@angular/cli/package.json index 9d4328c7ebef..e9c38c0998f2 100644 --- a/packages/@angular/cli/package.json +++ b/packages/@angular/cli/package.json @@ -73,7 +73,6 @@ "style-loader": "^0.13.1", "stylus": "^0.54.5", "stylus-loader": "^3.0.1", - "typescript": ">=2.0.0 <2.6.0", "uglifyjs-webpack-plugin": "1.0.0", "url-loader": "^0.6.2", "webpack": "~3.8.1", diff --git a/packages/@angular/cli/tasks/eject.ts b/packages/@angular/cli/tasks/eject.ts index d386e69eb5b4..71ebb88e75dc 100644 --- a/packages/@angular/cli/tasks/eject.ts +++ b/packages/@angular/cli/tasks/eject.ts @@ -1,6 +1,5 @@ import * as fs from 'fs'; import * as path from 'path'; -import * as ts from 'typescript'; import * as webpack from 'webpack'; import chalk from 'chalk'; @@ -8,6 +7,7 @@ import { getAppFromConfig } from '../utilities/app-utils'; import { EjectTaskOptions } from '../commands/eject'; import { NgCliWebpackConfig } from '../models/webpack-config'; import { CliConfig } from '../models/config'; +import { stripBom } from '../utilities/strip-bom'; import { AotPlugin, AngularCompilerPlugin } from '@ngtools/webpack'; import { LicenseWebpackPlugin } from 'license-webpack-plugin'; @@ -511,7 +511,7 @@ export default Task.extend({ }) // Read the package.json and update it to include npm scripts. We do this first so that if // an error already exists - .then(() => ts.sys.readFile('package.json')) + .then(() => stripBom(fs.readFileSync('package.json', 'utf-8'))) .then((packageJson: string) => JSON.parse(packageJson)) .then((packageJson: any) => { const scripts = packageJson['scripts']; @@ -587,7 +587,7 @@ export default Task.extend({ return writeFile('package.json', JSON.stringify(packageJson, null, 2) + '\n'); }) - .then(() => JSON.parse(ts.sys.readFile(tsConfigPath))) + .then(() => JSON.parse(stripBom(fs.readFileSync(tsConfigPath, 'utf-8')))) .then((tsConfigJson: any) => { if (!tsConfigJson.exclude || force) { // Make sure we now include tests. Do not touch otherwise. diff --git a/packages/@angular/cli/tasks/lint.ts b/packages/@angular/cli/tasks/lint.ts index b4fbda9319ca..04035626aa74 100644 --- a/packages/@angular/cli/tasks/lint.ts +++ b/packages/@angular/cli/tasks/lint.ts @@ -1,3 +1,5 @@ +// We only use typescript for type information here. +// @ignoreDep typescript import chalk from 'chalk'; import * as fs from 'fs'; import * as glob from 'glob'; diff --git a/packages/@angular/cli/utilities/read-tsconfig.ts b/packages/@angular/cli/utilities/read-tsconfig.ts index ec6db2883576..84baf70e71d0 100644 --- a/packages/@angular/cli/utilities/read-tsconfig.ts +++ b/packages/@angular/cli/utilities/read-tsconfig.ts @@ -1,9 +1,11 @@ import * as path from 'path'; import * as ts from 'typescript'; +import { requireProjectModule } from '../utilities/require-project-module'; export function readTsconfig(tsconfigPath: string) { - const configResult = ts.readConfigFile(tsconfigPath, ts.sys.readFile); - const tsConfig = ts.parseJsonConfigFileContent(configResult.config, ts.sys, + const projectTs = requireProjectModule(path.dirname(tsconfigPath), 'typescript'); + const configResult = projectTs.readConfigFile(tsconfigPath, ts.sys.readFile); + const tsConfig = projectTs.parseJsonConfigFileContent(configResult.config, ts.sys, path.dirname(tsconfigPath), undefined, tsconfigPath); return tsConfig; } diff --git a/packages/@angular/cli/utilities/strip-bom.ts b/packages/@angular/cli/utilities/strip-bom.ts new file mode 100644 index 000000000000..480c743982c2 --- /dev/null +++ b/packages/@angular/cli/utilities/strip-bom.ts @@ -0,0 +1,5 @@ +// Strip BOM from file data. +// https://stackoverflow.com/questions/24356713 +export function stripBom(data: string) { + return data.replace(/^\uFEFF/, ''); +} From 7a312744a8a478b4368866a0a7b85615c7459e03 Mon Sep 17 00:00:00 2001 From: Filipe Silva Date: Wed, 25 Oct 2017 16:03:35 +0100 Subject: [PATCH 3/4] fix(@ngtools/webpack): enforce typescript dep without peerDep --- .../@angular/cli/utilities/read-tsconfig.ts | 5 ++--- packages/@ngtools/webpack/package.json | 1 - .../webpack/src/angular_compiler_plugin.ts | 1 + .../@ngtools/webpack/src/compiler_host.ts | 1 + .../@ngtools/webpack/src/entry_resolver.ts | 1 + .../webpack/src/gather_diagnostics.ts | 1 + packages/@ngtools/webpack/src/index.ts | 21 +++++++++++++++++-- packages/@ngtools/webpack/src/ngtools_api.ts | 1 + packages/@ngtools/webpack/src/paths-plugin.ts | 1 + packages/@ngtools/webpack/src/plugin.ts | 1 + packages/@ngtools/webpack/src/refactor.ts | 1 + .../webpack/src/transformers/ast_helpers.ts | 1 + .../export_lazy_module_map.spec.ts | 1 + .../transformers/export_lazy_module_map.ts | 1 + .../src/transformers/export_ngfactory.spec.ts | 1 + .../src/transformers/export_ngfactory.ts | 1 + .../webpack/src/transformers/insert_import.ts | 1 + .../transformers/register_locale_data.spec.ts | 1 + .../src/transformers/register_locale_data.ts | 1 + .../webpack/src/transformers/remove_import.ts | 1 + .../transformers/replace_bootstrap.spec.ts | 1 + .../src/transformers/replace_bootstrap.ts | 1 + .../transformers/replace_resources.spec.ts | 1 + .../src/transformers/replace_resources.ts | 1 + packages/@ngtools/webpack/src/type_checker.ts | 1 + tests/e2e/tests/misc/version.ts | 3 +++ 26 files changed, 46 insertions(+), 6 deletions(-) diff --git a/packages/@angular/cli/utilities/read-tsconfig.ts b/packages/@angular/cli/utilities/read-tsconfig.ts index 84baf70e71d0..0ee83f2d43e1 100644 --- a/packages/@angular/cli/utilities/read-tsconfig.ts +++ b/packages/@angular/cli/utilities/read-tsconfig.ts @@ -1,11 +1,10 @@ import * as path from 'path'; -import * as ts from 'typescript'; import { requireProjectModule } from '../utilities/require-project-module'; export function readTsconfig(tsconfigPath: string) { const projectTs = requireProjectModule(path.dirname(tsconfigPath), 'typescript'); - const configResult = projectTs.readConfigFile(tsconfigPath, ts.sys.readFile); - const tsConfig = projectTs.parseJsonConfigFileContent(configResult.config, ts.sys, + const configResult = projectTs.readConfigFile(tsconfigPath, projectTs.sys.readFile); + const tsConfig = projectTs.parseJsonConfigFileContent(configResult.config, projectTs.sys, path.dirname(tsconfigPath), undefined, tsconfigPath); return tsConfig; } diff --git a/packages/@ngtools/webpack/package.json b/packages/@ngtools/webpack/package.json index acbd41a6a1f3..b52b6b475add 100644 --- a/packages/@ngtools/webpack/package.json +++ b/packages/@ngtools/webpack/package.json @@ -34,7 +34,6 @@ "source-map": "^0.5.6" }, "peerDependencies": { - "typescript": "^2.0.2", "webpack": "^2.2.0 || ^3.0.0" } } diff --git a/packages/@ngtools/webpack/src/angular_compiler_plugin.ts b/packages/@ngtools/webpack/src/angular_compiler_plugin.ts index 40d65fbd0d3d..d909b2d82691 100644 --- a/packages/@ngtools/webpack/src/angular_compiler_plugin.ts +++ b/packages/@ngtools/webpack/src/angular_compiler_plugin.ts @@ -1,3 +1,4 @@ +// @ignoreDep typescript import * as fs from 'fs'; import { fork, ForkOptions, ChildProcess } from 'child_process'; import * as path from 'path'; diff --git a/packages/@ngtools/webpack/src/compiler_host.ts b/packages/@ngtools/webpack/src/compiler_host.ts index f2ea6d4f346b..daa76e006fd9 100644 --- a/packages/@ngtools/webpack/src/compiler_host.ts +++ b/packages/@ngtools/webpack/src/compiler_host.ts @@ -1,3 +1,4 @@ +// @ignoreDep typescript import * as ts from 'typescript'; import {basename, dirname, join, sep} from 'path'; import * as fs from 'fs'; diff --git a/packages/@ngtools/webpack/src/entry_resolver.ts b/packages/@ngtools/webpack/src/entry_resolver.ts index e406dc60e900..51dbf2358037 100644 --- a/packages/@ngtools/webpack/src/entry_resolver.ts +++ b/packages/@ngtools/webpack/src/entry_resolver.ts @@ -1,3 +1,4 @@ +// @ignoreDep typescript import * as fs from 'fs'; import {join} from 'path'; import * as ts from 'typescript'; diff --git a/packages/@ngtools/webpack/src/gather_diagnostics.ts b/packages/@ngtools/webpack/src/gather_diagnostics.ts index 2145f045d4af..c321c3a45acc 100644 --- a/packages/@ngtools/webpack/src/gather_diagnostics.ts +++ b/packages/@ngtools/webpack/src/gather_diagnostics.ts @@ -1,3 +1,4 @@ +// @ignoreDep typescript import * as ts from 'typescript'; import { time, timeEnd } from './benchmark'; diff --git a/packages/@ngtools/webpack/src/index.ts b/packages/@ngtools/webpack/src/index.ts index 78778e1ad439..6292e36f96f8 100644 --- a/packages/@ngtools/webpack/src/index.ts +++ b/packages/@ngtools/webpack/src/index.ts @@ -1,5 +1,22 @@ +// @ignoreDep typescript +import { satisfies } from 'semver'; + +// Test if typescript is available. This is a hack. We should be using peerDependencies instead +// but can't until we split global and local packages. +// See https://github.com/angular/angular-cli/issues/8107#issuecomment-338185872 +try { + const version = require('typescript').version; + if (!satisfies(version, '^2.0.2')) { + throw new Error(); + } +} catch (e) { + throw new Error('Could not find local "typescript" package.' + + 'The "@ngtools/webpack" package requires a local "typescript@^2.0.2" package to be installed.' + + e); +} + export * from './plugin'; export * from './angular_compiler_plugin'; export * from './extract_i18n_plugin'; -export {ngcLoader as default} from './loader'; -export {PathsPlugin} from './paths-plugin'; +export { ngcLoader as default } from './loader'; +export { PathsPlugin } from './paths-plugin'; diff --git a/packages/@ngtools/webpack/src/ngtools_api.ts b/packages/@ngtools/webpack/src/ngtools_api.ts index d9d133c22133..4f37069ac08b 100644 --- a/packages/@ngtools/webpack/src/ngtools_api.ts +++ b/packages/@ngtools/webpack/src/ngtools_api.ts @@ -1,4 +1,5 @@ // @ignoreDep @angular/compiler-cli +// @ignoreDep typescript /** * This is a copy of types in @compiler-cli/src/ngtools_api.d.ts file, * together with safe imports for private apis for cases where @angular/compiler-cli isn't diff --git a/packages/@ngtools/webpack/src/paths-plugin.ts b/packages/@ngtools/webpack/src/paths-plugin.ts index 0c5d16da62c0..8f05721669a9 100644 --- a/packages/@ngtools/webpack/src/paths-plugin.ts +++ b/packages/@ngtools/webpack/src/paths-plugin.ts @@ -1,3 +1,4 @@ +// @ignoreDep typescript import * as path from 'path'; import * as ts from 'typescript'; import { diff --git a/packages/@ngtools/webpack/src/plugin.ts b/packages/@ngtools/webpack/src/plugin.ts index 02b1ca0f8403..8c256a366794 100644 --- a/packages/@ngtools/webpack/src/plugin.ts +++ b/packages/@ngtools/webpack/src/plugin.ts @@ -1,3 +1,4 @@ +// @ignoreDep typescript import * as fs from 'fs'; import * as path from 'path'; import * as ts from 'typescript'; diff --git a/packages/@ngtools/webpack/src/refactor.ts b/packages/@ngtools/webpack/src/refactor.ts index 98e816e7e0df..71356fdbd913 100644 --- a/packages/@ngtools/webpack/src/refactor.ts +++ b/packages/@ngtools/webpack/src/refactor.ts @@ -1,3 +1,4 @@ +// @ignoreDep typescript // TODO: move this in its own package. import * as path from 'path'; import * as ts from 'typescript'; diff --git a/packages/@ngtools/webpack/src/transformers/ast_helpers.ts b/packages/@ngtools/webpack/src/transformers/ast_helpers.ts index 87a33d025604..8e59157877eb 100644 --- a/packages/@ngtools/webpack/src/transformers/ast_helpers.ts +++ b/packages/@ngtools/webpack/src/transformers/ast_helpers.ts @@ -1,3 +1,4 @@ +// @ignoreDep typescript import * as ts from 'typescript'; import { WebpackCompilerHost } from '../compiler_host'; import { makeTransform, TransformOperation } from './make_transform'; diff --git a/packages/@ngtools/webpack/src/transformers/export_lazy_module_map.spec.ts b/packages/@ngtools/webpack/src/transformers/export_lazy_module_map.spec.ts index 59ad4f121cb7..1712e160dbde 100644 --- a/packages/@ngtools/webpack/src/transformers/export_lazy_module_map.spec.ts +++ b/packages/@ngtools/webpack/src/transformers/export_lazy_module_map.spec.ts @@ -1,3 +1,4 @@ +// @ignoreDep typescript import * as ts from 'typescript'; import { oneLine, stripIndent } from 'common-tags'; import { transformTypescript } from './ast_helpers'; diff --git a/packages/@ngtools/webpack/src/transformers/export_lazy_module_map.ts b/packages/@ngtools/webpack/src/transformers/export_lazy_module_map.ts index 40019cbc0339..fe56fe6191a5 100644 --- a/packages/@ngtools/webpack/src/transformers/export_lazy_module_map.ts +++ b/packages/@ngtools/webpack/src/transformers/export_lazy_module_map.ts @@ -1,3 +1,4 @@ +// @ignoreDep typescript import * as path from 'path'; import * as ts from 'typescript'; diff --git a/packages/@ngtools/webpack/src/transformers/export_ngfactory.spec.ts b/packages/@ngtools/webpack/src/transformers/export_ngfactory.spec.ts index 51f7715ba099..8477143fc2dc 100644 --- a/packages/@ngtools/webpack/src/transformers/export_ngfactory.spec.ts +++ b/packages/@ngtools/webpack/src/transformers/export_ngfactory.spec.ts @@ -1,3 +1,4 @@ +// @ignoreDep typescript import * as ts from 'typescript'; import { oneLine, stripIndent } from 'common-tags'; import { transformTypescript } from './ast_helpers'; diff --git a/packages/@ngtools/webpack/src/transformers/export_ngfactory.ts b/packages/@ngtools/webpack/src/transformers/export_ngfactory.ts index cb045fd58aae..4b46545d91ec 100644 --- a/packages/@ngtools/webpack/src/transformers/export_ngfactory.ts +++ b/packages/@ngtools/webpack/src/transformers/export_ngfactory.ts @@ -1,3 +1,4 @@ +// @ignoreDep typescript import * as ts from 'typescript'; import { findAstNodes, getFirstNode } from './ast_helpers'; diff --git a/packages/@ngtools/webpack/src/transformers/insert_import.ts b/packages/@ngtools/webpack/src/transformers/insert_import.ts index a8bb4966138e..c8ea9539fa5e 100644 --- a/packages/@ngtools/webpack/src/transformers/insert_import.ts +++ b/packages/@ngtools/webpack/src/transformers/insert_import.ts @@ -1,3 +1,4 @@ +// @ignoreDep typescript import * as ts from 'typescript'; import { findAstNodes, getFirstNode } from './ast_helpers'; diff --git a/packages/@ngtools/webpack/src/transformers/register_locale_data.spec.ts b/packages/@ngtools/webpack/src/transformers/register_locale_data.spec.ts index 203f11aa1fc0..f18ce7663b0c 100644 --- a/packages/@ngtools/webpack/src/transformers/register_locale_data.spec.ts +++ b/packages/@ngtools/webpack/src/transformers/register_locale_data.spec.ts @@ -1,3 +1,4 @@ +// @ignoreDep typescript import * as ts from 'typescript'; import { oneLine, stripIndent } from 'common-tags'; import { transformTypescript } from './ast_helpers'; diff --git a/packages/@ngtools/webpack/src/transformers/register_locale_data.ts b/packages/@ngtools/webpack/src/transformers/register_locale_data.ts index bd2f8fd45c7c..e389d8d3409d 100644 --- a/packages/@ngtools/webpack/src/transformers/register_locale_data.ts +++ b/packages/@ngtools/webpack/src/transformers/register_locale_data.ts @@ -1,3 +1,4 @@ +// @ignoreDep typescript import * as ts from 'typescript'; import { findAstNodes, getFirstNode } from './ast_helpers'; diff --git a/packages/@ngtools/webpack/src/transformers/remove_import.ts b/packages/@ngtools/webpack/src/transformers/remove_import.ts index 58cfdbcf6705..29df47b9de3f 100644 --- a/packages/@ngtools/webpack/src/transformers/remove_import.ts +++ b/packages/@ngtools/webpack/src/transformers/remove_import.ts @@ -1,3 +1,4 @@ +// @ignoreDep typescript import * as ts from 'typescript'; import { findAstNodes } from './ast_helpers'; diff --git a/packages/@ngtools/webpack/src/transformers/replace_bootstrap.spec.ts b/packages/@ngtools/webpack/src/transformers/replace_bootstrap.spec.ts index 65c317567e10..6dd34a23cb3b 100644 --- a/packages/@ngtools/webpack/src/transformers/replace_bootstrap.spec.ts +++ b/packages/@ngtools/webpack/src/transformers/replace_bootstrap.spec.ts @@ -1,3 +1,4 @@ +// @ignoreDep typescript import * as ts from 'typescript'; import { oneLine, stripIndent } from 'common-tags'; import { transformTypescript } from './ast_helpers'; diff --git a/packages/@ngtools/webpack/src/transformers/replace_bootstrap.ts b/packages/@ngtools/webpack/src/transformers/replace_bootstrap.ts index 3df2a1f0a5f0..d5fcce50fcec 100644 --- a/packages/@ngtools/webpack/src/transformers/replace_bootstrap.ts +++ b/packages/@ngtools/webpack/src/transformers/replace_bootstrap.ts @@ -1,3 +1,4 @@ +// @ignoreDep typescript import * as ts from 'typescript'; import { findAstNodes } from './ast_helpers'; diff --git a/packages/@ngtools/webpack/src/transformers/replace_resources.spec.ts b/packages/@ngtools/webpack/src/transformers/replace_resources.spec.ts index 5b6b4dbd5b2c..f687f4abbd1b 100644 --- a/packages/@ngtools/webpack/src/transformers/replace_resources.spec.ts +++ b/packages/@ngtools/webpack/src/transformers/replace_resources.spec.ts @@ -1,3 +1,4 @@ +// @ignoreDep typescript import * as ts from 'typescript'; import { oneLine, stripIndent } from 'common-tags'; import { transformTypescript } from './ast_helpers'; diff --git a/packages/@ngtools/webpack/src/transformers/replace_resources.ts b/packages/@ngtools/webpack/src/transformers/replace_resources.ts index 51091f2f0d18..460c749d904e 100644 --- a/packages/@ngtools/webpack/src/transformers/replace_resources.ts +++ b/packages/@ngtools/webpack/src/transformers/replace_resources.ts @@ -1,3 +1,4 @@ +// @ignoreDep typescript import * as ts from 'typescript'; import { findAstNodes, getFirstNode } from './ast_helpers'; diff --git a/packages/@ngtools/webpack/src/type_checker.ts b/packages/@ngtools/webpack/src/type_checker.ts index 30014cd0f48e..18c1629a3ee0 100644 --- a/packages/@ngtools/webpack/src/type_checker.ts +++ b/packages/@ngtools/webpack/src/type_checker.ts @@ -1,3 +1,4 @@ +// @ignoreDep typescript import * as process from 'process'; import * as ts from 'typescript'; import chalk from 'chalk'; diff --git a/tests/e2e/tests/misc/version.ts b/tests/e2e/tests/misc/version.ts index c49c0c62dae3..7b12da7f7da0 100644 --- a/tests/e2e/tests/misc/version.ts +++ b/tests/e2e/tests/misc/version.ts @@ -6,5 +6,8 @@ export default function() { return ng('version') .then(() => deleteFile('.angular-cli.json')) // doesn't fail on a project with missing .angular-cli.json + .then(() => ng('version')) + // Doesn't fail outside a project. + .then(() => process.chdir('/')) .then(() => ng('version')); } From 5ec4029e85d1ad29a21b5fb42b2ad685acef1f0b Mon Sep 17 00:00:00 2001 From: Filipe Silva Date: Wed, 25 Oct 2017 17:03:45 +0100 Subject: [PATCH 4/4] ci: ignore typescript as an excessive root dep --- scripts/publish/validate_dependencies.js | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/scripts/publish/validate_dependencies.js b/scripts/publish/validate_dependencies.js index eb7abda45224..ef52c1ec5420 100644 --- a/scripts/publish/validate_dependencies.js +++ b/scripts/publish/validate_dependencies.js @@ -29,7 +29,10 @@ const ANGULAR_PACKAGES = [ '@angular/core' ]; const OPTIONAL_PACKAGES = [ - '@angular/service-worker' + '@angular/service-worker', +]; +const PEERDEP_HACK_PACKAGES = [ + 'typescript', ]; @@ -168,7 +171,8 @@ const missingRootDeps = overallDeps.filter(d => allRootDeps.indexOf(d) == -1) reportMissingDependencies(missingRootDeps); const overRootDeps = allRootDeps.filter(d => overallDeps.indexOf(d) == -1) - .filter(x => ANGULAR_PACKAGES.indexOf(x) == -1); + .filter(x => ANGULAR_PACKAGES.indexOf(x) == -1) + .filter(x => PEERDEP_HACK_PACKAGES.indexOf(x) == -1); reportExcessiveDependencies(overRootDeps); process.exit(exitCode);