@esportsplus/template 0.35.1 → 0.38.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,92 @@
1
+ import { COMPILER_ENTRYPOINT, COMPILER_ENTRYPOINT_REACTIVITY, COMPILER_TYPES } from '~/constants';
2
+ import { ts } from '@esportsplus/typescript';
3
+
4
+
5
+ // Union types that mix functions with non-functions (e.g., Renderable)
6
+ // should fall through to runtime slot dispatch
7
+ function isTypeFunction(type: ts.Type, checker: ts.TypeChecker): boolean {
8
+ if (type.isUnion()) {
9
+ for (let i = 0, n = type.types.length; i < n; i++) {
10
+ if (!isTypeFunction(type.types[i], checker)) {
11
+ return false;
12
+ }
13
+ }
14
+
15
+ return type.types.length > 0;
16
+ }
17
+
18
+ return type.getCallSignatures().length > 0;
19
+ }
20
+
21
+
22
+ const analyze = (expr: ts.Expression, checker?: ts.TypeChecker): COMPILER_TYPES => {
23
+ while (ts.isParenthesizedExpression(expr)) {
24
+ expr = expr.expression;
25
+ }
26
+
27
+ if (ts.isArrowFunction(expr) || ts.isFunctionExpression(expr)) {
28
+ return COMPILER_TYPES.Effect;
29
+ }
30
+
31
+ // Only html.reactive() calls become ArraySlot - handled by generateReactiveInlining
32
+ if (
33
+ ts.isCallExpression(expr) &&
34
+ ts.isPropertyAccessExpression(expr.expression) &&
35
+ ts.isIdentifier(expr.expression.expression) &&
36
+ expr.expression.expression.text === COMPILER_ENTRYPOINT &&
37
+ expr.expression.name.text === COMPILER_ENTRYPOINT_REACTIVITY
38
+ ) {
39
+ return COMPILER_TYPES.ArraySlot;
40
+ }
41
+
42
+ if (ts.isTaggedTemplateExpression(expr) && ts.isIdentifier(expr.tag) && expr.tag.text === COMPILER_ENTRYPOINT) {
43
+ return COMPILER_TYPES.DocumentFragment;
44
+ }
45
+
46
+ if (
47
+ ts.isNumericLiteral(expr) ||
48
+ ts.isStringLiteral(expr) ||
49
+ ts.isNoSubstitutionTemplateLiteral(expr) ||
50
+ expr.kind === ts.SyntaxKind.TrueKeyword ||
51
+ expr.kind === ts.SyntaxKind.FalseKeyword ||
52
+ expr.kind === ts.SyntaxKind.NullKeyword ||
53
+ expr.kind === ts.SyntaxKind.UndefinedKeyword
54
+ ) {
55
+ return COMPILER_TYPES.Static;
56
+ }
57
+
58
+ if (ts.isTemplateExpression(expr)) {
59
+ return COMPILER_TYPES.Primitive;
60
+ }
61
+
62
+ if (ts.isConditionalExpression(expr)) {
63
+ let whenFalse = analyze(expr.whenFalse, checker),
64
+ whenTrue = analyze(expr.whenTrue, checker);
65
+
66
+ if (whenTrue === whenFalse) {
67
+ return whenTrue;
68
+ }
69
+
70
+ if (whenTrue === COMPILER_TYPES.Effect || whenFalse === COMPILER_TYPES.Effect) {
71
+ return COMPILER_TYPES.Effect;
72
+ }
73
+
74
+ return COMPILER_TYPES.Unknown;
75
+ }
76
+
77
+ if (checker && (ts.isIdentifier(expr) || ts.isPropertyAccessExpression(expr) || ts.isCallExpression(expr))) {
78
+ try {
79
+ let type = checker.getTypeAtLocation(expr);
80
+
81
+ if (isTypeFunction(type, checker)) {
82
+ return COMPILER_TYPES.Effect;
83
+ }
84
+ }
85
+ catch {
86
+ }
87
+ }
88
+
89
+ return COMPILER_TYPES.Unknown;
90
+ };
91
+
92
+ export { analyze };
@@ -1,8 +1,9 @@
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, PACKAGE } from '~/constants';
4
- import type { ReactiveCallInfo, TemplateInfo } from './ts-parser';
5
- import { analyzeExpression, generateAttributeBinding, generateSpreadBindings } from './type-analyzer';
3
+ import { ast, uid } from '@esportsplus/typescript/compiler';
4
+ import { COMPILER_ENTRYPOINT, COMPILER_TYPES, DIRECT_ATTACH_EVENTS, LIFECYCLE_EVENTS } from '~/constants';
5
+ import type { TemplateInfo } from './ts-parser';
6
+ import { analyze } from './analyzer';
6
7
  import parser from './parser';
7
8
 
8
9
 
@@ -24,8 +25,10 @@ type CodegenContext = {
24
25
  };
25
26
 
26
27
  type CodegenResult = {
27
- changed: boolean;
28
- code: string;
28
+ imports: Map<string, string>;
29
+ prepend: string[];
30
+ replacements: ReplacementIntent[];
31
+ templates: Map<string, string>;
29
32
  };
30
33
 
31
34
  type Node = {
@@ -38,6 +41,12 @@ type ParseResult = {
38
41
  slots: (Attribute | Node)[] | null;
39
42
  };
40
43
 
44
+ type Replacement = {
45
+ end: number;
46
+ newText: string;
47
+ start: number;
48
+ };
49
+
41
50
 
42
51
  const ARROW_EMPTY_PARAMS = /\(\s*\)\s*=>\s*$/;
43
52
 
@@ -66,7 +75,7 @@ function collectNestedTemplateReplacements(
66
75
  replacements.push({
67
76
  end: node.end - exprStart,
68
77
  newText: generateNestedTemplateCode(ctx, node as ts.TaggedTemplateExpression),
69
- start: node.getStart() - exprStart
78
+ start: node.getStart(ctx.sourceFile) - exprStart
70
79
  });
71
80
  }
72
81
  else {
@@ -74,6 +83,33 @@ function collectNestedTemplateReplacements(
74
83
  }
75
84
  }
76
85
 
86
+ function generateAttributeBinding(ctx: CodegenContext, element: string, name: string, expr: string, staticValue: string): string {
87
+ if (name.startsWith('on') && name.length > 2) {
88
+ let event = name.slice(2).toLowerCase(),
89
+ key = name.toLowerCase();
90
+
91
+ if (LIFECYCLE_EVENTS.has(key)) {
92
+ return `${addImport(ctx, key)}(${element}, ${expr});`;
93
+ }
94
+
95
+ if (DIRECT_ATTACH_EVENTS.has(key)) {
96
+ return `${addImport(ctx, 'on')}(${element}, '${event}', ${expr});`;
97
+ }
98
+
99
+ return `${addImport(ctx, 'delegate')}(${element}, '${event}', ${expr});`;
100
+ }
101
+
102
+ if (name === 'class') {
103
+ return `${addImport(ctx, 'setClass')}(${element}, '${staticValue}', ${expr});`;
104
+ }
105
+
106
+ if (name === 'style') {
107
+ return `${addImport(ctx, 'setStyle')}(${element}, '${staticValue}', ${expr});`;
108
+ }
109
+
110
+ return `${addImport(ctx, 'setProperty')}(${element}, '${name}', ${expr});`;
111
+ }
112
+
77
113
  function generateNestedTemplateCode(ctx: CodegenContext, node: ts.TaggedTemplateExpression): string {
78
114
  let expressions: ts.Expression[] = [],
79
115
  exprTexts: string[] = [],
@@ -113,7 +149,7 @@ function generateNodeBinding(ctx: CodegenContext, anchor: string, exprText: stri
113
149
  return `${anchor}.parentNode.insertBefore(${generateNestedTemplateCode(ctx, exprNode)}, ${anchor});`;
114
150
  }
115
151
 
116
- let slotType = analyzeExpression(exprNode, ctx.checker);
152
+ let slotType = analyze(exprNode, ctx.checker);
117
153
 
118
154
  switch (slotType) {
119
155
  case COMPILER_TYPES.Effect:
@@ -184,7 +220,6 @@ function generateTemplateCode(
184
220
  segments = path.slice(start),
185
221
  value = `${ancestor}.${segments.join('!.')}`;
186
222
 
187
- // Cast root.firstChild to Element since DocumentFragment.firstChild returns ChildNode
188
223
  if (ancestor === root && segments[0] === 'firstChild') {
189
224
  value = value.replace(`${ancestor}.firstChild!`, `(${ancestor}.firstChild! as ${alias})`);
190
225
  }
@@ -211,26 +246,19 @@ function generateTemplateCode(
211
246
  let name = names[j];
212
247
 
213
248
  if (name === COMPILER_TYPES.Attributes) {
214
- let bindings = generateSpreadBindings(
215
- exprTexts[index] || 'undefined',
216
- element,
217
- n => addImport(ctx, n)
218
- );
219
-
220
- for (let k = 0, o = bindings.length; k < o; k++) {
221
- code.push(bindings[k]);
222
- }
223
-
249
+ code.push(
250
+ `${addImport(ctx, 'setProperties')}(${element}, ${exprTexts[index] || 'undefined'});`
251
+ );
224
252
  index++;
225
253
  }
226
254
  else {
227
255
  code.push(
228
256
  generateAttributeBinding(
257
+ ctx,
229
258
  element,
230
259
  name,
231
260
  exprTexts[index++] || 'undefined',
232
- slot.attributes.statics[name] || '',
233
- n => addImport(ctx, n)
261
+ slot.attributes.statics[name] || ''
234
262
  )
235
263
  );
236
264
  }
@@ -265,6 +293,18 @@ function isNestedHtmlTemplate(expr: ts.Expression): expr is ts.TaggedTemplateExp
265
293
  return ts.isTaggedTemplateExpression(expr) && ts.isIdentifier(expr.tag) && expr.tag.text === COMPILER_ENTRYPOINT;
266
294
  }
267
295
 
296
+ function replaceReverse(text: string, replacements: Replacement[]): string {
297
+ let sorted = replacements.slice().sort((a, b) => b.start - a.start);
298
+
299
+ for (let i = 0, n = sorted.length; i < n; i++) {
300
+ let r = sorted[i];
301
+
302
+ text = text.slice(0, r.start) + r.newText + text.slice(r.end);
303
+ }
304
+
305
+ return text;
306
+ }
307
+
268
308
  function rewriteExpression(ctx: CodegenContext, expr: ts.Expression): string {
269
309
  if (isNestedHtmlTemplate(expr)) {
270
310
  return generateNestedTemplateCode(ctx, expr);
@@ -276,131 +316,97 @@ function rewriteExpression(ctx: CodegenContext, expr: ts.Expression): string {
276
316
 
277
317
  let replacements: Replacement[] = [];
278
318
 
279
- collectNestedTemplateReplacements(ctx, expr, expr.getStart(), replacements);
319
+ collectNestedTemplateReplacements(ctx, expr, expr.getStart(ctx.sourceFile), replacements);
280
320
 
281
- return c.replaceReverse(expr.getText(ctx.sourceFile), replacements);
321
+ return replaceReverse(expr.getText(ctx.sourceFile), replacements);
282
322
  }
283
323
 
284
324
 
285
- const generateCode = (templates: TemplateInfo[], originalCode: string, sourceFile: ts.SourceFile, checker?: ts.TypeChecker, existingAliases?: Map<string, string>): CodegenResult => {
325
+ const generateCode = (templates: TemplateInfo[], sourceFile: ts.SourceFile, checker?: ts.TypeChecker, existingAliases?: Map<string, string>): CodegenResult => {
326
+ let result: CodegenResult = {
327
+ imports: existingAliases ?? new Map(),
328
+ prepend: [],
329
+ replacements: [],
330
+ templates: new Map()
331
+ };
332
+
286
333
  if (templates.length === 0) {
287
- return { changed: false, code: originalCode };
334
+ return result;
288
335
  }
289
336
 
290
- // Precompute expression ranges for nested template detection
291
337
  let ranges: { end: number; start: number }[] = [];
292
338
 
293
339
  for (let i = 0, n = templates.length; i < n; i++) {
294
340
  let exprs = templates[i].expressions;
295
341
 
296
342
  for (let j = 0, m = exprs.length; j < m; j++) {
297
- ranges.push({ end: exprs[j].end, start: exprs[j].getStart() });
343
+ ranges.push({ end: exprs[j].end, start: exprs[j].getStart(sourceFile) });
298
344
  }
299
345
  }
300
346
 
301
- let rootTemplates = templates.filter(t => !ast.inRange(ranges, t.start, t.end));
347
+ let rootTemplates = templates.filter(t => !ast.inRange(ranges, t.node.getStart(sourceFile), t.node.end));
302
348
 
303
349
  if (rootTemplates.length === 0) {
304
- return { changed: false, code: originalCode };
350
+ return result;
305
351
  }
306
352
 
307
353
  let ctx: CodegenContext = {
308
354
  checker,
309
- templates: new Map(),
310
- imports: existingAliases ?? new Map(),
355
+ imports: result.imports,
311
356
  printer,
312
- sourceFile
357
+ sourceFile,
358
+ templates: result.templates
313
359
  },
314
- replacements: Replacement[] = [],
315
360
  templateAlias = addImport(ctx, 'template');
316
361
 
317
362
  for (let i = 0, n = rootTemplates.length; i < n; i++) {
318
- let exprTexts: string[] = [],
319
- template = rootTemplates[i];
363
+ let template = rootTemplates[i];
364
+
365
+ result.replacements.push({
366
+ generate: (sf) => {
367
+ let codeBefore = sf.getFullText().slice(0, template.node.getStart(sf)),
368
+ exprTexts: string[] = [],
369
+ isArrowBody = codeBefore.trimEnd().endsWith('=>'),
370
+ localCtx: CodegenContext = {
371
+ checker,
372
+ imports: ctx.imports,
373
+ printer,
374
+ sourceFile: sf,
375
+ templates: ctx.templates
376
+ },
377
+ parsed = parser.parse(template.literals) as ParseResult;
378
+
379
+ for (let j = 0, m = template.expressions.length; j < m; j++) {
380
+ exprTexts.push(rewriteExpression(localCtx, template.expressions[j]));
381
+ }
320
382
 
321
- for (let j = 0, m = template.expressions.length; j < m; j++) {
322
- exprTexts.push(rewriteExpression(ctx, template.expressions[j]));
323
- }
383
+ if (isArrowBody && (!parsed.slots || parsed.slots.length === 0)) {
384
+ let arrowMatch = codeBefore.match(ARROW_EMPTY_PARAMS);
324
385
 
325
- let codeBefore = originalCode.slice(0, template.start),
326
- isArrowBody = codeBefore.trimEnd().endsWith('=>'),
327
- parsed = parser.parse(template.literals) as ParseResult;
328
-
329
- // Optimize: when template has no slots and is `() => template`, use template directly
330
- if (isArrowBody && (!parsed.slots || parsed.slots.length === 0)) {
331
- let arrowMatch = codeBefore.match(ARROW_EMPTY_PARAMS);
332
-
333
- if (arrowMatch) {
334
- replacements.push({
335
- end: template.end,
336
- newText: getOrCreateTemplateId(ctx, parsed.html),
337
- start: template.start - arrowMatch[0].length
338
- });
339
- continue;
340
- }
341
- }
386
+ if (arrowMatch) {
387
+ return getOrCreateTemplateId(localCtx, parsed.html);
388
+ }
389
+ }
342
390
 
343
- replacements.push({
344
- end: template.end,
345
- newText: generateTemplateCode(
346
- ctx,
347
- parsed,
348
- exprTexts,
349
- template.expressions,
350
- isArrowBody
351
- ),
352
- start: template.start
391
+ return generateTemplateCode(
392
+ localCtx,
393
+ parsed,
394
+ exprTexts,
395
+ template.expressions,
396
+ isArrowBody
397
+ );
398
+ },
399
+ node: template.node
353
400
  });
354
401
  }
355
402
 
356
- let changed = replacements.length > 0,
357
- code = c.replaceReverse(originalCode, replacements);
358
-
359
- if (changed && ctx.templates.size > 0) {
360
- let aliasedImports: string[] = [],
361
- factories: string[] = [];
362
-
363
- for (let [name, alias] of ctx.imports) {
364
- aliasedImports.push(`${name} as ${alias}`);
365
- }
366
-
367
- for (let [html, id] of ctx.templates) {
368
- factories.push(`const ${id} = ${templateAlias}(\`${html}\`);`);
369
- }
370
-
371
- // Remove html entrypoint and add aliased imports
372
- code = imports.modify(code, sourceFile, PACKAGE, {
373
- add: new Set(aliasedImports),
374
- remove: [COMPILER_ENTRYPOINT]
375
- });
376
- code = factories.join('\n') + '\n\n' + code;
403
+ for (let [html, id] of ctx.templates) {
404
+ result.prepend.push(`const ${id} = ${templateAlias}(\`${html}\`);`);
377
405
  }
378
406
 
379
- return { changed, code };
407
+ return result;
380
408
  };
381
409
 
382
- const generateReactiveInlining = (calls: ReactiveCallInfo[], code: string, sourceFile: ts.SourceFile, arraySlotAlias: string): string => {
383
- if (calls.length === 0) {
384
- return code;
385
- }
386
-
387
- let replacements: Replacement[] = [];
388
-
389
- for (let i = 0, n = calls.length; i < n; i++) {
390
- let call = calls[i];
391
-
392
- replacements.push({
393
- end: call.end,
394
- newText: `new ${arraySlotAlias}(
395
- ${printer.printNode(ts.EmitHint.Expression, call.arrayArg, sourceFile)},
396
- ${printer.printNode(ts.EmitHint.Expression, call.callbackArg, sourceFile)}
397
- )`,
398
- start: call.start
399
- });
400
- }
401
-
402
- return c.replaceReverse(code, replacements);
403
- };
404
410
 
405
- export { generateCode, generateReactiveInlining };
411
+ export { generateCode };
406
412
  export type { CodegenResult };
@@ -1,86 +1,56 @@
1
- import { ts } from '@esportsplus/typescript';
2
- import { code as c, imports, uid } from '@esportsplus/typescript/compiler';
3
- import { COMPILER_ENTRYPOINT, COMPILER_ENTRYPOINT_REACTIVITY, PACKAGE } from '~/constants';
4
- import { generateCode, generateReactiveInlining } from './codegen';
5
- import { findHtmlTemplates, findReactiveCalls } from './ts-parser';
1
+ import type { ImportIntent, Plugin, TransformContext } from '@esportsplus/typescript/compiler';
2
+ import { COMPILER_ENTRYPOINT, PACKAGE } from '~/constants';
3
+ import { generateCode } from './codegen';
4
+ import reactiveInliningPlugin, { SHARED_KEY } from './reactive-inlining';
5
+ import { findHtmlTemplates } from './ts-parser';
6
6
 
7
7
 
8
- type TransformResult = {
9
- changed: boolean;
10
- code: string;
11
- sourceFile: ts.SourceFile;
12
- };
13
-
14
-
15
- const PATTERNS = [`${COMPILER_ENTRYPOINT}\``, `${COMPILER_ENTRYPOINT}.${COMPILER_ENTRYPOINT_REACTIVITY}`];
16
-
17
- const REGEX_BACKSLASH = /\\/g;
18
-
19
- const REGEX_FORWARD_SLASH = /\//g;
8
+ const PATTERNS = [`${COMPILER_ENTRYPOINT}\``];
20
9
 
21
10
 
22
- const transform = (sourceFile: ts.SourceFile, program: ts.Program): TransformResult => {
23
- let code = sourceFile.getFullText();
11
+ const templatePlugin: Plugin = {
12
+ patterns: PATTERNS,
24
13
 
25
- if (!c.contains(code, { patterns: PATTERNS })) {
26
- return { changed: false, code, sourceFile };
27
- }
28
-
29
- let checker: ts.TypeChecker | undefined,
30
- fileName = sourceFile.fileName,
31
- programSourceFile = program.getSourceFile(fileName)
32
- || program.getSourceFile(fileName.replace(REGEX_BACKSLASH, '/'))
33
- || program.getSourceFile(fileName.replace(REGEX_FORWARD_SLASH, '\\'));
34
-
35
- if (programSourceFile) {
36
- checker = program.getTypeChecker();
37
- sourceFile = programSourceFile;
38
- }
14
+ transform: (ctx: TransformContext) => {
15
+ let templates = findHtmlTemplates(ctx.sourceFile, ctx.checker);
39
16
 
40
- let changed = false,
41
- codegenChanged = false,
42
- existingAliases = new Map<string, string>(),
43
- reactiveCalls = findReactiveCalls(sourceFile, checker),
44
- result = code;
45
-
46
- if (reactiveCalls.length > 0) {
47
- let arraySlotAlias = uid('ArraySlot');
17
+ if (templates.length === 0) {
18
+ return {};
19
+ }
48
20
 
49
- changed = true;
50
- existingAliases.set('ArraySlot', arraySlotAlias);
51
- result = generateReactiveInlining(reactiveCalls, result, sourceFile, arraySlotAlias);
52
- sourceFile = ts.createSourceFile(sourceFile.fileName, result, sourceFile.languageVersion, true);
53
- }
21
+ let existingAliases = new Map<string, string>(),
22
+ existingArraySlotAlias = ctx.shared.get(SHARED_KEY) as string | undefined;
54
23
 
55
- let templates = findHtmlTemplates(sourceFile, checker);
24
+ if (existingArraySlotAlias) {
25
+ existingAliases.set('ArraySlot', existingArraySlotAlias);
26
+ }
56
27
 
57
- if (templates.length > 0) {
58
- let codegenResult = generateCode(templates, result, sourceFile, checker, existingAliases);
28
+ let result = generateCode(templates, ctx.sourceFile, ctx.checker, existingAliases);
59
29
 
60
- if (codegenResult.changed) {
61
- changed = true;
62
- codegenChanged = true;
63
- result = codegenResult.code;
30
+ if (result.replacements.length === 0) {
31
+ return {};
64
32
  }
65
- }
66
33
 
67
- // Add aliased ArraySlot import if reactive calls were processed but codegen didn't run
68
- if (existingAliases.size > 0 && !codegenChanged) {
69
34
  let aliasedImports: string[] = [];
70
35
 
71
- for (let [name, alias] of existingAliases) {
36
+ for (let [name, alias] of result.imports) {
72
37
  aliasedImports.push(`${name} as ${alias}`);
73
38
  }
74
39
 
75
- result = imports.modify(result, sourceFile, PACKAGE, { add: aliasedImports });
76
- }
77
-
78
- if (changed) {
79
- sourceFile = ts.createSourceFile(sourceFile.fileName, result, sourceFile.languageVersion, true);
40
+ let imports: ImportIntent[] = [{
41
+ add: aliasedImports,
42
+ package: PACKAGE,
43
+ remove: [COMPILER_ENTRYPOINT]
44
+ }];
45
+
46
+ return {
47
+ imports,
48
+ prepend: result.prepend,
49
+ replacements: result.replacements
50
+ };
80
51
  }
81
-
82
- return { changed, code: result, sourceFile };
83
52
  };
84
53
 
85
54
 
86
- export { transform };
55
+ export default templatePlugin;
56
+ export { reactiveInliningPlugin, templatePlugin };
@@ -1,5 +1,5 @@
1
1
  import { plugin } from '@esportsplus/typescript/compiler';
2
- import { transform } from '..';
2
+ import { reactiveInliningPlugin, templatePlugin } from '..';
3
3
 
4
4
 
5
- export default plugin.tsc(transform) as ReturnType<typeof plugin.tsc>;
5
+ export default plugin.tsc([reactiveInliningPlugin, templatePlugin]) as ReturnType<typeof plugin.tsc>;
@@ -1,9 +1,9 @@
1
1
  import { plugin } from '@esportsplus/typescript/compiler';
2
2
  import { PACKAGE } from '../../constants';
3
- import { transform } from '..';
3
+ import { reactiveInliningPlugin, templatePlugin } from '..';
4
4
 
5
5
 
6
6
  export default plugin.vite({
7
7
  name: PACKAGE,
8
- transform
8
+ plugins: [reactiveInliningPlugin, templatePlugin]
9
9
  });
@@ -0,0 +1,116 @@
1
+ import type { Plugin, ReplacementIntent, TransformContext } from '@esportsplus/typescript/compiler';
2
+ import { ts } from '@esportsplus/typescript';
3
+ import { uid } from '@esportsplus/typescript/compiler';
4
+ import { COMPILER_ENTRYPOINT, COMPILER_ENTRYPOINT_REACTIVITY, PACKAGE } from '~/constants';
5
+
6
+
7
+ type ReactiveCallInfo = {
8
+ arrayArg: ts.Expression;
9
+ callbackArg: ts.Expression;
10
+ node: ts.CallExpression;
11
+ };
12
+
13
+
14
+ const PATTERNS = [`${COMPILER_ENTRYPOINT}.${COMPILER_ENTRYPOINT_REACTIVITY}`];
15
+
16
+ const SHARED_KEY = 'template:arraySlotAlias';
17
+
18
+
19
+ let printer = ts.createPrinter({ newLine: ts.NewLineKind.LineFeed });
20
+
21
+
22
+ function isHtmlFromPackage(node: ts.Identifier, checker: ts.TypeChecker | undefined): boolean {
23
+ if (node.text !== COMPILER_ENTRYPOINT) {
24
+ return false;
25
+ }
26
+
27
+ if (!checker) {
28
+ return true;
29
+ }
30
+
31
+ let symbol = checker.getSymbolAtLocation(node);
32
+
33
+ if (!symbol) {
34
+ return true;
35
+ }
36
+
37
+ if (symbol.flags & ts.SymbolFlags.Alias) {
38
+ symbol = checker.getAliasedSymbol(symbol);
39
+ }
40
+
41
+ let declarations = symbol.getDeclarations();
42
+
43
+ if (!declarations || declarations.length === 0) {
44
+ return true;
45
+ }
46
+
47
+ for (let i = 0, n = declarations.length; i < n; i++) {
48
+ let filename = declarations[i].getSourceFile().fileName;
49
+
50
+ if (filename.includes(PACKAGE) || filename.includes('@esportsplus/template')) {
51
+ return true;
52
+ }
53
+ }
54
+
55
+ return false;
56
+ }
57
+
58
+ function visit(node: ts.Node, calls: ReactiveCallInfo[], checker: ts.TypeChecker | undefined): void {
59
+ if (
60
+ ts.isCallExpression(node) &&
61
+ ts.isPropertyAccessExpression(node.expression) &&
62
+ ts.isIdentifier(node.expression.expression) &&
63
+ node.expression.name.text === COMPILER_ENTRYPOINT_REACTIVITY &&
64
+ node.arguments.length === 2 &&
65
+ isHtmlFromPackage(node.expression.expression, checker)
66
+ ) {
67
+ calls.push({
68
+ arrayArg: node.arguments[0],
69
+ callbackArg: node.arguments[1],
70
+ node
71
+ });
72
+ }
73
+
74
+ ts.forEachChild(node, child => visit(child, calls, checker));
75
+ }
76
+
77
+
78
+ const plugin: Plugin = {
79
+ patterns: PATTERNS,
80
+
81
+ transform: (ctx: TransformContext) => {
82
+ let calls: ReactiveCallInfo[] = [];
83
+
84
+ visit(ctx.sourceFile, calls, ctx.checker);
85
+
86
+ if (calls.length === 0) {
87
+ return {};
88
+ }
89
+
90
+ let arraySlotAlias = uid('ArraySlot'),
91
+ replacements: ReplacementIntent[] = [];
92
+
93
+ ctx.shared.set(SHARED_KEY, arraySlotAlias);
94
+
95
+ for (let i = 0, n = calls.length; i < n; i++) {
96
+ let call = calls[i];
97
+
98
+ replacements.push({
99
+ generate: (sourceFile) => `new ${arraySlotAlias}(${printer.printNode(ts.EmitHint.Expression, call.arrayArg, sourceFile)}, ${printer.printNode(ts.EmitHint.Expression, call.callbackArg, sourceFile)})`,
100
+ node: call.node
101
+ });
102
+ }
103
+
104
+ return {
105
+ imports: [{
106
+ add: [`ArraySlot as ${arraySlotAlias}`],
107
+ package: PACKAGE
108
+ }],
109
+ replacements
110
+ };
111
+ }
112
+ };
113
+
114
+
115
+ export default plugin;
116
+ export { SHARED_KEY };