@sola-air-ui/compiler 1.0.6 → 1.0.8
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 +57 -0
- package/package.json +1 -1
- package/src/index.js +59 -9
package/README.md
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# @sola-air-ui/compiler
|
|
2
|
+
|
|
3
|
+
Compiler for [Sola AIR](https://sola-air.dev) single-file `.sola` components — transforms `<script>` + template + `<style>` into a dependency-free ES module (or IIFE) that mounts real DOM nodes with fine-grained reactivity. No virtual DOM, no diffing.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install @sola-air-ui/compiler
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Programmatic use
|
|
12
|
+
|
|
13
|
+
```js
|
|
14
|
+
import { compile } from '@sola-air-ui/compiler';
|
|
15
|
+
import { readFileSync } from 'node:fs';
|
|
16
|
+
|
|
17
|
+
const source = readFileSync('App.sola', 'utf-8');
|
|
18
|
+
const result = compile(source, {
|
|
19
|
+
target: 'esm', // or 'iife'
|
|
20
|
+
exportName: 'App', // used for iife builds
|
|
21
|
+
filename: 'App.sola', // improves error messages
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
console.log(result.code); // compiled JS
|
|
25
|
+
console.log(result.css); // scoped CSS
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
## CLI
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
npx sola build App.sola
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## What a `.sola` file looks like
|
|
35
|
+
|
|
36
|
+
```html
|
|
37
|
+
<script>
|
|
38
|
+
let count = $state(0);
|
|
39
|
+
let doubled = $derived(() => count() * 2);
|
|
40
|
+
</script>
|
|
41
|
+
|
|
42
|
+
<div>
|
|
43
|
+
<h1>Count: {count()}</h1>
|
|
44
|
+
<p>Doubled: {doubled()}</p>
|
|
45
|
+
<button onclick={() => count(count() + 1)}>+1</button>
|
|
46
|
+
</div>
|
|
47
|
+
|
|
48
|
+
<style>
|
|
49
|
+
h1 { color: #0ea5e9; font-weight: 800; }
|
|
50
|
+
</style>
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
Most projects won't call this package directly — use `@sola-air-ui/vite-plugin-sola` for a normal Vite dev/build setup, or the `sola-air` meta-package to install everything at once.
|
|
54
|
+
|
|
55
|
+
## License
|
|
56
|
+
|
|
57
|
+
MIT — see the [repo root](https://github.com/rbm3267/sola-air) for the full license and [changelog](https://github.com/rbm3267/sola-air/blob/main/CHANGELOG.md).
|
package/package.json
CHANGED
package/src/index.js
CHANGED
|
@@ -125,7 +125,9 @@ function preprocessTemplate(template, dynAttrs) {
|
|
|
125
125
|
} else if (template[i] === '"' || template[i] === "'") {
|
|
126
126
|
// Quoted attribute value — scan for {expr} interpolations
|
|
127
127
|
const quote = template[i++];
|
|
128
|
-
|
|
128
|
+
const parts = [];
|
|
129
|
+
let currentText = '';
|
|
130
|
+
let hasDyn = false;
|
|
129
131
|
while (i < template.length && template[i] !== quote) {
|
|
130
132
|
if (template[i] === '{') {
|
|
131
133
|
const closeIdx = matchBrace(template, i);
|
|
@@ -133,18 +135,35 @@ function preprocessTemplate(template, dynAttrs) {
|
|
|
133
135
|
const expr = template.slice(i + 1, closeIdx).trim();
|
|
134
136
|
if (!expr.startsWith('#') && !expr.startsWith('/') && !expr.startsWith(':')) {
|
|
135
137
|
hasDyn = true;
|
|
136
|
-
|
|
138
|
+
parts.push({ text: currentText });
|
|
139
|
+
currentText = '';
|
|
140
|
+
parts.push({ expr });
|
|
141
|
+
i = closeIdx + 1; continue;
|
|
137
142
|
}
|
|
138
143
|
}
|
|
139
144
|
}
|
|
140
|
-
|
|
145
|
+
currentText += template[i++];
|
|
141
146
|
}
|
|
147
|
+
parts.push({ text: currentText });
|
|
142
148
|
if (i < template.length) i++; // skip closing quote
|
|
143
149
|
if (hasDyn) {
|
|
144
|
-
|
|
150
|
+
// Build a string-concatenation expression instead of a runtime backtick
|
|
151
|
+
// template literal. Confirmed by driving a real browser against a live
|
|
152
|
+
// ServiceNow Service Portal instance: its widget-script delivery pipeline
|
|
153
|
+
// silently mangles `${expr}` interpolation inside a template literal down
|
|
154
|
+
// to bare, unevaluated expression text (e.g. `background: ${x}` becomes
|
|
155
|
+
// the literal string "background: x" — the function reference itself,
|
|
156
|
+
// never called). Every prior "the reactive binding doesn't do anything"
|
|
157
|
+
// report came from this exact code path. Concatenation is semantically
|
|
158
|
+
// identical and isn't affected by whatever that pipeline does to backticks.
|
|
159
|
+
const combined = parts
|
|
160
|
+
.filter((p) => p.expr !== undefined || p.text !== '')
|
|
161
|
+
.map((p) => (p.expr !== undefined ? `(${p.expr})` : JSON.stringify(p.text)))
|
|
162
|
+
.join(' + ');
|
|
163
|
+
dynAttrs.push(combined || `''`);
|
|
145
164
|
result += `${attrName}="__soladyn_${dynAttrs.length - 1}__"`;
|
|
146
165
|
} else {
|
|
147
|
-
result += `${attrName}="${
|
|
166
|
+
result += `${attrName}="${currentText}"`;
|
|
148
167
|
}
|
|
149
168
|
} else {
|
|
150
169
|
result += `${attrName}=`;
|
|
@@ -172,6 +191,10 @@ function extractExpressions(text) {
|
|
|
172
191
|
const inner = text.slice(i + 1, closeIdx).trim();
|
|
173
192
|
if (inner.startsWith('#') || inner.startsWith('/') || inner.startsWith(':')) {
|
|
174
193
|
result += text.slice(i, closeIdx + 1); i = closeIdx + 1;
|
|
194
|
+
} else if (inner.startsWith('@html')) {
|
|
195
|
+
const htmlExpr = inner.slice('@html'.length).trim();
|
|
196
|
+
result += `<sola-html expr="${htmlExpr.replace(/"/g, '"')}"></sola-html>`;
|
|
197
|
+
i = closeIdx + 1;
|
|
175
198
|
} else {
|
|
176
199
|
result += `<sola-expr expr="${inner.replace(/"/g, '"')}"></sola-expr>`;
|
|
177
200
|
i = closeIdx + 1;
|
|
@@ -245,7 +268,7 @@ export function compile(source, options = {}) {
|
|
|
245
268
|
templateNodes.pop();
|
|
246
269
|
}
|
|
247
270
|
}
|
|
248
|
-
}, { recognizeSelfClosing: true, lowerCaseTags: false });
|
|
271
|
+
}, { recognizeSelfClosing: true, lowerCaseTags: false, lowerCaseAttributeNames: false });
|
|
249
272
|
|
|
250
273
|
parser.write(rawTemplate.trim() || '<div></div>');
|
|
251
274
|
parser.end();
|
|
@@ -448,6 +471,20 @@ export function compile(source, options = {}) {
|
|
|
448
471
|
return;
|
|
449
472
|
}
|
|
450
473
|
|
|
474
|
+
// {@html expr} — renders expr as raw HTML via innerHTML, unlike {expr} which always
|
|
475
|
+
// goes through textContent. Wrapped in its own element so setting innerHTML can't
|
|
476
|
+
// clobber unrelated sibling nodes. Callers are responsible for trusting/sanitizing
|
|
477
|
+
// whatever markup they pass — same contract as innerHTML anywhere else.
|
|
478
|
+
if (node.name === 'sola-html') {
|
|
479
|
+
const expr = (node.attribs.expr || '').replace(/"/g, '"');
|
|
480
|
+
const id = `h${uid++}`;
|
|
481
|
+
domCode += ` const ${id} = document.createElement('div');\n`;
|
|
482
|
+
domCode += ` ${id}.style.display = 'contents';\n`;
|
|
483
|
+
domCode += ` ${parentVar}.appendChild(${id});\n`;
|
|
484
|
+
domCode += ` createEffect(() => { ${id}.innerHTML = String((${expr}) ?? ''); });\n`;
|
|
485
|
+
return;
|
|
486
|
+
}
|
|
487
|
+
|
|
451
488
|
// Nested custom component
|
|
452
489
|
if (importedComponentSet.has(node.name)) {
|
|
453
490
|
const cid = `comp${uid++}`;
|
|
@@ -458,10 +495,23 @@ export function compile(source, options = {}) {
|
|
|
458
495
|
// Dynamically-bound props (rows={items}) arrive here as __soladyn_N__ placeholders —
|
|
459
496
|
// resolve them back to the real expression from dynAttrs and pass it as a raw
|
|
460
497
|
// (unquoted) property so the child receives live data, not the placeholder text.
|
|
498
|
+
//
|
|
499
|
+
// `onXxx`-named props (e.g. onChange={handler}) are a special case: preprocessTemplate
|
|
500
|
+
// strips their {expr} down to raw identifier text before this code ever sees them
|
|
501
|
+
// (the same exception that makes real onclick/on:click DOM handlers work), so they
|
|
502
|
+
// never arrive as a __soladyn_N__ placeholder at all. Since preprocessTemplate can't
|
|
503
|
+
// yet tell a component apart from a native element, that exception can't distinguish
|
|
504
|
+
// "this is a DOM event on <button>" from "this is a callback prop on <Toggle>" — so
|
|
505
|
+
// treat any on-prefixed key here as a raw expression too, same as a real dynAttr.
|
|
461
506
|
const propEntries = [];
|
|
462
507
|
for (const [key, val] of Object.entries(node.attribs || {})) {
|
|
463
508
|
const dynMatch = /^__soladyn_(\d+)__$/.exec(val);
|
|
464
|
-
const
|
|
509
|
+
const isEventLikeProp = /^on:?[\w]+$/.test(key);
|
|
510
|
+
const propValue = dynMatch
|
|
511
|
+
? `(${dynAttrs[parseInt(dynMatch[1], 10)]})`
|
|
512
|
+
: isEventLikeProp
|
|
513
|
+
? `(${val})`
|
|
514
|
+
: JSON.stringify(val);
|
|
465
515
|
propEntries.push(`${JSON.stringify(key)}: ${propValue}`);
|
|
466
516
|
}
|
|
467
517
|
|
|
@@ -666,9 +716,9 @@ export function compile(source, options = {}) {
|
|
|
666
716
|
output += ` // Reactive DOM graph\n`;
|
|
667
717
|
output += domCode;
|
|
668
718
|
|
|
669
|
-
output += `\n __flush_mounts();\n`;
|
|
719
|
+
output += `\n __flush_mounts(__ctx);\n`;
|
|
670
720
|
output += `\n return () => {\n`;
|
|
671
|
-
output += ` __flush_destroys();\n`;
|
|
721
|
+
output += ` __flush_destroys(__ctx);\n`;
|
|
672
722
|
output += ` popContext(__ctx);\n`;
|
|
673
723
|
output += ` __target.innerHTML = '';\n`;
|
|
674
724
|
output += ` };\n`;
|