@esportsplus/template 0.37.0 → 0.38.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,7 +1,8 @@
1
+ import type { ReplacementIntent } from '@esportsplus/typescript/compiler';
1
2
  import { ts } from '@esportsplus/typescript';
2
- import { ast, code as c, imports, uid, type Replacement } from '@esportsplus/typescript/compiler';
3
- import { COMPILER_ENTRYPOINT, COMPILER_TYPES, DIRECT_ATTACH_EVENTS, LIFECYCLE_EVENTS, PACKAGE } from '~/constants';
4
- import type { ReactiveCallInfo, TemplateInfo } from './ts-parser';
3
+ import { ast, uid } from '@esportsplus/typescript/compiler';
4
+ import { COMPILER_ENTRYPOINT, COMPILER_ENTRYPOINT_REACTIVITY, COMPILER_NAMESPACE, COMPILER_TYPES, DIRECT_ATTACH_EVENTS, LIFECYCLE_EVENTS } from '~/constants';
5
+ import type { TemplateInfo } from './ts-parser';
5
6
  import { analyze } from './analyzer';
6
7
  import parser from './parser';
7
8
 
@@ -17,15 +18,15 @@ type Attribute = {
17
18
 
18
19
  type CodegenContext = {
19
20
  checker?: ts.TypeChecker;
20
- imports: Map<string, string>;
21
21
  printer: ts.Printer;
22
22
  sourceFile: ts.SourceFile;
23
23
  templates: Map<string, string>;
24
24
  };
25
25
 
26
26
  type CodegenResult = {
27
- changed: boolean;
28
- code: string;
27
+ prepend: string[];
28
+ replacements: ReplacementIntent[];
29
+ templates: Map<string, string>;
29
30
  };
30
31
 
31
32
  type Node = {
@@ -38,6 +39,12 @@ type ParseResult = {
38
39
  slots: (Attribute | Node)[] | null;
39
40
  };
40
41
 
42
+ type Replacement = {
43
+ end: number;
44
+ newText: string;
45
+ start: number;
46
+ };
47
+
41
48
 
42
49
  const ARROW_EMPTY_PARAMS = /\(\s*\)\s*=>\s*$/;
43
50
 
@@ -45,60 +52,56 @@ const ARROW_EMPTY_PARAMS = /\(\s*\)\s*=>\s*$/;
45
52
  let printer = ts.createPrinter({ newLine: ts.NewLineKind.LineFeed });
46
53
 
47
54
 
48
- function addImport(ctx: CodegenContext, name: string): string {
49
- let alias = ctx.imports.get(name);
50
-
51
- if (!alias) {
52
- alias = uid(name);
53
- ctx.imports.set(name, alias);
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));
54
77
  }
55
-
56
- return alias;
57
78
  }
58
79
 
59
- function generateAttributeBinding(ctx: CodegenContext, element: string, name: string, expr: string, staticValue: string): string {
80
+ function generateAttributeBinding(element: string, name: string, expr: string, staticValue: string): string {
60
81
  if (name.startsWith('on') && name.length > 2) {
61
82
  let event = name.slice(2).toLowerCase(),
62
83
  key = name.toLowerCase();
63
84
 
64
85
  if (LIFECYCLE_EVENTS.has(key)) {
65
- return `${addImport(ctx, key)}(${element}, ${expr});`;
86
+ return `${COMPILER_NAMESPACE}.${key}(${element}, ${expr});`;
66
87
  }
67
88
 
68
89
  if (DIRECT_ATTACH_EVENTS.has(key)) {
69
- return `${addImport(ctx, 'on')}(${element}, '${event}', ${expr});`;
90
+ return `${COMPILER_NAMESPACE}.on(${element}, '${event}', ${expr});`;
70
91
  }
71
92
 
72
- return `${addImport(ctx, 'delegate')}(${element}, '${event}', ${expr});`;
93
+ return `${COMPILER_NAMESPACE}.delegate(${element}, '${event}', ${expr});`;
73
94
  }
74
95
 
75
96
  if (name === 'class') {
76
- return `${addImport(ctx, 'setClass')}(${element}, '${staticValue}', ${expr});`;
97
+ return `${COMPILER_NAMESPACE}.setClass(${element}, '${staticValue}', ${expr});`;
77
98
  }
78
99
 
79
100
  if (name === 'style') {
80
- return `${addImport(ctx, 'setStyle')}(${element}, '${staticValue}', ${expr});`;
101
+ return `${COMPILER_NAMESPACE}.setStyle(${element}, '${staticValue}', ${expr});`;
81
102
  }
82
103
 
83
- return `${addImport(ctx, 'setProperty')}(${element}, '${name}', ${expr});`;
84
- }
85
-
86
- function collectNestedTemplateReplacements(
87
- ctx: CodegenContext,
88
- node: ts.Node,
89
- exprStart: number,
90
- replacements: Replacement[]
91
- ): void {
92
- if (isNestedHtmlTemplate(node as ts.Expression)) {
93
- replacements.push({
94
- end: node.end - exprStart,
95
- newText: generateNestedTemplateCode(ctx, node as ts.TaggedTemplateExpression),
96
- start: node.getStart() - exprStart
97
- });
98
- }
99
- else {
100
- ts.forEachChild(node, child => collectNestedTemplateReplacements(ctx, child, exprStart, replacements));
101
- }
104
+ return `${COMPILER_NAMESPACE}.setProperty(${element}, '${name}', ${expr});`;
102
105
  }
103
106
 
104
107
  function generateNestedTemplateCode(ctx: CodegenContext, node: ts.TaggedTemplateExpression): string {
@@ -133,30 +136,30 @@ function generateNestedTemplateCode(ctx: CodegenContext, node: ts.TaggedTemplate
133
136
 
134
137
  function generateNodeBinding(ctx: CodegenContext, anchor: string, exprText: string, exprNode: ts.Expression | undefined): string {
135
138
  if (!exprNode) {
136
- return `${addImport(ctx, 'slot')}(${anchor}, ${exprText});`;
139
+ return `${COMPILER_NAMESPACE}.slot(${anchor}, ${exprText});`;
137
140
  }
138
141
 
139
142
  if (isNestedHtmlTemplate(exprNode)) {
140
- return `${anchor}.parentNode.insertBefore(${generateNestedTemplateCode(ctx, exprNode)}, ${anchor});`;
143
+ return `${anchor}.parentNode!.insertBefore(${generateNestedTemplateCode(ctx, exprNode)}, ${anchor});`;
141
144
  }
142
145
 
143
146
  let slotType = analyze(exprNode, ctx.checker);
144
147
 
145
148
  switch (slotType) {
146
- case COMPILER_TYPES.Effect:
147
- return `new ${addImport(ctx, 'EffectSlot')}(${anchor}, ${exprText});`;
148
-
149
149
  case COMPILER_TYPES.ArraySlot:
150
- return `new ${addImport(ctx, 'ArraySlot')}(${anchor}, ${exprText});`;
150
+ return `${anchor}.parentNode!.insertBefore(new ${COMPILER_NAMESPACE}.ArraySlot(${exprText}).fragment, ${anchor});`;
151
+
152
+ case COMPILER_TYPES.DocumentFragment:
153
+ return `${anchor}.parentNode!.insertBefore(${exprText}, ${anchor});`;
154
+
155
+ case COMPILER_TYPES.Effect:
156
+ return `new ${COMPILER_NAMESPACE}.EffectSlot(${anchor}, ${exprText});`;
151
157
 
152
158
  case COMPILER_TYPES.Static:
153
159
  return `${anchor}.textContent = ${exprText};`;
154
160
 
155
- case COMPILER_TYPES.DocumentFragment:
156
- return `${anchor}.parentNode.insertBefore(${exprText}, ${anchor});`;
157
-
158
161
  default:
159
- return `${addImport(ctx, 'slot')}(${anchor}, ${exprText});`;
162
+ return `${COMPILER_NAMESPACE}.slot(${anchor}, ${exprText});`;
160
163
  }
161
164
  }
162
165
 
@@ -206,17 +209,15 @@ function generateTemplateCode(
206
209
  }
207
210
  }
208
211
 
209
- let alias = addImport(ctx, 'Element'),
210
- name = uid('element'),
212
+ let name = uid('element'),
211
213
  segments = path.slice(start),
212
214
  value = `${ancestor}.${segments.join('!.')}`;
213
215
 
214
- // Cast root.firstChild to Element since DocumentFragment.firstChild returns ChildNode
215
216
  if (ancestor === root && segments[0] === 'firstChild') {
216
- value = value.replace(`${ancestor}.firstChild!`, `(${ancestor}.firstChild! as ${alias})`);
217
+ value = value.replace(`${ancestor}.firstChild!`, `(${ancestor}.firstChild! as ${COMPILER_NAMESPACE}.Element)`);
217
218
  }
218
219
 
219
- declarations.push(`${name} = ${value} as ${alias}`);
220
+ declarations.push(`${name} = ${value} as ${COMPILER_NAMESPACE}.Element`);
220
221
  nodes.set(key, name);
221
222
  }
222
223
 
@@ -239,14 +240,13 @@ function generateTemplateCode(
239
240
 
240
241
  if (name === COMPILER_TYPES.Attributes) {
241
242
  code.push(
242
- `${addImport(ctx, 'setProperties')}(${element}, ${exprTexts[index] || 'undefined'});`
243
+ `${COMPILER_NAMESPACE}.setProperties(${element}, ${exprTexts[index] || 'undefined'});`
243
244
  );
244
245
  index++;
245
246
  }
246
247
  else {
247
248
  code.push(
248
249
  generateAttributeBinding(
249
- ctx,
250
250
  element,
251
251
  name,
252
252
  exprTexts[index++] || 'undefined',
@@ -285,143 +285,194 @@ function isNestedHtmlTemplate(expr: ts.Expression): expr is ts.TaggedTemplateExp
285
285
  return ts.isTaggedTemplateExpression(expr) && ts.isIdentifier(expr.tag) && expr.tag.text === COMPILER_ENTRYPOINT;
286
286
  }
287
287
 
288
+ function isReactiveCall(expr: ts.Expression): expr is ts.CallExpression {
289
+ return (
290
+ ts.isCallExpression(expr) &&
291
+ ts.isPropertyAccessExpression(expr.expression) &&
292
+ ts.isIdentifier(expr.expression.expression) &&
293
+ expr.expression.expression.text === COMPILER_ENTRYPOINT &&
294
+ expr.expression.name.text === COMPILER_ENTRYPOINT_REACTIVITY
295
+ );
296
+ }
297
+
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
+
288
310
  function rewriteExpression(ctx: CodegenContext, expr: ts.Expression): string {
289
311
  if (isNestedHtmlTemplate(expr)) {
290
312
  return generateNestedTemplateCode(ctx, expr);
291
313
  }
292
314
 
293
- if (!ast.hasMatch(expr, n => isNestedHtmlTemplate(n as ts.Expression))) {
315
+ if (isReactiveCall(expr)) {
316
+ return rewriteReactiveCall(ctx, expr);
317
+ }
318
+
319
+ if (!ast.hasMatch(expr, n => isNestedHtmlTemplate(n as ts.Expression) || isReactiveCall(n as ts.Expression))) {
294
320
  return ctx.printer.printNode(ts.EmitHint.Expression, expr, ctx.sourceFile);
295
321
  }
296
322
 
297
323
  let replacements: Replacement[] = [];
298
324
 
299
- collectNestedTemplateReplacements(ctx, expr, expr.getStart(), replacements);
325
+ collectNestedReplacements(ctx, expr, expr.getStart(ctx.sourceFile), replacements);
300
326
 
301
- return c.replaceReverse(expr.getText(ctx.sourceFile), replacements);
327
+ return replaceReverse(expr.getText(ctx.sourceFile), replacements);
302
328
  }
303
329
 
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);
304
335
 
305
- const generateCode = (templates: TemplateInfo[], originalCode: string, sourceFile: ts.SourceFile, checker?: ts.TypeChecker, existingAliases?: Map<string, string>): CodegenResult => {
306
- if (templates.length === 0) {
307
- return { changed: false, code: originalCode };
308
- }
336
+ return `${arrayText}, ${callbackText}`;
337
+ }
309
338
 
310
- // Precompute expression ranges for nested template detection
311
- let ranges: { end: number; start: number }[] = [];
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;
312
346
 
313
- for (let i = 0, n = templates.length; i < n; i++) {
314
- let exprs = templates[i].expressions;
347
+ if (ts.isNoSubstitutionTemplateLiteral(tpl)) {
348
+ literals.push(tpl.text);
349
+ }
350
+ else if (ts.isTemplateExpression(tpl)) {
351
+ literals.push(tpl.head.text);
315
352
 
316
- for (let j = 0, m = exprs.length; j < m; j++) {
317
- ranges.push({ end: exprs[j].end, start: exprs[j].getStart() });
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
+ }
318
357
  }
319
- }
320
358
 
321
- let rootTemplates = templates.filter(t => !ast.inRange(ranges, t.start, t.end));
359
+ let parsed = parser.parse(literals) as ParseResult;
322
360
 
323
- if (rootTemplates.length === 0) {
324
- return { changed: false, code: originalCode };
361
+ getOrCreateTemplateId(ctx, parsed.html);
362
+
363
+ for (let i = 0, n = expressions.length; i < n; i++) {
364
+ discoverTemplatesInExpression(ctx, expressions[i]);
365
+ }
325
366
  }
367
+ else if (isReactiveCall(node as ts.Expression)) {
368
+ let call = node as ts.CallExpression;
326
369
 
327
- let ctx: CodegenContext = {
328
- checker,
329
- imports: existingAliases ?? new Map(),
330
- printer,
331
- sourceFile,
332
- templates: new Map(),
333
- },
334
- replacements: Replacement[] = [],
335
- templateAlias = addImport(ctx, 'template');
370
+ if (call.arguments.length >= 2) {
371
+ discoverTemplatesInExpression(ctx, call.arguments[1]);
372
+ }
373
+ }
374
+ else {
375
+ ts.forEachChild(node, child => discoverTemplatesInExpression(ctx, child));
376
+ }
377
+ }
336
378
 
337
- for (let i = 0, n = rootTemplates.length; i < n; i++) {
338
- let exprTexts: string[] = [],
339
- template = rootTemplates[i];
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;
340
382
 
341
- for (let j = 0, m = template.expressions.length; j < m; j++) {
342
- exprTexts.push(rewriteExpression(ctx, template.expressions[j]));
343
- }
383
+ getOrCreateTemplateId(ctx, parsed.html);
344
384
 
345
- let codeBefore = originalCode.slice(0, template.start),
346
- isArrowBody = codeBefore.trimEnd().endsWith('=>'),
347
- parsed = parser.parse(template.literals) as ParseResult;
348
-
349
- // Optimize: when template has no slots and is `() => template`, use template directly
350
- if (isArrowBody && (!parsed.slots || parsed.slots.length === 0)) {
351
- let arrowMatch = codeBefore.match(ARROW_EMPTY_PARAMS);
352
-
353
- if (arrowMatch) {
354
- replacements.push({
355
- end: template.end,
356
- newText: getOrCreateTemplateId(ctx, parsed.html),
357
- start: template.start - arrowMatch[0].length
358
- });
359
- continue;
360
- }
385
+ for (let j = 0, m = templates[i].expressions.length; j < m; j++) {
386
+ discoverTemplatesInExpression(ctx, templates[i].expressions[j]);
361
387
  }
388
+ }
389
+ }
362
390
 
363
- replacements.push({
364
- end: template.end,
365
- newText: generateTemplateCode(
366
- ctx,
367
- parsed,
368
- exprTexts,
369
- template.expressions,
370
- isArrowBody
371
- ),
372
- start: template.start
373
- });
391
+
392
+ const generateCode = (templates: TemplateInfo[], sourceFile: ts.SourceFile, checker?: ts.TypeChecker): CodegenResult => {
393
+ let result: CodegenResult = {
394
+ prepend: [],
395
+ replacements: [],
396
+ templates: new Map()
397
+ };
398
+
399
+ if (templates.length === 0) {
400
+ return result;
374
401
  }
375
402
 
376
- let changed = replacements.length > 0,
377
- code = c.replaceReverse(originalCode, replacements);
403
+ let ranges: { end: number; start: number }[] = [];
378
404
 
379
- if (changed && ctx.templates.size > 0) {
380
- let aliasedImports: string[] = [],
381
- factories: string[] = [],
382
- updatedSourceFile = ts.createSourceFile(sourceFile.fileName, code, sourceFile.languageVersion, true);
405
+ for (let i = 0, n = templates.length; i < n; i++) {
406
+ let exprs = templates[i].expressions;
383
407
 
384
- for (let [name, alias] of ctx.imports) {
385
- aliasedImports.push(`${name} as ${alias}`);
408
+ for (let j = 0, m = exprs.length; j < m; j++) {
409
+ ranges.push({ end: exprs[j].end, start: exprs[j].getStart(sourceFile) });
386
410
  }
411
+ }
387
412
 
388
- for (let [html, id] of ctx.templates) {
389
- factories.push(`const ${id} = ${templateAlias}(\`${html}\`);`);
390
- }
413
+ let rootTemplates = templates.filter(t => !ast.inRange(ranges, t.node.getStart(sourceFile), t.node.end));
391
414
 
392
- // Remove html entrypoint and add aliased imports
393
- code = imports.modify(code, updatedSourceFile, PACKAGE, {
394
- add: new Set(aliasedImports),
395
- remove: [COMPILER_ENTRYPOINT]
396
- });
397
- code = factories.join('\n') + '\n\n' + code;
415
+ if (rootTemplates.length === 0) {
416
+ return result;
398
417
  }
399
418
 
400
- return { changed, code };
401
- };
419
+ let ctx: CodegenContext = {
420
+ checker,
421
+ printer,
422
+ sourceFile,
423
+ templates: result.templates
424
+ };
402
425
 
403
- const generateReactiveInlining = (calls: ReactiveCallInfo[], code: string, sourceFile: ts.SourceFile, arraySlotAlias: string): string => {
404
- if (calls.length === 0) {
405
- return code;
406
- }
426
+ for (let i = 0, n = rootTemplates.length; i < n; i++) {
427
+ let template = rootTemplates[i];
428
+
429
+ result.replacements.push({
430
+ generate: (sf) => {
431
+ let codeBefore = sf.getFullText().slice(0, template.node.getStart(sf)),
432
+ exprTexts: string[] = [],
433
+ isArrowBody = codeBefore.trimEnd().endsWith('=>'),
434
+ localCtx: CodegenContext = {
435
+ checker,
436
+ printer,
437
+ sourceFile: sf,
438
+ templates: ctx.templates
439
+ },
440
+ parsed = parser.parse(template.literals) as ParseResult;
441
+
442
+ for (let j = 0, m = template.expressions.length; j < m; j++) {
443
+ exprTexts.push(rewriteExpression(localCtx, template.expressions[j]));
444
+ }
407
445
 
408
- let replacements: Replacement[] = [];
446
+ if (isArrowBody && (!parsed.slots || parsed.slots.length === 0)) {
447
+ let arrowMatch = codeBefore.match(ARROW_EMPTY_PARAMS);
409
448
 
410
- for (let i = 0, n = calls.length; i < n; i++) {
411
- let call = calls[i];
449
+ if (arrowMatch) {
450
+ return getOrCreateTemplateId(localCtx, parsed.html);
451
+ }
452
+ }
412
453
 
413
- replacements.push({
414
- end: call.end,
415
- newText: `new ${arraySlotAlias}(
416
- ${printer.printNode(ts.EmitHint.Expression, call.arrayArg, sourceFile)},
417
- ${printer.printNode(ts.EmitHint.Expression, call.callbackArg, sourceFile)}
418
- )`,
419
- start: call.start
454
+ return generateTemplateCode(
455
+ localCtx,
456
+ parsed,
457
+ exprTexts,
458
+ template.expressions,
459
+ isArrowBody
460
+ );
461
+ },
462
+ node: template.node
420
463
  });
421
464
  }
422
465
 
423
- return c.replaceReverse(code, replacements);
466
+ // Eager discovery: find all templates before prepend generation
467
+ discoverAllTemplates(ctx, templates);
468
+
469
+ for (let [html, id] of ctx.templates) {
470
+ result.prepend.push(`const ${id} = ${COMPILER_NAMESPACE}.template(\`${html}\`);`);
471
+ }
472
+
473
+ return result;
424
474
  };
425
475
 
426
- export { generateCode, generateReactiveInlining };
476
+
477
+ export { generateCode };
427
478
  export type { CodegenResult };