@sola-air-ui/compiler 1.3.0 → 1.4.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/package.json +8 -3
- package/src/check-cli.js +54 -0
- package/src/check.js +102 -0
- package/src/index.js +8 -0
- package/types/check.d.ts +24 -0
package/package.json
CHANGED
|
@@ -1,14 +1,19 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sola-air-ui/compiler",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.4.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",
|
|
7
7
|
"bin": {
|
|
8
|
-
"sola": "src/cli.js"
|
|
8
|
+
"sola": "src/cli.js",
|
|
9
|
+
"sola-check": "src/check-cli.js"
|
|
9
10
|
},
|
|
10
11
|
"exports": {
|
|
11
|
-
".": "./src/index.js"
|
|
12
|
+
".": "./src/index.js",
|
|
13
|
+
"./check": {
|
|
14
|
+
"types": "./types/check.d.ts",
|
|
15
|
+
"default": "./src/check.js"
|
|
16
|
+
}
|
|
12
17
|
},
|
|
13
18
|
"scripts": {
|
|
14
19
|
"test": "node test/compiler.test.js"
|
package/src/check-cli.js
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { checkFiles, RULES } from './check.js';
|
|
4
|
+
|
|
5
|
+
const args = process.argv.slice(2);
|
|
6
|
+
|
|
7
|
+
if (args.includes('-h') || args.includes('--help')) {
|
|
8
|
+
console.log(`
|
|
9
|
+
Usage:
|
|
10
|
+
sola-check [paths...] [options]
|
|
11
|
+
|
|
12
|
+
Checks .sola files for compile errors and for unbalanced template blocks.
|
|
13
|
+
The compiler accepts an unclosed {#if} or a stray {/each} without an error
|
|
14
|
+
and renders it as literal text. With no paths it checks the current directory. node_modules, dist and .git
|
|
15
|
+
are skipped.
|
|
16
|
+
|
|
17
|
+
Options:
|
|
18
|
+
--strict Exit 1 on warnings as well as errors (no rule warns yet)
|
|
19
|
+
--json Print diagnostics as JSON
|
|
20
|
+
-h, --help Show this help message
|
|
21
|
+
|
|
22
|
+
Rules:
|
|
23
|
+
${Object.entries(RULES).map(([code, text]) => ` ${code} ${text}`).join('\n')}
|
|
24
|
+
`);
|
|
25
|
+
process.exit(0);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const strict = args.includes('--strict');
|
|
29
|
+
const json = args.includes('--json');
|
|
30
|
+
const targets = args.filter((a) => !a.startsWith('-'));
|
|
31
|
+
|
|
32
|
+
let result;
|
|
33
|
+
try {
|
|
34
|
+
result = checkFiles(targets.length ? targets : ['.']);
|
|
35
|
+
} catch (err) {
|
|
36
|
+
console.error(`sola-check: ${err.message}`);
|
|
37
|
+
process.exit(2);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const { files, diagnostics } = result;
|
|
41
|
+
const errors = diagnostics.filter((d) => d.severity === 'error').length;
|
|
42
|
+
const warnings = diagnostics.length - errors;
|
|
43
|
+
|
|
44
|
+
if (json) {
|
|
45
|
+
console.log(JSON.stringify({ files: files.length, errors, warnings, diagnostics }, null, 2));
|
|
46
|
+
} else {
|
|
47
|
+
for (const d of diagnostics) {
|
|
48
|
+
console.log(`${d.file}:${d.line}:${d.column} ${d.severity} ${d.code} ${d.message}`);
|
|
49
|
+
}
|
|
50
|
+
const noun = files.length === 1 ? 'file' : 'files';
|
|
51
|
+
console.log(`${diagnostics.length ? '\n' : ''}${files.length} ${noun} checked: ${errors} error${errors === 1 ? '' : 's'}, ${warnings} warning${warnings === 1 ? '' : 's'}`);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
process.exit(errors || (strict && warnings) ? 1 : 0);
|
package/src/check.js
ADDED
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
// Static checks for .sola files.
|
|
2
|
+
//
|
|
3
|
+
// The compiler accepts an unbalanced block (an unclosed `{#if}`, a stray
|
|
4
|
+
// `{/each}` or `{:else}`) without an error and emits it as literal text, so a
|
|
5
|
+
// file can compile and then show `{#if ...}` on the page. These checks catch
|
|
6
|
+
// that before it ships.
|
|
7
|
+
|
|
8
|
+
import fs from 'node:fs';
|
|
9
|
+
import path from 'node:path';
|
|
10
|
+
import { compile } from './index.js';
|
|
11
|
+
|
|
12
|
+
export const RULES = {
|
|
13
|
+
SOLA000: 'compile error',
|
|
14
|
+
SOLA001: 'unbalanced block'
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
// Blank out <script> and <style> bodies, keeping newlines so line numbers hold.
|
|
18
|
+
function maskNonTemplate(source) {
|
|
19
|
+
return source.replace(/<(script|style)\b[^>]*>[\s\S]*?<\/\1>/gi, (m) => m.replace(/[^\n]/g, ' '));
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function position(source, index) {
|
|
23
|
+
const before = source.slice(0, index);
|
|
24
|
+
const line = before.split('\n').length;
|
|
25
|
+
return { line, column: index - before.lastIndexOf('\n') };
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Check one component's source. Returns diagnostics:
|
|
30
|
+
* `{ file, line, column, severity: 'error' | 'warning', code, message }`.
|
|
31
|
+
*/
|
|
32
|
+
export function checkSource(source, filename = 'component.sola') {
|
|
33
|
+
const diagnostics = [];
|
|
34
|
+
const add = (index, severity, code, message) =>
|
|
35
|
+
diagnostics.push({ file: filename, ...position(source, index), severity, code, message });
|
|
36
|
+
|
|
37
|
+
try {
|
|
38
|
+
compile(source, { filename, sourcemap: false });
|
|
39
|
+
} catch (err) {
|
|
40
|
+
const m = /\(line (\d+), col (\d+)\)/.exec(err.message);
|
|
41
|
+
diagnostics.push({
|
|
42
|
+
file: filename,
|
|
43
|
+
line: m ? Number(m[1]) : 1,
|
|
44
|
+
column: m ? Number(m[2]) : 1,
|
|
45
|
+
severity: 'error',
|
|
46
|
+
code: 'SOLA000',
|
|
47
|
+
message: err.message.replace(/^\[sola compiler\]\s*/, '').split('\n').map((s) => s.trim()).filter(Boolean).join(' ')
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const template = maskNonTemplate(source);
|
|
52
|
+
const tag = /\{(#if|#each|\/if|\/each|:else)(?![\w])/g;
|
|
53
|
+
const stack = [];
|
|
54
|
+
let match;
|
|
55
|
+
while ((match = tag.exec(template))) {
|
|
56
|
+
const kind = match[1];
|
|
57
|
+
const at = match.index;
|
|
58
|
+
if (kind === '#if' || kind === '#each') {
|
|
59
|
+
stack.push({ kind: kind.slice(1), at });
|
|
60
|
+
} else if (kind === ':else') {
|
|
61
|
+
if (stack.at(-1)?.kind !== 'if') add(at, 'error', 'SOLA001', '`{:else}` outside an `{#if}` block. It renders as literal text.');
|
|
62
|
+
} else {
|
|
63
|
+
const want = kind.slice(1);
|
|
64
|
+
const open = stack.pop();
|
|
65
|
+
if (!open) add(at, 'error', 'SOLA001', `\`{/${want}}\` has no matching \`{#${want}}\`. It renders as literal text.`);
|
|
66
|
+
else if (open.kind !== want) add(at, 'error', 'SOLA001', `\`{/${want}}\` closes \`{#${open.kind}}\` from line ${position(source, open.at).line}.`);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
for (const open of stack) add(open.at, 'error', 'SOLA001', `\`{#${open.kind}}\` is never closed. It renders as literal text.`);
|
|
70
|
+
|
|
71
|
+
return diagnostics.sort((a, b) => a.line - b.line || a.column - b.column);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const SKIP_DIRS = new Set(['node_modules', 'dist', 'build', '.git', '.svelte-kit', '.vercel']);
|
|
75
|
+
|
|
76
|
+
/** Find every .sola file under the given files or directories. */
|
|
77
|
+
export function findSolaFiles(targets) {
|
|
78
|
+
const files = [];
|
|
79
|
+
const walk = (p) => {
|
|
80
|
+
const stat = fs.statSync(p);
|
|
81
|
+
if (stat.isFile()) {
|
|
82
|
+
if (p.endsWith('.sola')) files.push(p);
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
for (const entry of fs.readdirSync(p, { withFileTypes: true })) {
|
|
86
|
+
if (entry.isDirectory()) {
|
|
87
|
+
if (!SKIP_DIRS.has(entry.name)) walk(path.join(p, entry.name));
|
|
88
|
+
} else if (entry.name.endsWith('.sola')) {
|
|
89
|
+
files.push(path.join(p, entry.name));
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
};
|
|
93
|
+
for (const t of targets) walk(t);
|
|
94
|
+
return files.sort();
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** Check files on disk. Returns `{ files, diagnostics }`. */
|
|
98
|
+
export function checkFiles(targets) {
|
|
99
|
+
const files = findSolaFiles(targets);
|
|
100
|
+
const diagnostics = files.flatMap((f) => checkSource(fs.readFileSync(f, 'utf8'), f));
|
|
101
|
+
return { files, diagnostics };
|
|
102
|
+
}
|
package/src/index.js
CHANGED
|
@@ -976,6 +976,14 @@ export function compile(source, options = {}) {
|
|
|
976
976
|
edits.push({ start: call.end, end: call.end, replacement: ')' });
|
|
977
977
|
}
|
|
978
978
|
|
|
979
|
+
// $effect(fn) as a bare statement: the form the docs show. It compiles to
|
|
980
|
+
// createEffect(fn); the declaration form (const stop = $effect(fn)) is below.
|
|
981
|
+
if (node.type === 'ExpressionStatement' && node.expression.type === 'CallExpression' &&
|
|
982
|
+
node.expression.callee.type === 'Identifier' && node.expression.callee.name === '$effect') {
|
|
983
|
+
const callee = node.expression.callee;
|
|
984
|
+
edits.push({ start: callee.start, end: callee.end, replacement: 'createEffect' });
|
|
985
|
+
}
|
|
986
|
+
|
|
979
987
|
// $state()/$derived()/$effect()/$intent()/$data() macro declarations.
|
|
980
988
|
// Bracket edits (head + tail) leave the argument expression in place so
|
|
981
989
|
// the reactive rewrite pass below can still auto-call signals inside it.
|
package/types/check.d.ts
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
// Type declarations for @sola-air-ui/compiler/check.
|
|
2
|
+
|
|
3
|
+
export interface Diagnostic {
|
|
4
|
+
file: string;
|
|
5
|
+
/** 1-based. */
|
|
6
|
+
line: number;
|
|
7
|
+
/** 1-based. */
|
|
8
|
+
column: number;
|
|
9
|
+
severity: 'error' | 'warning';
|
|
10
|
+
/** SOLA000 compile error, SOLA001 unbalanced block. */
|
|
11
|
+
code: 'SOLA000' | 'SOLA001';
|
|
12
|
+
message: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export const RULES: Record<Diagnostic['code'], string>;
|
|
16
|
+
|
|
17
|
+
/** Check one component's source text. */
|
|
18
|
+
export function checkSource(source: string, filename?: string): Diagnostic[];
|
|
19
|
+
|
|
20
|
+
/** Every .sola file under the given files or directories, skipping node_modules, dist and .git. */
|
|
21
|
+
export function findSolaFiles(targets: string[]): string[];
|
|
22
|
+
|
|
23
|
+
/** Check .sola files on disk. */
|
|
24
|
+
export function checkFiles(targets: string[]): { files: string[]; diagnostics: Diagnostic[] };
|