@thyn/core 0.0.344 → 0.0.347
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/.github/workflows/static.yml +48 -0
- package/.github/workflows/test.yml +39 -0
- package/LICENSE +21 -0
- package/README.md +50 -0
- package/dist/{element.js → core/element.js} +14 -36
- package/dist/core/index.d.ts +1 -0
- package/dist/core/index.js +1 -0
- package/dist/{router.d.ts → core/router.d.ts} +1 -1
- package/dist/{router.js → core/router.js} +22 -5
- package/dist/index.d.ts +5 -2
- package/dist/index.js +5 -2
- package/dist/plugin/html-parser.d.ts +31 -0
- package/dist/plugin/html-parser.js +275 -0
- package/dist/plugin/index.d.ts +24 -0
- package/dist/plugin/index.js +1009 -0
- package/dist/plugin/utils.d.ts +12 -0
- package/dist/plugin/utils.js +194 -0
- package/docs/CNAME +1 -0
- package/docs/index.html +18 -0
- package/docs/package-lock.json +980 -0
- package/docs/package.json +15 -0
- package/docs/public/thyn.png +0 -0
- package/docs/public/thyn.svg +1 -0
- package/docs/src/App.thyn +10 -0
- package/docs/src/components/Button.thyn +3 -0
- package/docs/src/docs/GettingStarted.thyn +8 -0
- package/docs/src/main.css +17 -0
- package/docs/src/main.js +5 -0
- package/docs/src/pages/Home.thyn +147 -0
- package/docs/vite.config.js +7 -0
- package/package.json +18 -10
- package/src/{element.ts → core/element.ts} +14 -34
- package/src/core/index.ts +1 -0
- package/src/{router.ts → core/router.ts} +22 -6
- package/src/{signals.ts → core/signals.ts} +1 -1
- package/src/index.ts +5 -15
- package/src/plugin/html-parser.ts +332 -0
- package/src/plugin/index.ts +1127 -0
- package/src/plugin/utils.ts +213 -0
- package/tests/Bind.test.ts +14 -0
- package/tests/Bind.thyn +7 -0
- package/tests/ConsecInterps.test.ts +9 -0
- package/tests/ConsecInterps.thyn +9 -0
- package/tests/Counter.test.ts +12 -0
- package/tests/Counter.thyn +7 -0
- package/tests/DoubleQuotes.test.ts +9 -0
- package/tests/DoubleQuotes.thyn +3 -0
- package/tests/Escape.test.ts +9 -0
- package/tests/Escape.thyn +3 -0
- package/tests/EscapeDollar.test.ts +9 -0
- package/tests/EscapeDollar.thyn +5 -0
- package/tests/EventPipes.test.ts +13 -0
- package/tests/EventPipes.thyn +11 -0
- package/tests/List.test.ts +21 -0
- package/tests/List.thyn +15 -0
- package/tests/ListV2.test.ts +20 -0
- package/tests/ListV2.thyn +16 -0
- package/tests/MixElemAndText.test.ts +9 -0
- package/tests/MixElemAndText.thyn +12 -0
- package/tests/Show.test.ts +13 -0
- package/tests/Show.thyn +8 -0
- package/tests/Template.test.ts +9 -0
- package/tests/Template.thyn +8 -0
- package/tests/list/comprehensive.test.ts +659 -0
- package/tests/list/operations/ChildrenAppend.thyn +11 -0
- package/tests/list/operations/ChildrenFilter.thyn +11 -0
- package/tests/list/operations/ChildrenInsert.thyn +11 -0
- package/tests/list/operations/ChildrenNoneToSome.thyn +11 -0
- package/tests/list/operations/ChildrenPrepend.thyn +11 -0
- package/tests/list/operations/ChildrenRemove.thyn +11 -0
- package/tests/list/operations/ChildrenReplaceAll.thyn +11 -0
- package/tests/list/operations/ChildrenSomeToNone.thyn +11 -0
- package/tests/list/operations/ChildrenSort.thyn +11 -0
- package/tests/list/operations/IsolatedAppend.thyn +10 -0
- package/tests/list/operations/IsolatedFilter.thyn +16 -0
- package/tests/list/operations/IsolatedInsert.thyn +10 -0
- package/tests/list/operations/IsolatedMove.thyn +16 -0
- package/tests/list/operations/IsolatedNoneToSome.thyn +16 -0
- package/tests/list/operations/IsolatedPrepend.thyn +10 -0
- package/tests/list/operations/IsolatedRemove.thyn +17 -0
- package/tests/list/operations/IsolatedReplaceAll.thyn +10 -0
- package/tests/list/operations/IsolatedSomeToNone.thyn +10 -0
- package/tests/list/operations/IsolatedSort.thyn +16 -0
- package/tests/list/operations/TerminalAppend.thyn +12 -0
- package/tests/list/operations/TerminalFilter.thyn +12 -0
- package/tests/list/operations/TerminalInsert.thyn +12 -0
- package/tests/list/operations/TerminalNoneToSome.thyn +12 -0
- package/tests/list/operations/TerminalPrepend.thyn +12 -0
- package/tests/list/operations/TerminalRemove.thyn +12 -0
- package/tests/list/operations/TerminalReplaceAll.thyn +12 -0
- package/tests/list/operations/TerminalSomeToNone.thyn +12 -0
- package/tests/list/operations/TerminalSort.thyn +12 -0
- package/tests/tsconfig.json +14 -0
- package/tsconfig.json +11 -6
- package/types/thyn.d.ts +4 -0
- package/vitest.config.ts +7 -2
- package/tests/fx.test.ts +0 -31
- package/tests/lists.test.ts +0 -184
- package/tests/router.test.ts +0 -69
- package/tests/show.test.ts +0 -66
- package/tests/utils.ts +0 -3
- package/tsconfig.tsbuildinfo +0 -1
- /package/dist/{element.d.ts → core/element.d.ts} +0 -0
- /package/dist/{signals.d.ts → core/signals.d.ts} +0 -0
- /package/dist/{signals.js → core/signals.js} +0 -0
|
@@ -0,0 +1,1009 @@
|
|
|
1
|
+
import * as acorn from "acorn";
|
|
2
|
+
import * as acornwalk from "acorn-walk";
|
|
3
|
+
import MagicString from "magic-string";
|
|
4
|
+
import postcss from 'postcss';
|
|
5
|
+
import selectorParser from 'postcss-selector-parser';
|
|
6
|
+
import { parseHTML } from "./html-parser.js";
|
|
7
|
+
import { escapeHtml, escapeTemplateLiteral, extractParts, splitScript } from "./utils.js";
|
|
8
|
+
const esbuildModule = import("esbuild");
|
|
9
|
+
async function scopeSelectors(css, scopeId) {
|
|
10
|
+
const result = await postcss([
|
|
11
|
+
{
|
|
12
|
+
postcssPlugin: 'postcss-scope-thyn',
|
|
13
|
+
Rule(rule) {
|
|
14
|
+
if (!rule.selector)
|
|
15
|
+
return;
|
|
16
|
+
const processor = selectorParser((selectors) => {
|
|
17
|
+
selectors.walkClasses(() => { }); // just to trigger full parse
|
|
18
|
+
selectors.each((sel) => {
|
|
19
|
+
sel.append(selectorParser.className({ value: scopeId }));
|
|
20
|
+
});
|
|
21
|
+
});
|
|
22
|
+
rule.selector = processor.processSync(rule.selector);
|
|
23
|
+
},
|
|
24
|
+
},
|
|
25
|
+
]).process(css, { from: undefined });
|
|
26
|
+
return result.css;
|
|
27
|
+
}
|
|
28
|
+
const DIRECTIVES = ["#for", "#if", "#then", "#else", "#else-if", "#props"];
|
|
29
|
+
function isReactiveExpression(expr) {
|
|
30
|
+
const ast = acorn.parseExpressionAt(expr, 0, { ecmaVersion: 2022 });
|
|
31
|
+
if (["ArrowFunctionExpression", "FunctionExpression"].includes(ast.type)) {
|
|
32
|
+
return false;
|
|
33
|
+
}
|
|
34
|
+
let isReactive = false;
|
|
35
|
+
acornwalk.simple(ast, {
|
|
36
|
+
CallExpression(node) {
|
|
37
|
+
isReactive = true;
|
|
38
|
+
},
|
|
39
|
+
});
|
|
40
|
+
return isReactive;
|
|
41
|
+
}
|
|
42
|
+
const DOUBLE_QUOTE = "__THYN__DOUBLE_QUOTE__";
|
|
43
|
+
const PLACEHOLDER_CHAR = "\uE000";
|
|
44
|
+
function parseAttributes(el) {
|
|
45
|
+
const result = {};
|
|
46
|
+
for (const attr of el.attributes) {
|
|
47
|
+
let { name, value } = attr;
|
|
48
|
+
name = name.replace(/__thyn_attribute_(:?[a-z-]+)/g, (match, kebabName) => {
|
|
49
|
+
const camelCase = kebabName.replace(/-([a-z])/g, (_, letter) => letter.toUpperCase());
|
|
50
|
+
return camelCase;
|
|
51
|
+
});
|
|
52
|
+
if (name.startsWith(":")) {
|
|
53
|
+
if (name.startsWith(":on")) {
|
|
54
|
+
let pipes;
|
|
55
|
+
[name, ...pipes] = name.split(".");
|
|
56
|
+
for (const pipe of pipes) {
|
|
57
|
+
if (pipe === "stop") {
|
|
58
|
+
value = `(e) => { e.stopPropagation(); (${value})(e); }`;
|
|
59
|
+
}
|
|
60
|
+
else if (pipe === "prevent") {
|
|
61
|
+
value = `(e) => { e.preventDefault(); (${value})(e); }`;
|
|
62
|
+
}
|
|
63
|
+
else if (pipe === "enter") {
|
|
64
|
+
value = `(e) => { if (e.key === 'Enter') (${value})(e); }`;
|
|
65
|
+
}
|
|
66
|
+
else if (pipe === "ctrl") {
|
|
67
|
+
value = `(e) => { if (e.ctrlKey) (${value})(e); }`;
|
|
68
|
+
}
|
|
69
|
+
else if (pipe === "meta") {
|
|
70
|
+
value = `(e) => { if (e.metaKey) (${value})(e); }`;
|
|
71
|
+
}
|
|
72
|
+
else if (pipe === "alt") {
|
|
73
|
+
value = `(e) => { if (e.altKey) (${value})(e); }`;
|
|
74
|
+
}
|
|
75
|
+
else if (pipe === "shift") {
|
|
76
|
+
value = `(e) => { if (e.shiftKey) (${value})(e); }`;
|
|
77
|
+
}
|
|
78
|
+
else {
|
|
79
|
+
throw new Error(`Unknown event modifier: ${pipe}`);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
const reactive = value && isReactiveExpression(value);
|
|
84
|
+
if (reactive && name !== ":#for" && name !== ":#if" &&
|
|
85
|
+
name !== ":#else-if" && name !== ":#else" && name !== ":#props") {
|
|
86
|
+
value = `() => ${value}`;
|
|
87
|
+
result[name.slice(1)] = { raw: value.replace(new RegExp(DOUBLE_QUOTE, "g"), '"') };
|
|
88
|
+
}
|
|
89
|
+
else {
|
|
90
|
+
result[name.slice(1)] = { raw: value.replace(new RegExp(DOUBLE_QUOTE, "g"), '"') };
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
else {
|
|
94
|
+
result[name] = { quoted: value };
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
return result;
|
|
98
|
+
}
|
|
99
|
+
function parseTextContent(text) {
|
|
100
|
+
text = text.trim();
|
|
101
|
+
// First, handle escaped braces by temporarily replacing them
|
|
102
|
+
const escapedOpenBrace = '\u0001'; // Use control character as placeholder
|
|
103
|
+
const escapedCloseBrace = '\u0002';
|
|
104
|
+
// Replace escaped braces with placeholders
|
|
105
|
+
text = text.replace(/\\(\{\{|\}\})/g, (match, braces) => {
|
|
106
|
+
return braces === '{{' ? escapedOpenBrace : escapedCloseBrace;
|
|
107
|
+
});
|
|
108
|
+
const regex = /\{\{([^}]+)\}\}/g;
|
|
109
|
+
let lastIndex = 0;
|
|
110
|
+
let match;
|
|
111
|
+
const parts = [];
|
|
112
|
+
let hasReactive = false;
|
|
113
|
+
let hasInterpolations = false;
|
|
114
|
+
while ((match = regex.exec(text)) !== null) {
|
|
115
|
+
const staticText = text.slice(lastIndex, match.index);
|
|
116
|
+
if (staticText) {
|
|
117
|
+
// Restore escaped braces in static text
|
|
118
|
+
const restoredText = staticText
|
|
119
|
+
.replace(new RegExp(escapedOpenBrace, 'g'), '{{')
|
|
120
|
+
.replace(new RegExp(escapedCloseBrace, 'g'), '}}');
|
|
121
|
+
parts.push(restoredText);
|
|
122
|
+
}
|
|
123
|
+
const expr = match[1].trim();
|
|
124
|
+
const isReactive = isReactiveExpression(expr);
|
|
125
|
+
hasReactive || (hasReactive = isReactive);
|
|
126
|
+
hasInterpolations = true;
|
|
127
|
+
parts.push({ expr, isReactive });
|
|
128
|
+
lastIndex = regex.lastIndex;
|
|
129
|
+
}
|
|
130
|
+
if (lastIndex < text.length) {
|
|
131
|
+
// Restore escaped braces in remaining text
|
|
132
|
+
const remainingText = text.slice(lastIndex)
|
|
133
|
+
.replace(new RegExp(escapedOpenBrace, 'g'), '{{')
|
|
134
|
+
.replace(new RegExp(escapedCloseBrace, 'g'), '}}');
|
|
135
|
+
parts.push(remainingText);
|
|
136
|
+
}
|
|
137
|
+
if (!hasInterpolations) {
|
|
138
|
+
// Restore escaped braces if no interpolations
|
|
139
|
+
const finalText = text
|
|
140
|
+
.replace(new RegExp(escapedOpenBrace, 'g'), '{{')
|
|
141
|
+
.replace(new RegExp(escapedCloseBrace, 'g'), '}}');
|
|
142
|
+
return {
|
|
143
|
+
code: `document.createTextNode(\`${escapeTemplateLiteral(finalText)}\`)`,
|
|
144
|
+
hasReactive: false,
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
const interpolated = parts.map((part) => {
|
|
148
|
+
if (typeof part === "string") {
|
|
149
|
+
return escapeTemplateLiteral(part);
|
|
150
|
+
}
|
|
151
|
+
return `$\{${part.expr}\}`;
|
|
152
|
+
}).join("");
|
|
153
|
+
if (hasReactive) {
|
|
154
|
+
let code = `__THYN__CORE__.createReactiveTextNode(() => \`${interpolated}\`)`;
|
|
155
|
+
if (parts.length === 1) {
|
|
156
|
+
const ast = acorn.parseExpressionAt(interpolated.slice(2, -1), 0, {
|
|
157
|
+
ecmaVersion: 2022,
|
|
158
|
+
});
|
|
159
|
+
if (ast.type === "CallExpression" && !ast.arguments.length) {
|
|
160
|
+
code = `__THYN__CORE__.createReactiveTextNode(${interpolated.slice(2, -1).replace(/\(\s*\)\s*$/, "")})`;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
return {
|
|
164
|
+
code,
|
|
165
|
+
hasReactive,
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
if (parts.length === 1) {
|
|
169
|
+
return {
|
|
170
|
+
code: `document.createTextNode(${interpolated.slice(2, -1)})`,
|
|
171
|
+
hasReactive,
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
return {
|
|
175
|
+
code: `document.createTextNode(\`${interpolated}\`)`,
|
|
176
|
+
hasReactive,
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
function generateTextContentTemplate(text, parent, prevSibling) {
|
|
180
|
+
text = text.trim();
|
|
181
|
+
// First, handle escaped braces by temporarily replacing them
|
|
182
|
+
const escapedOpenBrace = '\u0001'; // Use control character as placeholder
|
|
183
|
+
const escapedCloseBrace = '\u0002';
|
|
184
|
+
// Replace escaped braces with placeholders
|
|
185
|
+
text = text.replace(/\\(\{\{|\}\})/g, (match, braces) => {
|
|
186
|
+
return braces === '{{' ? escapedOpenBrace : escapedCloseBrace;
|
|
187
|
+
});
|
|
188
|
+
const regex = /\{\{([^}]+)\}\}/g;
|
|
189
|
+
let lastIndex = 0;
|
|
190
|
+
let match;
|
|
191
|
+
const parts = [];
|
|
192
|
+
let hasReactive = false;
|
|
193
|
+
let hasInterpolations = false;
|
|
194
|
+
while ((match = regex.exec(text)) !== null) {
|
|
195
|
+
const staticText = text.slice(lastIndex, match.index);
|
|
196
|
+
if (staticText) {
|
|
197
|
+
// Restore escaped braces in static text
|
|
198
|
+
const restoredText = staticText
|
|
199
|
+
.replace(new RegExp(escapedOpenBrace, 'g'), '{{')
|
|
200
|
+
.replace(new RegExp(escapedCloseBrace, 'g'), '}}');
|
|
201
|
+
parts.push(restoredText);
|
|
202
|
+
}
|
|
203
|
+
const expr = match[1].trim();
|
|
204
|
+
const isReactive = isReactiveExpression(expr);
|
|
205
|
+
hasReactive || (hasReactive = isReactive);
|
|
206
|
+
hasInterpolations = true;
|
|
207
|
+
parts.push({ expr, isReactive });
|
|
208
|
+
lastIndex = regex.lastIndex;
|
|
209
|
+
}
|
|
210
|
+
if (lastIndex < text.length) {
|
|
211
|
+
// Restore escaped braces in remaining text
|
|
212
|
+
const remainingText = text.slice(lastIndex)
|
|
213
|
+
.replace(new RegExp(escapedOpenBrace, 'g'), '{{')
|
|
214
|
+
.replace(new RegExp(escapedCloseBrace, 'g'), '}}');
|
|
215
|
+
parts.push(remainingText);
|
|
216
|
+
}
|
|
217
|
+
const root = makeVariable();
|
|
218
|
+
if (!hasInterpolations) {
|
|
219
|
+
// Restore escaped braces if no interpolations
|
|
220
|
+
const finalText = text
|
|
221
|
+
.replace(new RegExp(escapedOpenBrace, 'g'), '{{')
|
|
222
|
+
.replace(new RegExp(escapedCloseBrace, 'g'), '}}');
|
|
223
|
+
return {
|
|
224
|
+
static: escapeHtml(finalText),
|
|
225
|
+
dynamic: "",
|
|
226
|
+
root: "",
|
|
227
|
+
staticRoot: root,
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
const interpolated = parts.map((part) => {
|
|
231
|
+
if (typeof part === "string") {
|
|
232
|
+
return escapeTemplateLiteral(part);
|
|
233
|
+
}
|
|
234
|
+
return `$\{${part.expr}\}`;
|
|
235
|
+
}).join("");
|
|
236
|
+
const textNode = prevSibling
|
|
237
|
+
? `${prevSibling}.nextSibling`
|
|
238
|
+
: `${parent}.firstChild`;
|
|
239
|
+
if (hasReactive) {
|
|
240
|
+
let fn = `\`${interpolated}\``;
|
|
241
|
+
if (parts.length === 1) {
|
|
242
|
+
const ast = acorn.parseExpressionAt(interpolated.slice(2, -1), 0, {
|
|
243
|
+
ecmaVersion: 2022,
|
|
244
|
+
});
|
|
245
|
+
if (ast.type === "CallExpression" && !ast.arguments.length) {
|
|
246
|
+
fn = interpolated.slice(2, -1).replace(/\(\s*\)\s*$/, "") + "()";
|
|
247
|
+
}
|
|
248
|
+
else {
|
|
249
|
+
fn = interpolated.slice(2, -1);
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
// Use a space as placeholder for the text node
|
|
253
|
+
const stat = PLACEHOLDER_CHAR;
|
|
254
|
+
const dynamic = `__THYN__CORE__.staticEffect(() => {
|
|
255
|
+
${textNode}.nodeValue = ${fn};
|
|
256
|
+
});\n`;
|
|
257
|
+
return {
|
|
258
|
+
static: stat,
|
|
259
|
+
dynamic,
|
|
260
|
+
root: "",
|
|
261
|
+
staticRoot: root,
|
|
262
|
+
};
|
|
263
|
+
}
|
|
264
|
+
if (parts.length === 1) {
|
|
265
|
+
return {
|
|
266
|
+
dynamic: `${textNode}.nodeValue = ${interpolated.slice(2, -1)};\n`,
|
|
267
|
+
static: PLACEHOLDER_CHAR,
|
|
268
|
+
root: "",
|
|
269
|
+
staticRoot: root,
|
|
270
|
+
};
|
|
271
|
+
}
|
|
272
|
+
return {
|
|
273
|
+
dynamic: `${textNode}.nodeValue = \`${interpolated}\`;\n`,
|
|
274
|
+
static: PLACEHOLDER_CHAR,
|
|
275
|
+
root: "",
|
|
276
|
+
staticRoot: root,
|
|
277
|
+
};
|
|
278
|
+
}
|
|
279
|
+
const NAMESPACE = "__THYN__";
|
|
280
|
+
const HOIST_PREFIX = `${NAMESPACE}HOIST__`;
|
|
281
|
+
function cloneIfNeeded(code) {
|
|
282
|
+
if (code.startsWith(HOIST_PREFIX)) {
|
|
283
|
+
return `${code}.cloneNode()`;
|
|
284
|
+
}
|
|
285
|
+
return code;
|
|
286
|
+
}
|
|
287
|
+
function createHoisting(code, hoist) {
|
|
288
|
+
const existing = hoist.find((h) => h.endsWith(` = ${code};`));
|
|
289
|
+
if (existing) {
|
|
290
|
+
return existing.slice(6, existing.indexOf(" = "));
|
|
291
|
+
}
|
|
292
|
+
const id = `${HOIST_PREFIX}${hoist.length}`;
|
|
293
|
+
hoist.push(`const ${id} = ${code};`);
|
|
294
|
+
return id;
|
|
295
|
+
}
|
|
296
|
+
function createObjectCode(obj) {
|
|
297
|
+
const keys = Object.keys(obj);
|
|
298
|
+
if (!keys.length)
|
|
299
|
+
return "null";
|
|
300
|
+
return `{${keys.map((k) => `'${k}': ${obj[k]}`).join(",")}}`;
|
|
301
|
+
}
|
|
302
|
+
let varId = 0;
|
|
303
|
+
function makeVariable() {
|
|
304
|
+
return `${NAMESPACE}${varId++}`;
|
|
305
|
+
}
|
|
306
|
+
function makeTemplate(node, parent, prevSibling) {
|
|
307
|
+
if (node.nodeType === 3) {
|
|
308
|
+
const text = node.textContent;
|
|
309
|
+
return generateTextContentTemplate(text, parent, prevSibling);
|
|
310
|
+
}
|
|
311
|
+
const tag = node.tagName.toLowerCase();
|
|
312
|
+
const attrs = parseAttributes(node);
|
|
313
|
+
let statRoot = makeVariable();
|
|
314
|
+
let template = `<${tag}`;
|
|
315
|
+
if (!parent) {
|
|
316
|
+
statRoot = "__THYN__template";
|
|
317
|
+
}
|
|
318
|
+
let code = "";
|
|
319
|
+
let dynRoot = makeVariable();
|
|
320
|
+
const childNodes = Array.from(node.childNodes).filter((n) => n.nodeType !== 3 || n.textContent.trim());
|
|
321
|
+
const children = [];
|
|
322
|
+
let ps = undefined;
|
|
323
|
+
for (let i = 0; i < childNodes.length; i++) {
|
|
324
|
+
const cn = childNodes[i];
|
|
325
|
+
const ch = makeTemplate(cn, dynRoot, ps);
|
|
326
|
+
children.push(ch);
|
|
327
|
+
ps = ch.root || `${dynRoot}.childNodes[${i}]`;
|
|
328
|
+
}
|
|
329
|
+
if (!parent) {
|
|
330
|
+
code = `const ${dynRoot} = __THYN__template_generate();\n`;
|
|
331
|
+
}
|
|
332
|
+
else if (!prevSibling) {
|
|
333
|
+
code = `const ${dynRoot} = ${parent}.firstChild;\n`;
|
|
334
|
+
}
|
|
335
|
+
else {
|
|
336
|
+
code = `const ${dynRoot} = ${prevSibling}.nextSibling;\n`;
|
|
337
|
+
}
|
|
338
|
+
for (const [key, val] of Object.entries(attrs)) {
|
|
339
|
+
if (DIRECTIVES.includes(key))
|
|
340
|
+
continue;
|
|
341
|
+
if ("quoted" in val) {
|
|
342
|
+
if (key === "class") {
|
|
343
|
+
template += ` class="${escapeHtml(val.quoted)}"`;
|
|
344
|
+
}
|
|
345
|
+
else {
|
|
346
|
+
template += ` ${key}="${escapeHtml(val.quoted)}"`;
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
template += ">";
|
|
351
|
+
for (const [key, val] of Object.entries(attrs)) {
|
|
352
|
+
if (DIRECTIVES.includes(key))
|
|
353
|
+
continue;
|
|
354
|
+
if (!("raw" in val))
|
|
355
|
+
continue;
|
|
356
|
+
if (key.startsWith("on")) {
|
|
357
|
+
code += `${dynRoot}.${key} = ${val.raw};\n`;
|
|
358
|
+
continue;
|
|
359
|
+
}
|
|
360
|
+
const reactive = isReactiveExpression(val.raw.replace(/^\(\) => /, ""));
|
|
361
|
+
if (reactive) {
|
|
362
|
+
val.raw = val.raw.replace(/^\(\) => /, "");
|
|
363
|
+
if (key === "class") {
|
|
364
|
+
const prev = makeVariable();
|
|
365
|
+
code += `let ${prev};\n`;
|
|
366
|
+
code += `__THYN__CORE__.staticEffect(() => {
|
|
367
|
+
const val = ${val.raw};
|
|
368
|
+
if (val !== ${prev}) {
|
|
369
|
+
if (${prev} = val) ${dynRoot}.className = val;
|
|
370
|
+
else ${dynRoot}.removeAttribute("class");
|
|
371
|
+
}
|
|
372
|
+
});\n`;
|
|
373
|
+
continue;
|
|
374
|
+
}
|
|
375
|
+
if (key.includes("-")) {
|
|
376
|
+
const ran = makeVariable();
|
|
377
|
+
code += `let ${ran};\n`;
|
|
378
|
+
code += `__THYN__CORE__.staticEffect(() => {
|
|
379
|
+
const val = ${val.raw};
|
|
380
|
+
if (val === undefined) {
|
|
381
|
+
if (${ran} && ${dynRoot}.hasAttribute("${key}")) {
|
|
382
|
+
${dynRoot}.removeAttribute("${key}");
|
|
383
|
+
}
|
|
384
|
+
} else {
|
|
385
|
+
${dynRoot}.setAttribute("${key}", val);\n
|
|
386
|
+
}
|
|
387
|
+
${ran} = true;
|
|
388
|
+
});\n`;
|
|
389
|
+
}
|
|
390
|
+
else {
|
|
391
|
+
const ran = makeVariable();
|
|
392
|
+
code += `let ${ran} = false;\n`;
|
|
393
|
+
code += `__THYN__CORE__.staticEffect(() => {
|
|
394
|
+
const val = ${val.raw};
|
|
395
|
+
if (val || ${ran}) {
|
|
396
|
+
${dynRoot}.${key} = val;\n
|
|
397
|
+
}
|
|
398
|
+
${ran} = true;
|
|
399
|
+
});\n`;
|
|
400
|
+
}
|
|
401
|
+
continue;
|
|
402
|
+
}
|
|
403
|
+
if (key === "class") {
|
|
404
|
+
code += `${dynRoot}.className = ${val.raw};\n`;
|
|
405
|
+
continue;
|
|
406
|
+
}
|
|
407
|
+
if (key.includes("-")) {
|
|
408
|
+
code += `${dynRoot}.setAttribute("${key}", ${val.raw});\n`;
|
|
409
|
+
}
|
|
410
|
+
else {
|
|
411
|
+
code += `${dynRoot}.${key} = ${val.raw};\n`;
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
if (children.length) {
|
|
415
|
+
for (let i = 0; i < children.length; i++) {
|
|
416
|
+
const ch = children[i];
|
|
417
|
+
if (ch.static) {
|
|
418
|
+
template += ch.static;
|
|
419
|
+
}
|
|
420
|
+
if (ch.dynamic) {
|
|
421
|
+
code += ch.dynamic;
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
template += `</${tag}>`;
|
|
426
|
+
return {
|
|
427
|
+
root: dynRoot,
|
|
428
|
+
staticRoot: statRoot,
|
|
429
|
+
static: template,
|
|
430
|
+
dynamic: code,
|
|
431
|
+
};
|
|
432
|
+
}
|
|
433
|
+
function walkConditionChain(nodes, i) {
|
|
434
|
+
const chain = [];
|
|
435
|
+
for (; i < nodes.length; i++) {
|
|
436
|
+
const node = nodes[i];
|
|
437
|
+
if (node.nodeType !== 1)
|
|
438
|
+
continue;
|
|
439
|
+
const el = node;
|
|
440
|
+
const attrs = parseAttributes(el);
|
|
441
|
+
if ("#if" in attrs || "#else-if" in attrs || "#else" in attrs) {
|
|
442
|
+
chain.push({ node, attrs });
|
|
443
|
+
}
|
|
444
|
+
else {
|
|
445
|
+
break;
|
|
446
|
+
}
|
|
447
|
+
if ("#else" in attrs)
|
|
448
|
+
break;
|
|
449
|
+
}
|
|
450
|
+
return chain;
|
|
451
|
+
}
|
|
452
|
+
function walk(node, hoist, siblings, index) {
|
|
453
|
+
if (node.nodeType === 3) {
|
|
454
|
+
const text = node.textContent;
|
|
455
|
+
if (!text.trim())
|
|
456
|
+
return null;
|
|
457
|
+
const result = parseTextContent(text);
|
|
458
|
+
return {
|
|
459
|
+
code: result.code,
|
|
460
|
+
isComponent: false,
|
|
461
|
+
hasReactive: result.hasReactive,
|
|
462
|
+
};
|
|
463
|
+
}
|
|
464
|
+
if (node.nodeType !== 1)
|
|
465
|
+
return null;
|
|
466
|
+
const el = node;
|
|
467
|
+
const tag = el.tagName.toLowerCase();
|
|
468
|
+
let isComponent = el.hasAttribute("__thyn_component");
|
|
469
|
+
const makeArg = isComponent
|
|
470
|
+
? el.getAttribute("__thyn_component")
|
|
471
|
+
: `"${tag}"`;
|
|
472
|
+
el.removeAttribute("__thyn_component");
|
|
473
|
+
const attrs = parseAttributes(el);
|
|
474
|
+
if (isComponent)
|
|
475
|
+
el.setAttribute("__thyn_component", makeArg);
|
|
476
|
+
const children = [];
|
|
477
|
+
let skip = 0;
|
|
478
|
+
for (let i = 0; i < node.childNodes.length; i++) {
|
|
479
|
+
const result = walk(node.childNodes[i], hoist, node.childNodes, i);
|
|
480
|
+
if (result) {
|
|
481
|
+
if (skip) {
|
|
482
|
+
skip--;
|
|
483
|
+
continue;
|
|
484
|
+
}
|
|
485
|
+
children.push(result);
|
|
486
|
+
if ("skip" in result) {
|
|
487
|
+
skip = result.skip;
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
const hasReactiveChildren = children.some((c) => c.hasReactive);
|
|
492
|
+
const hasComponentChildren = children.some((c) => c.isComponent);
|
|
493
|
+
let hasReactive = hasReactiveChildren || hasComponentChildren;
|
|
494
|
+
if (tag === "slot") {
|
|
495
|
+
return {
|
|
496
|
+
code: `...$props.slot ?? ${children.map((c) => cloneIfNeeded(c.code)).join(", ") || "[]"}`,
|
|
497
|
+
isComponent: false,
|
|
498
|
+
hasReactive,
|
|
499
|
+
};
|
|
500
|
+
}
|
|
501
|
+
let code = "";
|
|
502
|
+
let hasOwnEffects = false;
|
|
503
|
+
if (isComponent) {
|
|
504
|
+
const props = {};
|
|
505
|
+
let propsDirective = null;
|
|
506
|
+
let propCount = 0;
|
|
507
|
+
for (const [key, val] of Object.entries(attrs)) {
|
|
508
|
+
if (["#for", "#if", "#then", "#else", "#else-if"].includes(key)) {
|
|
509
|
+
continue;
|
|
510
|
+
}
|
|
511
|
+
if (key === "#props") {
|
|
512
|
+
propsDirective = "raw" in val ? val.raw : JSON.stringify(val.quoted);
|
|
513
|
+
continue;
|
|
514
|
+
}
|
|
515
|
+
const value = "raw" in val ? val.raw : JSON.stringify(val.quoted);
|
|
516
|
+
props[key] = value;
|
|
517
|
+
propCount++;
|
|
518
|
+
}
|
|
519
|
+
if (children.length) {
|
|
520
|
+
props.slot = `[${children.map((c) => cloneIfNeeded(c.code)).join(", ")}]`;
|
|
521
|
+
propCount++;
|
|
522
|
+
}
|
|
523
|
+
let propsCode;
|
|
524
|
+
if (propsDirective) {
|
|
525
|
+
propsCode = propsDirective;
|
|
526
|
+
}
|
|
527
|
+
else {
|
|
528
|
+
propsCode = createObjectCode(props);
|
|
529
|
+
}
|
|
530
|
+
code = `__THYN__CORE__.${hasComponentChildren ? 'component' : 'fixedComponent'}(${makeArg}, ${propsCode})`;
|
|
531
|
+
}
|
|
532
|
+
else {
|
|
533
|
+
code = createHoisting(`document.createElement(${makeArg})`, hoist);
|
|
534
|
+
for (const [key, val] of Object.entries(attrs)) {
|
|
535
|
+
if (DIRECTIVES.includes(key))
|
|
536
|
+
continue;
|
|
537
|
+
if ("quoted" in val) {
|
|
538
|
+
if (key === "class" || key.includes("-")) {
|
|
539
|
+
code = createHoisting(`__THYN__CORE__.setAttribute(${cloneIfNeeded(code)}, "${key}", "${val.quoted}")`, hoist);
|
|
540
|
+
}
|
|
541
|
+
else {
|
|
542
|
+
code = createHoisting(`__THYN__CORE__.setProperty(${cloneIfNeeded(code)}, "${key}", "${val.quoted}")`, hoist);
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
for (const [key, val] of Object.entries(attrs)) {
|
|
547
|
+
if (DIRECTIVES.includes(key))
|
|
548
|
+
continue;
|
|
549
|
+
if (!("raw" in val))
|
|
550
|
+
continue;
|
|
551
|
+
if (key.startsWith("on")) {
|
|
552
|
+
code = `__THYN__CORE__.setProperty(${cloneIfNeeded(code)}, "${key}", ${val.raw})`;
|
|
553
|
+
continue;
|
|
554
|
+
}
|
|
555
|
+
const reactive = isReactiveExpression(val.raw.replace(/^\(\) => /, ""));
|
|
556
|
+
if (reactive) {
|
|
557
|
+
hasOwnEffects = true;
|
|
558
|
+
hasReactive = true;
|
|
559
|
+
if (key === "class" || key.includes("-")) {
|
|
560
|
+
code = `__THYN__CORE__.setReactiveAttribute(${cloneIfNeeded(code)}, "${key}", ${val.raw})`;
|
|
561
|
+
}
|
|
562
|
+
else {
|
|
563
|
+
code = `__THYN__CORE__.setReactiveProperty(${cloneIfNeeded(code)}, "${key}", ${val.raw})`;
|
|
564
|
+
}
|
|
565
|
+
continue;
|
|
566
|
+
}
|
|
567
|
+
if (key === "class" || key.includes("-")) {
|
|
568
|
+
code = `__THYN__CORE__.setAttribute(${cloneIfNeeded(code)}, "${key}", ${val.raw})`;
|
|
569
|
+
}
|
|
570
|
+
else {
|
|
571
|
+
code = `__THYN__CORE__.setProperty(${cloneIfNeeded(code)}, "${key}", ${val.raw})`;
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
if (children.length) {
|
|
575
|
+
code = `__THYN__CORE__.addChildren(${cloneIfNeeded(code)}, [${children.map((c) => cloneIfNeeded(c.code)).join(", ")}])`;
|
|
576
|
+
}
|
|
577
|
+
if (!hasOwnEffects && hasReactiveChildren) {
|
|
578
|
+
code = `__THYN__CORE__.markAsReactive(${cloneIfNeeded(code)})`;
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
if ("#for" in attrs && "raw" in attrs["#for"]) {
|
|
582
|
+
const isolated = siblings ? Array.from(siblings).filter((n) => n.nodeType !== 3 || n.textContent.trim()).length === 1 : true;
|
|
583
|
+
const forAttr = attrs["#for"].raw;
|
|
584
|
+
const [item, iterable] = forAttr.split(" in ").map((s) => s.trim());
|
|
585
|
+
code = `__THYN__CORE__.component(${hasComponentChildren
|
|
586
|
+
? "__THYN__CORE__.list"
|
|
587
|
+
: (isolated ? "__THYN__CORE__.isolatedTerminalList" : "__THYN__CORE__.terminalList")}, {
|
|
588
|
+
items: () => ${iterable},
|
|
589
|
+
render: (${item}) => ${code},
|
|
590
|
+
})`;
|
|
591
|
+
isComponent = true;
|
|
592
|
+
}
|
|
593
|
+
if ("#if" in attrs) {
|
|
594
|
+
const chain = walkConditionChain(siblings, index);
|
|
595
|
+
const branches = [];
|
|
596
|
+
for (const entry of chain) {
|
|
597
|
+
const subEl = entry.node;
|
|
598
|
+
const subAttrs = entry.attrs;
|
|
599
|
+
subEl.removeAttribute(":#if");
|
|
600
|
+
subEl.removeAttribute(":#else-if");
|
|
601
|
+
subEl.removeAttribute(":#else");
|
|
602
|
+
const ch = walk(subEl, hoist);
|
|
603
|
+
if ("#if" in subAttrs || "#else-if" in subAttrs) {
|
|
604
|
+
const cond = subAttrs["#if"]?.raw ?? subAttrs["#else-if"].raw;
|
|
605
|
+
branches.push(`{ if: () => ${cond}, then: () => ${ch.code} }`);
|
|
606
|
+
}
|
|
607
|
+
else if ("#else" in subAttrs) {
|
|
608
|
+
branches.push(`{ then: () => ${ch.code} }`);
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
code = `__THYN__CORE__.component(__THYN__CORE__.show, [\n${branches.join(",\n")}\n])`;
|
|
612
|
+
return {
|
|
613
|
+
code,
|
|
614
|
+
isComponent: true,
|
|
615
|
+
hasReactive: true,
|
|
616
|
+
skip: chain.length - 1,
|
|
617
|
+
};
|
|
618
|
+
}
|
|
619
|
+
return {
|
|
620
|
+
code,
|
|
621
|
+
isComponent,
|
|
622
|
+
hasReactive: hasOwnEffects || isComponent || hasReactiveChildren,
|
|
623
|
+
};
|
|
624
|
+
}
|
|
625
|
+
function hasComponentChildren(node) {
|
|
626
|
+
if (node.nodeType === 3) {
|
|
627
|
+
return false;
|
|
628
|
+
}
|
|
629
|
+
if (node.nodeType !== 1)
|
|
630
|
+
return false;
|
|
631
|
+
const tag = node.tagName.toLowerCase();
|
|
632
|
+
if (tag === "slot")
|
|
633
|
+
return true;
|
|
634
|
+
let isComponent = node.hasAttribute("__thyn_component");
|
|
635
|
+
if (isComponent)
|
|
636
|
+
return true;
|
|
637
|
+
for (const attr of node.attributes) {
|
|
638
|
+
if ([":#for", ":#if", ":#else", ":#else-if"].includes(attr.name)) {
|
|
639
|
+
return true;
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
return Array.from(node.childNodes).some((n) => hasComponentChildren(n));
|
|
643
|
+
}
|
|
644
|
+
const COMPONENT_TAG_REGEX = /<\/?([A-Z][a-zA-Z0-9]*)(\s(?:[^"'<>\/]|"[^"]*"|'[^']*')*)?(\/?)>/g;
|
|
645
|
+
function convertToColonBindings(html) {
|
|
646
|
+
let result = '';
|
|
647
|
+
let i = 0;
|
|
648
|
+
while (i < html.length) {
|
|
649
|
+
const start = html.indexOf('={', i);
|
|
650
|
+
if (start === -1) {
|
|
651
|
+
result += html.slice(i);
|
|
652
|
+
break;
|
|
653
|
+
}
|
|
654
|
+
// Check if we're inside escaped HTML (< ... >)
|
|
655
|
+
let beforeStart = html.slice(0, start);
|
|
656
|
+
let lastLt = beforeStart.lastIndexOf('<');
|
|
657
|
+
let lastAmpLt = beforeStart.lastIndexOf('<');
|
|
658
|
+
// If the last < is more recent than the last <, we're in escaped HTML
|
|
659
|
+
if (lastAmpLt > lastLt) {
|
|
660
|
+
result += html.slice(i, start + 2);
|
|
661
|
+
i = start + 2;
|
|
662
|
+
continue;
|
|
663
|
+
}
|
|
664
|
+
// Find the most recent < and > before our position
|
|
665
|
+
let lastTagOpen = beforeStart.lastIndexOf('<');
|
|
666
|
+
let lastTagClose = beforeStart.lastIndexOf('>');
|
|
667
|
+
// If there's no unclosed tag (last > is more recent than last <), skip
|
|
668
|
+
if (lastTagClose >= lastTagOpen) {
|
|
669
|
+
result += html.slice(i, start + 2);
|
|
670
|
+
i = start + 2;
|
|
671
|
+
continue;
|
|
672
|
+
}
|
|
673
|
+
// Find the attribute name by going backwards
|
|
674
|
+
let attrEnd = start - 1;
|
|
675
|
+
while (attrEnd >= 0 && /\s/.test(html[attrEnd]))
|
|
676
|
+
attrEnd--;
|
|
677
|
+
let attrStart = attrEnd;
|
|
678
|
+
// Updated to include dots in attribute names
|
|
679
|
+
while (attrStart >= 0 && /[a-zA-Z0-9_#.-]/.test(html[attrStart])) {
|
|
680
|
+
attrStart--;
|
|
681
|
+
}
|
|
682
|
+
attrStart++; // Move to first character of attribute name
|
|
683
|
+
if (attrStart > attrEnd) {
|
|
684
|
+
// No valid attribute name found
|
|
685
|
+
result += html.slice(i, start + 2);
|
|
686
|
+
i = start + 2;
|
|
687
|
+
continue;
|
|
688
|
+
}
|
|
689
|
+
const attrName = html.slice(attrStart, attrEnd + 1);
|
|
690
|
+
// Updated validation to allow dots in attribute names
|
|
691
|
+
if (!/^[#a-zA-Z_][\w\-\.]*$/.test(attrName)) {
|
|
692
|
+
result += html.slice(i, start + 2);
|
|
693
|
+
i = start + 2;
|
|
694
|
+
continue;
|
|
695
|
+
}
|
|
696
|
+
// Parse balanced {}
|
|
697
|
+
let braceCount = 1;
|
|
698
|
+
let j = start + 2;
|
|
699
|
+
while (j < html.length && braceCount > 0) {
|
|
700
|
+
if (html[j] === '{')
|
|
701
|
+
braceCount++;
|
|
702
|
+
else if (html[j] === '}')
|
|
703
|
+
braceCount--;
|
|
704
|
+
j++;
|
|
705
|
+
}
|
|
706
|
+
if (braceCount !== 0) {
|
|
707
|
+
// Unbalanced braces, treat as raw
|
|
708
|
+
result += html.slice(i, j);
|
|
709
|
+
i = j;
|
|
710
|
+
continue;
|
|
711
|
+
}
|
|
712
|
+
const jsExpr = html.slice(start + 2, j - 1).trim();
|
|
713
|
+
const prefix = attrName.startsWith('#') ? ':#' + attrName.slice(1) : ':' + attrName;
|
|
714
|
+
const beforeAttr = html.slice(i, attrStart);
|
|
715
|
+
result += beforeAttr + `${prefix}="${jsExpr.replace(/"/g, DOUBLE_QUOTE)}"`;
|
|
716
|
+
i = j;
|
|
717
|
+
}
|
|
718
|
+
return result;
|
|
719
|
+
}
|
|
720
|
+
function preprocessHTML(html) {
|
|
721
|
+
html = convertToColonBindings(html);
|
|
722
|
+
html = addComponentAttributes(html);
|
|
723
|
+
html = preserveCamelCaseAttributes(html);
|
|
724
|
+
return html;
|
|
725
|
+
}
|
|
726
|
+
function preserveCamelCaseAttributes(html) {
|
|
727
|
+
return html.replace(/<([^>]+)>/g, (match, tagContent) => {
|
|
728
|
+
const processedContent = tagContent.replace(/\s+(:?[a-z][a-zA-Z]*[A-Z][a-zA-Z]*)\s*=/g, (_attrMatch, attrName) => {
|
|
729
|
+
const kebabCase = attrName.replace(/[A-Z]/g, (letter) => `-${letter.toLowerCase()}`);
|
|
730
|
+
return ` __thyn_attribute_${kebabCase}=`;
|
|
731
|
+
});
|
|
732
|
+
return `<${processedContent}>`;
|
|
733
|
+
});
|
|
734
|
+
}
|
|
735
|
+
function addComponentAttributes(html) {
|
|
736
|
+
let processedHTML = html;
|
|
737
|
+
processedHTML = processedHTML.replace(COMPONENT_TAG_REGEX, (componentMatch, componentName, attributes, selfClose) => {
|
|
738
|
+
const isClosing = componentMatch.startsWith("</");
|
|
739
|
+
return isClosing
|
|
740
|
+
? "</div>"
|
|
741
|
+
: selfClose
|
|
742
|
+
? `<div${attributes || ""} __thyn_component="${componentName}"></div>`
|
|
743
|
+
: `<div${attributes || ""} __thyn_component="${componentName}">`;
|
|
744
|
+
});
|
|
745
|
+
return processedHTML;
|
|
746
|
+
}
|
|
747
|
+
function addScopeId(el, scopeId) {
|
|
748
|
+
el.classList.add(scopeId);
|
|
749
|
+
for (const child of el.children) {
|
|
750
|
+
addScopeId(child, scopeId);
|
|
751
|
+
}
|
|
752
|
+
}
|
|
753
|
+
function removeUnusedThynVars(code) {
|
|
754
|
+
const varDeclRE = /const (\__THYN__\d+) = ([^\n;]+);/g;
|
|
755
|
+
const varToExpr = new Map();
|
|
756
|
+
const exprDeps = new Map();
|
|
757
|
+
const allVars = new Set();
|
|
758
|
+
const directlyUsed = new Set();
|
|
759
|
+
// 1. Collect variable declarations
|
|
760
|
+
let match;
|
|
761
|
+
while ((match = varDeclRE.exec(code))) {
|
|
762
|
+
const name = match[1];
|
|
763
|
+
const expr = match[2];
|
|
764
|
+
varToExpr.set(name, expr);
|
|
765
|
+
allVars.add(name);
|
|
766
|
+
// Track dependencies (i.e., const a = b.c → b is a dependency)
|
|
767
|
+
const deps = new Set();
|
|
768
|
+
for (const dep of expr.match(/\__THYN__\d+/g) || []) {
|
|
769
|
+
deps.add(dep);
|
|
770
|
+
}
|
|
771
|
+
exprDeps.set(name, deps);
|
|
772
|
+
}
|
|
773
|
+
// 2. Detect any variable used outside its own declaration
|
|
774
|
+
for (const name of allVars) {
|
|
775
|
+
const usageRE = new RegExp(`\\b${name}\\b`, "g");
|
|
776
|
+
while ((match = usageRE.exec(code))) {
|
|
777
|
+
const idx = match.index;
|
|
778
|
+
const lineStart = code.lastIndexOf("\n", idx);
|
|
779
|
+
const line = code.slice(lineStart + 1, code.indexOf("\n", idx + 1));
|
|
780
|
+
if (!line.startsWith(`const ${name} =`)) {
|
|
781
|
+
directlyUsed.add(name);
|
|
782
|
+
break;
|
|
783
|
+
}
|
|
784
|
+
}
|
|
785
|
+
}
|
|
786
|
+
// 3. Add any variable that is `return`ed
|
|
787
|
+
const returnRE = /return (\__THYN__\d+);/g;
|
|
788
|
+
while ((match = returnRE.exec(code))) {
|
|
789
|
+
directlyUsed.add(match[1]);
|
|
790
|
+
}
|
|
791
|
+
// 4. Walk transitive dependency graph
|
|
792
|
+
const keep = new Set(directlyUsed);
|
|
793
|
+
const stack = [...directlyUsed];
|
|
794
|
+
while (stack.length) {
|
|
795
|
+
const current = stack.pop();
|
|
796
|
+
const deps = exprDeps.get(current);
|
|
797
|
+
if (!deps)
|
|
798
|
+
continue;
|
|
799
|
+
for (const dep of deps) {
|
|
800
|
+
if (!keep.has(dep)) {
|
|
801
|
+
keep.add(dep);
|
|
802
|
+
stack.push(dep);
|
|
803
|
+
}
|
|
804
|
+
}
|
|
805
|
+
}
|
|
806
|
+
// 5. Remove any declaration not in the keep set
|
|
807
|
+
const cleaned = code.replace(varDeclRE, (decl, name) => {
|
|
808
|
+
return keep.has(name) ? decl : "";
|
|
809
|
+
});
|
|
810
|
+
return cleaned;
|
|
811
|
+
}
|
|
812
|
+
async function transformHTMLtoJSX(html, style) {
|
|
813
|
+
const scopeId = `thyn-${(styleId++).toString(36)}`;
|
|
814
|
+
const processedHTML = preprocessHTML(html);
|
|
815
|
+
const template = parseHTML("<template>" + processedHTML + "</template>");
|
|
816
|
+
const rootElement = template.content.firstElementChild;
|
|
817
|
+
let scopedStyle = null;
|
|
818
|
+
if (style) {
|
|
819
|
+
addScopeId(rootElement, scopeId);
|
|
820
|
+
scopedStyle = await scopeSelectors(style, scopeId);
|
|
821
|
+
}
|
|
822
|
+
if (hasComponentChildren(rootElement)) {
|
|
823
|
+
const hoist = [];
|
|
824
|
+
const { code } = walk(rootElement, hoist);
|
|
825
|
+
const root = makeVariable();
|
|
826
|
+
return [root, `const ${root} = ${code};`, hoist, scopedStyle];
|
|
827
|
+
}
|
|
828
|
+
const { root, static: tmpl, dynamic } = makeTemplate(rootElement);
|
|
829
|
+
const hoist = [`
|
|
830
|
+
let __THYN__template;
|
|
831
|
+
function __THYN__template_generate() {
|
|
832
|
+
if (!__THYN__template) {
|
|
833
|
+
const t = document.createElement("template");
|
|
834
|
+
t.innerHTML = \`${escapeTemplateLiteral(tmpl)}\`;
|
|
835
|
+
const w = document.createTreeWalker(t.content, 4);
|
|
836
|
+
while(w.nextNode()) {
|
|
837
|
+
if (w.currentNode.nodeValue === "\\uE000") w.currentNode.nodeValue = "";
|
|
838
|
+
}
|
|
839
|
+
__THYN__template = t.content.firstChild;
|
|
840
|
+
}
|
|
841
|
+
return __THYN__template.cloneNode(true);
|
|
842
|
+
}`];
|
|
843
|
+
return [root, dynamic, hoist, scopedStyle];
|
|
844
|
+
}
|
|
845
|
+
async function transformTypeScript(code, id) {
|
|
846
|
+
try {
|
|
847
|
+
const esbuild = await esbuildModule;
|
|
848
|
+
const result = await esbuild.transform(code, {
|
|
849
|
+
loader: "ts",
|
|
850
|
+
target: "es2022",
|
|
851
|
+
format: "esm",
|
|
852
|
+
sourcemap: true,
|
|
853
|
+
sourcefile: id,
|
|
854
|
+
});
|
|
855
|
+
return {
|
|
856
|
+
code: result.code,
|
|
857
|
+
map: result.map,
|
|
858
|
+
};
|
|
859
|
+
}
|
|
860
|
+
catch (error) {
|
|
861
|
+
throw new Error(`TypeScript compilation failed for ${id}: ${error.message}`);
|
|
862
|
+
}
|
|
863
|
+
}
|
|
864
|
+
export async function transformSFC(source, id) {
|
|
865
|
+
const name = id.split("/").pop()?.replace(/\.thyn$/, "");
|
|
866
|
+
const { script, scriptLang, html, style } = extractParts(source);
|
|
867
|
+
const { imports, body } = splitScript(script);
|
|
868
|
+
const s = new MagicString("");
|
|
869
|
+
if (!imports.some((imp) => imp.includes("$signal"))) {
|
|
870
|
+
s.prepend("import { $signal } from '@thyn/core';\n");
|
|
871
|
+
}
|
|
872
|
+
if (!imports.some((imp) => imp.includes("$effect"))) {
|
|
873
|
+
s.prepend("import { $effect } from '@thyn/core';\n");
|
|
874
|
+
}
|
|
875
|
+
s.prepend("import * as __THYN__CORE__ from '@thyn/core';\n");
|
|
876
|
+
s.append(imports.join("\n") + "\n");
|
|
877
|
+
let [root, transformed, hoist, scopedStyle] = await transformHTMLtoJSX(html, style);
|
|
878
|
+
s.append(hoist.join("\n") + "\n");
|
|
879
|
+
s.append([
|
|
880
|
+
"",
|
|
881
|
+
`export default function ${name}($props) {`,
|
|
882
|
+
...body.map((l) => " " + l),
|
|
883
|
+
removeUnusedThynVars(` ${transformed} return ${root};`),
|
|
884
|
+
`}`,
|
|
885
|
+
].join("\n"));
|
|
886
|
+
let output = s.toString();
|
|
887
|
+
let sourceMap = s.generateMap({
|
|
888
|
+
source: id,
|
|
889
|
+
includeContent: true,
|
|
890
|
+
hires: true,
|
|
891
|
+
});
|
|
892
|
+
return { output, sourceMap, scopedStyle, scriptLang };
|
|
893
|
+
}
|
|
894
|
+
export async function compileSFC(source, id) {
|
|
895
|
+
let { scriptLang, output, sourceMap, scopedStyle } = await transformSFC(source, id);
|
|
896
|
+
if (scriptLang === "ts" || scriptLang === "typescript") {
|
|
897
|
+
const tsResult = await transformTypeScript(output, id);
|
|
898
|
+
output = tsResult.code;
|
|
899
|
+
if (tsResult.map) {
|
|
900
|
+
// @ts-expect-error
|
|
901
|
+
sourceMap = tsResult.map;
|
|
902
|
+
}
|
|
903
|
+
}
|
|
904
|
+
return {
|
|
905
|
+
js: output,
|
|
906
|
+
css: scopedStyle || null,
|
|
907
|
+
cssModuleId: scopedStyle ? `${id}.css` : null,
|
|
908
|
+
map: sourceMap,
|
|
909
|
+
};
|
|
910
|
+
}
|
|
911
|
+
async function compileThynScript(source, id) {
|
|
912
|
+
const s = new MagicString(source);
|
|
913
|
+
const { imports } = splitScript(source);
|
|
914
|
+
if (!imports.some((imp) => imp.includes("$signal"))) {
|
|
915
|
+
s.prepend("import { $signal } from '@thyn/core';\n");
|
|
916
|
+
}
|
|
917
|
+
if (!imports.some((imp) => imp.includes("$effect"))) {
|
|
918
|
+
s.prepend("import { $effect } from '@thyn/core';\n");
|
|
919
|
+
}
|
|
920
|
+
let output = s.toString();
|
|
921
|
+
let sourceMap = s.generateMap({
|
|
922
|
+
source: id,
|
|
923
|
+
includeContent: true,
|
|
924
|
+
hires: true,
|
|
925
|
+
});
|
|
926
|
+
if (id.endsWith(".thyn.ts")) {
|
|
927
|
+
const tsResult = await transformTypeScript(output, id);
|
|
928
|
+
output = tsResult.code;
|
|
929
|
+
if (tsResult.map) {
|
|
930
|
+
// @ts-expect-error
|
|
931
|
+
sourceMap = tsResult.map;
|
|
932
|
+
}
|
|
933
|
+
}
|
|
934
|
+
return {
|
|
935
|
+
code: output,
|
|
936
|
+
map: sourceMap,
|
|
937
|
+
};
|
|
938
|
+
}
|
|
939
|
+
let styleId = 0;
|
|
940
|
+
export default function thyn() {
|
|
941
|
+
const collectedCSS = [];
|
|
942
|
+
let isDev = false;
|
|
943
|
+
return {
|
|
944
|
+
name: "thyn",
|
|
945
|
+
enforce: "pre",
|
|
946
|
+
configResolved(config) {
|
|
947
|
+
isDev = config.command === "serve";
|
|
948
|
+
},
|
|
949
|
+
buildStart() {
|
|
950
|
+
collectedCSS.length = 0;
|
|
951
|
+
},
|
|
952
|
+
async transform(code, id) {
|
|
953
|
+
if (id.endsWith(".thyn.js") || id.endsWith(".thyn.ts")) {
|
|
954
|
+
return await compileThynScript(code, id);
|
|
955
|
+
}
|
|
956
|
+
if (!id.endsWith(".thyn"))
|
|
957
|
+
return;
|
|
958
|
+
const { js, css, map } = await compileSFC(code, id);
|
|
959
|
+
let finalCode = js;
|
|
960
|
+
if (isDev && css) {
|
|
961
|
+
const escapedCSS = JSON.stringify(css);
|
|
962
|
+
finalCode += `\n
|
|
963
|
+
if (typeof document !== 'undefined') {
|
|
964
|
+
const style = document.createElement('style');
|
|
965
|
+
style.textContent = ${escapedCSS};
|
|
966
|
+
document.head.appendChild(style);
|
|
967
|
+
}`;
|
|
968
|
+
}
|
|
969
|
+
else if (css) {
|
|
970
|
+
collectedCSS.push(css);
|
|
971
|
+
}
|
|
972
|
+
return {
|
|
973
|
+
code: finalCode,
|
|
974
|
+
map,
|
|
975
|
+
};
|
|
976
|
+
},
|
|
977
|
+
transformIndexHtml(html) {
|
|
978
|
+
if (collectedCSS.length === 0)
|
|
979
|
+
return html;
|
|
980
|
+
const s = new MagicString(html);
|
|
981
|
+
const headCloseIndex = html.indexOf("</head>");
|
|
982
|
+
if (headCloseIndex !== -1) {
|
|
983
|
+
if (isDev) {
|
|
984
|
+
const combinedCSS = collectedCSS.join("\n");
|
|
985
|
+
s.appendLeft(headCloseIndex, ` <style>\n${combinedCSS}\n </style>\n`);
|
|
986
|
+
}
|
|
987
|
+
else {
|
|
988
|
+
s.appendLeft(headCloseIndex, ' <link rel="stylesheet" href="/main.css">\n');
|
|
989
|
+
}
|
|
990
|
+
}
|
|
991
|
+
return s.toString();
|
|
992
|
+
},
|
|
993
|
+
async generateBundle() {
|
|
994
|
+
if (isDev || collectedCSS.length === 0)
|
|
995
|
+
return;
|
|
996
|
+
const combinedCSS = collectedCSS.join("\n");
|
|
997
|
+
const esbuild = await esbuildModule;
|
|
998
|
+
const result = await esbuild.transform(combinedCSS, {
|
|
999
|
+
loader: "css",
|
|
1000
|
+
minify: true,
|
|
1001
|
+
});
|
|
1002
|
+
this.emitFile({
|
|
1003
|
+
type: "asset",
|
|
1004
|
+
fileName: "main.css",
|
|
1005
|
+
source: result.code,
|
|
1006
|
+
});
|
|
1007
|
+
},
|
|
1008
|
+
};
|
|
1009
|
+
}
|