@esportsplus/template 0.35.0 → 0.37.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/attributes.d.ts +3 -3
- package/build/attributes.js +4 -1
- package/build/compiler/analyzer.d.ts +4 -0
- package/build/compiler/analyzer.js +65 -0
- package/build/compiler/codegen.d.ts +3 -4
- package/build/compiler/codegen.js +67 -66
- package/build/compiler/index.d.ts +4 -2
- package/build/compiler/index.js +55 -18
- package/build/compiler/plugins/tsc.js +2 -2
- package/build/compiler/plugins/vite.d.ts +4 -4
- package/build/compiler/plugins/vite.js +2 -1
- package/build/compiler/ts-parser.js +2 -2
- package/package.json +4 -4
- package/src/attributes.ts +9 -6
- package/src/compiler/analyzer.ts +92 -0
- package/src/compiler/codegen.ts +95 -91
- package/src/compiler/index.ts +85 -23
- package/src/compiler/plugins/tsc.ts +2 -2
- package/src/compiler/plugins/vite.ts +2 -1
- 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/build/compiler/type-analyzer.js +0 -193
- package/src/compiler/type-analyzer.ts +0 -274
|
@@ -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,8 @@
|
|
|
1
1
|
import { ts } from '@esportsplus/typescript';
|
|
2
2
|
import { ast, code as c, imports, uid, type Replacement } from '@esportsplus/typescript/compiler';
|
|
3
|
-
import { COMPILER_ENTRYPOINT, COMPILER_TYPES, PACKAGE } from '~/constants';
|
|
3
|
+
import { COMPILER_ENTRYPOINT, COMPILER_TYPES, DIRECT_ATTACH_EVENTS, LIFECYCLE_EVENTS, PACKAGE } from '~/constants';
|
|
4
4
|
import type { ReactiveCallInfo, TemplateInfo } from './ts-parser';
|
|
5
|
-
import {
|
|
5
|
+
import { analyze } from './analyzer';
|
|
6
6
|
import parser from './parser';
|
|
7
7
|
|
|
8
8
|
|
|
@@ -17,11 +17,10 @@ type Attribute = {
|
|
|
17
17
|
|
|
18
18
|
type CodegenContext = {
|
|
19
19
|
checker?: ts.TypeChecker;
|
|
20
|
-
|
|
21
|
-
htmlToTemplateId: Map<string, string>;
|
|
22
|
-
neededImports: Set<string>;
|
|
20
|
+
imports: Map<string, string>;
|
|
23
21
|
printer: ts.Printer;
|
|
24
22
|
sourceFile: ts.SourceFile;
|
|
23
|
+
templates: Map<string, string>;
|
|
25
24
|
};
|
|
26
25
|
|
|
27
26
|
type CodegenResult = {
|
|
@@ -46,6 +45,44 @@ const ARROW_EMPTY_PARAMS = /\(\s*\)\s*=>\s*$/;
|
|
|
46
45
|
let printer = ts.createPrinter({ newLine: ts.NewLineKind.LineFeed });
|
|
47
46
|
|
|
48
47
|
|
|
48
|
+
function addImport(ctx: CodegenContext, name: string): string {
|
|
49
|
+
let alias = ctx.imports.get(name);
|
|
50
|
+
|
|
51
|
+
if (!alias) {
|
|
52
|
+
alias = uid(name);
|
|
53
|
+
ctx.imports.set(name, alias);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return alias;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function generateAttributeBinding(ctx: CodegenContext, element: string, name: string, expr: string, staticValue: string): string {
|
|
60
|
+
if (name.startsWith('on') && name.length > 2) {
|
|
61
|
+
let event = name.slice(2).toLowerCase(),
|
|
62
|
+
key = name.toLowerCase();
|
|
63
|
+
|
|
64
|
+
if (LIFECYCLE_EVENTS.has(key)) {
|
|
65
|
+
return `${addImport(ctx, key)}(${element}, ${expr});`;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
if (DIRECT_ATTACH_EVENTS.has(key)) {
|
|
69
|
+
return `${addImport(ctx, 'on')}(${element}, '${event}', ${expr});`;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return `${addImport(ctx, 'delegate')}(${element}, '${event}', ${expr});`;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
if (name === 'class') {
|
|
76
|
+
return `${addImport(ctx, 'setClass')}(${element}, '${staticValue}', ${expr});`;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if (name === 'style') {
|
|
80
|
+
return `${addImport(ctx, 'setStyle')}(${element}, '${staticValue}', ${expr});`;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return `${addImport(ctx, 'setProperty')}(${element}, '${name}', ${expr});`;
|
|
84
|
+
}
|
|
85
|
+
|
|
49
86
|
function collectNestedTemplateReplacements(
|
|
50
87
|
ctx: CodegenContext,
|
|
51
88
|
node: ts.Node,
|
|
@@ -96,24 +133,21 @@ function generateNestedTemplateCode(ctx: CodegenContext, node: ts.TaggedTemplate
|
|
|
96
133
|
|
|
97
134
|
function generateNodeBinding(ctx: CodegenContext, anchor: string, exprText: string, exprNode: ts.Expression | undefined): string {
|
|
98
135
|
if (!exprNode) {
|
|
99
|
-
ctx
|
|
100
|
-
return `slot(${anchor}, ${exprText});`;
|
|
136
|
+
return `${addImport(ctx, 'slot')}(${anchor}, ${exprText});`;
|
|
101
137
|
}
|
|
102
138
|
|
|
103
139
|
if (isNestedHtmlTemplate(exprNode)) {
|
|
104
140
|
return `${anchor}.parentNode.insertBefore(${generateNestedTemplateCode(ctx, exprNode)}, ${anchor});`;
|
|
105
141
|
}
|
|
106
142
|
|
|
107
|
-
let slotType =
|
|
143
|
+
let slotType = analyze(exprNode, ctx.checker);
|
|
108
144
|
|
|
109
145
|
switch (slotType) {
|
|
110
146
|
case COMPILER_TYPES.Effect:
|
|
111
|
-
ctx
|
|
112
|
-
return `new EffectSlot(${anchor}, ${exprText});`;
|
|
147
|
+
return `new ${addImport(ctx, 'EffectSlot')}(${anchor}, ${exprText});`;
|
|
113
148
|
|
|
114
149
|
case COMPILER_TYPES.ArraySlot:
|
|
115
|
-
ctx
|
|
116
|
-
return `new ArraySlot(${anchor}, ${exprText});`;
|
|
150
|
+
return `new ${addImport(ctx, 'ArraySlot')}(${anchor}, ${exprText});`;
|
|
117
151
|
|
|
118
152
|
case COMPILER_TYPES.Static:
|
|
119
153
|
return `${anchor}.textContent = ${exprText};`;
|
|
@@ -122,8 +156,7 @@ function generateNodeBinding(ctx: CodegenContext, anchor: string, exprText: stri
|
|
|
122
156
|
return `${anchor}.parentNode.insertBefore(${exprText}, ${anchor});`;
|
|
123
157
|
|
|
124
158
|
default:
|
|
125
|
-
ctx
|
|
126
|
-
return `slot(${anchor}, ${exprText});`;
|
|
159
|
+
return `${addImport(ctx, 'slot')}(${anchor}, ${exprText});`;
|
|
127
160
|
}
|
|
128
161
|
}
|
|
129
162
|
|
|
@@ -160,24 +193,30 @@ function generateTemplateCode(
|
|
|
160
193
|
continue;
|
|
161
194
|
}
|
|
162
195
|
|
|
163
|
-
let
|
|
164
|
-
|
|
196
|
+
let ancestor = root,
|
|
197
|
+
start = 0;
|
|
165
198
|
|
|
166
199
|
for (let j = path.length - 1; j >= 0; j--) {
|
|
167
200
|
let prefix = path.slice(0, j).join('.');
|
|
168
201
|
|
|
169
202
|
if (nodes.has(prefix)) {
|
|
170
|
-
|
|
171
|
-
|
|
203
|
+
ancestor = nodes.get(prefix)!;
|
|
204
|
+
start = j;
|
|
172
205
|
break;
|
|
173
206
|
}
|
|
174
207
|
}
|
|
175
208
|
|
|
176
|
-
let
|
|
177
|
-
|
|
209
|
+
let alias = addImport(ctx, 'Element'),
|
|
210
|
+
name = uid('element'),
|
|
211
|
+
segments = path.slice(start),
|
|
212
|
+
value = `${ancestor}.${segments.join('!.')}`;
|
|
178
213
|
|
|
179
|
-
|
|
180
|
-
|
|
214
|
+
// Cast root.firstChild to Element since DocumentFragment.firstChild returns ChildNode
|
|
215
|
+
if (ancestor === root && segments[0] === 'firstChild') {
|
|
216
|
+
value = value.replace(`${ancestor}.firstChild!`, `(${ancestor}.firstChild! as ${alias})`);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
declarations.push(`${name} = ${value} as ${alias}`);
|
|
181
220
|
nodes.set(key, name);
|
|
182
221
|
}
|
|
183
222
|
|
|
@@ -187,7 +226,7 @@ function generateTemplateCode(
|
|
|
187
226
|
);
|
|
188
227
|
|
|
189
228
|
for (let i = 0, n = slots.length; i < n; i++) {
|
|
190
|
-
let
|
|
229
|
+
let element = slots[i].path.length === 0
|
|
191
230
|
? root
|
|
192
231
|
: (nodes.get(slots[i].path.join('.')) || root),
|
|
193
232
|
slot = slots[i];
|
|
@@ -199,28 +238,19 @@ function generateTemplateCode(
|
|
|
199
238
|
let name = names[j];
|
|
200
239
|
|
|
201
240
|
if (name === COMPILER_TYPES.Attributes) {
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
elementVar,
|
|
206
|
-
ctx.checker,
|
|
207
|
-
ctx.neededImports
|
|
208
|
-
);
|
|
209
|
-
|
|
210
|
-
for (let k = 0, o = bindings.length; k < o; k++) {
|
|
211
|
-
code.push(bindings[k]);
|
|
212
|
-
}
|
|
213
|
-
|
|
241
|
+
code.push(
|
|
242
|
+
`${addImport(ctx, 'setProperties')}(${element}, ${exprTexts[index] || 'undefined'});`
|
|
243
|
+
);
|
|
214
244
|
index++;
|
|
215
245
|
}
|
|
216
246
|
else {
|
|
217
247
|
code.push(
|
|
218
248
|
generateAttributeBinding(
|
|
219
|
-
|
|
249
|
+
ctx,
|
|
250
|
+
element,
|
|
220
251
|
name,
|
|
221
252
|
exprTexts[index++] || 'undefined',
|
|
222
|
-
slot.attributes.statics[name] || ''
|
|
223
|
-
ctx.neededImports
|
|
253
|
+
slot.attributes.statics[name] || ''
|
|
224
254
|
)
|
|
225
255
|
);
|
|
226
256
|
}
|
|
@@ -228,7 +258,7 @@ function generateTemplateCode(
|
|
|
228
258
|
}
|
|
229
259
|
else {
|
|
230
260
|
code.push(
|
|
231
|
-
generateNodeBinding(ctx,
|
|
261
|
+
generateNodeBinding(ctx, element, exprTexts[index] || 'undefined', exprNodes[index])
|
|
232
262
|
);
|
|
233
263
|
index++;
|
|
234
264
|
}
|
|
@@ -241,41 +271,16 @@ function generateTemplateCode(
|
|
|
241
271
|
}
|
|
242
272
|
|
|
243
273
|
function getOrCreateTemplateId(ctx: CodegenContext, html: string): string {
|
|
244
|
-
let id = ctx.
|
|
274
|
+
let id = ctx.templates.get(html);
|
|
245
275
|
|
|
246
276
|
if (!id) {
|
|
247
|
-
id = uid('
|
|
248
|
-
ctx.
|
|
249
|
-
ctx.htmlToTemplateId.set(html, id);
|
|
277
|
+
id = uid('template');
|
|
278
|
+
ctx.templates.set(html, id);
|
|
250
279
|
}
|
|
251
280
|
|
|
252
281
|
return id;
|
|
253
282
|
}
|
|
254
283
|
|
|
255
|
-
function hasArraySlotImport(sourceFile: ts.SourceFile): boolean {
|
|
256
|
-
for (let i = 0, n = sourceFile.statements.length; i < n; i++) {
|
|
257
|
-
let stmt = sourceFile.statements[i];
|
|
258
|
-
|
|
259
|
-
if (!ts.isImportDeclaration(stmt) || !stmt.importClause?.namedBindings) {
|
|
260
|
-
continue;
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
let bindings = stmt.importClause.namedBindings;
|
|
264
|
-
|
|
265
|
-
if (!ts.isNamedImports(bindings)) {
|
|
266
|
-
continue;
|
|
267
|
-
}
|
|
268
|
-
|
|
269
|
-
for (let j = 0, m = bindings.elements.length; j < m; j++) {
|
|
270
|
-
if (bindings.elements[j].name.text === 'ArraySlot') {
|
|
271
|
-
return true;
|
|
272
|
-
}
|
|
273
|
-
}
|
|
274
|
-
}
|
|
275
|
-
|
|
276
|
-
return false;
|
|
277
|
-
}
|
|
278
|
-
|
|
279
284
|
function isNestedHtmlTemplate(expr: ts.Expression): expr is ts.TaggedTemplateExpression {
|
|
280
285
|
return ts.isTaggedTemplateExpression(expr) && ts.isIdentifier(expr.tag) && expr.tag.text === COMPILER_ENTRYPOINT;
|
|
281
286
|
}
|
|
@@ -297,7 +302,7 @@ function rewriteExpression(ctx: CodegenContext, expr: ts.Expression): string {
|
|
|
297
302
|
}
|
|
298
303
|
|
|
299
304
|
|
|
300
|
-
const generateCode = (templates: TemplateInfo[], originalCode: string, sourceFile: ts.SourceFile, checker?: ts.TypeChecker): CodegenResult => {
|
|
305
|
+
const generateCode = (templates: TemplateInfo[], originalCode: string, sourceFile: ts.SourceFile, checker?: ts.TypeChecker, existingAliases?: Map<string, string>): CodegenResult => {
|
|
301
306
|
if (templates.length === 0) {
|
|
302
307
|
return { changed: false, code: originalCode };
|
|
303
308
|
}
|
|
@@ -321,13 +326,13 @@ const generateCode = (templates: TemplateInfo[], originalCode: string, sourceFil
|
|
|
321
326
|
|
|
322
327
|
let ctx: CodegenContext = {
|
|
323
328
|
checker,
|
|
324
|
-
|
|
325
|
-
htmlToTemplateId: new Map(),
|
|
326
|
-
neededImports: new Set(['template']),
|
|
329
|
+
imports: existingAliases ?? new Map(),
|
|
327
330
|
printer,
|
|
328
|
-
sourceFile
|
|
331
|
+
sourceFile,
|
|
332
|
+
templates: new Map(),
|
|
329
333
|
},
|
|
330
|
-
replacements: Replacement[] = []
|
|
334
|
+
replacements: Replacement[] = [],
|
|
335
|
+
templateAlias = addImport(ctx, 'template');
|
|
331
336
|
|
|
332
337
|
for (let i = 0, n = rootTemplates.length; i < n; i++) {
|
|
333
338
|
let exprTexts: string[] = [],
|
|
@@ -371,23 +376,31 @@ const generateCode = (templates: TemplateInfo[], originalCode: string, sourceFil
|
|
|
371
376
|
let changed = replacements.length > 0,
|
|
372
377
|
code = c.replaceReverse(originalCode, replacements);
|
|
373
378
|
|
|
374
|
-
if (changed && ctx.
|
|
375
|
-
let
|
|
379
|
+
if (changed && ctx.templates.size > 0) {
|
|
380
|
+
let aliasedImports: string[] = [],
|
|
381
|
+
factories: string[] = [],
|
|
382
|
+
updatedSourceFile = ts.createSourceFile(sourceFile.fileName, code, sourceFile.languageVersion, true);
|
|
376
383
|
|
|
377
|
-
for (let [
|
|
378
|
-
|
|
384
|
+
for (let [name, alias] of ctx.imports) {
|
|
385
|
+
aliasedImports.push(`${name} as ${alias}`);
|
|
379
386
|
}
|
|
380
387
|
|
|
381
|
-
|
|
382
|
-
|
|
388
|
+
for (let [html, id] of ctx.templates) {
|
|
389
|
+
factories.push(`const ${id} = ${templateAlias}(\`${html}\`);`);
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
// Remove html entrypoint and add aliased imports
|
|
393
|
+
code = imports.modify(code, updatedSourceFile, PACKAGE, {
|
|
394
|
+
add: new Set(aliasedImports),
|
|
383
395
|
remove: [COMPILER_ENTRYPOINT]
|
|
384
396
|
});
|
|
397
|
+
code = factories.join('\n') + '\n\n' + code;
|
|
385
398
|
}
|
|
386
399
|
|
|
387
400
|
return { changed, code };
|
|
388
401
|
};
|
|
389
402
|
|
|
390
|
-
const generateReactiveInlining = (calls: ReactiveCallInfo[], code: string, sourceFile: ts.SourceFile): string => {
|
|
403
|
+
const generateReactiveInlining = (calls: ReactiveCallInfo[], code: string, sourceFile: ts.SourceFile, arraySlotAlias: string): string => {
|
|
391
404
|
if (calls.length === 0) {
|
|
392
405
|
return code;
|
|
393
406
|
}
|
|
@@ -399,7 +412,7 @@ const generateReactiveInlining = (calls: ReactiveCallInfo[], code: string, sourc
|
|
|
399
412
|
|
|
400
413
|
replacements.push({
|
|
401
414
|
end: call.end,
|
|
402
|
-
newText: `new
|
|
415
|
+
newText: `new ${arraySlotAlias}(
|
|
403
416
|
${printer.printNode(ts.EmitHint.Expression, call.arrayArg, sourceFile)},
|
|
404
417
|
${printer.printNode(ts.EmitHint.Expression, call.callbackArg, sourceFile)}
|
|
405
418
|
)`,
|
|
@@ -410,14 +423,5 @@ const generateReactiveInlining = (calls: ReactiveCallInfo[], code: string, sourc
|
|
|
410
423
|
return c.replaceReverse(code, replacements);
|
|
411
424
|
};
|
|
412
425
|
|
|
413
|
-
|
|
414
|
-
return ast.hasMatch(sourceFile, n =>
|
|
415
|
-
ts.isNewExpression(n) &&
|
|
416
|
-
ts.isIdentifier(n.expression) &&
|
|
417
|
-
n.expression.text === 'ArraySlot'
|
|
418
|
-
) && !hasArraySlotImport(sourceFile);
|
|
419
|
-
};
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
export { generateCode, generateReactiveInlining, needsArraySlotImport };
|
|
426
|
+
export { generateCode, generateReactiveInlining };
|
|
423
427
|
export type { CodegenResult };
|
package/src/compiler/index.ts
CHANGED
|
@@ -1,10 +1,16 @@
|
|
|
1
|
+
import type { PluginContext } from '@esportsplus/typescript/compiler';
|
|
1
2
|
import { ts } from '@esportsplus/typescript';
|
|
2
|
-
import { code as c, imports } from '@esportsplus/typescript/compiler';
|
|
3
|
+
import { code as c, imports, uid } from '@esportsplus/typescript/compiler';
|
|
3
4
|
import { COMPILER_ENTRYPOINT, COMPILER_ENTRYPOINT_REACTIVITY, PACKAGE } from '~/constants';
|
|
4
|
-
import { generateCode, generateReactiveInlining
|
|
5
|
-
import { findHtmlTemplates, findReactiveCalls } from './ts-parser';
|
|
5
|
+
import { generateCode, generateReactiveInlining } from './codegen';
|
|
6
|
+
import { findHtmlTemplates, findReactiveCalls, type ReactiveCallInfo, type TemplateInfo } from './ts-parser';
|
|
6
7
|
|
|
7
8
|
|
|
9
|
+
type AnalyzedFile = {
|
|
10
|
+
reactiveCalls: ReactiveCallInfo[];
|
|
11
|
+
templates: TemplateInfo[];
|
|
12
|
+
};
|
|
13
|
+
|
|
8
14
|
type TransformResult = {
|
|
9
15
|
changed: boolean;
|
|
10
16
|
code: string;
|
|
@@ -12,6 +18,8 @@ type TransformResult = {
|
|
|
12
18
|
};
|
|
13
19
|
|
|
14
20
|
|
|
21
|
+
const CONTEXT_KEY = 'template:analyzed';
|
|
22
|
+
|
|
15
23
|
const PATTERNS = [`${COMPILER_ENTRYPOINT}\``, `${COMPILER_ENTRYPOINT}.${COMPILER_ENTRYPOINT_REACTIVITY}`];
|
|
16
24
|
|
|
17
25
|
const REGEX_BACKSLASH = /\\/g;
|
|
@@ -19,41 +27,88 @@ const REGEX_BACKSLASH = /\\/g;
|
|
|
19
27
|
const REGEX_FORWARD_SLASH = /\//g;
|
|
20
28
|
|
|
21
29
|
|
|
22
|
-
|
|
30
|
+
function getAnalyzedFile(context: PluginContext | undefined, filename: string): AnalyzedFile | undefined {
|
|
31
|
+
return (context?.get(CONTEXT_KEY) as Map<string, AnalyzedFile> | undefined)?.get(filename);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
const analyze = (sourceFile: ts.SourceFile, program: ts.Program, context: PluginContext): void => {
|
|
23
36
|
let code = sourceFile.getFullText();
|
|
24
37
|
|
|
25
38
|
if (!c.contains(code, { patterns: PATTERNS })) {
|
|
26
|
-
return
|
|
39
|
+
return;
|
|
27
40
|
}
|
|
28
41
|
|
|
29
|
-
let checker
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|| program.getSourceFile(
|
|
42
|
+
let checker = program.getTypeChecker(),
|
|
43
|
+
filename = sourceFile.fileName,
|
|
44
|
+
files = context.get(CONTEXT_KEY) as Map<string, AnalyzedFile> | undefined,
|
|
45
|
+
programSourceFile = program.getSourceFile(filename)
|
|
46
|
+
|| program.getSourceFile(filename.replace(REGEX_BACKSLASH, '/'))
|
|
47
|
+
|| program.getSourceFile(filename.replace(REGEX_FORWARD_SLASH, '\\'));
|
|
34
48
|
|
|
35
49
|
if (programSourceFile) {
|
|
36
|
-
checker = program.getTypeChecker();
|
|
37
50
|
sourceFile = programSourceFile;
|
|
38
51
|
}
|
|
39
52
|
|
|
53
|
+
if (!files) {
|
|
54
|
+
files = new Map();
|
|
55
|
+
context.set(CONTEXT_KEY, files);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
files.set(filename, {
|
|
59
|
+
reactiveCalls: findReactiveCalls(sourceFile, checker),
|
|
60
|
+
templates: findHtmlTemplates(sourceFile, checker)
|
|
61
|
+
});
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
const transform = (sourceFile: ts.SourceFile, program: ts.Program, context?: PluginContext): TransformResult => {
|
|
65
|
+
let code = sourceFile.getFullText(),
|
|
66
|
+
filename = sourceFile.fileName;
|
|
67
|
+
|
|
68
|
+
// Try to get pre-analyzed data from context
|
|
69
|
+
let analyzed = getAnalyzedFile(context, filename);
|
|
70
|
+
|
|
71
|
+
// Fall back to inline analysis (for Vite or when context unavailable)
|
|
72
|
+
if (!analyzed) {
|
|
73
|
+
if (!c.contains(code, { patterns: PATTERNS })) {
|
|
74
|
+
return { changed: false, code, sourceFile };
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
let checker = program.getTypeChecker(),
|
|
78
|
+
programSourceFile = program.getSourceFile(filename)
|
|
79
|
+
|| program.getSourceFile(filename.replace(REGEX_BACKSLASH, '/'))
|
|
80
|
+
|| program.getSourceFile(filename.replace(REGEX_FORWARD_SLASH, '\\'));
|
|
81
|
+
|
|
82
|
+
if (programSourceFile) {
|
|
83
|
+
sourceFile = programSourceFile;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
analyzed = {
|
|
87
|
+
reactiveCalls: findReactiveCalls(sourceFile, checker),
|
|
88
|
+
templates: findHtmlTemplates(sourceFile, checker)
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
|
|
40
92
|
let changed = false,
|
|
41
93
|
codegenChanged = false,
|
|
42
|
-
|
|
43
|
-
reactiveCalls = findReactiveCalls(sourceFile, checker),
|
|
94
|
+
existingAliases = new Map<string, string>(),
|
|
44
95
|
result = code;
|
|
45
96
|
|
|
46
|
-
if (reactiveCalls.length > 0) {
|
|
97
|
+
if (analyzed.reactiveCalls.length > 0) {
|
|
47
98
|
changed = true;
|
|
48
|
-
|
|
99
|
+
existingAliases.set('ArraySlot', uid('ArraySlot'));
|
|
100
|
+
result = generateReactiveInlining(analyzed.reactiveCalls, result, sourceFile, existingAliases.get('ArraySlot')!);
|
|
49
101
|
sourceFile = ts.createSourceFile(sourceFile.fileName, result, sourceFile.languageVersion, true);
|
|
50
|
-
needsImport = needsArraySlotImport(sourceFile);
|
|
51
|
-
}
|
|
52
102
|
|
|
53
|
-
|
|
103
|
+
// Re-analyze templates after reactive inlining modifies the code
|
|
104
|
+
analyzed = {
|
|
105
|
+
reactiveCalls: [],
|
|
106
|
+
templates: findHtmlTemplates(sourceFile, program.getTypeChecker())
|
|
107
|
+
};
|
|
108
|
+
}
|
|
54
109
|
|
|
55
|
-
if (templates.length > 0) {
|
|
56
|
-
let codegenResult = generateCode(templates, result, sourceFile,
|
|
110
|
+
if (analyzed.templates.length > 0) {
|
|
111
|
+
let codegenResult = generateCode(analyzed.templates, result, sourceFile, program.getTypeChecker(), existingAliases);
|
|
57
112
|
|
|
58
113
|
if (codegenResult.changed) {
|
|
59
114
|
changed = true;
|
|
@@ -62,8 +117,15 @@ const transform = (sourceFile: ts.SourceFile, program: ts.Program): TransformRes
|
|
|
62
117
|
}
|
|
63
118
|
}
|
|
64
119
|
|
|
65
|
-
if
|
|
66
|
-
|
|
120
|
+
// Add aliased ArraySlot import if reactive calls were processed but codegen didn't run
|
|
121
|
+
if (existingAliases.size > 0 && !codegenChanged) {
|
|
122
|
+
let aliasedImports: string[] = [];
|
|
123
|
+
|
|
124
|
+
for (let [name, alias] of existingAliases) {
|
|
125
|
+
aliasedImports.push(`${name} as ${alias}`);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
result = imports.modify(result, sourceFile, PACKAGE, { add: aliasedImports });
|
|
67
129
|
}
|
|
68
130
|
|
|
69
131
|
if (changed) {
|
|
@@ -74,4 +136,4 @@ const transform = (sourceFile: ts.SourceFile, program: ts.Program): TransformRes
|
|
|
74
136
|
};
|
|
75
137
|
|
|
76
138
|
|
|
77
|
-
export { transform };
|
|
139
|
+
export { analyze, transform };
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { plugin } from '@esportsplus/typescript/compiler';
|
|
2
|
-
import { transform } from '..';
|
|
2
|
+
import { analyze, transform } from '..';
|
|
3
3
|
|
|
4
4
|
|
|
5
|
-
export default plugin.tsc(transform) as ReturnType<typeof plugin.tsc>;
|
|
5
|
+
export default plugin.tsc({ analyze, transform }) as ReturnType<typeof plugin.tsc>;
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { plugin } from '@esportsplus/typescript/compiler';
|
|
2
2
|
import { PACKAGE } from '../../constants';
|
|
3
|
-
import { transform } from '..';
|
|
3
|
+
import { analyze, transform } from '..';
|
|
4
4
|
|
|
5
5
|
|
|
6
6
|
export default plugin.vite({
|
|
7
|
+
analyze,
|
|
7
8
|
name: PACKAGE,
|
|
8
9
|
transform
|
|
9
10
|
});
|
|
@@ -50,10 +50,10 @@ function isHtmlFromPackage(node: ts.Identifier, checker: ts.TypeChecker | undefi
|
|
|
50
50
|
|
|
51
51
|
// Check if any declaration is from our package
|
|
52
52
|
for (let i = 0, n = declarations.length; i < n; i++) {
|
|
53
|
-
let
|
|
53
|
+
let filename = declarations[i].getSourceFile().fileName;
|
|
54
54
|
|
|
55
55
|
// Check for package in node_modules path or direct package reference
|
|
56
|
-
if (
|
|
56
|
+
if (filename.includes(PACKAGE) || filename.includes('@esportsplus/template')) {
|
|
57
57
|
return true;
|
|
58
58
|
}
|
|
59
59
|
}
|
package/test/vite.config.ts
CHANGED
|
@@ -19,7 +19,7 @@ export default defineConfig({
|
|
|
19
19
|
'templates': resolve(__dirname, 'templates.ts')
|
|
20
20
|
},
|
|
21
21
|
formats: ['es'],
|
|
22
|
-
|
|
22
|
+
filename: (_, entryName) => `${entryName}.js`
|
|
23
23
|
},
|
|
24
24
|
outDir: resolve(__dirname, 'build'),
|
|
25
25
|
emptyOutDir: true,
|
|
@@ -1,6 +0,0 @@
|
|
|
1
|
-
import { COMPILER_TYPES } from '../constants.js';
|
|
2
|
-
import { ts } from '@esportsplus/typescript';
|
|
3
|
-
declare const analyzeExpression: (expr: ts.Expression, checker?: ts.TypeChecker) => COMPILER_TYPES;
|
|
4
|
-
declare const generateAttributeBinding: (elementVar: string, name: string, expr: string, staticValue: string, neededImports: Set<string>) => string;
|
|
5
|
-
declare const generateSpreadBindings: (expr: ts.Expression, exprCode: string, elementVar: string, checker: ts.TypeChecker | undefined, neededImports: Set<string>) => string[];
|
|
6
|
-
export { analyzeExpression, generateAttributeBinding, generateSpreadBindings };
|