@esportsplus/template 0.38.0 → 0.38.2

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.
@@ -2,11 +2,10 @@ import type { ReplacementIntent } from '@esportsplus/typescript/compiler';
2
2
  import { ts } from '@esportsplus/typescript';
3
3
  import type { TemplateInfo } from './ts-parser.js';
4
4
  type CodegenResult = {
5
- imports: Map<string, string>;
6
5
  prepend: string[];
7
6
  replacements: ReplacementIntent[];
8
7
  templates: Map<string, string>;
9
8
  };
10
- declare const generateCode: (templates: TemplateInfo[], sourceFile: ts.SourceFile, checker?: ts.TypeChecker, existingAliases?: Map<string, string>) => CodegenResult;
9
+ declare const generateCode: (templates: TemplateInfo[], sourceFile: ts.SourceFile, checker?: ts.TypeChecker) => CodegenResult;
11
10
  export { generateCode };
12
11
  export type { CodegenResult };
@@ -1,19 +1,11 @@
1
1
  import { ts } from '@esportsplus/typescript';
2
2
  import { ast, uid } from '@esportsplus/typescript/compiler';
3
- import { COMPILER_ENTRYPOINT, COMPILER_TYPES, DIRECT_ATTACH_EVENTS, LIFECYCLE_EVENTS } from '../constants.js';
3
+ import { COMPILER_ENTRYPOINT, COMPILER_ENTRYPOINT_REACTIVITY, COMPILER_NAMESPACE, COMPILER_TYPES, DIRECT_ATTACH_EVENTS, LIFECYCLE_EVENTS } from '../constants.js';
4
4
  import { analyze } from './analyzer.js';
5
5
  import parser from './parser.js';
6
6
  const ARROW_EMPTY_PARAMS = /\(\s*\)\s*=>\s*$/;
7
7
  let printer = ts.createPrinter({ newLine: ts.NewLineKind.LineFeed });
8
- function addImport(ctx, name) {
9
- let alias = ctx.imports.get(name);
10
- if (!alias) {
11
- alias = uid(name);
12
- ctx.imports.set(name, alias);
13
- }
14
- return alias;
15
- }
16
- function collectNestedTemplateReplacements(ctx, node, exprStart, replacements) {
8
+ function collectNestedReplacements(ctx, node, exprStart, replacements) {
17
9
  if (isNestedHtmlTemplate(node)) {
18
10
  replacements.push({
19
11
  end: node.end - exprStart,
@@ -21,28 +13,35 @@ function collectNestedTemplateReplacements(ctx, node, exprStart, replacements) {
21
13
  start: node.getStart(ctx.sourceFile) - exprStart
22
14
  });
23
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
+ }
24
23
  else {
25
- ts.forEachChild(node, child => collectNestedTemplateReplacements(ctx, child, exprStart, replacements));
24
+ ts.forEachChild(node, child => collectNestedReplacements(ctx, child, exprStart, replacements));
26
25
  }
27
26
  }
28
- function generateAttributeBinding(ctx, element, name, expr, staticValue) {
27
+ function generateAttributeBinding(element, name, expr, staticValue) {
29
28
  if (name.startsWith('on') && name.length > 2) {
30
29
  let event = name.slice(2).toLowerCase(), key = name.toLowerCase();
31
30
  if (LIFECYCLE_EVENTS.has(key)) {
32
- return `${addImport(ctx, key)}(${element}, ${expr});`;
31
+ return `${COMPILER_NAMESPACE}.${key}(${element}, ${expr});`;
33
32
  }
34
33
  if (DIRECT_ATTACH_EVENTS.has(key)) {
35
- return `${addImport(ctx, 'on')}(${element}, '${event}', ${expr});`;
34
+ return `${COMPILER_NAMESPACE}.on(${element}, '${event}', ${expr});`;
36
35
  }
37
- return `${addImport(ctx, 'delegate')}(${element}, '${event}', ${expr});`;
36
+ return `${COMPILER_NAMESPACE}.delegate(${element}, '${event}', ${expr});`;
38
37
  }
39
38
  if (name === 'class') {
40
- return `${addImport(ctx, 'setClass')}(${element}, '${staticValue}', ${expr});`;
39
+ return `${COMPILER_NAMESPACE}.setClass(${element}, '${staticValue}', ${expr});`;
41
40
  }
42
41
  if (name === 'style') {
43
- return `${addImport(ctx, 'setStyle')}(${element}, '${staticValue}', ${expr});`;
42
+ return `${COMPILER_NAMESPACE}.setStyle(${element}, '${staticValue}', ${expr});`;
44
43
  }
45
- return `${addImport(ctx, 'setProperty')}(${element}, '${name}', ${expr});`;
44
+ return `${COMPILER_NAMESPACE}.setProperty(${element}, '${name}', ${expr});`;
46
45
  }
47
46
  function generateNestedTemplateCode(ctx, node) {
48
47
  let expressions = [], exprTexts = [], literals = [], template = node.template;
@@ -62,23 +61,23 @@ function generateNestedTemplateCode(ctx, node) {
62
61
  }
63
62
  function generateNodeBinding(ctx, anchor, exprText, exprNode) {
64
63
  if (!exprNode) {
65
- return `${addImport(ctx, 'slot')}(${anchor}, ${exprText});`;
64
+ return `${COMPILER_NAMESPACE}.slot(${anchor}, ${exprText});`;
66
65
  }
67
66
  if (isNestedHtmlTemplate(exprNode)) {
68
- return `${anchor}.parentNode.insertBefore(${generateNestedTemplateCode(ctx, exprNode)}, ${anchor});`;
67
+ return `${anchor}.parentNode!.insertBefore(${generateNestedTemplateCode(ctx, exprNode)}, ${anchor});`;
69
68
  }
70
69
  let slotType = analyze(exprNode, ctx.checker);
71
70
  switch (slotType) {
72
- case COMPILER_TYPES.Effect:
73
- return `new ${addImport(ctx, 'EffectSlot')}(${anchor}, ${exprText});`;
74
71
  case COMPILER_TYPES.ArraySlot:
75
- return `new ${addImport(ctx, 'ArraySlot')}(${anchor}, ${exprText});`;
72
+ return `${anchor}.parentNode!.insertBefore(new ${COMPILER_NAMESPACE}.ArraySlot(${exprText}).fragment, ${anchor});`;
73
+ case COMPILER_TYPES.DocumentFragment:
74
+ return `${anchor}.parentNode!.insertBefore(${exprText}, ${anchor});`;
75
+ case COMPILER_TYPES.Effect:
76
+ return `new ${COMPILER_NAMESPACE}.EffectSlot(${anchor}, ${exprText});`;
76
77
  case COMPILER_TYPES.Static:
77
78
  return `${anchor}.textContent = ${exprText};`;
78
- case COMPILER_TYPES.DocumentFragment:
79
- return `${anchor}.parentNode.insertBefore(${exprText}, ${anchor});`;
80
79
  default:
81
- return `${addImport(ctx, 'slot')}(${anchor}, ${exprText});`;
80
+ return `${COMPILER_NAMESPACE}.slot(${anchor}, ${exprText});`;
82
81
  }
83
82
  }
84
83
  function generateTemplateCode(ctx, { html, slots }, exprTexts, exprNodes, isArrowBody) {
@@ -106,11 +105,11 @@ function generateTemplateCode(ctx, { html, slots }, exprTexts, exprNodes, isArro
106
105
  break;
107
106
  }
108
107
  }
109
- let alias = addImport(ctx, 'Element'), name = uid('element'), segments = path.slice(start), value = `${ancestor}.${segments.join('!.')}`;
108
+ let name = uid('element'), segments = path.slice(start), value = `${ancestor}.${segments.join('!.')}`;
110
109
  if (ancestor === root && segments[0] === 'firstChild') {
111
- value = value.replace(`${ancestor}.firstChild!`, `(${ancestor}.firstChild! as ${alias})`);
110
+ value = value.replace(`${ancestor}.firstChild!`, `(${ancestor}.firstChild! as ${COMPILER_NAMESPACE}.Element)`);
112
111
  }
113
- declarations.push(`${name} = ${value} as ${alias}`);
112
+ declarations.push(`${name} = ${value} as ${COMPILER_NAMESPACE}.Element`);
114
113
  nodes.set(key, name);
115
114
  }
116
115
  code.push(isArrowBody ? '{' : `(() => {`, `let ${declarations.join(',\n')};`);
@@ -123,11 +122,11 @@ function generateTemplateCode(ctx, { html, slots }, exprTexts, exprNodes, isArro
123
122
  for (let j = 0, m = names.length; j < m; j++) {
124
123
  let name = names[j];
125
124
  if (name === COMPILER_TYPES.Attributes) {
126
- code.push(`${addImport(ctx, 'setProperties')}(${element}, ${exprTexts[index] || 'undefined'});`);
125
+ code.push(`${COMPILER_NAMESPACE}.setProperties(${element}, ${exprTexts[index] || 'undefined'});`);
127
126
  index++;
128
127
  }
129
128
  else {
130
- code.push(generateAttributeBinding(ctx, element, name, exprTexts[index++] || 'undefined', slot.attributes.statics[name] || ''));
129
+ code.push(generateAttributeBinding(element, name, exprTexts[index++] || 'undefined', slot.attributes.statics[name] || ''));
131
130
  }
132
131
  }
133
132
  }
@@ -151,6 +150,13 @@ function getOrCreateTemplateId(ctx, html) {
151
150
  function isNestedHtmlTemplate(expr) {
152
151
  return ts.isTaggedTemplateExpression(expr) && ts.isIdentifier(expr.tag) && expr.tag.text === COMPILER_ENTRYPOINT;
153
152
  }
153
+ function isReactiveCall(expr) {
154
+ return (ts.isCallExpression(expr) &&
155
+ ts.isPropertyAccessExpression(expr.expression) &&
156
+ ts.isIdentifier(expr.expression.expression) &&
157
+ expr.expression.expression.text === COMPILER_ENTRYPOINT &&
158
+ expr.expression.name.text === COMPILER_ENTRYPOINT_REACTIVITY);
159
+ }
154
160
  function replaceReverse(text, replacements) {
155
161
  let sorted = replacements.slice().sort((a, b) => b.start - a.start);
156
162
  for (let i = 0, n = sorted.length; i < n; i++) {
@@ -163,16 +169,64 @@ function rewriteExpression(ctx, expr) {
163
169
  if (isNestedHtmlTemplate(expr)) {
164
170
  return generateNestedTemplateCode(ctx, expr);
165
171
  }
166
- if (!ast.hasMatch(expr, n => isNestedHtmlTemplate(n))) {
172
+ if (isReactiveCall(expr)) {
173
+ return rewriteReactiveCall(ctx, expr);
174
+ }
175
+ if (!ast.hasMatch(expr, n => isNestedHtmlTemplate(n) || isReactiveCall(n))) {
167
176
  return ctx.printer.printNode(ts.EmitHint.Expression, expr, ctx.sourceFile);
168
177
  }
169
178
  let replacements = [];
170
- collectNestedTemplateReplacements(ctx, expr, expr.getStart(ctx.sourceFile), replacements);
179
+ collectNestedReplacements(ctx, expr, expr.getStart(ctx.sourceFile), replacements);
171
180
  return replaceReverse(expr.getText(ctx.sourceFile), replacements);
172
181
  }
173
- const generateCode = (templates, sourceFile, checker, existingAliases) => {
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}`;
185
+ }
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) {
191
+ 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
+ }
202
+ }
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]);
207
+ }
208
+ }
209
+ else if (isReactiveCall(node)) {
210
+ let call = node;
211
+ if (call.arguments.length >= 2) {
212
+ discoverTemplatesInExpression(ctx, call.arguments[1]);
213
+ }
214
+ }
215
+ else {
216
+ ts.forEachChild(node, child => discoverTemplatesInExpression(ctx, child));
217
+ }
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]);
225
+ }
226
+ }
227
+ }
228
+ const generateCode = (templates, sourceFile, checker) => {
174
229
  let result = {
175
- imports: existingAliases ?? new Map(),
176
230
  prepend: [],
177
231
  replacements: [],
178
232
  templates: new Map()
@@ -193,18 +247,16 @@ const generateCode = (templates, sourceFile, checker, existingAliases) => {
193
247
  }
194
248
  let ctx = {
195
249
  checker,
196
- imports: result.imports,
197
250
  printer,
198
251
  sourceFile,
199
252
  templates: result.templates
200
- }, templateAlias = addImport(ctx, 'template');
253
+ };
201
254
  for (let i = 0, n = rootTemplates.length; i < n; i++) {
202
255
  let template = rootTemplates[i];
203
256
  result.replacements.push({
204
257
  generate: (sf) => {
205
258
  let codeBefore = sf.getFullText().slice(0, template.node.getStart(sf)), exprTexts = [], isArrowBody = codeBefore.trimEnd().endsWith('=>'), localCtx = {
206
259
  checker,
207
- imports: ctx.imports,
208
260
  printer,
209
261
  sourceFile: sf,
210
262
  templates: ctx.templates
@@ -223,8 +275,9 @@ const generateCode = (templates, sourceFile, checker, existingAliases) => {
223
275
  node: template.node
224
276
  });
225
277
  }
278
+ discoverAllTemplates(ctx, templates);
226
279
  for (let [html, id] of ctx.templates) {
227
- result.prepend.push(`const ${id} = ${templateAlias}(\`${html}\`);`);
280
+ result.prepend.push(`const ${id} = ${COMPILER_NAMESPACE}.template(\`${html}\`);`);
228
281
  }
229
282
  return result;
230
283
  };
@@ -1,5 +1,3 @@
1
1
  import type { Plugin } from '@esportsplus/typescript/compiler';
2
- import reactiveInliningPlugin from './reactive-inlining.js';
3
- declare const templatePlugin: Plugin;
4
- export default templatePlugin;
5
- export { reactiveInliningPlugin, templatePlugin };
2
+ declare const plugin: Plugin;
3
+ export default plugin;
@@ -1,38 +1,62 @@
1
- import { COMPILER_ENTRYPOINT, PACKAGE } from '../constants.js';
1
+ import { ts } from '@esportsplus/typescript';
2
+ import { COMPILER_ENTRYPOINT, COMPILER_ENTRYPOINT_REACTIVITY, COMPILER_NAMESPACE, PACKAGE } from '../constants.js';
2
3
  import { generateCode } from './codegen.js';
3
- import reactiveInliningPlugin, { SHARED_KEY } from './reactive-inlining.js';
4
- import { findHtmlTemplates } from './ts-parser.js';
5
- const PATTERNS = [`${COMPILER_ENTRYPOINT}\``];
6
- const templatePlugin = {
7
- patterns: PATTERNS,
4
+ 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
+ const plugin = {
16
+ patterns: [
17
+ `${COMPILER_ENTRYPOINT}\``,
18
+ `${COMPILER_ENTRYPOINT}.${COMPILER_ENTRYPOINT_REACTIVITY}`
19
+ ],
8
20
  transform: (ctx) => {
21
+ let importsIntent = [], prepend = [], replacements = [], removeImports = [];
9
22
  let templates = findHtmlTemplates(ctx.sourceFile, ctx.checker);
10
- if (templates.length === 0) {
11
- return {};
23
+ let templateRanges = [];
24
+ for (let i = 0, n = templates.length; i < n; i++) {
25
+ templateRanges.push({
26
+ end: templates[i].end,
27
+ start: templates[i].start
28
+ });
12
29
  }
13
- let existingAliases = new Map(), existingArraySlotAlias = ctx.shared.get(SHARED_KEY);
14
- if (existingArraySlotAlias) {
15
- existingAliases.set('ArraySlot', existingArraySlotAlias);
30
+ let reactiveCalls = findReactiveCalls(ctx.sourceFile, ctx.checker);
31
+ for (let i = 0, n = reactiveCalls.length; i < n; i++) {
32
+ let call = reactiveCalls[i];
33
+ if (isInRange(templateRanges, call.start, call.end)) {
34
+ continue;
35
+ }
36
+ 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)})`,
38
+ node: call.node
39
+ });
16
40
  }
17
- let result = generateCode(templates, ctx.sourceFile, ctx.checker, existingAliases);
18
- if (result.replacements.length === 0) {
19
- return {};
41
+ if (templates.length > 0) {
42
+ let result = generateCode(templates, ctx.sourceFile, ctx.checker);
43
+ prepend.push(...result.prepend);
44
+ replacements.push(...result.replacements);
45
+ removeImports.push(COMPILER_ENTRYPOINT);
20
46
  }
21
- let aliasedImports = [];
22
- for (let [name, alias] of result.imports) {
23
- aliasedImports.push(`${name} as ${alias}`);
47
+ if (replacements.length === 0 && prepend.length === 0) {
48
+ return {};
24
49
  }
25
- let imports = [{
26
- add: aliasedImports,
27
- package: PACKAGE,
28
- remove: [COMPILER_ENTRYPOINT]
29
- }];
50
+ importsIntent.push({
51
+ namespace: COMPILER_NAMESPACE,
52
+ package: PACKAGE,
53
+ remove: removeImports
54
+ });
30
55
  return {
31
- imports,
32
- prepend: result.prepend,
33
- replacements: result.replacements
56
+ imports: importsIntent,
57
+ prepend,
58
+ replacements
34
59
  };
35
60
  }
36
61
  };
37
- export default templatePlugin;
38
- export { reactiveInliningPlugin, templatePlugin };
62
+ export default plugin;
@@ -1,3 +1,2 @@
1
- import { plugin } from '@esportsplus/typescript/compiler';
2
- declare const _default: ReturnType<typeof plugin.tsc>;
3
- export default _default;
1
+ import plugin from '../index.js';
2
+ export default plugin;
@@ -1,3 +1,2 @@
1
- import { plugin } from '@esportsplus/typescript/compiler';
2
- import { reactiveInliningPlugin, templatePlugin } from '../index.js';
3
- export default plugin.tsc([reactiveInliningPlugin, templatePlugin]);
1
+ import plugin from '../index.js';
2
+ export default plugin;
@@ -1,7 +1,7 @@
1
1
  import { plugin } from '@esportsplus/typescript/compiler';
2
2
  import { PACKAGE } from '../../constants.js';
3
- import { reactiveInliningPlugin, templatePlugin } from '../index.js';
3
+ import templatePlugin from '../index.js';
4
4
  export default plugin.vite({
5
5
  name: PACKAGE,
6
- plugins: [reactiveInliningPlugin, templatePlugin]
6
+ plugins: [templatePlugin]
7
7
  });
@@ -2,6 +2,7 @@ declare const ARRAY_SLOT: unique symbol;
2
2
  declare const CLEANUP: unique symbol;
3
3
  declare const COMPILER_ENTRYPOINT = "html";
4
4
  declare const COMPILER_ENTRYPOINT_REACTIVITY = "reactive";
5
+ declare const COMPILER_NAMESPACE: string;
5
6
  declare const enum COMPILER_TYPES {
6
7
  ArraySlot = "array-slot",
7
8
  Attributes = "attributes",
@@ -21,4 +22,4 @@ declare const STATE_HYDRATING = 0;
21
22
  declare const STATE_NONE = 1;
22
23
  declare const STATE_WAITING = 2;
23
24
  declare const STORE: unique symbol;
24
- export { ARRAY_SLOT, CLEANUP, COMPILER_ENTRYPOINT, COMPILER_ENTRYPOINT_REACTIVITY, COMPILER_TYPES, DIRECT_ATTACH_EVENTS, LIFECYCLE_EVENTS, PACKAGE, SLOT_HTML, STATE_HYDRATING, STATE_NONE, STATE_WAITING, STORE, };
25
+ export { ARRAY_SLOT, CLEANUP, COMPILER_ENTRYPOINT, COMPILER_ENTRYPOINT_REACTIVITY, COMPILER_NAMESPACE, COMPILER_TYPES, DIRECT_ATTACH_EVENTS, LIFECYCLE_EVENTS, PACKAGE, SLOT_HTML, STATE_HYDRATING, STATE_NONE, STATE_WAITING, STORE, };
@@ -1,7 +1,9 @@
1
+ import { uid } from '@esportsplus/typescript/compiler';
1
2
  const ARRAY_SLOT = Symbol('template.array.slot');
2
3
  const CLEANUP = Symbol('template.cleanup');
3
4
  const COMPILER_ENTRYPOINT = 'html';
4
5
  const COMPILER_ENTRYPOINT_REACTIVITY = 'reactive';
6
+ const COMPILER_NAMESPACE = uid('template');
5
7
  var COMPILER_TYPES;
6
8
  (function (COMPILER_TYPES) {
7
9
  COMPILER_TYPES["ArraySlot"] = "array-slot";
@@ -33,4 +35,4 @@ const STATE_HYDRATING = 0;
33
35
  const STATE_NONE = 1;
34
36
  const STATE_WAITING = 2;
35
37
  const STORE = Symbol('template.store');
36
- export { ARRAY_SLOT, CLEANUP, COMPILER_ENTRYPOINT, COMPILER_ENTRYPOINT_REACTIVITY, COMPILER_TYPES, DIRECT_ATTACH_EVENTS, LIFECYCLE_EVENTS, PACKAGE, SLOT_HTML, STATE_HYDRATING, STATE_NONE, STATE_WAITING, STORE, };
38
+ export { ARRAY_SLOT, CLEANUP, COMPILER_ENTRYPOINT, COMPILER_ENTRYPOINT_REACTIVITY, COMPILER_NAMESPACE, COMPILER_TYPES, DIRECT_ATTACH_EVENTS, LIFECYCLE_EVENTS, PACKAGE, SLOT_HTML, STATE_HYDRATING, STATE_NONE, STATE_WAITING, STORE, };
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "author": "ICJR",
3
3
  "dependencies": {
4
4
  "@esportsplus/queue": "^0.2.0",
5
- "@esportsplus/reactivity": "^0.28.0",
5
+ "@esportsplus/reactivity": "^0.28.1",
6
6
  "@esportsplus/typescript": "^0.25.0",
7
7
  "@esportsplus/utilities": "^0.27.2",
8
8
  "serve": "^14.2.5"
@@ -35,7 +35,7 @@
35
35
  },
36
36
  "type": "module",
37
37
  "types": "./build/index.d.ts",
38
- "version": "0.38.0",
38
+ "version": "0.38.2",
39
39
  "scripts": {
40
40
  "build": "tsc",
41
41
  "build:test": "vite build --config test/vite.config.ts",
@@ -1,7 +1,7 @@
1
1
  import type { ReplacementIntent } from '@esportsplus/typescript/compiler';
2
2
  import { ts } from '@esportsplus/typescript';
3
3
  import { ast, uid } from '@esportsplus/typescript/compiler';
4
- import { COMPILER_ENTRYPOINT, COMPILER_TYPES, DIRECT_ATTACH_EVENTS, LIFECYCLE_EVENTS } from '~/constants';
4
+ import { COMPILER_ENTRYPOINT, COMPILER_ENTRYPOINT_REACTIVITY, COMPILER_NAMESPACE, COMPILER_TYPES, DIRECT_ATTACH_EVENTS, LIFECYCLE_EVENTS } from '~/constants';
5
5
  import type { TemplateInfo } from './ts-parser';
6
6
  import { analyze } from './analyzer';
7
7
  import parser from './parser';
@@ -18,14 +18,12 @@ type Attribute = {
18
18
 
19
19
  type CodegenContext = {
20
20
  checker?: ts.TypeChecker;
21
- imports: Map<string, string>;
22
21
  printer: ts.Printer;
23
22
  sourceFile: ts.SourceFile;
24
23
  templates: Map<string, string>;
25
24
  };
26
25
 
27
26
  type CodegenResult = {
28
- imports: Map<string, string>;
29
27
  prepend: string[];
30
28
  replacements: ReplacementIntent[];
31
29
  templates: Map<string, string>;
@@ -54,18 +52,7 @@ const ARROW_EMPTY_PARAMS = /\(\s*\)\s*=>\s*$/;
54
52
  let printer = ts.createPrinter({ newLine: ts.NewLineKind.LineFeed });
55
53
 
56
54
 
57
- function addImport(ctx: CodegenContext, name: string): string {
58
- let alias = ctx.imports.get(name);
59
-
60
- if (!alias) {
61
- alias = uid(name);
62
- ctx.imports.set(name, alias);
63
- }
64
-
65
- return alias;
66
- }
67
-
68
- function collectNestedTemplateReplacements(
55
+ function collectNestedReplacements(
69
56
  ctx: CodegenContext,
70
57
  node: ts.Node,
71
58
  exprStart: number,
@@ -78,36 +65,44 @@ function collectNestedTemplateReplacements(
78
65
  start: node.getStart(ctx.sourceFile) - exprStart
79
66
  });
80
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
+ }
81
76
  else {
82
- ts.forEachChild(node, child => collectNestedTemplateReplacements(ctx, child, exprStart, replacements));
77
+ ts.forEachChild(node, child => collectNestedReplacements(ctx, child, exprStart, replacements));
83
78
  }
84
79
  }
85
80
 
86
- function generateAttributeBinding(ctx: CodegenContext, element: string, name: string, expr: string, staticValue: string): string {
81
+ function generateAttributeBinding(element: string, name: string, expr: string, staticValue: string): string {
87
82
  if (name.startsWith('on') && name.length > 2) {
88
83
  let event = name.slice(2).toLowerCase(),
89
84
  key = name.toLowerCase();
90
85
 
91
86
  if (LIFECYCLE_EVENTS.has(key)) {
92
- return `${addImport(ctx, key)}(${element}, ${expr});`;
87
+ return `${COMPILER_NAMESPACE}.${key}(${element}, ${expr});`;
93
88
  }
94
89
 
95
90
  if (DIRECT_ATTACH_EVENTS.has(key)) {
96
- return `${addImport(ctx, 'on')}(${element}, '${event}', ${expr});`;
91
+ return `${COMPILER_NAMESPACE}.on(${element}, '${event}', ${expr});`;
97
92
  }
98
93
 
99
- return `${addImport(ctx, 'delegate')}(${element}, '${event}', ${expr});`;
94
+ return `${COMPILER_NAMESPACE}.delegate(${element}, '${event}', ${expr});`;
100
95
  }
101
96
 
102
97
  if (name === 'class') {
103
- return `${addImport(ctx, 'setClass')}(${element}, '${staticValue}', ${expr});`;
98
+ return `${COMPILER_NAMESPACE}.setClass(${element}, '${staticValue}', ${expr});`;
104
99
  }
105
100
 
106
101
  if (name === 'style') {
107
- return `${addImport(ctx, 'setStyle')}(${element}, '${staticValue}', ${expr});`;
102
+ return `${COMPILER_NAMESPACE}.setStyle(${element}, '${staticValue}', ${expr});`;
108
103
  }
109
104
 
110
- return `${addImport(ctx, 'setProperty')}(${element}, '${name}', ${expr});`;
105
+ return `${COMPILER_NAMESPACE}.setProperty(${element}, '${name}', ${expr});`;
111
106
  }
112
107
 
113
108
  function generateNestedTemplateCode(ctx: CodegenContext, node: ts.TaggedTemplateExpression): string {
@@ -142,30 +137,30 @@ function generateNestedTemplateCode(ctx: CodegenContext, node: ts.TaggedTemplate
142
137
 
143
138
  function generateNodeBinding(ctx: CodegenContext, anchor: string, exprText: string, exprNode: ts.Expression | undefined): string {
144
139
  if (!exprNode) {
145
- return `${addImport(ctx, 'slot')}(${anchor}, ${exprText});`;
140
+ return `${COMPILER_NAMESPACE}.slot(${anchor}, ${exprText});`;
146
141
  }
147
142
 
148
143
  if (isNestedHtmlTemplate(exprNode)) {
149
- return `${anchor}.parentNode.insertBefore(${generateNestedTemplateCode(ctx, exprNode)}, ${anchor});`;
144
+ return `${anchor}.parentNode!.insertBefore(${generateNestedTemplateCode(ctx, exprNode)}, ${anchor});`;
150
145
  }
151
146
 
152
147
  let slotType = analyze(exprNode, ctx.checker);
153
148
 
154
149
  switch (slotType) {
155
- case COMPILER_TYPES.Effect:
156
- return `new ${addImport(ctx, 'EffectSlot')}(${anchor}, ${exprText});`;
157
-
158
150
  case COMPILER_TYPES.ArraySlot:
159
- return `new ${addImport(ctx, 'ArraySlot')}(${anchor}, ${exprText});`;
151
+ return `${anchor}.parentNode!.insertBefore(new ${COMPILER_NAMESPACE}.ArraySlot(${exprText}).fragment, ${anchor});`;
152
+
153
+ case COMPILER_TYPES.DocumentFragment:
154
+ return `${anchor}.parentNode!.insertBefore(${exprText}, ${anchor});`;
155
+
156
+ case COMPILER_TYPES.Effect:
157
+ return `new ${COMPILER_NAMESPACE}.EffectSlot(${anchor}, ${exprText});`;
160
158
 
161
159
  case COMPILER_TYPES.Static:
162
160
  return `${anchor}.textContent = ${exprText};`;
163
161
 
164
- case COMPILER_TYPES.DocumentFragment:
165
- return `${anchor}.parentNode.insertBefore(${exprText}, ${anchor});`;
166
-
167
162
  default:
168
- return `${addImport(ctx, 'slot')}(${anchor}, ${exprText});`;
163
+ return `${COMPILER_NAMESPACE}.slot(${anchor}, ${exprText});`;
169
164
  }
170
165
  }
171
166
 
@@ -215,16 +210,15 @@ function generateTemplateCode(
215
210
  }
216
211
  }
217
212
 
218
- let alias = addImport(ctx, 'Element'),
219
- name = uid('element'),
213
+ let name = uid('element'),
220
214
  segments = path.slice(start),
221
215
  value = `${ancestor}.${segments.join('!.')}`;
222
216
 
223
217
  if (ancestor === root && segments[0] === 'firstChild') {
224
- value = value.replace(`${ancestor}.firstChild!`, `(${ancestor}.firstChild! as ${alias})`);
218
+ value = value.replace(`${ancestor}.firstChild!`, `(${ancestor}.firstChild! as ${COMPILER_NAMESPACE}.Element)`);
225
219
  }
226
220
 
227
- declarations.push(`${name} = ${value} as ${alias}`);
221
+ declarations.push(`${name} = ${value} as ${COMPILER_NAMESPACE}.Element`);
228
222
  nodes.set(key, name);
229
223
  }
230
224
 
@@ -247,14 +241,13 @@ function generateTemplateCode(
247
241
 
248
242
  if (name === COMPILER_TYPES.Attributes) {
249
243
  code.push(
250
- `${addImport(ctx, 'setProperties')}(${element}, ${exprTexts[index] || 'undefined'});`
244
+ `${COMPILER_NAMESPACE}.setProperties(${element}, ${exprTexts[index] || 'undefined'});`
251
245
  );
252
246
  index++;
253
247
  }
254
248
  else {
255
249
  code.push(
256
250
  generateAttributeBinding(
257
- ctx,
258
251
  element,
259
252
  name,
260
253
  exprTexts[index++] || 'undefined',
@@ -293,6 +286,16 @@ function isNestedHtmlTemplate(expr: ts.Expression): expr is ts.TaggedTemplateExp
293
286
  return ts.isTaggedTemplateExpression(expr) && ts.isIdentifier(expr.tag) && expr.tag.text === COMPILER_ENTRYPOINT;
294
287
  }
295
288
 
289
+ function isReactiveCall(expr: ts.Expression): expr is ts.CallExpression {
290
+ return (
291
+ ts.isCallExpression(expr) &&
292
+ ts.isPropertyAccessExpression(expr.expression) &&
293
+ ts.isIdentifier(expr.expression.expression) &&
294
+ expr.expression.expression.text === COMPILER_ENTRYPOINT &&
295
+ expr.expression.name.text === COMPILER_ENTRYPOINT_REACTIVITY
296
+ );
297
+ }
298
+
296
299
  function replaceReverse(text: string, replacements: Replacement[]): string {
297
300
  let sorted = replacements.slice().sort((a, b) => b.start - a.start);
298
301
 
@@ -310,21 +313,96 @@ function rewriteExpression(ctx: CodegenContext, expr: ts.Expression): string {
310
313
  return generateNestedTemplateCode(ctx, expr);
311
314
  }
312
315
 
313
- if (!ast.hasMatch(expr, n => isNestedHtmlTemplate(n as ts.Expression))) {
316
+ if (isReactiveCall(expr)) {
317
+ return rewriteReactiveCall(ctx, expr);
318
+ }
319
+
320
+ if (!ast.hasMatch(expr, n => isNestedHtmlTemplate(n as ts.Expression) || isReactiveCall(n as ts.Expression))) {
314
321
  return ctx.printer.printNode(ts.EmitHint.Expression, expr, ctx.sourceFile);
315
322
  }
316
323
 
317
324
  let replacements: Replacement[] = [];
318
325
 
319
- collectNestedTemplateReplacements(ctx, expr, expr.getStart(ctx.sourceFile), replacements);
326
+ collectNestedReplacements(ctx, expr, expr.getStart(ctx.sourceFile), replacements);
320
327
 
321
328
  return replaceReverse(expr.getText(ctx.sourceFile), replacements);
322
329
  }
323
330
 
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);
337
+
338
+ return `${arrayText}, ${callbackText}`;
339
+ }
340
+
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);
347
+
348
+ return `new ${COMPILER_NAMESPACE}.ArraySlot(${arrayText}, ${callbackText})`;
349
+ }
350
+
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;
358
+
359
+ if (ts.isNoSubstitutionTemplateLiteral(tpl)) {
360
+ literals.push(tpl.text);
361
+ }
362
+ else if (ts.isTemplateExpression(tpl)) {
363
+ literals.push(tpl.head.text);
364
+
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
+ }
370
+
371
+ let parsed = parser.parse(literals) as ParseResult;
372
+
373
+ getOrCreateTemplateId(ctx, parsed.html);
374
+
375
+ for (let i = 0, n = expressions.length; i < n; i++) {
376
+ discoverTemplatesInExpression(ctx, expressions[i]);
377
+ }
378
+ }
379
+ else if (isReactiveCall(node as ts.Expression)) {
380
+ let call = node as ts.CallExpression;
381
+
382
+ if (call.arguments.length >= 2) {
383
+ discoverTemplatesInExpression(ctx, call.arguments[1]);
384
+ }
385
+ }
386
+ else {
387
+ ts.forEachChild(node, child => discoverTemplatesInExpression(ctx, child));
388
+ }
389
+ }
390
+
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;
394
+
395
+ getOrCreateTemplateId(ctx, parsed.html);
396
+
397
+ for (let j = 0, m = templates[i].expressions.length; j < m; j++) {
398
+ discoverTemplatesInExpression(ctx, templates[i].expressions[j]);
399
+ }
400
+ }
401
+ }
402
+
324
403
 
325
- const generateCode = (templates: TemplateInfo[], sourceFile: ts.SourceFile, checker?: ts.TypeChecker, existingAliases?: Map<string, string>): CodegenResult => {
404
+ const generateCode = (templates: TemplateInfo[], sourceFile: ts.SourceFile, checker?: ts.TypeChecker): CodegenResult => {
326
405
  let result: CodegenResult = {
327
- imports: existingAliases ?? new Map(),
328
406
  prepend: [],
329
407
  replacements: [],
330
408
  templates: new Map()
@@ -351,13 +429,11 @@ const generateCode = (templates: TemplateInfo[], sourceFile: ts.SourceFile, chec
351
429
  }
352
430
 
353
431
  let ctx: CodegenContext = {
354
- checker,
355
- imports: result.imports,
356
- printer,
357
- sourceFile,
358
- templates: result.templates
359
- },
360
- templateAlias = addImport(ctx, 'template');
432
+ checker,
433
+ printer,
434
+ sourceFile,
435
+ templates: result.templates
436
+ };
361
437
 
362
438
  for (let i = 0, n = rootTemplates.length; i < n; i++) {
363
439
  let template = rootTemplates[i];
@@ -369,7 +445,6 @@ const generateCode = (templates: TemplateInfo[], sourceFile: ts.SourceFile, chec
369
445
  isArrowBody = codeBefore.trimEnd().endsWith('=>'),
370
446
  localCtx: CodegenContext = {
371
447
  checker,
372
- imports: ctx.imports,
373
448
  printer,
374
449
  sourceFile: sf,
375
450
  templates: ctx.templates
@@ -400,8 +475,11 @@ const generateCode = (templates: TemplateInfo[], sourceFile: ts.SourceFile, chec
400
475
  });
401
476
  }
402
477
 
478
+ // Eager discovery: find all templates before prepend generation
479
+ discoverAllTemplates(ctx, templates);
480
+
403
481
  for (let [html, id] of ctx.templates) {
404
- result.prepend.push(`const ${id} = ${templateAlias}(\`${html}\`);`);
482
+ result.prepend.push(`const ${id} = ${COMPILER_NAMESPACE}.template(\`${html}\`);`);
405
483
  }
406
484
 
407
485
  return result;
@@ -1,56 +1,94 @@
1
- import type { ImportIntent, Plugin, TransformContext } from '@esportsplus/typescript/compiler';
2
- import { COMPILER_ENTRYPOINT, PACKAGE } from '~/constants';
1
+ import type { ImportIntent, Plugin, ReplacementIntent, TransformContext } from '@esportsplus/typescript/compiler';
2
+ import { ts } from '@esportsplus/typescript';
3
+ import { COMPILER_ENTRYPOINT, COMPILER_ENTRYPOINT_REACTIVITY, COMPILER_NAMESPACE, PACKAGE } from '~/constants';
3
4
  import { generateCode } from './codegen';
4
- import reactiveInliningPlugin, { SHARED_KEY } from './reactive-inlining';
5
- import { findHtmlTemplates } from './ts-parser';
5
+ import { findHtmlTemplates, findReactiveCalls } from './ts-parser';
6
6
 
7
7
 
8
- const PATTERNS = [`${COMPILER_ENTRYPOINT}\``];
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
+ }
9
19
 
10
20
 
11
- const templatePlugin: Plugin = {
12
- patterns: PATTERNS,
21
+ let printer = ts.createPrinter({ newLine: ts.NewLineKind.LineFeed });
22
+
23
+
24
+ const plugin: Plugin = {
25
+ patterns: [
26
+ `${COMPILER_ENTRYPOINT}\``,
27
+ `${COMPILER_ENTRYPOINT}.${COMPILER_ENTRYPOINT_REACTIVITY}`
28
+ ],
13
29
 
14
30
  transform: (ctx: TransformContext) => {
31
+ let importsIntent: ImportIntent[] = [],
32
+ prepend: string[] = [],
33
+ replacements: ReplacementIntent[] = [],
34
+ removeImports: string[] = [];
35
+
36
+ // Find templates first to build exclusion ranges
15
37
  let templates = findHtmlTemplates(ctx.sourceFile, ctx.checker);
16
38
 
17
- if (templates.length === 0) {
18
- return {};
39
+ // Build ranges for all template nodes - reactive calls inside these are handled by template codegen
40
+ let templateRanges: { end: number; start: number }[] = [];
41
+
42
+ for (let i = 0, n = templates.length; i < n; i++) {
43
+ templateRanges.push({
44
+ end: templates[i].end,
45
+ start: templates[i].start
46
+ });
19
47
  }
20
48
 
21
- let existingAliases = new Map<string, string>(),
22
- existingArraySlotAlias = ctx.shared.get(SHARED_KEY) as string | undefined;
49
+ // Transform standalone html.reactive() calls (exclude those inside templates)
50
+ let reactiveCalls = findReactiveCalls(ctx.sourceFile, ctx.checker);
23
51
 
24
- if (existingArraySlotAlias) {
25
- existingAliases.set('ArraySlot', existingArraySlotAlias);
26
- }
52
+ for (let i = 0, n = reactiveCalls.length; i < n; i++) {
53
+ let call = reactiveCalls[i];
27
54
 
28
- let result = generateCode(templates, ctx.sourceFile, ctx.checker, existingAliases);
55
+ // Skip reactive calls that are inside template expressions - handled by template codegen
56
+ if (isInRange(templateRanges, call.start, call.end)) {
57
+ continue;
58
+ }
29
59
 
30
- if (result.replacements.length === 0) {
31
- return {};
60
+ 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)})`,
62
+ node: call.node
63
+ });
32
64
  }
33
65
 
34
- let aliasedImports: string[] = [];
66
+ // Transform html`` templates
67
+ if (templates.length > 0) {
68
+ let result = generateCode(templates, ctx.sourceFile, ctx.checker);
35
69
 
36
- for (let [name, alias] of result.imports) {
37
- aliasedImports.push(`${name} as ${alias}`);
70
+ prepend.push(...result.prepend);
71
+ replacements.push(...result.replacements);
72
+ removeImports.push(COMPILER_ENTRYPOINT);
73
+ }
74
+
75
+ if (replacements.length === 0 && prepend.length === 0) {
76
+ return {};
38
77
  }
39
78
 
40
- let imports: ImportIntent[] = [{
41
- add: aliasedImports,
79
+ importsIntent.push({
80
+ namespace: COMPILER_NAMESPACE,
42
81
  package: PACKAGE,
43
- remove: [COMPILER_ENTRYPOINT]
44
- }];
82
+ remove: removeImports
83
+ });
45
84
 
46
85
  return {
47
- imports,
48
- prepend: result.prepend,
49
- replacements: result.replacements
86
+ imports: importsIntent,
87
+ prepend,
88
+ replacements
50
89
  };
51
90
  }
52
91
  };
53
92
 
54
93
 
55
- export default templatePlugin;
56
- export { reactiveInliningPlugin, templatePlugin };
94
+ export default plugin;
@@ -1,5 +1,4 @@
1
- import { plugin } from '@esportsplus/typescript/compiler';
2
- import { reactiveInliningPlugin, templatePlugin } from '..';
1
+ import plugin from '..';
3
2
 
4
3
 
5
- export default plugin.tsc([reactiveInliningPlugin, templatePlugin]) as ReturnType<typeof plugin.tsc>;
4
+ export default plugin;
@@ -1,9 +1,9 @@
1
1
  import { plugin } from '@esportsplus/typescript/compiler';
2
- import { PACKAGE } from '../../constants';
3
- import { reactiveInliningPlugin, templatePlugin } from '..';
2
+ import { PACKAGE } from '~/constants';
3
+ import templatePlugin from '..';
4
4
 
5
5
 
6
6
  export default plugin.vite({
7
7
  name: PACKAGE,
8
- plugins: [reactiveInliningPlugin, templatePlugin]
8
+ plugins: [templatePlugin]
9
9
  });
package/src/constants.ts CHANGED
@@ -1,3 +1,6 @@
1
+ import { uid } from '@esportsplus/typescript/compiler';
2
+
3
+
1
4
  const ARRAY_SLOT = Symbol('template.array.slot');
2
5
 
3
6
  const CLEANUP = Symbol('template.cleanup');
@@ -6,6 +9,8 @@ const COMPILER_ENTRYPOINT = 'html';
6
9
 
7
10
  const COMPILER_ENTRYPOINT_REACTIVITY = 'reactive';
8
11
 
12
+ const COMPILER_NAMESPACE = uid('template');
13
+
9
14
  const enum COMPILER_TYPES {
10
15
  ArraySlot = 'array-slot',
11
16
  Attributes = 'attributes',
@@ -48,7 +53,7 @@ const STORE = Symbol('template.store');
48
53
  export {
49
54
  ARRAY_SLOT,
50
55
  CLEANUP,
51
- COMPILER_ENTRYPOINT, COMPILER_ENTRYPOINT_REACTIVITY, COMPILER_TYPES,
56
+ COMPILER_ENTRYPOINT, COMPILER_ENTRYPOINT_REACTIVITY, COMPILER_NAMESPACE, COMPILER_TYPES,
52
57
  DIRECT_ATTACH_EVENTS,
53
58
  LIFECYCLE_EVENTS,
54
59
  PACKAGE,
@@ -0,0 +1,90 @@
1
+ /**
2
+ * Integration test: Combined reactivity + template plugins
3
+ *
4
+ * This file tests that both plugins work together correctly:
5
+ * 1. reactive() from @esportsplus/reactivity - creates reactive objects/arrays
6
+ * 2. html`` from @esportsplus/template - compiles templates
7
+ * 3. html.reactive() from @esportsplus/template - renders reactive arrays
8
+ */
9
+
10
+ import { reactive } from '@esportsplus/reactivity';
11
+ import { html } from '@esportsplus/template';
12
+
13
+
14
+ // Test 1: Basic reactive object with template
15
+ const state = reactive({
16
+ count: 0,
17
+ name: 'World'
18
+ });
19
+
20
+ export const basicTemplate = () => html`
21
+ <div class="counter">
22
+ <span>Hello ${() => state.name}!</span>
23
+ <span>Count: ${() => state.count}</span>
24
+ </div>
25
+ `;
26
+
27
+
28
+ // Test 2: Reactive array with html.reactive()
29
+ const items = reactive([
30
+ { id: 1, text: 'First' },
31
+ { id: 2, text: 'Second' },
32
+ { id: 3, text: 'Third' }
33
+ ]);
34
+
35
+ export const reactiveList = () => html`
36
+ <ul class="list">
37
+ ${html.reactive(items, (item) => html`
38
+ <li data-id="${item.id}">${item.text}</li>
39
+ `)}
40
+ </ul>
41
+ `;
42
+
43
+
44
+ // Test 3: Combined - reactive object + reactive array in same template
45
+ type Todo = { id: number; done: boolean; text: string };
46
+
47
+ const todos = reactive<Todo[]>([
48
+ { id: 1, done: false, text: 'Learn TypeScript' },
49
+ { id: 2, done: true, text: 'Build compiler' }
50
+ ]);
51
+
52
+ const app = reactive({
53
+ title: 'Todo App'
54
+ });
55
+
56
+ export const combinedTemplate = () => html`
57
+ <div class="app">
58
+ <h1>${() => app.title}</h1>
59
+ <ul>
60
+ ${html.reactive(todos, (todo) => html`
61
+ <li class="${() => todo.done ? 'done' : ''}">
62
+ <input type="checkbox" checked="${() => todo.done}" />
63
+ <span>${todo.text}</span>
64
+ </li>
65
+ `)}
66
+ </ul>
67
+ </div>
68
+ `;
69
+
70
+
71
+ // Test 4: Static template (no reactive expressions)
72
+ export const staticTemplate = () => html`
73
+ <footer>
74
+ <p>Static content - no transformations needed</p>
75
+ </footer>
76
+ `;
77
+
78
+
79
+ // Test 5: Nested templates with effects
80
+ export const nestedTemplate = () => html`
81
+ <div class="wrapper">
82
+ ${html`<header>Header</header>`}
83
+ <main>
84
+ ${() => state.count > 5
85
+ ? html`<span>High count!</span>`
86
+ : html`<span>Low count</span>`
87
+ }
88
+ </main>
89
+ </div>
90
+ `;
@@ -0,0 +1,17 @@
1
+ {
2
+ "extends": "@esportsplus/typescript/tsconfig.package.json",
3
+ "compilerOptions": {
4
+ "rootDir": ".",
5
+ "outDir": "./build",
6
+ "declaration": false,
7
+ "declarationDir": null,
8
+ "noUnusedLocals": false,
9
+ "plugins": [
10
+ { "transform": "@esportsplus/reactivity/compiler/tsc" },
11
+ { "transform": "../../build/compiler/plugins/tsc.js" }
12
+ ]
13
+ },
14
+ "include": [
15
+ "./**/*.ts"
16
+ ]
17
+ }
@@ -1,5 +0,0 @@
1
- import type { Plugin } from '@esportsplus/typescript/compiler';
2
- declare const SHARED_KEY = "template:arraySlotAlias";
3
- declare const plugin: Plugin;
4
- export default plugin;
5
- export { SHARED_KEY };
@@ -1,75 +0,0 @@
1
- import { ts } from '@esportsplus/typescript';
2
- import { uid } from '@esportsplus/typescript/compiler';
3
- import { COMPILER_ENTRYPOINT, COMPILER_ENTRYPOINT_REACTIVITY, PACKAGE } from '../constants.js';
4
- const PATTERNS = [`${COMPILER_ENTRYPOINT}.${COMPILER_ENTRYPOINT_REACTIVITY}`];
5
- const SHARED_KEY = 'template:arraySlotAlias';
6
- let printer = ts.createPrinter({ newLine: ts.NewLineKind.LineFeed });
7
- function isHtmlFromPackage(node, checker) {
8
- if (node.text !== COMPILER_ENTRYPOINT) {
9
- return false;
10
- }
11
- if (!checker) {
12
- return true;
13
- }
14
- let symbol = checker.getSymbolAtLocation(node);
15
- if (!symbol) {
16
- return true;
17
- }
18
- if (symbol.flags & ts.SymbolFlags.Alias) {
19
- symbol = checker.getAliasedSymbol(symbol);
20
- }
21
- let declarations = symbol.getDeclarations();
22
- if (!declarations || declarations.length === 0) {
23
- return true;
24
- }
25
- for (let i = 0, n = declarations.length; i < n; i++) {
26
- let filename = declarations[i].getSourceFile().fileName;
27
- if (filename.includes(PACKAGE) || filename.includes('@esportsplus/template')) {
28
- return true;
29
- }
30
- }
31
- return false;
32
- }
33
- function visit(node, calls, checker) {
34
- if (ts.isCallExpression(node) &&
35
- ts.isPropertyAccessExpression(node.expression) &&
36
- ts.isIdentifier(node.expression.expression) &&
37
- node.expression.name.text === COMPILER_ENTRYPOINT_REACTIVITY &&
38
- node.arguments.length === 2 &&
39
- isHtmlFromPackage(node.expression.expression, checker)) {
40
- calls.push({
41
- arrayArg: node.arguments[0],
42
- callbackArg: node.arguments[1],
43
- node
44
- });
45
- }
46
- ts.forEachChild(node, child => visit(child, calls, checker));
47
- }
48
- const plugin = {
49
- patterns: PATTERNS,
50
- transform: (ctx) => {
51
- let calls = [];
52
- visit(ctx.sourceFile, calls, ctx.checker);
53
- if (calls.length === 0) {
54
- return {};
55
- }
56
- let arraySlotAlias = uid('ArraySlot'), replacements = [];
57
- ctx.shared.set(SHARED_KEY, arraySlotAlias);
58
- for (let i = 0, n = calls.length; i < n; i++) {
59
- let call = calls[i];
60
- replacements.push({
61
- generate: (sourceFile) => `new ${arraySlotAlias}(${printer.printNode(ts.EmitHint.Expression, call.arrayArg, sourceFile)}, ${printer.printNode(ts.EmitHint.Expression, call.callbackArg, sourceFile)})`,
62
- node: call.node
63
- });
64
- }
65
- return {
66
- imports: [{
67
- add: [`ArraySlot as ${arraySlotAlias}`],
68
- package: PACKAGE
69
- }],
70
- replacements
71
- };
72
- }
73
- };
74
- export default plugin;
75
- export { SHARED_KEY };
@@ -1,116 +0,0 @@
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 };