@sprlab/wccompiler 0.3.0 → 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.
- package/README.md +130 -37
- package/bin/wcc.js +4 -5
- package/bin/wcc.test.js +23 -16
- package/lib/codegen.js +150 -36
- package/lib/compiler-browser.js +21 -0
- package/lib/compiler.js +225 -91
- package/lib/parser-extractors.js +20 -21
- package/lib/sfc-parser.js +262 -0
- package/lib/tree-walker.js +18 -10
- package/lib/types.js +2 -1
- package/package.json +3 -3
- package/types/wcc.d.ts +4 -5
- package/types/wcc.test.js +2 -2
- package/lib/printer.js +0 -118
|
@@ -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
|
+
}
|
package/lib/tree-walker.js
CHANGED
|
@@ -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
|
|
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
|
|
255
|
-
type: bindingType(
|
|
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
|
|
284
|
-
type: bindingType(
|
|
291
|
+
name,
|
|
292
|
+
type: bindingType(name),
|
|
285
293
|
path: [...parentPath, `childNodes[${baseIndex + offset}]`],
|
|
286
294
|
});
|
|
287
295
|
offset++;
|
package/lib/types.js
CHANGED
|
@@ -31,7 +31,8 @@
|
|
|
31
31
|
|
|
32
32
|
/**
|
|
33
33
|
* @typedef {Object} WatcherDef
|
|
34
|
-
* @property {
|
|
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')
|
|
35
36
|
* @property {string} newParam — Parameter name for new value (e.g., 'newVal')
|
|
36
37
|
* @property {string} oldParam — Parameter name for old value (e.g., 'oldVal')
|
|
37
38
|
* @property {string} body — Callback body
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sprlab/wccompiler",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Zero-runtime compiler that transforms .
|
|
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,14 +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:
|
|
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;
|
|
11
12
|
export function defineComponent(options: {
|
|
12
13
|
tag: string;
|
|
13
|
-
template: string;
|
|
14
|
-
styles?: string;
|
|
15
14
|
}): void;
|
|
16
15
|
|
|
17
|
-
export function defineProps<T extends Record<string, any>>(defaults
|
|
16
|
+
export function defineProps<T extends Record<string, any>>(defaults: T): T;
|
|
18
17
|
export function defineProps(names: string[]): Record<string, any>;
|
|
19
18
|
|
|
20
19
|
export function defineEmits<T>(): T;
|
|
@@ -24,5 +23,5 @@ declare module 'wcc' {
|
|
|
24
23
|
|
|
25
24
|
export function onMount(fn: () => void | Promise<void>): void;
|
|
26
25
|
export function onDestroy(fn: () => void | Promise<void>): void;
|
|
27
|
-
export function
|
|
26
|
+
export function defineExpose(bindings: Record<string, any>): void;
|
|
28
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
|
|
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
|
-
}
|