@esportsplus/template 0.28.3 → 0.29.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.
Files changed (109) hide show
  1. package/build/attributes.d.ts +7 -1
  2. package/build/attributes.js +86 -33
  3. package/build/constants.d.ts +3 -11
  4. package/build/constants.js +4 -32
  5. package/build/event/constants.d.ts +3 -0
  6. package/build/event/constants.js +13 -0
  7. package/build/event/index.d.ts +9 -1
  8. package/build/event/index.js +29 -35
  9. package/build/event/ontick.js +6 -9
  10. package/build/html.d.ts +9 -0
  11. package/build/html.js +7 -0
  12. package/build/index.d.ts +8 -2
  13. package/build/index.js +8 -1
  14. package/build/render.d.ts +2 -2
  15. package/build/render.js +2 -3
  16. package/build/runtime.d.ts +1 -0
  17. package/build/runtime.js +5 -0
  18. package/build/slot/array.d.ts +3 -3
  19. package/build/slot/array.js +11 -14
  20. package/build/slot/cleanup.d.ts +1 -1
  21. package/build/slot/cleanup.js +1 -2
  22. package/build/slot/effect.js +5 -7
  23. package/build/slot/index.js +1 -7
  24. package/build/slot/render.js +6 -8
  25. package/build/svg.d.ts +1 -1
  26. package/build/svg.js +1 -1
  27. package/build/transformer/codegen.d.ts +18 -0
  28. package/build/transformer/codegen.js +316 -0
  29. package/build/transformer/index.d.ts +12 -0
  30. package/build/transformer/index.js +62 -0
  31. package/build/transformer/parser.d.ts +18 -0
  32. package/build/transformer/parser.js +166 -0
  33. package/build/transformer/plugins/esbuild.d.ts +5 -0
  34. package/build/transformer/plugins/esbuild.js +35 -0
  35. package/build/transformer/plugins/tsc.d.ts +3 -0
  36. package/build/transformer/plugins/tsc.js +4 -0
  37. package/build/transformer/plugins/vite.d.ts +5 -0
  38. package/build/transformer/plugins/vite.js +37 -0
  39. package/build/transformer/ts-parser.d.ts +21 -0
  40. package/build/transformer/ts-parser.js +72 -0
  41. package/build/transformer/type-analyzer.d.ts +7 -0
  42. package/build/transformer/type-analyzer.js +230 -0
  43. package/build/types.d.ts +2 -3
  44. package/build/utilities.d.ts +7 -0
  45. package/build/utilities.js +31 -0
  46. package/package.json +11 -4
  47. package/src/attributes.ts +115 -51
  48. package/src/constants.ts +6 -53
  49. package/src/event/constants.ts +16 -0
  50. package/src/event/index.ts +36 -42
  51. package/src/event/onconnect.ts +1 -1
  52. package/src/event/onresize.ts +1 -1
  53. package/src/event/ontick.ts +7 -11
  54. package/src/html.ts +18 -0
  55. package/src/index.ts +8 -2
  56. package/src/render.ts +6 -7
  57. package/src/runtime.ts +8 -0
  58. package/src/slot/array.ts +18 -24
  59. package/src/slot/cleanup.ts +3 -4
  60. package/src/slot/effect.ts +6 -8
  61. package/src/slot/index.ts +2 -8
  62. package/src/slot/render.ts +7 -9
  63. package/src/svg.ts +1 -1
  64. package/src/transformer/codegen.ts +518 -0
  65. package/src/transformer/index.ts +98 -0
  66. package/src/transformer/parser.ts +239 -0
  67. package/src/transformer/plugins/esbuild.ts +46 -0
  68. package/src/transformer/plugins/tsc.ts +7 -0
  69. package/src/transformer/plugins/vite.ts +49 -0
  70. package/src/transformer/ts-parser.ts +123 -0
  71. package/src/transformer/type-analyzer.ts +334 -0
  72. package/src/types.ts +3 -4
  73. package/src/utilities.ts +52 -0
  74. package/storage/rewrite-analysis-2026-01-04.md +439 -0
  75. package/test/constants.ts +69 -0
  76. package/test/effects.ts +237 -0
  77. package/test/events.ts +318 -0
  78. package/test/imported-values.ts +253 -0
  79. package/test/nested.ts +298 -0
  80. package/test/slots.ts +259 -0
  81. package/test/spread.ts +290 -0
  82. package/test/static.ts +118 -0
  83. package/test/templates.ts +473 -0
  84. package/test/tsconfig.json +17 -0
  85. package/test/vite.config.ts +50 -0
  86. package/build/html/index.d.ts +0 -9
  87. package/build/html/index.js +0 -29
  88. package/build/html/parser.d.ts +0 -5
  89. package/build/html/parser.js +0 -165
  90. package/build/utilities/element.d.ts +0 -11
  91. package/build/utilities/element.js +0 -9
  92. package/build/utilities/fragment.d.ts +0 -3
  93. package/build/utilities/fragment.js +0 -10
  94. package/build/utilities/marker.d.ts +0 -2
  95. package/build/utilities/marker.js +0 -4
  96. package/build/utilities/node.d.ts +0 -9
  97. package/build/utilities/node.js +0 -10
  98. package/build/utilities/raf.d.ts +0 -2
  99. package/build/utilities/raf.js +0 -1
  100. package/build/utilities/text.d.ts +0 -2
  101. package/build/utilities/text.js +0 -9
  102. package/src/html/index.ts +0 -48
  103. package/src/html/parser.ts +0 -235
  104. package/src/utilities/element.ts +0 -28
  105. package/src/utilities/fragment.ts +0 -19
  106. package/src/utilities/marker.ts +0 -6
  107. package/src/utilities/node.ts +0 -29
  108. package/src/utilities/raf.ts +0 -1
  109. package/src/utilities/text.ts +0 -15
@@ -0,0 +1,239 @@
1
+ // Inlined to avoid importing constants.ts which depends on utilities.ts (uses document)
2
+ const SLOT_HTML = '<!--$-->';
3
+
4
+
5
+ type NodePath = ('firstChild' | 'firstElementChild' | 'nextElementSibling' | 'nextSibling')[];
6
+
7
+
8
+ const ATTRIBUTE_DELIMITERS: Record<string, string> = {
9
+ class: ' ',
10
+ style: ';'
11
+ };
12
+
13
+ const NODE_CLOSING = 1;
14
+
15
+ const NODE_COMMENT = 2;
16
+
17
+ const NODE_ELEMENT = 3;
18
+
19
+ const NODE_SLOT = 4;
20
+
21
+ const NODE_VOID = 5;
22
+
23
+ const NODE_WHITELIST: Record<string, number> = {
24
+ '!': NODE_COMMENT,
25
+ '/': NODE_CLOSING,
26
+ 'area': NODE_VOID,
27
+ 'base': NODE_VOID,
28
+ 'br': NODE_VOID,
29
+ 'col': NODE_VOID,
30
+ 'embed': NODE_VOID,
31
+ 'hr': NODE_VOID,
32
+ 'img': NODE_VOID,
33
+ 'input': NODE_VOID,
34
+ 'keygen': NODE_VOID,
35
+ 'link': NODE_VOID,
36
+ 'menuitem': NODE_VOID,
37
+ 'meta': NODE_VOID,
38
+ 'param': NODE_VOID,
39
+ 'source': NODE_VOID,
40
+ 'track': NODE_VOID,
41
+ 'wbr': NODE_VOID
42
+ };
43
+
44
+ const REGEX_EMPTY_ATTRIBUTES = /\s+[\w:-]+\s*=\s*["']\s*["']|\s+(?=>)/g;
45
+
46
+ const REGEX_EMPTY_TEXT_NODES = /(>|}|\s)\s+(<|{|\s)/g;
47
+
48
+ const REGEX_EVENTS = /(?:\s*on[\w-:]+\s*=(?:\s*["'][^"']*["'])*)/g;
49
+
50
+ const REGEX_SLOT_ATTRIBUTES = /<[\w-]+([^><]*{{\$}}[^><]*)>/g;
51
+
52
+ const REGEX_SLOT_NODES = /<([\w-]+|[\/!])(?:([^><]*{{\$}}[^><]*)|(?:[^><]*))?>|{{\$}}/g;
53
+
54
+ const SLOT_MARKER = '{{$}}';
55
+
56
+
57
+ function methods(children: number, copy: NodePath, first: NodePath[number], next: NodePath[number]) {
58
+ let length = copy.length,
59
+ result: NodePath = new Array(length + 1 + children);
60
+
61
+ for (let i = 0, n = length; i < n; i++) {
62
+ result[i] = copy[i];
63
+ }
64
+
65
+ result[length] = first;
66
+
67
+ for (let i = 0, n = children; i < n; i++) {
68
+ result[length + 1 + i] = next;
69
+ }
70
+
71
+ return result;
72
+ }
73
+
74
+
75
+ const parse = (literals: string[]) => {
76
+ let html = literals
77
+ .join(SLOT_MARKER)
78
+ .replace(REGEX_EMPTY_TEXT_NODES, '$1$2')
79
+ .trim(),
80
+ n = literals.length - 1;
81
+
82
+ if (n === 0) {
83
+ return { html, slots: null };
84
+ }
85
+
86
+ let attributes: Record<string, { names: string[], statics: Record<string, string> }> = {},
87
+ buffer = '',
88
+ events = false,
89
+ index = 0,
90
+ level = 0,
91
+ levels = [{ children: 0, elements: 0, path: [] as NodePath }],
92
+ parsed = html.split(SLOT_MARKER),
93
+ slot = 0,
94
+ slots: (
95
+ { path: NodePath; type: 'slot' } |
96
+ { attributes: typeof attributes[string]; path: NodePath; type: 'attributes' }
97
+ )[] = [];
98
+
99
+ {
100
+ let attribute = '',
101
+ buffer = '',
102
+ char = '',
103
+ quote = '';
104
+
105
+ for (let match of html.matchAll(REGEX_SLOT_ATTRIBUTES)) {
106
+ let found = match[1];
107
+
108
+ if (attributes[found]) {
109
+ continue;
110
+ }
111
+
112
+ let { names, statics } = attributes[found] = { names: [], statics: {} } as typeof attributes[string];
113
+
114
+ for (let i = 0, n = found.length; i < n; i++) {
115
+ char = found[i];
116
+
117
+ if (char === ' ') {
118
+ buffer = '';
119
+ }
120
+ else if (char === '=') {
121
+ attribute = buffer;
122
+ buffer = '';
123
+ }
124
+ else if (char === '"' || char === "'") {
125
+ if (!attribute) {
126
+ continue;
127
+ }
128
+ else if (!quote) {
129
+ quote = char;
130
+ }
131
+ else if (quote === char) {
132
+ if (attribute) {
133
+ statics[attribute] ??= '';
134
+ statics[attribute] += `${ATTRIBUTE_DELIMITERS[attribute] || ''}${buffer}`;
135
+ }
136
+
137
+ attribute = '';
138
+ buffer = '';
139
+ quote = '';
140
+ }
141
+ }
142
+ else if (char === '{' && char !== buffer) {
143
+ buffer = char;
144
+ }
145
+ else {
146
+ buffer += char;
147
+
148
+ if (buffer === SLOT_MARKER) {
149
+ buffer = '';
150
+
151
+ if (attribute) {
152
+ names.push(attribute);
153
+
154
+ if (!quote) {
155
+ attribute = '';
156
+ }
157
+ }
158
+ else {
159
+ names.push('spread');
160
+ }
161
+ }
162
+ else if (buffer === 'on') {
163
+ events = true;
164
+ }
165
+ }
166
+ }
167
+ }
168
+ }
169
+
170
+ {
171
+ for (let match of html.matchAll(REGEX_SLOT_NODES)) {
172
+ let parent = levels[level],
173
+ type = match[1] === undefined ? NODE_SLOT : (NODE_WHITELIST[match[1].toLowerCase()] || NODE_ELEMENT);
174
+
175
+ if ((match.index || 1) - 1 > index) {
176
+ parent.children++;
177
+ }
178
+
179
+ if (type === NODE_ELEMENT || type === NODE_VOID) {
180
+ let attr = match[2],
181
+ path = parent.path.length
182
+ ? methods(parent.elements, parent.path, 'firstElementChild', 'nextElementSibling')
183
+ : methods(parent.children, [], 'firstChild', 'nextSibling');
184
+
185
+ if (attr) {
186
+ let attrs = attributes[attr];
187
+
188
+ if (!attrs) {
189
+ throw new Error(`@esportsplus/template: attribute metadata could not be found for '${attr}'`);
190
+ }
191
+
192
+ slots.push({ attributes: attrs, path, type: 'attributes' });
193
+
194
+ for (let i = 0, n = attrs.names.length; i < n; i++) {
195
+ buffer += parsed[slot++];
196
+ }
197
+ }
198
+
199
+ if (type === NODE_ELEMENT) {
200
+ levels[++level] = { children: 0, elements: 0, path };
201
+ }
202
+
203
+ parent.elements++;
204
+ }
205
+ else if (type === NODE_SLOT) {
206
+ buffer += parsed[slot++] + SLOT_HTML;
207
+ slots.push({ path: methods(parent.children, parent.path, 'firstChild', 'nextSibling'), type: 'slot' });
208
+ }
209
+
210
+ if (n === slot) {
211
+ buffer += parsed[slot];
212
+ break;
213
+ }
214
+
215
+ if (type === NODE_CLOSING) {
216
+ level--;
217
+ }
218
+ else {
219
+ parent.children++;
220
+ }
221
+
222
+ index = (match.index || 0) + match[0].length;
223
+ }
224
+ }
225
+
226
+ if (events) {
227
+ buffer = buffer.replace(REGEX_EVENTS, '');
228
+ }
229
+
230
+ buffer = buffer.replace(REGEX_EMPTY_ATTRIBUTES, '');
231
+
232
+ return {
233
+ html: buffer,
234
+ slots: slots.length ? slots : null
235
+ };
236
+ };
237
+
238
+
239
+ export default { parse };
@@ -0,0 +1,46 @@
1
+ import { mightNeedTransform, PATTERNS, transform } from '../index';
2
+ import { program, TRANSFORM_PATTERN } from '@esportsplus/typescript/transformer';
3
+ import type { OnLoadArgs, Plugin, PluginBuild } from 'esbuild';
4
+ import fs from 'fs';
5
+ import ts from 'typescript';
6
+
7
+
8
+ export default (options?: { root?: string }): Plugin => {
9
+ let root = options?.root ?? process.cwd();
10
+
11
+ return {
12
+ name: '@esportsplus/template/plugin-esbuild',
13
+
14
+ setup(build: PluginBuild) {
15
+ build.onLoad({ filter: TRANSFORM_PATTERN }, async (args: OnLoadArgs) => {
16
+ let code = await fs.promises.readFile(args.path, 'utf8');
17
+
18
+ if (!mightNeedTransform(code, { patterns: PATTERNS })) {
19
+ return null;
20
+ }
21
+
22
+ try {
23
+ let sourceFile = ts.createSourceFile(args.path, code, ts.ScriptTarget.Latest, true),
24
+ result = transform(sourceFile, program.get(root));
25
+
26
+ if (!result.changed) {
27
+ return null;
28
+ }
29
+
30
+ return {
31
+ contents: result.code,
32
+ loader: args.path.endsWith('x') ? 'tsx' : 'ts'
33
+ };
34
+ }
35
+ catch (error) {
36
+ console.error(`@esportsplus/template: Error transforming ${args.path}:`, error);
37
+ return null;
38
+ }
39
+ });
40
+
41
+ build.onEnd(() => {
42
+ program.delete(root);
43
+ });
44
+ }
45
+ };
46
+ };
@@ -0,0 +1,7 @@
1
+ import { createTransformer } from '../index';
2
+ import ts from 'typescript';
3
+
4
+
5
+ export default (program: ts.Program): ts.TransformerFactory<ts.SourceFile> => {
6
+ return createTransformer(program);
7
+ };
@@ -0,0 +1,49 @@
1
+ import { mightNeedTransform, PATTERNS, transform } from '../index';
2
+ import { program, TRANSFORM_PATTERN } from '@esportsplus/typescript/transformer';
3
+ import type { Plugin, ResolvedConfig } from 'vite';
4
+ import ts from 'typescript';
5
+
6
+
7
+ export default (options?: { root?: string }): Plugin => {
8
+ let root: string;
9
+
10
+ return {
11
+ enforce: 'pre',
12
+ name: '@esportsplus/template/plugin-vite',
13
+
14
+ configResolved(config: ResolvedConfig) {
15
+ root = options?.root ?? config.root;
16
+ },
17
+
18
+ transform(code: string, id: string) {
19
+ if (!TRANSFORM_PATTERN.test(id) || id.includes('node_modules')) {
20
+ return null;
21
+ }
22
+
23
+ if (!mightNeedTransform(code, { patterns: PATTERNS })) {
24
+ return null;
25
+ }
26
+
27
+ try {
28
+ let sourceFile = ts.createSourceFile(id, code, ts.ScriptTarget.Latest, true),
29
+ result = transform(sourceFile, program.get(root));
30
+
31
+ if (!result.changed) {
32
+ return null;
33
+ }
34
+
35
+ return { code: result.code, map: null };
36
+ }
37
+ catch (error) {
38
+ console.error(`@esportsplus/template: Error transforming ${id}:`, error);
39
+ return null;
40
+ }
41
+ },
42
+
43
+ watchChange(id: string) {
44
+ if (TRANSFORM_PATTERN.test(id)) {
45
+ program.delete(root);
46
+ }
47
+ }
48
+ };
49
+ };
@@ -0,0 +1,123 @@
1
+ import ts from 'typescript';
2
+
3
+
4
+ type ReactiveCallInfo = {
5
+ arrayArg: ts.Expression;
6
+ callbackArg: ts.Expression;
7
+ end: number;
8
+ node: ts.CallExpression;
9
+ start: number;
10
+ };
11
+
12
+ type TemplateInfo = {
13
+ depth: number;
14
+ end: number;
15
+ expressions: ts.Expression[];
16
+ literals: string[];
17
+ node: ts.TaggedTemplateExpression;
18
+ start: number;
19
+ };
20
+
21
+
22
+ function extractTemplateInfo(node: ts.TaggedTemplateExpression, depth: number): TemplateInfo {
23
+ let expressions: ts.Expression[] = [],
24
+ literals: string[] = [],
25
+ template = node.template;
26
+
27
+ if (ts.isNoSubstitutionTemplateLiteral(template)) {
28
+ literals.push(template.text);
29
+ }
30
+ else if (ts.isTemplateExpression(template)) {
31
+ literals.push(template.head.text);
32
+
33
+ for (let i = 0, n = template.templateSpans.length; i < n; i++) {
34
+ let span = template.templateSpans[i];
35
+
36
+ expressions.push(span.expression);
37
+ literals.push(span.literal.text);
38
+ }
39
+ }
40
+
41
+ return {
42
+ depth,
43
+ end: node.end,
44
+ expressions,
45
+ literals,
46
+ node,
47
+ start: node.getStart()
48
+ };
49
+ }
50
+
51
+ function isFunctionNode(node: ts.Node): boolean {
52
+ return (
53
+ ts.isArrowFunction(node) ||
54
+ ts.isFunctionDeclaration(node) ||
55
+ ts.isFunctionExpression(node) ||
56
+ ts.isMethodDeclaration(node)
57
+ );
58
+ }
59
+
60
+ function visitReactiveCalls(node: ts.Node, calls: ReactiveCallInfo[]): void {
61
+ if (
62
+ ts.isCallExpression(node) &&
63
+ ts.isPropertyAccessExpression(node.expression) &&
64
+ ts.isIdentifier(node.expression.expression) &&
65
+ node.expression.expression.text === 'html' &&
66
+ node.expression.name.text === 'reactive' &&
67
+ node.arguments.length === 2
68
+ ) {
69
+ calls.push({
70
+ arrayArg: node.arguments[0],
71
+ callbackArg: node.arguments[1],
72
+ end: node.end,
73
+ node,
74
+ start: node.getStart()
75
+ });
76
+ }
77
+
78
+ ts.forEachChild(node, child => visitReactiveCalls(child, calls));
79
+ }
80
+
81
+ function visitTemplates(node: ts.Node, depth: number, templates: TemplateInfo[]): void {
82
+ let nextDepth = isFunctionNode(node) ? depth + 1 : depth;
83
+
84
+ if (ts.isTaggedTemplateExpression(node) && ts.isIdentifier(node.tag) && node.tag.text === 'html') {
85
+ templates.push(extractTemplateInfo(node, depth));
86
+ }
87
+
88
+ ts.forEachChild(node, child => visitTemplates(child, nextDepth, templates));
89
+ }
90
+
91
+
92
+ const findHtmlTemplates = (sourceFile: ts.SourceFile): TemplateInfo[] => {
93
+ let templates: TemplateInfo[] = [];
94
+
95
+ visitTemplates(sourceFile, 0, templates);
96
+
97
+ // Sort by depth descending (deepest first), then by position for stable ordering
98
+ templates.sort((a, b) => a.depth !== b.depth ? b.depth - a.depth : a.start - b.start);
99
+
100
+ return templates;
101
+ };
102
+
103
+ const findReactiveCalls = (sourceFile: ts.SourceFile): ReactiveCallInfo[] => {
104
+ let calls: ReactiveCallInfo[] = [];
105
+
106
+ visitReactiveCalls(sourceFile, calls);
107
+
108
+ return calls;
109
+ };
110
+
111
+ const getTemplateExpressions = (info: TemplateInfo, sourceFile: ts.SourceFile): string[] => {
112
+ let exprs: string[] = [];
113
+
114
+ for (let i = 0, n = info.expressions.length; i < n; i++) {
115
+ exprs.push(info.expressions[i].getText(sourceFile));
116
+ }
117
+
118
+ return exprs;
119
+ };
120
+
121
+
122
+ export { findHtmlTemplates, findReactiveCalls, getTemplateExpressions };
123
+ export type { ReactiveCallInfo, TemplateInfo };