@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 +37 -0
- package/bin/wcc.js +87 -3
- package/lib/compiler.js +3 -1
- package/lib/template-normalizer.js +89 -0
- package/package.json +1 -1
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 =
|
|
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(
|
|
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
|
|
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
|
|
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