@thyn/core 0.0.344 → 0.0.346

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