-
Notifications
You must be signed in to change notification settings - Fork 309
/
InlineHtmlStripStylesTransformer.ts
172 lines (147 loc) · 5.2 KB
/
InlineHtmlStripStylesTransformer.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
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
/*
* Code is inspired by
* https://github.com/kulshekhar/ts-jest/blob/25e1c63dd3797793b0f46fa52fdee580b46f66ae/src/transformers/hoist-jest.ts
*
*/
/*
* IMPLEMENTATION DETAILS:
* This transformer handles two concerns: removing styles and inlining referenced templates.
*
* The assignments can be located anywhere in a file.
* Caveats:
* All properties 'templateUrl', 'styles', 'styleUrls' ANYWHERE will be modified, even if they
* are not used in the context of an Angular Component.
*
* The AST has to simply look like this anywhere in a ts file:
*
* PropertyAssignment
* Identifier
* Initializer
*/
// only import types, for the rest use injected `ConfigSet.compilerModule`
import TS, {
Node,
SourceFile,
TransformationContext,
Transformer,
Visitor,
PropertyAssignment,
Identifier,
StringLiteral,
} from 'typescript'
// replace original ts-jest ConfigSet with this simple interface, as it would require
// jest-preset-angular to add several babel devDependencies to get the other types
// inside the ConfigSet right
interface ConfigSet {
compilerModule: typeof TS
}
/** Angular component decorator TemplateUrl property name */
const TEMPLATE_URL = 'templateUrl'
/** Angular component decorator StyleUrls property name */
const STYLE_URLS = 'styleUrls'
/** Angular component decorator Styles property name */
const STYLES = 'styles'
/** Angular component decorator Template property name */
const TEMPLATE = 'template'
/** Node require function name */
const REQUIRE = 'require'
/**
* Property names inside the decorator argument to transform
*/
const TRANSFORM_PROPS = [TEMPLATE_URL, STYLES, STYLE_URLS]
/**
* Transformer ID
* @internal
*/
export const name = 'angular-component-inline-template-strip-styles'
// increment this each time the code is modified
/**
* Transformer Version
* @internal
*/
export const version = 1
/**
* The factory of hoisting transformer factory
* @internal
*/
export function factory(cs: ConfigSet) {
/**
* Our compiler (typescript, or a module with typescript-like interface)
*/
const ts = cs.compilerModule
/**
* Traverses the AST down to the relevant assignments in the decorator
* argument and returns them in an array.
*/
function isPropertyAssignmentToTransform(node: Node): node is PropertyAssignment {
return ts.isPropertyAssignment(node) &&
ts.isIdentifier(node.name) &&
TRANSFORM_PROPS.includes(node.name.text)
}
/**
* Clones the assignment and manipulates it depending on its name.
* @param node the property assignment to change
*/
function transfromPropertyAssignmentForJest(node: PropertyAssignment) {
const mutableAssignment = ts.getMutableClone(node)
const assignmentNameText = (mutableAssignment.name as Identifier).text
switch (assignmentNameText) {
case TEMPLATE_URL:
// reuse the right-hand-side literal from the assignment
let templatePathLiteral = mutableAssignment.initializer
// fix templatePathLiteral if it was a non-relative path
if (ts.isStringLiteral(mutableAssignment.initializer)) {
const templatePathStringLiteral: StringLiteral = mutableAssignment.initializer;
// match if it starts with ./ or ../ or /
if (templatePathStringLiteral.text &&
!templatePathStringLiteral.text.match(/^(\.\/|\.\.\/|\/)/)) {
// make path relative by appending './'
templatePathLiteral = ts.createStringLiteral(`./${templatePathStringLiteral.text}`)
}
}
// replace 'templateUrl' with 'template'
mutableAssignment.name = ts.createIdentifier(TEMPLATE)
// replace current initializer with require(path)
mutableAssignment.initializer = ts.createCall(
/* expression */ ts.createIdentifier(REQUIRE),
/* type arguments */ undefined,
/* arguments array */ [templatePathLiteral]
)
break;
case STYLES:
case STYLE_URLS:
// replace initializer array with empty array
mutableAssignment.initializer = ts.createArrayLiteral()
break;
}
return mutableAssignment
}
/**
* Create a source file visitor which will visit all nodes in a source file
* @param ctx The typescript transformation context
* @param _ The owning source file
*/
function createVisitor(ctx: TransformationContext, _: SourceFile) {
/**
* Our main visitor, which will be called recursively for each node in the source file's AST
* @param node The node to be visited
*/
const visitor: Visitor = node => {
let resultNode: Node
// before we create a deep clone to modify, we make sure that
// this is an assignment which we want to transform
if (isPropertyAssignmentToTransform(node)) {
// get transformed node with changed properties
resultNode = transfromPropertyAssignmentForJest(node)
} else {
// look for interesting assignments inside this node
resultNode = ts.visitEachChild(node, visitor, ctx)
}
// finally return the currently visited node
return resultNode
}
return visitor
}
return (ctx: TransformationContext): Transformer<SourceFile> =>
(sf: SourceFile) => ts.visitNode(sf, createVisitor(ctx, sf))
}