@sprlab/wccompiler 0.3.0 → 0.4.1
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 +6 -6
- package/bin/wcc.test.js +23 -16
- package/lib/codegen.js +169 -38
- package/lib/compiler-browser.js +21 -0
- package/lib/compiler.js +228 -91
- package/lib/dev-server.js +30 -1
- package/lib/parser-extractors.js +43 -21
- package/lib/sfc-parser.js +262 -0
- package/lib/tree-walker.js +25 -18
- package/lib/types.js +3 -1
- package/package.json +3 -3
- package/types/wcc.d.ts +5 -6
- 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,
|
|
@@ -130,14 +131,13 @@ export function walkTree(rootEl, signalNames, computedNames, propNames = new Set
|
|
|
130
131
|
}
|
|
131
132
|
}
|
|
132
133
|
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
}
|
|
134
|
+
// Always register child component for auto-import (even without prop bindings)
|
|
135
|
+
childComponents.push({
|
|
136
|
+
tag: tagLower,
|
|
137
|
+
varName: `__child${childIdx++}`,
|
|
138
|
+
path: [...pathParts],
|
|
139
|
+
propBindings,
|
|
140
|
+
});
|
|
141
141
|
}
|
|
142
142
|
|
|
143
143
|
// Check for @event attributes
|
|
@@ -240,19 +240,25 @@ export function walkTree(rootEl, signalNames, computedNames, propNames = new Set
|
|
|
240
240
|
}
|
|
241
241
|
|
|
242
242
|
// --- Text node with interpolations ---
|
|
243
|
-
if (node.nodeType === 3 && /\{\{[\w.]+\}\}/.test(node.textContent)) {
|
|
243
|
+
if (node.nodeType === 3 && /\{\{[\w.()]+\}\}/.test(node.textContent)) {
|
|
244
244
|
const text = node.textContent;
|
|
245
245
|
const trimmed = text.trim();
|
|
246
|
-
const soleMatch = trimmed.match(/^\{\{([\w.]+)\}\}$/);
|
|
246
|
+
const soleMatch = trimmed.match(/^\{\{([\w.()]+)\}\}$/);
|
|
247
247
|
const parent = node.parentNode;
|
|
248
248
|
|
|
249
|
+
// Strip trailing () from expression to get the base name for type lookup
|
|
250
|
+
function baseName(expr) {
|
|
251
|
+
return expr.endsWith('()') ? expr.slice(0, -2) : expr;
|
|
252
|
+
}
|
|
253
|
+
|
|
249
254
|
// Case 1: {{var}} is the sole content of the parent element and parent has only one child text node
|
|
250
255
|
if (soleMatch && parent.childNodes.length === 1) {
|
|
251
256
|
const varName = `__b${bindIdx++}`;
|
|
257
|
+
const name = baseName(soleMatch[1]);
|
|
252
258
|
bindings.push({
|
|
253
259
|
varName,
|
|
254
|
-
name
|
|
255
|
-
type: bindingType(
|
|
260
|
+
name,
|
|
261
|
+
type: bindingType(name),
|
|
256
262
|
path: pathParts.slice(0, -1), // path to parent, not text node
|
|
257
263
|
});
|
|
258
264
|
parent.textContent = '';
|
|
@@ -262,7 +268,7 @@ export function walkTree(rootEl, signalNames, computedNames, propNames = new Set
|
|
|
262
268
|
// Case 2: Mixed text and interpolations — split into spans
|
|
263
269
|
const doc = node.ownerDocument;
|
|
264
270
|
const fragment = doc.createDocumentFragment();
|
|
265
|
-
const parts = text.split(/(\{\{[\w.]+\}\})/);
|
|
271
|
+
const parts = text.split(/(\{\{[\w.()]+\}\})/);
|
|
266
272
|
const parentPath = pathParts.slice(0, -1);
|
|
267
273
|
|
|
268
274
|
// Find the index of this text node among its siblings
|
|
@@ -274,14 +280,15 @@ export function walkTree(rootEl, signalNames, computedNames, propNames = new Set
|
|
|
274
280
|
|
|
275
281
|
let offset = 0;
|
|
276
282
|
for (const part of parts) {
|
|
277
|
-
const bm = part.match(/^\{\{([\w.]+)\}\}$/);
|
|
283
|
+
const bm = part.match(/^\{\{([\w.()]+)\}\}$/);
|
|
278
284
|
if (bm) {
|
|
279
285
|
fragment.appendChild(doc.createElement('span'));
|
|
280
286
|
const varName = `__b${bindIdx++}`;
|
|
287
|
+
const name = baseName(bm[1]);
|
|
281
288
|
bindings.push({
|
|
282
289
|
varName,
|
|
283
|
-
name
|
|
284
|
-
type: bindingType(
|
|
290
|
+
name,
|
|
291
|
+
type: bindingType(name),
|
|
285
292
|
path: [...parentPath, `childNodes[${baseIndex + offset}]`],
|
|
286
293
|
});
|
|
287
294
|
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
|
|
@@ -98,6 +99,7 @@
|
|
|
98
99
|
* @property {RefBinding[]} refBindings — ref attribute bindings from template (empty array if none)
|
|
99
100
|
* @property {ChildComponentBinding[]} childComponents — Child component bindings (empty array if none)
|
|
100
101
|
* @property {ChildComponentImport[]} childImports — Resolved child component imports (empty array if none)
|
|
102
|
+
* @property {string[]} exposeNames — Property names from defineExpose (empty array if none)
|
|
101
103
|
*/
|
|
102
104
|
|
|
103
105
|
/**
|
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.1",
|
|
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,22 +7,21 @@ 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;
|
|
21
20
|
export function defineEmits(names: string[]): (name: string, detail?: any) => void;
|
|
22
21
|
|
|
23
|
-
export function templateRef(name: string): { value: HTMLElement | null };
|
|
22
|
+
export function templateRef<T = HTMLElement>(name: string): { value: (T & HTMLElement) | null };
|
|
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
|
-
}
|