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