@sola-air-ui/compiler 1.0.0

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 +31 -0
  2. package/src/cli.js +71 -0
  3. package/src/index.js +449 -0
package/package.json ADDED
@@ -0,0 +1,31 @@
1
+ {
2
+ "name": "@sola-air-ui/compiler",
3
+ "version": "1.0.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": ["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
+ }
package/src/cli.js ADDED
@@ -0,0 +1,71 @@
1
+ #!/usr/bin/env node
2
+
3
+ import fs from 'node:fs';
4
+ import path from 'node:path';
5
+ import { compile } from './index.js';
6
+
7
+ const args = process.argv.slice(2);
8
+
9
+ if (args.length === 0 || args.includes('-h') || args.includes('--help')) {
10
+ console.log(`
11
+ Usage:
12
+ sola compile <file.sola> [options]
13
+
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
18
+
19
+ Examples:
20
+ sola compile App.sola -o dist/App.js
21
+ sola compile Component.sola
22
+ `);
23
+ process.exit(0);
24
+ }
25
+
26
+ if (args.includes('-v') || args.includes('--version')) {
27
+ console.log('Sola Compiler v1.0.0');
28
+ process.exit(0);
29
+ }
30
+
31
+ let inputFile = null;
32
+ let outputFile = null;
33
+
34
+ for (let i = 0; i < args.length; i++) {
35
+ const arg = args[i];
36
+ if (arg === 'compile') continue;
37
+ if (arg === '-o' || arg === '--out') {
38
+ outputFile = args[++i];
39
+ } else if (!inputFile && !arg.startsWith('-')) {
40
+ inputFile = arg;
41
+ }
42
+ }
43
+
44
+ if (!inputFile) {
45
+ console.error('Error: No input .sola file specified.');
46
+ process.exit(1);
47
+ }
48
+
49
+ const inputPath = path.resolve(process.cwd(), inputFile);
50
+ if (!fs.existsSync(inputPath)) {
51
+ console.error(`Error: File not found: ${inputFile}`);
52
+ process.exit(1);
53
+ }
54
+
55
+ try {
56
+ const source = fs.readFileSync(inputPath, 'utf8');
57
+ const result = compile(source, inputFile);
58
+ const code = result.code || result;
59
+
60
+ if (outputFile) {
61
+ const outPath = path.resolve(process.cwd(), outputFile);
62
+ fs.mkdirSync(path.dirname(outPath), { recursive: true });
63
+ fs.writeFileSync(outPath, code, 'utf8');
64
+ console.log(`✓ Compiled ${inputFile} -> ${outputFile} (${code.length} bytes)`);
65
+ } else {
66
+ console.log(code);
67
+ }
68
+ } catch (err) {
69
+ console.error(`Compilation Error: ${err.message}`);
70
+ process.exit(1);
71
+ }
package/src/index.js ADDED
@@ -0,0 +1,449 @@
1
+ import * as acorn from 'acorn';
2
+ import * as htmlparser2 from 'htmlparser2';
3
+
4
+ const VOID_ELEMENTS = new Set([
5
+ 'area', 'base', 'br', 'col', 'embed', 'hr', 'img', 'input',
6
+ 'link', 'meta', 'param', 'source', 'track', 'wbr'
7
+ ]);
8
+
9
+ function hashString(str) {
10
+ let hash = 5381;
11
+ for (let i = 0; i < str.length; i++) {
12
+ hash = ((hash << 5) + hash) + str.charCodeAt(i);
13
+ hash |= 0;
14
+ }
15
+ return 'sola-' + Math.abs(hash).toString(36);
16
+ }
17
+
18
+ function scopeStyles(css, scopeHash) {
19
+ return css.replace(/([^\r\n,{}]+)(,(?=[^{}]*{)|\s*{)/g, (match, selector, trailing) => {
20
+ if (selector.trim().startsWith('@') || selector.trim().startsWith('from') || selector.trim().startsWith('to') || /^\d+%/.test(selector.trim())) {
21
+ return match;
22
+ }
23
+ const scopedSelector = selector
24
+ .split(',')
25
+ .map(part => {
26
+ const trimmed = part.trim();
27
+ if (!trimmed) return part;
28
+ return `${trimmed}.${scopeHash}`;
29
+ })
30
+ .join(', ');
31
+ return scopedSelector + trailing;
32
+ });
33
+ }
34
+
35
+ function extractScriptAndStyle(source) {
36
+ let script = '';
37
+ let style = '';
38
+ let template = source;
39
+
40
+ const scriptMatch = template.match(/<script(?:\s+lang=["'](?:ts|js)["'])?>([\s\S]*?)<\/script>/);
41
+ if (scriptMatch) {
42
+ script = scriptMatch[1];
43
+ template = template.replace(scriptMatch[0], '');
44
+ }
45
+
46
+ const styleMatch = template.match(/<style(?:\s+scoped)?>([\s\S]*?)<\/style>/);
47
+ if (styleMatch) {
48
+ style = styleMatch[1];
49
+ template = template.replace(styleMatch[0], '');
50
+ }
51
+
52
+ return { script, style, template };
53
+ }
54
+
55
+ function extractExpressions(text) {
56
+ return text.replace(/{([^{}]+)}/g, (match, expr) => {
57
+ const trimmed = expr.trim();
58
+ if (trimmed.startsWith('#') || trimmed.startsWith('/') || trimmed.startsWith(':')) {
59
+ return match;
60
+ }
61
+ return `<sola-expr expr="${trimmed.replace(/"/g, '&quot;')}"></sola-expr>`;
62
+ });
63
+ }
64
+
65
+ export function compile(source, options = {}) {
66
+ const { script: scriptContent, style: styleContent, template: rawTemplateSource } = extractScriptAndStyle(source);
67
+ const scopeHash = hashString(styleContent || rawTemplateSource);
68
+ const hasStyles = styleContent.trim().length > 0;
69
+ const scopedCss = hasStyles ? scopeStyles(styleContent, scopeHash) : '';
70
+
71
+ let rawTemplate = rawTemplateSource;
72
+ rawTemplate = extractExpressions(rawTemplate);
73
+
74
+ // Pre-process logic blocks: structured if-then-else
75
+ // Process nested or sequential if/else blocks cleanly
76
+ rawTemplate = rawTemplate
77
+ .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>')
78
+ .replace(/{#if\s+([\s\S]*?)}([\s\S]*?){\/if}/g, '<sola-if condition="$1"><sola-then>$2</sola-then></sola-if>')
79
+ .replace(/{#each\s+([\s\S]*?)\s+as\s+([\w]+)(?:\s*,\s*([\w]+))?}/g,
80
+ (_, arr, item, index) => `<sola-each array="${arr}" item="${item}"${index ? ` index="${index}"` : ''}>`)
81
+ .replace(/{\/each}/g, '</sola-each>');
82
+
83
+ // Parse HTML AST
84
+ const templateNodes = [];
85
+ let root = { type: 'element', name: '__root__', attribs: {}, children: [] };
86
+ templateNodes.push(root);
87
+
88
+ const parser = new htmlparser2.Parser({
89
+ onopentag(name, attribs) {
90
+ const node = { type: 'element', name, attribs, children: [] };
91
+ templateNodes[templateNodes.length - 1].children.push(node);
92
+ if (!VOID_ELEMENTS.has(name)) {
93
+ templateNodes.push(node);
94
+ }
95
+ },
96
+ ontext(text) {
97
+ if (text.trim().length > 0 && templateNodes.length > 0) {
98
+ templateNodes[templateNodes.length - 1].children.push({ type: 'text', data: text });
99
+ }
100
+ },
101
+ onclosetag(name) {
102
+ if (templateNodes.length > 1 && !VOID_ELEMENTS.has(name)) {
103
+ templateNodes.pop();
104
+ }
105
+ }
106
+ }, { recognizeSelfClosing: true });
107
+
108
+ parser.write(rawTemplate.trim() || '<div></div>');
109
+ parser.end();
110
+
111
+ // Parse JS AST with Acorn
112
+ let jsOutput = scriptContent;
113
+ const stateVars = new Set();
114
+ const exportedProps = [];
115
+ const componentImports = [];
116
+
117
+ if (scriptContent.trim()) {
118
+ try {
119
+ const ast = acorn.parse(scriptContent, { ecmaVersion: 'latest', sourceType: 'module' });
120
+ const edits = [];
121
+
122
+ function walkAST(node, parent) {
123
+ if (!node || typeof node !== 'object') return;
124
+
125
+ // Track component imports
126
+ if (node.type === 'ImportDeclaration') {
127
+ const src = node.source.value;
128
+ if (src.endsWith('.sola')) {
129
+ node.specifiers.forEach(spec => {
130
+ componentImports.push({ localName: spec.local.name, path: src });
131
+ });
132
+ }
133
+ edits.push({
134
+ start: node.start,
135
+ end: node.end,
136
+ replacement: `// import hoisted: ${node.source.value}`
137
+ });
138
+ }
139
+
140
+ // Exported props
141
+ if (node.type === 'ExportNamedDeclaration' && node.declaration &&
142
+ node.declaration.type === 'VariableDeclaration') {
143
+ node.declaration.declarations.forEach(decl => {
144
+ const name = decl.id.name;
145
+ const defaultVal = decl.init
146
+ ? scriptContent.slice(decl.init.start, decl.init.end)
147
+ : 'undefined';
148
+ exportedProps.push({ name, default: defaultVal });
149
+ edits.push({
150
+ start: node.start,
151
+ end: node.start + 7,
152
+ replacement: ''
153
+ });
154
+ });
155
+ }
156
+
157
+ // $state() -> createSignal()
158
+ if (node.type === 'VariableDeclaration') {
159
+ node.declarations.forEach(decl => {
160
+ if (decl.init && decl.init.type === 'CallExpression') {
161
+ const callee = decl.init.callee;
162
+ if (callee.type === 'Identifier' && callee.name === '$state') {
163
+ const varName = decl.id.name;
164
+ stateVars.add(varName);
165
+ const initArg = decl.init.arguments[0];
166
+ const initVal = initArg ? scriptContent.slice(initArg.start, initArg.end) : 'undefined';
167
+ edits.push({
168
+ start: node.start,
169
+ end: node.end,
170
+ replacement: `const [${varName}, set_${varName}] = createSignal(${initVal});`
171
+ });
172
+ } else if (callee.type === 'Identifier' && callee.name === '$derived') {
173
+ const varName = decl.id.name;
174
+ const expr = decl.init.arguments[0];
175
+ const exprSrc = expr ? scriptContent.slice(expr.start, expr.end) : 'undefined';
176
+ edits.push({
177
+ start: node.start,
178
+ end: node.end,
179
+ replacement: `const ${varName} = createDerived(() => ${exprSrc});`
180
+ });
181
+ } else if (callee.type === 'Identifier' && callee.name === '$effect') {
182
+ const body = decl.init.arguments[0];
183
+ const bodySrc = body ? scriptContent.slice(body.start, body.end) : '() => {}';
184
+ edits.push({
185
+ start: node.start,
186
+ end: node.end,
187
+ replacement: `createEffect(${bodySrc});`
188
+ });
189
+ } else if (callee.type === 'Identifier' && callee.name === '$intent') {
190
+ const promptArg = decl.init.arguments[0]
191
+ ? scriptContent.slice(decl.init.arguments[0].start, decl.init.arguments[0].end)
192
+ : "''";
193
+ const fallbackArg = decl.init.arguments[1]
194
+ ? scriptContent.slice(decl.init.arguments[1].start, decl.init.arguments[1].end)
195
+ : 'null';
196
+ edits.push({
197
+ start: node.start,
198
+ end: node.end,
199
+ replacement: `const ${decl.id.name} = createIntent(${promptArg}, ${fallbackArg});`
200
+ });
201
+ } else if (callee.type === 'Identifier' && callee.name === '$data') {
202
+ const sourceArg = decl.init.arguments[0]
203
+ ? scriptContent.slice(decl.init.arguments[0].start, decl.init.arguments[0].end)
204
+ : "''";
205
+ const optsArg = decl.init.arguments[1]
206
+ ? ', ' + scriptContent.slice(decl.init.arguments[1].start, decl.init.arguments[1].end)
207
+ : '';
208
+ edits.push({
209
+ start: node.start,
210
+ end: node.end,
211
+ replacement: `const ${decl.id.name} = createData(${sourceArg}${optsArg});`
212
+ });
213
+ }
214
+ }
215
+ });
216
+ }
217
+
218
+ // Assignment to state var: count = x -> set_count(x) (supporting all 10 assignment operators)
219
+ if (node.type === 'AssignmentExpression' &&
220
+ node.left.type === 'Identifier' &&
221
+ stateVars.has(node.left.name)) {
222
+ const name = node.left.name;
223
+ const rightSource = scriptContent.slice(node.right.start, node.right.end);
224
+ if (node.operator === '=') {
225
+ edits.push({ start: node.start, end: node.end, replacement: `set_${name}(${rightSource})` });
226
+ } else {
227
+ const rawOp = node.operator.slice(0, -1);
228
+ edits.push({ start: node.start, end: node.end, replacement: `set_${name}(${name}() ${rawOp} (${rightSource}))` });
229
+ }
230
+ }
231
+
232
+ // Update expressions: count++ / count--
233
+ if (node.type === 'UpdateExpression' &&
234
+ node.argument.type === 'Identifier' &&
235
+ stateVars.has(node.argument.name)) {
236
+ const name = node.argument.name;
237
+ const op = node.operator === '++' ? '+' : '-';
238
+ edits.push({
239
+ start: node.start,
240
+ end: node.end,
241
+ replacement: `set_${name}(${name}() ${op} 1)`
242
+ });
243
+ }
244
+
245
+ for (const key of Object.keys(node)) {
246
+ if (key === 'type') continue;
247
+ const child = node[key];
248
+ if (Array.isArray(child)) {
249
+ child.forEach(c => walkAST(c, node));
250
+ } else if (child && typeof child === 'object' && child.type) {
251
+ walkAST(child, node);
252
+ }
253
+ }
254
+ }
255
+
256
+ walkAST(ast, null);
257
+
258
+ edits.sort((a, b) => b.start - a.start);
259
+ for (const edit of edits) {
260
+ jsOutput = jsOutput.slice(0, edit.start) + edit.replacement + jsOutput.slice(edit.end);
261
+ }
262
+ } catch (err) {
263
+ console.warn('[sola compiler] Acorn parse warning:', err.message);
264
+ }
265
+ }
266
+
267
+ // Generate DOM creation instructions
268
+ let uid = 0;
269
+ let domCode = '';
270
+ const importedComponentSet = new Set(componentImports.map(i => i.localName));
271
+
272
+ function emitNode(node, parentVar) {
273
+ if (!node) return;
274
+
275
+ if (node.type === 'text') {
276
+ const text = node.data.replace(/\\/g, '\\\\').replace(/`/g, '\\`');
277
+ if (text.trim().length > 0) {
278
+ const id = `t${uid++}`;
279
+ domCode += ` const ${id} = document.createTextNode(\`${text}\`);\n`;
280
+ domCode += ` ${parentVar}.appendChild(${id});\n`;
281
+ }
282
+ return;
283
+ }
284
+
285
+ if (node.name === 'sola-expr') {
286
+ const expr = (node.attribs.expr || '').replace(/&quot;/g, '"');
287
+ const id = `e${uid++}`;
288
+ domCode += ` const ${id} = document.createTextNode('');\n`;
289
+ domCode += ` ${parentVar}.appendChild(${id});\n`;
290
+ domCode += ` createEffect(() => { ${id}.textContent = String(${expr} ?? ''); });\n`;
291
+ return;
292
+ }
293
+
294
+ // Nested custom component
295
+ if (importedComponentSet.has(node.name)) {
296
+ const cid = `comp${uid++}`;
297
+ domCode += ` const ${cid}_target = document.createElement('div');\n`;
298
+ domCode += ` ${cid}_target.className = 'sola-component-root';\n`;
299
+ domCode += ` ${parentVar}.appendChild(${cid}_target);\n`;
300
+ const propsObj = {};
301
+ for (const [key, val] of Object.entries(node.attribs || {})) {
302
+ propsObj[key] = val;
303
+ }
304
+ domCode += ` ${node.name}(${cid}_target, ${JSON.stringify(propsObj)});\n`;
305
+ return;
306
+ }
307
+
308
+ // {#if ...} {:else} ... {/if}
309
+ if (node.name === 'sola-if') {
310
+ const cid = `c${uid++}`;
311
+ domCode += ` const ${cid}_a = document.createComment('if');\n`;
312
+ domCode += ` ${parentVar}.appendChild(${cid}_a);\n`;
313
+ domCode += ` let ${cid}_els = [];\n`;
314
+
315
+ const thenChild = node.children.find(c => c.name === 'sola-then');
316
+ const elseChild = node.children.find(c => c.name === 'sola-else');
317
+
318
+ domCode += ` createEffect(() => {\n`;
319
+ domCode += ` ${cid}_els.forEach(e => e.remove()); ${cid}_els = [];\n`;
320
+ domCode += ` if (${node.attribs.condition}) {\n`;
321
+ if (thenChild) {
322
+ domCode += ` const f = document.createDocumentFragment();\n`;
323
+ thenChild.children.forEach(child => emitNode(child, 'f'));
324
+ domCode += ` Array.from(f.childNodes).forEach(n => ${cid}_els.push(n));\n`;
325
+ domCode += ` ${cid}_a.parentNode.insertBefore(f, ${cid}_a.nextSibling);\n`;
326
+ }
327
+ domCode += ` } else {\n`;
328
+ if (elseChild) {
329
+ domCode += ` const f = document.createDocumentFragment();\n`;
330
+ elseChild.children.forEach(child => emitNode(child, 'f'));
331
+ domCode += ` Array.from(f.childNodes).forEach(n => ${cid}_els.push(n));\n`;
332
+ domCode += ` ${cid}_a.parentNode.insertBefore(f, ${cid}_a.nextSibling);\n`;
333
+ }
334
+ domCode += ` }\n`;
335
+ domCode += ` });\n`;
336
+ return;
337
+ }
338
+
339
+ // {#each} (Rendering in correct ascending order)
340
+ if (node.name === 'sola-each') {
341
+ const eid = `e${uid++}`;
342
+ domCode += ` const ${eid}_a = document.createComment('each');\n`;
343
+ domCode += ` ${parentVar}.appendChild(${eid}_a);\n`;
344
+ domCode += ` let ${eid}_els = [];\n`;
345
+ domCode += ` createEffect(() => {\n`;
346
+ domCode += ` ${eid}_els.forEach(e => e.remove()); ${eid}_els = [];\n`;
347
+ domCode += ` const _items = ${node.attribs.array} || [];\n`;
348
+ domCode += ` const f = document.createDocumentFragment();\n`;
349
+ domCode += ` for (let _i = 0; _i < _items.length; _i++) {\n`;
350
+ domCode += ` const ${node.attribs.item} = _items[_i];\n`;
351
+ if (node.attribs.index) {
352
+ domCode += ` const ${node.attribs.index} = _i;\n`;
353
+ }
354
+ node.children.forEach(child => emitNode(child, 'f'));
355
+ domCode += ` }\n`;
356
+ domCode += ` Array.from(f.childNodes).forEach(n => ${eid}_els.push(n));\n`;
357
+ domCode += ` ${eid}_a.parentNode.insertBefore(f, ${eid}_a.nextSibling);\n`;
358
+ domCode += ` });\n`;
359
+ return;
360
+ }
361
+
362
+ // Normal element
363
+ const id = `n${uid++}`;
364
+ domCode += ` const ${id} = document.createElement('${node.name}');\n`;
365
+
366
+ if (hasStyles) {
367
+ domCode += ` ${id}.classList.add('${scopeHash}');\n`;
368
+ }
369
+
370
+ for (const [key, val] of Object.entries(node.attribs || {})) {
371
+ if (key.startsWith('on:')) {
372
+ const eventName = key.slice(3);
373
+ if (!/^[a-zA-Z][a-zA-Z0-9]*$/.test(eventName)) continue;
374
+ domCode += ` ${id}.addEventListener('${eventName}', (e) => { ${val} });\n`;
375
+ } else if (key.startsWith('on')) {
376
+ const eventName = key.slice(2).toLowerCase();
377
+ if (!/^[a-zA-Z][a-zA-Z0-9]*$/.test(eventName)) continue;
378
+ domCode += ` ${id}.addEventListener('${eventName}', (e) => { ${val} });\n`;
379
+ } else if (key.startsWith('bind:')) {
380
+ const prop = key.slice(5);
381
+ if (prop === 'value') {
382
+ domCode += ` ${id}.addEventListener('input', (e) => { set_${val}(e.target.value); });\n`;
383
+ domCode += ` createEffect(() => { ${id}.value = ${val}() ?? ''; });\n`;
384
+ }
385
+ } else if (key === 'class' || key === 'className') {
386
+ domCode += ` ${id}.className += ' ' + \`${val}\`;\n`;
387
+ } else {
388
+ domCode += ` ${id}.setAttribute('${key}', \`${val}\`);\n`;
389
+ }
390
+ }
391
+
392
+ domCode += ` ${parentVar}.appendChild(${id});\n`;
393
+
394
+ if (node.children && !VOID_ELEMENTS.has(node.name)) {
395
+ for (const child of node.children) {
396
+ emitNode(child, id);
397
+ }
398
+ }
399
+ }
400
+
401
+ root.children.forEach(child => emitNode(child, '__target'));
402
+
403
+ // Assemble final output module
404
+ let output = '';
405
+ output += `// Compiled by @sola-air-ui/compiler v1.0.0\n`;
406
+ output += `import { createSignal, createDerived, createEffect, createIntent, createData, onMount, onDestroy, pushContext, popContext, __flush_mounts, __flush_destroys } from '@sola-air-ui/core';\n`;
407
+
408
+ for (const imp of componentImports) {
409
+ output += `import ${imp.localName} from '${imp.path}';\n`;
410
+ }
411
+
412
+ output += `\n`;
413
+ output += `export default function mount(__target, props = {}) {\n`;
414
+ output += ` const __ctx = pushContext();\n\n`;
415
+
416
+ if (hasStyles) {
417
+ output += ` if (typeof document !== 'undefined' && !window['__SOLA_STYLE_${scopeHash}']) {\n`;
418
+ output += ` window['__SOLA_STYLE_${scopeHash}'] = true;\n`;
419
+ output += ` const styleEl = document.createElement('style');\n`;
420
+ output += ` styleEl.textContent = \`${scopedCss}\`;\n`;
421
+ output += ` document.head.appendChild(styleEl);\n`;
422
+ output += ` }\n\n`;
423
+ }
424
+
425
+ for (const prop of exportedProps) {
426
+ output += ` let ${prop.name} = props.${prop.name} !== undefined ? props.${prop.name} : ${prop.default};\n`;
427
+ }
428
+
429
+ output += `\n // User script\n`;
430
+ output += jsOutput.split('\n').map(l => ' ' + l).join('\n') + '\n\n';
431
+
432
+ output += ` // Reactive DOM graph\n`;
433
+ output += domCode;
434
+
435
+ output += `\n __flush_mounts();\n`;
436
+ output += `\n return () => {\n`;
437
+ output += ` // Cleanup\n`;
438
+ output += ` __flush_destroys();\n`;
439
+ output += ` popContext(__ctx);\n`;
440
+ output += ` __target.innerHTML = '';\n`;
441
+ output += ` };\n`;
442
+ output += `}\n`;
443
+
444
+ return {
445
+ code: output,
446
+ scopeHash,
447
+ css: scopedCss
448
+ };
449
+ }