@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.
Files changed (3) hide show
  1. package/package.json +3 -3
  2. package/src/cli.js +26 -7
  3. 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",
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": "./src/cli.js"
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> Write compiled output to a file (default: stdout)
16
- -v, --version Print compiler version
17
- -h, --help Show this help message
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 Component.sola
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('Sola Compiler v1.0.0');
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
- const scriptMatch = template.match(/<script(?:\s+lang=["'](?:ts|js)["'])?>([\s\S]*?)<\/script>/);
50
+ let isTypeScript = false;
51
+ const scriptMatch = template.match(/<script(?:\s+lang=["'](ts|js)["'])?>([\s\S]*?)<\/script>/);
46
52
  if (scriptMatch) {
47
- script = scriptMatch[1];
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:[\w]+$/.test(attrName) || /^bind:[\w]+$/.test(attrName)) {
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
- const strippedScript = scriptContent.trim() ? stripTypeScript(scriptContent) : scriptContent;
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
- const propsObj = {};
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
- propsObj[key] = val;
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
- domCode += ` ${node.name}(${cid}_target, ${JSON.stringify(propsObj)});\n`;
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 v1.0.3\n`;
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