forked from thymikee/jest-preset-angular
-
Notifications
You must be signed in to change notification settings - Fork 0
/
InlineHtmlStripStylesTransformer.ts
244 lines (220 loc) · 7.83 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
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
/*
* Code is inspired by
* https://github.com/kulshekhar/ts-jest/blob/25e1c63dd3797793b0f46fa52fdee580b46f66ae/src/transformers/hoist-jest.ts
* original license:
*
* MIT License
*
* Copyright (c) 2016-2018
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*
*/
/*
* IMPLEMENTATION DETAILS:
* This transformer handles two concerns: removing styles and inlining referenced templates,
* as they both are handled at the same location in the AST.
*
* The Component can be located anywhere in a file, except inside another Angular Component.
* The Decorator is not checked for the name 'Component', as someone might import it under
* a different name, or have their custom, modified version of the component decorator.
* Instead it checks for specific properties inside any class decorator.
* Caveats:
* All properties 'templateUrl', 'styles', 'styleUrls' inside ANY decorator will be modified.
* If the decorator content is referenced, it will not work:
* ```ts
* const componentArgs = {}
* @Component(componentArgs)
* class MyComponent { }
* ```
*
* The AST has to look like this:
*
* ClassDeclaration
* Decorator
* CallExpression
* ObjectLiteralExpression
* PropertyAssignment
* Identifier
* StringLiteral
*
* Where some additional Check have to be made to identify the node as the required kind:
*
* ClassDeclaration: isClassDeclaration
* Decorator
* CallExpression: isCallExpression
* ObjectLiteralExpression: isObjectLiteral
* PropertyAssignment: isPropertyAssignment
* Identifier: isIdentifier
* StringLiteral: isStringLiteral
*
*/
// take care of importing only types, for the rest use injected `ts`
import TS, {
Node,
SourceFile,
TransformationContext,
Transformer,
Visitor,
isClassDeclaration,
isCallExpression,
isObjectLiteralExpression,
isPropertyAssignment,
isIdentifier,
ClassDeclaration,
CallExpression,
ObjectLiteralExpression,
PropertyAssignment,
Identifier,
} from 'typescript'
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) {
/**
* Array Flatten function, as there were problems to make the compiler use
* esnext's Array.flat, can be removed in the future.
* @param arr Array to be flattened
*/
function flatten<T>(arr: (T | T[] | T[][])[]): T[] {
return arr.reduce(
(xs: T[], x) => Array.isArray(x) ? xs.concat(flatten(x as T[])) : xs.concat(x),
[]
) as T[]
}
/**
* 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 getPropertyAssignmentsToTransform(classDeclaration: ClassDeclaration) {
return flatten<PropertyAssignment>(classDeclaration.decorators!
.map(dec => dec.expression)
.filter(isCallExpression)
.map(callExpr => (callExpr as CallExpression).arguments
.filter(isObjectLiteralExpression)
.map(arg => (arg as ObjectLiteralExpression).properties
.filter(isPropertyAssignment)
.map(arg => arg as PropertyAssignment)
.filter(propAss => isIdentifier(propAss.name))
.filter(propAss => TRANSFORM_PROPS.includes((propAss.name as Identifier).text))
)
)
)
}
/**
* Clones the node, identifies the properties to transform in the decorator and modifies them.
* @param node class declaration with decorators
*/
function transfromDecoratorForJest(node: ClassDeclaration) {
const mutable = ts.getMutableClone(node)
const assignments = getPropertyAssignmentsToTransform(mutable)
assignments.forEach(assignment => {
switch((assignment.name as Identifier).text) {
case TEMPLATE_URL:
// we can reuse the right-hand-side literal from the assignment
const templatePathLiteral = assignment.initializer
// replace 'templateUrl' with 'template'
assignment.name = ts.createIdentifier(TEMPLATE)
// replace current initializer with require(path)
assignment.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
assignment.initializer = ts.createArrayLiteral()
break;
}
})
return mutable
}
/**
* Create a source file visitor which will visit all nodes in a source file
* @param ctx The typescript transformation context
* @param sf 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 class has the decorator arguments of interest.
if (
isClassDeclaration(node)
&& node.decorators
&& getPropertyAssignmentsToTransform(node).length
) {
// get mutable node and change properties
// NOTE: classes can be inside methods, but we do not
// look for them inside Angular Components!
// recursion ends here, as ts.visitEachChild is not called.
resultNode = transfromDecoratorForJest(node)
} else {
// look for other classes with decorators
// classes can be inside other statements (like if blocks)
resultNode = ts.visitEachChild(node, visitor, ctx)
}
// finally returns the currently visited node
return resultNode
}
return visitor
}
// returns the transformer factory
return (ctx: TransformationContext): Transformer<SourceFile> =>
(sf: SourceFile) => ts.visitNode(sf, createVisitor(ctx, sf))
}