@esportsplus/template 0.35.1 → 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,4 @@
1
+ import { COMPILER_TYPES } from '../constants.js';
2
+ import { ts } from '@esportsplus/typescript';
3
+ declare const analyze: (expr: ts.Expression, checker?: ts.TypeChecker) => COMPILER_TYPES;
4
+ export { analyze };
@@ -1,6 +1,17 @@
1
- import { COMPILER_ENTRYPOINT, COMPILER_ENTRYPOINT_REACTIVITY, COMPILER_TYPES, DIRECT_ATTACH_EVENTS, LIFECYCLE_EVENTS } from '../constants.js';
1
+ import { COMPILER_ENTRYPOINT, COMPILER_ENTRYPOINT_REACTIVITY, COMPILER_TYPES } from '../constants.js';
2
2
  import { ts } from '@esportsplus/typescript';
3
- function inferCOMPILER_TYPES(expr, checker) {
3
+ function isTypeFunction(type, checker) {
4
+ if (type.isUnion()) {
5
+ for (let i = 0, n = type.types.length; i < n; i++) {
6
+ if (!isTypeFunction(type.types[i], checker)) {
7
+ return false;
8
+ }
9
+ }
10
+ return type.types.length > 0;
11
+ }
12
+ return type.getCallSignatures().length > 0;
13
+ }
14
+ const analyze = (expr, checker) => {
4
15
  while (ts.isParenthesizedExpression(expr)) {
5
16
  expr = expr.expression;
6
17
  }
@@ -30,7 +41,7 @@ function inferCOMPILER_TYPES(expr, checker) {
30
41
  return COMPILER_TYPES.Primitive;
31
42
  }
32
43
  if (ts.isConditionalExpression(expr)) {
33
- let whenFalse = inferCOMPILER_TYPES(expr.whenFalse, checker), whenTrue = inferCOMPILER_TYPES(expr.whenTrue, checker);
44
+ let whenFalse = analyze(expr.whenFalse, checker), whenTrue = analyze(expr.whenTrue, checker);
34
45
  if (whenTrue === whenFalse) {
35
46
  return whenTrue;
36
47
  }
@@ -50,48 +61,5 @@ function inferCOMPILER_TYPES(expr, checker) {
50
61
  }
51
62
  }
52
63
  return COMPILER_TYPES.Unknown;
53
- }
54
- function isTypeFunction(type, checker) {
55
- if (type.isUnion()) {
56
- let allFunctions = true, hasFunction = false;
57
- for (let i = 0, n = type.types.length; i < n; i++) {
58
- if (isTypeFunction(type.types[i], checker)) {
59
- hasFunction = true;
60
- }
61
- else {
62
- allFunctions = false;
63
- }
64
- }
65
- return hasFunction && allFunctions;
66
- }
67
- return type.getCallSignatures().length > 0;
68
- }
69
- const analyzeExpression = (expr, checker) => {
70
- return inferCOMPILER_TYPES(expr, checker);
71
- };
72
- const generateAttributeBinding = (elementVar, name, expr, staticValue, addImport) => {
73
- if (name.startsWith('on') && name.length > 2) {
74
- let event = name.slice(2).toLowerCase(), key = name.toLowerCase();
75
- if (LIFECYCLE_EVENTS.has(key)) {
76
- return `${addImport(key)}(${elementVar}, ${expr});`;
77
- }
78
- if (DIRECT_ATTACH_EVENTS.has(key)) {
79
- return `${addImport('on')}(${elementVar}, '${event}', ${expr});`;
80
- }
81
- return `${addImport('delegate')}(${elementVar}, '${event}', ${expr});`;
82
- }
83
- if (name === 'class') {
84
- return `${addImport('setClass')}(${elementVar}, '${staticValue}', ${expr});`;
85
- }
86
- if (name === COMPILER_TYPES.Attributes) {
87
- return `${addImport('setProperties')}(${elementVar}, ${expr});`;
88
- }
89
- if (name === 'style') {
90
- return `${addImport('setStyle')}(${elementVar}, '${staticValue}', ${expr});`;
91
- }
92
- return `${addImport('setProperty')}(${elementVar}, '${name}', ${expr});`;
93
- };
94
- const generateSpreadBindings = (exprCode, elementVar, addImport) => {
95
- return [`${addImport('setProperties')}(${elementVar}, ${exprCode});`];
96
64
  };
97
- export { analyzeExpression, generateAttributeBinding, generateSpreadBindings };
65
+ export { analyze };
@@ -1,7 +1,7 @@
1
1
  import { ts } from '@esportsplus/typescript';
2
2
  import { ast, code as c, imports, uid } from '@esportsplus/typescript/compiler';
3
- import { COMPILER_ENTRYPOINT, COMPILER_TYPES, PACKAGE } from '../constants.js';
4
- import { analyzeExpression, generateAttributeBinding, generateSpreadBindings } from './type-analyzer.js';
3
+ import { COMPILER_ENTRYPOINT, COMPILER_TYPES, DIRECT_ATTACH_EVENTS, LIFECYCLE_EVENTS, PACKAGE } from '../constants.js';
4
+ import { analyze } from './analyzer.js';
5
5
  import parser from './parser.js';
6
6
  const ARROW_EMPTY_PARAMS = /\(\s*\)\s*=>\s*$/;
7
7
  let printer = ts.createPrinter({ newLine: ts.NewLineKind.LineFeed });
@@ -13,6 +13,25 @@ function addImport(ctx, name) {
13
13
  }
14
14
  return alias;
15
15
  }
16
+ function generateAttributeBinding(ctx, element, name, expr, staticValue) {
17
+ if (name.startsWith('on') && name.length > 2) {
18
+ let event = name.slice(2).toLowerCase(), key = name.toLowerCase();
19
+ if (LIFECYCLE_EVENTS.has(key)) {
20
+ return `${addImport(ctx, key)}(${element}, ${expr});`;
21
+ }
22
+ if (DIRECT_ATTACH_EVENTS.has(key)) {
23
+ return `${addImport(ctx, 'on')}(${element}, '${event}', ${expr});`;
24
+ }
25
+ return `${addImport(ctx, 'delegate')}(${element}, '${event}', ${expr});`;
26
+ }
27
+ if (name === 'class') {
28
+ return `${addImport(ctx, 'setClass')}(${element}, '${staticValue}', ${expr});`;
29
+ }
30
+ if (name === 'style') {
31
+ return `${addImport(ctx, 'setStyle')}(${element}, '${staticValue}', ${expr});`;
32
+ }
33
+ return `${addImport(ctx, 'setProperty')}(${element}, '${name}', ${expr});`;
34
+ }
16
35
  function collectNestedTemplateReplacements(ctx, node, exprStart, replacements) {
17
36
  if (isNestedHtmlTemplate(node)) {
18
37
  replacements.push({
@@ -48,7 +67,7 @@ function generateNodeBinding(ctx, anchor, exprText, exprNode) {
48
67
  if (isNestedHtmlTemplate(exprNode)) {
49
68
  return `${anchor}.parentNode.insertBefore(${generateNestedTemplateCode(ctx, exprNode)}, ${anchor});`;
50
69
  }
51
- let slotType = analyzeExpression(exprNode, ctx.checker);
70
+ let slotType = analyze(exprNode, ctx.checker);
52
71
  switch (slotType) {
53
72
  case COMPILER_TYPES.Effect:
54
73
  return `new ${addImport(ctx, 'EffectSlot')}(${anchor}, ${exprText});`;
@@ -104,14 +123,11 @@ function generateTemplateCode(ctx, { html, slots }, exprTexts, exprNodes, isArro
104
123
  for (let j = 0, m = names.length; j < m; j++) {
105
124
  let name = names[j];
106
125
  if (name === COMPILER_TYPES.Attributes) {
107
- let bindings = generateSpreadBindings(exprTexts[index] || 'undefined', element, n => addImport(ctx, n));
108
- for (let k = 0, o = bindings.length; k < o; k++) {
109
- code.push(bindings[k]);
110
- }
126
+ code.push(`${addImport(ctx, 'setProperties')}(${element}, ${exprTexts[index] || 'undefined'});`);
111
127
  index++;
112
128
  }
113
129
  else {
114
- code.push(generateAttributeBinding(element, name, exprTexts[index++] || 'undefined', slot.attributes.statics[name] || '', n => addImport(ctx, n)));
130
+ code.push(generateAttributeBinding(ctx, element, name, exprTexts[index++] || 'undefined', slot.attributes.statics[name] || ''));
115
131
  }
116
132
  }
117
133
  }
@@ -163,10 +179,10 @@ const generateCode = (templates, originalCode, sourceFile, checker, existingAlia
163
179
  }
164
180
  let ctx = {
165
181
  checker,
166
- templates: new Map(),
167
182
  imports: existingAliases ?? new Map(),
168
183
  printer,
169
- sourceFile
184
+ sourceFile,
185
+ templates: new Map(),
170
186
  }, replacements = [], templateAlias = addImport(ctx, 'template');
171
187
  for (let i = 0, n = rootTemplates.length; i < n; i++) {
172
188
  let exprTexts = [], template = rootTemplates[i];
@@ -193,14 +209,14 @@ const generateCode = (templates, originalCode, sourceFile, checker, existingAlia
193
209
  }
194
210
  let changed = replacements.length > 0, code = c.replaceReverse(originalCode, replacements);
195
211
  if (changed && ctx.templates.size > 0) {
196
- let aliasedImports = [], factories = [];
212
+ let aliasedImports = [], factories = [], updatedSourceFile = ts.createSourceFile(sourceFile.fileName, code, sourceFile.languageVersion, true);
197
213
  for (let [name, alias] of ctx.imports) {
198
214
  aliasedImports.push(`${name} as ${alias}`);
199
215
  }
200
216
  for (let [html, id] of ctx.templates) {
201
217
  factories.push(`const ${id} = ${templateAlias}(\`${html}\`);`);
202
218
  }
203
- code = imports.modify(code, sourceFile, PACKAGE, {
219
+ code = imports.modify(code, updatedSourceFile, PACKAGE, {
204
220
  add: new Set(aliasedImports),
205
221
  remove: [COMPILER_ENTRYPOINT]
206
222
  });
@@ -1,8 +1,10 @@
1
+ import type { PluginContext } from '@esportsplus/typescript/compiler';
1
2
  import { ts } from '@esportsplus/typescript';
2
3
  type TransformResult = {
3
4
  changed: boolean;
4
5
  code: string;
5
6
  sourceFile: ts.SourceFile;
6
7
  };
7
- declare const transform: (sourceFile: ts.SourceFile, program: ts.Program) => TransformResult;
8
- export { transform };
8
+ declare const analyze: (sourceFile: ts.SourceFile, program: ts.Program, context: PluginContext) => void;
9
+ declare const transform: (sourceFile: ts.SourceFile, program: ts.Program, context?: PluginContext) => TransformResult;
10
+ export { analyze, transform };
@@ -3,32 +3,64 @@ import { code as c, imports, uid } from '@esportsplus/typescript/compiler';
3
3
  import { COMPILER_ENTRYPOINT, COMPILER_ENTRYPOINT_REACTIVITY, PACKAGE } from '../constants.js';
4
4
  import { generateCode, generateReactiveInlining } from './codegen.js';
5
5
  import { findHtmlTemplates, findReactiveCalls } from './ts-parser.js';
6
+ const CONTEXT_KEY = 'template:analyzed';
6
7
  const PATTERNS = [`${COMPILER_ENTRYPOINT}\``, `${COMPILER_ENTRYPOINT}.${COMPILER_ENTRYPOINT_REACTIVITY}`];
7
8
  const REGEX_BACKSLASH = /\\/g;
8
9
  const REGEX_FORWARD_SLASH = /\//g;
9
- const transform = (sourceFile, program) => {
10
+ function getAnalyzedFile(context, filename) {
11
+ return context?.get(CONTEXT_KEY)?.get(filename);
12
+ }
13
+ const analyze = (sourceFile, program, context) => {
10
14
  let code = sourceFile.getFullText();
11
15
  if (!c.contains(code, { patterns: PATTERNS })) {
12
- return { changed: false, code, sourceFile };
16
+ return;
13
17
  }
14
- let checker, fileName = sourceFile.fileName, programSourceFile = program.getSourceFile(fileName)
15
- || program.getSourceFile(fileName.replace(REGEX_BACKSLASH, '/'))
16
- || program.getSourceFile(fileName.replace(REGEX_FORWARD_SLASH, '\\'));
18
+ let checker = program.getTypeChecker(), filename = sourceFile.fileName, files = context.get(CONTEXT_KEY), programSourceFile = program.getSourceFile(filename)
19
+ || program.getSourceFile(filename.replace(REGEX_BACKSLASH, '/'))
20
+ || program.getSourceFile(filename.replace(REGEX_FORWARD_SLASH, '\\'));
17
21
  if (programSourceFile) {
18
- checker = program.getTypeChecker();
19
22
  sourceFile = programSourceFile;
20
23
  }
21
- let changed = false, codegenChanged = false, existingAliases = new Map(), reactiveCalls = findReactiveCalls(sourceFile, checker), result = code;
22
- if (reactiveCalls.length > 0) {
23
- let arraySlotAlias = uid('ArraySlot');
24
+ if (!files) {
25
+ files = new Map();
26
+ context.set(CONTEXT_KEY, files);
27
+ }
28
+ files.set(filename, {
29
+ reactiveCalls: findReactiveCalls(sourceFile, checker),
30
+ templates: findHtmlTemplates(sourceFile, checker)
31
+ });
32
+ };
33
+ const transform = (sourceFile, program, context) => {
34
+ let code = sourceFile.getFullText(), filename = sourceFile.fileName;
35
+ let analyzed = getAnalyzedFile(context, filename);
36
+ if (!analyzed) {
37
+ if (!c.contains(code, { patterns: PATTERNS })) {
38
+ return { changed: false, code, sourceFile };
39
+ }
40
+ let checker = program.getTypeChecker(), programSourceFile = program.getSourceFile(filename)
41
+ || program.getSourceFile(filename.replace(REGEX_BACKSLASH, '/'))
42
+ || program.getSourceFile(filename.replace(REGEX_FORWARD_SLASH, '\\'));
43
+ if (programSourceFile) {
44
+ sourceFile = programSourceFile;
45
+ }
46
+ analyzed = {
47
+ reactiveCalls: findReactiveCalls(sourceFile, checker),
48
+ templates: findHtmlTemplates(sourceFile, checker)
49
+ };
50
+ }
51
+ let changed = false, codegenChanged = false, existingAliases = new Map(), result = code;
52
+ if (analyzed.reactiveCalls.length > 0) {
24
53
  changed = true;
25
- existingAliases.set('ArraySlot', arraySlotAlias);
26
- result = generateReactiveInlining(reactiveCalls, result, sourceFile, arraySlotAlias);
54
+ existingAliases.set('ArraySlot', uid('ArraySlot'));
55
+ result = generateReactiveInlining(analyzed.reactiveCalls, result, sourceFile, existingAliases.get('ArraySlot'));
27
56
  sourceFile = ts.createSourceFile(sourceFile.fileName, result, sourceFile.languageVersion, true);
57
+ analyzed = {
58
+ reactiveCalls: [],
59
+ templates: findHtmlTemplates(sourceFile, program.getTypeChecker())
60
+ };
28
61
  }
29
- let templates = findHtmlTemplates(sourceFile, checker);
30
- if (templates.length > 0) {
31
- let codegenResult = generateCode(templates, result, sourceFile, checker, existingAliases);
62
+ if (analyzed.templates.length > 0) {
63
+ let codegenResult = generateCode(analyzed.templates, result, sourceFile, program.getTypeChecker(), existingAliases);
32
64
  if (codegenResult.changed) {
33
65
  changed = true;
34
66
  codegenChanged = true;
@@ -47,4 +79,4 @@ const transform = (sourceFile, program) => {
47
79
  }
48
80
  return { changed, code: result, sourceFile };
49
81
  };
50
- export { transform };
82
+ export { analyze, transform };
@@ -1,3 +1,3 @@
1
1
  import { plugin } from '@esportsplus/typescript/compiler';
2
- import { transform } from '../index.js';
3
- export default plugin.tsc(transform);
2
+ import { analyze, transform } from '../index.js';
3
+ export default plugin.tsc({ analyze, transform });
@@ -1,13 +1,13 @@
1
1
  declare const _default: ({ root }?: {
2
2
  root?: string;
3
3
  }) => {
4
- configResolved(config: import("vite").ResolvedConfig): void;
5
- enforce: string;
4
+ configResolved: (config: unknown) => void;
5
+ enforce: "pre";
6
6
  name: string;
7
- transform(code: string, id: string): {
7
+ transform: (code: string, id: string) => {
8
8
  code: string;
9
9
  map: null;
10
10
  } | null;
11
- watchChange(id: string): void;
11
+ watchChange: (id: string) => void;
12
12
  };
13
13
  export default _default;
@@ -1,7 +1,8 @@
1
1
  import { plugin } from '@esportsplus/typescript/compiler';
2
2
  import { PACKAGE } from '../../constants.js';
3
- import { transform } from '../index.js';
3
+ import { analyze, transform } from '../index.js';
4
4
  export default plugin.vite({
5
+ analyze,
5
6
  name: PACKAGE,
6
7
  transform
7
8
  });
@@ -19,8 +19,8 @@ function isHtmlFromPackage(node, checker) {
19
19
  return true;
20
20
  }
21
21
  for (let i = 0, n = declarations.length; i < n; i++) {
22
- let fileName = declarations[i].getSourceFile().fileName;
23
- if (fileName.includes(PACKAGE) || fileName.includes('@esportsplus/template')) {
22
+ let filename = declarations[i].getSourceFile().fileName;
23
+ if (filename.includes(PACKAGE) || filename.includes('@esportsplus/template')) {
24
24
  return true;
25
25
  }
26
26
  }
package/package.json CHANGED
@@ -2,14 +2,14 @@
2
2
  "author": "ICJR",
3
3
  "dependencies": {
4
4
  "@esportsplus/queue": "^0.2.0",
5
- "@esportsplus/reactivity": "^0.27.0",
5
+ "@esportsplus/reactivity": "^0.27.3",
6
+ "@esportsplus/typescript": "^0.24.2",
6
7
  "@esportsplus/utilities": "^0.27.2",
7
8
  "serve": "^14.2.5"
8
9
  },
9
10
  "devDependencies": {
10
- "@esportsplus/typescript": "^0.22.0",
11
11
  "@types/node": "^25.0.3",
12
- "vite": "^7.3.0",
12
+ "vite": "^7.3.1",
13
13
  "vite-tsconfig-paths": "^6.0.3"
14
14
  },
15
15
  "exports": {
@@ -35,7 +35,7 @@
35
35
  },
36
36
  "type": "module",
37
37
  "types": "./build/index.d.ts",
38
- "version": "0.35.1",
38
+ "version": "0.37.0",
39
39
  "scripts": {
40
40
  "build": "tsc",
41
41
  "build:test": "vite build --config test/vite.config.ts",
@@ -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
 
@@ -56,6 +56,33 @@ function addImport(ctx: CodegenContext, name: string): string {
56
56
  return alias;
57
57
  }
58
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
+
59
86
  function collectNestedTemplateReplacements(
60
87
  ctx: CodegenContext,
61
88
  node: ts.Node,
@@ -113,7 +140,7 @@ function generateNodeBinding(ctx: CodegenContext, anchor: string, exprText: stri
113
140
  return `${anchor}.parentNode.insertBefore(${generateNestedTemplateCode(ctx, exprNode)}, ${anchor});`;
114
141
  }
115
142
 
116
- let slotType = analyzeExpression(exprNode, ctx.checker);
143
+ let slotType = analyze(exprNode, ctx.checker);
117
144
 
118
145
  switch (slotType) {
119
146
  case COMPILER_TYPES.Effect:
@@ -211,26 +238,19 @@ function generateTemplateCode(
211
238
  let name = names[j];
212
239
 
213
240
  if (name === COMPILER_TYPES.Attributes) {
214
- let bindings = generateSpreadBindings(
215
- exprTexts[index] || 'undefined',
216
- element,
217
- n => addImport(ctx, n)
218
- );
219
-
220
- for (let k = 0, o = bindings.length; k < o; k++) {
221
- code.push(bindings[k]);
222
- }
223
-
241
+ code.push(
242
+ `${addImport(ctx, 'setProperties')}(${element}, ${exprTexts[index] || 'undefined'});`
243
+ );
224
244
  index++;
225
245
  }
226
246
  else {
227
247
  code.push(
228
248
  generateAttributeBinding(
249
+ ctx,
229
250
  element,
230
251
  name,
231
252
  exprTexts[index++] || 'undefined',
232
- slot.attributes.statics[name] || '',
233
- n => addImport(ctx, n)
253
+ slot.attributes.statics[name] || ''
234
254
  )
235
255
  );
236
256
  }
@@ -306,10 +326,10 @@ const generateCode = (templates: TemplateInfo[], originalCode: string, sourceFil
306
326
 
307
327
  let ctx: CodegenContext = {
308
328
  checker,
309
- templates: new Map(),
310
329
  imports: existingAliases ?? new Map(),
311
330
  printer,
312
- sourceFile
331
+ sourceFile,
332
+ templates: new Map(),
313
333
  },
314
334
  replacements: Replacement[] = [],
315
335
  templateAlias = addImport(ctx, 'template');
@@ -358,7 +378,8 @@ const generateCode = (templates: TemplateInfo[], originalCode: string, sourceFil
358
378
 
359
379
  if (changed && ctx.templates.size > 0) {
360
380
  let aliasedImports: string[] = [],
361
- factories: string[] = [];
381
+ factories: string[] = [],
382
+ updatedSourceFile = ts.createSourceFile(sourceFile.fileName, code, sourceFile.languageVersion, true);
362
383
 
363
384
  for (let [name, alias] of ctx.imports) {
364
385
  aliasedImports.push(`${name} as ${alias}`);
@@ -369,7 +390,7 @@ const generateCode = (templates: TemplateInfo[], originalCode: string, sourceFil
369
390
  }
370
391
 
371
392
  // Remove html entrypoint and add aliased imports
372
- code = imports.modify(code, sourceFile, PACKAGE, {
393
+ code = imports.modify(code, updatedSourceFile, PACKAGE, {
373
394
  add: new Set(aliasedImports),
374
395
  remove: [COMPILER_ENTRYPOINT]
375
396
  });
@@ -1,10 +1,16 @@
1
+ import type { PluginContext } from '@esportsplus/typescript/compiler';
1
2
  import { ts } from '@esportsplus/typescript';
2
3
  import { code as c, imports, uid } from '@esportsplus/typescript/compiler';
3
4
  import { COMPILER_ENTRYPOINT, COMPILER_ENTRYPOINT_REACTIVITY, PACKAGE } from '~/constants';
4
5
  import { generateCode, generateReactiveInlining } from './codegen';
5
- import { findHtmlTemplates, findReactiveCalls } from './ts-parser';
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,43 +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
94
  existingAliases = new Map<string, string>(),
43
- reactiveCalls = findReactiveCalls(sourceFile, checker),
44
95
  result = code;
45
96
 
46
- if (reactiveCalls.length > 0) {
47
- let arraySlotAlias = uid('ArraySlot');
48
-
97
+ if (analyzed.reactiveCalls.length > 0) {
49
98
  changed = true;
50
- existingAliases.set('ArraySlot', arraySlotAlias);
51
- result = generateReactiveInlining(reactiveCalls, result, sourceFile, arraySlotAlias);
99
+ existingAliases.set('ArraySlot', uid('ArraySlot'));
100
+ result = generateReactiveInlining(analyzed.reactiveCalls, result, sourceFile, existingAliases.get('ArraySlot')!);
52
101
  sourceFile = ts.createSourceFile(sourceFile.fileName, result, sourceFile.languageVersion, true);
53
- }
54
102
 
55
- 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
+ }
56
109
 
57
- if (templates.length > 0) {
58
- let codegenResult = generateCode(templates, result, sourceFile, checker, existingAliases);
110
+ if (analyzed.templates.length > 0) {
111
+ let codegenResult = generateCode(analyzed.templates, result, sourceFile, program.getTypeChecker(), existingAliases);
59
112
 
60
113
  if (codegenResult.changed) {
61
114
  changed = true;
@@ -83,4 +136,4 @@ const transform = (sourceFile: ts.SourceFile, program: ts.Program): TransformRes
83
136
  };
84
137
 
85
138
 
86
- 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, addImport: (name: string) => string) => string;
5
- declare const generateSpreadBindings: (exprCode: string, elementVar: string, addImport: (name: string) => string) => string[];
6
- export { analyzeExpression, generateAttributeBinding, generateSpreadBindings };
@@ -1,148 +0,0 @@
1
- import {
2
- COMPILER_ENTRYPOINT,
3
- COMPILER_ENTRYPOINT_REACTIVITY,
4
- COMPILER_TYPES,
5
- DIRECT_ATTACH_EVENTS,
6
- LIFECYCLE_EVENTS
7
- } from '~/constants';
8
- import { ts } from '@esportsplus/typescript';
9
-
10
-
11
- function inferCOMPILER_TYPES(expr: ts.Expression, checker?: ts.TypeChecker): COMPILER_TYPES {
12
- while (ts.isParenthesizedExpression(expr)) {
13
- expr = expr.expression;
14
- }
15
-
16
- if (ts.isArrowFunction(expr) || ts.isFunctionExpression(expr)) {
17
- return COMPILER_TYPES.Effect;
18
- }
19
-
20
- // Only html.reactive() calls become ArraySlot - handled by generateReactiveInlining
21
- if (
22
- ts.isCallExpression(expr) &&
23
- ts.isPropertyAccessExpression(expr.expression) &&
24
- ts.isIdentifier(expr.expression.expression) &&
25
- expr.expression.expression.text === COMPILER_ENTRYPOINT &&
26
- expr.expression.name.text === COMPILER_ENTRYPOINT_REACTIVITY
27
- ) {
28
- return COMPILER_TYPES.ArraySlot;
29
- }
30
-
31
- if (ts.isTaggedTemplateExpression(expr) && ts.isIdentifier(expr.tag) && expr.tag.text === COMPILER_ENTRYPOINT) {
32
- return COMPILER_TYPES.DocumentFragment;
33
- }
34
-
35
- if (
36
- ts.isNumericLiteral(expr) ||
37
- ts.isStringLiteral(expr) ||
38
- ts.isNoSubstitutionTemplateLiteral(expr) ||
39
- expr.kind === ts.SyntaxKind.TrueKeyword ||
40
- expr.kind === ts.SyntaxKind.FalseKeyword ||
41
- expr.kind === ts.SyntaxKind.NullKeyword ||
42
- expr.kind === ts.SyntaxKind.UndefinedKeyword
43
- ) {
44
- return COMPILER_TYPES.Static;
45
- }
46
-
47
- if (ts.isTemplateExpression(expr)) {
48
- return COMPILER_TYPES.Primitive;
49
- }
50
-
51
- if (ts.isConditionalExpression(expr)) {
52
- let whenFalse = inferCOMPILER_TYPES(expr.whenFalse, checker),
53
- whenTrue = inferCOMPILER_TYPES(expr.whenTrue, checker);
54
-
55
- if (whenTrue === whenFalse) {
56
- return whenTrue;
57
- }
58
-
59
- if (whenTrue === COMPILER_TYPES.Effect || whenFalse === COMPILER_TYPES.Effect) {
60
- return COMPILER_TYPES.Effect;
61
- }
62
-
63
- return COMPILER_TYPES.Unknown;
64
- }
65
-
66
- if (checker && (ts.isIdentifier(expr) || ts.isPropertyAccessExpression(expr) || ts.isCallExpression(expr))) {
67
- try {
68
- let type = checker.getTypeAtLocation(expr);
69
-
70
- if (isTypeFunction(type, checker)) {
71
- return COMPILER_TYPES.Effect;
72
- }
73
- }
74
- catch {
75
- }
76
- }
77
-
78
- return COMPILER_TYPES.Unknown;
79
- }
80
-
81
- function isTypeFunction(type: ts.Type, checker: ts.TypeChecker): boolean {
82
- // Union types that mix functions with non-functions (e.g., Renderable)
83
- // should fall through to runtime slot dispatch
84
- if (type.isUnion()) {
85
- let allFunctions = true,
86
- hasFunction = false;
87
-
88
- for (let i = 0, n = type.types.length; i < n; i++) {
89
- if (isTypeFunction(type.types[i], checker)) {
90
- hasFunction = true;
91
- }
92
- else {
93
- allFunctions = false;
94
- }
95
- }
96
-
97
- return hasFunction && allFunctions;
98
- }
99
-
100
- return type.getCallSignatures().length > 0;
101
- }
102
-
103
-
104
- const analyzeExpression = (expr: ts.Expression, checker?: ts.TypeChecker): COMPILER_TYPES => {
105
- return inferCOMPILER_TYPES(expr, checker);
106
- };
107
-
108
- const generateAttributeBinding = (elementVar: string, name: string, expr: string, staticValue: string, addImport: (name: string) => string): string => {
109
- if (name.startsWith('on') && name.length > 2) {
110
- let event = name.slice(2).toLowerCase(),
111
- key = name.toLowerCase();
112
-
113
- if (LIFECYCLE_EVENTS.has(key)) {
114
- return `${addImport(key)}(${elementVar}, ${expr});`;
115
- }
116
-
117
- if (DIRECT_ATTACH_EVENTS.has(key)) {
118
- return `${addImport('on')}(${elementVar}, '${event}', ${expr});`;
119
- }
120
-
121
- return `${addImport('delegate')}(${elementVar}, '${event}', ${expr});`;
122
- }
123
-
124
- if (name === 'class') {
125
- return `${addImport('setClass')}(${elementVar}, '${staticValue}', ${expr});`;
126
- }
127
-
128
- if (name === COMPILER_TYPES.Attributes) {
129
- return `${addImport('setProperties')}(${elementVar}, ${expr});`;
130
- }
131
-
132
- if (name === 'style') {
133
- return `${addImport('setStyle')}(${elementVar}, '${staticValue}', ${expr});`;
134
- }
135
-
136
- return `${addImport('setProperty')}(${elementVar}, '${name}', ${expr});`;
137
- };
138
-
139
- const generateSpreadBindings = (
140
- exprCode: string,
141
- elementVar: string,
142
- addImport: (name: string) => string
143
- ): string[] => {
144
- return [`${addImport('setProperties')}(${elementVar}, ${exprCode});`];
145
- };
146
-
147
-
148
- export { analyzeExpression, generateAttributeBinding, generateSpreadBindings };