@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.
Files changed (3) hide show
  1. package/package.json +38 -31
  2. package/src/cli.js +24 -7
  3. 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.2",
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": ["sola", "compiler", "sfc", "single-file-component", "zero-vdom", "build-tool"],
17
- "license": "MIT",
18
- "homepage": "https://sola-air.dev",
19
- "repository": {
20
- "type": "git",
21
- "url": "https://github.com/rbm3267/sola"
22
- },
23
- "dependencies": {
24
- "acorn": "^8.10.0",
25
- "htmlparser2": "^9.0.0"
26
- },
27
- "publishConfig": {
28
- "access": "public",
29
- "registry": "https://registry.npmjs.org/"
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> 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);
@@ -192,7 +194,8 @@ function stripTypeScript(code) {
192
194
  }
193
195
 
194
196
  export function compile(source, options = {}) {
195
- const { script: scriptContent, style: styleContent, template: rawTemplateSource } = extractScriptAndStyle(source);
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
- 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;
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
- 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 = [];
442
453
  for (const [key, val] of Object.entries(node.attribs || {})) {
443
- 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}`);
444
467
  }
445
- 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`;
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.2\n`;
590
- output += `import { createSignal, createDerived, createEffect, createIntent, createData, onMount, onDestroy, pushContext, popContext, __flush_mounts, __flush_destroys } from '@sola-air-ui/core';\n`;
591
-
592
- for (const imp of componentImports) {
593
- output += `import ${imp.localName} from '${imp.path}';\n`;
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 += `}\n`;
666
+ output += `};\n`;
667
+ if (target === 'iife') {
668
+ output += `})();\n`;
669
+ }
627
670
 
628
671
  return {
629
672
  code: output,