@esportsplus/template 0.38.1 → 0.39.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.
@@ -6,6 +6,7 @@ type CodegenResult = {
6
6
  replacements: ReplacementIntent[];
7
7
  templates: Map<string, string>;
8
8
  };
9
+ declare let printer: ts.Printer;
9
10
  declare const generateCode: (templates: TemplateInfo[], sourceFile: ts.SourceFile, checker?: ts.TypeChecker) => CodegenResult;
10
- export { generateCode };
11
+ export { generateCode, printer };
11
12
  export type { CodegenResult };
@@ -1,29 +1,10 @@
1
1
  import { ts } from '@esportsplus/typescript';
2
2
  import { ast, uid } from '@esportsplus/typescript/compiler';
3
3
  import { COMPILER_ENTRYPOINT, COMPILER_ENTRYPOINT_REACTIVITY, COMPILER_NAMESPACE, COMPILER_TYPES, DIRECT_ATTACH_EVENTS, LIFECYCLE_EVENTS } from '../constants.js';
4
- import { analyze } from './analyzer.js';
4
+ import { extractTemplateParts } from './ts-parser.js';
5
+ import { analyze } from './ts-analyzer.js';
5
6
  import parser from './parser.js';
6
- const ARROW_EMPTY_PARAMS = /\(\s*\)\s*=>\s*$/;
7
7
  let printer = ts.createPrinter({ newLine: ts.NewLineKind.LineFeed });
8
- function collectNestedReplacements(ctx, node, exprStart, replacements) {
9
- if (isNestedHtmlTemplate(node)) {
10
- replacements.push({
11
- end: node.end - exprStart,
12
- newText: generateNestedTemplateCode(ctx, node),
13
- start: node.getStart(ctx.sourceFile) - exprStart
14
- });
15
- }
16
- else if (isReactiveCall(node)) {
17
- replacements.push({
18
- end: node.end - exprStart,
19
- newText: rewriteReactiveCall(ctx, node),
20
- start: node.getStart(ctx.sourceFile) - exprStart
21
- });
22
- }
23
- else {
24
- ts.forEachChild(node, child => collectNestedReplacements(ctx, child, exprStart, replacements));
25
- }
26
- }
27
8
  function generateAttributeBinding(element, name, expr, staticValue) {
28
9
  if (name.startsWith('on') && name.length > 2) {
29
10
  let event = name.slice(2).toLowerCase(), key = name.toLowerCase();
@@ -44,20 +25,11 @@ function generateAttributeBinding(element, name, expr, staticValue) {
44
25
  return `${COMPILER_NAMESPACE}.setProperty(${element}, '${name}', ${expr});`;
45
26
  }
46
27
  function generateNestedTemplateCode(ctx, node) {
47
- let expressions = [], exprTexts = [], literals = [], template = node.template;
48
- if (ts.isNoSubstitutionTemplateLiteral(template)) {
49
- literals.push(template.text);
50
- }
51
- else if (ts.isTemplateExpression(template)) {
52
- literals.push(template.head.text);
53
- for (let i = 0, n = template.templateSpans.length; i < n; i++) {
54
- let expr = template.templateSpans[i].expression;
55
- expressions.push(expr);
56
- literals.push(template.templateSpans[i].literal.text);
57
- exprTexts.push(rewriteExpression(ctx, expr));
58
- }
28
+ let { expressions, literals } = extractTemplateParts(node.template), exprTexts = [];
29
+ for (let i = 0, n = expressions.length; i < n; i++) {
30
+ exprTexts.push(rewriteExpression(ctx, expressions[i]));
59
31
  }
60
- return generateTemplateCode(ctx, parser.parse(literals), exprTexts, expressions, false);
32
+ return generateTemplateCode(ctx, parser.parse(literals), exprTexts, expressions, node);
61
33
  }
62
34
  function generateNodeBinding(ctx, anchor, exprText, exprNode) {
63
35
  if (!exprNode) {
@@ -66,8 +38,7 @@ function generateNodeBinding(ctx, anchor, exprText, exprNode) {
66
38
  if (isNestedHtmlTemplate(exprNode)) {
67
39
  return `${anchor}.parentNode!.insertBefore(${generateNestedTemplateCode(ctx, exprNode)}, ${anchor});`;
68
40
  }
69
- let slotType = analyze(exprNode, ctx.checker);
70
- switch (slotType) {
41
+ switch (analyze(exprNode, ctx.checker)) {
71
42
  case COMPILER_TYPES.ArraySlot:
72
43
  return `${anchor}.parentNode!.insertBefore(new ${COMPILER_NAMESPACE}.ArraySlot(${exprText}).fragment, ${anchor});`;
73
44
  case COMPILER_TYPES.DocumentFragment:
@@ -80,11 +51,11 @@ function generateNodeBinding(ctx, anchor, exprText, exprNode) {
80
51
  return `${COMPILER_NAMESPACE}.slot(${anchor}, ${exprText});`;
81
52
  }
82
53
  }
83
- function generateTemplateCode(ctx, { html, slots }, exprTexts, exprNodes, isArrowBody) {
54
+ function generateTemplateCode(ctx, { html, slots }, exprTexts, exprNodes, templateNode) {
84
55
  if (!slots || slots.length === 0) {
85
56
  return `${getOrCreateTemplateId(ctx, html)}()`;
86
57
  }
87
- let code = [], declarations = [], index = 0, nodes = new Map(), root = uid('root');
58
+ let code = [], declarations = [], index = 0, isArrowBody = isArrowExpressionBody(templateNode), nodes = new Map(), root = uid('root');
88
59
  declarations.push(`${root} = ${getOrCreateTemplateId(ctx, html)}()`);
89
60
  nodes.set('', root);
90
61
  for (let i = 0, n = slots.length; i < n; i++) {
@@ -150,6 +121,9 @@ function getOrCreateTemplateId(ctx, html) {
150
121
  function isNestedHtmlTemplate(expr) {
151
122
  return ts.isTaggedTemplateExpression(expr) && ts.isIdentifier(expr.tag) && expr.tag.text === COMPILER_ENTRYPOINT;
152
123
  }
124
+ function isArrowExpressionBody(node) {
125
+ return ts.isArrowFunction(node.parent) && node.parent.body === node;
126
+ }
153
127
  function isReactiveCall(expr) {
154
128
  return (ts.isCallExpression(expr) &&
155
129
  ts.isPropertyAccessExpression(expr.expression) &&
@@ -157,69 +131,104 @@ function isReactiveCall(expr) {
157
131
  expr.expression.expression.text === COMPILER_ENTRYPOINT &&
158
132
  expr.expression.name.text === COMPILER_ENTRYPOINT_REACTIVITY);
159
133
  }
160
- function replaceReverse(text, replacements) {
161
- let sorted = replacements.slice().sort((a, b) => b.start - a.start);
162
- for (let i = 0, n = sorted.length; i < n; i++) {
163
- let r = sorted[i];
164
- text = text.slice(0, r.start) + r.newText + text.slice(r.end);
165
- }
166
- return text;
167
- }
168
134
  function rewriteExpression(ctx, expr) {
169
135
  if (isNestedHtmlTemplate(expr)) {
170
136
  return generateNestedTemplateCode(ctx, expr);
171
137
  }
172
138
  if (isReactiveCall(expr)) {
173
- return rewriteReactiveCall(ctx, expr);
139
+ return `
140
+ ${printer.printNode(ts.EmitHint.Expression, expr.arguments[0], ctx.sourceFile)},
141
+ ${rewriteExpression(ctx, expr.arguments[1])}
142
+ `;
174
143
  }
175
144
  if (!ast.hasMatch(expr, n => isNestedHtmlTemplate(n) || isReactiveCall(n))) {
176
- return ctx.printer.printNode(ts.EmitHint.Expression, expr, ctx.sourceFile);
145
+ return printer.printNode(ts.EmitHint.Expression, expr, ctx.sourceFile);
177
146
  }
178
- let replacements = [];
179
- collectNestedReplacements(ctx, expr, expr.getStart(ctx.sourceFile), replacements);
180
- return replaceReverse(expr.getText(ctx.sourceFile), replacements);
181
- }
182
- function rewriteReactiveCall(ctx, node) {
183
- let arrayArg = node.arguments[0], arrayText = ctx.printer.printNode(ts.EmitHint.Expression, arrayArg, ctx.sourceFile), callbackArg = node.arguments[1], callbackText = rewriteExpression(ctx, callbackArg);
184
- return `${arrayText}, ${callbackText}`;
147
+ return walkExpression(ctx, expr);
185
148
  }
186
- function discoverTemplatesInExpression(ctx, node) {
149
+ function walkExpression(ctx, node) {
187
150
  if (isNestedHtmlTemplate(node)) {
188
- let template = node, expressions = [], literals = [], tpl = template.template;
189
- if (ts.isNoSubstitutionTemplateLiteral(tpl)) {
190
- literals.push(tpl.text);
191
- }
192
- else if (ts.isTemplateExpression(tpl)) {
193
- literals.push(tpl.head.text);
194
- for (let i = 0, n = tpl.templateSpans.length; i < n; i++) {
195
- expressions.push(tpl.templateSpans[i].expression);
196
- literals.push(tpl.templateSpans[i].literal.text);
197
- }
151
+ return generateNestedTemplateCode(ctx, node);
152
+ }
153
+ if (isReactiveCall(node)) {
154
+ let call = node;
155
+ return `new ${COMPILER_NAMESPACE}.ArraySlot(${printer.printNode(ts.EmitHint.Expression, call.arguments[0], ctx.sourceFile)}, ${rewriteExpression(ctx, call.arguments[1])})`;
156
+ }
157
+ if (!ast.hasMatch(node, n => isNestedHtmlTemplate(n) || isReactiveCall(n))) {
158
+ return printer.printNode(ts.EmitHint.Expression, node, ctx.sourceFile);
159
+ }
160
+ if (ts.isConditionalExpression(node)) {
161
+ return `${walkExpression(ctx, node.condition)} ? ${walkExpression(ctx, node.whenTrue)} : ${walkExpression(ctx, node.whenFalse)}`;
162
+ }
163
+ if (ts.isBinaryExpression(node)) {
164
+ return `${walkExpression(ctx, node.left)} ${node.operatorToken.getText(ctx.sourceFile)} ${walkExpression(ctx, node.right)}`;
165
+ }
166
+ if (ts.isCallExpression(node)) {
167
+ let args = [];
168
+ for (let i = 0, n = node.arguments.length; i < n; i++) {
169
+ args.push(walkExpression(ctx, node.arguments[i]));
198
170
  }
199
- let parsed = parser.parse(literals);
200
- getOrCreateTemplateId(ctx, parsed.html);
201
- for (let i = 0, n = expressions.length; i < n; i++) {
202
- discoverTemplatesInExpression(ctx, expressions[i]);
171
+ return `${walkExpression(ctx, node.expression)}(${args.join(', ')})`;
172
+ }
173
+ if (ts.isPropertyAccessExpression(node)) {
174
+ return `${walkExpression(ctx, node.expression)}.${node.name.text}`;
175
+ }
176
+ if (ts.isElementAccessExpression(node)) {
177
+ return `${walkExpression(ctx, node.expression)}[${walkExpression(ctx, node.argumentExpression)}]`;
178
+ }
179
+ if (ts.isParenthesizedExpression(node)) {
180
+ return `(${walkExpression(ctx, node.expression)})`;
181
+ }
182
+ if (ts.isArrowFunction(node)) {
183
+ return `(${node.parameters.map(p => p.getText(ctx.sourceFile)).join(', ')}) => ${ts.isBlock(node.body) ? node.body.getText(ctx.sourceFile) : walkExpression(ctx, node.body)}`;
184
+ }
185
+ if (ts.isArrayLiteralExpression(node)) {
186
+ let elements = [];
187
+ for (let i = 0, n = node.elements.length; i < n; i++) {
188
+ elements.push(walkExpression(ctx, node.elements[i]));
203
189
  }
190
+ return `[${elements.join(', ')}]`;
204
191
  }
205
- else if (isReactiveCall(node)) {
206
- let call = node;
207
- if (call.arguments.length >= 2) {
208
- discoverTemplatesInExpression(ctx, call.arguments[1]);
192
+ if (ts.isObjectLiteralExpression(node)) {
193
+ let properties = [];
194
+ for (let i = 0, n = node.properties.length; i < n; i++) {
195
+ let prop = node.properties[i];
196
+ if (ts.isPropertyAssignment(prop)) {
197
+ properties.push(`${prop.name.getText(ctx.sourceFile)}: ${walkExpression(ctx, prop.initializer)}`);
198
+ }
199
+ else if (ts.isShorthandPropertyAssignment(prop)) {
200
+ properties.push(prop.name.text);
201
+ }
202
+ else if (ts.isSpreadAssignment(prop)) {
203
+ properties.push(`...${walkExpression(ctx, prop.expression)}`);
204
+ }
205
+ else {
206
+ properties.push(prop.getText(ctx.sourceFile));
207
+ }
209
208
  }
209
+ return `{ ${properties.join(', ')} }`;
210
210
  }
211
- else {
212
- ts.forEachChild(node, child => discoverTemplatesInExpression(ctx, child));
211
+ if (ts.isPrefixUnaryExpression(node)) {
212
+ return `${ts.tokenToString(node.operator)}${walkExpression(ctx, node.operand)}`;
213
213
  }
214
- }
215
- function discoverAllTemplates(ctx, templates) {
216
- for (let i = 0, n = templates.length; i < n; i++) {
217
- let parsed = parser.parse(templates[i].literals);
218
- getOrCreateTemplateId(ctx, parsed.html);
219
- for (let j = 0, m = templates[i].expressions.length; j < m; j++) {
220
- discoverTemplatesInExpression(ctx, templates[i].expressions[j]);
214
+ if (ts.isPostfixUnaryExpression(node)) {
215
+ return `${walkExpression(ctx, node.operand)}${ts.tokenToString(node.operator)}`;
216
+ }
217
+ if (ts.isTemplateExpression(node)) {
218
+ let parts = [node.head.text];
219
+ for (let i = 0, n = node.templateSpans.length; i < n; i++) {
220
+ let span = node.templateSpans[i];
221
+ parts.push('${' + walkExpression(ctx, span.expression) + '}' + span.literal.text);
221
222
  }
223
+ return '`' + parts.join('') + '`';
224
+ }
225
+ if (ts.isAsExpression(node) || ts.isTypeAssertionExpression(node)) {
226
+ return walkExpression(ctx, node.expression);
227
+ }
228
+ if (ts.isNonNullExpression(node)) {
229
+ return `${walkExpression(ctx, node.expression)}!`;
222
230
  }
231
+ return printer.printNode(ts.EmitHint.Expression, node, ctx.sourceFile);
223
232
  }
224
233
  const generateCode = (templates, sourceFile, checker) => {
225
234
  let result = {
@@ -243,7 +252,6 @@ const generateCode = (templates, sourceFile, checker) => {
243
252
  }
244
253
  let ctx = {
245
254
  checker,
246
- printer,
247
255
  sourceFile,
248
256
  templates: result.templates
249
257
  };
@@ -251,30 +259,27 @@ const generateCode = (templates, sourceFile, checker) => {
251
259
  let template = rootTemplates[i];
252
260
  result.replacements.push({
253
261
  generate: (sf) => {
254
- let codeBefore = sf.getFullText().slice(0, template.node.getStart(sf)), exprTexts = [], isArrowBody = codeBefore.trimEnd().endsWith('=>'), localCtx = {
262
+ let exprTexts = [], localCtx = {
255
263
  checker,
256
- printer,
257
264
  sourceFile: sf,
258
265
  templates: ctx.templates
259
266
  }, parsed = parser.parse(template.literals);
260
267
  for (let j = 0, m = template.expressions.length; j < m; j++) {
261
268
  exprTexts.push(rewriteExpression(localCtx, template.expressions[j]));
262
269
  }
263
- if (isArrowBody && (!parsed.slots || parsed.slots.length === 0)) {
264
- let arrowMatch = codeBefore.match(ARROW_EMPTY_PARAMS);
265
- if (arrowMatch) {
266
- return getOrCreateTemplateId(localCtx, parsed.html);
267
- }
270
+ if (isArrowExpressionBody(template.node) &&
271
+ template.node.parent.parameters.length === 0 &&
272
+ (!parsed.slots || parsed.slots.length === 0)) {
273
+ return getOrCreateTemplateId(localCtx, parsed.html);
268
274
  }
269
- return generateTemplateCode(localCtx, parsed, exprTexts, template.expressions, isArrowBody);
275
+ return generateTemplateCode(localCtx, parsed, exprTexts, template.expressions, template.node);
270
276
  },
271
277
  node: template.node
272
278
  });
273
279
  }
274
- discoverAllTemplates(ctx, templates);
275
280
  for (let [html, id] of ctx.templates) {
276
281
  result.prepend.push(`const ${id} = ${COMPILER_NAMESPACE}.template(\`${html}\`);`);
277
282
  }
278
283
  return result;
279
284
  };
280
- export { generateCode };
285
+ export { generateCode, printer };
@@ -1,29 +1,17 @@
1
1
  import { ts } from '@esportsplus/typescript';
2
+ import { ast } from '@esportsplus/typescript/compiler';
2
3
  import { COMPILER_ENTRYPOINT, COMPILER_ENTRYPOINT_REACTIVITY, COMPILER_NAMESPACE, PACKAGE } from '../constants.js';
3
- import { generateCode } from './codegen.js';
4
+ import { generateCode, printer } from './codegen.js';
4
5
  import { findHtmlTemplates, findReactiveCalls } from './ts-parser.js';
5
- const PATTERNS = [
6
- `${COMPILER_ENTRYPOINT}\``,
7
- `${COMPILER_ENTRYPOINT}.${COMPILER_ENTRYPOINT_REACTIVITY}`
8
- ];
9
- function isInRange(ranges, start, end) {
10
- for (let i = 0, n = ranges.length; i < n; i++) {
11
- let range = ranges[i];
12
- if (start >= range.start && end <= range.end) {
13
- return true;
14
- }
15
- }
16
- return false;
17
- }
18
- let printer = ts.createPrinter({ newLine: ts.NewLineKind.LineFeed });
19
6
  const plugin = {
20
- patterns: PATTERNS,
7
+ patterns: [
8
+ `${COMPILER_ENTRYPOINT}\``,
9
+ `${COMPILER_ENTRYPOINT}.${COMPILER_ENTRYPOINT_REACTIVITY}`
10
+ ],
21
11
  transform: (ctx) => {
22
- let importsIntent = [], prepend = [], replacements = [], removeImports = [];
23
- let templates = findHtmlTemplates(ctx.sourceFile, ctx.checker);
24
- let templateRanges = [];
12
+ let importsIntent = [], prepend = [], ranges = [], replacements = [], removeImports = [], templates = findHtmlTemplates(ctx.sourceFile, ctx.checker);
25
13
  for (let i = 0, n = templates.length; i < n; i++) {
26
- templateRanges.push({
14
+ ranges.push({
27
15
  end: templates[i].end,
28
16
  start: templates[i].start
29
17
  });
@@ -31,11 +19,14 @@ const plugin = {
31
19
  let reactiveCalls = findReactiveCalls(ctx.sourceFile, ctx.checker);
32
20
  for (let i = 0, n = reactiveCalls.length; i < n; i++) {
33
21
  let call = reactiveCalls[i];
34
- if (isInRange(templateRanges, call.start, call.end)) {
22
+ if (ast.inRange(ranges, call.start, call.end)) {
35
23
  continue;
36
24
  }
37
25
  replacements.push({
38
- generate: (sourceFile) => `new ${COMPILER_NAMESPACE}.ArraySlot(${printer.printNode(ts.EmitHint.Expression, call.arrayArg, sourceFile)}, ${printer.printNode(ts.EmitHint.Expression, call.callbackArg, sourceFile)})`,
26
+ generate: (sourceFile) => `new ${COMPILER_NAMESPACE}.ArraySlot(
27
+ ${printer.printNode(ts.EmitHint.Expression, call.arrayArg, sourceFile)},
28
+ ${printer.printNode(ts.EmitHint.Expression, call.callbackArg, sourceFile)}
29
+ )`,
39
30
  node: call.node
40
31
  });
41
32
  }
@@ -57,8 +57,7 @@ const analyze = (expr, checker) => {
57
57
  return COMPILER_TYPES.Effect;
58
58
  }
59
59
  }
60
- catch {
61
- }
60
+ catch { }
62
61
  }
63
62
  return COMPILER_TYPES.Unknown;
64
63
  };
@@ -14,7 +14,11 @@ type TemplateInfo = {
14
14
  node: ts.TaggedTemplateExpression;
15
15
  start: number;
16
16
  };
17
+ declare const extractTemplateParts: (template: ts.TemplateLiteral) => {
18
+ expressions: ts.Expression[];
19
+ literals: string[];
20
+ };
17
21
  declare const findHtmlTemplates: (sourceFile: ts.SourceFile, checker?: ts.TypeChecker) => TemplateInfo[];
18
22
  declare const findReactiveCalls: (sourceFile: ts.SourceFile, checker?: ts.TypeChecker) => ReactiveCallInfo[];
19
- export { findHtmlTemplates, findReactiveCalls };
23
+ export { extractTemplateParts, findHtmlTemplates, findReactiveCalls };
20
24
  export type { ReactiveCallInfo, TemplateInfo };
@@ -1,38 +1,14 @@
1
1
  import { ts } from '@esportsplus/typescript';
2
+ import { imports } from '@esportsplus/typescript/compiler';
2
3
  import { COMPILER_ENTRYPOINT, COMPILER_ENTRYPOINT_REACTIVITY, PACKAGE } from '../constants.js';
3
- function isHtmlFromPackage(node, checker) {
4
- if (node.text !== COMPILER_ENTRYPOINT) {
5
- return false;
6
- }
7
- if (!checker) {
8
- return true;
9
- }
10
- let symbol = checker.getSymbolAtLocation(node);
11
- if (!symbol) {
12
- return true;
13
- }
14
- if (symbol.flags & ts.SymbolFlags.Alias) {
15
- symbol = checker.getAliasedSymbol(symbol);
16
- }
17
- let declarations = symbol.getDeclarations();
18
- if (!declarations || declarations.length === 0) {
19
- return true;
20
- }
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')) {
24
- return true;
25
- }
26
- }
27
- return false;
28
- }
29
4
  function visitReactiveCalls(node, calls, checker) {
30
5
  if (ts.isCallExpression(node) &&
31
6
  ts.isPropertyAccessExpression(node.expression) &&
32
7
  ts.isIdentifier(node.expression.expression) &&
33
8
  node.expression.name.text === COMPILER_ENTRYPOINT_REACTIVITY &&
34
9
  node.arguments.length === 2 &&
35
- isHtmlFromPackage(node.expression.expression, checker)) {
10
+ node.expression.expression.text === COMPILER_ENTRYPOINT &&
11
+ (!checker || imports.inPackage(checker, node.expression.expression, PACKAGE, COMPILER_ENTRYPOINT))) {
36
12
  calls.push({
37
13
  arrayArg: node.arguments[0],
38
14
  callbackArg: node.arguments[1],
@@ -47,19 +23,11 @@ function visitTemplates(node, depth, templates, checker) {
47
23
  let nextDepth = (ts.isArrowFunction(node) || ts.isFunctionDeclaration(node) || ts.isFunctionExpression(node) || ts.isMethodDeclaration(node))
48
24
  ? depth + 1
49
25
  : depth;
50
- if (ts.isTaggedTemplateExpression(node) && ts.isIdentifier(node.tag) && isHtmlFromPackage(node.tag, checker)) {
51
- let expressions = [], literals = [], template = node.template;
52
- if (ts.isNoSubstitutionTemplateLiteral(template)) {
53
- literals.push(template.text);
54
- }
55
- else if (ts.isTemplateExpression(template)) {
56
- literals.push(template.head.text);
57
- for (let i = 0, n = template.templateSpans.length; i < n; i++) {
58
- let span = template.templateSpans[i];
59
- expressions.push(span.expression);
60
- literals.push(span.literal.text);
61
- }
62
- }
26
+ if (ts.isTaggedTemplateExpression(node) &&
27
+ ts.isIdentifier(node.tag) &&
28
+ node.tag.text === COMPILER_ENTRYPOINT &&
29
+ (!checker || imports.inPackage(checker, node.tag, PACKAGE, COMPILER_ENTRYPOINT))) {
30
+ let { expressions, literals } = extractTemplateParts(node.template);
63
31
  templates.push({
64
32
  depth,
65
33
  end: node.end,
@@ -71,15 +39,29 @@ function visitTemplates(node, depth, templates, checker) {
71
39
  }
72
40
  ts.forEachChild(node, child => visitTemplates(child, nextDepth, templates, checker));
73
41
  }
42
+ const extractTemplateParts = (template) => {
43
+ let expressions = [], literals = [];
44
+ if (ts.isNoSubstitutionTemplateLiteral(template)) {
45
+ literals.push(template.text);
46
+ }
47
+ else if (ts.isTemplateExpression(template)) {
48
+ literals.push(template.head.text);
49
+ for (let i = 0, n = template.templateSpans.length; i < n; i++) {
50
+ let span = template.templateSpans[i];
51
+ expressions.push(span.expression);
52
+ literals.push(span.literal.text);
53
+ }
54
+ }
55
+ return { expressions, literals };
56
+ };
74
57
  const findHtmlTemplates = (sourceFile, checker) => {
75
58
  let templates = [];
76
59
  visitTemplates(sourceFile, 0, templates, checker);
77
- templates.sort((a, b) => a.depth !== b.depth ? b.depth - a.depth : a.start - b.start);
78
- return templates;
60
+ return templates.sort((a, b) => a.depth !== b.depth ? b.depth - a.depth : a.start - b.start);
79
61
  };
80
62
  const findReactiveCalls = (sourceFile, checker) => {
81
63
  let calls = [];
82
64
  visitReactiveCalls(sourceFile, calls, checker);
83
65
  return calls;
84
66
  };
85
- export { findHtmlTemplates, findReactiveCalls };
67
+ export { extractTemplateParts, findHtmlTemplates, findReactiveCalls };
package/package.json CHANGED
@@ -2,8 +2,8 @@
2
2
  "author": "ICJR",
3
3
  "dependencies": {
4
4
  "@esportsplus/queue": "^0.2.0",
5
- "@esportsplus/reactivity": "^0.28.1",
6
- "@esportsplus/typescript": "^0.25.0",
5
+ "@esportsplus/reactivity": "^0.29.1",
6
+ "@esportsplus/typescript": "^0.26.0",
7
7
  "@esportsplus/utilities": "^0.27.2",
8
8
  "serve": "^14.2.5"
9
9
  },
@@ -35,7 +35,7 @@
35
35
  },
36
36
  "type": "module",
37
37
  "types": "./build/index.d.ts",
38
- "version": "0.38.1",
38
+ "version": "0.39.0",
39
39
  "scripts": {
40
40
  "build": "tsc",
41
41
  "build:test": "vite build --config test/vite.config.ts",
@@ -2,8 +2,9 @@ import type { ReplacementIntent } from '@esportsplus/typescript/compiler';
2
2
  import { ts } from '@esportsplus/typescript';
3
3
  import { ast, uid } from '@esportsplus/typescript/compiler';
4
4
  import { COMPILER_ENTRYPOINT, COMPILER_ENTRYPOINT_REACTIVITY, COMPILER_NAMESPACE, COMPILER_TYPES, DIRECT_ATTACH_EVENTS, LIFECYCLE_EVENTS } from '~/constants';
5
+ import { extractTemplateParts } from './ts-parser';
5
6
  import type { TemplateInfo } from './ts-parser';
6
- import { analyze } from './analyzer';
7
+ import { analyze } from './ts-analyzer';
7
8
  import parser from './parser';
8
9
 
9
10
 
@@ -18,7 +19,6 @@ type Attribute = {
18
19
 
19
20
  type CodegenContext = {
20
21
  checker?: ts.TypeChecker;
21
- printer: ts.Printer;
22
22
  sourceFile: ts.SourceFile;
23
23
  templates: Map<string, string>;
24
24
  };
@@ -39,44 +39,10 @@ type ParseResult = {
39
39
  slots: (Attribute | Node)[] | null;
40
40
  };
41
41
 
42
- type Replacement = {
43
- end: number;
44
- newText: string;
45
- start: number;
46
- };
47
-
48
-
49
- const ARROW_EMPTY_PARAMS = /\(\s*\)\s*=>\s*$/;
50
-
51
42
 
52
43
  let printer = ts.createPrinter({ newLine: ts.NewLineKind.LineFeed });
53
44
 
54
45
 
55
- function collectNestedReplacements(
56
- ctx: CodegenContext,
57
- node: ts.Node,
58
- exprStart: number,
59
- replacements: Replacement[]
60
- ): void {
61
- if (isNestedHtmlTemplate(node as ts.Expression)) {
62
- replacements.push({
63
- end: node.end - exprStart,
64
- newText: generateNestedTemplateCode(ctx, node as ts.TaggedTemplateExpression),
65
- start: node.getStart(ctx.sourceFile) - exprStart
66
- });
67
- }
68
- else if (isReactiveCall(node as ts.Expression)) {
69
- replacements.push({
70
- end: node.end - exprStart,
71
- newText: rewriteReactiveCall(ctx, node as ts.CallExpression),
72
- start: node.getStart(ctx.sourceFile) - exprStart
73
- });
74
- }
75
- else {
76
- ts.forEachChild(node, child => collectNestedReplacements(ctx, child, exprStart, replacements));
77
- }
78
- }
79
-
80
46
  function generateAttributeBinding(element: string, name: string, expr: string, staticValue: string): string {
81
47
  if (name.startsWith('on') && name.length > 2) {
82
48
  let event = name.slice(2).toLowerCase(),
@@ -105,24 +71,11 @@ function generateAttributeBinding(element: string, name: string, expr: string, s
105
71
  }
106
72
 
107
73
  function generateNestedTemplateCode(ctx: CodegenContext, node: ts.TaggedTemplateExpression): string {
108
- let expressions: ts.Expression[] = [],
109
- exprTexts: string[] = [],
110
- literals: string[] = [],
111
- template = node.template;
74
+ let { expressions, literals } = extractTemplateParts(node.template),
75
+ exprTexts: string[] = [];
112
76
 
113
- if (ts.isNoSubstitutionTemplateLiteral(template)) {
114
- literals.push(template.text);
115
- }
116
- else if (ts.isTemplateExpression(template)) {
117
- literals.push(template.head.text);
118
-
119
- for (let i = 0, n = template.templateSpans.length; i < n; i++) {
120
- let expr = template.templateSpans[i].expression;
121
-
122
- expressions.push(expr);
123
- literals.push(template.templateSpans[i].literal.text);
124
- exprTexts.push(rewriteExpression(ctx, expr));
125
- }
77
+ for (let i = 0, n = expressions.length; i < n; i++) {
78
+ exprTexts.push(rewriteExpression(ctx, expressions[i]));
126
79
  }
127
80
 
128
81
  return generateTemplateCode(
@@ -130,7 +83,7 @@ function generateNestedTemplateCode(ctx: CodegenContext, node: ts.TaggedTemplate
130
83
  parser.parse(literals) as ParseResult,
131
84
  exprTexts,
132
85
  expressions,
133
- false
86
+ node
134
87
  );
135
88
  }
136
89
 
@@ -143,9 +96,7 @@ function generateNodeBinding(ctx: CodegenContext, anchor: string, exprText: stri
143
96
  return `${anchor}.parentNode!.insertBefore(${generateNestedTemplateCode(ctx, exprNode)}, ${anchor});`;
144
97
  }
145
98
 
146
- let slotType = analyze(exprNode, ctx.checker);
147
-
148
- switch (slotType) {
99
+ switch (analyze(exprNode, ctx.checker)) {
149
100
  case COMPILER_TYPES.ArraySlot:
150
101
  return `${anchor}.parentNode!.insertBefore(new ${COMPILER_NAMESPACE}.ArraySlot(${exprText}).fragment, ${anchor});`;
151
102
 
@@ -168,7 +119,7 @@ function generateTemplateCode(
168
119
  { html, slots }: ParseResult,
169
120
  exprTexts: string[],
170
121
  exprNodes: ts.Expression[],
171
- isArrowBody: boolean
122
+ templateNode: ts.Node
172
123
  ): string {
173
124
  if (!slots || slots.length === 0) {
174
125
  return `${getOrCreateTemplateId(ctx, html)}()`;
@@ -177,6 +128,7 @@ function generateTemplateCode(
177
128
  let code: string[] = [],
178
129
  declarations: string[] = [],
179
130
  index = 0,
131
+ isArrowBody = isArrowExpressionBody(templateNode),
180
132
  nodes = new Map<string, string>(),
181
133
  root = uid('root');
182
134
 
@@ -285,6 +237,10 @@ function isNestedHtmlTemplate(expr: ts.Expression): expr is ts.TaggedTemplateExp
285
237
  return ts.isTaggedTemplateExpression(expr) && ts.isIdentifier(expr.tag) && expr.tag.text === COMPILER_ENTRYPOINT;
286
238
  }
287
239
 
240
+ function isArrowExpressionBody(node: ts.Node): boolean {
241
+ return ts.isArrowFunction(node.parent) && (node.parent as ts.ArrowFunction).body === node;
242
+ }
243
+
288
244
  function isReactiveCall(expr: ts.Expression): expr is ts.CallExpression {
289
245
  return (
290
246
  ts.isCallExpression(expr) &&
@@ -295,106 +251,152 @@ function isReactiveCall(expr: ts.Expression): expr is ts.CallExpression {
295
251
  );
296
252
  }
297
253
 
298
- function replaceReverse(text: string, replacements: Replacement[]): string {
299
- let sorted = replacements.slice().sort((a, b) => b.start - a.start);
300
-
301
- for (let i = 0, n = sorted.length; i < n; i++) {
302
- let r = sorted[i];
303
-
304
- text = text.slice(0, r.start) + r.newText + text.slice(r.end);
305
- }
306
-
307
- return text;
308
- }
309
-
310
254
  function rewriteExpression(ctx: CodegenContext, expr: ts.Expression): string {
311
255
  if (isNestedHtmlTemplate(expr)) {
312
256
  return generateNestedTemplateCode(ctx, expr);
313
257
  }
314
258
 
259
+ // Returns just args "array, callback" - for direct slot usage where generateNodeBinding wraps it
315
260
  if (isReactiveCall(expr)) {
316
- return rewriteReactiveCall(ctx, expr);
261
+ return `
262
+ ${printer.printNode(ts.EmitHint.Expression, expr.arguments[0], ctx.sourceFile)},
263
+ ${rewriteExpression(ctx, expr.arguments[1] as ts.Expression)}
264
+ `;
317
265
  }
318
266
 
319
267
  if (!ast.hasMatch(expr, n => isNestedHtmlTemplate(n as ts.Expression) || isReactiveCall(n as ts.Expression))) {
320
- return ctx.printer.printNode(ts.EmitHint.Expression, expr, ctx.sourceFile);
268
+ return printer.printNode(ts.EmitHint.Expression, expr, ctx.sourceFile);
321
269
  }
322
270
 
323
- let replacements: Replacement[] = [];
271
+ return walkExpression(ctx, expr);
272
+ }
324
273
 
325
- collectNestedReplacements(ctx, expr, expr.getStart(ctx.sourceFile), replacements);
274
+ function walkExpression(ctx: CodegenContext, node: ts.Node): string {
275
+ // Intercept special nodes
276
+ if (isNestedHtmlTemplate(node as ts.Expression)) {
277
+ return generateNestedTemplateCode(ctx, node as ts.TaggedTemplateExpression);
278
+ }
326
279
 
327
- return replaceReverse(expr.getText(ctx.sourceFile), replacements);
328
- }
280
+ if (isReactiveCall(node as ts.Expression)) {
281
+ let call = node as ts.CallExpression;
329
282
 
330
- function rewriteReactiveCall(ctx: CodegenContext, node: ts.CallExpression): string {
331
- let arrayArg = node.arguments[0],
332
- arrayText = ctx.printer.printNode(ts.EmitHint.Expression, arrayArg, ctx.sourceFile),
333
- callbackArg = node.arguments[1],
334
- callbackText = rewriteExpression(ctx, callbackArg as ts.Expression);
283
+ return `new ${COMPILER_NAMESPACE}.ArraySlot(${printer.printNode(ts.EmitHint.Expression, call.arguments[0], ctx.sourceFile)}, ${rewriteExpression(ctx, call.arguments[1] as ts.Expression)})`;
284
+ }
335
285
 
336
- return `${arrayText}, ${callbackText}`;
337
- }
286
+ // No nested constructs - use printer
287
+ if (!ast.hasMatch(node, n => isNestedHtmlTemplate(n as ts.Expression) || isReactiveCall(n as ts.Expression))) {
288
+ return printer.printNode(ts.EmitHint.Expression, node as ts.Expression, ctx.sourceFile);
289
+ }
338
290
 
339
- // Eager discovery - walk all expressions to find templates before prepend generation
340
- function discoverTemplatesInExpression(ctx: CodegenContext, node: ts.Node): void {
341
- if (isNestedHtmlTemplate(node as ts.Expression)) {
342
- let template = node as ts.TaggedTemplateExpression,
343
- expressions: ts.Expression[] = [],
344
- literals: string[] = [],
345
- tpl = template.template;
291
+ // Recursive handling by node type
292
+ if (ts.isConditionalExpression(node)) {
293
+ return `${walkExpression(ctx, node.condition)} ? ${walkExpression(ctx, node.whenTrue)} : ${walkExpression(ctx, node.whenFalse)}`;
294
+ }
346
295
 
347
- if (ts.isNoSubstitutionTemplateLiteral(tpl)) {
348
- literals.push(tpl.text);
349
- }
350
- else if (ts.isTemplateExpression(tpl)) {
351
- literals.push(tpl.head.text);
296
+ if (ts.isBinaryExpression(node)) {
297
+ return `${walkExpression(ctx, node.left)} ${node.operatorToken.getText(ctx.sourceFile)} ${walkExpression(ctx, node.right)}`;
298
+ }
352
299
 
353
- for (let i = 0, n = tpl.templateSpans.length; i < n; i++) {
354
- expressions.push(tpl.templateSpans[i].expression);
355
- literals.push(tpl.templateSpans[i].literal.text);
356
- }
300
+ if (ts.isCallExpression(node)) {
301
+ let args: string[] = [];
302
+
303
+ for (let i = 0, n = node.arguments.length; i < n; i++) {
304
+ args.push(walkExpression(ctx, node.arguments[i]));
357
305
  }
358
306
 
359
- let parsed = parser.parse(literals) as ParseResult;
307
+ return `${walkExpression(ctx, node.expression)}(${args.join(', ')})`;
308
+ }
309
+
310
+ if (ts.isPropertyAccessExpression(node)) {
311
+ return `${walkExpression(ctx, node.expression)}.${node.name.text}`;
312
+ }
313
+
314
+ if (ts.isElementAccessExpression(node)) {
315
+ return `${walkExpression(ctx, node.expression)}[${walkExpression(ctx, node.argumentExpression)}]`;
316
+ }
317
+
318
+ if (ts.isParenthesizedExpression(node)) {
319
+ return `(${walkExpression(ctx, node.expression)})`;
320
+ }
321
+
322
+ if (ts.isArrowFunction(node)) {
323
+ return `(${node.parameters.map(p => p.getText(ctx.sourceFile)).join(', ')}) => ${
324
+ ts.isBlock(node.body) ? node.body.getText(ctx.sourceFile) : walkExpression(ctx, node.body)
325
+ }`;
326
+ }
360
327
 
361
- getOrCreateTemplateId(ctx, parsed.html);
328
+ if (ts.isArrayLiteralExpression(node)) {
329
+ let elements: string[] = [];
362
330
 
363
- for (let i = 0, n = expressions.length; i < n; i++) {
364
- discoverTemplatesInExpression(ctx, expressions[i]);
331
+ for (let i = 0, n = node.elements.length; i < n; i++) {
332
+ elements.push( walkExpression(ctx, node.elements[i]) );
365
333
  }
334
+
335
+ return `[${elements.join(', ')}]`;
366
336
  }
367
- else if (isReactiveCall(node as ts.Expression)) {
368
- let call = node as ts.CallExpression;
369
337
 
370
- if (call.arguments.length >= 2) {
371
- discoverTemplatesInExpression(ctx, call.arguments[1]);
338
+ if (ts.isObjectLiteralExpression(node)) {
339
+ let properties: string[] = [];
340
+
341
+ for (let i = 0, n = node.properties.length; i < n; i++) {
342
+ let prop = node.properties[i];
343
+
344
+ if (ts.isPropertyAssignment(prop)) {
345
+ properties.push(`${prop.name.getText(ctx.sourceFile)}: ${walkExpression(ctx, prop.initializer)}`);
346
+ }
347
+ else if (ts.isShorthandPropertyAssignment(prop)) {
348
+ properties.push(prop.name.text);
349
+ }
350
+ else if (ts.isSpreadAssignment(prop)) {
351
+ properties.push(`...${walkExpression(ctx, prop.expression)}`);
352
+ }
353
+ else {
354
+ properties.push(prop.getText(ctx.sourceFile));
355
+ }
372
356
  }
357
+
358
+ return `{ ${properties.join(', ')} }`;
373
359
  }
374
- else {
375
- ts.forEachChild(node, child => discoverTemplatesInExpression(ctx, child));
360
+
361
+ if (ts.isPrefixUnaryExpression(node)) {
362
+ return `${ts.tokenToString(node.operator)}${walkExpression(ctx, node.operand)}`;
376
363
  }
377
- }
378
364
 
379
- function discoverAllTemplates(ctx: CodegenContext, templates: TemplateInfo[]): void {
380
- for (let i = 0, n = templates.length; i < n; i++) {
381
- let parsed = parser.parse(templates[i].literals) as ParseResult;
365
+ if (ts.isPostfixUnaryExpression(node)) {
366
+ return `${walkExpression(ctx, node.operand)}${ts.tokenToString(node.operator)}`;
367
+ }
382
368
 
383
- getOrCreateTemplateId(ctx, parsed.html);
369
+ if (ts.isTemplateExpression(node)) {
370
+ let parts = [node.head.text];
384
371
 
385
- for (let j = 0, m = templates[i].expressions.length; j < m; j++) {
386
- discoverTemplatesInExpression(ctx, templates[i].expressions[j]);
372
+ for (let i = 0, n = node.templateSpans.length; i < n; i++) {
373
+ let span = node.templateSpans[i];
374
+
375
+ parts.push('${' + walkExpression(ctx, span.expression) + '}' + span.literal.text);
387
376
  }
377
+
378
+ return '`' + parts.join('') + '`';
388
379
  }
380
+
381
+ if (ts.isAsExpression(node) || ts.isTypeAssertionExpression(node)) {
382
+ return walkExpression(ctx, node.expression);
383
+ }
384
+
385
+ if (ts.isNonNullExpression(node)) {
386
+ return `${walkExpression(ctx, node.expression)}!`;
387
+ }
388
+
389
+ // Fallback - use printer
390
+ return printer.printNode(ts.EmitHint.Expression, node as ts.Expression, ctx.sourceFile);
389
391
  }
390
392
 
391
393
 
392
394
  const generateCode = (templates: TemplateInfo[], sourceFile: ts.SourceFile, checker?: ts.TypeChecker): CodegenResult => {
393
395
  let result: CodegenResult = {
394
- prepend: [],
395
- replacements: [],
396
- templates: new Map()
397
- };
396
+ prepend: [],
397
+ replacements: [],
398
+ templates: new Map()
399
+ };
398
400
 
399
401
  if (templates.length === 0) {
400
402
  return result;
@@ -417,23 +419,19 @@ const generateCode = (templates: TemplateInfo[], sourceFile: ts.SourceFile, chec
417
419
  }
418
420
 
419
421
  let ctx: CodegenContext = {
420
- checker,
421
- printer,
422
- sourceFile,
423
- templates: result.templates
424
- };
422
+ checker,
423
+ sourceFile,
424
+ templates: result.templates
425
+ };
425
426
 
426
427
  for (let i = 0, n = rootTemplates.length; i < n; i++) {
427
428
  let template = rootTemplates[i];
428
429
 
429
430
  result.replacements.push({
430
431
  generate: (sf) => {
431
- let codeBefore = sf.getFullText().slice(0, template.node.getStart(sf)),
432
- exprTexts: string[] = [],
433
- isArrowBody = codeBefore.trimEnd().endsWith('=>'),
432
+ let exprTexts: string[] = [],
434
433
  localCtx: CodegenContext = {
435
434
  checker,
436
- printer,
437
435
  sourceFile: sf,
438
436
  templates: ctx.templates
439
437
  },
@@ -443,12 +441,13 @@ const generateCode = (templates: TemplateInfo[], sourceFile: ts.SourceFile, chec
443
441
  exprTexts.push(rewriteExpression(localCtx, template.expressions[j]));
444
442
  }
445
443
 
446
- if (isArrowBody && (!parsed.slots || parsed.slots.length === 0)) {
447
- let arrowMatch = codeBefore.match(ARROW_EMPTY_PARAMS);
448
-
449
- if (arrowMatch) {
450
- return getOrCreateTemplateId(localCtx, parsed.html);
451
- }
444
+ // Zero-param arrow with no slots just return template ID
445
+ if (
446
+ isArrowExpressionBody(template.node) &&
447
+ (template.node.parent as ts.ArrowFunction).parameters.length === 0 &&
448
+ (!parsed.slots || parsed.slots.length === 0)
449
+ ) {
450
+ return getOrCreateTemplateId(localCtx, parsed.html);
452
451
  }
453
452
 
454
453
  return generateTemplateCode(
@@ -456,16 +455,13 @@ const generateCode = (templates: TemplateInfo[], sourceFile: ts.SourceFile, chec
456
455
  parsed,
457
456
  exprTexts,
458
457
  template.expressions,
459
- isArrowBody
458
+ template.node
460
459
  );
461
460
  },
462
461
  node: template.node
463
462
  });
464
463
  }
465
464
 
466
- // Eager discovery: find all templates before prepend generation
467
- discoverAllTemplates(ctx, templates);
468
-
469
465
  for (let [html, id] of ctx.templates) {
470
466
  result.prepend.push(`const ${id} = ${COMPILER_NAMESPACE}.template(\`${html}\`);`);
471
467
  }
@@ -474,5 +470,5 @@ const generateCode = (templates: TemplateInfo[], sourceFile: ts.SourceFile, chec
474
470
  };
475
471
 
476
472
 
477
- export { generateCode };
473
+ export { generateCode, printer };
478
474
  export type { CodegenResult };
@@ -1,49 +1,29 @@
1
- import type { ImportIntent, Plugin, ReplacementIntent, TransformContext } from '@esportsplus/typescript/compiler';
2
1
  import { ts } from '@esportsplus/typescript';
2
+ import { ast } from '@esportsplus/typescript/compiler';
3
+ import type { ImportIntent, Plugin, ReplacementIntent, TransformContext } from '@esportsplus/typescript/compiler';
3
4
  import { COMPILER_ENTRYPOINT, COMPILER_ENTRYPOINT_REACTIVITY, COMPILER_NAMESPACE, PACKAGE } from '~/constants';
4
- import { generateCode } from './codegen';
5
+ import { generateCode, printer } from './codegen';
5
6
  import { findHtmlTemplates, findReactiveCalls } from './ts-parser';
6
7
 
7
8
 
8
- const PATTERNS = [
9
- `${COMPILER_ENTRYPOINT}\``,
10
- `${COMPILER_ENTRYPOINT}.${COMPILER_ENTRYPOINT_REACTIVITY}`
11
- ];
12
-
13
-
14
- function isInRange(ranges: { end: number; start: number }[], start: number, end: number): boolean {
15
- for (let i = 0, n = ranges.length; i < n; i++) {
16
- let range = ranges[i];
17
-
18
- if (start >= range.start && end <= range.end) {
19
- return true;
20
- }
21
- }
22
-
23
- return false;
24
- }
25
-
26
-
27
- let printer = ts.createPrinter({ newLine: ts.NewLineKind.LineFeed });
28
-
29
-
30
9
  const plugin: Plugin = {
31
- patterns: PATTERNS,
10
+ patterns: [
11
+ `${COMPILER_ENTRYPOINT}\``,
12
+ `${COMPILER_ENTRYPOINT}.${COMPILER_ENTRYPOINT_REACTIVITY}`
13
+ ],
32
14
 
33
15
  transform: (ctx: TransformContext) => {
34
16
  let importsIntent: ImportIntent[] = [],
35
17
  prepend: string[] = [],
18
+ // Build ranges for all template nodes - reactive calls inside these are handled by template codegen
19
+ ranges: { end: number; start: number }[] = [],
36
20
  replacements: ReplacementIntent[] = [],
37
- removeImports: string[] = [];
38
-
39
- // Find templates first to build exclusion ranges
40
- let templates = findHtmlTemplates(ctx.sourceFile, ctx.checker);
41
-
42
- // Build ranges for all template nodes - reactive calls inside these are handled by template codegen
43
- let templateRanges: { end: number; start: number }[] = [];
21
+ removeImports: string[] = [],
22
+ // Find templates first to build exclusion ranges
23
+ templates = findHtmlTemplates(ctx.sourceFile, ctx.checker);
44
24
 
45
25
  for (let i = 0, n = templates.length; i < n; i++) {
46
- templateRanges.push({
26
+ ranges.push({
47
27
  end: templates[i].end,
48
28
  start: templates[i].start
49
29
  });
@@ -56,12 +36,15 @@ const plugin: Plugin = {
56
36
  let call = reactiveCalls[i];
57
37
 
58
38
  // Skip reactive calls that are inside template expressions - handled by template codegen
59
- if (isInRange(templateRanges, call.start, call.end)) {
39
+ if (ast.inRange(ranges, call.start, call.end)) {
60
40
  continue;
61
41
  }
62
42
 
63
43
  replacements.push({
64
- generate: (sourceFile) => `new ${COMPILER_NAMESPACE}.ArraySlot(${printer.printNode(ts.EmitHint.Expression, call.arrayArg, sourceFile)}, ${printer.printNode(ts.EmitHint.Expression, call.callbackArg, sourceFile)})`,
44
+ generate: (sourceFile) => `new ${COMPILER_NAMESPACE}.ArraySlot(
45
+ ${printer.printNode(ts.EmitHint.Expression, call.arrayArg, sourceFile)},
46
+ ${printer.printNode(ts.EmitHint.Expression, call.callbackArg, sourceFile)}
47
+ )`,
65
48
  node: call.node
66
49
  });
67
50
  }
@@ -82,8 +82,7 @@ const analyze = (expr: ts.Expression, checker?: ts.TypeChecker): COMPILER_TYPES
82
82
  return COMPILER_TYPES.Effect;
83
83
  }
84
84
  }
85
- catch {
86
- }
85
+ catch {}
87
86
  }
88
87
 
89
88
  return COMPILER_TYPES.Unknown;
@@ -1,4 +1,5 @@
1
1
  import { ts } from '@esportsplus/typescript';
2
+ import { imports } from '@esportsplus/typescript/compiler';
2
3
  import { COMPILER_ENTRYPOINT, COMPILER_ENTRYPOINT_REACTIVITY, PACKAGE } from '~/constants';
3
4
 
4
5
 
@@ -20,47 +21,6 @@ type TemplateInfo = {
20
21
  };
21
22
 
22
23
 
23
- function isHtmlFromPackage(node: ts.Identifier, checker: ts.TypeChecker | undefined): boolean {
24
- // Fast path: check identifier name first
25
- if (node.text !== COMPILER_ENTRYPOINT) {
26
- return false;
27
- }
28
-
29
- // Without checker, fall back to string-based check
30
- if (!checker) {
31
- return true;
32
- }
33
-
34
- let symbol = checker.getSymbolAtLocation(node);
35
-
36
- if (!symbol) {
37
- return true;
38
- }
39
-
40
- // Follow aliases (re-exports) to find original symbol
41
- if (symbol.flags & ts.SymbolFlags.Alias) {
42
- symbol = checker.getAliasedSymbol(symbol);
43
- }
44
-
45
- let declarations = symbol.getDeclarations();
46
-
47
- if (!declarations || declarations.length === 0) {
48
- return true;
49
- }
50
-
51
- // Check if any declaration is from our package
52
- for (let i = 0, n = declarations.length; i < n; i++) {
53
- let filename = declarations[i].getSourceFile().fileName;
54
-
55
- // Check for package in node_modules path or direct package reference
56
- if (filename.includes(PACKAGE) || filename.includes('@esportsplus/template')) {
57
- return true;
58
- }
59
- }
60
-
61
- return false;
62
- }
63
-
64
24
  function visitReactiveCalls(node: ts.Node, calls: ReactiveCallInfo[], checker: ts.TypeChecker | undefined): void {
65
25
  if (
66
26
  ts.isCallExpression(node) &&
@@ -68,7 +28,8 @@ function visitReactiveCalls(node: ts.Node, calls: ReactiveCallInfo[], checker: t
68
28
  ts.isIdentifier(node.expression.expression) &&
69
29
  node.expression.name.text === COMPILER_ENTRYPOINT_REACTIVITY &&
70
30
  node.arguments.length === 2 &&
71
- isHtmlFromPackage(node.expression.expression, checker)
31
+ node.expression.expression.text === COMPILER_ENTRYPOINT &&
32
+ (!checker || imports.inPackage(checker, node.expression.expression, PACKAGE, COMPILER_ENTRYPOINT))
72
33
  ) {
73
34
  calls.push({
74
35
  arrayArg: node.arguments[0],
@@ -87,24 +48,13 @@ function visitTemplates(node: ts.Node, depth: number, templates: TemplateInfo[],
87
48
  ? depth + 1
88
49
  : depth;
89
50
 
90
- if (ts.isTaggedTemplateExpression(node) && ts.isIdentifier(node.tag) && isHtmlFromPackage(node.tag, checker)) {
91
- let expressions: ts.Expression[] = [],
92
- literals: string[] = [],
93
- template = node.template;
94
-
95
- if (ts.isNoSubstitutionTemplateLiteral(template)) {
96
- literals.push(template.text);
97
- }
98
- else if (ts.isTemplateExpression(template)) {
99
- literals.push(template.head.text);
100
-
101
- for (let i = 0, n = template.templateSpans.length; i < n; i++) {
102
- let span = template.templateSpans[i];
103
-
104
- expressions.push(span.expression);
105
- literals.push(span.literal.text);
106
- }
107
- }
51
+ if (
52
+ ts.isTaggedTemplateExpression(node) &&
53
+ ts.isIdentifier(node.tag) &&
54
+ node.tag.text === COMPILER_ENTRYPOINT &&
55
+ (!checker || imports.inPackage(checker, node.tag, PACKAGE, COMPILER_ENTRYPOINT))
56
+ ) {
57
+ let { expressions, literals } = extractTemplateParts(node.template);
108
58
 
109
59
  templates.push({
110
60
  depth,
@@ -120,15 +70,34 @@ function visitTemplates(node: ts.Node, depth: number, templates: TemplateInfo[],
120
70
  }
121
71
 
122
72
 
73
+ const extractTemplateParts = (template: ts.TemplateLiteral): { expressions: ts.Expression[]; literals: string[] } => {
74
+ let expressions: ts.Expression[] = [],
75
+ literals: string[] = [];
76
+
77
+ if (ts.isNoSubstitutionTemplateLiteral(template)) {
78
+ literals.push(template.text);
79
+ }
80
+ else if (ts.isTemplateExpression(template)) {
81
+ literals.push(template.head.text);
82
+
83
+ for (let i = 0, n = template.templateSpans.length; i < n; i++) {
84
+ let span = template.templateSpans[i];
85
+
86
+ expressions.push(span.expression);
87
+ literals.push(span.literal.text);
88
+ }
89
+ }
90
+
91
+ return { expressions, literals };
92
+ };
93
+
123
94
  const findHtmlTemplates = (sourceFile: ts.SourceFile, checker?: ts.TypeChecker): TemplateInfo[] => {
124
95
  let templates: TemplateInfo[] = [];
125
96
 
126
97
  visitTemplates(sourceFile, 0, templates, checker);
127
98
 
128
99
  // Sort by depth descending (deepest first), then by position for stable ordering
129
- templates.sort((a, b) => a.depth !== b.depth ? b.depth - a.depth : a.start - b.start);
130
-
131
- return templates;
100
+ return templates.sort((a, b) => a.depth !== b.depth ? b.depth - a.depth : a.start - b.start);
132
101
  };
133
102
 
134
103
  const findReactiveCalls = (sourceFile: ts.SourceFile, checker?: ts.TypeChecker): ReactiveCallInfo[] => {
@@ -140,5 +109,5 @@ const findReactiveCalls = (sourceFile: ts.SourceFile, checker?: ts.TypeChecker):
140
109
  };
141
110
 
142
111
 
143
- export { findHtmlTemplates, findReactiveCalls };
112
+ export { extractTemplateParts, findHtmlTemplates, findReactiveCalls };
144
113
  export type { ReactiveCallInfo, TemplateInfo };