@esportsplus/template 0.35.1 → 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/compiler/analyzer.d.ts +4 -0
- package/build/compiler/{type-analyzer.js → analyzer.js} +15 -47
- package/build/compiler/codegen.js +28 -12
- package/build/compiler/index.d.ts +4 -2
- package/build/compiler/index.js +47 -15
- 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/compiler/analyzer.ts +92 -0
- package/src/compiler/codegen.ts +40 -19
- package/src/compiler/index.ts +73 -20
- 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/src/compiler/type-analyzer.ts +0 -148
|
@@ -1,6 +1,17 @@
|
|
|
1
|
-
import { COMPILER_ENTRYPOINT, COMPILER_ENTRYPOINT_REACTIVITY, COMPILER_TYPES
|
|
1
|
+
import { COMPILER_ENTRYPOINT, COMPILER_ENTRYPOINT_REACTIVITY, COMPILER_TYPES } from '../constants.js';
|
|
2
2
|
import { ts } from '@esportsplus/typescript';
|
|
3
|
-
function
|
|
3
|
+
function isTypeFunction(type, checker) {
|
|
4
|
+
if (type.isUnion()) {
|
|
5
|
+
for (let i = 0, n = type.types.length; i < n; i++) {
|
|
6
|
+
if (!isTypeFunction(type.types[i], checker)) {
|
|
7
|
+
return false;
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
return type.types.length > 0;
|
|
11
|
+
}
|
|
12
|
+
return type.getCallSignatures().length > 0;
|
|
13
|
+
}
|
|
14
|
+
const analyze = (expr, checker) => {
|
|
4
15
|
while (ts.isParenthesizedExpression(expr)) {
|
|
5
16
|
expr = expr.expression;
|
|
6
17
|
}
|
|
@@ -30,7 +41,7 @@ function inferCOMPILER_TYPES(expr, checker) {
|
|
|
30
41
|
return COMPILER_TYPES.Primitive;
|
|
31
42
|
}
|
|
32
43
|
if (ts.isConditionalExpression(expr)) {
|
|
33
|
-
let whenFalse =
|
|
44
|
+
let whenFalse = analyze(expr.whenFalse, checker), whenTrue = analyze(expr.whenTrue, checker);
|
|
34
45
|
if (whenTrue === whenFalse) {
|
|
35
46
|
return whenTrue;
|
|
36
47
|
}
|
|
@@ -50,48 +61,5 @@ function inferCOMPILER_TYPES(expr, checker) {
|
|
|
50
61
|
}
|
|
51
62
|
}
|
|
52
63
|
return COMPILER_TYPES.Unknown;
|
|
53
|
-
}
|
|
54
|
-
function isTypeFunction(type, checker) {
|
|
55
|
-
if (type.isUnion()) {
|
|
56
|
-
let allFunctions = true, hasFunction = false;
|
|
57
|
-
for (let i = 0, n = type.types.length; i < n; i++) {
|
|
58
|
-
if (isTypeFunction(type.types[i], checker)) {
|
|
59
|
-
hasFunction = true;
|
|
60
|
-
}
|
|
61
|
-
else {
|
|
62
|
-
allFunctions = false;
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
return hasFunction && allFunctions;
|
|
66
|
-
}
|
|
67
|
-
return type.getCallSignatures().length > 0;
|
|
68
|
-
}
|
|
69
|
-
const analyzeExpression = (expr, checker) => {
|
|
70
|
-
return inferCOMPILER_TYPES(expr, checker);
|
|
71
|
-
};
|
|
72
|
-
const generateAttributeBinding = (elementVar, name, expr, staticValue, addImport) => {
|
|
73
|
-
if (name.startsWith('on') && name.length > 2) {
|
|
74
|
-
let event = name.slice(2).toLowerCase(), key = name.toLowerCase();
|
|
75
|
-
if (LIFECYCLE_EVENTS.has(key)) {
|
|
76
|
-
return `${addImport(key)}(${elementVar}, ${expr});`;
|
|
77
|
-
}
|
|
78
|
-
if (DIRECT_ATTACH_EVENTS.has(key)) {
|
|
79
|
-
return `${addImport('on')}(${elementVar}, '${event}', ${expr});`;
|
|
80
|
-
}
|
|
81
|
-
return `${addImport('delegate')}(${elementVar}, '${event}', ${expr});`;
|
|
82
|
-
}
|
|
83
|
-
if (name === 'class') {
|
|
84
|
-
return `${addImport('setClass')}(${elementVar}, '${staticValue}', ${expr});`;
|
|
85
|
-
}
|
|
86
|
-
if (name === COMPILER_TYPES.Attributes) {
|
|
87
|
-
return `${addImport('setProperties')}(${elementVar}, ${expr});`;
|
|
88
|
-
}
|
|
89
|
-
if (name === 'style') {
|
|
90
|
-
return `${addImport('setStyle')}(${elementVar}, '${staticValue}', ${expr});`;
|
|
91
|
-
}
|
|
92
|
-
return `${addImport('setProperty')}(${elementVar}, '${name}', ${expr});`;
|
|
93
|
-
};
|
|
94
|
-
const generateSpreadBindings = (exprCode, elementVar, addImport) => {
|
|
95
|
-
return [`${addImport('setProperties')}(${elementVar}, ${exprCode});`];
|
|
96
64
|
};
|
|
97
|
-
export {
|
|
65
|
+
export { analyze };
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { ts } from '@esportsplus/typescript';
|
|
2
2
|
import { ast, code as c, imports, uid } from '@esportsplus/typescript/compiler';
|
|
3
|
-
import { COMPILER_ENTRYPOINT, COMPILER_TYPES, PACKAGE } from '../constants.js';
|
|
4
|
-
import {
|
|
3
|
+
import { COMPILER_ENTRYPOINT, COMPILER_TYPES, DIRECT_ATTACH_EVENTS, LIFECYCLE_EVENTS, PACKAGE } from '../constants.js';
|
|
4
|
+
import { analyze } from './analyzer.js';
|
|
5
5
|
import parser from './parser.js';
|
|
6
6
|
const ARROW_EMPTY_PARAMS = /\(\s*\)\s*=>\s*$/;
|
|
7
7
|
let printer = ts.createPrinter({ newLine: ts.NewLineKind.LineFeed });
|
|
@@ -13,6 +13,25 @@ function addImport(ctx, name) {
|
|
|
13
13
|
}
|
|
14
14
|
return alias;
|
|
15
15
|
}
|
|
16
|
+
function generateAttributeBinding(ctx, element, name, expr, staticValue) {
|
|
17
|
+
if (name.startsWith('on') && name.length > 2) {
|
|
18
|
+
let event = name.slice(2).toLowerCase(), key = name.toLowerCase();
|
|
19
|
+
if (LIFECYCLE_EVENTS.has(key)) {
|
|
20
|
+
return `${addImport(ctx, key)}(${element}, ${expr});`;
|
|
21
|
+
}
|
|
22
|
+
if (DIRECT_ATTACH_EVENTS.has(key)) {
|
|
23
|
+
return `${addImport(ctx, 'on')}(${element}, '${event}', ${expr});`;
|
|
24
|
+
}
|
|
25
|
+
return `${addImport(ctx, 'delegate')}(${element}, '${event}', ${expr});`;
|
|
26
|
+
}
|
|
27
|
+
if (name === 'class') {
|
|
28
|
+
return `${addImport(ctx, 'setClass')}(${element}, '${staticValue}', ${expr});`;
|
|
29
|
+
}
|
|
30
|
+
if (name === 'style') {
|
|
31
|
+
return `${addImport(ctx, 'setStyle')}(${element}, '${staticValue}', ${expr});`;
|
|
32
|
+
}
|
|
33
|
+
return `${addImport(ctx, 'setProperty')}(${element}, '${name}', ${expr});`;
|
|
34
|
+
}
|
|
16
35
|
function collectNestedTemplateReplacements(ctx, node, exprStart, replacements) {
|
|
17
36
|
if (isNestedHtmlTemplate(node)) {
|
|
18
37
|
replacements.push({
|
|
@@ -48,7 +67,7 @@ function generateNodeBinding(ctx, anchor, exprText, exprNode) {
|
|
|
48
67
|
if (isNestedHtmlTemplate(exprNode)) {
|
|
49
68
|
return `${anchor}.parentNode.insertBefore(${generateNestedTemplateCode(ctx, exprNode)}, ${anchor});`;
|
|
50
69
|
}
|
|
51
|
-
let slotType =
|
|
70
|
+
let slotType = analyze(exprNode, ctx.checker);
|
|
52
71
|
switch (slotType) {
|
|
53
72
|
case COMPILER_TYPES.Effect:
|
|
54
73
|
return `new ${addImport(ctx, 'EffectSlot')}(${anchor}, ${exprText});`;
|
|
@@ -104,14 +123,11 @@ function generateTemplateCode(ctx, { html, slots }, exprTexts, exprNodes, isArro
|
|
|
104
123
|
for (let j = 0, m = names.length; j < m; j++) {
|
|
105
124
|
let name = names[j];
|
|
106
125
|
if (name === COMPILER_TYPES.Attributes) {
|
|
107
|
-
|
|
108
|
-
for (let k = 0, o = bindings.length; k < o; k++) {
|
|
109
|
-
code.push(bindings[k]);
|
|
110
|
-
}
|
|
126
|
+
code.push(`${addImport(ctx, 'setProperties')}(${element}, ${exprTexts[index] || 'undefined'});`);
|
|
111
127
|
index++;
|
|
112
128
|
}
|
|
113
129
|
else {
|
|
114
|
-
code.push(generateAttributeBinding(element, name, exprTexts[index++] || 'undefined', slot.attributes.statics[name] || ''
|
|
130
|
+
code.push(generateAttributeBinding(ctx, element, name, exprTexts[index++] || 'undefined', slot.attributes.statics[name] || ''));
|
|
115
131
|
}
|
|
116
132
|
}
|
|
117
133
|
}
|
|
@@ -163,10 +179,10 @@ const generateCode = (templates, originalCode, sourceFile, checker, existingAlia
|
|
|
163
179
|
}
|
|
164
180
|
let ctx = {
|
|
165
181
|
checker,
|
|
166
|
-
templates: new Map(),
|
|
167
182
|
imports: existingAliases ?? new Map(),
|
|
168
183
|
printer,
|
|
169
|
-
sourceFile
|
|
184
|
+
sourceFile,
|
|
185
|
+
templates: new Map(),
|
|
170
186
|
}, replacements = [], templateAlias = addImport(ctx, 'template');
|
|
171
187
|
for (let i = 0, n = rootTemplates.length; i < n; i++) {
|
|
172
188
|
let exprTexts = [], template = rootTemplates[i];
|
|
@@ -193,14 +209,14 @@ const generateCode = (templates, originalCode, sourceFile, checker, existingAlia
|
|
|
193
209
|
}
|
|
194
210
|
let changed = replacements.length > 0, code = c.replaceReverse(originalCode, replacements);
|
|
195
211
|
if (changed && ctx.templates.size > 0) {
|
|
196
|
-
let aliasedImports = [], factories = [];
|
|
212
|
+
let aliasedImports = [], factories = [], updatedSourceFile = ts.createSourceFile(sourceFile.fileName, code, sourceFile.languageVersion, true);
|
|
197
213
|
for (let [name, alias] of ctx.imports) {
|
|
198
214
|
aliasedImports.push(`${name} as ${alias}`);
|
|
199
215
|
}
|
|
200
216
|
for (let [html, id] of ctx.templates) {
|
|
201
217
|
factories.push(`const ${id} = ${templateAlias}(\`${html}\`);`);
|
|
202
218
|
}
|
|
203
|
-
code = imports.modify(code,
|
|
219
|
+
code = imports.modify(code, updatedSourceFile, PACKAGE, {
|
|
204
220
|
add: new Set(aliasedImports),
|
|
205
221
|
remove: [COMPILER_ENTRYPOINT]
|
|
206
222
|
});
|
|
@@ -1,8 +1,10 @@
|
|
|
1
|
+
import type { PluginContext } from '@esportsplus/typescript/compiler';
|
|
1
2
|
import { ts } from '@esportsplus/typescript';
|
|
2
3
|
type TransformResult = {
|
|
3
4
|
changed: boolean;
|
|
4
5
|
code: string;
|
|
5
6
|
sourceFile: ts.SourceFile;
|
|
6
7
|
};
|
|
7
|
-
declare const
|
|
8
|
-
|
|
8
|
+
declare const analyze: (sourceFile: ts.SourceFile, program: ts.Program, context: PluginContext) => void;
|
|
9
|
+
declare const transform: (sourceFile: ts.SourceFile, program: ts.Program, context?: PluginContext) => TransformResult;
|
|
10
|
+
export { analyze, transform };
|
package/build/compiler/index.js
CHANGED
|
@@ -3,32 +3,64 @@ import { code as c, imports, uid } from '@esportsplus/typescript/compiler';
|
|
|
3
3
|
import { COMPILER_ENTRYPOINT, COMPILER_ENTRYPOINT_REACTIVITY, PACKAGE } from '../constants.js';
|
|
4
4
|
import { generateCode, generateReactiveInlining } from './codegen.js';
|
|
5
5
|
import { findHtmlTemplates, findReactiveCalls } from './ts-parser.js';
|
|
6
|
+
const CONTEXT_KEY = 'template:analyzed';
|
|
6
7
|
const PATTERNS = [`${COMPILER_ENTRYPOINT}\``, `${COMPILER_ENTRYPOINT}.${COMPILER_ENTRYPOINT_REACTIVITY}`];
|
|
7
8
|
const REGEX_BACKSLASH = /\\/g;
|
|
8
9
|
const REGEX_FORWARD_SLASH = /\//g;
|
|
9
|
-
|
|
10
|
+
function getAnalyzedFile(context, filename) {
|
|
11
|
+
return context?.get(CONTEXT_KEY)?.get(filename);
|
|
12
|
+
}
|
|
13
|
+
const analyze = (sourceFile, program, context) => {
|
|
10
14
|
let code = sourceFile.getFullText();
|
|
11
15
|
if (!c.contains(code, { patterns: PATTERNS })) {
|
|
12
|
-
return
|
|
16
|
+
return;
|
|
13
17
|
}
|
|
14
|
-
let checker,
|
|
15
|
-
|| program.getSourceFile(
|
|
16
|
-
|| program.getSourceFile(
|
|
18
|
+
let checker = program.getTypeChecker(), filename = sourceFile.fileName, files = context.get(CONTEXT_KEY), programSourceFile = program.getSourceFile(filename)
|
|
19
|
+
|| program.getSourceFile(filename.replace(REGEX_BACKSLASH, '/'))
|
|
20
|
+
|| program.getSourceFile(filename.replace(REGEX_FORWARD_SLASH, '\\'));
|
|
17
21
|
if (programSourceFile) {
|
|
18
|
-
checker = program.getTypeChecker();
|
|
19
22
|
sourceFile = programSourceFile;
|
|
20
23
|
}
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
+
if (!files) {
|
|
25
|
+
files = new Map();
|
|
26
|
+
context.set(CONTEXT_KEY, files);
|
|
27
|
+
}
|
|
28
|
+
files.set(filename, {
|
|
29
|
+
reactiveCalls: findReactiveCalls(sourceFile, checker),
|
|
30
|
+
templates: findHtmlTemplates(sourceFile, checker)
|
|
31
|
+
});
|
|
32
|
+
};
|
|
33
|
+
const transform = (sourceFile, program, context) => {
|
|
34
|
+
let code = sourceFile.getFullText(), filename = sourceFile.fileName;
|
|
35
|
+
let analyzed = getAnalyzedFile(context, filename);
|
|
36
|
+
if (!analyzed) {
|
|
37
|
+
if (!c.contains(code, { patterns: PATTERNS })) {
|
|
38
|
+
return { changed: false, code, sourceFile };
|
|
39
|
+
}
|
|
40
|
+
let checker = program.getTypeChecker(), programSourceFile = program.getSourceFile(filename)
|
|
41
|
+
|| program.getSourceFile(filename.replace(REGEX_BACKSLASH, '/'))
|
|
42
|
+
|| program.getSourceFile(filename.replace(REGEX_FORWARD_SLASH, '\\'));
|
|
43
|
+
if (programSourceFile) {
|
|
44
|
+
sourceFile = programSourceFile;
|
|
45
|
+
}
|
|
46
|
+
analyzed = {
|
|
47
|
+
reactiveCalls: findReactiveCalls(sourceFile, checker),
|
|
48
|
+
templates: findHtmlTemplates(sourceFile, checker)
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
let changed = false, codegenChanged = false, existingAliases = new Map(), result = code;
|
|
52
|
+
if (analyzed.reactiveCalls.length > 0) {
|
|
24
53
|
changed = true;
|
|
25
|
-
existingAliases.set('ArraySlot',
|
|
26
|
-
result = generateReactiveInlining(reactiveCalls, result, sourceFile,
|
|
54
|
+
existingAliases.set('ArraySlot', uid('ArraySlot'));
|
|
55
|
+
result = generateReactiveInlining(analyzed.reactiveCalls, result, sourceFile, existingAliases.get('ArraySlot'));
|
|
27
56
|
sourceFile = ts.createSourceFile(sourceFile.fileName, result, sourceFile.languageVersion, true);
|
|
57
|
+
analyzed = {
|
|
58
|
+
reactiveCalls: [],
|
|
59
|
+
templates: findHtmlTemplates(sourceFile, program.getTypeChecker())
|
|
60
|
+
};
|
|
28
61
|
}
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
let codegenResult = generateCode(templates, result, sourceFile, checker, existingAliases);
|
|
62
|
+
if (analyzed.templates.length > 0) {
|
|
63
|
+
let codegenResult = generateCode(analyzed.templates, result, sourceFile, program.getTypeChecker(), existingAliases);
|
|
32
64
|
if (codegenResult.changed) {
|
|
33
65
|
changed = true;
|
|
34
66
|
codegenChanged = true;
|
|
@@ -47,4 +79,4 @@ const transform = (sourceFile, program) => {
|
|
|
47
79
|
}
|
|
48
80
|
return { changed, code: result, sourceFile };
|
|
49
81
|
};
|
|
50
|
-
export { transform };
|
|
82
|
+
export { analyze, transform };
|
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
import { plugin } from '@esportsplus/typescript/compiler';
|
|
2
|
-
import { transform } from '../index.js';
|
|
3
|
-
export default plugin.tsc(transform);
|
|
2
|
+
import { analyze, transform } from '../index.js';
|
|
3
|
+
export default plugin.tsc({ analyze, transform });
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
declare const _default: ({ root }?: {
|
|
2
2
|
root?: string;
|
|
3
3
|
}) => {
|
|
4
|
-
configResolved(config:
|
|
5
|
-
enforce:
|
|
4
|
+
configResolved: (config: unknown) => void;
|
|
5
|
+
enforce: "pre";
|
|
6
6
|
name: string;
|
|
7
|
-
transform(code: string, id: string)
|
|
7
|
+
transform: (code: string, id: string) => {
|
|
8
8
|
code: string;
|
|
9
9
|
map: null;
|
|
10
10
|
} | null;
|
|
11
|
-
watchChange(id: string)
|
|
11
|
+
watchChange: (id: string) => void;
|
|
12
12
|
};
|
|
13
13
|
export default _default;
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { plugin } from '@esportsplus/typescript/compiler';
|
|
2
2
|
import { PACKAGE } from '../../constants.js';
|
|
3
|
-
import { transform } from '../index.js';
|
|
3
|
+
import { analyze, transform } from '../index.js';
|
|
4
4
|
export default plugin.vite({
|
|
5
|
+
analyze,
|
|
5
6
|
name: PACKAGE,
|
|
6
7
|
transform
|
|
7
8
|
});
|
|
@@ -19,8 +19,8 @@ function isHtmlFromPackage(node, checker) {
|
|
|
19
19
|
return true;
|
|
20
20
|
}
|
|
21
21
|
for (let i = 0, n = declarations.length; i < n; i++) {
|
|
22
|
-
let
|
|
23
|
-
if (
|
|
22
|
+
let filename = declarations[i].getSourceFile().fileName;
|
|
23
|
+
if (filename.includes(PACKAGE) || filename.includes('@esportsplus/template')) {
|
|
24
24
|
return true;
|
|
25
25
|
}
|
|
26
26
|
}
|
package/package.json
CHANGED
|
@@ -2,14 +2,14 @@
|
|
|
2
2
|
"author": "ICJR",
|
|
3
3
|
"dependencies": {
|
|
4
4
|
"@esportsplus/queue": "^0.2.0",
|
|
5
|
-
"@esportsplus/reactivity": "^0.27.
|
|
5
|
+
"@esportsplus/reactivity": "^0.27.3",
|
|
6
|
+
"@esportsplus/typescript": "^0.24.2",
|
|
6
7
|
"@esportsplus/utilities": "^0.27.2",
|
|
7
8
|
"serve": "^14.2.5"
|
|
8
9
|
},
|
|
9
10
|
"devDependencies": {
|
|
10
|
-
"@esportsplus/typescript": "^0.22.0",
|
|
11
11
|
"@types/node": "^25.0.3",
|
|
12
|
-
"vite": "^7.3.
|
|
12
|
+
"vite": "^7.3.1",
|
|
13
13
|
"vite-tsconfig-paths": "^6.0.3"
|
|
14
14
|
},
|
|
15
15
|
"exports": {
|
|
@@ -35,7 +35,7 @@
|
|
|
35
35
|
},
|
|
36
36
|
"type": "module",
|
|
37
37
|
"types": "./build/index.d.ts",
|
|
38
|
-
"version": "0.
|
|
38
|
+
"version": "0.37.0",
|
|
39
39
|
"scripts": {
|
|
40
40
|
"build": "tsc",
|
|
41
41
|
"build:test": "vite build --config test/vite.config.ts",
|
|
@@ -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
|
|
|
@@ -56,6 +56,33 @@ function addImport(ctx: CodegenContext, name: string): string {
|
|
|
56
56
|
return alias;
|
|
57
57
|
}
|
|
58
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
|
+
|
|
59
86
|
function collectNestedTemplateReplacements(
|
|
60
87
|
ctx: CodegenContext,
|
|
61
88
|
node: ts.Node,
|
|
@@ -113,7 +140,7 @@ function generateNodeBinding(ctx: CodegenContext, anchor: string, exprText: stri
|
|
|
113
140
|
return `${anchor}.parentNode.insertBefore(${generateNestedTemplateCode(ctx, exprNode)}, ${anchor});`;
|
|
114
141
|
}
|
|
115
142
|
|
|
116
|
-
let slotType =
|
|
143
|
+
let slotType = analyze(exprNode, ctx.checker);
|
|
117
144
|
|
|
118
145
|
switch (slotType) {
|
|
119
146
|
case COMPILER_TYPES.Effect:
|
|
@@ -211,26 +238,19 @@ function generateTemplateCode(
|
|
|
211
238
|
let name = names[j];
|
|
212
239
|
|
|
213
240
|
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
|
-
|
|
241
|
+
code.push(
|
|
242
|
+
`${addImport(ctx, 'setProperties')}(${element}, ${exprTexts[index] || 'undefined'});`
|
|
243
|
+
);
|
|
224
244
|
index++;
|
|
225
245
|
}
|
|
226
246
|
else {
|
|
227
247
|
code.push(
|
|
228
248
|
generateAttributeBinding(
|
|
249
|
+
ctx,
|
|
229
250
|
element,
|
|
230
251
|
name,
|
|
231
252
|
exprTexts[index++] || 'undefined',
|
|
232
|
-
slot.attributes.statics[name] || ''
|
|
233
|
-
n => addImport(ctx, n)
|
|
253
|
+
slot.attributes.statics[name] || ''
|
|
234
254
|
)
|
|
235
255
|
);
|
|
236
256
|
}
|
|
@@ -306,10 +326,10 @@ const generateCode = (templates: TemplateInfo[], originalCode: string, sourceFil
|
|
|
306
326
|
|
|
307
327
|
let ctx: CodegenContext = {
|
|
308
328
|
checker,
|
|
309
|
-
templates: new Map(),
|
|
310
329
|
imports: existingAliases ?? new Map(),
|
|
311
330
|
printer,
|
|
312
|
-
sourceFile
|
|
331
|
+
sourceFile,
|
|
332
|
+
templates: new Map(),
|
|
313
333
|
},
|
|
314
334
|
replacements: Replacement[] = [],
|
|
315
335
|
templateAlias = addImport(ctx, 'template');
|
|
@@ -358,7 +378,8 @@ const generateCode = (templates: TemplateInfo[], originalCode: string, sourceFil
|
|
|
358
378
|
|
|
359
379
|
if (changed && ctx.templates.size > 0) {
|
|
360
380
|
let aliasedImports: string[] = [],
|
|
361
|
-
factories: string[] = []
|
|
381
|
+
factories: string[] = [],
|
|
382
|
+
updatedSourceFile = ts.createSourceFile(sourceFile.fileName, code, sourceFile.languageVersion, true);
|
|
362
383
|
|
|
363
384
|
for (let [name, alias] of ctx.imports) {
|
|
364
385
|
aliasedImports.push(`${name} as ${alias}`);
|
|
@@ -369,7 +390,7 @@ const generateCode = (templates: TemplateInfo[], originalCode: string, sourceFil
|
|
|
369
390
|
}
|
|
370
391
|
|
|
371
392
|
// Remove html entrypoint and add aliased imports
|
|
372
|
-
code = imports.modify(code,
|
|
393
|
+
code = imports.modify(code, updatedSourceFile, PACKAGE, {
|
|
373
394
|
add: new Set(aliasedImports),
|
|
374
395
|
remove: [COMPILER_ENTRYPOINT]
|
|
375
396
|
});
|
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
3
|
import { code as c, imports, uid } from '@esportsplus/typescript/compiler';
|
|
3
4
|
import { COMPILER_ENTRYPOINT, COMPILER_ENTRYPOINT_REACTIVITY, PACKAGE } from '~/constants';
|
|
4
5
|
import { generateCode, generateReactiveInlining } from './codegen';
|
|
5
|
-
import { findHtmlTemplates, findReactiveCalls } from './ts-parser';
|
|
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,43 +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
94
|
existingAliases = new Map<string, string>(),
|
|
43
|
-
reactiveCalls = findReactiveCalls(sourceFile, checker),
|
|
44
95
|
result = code;
|
|
45
96
|
|
|
46
|
-
if (reactiveCalls.length > 0) {
|
|
47
|
-
let arraySlotAlias = uid('ArraySlot');
|
|
48
|
-
|
|
97
|
+
if (analyzed.reactiveCalls.length > 0) {
|
|
49
98
|
changed = true;
|
|
50
|
-
existingAliases.set('ArraySlot',
|
|
51
|
-
result = generateReactiveInlining(reactiveCalls, result, sourceFile,
|
|
99
|
+
existingAliases.set('ArraySlot', uid('ArraySlot'));
|
|
100
|
+
result = generateReactiveInlining(analyzed.reactiveCalls, result, sourceFile, existingAliases.get('ArraySlot')!);
|
|
52
101
|
sourceFile = ts.createSourceFile(sourceFile.fileName, result, sourceFile.languageVersion, true);
|
|
53
|
-
}
|
|
54
102
|
|
|
55
|
-
|
|
103
|
+
// Re-analyze templates after reactive inlining modifies the code
|
|
104
|
+
analyzed = {
|
|
105
|
+
reactiveCalls: [],
|
|
106
|
+
templates: findHtmlTemplates(sourceFile, program.getTypeChecker())
|
|
107
|
+
};
|
|
108
|
+
}
|
|
56
109
|
|
|
57
|
-
if (templates.length > 0) {
|
|
58
|
-
let codegenResult = generateCode(templates, result, sourceFile,
|
|
110
|
+
if (analyzed.templates.length > 0) {
|
|
111
|
+
let codegenResult = generateCode(analyzed.templates, result, sourceFile, program.getTypeChecker(), existingAliases);
|
|
59
112
|
|
|
60
113
|
if (codegenResult.changed) {
|
|
61
114
|
changed = true;
|
|
@@ -83,4 +136,4 @@ const transform = (sourceFile: ts.SourceFile, program: ts.Program): TransformRes
|
|
|
83
136
|
};
|
|
84
137
|
|
|
85
138
|
|
|
86
|
-
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, addImport: (name: string) => string) => string;
|
|
5
|
-
declare const generateSpreadBindings: (exprCode: string, elementVar: string, addImport: (name: string) => string) => string[];
|
|
6
|
-
export { analyzeExpression, generateAttributeBinding, generateSpreadBindings };
|
|
@@ -1,148 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
COMPILER_ENTRYPOINT,
|
|
3
|
-
COMPILER_ENTRYPOINT_REACTIVITY,
|
|
4
|
-
COMPILER_TYPES,
|
|
5
|
-
DIRECT_ATTACH_EVENTS,
|
|
6
|
-
LIFECYCLE_EVENTS
|
|
7
|
-
} from '~/constants';
|
|
8
|
-
import { ts } from '@esportsplus/typescript';
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
function inferCOMPILER_TYPES(expr: ts.Expression, checker?: ts.TypeChecker): COMPILER_TYPES {
|
|
12
|
-
while (ts.isParenthesizedExpression(expr)) {
|
|
13
|
-
expr = expr.expression;
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
if (ts.isArrowFunction(expr) || ts.isFunctionExpression(expr)) {
|
|
17
|
-
return COMPILER_TYPES.Effect;
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
// Only html.reactive() calls become ArraySlot - handled by generateReactiveInlining
|
|
21
|
-
if (
|
|
22
|
-
ts.isCallExpression(expr) &&
|
|
23
|
-
ts.isPropertyAccessExpression(expr.expression) &&
|
|
24
|
-
ts.isIdentifier(expr.expression.expression) &&
|
|
25
|
-
expr.expression.expression.text === COMPILER_ENTRYPOINT &&
|
|
26
|
-
expr.expression.name.text === COMPILER_ENTRYPOINT_REACTIVITY
|
|
27
|
-
) {
|
|
28
|
-
return COMPILER_TYPES.ArraySlot;
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
if (ts.isTaggedTemplateExpression(expr) && ts.isIdentifier(expr.tag) && expr.tag.text === COMPILER_ENTRYPOINT) {
|
|
32
|
-
return COMPILER_TYPES.DocumentFragment;
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
if (
|
|
36
|
-
ts.isNumericLiteral(expr) ||
|
|
37
|
-
ts.isStringLiteral(expr) ||
|
|
38
|
-
ts.isNoSubstitutionTemplateLiteral(expr) ||
|
|
39
|
-
expr.kind === ts.SyntaxKind.TrueKeyword ||
|
|
40
|
-
expr.kind === ts.SyntaxKind.FalseKeyword ||
|
|
41
|
-
expr.kind === ts.SyntaxKind.NullKeyword ||
|
|
42
|
-
expr.kind === ts.SyntaxKind.UndefinedKeyword
|
|
43
|
-
) {
|
|
44
|
-
return COMPILER_TYPES.Static;
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
if (ts.isTemplateExpression(expr)) {
|
|
48
|
-
return COMPILER_TYPES.Primitive;
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
if (ts.isConditionalExpression(expr)) {
|
|
52
|
-
let whenFalse = inferCOMPILER_TYPES(expr.whenFalse, checker),
|
|
53
|
-
whenTrue = inferCOMPILER_TYPES(expr.whenTrue, checker);
|
|
54
|
-
|
|
55
|
-
if (whenTrue === whenFalse) {
|
|
56
|
-
return whenTrue;
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
if (whenTrue === COMPILER_TYPES.Effect || whenFalse === COMPILER_TYPES.Effect) {
|
|
60
|
-
return COMPILER_TYPES.Effect;
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
return COMPILER_TYPES.Unknown;
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
if (checker && (ts.isIdentifier(expr) || ts.isPropertyAccessExpression(expr) || ts.isCallExpression(expr))) {
|
|
67
|
-
try {
|
|
68
|
-
let type = checker.getTypeAtLocation(expr);
|
|
69
|
-
|
|
70
|
-
if (isTypeFunction(type, checker)) {
|
|
71
|
-
return COMPILER_TYPES.Effect;
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
catch {
|
|
75
|
-
}
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
return COMPILER_TYPES.Unknown;
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
function isTypeFunction(type: ts.Type, checker: ts.TypeChecker): boolean {
|
|
82
|
-
// Union types that mix functions with non-functions (e.g., Renderable)
|
|
83
|
-
// should fall through to runtime slot dispatch
|
|
84
|
-
if (type.isUnion()) {
|
|
85
|
-
let allFunctions = true,
|
|
86
|
-
hasFunction = false;
|
|
87
|
-
|
|
88
|
-
for (let i = 0, n = type.types.length; i < n; i++) {
|
|
89
|
-
if (isTypeFunction(type.types[i], checker)) {
|
|
90
|
-
hasFunction = true;
|
|
91
|
-
}
|
|
92
|
-
else {
|
|
93
|
-
allFunctions = false;
|
|
94
|
-
}
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
return hasFunction && allFunctions;
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
return type.getCallSignatures().length > 0;
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
const analyzeExpression = (expr: ts.Expression, checker?: ts.TypeChecker): COMPILER_TYPES => {
|
|
105
|
-
return inferCOMPILER_TYPES(expr, checker);
|
|
106
|
-
};
|
|
107
|
-
|
|
108
|
-
const generateAttributeBinding = (elementVar: string, name: string, expr: string, staticValue: string, addImport: (name: string) => string): string => {
|
|
109
|
-
if (name.startsWith('on') && name.length > 2) {
|
|
110
|
-
let event = name.slice(2).toLowerCase(),
|
|
111
|
-
key = name.toLowerCase();
|
|
112
|
-
|
|
113
|
-
if (LIFECYCLE_EVENTS.has(key)) {
|
|
114
|
-
return `${addImport(key)}(${elementVar}, ${expr});`;
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
if (DIRECT_ATTACH_EVENTS.has(key)) {
|
|
118
|
-
return `${addImport('on')}(${elementVar}, '${event}', ${expr});`;
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
return `${addImport('delegate')}(${elementVar}, '${event}', ${expr});`;
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
if (name === 'class') {
|
|
125
|
-
return `${addImport('setClass')}(${elementVar}, '${staticValue}', ${expr});`;
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
if (name === COMPILER_TYPES.Attributes) {
|
|
129
|
-
return `${addImport('setProperties')}(${elementVar}, ${expr});`;
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
if (name === 'style') {
|
|
133
|
-
return `${addImport('setStyle')}(${elementVar}, '${staticValue}', ${expr});`;
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
return `${addImport('setProperty')}(${elementVar}, '${name}', ${expr});`;
|
|
137
|
-
};
|
|
138
|
-
|
|
139
|
-
const generateSpreadBindings = (
|
|
140
|
-
exprCode: string,
|
|
141
|
-
elementVar: string,
|
|
142
|
-
addImport: (name: string) => string
|
|
143
|
-
): string[] => {
|
|
144
|
-
return [`${addImport('setProperties')}(${elementVar}, ${exprCode});`];
|
|
145
|
-
};
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
export { analyzeExpression, generateAttributeBinding, generateSpreadBindings };
|