@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
package/lib/compiler.js
CHANGED
|
@@ -1,107 +1,241 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Compiler — orchestrates the full compilation pipeline for wcCompiler v2.
|
|
3
3
|
*
|
|
4
|
-
* Pipeline: parse → jsdom template → tree-walk → codegen
|
|
4
|
+
* Pipeline: parse SFC → jsdom template → tree-walk → codegen
|
|
5
5
|
*
|
|
6
|
-
* Takes a .
|
|
7
|
-
* JavaScript web component string.
|
|
6
|
+
* Takes a .wcc file path and produces a self-contained JavaScript web component string.
|
|
8
7
|
*/
|
|
9
8
|
|
|
10
9
|
import { JSDOM } from 'jsdom';
|
|
11
|
-
import { readFileSync,
|
|
12
|
-
import {
|
|
13
|
-
import { parse } from './parser.js';
|
|
10
|
+
import { readFileSync, existsSync } from 'node:fs';
|
|
11
|
+
import { dirname, extname, basename, resolve } from 'node:path';
|
|
14
12
|
import { walkTree, processIfChains, processForBlocks, recomputeAnchorPath, detectRefs } from './tree-walker.js';
|
|
15
13
|
import { generateComponent } from './codegen.js';
|
|
14
|
+
import { parseSFC } from './sfc-parser.js';
|
|
15
|
+
import {
|
|
16
|
+
stripMacroImport,
|
|
17
|
+
toClassName,
|
|
18
|
+
camelToKebab,
|
|
19
|
+
extractPropsGeneric,
|
|
20
|
+
extractPropsArray,
|
|
21
|
+
extractPropsDefaults,
|
|
22
|
+
extractPropsObjectName,
|
|
23
|
+
extractEmitsFromCallSignatures,
|
|
24
|
+
extractEmits,
|
|
25
|
+
extractEmitsObjectName,
|
|
26
|
+
extractEmitsObjectNameFromGeneric,
|
|
27
|
+
extractSignals,
|
|
28
|
+
extractComputeds,
|
|
29
|
+
extractEffects,
|
|
30
|
+
extractWatchers,
|
|
31
|
+
extractFunctions,
|
|
32
|
+
extractLifecycleHooks,
|
|
33
|
+
extractRefs,
|
|
34
|
+
extractConstants,
|
|
35
|
+
extractExpose,
|
|
36
|
+
validatePropsAssignment,
|
|
37
|
+
validateDuplicateProps,
|
|
38
|
+
validatePropsConflicts,
|
|
39
|
+
validateEmitsAssignment,
|
|
40
|
+
validateDuplicateEmits,
|
|
41
|
+
validateEmitsConflicts,
|
|
42
|
+
validateUndeclaredEmits,
|
|
43
|
+
} from './parser-extractors.js';
|
|
44
|
+
import { stripTypes } from './parser.js';
|
|
45
|
+
|
|
16
46
|
/**
|
|
17
|
-
* Resolve a child component's
|
|
18
|
-
*
|
|
47
|
+
* Resolve a child component's source file path by tag name.
|
|
48
|
+
*
|
|
49
|
+
* Searches for a file named after the tag in the source directory,
|
|
50
|
+
* trying extensions in priority order: .wcc, .js, .ts
|
|
19
51
|
*
|
|
20
|
-
* @param {string} tag —
|
|
21
|
-
* @param {string} sourceDir — Directory of the parent component
|
|
22
|
-
* @param {object} [config] — Optional config
|
|
23
|
-
* @returns {string | null} Relative import path (e.g., './wcc-
|
|
52
|
+
* @param {string} tag — The custom element tag name (e.g., 'wcc-child')
|
|
53
|
+
* @param {string} sourceDir — Directory of the parent component
|
|
54
|
+
* @param {object} [config] — Optional config (reserved for future use)
|
|
55
|
+
* @returns {string | null} Relative import path (e.g., './wcc-child.js') or null if not found
|
|
24
56
|
*/
|
|
25
57
|
function resolveChildComponent(tag, sourceDir, config) {
|
|
26
|
-
|
|
27
|
-
const
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
searchDirs.push(parentDir);
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
for (const dir of searchDirs) {
|
|
36
|
-
if (!existsSync(dir)) continue;
|
|
37
|
-
try {
|
|
38
|
-
const entries = readdirSync(dir, { withFileTypes: true, recursive: true });
|
|
39
|
-
for (const entry of entries) {
|
|
40
|
-
if (!entry.isFile()) continue;
|
|
41
|
-
const ext = extname(entry.name);
|
|
42
|
-
if (ext !== '.js' && ext !== '.ts') continue;
|
|
43
|
-
if (entry.name.includes('.test.')) continue;
|
|
44
|
-
if (entry.name.endsWith('.d.ts')) continue;
|
|
45
|
-
|
|
46
|
-
const fullPath = resolve(dir, entry.parentPath ? relative(dir, entry.parentPath) : '', entry.name);
|
|
47
|
-
try {
|
|
48
|
-
const content = readFileSync(fullPath, 'utf-8');
|
|
49
|
-
// Quick check: does this file define the component with the matching tag?
|
|
50
|
-
const tagMatch = content.match(/defineComponent\(\s*\{[^}]*tag\s*:\s*['"]([^'"]+)['"]/);
|
|
51
|
-
if (tagMatch && tagMatch[1] === tag) {
|
|
52
|
-
// Compute relative path from sourceDir to this file, with .js extension
|
|
53
|
-
let relPath = relative(sourceDir, fullPath);
|
|
54
|
-
// Ensure .js extension (replace .ts)
|
|
55
|
-
relPath = relPath.replace(/\.ts$/, '.js');
|
|
56
|
-
// Ensure starts with ./
|
|
57
|
-
if (!relPath.startsWith('.')) relPath = './' + relPath;
|
|
58
|
-
return relPath;
|
|
59
|
-
}
|
|
60
|
-
} catch {
|
|
61
|
-
// Skip files that can't be read
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
} catch {
|
|
65
|
-
// Skip dirs that can't be listed
|
|
58
|
+
const extensions = ['.wcc', '.js', '.ts'];
|
|
59
|
+
for (const ext of extensions) {
|
|
60
|
+
const candidate = resolve(sourceDir, `${tag}${ext}`);
|
|
61
|
+
if (existsSync(candidate)) {
|
|
62
|
+
// Return as a relative .js import path (compiled output)
|
|
63
|
+
return `./${tag}.js`;
|
|
66
64
|
}
|
|
67
65
|
}
|
|
68
|
-
|
|
69
66
|
return null;
|
|
70
67
|
}
|
|
71
68
|
|
|
72
69
|
/**
|
|
73
|
-
* Compile a single .
|
|
70
|
+
* Compile a single .wcc SFC file into a self-contained JS component.
|
|
71
|
+
*
|
|
72
|
+
* Reads the file, parses the SFC blocks, extracts reactive declarations
|
|
73
|
+
* from the script block using parser-extractors.js, and processes template
|
|
74
|
+
* and style through the existing pipeline (jsdom → tree-walker → codegen).
|
|
74
75
|
*
|
|
75
|
-
* @param {string} filePath — Absolute or relative path to the
|
|
76
|
+
* @param {string} filePath — Absolute or relative path to the .wcc file
|
|
76
77
|
* @param {object} [config] — Optional config (reserved for future options)
|
|
77
78
|
* @returns {Promise<string>} The generated JavaScript component code
|
|
78
79
|
*/
|
|
79
|
-
|
|
80
|
-
// 1.
|
|
81
|
-
const
|
|
80
|
+
async function compileSFC(filePath, config) {
|
|
81
|
+
// 1. Read and parse the SFC file
|
|
82
|
+
const rawSource = readFileSync(filePath, 'utf-8');
|
|
83
|
+
const fileName = basename(filePath);
|
|
84
|
+
const descriptor = parseSFC(rawSource, fileName);
|
|
82
85
|
|
|
83
|
-
// 2.
|
|
84
|
-
|
|
85
|
-
|
|
86
|
+
// 2. Process script block — mirrors parser.js logic
|
|
87
|
+
let source = stripMacroImport(descriptor.script);
|
|
88
|
+
|
|
89
|
+
// 3. Extract props/emits from generic forms BEFORE type stripping
|
|
90
|
+
const propsFromGeneric = extractPropsGeneric(source);
|
|
91
|
+
const propsObjectNameFromGeneric = extractPropsObjectName(source);
|
|
92
|
+
const emitsFromCallSignatures = extractEmitsFromCallSignatures(source);
|
|
93
|
+
const emitsObjectNameFromGeneric = extractEmitsObjectNameFromGeneric(source);
|
|
86
94
|
|
|
87
|
-
//
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
const propNames = new Set((parseResult.propDefs || []).map(p => p.name));
|
|
95
|
+
// 4. Validate props/emits assignment (before type strip)
|
|
96
|
+
validatePropsAssignment(source, filePath);
|
|
97
|
+
validateEmitsAssignment(source, filePath);
|
|
91
98
|
|
|
92
|
-
//
|
|
93
|
-
|
|
99
|
+
// 5. Strip TypeScript types if lang === 'ts'
|
|
100
|
+
if (descriptor.lang === 'ts') {
|
|
101
|
+
source = await stripTypes(source);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// 6. Extract component metadata
|
|
105
|
+
const tagName = descriptor.tag;
|
|
106
|
+
const className = toClassName(tagName);
|
|
107
|
+
const template = descriptor.template;
|
|
108
|
+
const style = descriptor.style;
|
|
94
109
|
|
|
95
|
-
//
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
110
|
+
// 7. Extract lifecycle hooks (before other extractions)
|
|
111
|
+
const { onMountHooks, onDestroyHooks } = extractLifecycleHooks(source);
|
|
112
|
+
|
|
113
|
+
// 7b. Strip lifecycle/watcher blocks from source for extraction
|
|
114
|
+
let sourceForExtraction = source;
|
|
115
|
+
const hookLinePattern = /\bonMount\s*\(|\bonDestroy\s*\(|\bwatch\s*\(/;
|
|
116
|
+
const sourceLines = sourceForExtraction.split('\n');
|
|
117
|
+
const filteredLines = [];
|
|
118
|
+
let skipDepth = 0;
|
|
119
|
+
let skipping = false;
|
|
120
|
+
for (const line of sourceLines) {
|
|
121
|
+
if (!skipping && hookLinePattern.test(line)) {
|
|
122
|
+
skipping = true;
|
|
123
|
+
skipDepth = 0;
|
|
124
|
+
for (const ch of line) {
|
|
125
|
+
if (ch === '{') skipDepth++;
|
|
126
|
+
if (ch === '}') skipDepth--;
|
|
127
|
+
}
|
|
128
|
+
if (skipDepth <= 0) skipping = false;
|
|
129
|
+
continue;
|
|
130
|
+
}
|
|
131
|
+
if (skipping) {
|
|
132
|
+
for (const ch of line) {
|
|
133
|
+
if (ch === '{') skipDepth++;
|
|
134
|
+
if (ch === '}') skipDepth--;
|
|
135
|
+
}
|
|
136
|
+
if (skipDepth <= 0) skipping = false;
|
|
137
|
+
continue;
|
|
138
|
+
}
|
|
139
|
+
filteredLines.push(line);
|
|
140
|
+
}
|
|
141
|
+
sourceForExtraction = filteredLines.join('\n');
|
|
142
|
+
|
|
143
|
+
// 8. Extract reactive declarations and functions
|
|
144
|
+
const signals = extractSignals(sourceForExtraction);
|
|
145
|
+
const computeds = extractComputeds(sourceForExtraction);
|
|
146
|
+
const effects = extractEffects(sourceForExtraction);
|
|
147
|
+
const watchers = extractWatchers(source);
|
|
148
|
+
const methods = extractFunctions(sourceForExtraction);
|
|
149
|
+
const refs = extractRefs(sourceForExtraction);
|
|
150
|
+
const constantVars = extractConstants(sourceForExtraction);
|
|
151
|
+
const exposeNames = extractExpose(source);
|
|
152
|
+
|
|
153
|
+
// 9. Extract props (array form — after type strip, if generic didn't find any)
|
|
154
|
+
const propsFromArray = propsFromGeneric.length > 0 ? [] : extractPropsArray(source);
|
|
155
|
+
let propNames = propsFromGeneric.length > 0 ? propsFromGeneric : propsFromArray;
|
|
156
|
+
|
|
157
|
+
// 10. Extract props defaults
|
|
158
|
+
const propsDefaults = extractPropsDefaults(source);
|
|
159
|
+
if (propNames.length === 0 && Object.keys(propsDefaults).length > 0) {
|
|
160
|
+
propNames = Object.keys(propsDefaults);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// 11. Extract propsObjectName
|
|
164
|
+
const propsObjectName = propsObjectNameFromGeneric ?? extractPropsObjectName(source);
|
|
165
|
+
|
|
166
|
+
// 12. Validate props
|
|
167
|
+
validateDuplicateProps(propNames, filePath);
|
|
168
|
+
const signalNameSet = new Set(signals.map(s => s.name));
|
|
169
|
+
const computedNameSet = new Set(computeds.map(c => c.name));
|
|
170
|
+
const constantNameSet = new Set(constantVars.map(v => v.name));
|
|
171
|
+
validatePropsConflicts(propsObjectName, signalNameSet, computedNameSet, constantNameSet, filePath);
|
|
172
|
+
|
|
173
|
+
/** @type {import('./types.js').PropDef[]} */
|
|
174
|
+
const propDefs = propNames.map(name => ({
|
|
175
|
+
name,
|
|
176
|
+
default: propsDefaults[name] ?? 'undefined',
|
|
177
|
+
attrName: camelToKebab(name),
|
|
178
|
+
}));
|
|
179
|
+
|
|
180
|
+
// 13. Extract emits
|
|
181
|
+
const emitsFromArray = emitsFromCallSignatures.length > 0 ? [] : extractEmits(source);
|
|
182
|
+
const emitNames = emitsFromCallSignatures.length > 0 ? emitsFromCallSignatures : emitsFromArray;
|
|
183
|
+
const emitsObjectName = emitsObjectNameFromGeneric ?? extractEmitsObjectName(source);
|
|
184
|
+
|
|
185
|
+
// 14. Validate emits
|
|
186
|
+
validateDuplicateEmits(emitNames, filePath);
|
|
187
|
+
const propNameSet = new Set(propNames);
|
|
188
|
+
validateEmitsConflicts(emitsObjectName, signalNameSet, computedNameSet, constantNameSet, propNameSet, propsObjectName, filePath);
|
|
189
|
+
validateUndeclaredEmits(source, emitsObjectName, emitNames, filePath);
|
|
190
|
+
|
|
191
|
+
// 15. Build initial ParseResult
|
|
192
|
+
/** @type {import('./types.js').ParseResult} */
|
|
193
|
+
const parseResult = {
|
|
194
|
+
tagName,
|
|
195
|
+
className,
|
|
196
|
+
template,
|
|
197
|
+
style,
|
|
198
|
+
signals,
|
|
199
|
+
computeds,
|
|
200
|
+
effects,
|
|
201
|
+
constantVars,
|
|
202
|
+
watchers,
|
|
203
|
+
methods,
|
|
204
|
+
propDefs,
|
|
205
|
+
propsObjectName: propsObjectName ?? null,
|
|
206
|
+
emits: emitNames,
|
|
207
|
+
emitsObjectName: emitsObjectName ?? null,
|
|
208
|
+
bindings: [],
|
|
209
|
+
events: [],
|
|
210
|
+
processedTemplate: null,
|
|
211
|
+
ifBlocks: [],
|
|
212
|
+
showBindings: [],
|
|
213
|
+
forBlocks: [],
|
|
214
|
+
onMountHooks,
|
|
215
|
+
onDestroyHooks,
|
|
216
|
+
modelBindings: [],
|
|
217
|
+
attrBindings: [],
|
|
218
|
+
slots: [],
|
|
219
|
+
refs,
|
|
220
|
+
refBindings: [],
|
|
221
|
+
childComponents: [],
|
|
222
|
+
childImports: [],
|
|
223
|
+
exposeNames,
|
|
224
|
+
};
|
|
225
|
+
|
|
226
|
+
// 16. Process template through jsdom → tree-walker → codegen (same as compile())
|
|
227
|
+
const dom = new JSDOM(`<div id="__root">${template}</div>`);
|
|
228
|
+
const rootEl = dom.window.document.getElementById('__root');
|
|
229
|
+
|
|
230
|
+
const signalNames = new Set(signals.map(s => s.name));
|
|
231
|
+
const computedNames = new Set(computeds.map(c => c.name));
|
|
232
|
+
const propNamesSet = new Set(propDefs.map(p => p.name));
|
|
233
|
+
|
|
234
|
+
const forBlocks = processForBlocks(rootEl, [], signalNames, computedNames, propNamesSet);
|
|
235
|
+
const ifBlocks = processIfChains(rootEl, [], signalNames, computedNames, propNamesSet);
|
|
99
236
|
|
|
100
|
-
// 6. Normalize DOM after all directive processing to merge adjacent text nodes
|
|
101
237
|
rootEl.normalize();
|
|
102
238
|
|
|
103
|
-
// 7. Recompute anchor paths after normalization since text node merging
|
|
104
|
-
// may have changed childNode indices
|
|
105
239
|
for (const fb of forBlocks) {
|
|
106
240
|
fb.anchorPath = recomputeAnchorPath(rootEl, fb._anchorNode);
|
|
107
241
|
}
|
|
@@ -109,16 +243,11 @@ export async function compile(filePath, config) {
|
|
|
109
243
|
ib.anchorPath = recomputeAnchorPath(rootEl, ib._anchorNode);
|
|
110
244
|
}
|
|
111
245
|
|
|
112
|
-
|
|
113
|
-
const { bindings, events, showBindings, modelBindings, attrBindings, slots, childComponents } = walkTree(rootEl, signalNames, computedNames, propNames);
|
|
246
|
+
const { bindings, events, showBindings, modelBindings, attrBindings, slots, childComponents } = walkTree(rootEl, signalNames, computedNames, propNamesSet);
|
|
114
247
|
|
|
115
|
-
// 9. Detect refs (after walkTree — ref attributes are compile-time directives)
|
|
116
248
|
const refBindings = detectRefs(rootEl);
|
|
117
249
|
|
|
118
|
-
//
|
|
119
|
-
const refs = parseResult.refs || [];
|
|
120
|
-
|
|
121
|
-
// REF_NOT_FOUND: templateRef('name') with no matching ref="name" in template
|
|
250
|
+
// 17. Validate refs
|
|
122
251
|
for (const decl of refs) {
|
|
123
252
|
if (!refBindings.find(b => b.refName === decl.refName)) {
|
|
124
253
|
const error = new Error(`templateRef('${decl.refName}') has no matching ref="${decl.refName}" in template`);
|
|
@@ -127,18 +256,16 @@ export async function compile(filePath, config) {
|
|
|
127
256
|
throw error;
|
|
128
257
|
}
|
|
129
258
|
}
|
|
130
|
-
|
|
131
|
-
// Unused ref warning: ref="name" in template with no matching templateRef('name') in script
|
|
132
259
|
for (const binding of refBindings) {
|
|
133
260
|
if (!refs.find(d => d.refName === binding.refName)) {
|
|
134
261
|
console.warn(`Warning: ref="${binding.refName}" in template has no matching templateRef('${binding.refName}') in script`);
|
|
135
262
|
}
|
|
136
263
|
}
|
|
137
264
|
|
|
138
|
-
//
|
|
139
|
-
const
|
|
265
|
+
// 17b. Validate model bindings
|
|
266
|
+
const constantNamesForModel = new Set(constantVars.map(v => v.name));
|
|
140
267
|
for (const mb of modelBindings) {
|
|
141
|
-
if (
|
|
268
|
+
if (propNamesSet.has(mb.signal)) {
|
|
142
269
|
const error = new Error(`model cannot bind to prop '${mb.signal}' (read-only)`);
|
|
143
270
|
/** @ts-expect-error — custom error code */
|
|
144
271
|
error.code = 'MODEL_READONLY';
|
|
@@ -150,7 +277,7 @@ export async function compile(filePath, config) {
|
|
|
150
277
|
error.code = 'MODEL_READONLY';
|
|
151
278
|
throw error;
|
|
152
279
|
}
|
|
153
|
-
if (
|
|
280
|
+
if (constantNamesForModel.has(mb.signal)) {
|
|
154
281
|
const error = new Error(`model cannot bind to constant '${mb.signal}' (read-only)`);
|
|
155
282
|
/** @ts-expect-error — custom error code */
|
|
156
283
|
error.code = 'MODEL_READONLY';
|
|
@@ -164,7 +291,7 @@ export async function compile(filePath, config) {
|
|
|
164
291
|
}
|
|
165
292
|
}
|
|
166
293
|
|
|
167
|
-
//
|
|
294
|
+
// 18. Resolve child component imports
|
|
168
295
|
/** @type {import('./types.js').ChildComponentImport[]} */
|
|
169
296
|
const childImports = [];
|
|
170
297
|
if (childComponents.length > 0) {
|
|
@@ -181,7 +308,7 @@ export async function compile(filePath, config) {
|
|
|
181
308
|
}
|
|
182
309
|
}
|
|
183
310
|
|
|
184
|
-
//
|
|
311
|
+
// 19. Merge tree-walker results into ParseResult
|
|
185
312
|
parseResult.bindings = bindings;
|
|
186
313
|
parseResult.events = events;
|
|
187
314
|
parseResult.showBindings = showBindings;
|
|
@@ -193,9 +320,19 @@ export async function compile(filePath, config) {
|
|
|
193
320
|
parseResult.refBindings = refBindings;
|
|
194
321
|
parseResult.childComponents = childComponents;
|
|
195
322
|
parseResult.childImports = childImports;
|
|
196
|
-
// Recompute processedTemplate after all directive replacements (including ref removal)
|
|
197
323
|
parseResult.processedTemplate = rootEl.innerHTML;
|
|
198
324
|
|
|
199
|
-
//
|
|
325
|
+
// 20. Generate component
|
|
200
326
|
return generateComponent(parseResult);
|
|
201
327
|
}
|
|
328
|
+
|
|
329
|
+
/**
|
|
330
|
+
* Compile a single .wcc SFC file into a self-contained JS component.
|
|
331
|
+
*
|
|
332
|
+
* @param {string} filePath — Absolute or relative path to the .wcc file
|
|
333
|
+
* @param {object} [config] — Optional config (reserved for future options)
|
|
334
|
+
* @returns {Promise<string>} The generated JavaScript component code
|
|
335
|
+
*/
|
|
336
|
+
export async function compile(filePath, config) {
|
|
337
|
+
return compileSFC(filePath, config);
|
|
338
|
+
}
|
package/lib/dev-server.js
CHANGED
|
@@ -36,8 +36,25 @@ const MIME_TYPES = {
|
|
|
36
36
|
const SSE_SNIPPET = `<script>
|
|
37
37
|
(function() {
|
|
38
38
|
var es = new EventSource('/__sse');
|
|
39
|
+
var overlay = null;
|
|
40
|
+
function showError(msg) {
|
|
41
|
+
hideError();
|
|
42
|
+
overlay = document.createElement('div');
|
|
43
|
+
overlay.id = '__wcc_error_overlay';
|
|
44
|
+
overlay.style.cssText = 'position:fixed;inset:0;z-index:99999;background:rgba(0,0,0,0.85);color:#fff;font-family:monospace;font-size:14px;padding:32px;overflow:auto;display:flex;align-items:flex-start;justify-content:center;';
|
|
45
|
+
var box = document.createElement('div');
|
|
46
|
+
box.style.cssText = 'background:#1e1e1e;border:2px solid #f44;border-radius:8px;padding:24px;max-width:700px;width:100%;white-space:pre-wrap;word-break:break-word;';
|
|
47
|
+
box.innerHTML = '<div style="color:#f44;font-size:16px;font-weight:bold;margin-bottom:12px;">\\u274C Compilation Error</div>' + msg.replace(/</g,'<').replace(/>/g,'>');
|
|
48
|
+
overlay.appendChild(box);
|
|
49
|
+
overlay.addEventListener('click', hideError);
|
|
50
|
+
document.body.appendChild(overlay);
|
|
51
|
+
}
|
|
52
|
+
function hideError() {
|
|
53
|
+
if (overlay) { overlay.remove(); overlay = null; }
|
|
54
|
+
}
|
|
39
55
|
es.onmessage = function(e) {
|
|
40
|
-
if (e.data === 'reload') location.reload();
|
|
56
|
+
if (e.data === 'reload') { hideError(); location.reload(); }
|
|
57
|
+
else if (e.data.startsWith('error:')) { showError(e.data.slice(6).replace(/\\\\n/g,'\\n')); }
|
|
41
58
|
};
|
|
42
59
|
es.onerror = function() {
|
|
43
60
|
es.close();
|
|
@@ -70,6 +87,17 @@ export function startDevServer({ port, root, outputDir }) {
|
|
|
70
87
|
}
|
|
71
88
|
}
|
|
72
89
|
|
|
90
|
+
/** Send an error event to all connected SSE clients */
|
|
91
|
+
function notifyError(message) {
|
|
92
|
+
for (const res of sseClients) {
|
|
93
|
+
try {
|
|
94
|
+
res.write(`data: error:${message.replace(/\n/g, '\\n')}\n\n`);
|
|
95
|
+
} catch {
|
|
96
|
+
sseClients.delete(res);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
73
101
|
const server = createServer((req, res) => {
|
|
74
102
|
const url = req.url.split('?')[0];
|
|
75
103
|
|
|
@@ -151,6 +179,7 @@ export function startDevServer({ port, root, outputDir }) {
|
|
|
151
179
|
|
|
152
180
|
return {
|
|
153
181
|
server,
|
|
182
|
+
notifyError,
|
|
154
183
|
close() {
|
|
155
184
|
// Close all SSE connections
|
|
156
185
|
for (const res of sseClients) {
|
package/lib/parser-extractors.js
CHANGED
|
@@ -244,25 +244,15 @@ export function extractPropsObjectName(source) {
|
|
|
244
244
|
// ── Props validation ────────────────────────────────────────────────
|
|
245
245
|
|
|
246
246
|
/**
|
|
247
|
-
* Validate that defineProps is assigned to a variable.
|
|
248
|
-
*
|
|
247
|
+
* Validate that defineProps is assigned to a variable (if props are accessed via object).
|
|
248
|
+
* No longer throws — bare defineProps() calls are valid when props are only used in template.
|
|
249
249
|
*
|
|
250
|
-
* @param {string}
|
|
251
|
-
* @param {string}
|
|
250
|
+
* @param {string} _source
|
|
251
|
+
* @param {string} _fileName
|
|
252
252
|
*/
|
|
253
|
-
export function validatePropsAssignment(
|
|
254
|
-
//
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
// Check if it's assigned to a variable
|
|
258
|
-
if (extractPropsObjectName(source) !== null) return;
|
|
259
|
-
|
|
260
|
-
const error = new Error(
|
|
261
|
-
`Error en '${fileName}': defineProps() debe asignarse a una variable (const props = defineProps(...))`
|
|
262
|
-
);
|
|
263
|
-
/** @ts-expect-error — custom error code for programmatic handling */
|
|
264
|
-
error.code = 'PROPS_ASSIGNMENT_REQUIRED';
|
|
265
|
-
throw error;
|
|
253
|
+
export function validatePropsAssignment(_source, _fileName) {
|
|
254
|
+
// No-op: bare defineProps() is valid in .wcc SFC format
|
|
255
|
+
// Props are accessible in the template without needing a variable reference
|
|
266
256
|
}
|
|
267
257
|
|
|
268
258
|
/**
|
|
@@ -606,7 +596,7 @@ export function extractSignals(source) {
|
|
|
606
596
|
/**
|
|
607
597
|
* Known macro/reactive call patterns that should NOT be treated as constants.
|
|
608
598
|
*/
|
|
609
|
-
export const REACTIVE_CALLS = /\b(?:signal|computed|effect|watch|defineProps|defineEmits|defineComponent|templateRef|
|
|
599
|
+
export const REACTIVE_CALLS = /\b(?:signal|computed|effect|watch|defineProps|defineEmits|defineComponent|templateRef|defineExpose|onMount|onDestroy)\s*[<(]/;
|
|
610
600
|
|
|
611
601
|
/**
|
|
612
602
|
* Extract plain const/let/var declarations that are NOT reactive calls.
|
|
@@ -775,7 +765,9 @@ export function extractEffects(source) {
|
|
|
775
765
|
|
|
776
766
|
/**
|
|
777
767
|
* Extract watch() declarations from source.
|
|
778
|
-
*
|
|
768
|
+
* Supports two forms:
|
|
769
|
+
* Form 1 — Signal direct: watch(count, (newVal, oldVal) => { body })
|
|
770
|
+
* Form 2 — Getter function: watch(() => expr, (newVal, oldVal) => { body })
|
|
779
771
|
* Uses brace depth tracking to capture multi-line bodies.
|
|
780
772
|
*
|
|
781
773
|
* @param {string} source
|
|
@@ -789,9 +781,16 @@ export function extractWatchers(source) {
|
|
|
789
781
|
|
|
790
782
|
while (i < lines.length) {
|
|
791
783
|
const line = lines[i];
|
|
792
|
-
|
|
784
|
+
|
|
785
|
+
// Form 2 — Getter function: watch(() => expr, (newVal, oldVal) => {
|
|
786
|
+
const mGetter = line.match(/\bwatch\s*\(\s*\(\)\s*=>\s*(.+?)\s*,\s*\((\w+)\s*,\s*(\w+)\)\s*=>\s*\{/);
|
|
787
|
+
// Form 1 — Signal direct: watch(identifier, (newVal, oldVal) => {
|
|
788
|
+
const mSignal = !mGetter ? line.match(/\bwatch\s*\(\s*(\w+)\s*,\s*\((\w+)\s*,\s*(\w+)\)\s*=>\s*\{/) : null;
|
|
789
|
+
|
|
790
|
+
const m = mGetter || mSignal;
|
|
793
791
|
|
|
794
792
|
if (m) {
|
|
793
|
+
const kind = mGetter ? 'getter' : 'signal';
|
|
795
794
|
const target = m[1];
|
|
796
795
|
const newParam = m[2];
|
|
797
796
|
const oldParam = m[3];
|
|
@@ -843,7 +842,7 @@ export function extractWatchers(source) {
|
|
|
843
842
|
if (minIndent === Infinity) minIndent = 0;
|
|
844
843
|
const body = bodyLines.map(bl => bl.substring(minIndent)).join('\n').trim();
|
|
845
844
|
|
|
846
|
-
watchers.push({ target, newParam, oldParam, body });
|
|
845
|
+
watchers.push({ kind, target, newParam, oldParam, body });
|
|
847
846
|
}
|
|
848
847
|
i++;
|
|
849
848
|
}
|
|
@@ -1028,3 +1027,26 @@ export function extractRefs(source) {
|
|
|
1028
1027
|
}
|
|
1029
1028
|
return refs;
|
|
1030
1029
|
}
|
|
1030
|
+
|
|
1031
|
+
// ── defineExpose extraction ─────────────────────────────────────────
|
|
1032
|
+
|
|
1033
|
+
/**
|
|
1034
|
+
* Extract property names from defineExpose({ key1, key2, ... }).
|
|
1035
|
+
* Supports shorthand properties: defineExpose({ doubled, handleUpdate })
|
|
1036
|
+
*
|
|
1037
|
+
* @param {string} source — Source code (after type stripping)
|
|
1038
|
+
* @returns {string[]} Array of exposed property names
|
|
1039
|
+
*/
|
|
1040
|
+
export function extractExpose(source) {
|
|
1041
|
+
const m = source.match(/defineExpose\(\s*\{([^}]*)\}\s*\)/);
|
|
1042
|
+
if (!m) return [];
|
|
1043
|
+
|
|
1044
|
+
const body = m[1];
|
|
1045
|
+
const names = [];
|
|
1046
|
+
const re = /\b(\w+)\b/g;
|
|
1047
|
+
let match;
|
|
1048
|
+
while ((match = re.exec(body)) !== null) {
|
|
1049
|
+
names.push(match[1]);
|
|
1050
|
+
}
|
|
1051
|
+
return names;
|
|
1052
|
+
}
|