@esportsplus/template 0.28.1 → 0.29.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.
Files changed (112) hide show
  1. package/.github/workflows/bump.yml +2 -2
  2. package/.github/workflows/dependabot.yml +1 -1
  3. package/.github/workflows/publish.yml +2 -2
  4. package/build/attributes.d.ts +7 -1
  5. package/build/attributes.js +86 -33
  6. package/build/constants.d.ts +3 -11
  7. package/build/constants.js +4 -32
  8. package/build/event/constants.d.ts +3 -0
  9. package/build/event/constants.js +13 -0
  10. package/build/event/index.d.ts +9 -1
  11. package/build/event/index.js +29 -35
  12. package/build/event/ontick.js +6 -9
  13. package/build/html.d.ts +9 -0
  14. package/build/html.js +7 -0
  15. package/build/index.d.ts +8 -2
  16. package/build/index.js +8 -1
  17. package/build/render.d.ts +2 -2
  18. package/build/render.js +2 -3
  19. package/build/runtime.d.ts +1 -0
  20. package/build/runtime.js +5 -0
  21. package/build/slot/array.d.ts +3 -3
  22. package/build/slot/array.js +11 -14
  23. package/build/slot/cleanup.d.ts +1 -1
  24. package/build/slot/cleanup.js +1 -2
  25. package/build/slot/effect.js +5 -7
  26. package/build/slot/index.js +1 -7
  27. package/build/slot/render.js +6 -8
  28. package/build/svg.d.ts +1 -1
  29. package/build/svg.js +1 -1
  30. package/build/transformer/codegen.d.ts +18 -0
  31. package/build/transformer/codegen.js +316 -0
  32. package/build/transformer/index.d.ts +12 -0
  33. package/build/transformer/index.js +62 -0
  34. package/build/transformer/parser.d.ts +18 -0
  35. package/build/transformer/parser.js +166 -0
  36. package/build/transformer/plugins/esbuild.d.ts +5 -0
  37. package/build/transformer/plugins/esbuild.js +35 -0
  38. package/build/transformer/plugins/tsc.d.ts +3 -0
  39. package/build/transformer/plugins/tsc.js +4 -0
  40. package/build/transformer/plugins/vite.d.ts +5 -0
  41. package/build/transformer/plugins/vite.js +37 -0
  42. package/build/transformer/ts-parser.d.ts +21 -0
  43. package/build/transformer/ts-parser.js +72 -0
  44. package/build/transformer/type-analyzer.d.ts +7 -0
  45. package/build/transformer/type-analyzer.js +230 -0
  46. package/build/types.d.ts +2 -3
  47. package/build/utilities.d.ts +7 -0
  48. package/build/utilities.js +31 -0
  49. package/package.json +11 -4
  50. package/src/attributes.ts +115 -51
  51. package/src/constants.ts +6 -53
  52. package/src/event/constants.ts +16 -0
  53. package/src/event/index.ts +36 -42
  54. package/src/event/onconnect.ts +1 -1
  55. package/src/event/onresize.ts +1 -1
  56. package/src/event/ontick.ts +7 -11
  57. package/src/html.ts +18 -0
  58. package/src/index.ts +8 -2
  59. package/src/render.ts +6 -7
  60. package/src/runtime.ts +8 -0
  61. package/src/slot/array.ts +18 -24
  62. package/src/slot/cleanup.ts +3 -4
  63. package/src/slot/effect.ts +6 -8
  64. package/src/slot/index.ts +2 -8
  65. package/src/slot/render.ts +7 -9
  66. package/src/svg.ts +1 -1
  67. package/src/transformer/codegen.ts +518 -0
  68. package/src/transformer/index.ts +98 -0
  69. package/src/transformer/parser.ts +239 -0
  70. package/src/transformer/plugins/esbuild.ts +46 -0
  71. package/src/transformer/plugins/tsc.ts +7 -0
  72. package/src/transformer/plugins/vite.ts +49 -0
  73. package/src/transformer/ts-parser.ts +123 -0
  74. package/src/transformer/type-analyzer.ts +334 -0
  75. package/src/types.ts +3 -4
  76. package/src/utilities.ts +52 -0
  77. package/storage/rewrite-analysis-2026-01-04.md +439 -0
  78. package/test/constants.ts +69 -0
  79. package/test/effects.ts +237 -0
  80. package/test/events.ts +318 -0
  81. package/test/imported-values.ts +253 -0
  82. package/test/nested.ts +298 -0
  83. package/test/slots.ts +259 -0
  84. package/test/spread.ts +290 -0
  85. package/test/static.ts +118 -0
  86. package/test/templates.ts +473 -0
  87. package/test/tsconfig.json +17 -0
  88. package/test/vite.config.ts +50 -0
  89. package/build/html/index.d.ts +0 -9
  90. package/build/html/index.js +0 -29
  91. package/build/html/parser.d.ts +0 -5
  92. package/build/html/parser.js +0 -165
  93. package/build/utilities/element.d.ts +0 -11
  94. package/build/utilities/element.js +0 -9
  95. package/build/utilities/fragment.d.ts +0 -3
  96. package/build/utilities/fragment.js +0 -10
  97. package/build/utilities/marker.d.ts +0 -2
  98. package/build/utilities/marker.js +0 -4
  99. package/build/utilities/node.d.ts +0 -9
  100. package/build/utilities/node.js +0 -10
  101. package/build/utilities/raf.d.ts +0 -2
  102. package/build/utilities/raf.js +0 -1
  103. package/build/utilities/text.d.ts +0 -2
  104. package/build/utilities/text.js +0 -9
  105. package/src/html/index.ts +0 -48
  106. package/src/html/parser.ts +0 -235
  107. package/src/utilities/element.ts +0 -28
  108. package/src/utilities/fragment.ts +0 -19
  109. package/src/utilities/marker.ts +0 -6
  110. package/src/utilities/node.ts +0 -29
  111. package/src/utilities/raf.ts +0 -1
  112. package/src/utilities/text.ts +0 -15
@@ -1,4 +1,4 @@
1
- import { SlotGroup } from '../types.js';
1
+ import { Element, SlotGroup } from '../types.js';
2
2
  declare const ondisconnect: (element: Element, fn: VoidFunction) => void;
3
3
  declare const remove: (...groups: SlotGroup[]) => void;
4
4
  export { ondisconnect, remove };
@@ -1,5 +1,4 @@
1
1
  import { CLEANUP } from '../constants.js';
2
- import { previousSibling } from '../utilities/node.js';
3
2
  const ondisconnect = (element, fn) => {
4
3
  (element[CLEANUP] ??= []).push(fn);
5
4
  };
@@ -13,7 +12,7 @@ const remove = (...groups) => {
13
12
  fn();
14
13
  }
15
14
  }
16
- next = previousSibling.call(tail);
15
+ next = tail.previousSibling;
17
16
  tail.remove();
18
17
  if (head === tail) {
19
18
  break;
@@ -1,8 +1,6 @@
1
1
  import { effect } from '@esportsplus/reactivity';
2
- import { firstChild, lastChild, nodeValue } from '../utilities/node.js';
3
2
  import { remove } from './cleanup.js';
4
- import raf from '../utilities/raf.js';
5
- import text from '../utilities/text.js';
3
+ import { raf, text } from '../utilities.js';
6
4
  import render from './render.js';
7
5
  function read(value) {
8
6
  if (typeof value === 'function') {
@@ -60,7 +58,7 @@ class EffectSlot {
60
58
  value = String(value);
61
59
  }
62
60
  if (textnode) {
63
- nodeValue.call(textnode, value);
61
+ textnode.nodeValue = value;
64
62
  if (!textnode.isConnected) {
65
63
  anchor.after(textnode);
66
64
  }
@@ -70,14 +68,14 @@ class EffectSlot {
70
68
  }
71
69
  }
72
70
  else {
73
- let fragment = render(anchor, value), head = firstChild.call(fragment);
71
+ let fragment = render(anchor, value), head = fragment.firstChild;
74
72
  if (textnode?.isConnected) {
75
73
  remove({ head: textnode, tail: textnode });
76
74
  }
77
75
  if (head) {
78
76
  this.group = {
79
- head,
80
- tail: lastChild.call(fragment)
77
+ head: head,
78
+ tail: fragment.lastChild
81
79
  };
82
80
  anchor.after(fragment);
83
81
  }
@@ -1,10 +1,4 @@
1
- import { EffectSlot } from './effect.js';
2
1
  import render from './render.js';
3
2
  export default (anchor, value) => {
4
- if (typeof value === 'function') {
5
- new EffectSlot(anchor, value);
6
- }
7
- else {
8
- anchor.after(render(anchor, value));
9
- }
3
+ anchor.after(render(anchor, value));
10
4
  };
@@ -1,8 +1,6 @@
1
1
  import { isArray } from '@esportsplus/utilities';
2
2
  import { ARRAY_SLOT, EMPTY_FRAGMENT } from '../constants.js';
3
- import { cloneNode, lastChild } from '../utilities/node.js';
4
- import { append } from '../utilities/fragment.js';
5
- import text from '../utilities/text.js';
3
+ import { clone, text } from '../utilities.js';
6
4
  export default function render(anchor, value) {
7
5
  if (value == null || value === false || value === '') {
8
6
  return EMPTY_FRAGMENT;
@@ -26,17 +24,17 @@ export default function render(anchor, value) {
26
24
  }
27
25
  }
28
26
  if (isArray(value)) {
29
- let fragment = cloneNode.call(EMPTY_FRAGMENT);
27
+ let fragment = clone(EMPTY_FRAGMENT);
30
28
  for (let i = 0; i < n; i++) {
31
- append.call(fragment, render(anchor, value[i]));
32
- anchor = lastChild.call(fragment);
29
+ fragment.append(render(anchor, value[i]));
30
+ anchor = fragment.lastChild;
33
31
  }
34
32
  return fragment;
35
33
  }
36
34
  if (value instanceof NodeList) {
37
- let fragment = cloneNode.call(EMPTY_FRAGMENT);
35
+ let fragment = clone(EMPTY_FRAGMENT);
38
36
  for (let i = 0; i < n; i++) {
39
- append.call(fragment, value[i]);
37
+ fragment.append(value[i]);
40
38
  }
41
39
  return fragment;
42
40
  }
package/build/svg.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import html from './html/index.js';
1
+ import html from './html.js';
2
2
  declare const svg: typeof html & {
3
3
  sprite: (href: string) => ReturnType<typeof html>;
4
4
  };
package/build/svg.js CHANGED
@@ -1,4 +1,4 @@
1
- import html from './html/index.js';
1
+ import html from './html.js';
2
2
  const svg = html.bind(null);
3
3
  svg.sprite = (href) => {
4
4
  if (href[0] !== '#') {
@@ -0,0 +1,18 @@
1
+ import type { ReactiveCallInfo, TemplateInfo } from './ts-parser.js';
2
+ import ts from 'typescript';
3
+ type CodegenResult = {
4
+ changed: boolean;
5
+ code: string;
6
+ };
7
+ declare const addArraySlotImport: (code: string) => string;
8
+ declare const generateCode: (templates: TemplateInfo[], originalCode: string, sourceFile: ts.SourceFile) => CodegenResult;
9
+ declare const generateReactiveInlining: (calls: ReactiveCallInfo[], code: string, sourceFile: ts.SourceFile) => string;
10
+ declare const getNames: () => {
11
+ attr: string;
12
+ event: string;
13
+ slot: string;
14
+ };
15
+ declare const needsArraySlotImport: (sourceFile: ts.SourceFile) => boolean;
16
+ declare const setTypeChecker: (checker: ts.TypeChecker | undefined) => void;
17
+ export { addArraySlotImport, generateCode, generateReactiveInlining, getNames, needsArraySlotImport, setTypeChecker };
18
+ export type { CodegenResult };
@@ -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;