@esportsplus/template 0.38.2 → 0.40.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. package/README.md +5 -26
  2. package/build/compiler/codegen.d.ts +3 -2
  3. package/build/compiler/codegen.js +102 -142
  4. package/build/compiler/constants.d.ts +16 -0
  5. package/build/compiler/constants.js +19 -0
  6. package/build/compiler/index.d.ts +6 -3
  7. package/build/compiler/index.js +29 -38
  8. package/build/compiler/parser.d.ts +3 -3
  9. package/build/compiler/parser.js +5 -4
  10. package/build/compiler/plugins/tsc.d.ts +3 -2
  11. package/build/compiler/plugins/tsc.js +4 -2
  12. package/build/compiler/plugins/vite.js +4 -3
  13. package/build/compiler/{analyzer.d.ts → ts-analyzer.d.ts} +2 -2
  14. package/build/compiler/{analyzer.js → ts-analyzer.js} +16 -18
  15. package/build/compiler/ts-parser.d.ts +5 -1
  16. package/build/compiler/ts-parser.js +27 -45
  17. package/build/constants.d.ts +1 -16
  18. package/build/constants.js +1 -19
  19. package/package.json +7 -3
  20. package/src/compiler/codegen.ts +135 -217
  21. package/src/compiler/constants.ts +26 -0
  22. package/src/compiler/index.ts +33 -58
  23. package/src/compiler/parser.ts +7 -6
  24. package/src/compiler/plugins/tsc.ts +4 -2
  25. package/src/compiler/plugins/vite.ts +4 -3
  26. package/src/compiler/{analyzer.ts → ts-analyzer.ts} +17 -20
  27. package/src/compiler/ts-parser.ts +35 -67
  28. package/src/constants.ts +0 -25
  29. package/test/counter.ts +113 -0
  30. package/test/effects.ts +1 -1
  31. package/test/events.ts +1 -1
  32. package/test/imported-values.ts +1 -1
  33. package/test/integration/tsconfig.json +0 -1
  34. package/test/nested.ts +20 -1
  35. package/test/slots.ts +1 -1
  36. package/test/spread.ts +1 -1
  37. package/test/static.ts +1 -1
  38. package/test/templates.ts +1 -1
  39. package/test/vite.config.ts +2 -1
@@ -1,94 +1,69 @@
1
- import type { ImportIntent, Plugin, ReplacementIntent, TransformContext } from '@esportsplus/typescript/compiler';
2
1
  import { ts } from '@esportsplus/typescript';
3
- import { COMPILER_ENTRYPOINT, COMPILER_ENTRYPOINT_REACTIVITY, COMPILER_NAMESPACE, PACKAGE } from '~/constants';
4
- import { generateCode } from './codegen';
2
+ import { ast } from '@esportsplus/typescript/compiler';
3
+ import type { ImportIntent, ReplacementIntent, TransformContext } from '@esportsplus/typescript/compiler';
4
+ import { ENTRYPOINT, ENTRYPOINT_REACTIVITY, NAMESPACE, PACKAGE } from './constants';
5
+ import { generateCode, printer } from './codegen';
5
6
  import { findHtmlTemplates, findReactiveCalls } from './ts-parser';
6
7
 
7
8
 
8
- function isInRange(ranges: { end: number; start: number }[], start: number, end: number): boolean {
9
- for (let i = 0, n = ranges.length; i < n; i++) {
10
- let range = ranges[i];
11
-
12
- if (start >= range.start && end <= range.end) {
13
- return true;
14
- }
15
- }
16
-
17
- return false;
18
- }
19
-
20
-
21
- let printer = ts.createPrinter({ newLine: ts.NewLineKind.LineFeed });
22
-
23
-
24
- const plugin: Plugin = {
9
+ export default {
25
10
  patterns: [
26
- `${COMPILER_ENTRYPOINT}\``,
27
- `${COMPILER_ENTRYPOINT}.${COMPILER_ENTRYPOINT_REACTIVITY}`
11
+ `${ENTRYPOINT}\``,
12
+ `${ENTRYPOINT}.${ENTRYPOINT_REACTIVITY}`
28
13
  ],
29
-
30
14
  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
37
- let templates = findHtmlTemplates(ctx.sourceFile, ctx.checker);
38
-
39
- // Build ranges for all template nodes - reactive calls inside these are handled by template codegen
40
- let templateRanges: { end: number; start: number }[] = [];
15
+ let intents = {
16
+ imports: [] as ImportIntent[],
17
+ prepend: [] as string[],
18
+ replacements: [] as ReplacementIntent[]
19
+ },
20
+ ranges: { end: number; start: number }[] = [],
21
+ remove: string[] = [],
22
+ templates = findHtmlTemplates(ctx.sourceFile, ctx.checker);
41
23
 
42
24
  for (let i = 0, n = templates.length; i < n; i++) {
43
- templateRanges.push({
25
+ ranges.push({
44
26
  end: templates[i].end,
45
27
  start: templates[i].start
46
28
  });
47
29
  }
48
30
 
49
- // Transform standalone html.reactive() calls (exclude those inside templates)
50
- let reactiveCalls = findReactiveCalls(ctx.sourceFile, ctx.checker);
31
+ let calls = findReactiveCalls(ctx.sourceFile, ctx.checker);
51
32
 
52
- for (let i = 0, n = reactiveCalls.length; i < n; i++) {
53
- let call = reactiveCalls[i];
33
+ for (let i = 0, n = calls.length; i < n; i++) {
34
+ let call = calls[i];
54
35
 
55
- // Skip reactive calls that are inside template expressions - handled by template codegen
56
- if (isInRange(templateRanges, call.start, call.end)) {
36
+ if (ast.inRange(ranges, call.start, call.end)) {
57
37
  continue;
58
38
  }
59
39
 
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)})`,
40
+ intents.replacements.push({
41
+ generate: (sourceFile) => `new ${NAMESPACE}.ArraySlot(
42
+ ${printer.printNode(ts.EmitHint.Expression, call.arrayArg, sourceFile)},
43
+ ${printer.printNode(ts.EmitHint.Expression, call.callbackArg, sourceFile)}
44
+ )`,
62
45
  node: call.node
63
46
  });
64
47
  }
65
48
 
66
- // Transform html`` templates
67
49
  if (templates.length > 0) {
68
50
  let result = generateCode(templates, ctx.sourceFile, ctx.checker);
69
51
 
70
- prepend.push(...result.prepend);
71
- replacements.push(...result.replacements);
72
- removeImports.push(COMPILER_ENTRYPOINT);
52
+ intents.prepend.push(...result.prepend);
53
+ intents.replacements.push(...result.replacements);
54
+ remove.push(ENTRYPOINT);
73
55
  }
74
56
 
75
- if (replacements.length === 0 && prepend.length === 0) {
57
+ if (intents.replacements.length === 0 && intents.prepend.length === 0) {
76
58
  return {};
77
59
  }
78
60
 
79
- importsIntent.push({
80
- namespace: COMPILER_NAMESPACE,
61
+ intents.imports.push({
62
+ namespace: NAMESPACE,
81
63
  package: PACKAGE,
82
- remove: removeImports
64
+ remove: remove
83
65
  });
84
66
 
85
- return {
86
- imports: importsIntent,
87
- prepend,
88
- replacements
89
- };
67
+ return intents;
90
68
  }
91
69
  };
92
-
93
-
94
- export default plugin;
@@ -1,4 +1,5 @@
1
- import { COMPILER_TYPES, PACKAGE, SLOT_HTML } from '~/constants';
1
+ import { SLOT_HTML } from '../constants';
2
+ import { PACKAGE, TYPES } from './constants';
2
3
 
3
4
 
4
5
  type NodePath = ('firstChild' | 'firstElementChild' | 'nextElementSibling' | 'nextSibling')[];
@@ -90,8 +91,8 @@ const parse = (literals: string[]) => {
90
91
  parsed = html.split(SLOT_MARKER),
91
92
  slot = 0,
92
93
  slots: (
93
- { path: NodePath; type: COMPILER_TYPES.Node } |
94
- { attributes: typeof attributes[string]; path: NodePath; type: COMPILER_TYPES.Attribute }
94
+ { path: NodePath; type: TYPES.Node } |
95
+ { attributes: typeof attributes[string]; path: NodePath; type: TYPES.Attribute }
95
96
  )[] = [];
96
97
 
97
98
  {
@@ -154,7 +155,7 @@ const parse = (literals: string[]) => {
154
155
  }
155
156
  }
156
157
  else {
157
- names.push(COMPILER_TYPES.Attributes);
158
+ names.push(TYPES.Attributes);
158
159
  }
159
160
  }
160
161
  }
@@ -184,7 +185,7 @@ const parse = (literals: string[]) => {
184
185
  throw new Error(`${PACKAGE}: attribute metadata could not be found for '${attr}'`);
185
186
  }
186
187
 
187
- slots.push({ attributes: attrs, path, type: COMPILER_TYPES.Attribute });
188
+ slots.push({ attributes: attrs, path, type: TYPES.Attribute });
188
189
 
189
190
  for (let i = 0, n = attrs.names.length; i < n; i++) {
190
191
  buffer += parsed[slot++];
@@ -201,7 +202,7 @@ const parse = (literals: string[]) => {
201
202
  buffer += parsed[slot++] + SLOT_HTML;
202
203
  slots.push({
203
204
  path: methods(parent.children, parent.path, 'firstChild', 'nextSibling'),
204
- type: COMPILER_TYPES.Node
205
+ type: TYPES.Node
205
206
  });
206
207
  }
207
208
 
@@ -1,4 +1,6 @@
1
- import plugin from '..';
1
+ import { plugin } from '@esportsplus/typescript/compiler';
2
+ import reactivity from '@esportsplus/reactivity/compiler';
3
+ import template from '..';
2
4
 
3
5
 
4
- export default plugin;
6
+ export default plugin.tsc([reactivity, template]) as ReturnType<typeof plugin.tsc>;
@@ -1,9 +1,10 @@
1
1
  import { plugin } from '@esportsplus/typescript/compiler';
2
- import { PACKAGE } from '~/constants';
3
- import templatePlugin from '..';
2
+ import { PACKAGE } from '../constants';
3
+ import reactivity from '@esportsplus/reactivity/compiler';
4
+ import template from '..';
4
5
 
5
6
 
6
7
  export default plugin.vite({
7
8
  name: PACKAGE,
8
- plugins: [templatePlugin]
9
+ plugins: [reactivity, template]
9
10
  });
@@ -1,5 +1,5 @@
1
- import { COMPILER_ENTRYPOINT, COMPILER_ENTRYPOINT_REACTIVITY, COMPILER_TYPES } from '~/constants';
2
1
  import { ts } from '@esportsplus/typescript';
2
+ import { ENTRYPOINT, ENTRYPOINT_REACTIVITY, TYPES } from './constants';
3
3
 
4
4
 
5
5
  // Union types that mix functions with non-functions (e.g., Renderable)
@@ -19,13 +19,13 @@ function isTypeFunction(type: ts.Type, checker: ts.TypeChecker): boolean {
19
19
  }
20
20
 
21
21
 
22
- const analyze = (expr: ts.Expression, checker?: ts.TypeChecker): COMPILER_TYPES => {
22
+ const analyze = (expr: ts.Expression, checker?: ts.TypeChecker): TYPES => {
23
23
  while (ts.isParenthesizedExpression(expr)) {
24
24
  expr = expr.expression;
25
25
  }
26
26
 
27
27
  if (ts.isArrowFunction(expr) || ts.isFunctionExpression(expr)) {
28
- return COMPILER_TYPES.Effect;
28
+ return TYPES.Effect;
29
29
  }
30
30
 
31
31
  // Only html.reactive() calls become ArraySlot - handled by generateReactiveInlining
@@ -33,14 +33,14 @@ const analyze = (expr: ts.Expression, checker?: ts.TypeChecker): COMPILER_TYPES
33
33
  ts.isCallExpression(expr) &&
34
34
  ts.isPropertyAccessExpression(expr.expression) &&
35
35
  ts.isIdentifier(expr.expression.expression) &&
36
- expr.expression.expression.text === COMPILER_ENTRYPOINT &&
37
- expr.expression.name.text === COMPILER_ENTRYPOINT_REACTIVITY
36
+ expr.expression.expression.text === ENTRYPOINT &&
37
+ expr.expression.name.text === ENTRYPOINT_REACTIVITY
38
38
  ) {
39
- return COMPILER_TYPES.ArraySlot;
39
+ return TYPES.ArraySlot;
40
40
  }
41
41
 
42
- if (ts.isTaggedTemplateExpression(expr) && ts.isIdentifier(expr.tag) && expr.tag.text === COMPILER_ENTRYPOINT) {
43
- return COMPILER_TYPES.DocumentFragment;
42
+ if (ts.isTaggedTemplateExpression(expr) && ts.isIdentifier(expr.tag) && expr.tag.text === ENTRYPOINT) {
43
+ return TYPES.DocumentFragment;
44
44
  }
45
45
 
46
46
  if (
@@ -52,11 +52,11 @@ const analyze = (expr: ts.Expression, checker?: ts.TypeChecker): COMPILER_TYPES
52
52
  expr.kind === ts.SyntaxKind.NullKeyword ||
53
53
  expr.kind === ts.SyntaxKind.UndefinedKeyword
54
54
  ) {
55
- return COMPILER_TYPES.Static;
55
+ return TYPES.Static;
56
56
  }
57
57
 
58
58
  if (ts.isTemplateExpression(expr)) {
59
- return COMPILER_TYPES.Primitive;
59
+ return TYPES.Primitive;
60
60
  }
61
61
 
62
62
  if (ts.isConditionalExpression(expr)) {
@@ -67,26 +67,23 @@ const analyze = (expr: ts.Expression, checker?: ts.TypeChecker): COMPILER_TYPES
67
67
  return whenTrue;
68
68
  }
69
69
 
70
- if (whenTrue === COMPILER_TYPES.Effect || whenFalse === COMPILER_TYPES.Effect) {
71
- return COMPILER_TYPES.Effect;
70
+ if (whenTrue === TYPES.Effect || whenFalse === TYPES.Effect) {
71
+ return TYPES.Effect;
72
72
  }
73
73
 
74
- return COMPILER_TYPES.Unknown;
74
+ return TYPES.Unknown;
75
75
  }
76
76
 
77
77
  if (checker && (ts.isIdentifier(expr) || ts.isPropertyAccessExpression(expr) || ts.isCallExpression(expr))) {
78
78
  try {
79
- let type = checker.getTypeAtLocation(expr);
80
-
81
- if (isTypeFunction(type, checker)) {
82
- return COMPILER_TYPES.Effect;
79
+ if (isTypeFunction(checker.getTypeAtLocation(expr), checker)) {
80
+ return TYPES.Effect;
83
81
  }
84
82
  }
85
- catch {
86
- }
83
+ catch {}
87
84
  }
88
85
 
89
- return COMPILER_TYPES.Unknown;
86
+ return TYPES.Unknown;
90
87
  };
91
88
 
92
89
  export { analyze };
@@ -1,5 +1,6 @@
1
1
  import { ts } from '@esportsplus/typescript';
2
- import { COMPILER_ENTRYPOINT, COMPILER_ENTRYPOINT_REACTIVITY, PACKAGE } from '~/constants';
2
+ import { imports } from '@esportsplus/typescript/compiler';
3
+ import { ENTRYPOINT, ENTRYPOINT_REACTIVITY, PACKAGE } from './constants';
3
4
 
4
5
 
5
6
  type ReactiveCallInfo = {
@@ -20,55 +21,15 @@ type TemplateInfo = {
20
21
  };
21
22
 
22
23
 
23
- function isHtmlFromPackage(node: ts.Identifier, checker: ts.TypeChecker | undefined): boolean {
24
- // Fast path: check identifier name first
25
- if (node.text !== COMPILER_ENTRYPOINT) {
26
- return false;
27
- }
28
-
29
- // Without checker, fall back to string-based check
30
- if (!checker) {
31
- return true;
32
- }
33
-
34
- let symbol = checker.getSymbolAtLocation(node);
35
-
36
- if (!symbol) {
37
- return true;
38
- }
39
-
40
- // Follow aliases (re-exports) to find original symbol
41
- if (symbol.flags & ts.SymbolFlags.Alias) {
42
- symbol = checker.getAliasedSymbol(symbol);
43
- }
44
-
45
- let declarations = symbol.getDeclarations();
46
-
47
- if (!declarations || declarations.length === 0) {
48
- return true;
49
- }
50
-
51
- // Check if any declaration is from our package
52
- for (let i = 0, n = declarations.length; i < n; i++) {
53
- let filename = declarations[i].getSourceFile().fileName;
54
-
55
- // Check for package in node_modules path or direct package reference
56
- if (filename.includes(PACKAGE) || filename.includes('@esportsplus/template')) {
57
- return true;
58
- }
59
- }
60
-
61
- return false;
62
- }
63
-
64
24
  function visitReactiveCalls(node: ts.Node, calls: ReactiveCallInfo[], checker: ts.TypeChecker | undefined): void {
65
25
  if (
66
26
  ts.isCallExpression(node) &&
67
27
  ts.isPropertyAccessExpression(node.expression) &&
68
28
  ts.isIdentifier(node.expression.expression) &&
69
- node.expression.name.text === COMPILER_ENTRYPOINT_REACTIVITY &&
29
+ node.expression.name.text === ENTRYPOINT_REACTIVITY &&
70
30
  node.arguments.length === 2 &&
71
- isHtmlFromPackage(node.expression.expression, checker)
31
+ node.expression.expression.text === ENTRYPOINT &&
32
+ (!checker || imports.includes(checker, node.expression.expression, PACKAGE, ENTRYPOINT))
72
33
  ) {
73
34
  calls.push({
74
35
  arrayArg: node.arguments[0],
@@ -87,24 +48,13 @@ function visitTemplates(node: ts.Node, depth: number, templates: TemplateInfo[],
87
48
  ? depth + 1
88
49
  : depth;
89
50
 
90
- if (ts.isTaggedTemplateExpression(node) && ts.isIdentifier(node.tag) && isHtmlFromPackage(node.tag, checker)) {
91
- let expressions: ts.Expression[] = [],
92
- literals: string[] = [],
93
- template = node.template;
94
-
95
- if (ts.isNoSubstitutionTemplateLiteral(template)) {
96
- literals.push(template.text);
97
- }
98
- else if (ts.isTemplateExpression(template)) {
99
- literals.push(template.head.text);
100
-
101
- for (let i = 0, n = template.templateSpans.length; i < n; i++) {
102
- let span = template.templateSpans[i];
103
-
104
- expressions.push(span.expression);
105
- literals.push(span.literal.text);
106
- }
107
- }
51
+ if (
52
+ ts.isTaggedTemplateExpression(node) &&
53
+ ts.isIdentifier(node.tag) &&
54
+ node.tag.text === ENTRYPOINT &&
55
+ (!checker || imports.includes(checker, node.tag, PACKAGE, ENTRYPOINT))
56
+ ) {
57
+ let { expressions, literals } = extractTemplateParts(node.template);
108
58
 
109
59
  templates.push({
110
60
  depth,
@@ -120,15 +70,33 @@ function visitTemplates(node: ts.Node, depth: number, templates: TemplateInfo[],
120
70
  }
121
71
 
122
72
 
73
+ const extractTemplateParts = (template: ts.TemplateLiteral): { expressions: ts.Expression[]; literals: string[] } => {
74
+ let expressions: ts.Expression[] = [],
75
+ literals: string[] = [];
76
+
77
+ if (ts.isNoSubstitutionTemplateLiteral(template)) {
78
+ literals.push(template.text);
79
+ }
80
+ else if (ts.isTemplateExpression(template)) {
81
+ literals.push(template.head.text);
82
+
83
+ for (let i = 0, n = template.templateSpans.length; i < n; i++) {
84
+ let span = template.templateSpans[i];
85
+
86
+ expressions.push(span.expression);
87
+ literals.push(span.literal.text);
88
+ }
89
+ }
90
+
91
+ return { expressions, literals };
92
+ };
93
+
123
94
  const findHtmlTemplates = (sourceFile: ts.SourceFile, checker?: ts.TypeChecker): TemplateInfo[] => {
124
95
  let templates: TemplateInfo[] = [];
125
96
 
126
97
  visitTemplates(sourceFile, 0, templates, checker);
127
98
 
128
- // Sort by depth descending (deepest first), then by position for stable ordering
129
- templates.sort((a, b) => a.depth !== b.depth ? b.depth - a.depth : a.start - b.start);
130
-
131
- return templates;
99
+ return templates.sort((a, b) => a.depth !== b.depth ? b.depth - a.depth : a.start - b.start);
132
100
  };
133
101
 
134
102
  const findReactiveCalls = (sourceFile: ts.SourceFile, checker?: ts.TypeChecker): ReactiveCallInfo[] => {
@@ -140,5 +108,5 @@ const findReactiveCalls = (sourceFile: ts.SourceFile, checker?: ts.TypeChecker):
140
108
  };
141
109
 
142
110
 
143
- export { findHtmlTemplates, findReactiveCalls };
111
+ export { extractTemplateParts, findHtmlTemplates, findReactiveCalls };
144
112
  export type { ReactiveCallInfo, TemplateInfo };
package/src/constants.ts CHANGED
@@ -1,28 +1,7 @@
1
- import { uid } from '@esportsplus/typescript/compiler';
2
-
3
-
4
1
  const ARRAY_SLOT = Symbol('template.array.slot');
5
2
 
6
3
  const CLEANUP = Symbol('template.cleanup');
7
4
 
8
- const COMPILER_ENTRYPOINT = 'html';
9
-
10
- const COMPILER_ENTRYPOINT_REACTIVITY = 'reactive';
11
-
12
- const COMPILER_NAMESPACE = uid('template');
13
-
14
- const enum COMPILER_TYPES {
15
- ArraySlot = 'array-slot',
16
- Attributes = 'attributes',
17
- Attribute = 'attribute',
18
- DocumentFragment = 'document-fragment',
19
- Effect = 'effect',
20
- Node = 'node',
21
- Primitive = 'primitive',
22
- Static = 'static',
23
- Unknown = 'unknown'
24
- };
25
-
26
5
  const DIRECT_ATTACH_EVENTS = new Set<string>([
27
6
  'onblur',
28
7
  'onerror',
@@ -37,8 +16,6 @@ const LIFECYCLE_EVENTS = new Set<string>([
37
16
  'onconnect', 'ondisconnect', 'onrender', 'onresize', 'ontick'
38
17
  ]);
39
18
 
40
- const PACKAGE = '@esportsplus/template';
41
-
42
19
  const SLOT_HTML = '<!--$-->';
43
20
 
44
21
  const STATE_HYDRATING = 0;
@@ -53,9 +30,7 @@ const STORE = Symbol('template.store');
53
30
  export {
54
31
  ARRAY_SLOT,
55
32
  CLEANUP,
56
- COMPILER_ENTRYPOINT, COMPILER_ENTRYPOINT_REACTIVITY, COMPILER_NAMESPACE, COMPILER_TYPES,
57
33
  DIRECT_ATTACH_EVENTS,
58
34
  LIFECYCLE_EVENTS,
59
- PACKAGE,
60
35
  SLOT_HTML, STATE_HYDRATING, STATE_NONE, STATE_WAITING, STORE,
61
36
  };
@@ -0,0 +1,113 @@
1
+ import { effect, reactive } from '@esportsplus/reactivity'
2
+ import { html, type Attributes } from '@esportsplus/template';
3
+ import { omit } from '@esportsplus/utilities';
4
+
5
+
6
+ const OMIT = ['currency', 'decimals', 'delay', 'max', 'state', 'suffix', 'value'];
7
+
8
+
9
+ let formatters: Record<string, Intl.NumberFormat> = {};
10
+
11
+
12
+ export default (attributes: Attributes & {
13
+ currency?: 'IGNORE' | 'EUR' | 'GBP' | 'USD';
14
+ decimals?: number;
15
+ delay?: number;
16
+ max?: number;
17
+ state?: { value: number },
18
+ suffix?: string;
19
+ value: number;
20
+ }) => {
21
+ let { currency, decimals, delay, max, suffix, value } = attributes,
22
+ api = attributes.state || reactive({ value: -1 }),
23
+ formatter = currency === 'IGNORE'
24
+ ? undefined
25
+ : formatters[currency || 'USD'] ??= new Intl.NumberFormat('en-US', {
26
+ style: 'currency',
27
+ currency: currency || 'USD'
28
+ }),
29
+ rendering = true,
30
+ state = reactive({
31
+ length: 0,
32
+ test: () => 'sds',
33
+ render: [] as string[]
34
+ }),
35
+ render = reactive([] as string[]);
36
+
37
+ decimals ??= 2;
38
+
39
+ effect(() => {
40
+ if (api.value !== -1) {
41
+ value = api.value;
42
+ }
43
+
44
+ let padding = (max || value).toFixed(decimals).length - value.toFixed(decimals).length,
45
+ values = value.toString().padStart( value.toString().length + padding, '1') as any;
46
+
47
+ if (formatter) {
48
+ values = formatter.format(values);
49
+ }
50
+ else {
51
+ values = Number(values).toLocaleString([], {
52
+ minimumFractionDigits: 0,
53
+ maximumFractionDigits: decimals
54
+ });
55
+ }
56
+
57
+ values = values.split('');
58
+
59
+ if (suffix) {
60
+ values.push(' ', ...suffix.split(''));
61
+ }
62
+
63
+ state.length = values.length;
64
+
65
+ for (let i = 0, n = values.length; i < n; i++) {
66
+ let value = values[i];
67
+
68
+ if (!isNaN(parseInt(value, 10)) && (rendering === true || padding > 0)) {
69
+ padding--;
70
+ value = '0';
71
+ }
72
+
73
+ render[i] = value;
74
+ }
75
+
76
+ if (rendering === true) {
77
+ rendering = false;
78
+ setTimeout(() => api.value = value, delay || 1000);
79
+ }
80
+ });
81
+
82
+ return html`
83
+ <div class='counter' ${omit(attributes, OMIT)}>
84
+ ${() => {
85
+ let n = state.length;
86
+
87
+ if (n === 0) {
88
+ return '';
89
+ }
90
+
91
+ return html.reactive(render, function (value) {
92
+ if (isNaN(parseInt(value, 10))) {
93
+ return html`
94
+ <span class='counter-character counter-character--symbol'>
95
+ ${value}
96
+ </span>
97
+ `;
98
+ }
99
+
100
+ return html`
101
+ <div class=' counter-character'>
102
+ <div class='counter-character-track' style='${`--value: ${value}`}'>
103
+ <span>9</span>
104
+ ${[0, 1, 2, 3, 4, 5, 6, 7, 8, 9].map((value) => html`<span>${value}</span>`)}
105
+ <span>0</span>
106
+ </div>
107
+ </div>
108
+ `;
109
+ });
110
+ }}
111
+ </div>
112
+ `;
113
+ };
package/test/effects.ts CHANGED
@@ -2,7 +2,7 @@
2
2
  // Tests EffectSlot detection for arrow functions and reactive patterns
3
3
 
4
4
 
5
- import { html } from '../src';
5
+ import { html } from '@esportsplus/template';
6
6
 
7
7
 
8
8
  // =============================================================================
package/test/events.ts CHANGED
@@ -2,7 +2,7 @@
2
2
  // Tests delegate, direct, and lifecycle event detection
3
3
 
4
4
 
5
- import { html } from '../src';
5
+ import { html } from '@esportsplus/template';
6
6
 
7
7
 
8
8
  // =============================================================================
@@ -2,7 +2,7 @@
2
2
  // Tests that imported values are preserved correctly during compilation
3
3
 
4
4
 
5
- import { html } from '../src';
5
+ import { html } from '@esportsplus/template';
6
6
  import {
7
7
  ACTIVE_CLASS,
8
8
  APP_NAME,
@@ -7,7 +7,6 @@
7
7
  "declarationDir": null,
8
8
  "noUnusedLocals": false,
9
9
  "plugins": [
10
- { "transform": "@esportsplus/reactivity/compiler/tsc" },
11
10
  { "transform": "../../build/compiler/plugins/tsc.js" }
12
11
  ]
13
12
  },
package/test/nested.ts CHANGED
@@ -2,7 +2,7 @@
2
2
  // Tests nested html`` calls, array mapping, and conditional templates
3
3
 
4
4
 
5
- import { html } from '../src';
5
+ import { html } from '@esportsplus/template';
6
6
 
7
7
 
8
8
  // =============================================================================
@@ -273,6 +273,25 @@ export const stressList50 = (items: string[]) =>
273
273
  export const stressList100 = (items: string[]) =>
274
274
  html`<ul>${items.slice(0, 100).map(item => html`<li>${item}</li>`)}</ul>`;
275
275
 
276
+ // Block body arrow function with nested template
277
+ export const blockBodyNested = (items: { name: string; active: boolean }[]) =>
278
+ html`<ul>${items.map((item) => {
279
+ let cls = item.active ? 'active' : 'inactive';
280
+ return html`<li class="${cls}">${item.name}</li>`;
281
+ })}</ul>`;
282
+
283
+ // Block body with conditional
284
+ export const blockBodyConditional = (data: { show: boolean; items: string[] }) =>
285
+ html`<div>${() => {
286
+ if (!data.show) {
287
+ return '';
288
+ }
289
+ return html`<ul>${data.items.map((item) => {
290
+ let processed = item.toUpperCase();
291
+ return html`<li>${processed}</li>`;
292
+ })}</ul>`;
293
+ }}</div>`;
294
+
276
295
  // Complex nested structure
277
296
  export const stressComplex = (data: {
278
297
  header: { title: string; subtitle: string };
package/test/slots.ts CHANGED
@@ -2,7 +2,7 @@
2
2
  // Tests text slots, attribute slots, and mixed combinations
3
3
 
4
4
 
5
- import { html } from '../src';
5
+ import { html } from '@esportsplus/template';
6
6
 
7
7
 
8
8
  // =============================================================================