@esportsplus/template 0.38.2 → 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: rewriteNestedReactiveCall(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,73 +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 rewriteNestedReactiveCall(ctx, node) {
187
- let arrayArg = node.arguments[0], arrayText = ctx.printer.printNode(ts.EmitHint.Expression, arrayArg, ctx.sourceFile), callbackArg = node.arguments[1], callbackText = rewriteExpression(ctx, callbackArg);
188
- return `new ${COMPILER_NAMESPACE}.ArraySlot(${arrayText}, ${callbackText})`;
189
- }
190
- function discoverTemplatesInExpression(ctx, node) {
149
+ function walkExpression(ctx, node) {
191
150
  if (isNestedHtmlTemplate(node)) {
192
- let template = node, expressions = [], literals = [], tpl = template.template;
193
- if (ts.isNoSubstitutionTemplateLiteral(tpl)) {
194
- literals.push(tpl.text);
195
- }
196
- else if (ts.isTemplateExpression(tpl)) {
197
- literals.push(tpl.head.text);
198
- for (let i = 0, n = tpl.templateSpans.length; i < n; i++) {
199
- expressions.push(tpl.templateSpans[i].expression);
200
- literals.push(tpl.templateSpans[i].literal.text);
201
- }
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]));
202
170
  }
203
- let parsed = parser.parse(literals);
204
- getOrCreateTemplateId(ctx, parsed.html);
205
- for (let i = 0, n = expressions.length; i < n; i++) {
206
- 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]));
207
189
  }
190
+ return `[${elements.join(', ')}]`;
208
191
  }
209
- else if (isReactiveCall(node)) {
210
- let call = node;
211
- if (call.arguments.length >= 2) {
212
- 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
+ }
213
208
  }
209
+ return `{ ${properties.join(', ')} }`;
214
210
  }
215
- else {
216
- ts.forEachChild(node, child => discoverTemplatesInExpression(ctx, child));
211
+ if (ts.isPrefixUnaryExpression(node)) {
212
+ return `${ts.tokenToString(node.operator)}${walkExpression(ctx, node.operand)}`;
217
213
  }
218
- }
219
- function discoverAllTemplates(ctx, templates) {
220
- for (let i = 0, n = templates.length; i < n; i++) {
221
- let parsed = parser.parse(templates[i].literals);
222
- getOrCreateTemplateId(ctx, parsed.html);
223
- for (let j = 0, m = templates[i].expressions.length; j < m; j++) {
224
- 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);
225
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)}!`;
226
230
  }
231
+ return printer.printNode(ts.EmitHint.Expression, node, ctx.sourceFile);
227
232
  }
228
233
  const generateCode = (templates, sourceFile, checker) => {
229
234
  let result = {
@@ -247,7 +252,6 @@ const generateCode = (templates, sourceFile, checker) => {
247
252
  }
248
253
  let ctx = {
249
254
  checker,
250
- printer,
251
255
  sourceFile,
252
256
  templates: result.templates
253
257
  };
@@ -255,30 +259,27 @@ const generateCode = (templates, sourceFile, checker) => {
255
259
  let template = rootTemplates[i];
256
260
  result.replacements.push({
257
261
  generate: (sf) => {
258
- let codeBefore = sf.getFullText().slice(0, template.node.getStart(sf)), exprTexts = [], isArrowBody = codeBefore.trimEnd().endsWith('=>'), localCtx = {
262
+ let exprTexts = [], localCtx = {
259
263
  checker,
260
- printer,
261
264
  sourceFile: sf,
262
265
  templates: ctx.templates
263
266
  }, parsed = parser.parse(template.literals);
264
267
  for (let j = 0, m = template.expressions.length; j < m; j++) {
265
268
  exprTexts.push(rewriteExpression(localCtx, template.expressions[j]));
266
269
  }
267
- if (isArrowBody && (!parsed.slots || parsed.slots.length === 0)) {
268
- let arrowMatch = codeBefore.match(ARROW_EMPTY_PARAMS);
269
- if (arrowMatch) {
270
- return getOrCreateTemplateId(localCtx, parsed.html);
271
- }
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);
272
274
  }
273
- return generateTemplateCode(localCtx, parsed, exprTexts, template.expressions, isArrowBody);
275
+ return generateTemplateCode(localCtx, parsed, exprTexts, template.expressions, template.node);
274
276
  },
275
277
  node: template.node
276
278
  });
277
279
  }
278
- discoverAllTemplates(ctx, templates);
279
280
  for (let [html, id] of ctx.templates) {
280
281
  result.prepend.push(`const ${id} = ${COMPILER_NAMESPACE}.template(\`${html}\`);`);
281
282
  }
282
283
  return result;
283
284
  };
284
- export { generateCode };
285
+ export { generateCode, printer };
@@ -1,28 +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
- function isInRange(ranges, start, end) {
6
- for (let i = 0, n = ranges.length; i < n; i++) {
7
- let range = ranges[i];
8
- if (start >= range.start && end <= range.end) {
9
- return true;
10
- }
11
- }
12
- return false;
13
- }
14
- let printer = ts.createPrinter({ newLine: ts.NewLineKind.LineFeed });
15
6
  const plugin = {
16
7
  patterns: [
17
8
  `${COMPILER_ENTRYPOINT}\``,
18
9
  `${COMPILER_ENTRYPOINT}.${COMPILER_ENTRYPOINT_REACTIVITY}`
19
10
  ],
20
11
  transform: (ctx) => {
21
- let importsIntent = [], prepend = [], replacements = [], removeImports = [];
22
- let templates = findHtmlTemplates(ctx.sourceFile, ctx.checker);
23
- let templateRanges = [];
12
+ let importsIntent = [], prepend = [], ranges = [], replacements = [], removeImports = [], templates = findHtmlTemplates(ctx.sourceFile, ctx.checker);
24
13
  for (let i = 0, n = templates.length; i < n; i++) {
25
- templateRanges.push({
14
+ ranges.push({
26
15
  end: templates[i].end,
27
16
  start: templates[i].start
28
17
  });
@@ -30,11 +19,14 @@ const plugin = {
30
19
  let reactiveCalls = findReactiveCalls(ctx.sourceFile, ctx.checker);
31
20
  for (let i = 0, n = reactiveCalls.length; i < n; i++) {
32
21
  let call = reactiveCalls[i];
33
- if (isInRange(templateRanges, call.start, call.end)) {
22
+ if (ast.inRange(ranges, call.start, call.end)) {
34
23
  continue;
35
24
  }
36
25
  replacements.push({
37
- 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
+ )`,
38
30
  node: call.node
39
31
  });
40
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.2",
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,45 +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
- // Nested reactive calls need full ArraySlot construction (not just args)
70
- replacements.push({
71
- end: node.end - exprStart,
72
- newText: rewriteNestedReactiveCall(ctx, node as ts.CallExpression),
73
- start: node.getStart(ctx.sourceFile) - exprStart
74
- });
75
- }
76
- else {
77
- ts.forEachChild(node, child => collectNestedReplacements(ctx, child, exprStart, replacements));
78
- }
79
- }
80
-
81
46
  function generateAttributeBinding(element: string, name: string, expr: string, staticValue: string): string {
82
47
  if (name.startsWith('on') && name.length > 2) {
83
48
  let event = name.slice(2).toLowerCase(),
@@ -106,24 +71,11 @@ function generateAttributeBinding(element: string, name: string, expr: string, s
106
71
  }
107
72
 
108
73
  function generateNestedTemplateCode(ctx: CodegenContext, node: ts.TaggedTemplateExpression): string {
109
- let expressions: ts.Expression[] = [],
110
- exprTexts: string[] = [],
111
- literals: string[] = [],
112
- template = node.template;
74
+ let { expressions, literals } = extractTemplateParts(node.template),
75
+ exprTexts: string[] = [];
113
76
 
114
- if (ts.isNoSubstitutionTemplateLiteral(template)) {
115
- literals.push(template.text);
116
- }
117
- else if (ts.isTemplateExpression(template)) {
118
- literals.push(template.head.text);
119
-
120
- for (let i = 0, n = template.templateSpans.length; i < n; i++) {
121
- let expr = template.templateSpans[i].expression;
122
-
123
- expressions.push(expr);
124
- literals.push(template.templateSpans[i].literal.text);
125
- exprTexts.push(rewriteExpression(ctx, expr));
126
- }
77
+ for (let i = 0, n = expressions.length; i < n; i++) {
78
+ exprTexts.push(rewriteExpression(ctx, expressions[i]));
127
79
  }
128
80
 
129
81
  return generateTemplateCode(
@@ -131,7 +83,7 @@ function generateNestedTemplateCode(ctx: CodegenContext, node: ts.TaggedTemplate
131
83
  parser.parse(literals) as ParseResult,
132
84
  exprTexts,
133
85
  expressions,
134
- false
86
+ node
135
87
  );
136
88
  }
137
89
 
@@ -144,9 +96,7 @@ function generateNodeBinding(ctx: CodegenContext, anchor: string, exprText: stri
144
96
  return `${anchor}.parentNode!.insertBefore(${generateNestedTemplateCode(ctx, exprNode)}, ${anchor});`;
145
97
  }
146
98
 
147
- let slotType = analyze(exprNode, ctx.checker);
148
-
149
- switch (slotType) {
99
+ switch (analyze(exprNode, ctx.checker)) {
150
100
  case COMPILER_TYPES.ArraySlot:
151
101
  return `${anchor}.parentNode!.insertBefore(new ${COMPILER_NAMESPACE}.ArraySlot(${exprText}).fragment, ${anchor});`;
152
102
 
@@ -169,7 +119,7 @@ function generateTemplateCode(
169
119
  { html, slots }: ParseResult,
170
120
  exprTexts: string[],
171
121
  exprNodes: ts.Expression[],
172
- isArrowBody: boolean
122
+ templateNode: ts.Node
173
123
  ): string {
174
124
  if (!slots || slots.length === 0) {
175
125
  return `${getOrCreateTemplateId(ctx, html)}()`;
@@ -178,6 +128,7 @@ function generateTemplateCode(
178
128
  let code: string[] = [],
179
129
  declarations: string[] = [],
180
130
  index = 0,
131
+ isArrowBody = isArrowExpressionBody(templateNode),
181
132
  nodes = new Map<string, string>(),
182
133
  root = uid('root');
183
134
 
@@ -286,6 +237,10 @@ function isNestedHtmlTemplate(expr: ts.Expression): expr is ts.TaggedTemplateExp
286
237
  return ts.isTaggedTemplateExpression(expr) && ts.isIdentifier(expr.tag) && expr.tag.text === COMPILER_ENTRYPOINT;
287
238
  }
288
239
 
240
+ function isArrowExpressionBody(node: ts.Node): boolean {
241
+ return ts.isArrowFunction(node.parent) && (node.parent as ts.ArrowFunction).body === node;
242
+ }
243
+
289
244
  function isReactiveCall(expr: ts.Expression): expr is ts.CallExpression {
290
245
  return (
291
246
  ts.isCallExpression(expr) &&
@@ -296,117 +251,152 @@ function isReactiveCall(expr: ts.Expression): expr is ts.CallExpression {
296
251
  );
297
252
  }
298
253
 
299
- function replaceReverse(text: string, replacements: Replacement[]): string {
300
- let sorted = replacements.slice().sort((a, b) => b.start - a.start);
301
-
302
- for (let i = 0, n = sorted.length; i < n; i++) {
303
- let r = sorted[i];
304
-
305
- text = text.slice(0, r.start) + r.newText + text.slice(r.end);
306
- }
307
-
308
- return text;
309
- }
310
-
311
254
  function rewriteExpression(ctx: CodegenContext, expr: ts.Expression): string {
312
255
  if (isNestedHtmlTemplate(expr)) {
313
256
  return generateNestedTemplateCode(ctx, expr);
314
257
  }
315
258
 
259
+ // Returns just args "array, callback" - for direct slot usage where generateNodeBinding wraps it
316
260
  if (isReactiveCall(expr)) {
317
- 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
+ `;
318
265
  }
319
266
 
320
267
  if (!ast.hasMatch(expr, n => isNestedHtmlTemplate(n as ts.Expression) || isReactiveCall(n as ts.Expression))) {
321
- return ctx.printer.printNode(ts.EmitHint.Expression, expr, ctx.sourceFile);
268
+ return printer.printNode(ts.EmitHint.Expression, expr, ctx.sourceFile);
322
269
  }
323
270
 
324
- let replacements: Replacement[] = [];
271
+ return walkExpression(ctx, expr);
272
+ }
325
273
 
326
- 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
+ }
327
279
 
328
- return replaceReverse(expr.getText(ctx.sourceFile), replacements);
329
- }
280
+ if (isReactiveCall(node as ts.Expression)) {
281
+ let call = node as ts.CallExpression;
330
282
 
331
- // Returns just args "array, callback" - for direct slot usage where generateNodeBinding wraps it
332
- function rewriteReactiveCall(ctx: CodegenContext, node: ts.CallExpression): string {
333
- let arrayArg = node.arguments[0],
334
- arrayText = ctx.printer.printNode(ts.EmitHint.Expression, arrayArg, ctx.sourceFile),
335
- callbackArg = node.arguments[1],
336
- 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
+ }
337
285
 
338
- return `${arrayText}, ${callbackText}`;
339
- }
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
+ }
340
290
 
341
- // Returns full "new ArraySlot(array, callback)" - for nested reactive calls inside expressions
342
- function rewriteNestedReactiveCall(ctx: CodegenContext, node: ts.CallExpression): string {
343
- let arrayArg = node.arguments[0],
344
- arrayText = ctx.printer.printNode(ts.EmitHint.Expression, arrayArg, ctx.sourceFile),
345
- callbackArg = node.arguments[1],
346
- callbackText = rewriteExpression(ctx, callbackArg as ts.Expression);
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
+ }
347
295
 
348
- return `new ${COMPILER_NAMESPACE}.ArraySlot(${arrayText}, ${callbackText})`;
349
- }
296
+ if (ts.isBinaryExpression(node)) {
297
+ return `${walkExpression(ctx, node.left)} ${node.operatorToken.getText(ctx.sourceFile)} ${walkExpression(ctx, node.right)}`;
298
+ }
350
299
 
351
- // Eager discovery - walk all expressions to find templates before prepend generation
352
- function discoverTemplatesInExpression(ctx: CodegenContext, node: ts.Node): void {
353
- if (isNestedHtmlTemplate(node as ts.Expression)) {
354
- let template = node as ts.TaggedTemplateExpression,
355
- expressions: ts.Expression[] = [],
356
- literals: string[] = [],
357
- tpl = template.template;
300
+ if (ts.isCallExpression(node)) {
301
+ let args: string[] = [];
358
302
 
359
- if (ts.isNoSubstitutionTemplateLiteral(tpl)) {
360
- literals.push(tpl.text);
303
+ for (let i = 0, n = node.arguments.length; i < n; i++) {
304
+ args.push(walkExpression(ctx, node.arguments[i]));
361
305
  }
362
- else if (ts.isTemplateExpression(tpl)) {
363
- literals.push(tpl.head.text);
364
306
 
365
- for (let i = 0, n = tpl.templateSpans.length; i < n; i++) {
366
- expressions.push(tpl.templateSpans[i].expression);
367
- literals.push(tpl.templateSpans[i].literal.text);
368
- }
369
- }
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
+ }
370
317
 
371
- let parsed = parser.parse(literals) as ParseResult;
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
+ }
372
327
 
373
- getOrCreateTemplateId(ctx, parsed.html);
328
+ if (ts.isArrayLiteralExpression(node)) {
329
+ let elements: string[] = [];
374
330
 
375
- for (let i = 0, n = expressions.length; i < n; i++) {
376
- discoverTemplatesInExpression(ctx, expressions[i]);
331
+ for (let i = 0, n = node.elements.length; i < n; i++) {
332
+ elements.push( walkExpression(ctx, node.elements[i]) );
377
333
  }
334
+
335
+ return `[${elements.join(', ')}]`;
378
336
  }
379
- else if (isReactiveCall(node as ts.Expression)) {
380
- let call = node as ts.CallExpression;
381
337
 
382
- if (call.arguments.length >= 2) {
383
- 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
+ }
384
356
  }
357
+
358
+ return `{ ${properties.join(', ')} }`;
385
359
  }
386
- else {
387
- ts.forEachChild(node, child => discoverTemplatesInExpression(ctx, child));
360
+
361
+ if (ts.isPrefixUnaryExpression(node)) {
362
+ return `${ts.tokenToString(node.operator)}${walkExpression(ctx, node.operand)}`;
388
363
  }
389
- }
390
364
 
391
- function discoverAllTemplates(ctx: CodegenContext, templates: TemplateInfo[]): void {
392
- for (let i = 0, n = templates.length; i < n; i++) {
393
- 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
+ }
394
368
 
395
- getOrCreateTemplateId(ctx, parsed.html);
369
+ if (ts.isTemplateExpression(node)) {
370
+ let parts = [node.head.text];
396
371
 
397
- for (let j = 0, m = templates[i].expressions.length; j < m; j++) {
398
- 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);
399
376
  }
377
+
378
+ return '`' + parts.join('') + '`';
400
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);
401
391
  }
402
392
 
403
393
 
404
394
  const generateCode = (templates: TemplateInfo[], sourceFile: ts.SourceFile, checker?: ts.TypeChecker): CodegenResult => {
405
395
  let result: CodegenResult = {
406
- prepend: [],
407
- replacements: [],
408
- templates: new Map()
409
- };
396
+ prepend: [],
397
+ replacements: [],
398
+ templates: new Map()
399
+ };
410
400
 
411
401
  if (templates.length === 0) {
412
402
  return result;
@@ -429,23 +419,19 @@ const generateCode = (templates: TemplateInfo[], sourceFile: ts.SourceFile, chec
429
419
  }
430
420
 
431
421
  let ctx: CodegenContext = {
432
- checker,
433
- printer,
434
- sourceFile,
435
- templates: result.templates
436
- };
422
+ checker,
423
+ sourceFile,
424
+ templates: result.templates
425
+ };
437
426
 
438
427
  for (let i = 0, n = rootTemplates.length; i < n; i++) {
439
428
  let template = rootTemplates[i];
440
429
 
441
430
  result.replacements.push({
442
431
  generate: (sf) => {
443
- let codeBefore = sf.getFullText().slice(0, template.node.getStart(sf)),
444
- exprTexts: string[] = [],
445
- isArrowBody = codeBefore.trimEnd().endsWith('=>'),
432
+ let exprTexts: string[] = [],
446
433
  localCtx: CodegenContext = {
447
434
  checker,
448
- printer,
449
435
  sourceFile: sf,
450
436
  templates: ctx.templates
451
437
  },
@@ -455,12 +441,13 @@ const generateCode = (templates: TemplateInfo[], sourceFile: ts.SourceFile, chec
455
441
  exprTexts.push(rewriteExpression(localCtx, template.expressions[j]));
456
442
  }
457
443
 
458
- if (isArrowBody && (!parsed.slots || parsed.slots.length === 0)) {
459
- let arrowMatch = codeBefore.match(ARROW_EMPTY_PARAMS);
460
-
461
- if (arrowMatch) {
462
- return getOrCreateTemplateId(localCtx, parsed.html);
463
- }
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);
464
451
  }
465
452
 
466
453
  return generateTemplateCode(
@@ -468,16 +455,13 @@ const generateCode = (templates: TemplateInfo[], sourceFile: ts.SourceFile, chec
468
455
  parsed,
469
456
  exprTexts,
470
457
  template.expressions,
471
- isArrowBody
458
+ template.node
472
459
  );
473
460
  },
474
461
  node: template.node
475
462
  });
476
463
  }
477
464
 
478
- // Eager discovery: find all templates before prepend generation
479
- discoverAllTemplates(ctx, templates);
480
-
481
465
  for (let [html, id] of ctx.templates) {
482
466
  result.prepend.push(`const ${id} = ${COMPILER_NAMESPACE}.template(\`${html}\`);`);
483
467
  }
@@ -486,5 +470,5 @@ const generateCode = (templates: TemplateInfo[], sourceFile: ts.SourceFile, chec
486
470
  };
487
471
 
488
472
 
489
- export { generateCode };
473
+ export { generateCode, printer };
490
474
  export type { CodegenResult };
@@ -1,26 +1,11 @@
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
- function isInRange(ranges: { end: number; start: number }[], start: number, end: number): boolean {
9
- for (let i = 0, n = ranges.length; i < n; i++) {
10
- let range = ranges[i];
11
-
12
- if (start >= range.start && end <= range.end) {
13
- return true;
14
- }
15
- }
16
-
17
- return false;
18
- }
19
-
20
-
21
- let printer = ts.createPrinter({ newLine: ts.NewLineKind.LineFeed });
22
-
23
-
24
9
  const plugin: Plugin = {
25
10
  patterns: [
26
11
  `${COMPILER_ENTRYPOINT}\``,
@@ -30,17 +15,15 @@ const plugin: Plugin = {
30
15
  transform: (ctx: TransformContext) => {
31
16
  let importsIntent: ImportIntent[] = [],
32
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 }[] = [],
33
20
  replacements: ReplacementIntent[] = [],
34
- removeImports: string[] = [];
35
-
36
- // Find templates first to build exclusion ranges
37
- let templates = findHtmlTemplates(ctx.sourceFile, ctx.checker);
38
-
39
- // Build ranges for all template nodes - reactive calls inside these are handled by template codegen
40
- let templateRanges: { end: number; start: number }[] = [];
21
+ removeImports: string[] = [],
22
+ // Find templates first to build exclusion ranges
23
+ templates = findHtmlTemplates(ctx.sourceFile, ctx.checker);
41
24
 
42
25
  for (let i = 0, n = templates.length; i < n; i++) {
43
- templateRanges.push({
26
+ ranges.push({
44
27
  end: templates[i].end,
45
28
  start: templates[i].start
46
29
  });
@@ -53,12 +36,15 @@ const plugin: Plugin = {
53
36
  let call = reactiveCalls[i];
54
37
 
55
38
  // Skip reactive calls that are inside template expressions - handled by template codegen
56
- if (isInRange(templateRanges, call.start, call.end)) {
39
+ if (ast.inRange(ranges, call.start, call.end)) {
57
40
  continue;
58
41
  }
59
42
 
60
43
  replacements.push({
61
- 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
+ )`,
62
48
  node: call.node
63
49
  });
64
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 };