@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/compiler.js CHANGED
@@ -1,479 +1,483 @@
1
- /**
2
- * Compiler — orchestrates the full compilation pipeline for wcCompiler v2.
3
- *
4
- * Pipeline: parse SFC → jsdom template → tree-walk → codegen
5
- *
6
- * Takes a .wcc file path and produces a self-contained JavaScript web component string.
7
- */
8
-
9
- import { parseHTML } from 'linkedom';
10
- import { readFileSync } from 'node:fs';
11
- import { basename } from 'node:path';
12
- import { walkTree, processIfChains, processForBlocks, processDynamicComponents, recomputeAnchorPath, detectRefs } from './tree-walker.js';
13
- import { generateComponent } from './codegen.js';
14
- import { parseSFC } from './sfc-parser.js';
15
- import {
16
- stripMacroImport,
17
- toClassName,
18
- camelToKebab,
19
- extractPropsGeneric,
20
- extractPropsArray,
21
- extractPropsDefaults,
22
- extractPropsObjectName,
23
- extractEmitsFromCallSignatures,
24
- extractEmits,
25
- extractEmitsObjectName,
26
- extractEmitsObjectNameFromGeneric,
27
- extractSignals,
28
- extractComputeds,
29
- extractEffects,
30
- extractWatchers,
31
- extractFunctions,
32
- extractLifecycleHooks,
33
- extractRefs,
34
- extractConstants,
35
- extractExpose,
36
- extractModels,
37
- validatePropsAssignment,
38
- validateDuplicateProps,
39
- validatePropsConflicts,
40
- validateEmitsAssignment,
41
- validateDuplicateEmits,
42
- validateEmitsConflicts,
43
- validateUndeclaredEmits,
44
- } from './parser-extractors.js';
45
- import { stripTypes } from './parser.js';
46
- import { normalizeTemplate, pascalToKebab } from './template-normalizer.js';
47
- import { extractWccImports } from './import-resolver.js';
48
-
49
- /**
50
- * Compile a single .wcc SFC file into a self-contained JS component.
51
- *
52
- * Reads the file, parses the SFC blocks, extracts reactive declarations
53
- * from the script block using parser-extractors.js, and processes template
54
- * and style through the existing pipeline (jsdom → tree-walker codegen).
55
- *
56
- * @param {string} filePath — Absolute or relative path to the .wcc file
57
- * @param {object} [config]Optional config (reserved for future options)
58
- * @returns {Promise<string>} The generated JavaScript component code
59
- */
60
- async function compileSFC(filePath, config) {
61
- // 1. Read and parse the SFC file
62
- const rawSource = readFileSync(filePath, 'utf-8');
63
- const fileName = basename(filePath);
64
- const descriptor = parseSFC(rawSource, fileName);
65
-
66
- // 2. Process script block — mirrors parser.js logic
67
- let source = stripMacroImport(descriptor.script);
68
-
69
- // 2b. Extract .wcc imports using the import resolver
70
- const wccImports = extractWccImports(source, fileName);
71
-
72
- // Build importMap: PascalCase identifier → kebab-case tag
73
- /** @type {Map<string, string>} */
74
- const importMap = new Map();
75
- for (const imp of wccImports.named) {
76
- importMap.set(imp.identifier, pascalToKebab(imp.identifier));
77
- }
78
-
79
- // Build childImports from extracted imports
80
- /** @type {import('./types.js').ChildComponentImport[]} */
81
- const childImports = [];
82
- for (const imp of wccImports.named) {
83
- childImports.push({
84
- tag: pascalToKebab(imp.identifier),
85
- identifier: imp.identifier,
86
- importPath: imp.compiledPath,
87
- sideEffect: false,
88
- });
89
- }
90
- for (const imp of wccImports.sideEffect) {
91
- childImports.push({
92
- tag: '',
93
- identifier: '',
94
- importPath: imp.compiledPath,
95
- sideEffect: true,
96
- });
97
- }
98
-
99
- // Use strippedSource (with .wcc imports removed) for subsequent extraction steps
100
- source = wccImports.strippedSource;
101
-
102
- // 3. Extract props/emits from generic forms BEFORE type stripping
103
- const propsFromGeneric = extractPropsGeneric(source);
104
- const propsObjectNameFromGeneric = extractPropsObjectName(source);
105
- const emitsFromCallSignatures = extractEmitsFromCallSignatures(source);
106
- const emitsObjectNameFromGeneric = extractEmitsObjectNameFromGeneric(source);
107
-
108
- // 4. Validate props/emits assignment (before type strip)
109
- validatePropsAssignment(source, filePath);
110
- validateEmitsAssignment(source, filePath);
111
-
112
- // 5. Strip TypeScript types if lang === 'ts'
113
- if (descriptor.lang === 'ts') {
114
- source = await stripTypes(source);
115
- }
116
-
117
- // 6. Extract component metadata
118
- const tagName = descriptor.tag;
119
- const className = toClassName(tagName);
120
- const template = descriptor.template;
121
- const style = descriptor.style;
122
-
123
- // 7. Extract lifecycle hooks (before other extractions)
124
- const { onMountHooks, onDestroyHooks, onAdoptHooks } = extractLifecycleHooks(source);
125
-
126
- // 7b. Strip lifecycle/watcher blocks from source for extraction
127
- let sourceForExtraction = source;
128
- const hookLinePattern = /\bonMount\s*\(|\bonDestroy\s*\(|\bonAdopt\s*\(|\bwatch\s*\(/;
129
- const sourceLines = sourceForExtraction.split('\n');
130
- const filteredLines = [];
131
- let skipDepth = 0;
132
- let skipping = false;
133
- for (const line of sourceLines) {
134
- if (!skipping && hookLinePattern.test(line)) {
135
- skipping = true;
136
- skipDepth = 0;
137
- for (const ch of line) {
138
- if (ch === '{') skipDepth++;
139
- if (ch === '}') skipDepth--;
140
- }
141
- if (skipDepth <= 0) skipping = false;
142
- continue;
143
- }
144
- if (skipping) {
145
- for (const ch of line) {
146
- if (ch === '{') skipDepth++;
147
- if (ch === '}') skipDepth--;
148
- }
149
- if (skipDepth <= 0) skipping = false;
150
- continue;
151
- }
152
- filteredLines.push(line);
153
- }
154
- sourceForExtraction = filteredLines.join('\n');
155
-
156
- // 8. Extract reactive declarations and functions
157
- const signals = extractSignals(sourceForExtraction);
158
- const computeds = extractComputeds(sourceForExtraction);
159
- const effects = extractEffects(sourceForExtraction);
160
- const watchers = extractWatchers(source);
161
- const methods = extractFunctions(sourceForExtraction);
162
- const refs = extractRefs(sourceForExtraction);
163
- const constantVars = extractConstants(sourceForExtraction);
164
- const exposeNames = extractExpose(source);
165
- const modelDefs = extractModels(sourceForExtraction);
166
-
167
- // 9. Extract props (array form — after type strip, if generic didn't find any)
168
- const propsFromArray = propsFromGeneric.length > 0 ? [] : extractPropsArray(source);
169
- let propNames = propsFromGeneric.length > 0 ? propsFromGeneric : propsFromArray;
170
-
171
- // 10. Extract props defaults
172
- const propsDefaults = extractPropsDefaults(source);
173
- if (propNames.length === 0 && Object.keys(propsDefaults).length > 0) {
174
- propNames = Object.keys(propsDefaults);
175
- }
176
-
177
- // 11. Extract propsObjectName
178
- const propsObjectName = propsObjectNameFromGeneric ?? extractPropsObjectName(source);
179
-
180
- // 12. Validate props
181
- validateDuplicateProps(propNames, filePath);
182
- const signalNameSet = new Set(signals.map(s => s.name));
183
- const computedNameSet = new Set(computeds.map(c => c.name));
184
- const constantNameSet = new Set(constantVars.map(v => v.name));
185
- validatePropsConflicts(propsObjectName, signalNameSet, computedNameSet, constantNameSet, filePath);
186
-
187
- /** @type {import('./types.js').PropDef[]} */
188
- const propDefs = propNames.map(name => ({
189
- name,
190
- default: propsDefaults[name] ?? 'undefined',
191
- attrName: camelToKebab(name),
192
- }));
193
-
194
- // 13. Extract emits
195
- const emitsFromArray = emitsFromCallSignatures.length > 0 ? [] : extractEmits(source);
196
- const emitNames = emitsFromCallSignatures.length > 0 ? emitsFromCallSignatures : emitsFromArray;
197
- const emitsObjectName = emitsObjectNameFromGeneric ?? extractEmitsObjectName(source);
198
-
199
- // 14. Validate emits
200
- validateDuplicateEmits(emitNames, filePath);
201
- const propNameSet = new Set(propNames);
202
- validateEmitsConflicts(emitsObjectName, signalNameSet, computedNameSet, constantNameSet, propNameSet, propsObjectName, filePath);
203
- validateUndeclaredEmits(source, emitsObjectName, emitNames, filePath);
204
-
205
- // 14b. Validate defineModel declarations
206
- // MODEL_NO_ASSIGNMENT: detect bare defineModel() calls not assigned to a variable
207
- const bareModelRe = /\bdefineModel\s*\(/g;
208
- const assignedModelRe = /(?:const|let|var)\s+\w+\s*=\s*defineModel\s*\(/g;
209
- const bareModelCount = (sourceForExtraction.match(bareModelRe) || []).length;
210
- const assignedModelCount = (sourceForExtraction.match(assignedModelRe) || []).length;
211
- if (bareModelCount > assignedModelCount) {
212
- const error = new Error(`defineModel() must be assigned to a variable`);
213
- /** @ts-expect-error custom error code */
214
- error.code = 'MODEL_NO_ASSIGNMENT';
215
- throw error;
216
- }
217
-
218
- // MODEL_MISSING_NAME: check each extracted model has a name property
219
- for (const md of modelDefs) {
220
- if (!md.name) {
221
- const error = new Error(`defineModel() requires a 'name' property in the options object`);
222
- /** @ts-expect-error custom error code */
223
- error.code = 'MODEL_MISSING_NAME';
224
- throw error;
225
- }
226
- }
227
-
228
- // MODEL_NAME_CONFLICT: check model prop names against signals, computeds, constants, and props
229
- for (const md of modelDefs) {
230
- if (!md.name) continue;
231
- if (signalNameSet.has(md.name)) {
232
- const error = new Error(`defineModel prop '${md.name}' conflicts with existing signal '${md.name}'`);
233
- /** @ts-expect-error custom error code */
234
- error.code = 'MODEL_NAME_CONFLICT';
235
- throw error;
236
- }
237
- if (computedNameSet.has(md.name)) {
238
- const error = new Error(`defineModel prop '${md.name}' conflicts with existing computed '${md.name}'`);
239
- /** @ts-expect-error — custom error code */
240
- error.code = 'MODEL_NAME_CONFLICT';
241
- throw error;
242
- }
243
- if (constantNameSet.has(md.name)) {
244
- const error = new Error(`defineModel prop '${md.name}' conflicts with existing constant '${md.name}'`);
245
- /** @ts-expect-error — custom error code */
246
- error.code = 'MODEL_NAME_CONFLICT';
247
- throw error;
248
- }
249
- if (propNameSet.has(md.name)) {
250
- const error = new Error(`defineModel prop '${md.name}' conflicts with existing prop '${md.name}'`);
251
- /** @ts-expect-error — custom error code */
252
- error.code = 'MODEL_NAME_CONFLICT';
253
- throw error;
254
- }
255
- }
256
-
257
- // 15. Build initial ParseResult
258
- /** @type {import('./types.js').ParseResult} */
259
- const parseResult = {
260
- tagName,
261
- className,
262
- template,
263
- style,
264
- signals,
265
- computeds,
266
- effects,
267
- constantVars,
268
- watchers,
269
- methods,
270
- propDefs,
271
- propsObjectName: propsObjectName ?? null,
272
- emits: emitNames,
273
- emitsObjectName: emitsObjectName ?? null,
274
- bindings: [],
275
- events: [],
276
- processedTemplate: null,
277
- ifBlocks: [],
278
- showBindings: [],
279
- forBlocks: [],
280
- onMountHooks,
281
- onDestroyHooks,
282
- onAdoptHooks,
283
- modelBindings: [],
284
- modelPropBindings: [],
285
- attrBindings: [],
286
- slots: [],
287
- refs,
288
- refBindings: [],
289
- childComponents: [],
290
- childImports: [],
291
- exposeNames,
292
- modelDefs,
293
- dynamicComponents: [],
294
- };
295
-
296
- // 16. Process template through linkedom → tree-walker → codegen
297
- const normalizedTemplate = normalizeTemplate(template, { importMap, fileName });
298
- const { document } = parseHTML(`<div id="__root">${normalizedTemplate}</div>`);
299
- const rootEl = document.getElementById('__root');
300
-
301
- const signalNames = new Set(signals.map(s => s.name));
302
- // Add model var names so they are recognized as writable signals in tree-walker
303
- for (const md of modelDefs) {
304
- signalNames.add(md.varName);
305
- }
306
- const computedNames = new Set(computeds.map(c => c.name));
307
- const propNamesSet = new Set(propDefs.map(p => p.name));
308
-
309
- const forBlocks = processForBlocks(rootEl, [], signalNames, computedNames, propNamesSet);
310
- const ifBlocks = processIfChains(rootEl, [], signalNames, computedNames, propNamesSet);
311
- const dynamicComponents = processDynamicComponents(rootEl, []);
312
-
313
- rootEl.normalize();
314
-
315
- for (const fb of forBlocks) {
316
- fb.anchorPath = recomputeAnchorPath(rootEl, fb._anchorNode);
317
- }
318
- for (const ib of ifBlocks) {
319
- ib.anchorPath = recomputeAnchorPath(rootEl, ib._anchorNode);
320
- }
321
- for (const dc of dynamicComponents) {
322
- dc.anchorPath = recomputeAnchorPath(rootEl, dc._anchorNode);
323
- }
324
-
325
- const { bindings, events, showBindings, modelBindings, modelPropBindings, attrBindings, slots, childComponents } = walkTree(rootEl, signalNames, computedNames, propNamesSet);
326
-
327
- const refBindings = detectRefs(rootEl);
328
-
329
- // 17. Validate refs
330
- for (const decl of refs) {
331
- if (!refBindings.find(b => b.refName === decl.refName)) {
332
- const error = new Error(`templateRef('${decl.refName}') has no matching ref="${decl.refName}" in template`);
333
- /** @ts-expect-error custom error code */
334
- error.code = 'REF_NOT_FOUND';
335
- throw error;
336
- }
337
- }
338
- for (const binding of refBindings) {
339
- if (!refs.find(d => d.refName === binding.refName)) {
340
- console.warn(`Warning: ref="${binding.refName}" in template has no matching templateRef('${binding.refName}') in script`);
341
- }
342
- }
343
-
344
- // 17b. Validate model bindings
345
- const constantNamesForModel = new Set(constantVars.map(v => v.name));
346
- for (const mb of modelBindings) {
347
- if (propNamesSet.has(mb.signal)) {
348
- const error = new Error(`model cannot bind to prop '${mb.signal}' (read-only)`);
349
- /** @ts-expect-error custom error code */
350
- error.code = 'MODEL_READONLY';
351
- throw error;
352
- }
353
- if (computedNames.has(mb.signal)) {
354
- const error = new Error(`model cannot bind to computed '${mb.signal}' (read-only)`);
355
- /** @ts-expect-error — custom error code */
356
- error.code = 'MODEL_READONLY';
357
- throw error;
358
- }
359
- if (constantNamesForModel.has(mb.signal)) {
360
- const error = new Error(`model cannot bind to constant '${mb.signal}' (read-only)`);
361
- /** @ts-expect-error — custom error code */
362
- error.code = 'MODEL_READONLY';
363
- throw error;
364
- }
365
- if (!signalNames.has(mb.signal)) {
366
- const error = new Error(`model references undeclared variable '${mb.signal}'`);
367
- /** @ts-expect-error — custom error code */
368
- error.code = 'MODEL_UNKNOWN_VAR';
369
- throw error;
370
- }
371
- }
372
-
373
- // 17c. Validate model:propName bindings
374
- for (const mpb of modelPropBindings) {
375
- const name = mpb.signal;
376
- // Check if the referenced variable exists at all
377
- const isKnown = signalNames.has(name) || computedNames.has(name) || propNamesSet.has(name) || constantNamesForModel.has(name);
378
- if (!isKnown) {
379
- const error = new Error(`model:propName references undeclared variable '${name}'`);
380
- /** @ts-expect-error custom error code */
381
- error.code = 'MODEL_PROP_UNKNOWN_VAR';
382
- throw error;
383
- }
384
- // Check if the referenced variable is read-only
385
- if (propNamesSet.has(name)) {
386
- const error = new Error(`model:propName cannot bind to prop '${name}' (read-only)`);
387
- /** @ts-expect-error — custom error code */
388
- error.code = 'MODEL_PROP_READONLY';
389
- throw error;
390
- }
391
- if (computedNames.has(name)) {
392
- const error = new Error(`model:propName cannot bind to computed '${name}' (read-only)`);
393
- /** @ts-expect-error — custom error code */
394
- error.code = 'MODEL_PROP_READONLY';
395
- throw error;
396
- }
397
- if (constantNamesForModel.has(name)) {
398
- const error = new Error(`model:propName cannot bind to constant '${name}' (read-only)`);
399
- /** @ts-expect-error — custom error code */
400
- error.code = 'MODEL_PROP_READONLY';
401
- throw error;
402
- }
403
- }
404
-
405
- // 18. Child imports already built from extractWccImports (step 2b)
406
- // No filesystem scanning needed — imports are explicit
407
-
408
- // 19. Merge tree-walker results into ParseResult
409
- parseResult.bindings = bindings;
410
- parseResult.events = events;
411
- parseResult.showBindings = showBindings;
412
- parseResult.modelBindings = modelBindings;
413
- parseResult.modelPropBindings = modelPropBindings;
414
- parseResult.attrBindings = attrBindings;
415
- parseResult.ifBlocks = ifBlocks;
416
- parseResult.forBlocks = forBlocks;
417
- parseResult.slots = slots;
418
- parseResult.refBindings = refBindings;
419
- parseResult.childComponents = childComponents;
420
- parseResult.dynamicComponents = dynamicComponents;
421
-
422
- parseResult.childImports = childImports;
423
- parseResult.processedTemplate = rootEl.innerHTML;
424
-
425
- // 20. Resolve standalone and generate component
426
- const standaloneResolved = resolveStandalone(descriptor.standalone, config?.standalone ?? false);
427
- const genOptions = { ...config, sourceFile: fileName };
428
-
429
- if (standaloneResolved) {
430
- // Force inline runtime ignore any runtimeImportPath
431
- genOptions.runtimeImportPath = undefined;
432
- }
433
- // If standaloneResolved is false, keep config.runtimeImportPath as-is (CLI provides it)
434
-
435
- const code = generateComponent(parseResult, genOptions);
436
- const usesSharedRuntime = !standaloneResolved && !!genOptions.runtimeImportPath;
437
- return { code, usesSharedRuntime };
438
- }
439
-
440
- /**
441
- * Resolve the final standalone value.
442
- * Component-level has priority over global.
443
- *
444
- * @param {boolean | undefined} componentValue — standalone from defineComponent (true, false, or undefined)
445
- * @param {boolean} globalValue standalone from config (true or false)
446
- * @returns {boolean}
447
- */
448
- export function resolveStandalone(componentValue, globalValue) {
449
- if (componentValue === true || componentValue === false) return componentValue;
450
- return globalValue;
451
- }
452
-
453
- /**
454
- * Compile a single .wcc SFC file into a self-contained JS component.
455
- *
456
- * @param {string} filePath — Absolute or relative path to the .wcc file
457
- * @param {object} [config] — Optional config (reserved for future options)
458
- * @returns {Promise<{code: string, usesSharedRuntime: boolean}>} The generated JavaScript component code and metadata
459
- */
460
- export async function compile(filePath, config) {
461
- const result = await compileSFC(filePath, config);
462
-
463
- if (config?.minify) {
464
- const { transform } = await import('esbuild');
465
- try {
466
- const minified = await transform(result.code, {
467
- minify: true,
468
- loader: 'js',
469
- target: 'esnext',
470
- });
471
- result.code = minified.code;
472
- } catch {
473
- // If minification fails (e.g., edge-case syntax), return unminified code
474
- // This is a graceful fallback — the code still works at runtime
475
- }
476
- }
477
-
478
- return result;
479
- }
1
+ /**
2
+ * Compiler — orchestrates the full compilation pipeline for wcCompiler v2.
3
+ *
4
+ * Pipeline: parse SFC → jsdom template → tree-walk → codegen
5
+ *
6
+ * Takes a .wcc file path and produces a self-contained JavaScript web component string.
7
+ */
8
+
9
+ import { parseHTML } from 'linkedom';
10
+ import { readFileSync } from 'node:fs';
11
+ import { basename } from 'node:path';
12
+ import { walkTree, processIfChains, processForBlocks, processDynamicComponents, recomputeAnchorPath, detectRefs } from './tree-walker.js';
13
+ import { generateComponent } from './codegen.js';
14
+ import { parseSFC } from './sfc-parser.js';
15
+ import {
16
+ stripMacroImport,
17
+ toClassName,
18
+ camelToKebab,
19
+ extractPropsGeneric,
20
+ extractPropsArray,
21
+ extractPropsDefaults,
22
+ extractPropsObjectName,
23
+ extractEmitsFromCallSignatures,
24
+ extractEmits,
25
+ extractEmitsObjectName,
26
+ extractEmitsObjectNameFromGeneric,
27
+ extractSignals,
28
+ extractComputeds,
29
+ extractEffects,
30
+ extractWatchers,
31
+ extractFunctions,
32
+ extractLifecycleHooks,
33
+ extractRefs,
34
+ extractConstants,
35
+ extractExpose,
36
+ extractModels,
37
+ validatePropsAssignment,
38
+ validateDuplicateProps,
39
+ validatePropsConflicts,
40
+ validateEmitsAssignment,
41
+ validateDuplicateEmits,
42
+ validateEmitsConflicts,
43
+ validateUndeclaredEmits,
44
+ validateNameCollisions,
45
+ } from './parser-extractors.js';
46
+ import { stripTypes } from './parser.js';
47
+ import { normalizeTemplate, pascalToKebab } from './template-normalizer.js';
48
+ import { extractWccImports } from './import-resolver.js';
49
+
50
+ /**
51
+ * Compile a single .wcc SFC file into a self-contained JS component.
52
+ *
53
+ * Reads the file, parses the SFC blocks, extracts reactive declarations
54
+ * from the script block using parser-extractors.js, and processes template
55
+ * and style through the existing pipeline (jsdom → tree-walker → codegen).
56
+ *
57
+ * @param {string} filePathAbsolute or relative path to the .wcc file
58
+ * @param {object} [config] Optional config (reserved for future options)
59
+ * @returns {Promise<string>} The generated JavaScript component code
60
+ */
61
+ async function compileSFC(filePath, config) {
62
+ // 1. Read and parse the SFC file
63
+ const rawSource = readFileSync(filePath, 'utf-8');
64
+ const fileName = basename(filePath);
65
+ const descriptor = parseSFC(rawSource, fileName);
66
+
67
+ // 2. Process script block — mirrors parser.js logic
68
+ let source = stripMacroImport(descriptor.script);
69
+
70
+ // 2b. Extract .wcc imports using the import resolver
71
+ const wccImports = extractWccImports(source, fileName);
72
+
73
+ // Build importMap: PascalCase identifier → kebab-case tag
74
+ /** @type {Map<string, string>} */
75
+ const importMap = new Map();
76
+ for (const imp of wccImports.named) {
77
+ importMap.set(imp.identifier, pascalToKebab(imp.identifier));
78
+ }
79
+
80
+ // Build childImports from extracted imports
81
+ /** @type {import('./types.js').ChildComponentImport[]} */
82
+ const childImports = [];
83
+ for (const imp of wccImports.named) {
84
+ childImports.push({
85
+ tag: pascalToKebab(imp.identifier),
86
+ identifier: imp.identifier,
87
+ importPath: imp.compiledPath,
88
+ sideEffect: false,
89
+ });
90
+ }
91
+ for (const imp of wccImports.sideEffect) {
92
+ childImports.push({
93
+ tag: '',
94
+ identifier: '',
95
+ importPath: imp.compiledPath,
96
+ sideEffect: true,
97
+ });
98
+ }
99
+
100
+ // Use strippedSource (with .wcc imports removed) for subsequent extraction steps
101
+ source = wccImports.strippedSource;
102
+
103
+ // 3. Extract props/emits from generic forms BEFORE type stripping
104
+ const propsFromGeneric = extractPropsGeneric(source);
105
+ const propsObjectNameFromGeneric = extractPropsObjectName(source);
106
+ const emitsFromCallSignatures = extractEmitsFromCallSignatures(source);
107
+ const emitsObjectNameFromGeneric = extractEmitsObjectNameFromGeneric(source);
108
+
109
+ // 4. Validate props/emits assignment (before type strip)
110
+ validatePropsAssignment(source, filePath);
111
+ validateEmitsAssignment(source, filePath);
112
+
113
+ // 5. Strip TypeScript types if lang === 'ts'
114
+ if (descriptor.lang === 'ts') {
115
+ source = await stripTypes(source);
116
+ }
117
+
118
+ // 6. Extract component metadata
119
+ const tagName = descriptor.tag;
120
+ const className = toClassName(tagName);
121
+ const template = descriptor.template;
122
+ const style = descriptor.style;
123
+
124
+ // 7. Extract lifecycle hooks (before other extractions)
125
+ const { onMountHooks, onDestroyHooks, onAdoptHooks } = extractLifecycleHooks(source);
126
+
127
+ // 7b. Strip lifecycle/watcher blocks from source for extraction
128
+ let sourceForExtraction = source;
129
+ const hookLinePattern = /\bonMount\s*\(|\bonDestroy\s*\(|\bonAdopt\s*\(|\bwatch\s*\(/;
130
+ const sourceLines = sourceForExtraction.split('\n');
131
+ const filteredLines = [];
132
+ let skipDepth = 0;
133
+ let skipping = false;
134
+ for (const line of sourceLines) {
135
+ if (!skipping && hookLinePattern.test(line)) {
136
+ skipping = true;
137
+ skipDepth = 0;
138
+ for (const ch of line) {
139
+ if (ch === '{') skipDepth++;
140
+ if (ch === '}') skipDepth--;
141
+ }
142
+ if (skipDepth <= 0) skipping = false;
143
+ continue;
144
+ }
145
+ if (skipping) {
146
+ for (const ch of line) {
147
+ if (ch === '{') skipDepth++;
148
+ if (ch === '}') skipDepth--;
149
+ }
150
+ if (skipDepth <= 0) skipping = false;
151
+ continue;
152
+ }
153
+ filteredLines.push(line);
154
+ }
155
+ sourceForExtraction = filteredLines.join('\n');
156
+
157
+ // 8. Extract reactive declarations and functions
158
+ const signals = extractSignals(sourceForExtraction);
159
+ const computeds = extractComputeds(sourceForExtraction);
160
+ const effects = extractEffects(sourceForExtraction);
161
+ const watchers = extractWatchers(source);
162
+ const methods = extractFunctions(sourceForExtraction);
163
+ const refs = extractRefs(sourceForExtraction);
164
+ const constantVars = extractConstants(sourceForExtraction);
165
+ const exposeNames = extractExpose(source);
166
+ const modelDefs = extractModels(sourceForExtraction);
167
+
168
+ // 9. Extract props (array form after type strip, if generic didn't find any)
169
+ const propsFromArray = propsFromGeneric.length > 0 ? [] : extractPropsArray(source);
170
+ let propNames = propsFromGeneric.length > 0 ? propsFromGeneric : propsFromArray;
171
+
172
+ // 10. Extract props defaults
173
+ const propsDefaults = extractPropsDefaults(source);
174
+ if (propNames.length === 0 && Object.keys(propsDefaults).length > 0) {
175
+ propNames = Object.keys(propsDefaults);
176
+ }
177
+
178
+ // 11. Extract propsObjectName
179
+ const propsObjectName = propsObjectNameFromGeneric ?? extractPropsObjectName(source);
180
+
181
+ // 12. Validate props
182
+ validateDuplicateProps(propNames, filePath);
183
+ const signalNameSet = new Set(signals.map(s => s.name));
184
+ const computedNameSet = new Set(computeds.map(c => c.name));
185
+ const constantNameSet = new Set(constantVars.map(v => v.name));
186
+ validatePropsConflicts(propsObjectName, signalNameSet, computedNameSet, constantNameSet, filePath);
187
+
188
+ /** @type {import('./types.js').PropDef[]} */
189
+ const propDefs = propNames.map(name => ({
190
+ name,
191
+ default: propsDefaults[name] ?? 'undefined',
192
+ attrName: camelToKebab(name),
193
+ }));
194
+
195
+ // 13. Extract emits
196
+ const emitsFromArray = emitsFromCallSignatures.length > 0 ? [] : extractEmits(source);
197
+ const emitNames = emitsFromCallSignatures.length > 0 ? emitsFromCallSignatures : emitsFromArray;
198
+ const emitsObjectName = emitsObjectNameFromGeneric ?? extractEmitsObjectName(source);
199
+
200
+ // 14. Validate emits
201
+ validateDuplicateEmits(emitNames, filePath);
202
+ const propNameSet = new Set(propNames);
203
+ validateEmitsConflicts(emitsObjectName, signalNameSet, computedNameSet, constantNameSet, propNameSet, propsObjectName, filePath);
204
+ validateUndeclaredEmits(source, emitsObjectName, emitNames, filePath);
205
+
206
+ // 14b. Validate name collisions between signals/computeds/props and methods
207
+ validateNameCollisions(signalNameSet, computedNameSet, propNameSet, methods, filePath);
208
+
209
+ // 14c. Validate defineModel declarations
210
+ // MODEL_NO_ASSIGNMENT: detect bare defineModel() calls not assigned to a variable
211
+ const bareModelRe = /\bdefineModel\s*\(/g;
212
+ const assignedModelRe = /(?:const|let|var)\s+\w+\s*=\s*defineModel\s*\(/g;
213
+ const bareModelCount = (sourceForExtraction.match(bareModelRe) || []).length;
214
+ const assignedModelCount = (sourceForExtraction.match(assignedModelRe) || []).length;
215
+ if (bareModelCount > assignedModelCount) {
216
+ const error = new Error(`defineModel() must be assigned to a variable`);
217
+ /** @ts-expect-error — custom error code */
218
+ error.code = 'MODEL_NO_ASSIGNMENT';
219
+ throw error;
220
+ }
221
+
222
+ // MODEL_MISSING_NAME: check each extracted model has a name property
223
+ for (const md of modelDefs) {
224
+ if (!md.name) {
225
+ const error = new Error(`defineModel() requires a 'name' property in the options object`);
226
+ /** @ts-expect-error — custom error code */
227
+ error.code = 'MODEL_MISSING_NAME';
228
+ throw error;
229
+ }
230
+ }
231
+
232
+ // MODEL_NAME_CONFLICT: check model prop names against signals, computeds, constants, and props
233
+ for (const md of modelDefs) {
234
+ if (!md.name) continue;
235
+ if (signalNameSet.has(md.name)) {
236
+ const error = new Error(`defineModel prop '${md.name}' conflicts with existing signal '${md.name}'`);
237
+ /** @ts-expect-error — custom error code */
238
+ error.code = 'MODEL_NAME_CONFLICT';
239
+ throw error;
240
+ }
241
+ if (computedNameSet.has(md.name)) {
242
+ const error = new Error(`defineModel prop '${md.name}' conflicts with existing computed '${md.name}'`);
243
+ /** @ts-expect-error — custom error code */
244
+ error.code = 'MODEL_NAME_CONFLICT';
245
+ throw error;
246
+ }
247
+ if (constantNameSet.has(md.name)) {
248
+ const error = new Error(`defineModel prop '${md.name}' conflicts with existing constant '${md.name}'`);
249
+ /** @ts-expect-error — custom error code */
250
+ error.code = 'MODEL_NAME_CONFLICT';
251
+ throw error;
252
+ }
253
+ if (propNameSet.has(md.name)) {
254
+ const error = new Error(`defineModel prop '${md.name}' conflicts with existing prop '${md.name}'`);
255
+ /** @ts-expect-error — custom error code */
256
+ error.code = 'MODEL_NAME_CONFLICT';
257
+ throw error;
258
+ }
259
+ }
260
+
261
+ // 15. Build initial ParseResult
262
+ /** @type {import('./types.js').ParseResult} */
263
+ const parseResult = {
264
+ tagName,
265
+ className,
266
+ template,
267
+ style,
268
+ signals,
269
+ computeds,
270
+ effects,
271
+ constantVars,
272
+ watchers,
273
+ methods,
274
+ propDefs,
275
+ propsObjectName: propsObjectName ?? null,
276
+ emits: emitNames,
277
+ emitsObjectName: emitsObjectName ?? null,
278
+ bindings: [],
279
+ events: [],
280
+ processedTemplate: null,
281
+ ifBlocks: [],
282
+ showBindings: [],
283
+ forBlocks: [],
284
+ onMountHooks,
285
+ onDestroyHooks,
286
+ onAdoptHooks,
287
+ modelBindings: [],
288
+ modelPropBindings: [],
289
+ attrBindings: [],
290
+ slots: [],
291
+ refs,
292
+ refBindings: [],
293
+ childComponents: [],
294
+ childImports: [],
295
+ exposeNames,
296
+ modelDefs,
297
+ dynamicComponents: [],
298
+ };
299
+
300
+ // 16. Process template through linkedom → tree-walker → codegen
301
+ const normalizedTemplate = normalizeTemplate(template, { importMap, fileName });
302
+ const { document } = parseHTML(`<div id="__root">${normalizedTemplate}</div>`);
303
+ const rootEl = document.getElementById('__root');
304
+
305
+ const signalNames = new Set(signals.map(s => s.name));
306
+ // Add model var names so they are recognized as writable signals in tree-walker
307
+ for (const md of modelDefs) {
308
+ signalNames.add(md.varName);
309
+ }
310
+ const computedNames = new Set(computeds.map(c => c.name));
311
+ const propNamesSet = new Set(propDefs.map(p => p.name));
312
+
313
+ const forBlocks = processForBlocks(rootEl, [], signalNames, computedNames, propNamesSet);
314
+ const ifBlocks = processIfChains(rootEl, [], signalNames, computedNames, propNamesSet);
315
+ const dynamicComponents = processDynamicComponents(rootEl, []);
316
+
317
+ rootEl.normalize();
318
+
319
+ for (const fb of forBlocks) {
320
+ fb.anchorPath = recomputeAnchorPath(rootEl, fb._anchorNode);
321
+ }
322
+ for (const ib of ifBlocks) {
323
+ ib.anchorPath = recomputeAnchorPath(rootEl, ib._anchorNode);
324
+ }
325
+ for (const dc of dynamicComponents) {
326
+ dc.anchorPath = recomputeAnchorPath(rootEl, dc._anchorNode);
327
+ }
328
+
329
+ const { bindings, events, showBindings, modelBindings, modelPropBindings, attrBindings, slots, childComponents } = walkTree(rootEl, signalNames, computedNames, propNamesSet);
330
+
331
+ const refBindings = detectRefs(rootEl);
332
+
333
+ // 17. Validate refs
334
+ for (const decl of refs) {
335
+ if (!refBindings.find(b => b.refName === decl.refName)) {
336
+ const error = new Error(`templateRef('${decl.refName}') has no matching ref="${decl.refName}" in template`);
337
+ /** @ts-expect-error — custom error code */
338
+ error.code = 'REF_NOT_FOUND';
339
+ throw error;
340
+ }
341
+ }
342
+ for (const binding of refBindings) {
343
+ if (!refs.find(d => d.refName === binding.refName)) {
344
+ console.warn(`Warning: ref="${binding.refName}" in template has no matching templateRef('${binding.refName}') in script`);
345
+ }
346
+ }
347
+
348
+ // 17b. Validate model bindings
349
+ const constantNamesForModel = new Set(constantVars.map(v => v.name));
350
+ for (const mb of modelBindings) {
351
+ if (propNamesSet.has(mb.signal)) {
352
+ const error = new Error(`model cannot bind to prop '${mb.signal}' (read-only)`);
353
+ /** @ts-expect-error — custom error code */
354
+ error.code = 'MODEL_READONLY';
355
+ throw error;
356
+ }
357
+ if (computedNames.has(mb.signal)) {
358
+ const error = new Error(`model cannot bind to computed '${mb.signal}' (read-only)`);
359
+ /** @ts-expect-error — custom error code */
360
+ error.code = 'MODEL_READONLY';
361
+ throw error;
362
+ }
363
+ if (constantNamesForModel.has(mb.signal)) {
364
+ const error = new Error(`model cannot bind to constant '${mb.signal}' (read-only)`);
365
+ /** @ts-expect-error — custom error code */
366
+ error.code = 'MODEL_READONLY';
367
+ throw error;
368
+ }
369
+ if (!signalNames.has(mb.signal)) {
370
+ const error = new Error(`model references undeclared variable '${mb.signal}'`);
371
+ /** @ts-expect-error — custom error code */
372
+ error.code = 'MODEL_UNKNOWN_VAR';
373
+ throw error;
374
+ }
375
+ }
376
+
377
+ // 17c. Validate model:propName bindings
378
+ for (const mpb of modelPropBindings) {
379
+ const name = mpb.signal;
380
+ // Check if the referenced variable exists at all
381
+ const isKnown = signalNames.has(name) || computedNames.has(name) || propNamesSet.has(name) || constantNamesForModel.has(name);
382
+ if (!isKnown) {
383
+ const error = new Error(`model:propName references undeclared variable '${name}'`);
384
+ /** @ts-expect-error custom error code */
385
+ error.code = 'MODEL_PROP_UNKNOWN_VAR';
386
+ throw error;
387
+ }
388
+ // Check if the referenced variable is read-only
389
+ if (propNamesSet.has(name)) {
390
+ const error = new Error(`model:propName cannot bind to prop '${name}' (read-only)`);
391
+ /** @ts-expect-error — custom error code */
392
+ error.code = 'MODEL_PROP_READONLY';
393
+ throw error;
394
+ }
395
+ if (computedNames.has(name)) {
396
+ const error = new Error(`model:propName cannot bind to computed '${name}' (read-only)`);
397
+ /** @ts-expect-error — custom error code */
398
+ error.code = 'MODEL_PROP_READONLY';
399
+ throw error;
400
+ }
401
+ if (constantNamesForModel.has(name)) {
402
+ const error = new Error(`model:propName cannot bind to constant '${name}' (read-only)`);
403
+ /** @ts-expect-error — custom error code */
404
+ error.code = 'MODEL_PROP_READONLY';
405
+ throw error;
406
+ }
407
+ }
408
+
409
+ // 18. Child imports already built from extractWccImports (step 2b)
410
+ // No filesystem scanning needed — imports are explicit
411
+
412
+ // 19. Merge tree-walker results into ParseResult
413
+ parseResult.bindings = bindings;
414
+ parseResult.events = events;
415
+ parseResult.showBindings = showBindings;
416
+ parseResult.modelBindings = modelBindings;
417
+ parseResult.modelPropBindings = modelPropBindings;
418
+ parseResult.attrBindings = attrBindings;
419
+ parseResult.ifBlocks = ifBlocks;
420
+ parseResult.forBlocks = forBlocks;
421
+ parseResult.slots = slots;
422
+ parseResult.refBindings = refBindings;
423
+ parseResult.childComponents = childComponents;
424
+ parseResult.dynamicComponents = dynamicComponents;
425
+
426
+ parseResult.childImports = childImports;
427
+ parseResult.processedTemplate = rootEl.innerHTML;
428
+
429
+ // 20. Resolve standalone and generate component
430
+ const standaloneResolved = resolveStandalone(descriptor.standalone, config?.standalone ?? false);
431
+ const genOptions = { ...config, sourceFile: fileName };
432
+
433
+ if (standaloneResolved) {
434
+ // Force inline runtime — ignore any runtimeImportPath
435
+ genOptions.runtimeImportPath = undefined;
436
+ }
437
+ // If standaloneResolved is false, keep config.runtimeImportPath as-is (CLI provides it)
438
+
439
+ const code = generateComponent(parseResult, genOptions);
440
+ const usesSharedRuntime = !standaloneResolved && !!genOptions.runtimeImportPath;
441
+ return { code, usesSharedRuntime };
442
+ }
443
+
444
+ /**
445
+ * Resolve the final standalone value.
446
+ * Component-level has priority over global.
447
+ *
448
+ * @param {boolean | undefined} componentValue — standalone from defineComponent (true, false, or undefined)
449
+ * @param {boolean} globalValue standalone from config (true or false)
450
+ * @returns {boolean}
451
+ */
452
+ export function resolveStandalone(componentValue, globalValue) {
453
+ if (componentValue === true || componentValue === false) return componentValue;
454
+ return globalValue;
455
+ }
456
+
457
+ /**
458
+ * Compile a single .wcc SFC file into a self-contained JS component.
459
+ *
460
+ * @param {string} filePath Absolute or relative path to the .wcc file
461
+ * @param {object} [config] — Optional config (reserved for future options)
462
+ * @returns {Promise<{code: string, usesSharedRuntime: boolean}>} The generated JavaScript component code and metadata
463
+ */
464
+ export async function compile(filePath, config) {
465
+ const result = await compileSFC(filePath, config);
466
+
467
+ if (config?.minify) {
468
+ const { transform } = await import('esbuild');
469
+ try {
470
+ const minified = await transform(result.code, {
471
+ minify: true,
472
+ loader: 'js',
473
+ target: 'esnext',
474
+ });
475
+ result.code = minified.code;
476
+ } catch {
477
+ // If minification fails (e.g., edge-case syntax), return unminified code
478
+ // This is a graceful fallback — the code still works at runtime
479
+ }
480
+ }
481
+
482
+ return result;
483
+ }