@sprlab/wccompiler 0.2.1 → 0.4.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.
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Template string containing the mini reactive runtime (~40 lines).
2
+ * Template string containing the mini reactive runtime.
3
3
  * This gets inlined at the top of each compiled component so the output
4
4
  * is fully self-contained with zero imports.
5
5
  *
@@ -7,12 +7,16 @@
7
7
  * - __signal(initialValue): getter/setter function with subscriber tracking
8
8
  * - __computed(fn): cached derived value that auto-invalidates
9
9
  * - __effect(fn): runs fn immediately and re-runs when dependencies change
10
+ * - __batch(fn): batch multiple signal writes, flush effects once at the end
10
11
  *
11
12
  * Dependency tracking uses a global stack (__currentEffect).
13
+ * Batching uses a depth counter — nested batches are supported.
12
14
  */
13
15
  /** @type {string} */
14
16
  export const reactiveRuntime = `
15
17
  let __currentEffect = null;
18
+ let __batchDepth = 0;
19
+ const __pendingEffects = [];
16
20
 
17
21
  function __signal(initial) {
18
22
  let _value = initial;
@@ -25,7 +29,13 @@ function __signal(initial) {
25
29
  const old = _value;
26
30
  _value = args[0];
27
31
  if (old !== _value) {
28
- for (const fn of [..._subs]) fn();
32
+ if (__batchDepth > 0) {
33
+ for (const fn of _subs) {
34
+ if (!__pendingEffects.includes(fn)) __pendingEffects.push(fn);
35
+ }
36
+ } else {
37
+ for (const fn of [..._subs]) fn();
38
+ }
29
39
  }
30
40
  };
31
41
  }
@@ -35,7 +45,13 @@ function __computed(fn) {
35
45
  const _subs = new Set();
36
46
  const recompute = () => {
37
47
  _dirty = true;
38
- for (const fn of [..._subs]) fn();
48
+ if (__batchDepth > 0) {
49
+ for (const fn of _subs) {
50
+ if (!__pendingEffects.includes(fn)) __pendingEffects.push(fn);
51
+ }
52
+ } else {
53
+ for (const fn of [..._subs]) fn();
54
+ }
39
55
  };
40
56
  return () => {
41
57
  if (__currentEffect) _subs.add(__currentEffect);
@@ -51,12 +67,27 @@ function __computed(fn) {
51
67
  }
52
68
 
53
69
  function __effect(fn) {
70
+ let _cleanup = null;
54
71
  const run = () => {
72
+ if (typeof _cleanup === 'function') _cleanup();
55
73
  const prev = __currentEffect;
56
74
  __currentEffect = run;
57
- fn();
75
+ _cleanup = fn();
58
76
  __currentEffect = prev;
59
77
  };
60
78
  run();
61
79
  }
80
+
81
+ function __batch(fn) {
82
+ __batchDepth++;
83
+ try {
84
+ fn();
85
+ } finally {
86
+ __batchDepth--;
87
+ if (__batchDepth === 0) {
88
+ const pending = __pendingEffects.splice(0);
89
+ for (const f of pending) f();
90
+ }
91
+ }
92
+ }
62
93
  `;
@@ -0,0 +1,262 @@
1
+ /**
2
+ * SFC Parser — extracts <script>, <template> and <style> blocks from .wcc files.
3
+ *
4
+ * Pure ESM module with no Node.js dependencies (usable in browser and server).
5
+ *
6
+ * Two-phase algorithm:
7
+ * Phase 1: Block extraction via regex
8
+ * Phase 2: Validation (required blocks, duplicates, unexpected content, defineComponent)
9
+ */
10
+
11
+ /**
12
+ * @typedef {Object} SFCDescriptor
13
+ * @property {string} script — Content of the <script> block
14
+ * @property {string} template — Content of the <template> block
15
+ * @property {string} style — Content of the <style> block ('' if absent)
16
+ * @property {string} lang — 'ts' | 'js'
17
+ * @property {string} tag — Tag name extracted from defineComponent({ tag })
18
+ */
19
+
20
+ // ── Helpers ─────────────────────────────────────────────────────────
21
+
22
+ /**
23
+ * Create an Error with a `.code` property (project convention).
24
+ * @param {string} code
25
+ * @param {string} message
26
+ * @returns {Error}
27
+ */
28
+ function sfcError(code, message) {
29
+ const error = new Error(message);
30
+ /** @ts-expect-error — custom error code for programmatic handling */
31
+ error.code = code;
32
+ return error;
33
+ }
34
+
35
+ // ── Phase 1: Block extraction ───────────────────────────────────────
36
+
37
+ /**
38
+ * @typedef {Object} BlockMatch
39
+ * @property {string} content — Inner content between open and close tags
40
+ * @property {string} attrs — Attributes string from the opening tag
41
+ * @property {number} start — Start index of the opening tag in source
42
+ * @property {number} end — End index (after closing tag) in source
43
+ */
44
+
45
+ /**
46
+ * Find all occurrences of a given block type in the source.
47
+ *
48
+ * @param {string} source
49
+ * @param {string} blockName — 'script' | 'template' | 'style'
50
+ * @returns {BlockMatch[]}
51
+ */
52
+ function findBlocks(source, blockName) {
53
+ const openRe = new RegExp(`<${blockName}(\\s[^>]*)?>`, 'g');
54
+ const closeTag = `</${blockName}>`;
55
+ /** @type {BlockMatch[]} */
56
+ const matches = [];
57
+ let m;
58
+
59
+ while ((m = openRe.exec(source)) !== null) {
60
+ const openEnd = m.index + m[0].length;
61
+ const closeIdx = source.indexOf(closeTag, openEnd);
62
+ if (closeIdx === -1) continue; // unclosed tag — skip
63
+
64
+ matches.push({
65
+ content: source.slice(openEnd, closeIdx),
66
+ attrs: m[1] || '',
67
+ start: m.index,
68
+ end: closeIdx + closeTag.length,
69
+ });
70
+ }
71
+
72
+ return matches;
73
+ }
74
+
75
+ /**
76
+ * Extract the `lang` attribute value from an attributes string.
77
+ * Returns 'ts' if lang="ts", otherwise 'js'.
78
+ *
79
+ * @param {string} attrs
80
+ * @returns {'ts' | 'js'}
81
+ */
82
+ function extractLang(attrs) {
83
+ const langMatch = attrs.match(/lang\s*=\s*["']([^"']+)["']/);
84
+ return langMatch && langMatch[1] === 'ts' ? 'ts' : 'js';
85
+ }
86
+
87
+ // ── Phase 2: Validation ─────────────────────────────────────────────
88
+
89
+ /**
90
+ * Extract the tag name from a defineComponent({ tag: '...' }) call.
91
+ *
92
+ * @param {string} script
93
+ * @param {string} fileName
94
+ * @returns {string}
95
+ */
96
+ function extractTagFromDefineComponent(script, fileName) {
97
+ const dcMatch = script.match(/defineComponent\(\s*\{([^}]*)\}\s*\)/);
98
+ if (!dcMatch) {
99
+ throw sfcError(
100
+ 'MISSING_DEFINE_COMPONENT',
101
+ `Error en '${fileName}': defineComponent() es obligatorio`
102
+ );
103
+ }
104
+
105
+ const body = dcMatch[1];
106
+
107
+ // Reject template/styles fields inside defineComponent in SFC mode
108
+ if (/\btemplate\s*:/.test(body)) {
109
+ throw sfcError(
110
+ 'SFC_INLINE_PATHS_FORBIDDEN',
111
+ `SFC file '${fileName}': template/styles paths are not allowed in SFC mode (content is inline)`
112
+ );
113
+ }
114
+ if (/\bstyles\s*:/.test(body)) {
115
+ throw sfcError(
116
+ 'SFC_INLINE_PATHS_FORBIDDEN',
117
+ `SFC file '${fileName}': template/styles paths are not allowed in SFC mode (content is inline)`
118
+ );
119
+ }
120
+
121
+ const tagMatch = body.match(/tag\s*:\s*['"]([^'"]+)['"]/);
122
+ if (!tagMatch) {
123
+ throw sfcError(
124
+ 'MISSING_DEFINE_COMPONENT',
125
+ `Error en '${fileName}': defineComponent() must include a tag field`
126
+ );
127
+ }
128
+
129
+ return tagMatch[1];
130
+ }
131
+
132
+ /**
133
+ * Check that no non-whitespace content exists outside the recognized blocks.
134
+ *
135
+ * @param {string} source
136
+ * @param {Array<{start: number, end: number}>} blockRanges — sorted by start
137
+ * @param {string} fileName
138
+ */
139
+ function validateNoUnexpectedContent(source, blockRanges, fileName) {
140
+ let cursor = 0;
141
+
142
+ for (const range of blockRanges) {
143
+ const outside = source.slice(cursor, range.start);
144
+ if (outside.trim().length > 0) {
145
+ throw sfcError(
146
+ 'SFC_UNEXPECTED_CONTENT',
147
+ `SFC file '${fileName}' contains unexpected content outside blocks`
148
+ );
149
+ }
150
+ cursor = range.end;
151
+ }
152
+
153
+ // Check trailing content after last block
154
+ const trailing = source.slice(cursor);
155
+ if (trailing.trim().length > 0) {
156
+ throw sfcError(
157
+ 'SFC_UNEXPECTED_CONTENT',
158
+ `SFC file '${fileName}' contains unexpected content outside blocks`
159
+ );
160
+ }
161
+ }
162
+
163
+ // ── Public API ──────────────────────────────────────────────────────
164
+
165
+ /**
166
+ * Parse an SFC source string and extract its blocks.
167
+ *
168
+ * @param {string} source — Full content of the .wcc file
169
+ * @param {string} [fileName='<unknown>'] — File name for error messages
170
+ * @returns {SFCDescriptor}
171
+ * @throws {Error} with codes: SFC_MISSING_TEMPLATE, SFC_MISSING_SCRIPT,
172
+ * SFC_DUPLICATE_BLOCK, SFC_UNEXPECTED_CONTENT,
173
+ * SFC_INLINE_PATHS_FORBIDDEN, MISSING_DEFINE_COMPONENT
174
+ */
175
+ export function parseSFC(source, fileName = '<unknown>') {
176
+ // ── Phase 1: Extract blocks ─────────────────────────────────────
177
+
178
+ const scriptBlocks = findBlocks(source, 'script');
179
+ const templateBlocks = findBlocks(source, 'template');
180
+ const styleBlocks = findBlocks(source, 'style');
181
+
182
+ // Check for duplicates
183
+ if (scriptBlocks.length > 1) {
184
+ throw sfcError(
185
+ 'SFC_DUPLICATE_BLOCK',
186
+ `SFC file '${fileName}' contains duplicate <script> blocks`
187
+ );
188
+ }
189
+ if (templateBlocks.length > 1) {
190
+ throw sfcError(
191
+ 'SFC_DUPLICATE_BLOCK',
192
+ `SFC file '${fileName}' contains duplicate <template> blocks`
193
+ );
194
+ }
195
+ if (styleBlocks.length > 1) {
196
+ throw sfcError(
197
+ 'SFC_DUPLICATE_BLOCK',
198
+ `SFC file '${fileName}' contains duplicate <style> blocks`
199
+ );
200
+ }
201
+
202
+ // ── Phase 2: Validation ─────────────────────────────────────────
203
+
204
+ // Required blocks
205
+ if (templateBlocks.length === 0) {
206
+ throw sfcError(
207
+ 'SFC_MISSING_TEMPLATE',
208
+ `SFC file '${fileName}' is missing a <template> block`
209
+ );
210
+ }
211
+ if (scriptBlocks.length === 0) {
212
+ throw sfcError(
213
+ 'SFC_MISSING_SCRIPT',
214
+ `SFC file '${fileName}' is missing a <script> block`
215
+ );
216
+ }
217
+
218
+ // Collect all block ranges for unexpected-content check
219
+ /** @type {Array<{start: number, end: number}>} */
220
+ const allRanges = [
221
+ ...scriptBlocks,
222
+ ...templateBlocks,
223
+ ...styleBlocks,
224
+ ].sort((a, b) => a.start - b.start);
225
+
226
+ validateNoUnexpectedContent(source, allRanges, fileName);
227
+
228
+ // Extract block contents
229
+ const scriptContent = scriptBlocks[0].content;
230
+ const templateContent = templateBlocks[0].content;
231
+ const styleContent = styleBlocks.length > 0 ? styleBlocks[0].content : '';
232
+ const lang = extractLang(scriptBlocks[0].attrs);
233
+
234
+ // Validate defineComponent and extract tag
235
+ const tag = extractTagFromDefineComponent(scriptContent, fileName);
236
+
237
+ return {
238
+ script: scriptContent,
239
+ template: templateContent,
240
+ style: styleContent,
241
+ lang,
242
+ tag,
243
+ };
244
+ }
245
+
246
+ /**
247
+ * Pretty-printer: reconstruct an SFC string from a descriptor.
248
+ *
249
+ * @param {SFCDescriptor} descriptor
250
+ * @returns {string}
251
+ */
252
+ export function printSFC(descriptor) {
253
+ const langAttr = descriptor.lang === 'ts' ? ' lang="ts"' : '';
254
+ let result = `<script${langAttr}>${descriptor.script}</script>\n\n`;
255
+ result += `<template>${descriptor.template}</template>`;
256
+
257
+ if (descriptor.style && descriptor.style.length > 0) {
258
+ result += `\n\n<style>${descriptor.style}</style>`;
259
+ }
260
+
261
+ return result;
262
+ }
@@ -117,9 +117,10 @@ export function walkTree(rootEl, signalNames, computedNames, propNames = new Set
117
117
  if (['show', 'model', 'if', 'else-if', 'else', 'each', 'ref'].includes(attr.name)) continue;
118
118
 
119
119
  // Check for {{interpolation}} in attribute value
120
- const interpMatch = attr.value.match(/^\{\{([\w.]+)\}\}$/);
120
+ const interpMatch = attr.value.match(/^\{\{([\w.()]+)\}\}$/);
121
121
  if (interpMatch) {
122
- const expr = interpMatch[1];
122
+ const rawExpr = interpMatch[1];
123
+ const expr = rawExpr.endsWith('()') ? rawExpr.slice(0, -2) : rawExpr;
123
124
  propBindings.push({
124
125
  attr: attr.name,
125
126
  expr,
@@ -240,19 +241,25 @@ export function walkTree(rootEl, signalNames, computedNames, propNames = new Set
240
241
  }
241
242
 
242
243
  // --- Text node with interpolations ---
243
- if (node.nodeType === 3 && /\{\{[\w.]+\}\}/.test(node.textContent)) {
244
+ if (node.nodeType === 3 && /\{\{[\w.()]+\}\}/.test(node.textContent)) {
244
245
  const text = node.textContent;
245
246
  const trimmed = text.trim();
246
- const soleMatch = trimmed.match(/^\{\{([\w.]+)\}\}$/);
247
+ const soleMatch = trimmed.match(/^\{\{([\w.()]+)\}\}$/);
247
248
  const parent = node.parentNode;
248
249
 
250
+ // Strip trailing () from expression to get the base name for type lookup
251
+ function baseName(expr) {
252
+ return expr.endsWith('()') ? expr.slice(0, -2) : expr;
253
+ }
254
+
249
255
  // Case 1: {{var}} is the sole content of the parent element and parent has only one child text node
250
256
  if (soleMatch && parent.childNodes.length === 1) {
251
257
  const varName = `__b${bindIdx++}`;
258
+ const name = baseName(soleMatch[1]);
252
259
  bindings.push({
253
260
  varName,
254
- name: soleMatch[1],
255
- type: bindingType(soleMatch[1]),
261
+ name,
262
+ type: bindingType(name),
256
263
  path: pathParts.slice(0, -1), // path to parent, not text node
257
264
  });
258
265
  parent.textContent = '';
@@ -262,7 +269,7 @@ export function walkTree(rootEl, signalNames, computedNames, propNames = new Set
262
269
  // Case 2: Mixed text and interpolations — split into spans
263
270
  const doc = node.ownerDocument;
264
271
  const fragment = doc.createDocumentFragment();
265
- const parts = text.split(/(\{\{[\w.]+\}\})/);
272
+ const parts = text.split(/(\{\{[\w.()]+\}\})/);
266
273
  const parentPath = pathParts.slice(0, -1);
267
274
 
268
275
  // Find the index of this text node among its siblings
@@ -274,14 +281,15 @@ export function walkTree(rootEl, signalNames, computedNames, propNames = new Set
274
281
 
275
282
  let offset = 0;
276
283
  for (const part of parts) {
277
- const bm = part.match(/^\{\{([\w.]+)\}\}$/);
284
+ const bm = part.match(/^\{\{([\w.()]+)\}\}$/);
278
285
  if (bm) {
279
286
  fragment.appendChild(doc.createElement('span'));
280
287
  const varName = `__b${bindIdx++}`;
288
+ const name = baseName(bm[1]);
281
289
  bindings.push({
282
290
  varName,
283
- name: bm[1],
284
- type: bindingType(bm[1]),
291
+ name,
292
+ type: bindingType(name),
285
293
  path: [...parentPath, `childNodes[${baseIndex + offset}]`],
286
294
  });
287
295
  offset++;
package/lib/types.js CHANGED
@@ -26,6 +26,16 @@
26
26
  /**
27
27
  * @typedef {Object} LifecycleHook
28
28
  * @property {string} body — The callback body (JavaScript code)
29
+ * @property {boolean} async — Whether the callback is async
30
+ */
31
+
32
+ /**
33
+ * @typedef {Object} WatcherDef
34
+ * @property {'signal' | 'getter'} kind — Type of watch target
35
+ * @property {string} target — Signal/computed name (for kind='signal') or getter expression (for kind='getter')
36
+ * @property {string} newParam — Parameter name for new value (e.g., 'newVal')
37
+ * @property {string} oldParam — Parameter name for old value (e.g., 'oldVal')
38
+ * @property {string} body — Callback body
29
39
  */
30
40
 
31
41
  /**
@@ -68,6 +78,7 @@
68
78
  * @property {ComputedDef[]} computeds — computed() declarations
69
79
  * @property {EffectDef[]} effects — effect() declarations
70
80
  * @property {ConstantVar[]} constantVars — Plain const declarations (non-reactive)
81
+ * @property {WatcherDef[]} watchers — watch() declarations
71
82
  * @property {MethodDef[]} methods — function declarations
72
83
  * @property {PropDef[]} propDefs — Prop definitions with names and defaults
73
84
  * @property {string|null} propsObjectName — Variable name from `const X = defineProps(...)`
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@sprlab/wccompiler",
3
- "version": "0.2.1",
4
- "description": "Zero-runtime compiler that transforms .ts/.js component files into native web components with signals-based reactivity",
3
+ "version": "0.4.0",
4
+ "description": "Zero-runtime compiler that transforms .wcc single-file components into native web components with signals-based reactivity",
5
5
  "type": "module",
6
6
  "bin": {
7
7
  "wcc": "./bin/wcc.js"
@@ -44,4 +44,4 @@
44
44
  "node": "24.0.0",
45
45
  "yarn": "4.12.0"
46
46
  }
47
- }
47
+ }
package/types/wcc.d.ts CHANGED
@@ -7,13 +7,13 @@ declare module 'wcc' {
7
7
  export function signal<T>(value: T): Signal<T>;
8
8
  export function computed<T>(fn: () => T): () => T;
9
9
  export function effect(fn: () => void): void;
10
+ export function watch<T>(target: Signal<T>, fn: (newVal: T, oldVal: T) => void): void;
11
+ export function watch<T>(target: () => T, fn: (newVal: T, oldVal: T) => void): void;
10
12
  export function defineComponent(options: {
11
13
  tag: string;
12
- template: string;
13
- styles?: string;
14
14
  }): void;
15
15
 
16
- export function defineProps<T extends Record<string, any>>(defaults?: Partial<T>): T;
16
+ export function defineProps<T extends Record<string, any>>(defaults: T): T;
17
17
  export function defineProps(names: string[]): Record<string, any>;
18
18
 
19
19
  export function defineEmits<T>(): T;
@@ -21,7 +21,7 @@ declare module 'wcc' {
21
21
 
22
22
  export function templateRef(name: string): { value: HTMLElement | null };
23
23
 
24
- export function onMount(fn: () => void): void;
25
- export function onDestroy(fn: () => void): void;
26
- export function templateBindings(bindings: Record<string, any>): void;
24
+ export function onMount(fn: () => void | Promise<void>): void;
25
+ export function onDestroy(fn: () => void | Promise<void>): void;
26
+ export function defineExpose(bindings: Record<string, any>): void;
27
27
  }
package/types/wcc.test.js CHANGED
@@ -30,8 +30,8 @@ describe('type declarations (wcc.d.ts)', () => {
30
30
  const content = readFileSync(dtsPath, 'utf-8');
31
31
  expect(content).toContain('function defineComponent');
32
32
  expect(content).toContain('tag: string');
33
- expect(content).toContain('template: string');
34
- expect(content).toContain('styles?: string');
33
+ expect(content).not.toContain('template?: string');
34
+ expect(content).not.toContain('styles?: string');
35
35
  });
36
36
 
37
37
  it('Signal<T> interface has call signature (): T', () => {
package/lib/printer.js DELETED
@@ -1,118 +0,0 @@
1
- /**
2
- * Pretty Printer — serializes a ParseResult IR back to valid .js source format.
3
- *
4
- * Used for round-trip testing: parse → prettyPrint → parse should yield
5
- * an equivalent IR.
6
- */
7
-
8
- /** @import { ParseResult } from './types.js' */
9
-
10
- /**
11
- * Pretty-print a ParseResult IR back to component source format.
12
- *
13
- * @param {ParseResult} ir — The intermediate representation
14
- * @returns {string} Reconstructed source code
15
- */
16
- export function prettyPrint(ir) {
17
- const sections = [];
18
-
19
- // 1. Import statement — include only macros actually used
20
- const macros = ['defineComponent'];
21
- if ((ir.propDefs || []).length > 0) macros.push('defineProps');
22
- if ((ir.emits || []).length > 0) macros.push('defineEmits');
23
- if (ir.signals.length > 0) macros.push('signal');
24
- if (ir.computeds.length > 0) macros.push('computed');
25
- if (ir.effects.length > 0) macros.push('effect');
26
- if ((ir.onMountHooks || []).length > 0) macros.push('onMount');
27
- if ((ir.onDestroyHooks || []).length > 0) macros.push('onDestroy');
28
- sections.push(`import { ${macros.join(', ')} } from 'wcc'`);
29
-
30
- // 2. defineComponent call
31
- const defParts = [];
32
- defParts.push(` tag: '${ir.tagName}',`);
33
- defParts.push(` template: './${ir.tagName}.html',`);
34
- if (ir.style !== '') {
35
- defParts.push(` styles: './${ir.tagName}.css',`);
36
- }
37
- sections.push(`export default defineComponent({\n${defParts.join('\n')}\n})`);
38
-
39
- // 3. defineProps (if present)
40
- if ((ir.propDefs || []).length > 0) {
41
- const propsObjName = ir.propsObjectName || 'props';
42
- const propEntries = ir.propDefs.map(p => `${p.name}: ${p.default}`);
43
- sections.push(`const ${propsObjName} = defineProps({ ${propEntries.join(', ')} })`);
44
- }
45
-
46
- // 3b. defineEmits (if present)
47
- if ((ir.emits || []).length > 0) {
48
- const emitsObjName = ir.emitsObjectName || 'emit';
49
- const emitEntries = ir.emits.map(e => `'${e}'`).join(', ');
50
- sections.push(`const ${emitsObjName} = defineEmits([${emitEntries}])`);
51
- }
52
-
53
- // 4. Signal declarations
54
- if (ir.signals.length > 0) {
55
- const signalLines = ir.signals.map(
56
- s => `const ${s.name} = signal(${s.value})`
57
- );
58
- sections.push(signalLines.join('\n'));
59
- }
60
-
61
- // 5. Computed declarations
62
- if (ir.computeds.length > 0) {
63
- const computedLines = ir.computeds.map(
64
- c => `const ${c.name} = computed(() => ${c.body})`
65
- );
66
- sections.push(computedLines.join('\n'));
67
- }
68
-
69
- // 6. Effect declarations
70
- if (ir.effects.length > 0) {
71
- const effectBlocks = ir.effects.map(e => {
72
- const indentedBody = e.body
73
- .split('\n')
74
- .map(line => ` ${line}`)
75
- .join('\n');
76
- return `effect(() => {\n${indentedBody}\n})`;
77
- });
78
- sections.push(effectBlocks.join('\n\n'));
79
- }
80
-
81
- // 7. Function declarations
82
- if (ir.methods.length > 0) {
83
- const fnBlocks = ir.methods.map(m => {
84
- const indentedBody = m.body
85
- .split('\n')
86
- .map(line => ` ${line}`)
87
- .join('\n');
88
- return `function ${m.name}(${m.params}) {\n${indentedBody}\n}`;
89
- });
90
- sections.push(fnBlocks.join('\n\n'));
91
- }
92
-
93
- // 8. Lifecycle hooks — onMount
94
- if ((ir.onMountHooks || []).length > 0) {
95
- const mountBlocks = ir.onMountHooks.map(h => {
96
- const indentedBody = h.body
97
- .split('\n')
98
- .map(line => ` ${line}`)
99
- .join('\n');
100
- return `onMount(() => {\n${indentedBody}\n})`;
101
- });
102
- sections.push(mountBlocks.join('\n\n'));
103
- }
104
-
105
- // 9. Lifecycle hooks — onDestroy
106
- if ((ir.onDestroyHooks || []).length > 0) {
107
- const destroyBlocks = ir.onDestroyHooks.map(h => {
108
- const indentedBody = h.body
109
- .split('\n')
110
- .map(line => ` ${line}`)
111
- .join('\n');
112
- return `onDestroy(() => {\n${indentedBody}\n})`;
113
- });
114
- sections.push(destroyBlocks.join('\n\n'));
115
- }
116
-
117
- return sections.join('\n\n') + '\n';
118
- }