@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.
- package/build/attributes.d.ts +7 -1
- package/build/attributes.js +86 -33
- package/build/constants.d.ts +3 -11
- package/build/constants.js +4 -32
- package/build/event/constants.d.ts +3 -0
- package/build/event/constants.js +13 -0
- package/build/event/index.d.ts +9 -1
- package/build/event/index.js +29 -35
- package/build/event/ontick.js +6 -9
- package/build/html.d.ts +9 -0
- package/build/html.js +7 -0
- package/build/index.d.ts +8 -2
- package/build/index.js +8 -1
- package/build/render.d.ts +2 -2
- package/build/render.js +2 -3
- package/build/runtime.d.ts +1 -0
- package/build/runtime.js +5 -0
- package/build/slot/array.d.ts +3 -3
- package/build/slot/array.js +11 -14
- package/build/slot/cleanup.d.ts +1 -1
- package/build/slot/cleanup.js +1 -2
- package/build/slot/effect.js +5 -7
- package/build/slot/index.js +1 -7
- package/build/slot/render.js +6 -8
- package/build/svg.d.ts +1 -1
- package/build/svg.js +1 -1
- package/build/transformer/codegen.d.ts +18 -0
- package/build/transformer/codegen.js +316 -0
- package/build/transformer/index.d.ts +12 -0
- package/build/transformer/index.js +62 -0
- package/build/transformer/parser.d.ts +18 -0
- package/build/transformer/parser.js +166 -0
- package/build/transformer/plugins/esbuild.d.ts +5 -0
- package/build/transformer/plugins/esbuild.js +35 -0
- package/build/transformer/plugins/tsc.d.ts +3 -0
- package/build/transformer/plugins/tsc.js +4 -0
- package/build/transformer/plugins/vite.d.ts +5 -0
- package/build/transformer/plugins/vite.js +37 -0
- package/build/transformer/ts-parser.d.ts +21 -0
- package/build/transformer/ts-parser.js +72 -0
- package/build/transformer/type-analyzer.d.ts +7 -0
- package/build/transformer/type-analyzer.js +230 -0
- package/build/types.d.ts +2 -3
- package/build/utilities.d.ts +7 -0
- package/build/utilities.js +31 -0
- package/package.json +11 -4
- package/src/attributes.ts +115 -51
- package/src/constants.ts +6 -53
- package/src/event/constants.ts +16 -0
- package/src/event/index.ts +36 -42
- package/src/event/onconnect.ts +1 -1
- package/src/event/onresize.ts +1 -1
- package/src/event/ontick.ts +7 -11
- package/src/html.ts +18 -0
- package/src/index.ts +8 -2
- package/src/render.ts +6 -7
- package/src/runtime.ts +8 -0
- package/src/slot/array.ts +18 -24
- package/src/slot/cleanup.ts +3 -4
- package/src/slot/effect.ts +6 -8
- package/src/slot/index.ts +2 -8
- package/src/slot/render.ts +7 -9
- package/src/svg.ts +1 -1
- package/src/transformer/codegen.ts +518 -0
- package/src/transformer/index.ts +98 -0
- package/src/transformer/parser.ts +239 -0
- package/src/transformer/plugins/esbuild.ts +46 -0
- package/src/transformer/plugins/tsc.ts +7 -0
- package/src/transformer/plugins/vite.ts +49 -0
- package/src/transformer/ts-parser.ts +123 -0
- package/src/transformer/type-analyzer.ts +334 -0
- package/src/types.ts +3 -4
- package/src/utilities.ts +52 -0
- package/storage/rewrite-analysis-2026-01-04.md +439 -0
- package/test/constants.ts +69 -0
- package/test/effects.ts +237 -0
- package/test/events.ts +318 -0
- package/test/imported-values.ts +253 -0
- package/test/nested.ts +298 -0
- package/test/slots.ts +259 -0
- package/test/spread.ts +290 -0
- package/test/static.ts +118 -0
- package/test/templates.ts +473 -0
- package/test/tsconfig.json +17 -0
- package/test/vite.config.ts +50 -0
- package/build/html/index.d.ts +0 -9
- package/build/html/index.js +0 -29
- package/build/html/parser.d.ts +0 -5
- package/build/html/parser.js +0 -165
- package/build/utilities/element.d.ts +0 -11
- package/build/utilities/element.js +0 -9
- package/build/utilities/fragment.d.ts +0 -3
- package/build/utilities/fragment.js +0 -10
- package/build/utilities/marker.d.ts +0 -2
- package/build/utilities/marker.js +0 -4
- package/build/utilities/node.d.ts +0 -9
- package/build/utilities/node.js +0 -10
- package/build/utilities/raf.d.ts +0 -2
- package/build/utilities/raf.js +0 -1
- package/build/utilities/text.d.ts +0 -2
- package/build/utilities/text.js +0 -9
- package/src/html/index.ts +0 -48
- package/src/html/parser.ts +0 -235
- package/src/utilities/element.ts +0 -28
- package/src/utilities/fragment.ts +0 -19
- package/src/utilities/marker.ts +0 -6
- package/src/utilities/node.ts +0 -29
- package/src/utilities/raf.ts +0 -1
- package/src/utilities/text.ts +0 -15
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
const SLOT_HTML = '<!--$-->';
|
|
2
|
+
const ATTRIBUTE_DELIMITERS = {
|
|
3
|
+
class: ' ',
|
|
4
|
+
style: ';'
|
|
5
|
+
};
|
|
6
|
+
const NODE_CLOSING = 1;
|
|
7
|
+
const NODE_COMMENT = 2;
|
|
8
|
+
const NODE_ELEMENT = 3;
|
|
9
|
+
const NODE_SLOT = 4;
|
|
10
|
+
const NODE_VOID = 5;
|
|
11
|
+
const NODE_WHITELIST = {
|
|
12
|
+
'!': NODE_COMMENT,
|
|
13
|
+
'/': NODE_CLOSING,
|
|
14
|
+
'area': NODE_VOID,
|
|
15
|
+
'base': NODE_VOID,
|
|
16
|
+
'br': NODE_VOID,
|
|
17
|
+
'col': NODE_VOID,
|
|
18
|
+
'embed': NODE_VOID,
|
|
19
|
+
'hr': NODE_VOID,
|
|
20
|
+
'img': NODE_VOID,
|
|
21
|
+
'input': NODE_VOID,
|
|
22
|
+
'keygen': NODE_VOID,
|
|
23
|
+
'link': NODE_VOID,
|
|
24
|
+
'menuitem': NODE_VOID,
|
|
25
|
+
'meta': NODE_VOID,
|
|
26
|
+
'param': NODE_VOID,
|
|
27
|
+
'source': NODE_VOID,
|
|
28
|
+
'track': NODE_VOID,
|
|
29
|
+
'wbr': NODE_VOID
|
|
30
|
+
};
|
|
31
|
+
const REGEX_EMPTY_ATTRIBUTES = /\s+[\w:-]+\s*=\s*["']\s*["']|\s+(?=>)/g;
|
|
32
|
+
const REGEX_EMPTY_TEXT_NODES = /(>|}|\s)\s+(<|{|\s)/g;
|
|
33
|
+
const REGEX_EVENTS = /(?:\s*on[\w-:]+\s*=(?:\s*["'][^"']*["'])*)/g;
|
|
34
|
+
const REGEX_SLOT_ATTRIBUTES = /<[\w-]+([^><]*{{\$}}[^><]*)>/g;
|
|
35
|
+
const REGEX_SLOT_NODES = /<([\w-]+|[\/!])(?:([^><]*{{\$}}[^><]*)|(?:[^><]*))?>|{{\$}}/g;
|
|
36
|
+
const SLOT_MARKER = '{{$}}';
|
|
37
|
+
function methods(children, copy, first, next) {
|
|
38
|
+
let length = copy.length, result = new Array(length + 1 + children);
|
|
39
|
+
for (let i = 0, n = length; i < n; i++) {
|
|
40
|
+
result[i] = copy[i];
|
|
41
|
+
}
|
|
42
|
+
result[length] = first;
|
|
43
|
+
for (let i = 0, n = children; i < n; i++) {
|
|
44
|
+
result[length + 1 + i] = next;
|
|
45
|
+
}
|
|
46
|
+
return result;
|
|
47
|
+
}
|
|
48
|
+
const parse = (literals) => {
|
|
49
|
+
let html = literals
|
|
50
|
+
.join(SLOT_MARKER)
|
|
51
|
+
.replace(REGEX_EMPTY_TEXT_NODES, '$1$2')
|
|
52
|
+
.trim(), n = literals.length - 1;
|
|
53
|
+
if (n === 0) {
|
|
54
|
+
return { html, slots: null };
|
|
55
|
+
}
|
|
56
|
+
let attributes = {}, buffer = '', events = false, index = 0, level = 0, levels = [{ children: 0, elements: 0, path: [] }], parsed = html.split(SLOT_MARKER), slot = 0, slots = [];
|
|
57
|
+
{
|
|
58
|
+
let attribute = '', buffer = '', char = '', quote = '';
|
|
59
|
+
for (let match of html.matchAll(REGEX_SLOT_ATTRIBUTES)) {
|
|
60
|
+
let found = match[1];
|
|
61
|
+
if (attributes[found]) {
|
|
62
|
+
continue;
|
|
63
|
+
}
|
|
64
|
+
let { names, statics } = attributes[found] = { names: [], statics: {} };
|
|
65
|
+
for (let i = 0, n = found.length; i < n; i++) {
|
|
66
|
+
char = found[i];
|
|
67
|
+
if (char === ' ') {
|
|
68
|
+
buffer = '';
|
|
69
|
+
}
|
|
70
|
+
else if (char === '=') {
|
|
71
|
+
attribute = buffer;
|
|
72
|
+
buffer = '';
|
|
73
|
+
}
|
|
74
|
+
else if (char === '"' || char === "'") {
|
|
75
|
+
if (!attribute) {
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
78
|
+
else if (!quote) {
|
|
79
|
+
quote = char;
|
|
80
|
+
}
|
|
81
|
+
else if (quote === char) {
|
|
82
|
+
if (attribute) {
|
|
83
|
+
statics[attribute] ??= '';
|
|
84
|
+
statics[attribute] += `${ATTRIBUTE_DELIMITERS[attribute] || ''}${buffer}`;
|
|
85
|
+
}
|
|
86
|
+
attribute = '';
|
|
87
|
+
buffer = '';
|
|
88
|
+
quote = '';
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
else if (char === '{' && char !== buffer) {
|
|
92
|
+
buffer = char;
|
|
93
|
+
}
|
|
94
|
+
else {
|
|
95
|
+
buffer += char;
|
|
96
|
+
if (buffer === SLOT_MARKER) {
|
|
97
|
+
buffer = '';
|
|
98
|
+
if (attribute) {
|
|
99
|
+
names.push(attribute);
|
|
100
|
+
if (!quote) {
|
|
101
|
+
attribute = '';
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
else {
|
|
105
|
+
names.push('spread');
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
else if (buffer === 'on') {
|
|
109
|
+
events = true;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
{
|
|
116
|
+
for (let match of html.matchAll(REGEX_SLOT_NODES)) {
|
|
117
|
+
let parent = levels[level], type = match[1] === undefined ? NODE_SLOT : (NODE_WHITELIST[match[1].toLowerCase()] || NODE_ELEMENT);
|
|
118
|
+
if ((match.index || 1) - 1 > index) {
|
|
119
|
+
parent.children++;
|
|
120
|
+
}
|
|
121
|
+
if (type === NODE_ELEMENT || type === NODE_VOID) {
|
|
122
|
+
let attr = match[2], path = parent.path.length
|
|
123
|
+
? methods(parent.elements, parent.path, 'firstElementChild', 'nextElementSibling')
|
|
124
|
+
: methods(parent.children, [], 'firstChild', 'nextSibling');
|
|
125
|
+
if (attr) {
|
|
126
|
+
let attrs = attributes[attr];
|
|
127
|
+
if (!attrs) {
|
|
128
|
+
throw new Error(`@esportsplus/template: attribute metadata could not be found for '${attr}'`);
|
|
129
|
+
}
|
|
130
|
+
slots.push({ attributes: attrs, path, type: 'attributes' });
|
|
131
|
+
for (let i = 0, n = attrs.names.length; i < n; i++) {
|
|
132
|
+
buffer += parsed[slot++];
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
if (type === NODE_ELEMENT) {
|
|
136
|
+
levels[++level] = { children: 0, elements: 0, path };
|
|
137
|
+
}
|
|
138
|
+
parent.elements++;
|
|
139
|
+
}
|
|
140
|
+
else if (type === NODE_SLOT) {
|
|
141
|
+
buffer += parsed[slot++] + SLOT_HTML;
|
|
142
|
+
slots.push({ path: methods(parent.children, parent.path, 'firstChild', 'nextSibling'), type: 'slot' });
|
|
143
|
+
}
|
|
144
|
+
if (n === slot) {
|
|
145
|
+
buffer += parsed[slot];
|
|
146
|
+
break;
|
|
147
|
+
}
|
|
148
|
+
if (type === NODE_CLOSING) {
|
|
149
|
+
level--;
|
|
150
|
+
}
|
|
151
|
+
else {
|
|
152
|
+
parent.children++;
|
|
153
|
+
}
|
|
154
|
+
index = (match.index || 0) + match[0].length;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
if (events) {
|
|
158
|
+
buffer = buffer.replace(REGEX_EVENTS, '');
|
|
159
|
+
}
|
|
160
|
+
buffer = buffer.replace(REGEX_EMPTY_ATTRIBUTES, '');
|
|
161
|
+
return {
|
|
162
|
+
html: buffer,
|
|
163
|
+
slots: slots.length ? slots : null
|
|
164
|
+
};
|
|
165
|
+
};
|
|
166
|
+
export default { parse };
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { mightNeedTransform, PATTERNS, transform } from '../index.js';
|
|
2
|
+
import { program, TRANSFORM_PATTERN } from '@esportsplus/typescript/transformer';
|
|
3
|
+
import fs from 'fs';
|
|
4
|
+
import ts from 'typescript';
|
|
5
|
+
export default (options) => {
|
|
6
|
+
let root = options?.root ?? process.cwd();
|
|
7
|
+
return {
|
|
8
|
+
name: '@esportsplus/template/plugin-esbuild',
|
|
9
|
+
setup(build) {
|
|
10
|
+
build.onLoad({ filter: TRANSFORM_PATTERN }, async (args) => {
|
|
11
|
+
let code = await fs.promises.readFile(args.path, 'utf8');
|
|
12
|
+
if (!mightNeedTransform(code, { patterns: PATTERNS })) {
|
|
13
|
+
return null;
|
|
14
|
+
}
|
|
15
|
+
try {
|
|
16
|
+
let sourceFile = ts.createSourceFile(args.path, code, ts.ScriptTarget.Latest, true), result = transform(sourceFile, program.get(root));
|
|
17
|
+
if (!result.changed) {
|
|
18
|
+
return null;
|
|
19
|
+
}
|
|
20
|
+
return {
|
|
21
|
+
contents: result.code,
|
|
22
|
+
loader: args.path.endsWith('x') ? 'tsx' : 'ts'
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
catch (error) {
|
|
26
|
+
console.error(`@esportsplus/template: Error transforming ${args.path}:`, error);
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
29
|
+
});
|
|
30
|
+
build.onEnd(() => {
|
|
31
|
+
program.delete(root);
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
};
|
|
35
|
+
};
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { mightNeedTransform, PATTERNS, transform } from '../index.js';
|
|
2
|
+
import { program, TRANSFORM_PATTERN } from '@esportsplus/typescript/transformer';
|
|
3
|
+
import ts from 'typescript';
|
|
4
|
+
export default (options) => {
|
|
5
|
+
let root;
|
|
6
|
+
return {
|
|
7
|
+
enforce: 'pre',
|
|
8
|
+
name: '@esportsplus/template/plugin-vite',
|
|
9
|
+
configResolved(config) {
|
|
10
|
+
root = options?.root ?? config.root;
|
|
11
|
+
},
|
|
12
|
+
transform(code, id) {
|
|
13
|
+
if (!TRANSFORM_PATTERN.test(id) || id.includes('node_modules')) {
|
|
14
|
+
return null;
|
|
15
|
+
}
|
|
16
|
+
if (!mightNeedTransform(code, { patterns: PATTERNS })) {
|
|
17
|
+
return null;
|
|
18
|
+
}
|
|
19
|
+
try {
|
|
20
|
+
let sourceFile = ts.createSourceFile(id, code, ts.ScriptTarget.Latest, true), result = transform(sourceFile, program.get(root));
|
|
21
|
+
if (!result.changed) {
|
|
22
|
+
return null;
|
|
23
|
+
}
|
|
24
|
+
return { code: result.code, map: null };
|
|
25
|
+
}
|
|
26
|
+
catch (error) {
|
|
27
|
+
console.error(`@esportsplus/template: Error transforming ${id}:`, error);
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
30
|
+
},
|
|
31
|
+
watchChange(id) {
|
|
32
|
+
if (TRANSFORM_PATTERN.test(id)) {
|
|
33
|
+
program.delete(root);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
};
|
|
37
|
+
};
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import ts from 'typescript';
|
|
2
|
+
type ReactiveCallInfo = {
|
|
3
|
+
arrayArg: ts.Expression;
|
|
4
|
+
callbackArg: ts.Expression;
|
|
5
|
+
end: number;
|
|
6
|
+
node: ts.CallExpression;
|
|
7
|
+
start: number;
|
|
8
|
+
};
|
|
9
|
+
type TemplateInfo = {
|
|
10
|
+
depth: number;
|
|
11
|
+
end: number;
|
|
12
|
+
expressions: ts.Expression[];
|
|
13
|
+
literals: string[];
|
|
14
|
+
node: ts.TaggedTemplateExpression;
|
|
15
|
+
start: number;
|
|
16
|
+
};
|
|
17
|
+
declare const findHtmlTemplates: (sourceFile: ts.SourceFile) => TemplateInfo[];
|
|
18
|
+
declare const findReactiveCalls: (sourceFile: ts.SourceFile) => ReactiveCallInfo[];
|
|
19
|
+
declare const getTemplateExpressions: (info: TemplateInfo, sourceFile: ts.SourceFile) => string[];
|
|
20
|
+
export { findHtmlTemplates, findReactiveCalls, getTemplateExpressions };
|
|
21
|
+
export type { ReactiveCallInfo, TemplateInfo };
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import ts from 'typescript';
|
|
2
|
+
function extractTemplateInfo(node, depth) {
|
|
3
|
+
let expressions = [], literals = [], template = node.template;
|
|
4
|
+
if (ts.isNoSubstitutionTemplateLiteral(template)) {
|
|
5
|
+
literals.push(template.text);
|
|
6
|
+
}
|
|
7
|
+
else if (ts.isTemplateExpression(template)) {
|
|
8
|
+
literals.push(template.head.text);
|
|
9
|
+
for (let i = 0, n = template.templateSpans.length; i < n; i++) {
|
|
10
|
+
let span = template.templateSpans[i];
|
|
11
|
+
expressions.push(span.expression);
|
|
12
|
+
literals.push(span.literal.text);
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
return {
|
|
16
|
+
depth,
|
|
17
|
+
end: node.end,
|
|
18
|
+
expressions,
|
|
19
|
+
literals,
|
|
20
|
+
node,
|
|
21
|
+
start: node.getStart()
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
function isFunctionNode(node) {
|
|
25
|
+
return (ts.isArrowFunction(node) ||
|
|
26
|
+
ts.isFunctionDeclaration(node) ||
|
|
27
|
+
ts.isFunctionExpression(node) ||
|
|
28
|
+
ts.isMethodDeclaration(node));
|
|
29
|
+
}
|
|
30
|
+
function visitReactiveCalls(node, calls) {
|
|
31
|
+
if (ts.isCallExpression(node) &&
|
|
32
|
+
ts.isPropertyAccessExpression(node.expression) &&
|
|
33
|
+
ts.isIdentifier(node.expression.expression) &&
|
|
34
|
+
node.expression.expression.text === 'html' &&
|
|
35
|
+
node.expression.name.text === 'reactive' &&
|
|
36
|
+
node.arguments.length === 2) {
|
|
37
|
+
calls.push({
|
|
38
|
+
arrayArg: node.arguments[0],
|
|
39
|
+
callbackArg: node.arguments[1],
|
|
40
|
+
end: node.end,
|
|
41
|
+
node,
|
|
42
|
+
start: node.getStart()
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
ts.forEachChild(node, child => visitReactiveCalls(child, calls));
|
|
46
|
+
}
|
|
47
|
+
function visitTemplates(node, depth, templates) {
|
|
48
|
+
let nextDepth = isFunctionNode(node) ? depth + 1 : depth;
|
|
49
|
+
if (ts.isTaggedTemplateExpression(node) && ts.isIdentifier(node.tag) && node.tag.text === 'html') {
|
|
50
|
+
templates.push(extractTemplateInfo(node, depth));
|
|
51
|
+
}
|
|
52
|
+
ts.forEachChild(node, child => visitTemplates(child, nextDepth, templates));
|
|
53
|
+
}
|
|
54
|
+
const findHtmlTemplates = (sourceFile) => {
|
|
55
|
+
let templates = [];
|
|
56
|
+
visitTemplates(sourceFile, 0, templates);
|
|
57
|
+
templates.sort((a, b) => a.depth !== b.depth ? b.depth - a.depth : a.start - b.start);
|
|
58
|
+
return templates;
|
|
59
|
+
};
|
|
60
|
+
const findReactiveCalls = (sourceFile) => {
|
|
61
|
+
let calls = [];
|
|
62
|
+
visitReactiveCalls(sourceFile, calls);
|
|
63
|
+
return calls;
|
|
64
|
+
};
|
|
65
|
+
const getTemplateExpressions = (info, sourceFile) => {
|
|
66
|
+
let exprs = [];
|
|
67
|
+
for (let i = 0, n = info.expressions.length; i < n; i++) {
|
|
68
|
+
exprs.push(info.expressions[i].getText(sourceFile));
|
|
69
|
+
}
|
|
70
|
+
return exprs;
|
|
71
|
+
};
|
|
72
|
+
export { findHtmlTemplates, findReactiveCalls, getTemplateExpressions };
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import ts from 'typescript';
|
|
2
|
+
type SlotType = 'array-slot' | 'document-fragment' | 'effect' | 'node' | 'primitive' | 'static' | 'unknown';
|
|
3
|
+
declare const analyzeExpression: (expr: ts.Expression, checker?: ts.TypeChecker) => SlotType;
|
|
4
|
+
declare const generateAttributeBinding: (elementVar: string, name: string, expr: string, staticValue: string) => string;
|
|
5
|
+
declare const generateSpreadBindings: (expr: ts.Expression, exprCode: string, elementVar: string, sourceFile: ts.SourceFile, checker?: ts.TypeChecker) => string[];
|
|
6
|
+
export { analyzeExpression, generateAttributeBinding, generateSpreadBindings };
|
|
7
|
+
export type { SlotType };
|
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
import { getNames } from './codegen.js';
|
|
2
|
+
import { DIRECT_ATTACH_EVENTS, LIFECYCLE_EVENTS } from '../event/constants.js';
|
|
3
|
+
import ts from 'typescript';
|
|
4
|
+
function analyzeSpread(expr, checker) {
|
|
5
|
+
while (ts.isParenthesizedExpression(expr)) {
|
|
6
|
+
expr = expr.expression;
|
|
7
|
+
}
|
|
8
|
+
if (ts.isObjectLiteralExpression(expr)) {
|
|
9
|
+
let keys = [];
|
|
10
|
+
for (let i = 0, n = expr.properties.length; i < n; i++) {
|
|
11
|
+
let prop = expr.properties[i];
|
|
12
|
+
if (ts.isPropertyAssignment(prop)) {
|
|
13
|
+
if (ts.isIdentifier(prop.name) || ts.isStringLiteral(prop.name)) {
|
|
14
|
+
keys.push(prop.name.text);
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
else if (ts.isShorthandPropertyAssignment(prop)) {
|
|
18
|
+
keys.push(prop.name.text);
|
|
19
|
+
}
|
|
20
|
+
else if (ts.isSpreadAssignment(prop)) {
|
|
21
|
+
return { canUnpack: false, keys: [] };
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
return { canUnpack: true, keys };
|
|
25
|
+
}
|
|
26
|
+
if (checker && (ts.isIdentifier(expr) || ts.isPropertyAccessExpression(expr))) {
|
|
27
|
+
try {
|
|
28
|
+
let keys = extractTypePropertyKeys(checker.getTypeAtLocation(expr));
|
|
29
|
+
if (keys.length > 0) {
|
|
30
|
+
return { canUnpack: true, keys };
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
catch {
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
return { canUnpack: false, keys: [] };
|
|
37
|
+
}
|
|
38
|
+
function extractTypePropertyKeys(type) {
|
|
39
|
+
let keys = [], props = type.getProperties();
|
|
40
|
+
for (let i = 0, n = props.length; i < n; i++) {
|
|
41
|
+
let name = props[i].getName();
|
|
42
|
+
if (name.startsWith('__') || name.startsWith('[')) {
|
|
43
|
+
continue;
|
|
44
|
+
}
|
|
45
|
+
keys.push(name);
|
|
46
|
+
}
|
|
47
|
+
return keys;
|
|
48
|
+
}
|
|
49
|
+
function getObjectPropertyValue(expr, key, sourceFile) {
|
|
50
|
+
for (let i = 0, n = expr.properties.length; i < n; i++) {
|
|
51
|
+
let prop = expr.properties[i];
|
|
52
|
+
if (ts.isPropertyAssignment(prop)) {
|
|
53
|
+
let name = ts.isIdentifier(prop.name)
|
|
54
|
+
? prop.name.text
|
|
55
|
+
: ts.isStringLiteral(prop.name) ? prop.name.text : null;
|
|
56
|
+
if (name === key) {
|
|
57
|
+
return prop.initializer.getText(sourceFile);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
else if (ts.isShorthandPropertyAssignment(prop) && prop.name.text === key) {
|
|
61
|
+
return prop.name.text;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
return null;
|
|
65
|
+
}
|
|
66
|
+
function inferSlotType(expr, ctx) {
|
|
67
|
+
while (ts.isParenthesizedExpression(expr)) {
|
|
68
|
+
expr = expr.expression;
|
|
69
|
+
}
|
|
70
|
+
if (ts.isArrowFunction(expr) || ts.isFunctionExpression(expr)) {
|
|
71
|
+
return 'effect';
|
|
72
|
+
}
|
|
73
|
+
if (ts.isCallExpression(expr) &&
|
|
74
|
+
ts.isPropertyAccessExpression(expr.expression) &&
|
|
75
|
+
ts.isIdentifier(expr.expression.expression) &&
|
|
76
|
+
expr.expression.expression.text === 'html' &&
|
|
77
|
+
expr.expression.name.text === 'reactive') {
|
|
78
|
+
return 'array-slot';
|
|
79
|
+
}
|
|
80
|
+
if (ts.isTaggedTemplateExpression(expr) && ts.isIdentifier(expr.tag) && expr.tag.text === 'html') {
|
|
81
|
+
return 'document-fragment';
|
|
82
|
+
}
|
|
83
|
+
if (ts.isArrayLiteralExpression(expr)) {
|
|
84
|
+
return 'array-slot';
|
|
85
|
+
}
|
|
86
|
+
if (ts.isStringLiteral(expr) || ts.isNoSubstitutionTemplateLiteral(expr)) {
|
|
87
|
+
return 'static';
|
|
88
|
+
}
|
|
89
|
+
if (ts.isNumericLiteral(expr)) {
|
|
90
|
+
return 'static';
|
|
91
|
+
}
|
|
92
|
+
if (expr.kind === ts.SyntaxKind.TrueKeyword || expr.kind === ts.SyntaxKind.FalseKeyword) {
|
|
93
|
+
return 'static';
|
|
94
|
+
}
|
|
95
|
+
if (expr.kind === ts.SyntaxKind.NullKeyword || expr.kind === ts.SyntaxKind.UndefinedKeyword) {
|
|
96
|
+
return 'static';
|
|
97
|
+
}
|
|
98
|
+
if (ts.isTemplateExpression(expr)) {
|
|
99
|
+
return 'primitive';
|
|
100
|
+
}
|
|
101
|
+
if (ts.isConditionalExpression(expr)) {
|
|
102
|
+
let whenFalse = inferSlotType(expr.whenFalse, ctx), whenTrue = inferSlotType(expr.whenTrue, ctx);
|
|
103
|
+
if (whenTrue === whenFalse) {
|
|
104
|
+
return whenTrue;
|
|
105
|
+
}
|
|
106
|
+
if (whenTrue === 'effect' || whenFalse === 'effect') {
|
|
107
|
+
return 'effect';
|
|
108
|
+
}
|
|
109
|
+
return 'unknown';
|
|
110
|
+
}
|
|
111
|
+
if (ctx?.checker) {
|
|
112
|
+
let checker = ctx.checker;
|
|
113
|
+
if (ts.isIdentifier(expr)) {
|
|
114
|
+
try {
|
|
115
|
+
let type = checker.getTypeAtLocation(expr);
|
|
116
|
+
if (isTypeFunction(type, checker)) {
|
|
117
|
+
return 'effect';
|
|
118
|
+
}
|
|
119
|
+
if (isTypeArray(type, checker)) {
|
|
120
|
+
return 'array-slot';
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
catch {
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
if (ts.isPropertyAccessExpression(expr)) {
|
|
127
|
+
try {
|
|
128
|
+
let type = checker.getTypeAtLocation(expr);
|
|
129
|
+
if (isTypeFunction(type, checker)) {
|
|
130
|
+
return 'effect';
|
|
131
|
+
}
|
|
132
|
+
if (isTypeArray(type, checker)) {
|
|
133
|
+
return 'array-slot';
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
catch {
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
if (ts.isCallExpression(expr)) {
|
|
140
|
+
try {
|
|
141
|
+
let type = checker.getTypeAtLocation(expr);
|
|
142
|
+
if (isTypeFunction(type, checker)) {
|
|
143
|
+
return 'effect';
|
|
144
|
+
}
|
|
145
|
+
if (isTypeArray(type, checker)) {
|
|
146
|
+
return 'array-slot';
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
catch {
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
return 'unknown';
|
|
154
|
+
}
|
|
155
|
+
function isTypeArray(type, checker) {
|
|
156
|
+
let typeStr = checker.typeToString(type);
|
|
157
|
+
if (typeStr.endsWith('[]') || typeStr.startsWith('Array<') || typeStr.startsWith('ReactiveArray<')) {
|
|
158
|
+
return true;
|
|
159
|
+
}
|
|
160
|
+
let symbol = type.getSymbol();
|
|
161
|
+
if (symbol && (symbol.getName() === 'Array' || symbol.getName() === 'ReactiveArray')) {
|
|
162
|
+
return true;
|
|
163
|
+
}
|
|
164
|
+
return false;
|
|
165
|
+
}
|
|
166
|
+
function isTypeFunction(type, checker) {
|
|
167
|
+
if (type.getCallSignatures().length > 0) {
|
|
168
|
+
return true;
|
|
169
|
+
}
|
|
170
|
+
if (type.isUnion()) {
|
|
171
|
+
for (let i = 0, n = type.types.length; i < n; i++) {
|
|
172
|
+
if (isTypeFunction(type.types[i], checker)) {
|
|
173
|
+
return true;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
return false;
|
|
178
|
+
}
|
|
179
|
+
const analyzeExpression = (expr, checker) => {
|
|
180
|
+
return inferSlotType(expr, checker ? { checker } : undefined);
|
|
181
|
+
};
|
|
182
|
+
const generateAttributeBinding = (elementVar, name, expr, staticValue) => {
|
|
183
|
+
let n = getNames();
|
|
184
|
+
if (name.startsWith('on') && name.length > 2) {
|
|
185
|
+
let event = name.slice(2).toLowerCase(), key = name.toLowerCase();
|
|
186
|
+
if (LIFECYCLE_EVENTS.has(key)) {
|
|
187
|
+
return `${n.event}.${key}(${elementVar}, ${expr});`;
|
|
188
|
+
}
|
|
189
|
+
if (DIRECT_ATTACH_EVENTS.has(key)) {
|
|
190
|
+
return `${n.event}.direct(${elementVar}, '${event}', ${expr});`;
|
|
191
|
+
}
|
|
192
|
+
return `${n.event}.delegate(${elementVar}, '${event}', ${expr});`;
|
|
193
|
+
}
|
|
194
|
+
if (name === 'class') {
|
|
195
|
+
return `${n.attr}.setClass(${elementVar}, '${staticValue}', ${expr});`;
|
|
196
|
+
}
|
|
197
|
+
if (name === 'spread') {
|
|
198
|
+
return `${n.attr}.spread(${elementVar}, ${expr});`;
|
|
199
|
+
}
|
|
200
|
+
if (name === 'style') {
|
|
201
|
+
return `${n.attr}.setStyle(${elementVar}, '${staticValue}', ${expr});`;
|
|
202
|
+
}
|
|
203
|
+
return `${n.attr}.setProperty(${elementVar}, '${name}', ${expr});`;
|
|
204
|
+
};
|
|
205
|
+
const generateSpreadBindings = (expr, exprCode, elementVar, sourceFile, checker) => {
|
|
206
|
+
while (ts.isParenthesizedExpression(expr)) {
|
|
207
|
+
expr = expr.expression;
|
|
208
|
+
}
|
|
209
|
+
let analysis = analyzeSpread(expr, checker);
|
|
210
|
+
if (!analysis.canUnpack) {
|
|
211
|
+
return [`${getNames().attr}.spread(${elementVar}, ${exprCode});`];
|
|
212
|
+
}
|
|
213
|
+
let lines = [];
|
|
214
|
+
if (ts.isObjectLiteralExpression(expr)) {
|
|
215
|
+
for (let i = 0, n = analysis.keys.length; i < n; i++) {
|
|
216
|
+
let key = analysis.keys[i], value = getObjectPropertyValue(expr, key, sourceFile);
|
|
217
|
+
if (value !== null) {
|
|
218
|
+
lines.push(generateAttributeBinding(elementVar, key, value, ''));
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
else {
|
|
223
|
+
for (let i = 0, n = analysis.keys.length; i < n; i++) {
|
|
224
|
+
let key = analysis.keys[i];
|
|
225
|
+
lines.push(generateAttributeBinding(elementVar, key, `${exprCode}.${key}`, ''));
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
return lines;
|
|
229
|
+
};
|
|
230
|
+
export { analyzeExpression, generateAttributeBinding, generateSpreadBindings };
|
package/build/types.d.ts
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { firstChild } from './utilities/node.js';
|
|
2
1
|
import { ArraySlot } from './slot/array.js';
|
|
3
2
|
import attributes from './attributes.js';
|
|
4
3
|
import slot from './slot/index.js';
|
|
@@ -28,9 +27,9 @@ type Template = {
|
|
|
28
27
|
html: string;
|
|
29
28
|
literals: TemplateStringsArray;
|
|
30
29
|
slots: {
|
|
31
|
-
fn: typeof attributes.
|
|
30
|
+
fn: typeof attributes.spread | typeof slot;
|
|
32
31
|
name: string | null;
|
|
33
|
-
path:
|
|
32
|
+
path: (() => ChildNode | null)[];
|
|
34
33
|
}[] | null;
|
|
35
34
|
};
|
|
36
35
|
export type { Attribute, Attributes, Effect, Element, Renderable, SlotGroup, Template };
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
declare const clone: <T extends Node>(node: T, options?: boolean | ImportNodeOptions) => T;
|
|
2
|
+
declare const fragment: (html: string) => DocumentFragment;
|
|
3
|
+
declare const marker: ChildNode;
|
|
4
|
+
declare const raf: typeof requestAnimationFrame;
|
|
5
|
+
declare const template: (html: string) => () => DocumentFragment;
|
|
6
|
+
declare const text: (value: string) => Node;
|
|
7
|
+
export { clone, fragment, template, marker, raf, text };
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { SLOT_HTML } from './constants.js';
|
|
2
|
+
let clonableTemplate = document.createElement('template'), clonableText = document.createTextNode('');
|
|
3
|
+
const clone = typeof navigator !== 'undefined' && navigator.userAgent.includes('Firefox')
|
|
4
|
+
? document.importNode.bind(document)
|
|
5
|
+
: (node, deep = true) => node.cloneNode(deep);
|
|
6
|
+
const fragment = (html) => {
|
|
7
|
+
let element = clonableTemplate.cloneNode();
|
|
8
|
+
element.innerHTML = html;
|
|
9
|
+
return element.content;
|
|
10
|
+
};
|
|
11
|
+
const marker = fragment(SLOT_HTML).firstChild;
|
|
12
|
+
const raf = globalThis?.requestAnimationFrame;
|
|
13
|
+
const template = (html) => {
|
|
14
|
+
let cached;
|
|
15
|
+
return () => {
|
|
16
|
+
if (!cached) {
|
|
17
|
+
let element = clonableTemplate.cloneNode();
|
|
18
|
+
element.innerHTML = html;
|
|
19
|
+
cached = element.content;
|
|
20
|
+
}
|
|
21
|
+
return clone(cached, true);
|
|
22
|
+
};
|
|
23
|
+
};
|
|
24
|
+
const text = (value) => {
|
|
25
|
+
let element = clonableText.cloneNode();
|
|
26
|
+
if (value !== '') {
|
|
27
|
+
element.nodeValue = value;
|
|
28
|
+
}
|
|
29
|
+
return element;
|
|
30
|
+
};
|
|
31
|
+
export { clone, fragment, template, marker, raf, text };
|