@esportsplus/template 0.35.1 → 0.38.0
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/build/compiler/analyzer.d.ts +4 -0
- package/build/compiler/{type-analyzer.js → analyzer.js} +15 -47
- package/build/compiler/codegen.d.ts +8 -6
- package/build/compiler/codegen.js +77 -75
- package/build/compiler/index.d.ts +5 -8
- package/build/compiler/index.js +32 -44
- package/build/compiler/plugins/tsc.js +2 -2
- package/build/compiler/plugins/vite.d.ts +4 -4
- package/build/compiler/plugins/vite.js +2 -2
- package/build/compiler/reactive-inlining.d.ts +5 -0
- package/build/compiler/reactive-inlining.js +75 -0
- package/build/compiler/ts-parser.js +2 -2
- package/package.json +4 -4
- package/src/compiler/analyzer.ts +92 -0
- package/src/compiler/codegen.ts +116 -110
- package/src/compiler/index.ts +35 -65
- package/src/compiler/plugins/tsc.ts +2 -2
- package/src/compiler/plugins/vite.ts +2 -2
- package/src/compiler/reactive-inlining.ts +116 -0
- package/src/compiler/ts-parser.ts +2 -2
- package/test/vite.config.ts +1 -1
- package/build/compiler/type-analyzer.d.ts +0 -6
- package/src/compiler/type-analyzer.ts +0 -148
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { COMPILER_ENTRYPOINT, COMPILER_ENTRYPOINT_REACTIVITY, COMPILER_TYPES } from '~/constants';
|
|
2
|
+
import { ts } from '@esportsplus/typescript';
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
// Union types that mix functions with non-functions (e.g., Renderable)
|
|
6
|
+
// should fall through to runtime slot dispatch
|
|
7
|
+
function isTypeFunction(type: ts.Type, checker: ts.TypeChecker): boolean {
|
|
8
|
+
if (type.isUnion()) {
|
|
9
|
+
for (let i = 0, n = type.types.length; i < n; i++) {
|
|
10
|
+
if (!isTypeFunction(type.types[i], checker)) {
|
|
11
|
+
return false;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
return type.types.length > 0;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
return type.getCallSignatures().length > 0;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
const analyze = (expr: ts.Expression, checker?: ts.TypeChecker): COMPILER_TYPES => {
|
|
23
|
+
while (ts.isParenthesizedExpression(expr)) {
|
|
24
|
+
expr = expr.expression;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
if (ts.isArrowFunction(expr) || ts.isFunctionExpression(expr)) {
|
|
28
|
+
return COMPILER_TYPES.Effect;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Only html.reactive() calls become ArraySlot - handled by generateReactiveInlining
|
|
32
|
+
if (
|
|
33
|
+
ts.isCallExpression(expr) &&
|
|
34
|
+
ts.isPropertyAccessExpression(expr.expression) &&
|
|
35
|
+
ts.isIdentifier(expr.expression.expression) &&
|
|
36
|
+
expr.expression.expression.text === COMPILER_ENTRYPOINT &&
|
|
37
|
+
expr.expression.name.text === COMPILER_ENTRYPOINT_REACTIVITY
|
|
38
|
+
) {
|
|
39
|
+
return COMPILER_TYPES.ArraySlot;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if (ts.isTaggedTemplateExpression(expr) && ts.isIdentifier(expr.tag) && expr.tag.text === COMPILER_ENTRYPOINT) {
|
|
43
|
+
return COMPILER_TYPES.DocumentFragment;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (
|
|
47
|
+
ts.isNumericLiteral(expr) ||
|
|
48
|
+
ts.isStringLiteral(expr) ||
|
|
49
|
+
ts.isNoSubstitutionTemplateLiteral(expr) ||
|
|
50
|
+
expr.kind === ts.SyntaxKind.TrueKeyword ||
|
|
51
|
+
expr.kind === ts.SyntaxKind.FalseKeyword ||
|
|
52
|
+
expr.kind === ts.SyntaxKind.NullKeyword ||
|
|
53
|
+
expr.kind === ts.SyntaxKind.UndefinedKeyword
|
|
54
|
+
) {
|
|
55
|
+
return COMPILER_TYPES.Static;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if (ts.isTemplateExpression(expr)) {
|
|
59
|
+
return COMPILER_TYPES.Primitive;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
if (ts.isConditionalExpression(expr)) {
|
|
63
|
+
let whenFalse = analyze(expr.whenFalse, checker),
|
|
64
|
+
whenTrue = analyze(expr.whenTrue, checker);
|
|
65
|
+
|
|
66
|
+
if (whenTrue === whenFalse) {
|
|
67
|
+
return whenTrue;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if (whenTrue === COMPILER_TYPES.Effect || whenFalse === COMPILER_TYPES.Effect) {
|
|
71
|
+
return COMPILER_TYPES.Effect;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return COMPILER_TYPES.Unknown;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if (checker && (ts.isIdentifier(expr) || ts.isPropertyAccessExpression(expr) || ts.isCallExpression(expr))) {
|
|
78
|
+
try {
|
|
79
|
+
let type = checker.getTypeAtLocation(expr);
|
|
80
|
+
|
|
81
|
+
if (isTypeFunction(type, checker)) {
|
|
82
|
+
return COMPILER_TYPES.Effect;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
catch {
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return COMPILER_TYPES.Unknown;
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
export { analyze };
|
package/src/compiler/codegen.ts
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
|
+
import type { ReplacementIntent } from '@esportsplus/typescript/compiler';
|
|
1
2
|
import { ts } from '@esportsplus/typescript';
|
|
2
|
-
import { ast,
|
|
3
|
-
import { COMPILER_ENTRYPOINT, COMPILER_TYPES,
|
|
4
|
-
import type {
|
|
5
|
-
import {
|
|
3
|
+
import { ast, uid } from '@esportsplus/typescript/compiler';
|
|
4
|
+
import { COMPILER_ENTRYPOINT, COMPILER_TYPES, DIRECT_ATTACH_EVENTS, LIFECYCLE_EVENTS } from '~/constants';
|
|
5
|
+
import type { TemplateInfo } from './ts-parser';
|
|
6
|
+
import { analyze } from './analyzer';
|
|
6
7
|
import parser from './parser';
|
|
7
8
|
|
|
8
9
|
|
|
@@ -24,8 +25,10 @@ type CodegenContext = {
|
|
|
24
25
|
};
|
|
25
26
|
|
|
26
27
|
type CodegenResult = {
|
|
27
|
-
|
|
28
|
-
|
|
28
|
+
imports: Map<string, string>;
|
|
29
|
+
prepend: string[];
|
|
30
|
+
replacements: ReplacementIntent[];
|
|
31
|
+
templates: Map<string, string>;
|
|
29
32
|
};
|
|
30
33
|
|
|
31
34
|
type Node = {
|
|
@@ -38,6 +41,12 @@ type ParseResult = {
|
|
|
38
41
|
slots: (Attribute | Node)[] | null;
|
|
39
42
|
};
|
|
40
43
|
|
|
44
|
+
type Replacement = {
|
|
45
|
+
end: number;
|
|
46
|
+
newText: string;
|
|
47
|
+
start: number;
|
|
48
|
+
};
|
|
49
|
+
|
|
41
50
|
|
|
42
51
|
const ARROW_EMPTY_PARAMS = /\(\s*\)\s*=>\s*$/;
|
|
43
52
|
|
|
@@ -66,7 +75,7 @@ function collectNestedTemplateReplacements(
|
|
|
66
75
|
replacements.push({
|
|
67
76
|
end: node.end - exprStart,
|
|
68
77
|
newText: generateNestedTemplateCode(ctx, node as ts.TaggedTemplateExpression),
|
|
69
|
-
start: node.getStart() - exprStart
|
|
78
|
+
start: node.getStart(ctx.sourceFile) - exprStart
|
|
70
79
|
});
|
|
71
80
|
}
|
|
72
81
|
else {
|
|
@@ -74,6 +83,33 @@ function collectNestedTemplateReplacements(
|
|
|
74
83
|
}
|
|
75
84
|
}
|
|
76
85
|
|
|
86
|
+
function generateAttributeBinding(ctx: CodegenContext, element: string, name: string, expr: string, staticValue: string): string {
|
|
87
|
+
if (name.startsWith('on') && name.length > 2) {
|
|
88
|
+
let event = name.slice(2).toLowerCase(),
|
|
89
|
+
key = name.toLowerCase();
|
|
90
|
+
|
|
91
|
+
if (LIFECYCLE_EVENTS.has(key)) {
|
|
92
|
+
return `${addImport(ctx, key)}(${element}, ${expr});`;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
if (DIRECT_ATTACH_EVENTS.has(key)) {
|
|
96
|
+
return `${addImport(ctx, 'on')}(${element}, '${event}', ${expr});`;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return `${addImport(ctx, 'delegate')}(${element}, '${event}', ${expr});`;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
if (name === 'class') {
|
|
103
|
+
return `${addImport(ctx, 'setClass')}(${element}, '${staticValue}', ${expr});`;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
if (name === 'style') {
|
|
107
|
+
return `${addImport(ctx, 'setStyle')}(${element}, '${staticValue}', ${expr});`;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
return `${addImport(ctx, 'setProperty')}(${element}, '${name}', ${expr});`;
|
|
111
|
+
}
|
|
112
|
+
|
|
77
113
|
function generateNestedTemplateCode(ctx: CodegenContext, node: ts.TaggedTemplateExpression): string {
|
|
78
114
|
let expressions: ts.Expression[] = [],
|
|
79
115
|
exprTexts: string[] = [],
|
|
@@ -113,7 +149,7 @@ function generateNodeBinding(ctx: CodegenContext, anchor: string, exprText: stri
|
|
|
113
149
|
return `${anchor}.parentNode.insertBefore(${generateNestedTemplateCode(ctx, exprNode)}, ${anchor});`;
|
|
114
150
|
}
|
|
115
151
|
|
|
116
|
-
let slotType =
|
|
152
|
+
let slotType = analyze(exprNode, ctx.checker);
|
|
117
153
|
|
|
118
154
|
switch (slotType) {
|
|
119
155
|
case COMPILER_TYPES.Effect:
|
|
@@ -184,7 +220,6 @@ function generateTemplateCode(
|
|
|
184
220
|
segments = path.slice(start),
|
|
185
221
|
value = `${ancestor}.${segments.join('!.')}`;
|
|
186
222
|
|
|
187
|
-
// Cast root.firstChild to Element since DocumentFragment.firstChild returns ChildNode
|
|
188
223
|
if (ancestor === root && segments[0] === 'firstChild') {
|
|
189
224
|
value = value.replace(`${ancestor}.firstChild!`, `(${ancestor}.firstChild! as ${alias})`);
|
|
190
225
|
}
|
|
@@ -211,26 +246,19 @@ function generateTemplateCode(
|
|
|
211
246
|
let name = names[j];
|
|
212
247
|
|
|
213
248
|
if (name === COMPILER_TYPES.Attributes) {
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
n => addImport(ctx, n)
|
|
218
|
-
);
|
|
219
|
-
|
|
220
|
-
for (let k = 0, o = bindings.length; k < o; k++) {
|
|
221
|
-
code.push(bindings[k]);
|
|
222
|
-
}
|
|
223
|
-
|
|
249
|
+
code.push(
|
|
250
|
+
`${addImport(ctx, 'setProperties')}(${element}, ${exprTexts[index] || 'undefined'});`
|
|
251
|
+
);
|
|
224
252
|
index++;
|
|
225
253
|
}
|
|
226
254
|
else {
|
|
227
255
|
code.push(
|
|
228
256
|
generateAttributeBinding(
|
|
257
|
+
ctx,
|
|
229
258
|
element,
|
|
230
259
|
name,
|
|
231
260
|
exprTexts[index++] || 'undefined',
|
|
232
|
-
slot.attributes.statics[name] || ''
|
|
233
|
-
n => addImport(ctx, n)
|
|
261
|
+
slot.attributes.statics[name] || ''
|
|
234
262
|
)
|
|
235
263
|
);
|
|
236
264
|
}
|
|
@@ -265,6 +293,18 @@ function isNestedHtmlTemplate(expr: ts.Expression): expr is ts.TaggedTemplateExp
|
|
|
265
293
|
return ts.isTaggedTemplateExpression(expr) && ts.isIdentifier(expr.tag) && expr.tag.text === COMPILER_ENTRYPOINT;
|
|
266
294
|
}
|
|
267
295
|
|
|
296
|
+
function replaceReverse(text: string, replacements: Replacement[]): string {
|
|
297
|
+
let sorted = replacements.slice().sort((a, b) => b.start - a.start);
|
|
298
|
+
|
|
299
|
+
for (let i = 0, n = sorted.length; i < n; i++) {
|
|
300
|
+
let r = sorted[i];
|
|
301
|
+
|
|
302
|
+
text = text.slice(0, r.start) + r.newText + text.slice(r.end);
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
return text;
|
|
306
|
+
}
|
|
307
|
+
|
|
268
308
|
function rewriteExpression(ctx: CodegenContext, expr: ts.Expression): string {
|
|
269
309
|
if (isNestedHtmlTemplate(expr)) {
|
|
270
310
|
return generateNestedTemplateCode(ctx, expr);
|
|
@@ -276,131 +316,97 @@ function rewriteExpression(ctx: CodegenContext, expr: ts.Expression): string {
|
|
|
276
316
|
|
|
277
317
|
let replacements: Replacement[] = [];
|
|
278
318
|
|
|
279
|
-
collectNestedTemplateReplacements(ctx, expr, expr.getStart(), replacements);
|
|
319
|
+
collectNestedTemplateReplacements(ctx, expr, expr.getStart(ctx.sourceFile), replacements);
|
|
280
320
|
|
|
281
|
-
return
|
|
321
|
+
return replaceReverse(expr.getText(ctx.sourceFile), replacements);
|
|
282
322
|
}
|
|
283
323
|
|
|
284
324
|
|
|
285
|
-
const generateCode = (templates: TemplateInfo[],
|
|
325
|
+
const generateCode = (templates: TemplateInfo[], sourceFile: ts.SourceFile, checker?: ts.TypeChecker, existingAliases?: Map<string, string>): CodegenResult => {
|
|
326
|
+
let result: CodegenResult = {
|
|
327
|
+
imports: existingAliases ?? new Map(),
|
|
328
|
+
prepend: [],
|
|
329
|
+
replacements: [],
|
|
330
|
+
templates: new Map()
|
|
331
|
+
};
|
|
332
|
+
|
|
286
333
|
if (templates.length === 0) {
|
|
287
|
-
return
|
|
334
|
+
return result;
|
|
288
335
|
}
|
|
289
336
|
|
|
290
|
-
// Precompute expression ranges for nested template detection
|
|
291
337
|
let ranges: { end: number; start: number }[] = [];
|
|
292
338
|
|
|
293
339
|
for (let i = 0, n = templates.length; i < n; i++) {
|
|
294
340
|
let exprs = templates[i].expressions;
|
|
295
341
|
|
|
296
342
|
for (let j = 0, m = exprs.length; j < m; j++) {
|
|
297
|
-
ranges.push({ end: exprs[j].end, start: exprs[j].getStart() });
|
|
343
|
+
ranges.push({ end: exprs[j].end, start: exprs[j].getStart(sourceFile) });
|
|
298
344
|
}
|
|
299
345
|
}
|
|
300
346
|
|
|
301
|
-
let rootTemplates = templates.filter(t => !ast.inRange(ranges, t.
|
|
347
|
+
let rootTemplates = templates.filter(t => !ast.inRange(ranges, t.node.getStart(sourceFile), t.node.end));
|
|
302
348
|
|
|
303
349
|
if (rootTemplates.length === 0) {
|
|
304
|
-
return
|
|
350
|
+
return result;
|
|
305
351
|
}
|
|
306
352
|
|
|
307
353
|
let ctx: CodegenContext = {
|
|
308
354
|
checker,
|
|
309
|
-
|
|
310
|
-
imports: existingAliases ?? new Map(),
|
|
355
|
+
imports: result.imports,
|
|
311
356
|
printer,
|
|
312
|
-
sourceFile
|
|
357
|
+
sourceFile,
|
|
358
|
+
templates: result.templates
|
|
313
359
|
},
|
|
314
|
-
replacements: Replacement[] = [],
|
|
315
360
|
templateAlias = addImport(ctx, 'template');
|
|
316
361
|
|
|
317
362
|
for (let i = 0, n = rootTemplates.length; i < n; i++) {
|
|
318
|
-
let
|
|
319
|
-
|
|
363
|
+
let template = rootTemplates[i];
|
|
364
|
+
|
|
365
|
+
result.replacements.push({
|
|
366
|
+
generate: (sf) => {
|
|
367
|
+
let codeBefore = sf.getFullText().slice(0, template.node.getStart(sf)),
|
|
368
|
+
exprTexts: string[] = [],
|
|
369
|
+
isArrowBody = codeBefore.trimEnd().endsWith('=>'),
|
|
370
|
+
localCtx: CodegenContext = {
|
|
371
|
+
checker,
|
|
372
|
+
imports: ctx.imports,
|
|
373
|
+
printer,
|
|
374
|
+
sourceFile: sf,
|
|
375
|
+
templates: ctx.templates
|
|
376
|
+
},
|
|
377
|
+
parsed = parser.parse(template.literals) as ParseResult;
|
|
378
|
+
|
|
379
|
+
for (let j = 0, m = template.expressions.length; j < m; j++) {
|
|
380
|
+
exprTexts.push(rewriteExpression(localCtx, template.expressions[j]));
|
|
381
|
+
}
|
|
320
382
|
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
}
|
|
383
|
+
if (isArrowBody && (!parsed.slots || parsed.slots.length === 0)) {
|
|
384
|
+
let arrowMatch = codeBefore.match(ARROW_EMPTY_PARAMS);
|
|
324
385
|
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
// Optimize: when template has no slots and is `() => template`, use template directly
|
|
330
|
-
if (isArrowBody && (!parsed.slots || parsed.slots.length === 0)) {
|
|
331
|
-
let arrowMatch = codeBefore.match(ARROW_EMPTY_PARAMS);
|
|
332
|
-
|
|
333
|
-
if (arrowMatch) {
|
|
334
|
-
replacements.push({
|
|
335
|
-
end: template.end,
|
|
336
|
-
newText: getOrCreateTemplateId(ctx, parsed.html),
|
|
337
|
-
start: template.start - arrowMatch[0].length
|
|
338
|
-
});
|
|
339
|
-
continue;
|
|
340
|
-
}
|
|
341
|
-
}
|
|
386
|
+
if (arrowMatch) {
|
|
387
|
+
return getOrCreateTemplateId(localCtx, parsed.html);
|
|
388
|
+
}
|
|
389
|
+
}
|
|
342
390
|
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
start: template.start
|
|
391
|
+
return generateTemplateCode(
|
|
392
|
+
localCtx,
|
|
393
|
+
parsed,
|
|
394
|
+
exprTexts,
|
|
395
|
+
template.expressions,
|
|
396
|
+
isArrowBody
|
|
397
|
+
);
|
|
398
|
+
},
|
|
399
|
+
node: template.node
|
|
353
400
|
});
|
|
354
401
|
}
|
|
355
402
|
|
|
356
|
-
let
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
if (changed && ctx.templates.size > 0) {
|
|
360
|
-
let aliasedImports: string[] = [],
|
|
361
|
-
factories: string[] = [];
|
|
362
|
-
|
|
363
|
-
for (let [name, alias] of ctx.imports) {
|
|
364
|
-
aliasedImports.push(`${name} as ${alias}`);
|
|
365
|
-
}
|
|
366
|
-
|
|
367
|
-
for (let [html, id] of ctx.templates) {
|
|
368
|
-
factories.push(`const ${id} = ${templateAlias}(\`${html}\`);`);
|
|
369
|
-
}
|
|
370
|
-
|
|
371
|
-
// Remove html entrypoint and add aliased imports
|
|
372
|
-
code = imports.modify(code, sourceFile, PACKAGE, {
|
|
373
|
-
add: new Set(aliasedImports),
|
|
374
|
-
remove: [COMPILER_ENTRYPOINT]
|
|
375
|
-
});
|
|
376
|
-
code = factories.join('\n') + '\n\n' + code;
|
|
403
|
+
for (let [html, id] of ctx.templates) {
|
|
404
|
+
result.prepend.push(`const ${id} = ${templateAlias}(\`${html}\`);`);
|
|
377
405
|
}
|
|
378
406
|
|
|
379
|
-
return
|
|
407
|
+
return result;
|
|
380
408
|
};
|
|
381
409
|
|
|
382
|
-
const generateReactiveInlining = (calls: ReactiveCallInfo[], code: string, sourceFile: ts.SourceFile, arraySlotAlias: string): string => {
|
|
383
|
-
if (calls.length === 0) {
|
|
384
|
-
return code;
|
|
385
|
-
}
|
|
386
|
-
|
|
387
|
-
let replacements: Replacement[] = [];
|
|
388
|
-
|
|
389
|
-
for (let i = 0, n = calls.length; i < n; i++) {
|
|
390
|
-
let call = calls[i];
|
|
391
|
-
|
|
392
|
-
replacements.push({
|
|
393
|
-
end: call.end,
|
|
394
|
-
newText: `new ${arraySlotAlias}(
|
|
395
|
-
${printer.printNode(ts.EmitHint.Expression, call.arrayArg, sourceFile)},
|
|
396
|
-
${printer.printNode(ts.EmitHint.Expression, call.callbackArg, sourceFile)}
|
|
397
|
-
)`,
|
|
398
|
-
start: call.start
|
|
399
|
-
});
|
|
400
|
-
}
|
|
401
|
-
|
|
402
|
-
return c.replaceReverse(code, replacements);
|
|
403
|
-
};
|
|
404
410
|
|
|
405
|
-
export { generateCode
|
|
411
|
+
export { generateCode };
|
|
406
412
|
export type { CodegenResult };
|
package/src/compiler/index.ts
CHANGED
|
@@ -1,86 +1,56 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import { findHtmlTemplates
|
|
1
|
+
import type { ImportIntent, Plugin, TransformContext } from '@esportsplus/typescript/compiler';
|
|
2
|
+
import { COMPILER_ENTRYPOINT, PACKAGE } from '~/constants';
|
|
3
|
+
import { generateCode } from './codegen';
|
|
4
|
+
import reactiveInliningPlugin, { SHARED_KEY } from './reactive-inlining';
|
|
5
|
+
import { findHtmlTemplates } from './ts-parser';
|
|
6
6
|
|
|
7
7
|
|
|
8
|
-
|
|
9
|
-
changed: boolean;
|
|
10
|
-
code: string;
|
|
11
|
-
sourceFile: ts.SourceFile;
|
|
12
|
-
};
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
const PATTERNS = [`${COMPILER_ENTRYPOINT}\``, `${COMPILER_ENTRYPOINT}.${COMPILER_ENTRYPOINT_REACTIVITY}`];
|
|
16
|
-
|
|
17
|
-
const REGEX_BACKSLASH = /\\/g;
|
|
18
|
-
|
|
19
|
-
const REGEX_FORWARD_SLASH = /\//g;
|
|
8
|
+
const PATTERNS = [`${COMPILER_ENTRYPOINT}\``];
|
|
20
9
|
|
|
21
10
|
|
|
22
|
-
const
|
|
23
|
-
|
|
11
|
+
const templatePlugin: Plugin = {
|
|
12
|
+
patterns: PATTERNS,
|
|
24
13
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
let checker: ts.TypeChecker | undefined,
|
|
30
|
-
fileName = sourceFile.fileName,
|
|
31
|
-
programSourceFile = program.getSourceFile(fileName)
|
|
32
|
-
|| program.getSourceFile(fileName.replace(REGEX_BACKSLASH, '/'))
|
|
33
|
-
|| program.getSourceFile(fileName.replace(REGEX_FORWARD_SLASH, '\\'));
|
|
34
|
-
|
|
35
|
-
if (programSourceFile) {
|
|
36
|
-
checker = program.getTypeChecker();
|
|
37
|
-
sourceFile = programSourceFile;
|
|
38
|
-
}
|
|
14
|
+
transform: (ctx: TransformContext) => {
|
|
15
|
+
let templates = findHtmlTemplates(ctx.sourceFile, ctx.checker);
|
|
39
16
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
reactiveCalls = findReactiveCalls(sourceFile, checker),
|
|
44
|
-
result = code;
|
|
45
|
-
|
|
46
|
-
if (reactiveCalls.length > 0) {
|
|
47
|
-
let arraySlotAlias = uid('ArraySlot');
|
|
17
|
+
if (templates.length === 0) {
|
|
18
|
+
return {};
|
|
19
|
+
}
|
|
48
20
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
result = generateReactiveInlining(reactiveCalls, result, sourceFile, arraySlotAlias);
|
|
52
|
-
sourceFile = ts.createSourceFile(sourceFile.fileName, result, sourceFile.languageVersion, true);
|
|
53
|
-
}
|
|
21
|
+
let existingAliases = new Map<string, string>(),
|
|
22
|
+
existingArraySlotAlias = ctx.shared.get(SHARED_KEY) as string | undefined;
|
|
54
23
|
|
|
55
|
-
|
|
24
|
+
if (existingArraySlotAlias) {
|
|
25
|
+
existingAliases.set('ArraySlot', existingArraySlotAlias);
|
|
26
|
+
}
|
|
56
27
|
|
|
57
|
-
|
|
58
|
-
let codegenResult = generateCode(templates, result, sourceFile, checker, existingAliases);
|
|
28
|
+
let result = generateCode(templates, ctx.sourceFile, ctx.checker, existingAliases);
|
|
59
29
|
|
|
60
|
-
if (
|
|
61
|
-
|
|
62
|
-
codegenChanged = true;
|
|
63
|
-
result = codegenResult.code;
|
|
30
|
+
if (result.replacements.length === 0) {
|
|
31
|
+
return {};
|
|
64
32
|
}
|
|
65
|
-
}
|
|
66
33
|
|
|
67
|
-
// Add aliased ArraySlot import if reactive calls were processed but codegen didn't run
|
|
68
|
-
if (existingAliases.size > 0 && !codegenChanged) {
|
|
69
34
|
let aliasedImports: string[] = [];
|
|
70
35
|
|
|
71
|
-
for (let [name, alias] of
|
|
36
|
+
for (let [name, alias] of result.imports) {
|
|
72
37
|
aliasedImports.push(`${name} as ${alias}`);
|
|
73
38
|
}
|
|
74
39
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
40
|
+
let imports: ImportIntent[] = [{
|
|
41
|
+
add: aliasedImports,
|
|
42
|
+
package: PACKAGE,
|
|
43
|
+
remove: [COMPILER_ENTRYPOINT]
|
|
44
|
+
}];
|
|
45
|
+
|
|
46
|
+
return {
|
|
47
|
+
imports,
|
|
48
|
+
prepend: result.prepend,
|
|
49
|
+
replacements: result.replacements
|
|
50
|
+
};
|
|
80
51
|
}
|
|
81
|
-
|
|
82
|
-
return { changed, code: result, sourceFile };
|
|
83
52
|
};
|
|
84
53
|
|
|
85
54
|
|
|
86
|
-
export
|
|
55
|
+
export default templatePlugin;
|
|
56
|
+
export { reactiveInliningPlugin, templatePlugin };
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { plugin } from '@esportsplus/typescript/compiler';
|
|
2
|
-
import {
|
|
2
|
+
import { reactiveInliningPlugin, templatePlugin } from '..';
|
|
3
3
|
|
|
4
4
|
|
|
5
|
-
export default plugin.tsc(
|
|
5
|
+
export default plugin.tsc([reactiveInliningPlugin, templatePlugin]) as ReturnType<typeof plugin.tsc>;
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { plugin } from '@esportsplus/typescript/compiler';
|
|
2
2
|
import { PACKAGE } from '../../constants';
|
|
3
|
-
import {
|
|
3
|
+
import { reactiveInliningPlugin, templatePlugin } from '..';
|
|
4
4
|
|
|
5
5
|
|
|
6
6
|
export default plugin.vite({
|
|
7
7
|
name: PACKAGE,
|
|
8
|
-
|
|
8
|
+
plugins: [reactiveInliningPlugin, templatePlugin]
|
|
9
9
|
});
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import type { Plugin, ReplacementIntent, TransformContext } from '@esportsplus/typescript/compiler';
|
|
2
|
+
import { ts } from '@esportsplus/typescript';
|
|
3
|
+
import { uid } from '@esportsplus/typescript/compiler';
|
|
4
|
+
import { COMPILER_ENTRYPOINT, COMPILER_ENTRYPOINT_REACTIVITY, PACKAGE } from '~/constants';
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
type ReactiveCallInfo = {
|
|
8
|
+
arrayArg: ts.Expression;
|
|
9
|
+
callbackArg: ts.Expression;
|
|
10
|
+
node: ts.CallExpression;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
const PATTERNS = [`${COMPILER_ENTRYPOINT}.${COMPILER_ENTRYPOINT_REACTIVITY}`];
|
|
15
|
+
|
|
16
|
+
const SHARED_KEY = 'template:arraySlotAlias';
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
let printer = ts.createPrinter({ newLine: ts.NewLineKind.LineFeed });
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
function isHtmlFromPackage(node: ts.Identifier, checker: ts.TypeChecker | undefined): boolean {
|
|
23
|
+
if (node.text !== COMPILER_ENTRYPOINT) {
|
|
24
|
+
return false;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
if (!checker) {
|
|
28
|
+
return true;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
let symbol = checker.getSymbolAtLocation(node);
|
|
32
|
+
|
|
33
|
+
if (!symbol) {
|
|
34
|
+
return true;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
if (symbol.flags & ts.SymbolFlags.Alias) {
|
|
38
|
+
symbol = checker.getAliasedSymbol(symbol);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
let declarations = symbol.getDeclarations();
|
|
42
|
+
|
|
43
|
+
if (!declarations || declarations.length === 0) {
|
|
44
|
+
return true;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
for (let i = 0, n = declarations.length; i < n; i++) {
|
|
48
|
+
let filename = declarations[i].getSourceFile().fileName;
|
|
49
|
+
|
|
50
|
+
if (filename.includes(PACKAGE) || filename.includes('@esportsplus/template')) {
|
|
51
|
+
return true;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return false;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function visit(node: ts.Node, calls: ReactiveCallInfo[], checker: ts.TypeChecker | undefined): void {
|
|
59
|
+
if (
|
|
60
|
+
ts.isCallExpression(node) &&
|
|
61
|
+
ts.isPropertyAccessExpression(node.expression) &&
|
|
62
|
+
ts.isIdentifier(node.expression.expression) &&
|
|
63
|
+
node.expression.name.text === COMPILER_ENTRYPOINT_REACTIVITY &&
|
|
64
|
+
node.arguments.length === 2 &&
|
|
65
|
+
isHtmlFromPackage(node.expression.expression, checker)
|
|
66
|
+
) {
|
|
67
|
+
calls.push({
|
|
68
|
+
arrayArg: node.arguments[0],
|
|
69
|
+
callbackArg: node.arguments[1],
|
|
70
|
+
node
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
ts.forEachChild(node, child => visit(child, calls, checker));
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
const plugin: Plugin = {
|
|
79
|
+
patterns: PATTERNS,
|
|
80
|
+
|
|
81
|
+
transform: (ctx: TransformContext) => {
|
|
82
|
+
let calls: ReactiveCallInfo[] = [];
|
|
83
|
+
|
|
84
|
+
visit(ctx.sourceFile, calls, ctx.checker);
|
|
85
|
+
|
|
86
|
+
if (calls.length === 0) {
|
|
87
|
+
return {};
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
let arraySlotAlias = uid('ArraySlot'),
|
|
91
|
+
replacements: ReplacementIntent[] = [];
|
|
92
|
+
|
|
93
|
+
ctx.shared.set(SHARED_KEY, arraySlotAlias);
|
|
94
|
+
|
|
95
|
+
for (let i = 0, n = calls.length; i < n; i++) {
|
|
96
|
+
let call = calls[i];
|
|
97
|
+
|
|
98
|
+
replacements.push({
|
|
99
|
+
generate: (sourceFile) => `new ${arraySlotAlias}(${printer.printNode(ts.EmitHint.Expression, call.arrayArg, sourceFile)}, ${printer.printNode(ts.EmitHint.Expression, call.callbackArg, sourceFile)})`,
|
|
100
|
+
node: call.node
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return {
|
|
105
|
+
imports: [{
|
|
106
|
+
add: [`ArraySlot as ${arraySlotAlias}`],
|
|
107
|
+
package: PACKAGE
|
|
108
|
+
}],
|
|
109
|
+
replacements
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
export default plugin;
|
|
116
|
+
export { SHARED_KEY };
|