@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.
Files changed (110) hide show
  1. package/README.md +431 -0
  2. package/build/attributes.d.ts +7 -1
  3. package/build/attributes.js +86 -33
  4. package/build/constants.d.ts +3 -11
  5. package/build/constants.js +4 -32
  6. package/build/event/constants.d.ts +3 -0
  7. package/build/event/constants.js +13 -0
  8. package/build/event/index.d.ts +9 -1
  9. package/build/event/index.js +29 -35
  10. package/build/event/ontick.js +6 -9
  11. package/build/html.d.ts +9 -0
  12. package/build/html.js +7 -0
  13. package/build/index.d.ts +8 -2
  14. package/build/index.js +8 -1
  15. package/build/render.d.ts +2 -2
  16. package/build/render.js +2 -3
  17. package/build/runtime.d.ts +1 -0
  18. package/build/runtime.js +5 -0
  19. package/build/slot/array.d.ts +3 -3
  20. package/build/slot/array.js +11 -14
  21. package/build/slot/cleanup.d.ts +1 -1
  22. package/build/slot/cleanup.js +1 -2
  23. package/build/slot/effect.js +5 -7
  24. package/build/slot/index.js +1 -7
  25. package/build/slot/render.js +6 -8
  26. package/build/svg.d.ts +1 -1
  27. package/build/svg.js +1 -1
  28. package/build/transformer/codegen.d.ts +18 -0
  29. package/build/transformer/codegen.js +316 -0
  30. package/build/transformer/index.d.ts +12 -0
  31. package/build/transformer/index.js +62 -0
  32. package/build/transformer/parser.d.ts +18 -0
  33. package/build/transformer/parser.js +166 -0
  34. package/build/transformer/plugins/esbuild.d.ts +5 -0
  35. package/build/transformer/plugins/esbuild.js +35 -0
  36. package/build/transformer/plugins/tsc.d.ts +3 -0
  37. package/build/transformer/plugins/tsc.js +4 -0
  38. package/build/transformer/plugins/vite.d.ts +5 -0
  39. package/build/transformer/plugins/vite.js +37 -0
  40. package/build/transformer/ts-parser.d.ts +21 -0
  41. package/build/transformer/ts-parser.js +72 -0
  42. package/build/transformer/type-analyzer.d.ts +7 -0
  43. package/build/transformer/type-analyzer.js +230 -0
  44. package/build/types.d.ts +2 -3
  45. package/build/utilities.d.ts +7 -0
  46. package/build/utilities.js +31 -0
  47. package/package.json +33 -4
  48. package/src/attributes.ts +115 -51
  49. package/src/constants.ts +6 -53
  50. package/src/event/constants.ts +16 -0
  51. package/src/event/index.ts +36 -42
  52. package/src/event/onconnect.ts +1 -1
  53. package/src/event/onresize.ts +1 -1
  54. package/src/event/ontick.ts +7 -11
  55. package/src/html.ts +18 -0
  56. package/src/index.ts +8 -2
  57. package/src/render.ts +6 -7
  58. package/src/runtime.ts +8 -0
  59. package/src/slot/array.ts +18 -24
  60. package/src/slot/cleanup.ts +3 -4
  61. package/src/slot/effect.ts +6 -8
  62. package/src/slot/index.ts +2 -8
  63. package/src/slot/render.ts +7 -9
  64. package/src/svg.ts +1 -1
  65. package/src/transformer/codegen.ts +518 -0
  66. package/src/transformer/index.ts +98 -0
  67. package/src/transformer/parser.ts +239 -0
  68. package/src/transformer/plugins/esbuild.ts +46 -0
  69. package/src/transformer/plugins/tsc.ts +7 -0
  70. package/src/transformer/plugins/vite.ts +49 -0
  71. package/src/transformer/ts-parser.ts +123 -0
  72. package/src/transformer/type-analyzer.ts +334 -0
  73. package/src/types.ts +3 -4
  74. package/src/utilities.ts +52 -0
  75. package/storage/rewrite-analysis-2026-01-04.md +439 -0
  76. package/test/constants.ts +69 -0
  77. package/test/effects.ts +237 -0
  78. package/test/events.ts +318 -0
  79. package/test/imported-values.ts +253 -0
  80. package/test/nested.ts +298 -0
  81. package/test/slots.ts +259 -0
  82. package/test/spread.ts +290 -0
  83. package/test/static.ts +118 -0
  84. package/test/templates.ts +473 -0
  85. package/test/tsconfig.json +17 -0
  86. package/test/vite.config.ts +50 -0
  87. package/build/html/index.d.ts +0 -9
  88. package/build/html/index.js +0 -29
  89. package/build/html/parser.d.ts +0 -5
  90. package/build/html/parser.js +0 -165
  91. package/build/utilities/element.d.ts +0 -11
  92. package/build/utilities/element.js +0 -9
  93. package/build/utilities/fragment.d.ts +0 -3
  94. package/build/utilities/fragment.js +0 -10
  95. package/build/utilities/marker.d.ts +0 -2
  96. package/build/utilities/marker.js +0 -4
  97. package/build/utilities/node.d.ts +0 -9
  98. package/build/utilities/node.js +0 -10
  99. package/build/utilities/raf.d.ts +0 -2
  100. package/build/utilities/raf.js +0 -1
  101. package/build/utilities/text.d.ts +0 -2
  102. package/build/utilities/text.js +0 -9
  103. package/src/html/index.ts +0 -48
  104. package/src/html/parser.ts +0 -235
  105. package/src/utilities/element.ts +0 -28
  106. package/src/utilities/fragment.ts +0 -19
  107. package/src/utilities/marker.ts +0 -6
  108. package/src/utilities/node.ts +0 -29
  109. package/src/utilities/raf.ts +0 -1
  110. 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,5 @@
1
+ import type { Plugin } from 'esbuild';
2
+ declare const _default: (options?: {
3
+ root?: string;
4
+ }) => Plugin;
5
+ export default _default;
@@ -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,3 @@
1
+ import ts from 'typescript';
2
+ declare const _default: (program: ts.Program) => ts.TransformerFactory<ts.SourceFile>;
3
+ export default _default;
@@ -0,0 +1,4 @@
1
+ import { createTransformer } from '../index.js';
2
+ export default (program) => {
3
+ return createTransformer(program);
4
+ };
@@ -0,0 +1,5 @@
1
+ import type { Plugin } from 'vite';
2
+ declare const _default: (options?: {
3
+ root?: string;
4
+ }) => Plugin;
5
+ export default _default;
@@ -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
+ };