@esportsplus/template 0.31.7 → 0.32.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 (39) hide show
  1. package/build/attributes.js +1 -2
  2. package/build/constants.d.ts +18 -3
  3. package/build/constants.js +31 -4
  4. package/build/html.d.ts +3 -3
  5. package/build/slot/array.d.ts +2 -2
  6. package/build/slot/array.js +5 -4
  7. package/build/slot/render.js +3 -2
  8. package/build/transformer/codegen.d.ts +2 -9
  9. package/build/transformer/codegen.js +87 -101
  10. package/build/transformer/index.d.ts +2 -3
  11. package/build/transformer/index.js +34 -32
  12. package/build/transformer/parser.d.ts +3 -2
  13. package/build/transformer/parser.js +4 -4
  14. package/build/transformer/plugins/tsc.js +8 -2
  15. package/build/transformer/plugins/vite.js +9 -14
  16. package/build/transformer/ts-parser.d.ts +1 -2
  17. package/build/transformer/ts-parser.js +25 -34
  18. package/build/transformer/type-analyzer.d.ts +4 -5
  19. package/build/transformer/type-analyzer.js +61 -71
  20. package/build/types.d.ts +1 -1
  21. package/package.json +7 -7
  22. package/src/attributes.ts +1 -4
  23. package/src/constants.ts +42 -6
  24. package/src/html.ts +3 -3
  25. package/src/slot/array.ts +9 -6
  26. package/src/slot/render.ts +5 -2
  27. package/src/transformer/codegen.ts +113 -124
  28. package/src/transformer/index.ts +44 -49
  29. package/src/transformer/parser.ts +10 -7
  30. package/src/transformer/plugins/tsc.ts +10 -2
  31. package/src/transformer/plugins/vite.ts +12 -25
  32. package/src/transformer/ts-parser.ts +31 -44
  33. package/src/transformer/type-analyzer.ts +75 -93
  34. package/src/types.ts +1 -1
  35. package/test/vite.config.ts +1 -1
  36. package/build/event/constants.d.ts +0 -3
  37. package/build/event/constants.js +0 -13
  38. package/src/event/constants.ts +0 -16
  39. package/storage/rewrite-analysis-2026-01-04.md +0 -439
@@ -1,8 +1,14 @@
1
- import { applyReplacementsReverse, uid } from '@esportsplus/typescript/transformer';
1
+ import { code as c, uid } from '@esportsplus/typescript/transformer';
2
2
  import type { Replacement } from '@esportsplus/typescript/transformer';
3
3
  import type { ReactiveCallInfo, TemplateInfo } from './ts-parser';
4
4
  import { analyzeExpression, generateAttributeBinding, generateSpreadBindings } from './type-analyzer';
5
5
  import { ts } from '@esportsplus/typescript';
6
+ import {
7
+ COMPILER_ENTRYPOINT,
8
+ COMPILER_NAMESPACE,
9
+ COMPILER_TYPES,
10
+ PACKAGE
11
+ } from '../constants';
6
12
  import parser from './parser';
7
13
 
8
14
 
@@ -12,7 +18,15 @@ type AttributeSlot = {
12
18
  statics: Record<string, string>;
13
19
  };
14
20
  path: string[];
15
- type: 'attributes';
21
+ type: COMPILER_TYPES.AttributeSlot;
22
+ };
23
+
24
+ type CodegenContext = {
25
+ checker?: ts.TypeChecker;
26
+ hoistedFactories: Map<string, string>;
27
+ htmlToTemplateId: Map<string, string>;
28
+ printer: ts.Printer;
29
+ sourceFile: ts.SourceFile;
16
30
  };
17
31
 
18
32
  type CodegenResult = {
@@ -22,7 +36,7 @@ type CodegenResult = {
22
36
 
23
37
  type NodeSlot = {
24
38
  path: string[];
25
- type: 'slot';
39
+ type: COMPILER_TYPES.NodeSlot;
26
40
  };
27
41
 
28
42
  type ParseResult = {
@@ -33,38 +47,28 @@ type ParseResult = {
33
47
 
34
48
  const ARROW_EMPTY_PARAMS = /\(\s*\)\s*=>\s*$/;
35
49
 
36
- const TEMPLATE_IMPORT = /import\s+\{[^}]*\}\s+from\s+['"]@esportsplus\/template['"];?\s*\n?/g;
37
-
38
-
39
- let currentChecker: ts.TypeChecker | undefined,
40
- hoistedFactories = new Map<string, string>(),
41
- htmlToTemplateId = new Map<string, string>(),
42
- ns = '';
50
+ let printer = ts.createPrinter({ newLine: ts.NewLineKind.LineFeed });
43
51
 
44
52
 
45
53
  function collectNestedTemplateReplacements(
54
+ ctx: CodegenContext,
46
55
  node: ts.Node,
47
56
  exprStart: number,
48
- sourceFile: ts.SourceFile,
49
57
  replacements: Replacement[]
50
58
  ): void {
51
59
  if (isNestedHtmlTemplate(node as ts.Expression)) {
52
60
  replacements.push({
53
61
  end: node.end - exprStart,
54
- newText: generateNestedTemplateCode(node as ts.TaggedTemplateExpression, sourceFile),
62
+ newText: generateNestedTemplateCode(ctx, node as ts.TaggedTemplateExpression),
55
63
  start: node.getStart() - exprStart
56
64
  });
57
65
  }
58
66
  else {
59
- ts.forEachChild(node, child => collectNestedTemplateReplacements(child, exprStart, sourceFile, replacements));
67
+ ts.forEachChild(node, child => collectNestedTemplateReplacements(ctx, child, exprStart, replacements));
60
68
  }
61
69
  }
62
70
 
63
- function generateImports(): string {
64
- return `import * as ${ns} from '@esportsplus/template';`;
65
- }
66
-
67
- function generateNestedTemplateCode(node: ts.TaggedTemplateExpression, sourceFile: ts.SourceFile): string {
71
+ function generateNestedTemplateCode(ctx: CodegenContext, node: ts.TaggedTemplateExpression): string {
68
72
  let expressions: ts.Expression[] = [],
69
73
  exprTexts: string[] = [],
70
74
  literals: string[] = [],
@@ -81,57 +85,57 @@ function generateNestedTemplateCode(node: ts.TaggedTemplateExpression, sourceFil
81
85
 
82
86
  expressions.push(expr);
83
87
  literals.push(template.templateSpans[i].literal.text);
84
- exprTexts.push(rewriteExpression(expr, sourceFile));
88
+ exprTexts.push(rewriteExpression(ctx, expr));
85
89
  }
86
90
  }
87
91
 
88
92
  return generateTemplateCode(
93
+ ctx,
89
94
  parser.parse(literals) as ParseResult,
90
95
  exprTexts,
91
96
  expressions,
92
- sourceFile,
93
97
  false
94
98
  );
95
99
  }
96
100
 
97
- function generateNodeBinding(anchor: string, exprText: string, exprNode: ts.Expression | undefined, sourceFile: ts.SourceFile): string {
101
+ function generateNodeBinding(ctx: CodegenContext, anchor: string, exprText: string, exprNode: ts.Expression | undefined): string {
98
102
  if (!exprNode) {
99
- return `${ns}.slot(${anchor}, ${exprText});`;
103
+ return `${COMPILER_NAMESPACE}.slot(${anchor}, ${exprText});`;
100
104
  }
101
105
 
102
106
  if (isNestedHtmlTemplate(exprNode)) {
103
- return `${anchor}.parentNode.insertBefore(${generateNestedTemplateCode(exprNode, sourceFile)}, ${anchor});`;
107
+ return `${anchor}.parentNode.insertBefore(${generateNestedTemplateCode(ctx, exprNode)}, ${anchor});`;
104
108
  }
105
109
 
106
- let slotType = analyzeExpression(exprNode, currentChecker);
110
+ let slotType = analyzeExpression(exprNode, ctx.checker);
107
111
 
108
112
  switch (slotType) {
109
- case 'effect':
110
- return `new ${ns}.EffectSlot(${anchor}, ${exprText});`;
113
+ case COMPILER_TYPES.Effect:
114
+ return `new ${COMPILER_NAMESPACE}.EffectSlot(${anchor}, ${exprText});`;
111
115
 
112
- case 'array-slot':
113
- return `new ${ns}.ArraySlot(${anchor}, ${exprText});`;
116
+ case COMPILER_TYPES.ArraySlot:
117
+ return `new ${COMPILER_NAMESPACE}.ArraySlot(${anchor}, ${exprText});`;
114
118
 
115
- case 'static':
119
+ case COMPILER_TYPES.Static:
116
120
  return `${anchor}.textContent = ${exprText};`;
117
121
 
118
- case 'document-fragment':
122
+ case COMPILER_TYPES.DocumentFragment:
119
123
  return `${anchor}.parentNode.insertBefore(${exprText}, ${anchor});`;
120
124
 
121
125
  default:
122
- return `${ns}.slot(${anchor}, ${exprText});`;
126
+ return `${COMPILER_NAMESPACE}.slot(${anchor}, ${exprText});`;
123
127
  }
124
128
  }
125
129
 
126
130
  function generateTemplateCode(
131
+ ctx: CodegenContext,
127
132
  { html, slots }: ParseResult,
128
133
  exprTexts: string[],
129
134
  exprNodes: ts.Expression[],
130
- sourceFile: ts.SourceFile,
131
135
  isArrowBody: boolean
132
136
  ): string {
133
137
  if (!slots || slots.length === 0) {
134
- return `${getOrCreateTemplateId(html)}()`;
138
+ return `${getOrCreateTemplateId(ctx, html)}()`;
135
139
  }
136
140
 
137
141
  let code: string[] = [],
@@ -140,7 +144,7 @@ function generateTemplateCode(
140
144
  nodes = new Map<string, string>(),
141
145
  root = uid('root');
142
146
 
143
- declarations.push(`${root} = ${getOrCreateTemplateId(html)}()`);
147
+ declarations.push(`${root} = ${getOrCreateTemplateId(ctx, html)}()`);
144
148
  nodes.set('', root);
145
149
 
146
150
  for (let i = 0, n = slots.length; i < n; i++) {
@@ -187,7 +191,7 @@ function generateTemplateCode(
187
191
  : (nodes.get(slots[i].path.join('.')) || root),
188
192
  slot = slots[i];
189
193
 
190
- if (slot.type === 'attributes') {
194
+ if (slot.type === COMPILER_TYPES.AttributeSlot) {
191
195
  for (let j = 0, m = slot.attributes.names.length; j < m; j++) {
192
196
  let name = slot.attributes.names[j];
193
197
 
@@ -196,8 +200,9 @@ function generateTemplateCode(
196
200
  exprNodes[index],
197
201
  exprTexts[index] || 'undefined',
198
202
  elementVar,
199
- sourceFile,
200
- currentChecker
203
+ ctx.sourceFile,
204
+ ctx.checker,
205
+ COMPILER_NAMESPACE
201
206
  );
202
207
 
203
208
  for (let k = 0, o = bindings.length; k < o; k++) {
@@ -211,7 +216,8 @@ function generateTemplateCode(
211
216
  elementVar,
212
217
  name,
213
218
  exprTexts[index++] || 'undefined',
214
- slot.attributes.statics[name] || ''
219
+ slot.attributes.statics[name] || '',
220
+ COMPILER_NAMESPACE
215
221
  );
216
222
 
217
223
  code.push(binding);
@@ -219,7 +225,7 @@ function generateTemplateCode(
219
225
  }
220
226
  }
221
227
  else {
222
- code.push(generateNodeBinding(elementVar, exprTexts[index] || 'undefined', exprNodes[index], sourceFile));
228
+ code.push(generateNodeBinding(ctx, elementVar, exprTexts[index] || 'undefined', exprNodes[index]));
223
229
  index++;
224
230
  }
225
231
  }
@@ -230,13 +236,13 @@ function generateTemplateCode(
230
236
  return code.join('\n');
231
237
  }
232
238
 
233
- function getOrCreateTemplateId(html: string): string {
234
- let id = htmlToTemplateId.get(html);
239
+ function getOrCreateTemplateId(ctx: CodegenContext, html: string): string {
240
+ let id = ctx.htmlToTemplateId.get(html);
235
241
 
236
242
  if (!id) {
237
243
  id = uid('tmpl');
238
- hoistedFactories.set(id, html);
239
- htmlToTemplateId.set(html, id);
244
+ ctx.hoistedFactories.set(id, html);
245
+ ctx.htmlToTemplateId.set(html, id);
240
246
  }
241
247
 
242
248
  return id;
@@ -266,109 +272,109 @@ function hasArraySlotImport(sourceFile: ts.SourceFile): boolean {
266
272
  return false;
267
273
  }
268
274
 
269
- function hasArraySlotUsage(node: ts.Node): boolean {
270
- if (
271
- ts.isNewExpression(node) &&
272
- ts.isIdentifier(node.expression) &&
273
- node.expression.text === 'ArraySlot'
274
- ) {
275
+ function hasMatch(node: ts.Node, predicate: (n: ts.Node) => boolean): boolean {
276
+ if (predicate(node)) {
275
277
  return true;
276
278
  }
277
279
 
278
280
  let found = false;
279
281
 
280
282
  ts.forEachChild(node, child => {
281
- if (!found && hasArraySlotUsage(child)) {
282
- found = true;
283
+ if (!found) {
284
+ found = hasMatch(child, predicate);
283
285
  }
284
286
  });
285
287
 
286
288
  return found;
287
289
  }
288
290
 
289
- function hasNestedTemplates(node: ts.Node): boolean {
290
- if (isNestedHtmlTemplate(node as ts.Expression)) {
291
- return true;
292
- }
293
-
294
- let found = false;
295
-
296
- ts.forEachChild(node, child => {
297
- if (!found && hasNestedTemplates(child)) {
298
- found = true;
299
- }
300
- });
291
+ function hasArraySlotUsage(node: ts.Node): boolean {
292
+ return hasMatch(node, n =>
293
+ ts.isNewExpression(n) &&
294
+ ts.isPropertyAccessExpression(n.expression) &&
295
+ n.expression.name.text === 'ArraySlot'
296
+ );
297
+ }
301
298
 
302
- return found;
299
+ function hasNestedTemplates(node: ts.Node): boolean {
300
+ return hasMatch(node, n => isNestedHtmlTemplate(n as ts.Expression));
303
301
  }
304
302
 
305
303
  function isNestedHtmlTemplate(expr: ts.Expression): expr is ts.TaggedTemplateExpression {
306
- return ts.isTaggedTemplateExpression(expr) && ts.isIdentifier(expr.tag) && expr.tag.text === 'html';
304
+ return ts.isTaggedTemplateExpression(expr) && ts.isIdentifier(expr.tag) && expr.tag.text === COMPILER_ENTRYPOINT;
307
305
  }
308
306
 
309
- function isNestedTemplate(template: TemplateInfo, allTemplates: TemplateInfo[]): boolean {
310
- for (let i = 0, n = allTemplates.length; i < n; i++) {
311
- let other = allTemplates[i];
307
+ function isNestedTemplate(template: TemplateInfo, exprRanges: { end: number; start: number }[]): boolean {
308
+ for (let i = 0, n = exprRanges.length; i < n; i++) {
309
+ let range = exprRanges[i];
312
310
 
313
- if (other === template) {
314
- continue;
315
- }
316
-
317
- for (let j = 0, m = other.expressions.length; j < m; j++) {
318
- let expr = other.expressions[j];
319
-
320
- if (template.start >= expr.getStart() && template.end <= expr.end) {
321
- return true;
322
- }
311
+ if (template.start >= range.start && template.end <= range.end) {
312
+ return true;
323
313
  }
324
314
  }
325
315
 
326
316
  return false;
327
317
  }
328
318
 
329
- function rewriteExpression(expr: ts.Expression, sourceFile: ts.SourceFile): string {
319
+ function rewriteExpression(ctx: CodegenContext, expr: ts.Expression): string {
330
320
  if (isNestedHtmlTemplate(expr)) {
331
- return generateNestedTemplateCode(expr, sourceFile);
321
+ return generateNestedTemplateCode(ctx, expr);
332
322
  }
333
323
 
334
324
  if (!hasNestedTemplates(expr)) {
335
- return ts.createPrinter({ newLine: ts.NewLineKind.LineFeed }).printNode(ts.EmitHint.Expression, expr, sourceFile);
325
+ return ctx.printer.printNode(ts.EmitHint.Expression, expr, ctx.sourceFile);
336
326
  }
337
327
 
338
328
  let exprStart = expr.getStart(),
339
329
  replacements: Replacement[] = [];
340
330
 
341
- collectNestedTemplateReplacements(expr, exprStart, sourceFile, replacements);
331
+ collectNestedTemplateReplacements(ctx, expr, exprStart, replacements);
342
332
 
343
- return applyReplacementsReverse(expr.getText(sourceFile), replacements);
333
+ return c.replaceReverse(expr.getText(ctx.sourceFile), replacements);
344
334
  }
345
335
 
346
336
 
347
-
348
337
  const addArraySlotImport = (code: string): string => {
349
- // Strip original @esportsplus/template imports (they lose symbol bindings in TS 5.9.3)
350
- return `import * as ${ns} from '@esportsplus/template';\n\n` + code.replace(TEMPLATE_IMPORT, '');
338
+ return `import * as ${COMPILER_NAMESPACE} from '${PACKAGE}';\n` + code;
351
339
  };
352
340
 
353
- const generateCode = (templates: TemplateInfo[], originalCode: string, sourceFile: ts.SourceFile): CodegenResult => {
341
+ const generateCode = (templates: TemplateInfo[], originalCode: string, sourceFile: ts.SourceFile, checker?: ts.TypeChecker): CodegenResult => {
354
342
  if (templates.length === 0) {
355
343
  return { changed: false, code: originalCode };
356
344
  }
357
345
 
358
- let rootTemplates = templates.filter(t => !isNestedTemplate(t, templates));
346
+ // Precompute expression ranges for nested template detection
347
+ let exprRanges: { end: number; start: number }[] = [];
348
+
349
+ for (let i = 0, n = templates.length; i < n; i++) {
350
+ let exprs = templates[i].expressions;
351
+
352
+ for (let j = 0, m = exprs.length; j < m; j++) {
353
+ exprRanges.push({ end: exprs[j].end, start: exprs[j].getStart() });
354
+ }
355
+ }
356
+
357
+ let rootTemplates = templates.filter(t => !isNestedTemplate(t, exprRanges));
359
358
 
360
359
  if (rootTemplates.length === 0) {
361
360
  return { changed: false, code: originalCode };
362
361
  }
363
362
 
364
- let replacements: Replacement[] = [];
363
+ let ctx: CodegenContext = {
364
+ checker,
365
+ hoistedFactories: new Map(),
366
+ htmlToTemplateId: new Map(),
367
+ printer,
368
+ sourceFile
369
+ },
370
+ replacements: Replacement[] = [];
365
371
 
366
372
  for (let i = 0, n = rootTemplates.length; i < n; i++) {
367
373
  let exprTexts: string[] = [],
368
374
  template = rootTemplates[i];
369
375
 
370
376
  for (let j = 0, m = template.expressions.length; j < m; j++) {
371
- exprTexts.push(rewriteExpression(template.expressions[j], sourceFile));
377
+ exprTexts.push(rewriteExpression(ctx, template.expressions[j]));
372
378
  }
373
379
 
374
380
  let codeBefore = originalCode.slice(0, template.start),
@@ -382,7 +388,7 @@ const generateCode = (templates: TemplateInfo[], originalCode: string, sourceFil
382
388
  if (arrowMatch) {
383
389
  replacements.push({
384
390
  end: template.end,
385
- newText: getOrCreateTemplateId(parsed.html),
391
+ newText: getOrCreateTemplateId(ctx, parsed.html),
386
392
  start: template.start - arrowMatch[0].length
387
393
  });
388
394
  continue;
@@ -392,10 +398,10 @@ const generateCode = (templates: TemplateInfo[], originalCode: string, sourceFil
392
398
  replacements.push({
393
399
  end: template.end,
394
400
  newText: generateTemplateCode(
401
+ ctx,
395
402
  parsed,
396
403
  exprTexts,
397
404
  template.expressions,
398
- sourceFile,
399
405
  isArrowBody
400
406
  ),
401
407
  start: template.start
@@ -403,18 +409,16 @@ const generateCode = (templates: TemplateInfo[], originalCode: string, sourceFil
403
409
  }
404
410
 
405
411
  let changed = replacements.length > 0,
406
- code = applyReplacementsReverse(originalCode, replacements);
412
+ code = c.replaceReverse(originalCode, replacements);
407
413
 
408
- if (changed && hoistedFactories.size > 0) {
414
+ if (changed && ctx.hoistedFactories.size > 0) {
409
415
  let factories: string[] = [];
410
416
 
411
- for (let [id, html] of hoistedFactories) {
412
- factories.push(`const ${id} = ${ns}.template(\`${html}\`);`);
417
+ for (let [id, html] of ctx.hoistedFactories) {
418
+ factories.push(`const ${id} = ${COMPILER_NAMESPACE}.template(\`${html}\`);`);
413
419
  }
414
420
 
415
- // Strip original @esportsplus/template imports (they lose symbol bindings in TS 5.9.3)
416
- code = code.replace(TEMPLATE_IMPORT, '');
417
- code = generateImports() + '\n\n' + factories.join('\n') + '\n\n' + code;
421
+ code = `import * as ${COMPILER_NAMESPACE} from '${PACKAGE}';\n\n` + factories.join('\n') + '\n\n' + code;
418
422
  }
419
423
 
420
424
  return { changed, code };
@@ -425,43 +429,28 @@ const generateReactiveInlining = (calls: ReactiveCallInfo[], code: string, sourc
425
429
  return code;
426
430
  }
427
431
 
428
- let printer = ts.createPrinter({ newLine: ts.NewLineKind.LineFeed }),
429
- result = code;
432
+ let replacements: Replacement[] = [];
430
433
 
431
- for (let i = calls.length - 1; i >= 0; i--) {
434
+ for (let i = 0, n = calls.length; i < n; i++) {
432
435
  let call = calls[i];
433
436
 
434
- result = result.slice(0, call.start);
435
- result += `new ${ns}.ArraySlot(
437
+ replacements.push({
438
+ end: call.end,
439
+ newText: `new ${COMPILER_NAMESPACE}.ArraySlot(
436
440
  ${printer.printNode(ts.EmitHint.Expression, call.arrayArg, sourceFile)},
437
441
  ${printer.printNode(ts.EmitHint.Expression, call.callbackArg, sourceFile)}
438
- )`;
439
- result += result.slice(call.end);
442
+ )`,
443
+ start: call.start
444
+ });
440
445
  }
441
446
 
442
- return result;
443
- };
444
-
445
- const getNames = () => ({
446
- attr: `${ns}.attributes`,
447
- event: `${ns}.event`,
448
- slot: `${ns}.slot`
449
- });
450
-
451
- const initNamespace = (): void => {
452
- hoistedFactories.clear();
453
- htmlToTemplateId.clear();
454
- ns = uid('t');
447
+ return c.replaceReverse(code, replacements);
455
448
  };
456
449
 
457
450
  const needsArraySlotImport = (sourceFile: ts.SourceFile): boolean => {
458
451
  return hasArraySlotUsage(sourceFile) && !hasArraySlotImport(sourceFile);
459
452
  };
460
453
 
461
- const setTypeChecker = (checker: ts.TypeChecker | undefined): void => {
462
- currentChecker = checker;
463
- };
464
-
465
454
 
466
- export { addArraySlotImport, generateCode, generateReactiveInlining, getNames, initNamespace, needsArraySlotImport, setTypeChecker };
455
+ export { addArraySlotImport, generateCode, generateReactiveInlining, needsArraySlotImport };
467
456
  export type { CodegenResult };
@@ -1,5 +1,6 @@
1
- import { mightNeedTransform } from '@esportsplus/typescript/transformer';
2
- import { addArraySlotImport, generateCode, generateReactiveInlining, initNamespace, setTypeChecker } from './codegen';
1
+ import { code as c } from '@esportsplus/typescript/transformer';
2
+ import { addArraySlotImport, generateCode, generateReactiveInlining, needsArraySlotImport } from './codegen';
3
+ import { COMPILER_ENTRYPOINT, COMPILER_ENTRYPOINT_REACTIVITY } from '../constants';
3
4
  import { findHtmlTemplates, findReactiveCalls } from './ts-parser';
4
5
  import { ts } from '@esportsplus/typescript';
5
6
 
@@ -11,28 +12,21 @@ type TransformResult = {
11
12
  };
12
13
 
13
14
 
14
- const PATTERNS = ['html`', 'html.reactive'];
15
+ const PATTERNS = [`${COMPILER_ENTRYPOINT}\``, `${COMPILER_ENTRYPOINT}.${COMPILER_ENTRYPOINT_REACTIVITY}`];
15
16
 
16
17
 
17
18
  function createTransformer(program: ts.Program): ts.TransformerFactory<ts.SourceFile> {
18
- let printer = ts.createPrinter(),
19
- typeChecker = program.getTypeChecker();
19
+ let typeChecker = program.getTypeChecker();
20
20
 
21
21
  return (_context: ts.TransformationContext) => {
22
22
  return (sourceFile: ts.SourceFile): ts.SourceFile => {
23
- // Use printer to get current text representation (handles chained transformers)
24
- let code = printer.printFile(sourceFile);
23
+ let code = sourceFile.getFullText();
25
24
 
26
- if (!mightNeedTransform(code, { patterns: PATTERNS })) {
25
+ if (!c.contains(code, { patterns: PATTERNS })) {
27
26
  return sourceFile;
28
27
  }
29
28
 
30
- // Re-parse the printed code to get accurate positions
31
- let reparsed = ts.createSourceFile(sourceFile.fileName, code, sourceFile.languageVersion, true);
32
-
33
- setTypeChecker(typeChecker);
34
-
35
- let result = transformCode(code, reparsed);
29
+ let result = transformCode(code, sourceFile, typeChecker);
36
30
 
37
31
  if (!result.changed) {
38
32
  return sourceFile;
@@ -43,64 +37,65 @@ function createTransformer(program: ts.Program): ts.TransformerFactory<ts.Source
43
37
  };
44
38
  }
45
39
 
46
- function transformCode(code: string, sourceFile: ts.SourceFile): TransformResult {
47
- let changed = false,
48
- result = code;
49
40
 
50
- initNamespace();
41
+ const transform = (sourceFile: ts.SourceFile, program: ts.Program): TransformResult => {
42
+ let code = sourceFile.getFullText();
43
+
44
+ if (!c.contains(code, { patterns: PATTERNS })) {
45
+ return { changed: false, code, sourceFile };
46
+ }
51
47
 
52
- let reactiveCalls = findReactiveCalls(sourceFile);
48
+ let checker: ts.TypeChecker | undefined,
49
+ fileName = sourceFile.fileName,
50
+ programSourceFile = program.getSourceFile(fileName)
51
+ || program.getSourceFile(fileName.replace(/\\/g, '/'))
52
+ || program.getSourceFile(fileName.replace(/\//g, '\\'));
53
+
54
+ if (programSourceFile) {
55
+ checker = program.getTypeChecker();
56
+ sourceFile = programSourceFile;
57
+ }
58
+
59
+ return transformCode(code, sourceFile, checker);
60
+ };
61
+
62
+ const transformCode = (code: string, sourceFile: ts.SourceFile, checker?: ts.TypeChecker): TransformResult => {
63
+ let changed = false,
64
+ codegenChanged = false,
65
+ needsImport = false,
66
+ result = code,
67
+ reactiveCalls = findReactiveCalls(sourceFile);
53
68
 
54
69
  if (reactiveCalls.length > 0) {
55
70
  result = generateReactiveInlining(reactiveCalls, result, sourceFile);
56
- // Always add namespace import when reactive calls are inlined (strips original named imports)
57
- result = addArraySlotImport(result);
58
71
  changed = true;
59
72
  sourceFile = ts.createSourceFile(sourceFile.fileName, result, sourceFile.languageVersion, true);
60
-
61
- setTypeChecker(undefined);
73
+ needsImport = needsArraySlotImport(sourceFile);
74
+ checker = undefined;
62
75
  }
63
76
 
64
77
  let templates = findHtmlTemplates(sourceFile);
65
78
 
66
79
  if (templates.length > 0) {
67
- let codegenResult = generateCode(templates, result, sourceFile);
80
+ let codegenResult = generateCode(templates, result, sourceFile, checker);
68
81
 
69
82
  if (codegenResult.changed) {
70
83
  changed = true;
84
+ codegenChanged = true;
71
85
  result = codegenResult.code;
72
- sourceFile = ts.createSourceFile(sourceFile.fileName, result, sourceFile.languageVersion, true);
73
86
  }
74
87
  }
75
88
 
76
- return { changed, code: result, sourceFile };
77
- }
78
-
79
-
80
- const transform = (sourceFile: ts.SourceFile, program: ts.Program): TransformResult => {
81
- // Use printer to get current text representation (handles chained transformers)
82
- let printer = ts.createPrinter(),
83
- code = printer.printFile(sourceFile);
84
-
85
- if (!mightNeedTransform(code, { patterns: PATTERNS })) {
86
- return { changed: false, code, sourceFile };
89
+ if (needsImport && !codegenChanged) {
90
+ result = addArraySlotImport(result);
87
91
  }
88
92
 
89
- // Re-parse the printed code to get accurate positions
90
- let reparsed = ts.createSourceFile(sourceFile.fileName, code, sourceFile.languageVersion, true);
91
-
92
- let programSourceFile = program.getSourceFile(sourceFile.fileName);
93
-
94
- if (programSourceFile) {
95
- setTypeChecker(program.getTypeChecker());
96
- }
97
- else {
98
- setTypeChecker(undefined);
93
+ if (changed) {
94
+ sourceFile = ts.createSourceFile(sourceFile.fileName, result, sourceFile.languageVersion, true);
99
95
  }
100
96
 
101
- return transformCode(code, reparsed);
97
+ return { changed, code: result, sourceFile };
102
98
  };
103
99
 
104
100
 
105
- export type { TransformResult };
106
- export { createTransformer, mightNeedTransform, PATTERNS, transform };
101
+ export { createTransformer, transform, transformCode, PATTERNS };
@@ -1,5 +1,8 @@
1
- // Inlined to avoid importing constants.ts which depends on utilities.ts (uses document)
2
- const SLOT_HTML = '<!--$-->';
1
+ import {
2
+ COMPILER_TYPES,
3
+ PACKAGE,
4
+ SLOT_HTML
5
+ } from '../constants';
3
6
 
4
7
 
5
8
  type NodePath = ('firstChild' | 'firstElementChild' | 'nextElementSibling' | 'nextSibling')[];
@@ -92,8 +95,8 @@ const parse = (literals: string[]) => {
92
95
  parsed = html.split(SLOT_MARKER),
93
96
  slot = 0,
94
97
  slots: (
95
- { path: NodePath; type: 'slot' } |
96
- { attributes: typeof attributes[string]; path: NodePath; type: 'attributes' }
98
+ { path: NodePath; type: COMPILER_TYPES.NodeSlot } |
99
+ { attributes: typeof attributes[string]; path: NodePath; type: COMPILER_TYPES.AttributeSlot }
97
100
  )[] = [];
98
101
 
99
102
  {
@@ -186,10 +189,10 @@ const parse = (literals: string[]) => {
186
189
  let attrs = attributes[attr];
187
190
 
188
191
  if (!attrs) {
189
- throw new Error(`@esportsplus/template: attribute metadata could not be found for '${attr}'`);
192
+ throw new Error(`${PACKAGE}: attribute metadata could not be found for '${attr}'`);
190
193
  }
191
194
 
192
- slots.push({ attributes: attrs, path, type: 'attributes' });
195
+ slots.push({ attributes: attrs, path, type: COMPILER_TYPES.AttributeSlot });
193
196
 
194
197
  for (let i = 0, n = attrs.names.length; i < n; i++) {
195
198
  buffer += parsed[slot++];
@@ -204,7 +207,7 @@ const parse = (literals: string[]) => {
204
207
  }
205
208
  else if (type === NODE_SLOT) {
206
209
  buffer += parsed[slot++] + SLOT_HTML;
207
- slots.push({ path: methods(parent.children, parent.path, 'firstChild', 'nextSibling'), type: 'slot' });
210
+ slots.push({ path: methods(parent.children, parent.path, 'firstChild', 'nextSibling'), type: COMPILER_TYPES.NodeSlot });
208
211
  }
209
212
 
210
213
  if (n === slot) {