forked from angular/angular-cli
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathmake_transform.ts
More file actions
154 lines (132 loc) · 5.35 KB
/
make_transform.ts
File metadata and controls
154 lines (132 loc) · 5.35 KB
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
142
143
144
145
146
147
148
149
150
151
152
153
154
/**
* @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';
import { elideImports } from './elide_imports';
import {
AddNodeOperation,
OPERATION_KIND,
RemoveNodeOperation,
ReplaceNodeOperation,
StandardTransform,
TransformOperation,
} from './interfaces';
// Typescript below 2.7.0 needs a workaround.
const tsVersionParts = ts.version.split('.').map(p => Number(p));
const visitEachChild = tsVersionParts[0] <= 2 && tsVersionParts[1] < 7
? visitEachChildWorkaround
: ts.visitEachChild;
export function makeTransform(
standardTransform: StandardTransform,
getTypeChecker?: () => ts.TypeChecker,
): ts.TransformerFactory<ts.SourceFile> {
return (context: ts.TransformationContext): ts.Transformer<ts.SourceFile> => {
const transformer: ts.Transformer<ts.SourceFile> = (sf: ts.SourceFile) => {
const ops: TransformOperation[] = standardTransform(sf);
const removeOps = ops
.filter((op) => op.kind === OPERATION_KIND.Remove) as RemoveNodeOperation[];
const addOps = ops.filter((op) => op.kind === OPERATION_KIND.Add) as AddNodeOperation[];
const replaceOps = ops
.filter((op) => op.kind === OPERATION_KIND.Replace) as ReplaceNodeOperation[];
// If nodes are removed, elide the imports as well.
// Mainly a workaround for https://github.com/Microsoft/TypeScript/issues/17552.
// WARNING: this assumes that replaceOps DO NOT reuse any of the nodes they are replacing.
// This is currently true for transforms that use replaceOps (replace_bootstrap and
// replace_resources), but may not be true for new transforms.
if (getTypeChecker && removeOps.length + replaceOps.length > 0) {
const removedNodes = removeOps.concat(replaceOps).map((op) => op.target);
removeOps.push(...elideImports(sf, removedNodes, getTypeChecker));
}
const visitor: ts.Visitor = (node) => {
let modified = false;
let modifiedNodes = [node];
// Check if node should be dropped.
if (removeOps.find((op) => op.target === node)) {
modifiedNodes = [];
modified = true;
}
// Check if node should be replaced (only replaces with first op found).
const replace = replaceOps.find((op) => op.target === node);
if (replace) {
modifiedNodes = [replace.replacement];
modified = true;
}
// Check if node should be added to.
const add = addOps.filter((op) => op.target === node);
if (add.length > 0) {
modifiedNodes = [
...add.filter((op) => op.before).map(((op) => op.before)),
...modifiedNodes,
...add.filter((op) => op.after).map(((op) => op.after)),
] as ts.Node[];
modified = true;
}
// If we changed anything, return modified nodes without visiting further.
if (modified) {
return modifiedNodes;
} else {
// Otherwise return node as is and visit children.
return visitEachChild(node, visitor, context);
}
};
// Don't visit the sourcefile at all if we don't have ops for it.
if (ops.length === 0) {
return sf;
}
const result = ts.visitNode(sf, visitor);
// If we removed any decorators, we need to clean up the decorator arrays.
if (removeOps.some((op) => op.target.kind === ts.SyntaxKind.Decorator)) {
cleanupDecorators(result);
}
return result;
};
return transformer;
};
}
/**
* This is a version of `ts.visitEachChild` that works that calls our version
* of `updateSourceFileNode`, so that typescript doesn't lose type information
* for property decorators.
* See https://github.com/Microsoft/TypeScript/issues/17384 (fixed by
* https://github.com/Microsoft/TypeScript/pull/20314 and released in TS 2.7.0) and
* https://github.com/Microsoft/TypeScript/issues/17551 (fixed by
* https://github.com/Microsoft/TypeScript/pull/18051 and released on TS 2.5.0).
*/
function visitEachChildWorkaround(
node: ts.Node,
visitor: ts.Visitor,
context: ts.TransformationContext,
) {
if (node.kind === ts.SyntaxKind.SourceFile) {
const sf = node as ts.SourceFile;
const statements = ts.visitLexicalEnvironment(sf.statements, visitor, context);
if (statements === sf.statements) {
return sf;
}
// Note: Need to clone the original file (and not use `ts.updateSourceFileNode`)
// as otherwise TS fails when resolving types for decorators.
const sfClone = ts.getMutableClone(sf);
sfClone.statements = statements;
return sfClone;
}
return ts.visitEachChild(node, visitor, context);
}
// 1) If TS sees an empty decorator array, it will still emit a `__decorate` call.
// This seems to be a TS bug.
// 2) Also ensure nodes with modified decorators have parents
// built in TS transformers assume certain nodes have parents (fixed in TS 2.7+)
function cleanupDecorators(node: ts.Node) {
if (node.decorators) {
if (node.decorators.length == 0) {
node.decorators = undefined;
} else if (node.parent == undefined) {
const originalNode = ts.getParseTreeNode(node);
node.parent = originalNode.parent;
}
}
ts.forEachChild(node, node => cleanupDecorators(node));
}