/
strip-styles.ts
162 lines (148 loc) · 5.01 KB
/
strip-styles.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
/*
* Code is inspired by
* https://github.com/kulshekhar/ts-jest/blob/25e1c63dd3797793b0f46fa52fdee580b46f66ae/src/transformers/hoist-jest.ts
*
*
* IMPLEMENTATION DETAILS:
* This transformer handles one concern: removing styles.
*
* The property 'styles' inside a @Component(...) Decorator argument will
* be modified, even if they are not used in the context of an
* Angular Component.
*
* This is the required AST to trigger the transformation:
*
* ClassDeclaration
* Decorator
* CallExpression
* ObjectLiteralExpression
* PropertyAssignment
* Identifier
* StringLiteral
*/
// only import types, for the rest use injected `ConfigSet.compilerModule`
import type {
Node,
SourceFile,
TransformationContext,
Transformer,
Visitor,
Identifier,
ClassDeclaration,
PropertyAssignment,
CallExpression,
ObjectLiteralExpression,
} from 'typescript';
import type { ConfigSet } from './transform-utils';
/** Angular component decorator Styles property name */
const STYLES = 'styles';
/** Angular component decorator name */
const COMPONENT = 'Component';
/** All props to be transformed inside a decorator */
const TRANSFORM_IN_DECORATOR_PROPS = [STYLES];
/**
* Transformer ID
* @internal
*/
export const name = 'angular-component-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): (ctx: TransformationContext) => Transformer<SourceFile> {
/**
* Our compiler (typescript, or a module with typescript-like interface)
*/
const ts = cs.compilerModule;
/**
* Traverses the AST down inside a decorator to a styles assignment
* and returns a boolean indicating if it should be transformed.
*/
function isInDecoratorPropertyAssignmentToTransform(node: Node): node is ClassDeclaration {
return getInDecoratorPropertyAssignmentsToTransform(node).length > 0;
}
/**
* Traverses the AST down inside a decorator to a styles assignment
* returns it in an array.
*/
function getInDecoratorPropertyAssignmentsToTransform(node: Node) {
if (!ts.isClassDeclaration(node) || !node.decorators) {
return [];
}
return node.decorators
.map((dec) => dec.expression)
.filter(ts.isCallExpression)
.filter(
(callExpr: CallExpression) =>
ts.isIdentifier(callExpr.expression) && callExpr.expression.getText() === COMPONENT,
)
.reduce(
(acc, nxtCallExpr: CallExpression) =>
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
Array.prototype.concat.apply(
acc,
nxtCallExpr.arguments.filter(ts.isObjectLiteralExpression).reduce(
(acc, nxtArg: ObjectLiteralExpression) =>
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
Array.prototype.concat.apply(
acc,
nxtArg.properties
.filter(ts.isPropertyAssignment)
.filter(
(propAss) =>
ts.isIdentifier(propAss.name) && TRANSFORM_IN_DECORATOR_PROPS.includes(propAss.name.text),
),
),
[] as PropertyAssignment[],
),
),
[] as PropertyAssignment[],
);
}
/**
* Clones the styles assignment and manipulates it.
* @param node the property assignment to change
*/
function transformStylesAssignmentForJest(node: ClassDeclaration) {
const mutableNode = ts.getMutableClone(node);
const assignments = getInDecoratorPropertyAssignmentsToTransform(mutableNode);
assignments.forEach((assignment: PropertyAssignment) => {
if ((assignment.name as Identifier).text === STYLES) {
// replace initializer array with empty array
assignment.initializer = ts.createArrayLiteral();
}
});
return mutableNode;
}
/**
* 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) {
/**
* 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) => {
// before we create a deep clone to modify, we make sure that
// this is an assignment which we want to transform
if (isInDecoratorPropertyAssignmentToTransform(node)) {
// get transformed node with changed properties
return transformStylesAssignmentForJest(node);
} else {
// else look for assignments inside this node recursively
return ts.visitEachChild(node, visitor, ctx);
}
};
return visitor;
}
return (ctx: TransformationContext): Transformer<SourceFile> => (sf: SourceFile) =>
ts.visitNode(sf, createVisitor(ctx, sf));
}