@sola-air-ui/compiler 1.5.3 → 1.6.0

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 CHANGED
@@ -36,7 +36,7 @@ npx sola build App.sola
36
36
  npx sola-check src/
37
37
  ```
38
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`).
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`), and state written with `.set()` or `.update()` instead of assignment (`SOLA002`), which compiles but throws when it runs.
40
40
 
41
41
  ## What a `.sola` file looks like
42
42
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sola-air-ui/compiler",
3
- "version": "1.5.3",
3
+ "version": "1.6.0",
4
4
  "description": "Compiler for Sola single-file components (.sola) \u2014 transforms template + script + style to ES modules",
5
5
  "type": "module",
6
6
  "main": "./src/index.js",
package/src/check-cli.js CHANGED
File without changes
package/src/check.js CHANGED
@@ -11,9 +11,52 @@ import { compile } from './index.js';
11
11
 
12
12
  export const RULES = {
13
13
  SOLA000: 'compile error',
14
- SOLA001: 'unbalanced block'
14
+ SOLA001: 'unbalanced block',
15
+ SOLA002: 'state written with .set() or .update()'
15
16
  };
16
17
 
18
+ // Text of every <script> body. Plain index search, so `</script >` and odd
19
+ // casing are handled the same way the browser would end the element.
20
+ function scriptBodies(source) {
21
+ const lower = source.toLowerCase();
22
+ const bodies = [];
23
+ let from = 0;
24
+ for (;;) {
25
+ const open = lower.indexOf('<script', from);
26
+ if (open < 0) break;
27
+ const start = lower.indexOf('>', open);
28
+ if (start < 0) break;
29
+ const end = lower.indexOf('</script', start);
30
+ bodies.push(source.slice(start + 1, end < 0 ? source.length : end));
31
+ if (end < 0) break;
32
+ from = end + 8;
33
+ }
34
+ return bodies.join('\n');
35
+ }
36
+
37
+ // A `$state` / `$persist` name is its signal getter, so `count.set(1)` compiles
38
+ // to `count().set(1)` and throws when it runs. State is written by assignment.
39
+ function checkSetterCalls(source, add) {
40
+ const scripts = scriptBodies(source);
41
+ const names = new Set();
42
+ for (const m of scripts.matchAll(/\b(?:let|var|const)\s+([\w$]+)\s*=\s*\$(?:state|persist)\s*\(/g)) names.add(m[1]);
43
+ if (!names.size) return;
44
+ const blank = (m) => m.replace(/[^\n]/g, ' ');
45
+ const code = source
46
+ .replace(/<style\b[^>]*>[\s\S]*?<\/style>/gi, blank)
47
+ .replace(/<!--[\s\S]*?-->/g, blank)
48
+ .replace(/\/\*[\s\S]*?\*\//g, blank)
49
+ .replace(/(^|[^:])\/\/[^\n]*/g, (m, pre) => pre + blank(m.slice(pre.length)));
50
+ const call = /(?<![\w$.])([\w$]+)(\(\))?\s*\.\s*(set|update)\s*\(/g;
51
+ let m;
52
+ while ((m = call.exec(code))) {
53
+ const [, name, , method] = m;
54
+ if (!names.has(name)) continue;
55
+ const fix = method === 'set' ? `${name} = value` : `${name} = fn(${name})`;
56
+ add(m.index, 'error', 'SOLA002', `\`${name}.${method}()\` throws at runtime: \`${name}\` is state, which is written by assignment. Use \`${fix}\`.`);
57
+ }
58
+ }
59
+
17
60
  // Blank out <script> and <style> bodies and HTML comments, keeping newlines so
18
61
  // line numbers hold. A commented-out block is not a block.
19
62
  function maskNonTemplate(source) {
@@ -68,6 +111,8 @@ export function checkSource(source, filename = 'component.sola') {
68
111
  });
69
112
  }
70
113
 
114
+ checkSetterCalls(source, add);
115
+
71
116
  const template = maskNonTemplate(source);
72
117
  const tag = /\{(#if|#each|\/if|\/each|:else)(?![\w])/g;
73
118
  const stack = [];
@@ -105,7 +150,7 @@ export function checkSource(source, filename = 'component.sola') {
105
150
  add(at, 'error', 'SOLA001', '`{:else if}` is missing a condition. It renders as literal text.');
106
151
  }
107
152
  } else {
108
- add(at, 'error', 'SOLA001', '`{:else}` unexpected token (use `{:else}` or `{:else if <condition>}`). It renders as literal text.');
153
+ add(at, 'error', 'SOLA001', '`{:else}` has extra text after it (use `{:else}` or `{:else if <condition>}`). It renders as literal text.');
109
154
  }
110
155
  }
111
156
  }
package/src/index.js CHANGED
@@ -73,9 +73,9 @@ function addScopeToCompound(compound, scopeHash) {
73
73
  // :global(body) → body
74
74
  // The scope class always goes on the last compound that is not global.
75
75
  function scopeSelector(selector, scopeHash) {
76
- const tokens = []; // alternating compounds and combinators
76
+ const pieces = []; // alternating compounds and combinators
77
77
  let depth = 0, cur = '', comb = '';
78
- const flushCompound = () => { if (cur) { tokens.push({ compound: cur }); cur = ''; } };
78
+ const flushCompound = () => { if (cur) { pieces.push({ compound: cur }); cur = ''; } };
79
79
  for (let i = 0; i < selector.length; i++) {
80
80
  const c = selector[i];
81
81
  if (c === '(' || c === '[') depth++;
@@ -85,15 +85,15 @@ function scopeSelector(selector, scopeHash) {
85
85
  comb += c;
86
86
  continue;
87
87
  }
88
- if (comb) { tokens.push({ comb }); comb = ''; }
88
+ if (comb) { pieces.push({ comb }); comb = ''; }
89
89
  cur += c;
90
90
  }
91
91
  flushCompound();
92
- if (comb) tokens.push({ comb });
92
+ if (comb) pieces.push({ comb });
93
93
 
94
94
  let last = -1;
95
- for (let i = 0; i < tokens.length; i++) {
96
- const t = tokens[i];
95
+ for (let i = 0; i < pieces.length; i++) {
96
+ const t = pieces[i];
97
97
  if (t.compound === undefined) continue;
98
98
  const m = t.compound.match(/^:global\(([\s\S]*)\)$/);
99
99
  // A comma list must stay grouped, or `.a :global(.x, .y)` would turn into
@@ -105,8 +105,8 @@ function scopeSelector(selector, scopeHash) {
105
105
  t.global = true;
106
106
  } else { last = i; }
107
107
  }
108
- if (last >= 0) tokens[last].compound = addScopeToCompound(tokens[last].compound, scopeHash);
109
- return tokens.map(t => t.compound ?? t.comb).join('');
108
+ if (last >= 0) pieces[last].compound = addScopeToCompound(pieces[last].compound, scopeHash);
109
+ return pieces.map(t => t.compound ?? t.comb).join('');
110
110
  }
111
111
 
112
112
  function scopeSelectorList(selector, scopeHash) {
package/types/check.d.ts CHANGED
@@ -7,8 +7,8 @@ export interface Diagnostic {
7
7
  /** 1-based. */
8
8
  column: number;
9
9
  severity: 'error' | 'warning';
10
- /** SOLA000 compile error, SOLA001 unbalanced block. */
11
- code: 'SOLA000' | 'SOLA001';
10
+ /** SOLA000 compile error, SOLA001 unbalanced block, SOLA002 state written with .set() or .update(). */
11
+ code: 'SOLA000' | 'SOLA001' | 'SOLA002';
12
12
  message: string;
13
13
  }
14
14