@sola-air-ui/compiler 1.0.3 → 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.
Files changed (3) hide show
  1. package/package.json +1 -1
  2. package/src/cli.js +24 -7
  3. package/src/index.js +40 -11
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sola-air-ui/compiler",
3
- "version": "1.0.3",
3
+ "version": "1.0.4",
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",
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> Write compiled output to a file (default: stdout)
16
- -v, --version Print compiler version
17
- -h, --help Show this help message
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 Component.sola
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.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
- const scriptMatch = template.match(/<script(?:\s+lang=["'](?:ts|js)["'])?>([\s\S]*?)<\/script>/);
45
+ let isTypeScript = false;
46
+ const scriptMatch = template.match(/<script(?:\s+lang=["'](ts|js)["'])?>([\s\S]*?)<\/script>/);
46
47
  if (scriptMatch) {
47
- script = scriptMatch[1];
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:[\w]+$/.test(attrName) || /^bind:[\w]+$/.test(attrName)) {
109
+ if (/^on:?[\w]+$/.test(attrName) || /^bind:[\w]+$/.test(attrName)) {
108
110
  result += `${attrName}="${expr}"`;
109
111
  } else {
110
112
  dynAttrs.push(expr);
@@ -193,7 +195,7 @@ function stripTypeScript(code) {
193
195
 
194
196
  export function compile(source, options = {}) {
195
197
  const target = options.target || 'esm'; // 'esm' | 'iife'
196
- const { script: scriptContent, style: styleContent, template: rawTemplateSource } = extractScriptAndStyle(source);
198
+ const { script: scriptContent, style: styleContent, template: rawTemplateSource, isTypeScript } = extractScriptAndStyle(source);
197
199
  const scopeHash = hashString(styleContent || rawTemplateSource);
198
200
  const hasStyles = styleContent.trim().length > 0;
199
201
  const scopedCss = hasStyles ? scopeStyles(styleContent, scopeHash) : '';
@@ -238,14 +240,18 @@ export function compile(source, options = {}) {
238
240
  templateNodes.pop();
239
241
  }
240
242
  }
241
- }, { recognizeSelfClosing: true });
243
+ }, { recognizeSelfClosing: true, lowerCaseTags: false });
242
244
 
243
245
  parser.write(rawTemplate.trim() || '<div></div>');
244
246
  parser.end();
245
247
 
246
248
  // 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;
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;
249
255
  let jsOutput = strippedScript;
250
256
  const stateVars = new Set();
251
257
  const exportedProps = [];
@@ -439,11 +445,34 @@ export function compile(source, options = {}) {
439
445
  domCode += ` const ${cid}_target = document.createElement('div');\n`;
440
446
  domCode += ` ${cid}_target.className = 'sola-component-root';\n`;
441
447
  domCode += ` ${parentVar}.appendChild(${cid}_target);\n`;
442
- const propsObj = {};
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 = [];
443
453
  for (const [key, val] of Object.entries(node.attribs || {})) {
444
- propsObj[key] = val;
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}`);
445
467
  }
446
- domCode += ` ${node.name}(${cid}_target, ${JSON.stringify(propsObj)});\n`;
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`;
447
476
  return;
448
477
  }
449
478