Skip to content

Commit ae62b95

Browse files
hanslMRHarrison
authored andcommitted
feat(@ngtools/webpack): remove annotations (angular#4301)
And move constructor arguments to a static property understood by Angular.
1 parent 990245f commit ae62b95

File tree

5 files changed

+169
-8
lines changed

5 files changed

+169
-8
lines changed

packages/@ngtools/webpack/src/loader.ts

+129-3
Original file line numberDiff line numberDiff line change
@@ -18,11 +18,137 @@ function _getContentOfKeyLiteral(source: ts.SourceFile, node: ts.Node): string {
1818
}
1919
}
2020

21+
22+
function _angularImportsFromNode(node: ts.ImportDeclaration, sourceFile: ts.SourceFile): string[] {
23+
const ms = node.moduleSpecifier;
24+
let modulePath: string | null = null;
25+
switch (ms.kind) {
26+
case ts.SyntaxKind.StringLiteral:
27+
modulePath = (ms as ts.StringLiteral).text;
28+
break;
29+
default:
30+
return [];
31+
}
32+
33+
if (!modulePath.startsWith('@angular/')) {
34+
return [];
35+
}
36+
37+
if (node.importClause) {
38+
if (node.importClause.name) {
39+
// This is of the form `import Name from 'path'`. Ignore.
40+
return [];
41+
} else if (node.importClause.namedBindings) {
42+
const nb = node.importClause.namedBindings;
43+
if (nb.kind == ts.SyntaxKind.NamespaceImport) {
44+
// This is of the form `import * as name from 'path'`. Return `name.`.
45+
return [(nb as ts.NamespaceImport).name.text + '.'];
46+
} else {
47+
// This is of the form `import {a,b,c} from 'path'`
48+
const namedImports = nb as ts.NamedImports;
49+
50+
return namedImports.elements
51+
.map((is: ts.ImportSpecifier) => is.propertyName ? is.propertyName.text : is.name.text);
52+
}
53+
}
54+
} else {
55+
// This is of the form `import 'path';`. Nothing to do.
56+
return [];
57+
}
58+
}
59+
60+
61+
function _ctorParameterFromTypeReference(paramNode: ts.ParameterDeclaration,
62+
angularImports: string[],
63+
refactor: TypeScriptFileRefactor) {
64+
if (paramNode.type.kind == ts.SyntaxKind.TypeReference) {
65+
const type = paramNode.type as ts.TypeReferenceNode;
66+
const decorators = refactor.findAstNodes(paramNode, ts.SyntaxKind.Decorator) as ts.Decorator[];
67+
const decoratorStr = decorators
68+
.map(decorator => {
69+
const fnName =
70+
(refactor.findFirstAstNode(decorator, ts.SyntaxKind.CallExpression) as ts.CallExpression)
71+
.expression.getText(refactor.sourceFile);
72+
73+
if (angularImports.indexOf(fnName) === -1) {
74+
return null;
75+
} else {
76+
return fnName;
77+
}
78+
})
79+
.filter(x => !!x)
80+
.map(name => `{ type: ${name} }`)
81+
.join(', ');
82+
83+
if (type.typeName.kind == ts.SyntaxKind.Identifier) {
84+
const typeName = type.typeName as ts.Identifier;
85+
if (decorators.length > 0) {
86+
return `{ type: ${typeName.text}, decorators: [${decoratorStr}] }`;
87+
}
88+
return `{ type: ${typeName.text} }`;
89+
}
90+
}
91+
92+
return 'null';
93+
}
94+
95+
96+
function _addCtorParameters(classNode: ts.ClassDeclaration,
97+
angularImports: string[],
98+
refactor: TypeScriptFileRefactor) {
99+
// For every classes with constructors, output the ctorParameters function which contains a list
100+
// of injectable types.
101+
const ctor = (
102+
refactor.findFirstAstNode(classNode, ts.SyntaxKind.Constructor) as ts.ConstructorDeclaration);
103+
if (!ctor) {
104+
// A class can be missing a constructor, and that's _okay_.
105+
return;
106+
}
107+
108+
const params = Array.from(ctor.parameters).map(paramNode => {
109+
switch (paramNode.type.kind) {
110+
case ts.SyntaxKind.TypeReference:
111+
return _ctorParameterFromTypeReference(paramNode, angularImports, refactor);
112+
default:
113+
return 'null';
114+
}
115+
});
116+
117+
const ctorParametersDecl = `static ctorParameters() { return [ ${params.join(', ')} ]; }`;
118+
refactor.prependBefore(classNode.getLastToken(refactor.sourceFile), ctorParametersDecl);
119+
}
120+
121+
21122
function _removeDecorators(refactor: TypeScriptFileRefactor) {
22-
// TODO: replace this by tsickle.
123+
const angularImports: string[]
124+
= refactor.findAstNodes(refactor.sourceFile, ts.SyntaxKind.ImportDeclaration)
125+
.map((node: ts.ImportDeclaration) => _angularImportsFromNode(node, refactor.sourceFile))
126+
.reduce((acc: string[], current: string[]) => acc.concat(current), []);
127+
23128
// Find all decorators.
24-
// refactor.findAstNodes(refactor.sourceFile, ts.SyntaxKind.Decorator)
25-
// .forEach(d => refactor.removeNode(d));
129+
refactor.findAstNodes(refactor.sourceFile, ts.SyntaxKind.Decorator)
130+
.forEach(node => {
131+
// First, add decorators to classes to the classes array.
132+
if (node.parent) {
133+
const declarations = refactor.findAstNodes(node.parent,
134+
ts.SyntaxKind.ClassDeclaration, false, 1);
135+
if (declarations.length > 0) {
136+
_addCtorParameters(declarations[0] as ts.ClassDeclaration, angularImports, refactor);
137+
}
138+
}
139+
140+
refactor.findAstNodes(node, ts.SyntaxKind.CallExpression)
141+
.filter((node: ts.CallExpression) => {
142+
const fnName = node.expression.getText(refactor.sourceFile);
143+
if (fnName.indexOf('.') != -1) {
144+
// Since this is `a.b`, see if it's the same namespace as a namespace import.
145+
return angularImports.indexOf(fnName.replace(/\..*$/, '') + '.') != -1;
146+
} else {
147+
return angularImports.indexOf(fnName) != -1;
148+
}
149+
})
150+
.forEach(() => refactor.removeNode(node));
151+
});
26152
}
27153

28154

packages/@ngtools/webpack/src/refactor.ts

+11
Original file line numberDiff line numberDiff line change
@@ -120,9 +120,20 @@ export class TypeScriptFileRefactor {
120120
return arr;
121121
}
122122

123+
findFirstAstNode(node: ts.Node | null, kind: ts.SyntaxKind): ts.Node | null {
124+
return this.findAstNodes(node, kind, false, 1)[0] || null;
125+
}
126+
123127
appendAfter(node: ts.Node, text: string): void {
124128
this._sourceString.insertRight(node.getEnd(), text);
125129
}
130+
append(node: ts.Node, text: string): void {
131+
this._sourceString.insertLeft(node.getEnd(), text);
132+
}
133+
134+
prependBefore(node: ts.Node, text: string) {
135+
this._sourceString.insertLeft(node.getStart(), text);
136+
}
126137

127138
insertImport(symbolName: string, modulePath: string): void {
128139
// Find all imports.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import {ng} from '../../../utils/process';
2+
import {appendToFile, expectFileToMatch, prependToFile, replaceInFile} from '../../../utils/fs';
3+
import {expectToFail} from '../../../utils/utils';
4+
5+
export default function() {
6+
return ng('generate', 'component', 'test-component', '--module', 'app.module.ts')
7+
.then(() => prependToFile('src/app/test-component/test-component.component.ts', `
8+
import { Optional, SkipSelf } from '@angular/core';
9+
`))
10+
.then(() => replaceInFile('src/app/test-component/test-component.component.ts',
11+
/constructor.*/, `
12+
constructor(@Optional() @SkipSelf() public test: TestComponentComponent) {
13+
console.log(test);
14+
}
15+
`))
16+
.then(() => appendToFile('src/app/app.component.html', `
17+
<app-test-component></app-test-component>
18+
`))
19+
.then(() => ng('build', '--aot'))
20+
.then(() => expectToFail(() => expectFileToMatch('dist/main.bundle.js', /\bComponent\b/)))
21+
// Check that the decorators are still kept.
22+
.then(() => expectFileToMatch('dist/main.bundle.js', /ctorParameters.*Optional.*SkipSelf/))
23+
.then(() => expectToFail(() => expectFileToMatch('dist/main.bundle.js', /\bNgModule\b/)));
24+
}

tests/e2e/tests/build/aot-i18n.ts renamed to tests/e2e/tests/build/aot/aot-i18n.ts

+3-3
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1-
import {ng} from '../../utils/process';
2-
import {expectFileToMatch, writeFile, createDir, appendToFile} from '../../utils/fs';
3-
import {expectToFail} from '../../utils/utils';
1+
import {ng} from '../../../utils/process';
2+
import {expectFileToMatch, writeFile, createDir, appendToFile} from '../../../utils/fs';
3+
import {expectToFail} from '../../../utils/utils';
44

55
export default function() {
66
return Promise.resolve()

tests/e2e/tests/build/aot.ts renamed to tests/e2e/tests/build/aot/aot.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
import {ng} from '../../utils/process';
2-
import {expectFileToMatch} from '../../utils/fs';
1+
import {ng} from '../../../utils/process';
2+
import {expectFileToMatch} from '../../../utils/fs';
33

44
export default function() {
55
return ng('build', '--aot')

0 commit comments

Comments
 (0)