@thyn/vite-plugin 0.0.312 → 0.0.314

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/dist/index.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import * as acorn from "acorn";
2
2
  import * as acornwalk from "acorn-walk";
3
3
  import * as esbuild from "esbuild";
4
- import { JSDOM } from "jsdom";
4
+ import { parseHTML } from "./html-parser.js";
5
5
  import MagicString from "magic-string";
6
6
  import postcss from 'postcss';
7
7
  import selectorParser from 'postcss-selector-parser';
@@ -224,6 +224,7 @@ function generateTextContentTemplate(text, parent, prevSibling) {
224
224
  dynamic: "",
225
225
  root: "",
226
226
  staticRoot: root,
227
+ html: escapeHTML(finalText),
227
228
  };
228
229
  }
229
230
  const interpolated = parts.map((part) => {
@@ -235,6 +236,19 @@ function generateTextContentTemplate(text, parent, prevSibling) {
235
236
  const textNode = prevSibling
236
237
  ? `${prevSibling}.nextSibling`
237
238
  : `${parent}.firstChild`;
239
+ // For templates with interpolations, build HTML with the static parts
240
+ // For reactive/dynamic content, we use the initial value
241
+ let htmlContent = "";
242
+ for (const part of parts) {
243
+ if (typeof part === "string") {
244
+ htmlContent += escapeHTML(part);
245
+ }
246
+ else {
247
+ // For interpolation, we can't compute the value at compile time
248
+ // So we use an empty string as placeholder
249
+ htmlContent += "";
250
+ }
251
+ }
238
252
  if (hasReactive) {
239
253
  let fn = `(() => \`${interpolated}\`)`;
240
254
  if (parts.length === 1) {
@@ -254,6 +268,7 @@ function generateTextContentTemplate(text, parent, prevSibling) {
254
268
  dynamic,
255
269
  root: "",
256
270
  staticRoot: root,
271
+ html: htmlContent || " ", // Use space to ensure text node is created
257
272
  };
258
273
  }
259
274
  if (parts.length === 1) {
@@ -262,6 +277,7 @@ function generateTextContentTemplate(text, parent, prevSibling) {
262
277
  static: `const ${root} = document.createTextNode("");\n`,
263
278
  root: "",
264
279
  staticRoot: root,
280
+ html: htmlContent || " ",
265
281
  };
266
282
  }
267
283
  return {
@@ -269,6 +285,7 @@ function generateTextContentTemplate(text, parent, prevSibling) {
269
285
  static: `const ${root} = document.createTextNode("");\n`,
270
286
  root: "",
271
287
  staticRoot: root,
288
+ html: htmlContent || " ",
272
289
  };
273
290
  }
274
291
  const NAMESPACE = "__THYN__";
@@ -298,30 +315,63 @@ let varId = 0;
298
315
  function makeVariable() {
299
316
  return `${NAMESPACE}${varId++}`;
300
317
  }
301
- function makeTemplate(node, parent, prevSibling) {
318
+ function escapeHTML(str) {
319
+ return str
320
+ .replace(/&/g, '&')
321
+ .replace(/</g, '&lt;')
322
+ .replace(/>/g, '&gt;')
323
+ .replace(/"/g, '&quot;')
324
+ .replace(/'/g, '&#39;');
325
+ }
326
+ function processTextForHTML(text) {
327
+ // Handle escaped braces by removing the backslashes
328
+ text = text.replace(/\\\{\{/g, '{{').replace(/\\\}\}/g, '}}');
329
+ return escapeHTML(text.trim());
330
+ }
331
+ function makeTemplate(node, parent, prevSibling, path = "") {
302
332
  if (node.nodeType === 3) {
303
333
  const text = node.textContent;
304
- return generateTextContentTemplate(text, parent, prevSibling);
334
+ const result = generateTextContentTemplate(text, parent, prevSibling);
335
+ // For templates with interpolations, we need a placeholder
336
+ const html = result.html !== "<!-- -->" ? result.html : "<!-- -->";
337
+ return {
338
+ ...result,
339
+ html,
340
+ };
305
341
  }
306
342
  const tag = node.tagName.toLowerCase();
307
343
  const attrs = parseAttributes(node);
308
344
  let statRoot = makeVariable();
309
- let template = `const ${statRoot} = document.createElement("${tag}");\n`;
310
- if (!parent) {
311
- statRoot = "__THYN__template";
312
- template = `${statRoot} = document.createElement("${tag}");\n`;
313
- }
314
345
  let code = "";
315
346
  let dynRoot = makeVariable();
347
+ // Build HTML string for static template
348
+ let htmlAttrs = "";
349
+ const staticAttrs = {};
350
+ for (const [key, val] of Object.entries(attrs)) {
351
+ if (DIRECTIVES.includes(key))
352
+ continue;
353
+ if ("quoted" in val) {
354
+ staticAttrs[key] = val.quoted;
355
+ if (key === "class") {
356
+ htmlAttrs += ` class="${escapeHTML(val.quoted)}"`;
357
+ }
358
+ else {
359
+ htmlAttrs += ` ${key}="${escapeHTML(val.quoted)}"`;
360
+ }
361
+ }
362
+ }
316
363
  const childNodes = Array.from(node.childNodes).filter((n) => n.nodeType !== 3 || n.textContent.trim());
317
364
  const children = [];
318
365
  let ps = undefined;
366
+ let childHTML = "";
319
367
  for (let i = 0; i < childNodes.length; i++) {
320
368
  const cn = childNodes[i];
321
- const ch = makeTemplate(cn, dynRoot, ps);
369
+ const ch = makeTemplate(cn, dynRoot, ps, `${path}.childNodes[${i}]`);
322
370
  children.push(ch);
323
371
  ps = ch.root || `${dynRoot}.childNodes[${i}]`;
372
+ childHTML += ch.html || "";
324
373
  }
374
+ const fullHTML = `<${tag}${htmlAttrs}>${childHTML}</${tag}>`;
325
375
  if (!parent) {
326
376
  code = `const ${dynRoot} = __THYN__template_generate();\n`;
327
377
  }
@@ -331,21 +381,7 @@ function makeTemplate(node, parent, prevSibling) {
331
381
  else {
332
382
  code = `const ${dynRoot} = ${prevSibling}.nextSibling;\n`;
333
383
  }
334
- for (const [key, val] of Object.entries(attrs)) {
335
- if (DIRECTIVES.includes(key))
336
- continue;
337
- if ("quoted" in val) {
338
- if (key === "class") {
339
- template += `${statRoot}.className = "${val.quoted}";\n`;
340
- }
341
- else if (key.includes("-")) {
342
- template += `${statRoot}.setAttribute("${key}", "${val.quoted}");\n`;
343
- }
344
- else {
345
- template += `${statRoot}["${key}"] = "${val.quoted}";\n`;
346
- }
347
- }
348
- }
384
+ // Generate dynamic code for reactive attributes and events
349
385
  for (const [key, val] of Object.entries(attrs)) {
350
386
  if (DIRECTIVES.includes(key))
351
387
  continue;
@@ -410,24 +446,19 @@ function makeTemplate(node, parent, prevSibling) {
410
446
  code += `${dynRoot}.${key} = ${val.raw};\n`;
411
447
  }
412
448
  }
413
- if (children.length) {
414
- for (let i = 0; i < children.length; i++) {
415
- const ch = children[i];
416
- if (ch.static) {
417
- template += ch.static;
418
- const childStaticRoot = ch.staticRoot || ch.root;
419
- template += `${statRoot}.appendChild(${childStaticRoot});\n`;
420
- }
421
- if (ch.dynamic) {
422
- code += ch.dynamic;
423
- }
449
+ // Handle dynamic text content
450
+ for (let i = 0; i < children.length; i++) {
451
+ const ch = children[i];
452
+ if (ch.dynamic) {
453
+ code += ch.dynamic;
424
454
  }
425
455
  }
426
456
  return {
427
457
  root: dynRoot,
428
458
  staticRoot: statRoot,
429
- static: template,
459
+ static: "", // No longer used - template is built from HTML
430
460
  dynamic: code,
461
+ html: fullHTML,
431
462
  };
432
463
  }
433
464
  function walkConditionChain(nodes, i) {
@@ -641,14 +672,6 @@ function hasComponentChildren(node) {
641
672
  }
642
673
  return Array.from(node.childNodes).some((n) => hasComponentChildren(n));
643
674
  }
644
- const forcedChildren = new Map([
645
- ["tbody", "tr"],
646
- ["thead", "tr"],
647
- ["tfoot", "tr"],
648
- ["ul", "li"],
649
- ["ol", "li"],
650
- ["select", "option"],
651
- ]);
652
675
  const COMPONENT_TAG_REGEX = /<\/?([A-Z][a-zA-Z0-9]*)(\s(?:[^"'<>\/]|"[^"]*"|'[^']*')*)?(\/?)>/g;
653
676
  function convertToColonBindings(html) {
654
677
  let result = '';
@@ -742,20 +765,6 @@ function preserveCamelCaseAttributes(html) {
742
765
  }
743
766
  function addComponentAttributes(html) {
744
767
  let processedHTML = html;
745
- for (const [parentTag, childTag] of forcedChildren) {
746
- const parentRegex = new RegExp(`<${parentTag}([^>]*)>([\\s\\S]*?)<\\/${parentTag}>`, "gis");
747
- processedHTML = processedHTML.replace(parentRegex, (match, attributes, content) => {
748
- const processedContent = content.replace(COMPONENT_TAG_REGEX, (componentMatch, componentName, attributes, selfClose) => {
749
- const isClosing = componentMatch.startsWith("</");
750
- return isClosing
751
- ? `</${childTag}>`
752
- : selfClose
753
- ? `<${childTag}${attributes || ""} __thyn_component="${componentName}"/>`
754
- : `<${childTag}${attributes || ""} __thyn_component="${componentName}">`;
755
- });
756
- return `<${parentTag}${attributes}>${processedContent}</${parentTag}>`;
757
- });
758
- }
759
768
  processedHTML = processedHTML.replace(COMPONENT_TAG_REGEX, (componentMatch, componentName, attributes, selfClose) => {
760
769
  const isClosing = componentMatch.startsWith("</");
761
770
  return isClosing
@@ -833,10 +842,8 @@ function removeUnusedThynVars(code) {
833
842
  }
834
843
  async function transformHTMLtoJSX(html, style) {
835
844
  const scopeId = `thyn-${(styleId++).toString(36)}`;
836
- const div = new JSDOM("").window.document.createElement("div");
837
845
  const processedHTML = preprocessHTML(html);
838
- div.innerHTML = "<template>" + processedHTML + "</template>";
839
- const template = div.firstElementChild;
846
+ const template = parseHTML("<template>" + processedHTML + "</template>");
840
847
  const rootElement = template.content.firstElementChild;
841
848
  let scopedStyle = null;
842
849
  if (style) {
@@ -849,12 +856,14 @@ async function transformHTMLtoJSX(html, style) {
849
856
  const root = makeVariable();
850
857
  return [root, `const ${root} = ${code};`, hoist, scopedStyle];
851
858
  }
852
- const { root, static: tmpl, dynamic } = makeTemplate(rootElement);
859
+ const { root, dynamic, html: templateHTML } = makeTemplate(rootElement);
853
860
  const hoist = [`
854
861
  let __THYN__template;
855
862
  function __THYN__template_generate() {
856
863
  if (!__THYN__template) {
857
- ${tmpl}
864
+ const t = document.createElement('template');
865
+ t.innerHTML = \`${escapeTemplateLiteral(templateHTML)}\`;
866
+ __THYN__template = t.content.firstChild;
858
867
  return __THYN__template;
859
868
  }
860
869
  return __THYN__template.cloneNode(true);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@thyn/vite-plugin",
3
- "version": "0.0.312",
3
+ "version": "0.0.314",
4
4
  "scripts": {
5
5
  "build": "tsc",
6
6
  "pub": "tsc && npm version patch -f && npm -f publish --access=public",
@@ -19,7 +19,6 @@
19
19
  "acorn": "^8.15.0",
20
20
  "acorn-walk": "^8.3.4",
21
21
  "esbuild": "^0.27.2",
22
- "jsdom": "^27.4.0",
23
22
  "magic-string": "^0.30.21",
24
23
  "postcss": "^8.5.6",
25
24
  "postcss-selector-parser": "^7.1.1",
@@ -28,6 +27,7 @@
28
27
  "devDependencies": {
29
28
  "@thyn/core": "^0.0.298",
30
29
  "@types/jsdom": "^27.0.0",
30
+ "jsdom": "^27.4.0",
31
31
  "vite": "^7.3.1",
32
32
  "vitest": "^4.0.16"
33
33
  }
@@ -0,0 +1,332 @@
1
+ interface Node {
2
+ nodeType: number;
3
+ nodeName: string;
4
+ textContent: string;
5
+ childNodes: Node[];
6
+ }
7
+
8
+ interface Element extends Node {
9
+ tagName: string;
10
+ attributes: Array<{ name: string; value: string }>;
11
+ children: Element[];
12
+ firstElementChild: Element | null;
13
+ hasAttribute(name: string): boolean;
14
+ getAttribute(name: string): string | null;
15
+ setAttribute(name: string, value: string): void;
16
+ removeAttribute(name: string): void;
17
+ classList: { add(className: string): void };
18
+ }
19
+
20
+ interface DocumentFragment {
21
+ childNodes: Node[];
22
+ firstElementChild: Element | null;
23
+ }
24
+
25
+ interface TemplateElement extends Element {
26
+ content: DocumentFragment;
27
+ }
28
+
29
+ function parseAttributes(attrStr: string): Array<{ name: string; value: string }> {
30
+ const attrs: Array<{ name: string; value: string }> = []
31
+ let i = 0;
32
+
33
+ while (i < attrStr.length) {
34
+ // Skip whitespace
35
+ while (i < attrStr.length && /\s/.test(attrStr[i])) i++;
36
+ if (i >= attrStr.length) break;
37
+
38
+ // Parse attribute name
39
+ let name = "";
40
+ while (i < attrStr.length && !/[\s=]/.test(attrStr[i])) {
41
+ name += attrStr[i];
42
+ i++;
43
+ }
44
+
45
+ if (!name) break;
46
+
47
+ // Skip whitespace
48
+ while (i < attrStr.length && /\s/.test(attrStr[i])) i++;
49
+
50
+ let value = "";
51
+ if (i < attrStr.length && attrStr[i] === "=") {
52
+ i++; // skip '='
53
+ // Skip whitespace
54
+ while (i < attrStr.length && /\s/.test(attrStr[i])) i++;
55
+
56
+ if (i < attrStr.length) {
57
+ const quote = attrStr[i];
58
+ if (quote === '"' || quote === "'") {
59
+ i++; // skip opening quote
60
+ while (i < attrStr.length && attrStr[i] !== quote) {
61
+ value += attrStr[i];
62
+ i++;
63
+ }
64
+ if (i < attrStr.length) i++; // skip closing quote
65
+ } else {
66
+ // Unquoted value - take until whitespace
67
+ while (i < attrStr.length && !/\s/.test(attrStr[i])) {
68
+ value += attrStr[i];
69
+ i++;
70
+ }
71
+ }
72
+ }
73
+ }
74
+
75
+ attrs.push({ name, value });
76
+ }
77
+
78
+ return attrs;
79
+ }
80
+
81
+ function createTextNode(text: string): Node {
82
+ return {
83
+ nodeType: 3,
84
+ nodeName: "#text",
85
+ textContent: text,
86
+ childNodes: [],
87
+ };
88
+ }
89
+
90
+ function createElement(tagName: string, attributes: Array<{ name: string; value: string }> = []): Element {
91
+ const children: Element[] = [];
92
+ const childNodes: Node[] = [];
93
+
94
+ const element: Element = {
95
+ nodeType: 1,
96
+ nodeName: tagName.toUpperCase(),
97
+ tagName: tagName.toUpperCase(),
98
+ textContent: "",
99
+ attributes: [...attributes],
100
+ children,
101
+ childNodes,
102
+ firstElementChild: null,
103
+ hasAttribute(name: string): boolean {
104
+ return this.attributes.some((attr) => attr.name === name);
105
+ },
106
+ getAttribute(name: string): string | null {
107
+ const attr = this.attributes.find((attr) => attr.name === name);
108
+ return attr ? attr.value : null;
109
+ },
110
+ setAttribute(name: string, value: string): void {
111
+ const existing = this.attributes.find((attr) => attr.name === name);
112
+ if (existing) {
113
+ existing.value = value;
114
+ } else {
115
+ this.attributes.push({ name, value });
116
+ }
117
+ },
118
+ removeAttribute(name: string): void {
119
+ this.attributes = this.attributes.filter((attr) => attr.name !== name);
120
+ },
121
+ classList: {
122
+ add: (className: string) => {
123
+ const existing = element.getAttribute("class");
124
+ const classes = existing ? existing.split(" ").filter(Boolean) : [];
125
+ if (!classes.includes(className)) {
126
+ classes.push(className);
127
+ element.setAttribute("class", classes.join(" "));
128
+ }
129
+ },
130
+ },
131
+ };
132
+
133
+ return element;
134
+ }
135
+
136
+ // Find next tag position, properly handling quoted strings
137
+ function findNextTag(html: string, startIndex: number): { index: number; endIndex: number; isClose: boolean; tagName: string; attrs: string; isSelfClose: boolean } | null {
138
+ let i = startIndex;
139
+
140
+ while (i < html.length) {
141
+ // Find the next '<'
142
+ while (i < html.length && html[i] !== '<') {
143
+ i++;
144
+ }
145
+
146
+ if (i >= html.length) return null;
147
+
148
+ const tagStart = i;
149
+ i++; // skip '<'
150
+
151
+ // Check if it's a closing tag
152
+ const isClose = i < html.length && html[i] === '/';
153
+ if (isClose) i++;
154
+
155
+ // Parse tag name
156
+ let tagName = '';
157
+ while (i < html.length && /[a-zA-Z0-9-]/.test(html[i])) {
158
+ tagName += html[i];
159
+ i++;
160
+ }
161
+
162
+ if (!tagName) {
163
+ // Not a valid tag, continue searching
164
+ i = tagStart + 1;
165
+ continue;
166
+ }
167
+
168
+ // Parse attributes, respecting quotes
169
+ let attrs = '';
170
+ let inQuote: string | null = null;
171
+ let tagEnd = -1;
172
+
173
+ while (i < html.length) {
174
+ const char = html[i];
175
+
176
+ if (inQuote) {
177
+ attrs += char;
178
+ if (char === inQuote) {
179
+ inQuote = null;
180
+ }
181
+ i++;
182
+ } else if (char === '"' || char === "'") {
183
+ attrs += char;
184
+ inQuote = char;
185
+ i++;
186
+ } else if (char === '>') {
187
+ tagEnd = i + 1; // Include the '>'
188
+ i++;
189
+ break;
190
+ } else {
191
+ attrs += char;
192
+ i++;
193
+ }
194
+ }
195
+
196
+ if (tagEnd === -1) {
197
+ // Malformed tag (no closing >), continue searching
198
+ i = tagStart + 1;
199
+ continue;
200
+ }
201
+
202
+ // Check for self-closing
203
+ const trimmedAttrs = attrs.trim();
204
+ const isSelfClose = trimmedAttrs.endsWith('/');
205
+ const finalAttrs = isSelfClose ? trimmedAttrs.slice(0, -1).trim() : trimmedAttrs;
206
+
207
+ return {
208
+ index: tagStart,
209
+ endIndex: tagEnd,
210
+ isClose,
211
+ tagName,
212
+ attrs: finalAttrs,
213
+ isSelfClose
214
+ };
215
+ }
216
+
217
+ return null;
218
+ }
219
+
220
+ export function parseHTML(html: string): TemplateElement {
221
+ const match = html.match(/<template([^>]*)>([\s\S]*)<\/template>/i);
222
+
223
+ if (!match) {
224
+ throw new Error("No <template> tag found in HTML");
225
+ }
226
+
227
+ const content = match[2].trim();
228
+ const stack: Element[] = [];
229
+ const textChunks: string[] = [];
230
+ const fragmentChildren: Node[] = [];
231
+ const fragmentElements: Element[] = [];
232
+
233
+ let pos = 0;
234
+
235
+ const flushText = () => {
236
+ if (textChunks.length > 0) {
237
+ const text = textChunks.join("");
238
+ textChunks.length = 0;
239
+ const textNode = createTextNode(text);
240
+ if (stack.length > 0) {
241
+ const parent = stack[stack.length - 1];
242
+ parent.childNodes.push(textNode);
243
+ } else {
244
+ fragmentChildren.push(textNode);
245
+ }
246
+ }
247
+ };
248
+
249
+ while (pos < content.length) {
250
+ const tagInfo = findNextTag(content, pos);
251
+
252
+ if (!tagInfo) {
253
+ // No more tags, add remaining as text
254
+ if (pos < content.length) {
255
+ textChunks.push(content.slice(pos));
256
+ }
257
+ break;
258
+ }
259
+
260
+ // Add text before this tag
261
+ if (tagInfo.index > pos) {
262
+ textChunks.push(content.slice(pos, tagInfo.index));
263
+ }
264
+
265
+ const { isClose, tagName, attrs, isSelfClose, endIndex } = tagInfo;
266
+
267
+ if (isClose) {
268
+ flushText();
269
+ if (stack.length > 0) {
270
+ const closedElement = stack.pop()!;
271
+ if (closedElement.tagName.toLowerCase() !== tagName.toLowerCase()) {
272
+ throw new Error(`Mismatched tags: expected </${closedElement.tagName}>, got </${tagName}>`);
273
+ }
274
+
275
+ // Update parent's firstElementChild if needed
276
+ const parent = stack.length > 0 ? stack[stack.length - 1] : null;
277
+ if (parent && !parent.firstElementChild) {
278
+ parent.firstElementChild = closedElement;
279
+ }
280
+ }
281
+ } else {
282
+ flushText();
283
+ const attributes = parseAttributes(attrs);
284
+ const element = createElement(tagName, attributes);
285
+
286
+ if (stack.length === 0) {
287
+ // Top-level element
288
+ fragmentChildren.push(element);
289
+ fragmentElements.push(element);
290
+ } else {
291
+ const parent = stack[stack.length - 1];
292
+ parent.children.push(element);
293
+ parent.childNodes.push(element);
294
+ if (!parent.firstElementChild) {
295
+ parent.firstElementChild = element;
296
+ }
297
+ }
298
+
299
+ if (!isSelfClose) {
300
+ stack.push(element);
301
+ }
302
+ }
303
+
304
+ // Move position past this tag
305
+ pos = endIndex;
306
+ }
307
+
308
+ // Flush any remaining text
309
+ if (stack.length === 0) {
310
+ flushText();
311
+ }
312
+
313
+ if (stack.length > 0) {
314
+ throw new Error(`Unclosed tags remain: ${stack.map(e => e.tagName).join(', ')}`);
315
+ }
316
+
317
+ const fragment: DocumentFragment = {
318
+ childNodes: fragmentChildren,
319
+ firstElementChild: fragmentElements[0] || null,
320
+ };
321
+
322
+ const templateAttrs = parseAttributes(match[1].trim());
323
+ const templateElement: TemplateElement = {
324
+ ...createElement("template", templateAttrs),
325
+ content: fragment,
326
+ };
327
+ templateElement.childNodes = [...fragmentChildren];
328
+ templateElement.children = [...fragmentElements];
329
+ templateElement.firstElementChild = fragmentElements[0] || null;
330
+
331
+ return templateElement;
332
+ }