@sprlab/wccompiler 0.11.9 → 0.11.12

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 CHANGED
@@ -897,6 +897,43 @@ No configuration needed:
897
897
  </wcc-list>
898
898
  ```
899
899
 
900
+ ### TypeScript Types for Frameworks
901
+
902
+ `wcc build` auto-generates typed stubs for each framework in the `dist/` folder:
903
+
904
+ ```
905
+ dist/
906
+ ├── wcc-vue.d.ts ← Vue/Volar prop autocompletion
907
+ ├── wcc-vue.js ← Vue component stubs
908
+ ├── wcc-react.d.ts ← React compound component types
909
+ ├── wcc-react.js ← React component stubs
910
+ └── ...
911
+ ```
912
+
913
+ **Vue (Volar autocompletion)**
914
+
915
+ Add `dist/wcc-vue.d.ts` to your tsconfig to get prop/event autocompletion in `.vue` templates:
916
+
917
+ ```json
918
+ // tsconfig.json
919
+ {
920
+ "include": ["src/**/*", "dist/wcc-vue.d.ts"]
921
+ }
922
+ ```
923
+
924
+ After this, Volar provides:
925
+ - Prop autocompletion: `<wcc-counter :la|` → suggests `label`
926
+ - Type-checking: `<wcc-counter :count="'string'">` → type error (expects number)
927
+ - Event types on hover
928
+
929
+ **React**
930
+
931
+ React 19 treats custom elements (hyphenated tags) as `any` in JSX — this is by React's design. No additional type setup needed. Compound component stubs (`WccCard.Header`) are typed via `dist/wcc-react.d.ts` and work when imported directly.
932
+
933
+ **Angular**
934
+
935
+ Angular's `CUSTOM_ELEMENTS_SCHEMA` disables all type-checking on custom elements. No additional type setup possible from the library side.
936
+
900
937
  ## Editor Support
901
938
 
902
939
  The **wcCompiler (.wcc) Language Support** extension is available on the VS Code Marketplace. It provides syntax highlighting, completions, and diagnostics for `.wcc` files.
package/bin/wcc.js CHANGED
@@ -77,11 +77,21 @@ async function build(config, cwd) {
77
77
  */
78
78
  function generateFrameworkStubs(outputDir) {
79
79
 
80
- const files = readdirSync(outputDir).filter(f => f.endsWith('.js') && !f.startsWith('__') && f !== 'wcc-runtime.js' && f !== 'wcc-react.js' && f !== 'wcc-vue.js');
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);
81
91
  const components = [];
82
92
 
83
93
  for (const file of files) {
84
- const content = readFileSync(join(outputDir, file), 'utf-8');
94
+ const content = readFileSync(file, 'utf-8');
85
95
  // Match static __meta = { ... }; with balanced braces
86
96
  const metaStart = content.indexOf('static __meta = {');
87
97
  if (metaStart === -1) continue;
@@ -156,7 +166,8 @@ function generateFrameworkStubs(outputDir) {
156
166
  let vueJs = '// Auto-generated by wcc build — Vue component stubs\n';
157
167
  vueJs += '// Import these in your Vue SFC for type safety and IDE support.\n\n';
158
168
 
159
- let vueDts = '// Auto-generated by wcc build — Vue component types\n\n';
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';
160
171
 
161
172
  for (const comp of components) {
162
173
  const props = comp.meta.props || [];
@@ -189,8 +200,81 @@ function generateFrameworkStubs(outputDir) {
189
200
  vueDts += '\n';
190
201
  }
191
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
+
192
247
  writeFileSync(join(outputDir, 'wcc-vue.js'), vueJs);
193
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));
194
278
  }
195
279
 
196
280
  function discoverFiles(dir) {
package/lib/compiler.js CHANGED
@@ -43,6 +43,7 @@ import {
43
43
  validateUndeclaredEmits,
44
44
  } from './parser-extractors.js';
45
45
  import { stripTypes } from './parser.js';
46
+ import { normalizeTemplate } from './template-normalizer.js';
46
47
 
47
48
  /**
48
49
  * Resolve a child component's source file path by tag name.
@@ -290,7 +291,8 @@ async function compileSFC(filePath, config) {
290
291
  };
291
292
 
292
293
  // 16. Process template through linkedom → tree-walker → codegen
293
- const { document } = parseHTML(`<div id="__root">${template}</div>`);
294
+ const normalizedTemplate = normalizeTemplate(template);
295
+ const { document } = parseHTML(`<div id="__root">${normalizedTemplate}</div>`);
294
296
  const rootEl = document.getElementById('__root');
295
297
 
296
298
  const signalNames = new Set(signals.map(s => s.name));
@@ -0,0 +1,89 @@
1
+ /**
2
+ * Template Normalizer — pre-processes WCC template HTML before DOM parsing.
3
+ *
4
+ * Handles two transformations:
5
+ * 1. PascalCase tags → kebab-case (e.g. <WccBadge> → <wcc-badge>)
6
+ * 2. Self-closing custom elements → open+close (e.g. <wcc-badge /> → <wcc-badge></wcc-badge>)
7
+ *
8
+ * Must run BEFORE linkedom/jsdom parsing because HTML parsers:
9
+ * - Lowercase all tag names (losing PascalCase word boundaries)
10
+ * - Don't recognize self-closing syntax for non-void elements
11
+ */
12
+
13
+ // HTML void elements that are legitimately self-closing
14
+ const VOID_ELEMENTS = new Set([
15
+ 'area', 'base', 'br', 'col', 'embed', 'hr', 'img', 'input',
16
+ 'link', 'meta', 'param', 'source', 'track', 'wbr',
17
+ ]);
18
+
19
+ /**
20
+ * Convert a PascalCase tag name to kebab-case.
21
+ * e.g. "WccBadge" → "wcc-badge", "WccCardHeader" → "wcc-card-header"
22
+ *
23
+ * Only converts if the name starts with an uppercase letter (PascalCase).
24
+ *
25
+ * @param {string} name
26
+ * @returns {string}
27
+ */
28
+ export function pascalToKebab(name) {
29
+ // Insert hyphen before each uppercase letter that follows a lowercase letter or digit
30
+ return name
31
+ .replace(/([a-z0-9])([A-Z])/g, '$1-$2')
32
+ .toLowerCase();
33
+ }
34
+
35
+ /**
36
+ * Check if a tag name is PascalCase (starts with uppercase, has at least
37
+ * one more uppercase letter indicating a word boundary).
38
+ *
39
+ * @param {string} name
40
+ * @returns {boolean}
41
+ */
42
+ export function isPascalCase(name) {
43
+ return /^[A-Z][a-zA-Z0-9]*[A-Z][a-zA-Z0-9]*$/.test(name);
44
+ }
45
+
46
+ /**
47
+ * Normalize a WCC template string:
48
+ * 1. Convert PascalCase tags to kebab-case
49
+ * 2. Expand self-closing custom elements to open+close pairs
50
+ *
51
+ * @param {string} html — Raw template HTML
52
+ * @returns {string} — Normalized HTML ready for DOM parsing
53
+ */
54
+ export function normalizeTemplate(html) {
55
+ // Match opening tags (including self-closing): <TagName ...> or <TagName ... />
56
+ // Also match closing tags: </TagName>
57
+ //
58
+ // Regex breakdown:
59
+ // < — opening angle bracket
60
+ // (\/?)? — optional slash (closing tag)
61
+ // ([A-Za-z][\w-]*) — tag name
62
+ // ((?:\s[^>]*)?) — attributes (anything that's not >)
63
+ // (\s*\/)? — optional self-closing slash
64
+ // > — closing angle bracket
65
+ const TAG_RE = /<(\/?)([A-Za-z][\w-]*)((?:\s[^>]*?)?)(\/?)>/g;
66
+
67
+ return html.replace(TAG_RE, (match, closingSlash, tagName, attrs, selfClosing) => {
68
+ let normalizedTag = tagName;
69
+
70
+ // Step 1: Convert PascalCase to kebab-case
71
+ if (isPascalCase(tagName)) {
72
+ normalizedTag = pascalToKebab(tagName);
73
+ }
74
+
75
+ // Step 2: Handle self-closing tags
76
+ if (selfClosing === '/') {
77
+ // If it's a void element, keep it self-closing
78
+ if (VOID_ELEMENTS.has(normalizedTag.toLowerCase())) {
79
+ return `<${closingSlash}${normalizedTag}${attrs} />`;
80
+ }
81
+ // Otherwise expand to open+close pair (trim trailing whitespace from attrs)
82
+ const trimmedAttrs = attrs.trimEnd();
83
+ return `<${normalizedTag}${trimmedAttrs}></${normalizedTag}>`;
84
+ }
85
+
86
+ // Regular open or close tag — just replace the name
87
+ return `<${closingSlash}${normalizedTag}${attrs}>`;
88
+ });
89
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sprlab/wccompiler",
3
- "version": "0.11.9",
3
+ "version": "0.11.12",
4
4
  "description": "Zero-runtime compiler that transforms .wcc single-file components into native web components with signals-based reactivity",
5
5
  "type": "module",
6
6
  "exports": {