@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.
Files changed (3) hide show
  1. package/README.md +57 -0
  2. package/package.json +1 -1
  3. 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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sola-air-ui/compiler",
3
- "version": "1.0.6",
3
+ "version": "1.0.8",
4
4
  "description": "Compiler for Sola single-file components (.sola) — transforms template + script + style to ES modules",
5
5
  "type": "module",
6
6
  "main": "./src/index.js",
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
- let inner = ''; let hasDyn = false;
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
- inner += `\${${expr}}`; i = closeIdx + 1; continue;
138
+ parts.push({ text: currentText });
139
+ currentText = '';
140
+ parts.push({ expr });
141
+ i = closeIdx + 1; continue;
137
142
  }
138
143
  }
139
144
  }
140
- inner += template[i++];
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
- dynAttrs.push('`' + inner + '`');
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}="${inner}"`;
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, '&quot;')}"></sola-html>`;
197
+ i = closeIdx + 1;
175
198
  } else {
176
199
  result += `<sola-expr expr="${inner.replace(/"/g, '&quot;')}"></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(/&quot;/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 propValue = dynMatch ? `(${dynAttrs[parseInt(dynMatch[1], 10)]})` : JSON.stringify(val);
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`;