@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/lib/parser.js CHANGED
@@ -1,269 +1,273 @@
1
- /**
2
- * Parser for .ts/.js component source files using defineComponent().
3
- *
4
- * Extracts:
5
- * - defineComponent({ tag, template, styles }) metadata
6
- * - signal() declarations
7
- * - computed() declarations
8
- * - effect() declarations
9
- * - Top-level function declarations
10
- *
11
- * Tree walking (bindings, events, processedTemplate) is NOT handled
12
- * here — that's the responsibility of tree-walker.js.
13
- */
14
-
15
- /** @import { ParseResult, PropDef } from './types.js' */
16
-
17
- import { readFileSync, existsSync } from 'node:fs';
18
- import { resolve, dirname, extname } from 'node:path';
19
- import { transform } from 'esbuild';
20
-
21
- // Re-export all pure extraction functions so existing consumers are unaffected
22
- export * from './parser-extractors.js';
23
-
24
- import {
25
- stripMacroImport,
26
- toClassName,
27
- camelToKebab,
28
- extractPropsGeneric,
29
- extractPropsArray,
30
- extractPropsDefaults,
31
- extractPropsObjectName,
32
- extractEmitsFromCallSignatures,
33
- extractEmits,
34
- extractEmitsObjectName,
35
- extractEmitsObjectNameFromGeneric,
36
- extractSignals,
37
- extractComputeds,
38
- extractEffects,
39
- extractWatchers,
40
- extractFunctions,
41
- extractLifecycleHooks,
42
- extractRefs,
43
- extractConstants,
44
- extractDefineComponent,
45
- validatePropsAssignment,
46
- validateDuplicateProps,
47
- validatePropsConflicts,
48
- validateEmitsAssignment,
49
- validateDuplicateEmits,
50
- validateEmitsConflicts,
51
- validateUndeclaredEmits,
52
- } from './parser-extractors.js';
53
-
54
- // ── Type stripping ──────────────────────────────────────────────────
55
-
56
- /**
57
- * Strip TypeScript type annotations using esbuild, producing plain JavaScript.
58
- *
59
- * @param {string} tsCode - TypeScript source code
60
- * @returns {Promise<string>} - JavaScript without type annotations
61
- */
62
- export async function stripTypes(tsCode) {
63
- try {
64
- const result = await transform(tsCode, {
65
- loader: 'ts',
66
- target: 'esnext',
67
- sourcemap: false,
68
- });
69
- return result.code;
70
- } catch (err) {
71
- const error = new Error(`TypeScript syntax error: ${err.message}`);
72
- /** @ts-expect-error custom error code for programmatic handling */
73
- error.code = 'TS_SYNTAX_ERROR';
74
- throw error;
75
- }
76
- }
77
-
78
- // ── Main parse function ─────────────────────────────────────────────
79
-
80
- /**
81
- * Parse a .ts/.js component source file into a ParseResult IR.
82
- *
83
- * @param {string} filePath — Absolute path to the source file
84
- * @returns {Promise<ParseResult>}
85
- * @throws {Error} with code MISSING_DEFINE_COMPONENT, TEMPLATE_NOT_FOUND, STYLES_NOT_FOUND, TS_SYNTAX_ERROR
86
- */
87
- export async function parse(filePath) {
88
- // 1. Read the source file
89
- const rawSource = readFileSync(filePath, 'utf-8');
90
-
91
- // 2. Strip macro imports
92
- let source = stripMacroImport(rawSource);
93
-
94
- // 3. Extract props from generic form BEFORE type stripping (esbuild removes generics)
95
- const propsFromGeneric = extractPropsGeneric(source);
96
- const propsObjectNameFromGeneric = extractPropsObjectName(source);
97
-
98
- // 3b. Extract emits from call signatures form BEFORE type stripping
99
- const emitsFromCallSignatures = extractEmitsFromCallSignatures(source);
100
- const emitsObjectNameFromGeneric = extractEmitsObjectNameFromGeneric(source);
101
-
102
- // 4. Validate props assignment (before type strip, on original source)
103
- validatePropsAssignment(source, filePath);
104
-
105
- // 4b. Validate emits assignment (before type strip, on original source)
106
- validateEmitsAssignment(source, filePath);
107
-
108
- // 5. Strip TypeScript types if .ts file
109
- const ext = extname(filePath);
110
- if (ext === '.ts') {
111
- source = await stripTypes(source);
112
- }
113
-
114
- // 6. Extract defineComponent
115
- const componentDef = extractDefineComponent(source);
116
- if (!componentDef) {
117
- const error = new Error(
118
- `Error en '${filePath}': defineComponent() es obligatorio`
119
- );
120
- /** @ts-expect-error — custom error code for programmatic handling */
121
- error.code = 'MISSING_DEFINE_COMPONENT';
122
- throw error;
123
- }
124
-
125
- const { tag: tagName, template: templatePath, styles: stylesPath } = componentDef;
126
- const className = toClassName(tagName);
127
- const sourceDir = dirname(filePath);
128
-
129
- // 7. Resolve external files
130
- const resolvedTemplatePath = resolve(sourceDir, templatePath);
131
- if (!existsSync(resolvedTemplatePath)) {
132
- const error = new Error(
133
- `Error en '${filePath}': template no encontrado: '${templatePath}'`
134
- );
135
- /** @ts-expect-error — custom error code for programmatic handling */
136
- error.code = 'TEMPLATE_NOT_FOUND';
137
- throw error;
138
- }
139
- const template = readFileSync(resolvedTemplatePath, 'utf-8');
140
-
141
- let style = '';
142
- if (stylesPath) {
143
- const resolvedStylesPath = resolve(sourceDir, stylesPath);
144
- if (!existsSync(resolvedStylesPath)) {
145
- const error = new Error(
146
- `Error en '${filePath}': styles no encontrado: '${stylesPath}'`
147
- );
148
- /** @ts-expect-error — custom error code for programmatic handling */
149
- error.code = 'STYLES_NOT_FOUND';
150
- throw error;
151
- }
152
- style = readFileSync(resolvedStylesPath, 'utf-8');
153
- }
154
-
155
- // 8. Extract lifecycle hooks (before other extractions to avoid misidentification)
156
- const { onMountHooks, onDestroyHooks, onAdoptHooks } = extractLifecycleHooks(source);
157
-
158
- // 8b. Strip lifecycle hook blocks from source to prevent signal/computed/effect/function
159
- // extractors from misidentifying code inside hook bodies
160
- let sourceForExtraction = source;
161
- const hookLinePattern = /\bonMount\s*\(|\bonDestroy\s*\(|\bonAdopt\s*\(|\bwatch\s*\(/;
162
- const sourceLines = sourceForExtraction.split('\n');
163
- const filteredLines = [];
164
- let skipDepth = 0;
165
- let skipping = false;
166
- for (const line of sourceLines) {
167
- if (!skipping && hookLinePattern.test(line)) {
168
- skipping = true;
169
- skipDepth = 0;
170
- for (const ch of line) {
171
- if (ch === '{') skipDepth++;
172
- if (ch === '}') skipDepth--;
173
- }
174
- if (skipDepth <= 0) skipping = false;
175
- continue;
176
- }
177
- if (skipping) {
178
- for (const ch of line) {
179
- if (ch === '{') skipDepth++;
180
- if (ch === '}') skipDepth--;
181
- }
182
- if (skipDepth <= 0) skipping = false;
183
- continue;
184
- }
185
- filteredLines.push(line);
186
- }
187
- sourceForExtraction = filteredLines.join('\n');
188
-
189
- // 9. Extract reactive declarations and functions (from filtered source)
190
- const signals = extractSignals(sourceForExtraction);
191
- const computeds = extractComputeds(sourceForExtraction);
192
- const effects = extractEffects(sourceForExtraction);
193
- const watchers = extractWatchers(source); // Extract from unfiltered source (like lifecycle hooks)
194
- const methods = extractFunctions(sourceForExtraction);
195
- const refs = extractRefs(sourceForExtraction);
196
- const constantVars = extractConstants(sourceForExtraction);
197
-
198
- // 9. Extract props (array form — after type strip, if generic didn't find any)
199
- const propsFromArray = propsFromGeneric.length > 0 ? [] : extractPropsArray(source);
200
- let propNames = propsFromGeneric.length > 0 ? propsFromGeneric : propsFromArray;
201
-
202
- // 10. Extract props defaults (after type strip)
203
- const propsDefaults = extractPropsDefaults(source);
204
-
205
- // If neither generic nor array form found props, but defaults were found,
206
- // use the defaults object keys as prop names (object-only form: defineProps({ key: val }))
207
- if (propNames.length === 0 && Object.keys(propsDefaults).length > 0) {
208
- propNames = Object.keys(propsDefaults);
209
- }
210
-
211
- // 11. Extract propsObjectName (use generic result if found, otherwise post-strip)
212
- const propsObjectName = propsObjectNameFromGeneric ?? extractPropsObjectName(source);
213
-
214
- // 12. Validate props
215
- validateDuplicateProps(propNames, filePath);
216
-
217
- const signalNameSet = new Set(signals.map(s => s.name));
218
- const computedNameSet = new Set(computeds.map(c => c.name));
219
- // No constant extraction in v2 core, but use an empty set for validation
220
- const constantNameSet = new Set(constantVars.map(v => v.name));
221
- validatePropsConflicts(propsObjectName, signalNameSet, computedNameSet, constantNameSet, filePath);
222
-
223
- // 13. Build PropDef[]
224
- /** @type {PropDef[]} */
225
- const propDefs = propNames.map(name => ({
226
- name,
227
- default: propsDefaults[name] ?? 'undefined',
228
- attrName: camelToKebab(name),
229
- }));
230
-
231
- // 14. Extract emits (array form — after type strip, if call signatures didn't find any)
232
- const emitsFromArray = emitsFromCallSignatures.length > 0 ? [] : extractEmits(source);
233
- const emitNames = emitsFromCallSignatures.length > 0 ? emitsFromCallSignatures : emitsFromArray;
234
-
235
- // 15. Extract emitsObjectName (use generic result if found, otherwise post-strip)
236
- const emitsObjectName = emitsObjectNameFromGeneric ?? extractEmitsObjectName(source);
237
-
238
- // 16. Validate emits
239
- validateDuplicateEmits(emitNames, filePath);
240
-
241
- const propNameSet = new Set(propNames);
242
- validateEmitsConflicts(emitsObjectName, signalNameSet, computedNameSet, constantNameSet, propNameSet, propsObjectName, filePath);
243
- validateUndeclaredEmits(source, emitsObjectName, emitNames, filePath);
244
-
245
- // 17. Return ParseResult
246
- return {
247
- tagName,
248
- className,
249
- template,
250
- style,
251
- signals,
252
- computeds,
253
- effects,
254
- constantVars,
255
- watchers,
256
- methods,
257
- propDefs,
258
- propsObjectName: propsObjectName ?? null,
259
- emits: emitNames,
260
- emitsObjectName: emitsObjectName ?? null,
261
- bindings: [],
262
- events: [],
263
- processedTemplate: null,
264
- onMountHooks,
265
- onDestroyHooks,
266
- onAdoptHooks,
267
- refs,
268
- };
269
- }
1
+ /**
2
+ * Parser for .ts/.js component source files using defineComponent().
3
+ *
4
+ * Extracts:
5
+ * - defineComponent({ tag, template, styles }) metadata
6
+ * - signal() declarations
7
+ * - computed() declarations
8
+ * - effect() declarations
9
+ * - Top-level function declarations
10
+ *
11
+ * Tree walking (bindings, events, processedTemplate) is NOT handled
12
+ * here — that's the responsibility of tree-walker.js.
13
+ */
14
+
15
+ /** @import { ParseResult, PropDef } from './types.js' */
16
+
17
+ import { readFileSync, existsSync } from 'node:fs';
18
+ import { resolve, dirname, extname } from 'node:path';
19
+ import { transform } from 'esbuild';
20
+
21
+ // Re-export all pure extraction functions so existing consumers are unaffected
22
+ export * from './parser-extractors.js';
23
+
24
+ import {
25
+ stripMacroImport,
26
+ toClassName,
27
+ camelToKebab,
28
+ extractPropsGeneric,
29
+ extractPropsArray,
30
+ extractPropsDefaults,
31
+ extractPropsObjectName,
32
+ extractEmitsFromCallSignatures,
33
+ extractEmits,
34
+ extractEmitsObjectName,
35
+ extractEmitsObjectNameFromGeneric,
36
+ extractSignals,
37
+ extractComputeds,
38
+ extractEffects,
39
+ extractWatchers,
40
+ extractFunctions,
41
+ extractLifecycleHooks,
42
+ extractRefs,
43
+ extractConstants,
44
+ extractDefineComponent,
45
+ validatePropsAssignment,
46
+ validateDuplicateProps,
47
+ validatePropsConflicts,
48
+ validateEmitsAssignment,
49
+ validateDuplicateEmits,
50
+ validateEmitsConflicts,
51
+ validateUndeclaredEmits,
52
+ validateNameCollisions,
53
+ } from './parser-extractors.js';
54
+
55
+ // ── Type stripping ──────────────────────────────────────────────────
56
+
57
+ /**
58
+ * Strip TypeScript type annotations using esbuild, producing plain JavaScript.
59
+ *
60
+ * @param {string} tsCode - TypeScript source code
61
+ * @returns {Promise<string>} - JavaScript without type annotations
62
+ */
63
+ export async function stripTypes(tsCode) {
64
+ try {
65
+ const result = await transform(tsCode, {
66
+ loader: 'ts',
67
+ target: 'esnext',
68
+ sourcemap: false,
69
+ });
70
+ return result.code;
71
+ } catch (err) {
72
+ const error = new Error(`TypeScript syntax error: ${err.message}`);
73
+ /** @ts-expect-error — custom error code for programmatic handling */
74
+ error.code = 'TS_SYNTAX_ERROR';
75
+ throw error;
76
+ }
77
+ }
78
+
79
+ // ── Main parse function ─────────────────────────────────────────────
80
+
81
+ /**
82
+ * Parse a .ts/.js component source file into a ParseResult IR.
83
+ *
84
+ * @param {string} filePath — Absolute path to the source file
85
+ * @returns {Promise<ParseResult>}
86
+ * @throws {Error} with code MISSING_DEFINE_COMPONENT, TEMPLATE_NOT_FOUND, STYLES_NOT_FOUND, TS_SYNTAX_ERROR
87
+ */
88
+ export async function parse(filePath) {
89
+ // 1. Read the source file
90
+ const rawSource = readFileSync(filePath, 'utf-8');
91
+
92
+ // 2. Strip macro imports
93
+ let source = stripMacroImport(rawSource);
94
+
95
+ // 3. Extract props from generic form BEFORE type stripping (esbuild removes generics)
96
+ const propsFromGeneric = extractPropsGeneric(source);
97
+ const propsObjectNameFromGeneric = extractPropsObjectName(source);
98
+
99
+ // 3b. Extract emits from call signatures form BEFORE type stripping
100
+ const emitsFromCallSignatures = extractEmitsFromCallSignatures(source);
101
+ const emitsObjectNameFromGeneric = extractEmitsObjectNameFromGeneric(source);
102
+
103
+ // 4. Validate props assignment (before type strip, on original source)
104
+ validatePropsAssignment(source, filePath);
105
+
106
+ // 4b. Validate emits assignment (before type strip, on original source)
107
+ validateEmitsAssignment(source, filePath);
108
+
109
+ // 5. Strip TypeScript types if .ts file
110
+ const ext = extname(filePath);
111
+ if (ext === '.ts') {
112
+ source = await stripTypes(source);
113
+ }
114
+
115
+ // 6. Extract defineComponent
116
+ const componentDef = extractDefineComponent(source);
117
+ if (!componentDef) {
118
+ const error = new Error(
119
+ `Error en '${filePath}': defineComponent() es obligatorio`
120
+ );
121
+ /** @ts-expect-error — custom error code for programmatic handling */
122
+ error.code = 'MISSING_DEFINE_COMPONENT';
123
+ throw error;
124
+ }
125
+
126
+ const { tag: tagName, template: templatePath, styles: stylesPath } = componentDef;
127
+ const className = toClassName(tagName);
128
+ const sourceDir = dirname(filePath);
129
+
130
+ // 7. Resolve external files
131
+ const resolvedTemplatePath = resolve(sourceDir, templatePath);
132
+ if (!existsSync(resolvedTemplatePath)) {
133
+ const error = new Error(
134
+ `Error en '${filePath}': template no encontrado: '${templatePath}'`
135
+ );
136
+ /** @ts-expect-error — custom error code for programmatic handling */
137
+ error.code = 'TEMPLATE_NOT_FOUND';
138
+ throw error;
139
+ }
140
+ const template = readFileSync(resolvedTemplatePath, 'utf-8');
141
+
142
+ let style = '';
143
+ if (stylesPath) {
144
+ const resolvedStylesPath = resolve(sourceDir, stylesPath);
145
+ if (!existsSync(resolvedStylesPath)) {
146
+ const error = new Error(
147
+ `Error en '${filePath}': styles no encontrado: '${stylesPath}'`
148
+ );
149
+ /** @ts-expect-error — custom error code for programmatic handling */
150
+ error.code = 'STYLES_NOT_FOUND';
151
+ throw error;
152
+ }
153
+ style = readFileSync(resolvedStylesPath, 'utf-8');
154
+ }
155
+
156
+ // 8. Extract lifecycle hooks (before other extractions to avoid misidentification)
157
+ const { onMountHooks, onDestroyHooks, onAdoptHooks } = extractLifecycleHooks(source);
158
+
159
+ // 8b. Strip lifecycle hook blocks from source to prevent signal/computed/effect/function
160
+ // extractors from misidentifying code inside hook bodies
161
+ let sourceForExtraction = source;
162
+ const hookLinePattern = /\bonMount\s*\(|\bonDestroy\s*\(|\bonAdopt\s*\(|\bwatch\s*\(/;
163
+ const sourceLines = sourceForExtraction.split('\n');
164
+ const filteredLines = [];
165
+ let skipDepth = 0;
166
+ let skipping = false;
167
+ for (const line of sourceLines) {
168
+ if (!skipping && hookLinePattern.test(line)) {
169
+ skipping = true;
170
+ skipDepth = 0;
171
+ for (const ch of line) {
172
+ if (ch === '{') skipDepth++;
173
+ if (ch === '}') skipDepth--;
174
+ }
175
+ if (skipDepth <= 0) skipping = false;
176
+ continue;
177
+ }
178
+ if (skipping) {
179
+ for (const ch of line) {
180
+ if (ch === '{') skipDepth++;
181
+ if (ch === '}') skipDepth--;
182
+ }
183
+ if (skipDepth <= 0) skipping = false;
184
+ continue;
185
+ }
186
+ filteredLines.push(line);
187
+ }
188
+ sourceForExtraction = filteredLines.join('\n');
189
+
190
+ // 9. Extract reactive declarations and functions (from filtered source)
191
+ const signals = extractSignals(sourceForExtraction);
192
+ const computeds = extractComputeds(sourceForExtraction);
193
+ const effects = extractEffects(sourceForExtraction);
194
+ const watchers = extractWatchers(source); // Extract from unfiltered source (like lifecycle hooks)
195
+ const methods = extractFunctions(sourceForExtraction);
196
+ const refs = extractRefs(sourceForExtraction);
197
+ const constantVars = extractConstants(sourceForExtraction);
198
+
199
+ // 9. Extract props (array form after type strip, if generic didn't find any)
200
+ const propsFromArray = propsFromGeneric.length > 0 ? [] : extractPropsArray(source);
201
+ let propNames = propsFromGeneric.length > 0 ? propsFromGeneric : propsFromArray;
202
+
203
+ // 10. Extract props defaults (after type strip)
204
+ const propsDefaults = extractPropsDefaults(source);
205
+
206
+ // If neither generic nor array form found props, but defaults were found,
207
+ // use the defaults object keys as prop names (object-only form: defineProps({ key: val }))
208
+ if (propNames.length === 0 && Object.keys(propsDefaults).length > 0) {
209
+ propNames = Object.keys(propsDefaults);
210
+ }
211
+
212
+ // 11. Extract propsObjectName (use generic result if found, otherwise post-strip)
213
+ const propsObjectName = propsObjectNameFromGeneric ?? extractPropsObjectName(source);
214
+
215
+ // 12. Validate props
216
+ validateDuplicateProps(propNames, filePath);
217
+
218
+ const signalNameSet = new Set(signals.map(s => s.name));
219
+ const computedNameSet = new Set(computeds.map(c => c.name));
220
+ // No constant extraction in v2 core, but use an empty set for validation
221
+ const constantNameSet = new Set(constantVars.map(v => v.name));
222
+ validatePropsConflicts(propsObjectName, signalNameSet, computedNameSet, constantNameSet, filePath);
223
+
224
+ // 13. Build PropDef[]
225
+ /** @type {PropDef[]} */
226
+ const propDefs = propNames.map(name => ({
227
+ name,
228
+ default: propsDefaults[name] ?? 'undefined',
229
+ attrName: camelToKebab(name),
230
+ }));
231
+
232
+ // 14. Extract emits (array form after type strip, if call signatures didn't find any)
233
+ const emitsFromArray = emitsFromCallSignatures.length > 0 ? [] : extractEmits(source);
234
+ const emitNames = emitsFromCallSignatures.length > 0 ? emitsFromCallSignatures : emitsFromArray;
235
+
236
+ // 15. Extract emitsObjectName (use generic result if found, otherwise post-strip)
237
+ const emitsObjectName = emitsObjectNameFromGeneric ?? extractEmitsObjectName(source);
238
+
239
+ // 16. Validate emits
240
+ validateDuplicateEmits(emitNames, filePath);
241
+
242
+ const propNameSet = new Set(propNames);
243
+ validateEmitsConflicts(emitsObjectName, signalNameSet, computedNameSet, constantNameSet, propNameSet, propsObjectName, filePath);
244
+ validateUndeclaredEmits(source, emitsObjectName, emitNames, filePath);
245
+
246
+ // 16b. Validate name collisions between signals/computeds/props and methods
247
+ validateNameCollisions(signalNameSet, computedNameSet, propNameSet, methods, filePath);
248
+
249
+ // 17. Return ParseResult
250
+ return {
251
+ tagName,
252
+ className,
253
+ template,
254
+ style,
255
+ signals,
256
+ computeds,
257
+ effects,
258
+ constantVars,
259
+ watchers,
260
+ methods,
261
+ propDefs,
262
+ propsObjectName: propsObjectName ?? null,
263
+ emits: emitNames,
264
+ emitsObjectName: emitsObjectName ?? null,
265
+ bindings: [],
266
+ events: [],
267
+ processedTemplate: null,
268
+ onMountHooks,
269
+ onDestroyHooks,
270
+ onAdoptHooks,
271
+ refs,
272
+ };
273
+ }