diff --git a/packages/@ngtools/webpack/src/loader.ts b/packages/@ngtools/webpack/src/loader.ts index 1fc297b6de23..14434f7e5515 100644 --- a/packages/@ngtools/webpack/src/loader.ts +++ b/packages/@ngtools/webpack/src/loader.ts @@ -18,11 +18,137 @@ function _getContentOfKeyLiteral(source: ts.SourceFile, node: ts.Node): string { } } + +function _angularImportsFromNode(node: ts.ImportDeclaration, sourceFile: ts.SourceFile): 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 + '.']; + } 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); + } + } + } else { + // This is of the form `import 'path';`. Nothing to do. + return []; + } +} + + +function _ctorParameterFromTypeReference(paramNode: ts.ParameterDeclaration, + angularImports: string[], + refactor: TypeScriptFileRefactor) { + if (paramNode.type.kind == ts.SyntaxKind.TypeReference) { + const type = paramNode.type as ts.TypeReferenceNode; + const decorators = refactor.findAstNodes(paramNode, ts.SyntaxKind.Decorator) as ts.Decorator[]; + const decoratorStr = decorators + .map(decorator => { + const fnName = + (refactor.findFirstAstNode(decorator, ts.SyntaxKind.CallExpression) as ts.CallExpression) + .expression.getText(refactor.sourceFile); + + if (angularImports.indexOf(fnName) === -1) { + return null; + } else { + return fnName; + } + }) + .filter(x => !!x) + .map(name => `{ type: ${name} }`) + .join(', '); + + if (type.typeName.kind == ts.SyntaxKind.Identifier) { + const typeName = type.typeName as ts.Identifier; + if (decorators.length > 0) { + return `{ type: ${typeName.text}, decorators: [${decoratorStr}] }`; + } + return `{ type: ${typeName.text} }`; + } + } + + return 'null'; +} + + +function _addCtorParameters(classNode: ts.ClassDeclaration, + angularImports: string[], + refactor: TypeScriptFileRefactor) { + // For every classes with constructors, output the ctorParameters function which contains a list + // of injectable types. + const ctor = ( + refactor.findFirstAstNode(classNode, ts.SyntaxKind.Constructor) as ts.ConstructorDeclaration); + if (!ctor) { + // A class can be missing a constructor, and that's _okay_. + return; + } + + const params = Array.from(ctor.parameters).map(paramNode => { + switch (paramNode.type.kind) { + case ts.SyntaxKind.TypeReference: + return _ctorParameterFromTypeReference(paramNode, angularImports, refactor); + default: + return 'null'; + } + }); + + const ctorParametersDecl = `static ctorParameters() { return [ ${params.join(', ')} ]; }`; + refactor.prependBefore(classNode.getLastToken(refactor.sourceFile), ctorParametersDecl); +} + + function _removeDecorators(refactor: TypeScriptFileRefactor) { - // TODO: replace this by tsickle. + const angularImports: string[] + = refactor.findAstNodes(refactor.sourceFile, ts.SyntaxKind.ImportDeclaration) + .map((node: ts.ImportDeclaration) => _angularImportsFromNode(node, refactor.sourceFile)) + .reduce((acc: string[], current: string[]) => acc.concat(current), []); + // Find all decorators. - // refactor.findAstNodes(refactor.sourceFile, ts.SyntaxKind.Decorator) - // .forEach(d => refactor.removeNode(d)); + refactor.findAstNodes(refactor.sourceFile, ts.SyntaxKind.Decorator) + .forEach(node => { + // First, add decorators to classes to the classes array. + if (node.parent) { + const declarations = refactor.findAstNodes(node.parent, + ts.SyntaxKind.ClassDeclaration, false, 1); + if (declarations.length > 0) { + _addCtorParameters(declarations[0] as ts.ClassDeclaration, angularImports, refactor); + } + } + + refactor.findAstNodes(node, ts.SyntaxKind.CallExpression) + .filter((node: ts.CallExpression) => { + const fnName = node.expression.getText(refactor.sourceFile); + if (fnName.indexOf('.') != -1) { + // Since this is `a.b`, see if it's the same namespace as a namespace import. + return angularImports.indexOf(fnName.replace(/\..*$/, '') + '.') != -1; + } else { + return angularImports.indexOf(fnName) != -1; + } + }) + .forEach(() => refactor.removeNode(node)); + }); } diff --git a/packages/@ngtools/webpack/src/refactor.ts b/packages/@ngtools/webpack/src/refactor.ts index e001cfc974eb..7552c081dce9 100644 --- a/packages/@ngtools/webpack/src/refactor.ts +++ b/packages/@ngtools/webpack/src/refactor.ts @@ -120,9 +120,20 @@ export class TypeScriptFileRefactor { return arr; } + findFirstAstNode(node: ts.Node | null, kind: ts.SyntaxKind): ts.Node | null { + return this.findAstNodes(node, kind, false, 1)[0] || null; + } + appendAfter(node: ts.Node, text: string): void { this._sourceString.insertRight(node.getEnd(), text); } + append(node: ts.Node, text: string): void { + this._sourceString.insertLeft(node.getEnd(), text); + } + + prependBefore(node: ts.Node, text: string) { + this._sourceString.insertLeft(node.getStart(), text); + } insertImport(symbolName: string, modulePath: string): void { // Find all imports. diff --git a/tests/e2e/tests/build/aot/aot-decorators.ts b/tests/e2e/tests/build/aot/aot-decorators.ts new file mode 100644 index 000000000000..86d15b6448bc --- /dev/null +++ b/tests/e2e/tests/build/aot/aot-decorators.ts @@ -0,0 +1,24 @@ +import {ng} from '../../../utils/process'; +import {appendToFile, expectFileToMatch, prependToFile, replaceInFile} from '../../../utils/fs'; +import {expectToFail} from '../../../utils/utils'; + +export default function() { + return ng('generate', 'component', 'test-component', '--module', 'app.module.ts') + .then(() => prependToFile('src/app/test-component/test-component.component.ts', ` + import { Optional, SkipSelf } from '@angular/core'; + `)) + .then(() => replaceInFile('src/app/test-component/test-component.component.ts', + /constructor.*/, ` + constructor(@Optional() @SkipSelf() public test: TestComponentComponent) { + console.log(test); + } + `)) + .then(() => appendToFile('src/app/app.component.html', ` + + `)) + .then(() => ng('build', '--aot')) + .then(() => expectToFail(() => expectFileToMatch('dist/main.bundle.js', /\bComponent\b/))) + // Check that the decorators are still kept. + .then(() => expectFileToMatch('dist/main.bundle.js', /ctorParameters.*Optional.*SkipSelf/)) + .then(() => expectToFail(() => expectFileToMatch('dist/main.bundle.js', /\bNgModule\b/))); +} diff --git a/tests/e2e/tests/build/aot-i18n.ts b/tests/e2e/tests/build/aot/aot-i18n.ts similarity index 91% rename from tests/e2e/tests/build/aot-i18n.ts rename to tests/e2e/tests/build/aot/aot-i18n.ts index a4479ba7bb4d..e530d9179dbd 100644 --- a/tests/e2e/tests/build/aot-i18n.ts +++ b/tests/e2e/tests/build/aot/aot-i18n.ts @@ -1,6 +1,6 @@ -import {ng} from '../../utils/process'; -import {expectFileToMatch, writeFile, createDir, appendToFile} from '../../utils/fs'; -import {expectToFail} from '../../utils/utils'; +import {ng} from '../../../utils/process'; +import {expectFileToMatch, writeFile, createDir, appendToFile} from '../../../utils/fs'; +import {expectToFail} from '../../../utils/utils'; export default function() { return Promise.resolve() diff --git a/tests/e2e/tests/build/aot.ts b/tests/e2e/tests/build/aot/aot.ts similarity index 65% rename from tests/e2e/tests/build/aot.ts rename to tests/e2e/tests/build/aot/aot.ts index 0cfe62bc7364..d10cca8bf7a3 100644 --- a/tests/e2e/tests/build/aot.ts +++ b/tests/e2e/tests/build/aot/aot.ts @@ -1,5 +1,5 @@ -import {ng} from '../../utils/process'; -import {expectFileToMatch} from '../../utils/fs'; +import {ng} from '../../../utils/process'; +import {expectFileToMatch} from '../../../utils/fs'; export default function() { return ng('build', '--aot')