@esportsplus/template 0.28.3 → 0.29.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/README.md +431 -0
- package/build/attributes.d.ts +7 -1
- package/build/attributes.js +86 -33
- package/build/constants.d.ts +3 -11
- package/build/constants.js +4 -32
- package/build/event/constants.d.ts +3 -0
- package/build/event/constants.js +13 -0
- package/build/event/index.d.ts +9 -1
- package/build/event/index.js +29 -35
- package/build/event/ontick.js +6 -9
- package/build/html.d.ts +9 -0
- package/build/html.js +7 -0
- package/build/index.d.ts +8 -2
- package/build/index.js +8 -1
- package/build/render.d.ts +2 -2
- package/build/render.js +2 -3
- package/build/runtime.d.ts +1 -0
- package/build/runtime.js +5 -0
- package/build/slot/array.d.ts +3 -3
- package/build/slot/array.js +11 -14
- package/build/slot/cleanup.d.ts +1 -1
- package/build/slot/cleanup.js +1 -2
- package/build/slot/effect.js +5 -7
- package/build/slot/index.js +1 -7
- package/build/slot/render.js +6 -8
- package/build/svg.d.ts +1 -1
- package/build/svg.js +1 -1
- package/build/transformer/codegen.d.ts +18 -0
- package/build/transformer/codegen.js +316 -0
- package/build/transformer/index.d.ts +12 -0
- package/build/transformer/index.js +62 -0
- package/build/transformer/parser.d.ts +18 -0
- package/build/transformer/parser.js +166 -0
- package/build/transformer/plugins/esbuild.d.ts +5 -0
- package/build/transformer/plugins/esbuild.js +35 -0
- package/build/transformer/plugins/tsc.d.ts +3 -0
- package/build/transformer/plugins/tsc.js +4 -0
- package/build/transformer/plugins/vite.d.ts +5 -0
- package/build/transformer/plugins/vite.js +37 -0
- package/build/transformer/ts-parser.d.ts +21 -0
- package/build/transformer/ts-parser.js +72 -0
- package/build/transformer/type-analyzer.d.ts +7 -0
- package/build/transformer/type-analyzer.js +230 -0
- package/build/types.d.ts +2 -3
- package/build/utilities.d.ts +7 -0
- package/build/utilities.js +31 -0
- package/package.json +33 -4
- package/src/attributes.ts +115 -51
- package/src/constants.ts +6 -53
- package/src/event/constants.ts +16 -0
- package/src/event/index.ts +36 -42
- package/src/event/onconnect.ts +1 -1
- package/src/event/onresize.ts +1 -1
- package/src/event/ontick.ts +7 -11
- package/src/html.ts +18 -0
- package/src/index.ts +8 -2
- package/src/render.ts +6 -7
- package/src/runtime.ts +8 -0
- package/src/slot/array.ts +18 -24
- package/src/slot/cleanup.ts +3 -4
- package/src/slot/effect.ts +6 -8
- package/src/slot/index.ts +2 -8
- package/src/slot/render.ts +7 -9
- package/src/svg.ts +1 -1
- package/src/transformer/codegen.ts +518 -0
- package/src/transformer/index.ts +98 -0
- package/src/transformer/parser.ts +239 -0
- package/src/transformer/plugins/esbuild.ts +46 -0
- package/src/transformer/plugins/tsc.ts +7 -0
- package/src/transformer/plugins/vite.ts +49 -0
- package/src/transformer/ts-parser.ts +123 -0
- package/src/transformer/type-analyzer.ts +334 -0
- package/src/types.ts +3 -4
- package/src/utilities.ts +52 -0
- package/storage/rewrite-analysis-2026-01-04.md +439 -0
- package/test/constants.ts +69 -0
- package/test/effects.ts +237 -0
- package/test/events.ts +318 -0
- package/test/imported-values.ts +253 -0
- package/test/nested.ts +298 -0
- package/test/slots.ts +259 -0
- package/test/spread.ts +290 -0
- package/test/static.ts +118 -0
- package/test/templates.ts +473 -0
- package/test/tsconfig.json +17 -0
- package/test/vite.config.ts +50 -0
- package/build/html/index.d.ts +0 -9
- package/build/html/index.js +0 -29
- package/build/html/parser.d.ts +0 -5
- package/build/html/parser.js +0 -165
- package/build/utilities/element.d.ts +0 -11
- package/build/utilities/element.js +0 -9
- package/build/utilities/fragment.d.ts +0 -3
- package/build/utilities/fragment.js +0 -10
- package/build/utilities/marker.d.ts +0 -2
- package/build/utilities/marker.js +0 -4
- package/build/utilities/node.d.ts +0 -9
- package/build/utilities/node.js +0 -10
- package/build/utilities/raf.d.ts +0 -2
- package/build/utilities/raf.js +0 -1
- package/build/utilities/text.d.ts +0 -2
- package/build/utilities/text.js +0 -9
- package/src/html/index.ts +0 -48
- package/src/html/parser.ts +0 -235
- package/src/utilities/element.ts +0 -28
- package/src/utilities/fragment.ts +0 -19
- package/src/utilities/marker.ts +0 -6
- package/src/utilities/node.ts +0 -29
- package/src/utilities/raf.ts +0 -1
- package/src/utilities/text.ts +0 -15
|
@@ -0,0 +1,316 @@
|
|
|
1
|
+
import { addImport, applyReplacementsReverse, uid } from '@esportsplus/typescript/transformer';
|
|
2
|
+
import { analyzeExpression, generateAttributeBinding, generateSpreadBindings } from './type-analyzer.js';
|
|
3
|
+
import ts from 'typescript';
|
|
4
|
+
import parser from './parser.js';
|
|
5
|
+
const ARROW_EMPTY_PARAMS = /\(\s*\)\s*=>\s*$/;
|
|
6
|
+
let currentChecker, hoistedFactories = new Map(), htmlToTemplateId = new Map(), nameArraySlot = '', nameAttr = '', nameEffectSlot = '', nameEvent = '', nameSlot = '', nameTemplate = '', needsArraySlot = false, needsAttr = false, needsEffectSlot = false, needsEvent = false, needsSlot = false;
|
|
7
|
+
function collectNestedTemplateReplacements(node, exprStart, sourceFile, replacements) {
|
|
8
|
+
if (isNestedHtmlTemplate(node)) {
|
|
9
|
+
replacements.push({
|
|
10
|
+
end: node.end - exprStart,
|
|
11
|
+
newText: generateNestedTemplateCode(node, sourceFile),
|
|
12
|
+
start: node.getStart() - exprStart
|
|
13
|
+
});
|
|
14
|
+
}
|
|
15
|
+
else {
|
|
16
|
+
ts.forEachChild(node, child => collectNestedTemplateReplacements(child, exprStart, sourceFile, replacements));
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
function generateImports() {
|
|
20
|
+
let specifiers = [];
|
|
21
|
+
if (needsArraySlot) {
|
|
22
|
+
specifiers.push(`ArraySlot as ${nameArraySlot}`);
|
|
23
|
+
}
|
|
24
|
+
if (needsEffectSlot) {
|
|
25
|
+
specifiers.push(`EffectSlot as ${nameEffectSlot}`);
|
|
26
|
+
}
|
|
27
|
+
if (needsAttr) {
|
|
28
|
+
specifiers.push(`attributes as ${nameAttr}`);
|
|
29
|
+
}
|
|
30
|
+
if (needsEvent) {
|
|
31
|
+
specifiers.push(`event as ${nameEvent}`);
|
|
32
|
+
}
|
|
33
|
+
if (needsSlot) {
|
|
34
|
+
specifiers.push(`slot as ${nameSlot}`);
|
|
35
|
+
}
|
|
36
|
+
specifiers.push(`template as ${nameTemplate}`);
|
|
37
|
+
return `import { ${specifiers.join(', ')} } from '@esportsplus/template';`;
|
|
38
|
+
}
|
|
39
|
+
function generateNestedTemplateCode(node, sourceFile) {
|
|
40
|
+
let expressions = [], exprTexts = [], literals = [], template = node.template;
|
|
41
|
+
if (ts.isNoSubstitutionTemplateLiteral(template)) {
|
|
42
|
+
literals.push(template.text);
|
|
43
|
+
}
|
|
44
|
+
else if (ts.isTemplateExpression(template)) {
|
|
45
|
+
literals.push(template.head.text);
|
|
46
|
+
for (let i = 0, n = template.templateSpans.length; i < n; i++) {
|
|
47
|
+
let expr = template.templateSpans[i].expression;
|
|
48
|
+
expressions.push(expr);
|
|
49
|
+
literals.push(template.templateSpans[i].literal.text);
|
|
50
|
+
exprTexts.push(rewriteExpression(expr, sourceFile));
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
return generateTemplateCode(parser.parse(literals), exprTexts, expressions, sourceFile, false);
|
|
54
|
+
}
|
|
55
|
+
function generateNodeBinding(anchor, exprText, exprNode, sourceFile) {
|
|
56
|
+
if (!exprNode) {
|
|
57
|
+
needsSlot = true;
|
|
58
|
+
return `${nameSlot}(${anchor}, ${exprText});`;
|
|
59
|
+
}
|
|
60
|
+
if (isNestedHtmlTemplate(exprNode)) {
|
|
61
|
+
return `${anchor}.parentNode.insertBefore(${generateNestedTemplateCode(exprNode, sourceFile)}, ${anchor});`;
|
|
62
|
+
}
|
|
63
|
+
let slotType = analyzeExpression(exprNode, currentChecker);
|
|
64
|
+
switch (slotType) {
|
|
65
|
+
case 'effect':
|
|
66
|
+
needsEffectSlot = true;
|
|
67
|
+
return `new ${nameEffectSlot}(${anchor}, ${exprText});`;
|
|
68
|
+
case 'array-slot':
|
|
69
|
+
needsArraySlot = true;
|
|
70
|
+
return `new ${nameArraySlot}(${anchor}, ${exprText});`;
|
|
71
|
+
case 'static':
|
|
72
|
+
return `${anchor}.textContent = ${exprText};`;
|
|
73
|
+
case 'document-fragment':
|
|
74
|
+
return `${anchor}.parentNode.insertBefore(${exprText}, ${anchor});`;
|
|
75
|
+
default:
|
|
76
|
+
needsSlot = true;
|
|
77
|
+
return `${nameSlot}(${anchor}, ${exprText});`;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
function generateTemplateCode({ html, slots }, exprTexts, exprNodes, sourceFile, isArrowBody) {
|
|
81
|
+
if (!slots || slots.length === 0) {
|
|
82
|
+
return `${getOrCreateTemplateId(html)}()`;
|
|
83
|
+
}
|
|
84
|
+
let code = [], declarations = [], index = 0, nodes = new Map(), root = uid('root');
|
|
85
|
+
declarations.push(`${root} = ${getOrCreateTemplateId(html)}()`);
|
|
86
|
+
nodes.set('', root);
|
|
87
|
+
for (let i = 0, n = slots.length; i < n; i++) {
|
|
88
|
+
let path = slots[i].path;
|
|
89
|
+
if (path.length === 0) {
|
|
90
|
+
continue;
|
|
91
|
+
}
|
|
92
|
+
let key = path.join('.');
|
|
93
|
+
if (nodes.has(key)) {
|
|
94
|
+
continue;
|
|
95
|
+
}
|
|
96
|
+
let ancestorVar = root, startIdx = 0;
|
|
97
|
+
for (let j = path.length - 1; j >= 0; j--) {
|
|
98
|
+
let prefix = path.slice(0, j).join('.');
|
|
99
|
+
if (nodes.has(prefix)) {
|
|
100
|
+
ancestorVar = nodes.get(prefix);
|
|
101
|
+
startIdx = j;
|
|
102
|
+
break;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
let name = uid('element'), suffix = path.slice(startIdx).join('.');
|
|
106
|
+
declarations.push(`${name} = ${ancestorVar}.${suffix}`);
|
|
107
|
+
nodes.set(key, name);
|
|
108
|
+
}
|
|
109
|
+
code.push(isArrowBody ? '{' : `(() => {`, `let ${declarations.join(',\n')};`);
|
|
110
|
+
for (let i = 0, n = slots.length; i < n; i++) {
|
|
111
|
+
let elementVar = slots[i].path.length === 0
|
|
112
|
+
? root
|
|
113
|
+
: (nodes.get(slots[i].path.join('.')) || root), slot = slots[i];
|
|
114
|
+
if (slot.type === 'attributes') {
|
|
115
|
+
for (let j = 0, m = slot.attributes.names.length; j < m; j++) {
|
|
116
|
+
let name = slot.attributes.names[j];
|
|
117
|
+
if (name === 'spread') {
|
|
118
|
+
let bindings = generateSpreadBindings(exprNodes[index], exprTexts[index] || 'undefined', elementVar, sourceFile, currentChecker);
|
|
119
|
+
for (let k = 0, o = bindings.length; k < o; k++) {
|
|
120
|
+
trackBindingUsage(bindings[k]);
|
|
121
|
+
code.push(bindings[k]);
|
|
122
|
+
}
|
|
123
|
+
index++;
|
|
124
|
+
}
|
|
125
|
+
else {
|
|
126
|
+
let binding = generateAttributeBinding(elementVar, name, exprTexts[index++] || 'undefined', slot.attributes.statics[name] || '');
|
|
127
|
+
trackBindingUsage(binding);
|
|
128
|
+
code.push(binding);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
else {
|
|
133
|
+
code.push(generateNodeBinding(elementVar, exprTexts[index] || 'undefined', exprNodes[index], sourceFile));
|
|
134
|
+
index++;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
code.push(`return ${root};`);
|
|
138
|
+
code.push(isArrowBody ? `}` : `})()`);
|
|
139
|
+
return code.join('\n');
|
|
140
|
+
}
|
|
141
|
+
function getOrCreateTemplateId(html) {
|
|
142
|
+
let id = htmlToTemplateId.get(html);
|
|
143
|
+
if (!id) {
|
|
144
|
+
id = uid('tmpl');
|
|
145
|
+
hoistedFactories.set(id, html);
|
|
146
|
+
htmlToTemplateId.set(html, id);
|
|
147
|
+
}
|
|
148
|
+
return id;
|
|
149
|
+
}
|
|
150
|
+
function hasArraySlotImport(sourceFile) {
|
|
151
|
+
for (let i = 0, n = sourceFile.statements.length; i < n; i++) {
|
|
152
|
+
let stmt = sourceFile.statements[i];
|
|
153
|
+
if (!ts.isImportDeclaration(stmt) || !stmt.importClause?.namedBindings) {
|
|
154
|
+
continue;
|
|
155
|
+
}
|
|
156
|
+
let bindings = stmt.importClause.namedBindings;
|
|
157
|
+
if (!ts.isNamedImports(bindings)) {
|
|
158
|
+
continue;
|
|
159
|
+
}
|
|
160
|
+
for (let j = 0, m = bindings.elements.length; j < m; j++) {
|
|
161
|
+
if (bindings.elements[j].name.text === 'ArraySlot') {
|
|
162
|
+
return true;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
return false;
|
|
167
|
+
}
|
|
168
|
+
function hasArraySlotUsage(node) {
|
|
169
|
+
if (ts.isNewExpression(node) &&
|
|
170
|
+
ts.isIdentifier(node.expression) &&
|
|
171
|
+
node.expression.text === 'ArraySlot') {
|
|
172
|
+
return true;
|
|
173
|
+
}
|
|
174
|
+
let found = false;
|
|
175
|
+
ts.forEachChild(node, child => {
|
|
176
|
+
if (!found && hasArraySlotUsage(child)) {
|
|
177
|
+
found = true;
|
|
178
|
+
}
|
|
179
|
+
});
|
|
180
|
+
return found;
|
|
181
|
+
}
|
|
182
|
+
function hasNestedTemplates(node) {
|
|
183
|
+
if (isNestedHtmlTemplate(node)) {
|
|
184
|
+
return true;
|
|
185
|
+
}
|
|
186
|
+
let found = false;
|
|
187
|
+
ts.forEachChild(node, child => {
|
|
188
|
+
if (!found && hasNestedTemplates(child)) {
|
|
189
|
+
found = true;
|
|
190
|
+
}
|
|
191
|
+
});
|
|
192
|
+
return found;
|
|
193
|
+
}
|
|
194
|
+
function isNestedHtmlTemplate(expr) {
|
|
195
|
+
return ts.isTaggedTemplateExpression(expr) && ts.isIdentifier(expr.tag) && expr.tag.text === 'html';
|
|
196
|
+
}
|
|
197
|
+
function isNestedTemplate(template, allTemplates) {
|
|
198
|
+
for (let i = 0, n = allTemplates.length; i < n; i++) {
|
|
199
|
+
let other = allTemplates[i];
|
|
200
|
+
if (other === template) {
|
|
201
|
+
continue;
|
|
202
|
+
}
|
|
203
|
+
for (let j = 0, m = other.expressions.length; j < m; j++) {
|
|
204
|
+
let expr = other.expressions[j];
|
|
205
|
+
if (template.start >= expr.getStart() && template.end <= expr.end) {
|
|
206
|
+
return true;
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
return false;
|
|
211
|
+
}
|
|
212
|
+
function rewriteExpression(expr, sourceFile) {
|
|
213
|
+
if (isNestedHtmlTemplate(expr)) {
|
|
214
|
+
return generateNestedTemplateCode(expr, sourceFile);
|
|
215
|
+
}
|
|
216
|
+
if (!hasNestedTemplates(expr)) {
|
|
217
|
+
return ts.createPrinter({ newLine: ts.NewLineKind.LineFeed }).printNode(ts.EmitHint.Expression, expr, sourceFile);
|
|
218
|
+
}
|
|
219
|
+
let exprStart = expr.getStart(), replacements = [];
|
|
220
|
+
collectNestedTemplateReplacements(expr, exprStart, sourceFile, replacements);
|
|
221
|
+
return applyReplacementsReverse(expr.getText(sourceFile), replacements);
|
|
222
|
+
}
|
|
223
|
+
function trackBindingUsage(binding) {
|
|
224
|
+
if (binding.startsWith(nameEvent + '.')) {
|
|
225
|
+
needsEvent = true;
|
|
226
|
+
}
|
|
227
|
+
else if (binding.startsWith(nameAttr + '.')) {
|
|
228
|
+
needsAttr = true;
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
const addArraySlotImport = (code) => {
|
|
232
|
+
return addImport(code, '@esportsplus/template', ['ArraySlot']);
|
|
233
|
+
};
|
|
234
|
+
const generateCode = (templates, originalCode, sourceFile) => {
|
|
235
|
+
if (templates.length === 0) {
|
|
236
|
+
return { changed: false, code: originalCode };
|
|
237
|
+
}
|
|
238
|
+
hoistedFactories.clear();
|
|
239
|
+
htmlToTemplateId.clear();
|
|
240
|
+
nameArraySlot = uid('ArraySlot');
|
|
241
|
+
nameAttr = uid('attr');
|
|
242
|
+
nameEffectSlot = uid('EffectSlot');
|
|
243
|
+
nameEvent = uid('event');
|
|
244
|
+
nameSlot = uid('slot');
|
|
245
|
+
nameTemplate = uid('template');
|
|
246
|
+
needsArraySlot = false;
|
|
247
|
+
needsAttr = false;
|
|
248
|
+
needsEffectSlot = false;
|
|
249
|
+
needsEvent = false;
|
|
250
|
+
needsSlot = false;
|
|
251
|
+
let rootTemplates = templates.filter(t => !isNestedTemplate(t, templates));
|
|
252
|
+
if (rootTemplates.length === 0) {
|
|
253
|
+
return { changed: false, code: originalCode };
|
|
254
|
+
}
|
|
255
|
+
let replacements = [];
|
|
256
|
+
for (let i = 0, n = rootTemplates.length; i < n; i++) {
|
|
257
|
+
let exprTexts = [], template = rootTemplates[i];
|
|
258
|
+
for (let j = 0, m = template.expressions.length; j < m; j++) {
|
|
259
|
+
exprTexts.push(rewriteExpression(template.expressions[j], sourceFile));
|
|
260
|
+
}
|
|
261
|
+
let codeBefore = originalCode.slice(0, template.start), isArrowBody = codeBefore.trimEnd().endsWith('=>'), parsed = parser.parse(template.literals);
|
|
262
|
+
if (isArrowBody && (!parsed.slots || parsed.slots.length === 0)) {
|
|
263
|
+
let arrowMatch = codeBefore.match(ARROW_EMPTY_PARAMS);
|
|
264
|
+
if (arrowMatch) {
|
|
265
|
+
replacements.push({
|
|
266
|
+
end: template.end,
|
|
267
|
+
newText: getOrCreateTemplateId(parsed.html),
|
|
268
|
+
start: template.start - arrowMatch[0].length
|
|
269
|
+
});
|
|
270
|
+
continue;
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
replacements.push({
|
|
274
|
+
end: template.end,
|
|
275
|
+
newText: generateTemplateCode(parsed, exprTexts, template.expressions, sourceFile, isArrowBody),
|
|
276
|
+
start: template.start
|
|
277
|
+
});
|
|
278
|
+
}
|
|
279
|
+
let changed = replacements.length > 0, code = applyReplacementsReverse(originalCode, replacements);
|
|
280
|
+
if (changed && hoistedFactories.size > 0) {
|
|
281
|
+
let factories = [];
|
|
282
|
+
for (let [id, html] of hoistedFactories) {
|
|
283
|
+
factories.push(`const ${id} = ${nameTemplate}(\`${html}\`);`);
|
|
284
|
+
}
|
|
285
|
+
code = generateImports() + '\n\n' + factories.join('\n') + '\n\n' + code;
|
|
286
|
+
}
|
|
287
|
+
return { changed, code };
|
|
288
|
+
};
|
|
289
|
+
const generateReactiveInlining = (calls, code, sourceFile) => {
|
|
290
|
+
if (calls.length === 0) {
|
|
291
|
+
return code;
|
|
292
|
+
}
|
|
293
|
+
let printer = ts.createPrinter({ newLine: ts.NewLineKind.LineFeed }), result = code;
|
|
294
|
+
for (let i = calls.length - 1; i >= 0; i--) {
|
|
295
|
+
let call = calls[i];
|
|
296
|
+
result = result.slice(0, call.start);
|
|
297
|
+
result += `new ${nameArraySlot}(
|
|
298
|
+
${printer.printNode(ts.EmitHint.Expression, call.arrayArg, sourceFile)},
|
|
299
|
+
${printer.printNode(ts.EmitHint.Expression, call.callbackArg, sourceFile)}
|
|
300
|
+
)`;
|
|
301
|
+
result += result.slice(call.end);
|
|
302
|
+
}
|
|
303
|
+
return result;
|
|
304
|
+
};
|
|
305
|
+
const getNames = () => ({
|
|
306
|
+
attr: nameAttr,
|
|
307
|
+
event: nameEvent,
|
|
308
|
+
slot: nameSlot
|
|
309
|
+
});
|
|
310
|
+
const needsArraySlotImport = (sourceFile) => {
|
|
311
|
+
return hasArraySlotUsage(sourceFile) && !hasArraySlotImport(sourceFile);
|
|
312
|
+
};
|
|
313
|
+
const setTypeChecker = (checker) => {
|
|
314
|
+
currentChecker = checker;
|
|
315
|
+
};
|
|
316
|
+
export { addArraySlotImport, generateCode, generateReactiveInlining, getNames, needsArraySlotImport, setTypeChecker };
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { mightNeedTransform } from '@esportsplus/typescript/transformer';
|
|
2
|
+
import ts from 'typescript';
|
|
3
|
+
type TransformResult = {
|
|
4
|
+
changed: boolean;
|
|
5
|
+
code: string;
|
|
6
|
+
sourceFile: ts.SourceFile;
|
|
7
|
+
};
|
|
8
|
+
declare const PATTERNS: string[];
|
|
9
|
+
declare function createTransformer(program: ts.Program): ts.TransformerFactory<ts.SourceFile>;
|
|
10
|
+
declare const transform: (sourceFile: ts.SourceFile, program: ts.Program) => TransformResult;
|
|
11
|
+
export type { TransformResult };
|
|
12
|
+
export { createTransformer, mightNeedTransform, PATTERNS, transform };
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { mightNeedTransform } from '@esportsplus/typescript/transformer';
|
|
2
|
+
import { addArraySlotImport, generateCode, generateReactiveInlining, needsArraySlotImport, setTypeChecker } from './codegen.js';
|
|
3
|
+
import { findHtmlTemplates, findReactiveCalls } from './ts-parser.js';
|
|
4
|
+
import ts from 'typescript';
|
|
5
|
+
const PATTERNS = ['html`', 'html.reactive'];
|
|
6
|
+
function createTransformer(program) {
|
|
7
|
+
let typeChecker = program.getTypeChecker();
|
|
8
|
+
return (_context) => {
|
|
9
|
+
return (sourceFile) => {
|
|
10
|
+
let code = sourceFile.getFullText();
|
|
11
|
+
if (!mightNeedTransform(code, { patterns: PATTERNS })) {
|
|
12
|
+
return sourceFile;
|
|
13
|
+
}
|
|
14
|
+
setTypeChecker(typeChecker);
|
|
15
|
+
let result = transformCode(code, sourceFile);
|
|
16
|
+
if (!result.changed) {
|
|
17
|
+
return sourceFile;
|
|
18
|
+
}
|
|
19
|
+
return result.sourceFile;
|
|
20
|
+
};
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
function transformCode(code, sourceFile) {
|
|
24
|
+
let changed = false, result = code;
|
|
25
|
+
let reactiveCalls = findReactiveCalls(sourceFile);
|
|
26
|
+
if (reactiveCalls.length > 0) {
|
|
27
|
+
result = generateReactiveInlining(reactiveCalls, result, sourceFile);
|
|
28
|
+
changed = true;
|
|
29
|
+
sourceFile = ts.createSourceFile(sourceFile.fileName, result, sourceFile.languageVersion, true);
|
|
30
|
+
if (needsArraySlotImport(sourceFile)) {
|
|
31
|
+
result = addArraySlotImport(result);
|
|
32
|
+
sourceFile = ts.createSourceFile(sourceFile.fileName, result, sourceFile.languageVersion, true);
|
|
33
|
+
}
|
|
34
|
+
setTypeChecker(undefined);
|
|
35
|
+
}
|
|
36
|
+
let templates = findHtmlTemplates(sourceFile);
|
|
37
|
+
if (templates.length > 0) {
|
|
38
|
+
let codegenResult = generateCode(templates, result, sourceFile);
|
|
39
|
+
if (codegenResult.changed) {
|
|
40
|
+
changed = true;
|
|
41
|
+
result = codegenResult.code;
|
|
42
|
+
sourceFile = ts.createSourceFile(sourceFile.fileName, result, sourceFile.languageVersion, true);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
return { changed, code: result, sourceFile };
|
|
46
|
+
}
|
|
47
|
+
const transform = (sourceFile, program) => {
|
|
48
|
+
let code = sourceFile.getFullText();
|
|
49
|
+
if (!mightNeedTransform(code, { patterns: PATTERNS })) {
|
|
50
|
+
return { changed: false, code, sourceFile };
|
|
51
|
+
}
|
|
52
|
+
let programSourceFile = program.getSourceFile(sourceFile.fileName);
|
|
53
|
+
if (programSourceFile) {
|
|
54
|
+
setTypeChecker(program.getTypeChecker());
|
|
55
|
+
sourceFile = programSourceFile;
|
|
56
|
+
}
|
|
57
|
+
else {
|
|
58
|
+
setTypeChecker(undefined);
|
|
59
|
+
}
|
|
60
|
+
return transformCode(code, sourceFile);
|
|
61
|
+
};
|
|
62
|
+
export { createTransformer, mightNeedTransform, PATTERNS, transform };
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
type NodePath = ('firstChild' | 'firstElementChild' | 'nextElementSibling' | 'nextSibling')[];
|
|
2
|
+
declare const _default: {
|
|
3
|
+
parse: (literals: string[]) => {
|
|
4
|
+
html: string;
|
|
5
|
+
slots: ({
|
|
6
|
+
path: NodePath;
|
|
7
|
+
type: "slot";
|
|
8
|
+
} | {
|
|
9
|
+
attributes: {
|
|
10
|
+
names: string[];
|
|
11
|
+
statics: Record<string, string>;
|
|
12
|
+
};
|
|
13
|
+
path: NodePath;
|
|
14
|
+
type: "attributes";
|
|
15
|
+
})[] | null;
|
|
16
|
+
};
|
|
17
|
+
};
|
|
18
|
+
export default _default;
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
const SLOT_HTML = '<!--$-->';
|
|
2
|
+
const ATTRIBUTE_DELIMITERS = {
|
|
3
|
+
class: ' ',
|
|
4
|
+
style: ';'
|
|
5
|
+
};
|
|
6
|
+
const NODE_CLOSING = 1;
|
|
7
|
+
const NODE_COMMENT = 2;
|
|
8
|
+
const NODE_ELEMENT = 3;
|
|
9
|
+
const NODE_SLOT = 4;
|
|
10
|
+
const NODE_VOID = 5;
|
|
11
|
+
const NODE_WHITELIST = {
|
|
12
|
+
'!': NODE_COMMENT,
|
|
13
|
+
'/': NODE_CLOSING,
|
|
14
|
+
'area': NODE_VOID,
|
|
15
|
+
'base': NODE_VOID,
|
|
16
|
+
'br': NODE_VOID,
|
|
17
|
+
'col': NODE_VOID,
|
|
18
|
+
'embed': NODE_VOID,
|
|
19
|
+
'hr': NODE_VOID,
|
|
20
|
+
'img': NODE_VOID,
|
|
21
|
+
'input': NODE_VOID,
|
|
22
|
+
'keygen': NODE_VOID,
|
|
23
|
+
'link': NODE_VOID,
|
|
24
|
+
'menuitem': NODE_VOID,
|
|
25
|
+
'meta': NODE_VOID,
|
|
26
|
+
'param': NODE_VOID,
|
|
27
|
+
'source': NODE_VOID,
|
|
28
|
+
'track': NODE_VOID,
|
|
29
|
+
'wbr': NODE_VOID
|
|
30
|
+
};
|
|
31
|
+
const REGEX_EMPTY_ATTRIBUTES = /\s+[\w:-]+\s*=\s*["']\s*["']|\s+(?=>)/g;
|
|
32
|
+
const REGEX_EMPTY_TEXT_NODES = /(>|}|\s)\s+(<|{|\s)/g;
|
|
33
|
+
const REGEX_EVENTS = /(?:\s*on[\w-:]+\s*=(?:\s*["'][^"']*["'])*)/g;
|
|
34
|
+
const REGEX_SLOT_ATTRIBUTES = /<[\w-]+([^><]*{{\$}}[^><]*)>/g;
|
|
35
|
+
const REGEX_SLOT_NODES = /<([\w-]+|[\/!])(?:([^><]*{{\$}}[^><]*)|(?:[^><]*))?>|{{\$}}/g;
|
|
36
|
+
const SLOT_MARKER = '{{$}}';
|
|
37
|
+
function methods(children, copy, first, next) {
|
|
38
|
+
let length = copy.length, result = new Array(length + 1 + children);
|
|
39
|
+
for (let i = 0, n = length; i < n; i++) {
|
|
40
|
+
result[i] = copy[i];
|
|
41
|
+
}
|
|
42
|
+
result[length] = first;
|
|
43
|
+
for (let i = 0, n = children; i < n; i++) {
|
|
44
|
+
result[length + 1 + i] = next;
|
|
45
|
+
}
|
|
46
|
+
return result;
|
|
47
|
+
}
|
|
48
|
+
const parse = (literals) => {
|
|
49
|
+
let html = literals
|
|
50
|
+
.join(SLOT_MARKER)
|
|
51
|
+
.replace(REGEX_EMPTY_TEXT_NODES, '$1$2')
|
|
52
|
+
.trim(), n = literals.length - 1;
|
|
53
|
+
if (n === 0) {
|
|
54
|
+
return { html, slots: null };
|
|
55
|
+
}
|
|
56
|
+
let attributes = {}, buffer = '', events = false, index = 0, level = 0, levels = [{ children: 0, elements: 0, path: [] }], parsed = html.split(SLOT_MARKER), slot = 0, slots = [];
|
|
57
|
+
{
|
|
58
|
+
let attribute = '', buffer = '', char = '', quote = '';
|
|
59
|
+
for (let match of html.matchAll(REGEX_SLOT_ATTRIBUTES)) {
|
|
60
|
+
let found = match[1];
|
|
61
|
+
if (attributes[found]) {
|
|
62
|
+
continue;
|
|
63
|
+
}
|
|
64
|
+
let { names, statics } = attributes[found] = { names: [], statics: {} };
|
|
65
|
+
for (let i = 0, n = found.length; i < n; i++) {
|
|
66
|
+
char = found[i];
|
|
67
|
+
if (char === ' ') {
|
|
68
|
+
buffer = '';
|
|
69
|
+
}
|
|
70
|
+
else if (char === '=') {
|
|
71
|
+
attribute = buffer;
|
|
72
|
+
buffer = '';
|
|
73
|
+
}
|
|
74
|
+
else if (char === '"' || char === "'") {
|
|
75
|
+
if (!attribute) {
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
78
|
+
else if (!quote) {
|
|
79
|
+
quote = char;
|
|
80
|
+
}
|
|
81
|
+
else if (quote === char) {
|
|
82
|
+
if (attribute) {
|
|
83
|
+
statics[attribute] ??= '';
|
|
84
|
+
statics[attribute] += `${ATTRIBUTE_DELIMITERS[attribute] || ''}${buffer}`;
|
|
85
|
+
}
|
|
86
|
+
attribute = '';
|
|
87
|
+
buffer = '';
|
|
88
|
+
quote = '';
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
else if (char === '{' && char !== buffer) {
|
|
92
|
+
buffer = char;
|
|
93
|
+
}
|
|
94
|
+
else {
|
|
95
|
+
buffer += char;
|
|
96
|
+
if (buffer === SLOT_MARKER) {
|
|
97
|
+
buffer = '';
|
|
98
|
+
if (attribute) {
|
|
99
|
+
names.push(attribute);
|
|
100
|
+
if (!quote) {
|
|
101
|
+
attribute = '';
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
else {
|
|
105
|
+
names.push('spread');
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
else if (buffer === 'on') {
|
|
109
|
+
events = true;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
{
|
|
116
|
+
for (let match of html.matchAll(REGEX_SLOT_NODES)) {
|
|
117
|
+
let parent = levels[level], type = match[1] === undefined ? NODE_SLOT : (NODE_WHITELIST[match[1].toLowerCase()] || NODE_ELEMENT);
|
|
118
|
+
if ((match.index || 1) - 1 > index) {
|
|
119
|
+
parent.children++;
|
|
120
|
+
}
|
|
121
|
+
if (type === NODE_ELEMENT || type === NODE_VOID) {
|
|
122
|
+
let attr = match[2], path = parent.path.length
|
|
123
|
+
? methods(parent.elements, parent.path, 'firstElementChild', 'nextElementSibling')
|
|
124
|
+
: methods(parent.children, [], 'firstChild', 'nextSibling');
|
|
125
|
+
if (attr) {
|
|
126
|
+
let attrs = attributes[attr];
|
|
127
|
+
if (!attrs) {
|
|
128
|
+
throw new Error(`@esportsplus/template: attribute metadata could not be found for '${attr}'`);
|
|
129
|
+
}
|
|
130
|
+
slots.push({ attributes: attrs, path, type: 'attributes' });
|
|
131
|
+
for (let i = 0, n = attrs.names.length; i < n; i++) {
|
|
132
|
+
buffer += parsed[slot++];
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
if (type === NODE_ELEMENT) {
|
|
136
|
+
levels[++level] = { children: 0, elements: 0, path };
|
|
137
|
+
}
|
|
138
|
+
parent.elements++;
|
|
139
|
+
}
|
|
140
|
+
else if (type === NODE_SLOT) {
|
|
141
|
+
buffer += parsed[slot++] + SLOT_HTML;
|
|
142
|
+
slots.push({ path: methods(parent.children, parent.path, 'firstChild', 'nextSibling'), type: 'slot' });
|
|
143
|
+
}
|
|
144
|
+
if (n === slot) {
|
|
145
|
+
buffer += parsed[slot];
|
|
146
|
+
break;
|
|
147
|
+
}
|
|
148
|
+
if (type === NODE_CLOSING) {
|
|
149
|
+
level--;
|
|
150
|
+
}
|
|
151
|
+
else {
|
|
152
|
+
parent.children++;
|
|
153
|
+
}
|
|
154
|
+
index = (match.index || 0) + match[0].length;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
if (events) {
|
|
158
|
+
buffer = buffer.replace(REGEX_EVENTS, '');
|
|
159
|
+
}
|
|
160
|
+
buffer = buffer.replace(REGEX_EMPTY_ATTRIBUTES, '');
|
|
161
|
+
return {
|
|
162
|
+
html: buffer,
|
|
163
|
+
slots: slots.length ? slots : null
|
|
164
|
+
};
|
|
165
|
+
};
|
|
166
|
+
export default { parse };
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { mightNeedTransform, PATTERNS, transform } from '../index.js';
|
|
2
|
+
import { program, TRANSFORM_PATTERN } from '@esportsplus/typescript/transformer';
|
|
3
|
+
import fs from 'fs';
|
|
4
|
+
import ts from 'typescript';
|
|
5
|
+
export default (options) => {
|
|
6
|
+
let root = options?.root ?? process.cwd();
|
|
7
|
+
return {
|
|
8
|
+
name: '@esportsplus/template/plugin-esbuild',
|
|
9
|
+
setup(build) {
|
|
10
|
+
build.onLoad({ filter: TRANSFORM_PATTERN }, async (args) => {
|
|
11
|
+
let code = await fs.promises.readFile(args.path, 'utf8');
|
|
12
|
+
if (!mightNeedTransform(code, { patterns: PATTERNS })) {
|
|
13
|
+
return null;
|
|
14
|
+
}
|
|
15
|
+
try {
|
|
16
|
+
let sourceFile = ts.createSourceFile(args.path, code, ts.ScriptTarget.Latest, true), result = transform(sourceFile, program.get(root));
|
|
17
|
+
if (!result.changed) {
|
|
18
|
+
return null;
|
|
19
|
+
}
|
|
20
|
+
return {
|
|
21
|
+
contents: result.code,
|
|
22
|
+
loader: args.path.endsWith('x') ? 'tsx' : 'ts'
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
catch (error) {
|
|
26
|
+
console.error(`@esportsplus/template: Error transforming ${args.path}:`, error);
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
29
|
+
});
|
|
30
|
+
build.onEnd(() => {
|
|
31
|
+
program.delete(root);
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
};
|
|
35
|
+
};
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { mightNeedTransform, PATTERNS, transform } from '../index.js';
|
|
2
|
+
import { program, TRANSFORM_PATTERN } from '@esportsplus/typescript/transformer';
|
|
3
|
+
import ts from 'typescript';
|
|
4
|
+
export default (options) => {
|
|
5
|
+
let root;
|
|
6
|
+
return {
|
|
7
|
+
enforce: 'pre',
|
|
8
|
+
name: '@esportsplus/template/plugin-vite',
|
|
9
|
+
configResolved(config) {
|
|
10
|
+
root = options?.root ?? config.root;
|
|
11
|
+
},
|
|
12
|
+
transform(code, id) {
|
|
13
|
+
if (!TRANSFORM_PATTERN.test(id) || id.includes('node_modules')) {
|
|
14
|
+
return null;
|
|
15
|
+
}
|
|
16
|
+
if (!mightNeedTransform(code, { patterns: PATTERNS })) {
|
|
17
|
+
return null;
|
|
18
|
+
}
|
|
19
|
+
try {
|
|
20
|
+
let sourceFile = ts.createSourceFile(id, code, ts.ScriptTarget.Latest, true), result = transform(sourceFile, program.get(root));
|
|
21
|
+
if (!result.changed) {
|
|
22
|
+
return null;
|
|
23
|
+
}
|
|
24
|
+
return { code: result.code, map: null };
|
|
25
|
+
}
|
|
26
|
+
catch (error) {
|
|
27
|
+
console.error(`@esportsplus/template: Error transforming ${id}:`, error);
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
30
|
+
},
|
|
31
|
+
watchChange(id) {
|
|
32
|
+
if (TRANSFORM_PATTERN.test(id)) {
|
|
33
|
+
program.delete(root);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
};
|
|
37
|
+
};
|