@sprlab/wccompiler 0.13.0 → 0.15.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/bin/wcc.js CHANGED
@@ -1,412 +1,412 @@
1
- #!/usr/bin/env node
2
-
3
- import { readdirSync, writeFileSync, mkdirSync, existsSync, watch, copyFileSync, readFileSync } from 'node:fs';
4
- import { resolve, relative, extname, basename, dirname, join } from 'node:path';
5
- import { fileURLToPath } from 'node:url';
6
- import { loadConfig } from '../lib/config.js';
7
- import { compile } from '../lib/compiler.js';
8
- import { startDevServer } from '../lib/dev-server.js';
9
-
10
- const __filename = fileURLToPath(import.meta.url);
11
- const __dirname = dirname(__filename);
12
-
13
- const command = process.argv[2];
14
-
15
- async function build(config, cwd) {
16
- const inputDir = resolve(cwd, config.input);
17
- const outputDir = resolve(cwd, config.output);
18
-
19
- if (!existsSync(outputDir)) mkdirSync(outputDir, { recursive: true });
20
-
21
- const files = discoverFiles(inputDir);
22
- let errors = 0;
23
- let needsSharedRuntime = false;
24
-
25
- for (const file of files) {
26
- try {
27
- const relPath = relative(inputDir, file);
28
- const outPath = resolve(outputDir, relPath.replace(/\.wcc$/, '.js'));
29
- const outDir = dirname(outPath);
30
-
31
- // Calculate runtimeImportPath (always calculate it — the compiler decides whether to use it)
32
- const signalsDest = join(outputDir, '__wcc-signals.js');
33
- const runtimeRelPath = relative(outDir, signalsDest).replace(/\\/g, '/');
34
- const runtimeImportPath = runtimeRelPath.startsWith('.') ? runtimeRelPath : './' + runtimeRelPath;
35
-
36
- const { code, usesSharedRuntime } = await compile(file, {
37
- standalone: config.standalone,
38
- minify: config.minify,
39
- comments: config.comments,
40
- runtimeImportPath,
41
- });
42
-
43
- if (usesSharedRuntime) needsSharedRuntime = true;
44
-
45
- if (!existsSync(outDir)) mkdirSync(outDir, { recursive: true });
46
- writeFileSync(outPath, code);
47
- } catch (err) {
48
- console.error(`Error compiling ${file}: ${err.message}`);
49
- errors++;
50
- }
51
- }
52
-
53
- // Generate shared runtime ONLY if needed
54
- if (needsSharedRuntime) {
55
- const { reactiveRuntime } = await import('../lib/reactive-runtime.js');
56
- const signalsContent = reactiveRuntime.trim() + '\nexport { __signal, __computed, __effect, __batch, __untrack };\n';
57
- const signalsDest = join(outputDir, '__wcc-signals.js');
58
- writeFileSync(signalsDest, signalsContent);
59
- }
60
-
61
- // Copy wcc-runtime.js to output directory
62
- const runtimeSrc = resolve(__dirname, '../lib/wcc-runtime.js');
63
- const runtimeDest = join(outputDir, 'wcc-runtime.js');
64
- copyFileSync(runtimeSrc, runtimeDest);
65
-
66
- // Generate framework stubs (React + Vue) from compiled component metadata
67
- generateFrameworkStubs(outputDir);
68
-
69
- return errors;
70
- }
71
-
72
- /**
73
- * Generates framework stub files (React + Vue) from compiled component metadata.
74
- * Reads static __meta from each compiled .js file and produces:
75
- * - wcc-react.js + wcc-react.d.ts (importable stubs for React)
76
- * - wcc-vue.js + wcc-vue.d.ts (importable stubs for Vue)
77
- */
78
- function generateFrameworkStubs(outputDir) {
79
-
80
- const files = [];
81
- function collectJsFiles(dir) {
82
- for (const entry of readdirSync(dir, { withFileTypes: true })) {
83
- if (entry.isDirectory()) {
84
- collectJsFiles(join(dir, entry.name));
85
- } else if (entry.isFile() && entry.name.endsWith('.js') && !entry.name.startsWith('__') && entry.name !== 'wcc-runtime.js' && entry.name !== 'wcc-react.js' && entry.name !== 'wcc-vue.js') {
86
- files.push(join(dir, entry.name));
87
- }
88
- }
89
- }
90
- collectJsFiles(outputDir);
91
- const components = [];
92
-
93
- for (const file of files) {
94
- const content = readFileSync(file, 'utf-8');
95
- // Match static __meta = { ... }; with balanced braces
96
- const metaStart = content.indexOf('static __meta = {');
97
- if (metaStart === -1) continue;
98
-
99
- // Find the balanced closing brace
100
- let depth = 0;
101
- let metaEnd = -1;
102
- for (let i = metaStart + 'static __meta = '.length; i < content.length; i++) {
103
- if (content[i] === '{') depth++;
104
- else if (content[i] === '}') {
105
- depth--;
106
- if (depth === 0) { metaEnd = i + 1; break; }
107
- }
108
- }
109
- if (metaEnd === -1) continue;
110
-
111
- const metaStr = content.slice(metaStart + 'static __meta = '.length, metaEnd);
112
-
113
- try {
114
- const parsed = metaStr
115
- .replace(/'/g, '"')
116
- .replace(/(\w+)\s*:/g, '"$1":')
117
- .replace(/,\s*}/g, '}')
118
- .replace(/,\s*]/g, ']');
119
- const meta = JSON.parse(parsed);
120
- if (!meta.tag) continue;
121
-
122
- const pascalName = meta.tag.split('-').map(s => s[0].toUpperCase() + s.slice(1)).join('');
123
- components.push({ meta, pascalName, file });
124
- } catch (e) {
125
- // Skip unparseable
126
- }
127
- }
128
-
129
- if (components.length === 0) return;
130
-
131
- // ── React stubs ──
132
- let reactJs = '// Auto-generated by wcc build — React component stubs\n';
133
- reactJs += '// Import these in your JSX. The wccReactPlugin transforms them at build time.\n\n';
134
-
135
- let reactDts = '// Auto-generated by wcc build — React component types\n\n';
136
-
137
- for (const comp of components) {
138
- const slots = comp.meta.slots || [];
139
- const slotProps = slots.filter(s => s).map(s => {
140
- const pascal = s[0].toUpperCase() + s.slice(1);
141
- return `${pascal}: '${s}'`;
142
- });
143
-
144
- // JS stub
145
- reactJs += `export const ${comp.pascalName} = Object.assign('${comp.meta.tag}', { __tag: '${comp.meta.tag}'`;
146
- for (const s of slots) {
147
- if (!s) continue;
148
- const pascal = s[0].toUpperCase() + s.slice(1);
149
- reactJs += `, ${pascal}: '${s}'`;
150
- }
151
- reactJs += ` });\n`;
152
-
153
- // TypeScript declaration
154
- const slotTypes = slots.filter(s => s).map(s => {
155
- const pascal = s[0].toUpperCase() + s.slice(1);
156
- return ` ${pascal}: string;`;
157
- }).join('\n');
158
-
159
- reactDts += `export declare const ${comp.pascalName}: string & {\n __tag: '${comp.meta.tag}';\n${slotTypes}\n};\n\n`;
160
- }
161
-
162
- writeFileSync(join(outputDir, 'wcc-react.js'), reactJs);
163
- writeFileSync(join(outputDir, 'wcc-react.d.ts'), reactDts);
164
-
165
- // ── Vue stubs ──
166
- let vueJs = '// Auto-generated by wcc build — Vue component stubs\n';
167
- vueJs += '// Import these in your Vue SFC for type safety and IDE support.\n\n';
168
-
169
- let vueDts = '// Auto-generated by wcc build — Vue component types\n';
170
- vueDts += '// Include this file in your tsconfig.json for Volar autocompletion.\n\n';
171
-
172
- for (const comp of components) {
173
- const props = comp.meta.props || [];
174
- const events = comp.meta.events || [];
175
- const models = comp.meta.models || [];
176
- const slots = comp.meta.slots || [];
177
-
178
- // JS stub (just the tag name — Vue uses kebab-case directly)
179
- vueJs += `export const ${comp.pascalName} = '${comp.meta.tag}';\n`;
180
-
181
- // TypeScript declaration with props/events/slots info
182
- const propTypes = props.map(p => {
183
- const def = String(p.default);
184
- const type = def === 'true' || def === 'false' ? 'boolean'
185
- : /^-?\d+(\.\d+)?$/.test(def) ? 'number'
186
- : 'string';
187
- return ` ${p.name}?: ${type};`;
188
- }).join('\n');
189
-
190
- const eventTypes = events.map(e => ` '${e}': CustomEvent;`).join('\n');
191
- const modelTypes = models.map(m => ` '${m}': any;`).join('\n');
192
- const slotTypeEntries = slots.filter(s => s).map(s => ` '${s}': any;`).join('\n');
193
-
194
- vueDts += `export declare const ${comp.pascalName}: '${comp.meta.tag}';\n`;
195
- vueDts += `/** Component: ${comp.meta.tag} */\n`;
196
- if (props.length) vueDts += `export interface ${comp.pascalName}Props {\n${propTypes}\n}\n`;
197
- if (events.length) vueDts += `export interface ${comp.pascalName}Events {\n${eventTypes}\n}\n`;
198
- if (models.length) vueDts += `export interface ${comp.pascalName}Models {\n${modelTypes}\n}\n`;
199
- if (slots.filter(s => s).length) vueDts += `export interface ${comp.pascalName}Slots {\n${slotTypeEntries}\n}\n`;
200
- vueDts += '\n';
201
- }
202
-
203
- // ── Vue GlobalComponents (Volar autocompletion in templates) ──
204
- vueDts += '// ── Volar Global Component Types ──────────────────────────────────\n';
205
- vueDts += '// Add this file to tsconfig.json "include" for template autocompletion.\n\n';
206
- vueDts += "declare module 'vue' {\n";
207
- vueDts += ' export interface GlobalComponents {\n';
208
-
209
- for (const comp of components) {
210
- const props = comp.meta.props || [];
211
- const models = comp.meta.models || [];
212
- const events = comp.meta.events || [];
213
-
214
- // Build $props type inline
215
- const allProps = [
216
- ...props.map(p => {
217
- const def = String(p.default);
218
- const type = def === 'true' || def === 'false' ? 'boolean'
219
- : /^-?\d+(\.\d+)?$/.test(def) ? 'number'
220
- : 'string';
221
- return `${p.name}?: ${type}`;
222
- }),
223
- ...models.map(m => `${m}?: any`),
224
- ];
225
-
226
- const propsStr = allProps.length > 0
227
- ? `{ ${allProps.join('; ')} }`
228
- : '{}';
229
-
230
- // Build $emit type inline
231
- const emitEntries = [
232
- ...events.map(e => `(e: '${e}', value: any): void`),
233
- ...models.map(m => `(e: '${m}-changed', value: any): void`),
234
- ];
235
-
236
- if (emitEntries.length > 0) {
237
- vueDts += ` '${comp.meta.tag}': new () => { $props: ${propsStr}; $emit: { ${emitEntries.join('; ')} } };\n`;
238
- } else {
239
- vueDts += ` '${comp.meta.tag}': new () => { $props: ${propsStr} };\n`;
240
- }
241
- }
242
-
243
- vueDts += ' }\n';
244
- vueDts += '}\n\n';
245
- vueDts += 'export {}\n';
246
-
247
- writeFileSync(join(outputDir, 'wcc-vue.js'), vueJs);
248
- writeFileSync(join(outputDir, 'wcc-vue.d.ts'), vueDts);
249
-
250
- // ── HTML Custom Data (for VS Code / Kiro HTML intellisense) ──
251
- const htmlData = {
252
- version: 1.1,
253
- tags: components.map(comp => {
254
- const props = comp.meta.props || [];
255
- const events = comp.meta.events || [];
256
- const models = comp.meta.models || [];
257
-
258
- const attributes = [
259
- ...props.map(p => {
260
- const def = String(p.default);
261
- const type = def === 'true' || def === 'false' ? 'boolean'
262
- : /^-?\d+(\.\d+)?$/.test(def) ? 'number' : 'string';
263
- return { name: `:${p.name}`, description: `(prop) ${type}` };
264
- }),
265
- ...models.map(m => ({ name: `:${m}`, description: `(model) two-way binding` })),
266
- ...events.map(e => ({ name: `@${e}`, description: `(event)` })),
267
- ];
268
-
269
- return {
270
- name: comp.meta.tag,
271
- description: `WCC Component: ${comp.meta.tag}`,
272
- attributes,
273
- };
274
- }),
275
- };
276
-
277
- writeFileSync(join(outputDir, 'wcc-html-data.json'), JSON.stringify(htmlData, null, 2));
278
- }
279
-
280
- function discoverFiles(dir) {
281
- const results = [];
282
- const entries = readdirSync(dir, { withFileTypes: true, recursive: true });
283
- for (const entry of entries) {
284
- if (!entry.isFile()) continue;
285
- const ext = extname(entry.name);
286
- if (ext !== '.wcc') continue;
287
- if (entry.name.includes('.test.')) continue;
288
- const fullPath = resolve(dir, entry.parentPath ? relative(dir, entry.parentPath) : '', entry.name);
289
- results.push(fullPath);
290
- }
291
- return results;
292
- }
293
-
294
- /**
295
- * Discovers compiled .js entry points in the output directory.
296
- * Excludes runtime files, stubs, and metadata.
297
- */
298
- function discoverCompiledEntries(outputDir) {
299
- const skip = new Set(['__wcc-signals.js', 'wcc-runtime.js', 'wcc-react.js', 'wcc-vue.js', 'bundle.js']);
300
- const results = [];
301
- function walk(dir) {
302
- for (const entry of readdirSync(dir, { withFileTypes: true })) {
303
- if (entry.isDirectory()) {
304
- walk(join(dir, entry.name));
305
- } else if (entry.isFile() && entry.name.endsWith('.js') && !skip.has(entry.name) && !entry.name.endsWith('.d.ts')) {
306
- results.push(join(dir, entry.name));
307
- }
308
- }
309
- }
310
- walk(outputDir);
311
- return results;
312
- }
313
-
314
- async function main() {
315
- const cwd = process.cwd();
316
- const config = await loadConfig(cwd);
317
-
318
- // CLI flags override config
319
- if (process.argv.includes('--minify')) config.minify = true;
320
- if (process.argv.includes('--comments')) config.comments = true;
321
- const shouldBundle = process.argv.includes('--bundle');
322
-
323
- if (command === 'build') {
324
- const errors = await build(config, cwd);
325
- if (errors > 0) process.exit(1);
326
-
327
- // Bundle step: produce a single IIFE file from all compiled entry points
328
- if (shouldBundle) {
329
- const { build: esbuild } = await import('esbuild');
330
- const outputDir = resolve(cwd, config.output);
331
- const entryPoints = discoverCompiledEntries(outputDir);
332
-
333
- if (entryPoints.length > 0) {
334
- // Generate a virtual entry that imports all components
335
- const bundleEntry = join(outputDir, '__bundle-entry.js');
336
- const imports = entryPoints.map(f => {
337
- let rel = relative(outputDir, f).replace(/\\/g, '/');
338
- if (!rel.startsWith('.')) rel = './' + rel;
339
- return `import '${rel}';`;
340
- }).join('\n');
341
- writeFileSync(bundleEntry, imports);
342
-
343
- await esbuild({
344
- entryPoints: [bundleEntry],
345
- bundle: true,
346
- format: 'iife',
347
- outfile: join(outputDir, 'bundle.js'),
348
- minify: !!config.minify,
349
- });
350
-
351
- // Clean up temp entry
352
- const { unlinkSync } = await import('node:fs');
353
- unlinkSync(bundleEntry);
354
-
355
- console.log(`Bundled ${entryPoints.length} components → ${config.output}/bundle.js`);
356
- }
357
- }
358
- } else if (command === 'dev') {
359
- await build(config, cwd);
360
- const outputDir = resolve(cwd, config.output);
361
- const devServer = startDevServer({ port: config.port, root: cwd, outputDir });
362
- const inputDir = resolve(cwd, config.input);
363
- console.log(`Watching ${inputDir} for changes...`);
364
- watch(inputDir, { recursive: true }, async (eventType, filename) => {
365
- if (!filename) return;
366
- const ext = extname(filename);
367
- if (ext !== '.ts' && ext !== '.js' && ext !== '.wcc') return;
368
- if (filename.includes('.test.')) return;
369
- const filePath = resolve(inputDir, filename);
370
- try {
371
- const relPath = filename;
372
- const outPath = resolve(outputDir, relPath.replace(/\.ts$/, '.js').replace(/\.wcc$/, '.js'));
373
- const outDir = dirname(outPath);
374
-
375
- // Calculate runtimeImportPath for this file
376
- const signalsDest = join(outputDir, '__wcc-signals.js');
377
- const runtimeRelPath = relative(outDir, signalsDest).replace(/\\/g, '/');
378
- const runtimeImportPath = runtimeRelPath.startsWith('.') ? runtimeRelPath : './' + runtimeRelPath;
379
-
380
- const { code, usesSharedRuntime } = await compile(filePath, {
381
- standalone: config.standalone,
382
- minify: config.minify,
383
- comments: config.comments,
384
- runtimeImportPath,
385
- });
386
-
387
- if (!existsSync(outDir)) mkdirSync(outDir, { recursive: true });
388
- writeFileSync(outPath, code);
389
-
390
- // If this component uses shared runtime and the file doesn't exist yet, generate it
391
- if (usesSharedRuntime && !existsSync(signalsDest)) {
392
- const { reactiveRuntime } = await import('../lib/reactive-runtime.js');
393
- const signalsContent = reactiveRuntime.trim() + '\nexport { __signal, __computed, __effect, __batch, __untrack };\n';
394
- writeFileSync(signalsDest, signalsContent);
395
- }
396
-
397
- console.log(`Compiled: ${filename}`);
398
- } catch (err) {
399
- console.error(`Error compiling ${filename}: ${err.message}`);
400
- devServer.notifyError(`${filename}\n\n${err.message}`);
401
- }
402
- });
403
- } else {
404
- console.error('Usage: wcc <build|dev>');
405
- process.exit(1);
406
- }
407
- }
408
-
409
- main().catch(err => {
410
- console.error(err.message);
411
- process.exit(1);
412
- });
1
+ #!/usr/bin/env node
2
+
3
+ import { readdirSync, writeFileSync, mkdirSync, existsSync, watch, copyFileSync, readFileSync } from 'node:fs';
4
+ import { resolve, relative, extname, basename, dirname, join } from 'node:path';
5
+ import { fileURLToPath } from 'node:url';
6
+ import { loadConfig } from '../lib/config.js';
7
+ import { compile } from '../lib/compiler.js';
8
+ import { startDevServer } from '../lib/dev-server.js';
9
+
10
+ const __filename = fileURLToPath(import.meta.url);
11
+ const __dirname = dirname(__filename);
12
+
13
+ const command = process.argv[2];
14
+
15
+ async function build(config, cwd) {
16
+ const inputDir = resolve(cwd, config.input);
17
+ const outputDir = resolve(cwd, config.output);
18
+
19
+ if (!existsSync(outputDir)) mkdirSync(outputDir, { recursive: true });
20
+
21
+ const files = discoverFiles(inputDir);
22
+ let errors = 0;
23
+ let needsSharedRuntime = false;
24
+
25
+ for (const file of files) {
26
+ try {
27
+ const relPath = relative(inputDir, file);
28
+ const outPath = resolve(outputDir, relPath.replace(/\.wcc$/, '.js'));
29
+ const outDir = dirname(outPath);
30
+
31
+ // Calculate runtimeImportPath (always calculate it — the compiler decides whether to use it)
32
+ const signalsDest = join(outputDir, '__wcc-signals.js');
33
+ const runtimeRelPath = relative(outDir, signalsDest).replace(/\\/g, '/');
34
+ const runtimeImportPath = runtimeRelPath.startsWith('.') ? runtimeRelPath : './' + runtimeRelPath;
35
+
36
+ const { code, usesSharedRuntime } = await compile(file, {
37
+ standalone: config.standalone,
38
+ minify: config.minify,
39
+ comments: config.comments,
40
+ runtimeImportPath,
41
+ });
42
+
43
+ if (usesSharedRuntime) needsSharedRuntime = true;
44
+
45
+ if (!existsSync(outDir)) mkdirSync(outDir, { recursive: true });
46
+ writeFileSync(outPath, code);
47
+ } catch (err) {
48
+ console.error(`Error compiling ${file}: ${err.message}`);
49
+ errors++;
50
+ }
51
+ }
52
+
53
+ // Generate shared runtime ONLY if needed
54
+ if (needsSharedRuntime) {
55
+ const { reactiveRuntime } = await import('../lib/reactive-runtime.js');
56
+ const signalsContent = reactiveRuntime.trim() + '\nexport { __signal, __computed, __effect, __batch, __untrack };\n';
57
+ const signalsDest = join(outputDir, '__wcc-signals.js');
58
+ writeFileSync(signalsDest, signalsContent);
59
+ }
60
+
61
+ // Copy wcc-runtime.js to output directory
62
+ const runtimeSrc = resolve(__dirname, '../lib/wcc-runtime.js');
63
+ const runtimeDest = join(outputDir, 'wcc-runtime.js');
64
+ copyFileSync(runtimeSrc, runtimeDest);
65
+
66
+ // Generate framework stubs (React + Vue) from compiled component metadata
67
+ generateFrameworkStubs(outputDir);
68
+
69
+ return errors;
70
+ }
71
+
72
+ /**
73
+ * Generates framework stub files (React + Vue) from compiled component metadata.
74
+ * Reads static __meta from each compiled .js file and produces:
75
+ * - wcc-react.js + wcc-react.d.ts (importable stubs for React)
76
+ * - wcc-vue.js + wcc-vue.d.ts (importable stubs for Vue)
77
+ */
78
+ function generateFrameworkStubs(outputDir) {
79
+
80
+ const files = [];
81
+ function collectJsFiles(dir) {
82
+ for (const entry of readdirSync(dir, { withFileTypes: true })) {
83
+ if (entry.isDirectory()) {
84
+ collectJsFiles(join(dir, entry.name));
85
+ } else if (entry.isFile() && entry.name.endsWith('.js') && !entry.name.startsWith('__') && entry.name !== 'wcc-runtime.js' && entry.name !== 'wcc-react.js' && entry.name !== 'wcc-vue.js') {
86
+ files.push(join(dir, entry.name));
87
+ }
88
+ }
89
+ }
90
+ collectJsFiles(outputDir);
91
+ const components = [];
92
+
93
+ for (const file of files) {
94
+ const content = readFileSync(file, 'utf-8');
95
+ // Match static __meta = { ... }; with balanced braces
96
+ const metaStart = content.indexOf('static __meta = {');
97
+ if (metaStart === -1) continue;
98
+
99
+ // Find the balanced closing brace
100
+ let depth = 0;
101
+ let metaEnd = -1;
102
+ for (let i = metaStart + 'static __meta = '.length; i < content.length; i++) {
103
+ if (content[i] === '{') depth++;
104
+ else if (content[i] === '}') {
105
+ depth--;
106
+ if (depth === 0) { metaEnd = i + 1; break; }
107
+ }
108
+ }
109
+ if (metaEnd === -1) continue;
110
+
111
+ const metaStr = content.slice(metaStart + 'static __meta = '.length, metaEnd);
112
+
113
+ try {
114
+ const parsed = metaStr
115
+ .replace(/'/g, '"')
116
+ .replace(/(\w+)\s*:/g, '"$1":')
117
+ .replace(/,\s*}/g, '}')
118
+ .replace(/,\s*]/g, ']');
119
+ const meta = JSON.parse(parsed);
120
+ if (!meta.tag) continue;
121
+
122
+ const pascalName = meta.tag.split('-').map(s => s[0].toUpperCase() + s.slice(1)).join('');
123
+ components.push({ meta, pascalName, file });
124
+ } catch (e) {
125
+ // Skip unparseable
126
+ }
127
+ }
128
+
129
+ if (components.length === 0) return;
130
+
131
+ // ── React stubs ──
132
+ let reactJs = '// Auto-generated by wcc build — React component stubs\n';
133
+ reactJs += '// Import these in your JSX. The wccReactPlugin transforms them at build time.\n\n';
134
+
135
+ let reactDts = '// Auto-generated by wcc build — React component types\n\n';
136
+
137
+ for (const comp of components) {
138
+ const slots = comp.meta.slots || [];
139
+ const slotProps = slots.filter(s => s).map(s => {
140
+ const pascal = s[0].toUpperCase() + s.slice(1);
141
+ return `${pascal}: '${s}'`;
142
+ });
143
+
144
+ // JS stub
145
+ reactJs += `export const ${comp.pascalName} = Object.assign('${comp.meta.tag}', { __tag: '${comp.meta.tag}'`;
146
+ for (const s of slots) {
147
+ if (!s) continue;
148
+ const pascal = s[0].toUpperCase() + s.slice(1);
149
+ reactJs += `, ${pascal}: '${s}'`;
150
+ }
151
+ reactJs += ` });\n`;
152
+
153
+ // TypeScript declaration
154
+ const slotTypes = slots.filter(s => s).map(s => {
155
+ const pascal = s[0].toUpperCase() + s.slice(1);
156
+ return ` ${pascal}: string;`;
157
+ }).join('\n');
158
+
159
+ reactDts += `export declare const ${comp.pascalName}: string & {\n __tag: '${comp.meta.tag}';\n${slotTypes}\n};\n\n`;
160
+ }
161
+
162
+ writeFileSync(join(outputDir, 'wcc-react.js'), reactJs);
163
+ writeFileSync(join(outputDir, 'wcc-react.d.ts'), reactDts);
164
+
165
+ // ── Vue stubs ──
166
+ let vueJs = '// Auto-generated by wcc build — Vue component stubs\n';
167
+ vueJs += '// Import these in your Vue SFC for type safety and IDE support.\n\n';
168
+
169
+ let vueDts = '// Auto-generated by wcc build — Vue component types\n';
170
+ vueDts += '// Include this file in your tsconfig.json for Volar autocompletion.\n\n';
171
+
172
+ for (const comp of components) {
173
+ const props = comp.meta.props || [];
174
+ const events = comp.meta.events || [];
175
+ const models = comp.meta.models || [];
176
+ const slots = comp.meta.slots || [];
177
+
178
+ // JS stub (just the tag name — Vue uses kebab-case directly)
179
+ vueJs += `export const ${comp.pascalName} = '${comp.meta.tag}';\n`;
180
+
181
+ // TypeScript declaration with props/events/slots info
182
+ const propTypes = props.map(p => {
183
+ const def = String(p.default);
184
+ const type = def === 'true' || def === 'false' ? 'boolean'
185
+ : /^-?\d+(\.\d+)?$/.test(def) ? 'number'
186
+ : 'string';
187
+ return ` ${p.name}?: ${type};`;
188
+ }).join('\n');
189
+
190
+ const eventTypes = events.map(e => ` '${e}': CustomEvent;`).join('\n');
191
+ const modelTypes = models.map(m => ` '${m}': any;`).join('\n');
192
+ const slotTypeEntries = slots.filter(s => s).map(s => ` '${s}': any;`).join('\n');
193
+
194
+ vueDts += `export declare const ${comp.pascalName}: '${comp.meta.tag}';\n`;
195
+ vueDts += `/** Component: ${comp.meta.tag} */\n`;
196
+ if (props.length) vueDts += `export interface ${comp.pascalName}Props {\n${propTypes}\n}\n`;
197
+ if (events.length) vueDts += `export interface ${comp.pascalName}Events {\n${eventTypes}\n}\n`;
198
+ if (models.length) vueDts += `export interface ${comp.pascalName}Models {\n${modelTypes}\n}\n`;
199
+ if (slots.filter(s => s).length) vueDts += `export interface ${comp.pascalName}Slots {\n${slotTypeEntries}\n}\n`;
200
+ vueDts += '\n';
201
+ }
202
+
203
+ // ── Vue GlobalComponents (Volar autocompletion in templates) ──
204
+ vueDts += '// ── Volar Global Component Types ──────────────────────────────────\n';
205
+ vueDts += '// Add this file to tsconfig.json "include" for template autocompletion.\n\n';
206
+ vueDts += "declare module 'vue' {\n";
207
+ vueDts += ' export interface GlobalComponents {\n';
208
+
209
+ for (const comp of components) {
210
+ const props = comp.meta.props || [];
211
+ const models = comp.meta.models || [];
212
+ const events = comp.meta.events || [];
213
+
214
+ // Build $props type inline
215
+ const allProps = [
216
+ ...props.map(p => {
217
+ const def = String(p.default);
218
+ const type = def === 'true' || def === 'false' ? 'boolean'
219
+ : /^-?\d+(\.\d+)?$/.test(def) ? 'number'
220
+ : 'string';
221
+ return `${p.name}?: ${type}`;
222
+ }),
223
+ ...models.map(m => `${m}?: any`),
224
+ ];
225
+
226
+ const propsStr = allProps.length > 0
227
+ ? `{ ${allProps.join('; ')} }`
228
+ : '{}';
229
+
230
+ // Build $emit type inline
231
+ const emitEntries = [
232
+ ...events.map(e => `(e: '${e}', value: any): void`),
233
+ ...models.map(m => `(e: '${m}-changed', value: any): void`),
234
+ ];
235
+
236
+ if (emitEntries.length > 0) {
237
+ vueDts += ` '${comp.meta.tag}': new () => { $props: ${propsStr}; $emit: { ${emitEntries.join('; ')} } };\n`;
238
+ } else {
239
+ vueDts += ` '${comp.meta.tag}': new () => { $props: ${propsStr} };\n`;
240
+ }
241
+ }
242
+
243
+ vueDts += ' }\n';
244
+ vueDts += '}\n\n';
245
+ vueDts += 'export {}\n';
246
+
247
+ writeFileSync(join(outputDir, 'wcc-vue.js'), vueJs);
248
+ writeFileSync(join(outputDir, 'wcc-vue.d.ts'), vueDts);
249
+
250
+ // ── HTML Custom Data (for VS Code / Kiro HTML intellisense) ──
251
+ const htmlData = {
252
+ version: 1.1,
253
+ tags: components.map(comp => {
254
+ const props = comp.meta.props || [];
255
+ const events = comp.meta.events || [];
256
+ const models = comp.meta.models || [];
257
+
258
+ const attributes = [
259
+ ...props.map(p => {
260
+ const def = String(p.default);
261
+ const type = def === 'true' || def === 'false' ? 'boolean'
262
+ : /^-?\d+(\.\d+)?$/.test(def) ? 'number' : 'string';
263
+ return { name: `:${p.name}`, description: `(prop) ${type}` };
264
+ }),
265
+ ...models.map(m => ({ name: `:${m}`, description: `(model) two-way binding` })),
266
+ ...events.map(e => ({ name: `@${e}`, description: `(event)` })),
267
+ ];
268
+
269
+ return {
270
+ name: comp.meta.tag,
271
+ description: `WCC Component: ${comp.meta.tag}`,
272
+ attributes,
273
+ };
274
+ }),
275
+ };
276
+
277
+ writeFileSync(join(outputDir, 'wcc-html-data.json'), JSON.stringify(htmlData, null, 2));
278
+ }
279
+
280
+ function discoverFiles(dir) {
281
+ const results = [];
282
+ const entries = readdirSync(dir, { withFileTypes: true, recursive: true });
283
+ for (const entry of entries) {
284
+ if (!entry.isFile()) continue;
285
+ const ext = extname(entry.name);
286
+ if (ext !== '.wcc') continue;
287
+ if (entry.name.includes('.test.')) continue;
288
+ const fullPath = resolve(dir, entry.parentPath ? relative(dir, entry.parentPath) : '', entry.name);
289
+ results.push(fullPath);
290
+ }
291
+ return results;
292
+ }
293
+
294
+ /**
295
+ * Discovers compiled .js entry points in the output directory.
296
+ * Excludes runtime files, stubs, and metadata.
297
+ */
298
+ function discoverCompiledEntries(outputDir) {
299
+ const skip = new Set(['__wcc-signals.js', 'wcc-runtime.js', 'wcc-react.js', 'wcc-vue.js', 'bundle.js']);
300
+ const results = [];
301
+ function walk(dir) {
302
+ for (const entry of readdirSync(dir, { withFileTypes: true })) {
303
+ if (entry.isDirectory()) {
304
+ walk(join(dir, entry.name));
305
+ } else if (entry.isFile() && entry.name.endsWith('.js') && !skip.has(entry.name) && !entry.name.endsWith('.d.ts')) {
306
+ results.push(join(dir, entry.name));
307
+ }
308
+ }
309
+ }
310
+ walk(outputDir);
311
+ return results;
312
+ }
313
+
314
+ async function main() {
315
+ const cwd = process.cwd();
316
+ const config = await loadConfig(cwd);
317
+
318
+ // CLI flags override config
319
+ if (process.argv.includes('--minify')) config.minify = true;
320
+ if (process.argv.includes('--comments')) config.comments = true;
321
+ const shouldBundle = process.argv.includes('--bundle');
322
+
323
+ if (command === 'build') {
324
+ const errors = await build(config, cwd);
325
+ if (errors > 0) process.exit(1);
326
+
327
+ // Bundle step: produce a single IIFE file from all compiled entry points
328
+ if (shouldBundle) {
329
+ const { build: esbuild } = await import('esbuild');
330
+ const outputDir = resolve(cwd, config.output);
331
+ const entryPoints = discoverCompiledEntries(outputDir);
332
+
333
+ if (entryPoints.length > 0) {
334
+ // Generate a virtual entry that imports all components
335
+ const bundleEntry = join(outputDir, '__bundle-entry.js');
336
+ const imports = entryPoints.map(f => {
337
+ let rel = relative(outputDir, f).replace(/\\/g, '/');
338
+ if (!rel.startsWith('.')) rel = './' + rel;
339
+ return `import '${rel}';`;
340
+ }).join('\n');
341
+ writeFileSync(bundleEntry, imports);
342
+
343
+ await esbuild({
344
+ entryPoints: [bundleEntry],
345
+ bundle: true,
346
+ format: 'iife',
347
+ outfile: join(outputDir, 'bundle.js'),
348
+ minify: !!config.minify,
349
+ });
350
+
351
+ // Clean up temp entry
352
+ const { unlinkSync } = await import('node:fs');
353
+ unlinkSync(bundleEntry);
354
+
355
+ console.log(`Bundled ${entryPoints.length} components → ${config.output}/bundle.js`);
356
+ }
357
+ }
358
+ } else if (command === 'dev') {
359
+ await build(config, cwd);
360
+ const outputDir = resolve(cwd, config.output);
361
+ const devServer = startDevServer({ port: config.port, root: cwd, outputDir });
362
+ const inputDir = resolve(cwd, config.input);
363
+ console.log(`Watching ${inputDir} for changes...`);
364
+ watch(inputDir, { recursive: true }, async (eventType, filename) => {
365
+ if (!filename) return;
366
+ const ext = extname(filename);
367
+ if (ext !== '.ts' && ext !== '.js' && ext !== '.wcc') return;
368
+ if (filename.includes('.test.')) return;
369
+ const filePath = resolve(inputDir, filename);
370
+ try {
371
+ const relPath = filename;
372
+ const outPath = resolve(outputDir, relPath.replace(/\.ts$/, '.js').replace(/\.wcc$/, '.js'));
373
+ const outDir = dirname(outPath);
374
+
375
+ // Calculate runtimeImportPath for this file
376
+ const signalsDest = join(outputDir, '__wcc-signals.js');
377
+ const runtimeRelPath = relative(outDir, signalsDest).replace(/\\/g, '/');
378
+ const runtimeImportPath = runtimeRelPath.startsWith('.') ? runtimeRelPath : './' + runtimeRelPath;
379
+
380
+ const { code, usesSharedRuntime } = await compile(filePath, {
381
+ standalone: config.standalone,
382
+ minify: config.minify,
383
+ comments: config.comments,
384
+ runtimeImportPath,
385
+ });
386
+
387
+ if (!existsSync(outDir)) mkdirSync(outDir, { recursive: true });
388
+ writeFileSync(outPath, code);
389
+
390
+ // If this component uses shared runtime and the file doesn't exist yet, generate it
391
+ if (usesSharedRuntime && !existsSync(signalsDest)) {
392
+ const { reactiveRuntime } = await import('../lib/reactive-runtime.js');
393
+ const signalsContent = reactiveRuntime.trim() + '\nexport { __signal, __computed, __effect, __batch, __untrack };\n';
394
+ writeFileSync(signalsDest, signalsContent);
395
+ }
396
+
397
+ console.log(`Compiled: ${filename}`);
398
+ } catch (err) {
399
+ console.error(`Error compiling ${filename}: ${err.message}`);
400
+ devServer.notifyError(`${filename}\n\n${err.message}`);
401
+ }
402
+ });
403
+ } else {
404
+ console.error('Usage: wcc <build|dev>');
405
+ process.exit(1);
406
+ }
407
+ }
408
+
409
+ main().catch(err => {
410
+ console.error(err.message);
411
+ process.exit(1);
412
+ });