@sola-air-ui/compiler 1.0.3 → 1.0.5
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 +3 -3
- package/src/cli.js +26 -7
- package/src/index.js +46 -12
package/package.json
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sola-air-ui/compiler",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.5",
|
|
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",
|
|
7
7
|
"bin": {
|
|
8
|
-
"sola": "
|
|
8
|
+
"sola": "src/cli.js"
|
|
9
9
|
},
|
|
10
10
|
"exports": {
|
|
11
11
|
".": "./src/index.js"
|
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
"homepage": "https://sola-air.dev",
|
|
26
26
|
"repository": {
|
|
27
27
|
"type": "git",
|
|
28
|
-
"url": "https://github.com/rbm3267/sola-air"
|
|
28
|
+
"url": "git+https://github.com/rbm3267/sola-air.git"
|
|
29
29
|
},
|
|
30
30
|
"dependencies": {
|
|
31
31
|
"acorn": "^8.10.0",
|
package/src/cli.js
CHANGED
|
@@ -4,6 +4,8 @@ import fs from 'node:fs';
|
|
|
4
4
|
import path from 'node:path';
|
|
5
5
|
import { compile } from './index.js';
|
|
6
6
|
|
|
7
|
+
const { version } = JSON.parse(fs.readFileSync(new URL('../package.json', import.meta.url), 'utf8'));
|
|
8
|
+
|
|
7
9
|
const args = process.argv.slice(2);
|
|
8
10
|
|
|
9
11
|
if (args.length === 0 || args.includes('-h') || args.includes('--help')) {
|
|
@@ -12,30 +14,42 @@ if (args.length === 0 || args.includes('-h') || args.includes('--help')) {
|
|
|
12
14
|
sola compile <file.sola> [options]
|
|
13
15
|
|
|
14
16
|
Options:
|
|
15
|
-
-o, --out <file>
|
|
16
|
-
|
|
17
|
-
|
|
17
|
+
-o, --out <file> Write compiled output to a file (default: stdout)
|
|
18
|
+
--target <esm|iife> Output format (default: esm)
|
|
19
|
+
esm — ES module with import/export (default, for bundlers)
|
|
20
|
+
iife — Self-contained IIFE for no-bundler environments
|
|
21
|
+
(ServiceNow Service Portal, plain <script> tags)
|
|
22
|
+
--export-name <Name> Global name for IIFE target (default: SolaComponent)
|
|
23
|
+
The compiled component mounts as window['Name'](el, props)
|
|
24
|
+
-v, --version Print compiler version
|
|
25
|
+
-h, --help Show this help message
|
|
18
26
|
|
|
19
27
|
Examples:
|
|
20
28
|
sola compile App.sola -o dist/App.js
|
|
21
|
-
sola compile
|
|
29
|
+
sola compile Widget.sola --target iife --export-name MyWidget -o dist/my-widget.js
|
|
22
30
|
`);
|
|
23
31
|
process.exit(0);
|
|
24
32
|
}
|
|
25
33
|
|
|
26
34
|
if (args.includes('-v') || args.includes('--version')) {
|
|
27
|
-
console.log(
|
|
35
|
+
console.log(`Sola Compiler v${version}`);
|
|
28
36
|
process.exit(0);
|
|
29
37
|
}
|
|
30
38
|
|
|
31
39
|
let inputFile = null;
|
|
32
40
|
let outputFile = null;
|
|
41
|
+
let target = 'esm';
|
|
42
|
+
let exportName = 'SolaComponent';
|
|
33
43
|
|
|
34
44
|
for (let i = 0; i < args.length; i++) {
|
|
35
45
|
const arg = args[i];
|
|
36
46
|
if (arg === 'compile') continue;
|
|
37
47
|
if (arg === '-o' || arg === '--out') {
|
|
38
48
|
outputFile = args[++i];
|
|
49
|
+
} else if (arg === '--target') {
|
|
50
|
+
target = args[++i];
|
|
51
|
+
} else if (arg === '--export-name') {
|
|
52
|
+
exportName = args[++i];
|
|
39
53
|
} else if (!inputFile && !arg.startsWith('-')) {
|
|
40
54
|
inputFile = arg;
|
|
41
55
|
}
|
|
@@ -46,6 +60,11 @@ if (!inputFile) {
|
|
|
46
60
|
process.exit(1);
|
|
47
61
|
}
|
|
48
62
|
|
|
63
|
+
if (target !== 'esm' && target !== 'iife') {
|
|
64
|
+
console.error(`Error: --target must be "esm" or "iife", got "${target}"`);
|
|
65
|
+
process.exit(1);
|
|
66
|
+
}
|
|
67
|
+
|
|
49
68
|
const inputPath = path.resolve(process.cwd(), inputFile);
|
|
50
69
|
if (!fs.existsSync(inputPath)) {
|
|
51
70
|
console.error(`Error: File not found: ${inputFile}`);
|
|
@@ -54,14 +73,14 @@ if (!fs.existsSync(inputPath)) {
|
|
|
54
73
|
|
|
55
74
|
try {
|
|
56
75
|
const source = fs.readFileSync(inputPath, 'utf8');
|
|
57
|
-
const result = compile(source, inputFile);
|
|
76
|
+
const result = compile(source, { filename: inputFile, target, exportName });
|
|
58
77
|
const code = result.code || result;
|
|
59
78
|
|
|
60
79
|
if (outputFile) {
|
|
61
80
|
const outPath = path.resolve(process.cwd(), outputFile);
|
|
62
81
|
fs.mkdirSync(path.dirname(outPath), { recursive: true });
|
|
63
82
|
fs.writeFileSync(outPath, code, 'utf8');
|
|
64
|
-
console.log(`✓ Compiled ${inputFile} -> ${outputFile} (${code.length} bytes)`);
|
|
83
|
+
console.log(`✓ Compiled ${inputFile} -> ${outputFile} [${target}] (${code.length} bytes)`);
|
|
65
84
|
} else {
|
|
66
85
|
console.log(code);
|
|
67
86
|
}
|
package/src/index.js
CHANGED
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
import * as acorn from 'acorn';
|
|
2
2
|
import * as htmlparser2 from 'htmlparser2';
|
|
3
|
+
import { readFileSync } from 'node:fs';
|
|
4
|
+
|
|
5
|
+
const { version: COMPILER_VERSION } = JSON.parse(
|
|
6
|
+
readFileSync(new URL('../package.json', import.meta.url), 'utf8')
|
|
7
|
+
);
|
|
3
8
|
|
|
4
9
|
const VOID_ELEMENTS = new Set([
|
|
5
10
|
'area', 'base', 'br', 'col', 'embed', 'hr', 'img', 'input',
|
|
@@ -42,9 +47,11 @@ function extractScriptAndStyle(source) {
|
|
|
42
47
|
let style = '';
|
|
43
48
|
let template = source;
|
|
44
49
|
|
|
45
|
-
|
|
50
|
+
let isTypeScript = false;
|
|
51
|
+
const scriptMatch = template.match(/<script(?:\s+lang=["'](ts|js)["'])?>([\s\S]*?)<\/script>/);
|
|
46
52
|
if (scriptMatch) {
|
|
47
|
-
|
|
53
|
+
isTypeScript = scriptMatch[1] === 'ts';
|
|
54
|
+
script = scriptMatch[2];
|
|
48
55
|
template = template.replace(scriptMatch[0], '');
|
|
49
56
|
}
|
|
50
57
|
|
|
@@ -54,7 +61,7 @@ function extractScriptAndStyle(source) {
|
|
|
54
61
|
template = template.replace(styleMatch[0], '');
|
|
55
62
|
}
|
|
56
63
|
|
|
57
|
-
return { script, style, template };
|
|
64
|
+
return { script, style, template, isTypeScript };
|
|
58
65
|
}
|
|
59
66
|
|
|
60
67
|
// Returns the index of the closing } that matches the { at openPos,
|
|
@@ -104,7 +111,7 @@ function preprocessTemplate(template, dynAttrs) {
|
|
|
104
111
|
if (closeIdx !== -1) {
|
|
105
112
|
const expr = template.slice(i + 1, closeIdx).trim();
|
|
106
113
|
if (!expr.startsWith('#') && !expr.startsWith('/') && !expr.startsWith(':')) {
|
|
107
|
-
if (/^on
|
|
114
|
+
if (/^on:?[\w]+$/.test(attrName) || /^bind:[\w]+$/.test(attrName)) {
|
|
108
115
|
result += `${attrName}="${expr}"`;
|
|
109
116
|
} else {
|
|
110
117
|
dynAttrs.push(expr);
|
|
@@ -193,7 +200,7 @@ function stripTypeScript(code) {
|
|
|
193
200
|
|
|
194
201
|
export function compile(source, options = {}) {
|
|
195
202
|
const target = options.target || 'esm'; // 'esm' | 'iife'
|
|
196
|
-
const { script: scriptContent, style: styleContent, template: rawTemplateSource } = extractScriptAndStyle(source);
|
|
203
|
+
const { script: scriptContent, style: styleContent, template: rawTemplateSource, isTypeScript } = extractScriptAndStyle(source);
|
|
197
204
|
const scopeHash = hashString(styleContent || rawTemplateSource);
|
|
198
205
|
const hasStyles = styleContent.trim().length > 0;
|
|
199
206
|
const scopedCss = hasStyles ? scopeStyles(styleContent, scopeHash) : '';
|
|
@@ -238,14 +245,18 @@ export function compile(source, options = {}) {
|
|
|
238
245
|
templateNodes.pop();
|
|
239
246
|
}
|
|
240
247
|
}
|
|
241
|
-
}, { recognizeSelfClosing: true });
|
|
248
|
+
}, { recognizeSelfClosing: true, lowerCaseTags: false });
|
|
242
249
|
|
|
243
250
|
parser.write(rawTemplate.trim() || '<div></div>');
|
|
244
251
|
parser.end();
|
|
245
252
|
|
|
246
253
|
// Parse JS AST with Acorn
|
|
247
|
-
// Strip TS first — AST positions and all slice() calls operate on the stripped source
|
|
248
|
-
|
|
254
|
+
// Strip TS first — AST positions and all slice() calls operate on the stripped source.
|
|
255
|
+
// Only run on <script lang="ts"> — the strip regex can't reliably distinguish a type
|
|
256
|
+
// annotation's `:` from a ternary's `: falsyBranch` or an object literal's `key: value`,
|
|
257
|
+
// so applying it to plain JS risks corrupting valid code. Scripts that don't opt into TS
|
|
258
|
+
// are left untouched.
|
|
259
|
+
const strippedScript = scriptContent.trim() && isTypeScript ? stripTypeScript(scriptContent) : scriptContent;
|
|
249
260
|
let jsOutput = strippedScript;
|
|
250
261
|
const stateVars = new Set();
|
|
251
262
|
const exportedProps = [];
|
|
@@ -439,11 +450,34 @@ export function compile(source, options = {}) {
|
|
|
439
450
|
domCode += ` const ${cid}_target = document.createElement('div');\n`;
|
|
440
451
|
domCode += ` ${cid}_target.className = 'sola-component-root';\n`;
|
|
441
452
|
domCode += ` ${parentVar}.appendChild(${cid}_target);\n`;
|
|
442
|
-
|
|
453
|
+
|
|
454
|
+
// Dynamically-bound props (rows={items}) arrive here as __soladyn_N__ placeholders —
|
|
455
|
+
// resolve them back to the real expression from dynAttrs and pass it as a raw
|
|
456
|
+
// (unquoted) property so the child receives live data, not the placeholder text.
|
|
457
|
+
const propEntries = [];
|
|
443
458
|
for (const [key, val] of Object.entries(node.attribs || {})) {
|
|
444
|
-
|
|
459
|
+
const dynMatch = /^__soladyn_(\d+)__$/.exec(val);
|
|
460
|
+
const propValue = dynMatch ? `(${dynAttrs[parseInt(dynMatch[1], 10)]})` : JSON.stringify(val);
|
|
461
|
+
propEntries.push(`${JSON.stringify(key)}: ${propValue}`);
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
// Slot content: compile the tag's children into a function the child component can
|
|
465
|
+
// call to project them wherever it renders a <slot> in its own template.
|
|
466
|
+
if (node.children && node.children.length > 0) {
|
|
467
|
+
const slotVar = `${cid}_slot`;
|
|
468
|
+
domCode += ` const ${slotVar} = (__slot_target) => {\n`;
|
|
469
|
+
node.children.forEach(child => emitNode(child, '__slot_target'));
|
|
470
|
+
domCode += ` };\n`;
|
|
471
|
+
propEntries.push(`children: ${slotVar}`);
|
|
445
472
|
}
|
|
446
|
-
|
|
473
|
+
|
|
474
|
+
domCode += ` ${node.name}(${cid}_target, { ${propEntries.join(', ')} });\n`;
|
|
475
|
+
return;
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
// Slot placeholder: projects the children passed by a parent component, if any.
|
|
479
|
+
if (node.name === 'slot') {
|
|
480
|
+
domCode += ` if (typeof props.children === 'function') { props.children(${parentVar}); }\n`;
|
|
447
481
|
return;
|
|
448
482
|
}
|
|
449
483
|
|
|
@@ -588,7 +622,7 @@ export function compile(source, options = {}) {
|
|
|
588
622
|
// Assemble final output module
|
|
589
623
|
const CORE_EXPORTS = 'createSignal, createDerived, createEffect, createIntent, createData, onMount, onDestroy, pushContext, popContext, __flush_mounts, __flush_destroys';
|
|
590
624
|
let output = '';
|
|
591
|
-
output += `// Compiled by @sola-air-ui/compiler
|
|
625
|
+
output += `// Compiled by @sola-air-ui/compiler v${COMPILER_VERSION}\n`;
|
|
592
626
|
|
|
593
627
|
if (target === 'iife') {
|
|
594
628
|
// ServiceNow / no-bundler mode: pull from window.SolaCore global
|