@sola-air-ui/compiler 1.0.1 → 1.0.3

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 (2) hide show
  1. package/package.json +38 -31
  2. package/src/index.js +219 -74
package/package.json CHANGED
@@ -1,31 +1,38 @@
1
- {
2
- "name": "@sola-air-ui/compiler",
3
- "version": "1.0.1",
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.3",
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/index.js CHANGED
@@ -57,17 +57,142 @@ function extractScriptAndStyle(source) {
57
57
  return { script, style, template };
58
58
  }
59
59
 
60
+ // Returns the index of the closing } that matches the { at openPos,
61
+ // respecting string literals and nested braces. Returns -1 if unbalanced.
62
+ function matchBrace(text, openPos) {
63
+ let depth = 0;
64
+ let inStr = null;
65
+ for (let i = openPos; i < text.length; i++) {
66
+ const ch = text[i];
67
+ if (inStr) {
68
+ if (ch === '\\') { i++; continue; }
69
+ if (ch === inStr) inStr = null;
70
+ continue;
71
+ }
72
+ if (ch === '"' || ch === "'" || ch === '`') { inStr = ch; continue; }
73
+ if (ch === '{') depth++;
74
+ else if (ch === '}') { if (--depth === 0) return i; }
75
+ }
76
+ return -1;
77
+ }
78
+
79
+ // Walk the template and resolve {expr} in attribute values using balanced-brace
80
+ // matching, storing dynamic expressions in dynAttrs. Event/bind handlers are
81
+ // stored as plain strings; other expressions become __soladyn_N__ markers.
82
+ function preprocessTemplate(template, dynAttrs) {
83
+ let result = '';
84
+ let i = 0;
85
+ while (i < template.length) {
86
+ // Opening tag (not closing, not comment)
87
+ if (template[i] === '<' && template[i + 1] !== '/' && template[i + 1] !== '!') {
88
+ result += '<';
89
+ i++;
90
+ // Tag name
91
+ while (i < template.length && /[^\s>\/]/.test(template[i])) result += template[i++];
92
+ // Attributes
93
+ while (i < template.length && template[i] !== '>' && !(template[i] === '/' && template[i + 1] === '>')) {
94
+ if (/\s/.test(template[i])) { result += template[i++]; continue; }
95
+ // Attribute name
96
+ let attrName = '';
97
+ while (i < template.length && /[^\s=\/>]/.test(template[i])) attrName += template[i++];
98
+ if (!attrName) { result += template[i++]; continue; }
99
+ if (template[i] !== '=') { result += attrName; continue; }
100
+ i++; // skip =
101
+ if (template[i] === '{') {
102
+ // Unquoted brace expression: attr={expr}
103
+ const closeIdx = matchBrace(template, i);
104
+ if (closeIdx !== -1) {
105
+ const expr = template.slice(i + 1, closeIdx).trim();
106
+ if (!expr.startsWith('#') && !expr.startsWith('/') && !expr.startsWith(':')) {
107
+ if (/^on:[\w]+$/.test(attrName) || /^bind:[\w]+$/.test(attrName)) {
108
+ result += `${attrName}="${expr}"`;
109
+ } else {
110
+ dynAttrs.push(expr);
111
+ result += `${attrName}="__soladyn_${dynAttrs.length - 1}__"`;
112
+ }
113
+ i = closeIdx + 1;
114
+ continue;
115
+ }
116
+ }
117
+ result += `${attrName}={`; i++;
118
+ } else if (template[i] === '"' || template[i] === "'") {
119
+ // Quoted attribute value — scan for {expr} interpolations
120
+ const quote = template[i++];
121
+ let inner = ''; let hasDyn = false;
122
+ while (i < template.length && template[i] !== quote) {
123
+ if (template[i] === '{') {
124
+ const closeIdx = matchBrace(template, i);
125
+ if (closeIdx !== -1) {
126
+ const expr = template.slice(i + 1, closeIdx).trim();
127
+ if (!expr.startsWith('#') && !expr.startsWith('/') && !expr.startsWith(':')) {
128
+ hasDyn = true;
129
+ inner += `\${${expr}}`; i = closeIdx + 1; continue;
130
+ }
131
+ }
132
+ }
133
+ inner += template[i++];
134
+ }
135
+ if (i < template.length) i++; // skip closing quote
136
+ if (hasDyn) {
137
+ dynAttrs.push('`' + inner + '`');
138
+ result += `${attrName}="__soladyn_${dynAttrs.length - 1}__"`;
139
+ } else {
140
+ result += `${attrName}="${inner}"`;
141
+ }
142
+ } else {
143
+ result += `${attrName}=`;
144
+ }
145
+ continue;
146
+ }
147
+ if (i < template.length && template[i] === '/') { result += '/'; i++; }
148
+ if (i < template.length && template[i] === '>') { result += '>'; i++; }
149
+ continue;
150
+ }
151
+ result += template[i++];
152
+ }
153
+ return result;
154
+ }
155
+
156
+ // Replace {expr} in text content with <sola-expr> tags, using balanced-brace
157
+ // matching so expressions like {fn({key: val})} work correctly.
60
158
  function extractExpressions(text) {
61
- return text.replace(/{([^{}]+)}/g, (match, expr) => {
62
- const trimmed = expr.trim();
63
- if (trimmed.startsWith('#') || trimmed.startsWith('/') || trimmed.startsWith(':')) {
64
- return match;
159
+ let result = '';
160
+ let i = 0;
161
+ while (i < text.length) {
162
+ if (text[i] === '{') {
163
+ const closeIdx = matchBrace(text, i);
164
+ if (closeIdx === -1) { result += text[i++]; continue; }
165
+ const inner = text.slice(i + 1, closeIdx).trim();
166
+ if (inner.startsWith('#') || inner.startsWith('/') || inner.startsWith(':')) {
167
+ result += text.slice(i, closeIdx + 1); i = closeIdx + 1;
168
+ } else {
169
+ result += `<sola-expr expr="${inner.replace(/"/g, '&quot;')}"></sola-expr>`;
170
+ i = closeIdx + 1;
171
+ }
172
+ } else {
173
+ result += text[i++];
65
174
  }
66
- return `<sola-expr expr="${trimmed.replace(/"/g, '&quot;')}"></sola-expr>`;
67
- });
175
+ }
176
+ return result;
177
+ }
178
+
179
+ // Strip TypeScript type annotations so Acorn (plain JS parser) can handle
180
+ // script blocks written in TS. Covers the common patterns: type annotations,
181
+ // interface/type declarations, angle-bracket generics, and `as` casts.
182
+ function stripTypeScript(code) {
183
+ // Remove interface and type alias declarations
184
+ code = code.replace(/^\s*(export\s+)?(interface|type)\s+\w[\s\S]*?(?=\n(?:export|const|let|var|function|class|\/\/|$))/gm, '');
185
+ // Remove inline type annotations: `: Type` before = , ) , ; , , or end of line
186
+ code = code.replace(/:\s*[\w<>\[\]|&{},\s.?]+(?=[=,);{}\n])/g, '');
187
+ // Remove `as Type` casts
188
+ code = code.replace(/\s+as\s+[\w<>\[\]|&{}.?]+/g, '');
189
+ // Remove generic type parameters from function/class declarations
190
+ code = code.replace(/<[^>()=]+>(?=\s*\()/g, '');
191
+ return code;
68
192
  }
69
193
 
70
194
  export function compile(source, options = {}) {
195
+ const target = options.target || 'esm'; // 'esm' | 'iife'
71
196
  const { script: scriptContent, style: styleContent, template: rawTemplateSource } = extractScriptAndStyle(source);
72
197
  const scopeHash = hashString(styleContent || rawTemplateSource);
73
198
  const hasStyles = styleContent.trim().length > 0;
@@ -75,34 +200,10 @@ export function compile(source, options = {}) {
75
200
 
76
201
  let rawTemplate = rawTemplateSource;
77
202
 
78
- // Pre-process attribute-bound expressions BEFORE extractExpressions.
79
- // extractExpressions replaces {expr} with <sola-expr> HTML tags, which breaks
80
- // htmlparser2 when those tags land inside attribute values.
81
- const dynAttrs = []; // expressions for __soladyn_N__ markers
82
-
83
- // Event handlers: on:event={expr} → on:event="expr"
84
- rawTemplate = rawTemplate.replace(/\bon:([\w]+)=\{([^}]+)\}/g, 'on:$1="$2"');
85
- // Bind: bind:prop={expr} → bind:prop="expr"
86
- rawTemplate = rawTemplate.replace(/\bbind:([\w]+)=\{([^}]+)\}/g, 'bind:$1="$2"');
87
- // Quoted string interpolation: attr="text {expr} text" → attr="__soladyn_N__"
88
- // Run iteratively to handle multiple {expr} per value.
89
- let _prev;
90
- do {
91
- _prev = rawTemplate;
92
- rawTemplate = rawTemplate.replace(/([\w:-]+)="([^"]*\{[^{}]+\}[^"]*)"/g, (_, attr, val) => {
93
- const jsExpr = '`' + val.replace(/\{([^{}]+)\}/g, (__, e) => `\${${e.trim()}}`) + '`';
94
- const i = dynAttrs.length;
95
- dynAttrs.push(jsExpr);
96
- return `${attr}="__soladyn_${i}__"`;
97
- });
98
- } while (rawTemplate !== _prev);
99
- // Unquoted attr expressions: attr={expr} → attr="__soladyn_N__"
100
- rawTemplate = rawTemplate.replace(/([\w:-]+)=\{([^{}]+)\}/g, (_, attr, expr) => {
101
- const i = dynAttrs.length;
102
- dynAttrs.push(expr.trim());
103
- return `${attr}="__soladyn_${i}__"`;
104
- });
105
-
203
+ // Pre-process attribute-bound expressions BEFORE extractExpressions using
204
+ // balanced-brace matching (handles nested object literals, ternaries, etc.)
205
+ const dynAttrs = [];
206
+ rawTemplate = preprocessTemplate(rawTemplate, dynAttrs);
106
207
  rawTemplate = extractExpressions(rawTemplate);
107
208
 
108
209
  // Pre-process logic blocks: structured if-then-else
@@ -110,8 +211,8 @@ export function compile(source, options = {}) {
110
211
  rawTemplate = rawTemplate
111
212
  .replace(/{#if\s+([\s\S]*?)}([\s\S]*?){:else}([\s\S]*?){\/if}/g, '<sola-if condition="$1"><sola-then>$2</sola-then><sola-else>$3</sola-else></sola-if>')
112
213
  .replace(/{#if\s+([\s\S]*?)}([\s\S]*?){\/if}/g, '<sola-if condition="$1"><sola-then>$2</sola-then></sola-if>')
113
- .replace(/{#each\s+([\s\S]*?)\s+as\s+([\w]+)(?:\s*,\s*([\w]+))?}/g,
114
- (_, arr, item, index) => `<sola-each array="${arr}" item="${item}"${index ? ` index="${index}"` : ''}>`)
214
+ .replace(/{#each\s+([\s\S]*?)\s+as\s+([\w]+)(?:\s*,\s*([\w]+))?(?:\s*\(([^)]+)\))?}/g,
215
+ (_, arr, item, index, key) => `<sola-each array="${arr}" item="${item}"${index ? ` index="${index}"` : ''}${key ? ` key="${key.trim()}"` : ''}>`)
115
216
  .replace(/{\/each}/g, '</sola-each>');
116
217
 
117
218
  // Parse HTML AST
@@ -143,14 +244,16 @@ export function compile(source, options = {}) {
143
244
  parser.end();
144
245
 
145
246
  // Parse JS AST with Acorn
146
- let jsOutput = scriptContent;
247
+ // Strip TS first — AST positions and all slice() calls operate on the stripped source
248
+ const strippedScript = scriptContent.trim() ? stripTypeScript(scriptContent) : scriptContent;
249
+ let jsOutput = strippedScript;
147
250
  const stateVars = new Set();
148
251
  const exportedProps = [];
149
252
  const componentImports = [];
150
253
 
151
- if (scriptContent.trim()) {
254
+ if (strippedScript.trim()) {
152
255
  try {
153
- const ast = acorn.parse(scriptContent, { ecmaVersion: 'latest', sourceType: 'module' });
256
+ const ast = acorn.parse(strippedScript, { ecmaVersion: 'latest', sourceType: 'module' });
154
257
  const edits = [];
155
258
 
156
259
  function walkAST(node, parent) {
@@ -177,7 +280,7 @@ export function compile(source, options = {}) {
177
280
  node.declaration.declarations.forEach(decl => {
178
281
  const name = decl.id.name;
179
282
  const defaultVal = decl.init
180
- ? scriptContent.slice(decl.init.start, decl.init.end)
283
+ ? strippedScript.slice(decl.init.start, decl.init.end)
181
284
  : 'undefined';
182
285
  exportedProps.push({ name, default: defaultVal });
183
286
  edits.push({
@@ -197,7 +300,7 @@ export function compile(source, options = {}) {
197
300
  const varName = decl.id.name;
198
301
  stateVars.add(varName);
199
302
  const initArg = decl.init.arguments[0];
200
- const initVal = initArg ? scriptContent.slice(initArg.start, initArg.end) : 'undefined';
303
+ const initVal = initArg ? strippedScript.slice(initArg.start, initArg.end) : 'undefined';
201
304
  edits.push({
202
305
  start: node.start,
203
306
  end: node.end,
@@ -206,7 +309,7 @@ export function compile(source, options = {}) {
206
309
  } else if (callee.type === 'Identifier' && callee.name === '$derived') {
207
310
  const varName = decl.id.name;
208
311
  const expr = decl.init.arguments[0];
209
- const exprSrc = expr ? scriptContent.slice(expr.start, expr.end) : 'undefined';
312
+ const exprSrc = expr ? strippedScript.slice(expr.start, expr.end) : 'undefined';
210
313
  const isFn = expr && (expr.type === 'ArrowFunctionExpression' || expr.type === 'FunctionExpression');
211
314
  edits.push({
212
315
  start: node.start,
@@ -217,7 +320,7 @@ export function compile(source, options = {}) {
217
320
  });
218
321
  } else if (callee.type === 'Identifier' && callee.name === '$effect') {
219
322
  const body = decl.init.arguments[0];
220
- const bodySrc = body ? scriptContent.slice(body.start, body.end) : '() => {}';
323
+ const bodySrc = body ? strippedScript.slice(body.start, body.end) : '() => {}';
221
324
  edits.push({
222
325
  start: node.start,
223
326
  end: node.end,
@@ -225,10 +328,10 @@ export function compile(source, options = {}) {
225
328
  });
226
329
  } else if (callee.type === 'Identifier' && callee.name === '$intent') {
227
330
  const promptArg = decl.init.arguments[0]
228
- ? scriptContent.slice(decl.init.arguments[0].start, decl.init.arguments[0].end)
331
+ ? strippedScript.slice(decl.init.arguments[0].start, decl.init.arguments[0].end)
229
332
  : "''";
230
333
  const fallbackArg = decl.init.arguments[1]
231
- ? scriptContent.slice(decl.init.arguments[1].start, decl.init.arguments[1].end)
334
+ ? strippedScript.slice(decl.init.arguments[1].start, decl.init.arguments[1].end)
232
335
  : 'null';
233
336
  edits.push({
234
337
  start: node.start,
@@ -237,10 +340,10 @@ export function compile(source, options = {}) {
237
340
  });
238
341
  } else if (callee.type === 'Identifier' && callee.name === '$data') {
239
342
  const sourceArg = decl.init.arguments[0]
240
- ? scriptContent.slice(decl.init.arguments[0].start, decl.init.arguments[0].end)
343
+ ? strippedScript.slice(decl.init.arguments[0].start, decl.init.arguments[0].end)
241
344
  : "''";
242
345
  const optsArg = decl.init.arguments[1]
243
- ? ', ' + scriptContent.slice(decl.init.arguments[1].start, decl.init.arguments[1].end)
346
+ ? ', ' + strippedScript.slice(decl.init.arguments[1].start, decl.init.arguments[1].end)
244
347
  : '';
245
348
  edits.push({
246
349
  start: node.start,
@@ -257,7 +360,7 @@ export function compile(source, options = {}) {
257
360
  node.left.type === 'Identifier' &&
258
361
  stateVars.has(node.left.name)) {
259
362
  const name = node.left.name;
260
- const rightSource = scriptContent.slice(node.right.start, node.right.end);
363
+ const rightSource = strippedScript.slice(node.right.start, node.right.end);
261
364
  if (node.operator === '=') {
262
365
  edits.push({ start: node.start, end: node.end, replacement: `set_${name}(${rightSource})` });
263
366
  } else {
@@ -297,7 +400,9 @@ export function compile(source, options = {}) {
297
400
  jsOutput = jsOutput.slice(0, edit.start) + edit.replacement + jsOutput.slice(edit.end);
298
401
  }
299
402
  } catch (err) {
300
- console.warn('[sola compiler] Acorn parse warning:', err.message);
403
+ const loc = err.loc ? ` (line ${err.loc.line}, col ${err.loc.column})` : '';
404
+ const filePath = typeof options === 'string' ? options : (options.filename || '<script>');
405
+ throw new Error(`[sola compiler] Parse error in ${filePath}${loc}:\n ${err.message}`);
301
406
  }
302
407
  }
303
408
 
@@ -373,26 +478,53 @@ export function compile(source, options = {}) {
373
478
  return;
374
479
  }
375
480
 
376
- // {#each} (Rendering in correct ascending order)
481
+ // {#each} with optional keyed diffing: {#each items as item (item.id)}
377
482
  if (node.name === 'sola-each') {
378
483
  const eid = `e${uid++}`;
484
+ const keyExpr = node.attribs.key || null;
379
485
  domCode += ` const ${eid}_a = document.createComment('each');\n`;
380
486
  domCode += ` ${parentVar}.appendChild(${eid}_a);\n`;
381
- domCode += ` let ${eid}_els = [];\n`;
382
- domCode += ` createEffect(() => {\n`;
383
- domCode += ` ${eid}_els.forEach(e => e.remove()); ${eid}_els = [];\n`;
384
- domCode += ` const _items = ${node.attribs.array} || [];\n`;
385
- domCode += ` const f = document.createDocumentFragment();\n`;
386
- domCode += ` for (let _i = 0; _i < _items.length; _i++) {\n`;
387
- domCode += ` const ${node.attribs.item} = _items[_i];\n`;
388
- if (node.attribs.index) {
389
- domCode += ` const ${node.attribs.index} = _i;\n`;
487
+ if (keyExpr) {
488
+ // Keyed: maintain a Map<key, Node[]> and reconcile on each run
489
+ domCode += ` let ${eid}_keyMap = new Map();\n`;
490
+ domCode += ` createEffect(() => {\n`;
491
+ domCode += ` const _items = ${node.attribs.array} || [];\n`;
492
+ domCode += ` const _nextKeys = _items.map((${node.attribs.item}, _i) => String(${keyExpr}));\n`;
493
+ domCode += ` // Remove stale keys\n`;
494
+ domCode += ` for (const [_k, _nodes] of ${eid}_keyMap) {\n`;
495
+ domCode += ` if (!_nextKeys.includes(_k)) { _nodes.forEach(n => n.remove()); ${eid}_keyMap.delete(_k); }\n`;
496
+ domCode += ` }\n`;
497
+ domCode += ` // Insert/reorder items\n`;
498
+ domCode += ` let _anchor = ${eid}_a.nextSibling;\n`;
499
+ domCode += ` for (let _i = 0; _i < _items.length; _i++) {\n`;
500
+ domCode += ` const ${node.attribs.item} = _items[_i];\n`;
501
+ if (node.attribs.index) domCode += ` const ${node.attribs.index} = _i;\n`;
502
+ domCode += ` const _key = String(${keyExpr});\n`;
503
+ domCode += ` if (!${eid}_keyMap.has(_key)) {\n`;
504
+ domCode += ` const f = document.createDocumentFragment();\n`;
505
+ node.children.forEach(child => emitNode(child, 'f'));
506
+ domCode += ` const _nodes = Array.from(f.childNodes);\n`;
507
+ domCode += ` ${eid}_keyMap.set(_key, _nodes);\n`;
508
+ domCode += ` ${eid}_a.parentNode.insertBefore(f, _anchor);\n`;
509
+ domCode += ` }\n`;
510
+ domCode += ` }\n`;
511
+ domCode += ` });\n`;
512
+ } else {
513
+ // Unkeyed: full teardown + rebuild
514
+ domCode += ` let ${eid}_els = [];\n`;
515
+ domCode += ` createEffect(() => {\n`;
516
+ domCode += ` ${eid}_els.forEach(e => e.remove()); ${eid}_els = [];\n`;
517
+ domCode += ` const _items = ${node.attribs.array} || [];\n`;
518
+ domCode += ` const f = document.createDocumentFragment();\n`;
519
+ domCode += ` for (let _i = 0; _i < _items.length; _i++) {\n`;
520
+ domCode += ` const ${node.attribs.item} = _items[_i];\n`;
521
+ if (node.attribs.index) domCode += ` const ${node.attribs.index} = _i;\n`;
522
+ node.children.forEach(child => emitNode(child, 'f'));
523
+ domCode += ` }\n`;
524
+ domCode += ` Array.from(f.childNodes).forEach(n => ${eid}_els.push(n));\n`;
525
+ domCode += ` ${eid}_a.parentNode.insertBefore(f, ${eid}_a.nextSibling);\n`;
526
+ domCode += ` });\n`;
390
527
  }
391
- node.children.forEach(child => emitNode(child, 'f'));
392
- domCode += ` }\n`;
393
- domCode += ` Array.from(f.childNodes).forEach(n => ${eid}_els.push(n));\n`;
394
- domCode += ` ${eid}_a.parentNode.insertBefore(f, ${eid}_a.nextSibling);\n`;
395
- domCode += ` });\n`;
396
528
  return;
397
529
  }
398
530
 
@@ -454,16 +586,27 @@ export function compile(source, options = {}) {
454
586
  root.children.forEach(child => emitNode(child, '__target'));
455
587
 
456
588
  // Assemble final output module
589
+ const CORE_EXPORTS = 'createSignal, createDerived, createEffect, createIntent, createData, onMount, onDestroy, pushContext, popContext, __flush_mounts, __flush_destroys';
457
590
  let output = '';
458
- output += `// Compiled by @sola-air-ui/compiler v1.0.1\n`;
459
- output += `import { createSignal, createDerived, createEffect, createIntent, createData, onMount, onDestroy, pushContext, popContext, __flush_mounts, __flush_destroys } from '@sola-air-ui/core';\n`;
460
-
461
- for (const imp of componentImports) {
462
- output += `import ${imp.localName} from '${imp.path}';\n`;
591
+ output += `// Compiled by @sola-air-ui/compiler v1.0.3\n`;
592
+
593
+ if (target === 'iife') {
594
+ // ServiceNow / no-bundler mode: pull from window.SolaCore global
595
+ output += `(function() {\n`;
596
+ output += `const { ${CORE_EXPORTS} } = window.SolaCore;\n`;
597
+ for (const imp of componentImports) {
598
+ output += `// Note: sub-component ${imp.localName} must be pre-loaded before this script\n`;
599
+ }
600
+ output += `\n`;
601
+ output += `window['${options.exportName || 'SolaComponent'}'] = function mount(__target, props = {}) {\n`;
602
+ } else {
603
+ output += `import { ${CORE_EXPORTS} } from '@sola-air-ui/core';\n`;
604
+ for (const imp of componentImports) {
605
+ output += `import ${imp.localName} from '${imp.path}';\n`;
606
+ }
607
+ output += `\n`;
608
+ output += `export default function mount(__target, props = {}) {\n`;
463
609
  }
464
-
465
- output += `\n`;
466
- output += `export default function mount(__target, props = {}) {\n`;
467
610
  output += ` const __ctx = pushContext();\n\n`;
468
611
 
469
612
  if (hasStyles) {
@@ -487,12 +630,14 @@ export function compile(source, options = {}) {
487
630
 
488
631
  output += `\n __flush_mounts();\n`;
489
632
  output += `\n return () => {\n`;
490
- output += ` // Cleanup\n`;
491
633
  output += ` __flush_destroys();\n`;
492
634
  output += ` popContext(__ctx);\n`;
493
635
  output += ` __target.innerHTML = '';\n`;
494
636
  output += ` };\n`;
495
- output += `}\n`;
637
+ output += `};\n`;
638
+ if (target === 'iife') {
639
+ output += `})();\n`;
640
+ }
496
641
 
497
642
  return {
498
643
  code: output,