@esportsplus/template 0.38.0 → 0.38.2
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/codegen.d.ts +1 -2
- package/build/compiler/codegen.js +92 -39
- package/build/compiler/index.d.ts +2 -4
- package/build/compiler/index.js +51 -27
- package/build/compiler/plugins/tsc.d.ts +2 -3
- package/build/compiler/plugins/tsc.js +2 -3
- package/build/compiler/plugins/vite.js +2 -2
- package/build/constants.d.ts +2 -1
- package/build/constants.js +3 -1
- package/package.json +2 -2
- package/src/compiler/codegen.ts +130 -52
- package/src/compiler/index.ts +67 -29
- package/src/compiler/plugins/tsc.ts +2 -3
- package/src/compiler/plugins/vite.ts +3 -3
- package/src/constants.ts +6 -1
- package/test/integration/combined.ts +90 -0
- package/test/integration/tsconfig.json +17 -0
- package/build/compiler/reactive-inlining.d.ts +0 -5
- package/build/compiler/reactive-inlining.js +0 -75
- package/src/compiler/reactive-inlining.ts +0 -116
|
@@ -2,11 +2,10 @@ import type { ReplacementIntent } from '@esportsplus/typescript/compiler';
|
|
|
2
2
|
import { ts } from '@esportsplus/typescript';
|
|
3
3
|
import type { TemplateInfo } from './ts-parser.js';
|
|
4
4
|
type CodegenResult = {
|
|
5
|
-
imports: Map<string, string>;
|
|
6
5
|
prepend: string[];
|
|
7
6
|
replacements: ReplacementIntent[];
|
|
8
7
|
templates: Map<string, string>;
|
|
9
8
|
};
|
|
10
|
-
declare const generateCode: (templates: TemplateInfo[], sourceFile: ts.SourceFile, checker?: ts.TypeChecker
|
|
9
|
+
declare const generateCode: (templates: TemplateInfo[], sourceFile: ts.SourceFile, checker?: ts.TypeChecker) => CodegenResult;
|
|
11
10
|
export { generateCode };
|
|
12
11
|
export type { CodegenResult };
|
|
@@ -1,19 +1,11 @@
|
|
|
1
1
|
import { ts } from '@esportsplus/typescript';
|
|
2
2
|
import { ast, uid } from '@esportsplus/typescript/compiler';
|
|
3
|
-
import { COMPILER_ENTRYPOINT, COMPILER_TYPES, DIRECT_ATTACH_EVENTS, LIFECYCLE_EVENTS } from '../constants.js';
|
|
3
|
+
import { COMPILER_ENTRYPOINT, COMPILER_ENTRYPOINT_REACTIVITY, COMPILER_NAMESPACE, COMPILER_TYPES, DIRECT_ATTACH_EVENTS, LIFECYCLE_EVENTS } from '../constants.js';
|
|
4
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 });
|
|
8
|
-
function
|
|
9
|
-
let alias = ctx.imports.get(name);
|
|
10
|
-
if (!alias) {
|
|
11
|
-
alias = uid(name);
|
|
12
|
-
ctx.imports.set(name, alias);
|
|
13
|
-
}
|
|
14
|
-
return alias;
|
|
15
|
-
}
|
|
16
|
-
function collectNestedTemplateReplacements(ctx, node, exprStart, replacements) {
|
|
8
|
+
function collectNestedReplacements(ctx, node, exprStart, replacements) {
|
|
17
9
|
if (isNestedHtmlTemplate(node)) {
|
|
18
10
|
replacements.push({
|
|
19
11
|
end: node.end - exprStart,
|
|
@@ -21,28 +13,35 @@ function collectNestedTemplateReplacements(ctx, node, exprStart, replacements) {
|
|
|
21
13
|
start: node.getStart(ctx.sourceFile) - exprStart
|
|
22
14
|
});
|
|
23
15
|
}
|
|
16
|
+
else if (isReactiveCall(node)) {
|
|
17
|
+
replacements.push({
|
|
18
|
+
end: node.end - exprStart,
|
|
19
|
+
newText: rewriteNestedReactiveCall(ctx, node),
|
|
20
|
+
start: node.getStart(ctx.sourceFile) - exprStart
|
|
21
|
+
});
|
|
22
|
+
}
|
|
24
23
|
else {
|
|
25
|
-
ts.forEachChild(node, child =>
|
|
24
|
+
ts.forEachChild(node, child => collectNestedReplacements(ctx, child, exprStart, replacements));
|
|
26
25
|
}
|
|
27
26
|
}
|
|
28
|
-
function generateAttributeBinding(
|
|
27
|
+
function generateAttributeBinding(element, name, expr, staticValue) {
|
|
29
28
|
if (name.startsWith('on') && name.length > 2) {
|
|
30
29
|
let event = name.slice(2).toLowerCase(), key = name.toLowerCase();
|
|
31
30
|
if (LIFECYCLE_EVENTS.has(key)) {
|
|
32
|
-
return `${
|
|
31
|
+
return `${COMPILER_NAMESPACE}.${key}(${element}, ${expr});`;
|
|
33
32
|
}
|
|
34
33
|
if (DIRECT_ATTACH_EVENTS.has(key)) {
|
|
35
|
-
return `${
|
|
34
|
+
return `${COMPILER_NAMESPACE}.on(${element}, '${event}', ${expr});`;
|
|
36
35
|
}
|
|
37
|
-
return `${
|
|
36
|
+
return `${COMPILER_NAMESPACE}.delegate(${element}, '${event}', ${expr});`;
|
|
38
37
|
}
|
|
39
38
|
if (name === 'class') {
|
|
40
|
-
return `${
|
|
39
|
+
return `${COMPILER_NAMESPACE}.setClass(${element}, '${staticValue}', ${expr});`;
|
|
41
40
|
}
|
|
42
41
|
if (name === 'style') {
|
|
43
|
-
return `${
|
|
42
|
+
return `${COMPILER_NAMESPACE}.setStyle(${element}, '${staticValue}', ${expr});`;
|
|
44
43
|
}
|
|
45
|
-
return `${
|
|
44
|
+
return `${COMPILER_NAMESPACE}.setProperty(${element}, '${name}', ${expr});`;
|
|
46
45
|
}
|
|
47
46
|
function generateNestedTemplateCode(ctx, node) {
|
|
48
47
|
let expressions = [], exprTexts = [], literals = [], template = node.template;
|
|
@@ -62,23 +61,23 @@ function generateNestedTemplateCode(ctx, node) {
|
|
|
62
61
|
}
|
|
63
62
|
function generateNodeBinding(ctx, anchor, exprText, exprNode) {
|
|
64
63
|
if (!exprNode) {
|
|
65
|
-
return `${
|
|
64
|
+
return `${COMPILER_NAMESPACE}.slot(${anchor}, ${exprText});`;
|
|
66
65
|
}
|
|
67
66
|
if (isNestedHtmlTemplate(exprNode)) {
|
|
68
|
-
return `${anchor}.parentNode
|
|
67
|
+
return `${anchor}.parentNode!.insertBefore(${generateNestedTemplateCode(ctx, exprNode)}, ${anchor});`;
|
|
69
68
|
}
|
|
70
69
|
let slotType = analyze(exprNode, ctx.checker);
|
|
71
70
|
switch (slotType) {
|
|
72
|
-
case COMPILER_TYPES.Effect:
|
|
73
|
-
return `new ${addImport(ctx, 'EffectSlot')}(${anchor}, ${exprText});`;
|
|
74
71
|
case COMPILER_TYPES.ArraySlot:
|
|
75
|
-
return
|
|
72
|
+
return `${anchor}.parentNode!.insertBefore(new ${COMPILER_NAMESPACE}.ArraySlot(${exprText}).fragment, ${anchor});`;
|
|
73
|
+
case COMPILER_TYPES.DocumentFragment:
|
|
74
|
+
return `${anchor}.parentNode!.insertBefore(${exprText}, ${anchor});`;
|
|
75
|
+
case COMPILER_TYPES.Effect:
|
|
76
|
+
return `new ${COMPILER_NAMESPACE}.EffectSlot(${anchor}, ${exprText});`;
|
|
76
77
|
case COMPILER_TYPES.Static:
|
|
77
78
|
return `${anchor}.textContent = ${exprText};`;
|
|
78
|
-
case COMPILER_TYPES.DocumentFragment:
|
|
79
|
-
return `${anchor}.parentNode.insertBefore(${exprText}, ${anchor});`;
|
|
80
79
|
default:
|
|
81
|
-
return `${
|
|
80
|
+
return `${COMPILER_NAMESPACE}.slot(${anchor}, ${exprText});`;
|
|
82
81
|
}
|
|
83
82
|
}
|
|
84
83
|
function generateTemplateCode(ctx, { html, slots }, exprTexts, exprNodes, isArrowBody) {
|
|
@@ -106,11 +105,11 @@ function generateTemplateCode(ctx, { html, slots }, exprTexts, exprNodes, isArro
|
|
|
106
105
|
break;
|
|
107
106
|
}
|
|
108
107
|
}
|
|
109
|
-
let
|
|
108
|
+
let name = uid('element'), segments = path.slice(start), value = `${ancestor}.${segments.join('!.')}`;
|
|
110
109
|
if (ancestor === root && segments[0] === 'firstChild') {
|
|
111
|
-
value = value.replace(`${ancestor}.firstChild!`, `(${ancestor}.firstChild! as ${
|
|
110
|
+
value = value.replace(`${ancestor}.firstChild!`, `(${ancestor}.firstChild! as ${COMPILER_NAMESPACE}.Element)`);
|
|
112
111
|
}
|
|
113
|
-
declarations.push(`${name} = ${value} as ${
|
|
112
|
+
declarations.push(`${name} = ${value} as ${COMPILER_NAMESPACE}.Element`);
|
|
114
113
|
nodes.set(key, name);
|
|
115
114
|
}
|
|
116
115
|
code.push(isArrowBody ? '{' : `(() => {`, `let ${declarations.join(',\n')};`);
|
|
@@ -123,11 +122,11 @@ function generateTemplateCode(ctx, { html, slots }, exprTexts, exprNodes, isArro
|
|
|
123
122
|
for (let j = 0, m = names.length; j < m; j++) {
|
|
124
123
|
let name = names[j];
|
|
125
124
|
if (name === COMPILER_TYPES.Attributes) {
|
|
126
|
-
code.push(`${
|
|
125
|
+
code.push(`${COMPILER_NAMESPACE}.setProperties(${element}, ${exprTexts[index] || 'undefined'});`);
|
|
127
126
|
index++;
|
|
128
127
|
}
|
|
129
128
|
else {
|
|
130
|
-
code.push(generateAttributeBinding(
|
|
129
|
+
code.push(generateAttributeBinding(element, name, exprTexts[index++] || 'undefined', slot.attributes.statics[name] || ''));
|
|
131
130
|
}
|
|
132
131
|
}
|
|
133
132
|
}
|
|
@@ -151,6 +150,13 @@ function getOrCreateTemplateId(ctx, html) {
|
|
|
151
150
|
function isNestedHtmlTemplate(expr) {
|
|
152
151
|
return ts.isTaggedTemplateExpression(expr) && ts.isIdentifier(expr.tag) && expr.tag.text === COMPILER_ENTRYPOINT;
|
|
153
152
|
}
|
|
153
|
+
function isReactiveCall(expr) {
|
|
154
|
+
return (ts.isCallExpression(expr) &&
|
|
155
|
+
ts.isPropertyAccessExpression(expr.expression) &&
|
|
156
|
+
ts.isIdentifier(expr.expression.expression) &&
|
|
157
|
+
expr.expression.expression.text === COMPILER_ENTRYPOINT &&
|
|
158
|
+
expr.expression.name.text === COMPILER_ENTRYPOINT_REACTIVITY);
|
|
159
|
+
}
|
|
154
160
|
function replaceReverse(text, replacements) {
|
|
155
161
|
let sorted = replacements.slice().sort((a, b) => b.start - a.start);
|
|
156
162
|
for (let i = 0, n = sorted.length; i < n; i++) {
|
|
@@ -163,16 +169,64 @@ function rewriteExpression(ctx, expr) {
|
|
|
163
169
|
if (isNestedHtmlTemplate(expr)) {
|
|
164
170
|
return generateNestedTemplateCode(ctx, expr);
|
|
165
171
|
}
|
|
166
|
-
if (
|
|
172
|
+
if (isReactiveCall(expr)) {
|
|
173
|
+
return rewriteReactiveCall(ctx, expr);
|
|
174
|
+
}
|
|
175
|
+
if (!ast.hasMatch(expr, n => isNestedHtmlTemplate(n) || isReactiveCall(n))) {
|
|
167
176
|
return ctx.printer.printNode(ts.EmitHint.Expression, expr, ctx.sourceFile);
|
|
168
177
|
}
|
|
169
178
|
let replacements = [];
|
|
170
|
-
|
|
179
|
+
collectNestedReplacements(ctx, expr, expr.getStart(ctx.sourceFile), replacements);
|
|
171
180
|
return replaceReverse(expr.getText(ctx.sourceFile), replacements);
|
|
172
181
|
}
|
|
173
|
-
|
|
182
|
+
function rewriteReactiveCall(ctx, node) {
|
|
183
|
+
let arrayArg = node.arguments[0], arrayText = ctx.printer.printNode(ts.EmitHint.Expression, arrayArg, ctx.sourceFile), callbackArg = node.arguments[1], callbackText = rewriteExpression(ctx, callbackArg);
|
|
184
|
+
return `${arrayText}, ${callbackText}`;
|
|
185
|
+
}
|
|
186
|
+
function rewriteNestedReactiveCall(ctx, node) {
|
|
187
|
+
let arrayArg = node.arguments[0], arrayText = ctx.printer.printNode(ts.EmitHint.Expression, arrayArg, ctx.sourceFile), callbackArg = node.arguments[1], callbackText = rewriteExpression(ctx, callbackArg);
|
|
188
|
+
return `new ${COMPILER_NAMESPACE}.ArraySlot(${arrayText}, ${callbackText})`;
|
|
189
|
+
}
|
|
190
|
+
function discoverTemplatesInExpression(ctx, node) {
|
|
191
|
+
if (isNestedHtmlTemplate(node)) {
|
|
192
|
+
let template = node, expressions = [], literals = [], tpl = template.template;
|
|
193
|
+
if (ts.isNoSubstitutionTemplateLiteral(tpl)) {
|
|
194
|
+
literals.push(tpl.text);
|
|
195
|
+
}
|
|
196
|
+
else if (ts.isTemplateExpression(tpl)) {
|
|
197
|
+
literals.push(tpl.head.text);
|
|
198
|
+
for (let i = 0, n = tpl.templateSpans.length; i < n; i++) {
|
|
199
|
+
expressions.push(tpl.templateSpans[i].expression);
|
|
200
|
+
literals.push(tpl.templateSpans[i].literal.text);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
let parsed = parser.parse(literals);
|
|
204
|
+
getOrCreateTemplateId(ctx, parsed.html);
|
|
205
|
+
for (let i = 0, n = expressions.length; i < n; i++) {
|
|
206
|
+
discoverTemplatesInExpression(ctx, expressions[i]);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
else if (isReactiveCall(node)) {
|
|
210
|
+
let call = node;
|
|
211
|
+
if (call.arguments.length >= 2) {
|
|
212
|
+
discoverTemplatesInExpression(ctx, call.arguments[1]);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
else {
|
|
216
|
+
ts.forEachChild(node, child => discoverTemplatesInExpression(ctx, child));
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
function discoverAllTemplates(ctx, templates) {
|
|
220
|
+
for (let i = 0, n = templates.length; i < n; i++) {
|
|
221
|
+
let parsed = parser.parse(templates[i].literals);
|
|
222
|
+
getOrCreateTemplateId(ctx, parsed.html);
|
|
223
|
+
for (let j = 0, m = templates[i].expressions.length; j < m; j++) {
|
|
224
|
+
discoverTemplatesInExpression(ctx, templates[i].expressions[j]);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
const generateCode = (templates, sourceFile, checker) => {
|
|
174
229
|
let result = {
|
|
175
|
-
imports: existingAliases ?? new Map(),
|
|
176
230
|
prepend: [],
|
|
177
231
|
replacements: [],
|
|
178
232
|
templates: new Map()
|
|
@@ -193,18 +247,16 @@ const generateCode = (templates, sourceFile, checker, existingAliases) => {
|
|
|
193
247
|
}
|
|
194
248
|
let ctx = {
|
|
195
249
|
checker,
|
|
196
|
-
imports: result.imports,
|
|
197
250
|
printer,
|
|
198
251
|
sourceFile,
|
|
199
252
|
templates: result.templates
|
|
200
|
-
}
|
|
253
|
+
};
|
|
201
254
|
for (let i = 0, n = rootTemplates.length; i < n; i++) {
|
|
202
255
|
let template = rootTemplates[i];
|
|
203
256
|
result.replacements.push({
|
|
204
257
|
generate: (sf) => {
|
|
205
258
|
let codeBefore = sf.getFullText().slice(0, template.node.getStart(sf)), exprTexts = [], isArrowBody = codeBefore.trimEnd().endsWith('=>'), localCtx = {
|
|
206
259
|
checker,
|
|
207
|
-
imports: ctx.imports,
|
|
208
260
|
printer,
|
|
209
261
|
sourceFile: sf,
|
|
210
262
|
templates: ctx.templates
|
|
@@ -223,8 +275,9 @@ const generateCode = (templates, sourceFile, checker, existingAliases) => {
|
|
|
223
275
|
node: template.node
|
|
224
276
|
});
|
|
225
277
|
}
|
|
278
|
+
discoverAllTemplates(ctx, templates);
|
|
226
279
|
for (let [html, id] of ctx.templates) {
|
|
227
|
-
result.prepend.push(`const ${id} = ${
|
|
280
|
+
result.prepend.push(`const ${id} = ${COMPILER_NAMESPACE}.template(\`${html}\`);`);
|
|
228
281
|
}
|
|
229
282
|
return result;
|
|
230
283
|
};
|
|
@@ -1,5 +1,3 @@
|
|
|
1
1
|
import type { Plugin } from '@esportsplus/typescript/compiler';
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
export default templatePlugin;
|
|
5
|
-
export { reactiveInliningPlugin, templatePlugin };
|
|
2
|
+
declare const plugin: Plugin;
|
|
3
|
+
export default plugin;
|
package/build/compiler/index.js
CHANGED
|
@@ -1,38 +1,62 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { ts } from '@esportsplus/typescript';
|
|
2
|
+
import { COMPILER_ENTRYPOINT, COMPILER_ENTRYPOINT_REACTIVITY, COMPILER_NAMESPACE, PACKAGE } from '../constants.js';
|
|
2
3
|
import { generateCode } from './codegen.js';
|
|
3
|
-
import
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
4
|
+
import { findHtmlTemplates, findReactiveCalls } from './ts-parser.js';
|
|
5
|
+
function isInRange(ranges, start, end) {
|
|
6
|
+
for (let i = 0, n = ranges.length; i < n; i++) {
|
|
7
|
+
let range = ranges[i];
|
|
8
|
+
if (start >= range.start && end <= range.end) {
|
|
9
|
+
return true;
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
return false;
|
|
13
|
+
}
|
|
14
|
+
let printer = ts.createPrinter({ newLine: ts.NewLineKind.LineFeed });
|
|
15
|
+
const plugin = {
|
|
16
|
+
patterns: [
|
|
17
|
+
`${COMPILER_ENTRYPOINT}\``,
|
|
18
|
+
`${COMPILER_ENTRYPOINT}.${COMPILER_ENTRYPOINT_REACTIVITY}`
|
|
19
|
+
],
|
|
8
20
|
transform: (ctx) => {
|
|
21
|
+
let importsIntent = [], prepend = [], replacements = [], removeImports = [];
|
|
9
22
|
let templates = findHtmlTemplates(ctx.sourceFile, ctx.checker);
|
|
10
|
-
|
|
11
|
-
|
|
23
|
+
let templateRanges = [];
|
|
24
|
+
for (let i = 0, n = templates.length; i < n; i++) {
|
|
25
|
+
templateRanges.push({
|
|
26
|
+
end: templates[i].end,
|
|
27
|
+
start: templates[i].start
|
|
28
|
+
});
|
|
12
29
|
}
|
|
13
|
-
let
|
|
14
|
-
|
|
15
|
-
|
|
30
|
+
let reactiveCalls = findReactiveCalls(ctx.sourceFile, ctx.checker);
|
|
31
|
+
for (let i = 0, n = reactiveCalls.length; i < n; i++) {
|
|
32
|
+
let call = reactiveCalls[i];
|
|
33
|
+
if (isInRange(templateRanges, call.start, call.end)) {
|
|
34
|
+
continue;
|
|
35
|
+
}
|
|
36
|
+
replacements.push({
|
|
37
|
+
generate: (sourceFile) => `new ${COMPILER_NAMESPACE}.ArraySlot(${printer.printNode(ts.EmitHint.Expression, call.arrayArg, sourceFile)}, ${printer.printNode(ts.EmitHint.Expression, call.callbackArg, sourceFile)})`,
|
|
38
|
+
node: call.node
|
|
39
|
+
});
|
|
16
40
|
}
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
41
|
+
if (templates.length > 0) {
|
|
42
|
+
let result = generateCode(templates, ctx.sourceFile, ctx.checker);
|
|
43
|
+
prepend.push(...result.prepend);
|
|
44
|
+
replacements.push(...result.replacements);
|
|
45
|
+
removeImports.push(COMPILER_ENTRYPOINT);
|
|
20
46
|
}
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
aliasedImports.push(`${name} as ${alias}`);
|
|
47
|
+
if (replacements.length === 0 && prepend.length === 0) {
|
|
48
|
+
return {};
|
|
24
49
|
}
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
50
|
+
importsIntent.push({
|
|
51
|
+
namespace: COMPILER_NAMESPACE,
|
|
52
|
+
package: PACKAGE,
|
|
53
|
+
remove: removeImports
|
|
54
|
+
});
|
|
30
55
|
return {
|
|
31
|
-
imports,
|
|
32
|
-
prepend
|
|
33
|
-
replacements
|
|
56
|
+
imports: importsIntent,
|
|
57
|
+
prepend,
|
|
58
|
+
replacements
|
|
34
59
|
};
|
|
35
60
|
}
|
|
36
61
|
};
|
|
37
|
-
export default
|
|
38
|
-
export { reactiveInliningPlugin, templatePlugin };
|
|
62
|
+
export default plugin;
|
|
@@ -1,3 +1,2 @@
|
|
|
1
|
-
import
|
|
2
|
-
|
|
3
|
-
export default _default;
|
|
1
|
+
import plugin from '../index.js';
|
|
2
|
+
export default plugin;
|
|
@@ -1,3 +1,2 @@
|
|
|
1
|
-
import
|
|
2
|
-
|
|
3
|
-
export default plugin.tsc([reactiveInliningPlugin, templatePlugin]);
|
|
1
|
+
import plugin from '../index.js';
|
|
2
|
+
export default plugin;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { plugin } from '@esportsplus/typescript/compiler';
|
|
2
2
|
import { PACKAGE } from '../../constants.js';
|
|
3
|
-
import
|
|
3
|
+
import templatePlugin from '../index.js';
|
|
4
4
|
export default plugin.vite({
|
|
5
5
|
name: PACKAGE,
|
|
6
|
-
plugins: [
|
|
6
|
+
plugins: [templatePlugin]
|
|
7
7
|
});
|
package/build/constants.d.ts
CHANGED
|
@@ -2,6 +2,7 @@ declare const ARRAY_SLOT: unique symbol;
|
|
|
2
2
|
declare const CLEANUP: unique symbol;
|
|
3
3
|
declare const COMPILER_ENTRYPOINT = "html";
|
|
4
4
|
declare const COMPILER_ENTRYPOINT_REACTIVITY = "reactive";
|
|
5
|
+
declare const COMPILER_NAMESPACE: string;
|
|
5
6
|
declare const enum COMPILER_TYPES {
|
|
6
7
|
ArraySlot = "array-slot",
|
|
7
8
|
Attributes = "attributes",
|
|
@@ -21,4 +22,4 @@ declare const STATE_HYDRATING = 0;
|
|
|
21
22
|
declare const STATE_NONE = 1;
|
|
22
23
|
declare const STATE_WAITING = 2;
|
|
23
24
|
declare const STORE: unique symbol;
|
|
24
|
-
export { ARRAY_SLOT, CLEANUP, COMPILER_ENTRYPOINT, COMPILER_ENTRYPOINT_REACTIVITY, COMPILER_TYPES, DIRECT_ATTACH_EVENTS, LIFECYCLE_EVENTS, PACKAGE, SLOT_HTML, STATE_HYDRATING, STATE_NONE, STATE_WAITING, STORE, };
|
|
25
|
+
export { ARRAY_SLOT, CLEANUP, COMPILER_ENTRYPOINT, COMPILER_ENTRYPOINT_REACTIVITY, COMPILER_NAMESPACE, COMPILER_TYPES, DIRECT_ATTACH_EVENTS, LIFECYCLE_EVENTS, PACKAGE, SLOT_HTML, STATE_HYDRATING, STATE_NONE, STATE_WAITING, STORE, };
|
package/build/constants.js
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
|
+
import { uid } from '@esportsplus/typescript/compiler';
|
|
1
2
|
const ARRAY_SLOT = Symbol('template.array.slot');
|
|
2
3
|
const CLEANUP = Symbol('template.cleanup');
|
|
3
4
|
const COMPILER_ENTRYPOINT = 'html';
|
|
4
5
|
const COMPILER_ENTRYPOINT_REACTIVITY = 'reactive';
|
|
6
|
+
const COMPILER_NAMESPACE = uid('template');
|
|
5
7
|
var COMPILER_TYPES;
|
|
6
8
|
(function (COMPILER_TYPES) {
|
|
7
9
|
COMPILER_TYPES["ArraySlot"] = "array-slot";
|
|
@@ -33,4 +35,4 @@ const STATE_HYDRATING = 0;
|
|
|
33
35
|
const STATE_NONE = 1;
|
|
34
36
|
const STATE_WAITING = 2;
|
|
35
37
|
const STORE = Symbol('template.store');
|
|
36
|
-
export { ARRAY_SLOT, CLEANUP, COMPILER_ENTRYPOINT, COMPILER_ENTRYPOINT_REACTIVITY, COMPILER_TYPES, DIRECT_ATTACH_EVENTS, LIFECYCLE_EVENTS, PACKAGE, SLOT_HTML, STATE_HYDRATING, STATE_NONE, STATE_WAITING, STORE, };
|
|
38
|
+
export { ARRAY_SLOT, CLEANUP, COMPILER_ENTRYPOINT, COMPILER_ENTRYPOINT_REACTIVITY, COMPILER_NAMESPACE, COMPILER_TYPES, DIRECT_ATTACH_EVENTS, LIFECYCLE_EVENTS, PACKAGE, SLOT_HTML, STATE_HYDRATING, STATE_NONE, STATE_WAITING, STORE, };
|
package/package.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"author": "ICJR",
|
|
3
3
|
"dependencies": {
|
|
4
4
|
"@esportsplus/queue": "^0.2.0",
|
|
5
|
-
"@esportsplus/reactivity": "^0.28.
|
|
5
|
+
"@esportsplus/reactivity": "^0.28.1",
|
|
6
6
|
"@esportsplus/typescript": "^0.25.0",
|
|
7
7
|
"@esportsplus/utilities": "^0.27.2",
|
|
8
8
|
"serve": "^14.2.5"
|
|
@@ -35,7 +35,7 @@
|
|
|
35
35
|
},
|
|
36
36
|
"type": "module",
|
|
37
37
|
"types": "./build/index.d.ts",
|
|
38
|
-
"version": "0.38.
|
|
38
|
+
"version": "0.38.2",
|
|
39
39
|
"scripts": {
|
|
40
40
|
"build": "tsc",
|
|
41
41
|
"build:test": "vite build --config test/vite.config.ts",
|
package/src/compiler/codegen.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { ReplacementIntent } from '@esportsplus/typescript/compiler';
|
|
2
2
|
import { ts } from '@esportsplus/typescript';
|
|
3
3
|
import { ast, uid } from '@esportsplus/typescript/compiler';
|
|
4
|
-
import { COMPILER_ENTRYPOINT, COMPILER_TYPES, DIRECT_ATTACH_EVENTS, LIFECYCLE_EVENTS } from '~/constants';
|
|
4
|
+
import { COMPILER_ENTRYPOINT, COMPILER_ENTRYPOINT_REACTIVITY, COMPILER_NAMESPACE, COMPILER_TYPES, DIRECT_ATTACH_EVENTS, LIFECYCLE_EVENTS } from '~/constants';
|
|
5
5
|
import type { TemplateInfo } from './ts-parser';
|
|
6
6
|
import { analyze } from './analyzer';
|
|
7
7
|
import parser from './parser';
|
|
@@ -18,14 +18,12 @@ type Attribute = {
|
|
|
18
18
|
|
|
19
19
|
type CodegenContext = {
|
|
20
20
|
checker?: ts.TypeChecker;
|
|
21
|
-
imports: Map<string, string>;
|
|
22
21
|
printer: ts.Printer;
|
|
23
22
|
sourceFile: ts.SourceFile;
|
|
24
23
|
templates: Map<string, string>;
|
|
25
24
|
};
|
|
26
25
|
|
|
27
26
|
type CodegenResult = {
|
|
28
|
-
imports: Map<string, string>;
|
|
29
27
|
prepend: string[];
|
|
30
28
|
replacements: ReplacementIntent[];
|
|
31
29
|
templates: Map<string, string>;
|
|
@@ -54,18 +52,7 @@ const ARROW_EMPTY_PARAMS = /\(\s*\)\s*=>\s*$/;
|
|
|
54
52
|
let printer = ts.createPrinter({ newLine: ts.NewLineKind.LineFeed });
|
|
55
53
|
|
|
56
54
|
|
|
57
|
-
function
|
|
58
|
-
let alias = ctx.imports.get(name);
|
|
59
|
-
|
|
60
|
-
if (!alias) {
|
|
61
|
-
alias = uid(name);
|
|
62
|
-
ctx.imports.set(name, alias);
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
return alias;
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
function collectNestedTemplateReplacements(
|
|
55
|
+
function collectNestedReplacements(
|
|
69
56
|
ctx: CodegenContext,
|
|
70
57
|
node: ts.Node,
|
|
71
58
|
exprStart: number,
|
|
@@ -78,36 +65,44 @@ function collectNestedTemplateReplacements(
|
|
|
78
65
|
start: node.getStart(ctx.sourceFile) - exprStart
|
|
79
66
|
});
|
|
80
67
|
}
|
|
68
|
+
else if (isReactiveCall(node as ts.Expression)) {
|
|
69
|
+
// Nested reactive calls need full ArraySlot construction (not just args)
|
|
70
|
+
replacements.push({
|
|
71
|
+
end: node.end - exprStart,
|
|
72
|
+
newText: rewriteNestedReactiveCall(ctx, node as ts.CallExpression),
|
|
73
|
+
start: node.getStart(ctx.sourceFile) - exprStart
|
|
74
|
+
});
|
|
75
|
+
}
|
|
81
76
|
else {
|
|
82
|
-
ts.forEachChild(node, child =>
|
|
77
|
+
ts.forEachChild(node, child => collectNestedReplacements(ctx, child, exprStart, replacements));
|
|
83
78
|
}
|
|
84
79
|
}
|
|
85
80
|
|
|
86
|
-
function generateAttributeBinding(
|
|
81
|
+
function generateAttributeBinding(element: string, name: string, expr: string, staticValue: string): string {
|
|
87
82
|
if (name.startsWith('on') && name.length > 2) {
|
|
88
83
|
let event = name.slice(2).toLowerCase(),
|
|
89
84
|
key = name.toLowerCase();
|
|
90
85
|
|
|
91
86
|
if (LIFECYCLE_EVENTS.has(key)) {
|
|
92
|
-
return `${
|
|
87
|
+
return `${COMPILER_NAMESPACE}.${key}(${element}, ${expr});`;
|
|
93
88
|
}
|
|
94
89
|
|
|
95
90
|
if (DIRECT_ATTACH_EVENTS.has(key)) {
|
|
96
|
-
return `${
|
|
91
|
+
return `${COMPILER_NAMESPACE}.on(${element}, '${event}', ${expr});`;
|
|
97
92
|
}
|
|
98
93
|
|
|
99
|
-
return `${
|
|
94
|
+
return `${COMPILER_NAMESPACE}.delegate(${element}, '${event}', ${expr});`;
|
|
100
95
|
}
|
|
101
96
|
|
|
102
97
|
if (name === 'class') {
|
|
103
|
-
return `${
|
|
98
|
+
return `${COMPILER_NAMESPACE}.setClass(${element}, '${staticValue}', ${expr});`;
|
|
104
99
|
}
|
|
105
100
|
|
|
106
101
|
if (name === 'style') {
|
|
107
|
-
return `${
|
|
102
|
+
return `${COMPILER_NAMESPACE}.setStyle(${element}, '${staticValue}', ${expr});`;
|
|
108
103
|
}
|
|
109
104
|
|
|
110
|
-
return `${
|
|
105
|
+
return `${COMPILER_NAMESPACE}.setProperty(${element}, '${name}', ${expr});`;
|
|
111
106
|
}
|
|
112
107
|
|
|
113
108
|
function generateNestedTemplateCode(ctx: CodegenContext, node: ts.TaggedTemplateExpression): string {
|
|
@@ -142,30 +137,30 @@ function generateNestedTemplateCode(ctx: CodegenContext, node: ts.TaggedTemplate
|
|
|
142
137
|
|
|
143
138
|
function generateNodeBinding(ctx: CodegenContext, anchor: string, exprText: string, exprNode: ts.Expression | undefined): string {
|
|
144
139
|
if (!exprNode) {
|
|
145
|
-
return `${
|
|
140
|
+
return `${COMPILER_NAMESPACE}.slot(${anchor}, ${exprText});`;
|
|
146
141
|
}
|
|
147
142
|
|
|
148
143
|
if (isNestedHtmlTemplate(exprNode)) {
|
|
149
|
-
return `${anchor}.parentNode
|
|
144
|
+
return `${anchor}.parentNode!.insertBefore(${generateNestedTemplateCode(ctx, exprNode)}, ${anchor});`;
|
|
150
145
|
}
|
|
151
146
|
|
|
152
147
|
let slotType = analyze(exprNode, ctx.checker);
|
|
153
148
|
|
|
154
149
|
switch (slotType) {
|
|
155
|
-
case COMPILER_TYPES.Effect:
|
|
156
|
-
return `new ${addImport(ctx, 'EffectSlot')}(${anchor}, ${exprText});`;
|
|
157
|
-
|
|
158
150
|
case COMPILER_TYPES.ArraySlot:
|
|
159
|
-
return
|
|
151
|
+
return `${anchor}.parentNode!.insertBefore(new ${COMPILER_NAMESPACE}.ArraySlot(${exprText}).fragment, ${anchor});`;
|
|
152
|
+
|
|
153
|
+
case COMPILER_TYPES.DocumentFragment:
|
|
154
|
+
return `${anchor}.parentNode!.insertBefore(${exprText}, ${anchor});`;
|
|
155
|
+
|
|
156
|
+
case COMPILER_TYPES.Effect:
|
|
157
|
+
return `new ${COMPILER_NAMESPACE}.EffectSlot(${anchor}, ${exprText});`;
|
|
160
158
|
|
|
161
159
|
case COMPILER_TYPES.Static:
|
|
162
160
|
return `${anchor}.textContent = ${exprText};`;
|
|
163
161
|
|
|
164
|
-
case COMPILER_TYPES.DocumentFragment:
|
|
165
|
-
return `${anchor}.parentNode.insertBefore(${exprText}, ${anchor});`;
|
|
166
|
-
|
|
167
162
|
default:
|
|
168
|
-
return `${
|
|
163
|
+
return `${COMPILER_NAMESPACE}.slot(${anchor}, ${exprText});`;
|
|
169
164
|
}
|
|
170
165
|
}
|
|
171
166
|
|
|
@@ -215,16 +210,15 @@ function generateTemplateCode(
|
|
|
215
210
|
}
|
|
216
211
|
}
|
|
217
212
|
|
|
218
|
-
let
|
|
219
|
-
name = uid('element'),
|
|
213
|
+
let name = uid('element'),
|
|
220
214
|
segments = path.slice(start),
|
|
221
215
|
value = `${ancestor}.${segments.join('!.')}`;
|
|
222
216
|
|
|
223
217
|
if (ancestor === root && segments[0] === 'firstChild') {
|
|
224
|
-
value = value.replace(`${ancestor}.firstChild!`, `(${ancestor}.firstChild! as ${
|
|
218
|
+
value = value.replace(`${ancestor}.firstChild!`, `(${ancestor}.firstChild! as ${COMPILER_NAMESPACE}.Element)`);
|
|
225
219
|
}
|
|
226
220
|
|
|
227
|
-
declarations.push(`${name} = ${value} as ${
|
|
221
|
+
declarations.push(`${name} = ${value} as ${COMPILER_NAMESPACE}.Element`);
|
|
228
222
|
nodes.set(key, name);
|
|
229
223
|
}
|
|
230
224
|
|
|
@@ -247,14 +241,13 @@ function generateTemplateCode(
|
|
|
247
241
|
|
|
248
242
|
if (name === COMPILER_TYPES.Attributes) {
|
|
249
243
|
code.push(
|
|
250
|
-
`${
|
|
244
|
+
`${COMPILER_NAMESPACE}.setProperties(${element}, ${exprTexts[index] || 'undefined'});`
|
|
251
245
|
);
|
|
252
246
|
index++;
|
|
253
247
|
}
|
|
254
248
|
else {
|
|
255
249
|
code.push(
|
|
256
250
|
generateAttributeBinding(
|
|
257
|
-
ctx,
|
|
258
251
|
element,
|
|
259
252
|
name,
|
|
260
253
|
exprTexts[index++] || 'undefined',
|
|
@@ -293,6 +286,16 @@ function isNestedHtmlTemplate(expr: ts.Expression): expr is ts.TaggedTemplateExp
|
|
|
293
286
|
return ts.isTaggedTemplateExpression(expr) && ts.isIdentifier(expr.tag) && expr.tag.text === COMPILER_ENTRYPOINT;
|
|
294
287
|
}
|
|
295
288
|
|
|
289
|
+
function isReactiveCall(expr: ts.Expression): expr is ts.CallExpression {
|
|
290
|
+
return (
|
|
291
|
+
ts.isCallExpression(expr) &&
|
|
292
|
+
ts.isPropertyAccessExpression(expr.expression) &&
|
|
293
|
+
ts.isIdentifier(expr.expression.expression) &&
|
|
294
|
+
expr.expression.expression.text === COMPILER_ENTRYPOINT &&
|
|
295
|
+
expr.expression.name.text === COMPILER_ENTRYPOINT_REACTIVITY
|
|
296
|
+
);
|
|
297
|
+
}
|
|
298
|
+
|
|
296
299
|
function replaceReverse(text: string, replacements: Replacement[]): string {
|
|
297
300
|
let sorted = replacements.slice().sort((a, b) => b.start - a.start);
|
|
298
301
|
|
|
@@ -310,21 +313,96 @@ function rewriteExpression(ctx: CodegenContext, expr: ts.Expression): string {
|
|
|
310
313
|
return generateNestedTemplateCode(ctx, expr);
|
|
311
314
|
}
|
|
312
315
|
|
|
313
|
-
if (
|
|
316
|
+
if (isReactiveCall(expr)) {
|
|
317
|
+
return rewriteReactiveCall(ctx, expr);
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
if (!ast.hasMatch(expr, n => isNestedHtmlTemplate(n as ts.Expression) || isReactiveCall(n as ts.Expression))) {
|
|
314
321
|
return ctx.printer.printNode(ts.EmitHint.Expression, expr, ctx.sourceFile);
|
|
315
322
|
}
|
|
316
323
|
|
|
317
324
|
let replacements: Replacement[] = [];
|
|
318
325
|
|
|
319
|
-
|
|
326
|
+
collectNestedReplacements(ctx, expr, expr.getStart(ctx.sourceFile), replacements);
|
|
320
327
|
|
|
321
328
|
return replaceReverse(expr.getText(ctx.sourceFile), replacements);
|
|
322
329
|
}
|
|
323
330
|
|
|
331
|
+
// Returns just args "array, callback" - for direct slot usage where generateNodeBinding wraps it
|
|
332
|
+
function rewriteReactiveCall(ctx: CodegenContext, node: ts.CallExpression): string {
|
|
333
|
+
let arrayArg = node.arguments[0],
|
|
334
|
+
arrayText = ctx.printer.printNode(ts.EmitHint.Expression, arrayArg, ctx.sourceFile),
|
|
335
|
+
callbackArg = node.arguments[1],
|
|
336
|
+
callbackText = rewriteExpression(ctx, callbackArg as ts.Expression);
|
|
337
|
+
|
|
338
|
+
return `${arrayText}, ${callbackText}`;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
// Returns full "new ArraySlot(array, callback)" - for nested reactive calls inside expressions
|
|
342
|
+
function rewriteNestedReactiveCall(ctx: CodegenContext, node: ts.CallExpression): string {
|
|
343
|
+
let arrayArg = node.arguments[0],
|
|
344
|
+
arrayText = ctx.printer.printNode(ts.EmitHint.Expression, arrayArg, ctx.sourceFile),
|
|
345
|
+
callbackArg = node.arguments[1],
|
|
346
|
+
callbackText = rewriteExpression(ctx, callbackArg as ts.Expression);
|
|
347
|
+
|
|
348
|
+
return `new ${COMPILER_NAMESPACE}.ArraySlot(${arrayText}, ${callbackText})`;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
// Eager discovery - walk all expressions to find templates before prepend generation
|
|
352
|
+
function discoverTemplatesInExpression(ctx: CodegenContext, node: ts.Node): void {
|
|
353
|
+
if (isNestedHtmlTemplate(node as ts.Expression)) {
|
|
354
|
+
let template = node as ts.TaggedTemplateExpression,
|
|
355
|
+
expressions: ts.Expression[] = [],
|
|
356
|
+
literals: string[] = [],
|
|
357
|
+
tpl = template.template;
|
|
358
|
+
|
|
359
|
+
if (ts.isNoSubstitutionTemplateLiteral(tpl)) {
|
|
360
|
+
literals.push(tpl.text);
|
|
361
|
+
}
|
|
362
|
+
else if (ts.isTemplateExpression(tpl)) {
|
|
363
|
+
literals.push(tpl.head.text);
|
|
364
|
+
|
|
365
|
+
for (let i = 0, n = tpl.templateSpans.length; i < n; i++) {
|
|
366
|
+
expressions.push(tpl.templateSpans[i].expression);
|
|
367
|
+
literals.push(tpl.templateSpans[i].literal.text);
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
let parsed = parser.parse(literals) as ParseResult;
|
|
372
|
+
|
|
373
|
+
getOrCreateTemplateId(ctx, parsed.html);
|
|
374
|
+
|
|
375
|
+
for (let i = 0, n = expressions.length; i < n; i++) {
|
|
376
|
+
discoverTemplatesInExpression(ctx, expressions[i]);
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
else if (isReactiveCall(node as ts.Expression)) {
|
|
380
|
+
let call = node as ts.CallExpression;
|
|
381
|
+
|
|
382
|
+
if (call.arguments.length >= 2) {
|
|
383
|
+
discoverTemplatesInExpression(ctx, call.arguments[1]);
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
else {
|
|
387
|
+
ts.forEachChild(node, child => discoverTemplatesInExpression(ctx, child));
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
function discoverAllTemplates(ctx: CodegenContext, templates: TemplateInfo[]): void {
|
|
392
|
+
for (let i = 0, n = templates.length; i < n; i++) {
|
|
393
|
+
let parsed = parser.parse(templates[i].literals) as ParseResult;
|
|
394
|
+
|
|
395
|
+
getOrCreateTemplateId(ctx, parsed.html);
|
|
396
|
+
|
|
397
|
+
for (let j = 0, m = templates[i].expressions.length; j < m; j++) {
|
|
398
|
+
discoverTemplatesInExpression(ctx, templates[i].expressions[j]);
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
|
|
324
403
|
|
|
325
|
-
const generateCode = (templates: TemplateInfo[], sourceFile: ts.SourceFile, checker?: ts.TypeChecker
|
|
404
|
+
const generateCode = (templates: TemplateInfo[], sourceFile: ts.SourceFile, checker?: ts.TypeChecker): CodegenResult => {
|
|
326
405
|
let result: CodegenResult = {
|
|
327
|
-
imports: existingAliases ?? new Map(),
|
|
328
406
|
prepend: [],
|
|
329
407
|
replacements: [],
|
|
330
408
|
templates: new Map()
|
|
@@ -351,13 +429,11 @@ const generateCode = (templates: TemplateInfo[], sourceFile: ts.SourceFile, chec
|
|
|
351
429
|
}
|
|
352
430
|
|
|
353
431
|
let ctx: CodegenContext = {
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
},
|
|
360
|
-
templateAlias = addImport(ctx, 'template');
|
|
432
|
+
checker,
|
|
433
|
+
printer,
|
|
434
|
+
sourceFile,
|
|
435
|
+
templates: result.templates
|
|
436
|
+
};
|
|
361
437
|
|
|
362
438
|
for (let i = 0, n = rootTemplates.length; i < n; i++) {
|
|
363
439
|
let template = rootTemplates[i];
|
|
@@ -369,7 +445,6 @@ const generateCode = (templates: TemplateInfo[], sourceFile: ts.SourceFile, chec
|
|
|
369
445
|
isArrowBody = codeBefore.trimEnd().endsWith('=>'),
|
|
370
446
|
localCtx: CodegenContext = {
|
|
371
447
|
checker,
|
|
372
|
-
imports: ctx.imports,
|
|
373
448
|
printer,
|
|
374
449
|
sourceFile: sf,
|
|
375
450
|
templates: ctx.templates
|
|
@@ -400,8 +475,11 @@ const generateCode = (templates: TemplateInfo[], sourceFile: ts.SourceFile, chec
|
|
|
400
475
|
});
|
|
401
476
|
}
|
|
402
477
|
|
|
478
|
+
// Eager discovery: find all templates before prepend generation
|
|
479
|
+
discoverAllTemplates(ctx, templates);
|
|
480
|
+
|
|
403
481
|
for (let [html, id] of ctx.templates) {
|
|
404
|
-
result.prepend.push(`const ${id} = ${
|
|
482
|
+
result.prepend.push(`const ${id} = ${COMPILER_NAMESPACE}.template(\`${html}\`);`);
|
|
405
483
|
}
|
|
406
484
|
|
|
407
485
|
return result;
|
package/src/compiler/index.ts
CHANGED
|
@@ -1,56 +1,94 @@
|
|
|
1
|
-
import type { ImportIntent, Plugin, TransformContext } from '@esportsplus/typescript/compiler';
|
|
2
|
-
import {
|
|
1
|
+
import type { ImportIntent, Plugin, ReplacementIntent, TransformContext } from '@esportsplus/typescript/compiler';
|
|
2
|
+
import { ts } from '@esportsplus/typescript';
|
|
3
|
+
import { COMPILER_ENTRYPOINT, COMPILER_ENTRYPOINT_REACTIVITY, COMPILER_NAMESPACE, PACKAGE } from '~/constants';
|
|
3
4
|
import { generateCode } from './codegen';
|
|
4
|
-
import
|
|
5
|
-
import { findHtmlTemplates } from './ts-parser';
|
|
5
|
+
import { findHtmlTemplates, findReactiveCalls } from './ts-parser';
|
|
6
6
|
|
|
7
7
|
|
|
8
|
-
|
|
8
|
+
function isInRange(ranges: { end: number; start: number }[], start: number, end: number): boolean {
|
|
9
|
+
for (let i = 0, n = ranges.length; i < n; i++) {
|
|
10
|
+
let range = ranges[i];
|
|
11
|
+
|
|
12
|
+
if (start >= range.start && end <= range.end) {
|
|
13
|
+
return true;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
return false;
|
|
18
|
+
}
|
|
9
19
|
|
|
10
20
|
|
|
11
|
-
|
|
12
|
-
|
|
21
|
+
let printer = ts.createPrinter({ newLine: ts.NewLineKind.LineFeed });
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
const plugin: Plugin = {
|
|
25
|
+
patterns: [
|
|
26
|
+
`${COMPILER_ENTRYPOINT}\``,
|
|
27
|
+
`${COMPILER_ENTRYPOINT}.${COMPILER_ENTRYPOINT_REACTIVITY}`
|
|
28
|
+
],
|
|
13
29
|
|
|
14
30
|
transform: (ctx: TransformContext) => {
|
|
31
|
+
let importsIntent: ImportIntent[] = [],
|
|
32
|
+
prepend: string[] = [],
|
|
33
|
+
replacements: ReplacementIntent[] = [],
|
|
34
|
+
removeImports: string[] = [];
|
|
35
|
+
|
|
36
|
+
// Find templates first to build exclusion ranges
|
|
15
37
|
let templates = findHtmlTemplates(ctx.sourceFile, ctx.checker);
|
|
16
38
|
|
|
17
|
-
|
|
18
|
-
|
|
39
|
+
// Build ranges for all template nodes - reactive calls inside these are handled by template codegen
|
|
40
|
+
let templateRanges: { end: number; start: number }[] = [];
|
|
41
|
+
|
|
42
|
+
for (let i = 0, n = templates.length; i < n; i++) {
|
|
43
|
+
templateRanges.push({
|
|
44
|
+
end: templates[i].end,
|
|
45
|
+
start: templates[i].start
|
|
46
|
+
});
|
|
19
47
|
}
|
|
20
48
|
|
|
21
|
-
|
|
22
|
-
|
|
49
|
+
// Transform standalone html.reactive() calls (exclude those inside templates)
|
|
50
|
+
let reactiveCalls = findReactiveCalls(ctx.sourceFile, ctx.checker);
|
|
23
51
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
}
|
|
52
|
+
for (let i = 0, n = reactiveCalls.length; i < n; i++) {
|
|
53
|
+
let call = reactiveCalls[i];
|
|
27
54
|
|
|
28
|
-
|
|
55
|
+
// Skip reactive calls that are inside template expressions - handled by template codegen
|
|
56
|
+
if (isInRange(templateRanges, call.start, call.end)) {
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
29
59
|
|
|
30
|
-
|
|
31
|
-
|
|
60
|
+
replacements.push({
|
|
61
|
+
generate: (sourceFile) => `new ${COMPILER_NAMESPACE}.ArraySlot(${printer.printNode(ts.EmitHint.Expression, call.arrayArg, sourceFile)}, ${printer.printNode(ts.EmitHint.Expression, call.callbackArg, sourceFile)})`,
|
|
62
|
+
node: call.node
|
|
63
|
+
});
|
|
32
64
|
}
|
|
33
65
|
|
|
34
|
-
|
|
66
|
+
// Transform html`` templates
|
|
67
|
+
if (templates.length > 0) {
|
|
68
|
+
let result = generateCode(templates, ctx.sourceFile, ctx.checker);
|
|
35
69
|
|
|
36
|
-
|
|
37
|
-
|
|
70
|
+
prepend.push(...result.prepend);
|
|
71
|
+
replacements.push(...result.replacements);
|
|
72
|
+
removeImports.push(COMPILER_ENTRYPOINT);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
if (replacements.length === 0 && prepend.length === 0) {
|
|
76
|
+
return {};
|
|
38
77
|
}
|
|
39
78
|
|
|
40
|
-
|
|
41
|
-
|
|
79
|
+
importsIntent.push({
|
|
80
|
+
namespace: COMPILER_NAMESPACE,
|
|
42
81
|
package: PACKAGE,
|
|
43
|
-
remove:
|
|
44
|
-
}
|
|
82
|
+
remove: removeImports
|
|
83
|
+
});
|
|
45
84
|
|
|
46
85
|
return {
|
|
47
|
-
imports,
|
|
48
|
-
prepend
|
|
49
|
-
replacements
|
|
86
|
+
imports: importsIntent,
|
|
87
|
+
prepend,
|
|
88
|
+
replacements
|
|
50
89
|
};
|
|
51
90
|
}
|
|
52
91
|
};
|
|
53
92
|
|
|
54
93
|
|
|
55
|
-
export default
|
|
56
|
-
export { reactiveInliningPlugin, templatePlugin };
|
|
94
|
+
export default plugin;
|
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
import
|
|
2
|
-
import { reactiveInliningPlugin, templatePlugin } from '..';
|
|
1
|
+
import plugin from '..';
|
|
3
2
|
|
|
4
3
|
|
|
5
|
-
export default plugin
|
|
4
|
+
export default plugin;
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { plugin } from '@esportsplus/typescript/compiler';
|
|
2
|
-
import { PACKAGE } from '
|
|
3
|
-
import
|
|
2
|
+
import { PACKAGE } from '~/constants';
|
|
3
|
+
import templatePlugin from '..';
|
|
4
4
|
|
|
5
5
|
|
|
6
6
|
export default plugin.vite({
|
|
7
7
|
name: PACKAGE,
|
|
8
|
-
plugins: [
|
|
8
|
+
plugins: [templatePlugin]
|
|
9
9
|
});
|
package/src/constants.ts
CHANGED
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
import { uid } from '@esportsplus/typescript/compiler';
|
|
2
|
+
|
|
3
|
+
|
|
1
4
|
const ARRAY_SLOT = Symbol('template.array.slot');
|
|
2
5
|
|
|
3
6
|
const CLEANUP = Symbol('template.cleanup');
|
|
@@ -6,6 +9,8 @@ const COMPILER_ENTRYPOINT = 'html';
|
|
|
6
9
|
|
|
7
10
|
const COMPILER_ENTRYPOINT_REACTIVITY = 'reactive';
|
|
8
11
|
|
|
12
|
+
const COMPILER_NAMESPACE = uid('template');
|
|
13
|
+
|
|
9
14
|
const enum COMPILER_TYPES {
|
|
10
15
|
ArraySlot = 'array-slot',
|
|
11
16
|
Attributes = 'attributes',
|
|
@@ -48,7 +53,7 @@ const STORE = Symbol('template.store');
|
|
|
48
53
|
export {
|
|
49
54
|
ARRAY_SLOT,
|
|
50
55
|
CLEANUP,
|
|
51
|
-
COMPILER_ENTRYPOINT, COMPILER_ENTRYPOINT_REACTIVITY, COMPILER_TYPES,
|
|
56
|
+
COMPILER_ENTRYPOINT, COMPILER_ENTRYPOINT_REACTIVITY, COMPILER_NAMESPACE, COMPILER_TYPES,
|
|
52
57
|
DIRECT_ATTACH_EVENTS,
|
|
53
58
|
LIFECYCLE_EVENTS,
|
|
54
59
|
PACKAGE,
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Integration test: Combined reactivity + template plugins
|
|
3
|
+
*
|
|
4
|
+
* This file tests that both plugins work together correctly:
|
|
5
|
+
* 1. reactive() from @esportsplus/reactivity - creates reactive objects/arrays
|
|
6
|
+
* 2. html`` from @esportsplus/template - compiles templates
|
|
7
|
+
* 3. html.reactive() from @esportsplus/template - renders reactive arrays
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { reactive } from '@esportsplus/reactivity';
|
|
11
|
+
import { html } from '@esportsplus/template';
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
// Test 1: Basic reactive object with template
|
|
15
|
+
const state = reactive({
|
|
16
|
+
count: 0,
|
|
17
|
+
name: 'World'
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
export const basicTemplate = () => html`
|
|
21
|
+
<div class="counter">
|
|
22
|
+
<span>Hello ${() => state.name}!</span>
|
|
23
|
+
<span>Count: ${() => state.count}</span>
|
|
24
|
+
</div>
|
|
25
|
+
`;
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
// Test 2: Reactive array with html.reactive()
|
|
29
|
+
const items = reactive([
|
|
30
|
+
{ id: 1, text: 'First' },
|
|
31
|
+
{ id: 2, text: 'Second' },
|
|
32
|
+
{ id: 3, text: 'Third' }
|
|
33
|
+
]);
|
|
34
|
+
|
|
35
|
+
export const reactiveList = () => html`
|
|
36
|
+
<ul class="list">
|
|
37
|
+
${html.reactive(items, (item) => html`
|
|
38
|
+
<li data-id="${item.id}">${item.text}</li>
|
|
39
|
+
`)}
|
|
40
|
+
</ul>
|
|
41
|
+
`;
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
// Test 3: Combined - reactive object + reactive array in same template
|
|
45
|
+
type Todo = { id: number; done: boolean; text: string };
|
|
46
|
+
|
|
47
|
+
const todos = reactive<Todo[]>([
|
|
48
|
+
{ id: 1, done: false, text: 'Learn TypeScript' },
|
|
49
|
+
{ id: 2, done: true, text: 'Build compiler' }
|
|
50
|
+
]);
|
|
51
|
+
|
|
52
|
+
const app = reactive({
|
|
53
|
+
title: 'Todo App'
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
export const combinedTemplate = () => html`
|
|
57
|
+
<div class="app">
|
|
58
|
+
<h1>${() => app.title}</h1>
|
|
59
|
+
<ul>
|
|
60
|
+
${html.reactive(todos, (todo) => html`
|
|
61
|
+
<li class="${() => todo.done ? 'done' : ''}">
|
|
62
|
+
<input type="checkbox" checked="${() => todo.done}" />
|
|
63
|
+
<span>${todo.text}</span>
|
|
64
|
+
</li>
|
|
65
|
+
`)}
|
|
66
|
+
</ul>
|
|
67
|
+
</div>
|
|
68
|
+
`;
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
// Test 4: Static template (no reactive expressions)
|
|
72
|
+
export const staticTemplate = () => html`
|
|
73
|
+
<footer>
|
|
74
|
+
<p>Static content - no transformations needed</p>
|
|
75
|
+
</footer>
|
|
76
|
+
`;
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
// Test 5: Nested templates with effects
|
|
80
|
+
export const nestedTemplate = () => html`
|
|
81
|
+
<div class="wrapper">
|
|
82
|
+
${html`<header>Header</header>`}
|
|
83
|
+
<main>
|
|
84
|
+
${() => state.count > 5
|
|
85
|
+
? html`<span>High count!</span>`
|
|
86
|
+
: html`<span>Low count</span>`
|
|
87
|
+
}
|
|
88
|
+
</main>
|
|
89
|
+
</div>
|
|
90
|
+
`;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
{
|
|
2
|
+
"extends": "@esportsplus/typescript/tsconfig.package.json",
|
|
3
|
+
"compilerOptions": {
|
|
4
|
+
"rootDir": ".",
|
|
5
|
+
"outDir": "./build",
|
|
6
|
+
"declaration": false,
|
|
7
|
+
"declarationDir": null,
|
|
8
|
+
"noUnusedLocals": false,
|
|
9
|
+
"plugins": [
|
|
10
|
+
{ "transform": "@esportsplus/reactivity/compiler/tsc" },
|
|
11
|
+
{ "transform": "../../build/compiler/plugins/tsc.js" }
|
|
12
|
+
]
|
|
13
|
+
},
|
|
14
|
+
"include": [
|
|
15
|
+
"./**/*.ts"
|
|
16
|
+
]
|
|
17
|
+
}
|
|
@@ -1,75 +0,0 @@
|
|
|
1
|
-
import { ts } from '@esportsplus/typescript';
|
|
2
|
-
import { uid } from '@esportsplus/typescript/compiler';
|
|
3
|
-
import { COMPILER_ENTRYPOINT, COMPILER_ENTRYPOINT_REACTIVITY, PACKAGE } from '../constants.js';
|
|
4
|
-
const PATTERNS = [`${COMPILER_ENTRYPOINT}.${COMPILER_ENTRYPOINT_REACTIVITY}`];
|
|
5
|
-
const SHARED_KEY = 'template:arraySlotAlias';
|
|
6
|
-
let printer = ts.createPrinter({ newLine: ts.NewLineKind.LineFeed });
|
|
7
|
-
function isHtmlFromPackage(node, checker) {
|
|
8
|
-
if (node.text !== COMPILER_ENTRYPOINT) {
|
|
9
|
-
return false;
|
|
10
|
-
}
|
|
11
|
-
if (!checker) {
|
|
12
|
-
return true;
|
|
13
|
-
}
|
|
14
|
-
let symbol = checker.getSymbolAtLocation(node);
|
|
15
|
-
if (!symbol) {
|
|
16
|
-
return true;
|
|
17
|
-
}
|
|
18
|
-
if (symbol.flags & ts.SymbolFlags.Alias) {
|
|
19
|
-
symbol = checker.getAliasedSymbol(symbol);
|
|
20
|
-
}
|
|
21
|
-
let declarations = symbol.getDeclarations();
|
|
22
|
-
if (!declarations || declarations.length === 0) {
|
|
23
|
-
return true;
|
|
24
|
-
}
|
|
25
|
-
for (let i = 0, n = declarations.length; i < n; i++) {
|
|
26
|
-
let filename = declarations[i].getSourceFile().fileName;
|
|
27
|
-
if (filename.includes(PACKAGE) || filename.includes('@esportsplus/template')) {
|
|
28
|
-
return true;
|
|
29
|
-
}
|
|
30
|
-
}
|
|
31
|
-
return false;
|
|
32
|
-
}
|
|
33
|
-
function visit(node, calls, checker) {
|
|
34
|
-
if (ts.isCallExpression(node) &&
|
|
35
|
-
ts.isPropertyAccessExpression(node.expression) &&
|
|
36
|
-
ts.isIdentifier(node.expression.expression) &&
|
|
37
|
-
node.expression.name.text === COMPILER_ENTRYPOINT_REACTIVITY &&
|
|
38
|
-
node.arguments.length === 2 &&
|
|
39
|
-
isHtmlFromPackage(node.expression.expression, checker)) {
|
|
40
|
-
calls.push({
|
|
41
|
-
arrayArg: node.arguments[0],
|
|
42
|
-
callbackArg: node.arguments[1],
|
|
43
|
-
node
|
|
44
|
-
});
|
|
45
|
-
}
|
|
46
|
-
ts.forEachChild(node, child => visit(child, calls, checker));
|
|
47
|
-
}
|
|
48
|
-
const plugin = {
|
|
49
|
-
patterns: PATTERNS,
|
|
50
|
-
transform: (ctx) => {
|
|
51
|
-
let calls = [];
|
|
52
|
-
visit(ctx.sourceFile, calls, ctx.checker);
|
|
53
|
-
if (calls.length === 0) {
|
|
54
|
-
return {};
|
|
55
|
-
}
|
|
56
|
-
let arraySlotAlias = uid('ArraySlot'), replacements = [];
|
|
57
|
-
ctx.shared.set(SHARED_KEY, arraySlotAlias);
|
|
58
|
-
for (let i = 0, n = calls.length; i < n; i++) {
|
|
59
|
-
let call = calls[i];
|
|
60
|
-
replacements.push({
|
|
61
|
-
generate: (sourceFile) => `new ${arraySlotAlias}(${printer.printNode(ts.EmitHint.Expression, call.arrayArg, sourceFile)}, ${printer.printNode(ts.EmitHint.Expression, call.callbackArg, sourceFile)})`,
|
|
62
|
-
node: call.node
|
|
63
|
-
});
|
|
64
|
-
}
|
|
65
|
-
return {
|
|
66
|
-
imports: [{
|
|
67
|
-
add: [`ArraySlot as ${arraySlotAlias}`],
|
|
68
|
-
package: PACKAGE
|
|
69
|
-
}],
|
|
70
|
-
replacements
|
|
71
|
-
};
|
|
72
|
-
}
|
|
73
|
-
};
|
|
74
|
-
export default plugin;
|
|
75
|
-
export { SHARED_KEY };
|
|
@@ -1,116 +0,0 @@
|
|
|
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 };
|