@esportsplus/template 0.38.0 → 0.38.1
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 +88 -39
- package/build/compiler/index.d.ts +2 -4
- package/build/compiler/index.js +51 -26
- 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 +118 -52
- package/src/compiler/index.ts +69 -28
- 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: rewriteReactiveCall(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,60 @@ 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 discoverTemplatesInExpression(ctx, node) {
|
|
187
|
+
if (isNestedHtmlTemplate(node)) {
|
|
188
|
+
let template = node, expressions = [], literals = [], tpl = template.template;
|
|
189
|
+
if (ts.isNoSubstitutionTemplateLiteral(tpl)) {
|
|
190
|
+
literals.push(tpl.text);
|
|
191
|
+
}
|
|
192
|
+
else if (ts.isTemplateExpression(tpl)) {
|
|
193
|
+
literals.push(tpl.head.text);
|
|
194
|
+
for (let i = 0, n = tpl.templateSpans.length; i < n; i++) {
|
|
195
|
+
expressions.push(tpl.templateSpans[i].expression);
|
|
196
|
+
literals.push(tpl.templateSpans[i].literal.text);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
let parsed = parser.parse(literals);
|
|
200
|
+
getOrCreateTemplateId(ctx, parsed.html);
|
|
201
|
+
for (let i = 0, n = expressions.length; i < n; i++) {
|
|
202
|
+
discoverTemplatesInExpression(ctx, expressions[i]);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
else if (isReactiveCall(node)) {
|
|
206
|
+
let call = node;
|
|
207
|
+
if (call.arguments.length >= 2) {
|
|
208
|
+
discoverTemplatesInExpression(ctx, call.arguments[1]);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
else {
|
|
212
|
+
ts.forEachChild(node, child => discoverTemplatesInExpression(ctx, child));
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
function discoverAllTemplates(ctx, templates) {
|
|
216
|
+
for (let i = 0, n = templates.length; i < n; i++) {
|
|
217
|
+
let parsed = parser.parse(templates[i].literals);
|
|
218
|
+
getOrCreateTemplateId(ctx, parsed.html);
|
|
219
|
+
for (let j = 0, m = templates[i].expressions.length; j < m; j++) {
|
|
220
|
+
discoverTemplatesInExpression(ctx, templates[i].expressions[j]);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
const generateCode = (templates, sourceFile, checker) => {
|
|
174
225
|
let result = {
|
|
175
|
-
imports: existingAliases ?? new Map(),
|
|
176
226
|
prepend: [],
|
|
177
227
|
replacements: [],
|
|
178
228
|
templates: new Map()
|
|
@@ -193,18 +243,16 @@ const generateCode = (templates, sourceFile, checker, existingAliases) => {
|
|
|
193
243
|
}
|
|
194
244
|
let ctx = {
|
|
195
245
|
checker,
|
|
196
|
-
imports: result.imports,
|
|
197
246
|
printer,
|
|
198
247
|
sourceFile,
|
|
199
248
|
templates: result.templates
|
|
200
|
-
}
|
|
249
|
+
};
|
|
201
250
|
for (let i = 0, n = rootTemplates.length; i < n; i++) {
|
|
202
251
|
let template = rootTemplates[i];
|
|
203
252
|
result.replacements.push({
|
|
204
253
|
generate: (sf) => {
|
|
205
254
|
let codeBefore = sf.getFullText().slice(0, template.node.getStart(sf)), exprTexts = [], isArrowBody = codeBefore.trimEnd().endsWith('=>'), localCtx = {
|
|
206
255
|
checker,
|
|
207
|
-
imports: ctx.imports,
|
|
208
256
|
printer,
|
|
209
257
|
sourceFile: sf,
|
|
210
258
|
templates: ctx.templates
|
|
@@ -223,8 +271,9 @@ const generateCode = (templates, sourceFile, checker, existingAliases) => {
|
|
|
223
271
|
node: template.node
|
|
224
272
|
});
|
|
225
273
|
}
|
|
274
|
+
discoverAllTemplates(ctx, templates);
|
|
226
275
|
for (let [html, id] of ctx.templates) {
|
|
227
|
-
result.prepend.push(`const ${id} = ${
|
|
276
|
+
result.prepend.push(`const ${id} = ${COMPILER_NAMESPACE}.template(\`${html}\`);`);
|
|
228
277
|
}
|
|
229
278
|
return result;
|
|
230
279
|
};
|
|
@@ -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,63 @@
|
|
|
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
|
-
|
|
4
|
+
import { findHtmlTemplates, findReactiveCalls } from './ts-parser.js';
|
|
5
|
+
const PATTERNS = [
|
|
6
|
+
`${COMPILER_ENTRYPOINT}\``,
|
|
7
|
+
`${COMPILER_ENTRYPOINT}.${COMPILER_ENTRYPOINT_REACTIVITY}`
|
|
8
|
+
];
|
|
9
|
+
function isInRange(ranges, start, end) {
|
|
10
|
+
for (let i = 0, n = ranges.length; i < n; i++) {
|
|
11
|
+
let range = ranges[i];
|
|
12
|
+
if (start >= range.start && end <= range.end) {
|
|
13
|
+
return true;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
return false;
|
|
17
|
+
}
|
|
18
|
+
let printer = ts.createPrinter({ newLine: ts.NewLineKind.LineFeed });
|
|
19
|
+
const plugin = {
|
|
7
20
|
patterns: PATTERNS,
|
|
8
21
|
transform: (ctx) => {
|
|
22
|
+
let importsIntent = [], prepend = [], replacements = [], removeImports = [];
|
|
9
23
|
let templates = findHtmlTemplates(ctx.sourceFile, ctx.checker);
|
|
10
|
-
|
|
11
|
-
|
|
24
|
+
let templateRanges = [];
|
|
25
|
+
for (let i = 0, n = templates.length; i < n; i++) {
|
|
26
|
+
templateRanges.push({
|
|
27
|
+
end: templates[i].end,
|
|
28
|
+
start: templates[i].start
|
|
29
|
+
});
|
|
12
30
|
}
|
|
13
|
-
let
|
|
14
|
-
|
|
15
|
-
|
|
31
|
+
let reactiveCalls = findReactiveCalls(ctx.sourceFile, ctx.checker);
|
|
32
|
+
for (let i = 0, n = reactiveCalls.length; i < n; i++) {
|
|
33
|
+
let call = reactiveCalls[i];
|
|
34
|
+
if (isInRange(templateRanges, call.start, call.end)) {
|
|
35
|
+
continue;
|
|
36
|
+
}
|
|
37
|
+
replacements.push({
|
|
38
|
+
generate: (sourceFile) => `new ${COMPILER_NAMESPACE}.ArraySlot(${printer.printNode(ts.EmitHint.Expression, call.arrayArg, sourceFile)}, ${printer.printNode(ts.EmitHint.Expression, call.callbackArg, sourceFile)})`,
|
|
39
|
+
node: call.node
|
|
40
|
+
});
|
|
16
41
|
}
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
42
|
+
if (templates.length > 0) {
|
|
43
|
+
let result = generateCode(templates, ctx.sourceFile, ctx.checker);
|
|
44
|
+
prepend.push(...result.prepend);
|
|
45
|
+
replacements.push(...result.replacements);
|
|
46
|
+
removeImports.push(COMPILER_ENTRYPOINT);
|
|
20
47
|
}
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
aliasedImports.push(`${name} as ${alias}`);
|
|
48
|
+
if (replacements.length === 0 && prepend.length === 0) {
|
|
49
|
+
return {};
|
|
24
50
|
}
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
51
|
+
importsIntent.push({
|
|
52
|
+
namespace: COMPILER_NAMESPACE,
|
|
53
|
+
package: PACKAGE,
|
|
54
|
+
remove: removeImports
|
|
55
|
+
});
|
|
30
56
|
return {
|
|
31
|
-
imports,
|
|
32
|
-
prepend
|
|
33
|
-
replacements
|
|
57
|
+
imports: importsIntent,
|
|
58
|
+
prepend,
|
|
59
|
+
replacements
|
|
34
60
|
};
|
|
35
61
|
}
|
|
36
62
|
};
|
|
37
|
-
export default
|
|
38
|
-
export { reactiveInliningPlugin, templatePlugin };
|
|
63
|
+
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.1",
|
|
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,43 @@ function collectNestedTemplateReplacements(
|
|
|
78
65
|
start: node.getStart(ctx.sourceFile) - exprStart
|
|
79
66
|
});
|
|
80
67
|
}
|
|
68
|
+
else if (isReactiveCall(node as ts.Expression)) {
|
|
69
|
+
replacements.push({
|
|
70
|
+
end: node.end - exprStart,
|
|
71
|
+
newText: rewriteReactiveCall(ctx, node as ts.CallExpression),
|
|
72
|
+
start: node.getStart(ctx.sourceFile) - exprStart
|
|
73
|
+
});
|
|
74
|
+
}
|
|
81
75
|
else {
|
|
82
|
-
ts.forEachChild(node, child =>
|
|
76
|
+
ts.forEachChild(node, child => collectNestedReplacements(ctx, child, exprStart, replacements));
|
|
83
77
|
}
|
|
84
78
|
}
|
|
85
79
|
|
|
86
|
-
function generateAttributeBinding(
|
|
80
|
+
function generateAttributeBinding(element: string, name: string, expr: string, staticValue: string): string {
|
|
87
81
|
if (name.startsWith('on') && name.length > 2) {
|
|
88
82
|
let event = name.slice(2).toLowerCase(),
|
|
89
83
|
key = name.toLowerCase();
|
|
90
84
|
|
|
91
85
|
if (LIFECYCLE_EVENTS.has(key)) {
|
|
92
|
-
return `${
|
|
86
|
+
return `${COMPILER_NAMESPACE}.${key}(${element}, ${expr});`;
|
|
93
87
|
}
|
|
94
88
|
|
|
95
89
|
if (DIRECT_ATTACH_EVENTS.has(key)) {
|
|
96
|
-
return `${
|
|
90
|
+
return `${COMPILER_NAMESPACE}.on(${element}, '${event}', ${expr});`;
|
|
97
91
|
}
|
|
98
92
|
|
|
99
|
-
return `${
|
|
93
|
+
return `${COMPILER_NAMESPACE}.delegate(${element}, '${event}', ${expr});`;
|
|
100
94
|
}
|
|
101
95
|
|
|
102
96
|
if (name === 'class') {
|
|
103
|
-
return `${
|
|
97
|
+
return `${COMPILER_NAMESPACE}.setClass(${element}, '${staticValue}', ${expr});`;
|
|
104
98
|
}
|
|
105
99
|
|
|
106
100
|
if (name === 'style') {
|
|
107
|
-
return `${
|
|
101
|
+
return `${COMPILER_NAMESPACE}.setStyle(${element}, '${staticValue}', ${expr});`;
|
|
108
102
|
}
|
|
109
103
|
|
|
110
|
-
return `${
|
|
104
|
+
return `${COMPILER_NAMESPACE}.setProperty(${element}, '${name}', ${expr});`;
|
|
111
105
|
}
|
|
112
106
|
|
|
113
107
|
function generateNestedTemplateCode(ctx: CodegenContext, node: ts.TaggedTemplateExpression): string {
|
|
@@ -142,30 +136,30 @@ function generateNestedTemplateCode(ctx: CodegenContext, node: ts.TaggedTemplate
|
|
|
142
136
|
|
|
143
137
|
function generateNodeBinding(ctx: CodegenContext, anchor: string, exprText: string, exprNode: ts.Expression | undefined): string {
|
|
144
138
|
if (!exprNode) {
|
|
145
|
-
return `${
|
|
139
|
+
return `${COMPILER_NAMESPACE}.slot(${anchor}, ${exprText});`;
|
|
146
140
|
}
|
|
147
141
|
|
|
148
142
|
if (isNestedHtmlTemplate(exprNode)) {
|
|
149
|
-
return `${anchor}.parentNode
|
|
143
|
+
return `${anchor}.parentNode!.insertBefore(${generateNestedTemplateCode(ctx, exprNode)}, ${anchor});`;
|
|
150
144
|
}
|
|
151
145
|
|
|
152
146
|
let slotType = analyze(exprNode, ctx.checker);
|
|
153
147
|
|
|
154
148
|
switch (slotType) {
|
|
155
|
-
case COMPILER_TYPES.Effect:
|
|
156
|
-
return `new ${addImport(ctx, 'EffectSlot')}(${anchor}, ${exprText});`;
|
|
157
|
-
|
|
158
149
|
case COMPILER_TYPES.ArraySlot:
|
|
159
|
-
return
|
|
150
|
+
return `${anchor}.parentNode!.insertBefore(new ${COMPILER_NAMESPACE}.ArraySlot(${exprText}).fragment, ${anchor});`;
|
|
151
|
+
|
|
152
|
+
case COMPILER_TYPES.DocumentFragment:
|
|
153
|
+
return `${anchor}.parentNode!.insertBefore(${exprText}, ${anchor});`;
|
|
154
|
+
|
|
155
|
+
case COMPILER_TYPES.Effect:
|
|
156
|
+
return `new ${COMPILER_NAMESPACE}.EffectSlot(${anchor}, ${exprText});`;
|
|
160
157
|
|
|
161
158
|
case COMPILER_TYPES.Static:
|
|
162
159
|
return `${anchor}.textContent = ${exprText};`;
|
|
163
160
|
|
|
164
|
-
case COMPILER_TYPES.DocumentFragment:
|
|
165
|
-
return `${anchor}.parentNode.insertBefore(${exprText}, ${anchor});`;
|
|
166
|
-
|
|
167
161
|
default:
|
|
168
|
-
return `${
|
|
162
|
+
return `${COMPILER_NAMESPACE}.slot(${anchor}, ${exprText});`;
|
|
169
163
|
}
|
|
170
164
|
}
|
|
171
165
|
|
|
@@ -215,16 +209,15 @@ function generateTemplateCode(
|
|
|
215
209
|
}
|
|
216
210
|
}
|
|
217
211
|
|
|
218
|
-
let
|
|
219
|
-
name = uid('element'),
|
|
212
|
+
let name = uid('element'),
|
|
220
213
|
segments = path.slice(start),
|
|
221
214
|
value = `${ancestor}.${segments.join('!.')}`;
|
|
222
215
|
|
|
223
216
|
if (ancestor === root && segments[0] === 'firstChild') {
|
|
224
|
-
value = value.replace(`${ancestor}.firstChild!`, `(${ancestor}.firstChild! as ${
|
|
217
|
+
value = value.replace(`${ancestor}.firstChild!`, `(${ancestor}.firstChild! as ${COMPILER_NAMESPACE}.Element)`);
|
|
225
218
|
}
|
|
226
219
|
|
|
227
|
-
declarations.push(`${name} = ${value} as ${
|
|
220
|
+
declarations.push(`${name} = ${value} as ${COMPILER_NAMESPACE}.Element`);
|
|
228
221
|
nodes.set(key, name);
|
|
229
222
|
}
|
|
230
223
|
|
|
@@ -247,14 +240,13 @@ function generateTemplateCode(
|
|
|
247
240
|
|
|
248
241
|
if (name === COMPILER_TYPES.Attributes) {
|
|
249
242
|
code.push(
|
|
250
|
-
`${
|
|
243
|
+
`${COMPILER_NAMESPACE}.setProperties(${element}, ${exprTexts[index] || 'undefined'});`
|
|
251
244
|
);
|
|
252
245
|
index++;
|
|
253
246
|
}
|
|
254
247
|
else {
|
|
255
248
|
code.push(
|
|
256
249
|
generateAttributeBinding(
|
|
257
|
-
ctx,
|
|
258
250
|
element,
|
|
259
251
|
name,
|
|
260
252
|
exprTexts[index++] || 'undefined',
|
|
@@ -293,6 +285,16 @@ function isNestedHtmlTemplate(expr: ts.Expression): expr is ts.TaggedTemplateExp
|
|
|
293
285
|
return ts.isTaggedTemplateExpression(expr) && ts.isIdentifier(expr.tag) && expr.tag.text === COMPILER_ENTRYPOINT;
|
|
294
286
|
}
|
|
295
287
|
|
|
288
|
+
function isReactiveCall(expr: ts.Expression): expr is ts.CallExpression {
|
|
289
|
+
return (
|
|
290
|
+
ts.isCallExpression(expr) &&
|
|
291
|
+
ts.isPropertyAccessExpression(expr.expression) &&
|
|
292
|
+
ts.isIdentifier(expr.expression.expression) &&
|
|
293
|
+
expr.expression.expression.text === COMPILER_ENTRYPOINT &&
|
|
294
|
+
expr.expression.name.text === COMPILER_ENTRYPOINT_REACTIVITY
|
|
295
|
+
);
|
|
296
|
+
}
|
|
297
|
+
|
|
296
298
|
function replaceReverse(text: string, replacements: Replacement[]): string {
|
|
297
299
|
let sorted = replacements.slice().sort((a, b) => b.start - a.start);
|
|
298
300
|
|
|
@@ -310,21 +312,85 @@ function rewriteExpression(ctx: CodegenContext, expr: ts.Expression): string {
|
|
|
310
312
|
return generateNestedTemplateCode(ctx, expr);
|
|
311
313
|
}
|
|
312
314
|
|
|
313
|
-
if (
|
|
315
|
+
if (isReactiveCall(expr)) {
|
|
316
|
+
return rewriteReactiveCall(ctx, expr);
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
if (!ast.hasMatch(expr, n => isNestedHtmlTemplate(n as ts.Expression) || isReactiveCall(n as ts.Expression))) {
|
|
314
320
|
return ctx.printer.printNode(ts.EmitHint.Expression, expr, ctx.sourceFile);
|
|
315
321
|
}
|
|
316
322
|
|
|
317
323
|
let replacements: Replacement[] = [];
|
|
318
324
|
|
|
319
|
-
|
|
325
|
+
collectNestedReplacements(ctx, expr, expr.getStart(ctx.sourceFile), replacements);
|
|
320
326
|
|
|
321
327
|
return replaceReverse(expr.getText(ctx.sourceFile), replacements);
|
|
322
328
|
}
|
|
323
329
|
|
|
330
|
+
function rewriteReactiveCall(ctx: CodegenContext, node: ts.CallExpression): string {
|
|
331
|
+
let arrayArg = node.arguments[0],
|
|
332
|
+
arrayText = ctx.printer.printNode(ts.EmitHint.Expression, arrayArg, ctx.sourceFile),
|
|
333
|
+
callbackArg = node.arguments[1],
|
|
334
|
+
callbackText = rewriteExpression(ctx, callbackArg as ts.Expression);
|
|
335
|
+
|
|
336
|
+
return `${arrayText}, ${callbackText}`;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
// Eager discovery - walk all expressions to find templates before prepend generation
|
|
340
|
+
function discoverTemplatesInExpression(ctx: CodegenContext, node: ts.Node): void {
|
|
341
|
+
if (isNestedHtmlTemplate(node as ts.Expression)) {
|
|
342
|
+
let template = node as ts.TaggedTemplateExpression,
|
|
343
|
+
expressions: ts.Expression[] = [],
|
|
344
|
+
literals: string[] = [],
|
|
345
|
+
tpl = template.template;
|
|
324
346
|
|
|
325
|
-
|
|
347
|
+
if (ts.isNoSubstitutionTemplateLiteral(tpl)) {
|
|
348
|
+
literals.push(tpl.text);
|
|
349
|
+
}
|
|
350
|
+
else if (ts.isTemplateExpression(tpl)) {
|
|
351
|
+
literals.push(tpl.head.text);
|
|
352
|
+
|
|
353
|
+
for (let i = 0, n = tpl.templateSpans.length; i < n; i++) {
|
|
354
|
+
expressions.push(tpl.templateSpans[i].expression);
|
|
355
|
+
literals.push(tpl.templateSpans[i].literal.text);
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
let parsed = parser.parse(literals) as ParseResult;
|
|
360
|
+
|
|
361
|
+
getOrCreateTemplateId(ctx, parsed.html);
|
|
362
|
+
|
|
363
|
+
for (let i = 0, n = expressions.length; i < n; i++) {
|
|
364
|
+
discoverTemplatesInExpression(ctx, expressions[i]);
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
else if (isReactiveCall(node as ts.Expression)) {
|
|
368
|
+
let call = node as ts.CallExpression;
|
|
369
|
+
|
|
370
|
+
if (call.arguments.length >= 2) {
|
|
371
|
+
discoverTemplatesInExpression(ctx, call.arguments[1]);
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
else {
|
|
375
|
+
ts.forEachChild(node, child => discoverTemplatesInExpression(ctx, child));
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
function discoverAllTemplates(ctx: CodegenContext, templates: TemplateInfo[]): void {
|
|
380
|
+
for (let i = 0, n = templates.length; i < n; i++) {
|
|
381
|
+
let parsed = parser.parse(templates[i].literals) as ParseResult;
|
|
382
|
+
|
|
383
|
+
getOrCreateTemplateId(ctx, parsed.html);
|
|
384
|
+
|
|
385
|
+
for (let j = 0, m = templates[i].expressions.length; j < m; j++) {
|
|
386
|
+
discoverTemplatesInExpression(ctx, templates[i].expressions[j]);
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
|
|
392
|
+
const generateCode = (templates: TemplateInfo[], sourceFile: ts.SourceFile, checker?: ts.TypeChecker): CodegenResult => {
|
|
326
393
|
let result: CodegenResult = {
|
|
327
|
-
imports: existingAliases ?? new Map(),
|
|
328
394
|
prepend: [],
|
|
329
395
|
replacements: [],
|
|
330
396
|
templates: new Map()
|
|
@@ -351,13 +417,11 @@ const generateCode = (templates: TemplateInfo[], sourceFile: ts.SourceFile, chec
|
|
|
351
417
|
}
|
|
352
418
|
|
|
353
419
|
let ctx: CodegenContext = {
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
},
|
|
360
|
-
templateAlias = addImport(ctx, 'template');
|
|
420
|
+
checker,
|
|
421
|
+
printer,
|
|
422
|
+
sourceFile,
|
|
423
|
+
templates: result.templates
|
|
424
|
+
};
|
|
361
425
|
|
|
362
426
|
for (let i = 0, n = rootTemplates.length; i < n; i++) {
|
|
363
427
|
let template = rootTemplates[i];
|
|
@@ -369,7 +433,6 @@ const generateCode = (templates: TemplateInfo[], sourceFile: ts.SourceFile, chec
|
|
|
369
433
|
isArrowBody = codeBefore.trimEnd().endsWith('=>'),
|
|
370
434
|
localCtx: CodegenContext = {
|
|
371
435
|
checker,
|
|
372
|
-
imports: ctx.imports,
|
|
373
436
|
printer,
|
|
374
437
|
sourceFile: sf,
|
|
375
438
|
templates: ctx.templates
|
|
@@ -400,8 +463,11 @@ const generateCode = (templates: TemplateInfo[], sourceFile: ts.SourceFile, chec
|
|
|
400
463
|
});
|
|
401
464
|
}
|
|
402
465
|
|
|
466
|
+
// Eager discovery: find all templates before prepend generation
|
|
467
|
+
discoverAllTemplates(ctx, templates);
|
|
468
|
+
|
|
403
469
|
for (let [html, id] of ctx.templates) {
|
|
404
|
-
result.prepend.push(`const ${id} = ${
|
|
470
|
+
result.prepend.push(`const ${id} = ${COMPILER_NAMESPACE}.template(\`${html}\`);`);
|
|
405
471
|
}
|
|
406
472
|
|
|
407
473
|
return result;
|
package/src/compiler/index.ts
CHANGED
|
@@ -1,56 +1,97 @@
|
|
|
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
|
-
const PATTERNS = [
|
|
8
|
+
const PATTERNS = [
|
|
9
|
+
`${COMPILER_ENTRYPOINT}\``,
|
|
10
|
+
`${COMPILER_ENTRYPOINT}.${COMPILER_ENTRYPOINT_REACTIVITY}`
|
|
11
|
+
];
|
|
9
12
|
|
|
10
13
|
|
|
11
|
-
|
|
14
|
+
function isInRange(ranges: { end: number; start: number }[], start: number, end: number): boolean {
|
|
15
|
+
for (let i = 0, n = ranges.length; i < n; i++) {
|
|
16
|
+
let range = ranges[i];
|
|
17
|
+
|
|
18
|
+
if (start >= range.start && end <= range.end) {
|
|
19
|
+
return true;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
return false;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
let printer = ts.createPrinter({ newLine: ts.NewLineKind.LineFeed });
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
const plugin: Plugin = {
|
|
12
31
|
patterns: PATTERNS,
|
|
13
32
|
|
|
14
33
|
transform: (ctx: TransformContext) => {
|
|
34
|
+
let importsIntent: ImportIntent[] = [],
|
|
35
|
+
prepend: string[] = [],
|
|
36
|
+
replacements: ReplacementIntent[] = [],
|
|
37
|
+
removeImports: string[] = [];
|
|
38
|
+
|
|
39
|
+
// Find templates first to build exclusion ranges
|
|
15
40
|
let templates = findHtmlTemplates(ctx.sourceFile, ctx.checker);
|
|
16
41
|
|
|
17
|
-
|
|
18
|
-
|
|
42
|
+
// Build ranges for all template nodes - reactive calls inside these are handled by template codegen
|
|
43
|
+
let templateRanges: { end: number; start: number }[] = [];
|
|
44
|
+
|
|
45
|
+
for (let i = 0, n = templates.length; i < n; i++) {
|
|
46
|
+
templateRanges.push({
|
|
47
|
+
end: templates[i].end,
|
|
48
|
+
start: templates[i].start
|
|
49
|
+
});
|
|
19
50
|
}
|
|
20
51
|
|
|
21
|
-
|
|
22
|
-
|
|
52
|
+
// Transform standalone html.reactive() calls (exclude those inside templates)
|
|
53
|
+
let reactiveCalls = findReactiveCalls(ctx.sourceFile, ctx.checker);
|
|
23
54
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
}
|
|
55
|
+
for (let i = 0, n = reactiveCalls.length; i < n; i++) {
|
|
56
|
+
let call = reactiveCalls[i];
|
|
27
57
|
|
|
28
|
-
|
|
58
|
+
// Skip reactive calls that are inside template expressions - handled by template codegen
|
|
59
|
+
if (isInRange(templateRanges, call.start, call.end)) {
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
29
62
|
|
|
30
|
-
|
|
31
|
-
|
|
63
|
+
replacements.push({
|
|
64
|
+
generate: (sourceFile) => `new ${COMPILER_NAMESPACE}.ArraySlot(${printer.printNode(ts.EmitHint.Expression, call.arrayArg, sourceFile)}, ${printer.printNode(ts.EmitHint.Expression, call.callbackArg, sourceFile)})`,
|
|
65
|
+
node: call.node
|
|
66
|
+
});
|
|
32
67
|
}
|
|
33
68
|
|
|
34
|
-
|
|
69
|
+
// Transform html`` templates
|
|
70
|
+
if (templates.length > 0) {
|
|
71
|
+
let result = generateCode(templates, ctx.sourceFile, ctx.checker);
|
|
72
|
+
|
|
73
|
+
prepend.push(...result.prepend);
|
|
74
|
+
replacements.push(...result.replacements);
|
|
75
|
+
removeImports.push(COMPILER_ENTRYPOINT);
|
|
76
|
+
}
|
|
35
77
|
|
|
36
|
-
|
|
37
|
-
|
|
78
|
+
if (replacements.length === 0 && prepend.length === 0) {
|
|
79
|
+
return {};
|
|
38
80
|
}
|
|
39
81
|
|
|
40
|
-
|
|
41
|
-
|
|
82
|
+
importsIntent.push({
|
|
83
|
+
namespace: COMPILER_NAMESPACE,
|
|
42
84
|
package: PACKAGE,
|
|
43
|
-
remove:
|
|
44
|
-
}
|
|
85
|
+
remove: removeImports
|
|
86
|
+
});
|
|
45
87
|
|
|
46
88
|
return {
|
|
47
|
-
imports,
|
|
48
|
-
prepend
|
|
49
|
-
replacements
|
|
89
|
+
imports: importsIntent,
|
|
90
|
+
prepend,
|
|
91
|
+
replacements
|
|
50
92
|
};
|
|
51
93
|
}
|
|
52
94
|
};
|
|
53
95
|
|
|
54
96
|
|
|
55
|
-
export default
|
|
56
|
-
export { reactiveInliningPlugin, templatePlugin };
|
|
97
|
+
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 };
|