@sola-air-ui/compiler 1.4.1 → 1.5.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/README.md +11 -4
- package/package.json +1 -1
- package/src/check.js +50 -3
- package/src/index.js +204 -16
- package/types/index.d.ts +5 -1
package/README.md
CHANGED
|
@@ -12,9 +12,12 @@ npm install @sola-air-ui/compiler
|
|
|
12
12
|
|
|
13
13
|
```js
|
|
14
14
|
import { compile } from '@sola-air-ui/compiler';
|
|
15
|
+
import { checkSource, checkFiles } from '@sola-air-ui/compiler/check';
|
|
15
16
|
import { readFileSync } from 'node:fs';
|
|
16
17
|
|
|
17
18
|
const source = readFileSync('App.sola', 'utf-8');
|
|
19
|
+
const diagnostics = checkSource(source, 'App.sola');
|
|
20
|
+
|
|
18
21
|
const result = compile(source, {
|
|
19
22
|
target: 'esm', // or 'iife'
|
|
20
23
|
exportName: 'App', // used for iife builds
|
|
@@ -23,26 +26,30 @@ const result = compile(source, {
|
|
|
23
26
|
|
|
24
27
|
console.log(result.code); // compiled JS
|
|
25
28
|
console.log(result.css); // scoped CSS
|
|
29
|
+
console.log(result.dts); // generated .d.ts
|
|
26
30
|
```
|
|
27
31
|
|
|
28
32
|
## CLI
|
|
29
33
|
|
|
30
34
|
```bash
|
|
31
35
|
npx sola build App.sola
|
|
36
|
+
npx sola-check src/
|
|
32
37
|
```
|
|
33
38
|
|
|
39
|
+
`sola-check` validates `.sola` files on disk (pass `--json` for machine-readable output), reporting syntax errors (`SOLA000`) and unbalanced or malformed `{#if}` / `{#each}` / `{:else}` blocks (`SOLA001`).
|
|
40
|
+
|
|
34
41
|
## What a `.sola` file looks like
|
|
35
42
|
|
|
36
43
|
```html
|
|
37
44
|
<script>
|
|
38
45
|
let count = $state(0);
|
|
39
|
-
let doubled = $derived(
|
|
46
|
+
let doubled = $derived(count * 2);
|
|
40
47
|
</script>
|
|
41
48
|
|
|
42
49
|
<div>
|
|
43
|
-
<h1>Count: {count
|
|
44
|
-
<p>Doubled: {doubled
|
|
45
|
-
<button onclick={() => count
|
|
50
|
+
<h1>Count: {count}</h1>
|
|
51
|
+
<p>Doubled: {doubled}</p>
|
|
52
|
+
<button onclick={() => count++}>+1</button>
|
|
46
53
|
</div>
|
|
47
54
|
|
|
48
55
|
<style>
|
package/package.json
CHANGED
package/src/check.js
CHANGED
|
@@ -28,6 +28,23 @@ function position(source, index) {
|
|
|
28
28
|
return { line, column: index - before.lastIndexOf('\n') };
|
|
29
29
|
}
|
|
30
30
|
|
|
31
|
+
function matchBrace(text, openPos) {
|
|
32
|
+
let depth = 0;
|
|
33
|
+
let inStr = null;
|
|
34
|
+
for (let i = openPos; i < text.length; i++) {
|
|
35
|
+
const ch = text[i];
|
|
36
|
+
if (inStr) {
|
|
37
|
+
if (ch === '\\') { i++; continue; }
|
|
38
|
+
if (ch === inStr) inStr = null;
|
|
39
|
+
continue;
|
|
40
|
+
}
|
|
41
|
+
if (ch === '"' || ch === "'" || ch === '`') { inStr = ch; continue; }
|
|
42
|
+
if (ch === '{') depth++;
|
|
43
|
+
else if (ch === '}') { if (--depth === 0) return i; }
|
|
44
|
+
}
|
|
45
|
+
return -1;
|
|
46
|
+
}
|
|
47
|
+
|
|
31
48
|
/**
|
|
32
49
|
* Check one component's source. Returns diagnostics:
|
|
33
50
|
* `{ file, line, column, severity: 'error' | 'warning', code, message }`.
|
|
@@ -58,10 +75,40 @@ export function checkSource(source, filename = 'component.sola') {
|
|
|
58
75
|
while ((match = tag.exec(template))) {
|
|
59
76
|
const kind = match[1];
|
|
60
77
|
const at = match.index;
|
|
61
|
-
|
|
62
|
-
|
|
78
|
+
const closeIdx = matchBrace(template, at);
|
|
79
|
+
if (kind === '#if') {
|
|
80
|
+
stack.push({ kind: 'if', at });
|
|
81
|
+
if (closeIdx !== -1) {
|
|
82
|
+
const cond = template.slice(at + 4, closeIdx).trim();
|
|
83
|
+
if (!cond) {
|
|
84
|
+
add(at, 'error', 'SOLA001', '`{#if}` is missing a condition. It renders as literal text.');
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
} else if (kind === '#each') {
|
|
88
|
+
stack.push({ kind: 'each', at });
|
|
89
|
+
if (closeIdx !== -1) {
|
|
90
|
+
const head = template.slice(at, closeIdx + 1);
|
|
91
|
+
const parsed = /^\{#each\s+([\s\S]*?)\s+as\s+([\w$]+)(?:\s*,\s*([\w$]+))?(?:\s*\(([\s\S]+)\))?\}$/.exec(head);
|
|
92
|
+
if (!parsed || !parsed[1].trim()) {
|
|
93
|
+
add(at, 'error', 'SOLA001', '`{#each}` is missing `as <item>` (expected `{#each list as item}`). It renders as literal text.');
|
|
94
|
+
}
|
|
95
|
+
}
|
|
63
96
|
} else if (kind === ':else') {
|
|
64
|
-
if (stack.at(-1)?.kind !== 'if')
|
|
97
|
+
if (stack.at(-1)?.kind !== 'if') {
|
|
98
|
+
add(at, 'error', 'SOLA001', '`{:else}` outside an `{#if}` block. It renders as literal text.');
|
|
99
|
+
} else if (closeIdx !== -1) {
|
|
100
|
+
const inner = template.slice(at + 6, closeIdx);
|
|
101
|
+
if (inner.trim() !== '') {
|
|
102
|
+
if (/^\s+if\b/.test(inner)) {
|
|
103
|
+
const cond = inner.replace(/^\s+if\b/, '').trim();
|
|
104
|
+
if (!cond) {
|
|
105
|
+
add(at, 'error', 'SOLA001', '`{:else if}` is missing a condition. It renders as literal text.');
|
|
106
|
+
}
|
|
107
|
+
} else {
|
|
108
|
+
add(at, 'error', 'SOLA001', '`{:else}` unexpected token (use `{:else}` or `{:else if <condition>}`). It renders as literal text.');
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
65
112
|
} else {
|
|
66
113
|
const want = kind.slice(1);
|
|
67
114
|
const open = stack.pop();
|
package/src/index.js
CHANGED
|
@@ -398,7 +398,7 @@ function desugarElseIf(template) {
|
|
|
398
398
|
if (text.startsWith(OPEN, i)) { depth++; i += OPEN.length; continue; }
|
|
399
399
|
if (text.startsWith(CLOSE, i)) { depth--; i += CLOSE.length; continue; }
|
|
400
400
|
if (depth === 0 && text.startsWith('{:else', i)) {
|
|
401
|
-
const end = text
|
|
401
|
+
const end = matchBrace(text, i);
|
|
402
402
|
if (end === -1) return null;
|
|
403
403
|
const inner = text.slice(i + '{:else'.length, end);
|
|
404
404
|
if (/^\s+if\s+/.test(inner)) {
|
|
@@ -481,7 +481,7 @@ function transformBlocks(template) {
|
|
|
481
481
|
let i = 0;
|
|
482
482
|
while (i < text.length) {
|
|
483
483
|
if (text.startsWith('{#if', i)) {
|
|
484
|
-
const condEnd = text
|
|
484
|
+
const condEnd = matchBrace(text, i);
|
|
485
485
|
if (condEnd === -1) { out += text[i++]; continue; }
|
|
486
486
|
const condition = text.slice(i + 4, condEnd);
|
|
487
487
|
const end = matchEnd(text, i, '{#if', '{/if}');
|
|
@@ -500,10 +500,10 @@ function transformBlocks(template) {
|
|
|
500
500
|
}
|
|
501
501
|
|
|
502
502
|
if (text.startsWith('{#each', i)) {
|
|
503
|
-
const headEnd = text
|
|
503
|
+
const headEnd = matchBrace(text, i);
|
|
504
504
|
if (headEnd === -1) { out += text[i++]; continue; }
|
|
505
505
|
const head = text.slice(i, headEnd + 1);
|
|
506
|
-
const parsed = /^{#each\s+([\s\S]*?)\s+as\s+([\w$]+)(?:\s*,\s*([\w$]+))?(?:\s*\(([
|
|
506
|
+
const parsed = /^{#each\s+([\s\S]*?)\s+as\s+([\w$]+)(?:\s*,\s*([\w$]+))?(?:\s*\(([\s\S]+)\))?}$/.exec(head);
|
|
507
507
|
const end = matchEnd(text, i, '{#each', '{/each}');
|
|
508
508
|
if (!parsed || end === -1) { out += text[i++]; continue; }
|
|
509
509
|
|
|
@@ -555,14 +555,189 @@ function extractExpressions(text) {
|
|
|
555
555
|
// script blocks written in TS. Covers the common patterns: type annotations,
|
|
556
556
|
// interface/type declarations, angle-bracket generics, and `as` casts.
|
|
557
557
|
function stripTypeScript(code) {
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
// Remove
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
558
|
+
const blankKeepNewlines = (str) => str.replace(/[^\n]/g, ' ');
|
|
559
|
+
|
|
560
|
+
// 1. Remove interface declarations using balanced brace matching so statements
|
|
561
|
+
// following an interface ($effect, onMount, etc.) are never swallowed.
|
|
562
|
+
const ifaceRe = /^\s*(?:export\s+)?interface\s+[A-Za-z_$][\w$]*(?:\s*<[^>]*>)?(?:\s+extends\s+[^{]+)?\s*\{/gm;
|
|
563
|
+
let m;
|
|
564
|
+
while ((m = ifaceRe.exec(code)) !== null) {
|
|
565
|
+
const braceStart = m.index + m[0].lastIndexOf('{');
|
|
566
|
+
const braceEnd = matchBrace(code, braceStart);
|
|
567
|
+
if (braceEnd === -1) break;
|
|
568
|
+
let end = braceEnd + 1;
|
|
569
|
+
if (code[end] === ';') end++;
|
|
570
|
+
code = code.slice(0, m.index) + blankKeepNewlines(code.slice(m.index, end)) + code.slice(end);
|
|
571
|
+
ifaceRe.lastIndex = end;
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
// 2. Remove type alias declarations (respecting balanced {}, (), [], <>)
|
|
575
|
+
const typeRe = /^\s*(?:export\s+)?type\s+[A-Za-z_$][\w$]*(?:\s*<[^>]*>)?\s*=\s*/gm;
|
|
576
|
+
while ((m = typeRe.exec(code)) !== null) {
|
|
577
|
+
let i = m.index + m[0].length;
|
|
578
|
+
let depth = 0;
|
|
579
|
+
let inStr = null;
|
|
580
|
+
while (i < code.length) {
|
|
581
|
+
const ch = code[i];
|
|
582
|
+
if (inStr) {
|
|
583
|
+
if (ch === '\\') { i += 2; continue; }
|
|
584
|
+
if (ch === inStr) inStr = null;
|
|
585
|
+
i++;
|
|
586
|
+
continue;
|
|
587
|
+
}
|
|
588
|
+
if (ch === '"' || ch === "'" || ch === '`') { inStr = ch; i++; continue; }
|
|
589
|
+
if (ch === '{' || ch === '(' || ch === '[') depth++;
|
|
590
|
+
else if (ch === '}' || ch === ')' || ch === ']') depth = Math.max(0, depth - 1);
|
|
591
|
+
else if (depth === 0 && ch === ';') { i++; break; }
|
|
592
|
+
else if (depth === 0 && ch === '\n') {
|
|
593
|
+
const rest = code.slice(i + 1).match(/^\s*([|&])/);
|
|
594
|
+
if (!rest) break;
|
|
595
|
+
}
|
|
596
|
+
i++;
|
|
597
|
+
}
|
|
598
|
+
code = code.slice(0, m.index) + blankKeepNewlines(code.slice(m.index, i)) + code.slice(i);
|
|
599
|
+
typeRe.lastIndex = i;
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
// Helper to scan a type expression until a top-level delimiter in stopChars
|
|
603
|
+
function scanTypeEnd(str, start, stopChars) {
|
|
604
|
+
let i = start;
|
|
605
|
+
let depth = 0;
|
|
606
|
+
let angleDepth = 0;
|
|
607
|
+
let inStr = null;
|
|
608
|
+
while (i < str.length) {
|
|
609
|
+
const ch = str[i];
|
|
610
|
+
if (inStr) {
|
|
611
|
+
if (ch === '\\') { i += 2; continue; }
|
|
612
|
+
if (ch === inStr) inStr = null;
|
|
613
|
+
i++;
|
|
614
|
+
continue;
|
|
615
|
+
}
|
|
616
|
+
if (ch === '"' || ch === "'" || ch === '`') { inStr = ch; i++; continue; }
|
|
617
|
+
if (ch === '=' && str[i + 1] === '>') { i += 2; continue; }
|
|
618
|
+
if (ch === '<') { angleDepth++; i++; continue; }
|
|
619
|
+
if (ch === '>' && angleDepth > 0) { angleDepth--; i++; continue; }
|
|
620
|
+
if (ch === '{' || ch === '(' || ch === '[') { depth++; i++; continue; }
|
|
621
|
+
if (ch === '}' || ch === ')' || ch === ']') {
|
|
622
|
+
if (depth === 0 && angleDepth === 0 && stopChars.has(ch)) return i;
|
|
623
|
+
depth = Math.max(0, depth - 1);
|
|
624
|
+
i++;
|
|
625
|
+
continue;
|
|
626
|
+
}
|
|
627
|
+
if (depth === 0 && angleDepth === 0 && stopChars.has(ch)) return i;
|
|
628
|
+
i++;
|
|
629
|
+
}
|
|
630
|
+
return i;
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
// 3. Remove variable declaration type annotations: (const|let|var) id: Type
|
|
634
|
+
// Never touches object literals or ternaries on the RHS of '='.
|
|
635
|
+
const varRe = /\b(const|let|var)(\s+[A-Za-z_$][\w$]*)(\??\s*:\s*)/g;
|
|
636
|
+
const varStops = new Set(['=', ';', '\n']);
|
|
637
|
+
while ((m = varRe.exec(code)) !== null) {
|
|
638
|
+
const colonStart = m.index + m[1].length + m[2].length;
|
|
639
|
+
const typeStart = colonStart + m[3].length;
|
|
640
|
+
const typeEnd = scanTypeEnd(code, typeStart, varStops);
|
|
641
|
+
code = code.slice(0, colonStart) + blankKeepNewlines(code.slice(colonStart, typeEnd)) + code.slice(typeEnd);
|
|
642
|
+
varRe.lastIndex = colonStart;
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
// 4. Remove function / arrow parameter & return type annotations.
|
|
646
|
+
// Walk delimiter stack so we only strip 'param?: Type' when the innermost
|
|
647
|
+
// enclosing delimiter is '(' (never inside '{ a: 1, b: true }' object literals).
|
|
648
|
+
let out = '';
|
|
649
|
+
const delimStack = [];
|
|
650
|
+
let i = 0;
|
|
651
|
+
let inStr = null;
|
|
652
|
+
let inLineComment = false;
|
|
653
|
+
let inBlockComment = false;
|
|
654
|
+
const paramStops = new Set([',', ')', '=']);
|
|
655
|
+
|
|
656
|
+
while (i < code.length) {
|
|
657
|
+
const ch = code[i];
|
|
658
|
+
const next = code[i + 1];
|
|
659
|
+
|
|
660
|
+
if (inLineComment) {
|
|
661
|
+
out += ch;
|
|
662
|
+
if (ch === '\n') inLineComment = false;
|
|
663
|
+
i++;
|
|
664
|
+
continue;
|
|
665
|
+
}
|
|
666
|
+
if (inBlockComment) {
|
|
667
|
+
out += ch;
|
|
668
|
+
if (ch === '*' && next === '/') { out += '/'; i += 2; inBlockComment = false; continue; }
|
|
669
|
+
i++;
|
|
670
|
+
continue;
|
|
671
|
+
}
|
|
672
|
+
if (inStr) {
|
|
673
|
+
out += ch;
|
|
674
|
+
if (ch === '\\' && next !== undefined) { out += next; i += 2; continue; }
|
|
675
|
+
if (ch === inStr) inStr = null;
|
|
676
|
+
i++;
|
|
677
|
+
continue;
|
|
678
|
+
}
|
|
679
|
+
if (ch === '/' && next === '/') { inLineComment = true; out += '//'; i += 2; continue; }
|
|
680
|
+
if (ch === '/' && next === '*') { inBlockComment = true; out += '/*'; i += 2; continue; }
|
|
681
|
+
if (ch === '"' || ch === "'" || ch === '`') { inStr = ch; out += ch; i++; continue; }
|
|
682
|
+
|
|
683
|
+
if (ch === '(' || ch === '{' || ch === '[') {
|
|
684
|
+
delimStack.push(ch);
|
|
685
|
+
out += ch;
|
|
686
|
+
i++;
|
|
687
|
+
continue;
|
|
688
|
+
}
|
|
689
|
+
if (ch === ')' || ch === '}' || ch === ']') {
|
|
690
|
+
delimStack.pop();
|
|
691
|
+
out += ch;
|
|
692
|
+
i++;
|
|
693
|
+
// Check for return type annotation after ')': '): ReturnType {' or '): ReturnType =>'
|
|
694
|
+
if (ch === ')') {
|
|
695
|
+
const retMatch = /^(\s*:\s*)/.exec(code.slice(i));
|
|
696
|
+
if (retMatch) {
|
|
697
|
+
const typeStart = i + retMatch[1].length;
|
|
698
|
+
const typeEnd = scanTypeEnd(code, typeStart, new Set(['{', '=', ';', '\n']));
|
|
699
|
+
const after = code.slice(typeEnd);
|
|
700
|
+
// Only strip if followed by function body '{' or arrow '=>' (not a ternary branch)
|
|
701
|
+
if (/^\s*(\{|=>)/.test(after) && code.slice(typeStart, typeEnd).trim().length > 0) {
|
|
702
|
+
out += blankKeepNewlines(code.slice(i, typeEnd));
|
|
703
|
+
i = typeEnd;
|
|
704
|
+
}
|
|
705
|
+
}
|
|
706
|
+
}
|
|
707
|
+
continue;
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
// Inside '(...)', check for parameter type annotation after '(' or ','
|
|
711
|
+
if (delimStack[delimStack.length - 1] === '(') {
|
|
712
|
+
const prevNonWs = out.trimEnd().slice(-1);
|
|
713
|
+
if (prevNonWs === '(' || prevNonWs === ',') {
|
|
714
|
+
const paramMatch = /^(\s*(?:\.\.\.)?[A-Za-z_$][\w$]*)(\??\s*:\s*)/.exec(code.slice(i));
|
|
715
|
+
if (paramMatch) {
|
|
716
|
+
out += paramMatch[1];
|
|
717
|
+
const colonStart = i + paramMatch[1].length;
|
|
718
|
+
const typeStart = colonStart + paramMatch[2].length;
|
|
719
|
+
const typeEnd = scanTypeEnd(code, typeStart, paramStops);
|
|
720
|
+
out += blankKeepNewlines(code.slice(colonStart, typeEnd));
|
|
721
|
+
i = typeEnd;
|
|
722
|
+
continue;
|
|
723
|
+
}
|
|
724
|
+
}
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
out += ch;
|
|
728
|
+
i++;
|
|
729
|
+
}
|
|
730
|
+
code = out;
|
|
731
|
+
|
|
732
|
+
// 5. Remove 'as Type' casts outside import/export specifiers
|
|
733
|
+
code = code.replace(
|
|
734
|
+
/(\b(?:import|export)\s*\{[^}]*\})|\s+as\s+(?:const\b|[A-Za-z_$][\w$.<>\[\]|&?]*(?:\s*[|&]\s*[A-Za-z_$][\w$.<>\[\]|&?]*)*)/g,
|
|
735
|
+
(full, impExp) => impExp ? impExp : blankKeepNewlines(full)
|
|
736
|
+
);
|
|
737
|
+
|
|
738
|
+
// 6. Remove generic type parameters from function/class/call expressions: foo<T>(
|
|
739
|
+
code = code.replace(/(?<=[\w$])<[^>()=;]+>(?=\s*\()/g, (m) => blankKeepNewlines(m));
|
|
740
|
+
|
|
566
741
|
return code;
|
|
567
742
|
}
|
|
568
743
|
|
|
@@ -709,7 +884,10 @@ function emitReactiveEdits(rootNode, src, vars, edits, initialShadows) {
|
|
|
709
884
|
const isMemberProp = p.type === 'MemberExpression' && p.property === node && !p.computed;
|
|
710
885
|
const isCallee = (p.type === 'CallExpression' || p.type === 'NewExpression') && p.callee === node;
|
|
711
886
|
const isAssignTarget = (p.type === 'AssignmentExpression' && p.left === node) || p.type === 'UpdateExpression';
|
|
712
|
-
const isPropertyKey =
|
|
887
|
+
const isPropertyKey =
|
|
888
|
+
(p.type === 'Property' || p.type === 'MethodDefinition' || p.type === 'PropertyDefinition') &&
|
|
889
|
+
p.key === node &&
|
|
890
|
+
!p.computed;
|
|
713
891
|
const isShorthand = p.type === 'Property' && p.shorthand && p.value === node;
|
|
714
892
|
const isMemberObject = p.type === 'MemberExpression' && p.object === node;
|
|
715
893
|
|
|
@@ -836,9 +1014,13 @@ export function compile(source, options = {}) {
|
|
|
836
1014
|
const hasStyles = styleContent.trim().length > 0;
|
|
837
1015
|
const scopedCss = hasStyles ? scopeStyles(styleContent, scopeHash) : '';
|
|
838
1016
|
|
|
1017
|
+
// Strip HTML comments first so commented-out blocks or literal braces inside
|
|
1018
|
+
// comments never trigger block transforms or expression errors.
|
|
1019
|
+
let rawTemplate = rawTemplateSource.replace(/<!--[\s\S]*?-->/g, '');
|
|
1020
|
+
|
|
839
1021
|
// Flatten else-if chains into nested if/else before anything else looks at
|
|
840
1022
|
// the template, so the block handling below only ever sees the simple form.
|
|
841
|
-
|
|
1023
|
+
rawTemplate = desugarElseIf(rawTemplate);
|
|
842
1024
|
|
|
843
1025
|
// Pre-process attribute-bound expressions BEFORE extractExpressions using
|
|
844
1026
|
// balanced-brace matching (handles nested object literals, ternaries, etc.)
|
|
@@ -1126,7 +1308,7 @@ export function compile(source, options = {}) {
|
|
|
1126
1308
|
const rewrite = (expr) => transformTemplateExpression(expr, reactiveVars, locals);
|
|
1127
1309
|
|
|
1128
1310
|
if (node.type === 'text') {
|
|
1129
|
-
const text = node.data
|
|
1311
|
+
const text = escapeTemplateLiteral(node.data);
|
|
1130
1312
|
if (text.trim().length > 0) {
|
|
1131
1313
|
const id = `t${uid++}`;
|
|
1132
1314
|
domCode += ` const ${id} = document.createTextNode(\`${text}\`);\n`;
|
|
@@ -1400,10 +1582,16 @@ export function compile(source, options = {}) {
|
|
|
1400
1582
|
} else if (key.startsWith('bind:')) {
|
|
1401
1583
|
const prop = key.slice(5);
|
|
1402
1584
|
// bind:value={name} names a signal; the marker resolves back to it.
|
|
1403
|
-
const bound = resolveHandler(val);
|
|
1585
|
+
const bound = resolveHandler(val).replace(/^\{|\}$/g, '').trim();
|
|
1404
1586
|
if (prop === 'value') {
|
|
1405
1587
|
domCode += ` ${id}.addEventListener('input', (e) => { set_${bound}(e.target.value); });\n`;
|
|
1406
1588
|
domCode += ` createEffect(() => { ${id}.value = ${bound}() ?? ''; });\n`;
|
|
1589
|
+
} else if (prop === 'this') {
|
|
1590
|
+
if (stateVars.has(bound)) {
|
|
1591
|
+
domCode += ` set_${bound}(${id});\n`;
|
|
1592
|
+
} else {
|
|
1593
|
+
domCode += ` ${bound} = ${id};\n`;
|
|
1594
|
+
}
|
|
1407
1595
|
}
|
|
1408
1596
|
} else if (key === 'class' || key === 'className') {
|
|
1409
1597
|
// Static values: emit as a plain string literal. A template literal here
|
package/types/index.d.ts
CHANGED
|
@@ -13,6 +13,8 @@ export interface CompileOptions {
|
|
|
13
13
|
filename?: string;
|
|
14
14
|
/** Emit a source map mapping generated JS back to the .sola file. Default true. */
|
|
15
15
|
sourcemap?: boolean;
|
|
16
|
+
/** Also export `css` and `scopeHash` constants from ESM output for Shadow DOM hosts. */
|
|
17
|
+
exportStyles?: boolean;
|
|
16
18
|
}
|
|
17
19
|
|
|
18
20
|
export interface CompileResult {
|
|
@@ -24,6 +26,8 @@ export interface CompileResult {
|
|
|
24
26
|
css: string;
|
|
25
27
|
/** Source map for `code`, or null when `sourcemap` is false. */
|
|
26
28
|
map: SourceMap | null;
|
|
29
|
+
/** Generated TypeScript declaration (.d.ts) for the compiled component. */
|
|
30
|
+
dts: string;
|
|
27
31
|
}
|
|
28
32
|
|
|
29
33
|
export interface SourceMap {
|
|
@@ -40,6 +44,6 @@ export function compile(source: string, options?: CompileOptions): CompileResult
|
|
|
40
44
|
|
|
41
45
|
/** Mount function produced by compiling a .sola component. */
|
|
42
46
|
export type SolaComponent<P = Record<string, unknown>> = (
|
|
43
|
-
target:
|
|
47
|
+
target: Element | ShadowRoot,
|
|
44
48
|
props?: P
|
|
45
49
|
) => () => void;
|