-
Notifications
You must be signed in to change notification settings - Fork 9
Expand file tree
/
Copy pathjsxSlicePlugin.ts
More file actions
169 lines (157 loc) · 5.37 KB
/
jsxSlicePlugin.ts
File metadata and controls
169 lines (157 loc) · 5.37 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
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
/*
* Copyright (c) 2024 Huawei Technologies Co.,Ltd.
*
* openInula is licensed under Mulan PSL v2.
* You can use this software according to the terms and conditions of the Mulan PSL v2.
* You may obtain a copy of Mulan PSL v2 at:
*
* http://license.coscl.org.cn/MulanPSL2
*
* THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
* EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
* MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
* See the Mulan PSL v2 for more details.
*/
import babel, { NodePath, PluginObj } from '@babel/core';
import { types as t } from '@openinula/babel-api';
import type { InulaNextOption } from '../types';
import { register } from '@openinula/babel-api';
import { COMPONENT, CURRENT_COMPONENT, importMap } from '../constants';
function transformJSXSlice(path: NodePath<t.JSXElement> | NodePath<t.JSXFragment>) {
// don't handle the jsx in three cases:
// 1. return statement, like `return <div></div>`
// 2. jsx as a children of other jsx, like `<div><span></span></div>`
if (
(path.parentPath.isReturnStatement() && !path.parentPath.parentPath.parentPath?.isArrowFunctionExpression()) || // handle return statement in component
path.parentPath.isJSXElement() ||
path.parentPath.isJSXFragment()
) {
// skip the children
return;
}
// don't transform extracted component to prevent
if (path.parentPath.isArrowFunctionExpression() && path.node.extra?.transformJSXSlice) {
return;
}
let parentPath: NodePath<babel.types.Node> | null = path.parentPath;
if (parentPath.isReturnStatement() || parentPath.isArrowFunctionExpression()) {
while (parentPath) {
// don't transform jsxslice in return of for tag or () => </> in for
if (parentPath.isJSXElement()) {
const indetifier = parentPath.get('openingElement')?.node?.name;
if (t.isJSXIdentifier(indetifier) && indetifier.name === 'for') {
return;
}
}
if (parentPath.isDeclaration()) {
break;
}
parentPath = parentPath.parentPath;
}
}
// skip jsx in return in component
if (path.parentPath.isReturnStatement()) {
const callExpression = path.parentPath.scope.path.parentPath;
if (callExpression?.isCallExpression()) {
const id = callExpression.get('callee');
if (id.isIdentifier() && id.node?.name === COMPONENT) {
return;
}
}
}
const sliceCompNode = t.callExpression(t.identifier('Component'), [t.arrowFunctionExpression([], path.node)]);
// extract the jsx slice into a subcomponent,
// like const a = type? <div></div> : <span></span>
// transform it into:
// ```jsx
// const Div$$ = (() => {
// return <div>{count}</div>
// });
// const Span$$ = Component(() => {
// return <span></span>
// });
// const a = type? $$createComponentNode(Div$$) : $$createComponentNode(Span$$);
// const a = type ? slice(()=>createHTMLNode(div, () => {xxx(count})) : createHTMLNode(span)
// <div>{a}</div>
// ```
const sliceId = path.scope.generateUidIdentifier(genName(path.node));
sliceId.name = 'JSX' + sliceId.name;
// insert the subcomponent
const sliceComp = t.variableDeclaration('const', [t.variableDeclarator(sliceId, sliceCompNode)]);
path.node.extra = { ...path.node.extra, transformJSXSlice: true };
const jsxSliceAlternative =
t.arrowFunctionExpression(
[],
t.callExpression(t.identifier(importMap.createCompNode), [sliceId])
);
if (path.parentPath.isArrowFunctionExpression()) {
// special case: returned by arrow function
const block = t.blockStatement([
sliceComp,
t.returnStatement(jsxSliceAlternative),
]);
path.replaceWith(block);
} else {
// insert into the previous statement
const stmt = path.getStatementParent();
if (!stmt) {
throw new Error('Cannot find the statement parent');
}
stmt.insertBefore(sliceComp);
path.replaceWith(jsxSliceAlternative);
}
}
function genName(node: t.JSXElement | t.JSXFragment) {
if (t.isJSXFragment(node)) {
return 'Fragment';
}
const jsxName = node.openingElement.name;
if (t.isJSXIdentifier(jsxName)) {
return jsxName.name;
} else if (t.isJSXMemberExpression(jsxName)) {
// connect all parts with _
let result = jsxName.property.name;
let current: t.JSXMemberExpression | t.JSXIdentifier = jsxName.object;
while (t.isJSXMemberExpression(current)) {
result = current.property.name + '_' + result;
current = current.object;
}
result = current.name + '_' + result;
return result;
} else {
// JSXNamespacedName
return jsxName.name.name;
}
}
/**
* Analyze the JSX slice in the function component
* 1. VariableDeclaration, like `const a = <div />`
* 2. SubComponent, like `function Sub() { return <div /> }`
*
* i.e.
* ```jsx
* let jsxSlice = <div>{count}</div>
* // =>
* function Comp_$id$() {
* return <div>{count}</div>
* }
* let jsxSlice = Comp_$id$()
* ```
*/
export default function (api: typeof babel, options: InulaNextOption): PluginObj {
register(api);
return {
visitor: {
Program(program) {
program.traverse({
JSXElement(path: NodePath<t.JSXElement>) {
transformJSXSlice(path);
},
JSXFragment(path: NodePath<t.JSXFragment>) {
transformJSXSlice(path);
},
});
},
},
};
}