@sprlab/wccompiler 0.13.0 → 0.14.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 +998 -998
- package/adapters/angular-compiled/angular.d.ts +197 -197
- package/adapters/angular-compiled/angular.mjs +488 -488
- package/adapters/angular.js +54 -54
- package/adapters/angular.ts +630 -630
- package/adapters/react.js +114 -114
- package/adapters/vue.js +103 -103
- package/bin/wcc.js +412 -412
- package/bin/wcc.test.js +126 -126
- package/integrations/angular.js +73 -73
- package/integrations/react.js +859 -859
- package/integrations/vue.js +253 -253
- package/lib/codegen.js +2074 -2074
- package/lib/compiler-browser.js +545 -545
- package/lib/compiler.js +483 -479
- package/lib/config.js +71 -71
- package/lib/css-scoper.js +180 -180
- package/lib/dev-server.js +193 -193
- package/lib/import-resolver.js +160 -160
- package/lib/parser-extractors.js +1240 -1169
- package/lib/parser.js +273 -269
- package/lib/reactive-runtime.js +143 -143
- package/lib/sfc-parser.js +333 -333
- package/lib/template-normalizer.js +114 -114
- package/lib/tree-walker.js +1013 -1013
- package/lib/types.js +262 -262
- package/lib/wcc-runtime.js +68 -68
- package/package.json +85 -85
- package/types/wcc.d.ts +28 -28
- package/types/wcc.test.js +46 -46
package/lib/parser-extractors.js
CHANGED
|
@@ -1,1169 +1,1240 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Pure extraction functions for parsing .ts/.js component source files.
|
|
3
|
-
*
|
|
4
|
-
* These functions have NO Node.js-specific imports (no fs, path, or esbuild)
|
|
5
|
-
* and can be used in both Node.js and browser environments.
|
|
6
|
-
*
|
|
7
|
-
* Extracts:
|
|
8
|
-
* - defineComponent({ tag, template, styles }) metadata
|
|
9
|
-
* - signal() declarations
|
|
10
|
-
* - computed() declarations
|
|
11
|
-
* - effect() declarations
|
|
12
|
-
* - Top-level function declarations
|
|
13
|
-
* - Props and emits definitions
|
|
14
|
-
* - Lifecycle hooks
|
|
15
|
-
* - Template refs
|
|
16
|
-
* - Constants
|
|
17
|
-
*/
|
|
18
|
-
|
|
19
|
-
/** @import { ReactiveVar, ComputedDef, EffectDef, MethodDef, PropDef, LifecycleHook, RefDeclaration } from './types.js' */
|
|
20
|
-
|
|
21
|
-
// ── Macro import stripping ───────────────────────────────────────────
|
|
22
|
-
|
|
23
|
-
/**
|
|
24
|
-
* Remove `import { ... } from 'wcc'` and `import { ... } from '@sprlab/wccompiler'`
|
|
25
|
-
* statements from source content. These imports are purely cosmetic (for IDE DX)
|
|
26
|
-
* and must be stripped before any further processing.
|
|
27
|
-
*
|
|
28
|
-
* @param {string} source - Raw source content
|
|
29
|
-
* @returns {string} Source with macro imports removed
|
|
30
|
-
*/
|
|
31
|
-
export function stripMacroImport(source) {
|
|
32
|
-
return source.replace(
|
|
33
|
-
/import\s*\{[^}]*\}\s*from\s*['"](?:wcc|@sprlab\/wccompiler)['"]\s*;?/g,
|
|
34
|
-
''
|
|
35
|
-
);
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
// ── Name conversion ─────────────────────────────────────────────────
|
|
39
|
-
|
|
40
|
-
/**
|
|
41
|
-
* Convert a kebab-case tag name to PascalCase class name.
|
|
42
|
-
* e.g. "wcc-counter" → "WccCounter"
|
|
43
|
-
*
|
|
44
|
-
* @param {string} tagName
|
|
45
|
-
* @returns {string}
|
|
46
|
-
*/
|
|
47
|
-
export function toClassName(tagName) {
|
|
48
|
-
return tagName
|
|
49
|
-
.split('-')
|
|
50
|
-
.map(part => part.charAt(0).toUpperCase() + part.slice(1))
|
|
51
|
-
.join('');
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
// ── camelCase to kebab-case ─────────────────────────────────────────
|
|
55
|
-
|
|
56
|
-
/**
|
|
57
|
-
* Convert a camelCase identifier to kebab-case for HTML attribute names.
|
|
58
|
-
* e.g. 'itemCount' → 'item-count', 'label' → 'label'
|
|
59
|
-
*
|
|
60
|
-
* @param {string} name
|
|
61
|
-
* @returns {string}
|
|
62
|
-
*/
|
|
63
|
-
export function camelToKebab(name) {
|
|
64
|
-
return name.replace(/([a-z0-9])([A-Z])/g, '$1-$2').toLowerCase();
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
// ── Props extraction (generic form — BEFORE type strip) ─────────────
|
|
68
|
-
|
|
69
|
-
/**
|
|
70
|
-
* Extract prop names from the TypeScript generic form:
|
|
71
|
-
* defineProps<{ label: string, count: number }>({...})
|
|
72
|
-
* or defineProps<{ label: string }>()
|
|
73
|
-
*
|
|
74
|
-
* Must be called BEFORE stripTypes() since esbuild removes generics.
|
|
75
|
-
*
|
|
76
|
-
* @param {string} source
|
|
77
|
-
* @returns {string[]}
|
|
78
|
-
*/
|
|
79
|
-
export function extractPropsGeneric(source) {
|
|
80
|
-
const m = source.match(/defineProps\s*<\s*\{([^}]*)\}\s*>/);
|
|
81
|
-
if (!m) return [];
|
|
82
|
-
|
|
83
|
-
const body = m[1];
|
|
84
|
-
const props = [];
|
|
85
|
-
const re = /(\w+)\s*[?]?\s*:/g;
|
|
86
|
-
let match;
|
|
87
|
-
while ((match = re.exec(body)) !== null) {
|
|
88
|
-
props.push(match[1]);
|
|
89
|
-
}
|
|
90
|
-
return props;
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
// ── Props extraction (array form — AFTER type strip) ────────────────
|
|
94
|
-
|
|
95
|
-
/**
|
|
96
|
-
* Extract prop names from the array form:
|
|
97
|
-
* defineProps(['label', 'count'])
|
|
98
|
-
*
|
|
99
|
-
* Called AFTER type stripping.
|
|
100
|
-
*
|
|
101
|
-
* @param {string} source
|
|
102
|
-
* @returns {string[]}
|
|
103
|
-
*/
|
|
104
|
-
export function extractPropsArray(source) {
|
|
105
|
-
const m = source.match(/defineProps\(\s*\[([^\]]*)\]\s*\)/);
|
|
106
|
-
if (!m) return [];
|
|
107
|
-
|
|
108
|
-
const body = m[1];
|
|
109
|
-
const props = [];
|
|
110
|
-
const re = /['"]([^'"]+)['"]/g;
|
|
111
|
-
let match;
|
|
112
|
-
while ((match = re.exec(body)) !== null) {
|
|
113
|
-
props.push(match[1]);
|
|
114
|
-
}
|
|
115
|
-
return props;
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
// ── Props defaults extraction (AFTER type strip) ────────────────────
|
|
119
|
-
|
|
120
|
-
/**
|
|
121
|
-
* Extract default values from the defineProps argument object.
|
|
122
|
-
* After type stripping, the generic form becomes defineProps({...}).
|
|
123
|
-
* The array form is defineProps([...]) — no defaults.
|
|
124
|
-
*
|
|
125
|
-
* Uses parenthesis depth counting to handle nested objects/arrays.
|
|
126
|
-
*
|
|
127
|
-
* @param {string} source
|
|
128
|
-
* @returns {Record<string, string>}
|
|
129
|
-
*/
|
|
130
|
-
export function extractPropsDefaults(source) {
|
|
131
|
-
const idx = source.indexOf('defineProps(');
|
|
132
|
-
if (idx === -1) return {};
|
|
133
|
-
|
|
134
|
-
const start = idx + 'defineProps('.length;
|
|
135
|
-
// Check what the argument starts with (skip whitespace)
|
|
136
|
-
let argStart = start;
|
|
137
|
-
while (argStart < source.length && /\s/.test(source[argStart])) argStart++;
|
|
138
|
-
|
|
139
|
-
// If it starts with '[', it's the array form — no defaults
|
|
140
|
-
if (source[argStart] === '[') return {};
|
|
141
|
-
|
|
142
|
-
// If it doesn't start with '{', no defaults (e.g., empty call)
|
|
143
|
-
if (source[argStart] !== '{') return {};
|
|
144
|
-
|
|
145
|
-
// Use depth counting to extract the full object literal
|
|
146
|
-
let depth = 0;
|
|
147
|
-
let i = argStart;
|
|
148
|
-
/** @type {string | null} */
|
|
149
|
-
let inString = null;
|
|
150
|
-
|
|
151
|
-
for (; i < source.length; i++) {
|
|
152
|
-
const ch = source[i];
|
|
153
|
-
|
|
154
|
-
if (inString) {
|
|
155
|
-
if (ch === '\\') { i++; continue; }
|
|
156
|
-
if (ch === inString) inString = null;
|
|
157
|
-
continue;
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
if (ch === '"' || ch === "'" || ch === '`') {
|
|
161
|
-
inString = ch;
|
|
162
|
-
continue;
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
if (ch === '{') depth++;
|
|
166
|
-
if (ch === '}') {
|
|
167
|
-
depth--;
|
|
168
|
-
if (depth === 0) { i++; break; }
|
|
169
|
-
}
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
const objLiteral = source.slice(argStart, i).trim();
|
|
173
|
-
// Remove outer braces
|
|
174
|
-
const inner = objLiteral.slice(1, -1).trim();
|
|
175
|
-
if (!inner) return {};
|
|
176
|
-
|
|
177
|
-
// Parse key: value pairs using depth counting
|
|
178
|
-
/** @type {Record<string, string>} */
|
|
179
|
-
const defaults = {};
|
|
180
|
-
let pos = 0;
|
|
181
|
-
while (pos < inner.length) {
|
|
182
|
-
// Skip whitespace
|
|
183
|
-
while (pos < inner.length && /\s/.test(inner[pos])) pos++;
|
|
184
|
-
if (pos >= inner.length) break;
|
|
185
|
-
|
|
186
|
-
// Extract key
|
|
187
|
-
const keyMatch = inner.slice(pos).match(/^(\w+)\s*:\s*/);
|
|
188
|
-
if (!keyMatch) break;
|
|
189
|
-
const key = keyMatch[1];
|
|
190
|
-
pos += keyMatch[0].length;
|
|
191
|
-
|
|
192
|
-
// Extract value using depth counting
|
|
193
|
-
let valDepth = 0;
|
|
194
|
-
let valStart = pos;
|
|
195
|
-
/** @type {string | null} */
|
|
196
|
-
let valInString = null;
|
|
197
|
-
|
|
198
|
-
for (; pos < inner.length; pos++) {
|
|
199
|
-
const ch = inner[pos];
|
|
200
|
-
|
|
201
|
-
if (valInString) {
|
|
202
|
-
if (ch === '\\') { pos++; continue; }
|
|
203
|
-
if (ch === valInString) valInString = null;
|
|
204
|
-
continue;
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
if (ch === '"' || ch === "'" || ch === '`') {
|
|
208
|
-
valInString = ch;
|
|
209
|
-
continue;
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
if (ch === '(' || ch === '[' || ch === '{') valDepth++;
|
|
213
|
-
if (ch === ')' || ch === ']' || ch === '}') valDepth--;
|
|
214
|
-
|
|
215
|
-
if (valDepth === 0 && ch === ',') {
|
|
216
|
-
break;
|
|
217
|
-
}
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
const value = inner.slice(valStart, pos).trim();
|
|
221
|
-
defaults[key] = value;
|
|
222
|
-
|
|
223
|
-
// Skip comma
|
|
224
|
-
if (pos < inner.length && inner[pos] === ',') pos++;
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
return defaults;
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
// ── Props object name extraction ────────────────────────────────────
|
|
231
|
-
|
|
232
|
-
/**
|
|
233
|
-
* Extract the variable name from a props object binding.
|
|
234
|
-
* Pattern: const/let/var <identifier> = defineProps<...>(...) or defineProps(...)
|
|
235
|
-
*
|
|
236
|
-
* @param {string} source
|
|
237
|
-
* @returns {string | null}
|
|
238
|
-
*/
|
|
239
|
-
export function extractPropsObjectName(source) {
|
|
240
|
-
const m = source.match(/(?:const|let|var)\s+([$\w]+)\s*=\s*defineProps\s*[<(]/);
|
|
241
|
-
return m ? m[1] : null;
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
// ── Props validation ────────────────────────────────────────────────
|
|
245
|
-
|
|
246
|
-
/**
|
|
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
|
-
*
|
|
250
|
-
* @param {string} _source
|
|
251
|
-
* @param {string} _fileName
|
|
252
|
-
*/
|
|
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
|
|
256
|
-
}
|
|
257
|
-
|
|
258
|
-
/**
|
|
259
|
-
* Validate that there are no duplicate prop names.
|
|
260
|
-
*
|
|
261
|
-
* @param {string[]} propNames
|
|
262
|
-
* @param {string} fileName
|
|
263
|
-
*/
|
|
264
|
-
export function validateDuplicateProps(propNames, fileName) {
|
|
265
|
-
const seen = new Set();
|
|
266
|
-
const duplicates = new Set();
|
|
267
|
-
for (const p of propNames) {
|
|
268
|
-
if (seen.has(p)) duplicates.add(p);
|
|
269
|
-
seen.add(p);
|
|
270
|
-
}
|
|
271
|
-
if (duplicates.size > 0) {
|
|
272
|
-
const names = [...duplicates].join(', ');
|
|
273
|
-
const error = new Error(
|
|
274
|
-
`Error en '${fileName}': props duplicados: ${names}`
|
|
275
|
-
);
|
|
276
|
-
/** @ts-expect-error — custom error code for programmatic handling */
|
|
277
|
-
error.code = 'DUPLICATE_PROPS';
|
|
278
|
-
throw error;
|
|
279
|
-
}
|
|
280
|
-
}
|
|
281
|
-
|
|
282
|
-
/**
|
|
283
|
-
* Validate that the propsObjectName doesn't collide with signals, computeds, or constants.
|
|
284
|
-
*
|
|
285
|
-
* @param {string|null} propsObjectName
|
|
286
|
-
* @param {Set<string>} signalNames
|
|
287
|
-
* @param {Set<string>} computedNames
|
|
288
|
-
* @param {Set<string>} constantNames
|
|
289
|
-
* @param {string} fileName
|
|
290
|
-
*/
|
|
291
|
-
export function validatePropsConflicts(propsObjectName, signalNames, computedNames, constantNames, fileName) {
|
|
292
|
-
if (!propsObjectName) return;
|
|
293
|
-
|
|
294
|
-
if (signalNames.has(propsObjectName) || computedNames.has(propsObjectName) || constantNames.has(propsObjectName)) {
|
|
295
|
-
const error = new Error(
|
|
296
|
-
`Error en '${fileName}': '${propsObjectName}' colisiona con una declaración existente`
|
|
297
|
-
);
|
|
298
|
-
/** @ts-expect-error — custom error code for programmatic handling */
|
|
299
|
-
error.code = 'PROPS_OBJECT_CONFLICT';
|
|
300
|
-
throw error;
|
|
301
|
-
}
|
|
302
|
-
}
|
|
303
|
-
|
|
304
|
-
// ── Emits extraction (call signatures form — BEFORE type strip) ─────
|
|
305
|
-
|
|
306
|
-
/**
|
|
307
|
-
* Extract event names from the TypeScript call signatures form:
|
|
308
|
-
* defineEmits<{ (e: 'change', value: number): void; (e: 'reset'): void }>()
|
|
309
|
-
*
|
|
310
|
-
* Must be called BEFORE stripTypes() since esbuild removes generics.
|
|
311
|
-
*
|
|
312
|
-
* @param {string} source
|
|
313
|
-
* @returns {string[]}
|
|
314
|
-
*/
|
|
315
|
-
export function extractEmitsFromCallSignatures(source) {
|
|
316
|
-
const m = source.match(/defineEmits\s*<\s*\{([\s\S]*?)\}\s*>\s*\(\s*\)/);
|
|
317
|
-
if (!m) return [];
|
|
318
|
-
|
|
319
|
-
const body = m[1];
|
|
320
|
-
const emits = [];
|
|
321
|
-
const re = /\(\s*\w+\s*:\s*['"]([^'"]+)['"]/g;
|
|
322
|
-
let match;
|
|
323
|
-
while ((match = re.exec(body)) !== null) {
|
|
324
|
-
emits.push(match[1]);
|
|
325
|
-
}
|
|
326
|
-
return emits;
|
|
327
|
-
}
|
|
328
|
-
|
|
329
|
-
// ── Emits extraction (array form — AFTER type strip) ────────────────
|
|
330
|
-
|
|
331
|
-
/**
|
|
332
|
-
* Extract event names from the array form:
|
|
333
|
-
* defineEmits(['change', 'reset'])
|
|
334
|
-
*
|
|
335
|
-
* Called AFTER type stripping.
|
|
336
|
-
*
|
|
337
|
-
* @param {string} source
|
|
338
|
-
* @returns {string[]}
|
|
339
|
-
*/
|
|
340
|
-
export function extractEmits(source) {
|
|
341
|
-
const m = source.match(/defineEmits\(\[([^\]]*)\]\)/);
|
|
342
|
-
if (!m) return [];
|
|
343
|
-
|
|
344
|
-
const body = m[1];
|
|
345
|
-
const emits = [];
|
|
346
|
-
const re = /['"]([^'"]+)['"]/g;
|
|
347
|
-
let match;
|
|
348
|
-
while ((match = re.exec(body)) !== null) {
|
|
349
|
-
emits.push(match[1]);
|
|
350
|
-
}
|
|
351
|
-
return emits;
|
|
352
|
-
}
|
|
353
|
-
|
|
354
|
-
// ── Emits object name extraction ────────────────────────────────────
|
|
355
|
-
|
|
356
|
-
/**
|
|
357
|
-
* Extract the variable name from an emits object binding (AFTER type strip).
|
|
358
|
-
* Pattern: const/let/var <identifier> = defineEmits(...)
|
|
359
|
-
*
|
|
360
|
-
* @param {string} source
|
|
361
|
-
* @returns {string | null}
|
|
362
|
-
*/
|
|
363
|
-
export function extractEmitsObjectName(source) {
|
|
364
|
-
const m = source.match(/(?:const|let|var)\s+([$\w]+)\s*=\s*defineEmits\s*\(/);
|
|
365
|
-
return m ? m[1] : null;
|
|
366
|
-
}
|
|
367
|
-
|
|
368
|
-
/**
|
|
369
|
-
* Extract the variable name from an emits object binding (BEFORE type strip, generic form).
|
|
370
|
-
* Pattern: const/let/var <identifier> = defineEmits<{...}>()
|
|
371
|
-
*
|
|
372
|
-
* @param {string} source
|
|
373
|
-
* @returns {string | null}
|
|
374
|
-
*/
|
|
375
|
-
export function extractEmitsObjectNameFromGeneric(source) {
|
|
376
|
-
const m = source.match(/(?:const|let|var)\s+([$\w]+)\s*=\s*defineEmits\s*<\s*\{/);
|
|
377
|
-
return m ? m[1] : null;
|
|
378
|
-
}
|
|
379
|
-
|
|
380
|
-
// ── Emits validation ────────────────────────────────────────────────
|
|
381
|
-
|
|
382
|
-
/**
|
|
383
|
-
* Validate that defineEmits is assigned to a variable.
|
|
384
|
-
* Throws EMITS_ASSIGNMENT_REQUIRED if bare defineEmits() call detected.
|
|
385
|
-
*
|
|
386
|
-
* @param {string} source
|
|
387
|
-
* @param {string} fileName
|
|
388
|
-
*/
|
|
389
|
-
export function validateEmitsAssignment(source, fileName) {
|
|
390
|
-
// Check if defineEmits appears in source
|
|
391
|
-
if (!/defineEmits\s*[<(]/.test(source)) return;
|
|
392
|
-
|
|
393
|
-
// Check if it's assigned to a variable (either generic or non-generic form)
|
|
394
|
-
if (extractEmitsObjectName(source) !== null) return;
|
|
395
|
-
if (extractEmitsObjectNameFromGeneric(source) !== null) return;
|
|
396
|
-
|
|
397
|
-
const error = new Error(
|
|
398
|
-
`Error en '${fileName}': defineEmits() debe asignarse a una variable (const emit = defineEmits(...))`
|
|
399
|
-
);
|
|
400
|
-
/** @ts-expect-error — custom error code for programmatic handling */
|
|
401
|
-
error.code = 'EMITS_ASSIGNMENT_REQUIRED';
|
|
402
|
-
throw error;
|
|
403
|
-
}
|
|
404
|
-
|
|
405
|
-
/**
|
|
406
|
-
* Validate that there are no duplicate event names.
|
|
407
|
-
*
|
|
408
|
-
* @param {string[]} emitNames
|
|
409
|
-
* @param {string} fileName
|
|
410
|
-
*/
|
|
411
|
-
export function validateDuplicateEmits(emitNames, fileName) {
|
|
412
|
-
const seen = new Set();
|
|
413
|
-
const duplicates = new Set();
|
|
414
|
-
for (const e of emitNames) {
|
|
415
|
-
if (seen.has(e)) duplicates.add(e);
|
|
416
|
-
seen.add(e);
|
|
417
|
-
}
|
|
418
|
-
if (duplicates.size > 0) {
|
|
419
|
-
const names = [...duplicates].join(', ');
|
|
420
|
-
const error = new Error(
|
|
421
|
-
`Error en '${fileName}': emits duplicados: ${names}`
|
|
422
|
-
);
|
|
423
|
-
/** @ts-expect-error — custom error code for programmatic handling */
|
|
424
|
-
error.code = 'DUPLICATE_EMITS';
|
|
425
|
-
throw error;
|
|
426
|
-
}
|
|
427
|
-
}
|
|
428
|
-
|
|
429
|
-
/**
|
|
430
|
-
* Validate that the emitsObjectName doesn't collide with signals, computeds, constants, props, or propsObjectName.
|
|
431
|
-
*
|
|
432
|
-
* @param {string|null} emitsObjectName
|
|
433
|
-
* @param {Set<string>} signalNames
|
|
434
|
-
* @param {Set<string>} computedNames
|
|
435
|
-
* @param {Set<string>} constantNames
|
|
436
|
-
* @param {Set<string>} propNames
|
|
437
|
-
* @param {string|null} propsObjectName
|
|
438
|
-
* @param {string} fileName
|
|
439
|
-
*/
|
|
440
|
-
export function validateEmitsConflicts(emitsObjectName, signalNames, computedNames, constantNames, propNames, propsObjectName, fileName) {
|
|
441
|
-
if (!emitsObjectName) return;
|
|
442
|
-
|
|
443
|
-
if (
|
|
444
|
-
signalNames.has(emitsObjectName) ||
|
|
445
|
-
computedNames.has(emitsObjectName) ||
|
|
446
|
-
constantNames.has(emitsObjectName) ||
|
|
447
|
-
propNames.has(emitsObjectName) ||
|
|
448
|
-
(propsObjectName && emitsObjectName === propsObjectName)
|
|
449
|
-
) {
|
|
450
|
-
const error = new Error(
|
|
451
|
-
`Error en '${fileName}': '${emitsObjectName}' colisiona con una declaración existente`
|
|
452
|
-
);
|
|
453
|
-
/** @ts-expect-error — custom error code for programmatic handling */
|
|
454
|
-
error.code = 'EMITS_OBJECT_CONFLICT';
|
|
455
|
-
throw error;
|
|
456
|
-
}
|
|
457
|
-
}
|
|
458
|
-
|
|
459
|
-
/**
|
|
460
|
-
* Escape special regex characters in a string.
|
|
461
|
-
*
|
|
462
|
-
* @param {string} str
|
|
463
|
-
* @returns {string}
|
|
464
|
-
*/
|
|
465
|
-
export function escapeRegex(str) {
|
|
466
|
-
return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
467
|
-
}
|
|
468
|
-
|
|
469
|
-
/**
|
|
470
|
-
* Validate that all emit calls use declared event names.
|
|
471
|
-
*
|
|
472
|
-
* @param {string} source
|
|
473
|
-
* @param {string|null} emitsObjectName
|
|
474
|
-
* @param {string[]} emits
|
|
475
|
-
* @param {string} fileName
|
|
476
|
-
*/
|
|
477
|
-
export function validateUndeclaredEmits(source, emitsObjectName, emits, fileName) {
|
|
478
|
-
if (!emitsObjectName || emits.length === 0) return;
|
|
479
|
-
|
|
480
|
-
const emitsSet = new Set(emits);
|
|
481
|
-
const re = new RegExp(`\\b${escapeRegex(emitsObjectName)}\\(\\s*['"]([^'"]+)['"]`, 'g');
|
|
482
|
-
let match;
|
|
483
|
-
while ((match = re.exec(source)) !== null) {
|
|
484
|
-
const eventName = match[1];
|
|
485
|
-
if (!emitsSet.has(eventName)) {
|
|
486
|
-
const error = new Error(
|
|
487
|
-
`Error en '${fileName}': emit no declarado: '${eventName}'`
|
|
488
|
-
);
|
|
489
|
-
/** @ts-expect-error — custom error code for programmatic handling */
|
|
490
|
-
error.code = 'UNDECLARED_EMIT';
|
|
491
|
-
throw error;
|
|
492
|
-
}
|
|
493
|
-
}
|
|
494
|
-
}
|
|
495
|
-
|
|
496
|
-
// ── defineComponent extraction ──────────────────────────────────────
|
|
497
|
-
|
|
498
|
-
/**
|
|
499
|
-
* Extract defineComponent({ tag, template, styles }) from source.
|
|
500
|
-
*
|
|
501
|
-
* @param {string} source
|
|
502
|
-
* @returns {{ tag: string, template: string, styles: string | null }}
|
|
503
|
-
*/
|
|
504
|
-
export function extractDefineComponent(source) {
|
|
505
|
-
const m = source.match(/defineComponent\(\s*\{([^}]*)\}\s*\)/);
|
|
506
|
-
if (!m) return null;
|
|
507
|
-
|
|
508
|
-
const body = m[1];
|
|
509
|
-
|
|
510
|
-
const tagMatch = body.match(/tag\s*:\s*['"]([^'"]+)['"]/);
|
|
511
|
-
const templateMatch = body.match(/template\s*:\s*['"]([^'"]+)['"]/);
|
|
512
|
-
const stylesMatch = body.match(/styles\s*:\s*['"]([^'"]+)['"]/);
|
|
513
|
-
|
|
514
|
-
if (!tagMatch || !templateMatch) return null;
|
|
515
|
-
|
|
516
|
-
return {
|
|
517
|
-
tag: tagMatch[1],
|
|
518
|
-
template: templateMatch[1],
|
|
519
|
-
styles: stylesMatch ? stylesMatch[1] : null,
|
|
520
|
-
};
|
|
521
|
-
}
|
|
522
|
-
|
|
523
|
-
// ── Signal extraction ───────────────────────────────────────────────
|
|
524
|
-
|
|
525
|
-
/**
|
|
526
|
-
* Extract the argument of a `signal(...)` call starting at a given position.
|
|
527
|
-
* Uses parenthesis depth counting to correctly handle nested parentheses,
|
|
528
|
-
* e.g. `signal([1, 2, 3])` or `signal((a + b) * c)`.
|
|
529
|
-
* Also handles string literals so that parentheses inside strings are not counted.
|
|
530
|
-
*
|
|
531
|
-
* @param {string} source - Source code starting from after `signal(`
|
|
532
|
-
* @param {number} startIdx - Index right after `signal(`
|
|
533
|
-
* @returns {string} The trimmed argument string, or 'undefined' if empty
|
|
534
|
-
*/
|
|
535
|
-
export function extractSignalArgument(source, startIdx) {
|
|
536
|
-
let depth = 0;
|
|
537
|
-
let i = startIdx;
|
|
538
|
-
/** @type {string | null} */
|
|
539
|
-
let inString = null;
|
|
540
|
-
|
|
541
|
-
for (; i < source.length; i++) {
|
|
542
|
-
const ch = source[i];
|
|
543
|
-
|
|
544
|
-
// Handle string literal boundaries
|
|
545
|
-
if (inString) {
|
|
546
|
-
if (ch === '\\') {
|
|
547
|
-
i++; // skip escaped character
|
|
548
|
-
continue;
|
|
549
|
-
}
|
|
550
|
-
if (ch === inString) {
|
|
551
|
-
inString = null;
|
|
552
|
-
}
|
|
553
|
-
continue;
|
|
554
|
-
}
|
|
555
|
-
|
|
556
|
-
if (ch === '"' || ch === "'" || ch === '`') {
|
|
557
|
-
inString = ch;
|
|
558
|
-
continue;
|
|
559
|
-
}
|
|
560
|
-
|
|
561
|
-
if (ch === '(') depth++;
|
|
562
|
-
if (ch === ')') {
|
|
563
|
-
if (depth === 0) break;
|
|
564
|
-
depth--;
|
|
565
|
-
}
|
|
566
|
-
}
|
|
567
|
-
|
|
568
|
-
return source.slice(startIdx, i).trim() || 'undefined';
|
|
569
|
-
}
|
|
570
|
-
|
|
571
|
-
/**
|
|
572
|
-
* Extract signal declarations from source.
|
|
573
|
-
* Pattern: const/let/var name = signal(value)
|
|
574
|
-
*
|
|
575
|
-
* @param {string} source
|
|
576
|
-
* @returns {ReactiveVar[]}
|
|
577
|
-
*/
|
|
578
|
-
export function extractSignals(source) {
|
|
579
|
-
/** @type {ReactiveVar[]} */
|
|
580
|
-
const signals = [];
|
|
581
|
-
const re = /(?:const|let|var)\s+([$\w]+)\s*=\s*signal\(/g;
|
|
582
|
-
let m;
|
|
583
|
-
|
|
584
|
-
while ((m = re.exec(source)) !== null) {
|
|
585
|
-
const name = m[1];
|
|
586
|
-
const argStart = m.index + m[0].length;
|
|
587
|
-
const value = extractSignalArgument(source, argStart);
|
|
588
|
-
signals.push({ name, value });
|
|
589
|
-
}
|
|
590
|
-
|
|
591
|
-
return signals;
|
|
592
|
-
}
|
|
593
|
-
|
|
594
|
-
// ── Constant extraction ─────────────────────────────────────────────
|
|
595
|
-
|
|
596
|
-
/**
|
|
597
|
-
* Known macro/reactive call patterns that should NOT be treated as constants.
|
|
598
|
-
*/
|
|
599
|
-
export const REACTIVE_CALLS = /\b(?:signal|computed|effect|watch|defineProps|defineEmits|defineModel|defineComponent|templateRef|defineExpose|onMount|onDestroy)\s*[<(]/;
|
|
600
|
-
|
|
601
|
-
/**
|
|
602
|
-
* Extract plain const/let/var declarations that are NOT reactive calls.
|
|
603
|
-
* Only extracts root-level declarations (depth 0).
|
|
604
|
-
*
|
|
605
|
-
* @param {string} source
|
|
606
|
-
* @returns {import('./types.js').ConstantVar[]}
|
|
607
|
-
*/
|
|
608
|
-
export function extractConstants(source) {
|
|
609
|
-
/** @type {import('./types.js').ConstantVar[]} */
|
|
610
|
-
const constants = [];
|
|
611
|
-
let depth = 0;
|
|
612
|
-
|
|
613
|
-
for (const line of source.split('\n')) {
|
|
614
|
-
// Track brace depth to skip nested blocks
|
|
615
|
-
for (const ch of line) {
|
|
616
|
-
if (ch === '{') depth++;
|
|
617
|
-
if (ch === '}') depth--;
|
|
618
|
-
}
|
|
619
|
-
if (depth > 0) continue;
|
|
620
|
-
|
|
621
|
-
// Match const/let/var name = value at root level
|
|
622
|
-
const m = line.match(/^\s*(?:const|let|var)\s+([$\w]+)\s*=\s*(.+?);?\s*$/);
|
|
623
|
-
if (!m) continue;
|
|
624
|
-
|
|
625
|
-
const value = m[2].trim();
|
|
626
|
-
// Skip reactive/macro calls
|
|
627
|
-
if (REACTIVE_CALLS.test(value)) continue;
|
|
628
|
-
// Skip export default
|
|
629
|
-
if (/^\s*export\s+default/.test(line)) continue;
|
|
630
|
-
|
|
631
|
-
constants.push({ name: m[1], value });
|
|
632
|
-
}
|
|
633
|
-
|
|
634
|
-
return constants;
|
|
635
|
-
}
|
|
636
|
-
|
|
637
|
-
// ── Computed extraction ─────────────────────────────────────────────
|
|
638
|
-
|
|
639
|
-
/**
|
|
640
|
-
* Extract computed declarations from source.
|
|
641
|
-
* Pattern: const/let/var name = computed(() => expr)
|
|
642
|
-
* Uses parenthesis depth counting to handle expressions containing parens,
|
|
643
|
-
* e.g. `computed(() => count() * 2)`.
|
|
644
|
-
*
|
|
645
|
-
* @param {string} source
|
|
646
|
-
* @returns {ComputedDef[]}
|
|
647
|
-
*/
|
|
648
|
-
export function extractComputeds(source) {
|
|
649
|
-
/** @type {ComputedDef[]} */
|
|
650
|
-
const out = [];
|
|
651
|
-
const re = /(?:const|let|var)\s+(\w+)\s*=\s*computed\(\s*\(\)\s*=>\s*/g;
|
|
652
|
-
let m;
|
|
653
|
-
while ((m = re.exec(source)) !== null) {
|
|
654
|
-
const name = m[1];
|
|
655
|
-
const bodyStart = m.index + m[0].length;
|
|
656
|
-
// Use depth counting: we're inside `computed(` so depth starts at 1
|
|
657
|
-
// We need to find the matching `)` for the outer `computed(` call
|
|
658
|
-
let depth = 1;
|
|
659
|
-
let i = bodyStart;
|
|
660
|
-
/** @type {string | null} */
|
|
661
|
-
let inString = null;
|
|
662
|
-
|
|
663
|
-
for (; i < source.length; i++) {
|
|
664
|
-
const ch = source[i];
|
|
665
|
-
|
|
666
|
-
if (inString) {
|
|
667
|
-
if (ch === '\\') { i++; continue; }
|
|
668
|
-
if (ch === inString) inString = null;
|
|
669
|
-
continue;
|
|
670
|
-
}
|
|
671
|
-
|
|
672
|
-
if (ch === '"' || ch === "'" || ch === '`') {
|
|
673
|
-
inString = ch;
|
|
674
|
-
continue;
|
|
675
|
-
}
|
|
676
|
-
|
|
677
|
-
if (ch === '(') depth++;
|
|
678
|
-
if (ch === ')') {
|
|
679
|
-
depth--;
|
|
680
|
-
if (depth === 0) break;
|
|
681
|
-
}
|
|
682
|
-
}
|
|
683
|
-
|
|
684
|
-
const body = source.slice(bodyStart, i).trim();
|
|
685
|
-
if (body) {
|
|
686
|
-
out.push({ name, body });
|
|
687
|
-
}
|
|
688
|
-
}
|
|
689
|
-
return out;
|
|
690
|
-
}
|
|
691
|
-
|
|
692
|
-
// ── Effect extraction ───────────────────────────────────────────────
|
|
693
|
-
|
|
694
|
-
/**
|
|
695
|
-
* Extract effect declarations from source.
|
|
696
|
-
* Pattern: effect(() => { body })
|
|
697
|
-
* Uses brace depth tracking to capture multi-line bodies.
|
|
698
|
-
*
|
|
699
|
-
* @param {string} source
|
|
700
|
-
* @returns {EffectDef[]}
|
|
701
|
-
*/
|
|
702
|
-
export function extractEffects(source) {
|
|
703
|
-
/** @type {EffectDef[]} */
|
|
704
|
-
const effects = [];
|
|
705
|
-
const lines = source.split('\n');
|
|
706
|
-
let i = 0;
|
|
707
|
-
|
|
708
|
-
while (i < lines.length) {
|
|
709
|
-
const line = lines[i];
|
|
710
|
-
const effectMatch = line.match(/\beffect\s*\(\s*\(\s*\)\s*=>\s*\{/);
|
|
711
|
-
|
|
712
|
-
if (effectMatch) {
|
|
713
|
-
// Collect body by tracking brace depth
|
|
714
|
-
let depth = 0;
|
|
715
|
-
let bodyLines = [];
|
|
716
|
-
let started = false;
|
|
717
|
-
|
|
718
|
-
for (let j = i; j < lines.length; j++) {
|
|
719
|
-
const l = lines[j];
|
|
720
|
-
for (const ch of l) {
|
|
721
|
-
if (ch === '{') {
|
|
722
|
-
if (started) depth++;
|
|
723
|
-
else { depth = 1; started = true; }
|
|
724
|
-
}
|
|
725
|
-
if (ch === '}') depth--;
|
|
726
|
-
}
|
|
727
|
-
|
|
728
|
-
if (j === i) {
|
|
729
|
-
// First line: capture everything after the opening brace
|
|
730
|
-
const braceIdx = l.indexOf('{');
|
|
731
|
-
const afterBrace = l.substring(braceIdx + 1);
|
|
732
|
-
if (afterBrace.trim()) bodyLines.push(afterBrace);
|
|
733
|
-
} else if (depth <= 0) {
|
|
734
|
-
// Last line: capture everything before the closing brace
|
|
735
|
-
const lastBraceIdx = l.lastIndexOf('}');
|
|
736
|
-
const before = l.substring(0, lastBraceIdx);
|
|
737
|
-
if (before.trim()) bodyLines.push(before);
|
|
738
|
-
i = j;
|
|
739
|
-
break;
|
|
740
|
-
} else {
|
|
741
|
-
bodyLines.push(l);
|
|
742
|
-
}
|
|
743
|
-
}
|
|
744
|
-
|
|
745
|
-
// Dedent body lines
|
|
746
|
-
const nonEmptyLines = bodyLines.filter(l => l.trim().length > 0);
|
|
747
|
-
let minIndent = Infinity;
|
|
748
|
-
for (const bl of nonEmptyLines) {
|
|
749
|
-
const leadingSpaces = bl.match(/^(\s*)/)[1].length;
|
|
750
|
-
if (leadingSpaces < minIndent) minIndent = leadingSpaces;
|
|
751
|
-
}
|
|
752
|
-
if (minIndent === Infinity) minIndent = 0;
|
|
753
|
-
const dedentedLines = bodyLines.map(bl => bl.substring(minIndent));
|
|
754
|
-
const body = dedentedLines.join('\n').trim();
|
|
755
|
-
|
|
756
|
-
effects.push({ body });
|
|
757
|
-
}
|
|
758
|
-
i++;
|
|
759
|
-
}
|
|
760
|
-
|
|
761
|
-
return effects;
|
|
762
|
-
}
|
|
763
|
-
|
|
764
|
-
// ── Watcher extraction ──────────────────────────────────────────────
|
|
765
|
-
|
|
766
|
-
/**
|
|
767
|
-
* Extract watch() declarations from source.
|
|
768
|
-
* Supports two forms:
|
|
769
|
-
* Form 1 — Signal direct: watch(count, (newVal, oldVal) => { body })
|
|
770
|
-
* Form 2 — Getter function: watch(() => expr, (newVal, oldVal) => { body })
|
|
771
|
-
* Uses brace depth tracking to capture multi-line bodies.
|
|
772
|
-
*
|
|
773
|
-
* @param {string} source
|
|
774
|
-
* @returns {import('./types.js').WatcherDef[]}
|
|
775
|
-
*/
|
|
776
|
-
export function extractWatchers(source) {
|
|
777
|
-
/** @type {import('./types.js').WatcherDef[]} */
|
|
778
|
-
const watchers = [];
|
|
779
|
-
const lines = source.split('\n');
|
|
780
|
-
let i = 0;
|
|
781
|
-
|
|
782
|
-
while (i < lines.length) {
|
|
783
|
-
const line = lines[i];
|
|
784
|
-
|
|
785
|
-
// Form 2 — Getter function: watch(() => expr, (newVal, oldVal) => { OR watch(() => expr, (newVal) => {
|
|
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) => { OR watch(identifier, (newVal) => {
|
|
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;
|
|
791
|
-
|
|
792
|
-
if (m) {
|
|
793
|
-
const kind = mGetter ? 'getter' : 'signal';
|
|
794
|
-
const target = m[1];
|
|
795
|
-
const newParam = m[2];
|
|
796
|
-
const oldParam = m[3];
|
|
797
|
-
|
|
798
|
-
// Collect body by tracking brace depth
|
|
799
|
-
let depth = 0;
|
|
800
|
-
let bodyLines = [];
|
|
801
|
-
let started = false;
|
|
802
|
-
|
|
803
|
-
for (let j = i; j < lines.length; j++) {
|
|
804
|
-
const l = lines[j];
|
|
805
|
-
for (const ch of l) {
|
|
806
|
-
if (ch === '{') {
|
|
807
|
-
if (started) depth++;
|
|
808
|
-
else { depth = 1; started = true; }
|
|
809
|
-
}
|
|
810
|
-
if (ch === '}') depth--;
|
|
811
|
-
}
|
|
812
|
-
|
|
813
|
-
if (j === i) {
|
|
814
|
-
const braceIdx = l.indexOf('{');
|
|
815
|
-
const afterBrace = l.substring(braceIdx + 1);
|
|
816
|
-
if (depth <= 0) {
|
|
817
|
-
const lastBraceIdx = l.lastIndexOf('}');
|
|
818
|
-
const inner = l.substring(braceIdx + 1, lastBraceIdx);
|
|
819
|
-
if (inner.trim()) bodyLines.push(inner);
|
|
820
|
-
i = j;
|
|
821
|
-
break;
|
|
822
|
-
}
|
|
823
|
-
if (afterBrace.trim()) bodyLines.push(afterBrace);
|
|
824
|
-
} else if (depth <= 0) {
|
|
825
|
-
const lastBraceIdx = l.lastIndexOf('}');
|
|
826
|
-
const before = l.substring(0, lastBraceIdx);
|
|
827
|
-
if (before.trim()) bodyLines.push(before);
|
|
828
|
-
i = j;
|
|
829
|
-
break;
|
|
830
|
-
} else {
|
|
831
|
-
bodyLines.push(l);
|
|
832
|
-
}
|
|
833
|
-
}
|
|
834
|
-
|
|
835
|
-
// Dedent
|
|
836
|
-
const nonEmpty = bodyLines.filter(l => l.trim().length > 0);
|
|
837
|
-
let minIndent = Infinity;
|
|
838
|
-
for (const bl of nonEmpty) {
|
|
839
|
-
const leading = bl.match(/^(\s*)/)[1].length;
|
|
840
|
-
if (leading < minIndent) minIndent = leading;
|
|
841
|
-
}
|
|
842
|
-
if (minIndent === Infinity) minIndent = 0;
|
|
843
|
-
const body = bodyLines.map(bl => bl.substring(minIndent)).join('\n').trim();
|
|
844
|
-
|
|
845
|
-
watchers.push({ kind, target, newParam, oldParam, body });
|
|
846
|
-
}
|
|
847
|
-
i++;
|
|
848
|
-
}
|
|
849
|
-
|
|
850
|
-
return watchers;
|
|
851
|
-
}
|
|
852
|
-
|
|
853
|
-
// ── Function extraction ─────────────────────────────────────────────
|
|
854
|
-
|
|
855
|
-
/**
|
|
856
|
-
* Extract top-level function declarations from source.
|
|
857
|
-
* Pattern: function name(params) { body }
|
|
858
|
-
* Uses brace depth tracking to capture the full function body.
|
|
859
|
-
*
|
|
860
|
-
* @param {string} source
|
|
861
|
-
* @returns {MethodDef[]}
|
|
862
|
-
*/
|
|
863
|
-
export function extractFunctions(source) {
|
|
864
|
-
/** @type {MethodDef[]} */
|
|
865
|
-
const functions = [];
|
|
866
|
-
const lines = source.split('\n');
|
|
867
|
-
let i = 0;
|
|
868
|
-
|
|
869
|
-
while (i < lines.length) {
|
|
870
|
-
const line = lines[i];
|
|
871
|
-
const m = line.match(/^\s*function\s+(\w+)\s*\(([^)]*)\)\s*\{/);
|
|
872
|
-
if (m) {
|
|
873
|
-
const name = m[1];
|
|
874
|
-
const params = m[2].trim();
|
|
875
|
-
// Collect body by tracking brace depth
|
|
876
|
-
let depth = 0;
|
|
877
|
-
let bodyLines = [];
|
|
878
|
-
let started = false;
|
|
879
|
-
|
|
880
|
-
for (let j = i; j < lines.length; j++) {
|
|
881
|
-
const l = lines[j];
|
|
882
|
-
for (const ch of l) {
|
|
883
|
-
if (ch === '{') {
|
|
884
|
-
if (started) depth++;
|
|
885
|
-
else { depth = 1; started = true; }
|
|
886
|
-
}
|
|
887
|
-
if (ch === '}') depth--;
|
|
888
|
-
}
|
|
889
|
-
|
|
890
|
-
if (j === i) {
|
|
891
|
-
// First line: capture everything after the opening brace
|
|
892
|
-
const afterBrace = l.substring(l.indexOf('{') + 1);
|
|
893
|
-
// Single-line function: depth already closed on first line
|
|
894
|
-
if (depth <= 0) {
|
|
895
|
-
const lastBraceIdx = afterBrace.lastIndexOf('}');
|
|
896
|
-
const inner = lastBraceIdx >= 0 ? afterBrace.substring(0, lastBraceIdx) : afterBrace;
|
|
897
|
-
if (inner.trim()) bodyLines.push(inner);
|
|
898
|
-
i = j;
|
|
899
|
-
break;
|
|
900
|
-
}
|
|
901
|
-
if (afterBrace.trim()) bodyLines.push(afterBrace);
|
|
902
|
-
} else if (depth <= 0) {
|
|
903
|
-
// Last line: capture everything before the closing brace
|
|
904
|
-
const lastBraceIdx = l.lastIndexOf('}');
|
|
905
|
-
const before = l.substring(0, lastBraceIdx);
|
|
906
|
-
if (before.trim()) bodyLines.push(before);
|
|
907
|
-
i = j;
|
|
908
|
-
break;
|
|
909
|
-
} else {
|
|
910
|
-
bodyLines.push(l);
|
|
911
|
-
}
|
|
912
|
-
}
|
|
913
|
-
|
|
914
|
-
functions.push({
|
|
915
|
-
name,
|
|
916
|
-
params,
|
|
917
|
-
body: bodyLines.join('\n').trim(),
|
|
918
|
-
});
|
|
919
|
-
}
|
|
920
|
-
i++;
|
|
921
|
-
}
|
|
922
|
-
|
|
923
|
-
return functions;
|
|
924
|
-
}
|
|
925
|
-
|
|
926
|
-
// ── Lifecycle hook extraction ────────────────────────────────────────
|
|
927
|
-
|
|
928
|
-
/**
|
|
929
|
-
* Extract lifecycle hooks from the script.
|
|
930
|
-
* Patterns: onMount(() => { body }) and onDestroy(() => { body })
|
|
931
|
-
* Supports multiple calls of each type.
|
|
932
|
-
* Uses brace depth tracking to capture multi-line bodies.
|
|
933
|
-
* Only extracts top-level calls (brace depth === 0 when the call is encountered).
|
|
934
|
-
*
|
|
935
|
-
* @param {string} script - The script content (after type stripping)
|
|
936
|
-
* @returns {{ onMountHooks: LifecycleHook[], onDestroyHooks: LifecycleHook[], onAdoptHooks: LifecycleHook[] }}
|
|
937
|
-
*/
|
|
938
|
-
export function extractLifecycleHooks(script) {
|
|
939
|
-
/** @type {LifecycleHook[]} */
|
|
940
|
-
const onMountHooks = [];
|
|
941
|
-
/** @type {LifecycleHook[]} */
|
|
942
|
-
const onDestroyHooks = [];
|
|
943
|
-
/** @type {LifecycleHook[]} */
|
|
944
|
-
const onAdoptHooks = [];
|
|
945
|
-
const lines = script.split('\n');
|
|
946
|
-
let i = 0;
|
|
947
|
-
|
|
948
|
-
while (i < lines.length) {
|
|
949
|
-
const line = lines[i];
|
|
950
|
-
const mountMatch = line.match(/\bonMount\s*\(\s*(?:async\s*)?\(\s*\)\s*=>\s*\{/);
|
|
951
|
-
const destroyMatch = line.match(/\bonDestroy\s*\(\s*(?:async\s*)?\(\s*\)\s*=>\s*\{/);
|
|
952
|
-
const adoptMatch = line.match(/\bonAdopt\s*\(\s*(?:async\s*)?\(\s*\)\s*=>\s*\{/);
|
|
953
|
-
|
|
954
|
-
if (mountMatch || destroyMatch || adoptMatch) {
|
|
955
|
-
// Detect if the callback is async
|
|
956
|
-
const isAsync = /\basync\s*\(/.test(line);
|
|
957
|
-
|
|
958
|
-
// Collect body by tracking brace depth
|
|
959
|
-
let depth = 0;
|
|
960
|
-
let bodyLines = [];
|
|
961
|
-
let started = false;
|
|
962
|
-
|
|
963
|
-
for (let j = i; j < lines.length; j++) {
|
|
964
|
-
const l = lines[j];
|
|
965
|
-
for (const ch of l) {
|
|
966
|
-
if (ch === '{') {
|
|
967
|
-
if (started) depth++;
|
|
968
|
-
else { depth = 1; started = true; }
|
|
969
|
-
}
|
|
970
|
-
if (ch === '}') depth--;
|
|
971
|
-
}
|
|
972
|
-
|
|
973
|
-
if (j === i) {
|
|
974
|
-
// First line: capture everything after the opening brace
|
|
975
|
-
const braceIdx = l.indexOf('{');
|
|
976
|
-
const afterBrace = l.substring(braceIdx + 1);
|
|
977
|
-
// If depth already closed on the first line (single-line hook)
|
|
978
|
-
if (depth <= 0) {
|
|
979
|
-
// Extract content between first { and last }
|
|
980
|
-
const lastBraceIdx = l.lastIndexOf('}');
|
|
981
|
-
const inner = l.substring(braceIdx + 1, lastBraceIdx);
|
|
982
|
-
if (inner.trim()) bodyLines.push(inner);
|
|
983
|
-
i = j;
|
|
984
|
-
break;
|
|
985
|
-
}
|
|
986
|
-
if (afterBrace.trim()) bodyLines.push(afterBrace);
|
|
987
|
-
} else if (depth <= 0) {
|
|
988
|
-
// Last line: capture everything before the closing brace
|
|
989
|
-
const lastBraceIdx = l.lastIndexOf('}');
|
|
990
|
-
const before = l.substring(0, lastBraceIdx);
|
|
991
|
-
if (before.trim()) bodyLines.push(before);
|
|
992
|
-
i = j;
|
|
993
|
-
break;
|
|
994
|
-
} else {
|
|
995
|
-
bodyLines.push(l);
|
|
996
|
-
}
|
|
997
|
-
}
|
|
998
|
-
|
|
999
|
-
// Dedent body lines: remove common leading whitespace
|
|
1000
|
-
const nonEmptyLines = bodyLines.filter(l => l.trim().length > 0);
|
|
1001
|
-
let minIndent = Infinity;
|
|
1002
|
-
for (const bl of nonEmptyLines) {
|
|
1003
|
-
const leadingSpaces = bl.match(/^(\s*)/)[1].length;
|
|
1004
|
-
if (leadingSpaces < minIndent) minIndent = leadingSpaces;
|
|
1005
|
-
}
|
|
1006
|
-
if (minIndent === Infinity) minIndent = 0;
|
|
1007
|
-
const dedentedLines = bodyLines.map(bl => bl.substring(minIndent));
|
|
1008
|
-
const body = dedentedLines.join('\n').trim();
|
|
1009
|
-
|
|
1010
|
-
if (mountMatch) {
|
|
1011
|
-
onMountHooks.push({ body, async: isAsync });
|
|
1012
|
-
} else if (destroyMatch) {
|
|
1013
|
-
onDestroyHooks.push({ body, async: isAsync });
|
|
1014
|
-
} else {
|
|
1015
|
-
onAdoptHooks.push({ body, async: isAsync });
|
|
1016
|
-
}
|
|
1017
|
-
}
|
|
1018
|
-
i++;
|
|
1019
|
-
}
|
|
1020
|
-
|
|
1021
|
-
return { onMountHooks, onDestroyHooks, onAdoptHooks };
|
|
1022
|
-
}
|
|
1023
|
-
|
|
1024
|
-
// ── Ref extraction ───────────────────────────────────────────────────
|
|
1025
|
-
|
|
1026
|
-
/**
|
|
1027
|
-
* Extract templateRef('name') declarations from component source.
|
|
1028
|
-
* Pattern: const/let/var varName = templateRef('refName') or templateRef("refName")
|
|
1029
|
-
*
|
|
1030
|
-
* @param {string} source — Stripped source code
|
|
1031
|
-
* @returns {RefDeclaration[]}
|
|
1032
|
-
*/
|
|
1033
|
-
export function extractRefs(source) {
|
|
1034
|
-
/** @type {RefDeclaration[]} */
|
|
1035
|
-
const refs = [];
|
|
1036
|
-
const re = /(?:const|let|var)\s+([$\w]+)\s*=\s*templateRef\(\s*['"]([^'"]+)['"]\s*\)/g;
|
|
1037
|
-
let m;
|
|
1038
|
-
while ((m = re.exec(source)) !== null) {
|
|
1039
|
-
refs.push({ varName: m[1], refName: m[2] });
|
|
1040
|
-
}
|
|
1041
|
-
return refs;
|
|
1042
|
-
}
|
|
1043
|
-
|
|
1044
|
-
// ── defineModel extraction ───────────────────────────────────────────
|
|
1045
|
-
|
|
1046
|
-
/**
|
|
1047
|
-
* Extract defineModel() declarations from source.
|
|
1048
|
-
* Pattern: const/let/var varName = defineModel({ name: 'propName', default: value })
|
|
1049
|
-
* const/let/var varName = defineModel({ name: 'propName', required: true })
|
|
1050
|
-
*
|
|
1051
|
-
* @param {string} source
|
|
1052
|
-
* @returns {{ varName: string, name: string, default: string, required: boolean }[]}
|
|
1053
|
-
*/
|
|
1054
|
-
export function extractModels(source) {
|
|
1055
|
-
/** @type {{ varName: string, name: string, default: string, required: boolean }[]} */
|
|
1056
|
-
const models = [];
|
|
1057
|
-
const re = /(?:const|let|var)\s+(\w+)\s*=\s*defineModel\(\s*\{/g;
|
|
1058
|
-
let m;
|
|
1059
|
-
|
|
1060
|
-
while ((m = re.exec(source)) !== null) {
|
|
1061
|
-
const varName = m[1];
|
|
1062
|
-
const objStart = m.index + m[0].length - 1; // position of '{'
|
|
1063
|
-
|
|
1064
|
-
// Use depth counting to extract the full object literal
|
|
1065
|
-
let depth = 0;
|
|
1066
|
-
let i = objStart;
|
|
1067
|
-
/** @type {string | null} */
|
|
1068
|
-
let inString = null;
|
|
1069
|
-
|
|
1070
|
-
for (; i < source.length; i++) {
|
|
1071
|
-
const ch = source[i];
|
|
1072
|
-
|
|
1073
|
-
if (inString) {
|
|
1074
|
-
if (ch === '\\') { i++; continue; }
|
|
1075
|
-
if (ch === inString) inString = null;
|
|
1076
|
-
continue;
|
|
1077
|
-
}
|
|
1078
|
-
|
|
1079
|
-
if (ch === '"' || ch === "'" || ch === '`') {
|
|
1080
|
-
inString = ch;
|
|
1081
|
-
continue;
|
|
1082
|
-
}
|
|
1083
|
-
|
|
1084
|
-
if (ch === '{') depth++;
|
|
1085
|
-
if (ch === '}') {
|
|
1086
|
-
depth--;
|
|
1087
|
-
if (depth === 0) { i++; break; }
|
|
1088
|
-
}
|
|
1089
|
-
}
|
|
1090
|
-
|
|
1091
|
-
const objLiteral = source.slice(objStart, i).trim();
|
|
1092
|
-
// Remove outer braces
|
|
1093
|
-
const inner = objLiteral.slice(1, -1).trim();
|
|
1094
|
-
|
|
1095
|
-
// Extract 'name' property
|
|
1096
|
-
const nameMatch = inner.match(/name\s*:\s*['"]([^'"]+)['"]/);
|
|
1097
|
-
const propName = nameMatch ? nameMatch[1] : '';
|
|
1098
|
-
|
|
1099
|
-
// Extract 'default' property using depth counting
|
|
1100
|
-
let defaultValue = 'undefined';
|
|
1101
|
-
const defaultIdx = inner.search(/\bdefault\s*:\s*/);
|
|
1102
|
-
if (defaultIdx !== -1) {
|
|
1103
|
-
const afterDefault = inner.slice(defaultIdx);
|
|
1104
|
-
const colonMatch = afterDefault.match(/^default\s*:\s*/);
|
|
1105
|
-
if (colonMatch) {
|
|
1106
|
-
const valStart = defaultIdx + colonMatch[0].length;
|
|
1107
|
-
let valDepth = 0;
|
|
1108
|
-
let pos = valStart;
|
|
1109
|
-
/** @type {string | null} */
|
|
1110
|
-
let valInString = null;
|
|
1111
|
-
|
|
1112
|
-
for (; pos < inner.length; pos++) {
|
|
1113
|
-
const ch = inner[pos];
|
|
1114
|
-
|
|
1115
|
-
if (valInString) {
|
|
1116
|
-
if (ch === '\\') { pos++; continue; }
|
|
1117
|
-
if (ch === valInString) valInString = null;
|
|
1118
|
-
continue;
|
|
1119
|
-
}
|
|
1120
|
-
|
|
1121
|
-
if (ch === '"' || ch === "'" || ch === '`') {
|
|
1122
|
-
valInString = ch;
|
|
1123
|
-
continue;
|
|
1124
|
-
}
|
|
1125
|
-
|
|
1126
|
-
if (ch === '(' || ch === '[' || ch === '{') valDepth++;
|
|
1127
|
-
if (ch === ')' || ch === ']' || ch === '}') valDepth--;
|
|
1128
|
-
|
|
1129
|
-
if (valDepth === 0 && ch === ',') {
|
|
1130
|
-
break;
|
|
1131
|
-
}
|
|
1132
|
-
}
|
|
1133
|
-
|
|
1134
|
-
defaultValue = inner.slice(valStart, pos).trim();
|
|
1135
|
-
}
|
|
1136
|
-
}
|
|
1137
|
-
|
|
1138
|
-
// Extract 'required' property
|
|
1139
|
-
const requiredMatch = inner.match(/required\s*:\s*true/);
|
|
1140
|
-
const required = !!requiredMatch;
|
|
1141
|
-
|
|
1142
|
-
models.push({ varName, name: propName, default: defaultValue, required });
|
|
1143
|
-
}
|
|
1144
|
-
|
|
1145
|
-
return models;
|
|
1146
|
-
}
|
|
1147
|
-
|
|
1148
|
-
// ── defineExpose extraction ─────────────────────────────────────────
|
|
1149
|
-
|
|
1150
|
-
/**
|
|
1151
|
-
* Extract property names from defineExpose({ key1, key2, ... }).
|
|
1152
|
-
* Supports shorthand properties: defineExpose({ doubled, handleUpdate })
|
|
1153
|
-
*
|
|
1154
|
-
* @param {string} source — Source code (after type stripping)
|
|
1155
|
-
* @returns {string[]} Array of exposed property names
|
|
1156
|
-
*/
|
|
1157
|
-
export function extractExpose(source) {
|
|
1158
|
-
const m = source.match(/defineExpose\(\s*\{([^}]*)\}\s*\)/);
|
|
1159
|
-
if (!m) return [];
|
|
1160
|
-
|
|
1161
|
-
const body = m[1];
|
|
1162
|
-
const names = [];
|
|
1163
|
-
const re = /\b(\w+)\b/g;
|
|
1164
|
-
let match;
|
|
1165
|
-
while ((match = re.exec(body)) !== null) {
|
|
1166
|
-
names.push(match[1]);
|
|
1167
|
-
}
|
|
1168
|
-
return names;
|
|
1169
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* Pure extraction functions for parsing .ts/.js component source files.
|
|
3
|
+
*
|
|
4
|
+
* These functions have NO Node.js-specific imports (no fs, path, or esbuild)
|
|
5
|
+
* and can be used in both Node.js and browser environments.
|
|
6
|
+
*
|
|
7
|
+
* Extracts:
|
|
8
|
+
* - defineComponent({ tag, template, styles }) metadata
|
|
9
|
+
* - signal() declarations
|
|
10
|
+
* - computed() declarations
|
|
11
|
+
* - effect() declarations
|
|
12
|
+
* - Top-level function declarations
|
|
13
|
+
* - Props and emits definitions
|
|
14
|
+
* - Lifecycle hooks
|
|
15
|
+
* - Template refs
|
|
16
|
+
* - Constants
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
/** @import { ReactiveVar, ComputedDef, EffectDef, MethodDef, PropDef, LifecycleHook, RefDeclaration } from './types.js' */
|
|
20
|
+
|
|
21
|
+
// ── Macro import stripping ───────────────────────────────────────────
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Remove `import { ... } from 'wcc'` and `import { ... } from '@sprlab/wccompiler'`
|
|
25
|
+
* statements from source content. These imports are purely cosmetic (for IDE DX)
|
|
26
|
+
* and must be stripped before any further processing.
|
|
27
|
+
*
|
|
28
|
+
* @param {string} source - Raw source content
|
|
29
|
+
* @returns {string} Source with macro imports removed
|
|
30
|
+
*/
|
|
31
|
+
export function stripMacroImport(source) {
|
|
32
|
+
return source.replace(
|
|
33
|
+
/import\s*\{[^}]*\}\s*from\s*['"](?:wcc|@sprlab\/wccompiler)['"]\s*;?/g,
|
|
34
|
+
''
|
|
35
|
+
);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// ── Name conversion ─────────────────────────────────────────────────
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Convert a kebab-case tag name to PascalCase class name.
|
|
42
|
+
* e.g. "wcc-counter" → "WccCounter"
|
|
43
|
+
*
|
|
44
|
+
* @param {string} tagName
|
|
45
|
+
* @returns {string}
|
|
46
|
+
*/
|
|
47
|
+
export function toClassName(tagName) {
|
|
48
|
+
return tagName
|
|
49
|
+
.split('-')
|
|
50
|
+
.map(part => part.charAt(0).toUpperCase() + part.slice(1))
|
|
51
|
+
.join('');
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// ── camelCase to kebab-case ─────────────────────────────────────────
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Convert a camelCase identifier to kebab-case for HTML attribute names.
|
|
58
|
+
* e.g. 'itemCount' → 'item-count', 'label' → 'label'
|
|
59
|
+
*
|
|
60
|
+
* @param {string} name
|
|
61
|
+
* @returns {string}
|
|
62
|
+
*/
|
|
63
|
+
export function camelToKebab(name) {
|
|
64
|
+
return name.replace(/([a-z0-9])([A-Z])/g, '$1-$2').toLowerCase();
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// ── Props extraction (generic form — BEFORE type strip) ─────────────
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Extract prop names from the TypeScript generic form:
|
|
71
|
+
* defineProps<{ label: string, count: number }>({...})
|
|
72
|
+
* or defineProps<{ label: string }>()
|
|
73
|
+
*
|
|
74
|
+
* Must be called BEFORE stripTypes() since esbuild removes generics.
|
|
75
|
+
*
|
|
76
|
+
* @param {string} source
|
|
77
|
+
* @returns {string[]}
|
|
78
|
+
*/
|
|
79
|
+
export function extractPropsGeneric(source) {
|
|
80
|
+
const m = source.match(/defineProps\s*<\s*\{([^}]*)\}\s*>/);
|
|
81
|
+
if (!m) return [];
|
|
82
|
+
|
|
83
|
+
const body = m[1];
|
|
84
|
+
const props = [];
|
|
85
|
+
const re = /(\w+)\s*[?]?\s*:/g;
|
|
86
|
+
let match;
|
|
87
|
+
while ((match = re.exec(body)) !== null) {
|
|
88
|
+
props.push(match[1]);
|
|
89
|
+
}
|
|
90
|
+
return props;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// ── Props extraction (array form — AFTER type strip) ────────────────
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Extract prop names from the array form:
|
|
97
|
+
* defineProps(['label', 'count'])
|
|
98
|
+
*
|
|
99
|
+
* Called AFTER type stripping.
|
|
100
|
+
*
|
|
101
|
+
* @param {string} source
|
|
102
|
+
* @returns {string[]}
|
|
103
|
+
*/
|
|
104
|
+
export function extractPropsArray(source) {
|
|
105
|
+
const m = source.match(/defineProps\(\s*\[([^\]]*)\]\s*\)/);
|
|
106
|
+
if (!m) return [];
|
|
107
|
+
|
|
108
|
+
const body = m[1];
|
|
109
|
+
const props = [];
|
|
110
|
+
const re = /['"]([^'"]+)['"]/g;
|
|
111
|
+
let match;
|
|
112
|
+
while ((match = re.exec(body)) !== null) {
|
|
113
|
+
props.push(match[1]);
|
|
114
|
+
}
|
|
115
|
+
return props;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// ── Props defaults extraction (AFTER type strip) ────────────────────
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Extract default values from the defineProps argument object.
|
|
122
|
+
* After type stripping, the generic form becomes defineProps({...}).
|
|
123
|
+
* The array form is defineProps([...]) — no defaults.
|
|
124
|
+
*
|
|
125
|
+
* Uses parenthesis depth counting to handle nested objects/arrays.
|
|
126
|
+
*
|
|
127
|
+
* @param {string} source
|
|
128
|
+
* @returns {Record<string, string>}
|
|
129
|
+
*/
|
|
130
|
+
export function extractPropsDefaults(source) {
|
|
131
|
+
const idx = source.indexOf('defineProps(');
|
|
132
|
+
if (idx === -1) return {};
|
|
133
|
+
|
|
134
|
+
const start = idx + 'defineProps('.length;
|
|
135
|
+
// Check what the argument starts with (skip whitespace)
|
|
136
|
+
let argStart = start;
|
|
137
|
+
while (argStart < source.length && /\s/.test(source[argStart])) argStart++;
|
|
138
|
+
|
|
139
|
+
// If it starts with '[', it's the array form — no defaults
|
|
140
|
+
if (source[argStart] === '[') return {};
|
|
141
|
+
|
|
142
|
+
// If it doesn't start with '{', no defaults (e.g., empty call)
|
|
143
|
+
if (source[argStart] !== '{') return {};
|
|
144
|
+
|
|
145
|
+
// Use depth counting to extract the full object literal
|
|
146
|
+
let depth = 0;
|
|
147
|
+
let i = argStart;
|
|
148
|
+
/** @type {string | null} */
|
|
149
|
+
let inString = null;
|
|
150
|
+
|
|
151
|
+
for (; i < source.length; i++) {
|
|
152
|
+
const ch = source[i];
|
|
153
|
+
|
|
154
|
+
if (inString) {
|
|
155
|
+
if (ch === '\\') { i++; continue; }
|
|
156
|
+
if (ch === inString) inString = null;
|
|
157
|
+
continue;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
if (ch === '"' || ch === "'" || ch === '`') {
|
|
161
|
+
inString = ch;
|
|
162
|
+
continue;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
if (ch === '{') depth++;
|
|
166
|
+
if (ch === '}') {
|
|
167
|
+
depth--;
|
|
168
|
+
if (depth === 0) { i++; break; }
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
const objLiteral = source.slice(argStart, i).trim();
|
|
173
|
+
// Remove outer braces
|
|
174
|
+
const inner = objLiteral.slice(1, -1).trim();
|
|
175
|
+
if (!inner) return {};
|
|
176
|
+
|
|
177
|
+
// Parse key: value pairs using depth counting
|
|
178
|
+
/** @type {Record<string, string>} */
|
|
179
|
+
const defaults = {};
|
|
180
|
+
let pos = 0;
|
|
181
|
+
while (pos < inner.length) {
|
|
182
|
+
// Skip whitespace
|
|
183
|
+
while (pos < inner.length && /\s/.test(inner[pos])) pos++;
|
|
184
|
+
if (pos >= inner.length) break;
|
|
185
|
+
|
|
186
|
+
// Extract key
|
|
187
|
+
const keyMatch = inner.slice(pos).match(/^(\w+)\s*:\s*/);
|
|
188
|
+
if (!keyMatch) break;
|
|
189
|
+
const key = keyMatch[1];
|
|
190
|
+
pos += keyMatch[0].length;
|
|
191
|
+
|
|
192
|
+
// Extract value using depth counting
|
|
193
|
+
let valDepth = 0;
|
|
194
|
+
let valStart = pos;
|
|
195
|
+
/** @type {string | null} */
|
|
196
|
+
let valInString = null;
|
|
197
|
+
|
|
198
|
+
for (; pos < inner.length; pos++) {
|
|
199
|
+
const ch = inner[pos];
|
|
200
|
+
|
|
201
|
+
if (valInString) {
|
|
202
|
+
if (ch === '\\') { pos++; continue; }
|
|
203
|
+
if (ch === valInString) valInString = null;
|
|
204
|
+
continue;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
if (ch === '"' || ch === "'" || ch === '`') {
|
|
208
|
+
valInString = ch;
|
|
209
|
+
continue;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
if (ch === '(' || ch === '[' || ch === '{') valDepth++;
|
|
213
|
+
if (ch === ')' || ch === ']' || ch === '}') valDepth--;
|
|
214
|
+
|
|
215
|
+
if (valDepth === 0 && ch === ',') {
|
|
216
|
+
break;
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
const value = inner.slice(valStart, pos).trim();
|
|
221
|
+
defaults[key] = value;
|
|
222
|
+
|
|
223
|
+
// Skip comma
|
|
224
|
+
if (pos < inner.length && inner[pos] === ',') pos++;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
return defaults;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// ── Props object name extraction ────────────────────────────────────
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* Extract the variable name from a props object binding.
|
|
234
|
+
* Pattern: const/let/var <identifier> = defineProps<...>(...) or defineProps(...)
|
|
235
|
+
*
|
|
236
|
+
* @param {string} source
|
|
237
|
+
* @returns {string | null}
|
|
238
|
+
*/
|
|
239
|
+
export function extractPropsObjectName(source) {
|
|
240
|
+
const m = source.match(/(?:const|let|var)\s+([$\w]+)\s*=\s*defineProps\s*[<(]/);
|
|
241
|
+
return m ? m[1] : null;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// ── Props validation ────────────────────────────────────────────────
|
|
245
|
+
|
|
246
|
+
/**
|
|
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
|
+
*
|
|
250
|
+
* @param {string} _source
|
|
251
|
+
* @param {string} _fileName
|
|
252
|
+
*/
|
|
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
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* Validate that there are no duplicate prop names.
|
|
260
|
+
*
|
|
261
|
+
* @param {string[]} propNames
|
|
262
|
+
* @param {string} fileName
|
|
263
|
+
*/
|
|
264
|
+
export function validateDuplicateProps(propNames, fileName) {
|
|
265
|
+
const seen = new Set();
|
|
266
|
+
const duplicates = new Set();
|
|
267
|
+
for (const p of propNames) {
|
|
268
|
+
if (seen.has(p)) duplicates.add(p);
|
|
269
|
+
seen.add(p);
|
|
270
|
+
}
|
|
271
|
+
if (duplicates.size > 0) {
|
|
272
|
+
const names = [...duplicates].join(', ');
|
|
273
|
+
const error = new Error(
|
|
274
|
+
`Error en '${fileName}': props duplicados: ${names}`
|
|
275
|
+
);
|
|
276
|
+
/** @ts-expect-error — custom error code for programmatic handling */
|
|
277
|
+
error.code = 'DUPLICATE_PROPS';
|
|
278
|
+
throw error;
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
/**
|
|
283
|
+
* Validate that the propsObjectName doesn't collide with signals, computeds, or constants.
|
|
284
|
+
*
|
|
285
|
+
* @param {string|null} propsObjectName
|
|
286
|
+
* @param {Set<string>} signalNames
|
|
287
|
+
* @param {Set<string>} computedNames
|
|
288
|
+
* @param {Set<string>} constantNames
|
|
289
|
+
* @param {string} fileName
|
|
290
|
+
*/
|
|
291
|
+
export function validatePropsConflicts(propsObjectName, signalNames, computedNames, constantNames, fileName) {
|
|
292
|
+
if (!propsObjectName) return;
|
|
293
|
+
|
|
294
|
+
if (signalNames.has(propsObjectName) || computedNames.has(propsObjectName) || constantNames.has(propsObjectName)) {
|
|
295
|
+
const error = new Error(
|
|
296
|
+
`Error en '${fileName}': '${propsObjectName}' colisiona con una declaración existente`
|
|
297
|
+
);
|
|
298
|
+
/** @ts-expect-error — custom error code for programmatic handling */
|
|
299
|
+
error.code = 'PROPS_OBJECT_CONFLICT';
|
|
300
|
+
throw error;
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
// ── Emits extraction (call signatures form — BEFORE type strip) ─────
|
|
305
|
+
|
|
306
|
+
/**
|
|
307
|
+
* Extract event names from the TypeScript call signatures form:
|
|
308
|
+
* defineEmits<{ (e: 'change', value: number): void; (e: 'reset'): void }>()
|
|
309
|
+
*
|
|
310
|
+
* Must be called BEFORE stripTypes() since esbuild removes generics.
|
|
311
|
+
*
|
|
312
|
+
* @param {string} source
|
|
313
|
+
* @returns {string[]}
|
|
314
|
+
*/
|
|
315
|
+
export function extractEmitsFromCallSignatures(source) {
|
|
316
|
+
const m = source.match(/defineEmits\s*<\s*\{([\s\S]*?)\}\s*>\s*\(\s*\)/);
|
|
317
|
+
if (!m) return [];
|
|
318
|
+
|
|
319
|
+
const body = m[1];
|
|
320
|
+
const emits = [];
|
|
321
|
+
const re = /\(\s*\w+\s*:\s*['"]([^'"]+)['"]/g;
|
|
322
|
+
let match;
|
|
323
|
+
while ((match = re.exec(body)) !== null) {
|
|
324
|
+
emits.push(match[1]);
|
|
325
|
+
}
|
|
326
|
+
return emits;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
// ── Emits extraction (array form — AFTER type strip) ────────────────
|
|
330
|
+
|
|
331
|
+
/**
|
|
332
|
+
* Extract event names from the array form:
|
|
333
|
+
* defineEmits(['change', 'reset'])
|
|
334
|
+
*
|
|
335
|
+
* Called AFTER type stripping.
|
|
336
|
+
*
|
|
337
|
+
* @param {string} source
|
|
338
|
+
* @returns {string[]}
|
|
339
|
+
*/
|
|
340
|
+
export function extractEmits(source) {
|
|
341
|
+
const m = source.match(/defineEmits\(\[([^\]]*)\]\)/);
|
|
342
|
+
if (!m) return [];
|
|
343
|
+
|
|
344
|
+
const body = m[1];
|
|
345
|
+
const emits = [];
|
|
346
|
+
const re = /['"]([^'"]+)['"]/g;
|
|
347
|
+
let match;
|
|
348
|
+
while ((match = re.exec(body)) !== null) {
|
|
349
|
+
emits.push(match[1]);
|
|
350
|
+
}
|
|
351
|
+
return emits;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
// ── Emits object name extraction ────────────────────────────────────
|
|
355
|
+
|
|
356
|
+
/**
|
|
357
|
+
* Extract the variable name from an emits object binding (AFTER type strip).
|
|
358
|
+
* Pattern: const/let/var <identifier> = defineEmits(...)
|
|
359
|
+
*
|
|
360
|
+
* @param {string} source
|
|
361
|
+
* @returns {string | null}
|
|
362
|
+
*/
|
|
363
|
+
export function extractEmitsObjectName(source) {
|
|
364
|
+
const m = source.match(/(?:const|let|var)\s+([$\w]+)\s*=\s*defineEmits\s*\(/);
|
|
365
|
+
return m ? m[1] : null;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
/**
|
|
369
|
+
* Extract the variable name from an emits object binding (BEFORE type strip, generic form).
|
|
370
|
+
* Pattern: const/let/var <identifier> = defineEmits<{...}>()
|
|
371
|
+
*
|
|
372
|
+
* @param {string} source
|
|
373
|
+
* @returns {string | null}
|
|
374
|
+
*/
|
|
375
|
+
export function extractEmitsObjectNameFromGeneric(source) {
|
|
376
|
+
const m = source.match(/(?:const|let|var)\s+([$\w]+)\s*=\s*defineEmits\s*<\s*\{/);
|
|
377
|
+
return m ? m[1] : null;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
// ── Emits validation ────────────────────────────────────────────────
|
|
381
|
+
|
|
382
|
+
/**
|
|
383
|
+
* Validate that defineEmits is assigned to a variable.
|
|
384
|
+
* Throws EMITS_ASSIGNMENT_REQUIRED if bare defineEmits() call detected.
|
|
385
|
+
*
|
|
386
|
+
* @param {string} source
|
|
387
|
+
* @param {string} fileName
|
|
388
|
+
*/
|
|
389
|
+
export function validateEmitsAssignment(source, fileName) {
|
|
390
|
+
// Check if defineEmits appears in source
|
|
391
|
+
if (!/defineEmits\s*[<(]/.test(source)) return;
|
|
392
|
+
|
|
393
|
+
// Check if it's assigned to a variable (either generic or non-generic form)
|
|
394
|
+
if (extractEmitsObjectName(source) !== null) return;
|
|
395
|
+
if (extractEmitsObjectNameFromGeneric(source) !== null) return;
|
|
396
|
+
|
|
397
|
+
const error = new Error(
|
|
398
|
+
`Error en '${fileName}': defineEmits() debe asignarse a una variable (const emit = defineEmits(...))`
|
|
399
|
+
);
|
|
400
|
+
/** @ts-expect-error — custom error code for programmatic handling */
|
|
401
|
+
error.code = 'EMITS_ASSIGNMENT_REQUIRED';
|
|
402
|
+
throw error;
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
/**
|
|
406
|
+
* Validate that there are no duplicate event names.
|
|
407
|
+
*
|
|
408
|
+
* @param {string[]} emitNames
|
|
409
|
+
* @param {string} fileName
|
|
410
|
+
*/
|
|
411
|
+
export function validateDuplicateEmits(emitNames, fileName) {
|
|
412
|
+
const seen = new Set();
|
|
413
|
+
const duplicates = new Set();
|
|
414
|
+
for (const e of emitNames) {
|
|
415
|
+
if (seen.has(e)) duplicates.add(e);
|
|
416
|
+
seen.add(e);
|
|
417
|
+
}
|
|
418
|
+
if (duplicates.size > 0) {
|
|
419
|
+
const names = [...duplicates].join(', ');
|
|
420
|
+
const error = new Error(
|
|
421
|
+
`Error en '${fileName}': emits duplicados: ${names}`
|
|
422
|
+
);
|
|
423
|
+
/** @ts-expect-error — custom error code for programmatic handling */
|
|
424
|
+
error.code = 'DUPLICATE_EMITS';
|
|
425
|
+
throw error;
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
/**
|
|
430
|
+
* Validate that the emitsObjectName doesn't collide with signals, computeds, constants, props, or propsObjectName.
|
|
431
|
+
*
|
|
432
|
+
* @param {string|null} emitsObjectName
|
|
433
|
+
* @param {Set<string>} signalNames
|
|
434
|
+
* @param {Set<string>} computedNames
|
|
435
|
+
* @param {Set<string>} constantNames
|
|
436
|
+
* @param {Set<string>} propNames
|
|
437
|
+
* @param {string|null} propsObjectName
|
|
438
|
+
* @param {string} fileName
|
|
439
|
+
*/
|
|
440
|
+
export function validateEmitsConflicts(emitsObjectName, signalNames, computedNames, constantNames, propNames, propsObjectName, fileName) {
|
|
441
|
+
if (!emitsObjectName) return;
|
|
442
|
+
|
|
443
|
+
if (
|
|
444
|
+
signalNames.has(emitsObjectName) ||
|
|
445
|
+
computedNames.has(emitsObjectName) ||
|
|
446
|
+
constantNames.has(emitsObjectName) ||
|
|
447
|
+
propNames.has(emitsObjectName) ||
|
|
448
|
+
(propsObjectName && emitsObjectName === propsObjectName)
|
|
449
|
+
) {
|
|
450
|
+
const error = new Error(
|
|
451
|
+
`Error en '${fileName}': '${emitsObjectName}' colisiona con una declaración existente`
|
|
452
|
+
);
|
|
453
|
+
/** @ts-expect-error — custom error code for programmatic handling */
|
|
454
|
+
error.code = 'EMITS_OBJECT_CONFLICT';
|
|
455
|
+
throw error;
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
/**
|
|
460
|
+
* Escape special regex characters in a string.
|
|
461
|
+
*
|
|
462
|
+
* @param {string} str
|
|
463
|
+
* @returns {string}
|
|
464
|
+
*/
|
|
465
|
+
export function escapeRegex(str) {
|
|
466
|
+
return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
/**
|
|
470
|
+
* Validate that all emit calls use declared event names.
|
|
471
|
+
*
|
|
472
|
+
* @param {string} source
|
|
473
|
+
* @param {string|null} emitsObjectName
|
|
474
|
+
* @param {string[]} emits
|
|
475
|
+
* @param {string} fileName
|
|
476
|
+
*/
|
|
477
|
+
export function validateUndeclaredEmits(source, emitsObjectName, emits, fileName) {
|
|
478
|
+
if (!emitsObjectName || emits.length === 0) return;
|
|
479
|
+
|
|
480
|
+
const emitsSet = new Set(emits);
|
|
481
|
+
const re = new RegExp(`\\b${escapeRegex(emitsObjectName)}\\(\\s*['"]([^'"]+)['"]`, 'g');
|
|
482
|
+
let match;
|
|
483
|
+
while ((match = re.exec(source)) !== null) {
|
|
484
|
+
const eventName = match[1];
|
|
485
|
+
if (!emitsSet.has(eventName)) {
|
|
486
|
+
const error = new Error(
|
|
487
|
+
`Error en '${fileName}': emit no declarado: '${eventName}'`
|
|
488
|
+
);
|
|
489
|
+
/** @ts-expect-error — custom error code for programmatic handling */
|
|
490
|
+
error.code = 'UNDECLARED_EMIT';
|
|
491
|
+
throw error;
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
// ── defineComponent extraction ──────────────────────────────────────
|
|
497
|
+
|
|
498
|
+
/**
|
|
499
|
+
* Extract defineComponent({ tag, template, styles }) from source.
|
|
500
|
+
*
|
|
501
|
+
* @param {string} source
|
|
502
|
+
* @returns {{ tag: string, template: string, styles: string | null }}
|
|
503
|
+
*/
|
|
504
|
+
export function extractDefineComponent(source) {
|
|
505
|
+
const m = source.match(/defineComponent\(\s*\{([^}]*)\}\s*\)/);
|
|
506
|
+
if (!m) return null;
|
|
507
|
+
|
|
508
|
+
const body = m[1];
|
|
509
|
+
|
|
510
|
+
const tagMatch = body.match(/tag\s*:\s*['"]([^'"]+)['"]/);
|
|
511
|
+
const templateMatch = body.match(/template\s*:\s*['"]([^'"]+)['"]/);
|
|
512
|
+
const stylesMatch = body.match(/styles\s*:\s*['"]([^'"]+)['"]/);
|
|
513
|
+
|
|
514
|
+
if (!tagMatch || !templateMatch) return null;
|
|
515
|
+
|
|
516
|
+
return {
|
|
517
|
+
tag: tagMatch[1],
|
|
518
|
+
template: templateMatch[1],
|
|
519
|
+
styles: stylesMatch ? stylesMatch[1] : null,
|
|
520
|
+
};
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
// ── Signal extraction ───────────────────────────────────────────────
|
|
524
|
+
|
|
525
|
+
/**
|
|
526
|
+
* Extract the argument of a `signal(...)` call starting at a given position.
|
|
527
|
+
* Uses parenthesis depth counting to correctly handle nested parentheses,
|
|
528
|
+
* e.g. `signal([1, 2, 3])` or `signal((a + b) * c)`.
|
|
529
|
+
* Also handles string literals so that parentheses inside strings are not counted.
|
|
530
|
+
*
|
|
531
|
+
* @param {string} source - Source code starting from after `signal(`
|
|
532
|
+
* @param {number} startIdx - Index right after `signal(`
|
|
533
|
+
* @returns {string} The trimmed argument string, or 'undefined' if empty
|
|
534
|
+
*/
|
|
535
|
+
export function extractSignalArgument(source, startIdx) {
|
|
536
|
+
let depth = 0;
|
|
537
|
+
let i = startIdx;
|
|
538
|
+
/** @type {string | null} */
|
|
539
|
+
let inString = null;
|
|
540
|
+
|
|
541
|
+
for (; i < source.length; i++) {
|
|
542
|
+
const ch = source[i];
|
|
543
|
+
|
|
544
|
+
// Handle string literal boundaries
|
|
545
|
+
if (inString) {
|
|
546
|
+
if (ch === '\\') {
|
|
547
|
+
i++; // skip escaped character
|
|
548
|
+
continue;
|
|
549
|
+
}
|
|
550
|
+
if (ch === inString) {
|
|
551
|
+
inString = null;
|
|
552
|
+
}
|
|
553
|
+
continue;
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
if (ch === '"' || ch === "'" || ch === '`') {
|
|
557
|
+
inString = ch;
|
|
558
|
+
continue;
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
if (ch === '(') depth++;
|
|
562
|
+
if (ch === ')') {
|
|
563
|
+
if (depth === 0) break;
|
|
564
|
+
depth--;
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
return source.slice(startIdx, i).trim() || 'undefined';
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
/**
|
|
572
|
+
* Extract signal declarations from source.
|
|
573
|
+
* Pattern: const/let/var name = signal(value)
|
|
574
|
+
*
|
|
575
|
+
* @param {string} source
|
|
576
|
+
* @returns {ReactiveVar[]}
|
|
577
|
+
*/
|
|
578
|
+
export function extractSignals(source) {
|
|
579
|
+
/** @type {ReactiveVar[]} */
|
|
580
|
+
const signals = [];
|
|
581
|
+
const re = /(?:const|let|var)\s+([$\w]+)\s*=\s*signal\(/g;
|
|
582
|
+
let m;
|
|
583
|
+
|
|
584
|
+
while ((m = re.exec(source)) !== null) {
|
|
585
|
+
const name = m[1];
|
|
586
|
+
const argStart = m.index + m[0].length;
|
|
587
|
+
const value = extractSignalArgument(source, argStart);
|
|
588
|
+
signals.push({ name, value });
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
return signals;
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
// ── Constant extraction ─────────────────────────────────────────────
|
|
595
|
+
|
|
596
|
+
/**
|
|
597
|
+
* Known macro/reactive call patterns that should NOT be treated as constants.
|
|
598
|
+
*/
|
|
599
|
+
export const REACTIVE_CALLS = /\b(?:signal|computed|effect|watch|defineProps|defineEmits|defineModel|defineComponent|templateRef|defineExpose|onMount|onDestroy)\s*[<(]/;
|
|
600
|
+
|
|
601
|
+
/**
|
|
602
|
+
* Extract plain const/let/var declarations that are NOT reactive calls.
|
|
603
|
+
* Only extracts root-level declarations (depth 0).
|
|
604
|
+
*
|
|
605
|
+
* @param {string} source
|
|
606
|
+
* @returns {import('./types.js').ConstantVar[]}
|
|
607
|
+
*/
|
|
608
|
+
export function extractConstants(source) {
|
|
609
|
+
/** @type {import('./types.js').ConstantVar[]} */
|
|
610
|
+
const constants = [];
|
|
611
|
+
let depth = 0;
|
|
612
|
+
|
|
613
|
+
for (const line of source.split('\n')) {
|
|
614
|
+
// Track brace depth to skip nested blocks
|
|
615
|
+
for (const ch of line) {
|
|
616
|
+
if (ch === '{') depth++;
|
|
617
|
+
if (ch === '}') depth--;
|
|
618
|
+
}
|
|
619
|
+
if (depth > 0) continue;
|
|
620
|
+
|
|
621
|
+
// Match const/let/var name = value at root level
|
|
622
|
+
const m = line.match(/^\s*(?:const|let|var)\s+([$\w]+)\s*=\s*(.+?);?\s*$/);
|
|
623
|
+
if (!m) continue;
|
|
624
|
+
|
|
625
|
+
const value = m[2].trim();
|
|
626
|
+
// Skip reactive/macro calls
|
|
627
|
+
if (REACTIVE_CALLS.test(value)) continue;
|
|
628
|
+
// Skip export default
|
|
629
|
+
if (/^\s*export\s+default/.test(line)) continue;
|
|
630
|
+
|
|
631
|
+
constants.push({ name: m[1], value });
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
return constants;
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
// ── Computed extraction ─────────────────────────────────────────────
|
|
638
|
+
|
|
639
|
+
/**
|
|
640
|
+
* Extract computed declarations from source.
|
|
641
|
+
* Pattern: const/let/var name = computed(() => expr)
|
|
642
|
+
* Uses parenthesis depth counting to handle expressions containing parens,
|
|
643
|
+
* e.g. `computed(() => count() * 2)`.
|
|
644
|
+
*
|
|
645
|
+
* @param {string} source
|
|
646
|
+
* @returns {ComputedDef[]}
|
|
647
|
+
*/
|
|
648
|
+
export function extractComputeds(source) {
|
|
649
|
+
/** @type {ComputedDef[]} */
|
|
650
|
+
const out = [];
|
|
651
|
+
const re = /(?:const|let|var)\s+(\w+)\s*=\s*computed\(\s*\(\)\s*=>\s*/g;
|
|
652
|
+
let m;
|
|
653
|
+
while ((m = re.exec(source)) !== null) {
|
|
654
|
+
const name = m[1];
|
|
655
|
+
const bodyStart = m.index + m[0].length;
|
|
656
|
+
// Use depth counting: we're inside `computed(` so depth starts at 1
|
|
657
|
+
// We need to find the matching `)` for the outer `computed(` call
|
|
658
|
+
let depth = 1;
|
|
659
|
+
let i = bodyStart;
|
|
660
|
+
/** @type {string | null} */
|
|
661
|
+
let inString = null;
|
|
662
|
+
|
|
663
|
+
for (; i < source.length; i++) {
|
|
664
|
+
const ch = source[i];
|
|
665
|
+
|
|
666
|
+
if (inString) {
|
|
667
|
+
if (ch === '\\') { i++; continue; }
|
|
668
|
+
if (ch === inString) inString = null;
|
|
669
|
+
continue;
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
if (ch === '"' || ch === "'" || ch === '`') {
|
|
673
|
+
inString = ch;
|
|
674
|
+
continue;
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
if (ch === '(') depth++;
|
|
678
|
+
if (ch === ')') {
|
|
679
|
+
depth--;
|
|
680
|
+
if (depth === 0) break;
|
|
681
|
+
}
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
const body = source.slice(bodyStart, i).trim();
|
|
685
|
+
if (body) {
|
|
686
|
+
out.push({ name, body });
|
|
687
|
+
}
|
|
688
|
+
}
|
|
689
|
+
return out;
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
// ── Effect extraction ───────────────────────────────────────────────
|
|
693
|
+
|
|
694
|
+
/**
|
|
695
|
+
* Extract effect declarations from source.
|
|
696
|
+
* Pattern: effect(() => { body })
|
|
697
|
+
* Uses brace depth tracking to capture multi-line bodies.
|
|
698
|
+
*
|
|
699
|
+
* @param {string} source
|
|
700
|
+
* @returns {EffectDef[]}
|
|
701
|
+
*/
|
|
702
|
+
export function extractEffects(source) {
|
|
703
|
+
/** @type {EffectDef[]} */
|
|
704
|
+
const effects = [];
|
|
705
|
+
const lines = source.split('\n');
|
|
706
|
+
let i = 0;
|
|
707
|
+
|
|
708
|
+
while (i < lines.length) {
|
|
709
|
+
const line = lines[i];
|
|
710
|
+
const effectMatch = line.match(/\beffect\s*\(\s*\(\s*\)\s*=>\s*\{/);
|
|
711
|
+
|
|
712
|
+
if (effectMatch) {
|
|
713
|
+
// Collect body by tracking brace depth
|
|
714
|
+
let depth = 0;
|
|
715
|
+
let bodyLines = [];
|
|
716
|
+
let started = false;
|
|
717
|
+
|
|
718
|
+
for (let j = i; j < lines.length; j++) {
|
|
719
|
+
const l = lines[j];
|
|
720
|
+
for (const ch of l) {
|
|
721
|
+
if (ch === '{') {
|
|
722
|
+
if (started) depth++;
|
|
723
|
+
else { depth = 1; started = true; }
|
|
724
|
+
}
|
|
725
|
+
if (ch === '}') depth--;
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
if (j === i) {
|
|
729
|
+
// First line: capture everything after the opening brace
|
|
730
|
+
const braceIdx = l.indexOf('{');
|
|
731
|
+
const afterBrace = l.substring(braceIdx + 1);
|
|
732
|
+
if (afterBrace.trim()) bodyLines.push(afterBrace);
|
|
733
|
+
} else if (depth <= 0) {
|
|
734
|
+
// Last line: capture everything before the closing brace
|
|
735
|
+
const lastBraceIdx = l.lastIndexOf('}');
|
|
736
|
+
const before = l.substring(0, lastBraceIdx);
|
|
737
|
+
if (before.trim()) bodyLines.push(before);
|
|
738
|
+
i = j;
|
|
739
|
+
break;
|
|
740
|
+
} else {
|
|
741
|
+
bodyLines.push(l);
|
|
742
|
+
}
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
// Dedent body lines
|
|
746
|
+
const nonEmptyLines = bodyLines.filter(l => l.trim().length > 0);
|
|
747
|
+
let minIndent = Infinity;
|
|
748
|
+
for (const bl of nonEmptyLines) {
|
|
749
|
+
const leadingSpaces = bl.match(/^(\s*)/)[1].length;
|
|
750
|
+
if (leadingSpaces < minIndent) minIndent = leadingSpaces;
|
|
751
|
+
}
|
|
752
|
+
if (minIndent === Infinity) minIndent = 0;
|
|
753
|
+
const dedentedLines = bodyLines.map(bl => bl.substring(minIndent));
|
|
754
|
+
const body = dedentedLines.join('\n').trim();
|
|
755
|
+
|
|
756
|
+
effects.push({ body });
|
|
757
|
+
}
|
|
758
|
+
i++;
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
return effects;
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
// ── Watcher extraction ──────────────────────────────────────────────
|
|
765
|
+
|
|
766
|
+
/**
|
|
767
|
+
* Extract watch() declarations from source.
|
|
768
|
+
* Supports two forms:
|
|
769
|
+
* Form 1 — Signal direct: watch(count, (newVal, oldVal) => { body })
|
|
770
|
+
* Form 2 — Getter function: watch(() => expr, (newVal, oldVal) => { body })
|
|
771
|
+
* Uses brace depth tracking to capture multi-line bodies.
|
|
772
|
+
*
|
|
773
|
+
* @param {string} source
|
|
774
|
+
* @returns {import('./types.js').WatcherDef[]}
|
|
775
|
+
*/
|
|
776
|
+
export function extractWatchers(source) {
|
|
777
|
+
/** @type {import('./types.js').WatcherDef[]} */
|
|
778
|
+
const watchers = [];
|
|
779
|
+
const lines = source.split('\n');
|
|
780
|
+
let i = 0;
|
|
781
|
+
|
|
782
|
+
while (i < lines.length) {
|
|
783
|
+
const line = lines[i];
|
|
784
|
+
|
|
785
|
+
// Form 2 — Getter function: watch(() => expr, (newVal, oldVal) => { OR watch(() => expr, (newVal) => {
|
|
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) => { OR watch(identifier, (newVal) => {
|
|
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;
|
|
791
|
+
|
|
792
|
+
if (m) {
|
|
793
|
+
const kind = mGetter ? 'getter' : 'signal';
|
|
794
|
+
const target = m[1];
|
|
795
|
+
const newParam = m[2];
|
|
796
|
+
const oldParam = m[3];
|
|
797
|
+
|
|
798
|
+
// Collect body by tracking brace depth
|
|
799
|
+
let depth = 0;
|
|
800
|
+
let bodyLines = [];
|
|
801
|
+
let started = false;
|
|
802
|
+
|
|
803
|
+
for (let j = i; j < lines.length; j++) {
|
|
804
|
+
const l = lines[j];
|
|
805
|
+
for (const ch of l) {
|
|
806
|
+
if (ch === '{') {
|
|
807
|
+
if (started) depth++;
|
|
808
|
+
else { depth = 1; started = true; }
|
|
809
|
+
}
|
|
810
|
+
if (ch === '}') depth--;
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
if (j === i) {
|
|
814
|
+
const braceIdx = l.indexOf('{');
|
|
815
|
+
const afterBrace = l.substring(braceIdx + 1);
|
|
816
|
+
if (depth <= 0) {
|
|
817
|
+
const lastBraceIdx = l.lastIndexOf('}');
|
|
818
|
+
const inner = l.substring(braceIdx + 1, lastBraceIdx);
|
|
819
|
+
if (inner.trim()) bodyLines.push(inner);
|
|
820
|
+
i = j;
|
|
821
|
+
break;
|
|
822
|
+
}
|
|
823
|
+
if (afterBrace.trim()) bodyLines.push(afterBrace);
|
|
824
|
+
} else if (depth <= 0) {
|
|
825
|
+
const lastBraceIdx = l.lastIndexOf('}');
|
|
826
|
+
const before = l.substring(0, lastBraceIdx);
|
|
827
|
+
if (before.trim()) bodyLines.push(before);
|
|
828
|
+
i = j;
|
|
829
|
+
break;
|
|
830
|
+
} else {
|
|
831
|
+
bodyLines.push(l);
|
|
832
|
+
}
|
|
833
|
+
}
|
|
834
|
+
|
|
835
|
+
// Dedent
|
|
836
|
+
const nonEmpty = bodyLines.filter(l => l.trim().length > 0);
|
|
837
|
+
let minIndent = Infinity;
|
|
838
|
+
for (const bl of nonEmpty) {
|
|
839
|
+
const leading = bl.match(/^(\s*)/)[1].length;
|
|
840
|
+
if (leading < minIndent) minIndent = leading;
|
|
841
|
+
}
|
|
842
|
+
if (minIndent === Infinity) minIndent = 0;
|
|
843
|
+
const body = bodyLines.map(bl => bl.substring(minIndent)).join('\n').trim();
|
|
844
|
+
|
|
845
|
+
watchers.push({ kind, target, newParam, oldParam, body });
|
|
846
|
+
}
|
|
847
|
+
i++;
|
|
848
|
+
}
|
|
849
|
+
|
|
850
|
+
return watchers;
|
|
851
|
+
}
|
|
852
|
+
|
|
853
|
+
// ── Function extraction ─────────────────────────────────────────────
|
|
854
|
+
|
|
855
|
+
/**
|
|
856
|
+
* Extract top-level function declarations from source.
|
|
857
|
+
* Pattern: function name(params) { body }
|
|
858
|
+
* Uses brace depth tracking to capture the full function body.
|
|
859
|
+
*
|
|
860
|
+
* @param {string} source
|
|
861
|
+
* @returns {MethodDef[]}
|
|
862
|
+
*/
|
|
863
|
+
export function extractFunctions(source) {
|
|
864
|
+
/** @type {MethodDef[]} */
|
|
865
|
+
const functions = [];
|
|
866
|
+
const lines = source.split('\n');
|
|
867
|
+
let i = 0;
|
|
868
|
+
|
|
869
|
+
while (i < lines.length) {
|
|
870
|
+
const line = lines[i];
|
|
871
|
+
const m = line.match(/^\s*function\s+(\w+)\s*\(([^)]*)\)\s*\{/);
|
|
872
|
+
if (m) {
|
|
873
|
+
const name = m[1];
|
|
874
|
+
const params = m[2].trim();
|
|
875
|
+
// Collect body by tracking brace depth
|
|
876
|
+
let depth = 0;
|
|
877
|
+
let bodyLines = [];
|
|
878
|
+
let started = false;
|
|
879
|
+
|
|
880
|
+
for (let j = i; j < lines.length; j++) {
|
|
881
|
+
const l = lines[j];
|
|
882
|
+
for (const ch of l) {
|
|
883
|
+
if (ch === '{') {
|
|
884
|
+
if (started) depth++;
|
|
885
|
+
else { depth = 1; started = true; }
|
|
886
|
+
}
|
|
887
|
+
if (ch === '}') depth--;
|
|
888
|
+
}
|
|
889
|
+
|
|
890
|
+
if (j === i) {
|
|
891
|
+
// First line: capture everything after the opening brace
|
|
892
|
+
const afterBrace = l.substring(l.indexOf('{') + 1);
|
|
893
|
+
// Single-line function: depth already closed on first line
|
|
894
|
+
if (depth <= 0) {
|
|
895
|
+
const lastBraceIdx = afterBrace.lastIndexOf('}');
|
|
896
|
+
const inner = lastBraceIdx >= 0 ? afterBrace.substring(0, lastBraceIdx) : afterBrace;
|
|
897
|
+
if (inner.trim()) bodyLines.push(inner);
|
|
898
|
+
i = j;
|
|
899
|
+
break;
|
|
900
|
+
}
|
|
901
|
+
if (afterBrace.trim()) bodyLines.push(afterBrace);
|
|
902
|
+
} else if (depth <= 0) {
|
|
903
|
+
// Last line: capture everything before the closing brace
|
|
904
|
+
const lastBraceIdx = l.lastIndexOf('}');
|
|
905
|
+
const before = l.substring(0, lastBraceIdx);
|
|
906
|
+
if (before.trim()) bodyLines.push(before);
|
|
907
|
+
i = j;
|
|
908
|
+
break;
|
|
909
|
+
} else {
|
|
910
|
+
bodyLines.push(l);
|
|
911
|
+
}
|
|
912
|
+
}
|
|
913
|
+
|
|
914
|
+
functions.push({
|
|
915
|
+
name,
|
|
916
|
+
params,
|
|
917
|
+
body: bodyLines.join('\n').trim(),
|
|
918
|
+
});
|
|
919
|
+
}
|
|
920
|
+
i++;
|
|
921
|
+
}
|
|
922
|
+
|
|
923
|
+
return functions;
|
|
924
|
+
}
|
|
925
|
+
|
|
926
|
+
// ── Lifecycle hook extraction ────────────────────────────────────────
|
|
927
|
+
|
|
928
|
+
/**
|
|
929
|
+
* Extract lifecycle hooks from the script.
|
|
930
|
+
* Patterns: onMount(() => { body }) and onDestroy(() => { body })
|
|
931
|
+
* Supports multiple calls of each type.
|
|
932
|
+
* Uses brace depth tracking to capture multi-line bodies.
|
|
933
|
+
* Only extracts top-level calls (brace depth === 0 when the call is encountered).
|
|
934
|
+
*
|
|
935
|
+
* @param {string} script - The script content (after type stripping)
|
|
936
|
+
* @returns {{ onMountHooks: LifecycleHook[], onDestroyHooks: LifecycleHook[], onAdoptHooks: LifecycleHook[] }}
|
|
937
|
+
*/
|
|
938
|
+
export function extractLifecycleHooks(script) {
|
|
939
|
+
/** @type {LifecycleHook[]} */
|
|
940
|
+
const onMountHooks = [];
|
|
941
|
+
/** @type {LifecycleHook[]} */
|
|
942
|
+
const onDestroyHooks = [];
|
|
943
|
+
/** @type {LifecycleHook[]} */
|
|
944
|
+
const onAdoptHooks = [];
|
|
945
|
+
const lines = script.split('\n');
|
|
946
|
+
let i = 0;
|
|
947
|
+
|
|
948
|
+
while (i < lines.length) {
|
|
949
|
+
const line = lines[i];
|
|
950
|
+
const mountMatch = line.match(/\bonMount\s*\(\s*(?:async\s*)?\(\s*\)\s*=>\s*\{/);
|
|
951
|
+
const destroyMatch = line.match(/\bonDestroy\s*\(\s*(?:async\s*)?\(\s*\)\s*=>\s*\{/);
|
|
952
|
+
const adoptMatch = line.match(/\bonAdopt\s*\(\s*(?:async\s*)?\(\s*\)\s*=>\s*\{/);
|
|
953
|
+
|
|
954
|
+
if (mountMatch || destroyMatch || adoptMatch) {
|
|
955
|
+
// Detect if the callback is async
|
|
956
|
+
const isAsync = /\basync\s*\(/.test(line);
|
|
957
|
+
|
|
958
|
+
// Collect body by tracking brace depth
|
|
959
|
+
let depth = 0;
|
|
960
|
+
let bodyLines = [];
|
|
961
|
+
let started = false;
|
|
962
|
+
|
|
963
|
+
for (let j = i; j < lines.length; j++) {
|
|
964
|
+
const l = lines[j];
|
|
965
|
+
for (const ch of l) {
|
|
966
|
+
if (ch === '{') {
|
|
967
|
+
if (started) depth++;
|
|
968
|
+
else { depth = 1; started = true; }
|
|
969
|
+
}
|
|
970
|
+
if (ch === '}') depth--;
|
|
971
|
+
}
|
|
972
|
+
|
|
973
|
+
if (j === i) {
|
|
974
|
+
// First line: capture everything after the opening brace
|
|
975
|
+
const braceIdx = l.indexOf('{');
|
|
976
|
+
const afterBrace = l.substring(braceIdx + 1);
|
|
977
|
+
// If depth already closed on the first line (single-line hook)
|
|
978
|
+
if (depth <= 0) {
|
|
979
|
+
// Extract content between first { and last }
|
|
980
|
+
const lastBraceIdx = l.lastIndexOf('}');
|
|
981
|
+
const inner = l.substring(braceIdx + 1, lastBraceIdx);
|
|
982
|
+
if (inner.trim()) bodyLines.push(inner);
|
|
983
|
+
i = j;
|
|
984
|
+
break;
|
|
985
|
+
}
|
|
986
|
+
if (afterBrace.trim()) bodyLines.push(afterBrace);
|
|
987
|
+
} else if (depth <= 0) {
|
|
988
|
+
// Last line: capture everything before the closing brace
|
|
989
|
+
const lastBraceIdx = l.lastIndexOf('}');
|
|
990
|
+
const before = l.substring(0, lastBraceIdx);
|
|
991
|
+
if (before.trim()) bodyLines.push(before);
|
|
992
|
+
i = j;
|
|
993
|
+
break;
|
|
994
|
+
} else {
|
|
995
|
+
bodyLines.push(l);
|
|
996
|
+
}
|
|
997
|
+
}
|
|
998
|
+
|
|
999
|
+
// Dedent body lines: remove common leading whitespace
|
|
1000
|
+
const nonEmptyLines = bodyLines.filter(l => l.trim().length > 0);
|
|
1001
|
+
let minIndent = Infinity;
|
|
1002
|
+
for (const bl of nonEmptyLines) {
|
|
1003
|
+
const leadingSpaces = bl.match(/^(\s*)/)[1].length;
|
|
1004
|
+
if (leadingSpaces < minIndent) minIndent = leadingSpaces;
|
|
1005
|
+
}
|
|
1006
|
+
if (minIndent === Infinity) minIndent = 0;
|
|
1007
|
+
const dedentedLines = bodyLines.map(bl => bl.substring(minIndent));
|
|
1008
|
+
const body = dedentedLines.join('\n').trim();
|
|
1009
|
+
|
|
1010
|
+
if (mountMatch) {
|
|
1011
|
+
onMountHooks.push({ body, async: isAsync });
|
|
1012
|
+
} else if (destroyMatch) {
|
|
1013
|
+
onDestroyHooks.push({ body, async: isAsync });
|
|
1014
|
+
} else {
|
|
1015
|
+
onAdoptHooks.push({ body, async: isAsync });
|
|
1016
|
+
}
|
|
1017
|
+
}
|
|
1018
|
+
i++;
|
|
1019
|
+
}
|
|
1020
|
+
|
|
1021
|
+
return { onMountHooks, onDestroyHooks, onAdoptHooks };
|
|
1022
|
+
}
|
|
1023
|
+
|
|
1024
|
+
// ── Ref extraction ───────────────────────────────────────────────────
|
|
1025
|
+
|
|
1026
|
+
/**
|
|
1027
|
+
* Extract templateRef('name') declarations from component source.
|
|
1028
|
+
* Pattern: const/let/var varName = templateRef('refName') or templateRef("refName")
|
|
1029
|
+
*
|
|
1030
|
+
* @param {string} source — Stripped source code
|
|
1031
|
+
* @returns {RefDeclaration[]}
|
|
1032
|
+
*/
|
|
1033
|
+
export function extractRefs(source) {
|
|
1034
|
+
/** @type {RefDeclaration[]} */
|
|
1035
|
+
const refs = [];
|
|
1036
|
+
const re = /(?:const|let|var)\s+([$\w]+)\s*=\s*templateRef\(\s*['"]([^'"]+)['"]\s*\)/g;
|
|
1037
|
+
let m;
|
|
1038
|
+
while ((m = re.exec(source)) !== null) {
|
|
1039
|
+
refs.push({ varName: m[1], refName: m[2] });
|
|
1040
|
+
}
|
|
1041
|
+
return refs;
|
|
1042
|
+
}
|
|
1043
|
+
|
|
1044
|
+
// ── defineModel extraction ───────────────────────────────────────────
|
|
1045
|
+
|
|
1046
|
+
/**
|
|
1047
|
+
* Extract defineModel() declarations from source.
|
|
1048
|
+
* Pattern: const/let/var varName = defineModel({ name: 'propName', default: value })
|
|
1049
|
+
* const/let/var varName = defineModel({ name: 'propName', required: true })
|
|
1050
|
+
*
|
|
1051
|
+
* @param {string} source
|
|
1052
|
+
* @returns {{ varName: string, name: string, default: string, required: boolean }[]}
|
|
1053
|
+
*/
|
|
1054
|
+
export function extractModels(source) {
|
|
1055
|
+
/** @type {{ varName: string, name: string, default: string, required: boolean }[]} */
|
|
1056
|
+
const models = [];
|
|
1057
|
+
const re = /(?:const|let|var)\s+(\w+)\s*=\s*defineModel\(\s*\{/g;
|
|
1058
|
+
let m;
|
|
1059
|
+
|
|
1060
|
+
while ((m = re.exec(source)) !== null) {
|
|
1061
|
+
const varName = m[1];
|
|
1062
|
+
const objStart = m.index + m[0].length - 1; // position of '{'
|
|
1063
|
+
|
|
1064
|
+
// Use depth counting to extract the full object literal
|
|
1065
|
+
let depth = 0;
|
|
1066
|
+
let i = objStart;
|
|
1067
|
+
/** @type {string | null} */
|
|
1068
|
+
let inString = null;
|
|
1069
|
+
|
|
1070
|
+
for (; i < source.length; i++) {
|
|
1071
|
+
const ch = source[i];
|
|
1072
|
+
|
|
1073
|
+
if (inString) {
|
|
1074
|
+
if (ch === '\\') { i++; continue; }
|
|
1075
|
+
if (ch === inString) inString = null;
|
|
1076
|
+
continue;
|
|
1077
|
+
}
|
|
1078
|
+
|
|
1079
|
+
if (ch === '"' || ch === "'" || ch === '`') {
|
|
1080
|
+
inString = ch;
|
|
1081
|
+
continue;
|
|
1082
|
+
}
|
|
1083
|
+
|
|
1084
|
+
if (ch === '{') depth++;
|
|
1085
|
+
if (ch === '}') {
|
|
1086
|
+
depth--;
|
|
1087
|
+
if (depth === 0) { i++; break; }
|
|
1088
|
+
}
|
|
1089
|
+
}
|
|
1090
|
+
|
|
1091
|
+
const objLiteral = source.slice(objStart, i).trim();
|
|
1092
|
+
// Remove outer braces
|
|
1093
|
+
const inner = objLiteral.slice(1, -1).trim();
|
|
1094
|
+
|
|
1095
|
+
// Extract 'name' property
|
|
1096
|
+
const nameMatch = inner.match(/name\s*:\s*['"]([^'"]+)['"]/);
|
|
1097
|
+
const propName = nameMatch ? nameMatch[1] : '';
|
|
1098
|
+
|
|
1099
|
+
// Extract 'default' property using depth counting
|
|
1100
|
+
let defaultValue = 'undefined';
|
|
1101
|
+
const defaultIdx = inner.search(/\bdefault\s*:\s*/);
|
|
1102
|
+
if (defaultIdx !== -1) {
|
|
1103
|
+
const afterDefault = inner.slice(defaultIdx);
|
|
1104
|
+
const colonMatch = afterDefault.match(/^default\s*:\s*/);
|
|
1105
|
+
if (colonMatch) {
|
|
1106
|
+
const valStart = defaultIdx + colonMatch[0].length;
|
|
1107
|
+
let valDepth = 0;
|
|
1108
|
+
let pos = valStart;
|
|
1109
|
+
/** @type {string | null} */
|
|
1110
|
+
let valInString = null;
|
|
1111
|
+
|
|
1112
|
+
for (; pos < inner.length; pos++) {
|
|
1113
|
+
const ch = inner[pos];
|
|
1114
|
+
|
|
1115
|
+
if (valInString) {
|
|
1116
|
+
if (ch === '\\') { pos++; continue; }
|
|
1117
|
+
if (ch === valInString) valInString = null;
|
|
1118
|
+
continue;
|
|
1119
|
+
}
|
|
1120
|
+
|
|
1121
|
+
if (ch === '"' || ch === "'" || ch === '`') {
|
|
1122
|
+
valInString = ch;
|
|
1123
|
+
continue;
|
|
1124
|
+
}
|
|
1125
|
+
|
|
1126
|
+
if (ch === '(' || ch === '[' || ch === '{') valDepth++;
|
|
1127
|
+
if (ch === ')' || ch === ']' || ch === '}') valDepth--;
|
|
1128
|
+
|
|
1129
|
+
if (valDepth === 0 && ch === ',') {
|
|
1130
|
+
break;
|
|
1131
|
+
}
|
|
1132
|
+
}
|
|
1133
|
+
|
|
1134
|
+
defaultValue = inner.slice(valStart, pos).trim();
|
|
1135
|
+
}
|
|
1136
|
+
}
|
|
1137
|
+
|
|
1138
|
+
// Extract 'required' property
|
|
1139
|
+
const requiredMatch = inner.match(/required\s*:\s*true/);
|
|
1140
|
+
const required = !!requiredMatch;
|
|
1141
|
+
|
|
1142
|
+
models.push({ varName, name: propName, default: defaultValue, required });
|
|
1143
|
+
}
|
|
1144
|
+
|
|
1145
|
+
return models;
|
|
1146
|
+
}
|
|
1147
|
+
|
|
1148
|
+
// ── defineExpose extraction ─────────────────────────────────────────
|
|
1149
|
+
|
|
1150
|
+
/**
|
|
1151
|
+
* Extract property names from defineExpose({ key1, key2, ... }).
|
|
1152
|
+
* Supports shorthand properties: defineExpose({ doubled, handleUpdate })
|
|
1153
|
+
*
|
|
1154
|
+
* @param {string} source — Source code (after type stripping)
|
|
1155
|
+
* @returns {string[]} Array of exposed property names
|
|
1156
|
+
*/
|
|
1157
|
+
export function extractExpose(source) {
|
|
1158
|
+
const m = source.match(/defineExpose\(\s*\{([^}]*)\}\s*\)/);
|
|
1159
|
+
if (!m) return [];
|
|
1160
|
+
|
|
1161
|
+
const body = m[1];
|
|
1162
|
+
const names = [];
|
|
1163
|
+
const re = /\b(\w+)\b/g;
|
|
1164
|
+
let match;
|
|
1165
|
+
while ((match = re.exec(body)) !== null) {
|
|
1166
|
+
names.push(match[1]);
|
|
1167
|
+
}
|
|
1168
|
+
return names;
|
|
1169
|
+
}
|
|
1170
|
+
|
|
1171
|
+
/**
|
|
1172
|
+
* Validate that there are no name collisions between signals, computeds, props, and methods.
|
|
1173
|
+
*
|
|
1174
|
+
* JavaScript does not allow duplicate identifiers in the same scope, so having a signal
|
|
1175
|
+
* and a function with the same name would cause a runtime error.
|
|
1176
|
+
*
|
|
1177
|
+
* @param {Set<string>} signalNames - Set of signal variable names
|
|
1178
|
+
* @param {Set<string>} computedNames - Set of computed variable names
|
|
1179
|
+
* @param {Set<string>} propNames - Set of prop names
|
|
1180
|
+
* @param {MethodDef[]} methods - Array of method definitions
|
|
1181
|
+
* @param {string} fileName - File name for error messages
|
|
1182
|
+
*/
|
|
1183
|
+
export function validateNameCollisions(signalNames, computedNames, propNames, methods, fileName) {
|
|
1184
|
+
const allNames = new Map(); // name -> type
|
|
1185
|
+
|
|
1186
|
+
// Register signals
|
|
1187
|
+
for (const name of signalNames) {
|
|
1188
|
+
if (allNames.has(name)) {
|
|
1189
|
+
const existingType = allNames.get(name);
|
|
1190
|
+
throw new Error(
|
|
1191
|
+
`Error en '${fileName}': Colisión de nombres - '${name}' está definido como ${existingType} y signal.\n` +
|
|
1192
|
+
`Solución: Usa nombres diferentes o convierte a computed().`
|
|
1193
|
+
);
|
|
1194
|
+
}
|
|
1195
|
+
allNames.set(name, 'signal');
|
|
1196
|
+
}
|
|
1197
|
+
|
|
1198
|
+
// Register computeds
|
|
1199
|
+
for (const name of computedNames) {
|
|
1200
|
+
if (allNames.has(name)) {
|
|
1201
|
+
const existingType = allNames.get(name);
|
|
1202
|
+
throw new Error(
|
|
1203
|
+
`Error en '${fileName}': Colisión de nombres - '${name}' está definido como ${existingType} y computed.\n` +
|
|
1204
|
+
`Solución: Usa nombres diferentes.`
|
|
1205
|
+
);
|
|
1206
|
+
}
|
|
1207
|
+
allNames.set(name, 'computed');
|
|
1208
|
+
}
|
|
1209
|
+
|
|
1210
|
+
// Register props
|
|
1211
|
+
for (const name of propNames) {
|
|
1212
|
+
if (allNames.has(name)) {
|
|
1213
|
+
const existingType = allNames.get(name);
|
|
1214
|
+
throw new Error(
|
|
1215
|
+
`Error en '${fileName}': Colisión de nombres - '${name}' está definido como ${existingType} y prop.\n` +
|
|
1216
|
+
`Solución: Usa nombres diferentes.`
|
|
1217
|
+
);
|
|
1218
|
+
}
|
|
1219
|
+
allNames.set(name, 'prop');
|
|
1220
|
+
}
|
|
1221
|
+
|
|
1222
|
+
// Check methods against all other names
|
|
1223
|
+
for (const method of methods) {
|
|
1224
|
+
if (allNames.has(method.name)) {
|
|
1225
|
+
const existingType = allNames.get(method.name);
|
|
1226
|
+
throw new Error(
|
|
1227
|
+
`Error en '${fileName}': Colisión de nombres - '${method.name}' está definido como ${existingType} y function.\n` +
|
|
1228
|
+
`Solución: Usa un nombre diferente para la función (ej: get${method.name.charAt(0).toUpperCase()}${method.name.slice(1)}) o convierte el ${existingType} a una función.`
|
|
1229
|
+
);
|
|
1230
|
+
}
|
|
1231
|
+
// Also check for duplicate method names
|
|
1232
|
+
const methodCount = methods.filter(m => m.name === method.name).length;
|
|
1233
|
+
if (methodCount > 1) {
|
|
1234
|
+
throw new Error(
|
|
1235
|
+
`Error en '${fileName}': Función duplicada - '${method.name}' está definida múltiples veces.\n` +
|
|
1236
|
+
`Solución: Elimina la definición duplicada.`
|
|
1237
|
+
);
|
|
1238
|
+
}
|
|
1239
|
+
}
|
|
1240
|
+
}
|