forked from angular/angular-cli
-
Notifications
You must be signed in to change notification settings - Fork 1
/
Copy pathprefix-functions.ts
141 lines (114 loc) · 4.62 KB
/
prefix-functions.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import * as ts from 'typescript';
const pureFunctionComment = '@__PURE__';
export function getPrefixFunctionsTransformer(): ts.TransformerFactory<ts.SourceFile> {
return (context: ts.TransformationContext): ts.Transformer<ts.SourceFile> => {
const transformer: ts.Transformer<ts.SourceFile> = (sf: ts.SourceFile) => {
const topLevelFunctions = findTopLevelFunctions(sf);
const pureImports = findPureImports(sf);
const pureImportsComment = `* PURE_IMPORTS_START ${pureImports.join(',')} PURE_IMPORTS_END `;
const visitor: ts.Visitor = (node: ts.Node): ts.Node => {
// Add the pure imports comment to the first node.
if (node.parent && node.parent.parent === undefined && node.pos === 0) {
const newNode = ts.addSyntheticLeadingComment(
node, ts.SyntaxKind.MultiLineCommentTrivia, pureImportsComment, true);
// Replace node with modified one.
return ts.visitEachChild(newNode, visitor, context);
}
// Add pure function comment to top level functions.
if (topLevelFunctions.has(node)) {
const newNode = ts.addSyntheticLeadingComment(
node, ts.SyntaxKind.MultiLineCommentTrivia, pureFunctionComment, false);
// Replace node with modified one.
return ts.visitEachChild(newNode, visitor, context);
}
// Otherwise return node as is.
return ts.visitEachChild(node, visitor, context);
};
return ts.visitNode(sf, visitor);
};
return transformer;
};
}
export function findTopLevelFunctions(parentNode: ts.Node): Set<ts.Node> {
const topLevelFunctions = new Set<ts.Node>();
function cb(node: ts.Node) {
// Stop recursing into this branch if it's a definition construct.
// These are function expression, function declaration, class, or arrow function (lambda).
// The body of these constructs will not execute when loading the module, so we don't
// need to mark function calls inside them as pure.
// Class static initializers in ES2015 are an exception we don't cover. They would need similar
// processing as enums to prevent property setting from causing the class to be retained.
if (ts.isFunctionDeclaration(node)
|| ts.isFunctionExpression(node)
|| ts.isClassDeclaration(node)
|| ts.isArrowFunction(node)
|| ts.isMethodDeclaration(node)
) {
return;
}
let noPureComment = !hasPureComment(node);
let innerNode = node;
while (innerNode && ts.isParenthesizedExpression(innerNode)) {
innerNode = innerNode.expression;
noPureComment = noPureComment && !hasPureComment(innerNode);
}
if (!innerNode) {
return;
}
if (noPureComment) {
if (ts.isNewExpression(innerNode)) {
topLevelFunctions.add(node);
} else if (ts.isCallExpression(innerNode)) {
let expression: ts.Expression = innerNode.expression;
while (expression && ts.isParenthesizedExpression(expression)) {
expression = expression.expression;
}
if (expression) {
if (ts.isFunctionExpression(expression)) {
// Skip IIFE's with arguments
// This could be improved to check if there are any references to variables
if (innerNode.arguments.length === 0) {
topLevelFunctions.add(node);
}
} else {
topLevelFunctions.add(node);
}
}
}
}
ts.forEachChild(innerNode, cb);
}
ts.forEachChild(parentNode, cb);
return topLevelFunctions;
}
export function findPureImports(parentNode: ts.Node): string[] {
const pureImports: string[] = [];
ts.forEachChild(parentNode, cb);
function cb(node: ts.Node) {
if (node.kind === ts.SyntaxKind.ImportDeclaration
&& (node as ts.ImportDeclaration).importClause) {
// Save the path of the import transformed into snake case and remove relative paths.
const moduleSpecifier = (node as ts.ImportDeclaration).moduleSpecifier as ts.StringLiteral;
const pureImport = moduleSpecifier.text
.replace(/[\/@\-]/g, '_')
.replace(/^\.+/, '');
pureImports.push(pureImport);
}
ts.forEachChild(node, cb);
}
return pureImports;
}
function hasPureComment(node: ts.Node) {
if (!node) {
return false;
}
const leadingComment = ts.getSyntheticLeadingComments(node);
return leadingComment && leadingComment.some((comment) => comment.text === pureFunctionComment);
}