Skip to content

Commit

Permalink
feat(@ngtools/webpack): remove annotations (#4301)
Browse files Browse the repository at this point in the history
And move constructor arguments to a static property understood by Angular.
  • Loading branch information
hansl authored Jan 31, 2017
1 parent 2ab081e commit 439dcd7
Show file tree
Hide file tree
Showing 5 changed files with 169 additions and 8 deletions.
132 changes: 129 additions & 3 deletions packages/@ngtools/webpack/src/loader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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));
});
}


Expand Down
11 changes: 11 additions & 0 deletions packages/@ngtools/webpack/src/refactor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
24 changes: 24 additions & 0 deletions tests/e2e/tests/build/aot/aot-decorators.ts
Original file line number Diff line number Diff line change
@@ -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', `
<app-test-component></app-test-component>
`))
.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/)));
}
Original file line number Diff line number Diff line change
@@ -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()
Expand Down
Original file line number Diff line number Diff line change
@@ -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')
Expand Down

0 comments on commit 439dcd7

Please sign in to comment.