@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
package/lib/compiler.js
CHANGED
|
@@ -1,107 +1,238 @@
|
|
|
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
|
+
validatePropsAssignment,
|
|
36
|
+
validateDuplicateProps,
|
|
37
|
+
validatePropsConflicts,
|
|
38
|
+
validateEmitsAssignment,
|
|
39
|
+
validateDuplicateEmits,
|
|
40
|
+
validateEmitsConflicts,
|
|
41
|
+
validateUndeclaredEmits,
|
|
42
|
+
} from './parser-extractors.js';
|
|
43
|
+
import { stripTypes } from './parser.js';
|
|
44
|
+
|
|
16
45
|
/**
|
|
17
|
-
* Resolve a child component's
|
|
18
|
-
*
|
|
46
|
+
* Resolve a child component's source file path by tag name.
|
|
47
|
+
*
|
|
48
|
+
* Searches for a file named after the tag in the source directory,
|
|
49
|
+
* trying extensions in priority order: .wcc, .js, .ts
|
|
19
50
|
*
|
|
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-
|
|
51
|
+
* @param {string} tag — The custom element tag name (e.g., 'wcc-child')
|
|
52
|
+
* @param {string} sourceDir — Directory of the parent component
|
|
53
|
+
* @param {object} [config] — Optional config (reserved for future use)
|
|
54
|
+
* @returns {string | null} Relative import path (e.g., './wcc-child.js') or null if not found
|
|
24
55
|
*/
|
|
25
56
|
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
|
|
57
|
+
const extensions = ['.wcc', '.js', '.ts'];
|
|
58
|
+
for (const ext of extensions) {
|
|
59
|
+
const candidate = resolve(sourceDir, `${tag}${ext}`);
|
|
60
|
+
if (existsSync(candidate)) {
|
|
61
|
+
// Return as a relative .js import path (compiled output)
|
|
62
|
+
return `./${tag}.js`;
|
|
66
63
|
}
|
|
67
64
|
}
|
|
68
|
-
|
|
69
65
|
return null;
|
|
70
66
|
}
|
|
71
67
|
|
|
72
68
|
/**
|
|
73
|
-
* Compile a single .
|
|
69
|
+
* Compile a single .wcc SFC file into a self-contained JS component.
|
|
70
|
+
*
|
|
71
|
+
* Reads the file, parses the SFC blocks, extracts reactive declarations
|
|
72
|
+
* from the script block using parser-extractors.js, and processes template
|
|
73
|
+
* and style through the existing pipeline (jsdom → tree-walker → codegen).
|
|
74
74
|
*
|
|
75
|
-
* @param {string} filePath — Absolute or relative path to the
|
|
75
|
+
* @param {string} filePath — Absolute or relative path to the .wcc file
|
|
76
76
|
* @param {object} [config] — Optional config (reserved for future options)
|
|
77
77
|
* @returns {Promise<string>} The generated JavaScript component code
|
|
78
78
|
*/
|
|
79
|
-
|
|
80
|
-
// 1.
|
|
81
|
-
const
|
|
79
|
+
async function compileSFC(filePath, config) {
|
|
80
|
+
// 1. Read and parse the SFC file
|
|
81
|
+
const rawSource = readFileSync(filePath, 'utf-8');
|
|
82
|
+
const fileName = basename(filePath);
|
|
83
|
+
const descriptor = parseSFC(rawSource, fileName);
|
|
82
84
|
|
|
83
|
-
// 2.
|
|
84
|
-
|
|
85
|
-
|
|
85
|
+
// 2. Process script block — mirrors parser.js logic
|
|
86
|
+
let source = stripMacroImport(descriptor.script);
|
|
87
|
+
|
|
88
|
+
// 3. Extract props/emits from generic forms BEFORE type stripping
|
|
89
|
+
const propsFromGeneric = extractPropsGeneric(source);
|
|
90
|
+
const propsObjectNameFromGeneric = extractPropsObjectName(source);
|
|
91
|
+
const emitsFromCallSignatures = extractEmitsFromCallSignatures(source);
|
|
92
|
+
const emitsObjectNameFromGeneric = extractEmitsObjectNameFromGeneric(source);
|
|
86
93
|
|
|
87
|
-
//
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
const propNames = new Set((parseResult.propDefs || []).map(p => p.name));
|
|
94
|
+
// 4. Validate props/emits assignment (before type strip)
|
|
95
|
+
validatePropsAssignment(source, filePath);
|
|
96
|
+
validateEmitsAssignment(source, filePath);
|
|
91
97
|
|
|
92
|
-
//
|
|
93
|
-
|
|
98
|
+
// 5. Strip TypeScript types if lang === 'ts'
|
|
99
|
+
if (descriptor.lang === 'ts') {
|
|
100
|
+
source = await stripTypes(source);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// 6. Extract component metadata
|
|
104
|
+
const tagName = descriptor.tag;
|
|
105
|
+
const className = toClassName(tagName);
|
|
106
|
+
const template = descriptor.template;
|
|
107
|
+
const style = descriptor.style;
|
|
94
108
|
|
|
95
|
-
//
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
109
|
+
// 7. Extract lifecycle hooks (before other extractions)
|
|
110
|
+
const { onMountHooks, onDestroyHooks } = extractLifecycleHooks(source);
|
|
111
|
+
|
|
112
|
+
// 7b. Strip lifecycle/watcher blocks from source for extraction
|
|
113
|
+
let sourceForExtraction = source;
|
|
114
|
+
const hookLinePattern = /\bonMount\s*\(|\bonDestroy\s*\(|\bwatch\s*\(/;
|
|
115
|
+
const sourceLines = sourceForExtraction.split('\n');
|
|
116
|
+
const filteredLines = [];
|
|
117
|
+
let skipDepth = 0;
|
|
118
|
+
let skipping = false;
|
|
119
|
+
for (const line of sourceLines) {
|
|
120
|
+
if (!skipping && hookLinePattern.test(line)) {
|
|
121
|
+
skipping = true;
|
|
122
|
+
skipDepth = 0;
|
|
123
|
+
for (const ch of line) {
|
|
124
|
+
if (ch === '{') skipDepth++;
|
|
125
|
+
if (ch === '}') skipDepth--;
|
|
126
|
+
}
|
|
127
|
+
if (skipDepth <= 0) skipping = false;
|
|
128
|
+
continue;
|
|
129
|
+
}
|
|
130
|
+
if (skipping) {
|
|
131
|
+
for (const ch of line) {
|
|
132
|
+
if (ch === '{') skipDepth++;
|
|
133
|
+
if (ch === '}') skipDepth--;
|
|
134
|
+
}
|
|
135
|
+
if (skipDepth <= 0) skipping = false;
|
|
136
|
+
continue;
|
|
137
|
+
}
|
|
138
|
+
filteredLines.push(line);
|
|
139
|
+
}
|
|
140
|
+
sourceForExtraction = filteredLines.join('\n');
|
|
141
|
+
|
|
142
|
+
// 8. Extract reactive declarations and functions
|
|
143
|
+
const signals = extractSignals(sourceForExtraction);
|
|
144
|
+
const computeds = extractComputeds(sourceForExtraction);
|
|
145
|
+
const effects = extractEffects(sourceForExtraction);
|
|
146
|
+
const watchers = extractWatchers(source);
|
|
147
|
+
const methods = extractFunctions(sourceForExtraction);
|
|
148
|
+
const refs = extractRefs(sourceForExtraction);
|
|
149
|
+
const constantVars = extractConstants(sourceForExtraction);
|
|
150
|
+
|
|
151
|
+
// 9. Extract props (array form — after type strip, if generic didn't find any)
|
|
152
|
+
const propsFromArray = propsFromGeneric.length > 0 ? [] : extractPropsArray(source);
|
|
153
|
+
let propNames = propsFromGeneric.length > 0 ? propsFromGeneric : propsFromArray;
|
|
154
|
+
|
|
155
|
+
// 10. Extract props defaults
|
|
156
|
+
const propsDefaults = extractPropsDefaults(source);
|
|
157
|
+
if (propNames.length === 0 && Object.keys(propsDefaults).length > 0) {
|
|
158
|
+
propNames = Object.keys(propsDefaults);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// 11. Extract propsObjectName
|
|
162
|
+
const propsObjectName = propsObjectNameFromGeneric ?? extractPropsObjectName(source);
|
|
163
|
+
|
|
164
|
+
// 12. Validate props
|
|
165
|
+
validateDuplicateProps(propNames, filePath);
|
|
166
|
+
const signalNameSet = new Set(signals.map(s => s.name));
|
|
167
|
+
const computedNameSet = new Set(computeds.map(c => c.name));
|
|
168
|
+
const constantNameSet = new Set(constantVars.map(v => v.name));
|
|
169
|
+
validatePropsConflicts(propsObjectName, signalNameSet, computedNameSet, constantNameSet, filePath);
|
|
170
|
+
|
|
171
|
+
/** @type {import('./types.js').PropDef[]} */
|
|
172
|
+
const propDefs = propNames.map(name => ({
|
|
173
|
+
name,
|
|
174
|
+
default: propsDefaults[name] ?? 'undefined',
|
|
175
|
+
attrName: camelToKebab(name),
|
|
176
|
+
}));
|
|
177
|
+
|
|
178
|
+
// 13. Extract emits
|
|
179
|
+
const emitsFromArray = emitsFromCallSignatures.length > 0 ? [] : extractEmits(source);
|
|
180
|
+
const emitNames = emitsFromCallSignatures.length > 0 ? emitsFromCallSignatures : emitsFromArray;
|
|
181
|
+
const emitsObjectName = emitsObjectNameFromGeneric ?? extractEmitsObjectName(source);
|
|
182
|
+
|
|
183
|
+
// 14. Validate emits
|
|
184
|
+
validateDuplicateEmits(emitNames, filePath);
|
|
185
|
+
const propNameSet = new Set(propNames);
|
|
186
|
+
validateEmitsConflicts(emitsObjectName, signalNameSet, computedNameSet, constantNameSet, propNameSet, propsObjectName, filePath);
|
|
187
|
+
validateUndeclaredEmits(source, emitsObjectName, emitNames, filePath);
|
|
188
|
+
|
|
189
|
+
// 15. Build initial ParseResult
|
|
190
|
+
/** @type {import('./types.js').ParseResult} */
|
|
191
|
+
const parseResult = {
|
|
192
|
+
tagName,
|
|
193
|
+
className,
|
|
194
|
+
template,
|
|
195
|
+
style,
|
|
196
|
+
signals,
|
|
197
|
+
computeds,
|
|
198
|
+
effects,
|
|
199
|
+
constantVars,
|
|
200
|
+
watchers,
|
|
201
|
+
methods,
|
|
202
|
+
propDefs,
|
|
203
|
+
propsObjectName: propsObjectName ?? null,
|
|
204
|
+
emits: emitNames,
|
|
205
|
+
emitsObjectName: emitsObjectName ?? null,
|
|
206
|
+
bindings: [],
|
|
207
|
+
events: [],
|
|
208
|
+
processedTemplate: null,
|
|
209
|
+
ifBlocks: [],
|
|
210
|
+
showBindings: [],
|
|
211
|
+
forBlocks: [],
|
|
212
|
+
onMountHooks,
|
|
213
|
+
onDestroyHooks,
|
|
214
|
+
modelBindings: [],
|
|
215
|
+
attrBindings: [],
|
|
216
|
+
slots: [],
|
|
217
|
+
refs,
|
|
218
|
+
refBindings: [],
|
|
219
|
+
childComponents: [],
|
|
220
|
+
childImports: [],
|
|
221
|
+
};
|
|
222
|
+
|
|
223
|
+
// 16. Process template through jsdom → tree-walker → codegen (same as compile())
|
|
224
|
+
const dom = new JSDOM(`<div id="__root">${template}</div>`);
|
|
225
|
+
const rootEl = dom.window.document.getElementById('__root');
|
|
226
|
+
|
|
227
|
+
const signalNames = new Set(signals.map(s => s.name));
|
|
228
|
+
const computedNames = new Set(computeds.map(c => c.name));
|
|
229
|
+
const propNamesSet = new Set(propDefs.map(p => p.name));
|
|
230
|
+
|
|
231
|
+
const forBlocks = processForBlocks(rootEl, [], signalNames, computedNames, propNamesSet);
|
|
232
|
+
const ifBlocks = processIfChains(rootEl, [], signalNames, computedNames, propNamesSet);
|
|
99
233
|
|
|
100
|
-
// 6. Normalize DOM after all directive processing to merge adjacent text nodes
|
|
101
234
|
rootEl.normalize();
|
|
102
235
|
|
|
103
|
-
// 7. Recompute anchor paths after normalization since text node merging
|
|
104
|
-
// may have changed childNode indices
|
|
105
236
|
for (const fb of forBlocks) {
|
|
106
237
|
fb.anchorPath = recomputeAnchorPath(rootEl, fb._anchorNode);
|
|
107
238
|
}
|
|
@@ -109,16 +240,11 @@ export async function compile(filePath, config) {
|
|
|
109
240
|
ib.anchorPath = recomputeAnchorPath(rootEl, ib._anchorNode);
|
|
110
241
|
}
|
|
111
242
|
|
|
112
|
-
|
|
113
|
-
const { bindings, events, showBindings, modelBindings, attrBindings, slots, childComponents } = walkTree(rootEl, signalNames, computedNames, propNames);
|
|
243
|
+
const { bindings, events, showBindings, modelBindings, attrBindings, slots, childComponents } = walkTree(rootEl, signalNames, computedNames, propNamesSet);
|
|
114
244
|
|
|
115
|
-
// 9. Detect refs (after walkTree — ref attributes are compile-time directives)
|
|
116
245
|
const refBindings = detectRefs(rootEl);
|
|
117
246
|
|
|
118
|
-
//
|
|
119
|
-
const refs = parseResult.refs || [];
|
|
120
|
-
|
|
121
|
-
// REF_NOT_FOUND: templateRef('name') with no matching ref="name" in template
|
|
247
|
+
// 17. Validate refs
|
|
122
248
|
for (const decl of refs) {
|
|
123
249
|
if (!refBindings.find(b => b.refName === decl.refName)) {
|
|
124
250
|
const error = new Error(`templateRef('${decl.refName}') has no matching ref="${decl.refName}" in template`);
|
|
@@ -127,18 +253,16 @@ export async function compile(filePath, config) {
|
|
|
127
253
|
throw error;
|
|
128
254
|
}
|
|
129
255
|
}
|
|
130
|
-
|
|
131
|
-
// Unused ref warning: ref="name" in template with no matching templateRef('name') in script
|
|
132
256
|
for (const binding of refBindings) {
|
|
133
257
|
if (!refs.find(d => d.refName === binding.refName)) {
|
|
134
258
|
console.warn(`Warning: ref="${binding.refName}" in template has no matching templateRef('${binding.refName}') in script`);
|
|
135
259
|
}
|
|
136
260
|
}
|
|
137
261
|
|
|
138
|
-
//
|
|
139
|
-
const
|
|
262
|
+
// 17b. Validate model bindings
|
|
263
|
+
const constantNamesForModel = new Set(constantVars.map(v => v.name));
|
|
140
264
|
for (const mb of modelBindings) {
|
|
141
|
-
if (
|
|
265
|
+
if (propNamesSet.has(mb.signal)) {
|
|
142
266
|
const error = new Error(`model cannot bind to prop '${mb.signal}' (read-only)`);
|
|
143
267
|
/** @ts-expect-error — custom error code */
|
|
144
268
|
error.code = 'MODEL_READONLY';
|
|
@@ -150,7 +274,7 @@ export async function compile(filePath, config) {
|
|
|
150
274
|
error.code = 'MODEL_READONLY';
|
|
151
275
|
throw error;
|
|
152
276
|
}
|
|
153
|
-
if (
|
|
277
|
+
if (constantNamesForModel.has(mb.signal)) {
|
|
154
278
|
const error = new Error(`model cannot bind to constant '${mb.signal}' (read-only)`);
|
|
155
279
|
/** @ts-expect-error — custom error code */
|
|
156
280
|
error.code = 'MODEL_READONLY';
|
|
@@ -164,7 +288,7 @@ export async function compile(filePath, config) {
|
|
|
164
288
|
}
|
|
165
289
|
}
|
|
166
290
|
|
|
167
|
-
//
|
|
291
|
+
// 18. Resolve child component imports
|
|
168
292
|
/** @type {import('./types.js').ChildComponentImport[]} */
|
|
169
293
|
const childImports = [];
|
|
170
294
|
if (childComponents.length > 0) {
|
|
@@ -181,7 +305,7 @@ export async function compile(filePath, config) {
|
|
|
181
305
|
}
|
|
182
306
|
}
|
|
183
307
|
|
|
184
|
-
//
|
|
308
|
+
// 19. Merge tree-walker results into ParseResult
|
|
185
309
|
parseResult.bindings = bindings;
|
|
186
310
|
parseResult.events = events;
|
|
187
311
|
parseResult.showBindings = showBindings;
|
|
@@ -193,9 +317,19 @@ export async function compile(filePath, config) {
|
|
|
193
317
|
parseResult.refBindings = refBindings;
|
|
194
318
|
parseResult.childComponents = childComponents;
|
|
195
319
|
parseResult.childImports = childImports;
|
|
196
|
-
// Recompute processedTemplate after all directive replacements (including ref removal)
|
|
197
320
|
parseResult.processedTemplate = rootEl.innerHTML;
|
|
198
321
|
|
|
199
|
-
//
|
|
322
|
+
// 20. Generate component
|
|
200
323
|
return generateComponent(parseResult);
|
|
201
324
|
}
|
|
325
|
+
|
|
326
|
+
/**
|
|
327
|
+
* Compile a single .wcc SFC file into a self-contained JS component.
|
|
328
|
+
*
|
|
329
|
+
* @param {string} filePath — Absolute or relative path to the .wcc file
|
|
330
|
+
* @param {object} [config] — Optional config (reserved for future options)
|
|
331
|
+
* @returns {Promise<string>} The generated JavaScript component code
|
|
332
|
+
*/
|
|
333
|
+
export async function compile(filePath, config) {
|
|
334
|
+
return compileSFC(filePath, config);
|
|
335
|
+
}
|
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
|
}
|