@esportsplus/template 0.38.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.
@@ -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: rewriteReactiveCall(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,60 @@ 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 discoverTemplatesInExpression(ctx, node) {
187
+ if (isNestedHtmlTemplate(node)) {
188
+ let template = node, expressions = [], literals = [], tpl = template.template;
189
+ if (ts.isNoSubstitutionTemplateLiteral(tpl)) {
190
+ literals.push(tpl.text);
191
+ }
192
+ else if (ts.isTemplateExpression(tpl)) {
193
+ literals.push(tpl.head.text);
194
+ for (let i = 0, n = tpl.templateSpans.length; i < n; i++) {
195
+ expressions.push(tpl.templateSpans[i].expression);
196
+ literals.push(tpl.templateSpans[i].literal.text);
197
+ }
198
+ }
199
+ let parsed = parser.parse(literals);
200
+ getOrCreateTemplateId(ctx, parsed.html);
201
+ for (let i = 0, n = expressions.length; i < n; i++) {
202
+ discoverTemplatesInExpression(ctx, expressions[i]);
203
+ }
204
+ }
205
+ else if (isReactiveCall(node)) {
206
+ let call = node;
207
+ if (call.arguments.length >= 2) {
208
+ discoverTemplatesInExpression(ctx, call.arguments[1]);
209
+ }
210
+ }
211
+ else {
212
+ ts.forEachChild(node, child => discoverTemplatesInExpression(ctx, child));
213
+ }
214
+ }
215
+ function discoverAllTemplates(ctx, templates) {
216
+ for (let i = 0, n = templates.length; i < n; i++) {
217
+ let parsed = parser.parse(templates[i].literals);
218
+ getOrCreateTemplateId(ctx, parsed.html);
219
+ for (let j = 0, m = templates[i].expressions.length; j < m; j++) {
220
+ discoverTemplatesInExpression(ctx, templates[i].expressions[j]);
221
+ }
222
+ }
223
+ }
224
+ const generateCode = (templates, sourceFile, checker) => {
174
225
  let result = {
175
- imports: existingAliases ?? new Map(),
176
226
  prepend: [],
177
227
  replacements: [],
178
228
  templates: new Map()
@@ -193,18 +243,16 @@ const generateCode = (templates, sourceFile, checker, existingAliases) => {
193
243
  }
194
244
  let ctx = {
195
245
  checker,
196
- imports: result.imports,
197
246
  printer,
198
247
  sourceFile,
199
248
  templates: result.templates
200
- }, templateAlias = addImport(ctx, 'template');
249
+ };
201
250
  for (let i = 0, n = rootTemplates.length; i < n; i++) {
202
251
  let template = rootTemplates[i];
203
252
  result.replacements.push({
204
253
  generate: (sf) => {
205
254
  let codeBefore = sf.getFullText().slice(0, template.node.getStart(sf)), exprTexts = [], isArrowBody = codeBefore.trimEnd().endsWith('=>'), localCtx = {
206
255
  checker,
207
- imports: ctx.imports,
208
256
  printer,
209
257
  sourceFile: sf,
210
258
  templates: ctx.templates
@@ -223,8 +271,9 @@ const generateCode = (templates, sourceFile, checker, existingAliases) => {
223
271
  node: template.node
224
272
  });
225
273
  }
274
+ discoverAllTemplates(ctx, templates);
226
275
  for (let [html, id] of ctx.templates) {
227
- result.prepend.push(`const ${id} = ${templateAlias}(\`${html}\`);`);
276
+ result.prepend.push(`const ${id} = ${COMPILER_NAMESPACE}.template(\`${html}\`);`);
228
277
  }
229
278
  return result;
230
279
  };
@@ -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,63 @@
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 = {
4
+ import { findHtmlTemplates, findReactiveCalls } from './ts-parser.js';
5
+ const PATTERNS = [
6
+ `${COMPILER_ENTRYPOINT}\``,
7
+ `${COMPILER_ENTRYPOINT}.${COMPILER_ENTRYPOINT_REACTIVITY}`
8
+ ];
9
+ function isInRange(ranges, start, end) {
10
+ for (let i = 0, n = ranges.length; i < n; i++) {
11
+ let range = ranges[i];
12
+ if (start >= range.start && end <= range.end) {
13
+ return true;
14
+ }
15
+ }
16
+ return false;
17
+ }
18
+ let printer = ts.createPrinter({ newLine: ts.NewLineKind.LineFeed });
19
+ const plugin = {
7
20
  patterns: PATTERNS,
8
21
  transform: (ctx) => {
22
+ let importsIntent = [], prepend = [], replacements = [], removeImports = [];
9
23
  let templates = findHtmlTemplates(ctx.sourceFile, ctx.checker);
10
- if (templates.length === 0) {
11
- return {};
24
+ let templateRanges = [];
25
+ for (let i = 0, n = templates.length; i < n; i++) {
26
+ templateRanges.push({
27
+ end: templates[i].end,
28
+ start: templates[i].start
29
+ });
12
30
  }
13
- let existingAliases = new Map(), existingArraySlotAlias = ctx.shared.get(SHARED_KEY);
14
- if (existingArraySlotAlias) {
15
- existingAliases.set('ArraySlot', existingArraySlotAlias);
31
+ let reactiveCalls = findReactiveCalls(ctx.sourceFile, ctx.checker);
32
+ for (let i = 0, n = reactiveCalls.length; i < n; i++) {
33
+ let call = reactiveCalls[i];
34
+ if (isInRange(templateRanges, call.start, call.end)) {
35
+ continue;
36
+ }
37
+ replacements.push({
38
+ generate: (sourceFile) => `new ${COMPILER_NAMESPACE}.ArraySlot(${printer.printNode(ts.EmitHint.Expression, call.arrayArg, sourceFile)}, ${printer.printNode(ts.EmitHint.Expression, call.callbackArg, sourceFile)})`,
39
+ node: call.node
40
+ });
16
41
  }
17
- let result = generateCode(templates, ctx.sourceFile, ctx.checker, existingAliases);
18
- if (result.replacements.length === 0) {
19
- return {};
42
+ if (templates.length > 0) {
43
+ let result = generateCode(templates, ctx.sourceFile, ctx.checker);
44
+ prepend.push(...result.prepend);
45
+ replacements.push(...result.replacements);
46
+ removeImports.push(COMPILER_ENTRYPOINT);
20
47
  }
21
- let aliasedImports = [];
22
- for (let [name, alias] of result.imports) {
23
- aliasedImports.push(`${name} as ${alias}`);
48
+ if (replacements.length === 0 && prepend.length === 0) {
49
+ return {};
24
50
  }
25
- let imports = [{
26
- add: aliasedImports,
27
- package: PACKAGE,
28
- remove: [COMPILER_ENTRYPOINT]
29
- }];
51
+ importsIntent.push({
52
+ namespace: COMPILER_NAMESPACE,
53
+ package: PACKAGE,
54
+ remove: removeImports
55
+ });
30
56
  return {
31
- imports,
32
- prepend: result.prepend,
33
- replacements: result.replacements
57
+ imports: importsIntent,
58
+ prepend,
59
+ replacements
34
60
  };
35
61
  }
36
62
  };
37
- export default templatePlugin;
38
- export { reactiveInliningPlugin, templatePlugin };
63
+ 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.1",
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,43 @@ function collectNestedTemplateReplacements(
78
65
  start: node.getStart(ctx.sourceFile) - exprStart
79
66
  });
80
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
+ }
81
75
  else {
82
- ts.forEachChild(node, child => collectNestedTemplateReplacements(ctx, child, exprStart, replacements));
76
+ ts.forEachChild(node, child => collectNestedReplacements(ctx, child, exprStart, replacements));
83
77
  }
84
78
  }
85
79
 
86
- 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 {
87
81
  if (name.startsWith('on') && name.length > 2) {
88
82
  let event = name.slice(2).toLowerCase(),
89
83
  key = name.toLowerCase();
90
84
 
91
85
  if (LIFECYCLE_EVENTS.has(key)) {
92
- return `${addImport(ctx, key)}(${element}, ${expr});`;
86
+ return `${COMPILER_NAMESPACE}.${key}(${element}, ${expr});`;
93
87
  }
94
88
 
95
89
  if (DIRECT_ATTACH_EVENTS.has(key)) {
96
- return `${addImport(ctx, 'on')}(${element}, '${event}', ${expr});`;
90
+ return `${COMPILER_NAMESPACE}.on(${element}, '${event}', ${expr});`;
97
91
  }
98
92
 
99
- return `${addImport(ctx, 'delegate')}(${element}, '${event}', ${expr});`;
93
+ return `${COMPILER_NAMESPACE}.delegate(${element}, '${event}', ${expr});`;
100
94
  }
101
95
 
102
96
  if (name === 'class') {
103
- return `${addImport(ctx, 'setClass')}(${element}, '${staticValue}', ${expr});`;
97
+ return `${COMPILER_NAMESPACE}.setClass(${element}, '${staticValue}', ${expr});`;
104
98
  }
105
99
 
106
100
  if (name === 'style') {
107
- return `${addImport(ctx, 'setStyle')}(${element}, '${staticValue}', ${expr});`;
101
+ return `${COMPILER_NAMESPACE}.setStyle(${element}, '${staticValue}', ${expr});`;
108
102
  }
109
103
 
110
- return `${addImport(ctx, 'setProperty')}(${element}, '${name}', ${expr});`;
104
+ return `${COMPILER_NAMESPACE}.setProperty(${element}, '${name}', ${expr});`;
111
105
  }
112
106
 
113
107
  function generateNestedTemplateCode(ctx: CodegenContext, node: ts.TaggedTemplateExpression): string {
@@ -142,30 +136,30 @@ function generateNestedTemplateCode(ctx: CodegenContext, node: ts.TaggedTemplate
142
136
 
143
137
  function generateNodeBinding(ctx: CodegenContext, anchor: string, exprText: string, exprNode: ts.Expression | undefined): string {
144
138
  if (!exprNode) {
145
- return `${addImport(ctx, 'slot')}(${anchor}, ${exprText});`;
139
+ return `${COMPILER_NAMESPACE}.slot(${anchor}, ${exprText});`;
146
140
  }
147
141
 
148
142
  if (isNestedHtmlTemplate(exprNode)) {
149
- return `${anchor}.parentNode.insertBefore(${generateNestedTemplateCode(ctx, exprNode)}, ${anchor});`;
143
+ return `${anchor}.parentNode!.insertBefore(${generateNestedTemplateCode(ctx, exprNode)}, ${anchor});`;
150
144
  }
151
145
 
152
146
  let slotType = analyze(exprNode, ctx.checker);
153
147
 
154
148
  switch (slotType) {
155
- case COMPILER_TYPES.Effect:
156
- return `new ${addImport(ctx, 'EffectSlot')}(${anchor}, ${exprText});`;
157
-
158
149
  case COMPILER_TYPES.ArraySlot:
159
- 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});`;
160
157
 
161
158
  case COMPILER_TYPES.Static:
162
159
  return `${anchor}.textContent = ${exprText};`;
163
160
 
164
- case COMPILER_TYPES.DocumentFragment:
165
- return `${anchor}.parentNode.insertBefore(${exprText}, ${anchor});`;
166
-
167
161
  default:
168
- return `${addImport(ctx, 'slot')}(${anchor}, ${exprText});`;
162
+ return `${COMPILER_NAMESPACE}.slot(${anchor}, ${exprText});`;
169
163
  }
170
164
  }
171
165
 
@@ -215,16 +209,15 @@ function generateTemplateCode(
215
209
  }
216
210
  }
217
211
 
218
- let alias = addImport(ctx, 'Element'),
219
- name = uid('element'),
212
+ let name = uid('element'),
220
213
  segments = path.slice(start),
221
214
  value = `${ancestor}.${segments.join('!.')}`;
222
215
 
223
216
  if (ancestor === root && segments[0] === 'firstChild') {
224
- value = value.replace(`${ancestor}.firstChild!`, `(${ancestor}.firstChild! as ${alias})`);
217
+ value = value.replace(`${ancestor}.firstChild!`, `(${ancestor}.firstChild! as ${COMPILER_NAMESPACE}.Element)`);
225
218
  }
226
219
 
227
- declarations.push(`${name} = ${value} as ${alias}`);
220
+ declarations.push(`${name} = ${value} as ${COMPILER_NAMESPACE}.Element`);
228
221
  nodes.set(key, name);
229
222
  }
230
223
 
@@ -247,14 +240,13 @@ function generateTemplateCode(
247
240
 
248
241
  if (name === COMPILER_TYPES.Attributes) {
249
242
  code.push(
250
- `${addImport(ctx, 'setProperties')}(${element}, ${exprTexts[index] || 'undefined'});`
243
+ `${COMPILER_NAMESPACE}.setProperties(${element}, ${exprTexts[index] || 'undefined'});`
251
244
  );
252
245
  index++;
253
246
  }
254
247
  else {
255
248
  code.push(
256
249
  generateAttributeBinding(
257
- ctx,
258
250
  element,
259
251
  name,
260
252
  exprTexts[index++] || 'undefined',
@@ -293,6 +285,16 @@ function isNestedHtmlTemplate(expr: ts.Expression): expr is ts.TaggedTemplateExp
293
285
  return ts.isTaggedTemplateExpression(expr) && ts.isIdentifier(expr.tag) && expr.tag.text === COMPILER_ENTRYPOINT;
294
286
  }
295
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
+
296
298
  function replaceReverse(text: string, replacements: Replacement[]): string {
297
299
  let sorted = replacements.slice().sort((a, b) => b.start - a.start);
298
300
 
@@ -310,21 +312,85 @@ function rewriteExpression(ctx: CodegenContext, expr: ts.Expression): string {
310
312
  return generateNestedTemplateCode(ctx, expr);
311
313
  }
312
314
 
313
- 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))) {
314
320
  return ctx.printer.printNode(ts.EmitHint.Expression, expr, ctx.sourceFile);
315
321
  }
316
322
 
317
323
  let replacements: Replacement[] = [];
318
324
 
319
- collectNestedTemplateReplacements(ctx, expr, expr.getStart(ctx.sourceFile), replacements);
325
+ collectNestedReplacements(ctx, expr, expr.getStart(ctx.sourceFile), replacements);
320
326
 
321
327
  return replaceReverse(expr.getText(ctx.sourceFile), replacements);
322
328
  }
323
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);
335
+
336
+ return `${arrayText}, ${callbackText}`;
337
+ }
338
+
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;
324
346
 
325
- const generateCode = (templates: TemplateInfo[], sourceFile: ts.SourceFile, checker?: ts.TypeChecker, existingAliases?: Map<string, string>): CodegenResult => {
347
+ if (ts.isNoSubstitutionTemplateLiteral(tpl)) {
348
+ literals.push(tpl.text);
349
+ }
350
+ else if (ts.isTemplateExpression(tpl)) {
351
+ literals.push(tpl.head.text);
352
+
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
+ }
357
+ }
358
+
359
+ let parsed = parser.parse(literals) as ParseResult;
360
+
361
+ getOrCreateTemplateId(ctx, parsed.html);
362
+
363
+ for (let i = 0, n = expressions.length; i < n; i++) {
364
+ discoverTemplatesInExpression(ctx, expressions[i]);
365
+ }
366
+ }
367
+ else if (isReactiveCall(node as ts.Expression)) {
368
+ let call = node as ts.CallExpression;
369
+
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
+ }
378
+
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;
382
+
383
+ getOrCreateTemplateId(ctx, parsed.html);
384
+
385
+ for (let j = 0, m = templates[i].expressions.length; j < m; j++) {
386
+ discoverTemplatesInExpression(ctx, templates[i].expressions[j]);
387
+ }
388
+ }
389
+ }
390
+
391
+
392
+ const generateCode = (templates: TemplateInfo[], sourceFile: ts.SourceFile, checker?: ts.TypeChecker): CodegenResult => {
326
393
  let result: CodegenResult = {
327
- imports: existingAliases ?? new Map(),
328
394
  prepend: [],
329
395
  replacements: [],
330
396
  templates: new Map()
@@ -351,13 +417,11 @@ const generateCode = (templates: TemplateInfo[], sourceFile: ts.SourceFile, chec
351
417
  }
352
418
 
353
419
  let ctx: CodegenContext = {
354
- checker,
355
- imports: result.imports,
356
- printer,
357
- sourceFile,
358
- templates: result.templates
359
- },
360
- templateAlias = addImport(ctx, 'template');
420
+ checker,
421
+ printer,
422
+ sourceFile,
423
+ templates: result.templates
424
+ };
361
425
 
362
426
  for (let i = 0, n = rootTemplates.length; i < n; i++) {
363
427
  let template = rootTemplates[i];
@@ -369,7 +433,6 @@ const generateCode = (templates: TemplateInfo[], sourceFile: ts.SourceFile, chec
369
433
  isArrowBody = codeBefore.trimEnd().endsWith('=>'),
370
434
  localCtx: CodegenContext = {
371
435
  checker,
372
- imports: ctx.imports,
373
436
  printer,
374
437
  sourceFile: sf,
375
438
  templates: ctx.templates
@@ -400,8 +463,11 @@ const generateCode = (templates: TemplateInfo[], sourceFile: ts.SourceFile, chec
400
463
  });
401
464
  }
402
465
 
466
+ // Eager discovery: find all templates before prepend generation
467
+ discoverAllTemplates(ctx, templates);
468
+
403
469
  for (let [html, id] of ctx.templates) {
404
- result.prepend.push(`const ${id} = ${templateAlias}(\`${html}\`);`);
470
+ result.prepend.push(`const ${id} = ${COMPILER_NAMESPACE}.template(\`${html}\`);`);
405
471
  }
406
472
 
407
473
  return result;
@@ -1,56 +1,97 @@
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
+ const PATTERNS = [
9
+ `${COMPILER_ENTRYPOINT}\``,
10
+ `${COMPILER_ENTRYPOINT}.${COMPILER_ENTRYPOINT_REACTIVITY}`
11
+ ];
9
12
 
10
13
 
11
- const templatePlugin: Plugin = {
14
+ function isInRange(ranges: { end: number; start: number }[], start: number, end: number): boolean {
15
+ for (let i = 0, n = ranges.length; i < n; i++) {
16
+ let range = ranges[i];
17
+
18
+ if (start >= range.start && end <= range.end) {
19
+ return true;
20
+ }
21
+ }
22
+
23
+ return false;
24
+ }
25
+
26
+
27
+ let printer = ts.createPrinter({ newLine: ts.NewLineKind.LineFeed });
28
+
29
+
30
+ const plugin: Plugin = {
12
31
  patterns: PATTERNS,
13
32
 
14
33
  transform: (ctx: TransformContext) => {
34
+ let importsIntent: ImportIntent[] = [],
35
+ prepend: string[] = [],
36
+ replacements: ReplacementIntent[] = [],
37
+ removeImports: string[] = [];
38
+
39
+ // Find templates first to build exclusion ranges
15
40
  let templates = findHtmlTemplates(ctx.sourceFile, ctx.checker);
16
41
 
17
- if (templates.length === 0) {
18
- return {};
42
+ // Build ranges for all template nodes - reactive calls inside these are handled by template codegen
43
+ let templateRanges: { end: number; start: number }[] = [];
44
+
45
+ for (let i = 0, n = templates.length; i < n; i++) {
46
+ templateRanges.push({
47
+ end: templates[i].end,
48
+ start: templates[i].start
49
+ });
19
50
  }
20
51
 
21
- let existingAliases = new Map<string, string>(),
22
- existingArraySlotAlias = ctx.shared.get(SHARED_KEY) as string | undefined;
52
+ // Transform standalone html.reactive() calls (exclude those inside templates)
53
+ let reactiveCalls = findReactiveCalls(ctx.sourceFile, ctx.checker);
23
54
 
24
- if (existingArraySlotAlias) {
25
- existingAliases.set('ArraySlot', existingArraySlotAlias);
26
- }
55
+ for (let i = 0, n = reactiveCalls.length; i < n; i++) {
56
+ let call = reactiveCalls[i];
27
57
 
28
- let result = generateCode(templates, ctx.sourceFile, ctx.checker, existingAliases);
58
+ // Skip reactive calls that are inside template expressions - handled by template codegen
59
+ if (isInRange(templateRanges, call.start, call.end)) {
60
+ continue;
61
+ }
29
62
 
30
- if (result.replacements.length === 0) {
31
- return {};
63
+ replacements.push({
64
+ generate: (sourceFile) => `new ${COMPILER_NAMESPACE}.ArraySlot(${printer.printNode(ts.EmitHint.Expression, call.arrayArg, sourceFile)}, ${printer.printNode(ts.EmitHint.Expression, call.callbackArg, sourceFile)})`,
65
+ node: call.node
66
+ });
32
67
  }
33
68
 
34
- let aliasedImports: string[] = [];
69
+ // Transform html`` templates
70
+ if (templates.length > 0) {
71
+ let result = generateCode(templates, ctx.sourceFile, ctx.checker);
72
+
73
+ prepend.push(...result.prepend);
74
+ replacements.push(...result.replacements);
75
+ removeImports.push(COMPILER_ENTRYPOINT);
76
+ }
35
77
 
36
- for (let [name, alias] of result.imports) {
37
- aliasedImports.push(`${name} as ${alias}`);
78
+ if (replacements.length === 0 && prepend.length === 0) {
79
+ return {};
38
80
  }
39
81
 
40
- let imports: ImportIntent[] = [{
41
- add: aliasedImports,
82
+ importsIntent.push({
83
+ namespace: COMPILER_NAMESPACE,
42
84
  package: PACKAGE,
43
- remove: [COMPILER_ENTRYPOINT]
44
- }];
85
+ remove: removeImports
86
+ });
45
87
 
46
88
  return {
47
- imports,
48
- prepend: result.prepend,
49
- replacements: result.replacements
89
+ imports: importsIntent,
90
+ prepend,
91
+ replacements
50
92
  };
51
93
  }
52
94
  };
53
95
 
54
96
 
55
- export default templatePlugin;
56
- export { reactiveInliningPlugin, templatePlugin };
97
+ 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 };