@esportsplus/template 0.35.0 → 0.37.0

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.
@@ -0,0 +1,92 @@
1
+ import { COMPILER_ENTRYPOINT, COMPILER_ENTRYPOINT_REACTIVITY, COMPILER_TYPES } from '~/constants';
2
+ import { ts } from '@esportsplus/typescript';
3
+
4
+
5
+ // Union types that mix functions with non-functions (e.g., Renderable)
6
+ // should fall through to runtime slot dispatch
7
+ function isTypeFunction(type: ts.Type, checker: ts.TypeChecker): boolean {
8
+ if (type.isUnion()) {
9
+ for (let i = 0, n = type.types.length; i < n; i++) {
10
+ if (!isTypeFunction(type.types[i], checker)) {
11
+ return false;
12
+ }
13
+ }
14
+
15
+ return type.types.length > 0;
16
+ }
17
+
18
+ return type.getCallSignatures().length > 0;
19
+ }
20
+
21
+
22
+ const analyze = (expr: ts.Expression, checker?: ts.TypeChecker): COMPILER_TYPES => {
23
+ while (ts.isParenthesizedExpression(expr)) {
24
+ expr = expr.expression;
25
+ }
26
+
27
+ if (ts.isArrowFunction(expr) || ts.isFunctionExpression(expr)) {
28
+ return COMPILER_TYPES.Effect;
29
+ }
30
+
31
+ // Only html.reactive() calls become ArraySlot - handled by generateReactiveInlining
32
+ if (
33
+ ts.isCallExpression(expr) &&
34
+ ts.isPropertyAccessExpression(expr.expression) &&
35
+ ts.isIdentifier(expr.expression.expression) &&
36
+ expr.expression.expression.text === COMPILER_ENTRYPOINT &&
37
+ expr.expression.name.text === COMPILER_ENTRYPOINT_REACTIVITY
38
+ ) {
39
+ return COMPILER_TYPES.ArraySlot;
40
+ }
41
+
42
+ if (ts.isTaggedTemplateExpression(expr) && ts.isIdentifier(expr.tag) && expr.tag.text === COMPILER_ENTRYPOINT) {
43
+ return COMPILER_TYPES.DocumentFragment;
44
+ }
45
+
46
+ if (
47
+ ts.isNumericLiteral(expr) ||
48
+ ts.isStringLiteral(expr) ||
49
+ ts.isNoSubstitutionTemplateLiteral(expr) ||
50
+ expr.kind === ts.SyntaxKind.TrueKeyword ||
51
+ expr.kind === ts.SyntaxKind.FalseKeyword ||
52
+ expr.kind === ts.SyntaxKind.NullKeyword ||
53
+ expr.kind === ts.SyntaxKind.UndefinedKeyword
54
+ ) {
55
+ return COMPILER_TYPES.Static;
56
+ }
57
+
58
+ if (ts.isTemplateExpression(expr)) {
59
+ return COMPILER_TYPES.Primitive;
60
+ }
61
+
62
+ if (ts.isConditionalExpression(expr)) {
63
+ let whenFalse = analyze(expr.whenFalse, checker),
64
+ whenTrue = analyze(expr.whenTrue, checker);
65
+
66
+ if (whenTrue === whenFalse) {
67
+ return whenTrue;
68
+ }
69
+
70
+ if (whenTrue === COMPILER_TYPES.Effect || whenFalse === COMPILER_TYPES.Effect) {
71
+ return COMPILER_TYPES.Effect;
72
+ }
73
+
74
+ return COMPILER_TYPES.Unknown;
75
+ }
76
+
77
+ if (checker && (ts.isIdentifier(expr) || ts.isPropertyAccessExpression(expr) || ts.isCallExpression(expr))) {
78
+ try {
79
+ let type = checker.getTypeAtLocation(expr);
80
+
81
+ if (isTypeFunction(type, checker)) {
82
+ return COMPILER_TYPES.Effect;
83
+ }
84
+ }
85
+ catch {
86
+ }
87
+ }
88
+
89
+ return COMPILER_TYPES.Unknown;
90
+ };
91
+
92
+ export { analyze };
@@ -1,8 +1,8 @@
1
1
  import { ts } from '@esportsplus/typescript';
2
2
  import { ast, code as c, imports, uid, type Replacement } from '@esportsplus/typescript/compiler';
3
- import { COMPILER_ENTRYPOINT, COMPILER_TYPES, PACKAGE } from '~/constants';
3
+ import { COMPILER_ENTRYPOINT, COMPILER_TYPES, DIRECT_ATTACH_EVENTS, LIFECYCLE_EVENTS, PACKAGE } from '~/constants';
4
4
  import type { ReactiveCallInfo, TemplateInfo } from './ts-parser';
5
- import { analyzeExpression, generateAttributeBinding, generateSpreadBindings } from './type-analyzer';
5
+ import { analyze } from './analyzer';
6
6
  import parser from './parser';
7
7
 
8
8
 
@@ -17,11 +17,10 @@ type Attribute = {
17
17
 
18
18
  type CodegenContext = {
19
19
  checker?: ts.TypeChecker;
20
- hoistedFactories: Map<string, string>;
21
- htmlToTemplateId: Map<string, string>;
22
- neededImports: Set<string>;
20
+ imports: Map<string, string>;
23
21
  printer: ts.Printer;
24
22
  sourceFile: ts.SourceFile;
23
+ templates: Map<string, string>;
25
24
  };
26
25
 
27
26
  type CodegenResult = {
@@ -46,6 +45,44 @@ const ARROW_EMPTY_PARAMS = /\(\s*\)\s*=>\s*$/;
46
45
  let printer = ts.createPrinter({ newLine: ts.NewLineKind.LineFeed });
47
46
 
48
47
 
48
+ function addImport(ctx: CodegenContext, name: string): string {
49
+ let alias = ctx.imports.get(name);
50
+
51
+ if (!alias) {
52
+ alias = uid(name);
53
+ ctx.imports.set(name, alias);
54
+ }
55
+
56
+ return alias;
57
+ }
58
+
59
+ function generateAttributeBinding(ctx: CodegenContext, element: string, name: string, expr: string, staticValue: string): string {
60
+ if (name.startsWith('on') && name.length > 2) {
61
+ let event = name.slice(2).toLowerCase(),
62
+ key = name.toLowerCase();
63
+
64
+ if (LIFECYCLE_EVENTS.has(key)) {
65
+ return `${addImport(ctx, key)}(${element}, ${expr});`;
66
+ }
67
+
68
+ if (DIRECT_ATTACH_EVENTS.has(key)) {
69
+ return `${addImport(ctx, 'on')}(${element}, '${event}', ${expr});`;
70
+ }
71
+
72
+ return `${addImport(ctx, 'delegate')}(${element}, '${event}', ${expr});`;
73
+ }
74
+
75
+ if (name === 'class') {
76
+ return `${addImport(ctx, 'setClass')}(${element}, '${staticValue}', ${expr});`;
77
+ }
78
+
79
+ if (name === 'style') {
80
+ return `${addImport(ctx, 'setStyle')}(${element}, '${staticValue}', ${expr});`;
81
+ }
82
+
83
+ return `${addImport(ctx, 'setProperty')}(${element}, '${name}', ${expr});`;
84
+ }
85
+
49
86
  function collectNestedTemplateReplacements(
50
87
  ctx: CodegenContext,
51
88
  node: ts.Node,
@@ -96,24 +133,21 @@ function generateNestedTemplateCode(ctx: CodegenContext, node: ts.TaggedTemplate
96
133
 
97
134
  function generateNodeBinding(ctx: CodegenContext, anchor: string, exprText: string, exprNode: ts.Expression | undefined): string {
98
135
  if (!exprNode) {
99
- ctx.neededImports.add('slot');
100
- return `slot(${anchor}, ${exprText});`;
136
+ return `${addImport(ctx, 'slot')}(${anchor}, ${exprText});`;
101
137
  }
102
138
 
103
139
  if (isNestedHtmlTemplate(exprNode)) {
104
140
  return `${anchor}.parentNode.insertBefore(${generateNestedTemplateCode(ctx, exprNode)}, ${anchor});`;
105
141
  }
106
142
 
107
- let slotType = analyzeExpression(exprNode, ctx.checker);
143
+ let slotType = analyze(exprNode, ctx.checker);
108
144
 
109
145
  switch (slotType) {
110
146
  case COMPILER_TYPES.Effect:
111
- ctx.neededImports.add('EffectSlot');
112
- return `new EffectSlot(${anchor}, ${exprText});`;
147
+ return `new ${addImport(ctx, 'EffectSlot')}(${anchor}, ${exprText});`;
113
148
 
114
149
  case COMPILER_TYPES.ArraySlot:
115
- ctx.neededImports.add('ArraySlot');
116
- return `new ArraySlot(${anchor}, ${exprText});`;
150
+ return `new ${addImport(ctx, 'ArraySlot')}(${anchor}, ${exprText});`;
117
151
 
118
152
  case COMPILER_TYPES.Static:
119
153
  return `${anchor}.textContent = ${exprText};`;
@@ -122,8 +156,7 @@ function generateNodeBinding(ctx: CodegenContext, anchor: string, exprText: stri
122
156
  return `${anchor}.parentNode.insertBefore(${exprText}, ${anchor});`;
123
157
 
124
158
  default:
125
- ctx.neededImports.add('slot');
126
- return `slot(${anchor}, ${exprText});`;
159
+ return `${addImport(ctx, 'slot')}(${anchor}, ${exprText});`;
127
160
  }
128
161
  }
129
162
 
@@ -160,24 +193,30 @@ function generateTemplateCode(
160
193
  continue;
161
194
  }
162
195
 
163
- let ancestorVar = root,
164
- startIdx = 0;
196
+ let ancestor = root,
197
+ start = 0;
165
198
 
166
199
  for (let j = path.length - 1; j >= 0; j--) {
167
200
  let prefix = path.slice(0, j).join('.');
168
201
 
169
202
  if (nodes.has(prefix)) {
170
- ancestorVar = nodes.get(prefix)!;
171
- startIdx = j;
203
+ ancestor = nodes.get(prefix)!;
204
+ start = j;
172
205
  break;
173
206
  }
174
207
  }
175
208
 
176
- let name = uid('element'),
177
- suffix = path.slice(startIdx).join('.');
209
+ let alias = addImport(ctx, 'Element'),
210
+ name = uid('element'),
211
+ segments = path.slice(start),
212
+ value = `${ancestor}.${segments.join('!.')}`;
178
213
 
179
- ctx.neededImports.add('Element');
180
- declarations.push(`${name} = ${ancestorVar}.${suffix} as Element`);
214
+ // Cast root.firstChild to Element since DocumentFragment.firstChild returns ChildNode
215
+ if (ancestor === root && segments[0] === 'firstChild') {
216
+ value = value.replace(`${ancestor}.firstChild!`, `(${ancestor}.firstChild! as ${alias})`);
217
+ }
218
+
219
+ declarations.push(`${name} = ${value} as ${alias}`);
181
220
  nodes.set(key, name);
182
221
  }
183
222
 
@@ -187,7 +226,7 @@ function generateTemplateCode(
187
226
  );
188
227
 
189
228
  for (let i = 0, n = slots.length; i < n; i++) {
190
- let elementVar = slots[i].path.length === 0
229
+ let element = slots[i].path.length === 0
191
230
  ? root
192
231
  : (nodes.get(slots[i].path.join('.')) || root),
193
232
  slot = slots[i];
@@ -199,28 +238,19 @@ function generateTemplateCode(
199
238
  let name = names[j];
200
239
 
201
240
  if (name === COMPILER_TYPES.Attributes) {
202
- let bindings = generateSpreadBindings(
203
- exprNodes[index],
204
- exprTexts[index] || 'undefined',
205
- elementVar,
206
- ctx.checker,
207
- ctx.neededImports
208
- );
209
-
210
- for (let k = 0, o = bindings.length; k < o; k++) {
211
- code.push(bindings[k]);
212
- }
213
-
241
+ code.push(
242
+ `${addImport(ctx, 'setProperties')}(${element}, ${exprTexts[index] || 'undefined'});`
243
+ );
214
244
  index++;
215
245
  }
216
246
  else {
217
247
  code.push(
218
248
  generateAttributeBinding(
219
- elementVar,
249
+ ctx,
250
+ element,
220
251
  name,
221
252
  exprTexts[index++] || 'undefined',
222
- slot.attributes.statics[name] || '',
223
- ctx.neededImports
253
+ slot.attributes.statics[name] || ''
224
254
  )
225
255
  );
226
256
  }
@@ -228,7 +258,7 @@ function generateTemplateCode(
228
258
  }
229
259
  else {
230
260
  code.push(
231
- generateNodeBinding(ctx, elementVar, exprTexts[index] || 'undefined', exprNodes[index])
261
+ generateNodeBinding(ctx, element, exprTexts[index] || 'undefined', exprNodes[index])
232
262
  );
233
263
  index++;
234
264
  }
@@ -241,41 +271,16 @@ function generateTemplateCode(
241
271
  }
242
272
 
243
273
  function getOrCreateTemplateId(ctx: CodegenContext, html: string): string {
244
- let id = ctx.htmlToTemplateId.get(html);
274
+ let id = ctx.templates.get(html);
245
275
 
246
276
  if (!id) {
247
- id = uid('tmpl');
248
- ctx.hoistedFactories.set(id, html);
249
- ctx.htmlToTemplateId.set(html, id);
277
+ id = uid('template');
278
+ ctx.templates.set(html, id);
250
279
  }
251
280
 
252
281
  return id;
253
282
  }
254
283
 
255
- function hasArraySlotImport(sourceFile: ts.SourceFile): boolean {
256
- for (let i = 0, n = sourceFile.statements.length; i < n; i++) {
257
- let stmt = sourceFile.statements[i];
258
-
259
- if (!ts.isImportDeclaration(stmt) || !stmt.importClause?.namedBindings) {
260
- continue;
261
- }
262
-
263
- let bindings = stmt.importClause.namedBindings;
264
-
265
- if (!ts.isNamedImports(bindings)) {
266
- continue;
267
- }
268
-
269
- for (let j = 0, m = bindings.elements.length; j < m; j++) {
270
- if (bindings.elements[j].name.text === 'ArraySlot') {
271
- return true;
272
- }
273
- }
274
- }
275
-
276
- return false;
277
- }
278
-
279
284
  function isNestedHtmlTemplate(expr: ts.Expression): expr is ts.TaggedTemplateExpression {
280
285
  return ts.isTaggedTemplateExpression(expr) && ts.isIdentifier(expr.tag) && expr.tag.text === COMPILER_ENTRYPOINT;
281
286
  }
@@ -297,7 +302,7 @@ function rewriteExpression(ctx: CodegenContext, expr: ts.Expression): string {
297
302
  }
298
303
 
299
304
 
300
- const generateCode = (templates: TemplateInfo[], originalCode: string, sourceFile: ts.SourceFile, checker?: ts.TypeChecker): CodegenResult => {
305
+ const generateCode = (templates: TemplateInfo[], originalCode: string, sourceFile: ts.SourceFile, checker?: ts.TypeChecker, existingAliases?: Map<string, string>): CodegenResult => {
301
306
  if (templates.length === 0) {
302
307
  return { changed: false, code: originalCode };
303
308
  }
@@ -321,13 +326,13 @@ const generateCode = (templates: TemplateInfo[], originalCode: string, sourceFil
321
326
 
322
327
  let ctx: CodegenContext = {
323
328
  checker,
324
- hoistedFactories: new Map(),
325
- htmlToTemplateId: new Map(),
326
- neededImports: new Set(['template']),
329
+ imports: existingAliases ?? new Map(),
327
330
  printer,
328
- sourceFile
331
+ sourceFile,
332
+ templates: new Map(),
329
333
  },
330
- replacements: Replacement[] = [];
334
+ replacements: Replacement[] = [],
335
+ templateAlias = addImport(ctx, 'template');
331
336
 
332
337
  for (let i = 0, n = rootTemplates.length; i < n; i++) {
333
338
  let exprTexts: string[] = [],
@@ -371,23 +376,31 @@ const generateCode = (templates: TemplateInfo[], originalCode: string, sourceFil
371
376
  let changed = replacements.length > 0,
372
377
  code = c.replaceReverse(originalCode, replacements);
373
378
 
374
- if (changed && ctx.hoistedFactories.size > 0) {
375
- let factories: string[] = [];
379
+ if (changed && ctx.templates.size > 0) {
380
+ let aliasedImports: string[] = [],
381
+ factories: string[] = [],
382
+ updatedSourceFile = ts.createSourceFile(sourceFile.fileName, code, sourceFile.languageVersion, true);
376
383
 
377
- for (let [id, html] of ctx.hoistedFactories) {
378
- factories.push(`const ${id} = template(\`${html}\`);`);
384
+ for (let [name, alias] of ctx.imports) {
385
+ aliasedImports.push(`${name} as ${alias}`);
379
386
  }
380
387
 
381
- code = imports.modify(factories.join('\n') + code, sourceFile, PACKAGE, {
382
- add: ctx.neededImports,
388
+ for (let [html, id] of ctx.templates) {
389
+ factories.push(`const ${id} = ${templateAlias}(\`${html}\`);`);
390
+ }
391
+
392
+ // Remove html entrypoint and add aliased imports
393
+ code = imports.modify(code, updatedSourceFile, PACKAGE, {
394
+ add: new Set(aliasedImports),
383
395
  remove: [COMPILER_ENTRYPOINT]
384
396
  });
397
+ code = factories.join('\n') + '\n\n' + code;
385
398
  }
386
399
 
387
400
  return { changed, code };
388
401
  };
389
402
 
390
- const generateReactiveInlining = (calls: ReactiveCallInfo[], code: string, sourceFile: ts.SourceFile): string => {
403
+ const generateReactiveInlining = (calls: ReactiveCallInfo[], code: string, sourceFile: ts.SourceFile, arraySlotAlias: string): string => {
391
404
  if (calls.length === 0) {
392
405
  return code;
393
406
  }
@@ -399,7 +412,7 @@ const generateReactiveInlining = (calls: ReactiveCallInfo[], code: string, sourc
399
412
 
400
413
  replacements.push({
401
414
  end: call.end,
402
- newText: `new ArraySlot(
415
+ newText: `new ${arraySlotAlias}(
403
416
  ${printer.printNode(ts.EmitHint.Expression, call.arrayArg, sourceFile)},
404
417
  ${printer.printNode(ts.EmitHint.Expression, call.callbackArg, sourceFile)}
405
418
  )`,
@@ -410,14 +423,5 @@ const generateReactiveInlining = (calls: ReactiveCallInfo[], code: string, sourc
410
423
  return c.replaceReverse(code, replacements);
411
424
  };
412
425
 
413
- const needsArraySlotImport = (sourceFile: ts.SourceFile): boolean => {
414
- return ast.hasMatch(sourceFile, n =>
415
- ts.isNewExpression(n) &&
416
- ts.isIdentifier(n.expression) &&
417
- n.expression.text === 'ArraySlot'
418
- ) && !hasArraySlotImport(sourceFile);
419
- };
420
-
421
-
422
- export { generateCode, generateReactiveInlining, needsArraySlotImport };
426
+ export { generateCode, generateReactiveInlining };
423
427
  export type { CodegenResult };
@@ -1,10 +1,16 @@
1
+ import type { PluginContext } from '@esportsplus/typescript/compiler';
1
2
  import { ts } from '@esportsplus/typescript';
2
- import { code as c, imports } from '@esportsplus/typescript/compiler';
3
+ import { code as c, imports, uid } from '@esportsplus/typescript/compiler';
3
4
  import { COMPILER_ENTRYPOINT, COMPILER_ENTRYPOINT_REACTIVITY, PACKAGE } from '~/constants';
4
- import { generateCode, generateReactiveInlining, needsArraySlotImport } from './codegen';
5
- import { findHtmlTemplates, findReactiveCalls } from './ts-parser';
5
+ import { generateCode, generateReactiveInlining } from './codegen';
6
+ import { findHtmlTemplates, findReactiveCalls, type ReactiveCallInfo, type TemplateInfo } from './ts-parser';
6
7
 
7
8
 
9
+ type AnalyzedFile = {
10
+ reactiveCalls: ReactiveCallInfo[];
11
+ templates: TemplateInfo[];
12
+ };
13
+
8
14
  type TransformResult = {
9
15
  changed: boolean;
10
16
  code: string;
@@ -12,6 +18,8 @@ type TransformResult = {
12
18
  };
13
19
 
14
20
 
21
+ const CONTEXT_KEY = 'template:analyzed';
22
+
15
23
  const PATTERNS = [`${COMPILER_ENTRYPOINT}\``, `${COMPILER_ENTRYPOINT}.${COMPILER_ENTRYPOINT_REACTIVITY}`];
16
24
 
17
25
  const REGEX_BACKSLASH = /\\/g;
@@ -19,41 +27,88 @@ const REGEX_BACKSLASH = /\\/g;
19
27
  const REGEX_FORWARD_SLASH = /\//g;
20
28
 
21
29
 
22
- const transform = (sourceFile: ts.SourceFile, program: ts.Program): TransformResult => {
30
+ function getAnalyzedFile(context: PluginContext | undefined, filename: string): AnalyzedFile | undefined {
31
+ return (context?.get(CONTEXT_KEY) as Map<string, AnalyzedFile> | undefined)?.get(filename);
32
+ }
33
+
34
+
35
+ const analyze = (sourceFile: ts.SourceFile, program: ts.Program, context: PluginContext): void => {
23
36
  let code = sourceFile.getFullText();
24
37
 
25
38
  if (!c.contains(code, { patterns: PATTERNS })) {
26
- return { changed: false, code, sourceFile };
39
+ return;
27
40
  }
28
41
 
29
- let checker: ts.TypeChecker | undefined,
30
- fileName = sourceFile.fileName,
31
- programSourceFile = program.getSourceFile(fileName)
32
- || program.getSourceFile(fileName.replace(REGEX_BACKSLASH, '/'))
33
- || program.getSourceFile(fileName.replace(REGEX_FORWARD_SLASH, '\\'));
42
+ let checker = program.getTypeChecker(),
43
+ filename = sourceFile.fileName,
44
+ files = context.get(CONTEXT_KEY) as Map<string, AnalyzedFile> | undefined,
45
+ programSourceFile = program.getSourceFile(filename)
46
+ || program.getSourceFile(filename.replace(REGEX_BACKSLASH, '/'))
47
+ || program.getSourceFile(filename.replace(REGEX_FORWARD_SLASH, '\\'));
34
48
 
35
49
  if (programSourceFile) {
36
- checker = program.getTypeChecker();
37
50
  sourceFile = programSourceFile;
38
51
  }
39
52
 
53
+ if (!files) {
54
+ files = new Map();
55
+ context.set(CONTEXT_KEY, files);
56
+ }
57
+
58
+ files.set(filename, {
59
+ reactiveCalls: findReactiveCalls(sourceFile, checker),
60
+ templates: findHtmlTemplates(sourceFile, checker)
61
+ });
62
+ };
63
+
64
+ const transform = (sourceFile: ts.SourceFile, program: ts.Program, context?: PluginContext): TransformResult => {
65
+ let code = sourceFile.getFullText(),
66
+ filename = sourceFile.fileName;
67
+
68
+ // Try to get pre-analyzed data from context
69
+ let analyzed = getAnalyzedFile(context, filename);
70
+
71
+ // Fall back to inline analysis (for Vite or when context unavailable)
72
+ if (!analyzed) {
73
+ if (!c.contains(code, { patterns: PATTERNS })) {
74
+ return { changed: false, code, sourceFile };
75
+ }
76
+
77
+ let checker = program.getTypeChecker(),
78
+ programSourceFile = program.getSourceFile(filename)
79
+ || program.getSourceFile(filename.replace(REGEX_BACKSLASH, '/'))
80
+ || program.getSourceFile(filename.replace(REGEX_FORWARD_SLASH, '\\'));
81
+
82
+ if (programSourceFile) {
83
+ sourceFile = programSourceFile;
84
+ }
85
+
86
+ analyzed = {
87
+ reactiveCalls: findReactiveCalls(sourceFile, checker),
88
+ templates: findHtmlTemplates(sourceFile, checker)
89
+ };
90
+ }
91
+
40
92
  let changed = false,
41
93
  codegenChanged = false,
42
- needsImport = false,
43
- reactiveCalls = findReactiveCalls(sourceFile, checker),
94
+ existingAliases = new Map<string, string>(),
44
95
  result = code;
45
96
 
46
- if (reactiveCalls.length > 0) {
97
+ if (analyzed.reactiveCalls.length > 0) {
47
98
  changed = true;
48
- result = generateReactiveInlining(reactiveCalls, result, sourceFile);
99
+ existingAliases.set('ArraySlot', uid('ArraySlot'));
100
+ result = generateReactiveInlining(analyzed.reactiveCalls, result, sourceFile, existingAliases.get('ArraySlot')!);
49
101
  sourceFile = ts.createSourceFile(sourceFile.fileName, result, sourceFile.languageVersion, true);
50
- needsImport = needsArraySlotImport(sourceFile);
51
- }
52
102
 
53
- let templates = findHtmlTemplates(sourceFile, checker);
103
+ // Re-analyze templates after reactive inlining modifies the code
104
+ analyzed = {
105
+ reactiveCalls: [],
106
+ templates: findHtmlTemplates(sourceFile, program.getTypeChecker())
107
+ };
108
+ }
54
109
 
55
- if (templates.length > 0) {
56
- let codegenResult = generateCode(templates, result, sourceFile, checker);
110
+ if (analyzed.templates.length > 0) {
111
+ let codegenResult = generateCode(analyzed.templates, result, sourceFile, program.getTypeChecker(), existingAliases);
57
112
 
58
113
  if (codegenResult.changed) {
59
114
  changed = true;
@@ -62,8 +117,15 @@ const transform = (sourceFile: ts.SourceFile, program: ts.Program): TransformRes
62
117
  }
63
118
  }
64
119
 
65
- if (needsImport && !codegenChanged) {
66
- result = imports.modify(result, sourceFile, PACKAGE, { add: ['ArraySlot'] });
120
+ // Add aliased ArraySlot import if reactive calls were processed but codegen didn't run
121
+ if (existingAliases.size > 0 && !codegenChanged) {
122
+ let aliasedImports: string[] = [];
123
+
124
+ for (let [name, alias] of existingAliases) {
125
+ aliasedImports.push(`${name} as ${alias}`);
126
+ }
127
+
128
+ result = imports.modify(result, sourceFile, PACKAGE, { add: aliasedImports });
67
129
  }
68
130
 
69
131
  if (changed) {
@@ -74,4 +136,4 @@ const transform = (sourceFile: ts.SourceFile, program: ts.Program): TransformRes
74
136
  };
75
137
 
76
138
 
77
- export { transform };
139
+ export { analyze, transform };
@@ -1,5 +1,5 @@
1
1
  import { plugin } from '@esportsplus/typescript/compiler';
2
- import { transform } from '..';
2
+ import { analyze, transform } from '..';
3
3
 
4
4
 
5
- export default plugin.tsc(transform) as ReturnType<typeof plugin.tsc>;
5
+ export default plugin.tsc({ analyze, transform }) as ReturnType<typeof plugin.tsc>;
@@ -1,9 +1,10 @@
1
1
  import { plugin } from '@esportsplus/typescript/compiler';
2
2
  import { PACKAGE } from '../../constants';
3
- import { transform } from '..';
3
+ import { analyze, transform } from '..';
4
4
 
5
5
 
6
6
  export default plugin.vite({
7
+ analyze,
7
8
  name: PACKAGE,
8
9
  transform
9
10
  });
@@ -50,10 +50,10 @@ function isHtmlFromPackage(node: ts.Identifier, checker: ts.TypeChecker | undefi
50
50
 
51
51
  // Check if any declaration is from our package
52
52
  for (let i = 0, n = declarations.length; i < n; i++) {
53
- let fileName = declarations[i].getSourceFile().fileName;
53
+ let filename = declarations[i].getSourceFile().fileName;
54
54
 
55
55
  // Check for package in node_modules path or direct package reference
56
- if (fileName.includes(PACKAGE) || fileName.includes('@esportsplus/template')) {
56
+ if (filename.includes(PACKAGE) || filename.includes('@esportsplus/template')) {
57
57
  return true;
58
58
  }
59
59
  }
@@ -19,7 +19,7 @@ export default defineConfig({
19
19
  'templates': resolve(__dirname, 'templates.ts')
20
20
  },
21
21
  formats: ['es'],
22
- fileName: (_, entryName) => `${entryName}.js`
22
+ filename: (_, entryName) => `${entryName}.js`
23
23
  },
24
24
  outDir: resolve(__dirname, 'build'),
25
25
  emptyOutDir: true,
@@ -1,6 +0,0 @@
1
- import { COMPILER_TYPES } from '../constants.js';
2
- import { ts } from '@esportsplus/typescript';
3
- declare const analyzeExpression: (expr: ts.Expression, checker?: ts.TypeChecker) => COMPILER_TYPES;
4
- declare const generateAttributeBinding: (elementVar: string, name: string, expr: string, staticValue: string, neededImports: Set<string>) => string;
5
- declare const generateSpreadBindings: (expr: ts.Expression, exprCode: string, elementVar: string, checker: ts.TypeChecker | undefined, neededImports: Set<string>) => string[];
6
- export { analyzeExpression, generateAttributeBinding, generateSpreadBindings };