@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
@@ -0,0 +1,518 @@
1
+ import { addImport, applyReplacementsReverse, uid } from '@esportsplus/typescript/transformer';
2
+ import type { Replacement } from '@esportsplus/typescript/transformer';
3
+ import type { ReactiveCallInfo, TemplateInfo } from './ts-parser';
4
+ import { analyzeExpression, generateAttributeBinding, generateSpreadBindings } from './type-analyzer';
5
+ import ts from 'typescript';
6
+ import parser from './parser';
7
+
8
+
9
+ type AttributeSlot = {
10
+ attributes: {
11
+ names: string[];
12
+ statics: Record<string, string>;
13
+ };
14
+ path: string[];
15
+ type: 'attributes';
16
+ };
17
+
18
+ type CodegenResult = {
19
+ changed: boolean;
20
+ code: string;
21
+ };
22
+
23
+ type NodeSlot = {
24
+ path: string[];
25
+ type: 'slot';
26
+ };
27
+
28
+ type ParseResult = {
29
+ html: string;
30
+ slots: (AttributeSlot | NodeSlot)[] | null;
31
+ };
32
+
33
+
34
+ const ARROW_EMPTY_PARAMS = /\(\s*\)\s*=>\s*$/;
35
+
36
+
37
+ let currentChecker: ts.TypeChecker | undefined,
38
+ hoistedFactories = new Map<string, string>(),
39
+ htmlToTemplateId = new Map<string, string>(),
40
+ nameArraySlot = '',
41
+ nameAttr = '',
42
+ nameEffectSlot = '',
43
+ nameEvent = '',
44
+ nameSlot = '',
45
+ nameTemplate = '',
46
+ needsArraySlot = false,
47
+ needsAttr = false,
48
+ needsEffectSlot = false,
49
+ needsEvent = false,
50
+ needsSlot = false;
51
+
52
+
53
+ function collectNestedTemplateReplacements(
54
+ node: ts.Node,
55
+ exprStart: number,
56
+ sourceFile: ts.SourceFile,
57
+ replacements: Replacement[]
58
+ ): void {
59
+ if (isNestedHtmlTemplate(node as ts.Expression)) {
60
+ replacements.push({
61
+ end: node.end - exprStart,
62
+ newText: generateNestedTemplateCode(node as ts.TaggedTemplateExpression, sourceFile),
63
+ start: node.getStart() - exprStart
64
+ });
65
+ }
66
+ else {
67
+ ts.forEachChild(node, child => collectNestedTemplateReplacements(child, exprStart, sourceFile, replacements));
68
+ }
69
+ }
70
+
71
+ function generateImports(): string {
72
+ let specifiers: string[] = [];
73
+
74
+ if (needsArraySlot) {
75
+ specifiers.push(`ArraySlot as ${nameArraySlot}`);
76
+ }
77
+
78
+ if (needsEffectSlot) {
79
+ specifiers.push(`EffectSlot as ${nameEffectSlot}`);
80
+ }
81
+
82
+ if (needsAttr) {
83
+ specifiers.push(`attributes as ${nameAttr}`);
84
+ }
85
+
86
+ if (needsEvent) {
87
+ specifiers.push(`event as ${nameEvent}`);
88
+ }
89
+
90
+ if (needsSlot) {
91
+ specifiers.push(`slot as ${nameSlot}`);
92
+ }
93
+
94
+ specifiers.push(`template as ${nameTemplate}`);
95
+
96
+ return `import { ${specifiers.join(', ')} } from '@esportsplus/template';`;
97
+ }
98
+
99
+ function generateNestedTemplateCode(node: ts.TaggedTemplateExpression, sourceFile: ts.SourceFile): string {
100
+ let expressions: ts.Expression[] = [],
101
+ exprTexts: string[] = [],
102
+ literals: string[] = [],
103
+ template = node.template;
104
+
105
+ if (ts.isNoSubstitutionTemplateLiteral(template)) {
106
+ literals.push(template.text);
107
+ }
108
+ else if (ts.isTemplateExpression(template)) {
109
+ literals.push(template.head.text);
110
+
111
+ for (let i = 0, n = template.templateSpans.length; i < n; i++) {
112
+ let expr = template.templateSpans[i].expression;
113
+
114
+ expressions.push(expr);
115
+ literals.push(template.templateSpans[i].literal.text);
116
+ exprTexts.push(rewriteExpression(expr, sourceFile));
117
+ }
118
+ }
119
+
120
+ return generateTemplateCode(
121
+ parser.parse(literals) as ParseResult,
122
+ exprTexts,
123
+ expressions,
124
+ sourceFile,
125
+ false
126
+ );
127
+ }
128
+
129
+ function generateNodeBinding(anchor: string, exprText: string, exprNode: ts.Expression | undefined, sourceFile: ts.SourceFile): string {
130
+ if (!exprNode) {
131
+ needsSlot = true;
132
+ return `${nameSlot}(${anchor}, ${exprText});`;
133
+ }
134
+
135
+ if (isNestedHtmlTemplate(exprNode)) {
136
+ return `${anchor}.parentNode.insertBefore(${generateNestedTemplateCode(exprNode, sourceFile)}, ${anchor});`;
137
+ }
138
+
139
+ let slotType = analyzeExpression(exprNode, currentChecker);
140
+
141
+ switch (slotType) {
142
+ case 'effect':
143
+ needsEffectSlot = true;
144
+ return `new ${nameEffectSlot}(${anchor}, ${exprText});`;
145
+
146
+ case 'array-slot':
147
+ needsArraySlot = true;
148
+ return `new ${nameArraySlot}(${anchor}, ${exprText});`;
149
+
150
+ case 'static':
151
+ return `${anchor}.textContent = ${exprText};`;
152
+
153
+ case 'document-fragment':
154
+ return `${anchor}.parentNode.insertBefore(${exprText}, ${anchor});`;
155
+
156
+ default:
157
+ needsSlot = true;
158
+ return `${nameSlot}(${anchor}, ${exprText});`;
159
+ }
160
+ }
161
+
162
+ function generateTemplateCode(
163
+ { html, slots }: ParseResult,
164
+ exprTexts: string[],
165
+ exprNodes: ts.Expression[],
166
+ sourceFile: ts.SourceFile,
167
+ isArrowBody: boolean
168
+ ): string {
169
+ if (!slots || slots.length === 0) {
170
+ return `${getOrCreateTemplateId(html)}()`;
171
+ }
172
+
173
+ let code: string[] = [],
174
+ declarations: string[] = [],
175
+ index = 0,
176
+ nodes = new Map<string, string>(),
177
+ root = uid('root');
178
+
179
+ declarations.push(`${root} = ${getOrCreateTemplateId(html)}()`);
180
+ nodes.set('', root);
181
+
182
+ for (let i = 0, n = slots.length; i < n; i++) {
183
+ let path = slots[i].path;
184
+
185
+ if (path.length === 0) {
186
+ continue;
187
+ }
188
+
189
+ let key = path.join('.');
190
+
191
+ if (nodes.has(key)) {
192
+ continue;
193
+ }
194
+
195
+ let ancestorVar = root,
196
+ startIdx = 0;
197
+
198
+ for (let j = path.length - 1; j >= 0; j--) {
199
+ let prefix = path.slice(0, j).join('.');
200
+
201
+ if (nodes.has(prefix)) {
202
+ ancestorVar = nodes.get(prefix)!;
203
+ startIdx = j;
204
+ break;
205
+ }
206
+ }
207
+
208
+ let name = uid('element'),
209
+ suffix = path.slice(startIdx).join('.');
210
+
211
+ declarations.push(`${name} = ${ancestorVar}.${suffix}`);
212
+ nodes.set(key, name);
213
+ }
214
+
215
+ code.push(
216
+ isArrowBody ? '{' : `(() => {`,
217
+ `let ${declarations.join(',\n')};`
218
+ );
219
+
220
+ for (let i = 0, n = slots.length; i < n; i++) {
221
+ let elementVar = slots[i].path.length === 0
222
+ ? root
223
+ : (nodes.get(slots[i].path.join('.')) || root),
224
+ slot = slots[i];
225
+
226
+ if (slot.type === 'attributes') {
227
+ for (let j = 0, m = slot.attributes.names.length; j < m; j++) {
228
+ let name = slot.attributes.names[j];
229
+
230
+ if (name === 'spread') {
231
+ let bindings = generateSpreadBindings(
232
+ exprNodes[index],
233
+ exprTexts[index] || 'undefined',
234
+ elementVar,
235
+ sourceFile,
236
+ currentChecker
237
+ );
238
+
239
+ for (let k = 0, o = bindings.length; k < o; k++) {
240
+ trackBindingUsage(bindings[k]);
241
+ code.push(bindings[k]);
242
+ }
243
+
244
+ index++;
245
+ }
246
+ else {
247
+ let binding = generateAttributeBinding(
248
+ elementVar,
249
+ name,
250
+ exprTexts[index++] || 'undefined',
251
+ slot.attributes.statics[name] || ''
252
+ );
253
+
254
+ trackBindingUsage(binding);
255
+ code.push(binding);
256
+ }
257
+ }
258
+ }
259
+ else {
260
+ code.push(generateNodeBinding(elementVar, exprTexts[index] || 'undefined', exprNodes[index], sourceFile));
261
+ index++;
262
+ }
263
+ }
264
+
265
+ code.push(`return ${root};`);
266
+ code.push(isArrowBody ? `}` : `})()`);
267
+
268
+ return code.join('\n');
269
+ }
270
+
271
+ function getOrCreateTemplateId(html: string): string {
272
+ let id = htmlToTemplateId.get(html);
273
+
274
+ if (!id) {
275
+ id = uid('tmpl');
276
+ hoistedFactories.set(id, html);
277
+ htmlToTemplateId.set(html, id);
278
+ }
279
+
280
+ return id;
281
+ }
282
+
283
+ function hasArraySlotImport(sourceFile: ts.SourceFile): boolean {
284
+ for (let i = 0, n = sourceFile.statements.length; i < n; i++) {
285
+ let stmt = sourceFile.statements[i];
286
+
287
+ if (!ts.isImportDeclaration(stmt) || !stmt.importClause?.namedBindings) {
288
+ continue;
289
+ }
290
+
291
+ let bindings = stmt.importClause.namedBindings;
292
+
293
+ if (!ts.isNamedImports(bindings)) {
294
+ continue;
295
+ }
296
+
297
+ for (let j = 0, m = bindings.elements.length; j < m; j++) {
298
+ if (bindings.elements[j].name.text === 'ArraySlot') {
299
+ return true;
300
+ }
301
+ }
302
+ }
303
+
304
+ return false;
305
+ }
306
+
307
+ function hasArraySlotUsage(node: ts.Node): boolean {
308
+ if (
309
+ ts.isNewExpression(node) &&
310
+ ts.isIdentifier(node.expression) &&
311
+ node.expression.text === 'ArraySlot'
312
+ ) {
313
+ return true;
314
+ }
315
+
316
+ let found = false;
317
+
318
+ ts.forEachChild(node, child => {
319
+ if (!found && hasArraySlotUsage(child)) {
320
+ found = true;
321
+ }
322
+ });
323
+
324
+ return found;
325
+ }
326
+
327
+ function hasNestedTemplates(node: ts.Node): boolean {
328
+ if (isNestedHtmlTemplate(node as ts.Expression)) {
329
+ return true;
330
+ }
331
+
332
+ let found = false;
333
+
334
+ ts.forEachChild(node, child => {
335
+ if (!found && hasNestedTemplates(child)) {
336
+ found = true;
337
+ }
338
+ });
339
+
340
+ return found;
341
+ }
342
+
343
+ function isNestedHtmlTemplate(expr: ts.Expression): expr is ts.TaggedTemplateExpression {
344
+ return ts.isTaggedTemplateExpression(expr) && ts.isIdentifier(expr.tag) && expr.tag.text === 'html';
345
+ }
346
+
347
+ function isNestedTemplate(template: TemplateInfo, allTemplates: TemplateInfo[]): boolean {
348
+ for (let i = 0, n = allTemplates.length; i < n; i++) {
349
+ let other = allTemplates[i];
350
+
351
+ if (other === template) {
352
+ continue;
353
+ }
354
+
355
+ for (let j = 0, m = other.expressions.length; j < m; j++) {
356
+ let expr = other.expressions[j];
357
+
358
+ if (template.start >= expr.getStart() && template.end <= expr.end) {
359
+ return true;
360
+ }
361
+ }
362
+ }
363
+
364
+ return false;
365
+ }
366
+
367
+ function rewriteExpression(expr: ts.Expression, sourceFile: ts.SourceFile): string {
368
+ if (isNestedHtmlTemplate(expr)) {
369
+ return generateNestedTemplateCode(expr, sourceFile);
370
+ }
371
+
372
+ if (!hasNestedTemplates(expr)) {
373
+ return ts.createPrinter({ newLine: ts.NewLineKind.LineFeed }).printNode(ts.EmitHint.Expression, expr, sourceFile);
374
+ }
375
+
376
+ let exprStart = expr.getStart(),
377
+ replacements: Replacement[] = [];
378
+
379
+ collectNestedTemplateReplacements(expr, exprStart, sourceFile, replacements);
380
+
381
+ return applyReplacementsReverse(expr.getText(sourceFile), replacements);
382
+ }
383
+
384
+ function trackBindingUsage(binding: string): void {
385
+ if (binding.startsWith(nameEvent + '.')) {
386
+ needsEvent = true;
387
+ }
388
+ else if (binding.startsWith(nameAttr + '.')) {
389
+ needsAttr = true;
390
+ }
391
+ }
392
+
393
+
394
+ const addArraySlotImport = (code: string): string => {
395
+ return addImport(code, '@esportsplus/template', ['ArraySlot']);
396
+ };
397
+
398
+ const generateCode = (templates: TemplateInfo[], originalCode: string, sourceFile: ts.SourceFile): CodegenResult => {
399
+ if (templates.length === 0) {
400
+ return { changed: false, code: originalCode };
401
+ }
402
+
403
+ hoistedFactories.clear();
404
+ htmlToTemplateId.clear();
405
+ nameArraySlot = uid('ArraySlot');
406
+ nameAttr = uid('attr');
407
+ nameEffectSlot = uid('EffectSlot');
408
+ nameEvent = uid('event');
409
+ nameSlot = uid('slot');
410
+ nameTemplate = uid('template');
411
+ needsArraySlot = false;
412
+ needsAttr = false;
413
+ needsEffectSlot = false;
414
+ needsEvent = false;
415
+ needsSlot = false;
416
+
417
+ let rootTemplates = templates.filter(t => !isNestedTemplate(t, templates));
418
+
419
+ if (rootTemplates.length === 0) {
420
+ return { changed: false, code: originalCode };
421
+ }
422
+
423
+ let replacements: Replacement[] = [];
424
+
425
+ for (let i = 0, n = rootTemplates.length; i < n; i++) {
426
+ let exprTexts: string[] = [],
427
+ template = rootTemplates[i];
428
+
429
+ for (let j = 0, m = template.expressions.length; j < m; j++) {
430
+ exprTexts.push(rewriteExpression(template.expressions[j], sourceFile));
431
+ }
432
+
433
+ let codeBefore = originalCode.slice(0, template.start),
434
+ isArrowBody = codeBefore.trimEnd().endsWith('=>'),
435
+ parsed = parser.parse(template.literals) as ParseResult;
436
+
437
+ // Optimize: when template has no slots and is `() => template`, use template directly
438
+ if (isArrowBody && (!parsed.slots || parsed.slots.length === 0)) {
439
+ let arrowMatch = codeBefore.match(ARROW_EMPTY_PARAMS);
440
+
441
+ if (arrowMatch) {
442
+ replacements.push({
443
+ end: template.end,
444
+ newText: getOrCreateTemplateId(parsed.html),
445
+ start: template.start - arrowMatch[0].length
446
+ });
447
+ continue;
448
+ }
449
+ }
450
+
451
+ replacements.push({
452
+ end: template.end,
453
+ newText: generateTemplateCode(
454
+ parsed,
455
+ exprTexts,
456
+ template.expressions,
457
+ sourceFile,
458
+ isArrowBody
459
+ ),
460
+ start: template.start
461
+ });
462
+ }
463
+
464
+ let changed = replacements.length > 0,
465
+ code = applyReplacementsReverse(originalCode, replacements);
466
+
467
+ if (changed && hoistedFactories.size > 0) {
468
+ let factories: string[] = [];
469
+
470
+ for (let [id, html] of hoistedFactories) {
471
+ factories.push(`const ${id} = ${nameTemplate}(\`${html}\`);`);
472
+ }
473
+
474
+ code = generateImports() + '\n\n' + factories.join('\n') + '\n\n' + code;
475
+ }
476
+
477
+ return { changed, code };
478
+ };
479
+
480
+ const generateReactiveInlining = (calls: ReactiveCallInfo[], code: string, sourceFile: ts.SourceFile): string => {
481
+ if (calls.length === 0) {
482
+ return code;
483
+ }
484
+
485
+ let printer = ts.createPrinter({ newLine: ts.NewLineKind.LineFeed }),
486
+ result = code;
487
+
488
+ for (let i = calls.length - 1; i >= 0; i--) {
489
+ let call = calls[i];
490
+
491
+ result = result.slice(0, call.start);
492
+ result += `new ${nameArraySlot}(
493
+ ${printer.printNode(ts.EmitHint.Expression, call.arrayArg, sourceFile)},
494
+ ${printer.printNode(ts.EmitHint.Expression, call.callbackArg, sourceFile)}
495
+ )`;
496
+ result += result.slice(call.end);
497
+ }
498
+
499
+ return result;
500
+ };
501
+
502
+ const getNames = () => ({
503
+ attr: nameAttr,
504
+ event: nameEvent,
505
+ slot: nameSlot
506
+ });
507
+
508
+ const needsArraySlotImport = (sourceFile: ts.SourceFile): boolean => {
509
+ return hasArraySlotUsage(sourceFile) && !hasArraySlotImport(sourceFile);
510
+ };
511
+
512
+ const setTypeChecker = (checker: ts.TypeChecker | undefined): void => {
513
+ currentChecker = checker;
514
+ };
515
+
516
+
517
+ export { addArraySlotImport, generateCode, generateReactiveInlining, getNames, needsArraySlotImport, setTypeChecker };
518
+ export type { CodegenResult };
@@ -0,0 +1,98 @@
1
+ import { mightNeedTransform } from '@esportsplus/typescript/transformer';
2
+ import { addArraySlotImport, generateCode, generateReactiveInlining, needsArraySlotImport, setTypeChecker } from './codegen';
3
+ import { findHtmlTemplates, findReactiveCalls } from './ts-parser';
4
+ import ts from 'typescript';
5
+
6
+
7
+ type TransformResult = {
8
+ changed: boolean;
9
+ code: string;
10
+ sourceFile: ts.SourceFile;
11
+ };
12
+
13
+
14
+ const PATTERNS = ['html`', 'html.reactive'];
15
+
16
+
17
+ function createTransformer(program: ts.Program): ts.TransformerFactory<ts.SourceFile> {
18
+ let typeChecker = program.getTypeChecker();
19
+
20
+ return (_context: ts.TransformationContext) => {
21
+ return (sourceFile: ts.SourceFile): ts.SourceFile => {
22
+ let code = sourceFile.getFullText();
23
+
24
+ if (!mightNeedTransform(code, { patterns: PATTERNS })) {
25
+ return sourceFile;
26
+ }
27
+
28
+ setTypeChecker(typeChecker);
29
+
30
+ let result = transformCode(code, sourceFile);
31
+
32
+ if (!result.changed) {
33
+ return sourceFile;
34
+ }
35
+
36
+ return result.sourceFile;
37
+ };
38
+ };
39
+ }
40
+
41
+ function transformCode(code: string, sourceFile: ts.SourceFile): TransformResult {
42
+ let changed = false,
43
+ result = code;
44
+
45
+ let reactiveCalls = findReactiveCalls(sourceFile);
46
+
47
+ if (reactiveCalls.length > 0) {
48
+ result = generateReactiveInlining(reactiveCalls, result, sourceFile);
49
+ changed = true;
50
+ sourceFile = ts.createSourceFile(sourceFile.fileName, result, sourceFile.languageVersion, true);
51
+
52
+ if (needsArraySlotImport(sourceFile)) {
53
+ result = addArraySlotImport(result);
54
+ sourceFile = ts.createSourceFile(sourceFile.fileName, result, sourceFile.languageVersion, true);
55
+ }
56
+
57
+ setTypeChecker(undefined);
58
+ }
59
+
60
+ let templates = findHtmlTemplates(sourceFile);
61
+
62
+ if (templates.length > 0) {
63
+ let codegenResult = generateCode(templates, result, sourceFile);
64
+
65
+ if (codegenResult.changed) {
66
+ changed = true;
67
+ result = codegenResult.code;
68
+ sourceFile = ts.createSourceFile(sourceFile.fileName, result, sourceFile.languageVersion, true);
69
+ }
70
+ }
71
+
72
+ return { changed, code: result, sourceFile };
73
+ }
74
+
75
+
76
+ const transform = (sourceFile: ts.SourceFile, program: ts.Program): TransformResult => {
77
+ let code = sourceFile.getFullText();
78
+
79
+ if (!mightNeedTransform(code, { patterns: PATTERNS })) {
80
+ return { changed: false, code, sourceFile };
81
+ }
82
+
83
+ let programSourceFile = program.getSourceFile(sourceFile.fileName);
84
+
85
+ if (programSourceFile) {
86
+ setTypeChecker(program.getTypeChecker());
87
+ sourceFile = programSourceFile;
88
+ }
89
+ else {
90
+ setTypeChecker(undefined);
91
+ }
92
+
93
+ return transformCode(code, sourceFile);
94
+ };
95
+
96
+
97
+ export type { TransformResult };
98
+ export { createTransformer, mightNeedTransform, PATTERNS, transform };