@thyn/vite-plugin 0.0.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/src/index.ts ADDED
@@ -0,0 +1,822 @@
1
+ import * as acorn from "acorn";
2
+ import * as acornwalk from "acorn-walk";
3
+ import * as esbuild from "esbuild";
4
+ import { JSDOM } from "jsdom";
5
+ import MagicString from "magic-string";
6
+ import { extractParts, splitScript } from "./utils.js";
7
+
8
+ function isReactiveExpression(expr) {
9
+ const ast = acorn.parseExpressionAt(expr, 0, { ecmaVersion: 2022 });
10
+ if (["ArrowFunctionExpression", "FunctionExpression"].includes(ast.type)) {
11
+ return false;
12
+ }
13
+ let isReactive = false;
14
+ acornwalk.simple(ast, {
15
+ CallExpression(node) {
16
+ isReactive = true;
17
+ },
18
+ });
19
+ return isReactive;
20
+ }
21
+
22
+ function parseAttributes(el) {
23
+ const result: { [key: string]: { raw: string } | { quoted: string } } = {};
24
+ for (const attr of el.attributes) {
25
+ let { name, value } = attr;
26
+ name = name.replace(
27
+ /__thyn_attribute_(:?[a-z-]+)/g,
28
+ (match, kebabName) => {
29
+ // Convert kebab-case back to camelCase
30
+ const camelCase = kebabName.replace(
31
+ /-([a-z])/g,
32
+ (_, letter) => letter.toUpperCase(),
33
+ );
34
+ return camelCase;
35
+ },
36
+ );
37
+ if (name.startsWith(":")) {
38
+ if (name.startsWith(":on")) {
39
+ let pipes;
40
+ [name, ...pipes] = name.split(".");
41
+ for (const pipe of pipes) {
42
+ if (pipe === "stop") {
43
+ value = `(e) => { e.stopPropagation(); (${value})(e); }`;
44
+ } else if (pipe === "prevent") {
45
+ value = `(e) => { e.preventDefault(); (${value})(e); }`;
46
+ } else if (pipe === "enter") {
47
+ value = `(e) => { if (e.key === 'Enter') (${value})(e); }`;
48
+ } else if (pipe === "ctrl") {
49
+ value = `(e) => { if (e.ctrlKey) (${value})(e); }`;
50
+ } else if (pipe === "meta") {
51
+ value = `(e) => { if (e.metaKey) (${value})(e); }`;
52
+ } else if (pipe === "alt") {
53
+ value = `(e) => { if (e.altKey) (${value})(e); }`;
54
+ } else if (pipe === "shift") {
55
+ value = `(e) => { if (e.shiftKey) (${value})(e); }`;
56
+ } else {
57
+ throw new Error(`Unknown event modifier: ${pipe}`);
58
+ }
59
+ }
60
+ }
61
+ const reactive = isReactiveExpression(value);
62
+ if (reactive && name !== ":key" && name !== ":each" && name !== ":if") {
63
+ value = `() => ${value}`;
64
+ result[name.slice(1)] = { raw: value };
65
+ } else {
66
+ result[name.slice(1)] = { raw: value };
67
+ }
68
+ } else {
69
+ result[name] = { quoted: value };
70
+ }
71
+ }
72
+ return result;
73
+ }
74
+
75
+ function parseTextContent(text: string) {
76
+ text = text.trim();
77
+ const regex = /\{\{([^}]+)\}\}/g;
78
+ let lastIndex = 0;
79
+ let match;
80
+ const parts = [];
81
+ let hasReactive = false;
82
+ let hasInterpolations = false;
83
+ while ((match = regex.exec(text)) !== null) {
84
+ const staticText = text.slice(lastIndex, match.index);
85
+ if (staticText) {
86
+ parts.push(staticText);
87
+ }
88
+ const expr = match[1].trim();
89
+ const isReactive = isReactiveExpression(expr);
90
+ hasReactive || (hasReactive = isReactive);
91
+ hasInterpolations = true;
92
+ parts.push({ expr, isReactive });
93
+ lastIndex = regex.lastIndex;
94
+ }
95
+ if (lastIndex < text.length) {
96
+ parts.push(text.slice(lastIndex));
97
+ }
98
+ if (!hasInterpolations) {
99
+ return { code: `document.createTextNode(\`${text}\`)`, hasReactive: false }; // plain text
100
+ }
101
+ const interpolated = parts.map((part) => {
102
+ if (typeof part === "string") {
103
+ return part.replace(/[`\\$]/g, "\\$&"); // escape backticks and ${
104
+ }
105
+ return `$\{${part.expr}\}`;
106
+ }).join("");
107
+ if (hasReactive) {
108
+ let code = `__SPARKI__CORE__.createReactiveTextNode(() => \`${interpolated}\`)`;
109
+ const ast = acorn.parseExpressionAt(interpolated.slice(2, -1), 0, {
110
+ ecmaVersion: 2022,
111
+ });
112
+ if (ast.type === "CallExpression" && !ast.arguments.length) {
113
+ code = `__SPARKI__CORE__.createReactiveTextNode(${interpolated.slice(2, -1).replace(/\(\s*\)\s*$/, "")})`;
114
+ }
115
+ return {
116
+ code,
117
+ hasReactive,
118
+ };
119
+ }
120
+ if (parts.length === 1) {
121
+ return {
122
+ code: `document.createTextNode(${interpolated.slice(2, -1)})`,
123
+ hasReactive,
124
+ };
125
+ }
126
+ return {
127
+ code: `document.createTextNode(\`${interpolated}\`)`,
128
+ hasReactive,
129
+ };
130
+ }
131
+
132
+ function generateTextContentTemplate(text: string, parent: string, prevSibling?: string): { root: string, static: string, dynamic: string, staticRoot: string } {
133
+ text = text.trim();
134
+ const regex = /\{\{([^}]+)\}\}/g;
135
+ let lastIndex = 0;
136
+ let match;
137
+ const parts = [];
138
+ let hasReactive = false;
139
+ let hasInterpolations = false;
140
+ while ((match = regex.exec(text)) !== null) {
141
+ const staticText = text.slice(lastIndex, match.index);
142
+ if (staticText) {
143
+ parts.push(staticText);
144
+ }
145
+ const expr = match[1].trim();
146
+ const isReactive = isReactiveExpression(expr);
147
+ hasReactive || (hasReactive = isReactive);
148
+ hasInterpolations = true;
149
+ parts.push({ expr, isReactive });
150
+ lastIndex = regex.lastIndex;
151
+ }
152
+ if (lastIndex < text.length) {
153
+ parts.push(text.slice(lastIndex));
154
+ }
155
+ const root = makeVariable();
156
+ if (!hasInterpolations) {
157
+ return { static: `const ${root} = document.createTextNode(\`${text}\`);\n`, dynamic: "", root: "", staticRoot: root }; // plain text
158
+ }
159
+ const interpolated = parts.map((part) => {
160
+ if (typeof part === "string") {
161
+ return part.replace(/[`\\$]/g, "\\$&"); // escape backticks and ${
162
+ }
163
+ return `$\{${part.expr}\}`;
164
+ }).join("");
165
+ const textNode = prevSibling ? `${prevSibling}.nextSibling` : `${parent}.firstChild`;
166
+ if (hasReactive) {
167
+ let fn = `(() => \`${interpolated}\`)`;
168
+ const ast = acorn.parseExpressionAt(interpolated.slice(2, -1), 0, {
169
+ ecmaVersion: 2022,
170
+ });
171
+ if (ast.type === "CallExpression" && !ast.arguments.length) {
172
+ fn = interpolated.slice(2, -1).replace(/\(\s*\)\s*$/, "");
173
+ }
174
+ const stat = `const ${root} = document.createTextNode("");\n`;
175
+ const dynamic = `$effect(() => {
176
+ ${textNode}.nodeValue = ${fn}();
177
+ });\n`;
178
+ return {
179
+ static: stat,
180
+ dynamic,
181
+ root: "",
182
+ staticRoot: root,
183
+ };
184
+ }
185
+ if (parts.length === 1) {
186
+ return {
187
+ dynamic: `${textNode}.nodeValue = ${interpolated.slice(2, -1)};\n`,
188
+ static: `const ${root} = document.createTextNode("");\n`,
189
+ root: "",
190
+ staticRoot: root,
191
+ };
192
+ }
193
+ return {
194
+ dynamic: `${textNode}.nodeValue = \`${interpolated}\`;\n`,
195
+ static: `const ${root} = document.createTextNode("");\n`,
196
+ root: "",
197
+ staticRoot: root,
198
+ };
199
+ }
200
+
201
+ const NAMESPACE = "__SPARKI__";
202
+ const HOIST_PREFIX = `${NAMESPACE}HOIST__`;
203
+
204
+ function cloneIfNeeded(code: string): string {
205
+ if (code.startsWith(HOIST_PREFIX)) {
206
+ return `${code}.cloneNode()`;
207
+ }
208
+ return code;
209
+ }
210
+
211
+ function createHoisting(code: string, hoist: string[]) {
212
+ const existing = hoist.find((h) => h.endsWith(` = ${code};`));
213
+ if (existing) {
214
+ return existing.slice(6, existing.indexOf(" = "));
215
+ }
216
+ const id = `${HOIST_PREFIX}${hoist.length}`;
217
+ hoist.push(`const ${id} = ${code};`);
218
+ return id;
219
+ }
220
+
221
+ function createObjectCode(obj: object): string {
222
+ const keys = Object.keys(obj);
223
+ if (!keys.length) return "null";
224
+ return `{${keys.map((k) => `'${k}': ${obj[k]}`).join(",")}}`;
225
+ }
226
+
227
+ let varId = 0;
228
+
229
+ function makeVariable() {
230
+ return `${NAMESPACE}${varId++}`;
231
+ }
232
+
233
+ function makeTemplate(node: Node, parent?: string, prevSibling?: string): { root: string, staticRoot: string, static: string, dynamic: string } {
234
+ if (node.nodeType === 3) {
235
+ const text = node.textContent;
236
+ return generateTextContentTemplate(text, parent, prevSibling);
237
+ }
238
+
239
+ const tag = (node as Element).tagName.toLowerCase();
240
+ const attrs = parseAttributes(node);
241
+
242
+ const statRoot = makeVariable();
243
+ let template = `const ${statRoot} = document.createElement("${tag}");\n`;
244
+ let code = "";
245
+ let dynRoot = makeVariable();
246
+ const childNodes = Array.from(node.childNodes).filter(n => n.nodeType !== 3 || n.textContent.trim());
247
+ const children = [];
248
+ let ps: string | undefined = undefined;
249
+ for (const cn of childNodes) {
250
+ const ch = makeTemplate(cn, dynRoot, ps);
251
+ children.push(ch);
252
+ ps = ch.root;
253
+ }
254
+ if (!parent) {
255
+ code = `const ${dynRoot} = ${statRoot}.cloneNode(true);\n`;
256
+ } else if (!prevSibling) {
257
+ code = `const ${dynRoot} = ${parent}.firstChild;\n`;
258
+ } else {
259
+ code = `const ${dynRoot} = ${prevSibling}.nextSibling;\n`;
260
+ }
261
+ for (const [key, val] of Object.entries(attrs)) {
262
+ if (["each", "if", "then"].includes(key)) continue;
263
+ if ("quoted" in val) {
264
+ if (key === "class" || key.includes("-")) {
265
+ template += `${statRoot}.setAttribute("${key}", "${val.quoted}");\n`;
266
+ } else {
267
+ template += `${statRoot}["${key}"] = "${val.quoted}";\n`;
268
+ }
269
+ }
270
+ }
271
+ for (const [key, val] of Object.entries(attrs)) {
272
+ if (["each", "if", "then"].includes(key)) continue;
273
+ if (!("raw" in val)) continue;
274
+ if (key.startsWith("on")) {
275
+ code += `${dynRoot}.${key} = ${val.raw};\n`;
276
+ continue;
277
+ }
278
+ const reactive = isReactiveExpression(val.raw.replace(/^\(\) => /, ""));
279
+ if (reactive) {
280
+ val.raw = val.raw.replace(/^\(\) => /, "");
281
+ if (key === "class") {
282
+ code += `$effect(() => {
283
+ const val = ${val.raw};
284
+ if (val === undefined) {
285
+ if (${dynRoot}.className) {
286
+ ${dynRoot}.removeAttribute("class");
287
+ }
288
+ } else {
289
+ ${dynRoot}.className = val;
290
+ }
291
+ });\n`
292
+ continue;
293
+ }
294
+ if (key.includes("-")) {
295
+ const ran = makeVariable();
296
+ code += `let ${ran} = false;\n`;
297
+ code += `$effect(() => {
298
+ const val = ${val.raw};
299
+ if (val === undefined) {
300
+ if (${ran} && ${dynRoot}.hasAttribute("${key}")) {
301
+ ${dynRoot}.removeAttribute("${key}");
302
+ }
303
+ } else {
304
+ ${dynRoot}.setAttribute("${key}", val);\n
305
+ }
306
+ ${ran} = true;
307
+ });\n`
308
+ } else {
309
+ code += `$effect(() => {
310
+ const val = ${val.raw};
311
+ if (val === undefined) {
312
+ if (${dynRoot}.${key}) {
313
+ delete ${dynRoot}.${key};
314
+ }
315
+ } else {
316
+ ${dynRoot}.${key} = val;\n
317
+ }
318
+ });\n`
319
+ }
320
+ continue;
321
+ }
322
+ if (key === "class") {
323
+ code += `${dynRoot}.className = ${val.raw};\n`;
324
+ continue;
325
+ }
326
+ if (key.includes("-")) {
327
+ code += `${dynRoot}.setAttribute("${key}", ${val.raw});\n`;
328
+ } else {
329
+ code += `${dynRoot}.${key} = ${val.raw};\n`;
330
+ }
331
+ }
332
+ if (children.length) {
333
+ for (let i = 0; i < children.length; i++) {
334
+ const ch = children[i];
335
+ if (ch.static) {
336
+ template += ch.static;
337
+ const childStaticRoot = ch.staticRoot || ch.root;
338
+ template += `${statRoot}.appendChild(${childStaticRoot});\n`;
339
+ }
340
+ if (ch.dynamic) {
341
+ code += ch.dynamic;
342
+ }
343
+ }
344
+ }
345
+
346
+ if (parent && code.split("\n").length === 2) {
347
+ code = "";
348
+ }
349
+
350
+ return {
351
+ root: dynRoot,
352
+ staticRoot: statRoot,
353
+ static: template,
354
+ dynamic: code,
355
+ };
356
+ }
357
+
358
+ function walk(node, hoist: string[]) {
359
+ if (node.nodeType === 3) {
360
+ const text = node.textContent;
361
+ if (!text.trim()) return null;
362
+ const result = parseTextContent(text);
363
+ return {
364
+ code: result.code,
365
+ isComponent: false,
366
+ hasReactive: result.hasReactive,
367
+ };
368
+ }
369
+
370
+ if (node.nodeType !== 1) return null;
371
+
372
+ const el = node;
373
+ const tag = el.tagName.toLowerCase();
374
+
375
+ let isComponent = el.hasAttribute("__thyn_component");
376
+ const makeArg = isComponent ? el.getAttribute("__thyn_component") : `"${tag}"`;
377
+ el.removeAttribute("__thyn_component");
378
+ const attrs = parseAttributes(el);
379
+ const children = Array.from(el.childNodes).map((n) => walk(n, hoist))
380
+ .filter(
381
+ Boolean,
382
+ );
383
+
384
+ const hasReactiveChildren = children.some((c) => c.hasReactive);
385
+ const hasComponentChildren = children.some((c) => c.isComponent);
386
+ let hasReactive = hasReactiveChildren || hasComponentChildren;
387
+ if (tag === "slot") {
388
+ return {
389
+ code: `...$props.slot ?? ${children.map((c) => cloneIfNeeded(c.code)).join(", ") || "[]"}`,
390
+ isComponent: false,
391
+ hasReactive,
392
+ };
393
+ }
394
+
395
+ let code = "";
396
+ let hasOwnEffects = false;
397
+ if (isComponent) {
398
+ const props: any = {};
399
+ for (const [key, val] of Object.entries(attrs)) {
400
+ if (["each", "if", "then"].includes(key)) continue;
401
+ const value = "raw" in val ? val.raw : JSON.stringify(val.quoted);
402
+ props[key] = value;
403
+ }
404
+ if (children.length) {
405
+ props.slot = `[${children.map((c) => cloneIfNeeded(c.code)).join(", ")}]`;
406
+ }
407
+ code = `__SPARKI__CORE__.component(${makeArg}, ${createObjectCode(props)})`;
408
+ } else {
409
+ code = createHoisting(`document.createElement(${makeArg})`, hoist);
410
+ for (const [key, val] of Object.entries(attrs)) {
411
+ if (["each", "if", "then"].includes(key)) continue;
412
+ if ("quoted" in val) {
413
+ if (key === "class" || key.includes("-")) {
414
+ code = createHoisting(
415
+ `__SPARKI__CORE__.setAttribute(${cloneIfNeeded(code)}, "${key}", "${val.quoted}")`,
416
+ hoist,
417
+ );
418
+ } else {
419
+ code = createHoisting(
420
+ `__SPARKI__CORE__.setProperty(${cloneIfNeeded(code)}, "${key}", "${val.quoted}")`,
421
+ hoist,
422
+ );
423
+ }
424
+ }
425
+ }
426
+ for (const [key, val] of Object.entries(attrs)) {
427
+ if (["each", "if", "then"].includes(key)) continue;
428
+ if (!("raw" in val)) continue;
429
+ if (key.startsWith("on")) {
430
+ code = `__SPARKI__CORE__.setProperty(${cloneIfNeeded(code)}, "${key}", ${val.raw})`;
431
+ continue;
432
+ }
433
+ const reactive = isReactiveExpression(val.raw.replace(/^\(\) => /, ""));
434
+ if (reactive) {
435
+ hasOwnEffects = true;
436
+ hasReactive = true;
437
+ if (key === "class" || key.includes("-")) {
438
+ code = `__SPARKI__CORE__.setReactiveAttribute(${cloneIfNeeded(code)}, "${key}", ${val.raw})`;
439
+ } else {
440
+ code = `__SPARKI__CORE__.setReactiveProperty(${cloneIfNeeded(code)}, "${key}", ${val.raw})`;
441
+ }
442
+ continue;
443
+ }
444
+ if (key === "class" || key.includes("-")) {
445
+ code = createHoisting(
446
+ `__SPARKI__CORE__.setAttribute(${cloneIfNeeded(code)}, "${key}", ${val.raw})`,
447
+ hoist,
448
+ );
449
+ } else {
450
+ code = createHoisting(
451
+ `__SPARKI__CORE__.setProperty(${cloneIfNeeded(code)}, "${key}", ${val.raw})`,
452
+ hoist,
453
+ );
454
+ }
455
+ }
456
+ if (children.length) {
457
+ code = `__SPARKI__CORE__.addChildren(${cloneIfNeeded(code)}, [${children.map((c) => cloneIfNeeded(c.code)).join(", ")}])`;
458
+ }
459
+ if (!hasOwnEffects && hasReactiveChildren) {
460
+ code = `__SPARKI__CORE__.markAsReactive(${cloneIfNeeded(code)})`;
461
+ }
462
+ }
463
+
464
+ if ("each" in attrs && "raw" in attrs.each) {
465
+ const eachAttr = attrs.each.raw;
466
+ const [item, iterable] = eachAttr.split(" in ").map((s) => s.trim());
467
+ code = `__SPARKI__CORE__.component(${hasComponentChildren ? "__SPARKI__CORE__.list" : "__SPARKI__CORE__.terminalList"}, {
468
+ items: () => ${iterable},
469
+ render: (${item}) => ${code},
470
+ })`;
471
+ isComponent = true;
472
+ }
473
+
474
+ if ("if" in attrs && "raw" in attrs.if) {
475
+ const ifCond = attrs.if.raw;
476
+ code = `__SPARKI__CORE__.component(__SPARKI__CORE__.show, {
477
+ if: () => ${ifCond},
478
+ then: () => ${code},
479
+ })`;
480
+ isComponent = true;
481
+ }
482
+
483
+ return {
484
+ code,
485
+ isComponent,
486
+ hasReactive: hasOwnEffects || isComponent || hasReactiveChildren,
487
+ };
488
+ }
489
+
490
+ function hasComponentChildren(node: Element): boolean {
491
+ if (node.nodeType === 3) {
492
+ return false;
493
+ }
494
+ if (node.nodeType !== 1) return false;
495
+ const tag = node.tagName.toLowerCase();
496
+ if (tag === "slot") return true;
497
+ let isComponent = node.hasAttribute("__thyn_component");
498
+ if (isComponent) return true;
499
+ for (const attr of node.attributes) {
500
+ if ([":each", ":if", ":then"].includes(attr.name)) return true;
501
+ }
502
+ return Array.from(node.childNodes).some((n) => hasComponentChildren(n as Element));
503
+ }
504
+
505
+ const forcedChildren = new Map([
506
+ ["tbody", "tr"],
507
+ ["thead", "tr"],
508
+ ["tfoot", "tr"],
509
+ ["ul", "li"],
510
+ ["ol", "li"],
511
+ ["select", "option"],
512
+ ]);
513
+
514
+ const COMPONENT_TAG_REGEX = /<\/?([A-Z][a-zA-Z0-9]*)(\s(?:[^"'<>\/]|"[^"]*"|'[^']*')*)?(\/?)>/g;
515
+
516
+ function preprocessHTML(html: string): string {
517
+ html = addComponentAttributes(html);
518
+ html = preserveCamelCaseAttributes(html);
519
+ return html;
520
+ }
521
+
522
+ function preserveCamelCaseAttributes(html: string): string {
523
+ return html.replace(/<([^>]+)>/g, (match, tagContent) => {
524
+ const processedContent = tagContent.replace(
525
+ /\s+(:?[a-z][a-zA-Z]*[A-Z][a-zA-Z]*)\s*=/g,
526
+ (_attrMatch, attrName) => {
527
+ const kebabCase = attrName.replace(
528
+ /[A-Z]/g,
529
+ (letter) => `-${letter.toLowerCase()}`,
530
+ );
531
+ return ` __thyn_attribute_${kebabCase}=`;
532
+ },
533
+ );
534
+ return `<${processedContent}>`;
535
+ });
536
+ }
537
+
538
+ function addComponentAttributes(html: string): string {
539
+ let processedHTML = html;
540
+ for (const [parentTag, childTag] of forcedChildren) {
541
+ const parentRegex = new RegExp(
542
+ `<${parentTag}([^>]*)>([\\s\\S]*?)<\\/${parentTag}>`,
543
+ "gis",
544
+ );
545
+
546
+ processedHTML = processedHTML.replace(
547
+ parentRegex,
548
+ (match, attributes, content) => {
549
+ const processedContent = content.replace(
550
+ COMPONENT_TAG_REGEX,
551
+ (componentMatch, componentName, attributes, selfClose) => {
552
+ const isClosing = componentMatch.startsWith("</");
553
+ return isClosing
554
+ ? `</${childTag}>`
555
+ : selfClose ? `<${childTag}${attributes || ""} __thyn_component="${componentName}"/>` : `<${childTag}${attributes || ""} __thyn_component="${componentName}">`;
556
+ },
557
+ );
558
+ return `<${parentTag}${attributes}>${processedContent}</${parentTag}>`;
559
+ },
560
+ );
561
+ }
562
+
563
+ processedHTML = processedHTML.replace(
564
+ COMPONENT_TAG_REGEX,
565
+ (componentMatch, componentName, attributes, selfClose) => {
566
+ const isClosing = componentMatch.startsWith("</");
567
+ return isClosing
568
+ ? "</div>"
569
+ : selfClose
570
+ ? `<div${attributes || ""} __thyn_component="${componentName}"></div>`
571
+ : `<div${attributes || ""} __thyn_component="${componentName}">`;
572
+ },
573
+ );
574
+
575
+ return processedHTML;
576
+ }
577
+
578
+ function addScopeId(el, scopeId) {
579
+ el.classList.add(scopeId);
580
+ for (const child of el.children) {
581
+ addScopeId(child, scopeId);
582
+ }
583
+ }
584
+
585
+ function transformHTMLtoJSX(html: string, style: string) {
586
+ const scopeId = `thyn-${(styleId++).toString(36)}`;
587
+ const div = new JSDOM("").window.document.createElement("div");
588
+ const processedHTML = preprocessHTML(html);
589
+ div.innerHTML = "<template>" + processedHTML + "</template>";
590
+ const template = div.firstElementChild;
591
+ const rootElement = (template as HTMLTemplateElement).content.firstElementChild;
592
+
593
+ let scopedStyle = null;
594
+ if (style) {
595
+ addScopeId(rootElement, scopeId);
596
+ scopedStyle = style.replace(
597
+ /(^|\})\s*([^{\}]+)\s*\{/g,
598
+ (_, sep, selector) => {
599
+ const scoped = selector
600
+ .split(",")
601
+ .map((s: string) => {
602
+ const trimmed = s.trim();
603
+ return `${trimmed}.${scopeId}`;
604
+ })
605
+ .join(", ");
606
+ return `${sep} ${scoped} {`;
607
+ },
608
+ );
609
+ }
610
+
611
+ if (hasComponentChildren(rootElement)) {
612
+ const hoist = [];
613
+ const { code } = walk(rootElement, hoist);
614
+ const root = makeVariable();
615
+ return [root, `const ${root} = ${code};`, hoist, scopedStyle];
616
+ }
617
+ const { root, static: tmpl, dynamic } = makeTemplate(rootElement);
618
+ const hoist = [tmpl];
619
+ return [root, dynamic, hoist, scopedStyle];
620
+ }
621
+
622
+ async function transformTypeScript(code: string, id: string) {
623
+ try {
624
+ const result = await esbuild.transform(code, {
625
+ loader: "ts",
626
+ target: "es2022",
627
+ format: "esm",
628
+ sourcemap: true,
629
+ sourcefile: id,
630
+ });
631
+ return {
632
+ code: result.code,
633
+ map: result.map,
634
+ };
635
+ } catch (error) {
636
+ throw new Error(
637
+ `TypeScript compilation failed for ${id}: ${error.message}`,
638
+ );
639
+ }
640
+ }
641
+
642
+ export function transformSFC(source: string, id: string) {
643
+ const name = id.split("/").pop()?.replace(/\.thyn$/, "");
644
+ const { script, scriptLang, html, style } = extractParts(source);
645
+ const { imports, body } = splitScript(script);
646
+
647
+ const s = new MagicString("");
648
+ if (!imports.some((imp) => imp.includes("$state"))) {
649
+ s.prepend("import { $state } from '@thyn-js/core';\n");
650
+ }
651
+ if (!imports.some((imp) => imp.includes("$effect"))) {
652
+ s.prepend("import { $effect } from '@thyn-js/core';\n");
653
+ }
654
+ if (!imports.some((imp) => imp.includes("$computed"))) {
655
+ s.prepend("import { $computed } from '@thyn-js/core';\n");
656
+ }
657
+ if (!imports.some((imp) => imp.includes("$compare"))) {
658
+ s.prepend("import { $compare } from '@thyn-js/core';\n");
659
+ }
660
+ s.prepend("import * as __SPARKI__CORE__ from '@thyn-js/core';\n");
661
+ s.append(imports.join("\n") + "\n");
662
+
663
+ let [root, transformed, hoist, scopedStyle] = transformHTMLtoJSX(html, style);
664
+ s.append(hoist.join("\n") + "\n");
665
+
666
+ s.append([
667
+ "",
668
+ `export default function ${name}($props) {`,
669
+ ...body.map((l) => " " + l),
670
+ ` ${transformed} return ${root};`,
671
+ `}`,
672
+ ].join("\n"));
673
+
674
+ let output = s.toString();
675
+ let sourceMap = s.generateMap({
676
+ source: id,
677
+ includeContent: true,
678
+ hires: true,
679
+ });
680
+ return { output, sourceMap, scopedStyle, scriptLang };
681
+ }
682
+
683
+ export async function compileSFC(source: string, id: string) {
684
+ let { scriptLang, output, sourceMap, scopedStyle } = transformSFC(source, id);
685
+
686
+ if (scriptLang === "ts" || scriptLang === "typescript") {
687
+ const tsResult = await transformTypeScript(output, id);
688
+ output = tsResult.code;
689
+ if (tsResult.map) {
690
+ // @ts-expect-error
691
+ sourceMap = tsResult.map;
692
+ }
693
+ }
694
+
695
+ return {
696
+ js: output,
697
+ css: scopedStyle || null,
698
+ cssModuleId: scopedStyle ? `${id}.css` : null,
699
+ map: sourceMap,
700
+ };
701
+ }
702
+
703
+ async function compileThynScript(source, id) {
704
+ const s = new MagicString(source);
705
+ const { imports } = splitScript(source);
706
+ if (!imports.some((imp) => imp.includes("$state"))) {
707
+ s.prepend("import { $state } from '@thyn-js/core';\n");
708
+ }
709
+ if (!imports.some((imp) => imp.includes("$effect"))) {
710
+ s.prepend("import { $effect } from '@thyn-js/core';\n");
711
+ }
712
+ if (!imports.some((imp) => imp.includes("$compare"))) {
713
+ s.prepend("import { $compare } from '@thyn-js/core';\n");
714
+ }
715
+ if (!imports.some((imp) => imp.includes("$computed"))) {
716
+ s.prepend("import { $computed } from '@thyn-js/core';\n");
717
+ }
718
+
719
+ let output = s.toString();
720
+ let sourceMap = s.generateMap({
721
+ source: id,
722
+ includeContent: true,
723
+ hires: true,
724
+ });
725
+
726
+ // Transform TypeScript if it's a .ts file
727
+ if (id.endsWith(".thyn.ts")) {
728
+ const tsResult = await transformTypeScript(output, id);
729
+ output = tsResult.code;
730
+ if (tsResult.map) {
731
+ // @ts-expect-error
732
+ sourceMap = tsResult.map;
733
+ }
734
+ }
735
+
736
+ return {
737
+ code: output,
738
+ map: sourceMap,
739
+ };
740
+ }
741
+
742
+ let styleId = 1e6;
743
+
744
+ export default function thyn() {
745
+ const collectedCSS = [];
746
+ let isDev = false;
747
+
748
+ return {
749
+ name: "thyn",
750
+ enforce: "pre",
751
+
752
+ configResolved(config) {
753
+ isDev = config.command === 'serve';
754
+ },
755
+
756
+ buildStart() {
757
+ collectedCSS.length = 0;
758
+ },
759
+
760
+ async transform(code, id) {
761
+ if (id.endsWith(".thyn.js") || id.endsWith(".thyn.ts")) {
762
+ return await compileThynScript(code, id);
763
+ }
764
+ if (!id.endsWith(".thyn")) return;
765
+ const { js, css, map } = await compileSFC(code, id);
766
+ let finalCode = js;
767
+ if (isDev && css) {
768
+ const escapedCSS = JSON.stringify(css);
769
+ finalCode += `\n
770
+ if (typeof document !== 'undefined') {
771
+ const style = document.createElement('style');
772
+ style.textContent = ${escapedCSS};
773
+ document.head.appendChild(style);
774
+ }`;
775
+ } else if (css) {
776
+ collectedCSS.push(css);
777
+ }
778
+ return {
779
+ code: finalCode,
780
+ map,
781
+ };
782
+ },
783
+
784
+ transformIndexHtml(html) {
785
+ if (collectedCSS.length === 0) return html;
786
+
787
+ const s = new MagicString(html);
788
+ const headCloseIndex = html.indexOf("</head>");
789
+
790
+ if (headCloseIndex !== -1) {
791
+ if (isDev) {
792
+ const combinedCSS = collectedCSS.join("\n");
793
+ s.appendLeft(
794
+ headCloseIndex,
795
+ ` <style>\n${combinedCSS}\n </style>\n`,
796
+ );
797
+ } else {
798
+ s.appendLeft(
799
+ headCloseIndex,
800
+ ' <link rel="stylesheet" href="/main.css">\n',
801
+ );
802
+ }
803
+ }
804
+
805
+ return s.toString();
806
+ },
807
+
808
+ async generateBundle() {
809
+ if (isDev || collectedCSS.length === 0) return;
810
+ const combinedCSS = collectedCSS.join("\n");
811
+ const result = await esbuild.transform(combinedCSS, {
812
+ loader: "css",
813
+ minify: true,
814
+ });
815
+ this.emitFile({
816
+ type: "asset",
817
+ fileName: "main.css",
818
+ source: result.code,
819
+ });
820
+ },
821
+ };
822
+ }