@lithent/hmr-parser 0.2.2 → 0.2.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/helper/calculator.d.cts +8 -0
- package/dist/helper/calculator.d.ts +1 -1
- package/dist/index.d.cts +8 -0
- package/dist/index.d.ts +5 -5
- package/dist/lithentHmrParser.mjs +4214 -4370
- package/dist/lithentHmrParser.mjs.map +1 -1
- package/dist/lithentHmrParser.umd.js +137 -216
- package/dist/lithentHmrParser.umd.js.map +1 -1
- package/dist/markerParse.d.cts +2 -0
- package/dist/markerParse.d.ts +1 -1
- package/dist/noMarkerParse.d.cts +2 -0
- package/dist/noMarkerParse.d.ts +1 -1
- package/dist/shared.d.cts +4 -0
- package/dist/shared.d.ts +1 -1
- package/dist/strings.d.cts +1 -0
- package/dist/strings.d.ts +1 -1
- package/dist/tests/bootstrap.test.d.cts +1 -0
- package/dist/tests/bootstrap.test.d.ts +1 -0
- package/dist/tests/mark.test.d.cts +1 -0
- package/dist/tests/nomark.test.d.cts +1 -0
- package/dist/transform/componentRegister.d.cts +3 -0
- package/dist/transform/componentRegister.d.ts +2 -2
- package/dist/transform/exportSnapshot.d.cts +3 -0
- package/dist/transform/exportSnapshot.d.ts +3 -0
- package/dist/transform/renderGuard.d.cts +3 -0
- package/dist/transform/renderGuard.d.ts +1 -1
- package/dist/types.d.cts +28 -0
- package/dist/utils/ast/componentCollector.d.cts +9 -0
- package/dist/utils/ast/componentCollector.d.ts +5 -4
- package/package.json +2 -2
- package/src/helper/calculator.ts +27 -7
- package/src/markerParse.ts +30 -14
- package/src/noMarkerParse.ts +16 -4
- package/src/shared.ts +8 -2
- package/src/strings.ts +105 -177
- package/src/tests/bootstrap.test.ts +312 -0
- package/src/tests/mark.test.ts +61 -181
- package/src/tests/nomark.test.tsx +110 -89
- package/src/transform/componentRegister.ts +13 -304
- package/src/transform/exportSnapshot.ts +77 -0
- package/src/transform/renderGuard.ts +59 -49
- package/src/utils/ast/componentCollector.ts +195 -96
|
@@ -4,311 +4,20 @@ import type { MountInfo } from '../utils/ast/componentCollector';
|
|
|
4
4
|
export const stitchComponentRegistration = (
|
|
5
5
|
ms: MagicString,
|
|
6
6
|
mounts: MountInfo[],
|
|
7
|
-
|
|
8
|
-
|
|
7
|
+
_code: string,
|
|
8
|
+
_importInsertionPos: number
|
|
9
9
|
) => {
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
const pieces: string[] = [];
|
|
21
|
-
if (needsMountCallback) {
|
|
22
|
-
pieces.push('mountCallback');
|
|
23
|
-
}
|
|
24
|
-
if (needsGetComponentKey) {
|
|
25
|
-
pieces.push('getComponentKey');
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
const joined = pieces.join(', ');
|
|
29
|
-
ms.appendLeft(importInsertionPos, `import { ${joined} } from 'lithent';\n`);
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
for (const mount of mounts) {
|
|
33
|
-
const { insertPos, componentName, declarationEnd } = mount;
|
|
34
|
-
const unregisterBlock = `
|
|
35
|
-
const compKey = getComponentKey();
|
|
36
|
-
const unregister = compKey ? counterBoundary.register(compKey) : null;
|
|
37
|
-
if (unregister) {
|
|
38
|
-
mountCallback(() => () => unregister());
|
|
39
|
-
}
|
|
40
|
-
`;
|
|
41
|
-
ms.appendLeft(insertPos, unregisterBlock);
|
|
42
|
-
|
|
43
|
-
if (componentName) {
|
|
44
|
-
const hotStoreVar = `__lithentHotComponent_${componentName}`;
|
|
45
|
-
const storeSnippet = `
|
|
46
|
-
const ${hotStoreVar} = ${componentName};
|
|
47
|
-
if (__lithentModuleHotStore) {
|
|
48
|
-
__lithentModuleHotStore["${componentName}"] = ${hotStoreVar};
|
|
49
|
-
}
|
|
50
|
-
`;
|
|
51
|
-
ms.appendRight(declarationEnd, storeSnippet);
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
|
-
};
|
|
55
|
-
|
|
56
|
-
type ScannerState =
|
|
57
|
-
| 'code'
|
|
58
|
-
| 'single'
|
|
59
|
-
| 'double'
|
|
60
|
-
| 'template'
|
|
61
|
-
| 'lineComment'
|
|
62
|
-
| 'blockComment';
|
|
63
|
-
|
|
64
|
-
const extractImportStatements = (source: string): string[] => {
|
|
65
|
-
const imports: string[] = [];
|
|
66
|
-
let i = 0;
|
|
67
|
-
let state: ScannerState = 'code';
|
|
68
|
-
let templateDepth = 0;
|
|
69
|
-
|
|
70
|
-
while (i < source.length) {
|
|
71
|
-
const ch = source[i];
|
|
72
|
-
const next = source[i + 1];
|
|
73
|
-
|
|
74
|
-
switch (state) {
|
|
75
|
-
case 'code':
|
|
76
|
-
if (ch === '/' && next === '/') {
|
|
77
|
-
state = 'lineComment';
|
|
78
|
-
i += 2;
|
|
79
|
-
continue;
|
|
80
|
-
}
|
|
81
|
-
if (ch === '/' && next === '*') {
|
|
82
|
-
state = 'blockComment';
|
|
83
|
-
i += 2;
|
|
84
|
-
continue;
|
|
85
|
-
}
|
|
86
|
-
if (ch === "'") {
|
|
87
|
-
state = 'single';
|
|
88
|
-
i++;
|
|
89
|
-
continue;
|
|
90
|
-
}
|
|
91
|
-
if (ch === '"') {
|
|
92
|
-
state = 'double';
|
|
93
|
-
i++;
|
|
94
|
-
continue;
|
|
95
|
-
}
|
|
96
|
-
if (ch === '`') {
|
|
97
|
-
state = 'template';
|
|
98
|
-
templateDepth = 0;
|
|
99
|
-
i++;
|
|
100
|
-
continue;
|
|
101
|
-
}
|
|
102
|
-
if (
|
|
103
|
-
ch === 'i' &&
|
|
104
|
-
isImportKeyword(source, i) &&
|
|
105
|
-
!isDynamicImport(source, i)
|
|
106
|
-
) {
|
|
107
|
-
const end = findImportStatementEnd(source, i);
|
|
108
|
-
imports.push(source.slice(i, end));
|
|
109
|
-
i = end;
|
|
110
|
-
continue;
|
|
111
|
-
}
|
|
112
|
-
i++;
|
|
113
|
-
break;
|
|
114
|
-
|
|
115
|
-
case 'single':
|
|
116
|
-
if (ch === '\\') {
|
|
117
|
-
i += 2;
|
|
118
|
-
continue;
|
|
119
|
-
}
|
|
120
|
-
if (ch === "'") {
|
|
121
|
-
state = 'code';
|
|
122
|
-
}
|
|
123
|
-
i++;
|
|
124
|
-
break;
|
|
125
|
-
|
|
126
|
-
case 'double':
|
|
127
|
-
if (ch === '\\') {
|
|
128
|
-
i += 2;
|
|
129
|
-
continue;
|
|
130
|
-
}
|
|
131
|
-
if (ch === '"') {
|
|
132
|
-
state = 'code';
|
|
133
|
-
}
|
|
134
|
-
i++;
|
|
135
|
-
break;
|
|
136
|
-
|
|
137
|
-
case 'template':
|
|
138
|
-
if (ch === '\\') {
|
|
139
|
-
i += 2;
|
|
140
|
-
continue;
|
|
141
|
-
}
|
|
142
|
-
if (ch === '`') {
|
|
143
|
-
if (templateDepth === 0) {
|
|
144
|
-
state = 'code';
|
|
145
|
-
i++;
|
|
146
|
-
continue;
|
|
147
|
-
}
|
|
148
|
-
templateDepth--;
|
|
149
|
-
i++;
|
|
150
|
-
continue;
|
|
151
|
-
}
|
|
152
|
-
if (ch === '$' && next === '{') {
|
|
153
|
-
templateDepth++;
|
|
154
|
-
i += 2;
|
|
155
|
-
continue;
|
|
156
|
-
}
|
|
157
|
-
if (ch === '}' && templateDepth > 0) {
|
|
158
|
-
templateDepth--;
|
|
159
|
-
}
|
|
160
|
-
i++;
|
|
161
|
-
break;
|
|
162
|
-
|
|
163
|
-
case 'lineComment':
|
|
164
|
-
if (ch === '\n') {
|
|
165
|
-
state = 'code';
|
|
166
|
-
}
|
|
167
|
-
i++;
|
|
168
|
-
break;
|
|
169
|
-
|
|
170
|
-
case 'blockComment':
|
|
171
|
-
if (ch === '*' && next === '/') {
|
|
172
|
-
state = 'code';
|
|
173
|
-
i += 2;
|
|
174
|
-
continue;
|
|
175
|
-
}
|
|
176
|
-
i++;
|
|
177
|
-
break;
|
|
10
|
+
const hoisted: string[] = [];
|
|
11
|
+
for (const component of mounts) {
|
|
12
|
+
const name = JSON.stringify(component.componentName);
|
|
13
|
+
if (component.declaration) {
|
|
14
|
+
hoisted.push(
|
|
15
|
+
`${component.componentName} = __lithentWrapComponent(${name}, ${component.componentName});`
|
|
16
|
+
);
|
|
17
|
+
} else {
|
|
18
|
+
ms.appendLeft(component.start, `__lithentWrapComponent(${name}, `);
|
|
19
|
+
ms.appendRight(component.end, ')');
|
|
178
20
|
}
|
|
179
21
|
}
|
|
180
|
-
|
|
181
|
-
return imports;
|
|
182
|
-
};
|
|
183
|
-
|
|
184
|
-
const hasNamedImport = (imports: string[], name: string): boolean => {
|
|
185
|
-
const pattern = new RegExp(`\\b${escapeRegExp(name)}\\b`);
|
|
186
|
-
return imports.some(statement => {
|
|
187
|
-
const [clause] = statement.split(/from\s+/i);
|
|
188
|
-
return pattern.test(clause);
|
|
189
|
-
});
|
|
190
|
-
};
|
|
191
|
-
|
|
192
|
-
const escapeRegExp = (value: string): string =>
|
|
193
|
-
value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
194
|
-
|
|
195
|
-
const isImportKeyword = (source: string, index: number): boolean => {
|
|
196
|
-
if (!source.startsWith('import', index)) {
|
|
197
|
-
return false;
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
const prev = source[index - 1];
|
|
201
|
-
if (prev && /\w/.test(prev)) {
|
|
202
|
-
return false;
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
const next = source[index + 'import'.length];
|
|
206
|
-
if (next && /\w/.test(next)) {
|
|
207
|
-
return false;
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
return true;
|
|
211
|
-
};
|
|
212
|
-
|
|
213
|
-
const isDynamicImport = (source: string, index: number): boolean => {
|
|
214
|
-
const nextChar = source[index + 'import'.length];
|
|
215
|
-
return nextChar === '(' || nextChar === '.';
|
|
216
|
-
};
|
|
217
|
-
|
|
218
|
-
const findImportStatementEnd = (source: string, start: number): number => {
|
|
219
|
-
let i = start;
|
|
220
|
-
let state: ScannerState = 'code';
|
|
221
|
-
|
|
222
|
-
while (i < source.length) {
|
|
223
|
-
const ch = source[i];
|
|
224
|
-
const next = source[i + 1];
|
|
225
|
-
|
|
226
|
-
switch (state) {
|
|
227
|
-
case 'code':
|
|
228
|
-
if (ch === ';') {
|
|
229
|
-
return i + 1;
|
|
230
|
-
}
|
|
231
|
-
if (ch === '/' && next === '/') {
|
|
232
|
-
state = 'lineComment';
|
|
233
|
-
i += 2;
|
|
234
|
-
continue;
|
|
235
|
-
}
|
|
236
|
-
if (ch === '/' && next === '*') {
|
|
237
|
-
state = 'blockComment';
|
|
238
|
-
i += 2;
|
|
239
|
-
continue;
|
|
240
|
-
}
|
|
241
|
-
if (ch === "'") {
|
|
242
|
-
state = 'single';
|
|
243
|
-
i++;
|
|
244
|
-
continue;
|
|
245
|
-
}
|
|
246
|
-
if (ch === '"') {
|
|
247
|
-
state = 'double';
|
|
248
|
-
i++;
|
|
249
|
-
continue;
|
|
250
|
-
}
|
|
251
|
-
if (ch === '`') {
|
|
252
|
-
state = 'template';
|
|
253
|
-
i++;
|
|
254
|
-
continue;
|
|
255
|
-
}
|
|
256
|
-
if (ch === '\n' && source[i + 1] === '\n') {
|
|
257
|
-
return i + 1;
|
|
258
|
-
}
|
|
259
|
-
i++;
|
|
260
|
-
break;
|
|
261
|
-
|
|
262
|
-
case 'single':
|
|
263
|
-
if (ch === '\\') {
|
|
264
|
-
i += 2;
|
|
265
|
-
continue;
|
|
266
|
-
}
|
|
267
|
-
if (ch === "'") {
|
|
268
|
-
state = 'code';
|
|
269
|
-
}
|
|
270
|
-
i++;
|
|
271
|
-
break;
|
|
272
|
-
|
|
273
|
-
case 'double':
|
|
274
|
-
if (ch === '\\') {
|
|
275
|
-
i += 2;
|
|
276
|
-
continue;
|
|
277
|
-
}
|
|
278
|
-
if (ch === '"') {
|
|
279
|
-
state = 'code';
|
|
280
|
-
}
|
|
281
|
-
i++;
|
|
282
|
-
break;
|
|
283
|
-
|
|
284
|
-
case 'template':
|
|
285
|
-
if (ch === '\\') {
|
|
286
|
-
i += 2;
|
|
287
|
-
continue;
|
|
288
|
-
}
|
|
289
|
-
if (ch === '`') {
|
|
290
|
-
state = 'code';
|
|
291
|
-
}
|
|
292
|
-
i++;
|
|
293
|
-
break;
|
|
294
|
-
|
|
295
|
-
case 'lineComment':
|
|
296
|
-
if (ch === '\n') {
|
|
297
|
-
state = 'code';
|
|
298
|
-
}
|
|
299
|
-
i++;
|
|
300
|
-
break;
|
|
301
|
-
|
|
302
|
-
case 'blockComment':
|
|
303
|
-
if (ch === '*' && next === '/') {
|
|
304
|
-
state = 'code';
|
|
305
|
-
i += 2;
|
|
306
|
-
continue;
|
|
307
|
-
}
|
|
308
|
-
i++;
|
|
309
|
-
break;
|
|
310
|
-
}
|
|
311
|
-
}
|
|
312
|
-
|
|
313
|
-
return source.length;
|
|
22
|
+
return hoisted.join('\n');
|
|
314
23
|
};
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { getBindingIdentifiers } from '@babel/types';
|
|
2
|
+
import type { File } from '@babel/types';
|
|
3
|
+
import type { MountInfo } from '../utils/ast/componentCollector';
|
|
4
|
+
|
|
5
|
+
// Export values must be compared before self-accepting. Importers can keep
|
|
6
|
+
// stable component proxies, but need reevaluation when other exports change.
|
|
7
|
+
export const createExportSnapshot = (
|
|
8
|
+
ast: File,
|
|
9
|
+
components: MountInfo[]
|
|
10
|
+
): string => {
|
|
11
|
+
const bindings = new Map<string, string>();
|
|
12
|
+
let complete = true;
|
|
13
|
+
for (const node of ast.program.body) {
|
|
14
|
+
if (node.type === 'ExportAllDeclaration' && node.exportKind !== 'type')
|
|
15
|
+
complete = false;
|
|
16
|
+
if (node.type === 'ExportDefaultDeclaration') {
|
|
17
|
+
const declaration = node.declaration;
|
|
18
|
+
if (declaration.type === 'Identifier')
|
|
19
|
+
bindings.set('default', declaration.name);
|
|
20
|
+
else if (
|
|
21
|
+
(declaration.type === 'FunctionDeclaration' ||
|
|
22
|
+
declaration.type === 'ClassDeclaration') &&
|
|
23
|
+
declaration.id
|
|
24
|
+
)
|
|
25
|
+
bindings.set('default', declaration.id.name);
|
|
26
|
+
else if (
|
|
27
|
+
components.some(component => component.componentName === 'default')
|
|
28
|
+
)
|
|
29
|
+
bindings.set(
|
|
30
|
+
'default',
|
|
31
|
+
'__lithentCurrentComponents.default?.component'
|
|
32
|
+
);
|
|
33
|
+
else complete = false;
|
|
34
|
+
}
|
|
35
|
+
if (node.type !== 'ExportNamedDeclaration' || node.exportKind === 'type')
|
|
36
|
+
continue;
|
|
37
|
+
if (node.source) {
|
|
38
|
+
complete = false;
|
|
39
|
+
continue;
|
|
40
|
+
}
|
|
41
|
+
const declaration = node.declaration;
|
|
42
|
+
if (declaration?.type === 'VariableDeclaration') {
|
|
43
|
+
for (const declarator of declaration.declarations) {
|
|
44
|
+
for (const name of Object.keys(getBindingIdentifiers(declarator.id)))
|
|
45
|
+
bindings.set(name, name);
|
|
46
|
+
}
|
|
47
|
+
} else if (
|
|
48
|
+
(declaration?.type === 'FunctionDeclaration' ||
|
|
49
|
+
declaration?.type === 'ClassDeclaration') &&
|
|
50
|
+
declaration.id
|
|
51
|
+
) {
|
|
52
|
+
bindings.set(declaration.id.name, declaration.id.name);
|
|
53
|
+
} else if (
|
|
54
|
+
declaration &&
|
|
55
|
+
declaration.type !== 'TSInterfaceDeclaration' &&
|
|
56
|
+
declaration.type !== 'TSTypeAliasDeclaration'
|
|
57
|
+
) {
|
|
58
|
+
complete = false;
|
|
59
|
+
}
|
|
60
|
+
for (const specifier of node.specifiers) {
|
|
61
|
+
if (
|
|
62
|
+
specifier.type === 'ExportSpecifier' &&
|
|
63
|
+
specifier.exportKind !== 'type'
|
|
64
|
+
) {
|
|
65
|
+
const name =
|
|
66
|
+
specifier.exported.type === 'Identifier'
|
|
67
|
+
? specifier.exported.name
|
|
68
|
+
: specifier.exported.value;
|
|
69
|
+
bindings.set(name, specifier.local.name);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
const snapshot = complete
|
|
74
|
+
? `{${Array.from(bindings, ([name, local]) => `[${JSON.stringify(name)}]: ${local}`).join(', ')}}`
|
|
75
|
+
: 'null';
|
|
76
|
+
return `\n__lithentExportSnapshot = ${snapshot};\n`;
|
|
77
|
+
};
|
|
@@ -1,59 +1,69 @@
|
|
|
1
1
|
import MagicString from 'magic-string';
|
|
2
2
|
import type { File, Node, CallExpression } from '@babel/types';
|
|
3
|
-
import { VISITOR_KEYS } from '@babel/types';
|
|
4
|
-
|
|
5
|
-
const collectRenderCalls = (ast: Node, calls: CallExpression[]): void => {
|
|
6
|
-
const visit = (node: Node | null | undefined) => {
|
|
7
|
-
if (!node) return;
|
|
3
|
+
import { VISITOR_KEYS, isFunction } from '@babel/types';
|
|
8
4
|
|
|
5
|
+
export const wrapRenderCalls = (
|
|
6
|
+
ms: MagicString,
|
|
7
|
+
ast: File,
|
|
8
|
+
_code: string,
|
|
9
|
+
importSpecifier = 'lithent'
|
|
10
|
+
): boolean => {
|
|
11
|
+
const renderers = new Set<string>();
|
|
12
|
+
const namespaces = new Set<string>();
|
|
13
|
+
for (const node of ast.program.body) {
|
|
9
14
|
if (
|
|
10
|
-
node.type
|
|
11
|
-
node.
|
|
12
|
-
node.
|
|
13
|
-
)
|
|
14
|
-
|
|
15
|
+
node.type !== 'ImportDeclaration' ||
|
|
16
|
+
node.importKind === 'type' ||
|
|
17
|
+
node.source.value !== importSpecifier
|
|
18
|
+
)
|
|
19
|
+
continue;
|
|
20
|
+
for (const specifier of node.specifiers) {
|
|
21
|
+
if (specifier.type === 'ImportNamespaceSpecifier')
|
|
22
|
+
namespaces.add(specifier.local.name);
|
|
23
|
+
if (
|
|
24
|
+
specifier.type === 'ImportSpecifier' &&
|
|
25
|
+
specifier.importKind !== 'type' &&
|
|
26
|
+
specifier.imported.type === 'Identifier' &&
|
|
27
|
+
specifier.imported.name === 'render'
|
|
28
|
+
)
|
|
29
|
+
renderers.add(specifier.local.name);
|
|
15
30
|
}
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
31
|
+
}
|
|
32
|
+
const calls: CallExpression[] = [];
|
|
33
|
+
const visit = (node: Node): void => {
|
|
34
|
+
// A render inside a function can mount several independent applications.
|
|
35
|
+
// Guard only module-level mounts that run again when Vite evaluates it.
|
|
36
|
+
if (isFunction(node)) return;
|
|
37
|
+
if (node.type === 'CallExpression') {
|
|
38
|
+
const callee = node.callee;
|
|
39
|
+
if (
|
|
40
|
+
(callee.type === 'Identifier' && renderers.has(callee.name)) ||
|
|
41
|
+
(callee.type === 'MemberExpression' &&
|
|
42
|
+
!callee.computed &&
|
|
43
|
+
callee.object.type === 'Identifier' &&
|
|
44
|
+
namespaces.has(callee.object.name) &&
|
|
45
|
+
callee.property.type === 'Identifier' &&
|
|
46
|
+
callee.property.name === 'render')
|
|
47
|
+
) {
|
|
48
|
+
calls.push(node);
|
|
49
|
+
return;
|
|
28
50
|
}
|
|
29
51
|
}
|
|
52
|
+
for (const key of VISITOR_KEYS[node.type] ?? []) {
|
|
53
|
+
const value = (node as any)[key];
|
|
54
|
+
if (Array.isArray(value))
|
|
55
|
+
value.forEach(child => {
|
|
56
|
+
if (child?.type) visit(child);
|
|
57
|
+
});
|
|
58
|
+
else if (value?.type) visit(value);
|
|
59
|
+
}
|
|
30
60
|
};
|
|
31
|
-
|
|
32
61
|
visit(ast);
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
)
|
|
40
|
-
const calls: CallExpression[] = [];
|
|
41
|
-
collectRenderCalls(ast, calls);
|
|
42
|
-
|
|
43
|
-
if (!calls.length) return false;
|
|
44
|
-
|
|
45
|
-
const sorted = calls
|
|
46
|
-
.map(node => ({
|
|
47
|
-
start: node.start ?? 0,
|
|
48
|
-
end: node.end ?? 0,
|
|
49
|
-
}))
|
|
50
|
-
.filter(({ start, end }) => start >= 0 && end > start)
|
|
51
|
-
.sort((a, b) => b.start - a.start);
|
|
52
|
-
|
|
53
|
-
for (const { start, end } of sorted) {
|
|
54
|
-
const original = code.slice(start, end);
|
|
55
|
-
ms.overwrite(start, end, `__lithentRenderOnce(() => ${original})`);
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
return true;
|
|
62
|
+
calls
|
|
63
|
+
.sort((a, b) => a.start! - b.start!)
|
|
64
|
+
.forEach((node, id) => {
|
|
65
|
+
ms.appendLeft(node.start!, `__lithentRenderOnce(${id}, () => `);
|
|
66
|
+
ms.appendRight(node.end!, ')');
|
|
67
|
+
});
|
|
68
|
+
return Boolean(calls.length);
|
|
59
69
|
};
|