@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/README.md +998 -998
- package/adapters/angular-compiled/angular.d.ts +197 -197
- package/adapters/angular-compiled/angular.mjs +488 -488
- package/adapters/angular.js +54 -54
- package/adapters/angular.ts +630 -630
- package/adapters/react.js +114 -114
- package/adapters/vue.js +103 -103
- package/bin/wcc.js +412 -412
- package/bin/wcc.test.js +126 -126
- package/integrations/angular.js +73 -73
- package/integrations/react.js +859 -859
- package/integrations/vue.js +253 -253
- package/lib/codegen.js +2074 -2074
- package/lib/compiler-browser.js +545 -545
- package/lib/compiler.js +483 -479
- package/lib/config.js +71 -71
- package/lib/css-scoper.js +180 -180
- package/lib/dev-server.js +193 -193
- package/lib/import-resolver.js +160 -160
- package/lib/parser-extractors.js +1240 -1169
- package/lib/parser.js +273 -269
- package/lib/reactive-runtime.js +143 -143
- package/lib/sfc-parser.js +333 -333
- package/lib/template-normalizer.js +114 -114
- package/lib/tree-walker.js +1013 -1013
- package/lib/types.js +262 -262
- package/lib/wcc-runtime.js +68 -68
- package/package.json +85 -85
- package/types/wcc.d.ts +28 -28
- package/types/wcc.test.js +46 -46
package/README.md
CHANGED
|
@@ -1,998 +1,998 @@
|
|
|
1
|
-
# wcCompiler
|
|
2
|
-
|
|
3
|
-
Zero-runtime compiler that transforms `.wcc` single-file components into native web components. No framework, no virtual DOM, no runtime — just vanilla JavaScript custom elements with signals-based reactivity.
|
|
4
|
-
|
|
5
|
-
## Install
|
|
6
|
-
|
|
7
|
-
```bash
|
|
8
|
-
npm install -D @sprlab/wccompiler
|
|
9
|
-
```
|
|
10
|
-
|
|
11
|
-
## Quick Start
|
|
12
|
-
|
|
13
|
-
**1. Create a component**
|
|
14
|
-
|
|
15
|
-
```html
|
|
16
|
-
<!-- src/wcc-counter.wcc -->
|
|
17
|
-
<script>
|
|
18
|
-
import { defineComponent, signal } from 'wcc'
|
|
19
|
-
|
|
20
|
-
export default defineComponent({
|
|
21
|
-
tag: 'wcc-counter',
|
|
22
|
-
})
|
|
23
|
-
|
|
24
|
-
const count = signal(0)
|
|
25
|
-
|
|
26
|
-
function increment() {
|
|
27
|
-
count.set(count() + 1)
|
|
28
|
-
}
|
|
29
|
-
</script>
|
|
30
|
-
|
|
31
|
-
<template>
|
|
32
|
-
<div class="counter">
|
|
33
|
-
<span>{{count()}}</span>
|
|
34
|
-
<button @click="increment">+</button>
|
|
35
|
-
</div>
|
|
36
|
-
</template>
|
|
37
|
-
|
|
38
|
-
<style>
|
|
39
|
-
.counter { display: flex; gap: 8px; align-items: center; }
|
|
40
|
-
</style>
|
|
41
|
-
```
|
|
42
|
-
|
|
43
|
-
**2. Build**
|
|
44
|
-
|
|
45
|
-
```bash
|
|
46
|
-
npx wcc build
|
|
47
|
-
```
|
|
48
|
-
|
|
49
|
-
**3. Use**
|
|
50
|
-
|
|
51
|
-
```html
|
|
52
|
-
<script type="module" src="dist/wcc-counter.js"></script>
|
|
53
|
-
<wcc-counter></wcc-counter>
|
|
54
|
-
```
|
|
55
|
-
|
|
56
|
-
The compiled output is a single `.js` file with zero dependencies — works in any browser that supports custom elements.
|
|
57
|
-
|
|
58
|
-
## How It Works
|
|
59
|
-
|
|
60
|
-
```
|
|
61
|
-
src/wcc-counter.wcc dist/wcc-counter.js
|
|
62
|
-
┌──────────────────┐ ┌──────────────────────────┐
|
|
63
|
-
│ <script> │ │ // Reactive runtime │
|
|
64
|
-
│ signal, effect │ ───► │ // (inline or imported) │
|
|
65
|
-
│ <template> │ build │ class WccCounter extends │
|
|
66
|
-
│ {{count()}} │ │ HTMLElement { ... } │
|
|
67
|
-
│ <style> │ │ customElements.define(...) │
|
|
68
|
-
└──────────────────┘ └──────────────────────────┘
|
|
69
|
-
+
|
|
70
|
-
dist/__wcc-signals.js (shared mode)
|
|
71
|
-
```
|
|
72
|
-
|
|
73
|
-
The compiler reads your `.wcc` source, extracts script/template/style blocks, analyzes reactive declarations, walks the template DOM for bindings and directives, and generates a self-contained custom element class. CSS is automatically scoped by tag name.
|
|
74
|
-
|
|
75
|
-
## Single File Component (.wcc)
|
|
76
|
-
|
|
77
|
-
wcCompiler uses a single-file component format with the `.wcc` extension. Each file contains three blocks:
|
|
78
|
-
|
|
79
|
-
- `<script>` — Component logic (signals, props, events, lifecycle)
|
|
80
|
-
- `<template>` — HTML template with directives
|
|
81
|
-
- `<style>` — Scoped CSS
|
|
82
|
-
|
|
83
|
-
```html
|
|
84
|
-
<script>
|
|
85
|
-
import { defineComponent, signal } from 'wcc'
|
|
86
|
-
|
|
87
|
-
export default defineComponent({
|
|
88
|
-
tag: 'wcc-my-component',
|
|
89
|
-
})
|
|
90
|
-
|
|
91
|
-
const message = signal('Hello')
|
|
92
|
-
</script>
|
|
93
|
-
|
|
94
|
-
<template>
|
|
95
|
-
<p>{{message()}}</p>
|
|
96
|
-
</template>
|
|
97
|
-
|
|
98
|
-
<style>
|
|
99
|
-
p { color: steelblue; }
|
|
100
|
-
</style>
|
|
101
|
-
```
|
|
102
|
-
|
|
103
|
-
Use `<script lang="ts">` for TypeScript support. The CLI discovers and compiles all `.wcc` files in your source directory.
|
|
104
|
-
|
|
105
|
-
## Coming from Vue?
|
|
106
|
-
|
|
107
|
-
If you're familiar with Vue, here's how wcCompiler maps:
|
|
108
|
-
|
|
109
|
-
| Vue | wcCompiler |
|
|
110
|
-
|-----|------------|
|
|
111
|
-
| `ref(0)` | `signal(0)` |
|
|
112
|
-
| `computed(() => ...)` | `computed(() => ...)` |
|
|
113
|
-
| `watch(source, cb)` | `watch(source, cb)` |
|
|
114
|
-
| `v-if` | `if` |
|
|
115
|
-
| `v-else-if` | `else-if` |
|
|
116
|
-
| `v-else` | `else` |
|
|
117
|
-
| `v-for="item in items"` | `each="item in items()"` |
|
|
118
|
-
| `v-show` | `show` |
|
|
119
|
-
| `v-model` | `model` |
|
|
120
|
-
| `@click` | `@click` |
|
|
121
|
-
| `:prop` | `:prop` |
|
|
122
|
-
| `defineProps()` | `defineProps()` |
|
|
123
|
-
| `defineEmits()` | `defineEmits()` |
|
|
124
|
-
| `onMounted()` | `onMount()` |
|
|
125
|
-
| `onUnmounted()` | `onDestroy()` |
|
|
126
|
-
| `<slot>` | `<slot>` |
|
|
127
|
-
|
|
128
|
-
Key differences: signals use `.set()` to write and `()` to read. Template directives have no `v-` prefix. Output is vanilla JS with no runtime framework.
|
|
129
|
-
|
|
130
|
-
## Reactivity
|
|
131
|
-
|
|
132
|
-
### Signals
|
|
133
|
-
|
|
134
|
-
```js
|
|
135
|
-
const count = signal(0) // create
|
|
136
|
-
count() // read → 0
|
|
137
|
-
count.set(5) // write → 5
|
|
138
|
-
```
|
|
139
|
-
|
|
140
|
-
> **Note:** `.set()` is the public API for writing signals. The compiled output uses direct invocation (`count(5)`) as an internal optimization — both forms are equivalent, but `.set()` is the recommended way to write signals in your source code.
|
|
141
|
-
|
|
142
|
-
### Computed
|
|
143
|
-
|
|
144
|
-
```js
|
|
145
|
-
const doubled = computed(() => count() * 2)
|
|
146
|
-
doubled() // auto-updates when count changes
|
|
147
|
-
```
|
|
148
|
-
|
|
149
|
-
### Effects
|
|
150
|
-
|
|
151
|
-
```js
|
|
152
|
-
effect(() => {
|
|
153
|
-
console.log('Count is:', count()) // re-runs on change
|
|
154
|
-
})
|
|
155
|
-
```
|
|
156
|
-
|
|
157
|
-
Effects support cleanup — return a function to run before re-execution:
|
|
158
|
-
|
|
159
|
-
```js
|
|
160
|
-
effect(() => {
|
|
161
|
-
const id = setInterval(() => tick.set(tick() + 1), 1000)
|
|
162
|
-
return () => clearInterval(id) // called before re-run
|
|
163
|
-
})
|
|
164
|
-
```
|
|
165
|
-
|
|
166
|
-
### Batch
|
|
167
|
-
|
|
168
|
-
Group multiple signal writes into a single update pass:
|
|
169
|
-
|
|
170
|
-
```js
|
|
171
|
-
import { batch } from 'wcc'
|
|
172
|
-
|
|
173
|
-
batch(() => {
|
|
174
|
-
firstName.set('John')
|
|
175
|
-
lastName.set('Doe')
|
|
176
|
-
age.set(30)
|
|
177
|
-
})
|
|
178
|
-
// Effects run once after all three writes, not three times
|
|
179
|
-
```
|
|
180
|
-
|
|
181
|
-
Nested batches are supported — effects flush only when the outermost batch completes.
|
|
182
|
-
|
|
183
|
-
### Watch
|
|
184
|
-
|
|
185
|
-
```js
|
|
186
|
-
// Watch a signal directly
|
|
187
|
-
watch(count, (newVal, oldVal) => {
|
|
188
|
-
console.log(`Changed from ${oldVal} to ${newVal}`)
|
|
189
|
-
})
|
|
190
|
-
|
|
191
|
-
// Watch a getter function (useful for props or derived values)
|
|
192
|
-
watch(() => props.count, (newVal, oldVal) => {
|
|
193
|
-
console.log(`Prop changed: ${oldVal} → ${newVal}`)
|
|
194
|
-
})
|
|
195
|
-
|
|
196
|
-
// Watch a derived expression
|
|
197
|
-
watch(() => count() * 2, (newVal, oldVal) => {
|
|
198
|
-
console.log(`Doubled changed: ${oldVal} → ${newVal}`)
|
|
199
|
-
})
|
|
200
|
-
```
|
|
201
|
-
|
|
202
|
-
`watch` observes a specific signal or getter and provides both old and new values. The callback does not run on initial mount — only on subsequent changes.
|
|
203
|
-
|
|
204
|
-
### Constants
|
|
205
|
-
|
|
206
|
-
```js
|
|
207
|
-
const TAX_RATE = 0.21 // non-reactive, no signal() wrapper
|
|
208
|
-
```
|
|
209
|
-
|
|
210
|
-
## Props
|
|
211
|
-
|
|
212
|
-
```js
|
|
213
|
-
const props = defineProps({ label: 'Click', count: 0 })
|
|
214
|
-
```
|
|
215
|
-
|
|
216
|
-
```html
|
|
217
|
-
<wcc-counter label="Clicks:" count="5"></wcc-counter>
|
|
218
|
-
```
|
|
219
|
-
|
|
220
|
-
You can also call `defineProps` without assignment — the props are available by name in the template:
|
|
221
|
-
|
|
222
|
-
```js
|
|
223
|
-
defineProps({ label: 'Click' })
|
|
224
|
-
```
|
|
225
|
-
|
|
226
|
-
```html
|
|
227
|
-
<span>{{label}}</span>
|
|
228
|
-
```
|
|
229
|
-
|
|
230
|
-
TypeScript generics:
|
|
231
|
-
|
|
232
|
-
```ts
|
|
233
|
-
const props = defineProps<{ label: string, count: number }>({ label: 'Click', count: 0 })
|
|
234
|
-
```
|
|
235
|
-
|
|
236
|
-
Props are reactive — they update when attributes change. Supports boolean and number coercion.
|
|
237
|
-
|
|
238
|
-
## Custom Events
|
|
239
|
-
|
|
240
|
-
```js
|
|
241
|
-
const emit = defineEmits(['change', 'reset'])
|
|
242
|
-
|
|
243
|
-
function handleClick() {
|
|
244
|
-
emit('change', count())
|
|
245
|
-
}
|
|
246
|
-
```
|
|
247
|
-
|
|
248
|
-
TypeScript call signatures:
|
|
249
|
-
|
|
250
|
-
```ts
|
|
251
|
-
const emit = defineEmits<{ (e: 'change', value: number): void }>()
|
|
252
|
-
```
|
|
253
|
-
|
|
254
|
-
The compiler validates emit calls against declared events at compile time.
|
|
255
|
-
|
|
256
|
-
## defineModel (Two-Way Binding)
|
|
257
|
-
|
|
258
|
-
`defineModel` declares a prop that supports two-way binding across frameworks:
|
|
259
|
-
|
|
260
|
-
```js
|
|
261
|
-
import { defineModel } from 'wcc'
|
|
262
|
-
|
|
263
|
-
const count = defineModel({ name: 'count', default: 0 })
|
|
264
|
-
const title = defineModel({ name: 'title', default: 'untitled' })
|
|
265
|
-
```
|
|
266
|
-
|
|
267
|
-
Read and write like a signal:
|
|
268
|
-
```js
|
|
269
|
-
count() // read current value
|
|
270
|
-
count.set(5) // write — updates internal state + emits events
|
|
271
|
-
```
|
|
272
|
-
|
|
273
|
-
**Events emitted on write:**
|
|
274
|
-
|
|
275
|
-
| Event | Purpose |
|
|
276
|
-
|-------|---------|
|
|
277
|
-
| `count-changed` | Kebab-case — for Vue plugin, addEventListener |
|
|
278
|
-
| `countChanged` | camelCase — for Angular, direct binding |
|
|
279
|
-
| `countChange` | Angular `[(count)]` banana-box syntax |
|
|
280
|
-
| `wcc:model` | Generic — for vanilla JS and WCC-to-WCC |
|
|
281
|
-
|
|
282
|
-
**Usage per framework:**
|
|
283
|
-
|
|
284
|
-
```vue
|
|
285
|
-
<!-- Vue (with plugin) -->
|
|
286
|
-
<wcc-counter v-model:count="ref"></wcc-counter>
|
|
287
|
-
|
|
288
|
-
<!-- Vue (without plugin) -->
|
|
289
|
-
<wcc-counter :count="ref" @count-changed="ref = $event.detail"></wcc-counter>
|
|
290
|
-
```
|
|
291
|
-
|
|
292
|
-
```jsx
|
|
293
|
-
// React (with wrapper)
|
|
294
|
-
<WccCounter count={count} onCountChange={(value) => setCount(value)} />
|
|
295
|
-
|
|
296
|
-
// React (native CE)
|
|
297
|
-
<wcc-counter count={count} oncountchanged={(e) => setCount(e.detail)} />
|
|
298
|
-
```
|
|
299
|
-
|
|
300
|
-
```html
|
|
301
|
-
<!-- Angular (zero-config two-way) -->
|
|
302
|
-
<wcc-counter [(count)]="signal"></wcc-counter>
|
|
303
|
-
|
|
304
|
-
<!-- Angular (manual) -->
|
|
305
|
-
<wcc-counter [count]="signal()" (countChange)="signal.set($event.detail)"></wcc-counter>
|
|
306
|
-
```
|
|
307
|
-
|
|
308
|
-
## Template Directives
|
|
309
|
-
|
|
310
|
-
### Text Interpolation
|
|
311
|
-
|
|
312
|
-
Signals and computeds require `()` to read their value in templates:
|
|
313
|
-
|
|
314
|
-
```html
|
|
315
|
-
<span>{{count()}}</span>
|
|
316
|
-
<p>You have {{items().length}} items.</p>
|
|
317
|
-
<span>{{doubled()}}</span>
|
|
318
|
-
```
|
|
319
|
-
|
|
320
|
-
Props accessed without assignment use their name directly (no parentheses):
|
|
321
|
-
|
|
322
|
-
```html
|
|
323
|
-
<span>{{label}}</span>
|
|
324
|
-
<p>Hello, {{name}}!</p>
|
|
325
|
-
```
|
|
326
|
-
|
|
327
|
-
### Event Binding
|
|
328
|
-
|
|
329
|
-
```html
|
|
330
|
-
<button @click="increment">+</button>
|
|
331
|
-
<input @input="handleInput">
|
|
332
|
-
```
|
|
333
|
-
|
|
334
|
-
Event handlers support expressions and inline arguments:
|
|
335
|
-
|
|
336
|
-
```html
|
|
337
|
-
<button @click="removeItem(item)">×</button>
|
|
338
|
-
<button @click="() => doSomething()">Do it</button>
|
|
339
|
-
```
|
|
340
|
-
|
|
341
|
-
### Conditional Rendering
|
|
342
|
-
|
|
343
|
-
```html
|
|
344
|
-
<div if="status() === 'active'">Active</div>
|
|
345
|
-
<div else-if="status() === 'pending'">Pending</div>
|
|
346
|
-
<div else>Inactive</div>
|
|
347
|
-
```
|
|
348
|
-
|
|
349
|
-
### List Rendering
|
|
350
|
-
|
|
351
|
-
```html
|
|
352
|
-
<li each="item in items()">{{item.name}}</li>
|
|
353
|
-
<li each="(item, index) in items()">{{index}}: {{item.name}}</li>
|
|
354
|
-
```
|
|
355
|
-
|
|
356
|
-
The source expression calls the signal (`items()`) to read the current array. Supports keyed rendering with `:key`:
|
|
357
|
-
|
|
358
|
-
```html
|
|
359
|
-
<li each="item in items()" :key="item.id">{{item.name}}</li>
|
|
360
|
-
```
|
|
361
|
-
|
|
362
|
-
Numeric ranges are also supported:
|
|
363
|
-
|
|
364
|
-
```html
|
|
365
|
-
<li each="n in 5">Item {{n}}</li>
|
|
366
|
-
```
|
|
367
|
-
|
|
368
|
-
#### Nested Directives in `each`
|
|
369
|
-
|
|
370
|
-
Directives work inside `each` blocks — including conditionals and nested loops:
|
|
371
|
-
|
|
372
|
-
```html
|
|
373
|
-
<div each="user in users()">
|
|
374
|
-
<span>{{user.name}}</span>
|
|
375
|
-
<span if="user.active" class="badge">Active</span>
|
|
376
|
-
<span else class="badge muted">Inactive</span>
|
|
377
|
-
<ul>
|
|
378
|
-
<li each="role in user.roles">{{role}}</li>
|
|
379
|
-
</ul>
|
|
380
|
-
</div>
|
|
381
|
-
```
|
|
382
|
-
|
|
383
|
-
### Visibility Toggle
|
|
384
|
-
|
|
385
|
-
```html
|
|
386
|
-
<div show="isVisible()">Shown or hidden via CSS display</div>
|
|
387
|
-
```
|
|
388
|
-
|
|
389
|
-
### Two-Way Binding
|
|
390
|
-
|
|
391
|
-
```html
|
|
392
|
-
<input type="text" model="name">
|
|
393
|
-
<input type="number" model="age">
|
|
394
|
-
<input type="checkbox" model="agree">
|
|
395
|
-
<input type="radio" name="color" value="red" model="color">
|
|
396
|
-
<select model="country">...</select>
|
|
397
|
-
<textarea model="bio"></textarea>
|
|
398
|
-
```
|
|
399
|
-
|
|
400
|
-
### Attribute Binding
|
|
401
|
-
|
|
402
|
-
```html
|
|
403
|
-
<a :href="url()">Link</a>
|
|
404
|
-
<button :disabled="isLoading()">Submit</button>
|
|
405
|
-
<div :class="{ active: isActive(), error: hasError() }">...</div>
|
|
406
|
-
<div :style="{ color: textColor() }">...</div>
|
|
407
|
-
```
|
|
408
|
-
|
|
409
|
-
### Template Refs
|
|
410
|
-
|
|
411
|
-
```js
|
|
412
|
-
const canvas = templateRef('myCanvas')
|
|
413
|
-
|
|
414
|
-
onMount(() => {
|
|
415
|
-
const ctx = canvas.value.getContext('2d')
|
|
416
|
-
})
|
|
417
|
-
```
|
|
418
|
-
|
|
419
|
-
```html
|
|
420
|
-
<canvas ref="myCanvas"></canvas>
|
|
421
|
-
```
|
|
422
|
-
|
|
423
|
-
## Slots
|
|
424
|
-
|
|
425
|
-
### Named Slots
|
|
426
|
-
|
|
427
|
-
Component template:
|
|
428
|
-
```html
|
|
429
|
-
<div class="card">
|
|
430
|
-
<slot name="header">Default Header</slot>
|
|
431
|
-
<slot>Default Body</slot>
|
|
432
|
-
<slot name="footer">Default Footer</slot>
|
|
433
|
-
</div>
|
|
434
|
-
```
|
|
435
|
-
|
|
436
|
-
Consumer:
|
|
437
|
-
```html
|
|
438
|
-
<wcc-card>
|
|
439
|
-
<template #header><strong>Custom Header</strong></template>
|
|
440
|
-
<p>Custom body content</p>
|
|
441
|
-
<template #footer>Custom footer</template>
|
|
442
|
-
</wcc-card>
|
|
443
|
-
```
|
|
444
|
-
|
|
445
|
-
### Scoped Slots
|
|
446
|
-
|
|
447
|
-
Component template (passes reactive data to consumer):
|
|
448
|
-
```html
|
|
449
|
-
<slot name="stats" :likes="likes">Likes: {{likes}}</slot>
|
|
450
|
-
```
|
|
451
|
-
|
|
452
|
-
Consumer (receives data via template props):
|
|
453
|
-
```html
|
|
454
|
-
<wcc-card>
|
|
455
|
-
<template #stats="{ likes }">🔥 {{likes}} likes!</template>
|
|
456
|
-
</wcc-card>
|
|
457
|
-
```
|
|
458
|
-
|
|
459
|
-
## Nested Components
|
|
460
|
-
|
|
461
|
-
Components can import and use other components in their templates using PascalCase tags:
|
|
462
|
-
|
|
463
|
-
```html
|
|
464
|
-
<!-- src/nested/wcc-profile.wcc -->
|
|
465
|
-
<script>
|
|
466
|
-
import { defineComponent, signal } from 'wcc'
|
|
467
|
-
import WccBadge from './wcc-badge.wcc'
|
|
468
|
-
|
|
469
|
-
export default defineComponent({ tag: 'wcc-profile' })
|
|
470
|
-
|
|
471
|
-
const count = signal(0)
|
|
472
|
-
|
|
473
|
-
function increment() {
|
|
474
|
-
count.set(count() + 1)
|
|
475
|
-
}
|
|
476
|
-
</script>
|
|
477
|
-
|
|
478
|
-
<template>
|
|
479
|
-
<div class="profile">
|
|
480
|
-
<WccBadge :count="count()" @click="increment"></WccBadge>
|
|
481
|
-
</div>
|
|
482
|
-
</template>
|
|
483
|
-
```
|
|
484
|
-
|
|
485
|
-
- **Named import**: `import WccBadge from './wcc-badge.wcc'` — the PascalCase identifier becomes the tag alias in the template
|
|
486
|
-
- **Side-effect import**: `import './wcc-child.wcc'` — registers the child without using it in the template (for programmatic creation)
|
|
487
|
-
- **Reactive props**: Use `:prop="expr"` to pass reactive data down — updates automatically when the expression changes
|
|
488
|
-
- **Event listening**: Use `@event="handler"` to listen to custom events emitted by the child
|
|
489
|
-
- **Compile-time validation**: Using a PascalCase tag without a matching import throws an error at build time
|
|
490
|
-
- **Hyphenated tags**: Tags like `<my-element>` without a corresponding import are treated as plain custom elements (no import generated)
|
|
491
|
-
|
|
492
|
-
## Lifecycle Hooks
|
|
493
|
-
|
|
494
|
-
```js
|
|
495
|
-
onMount(() => {
|
|
496
|
-
console.log('Component connected to DOM')
|
|
497
|
-
})
|
|
498
|
-
|
|
499
|
-
onMount(async () => {
|
|
500
|
-
const data = await fetch('/api/items').then(r => r.json())
|
|
501
|
-
items.set(data)
|
|
502
|
-
})
|
|
503
|
-
|
|
504
|
-
onDestroy(() => {
|
|
505
|
-
console.log('Component removed from DOM')
|
|
506
|
-
})
|
|
507
|
-
```
|
|
508
|
-
|
|
509
|
-
Async callbacks are wrapped in an IIFE — `connectedCallback` itself stays synchronous.
|
|
510
|
-
|
|
511
|
-
**Details:**
|
|
512
|
-
- Multiple `onMount` / `onDestroy` calls are supported — they all run in declaration order
|
|
513
|
-
- `connectedCallback` is idempotent — re-mounting a component (e.g., moving it in the DOM) re-attaches listeners and effects cleanly
|
|
514
|
-
- All effects and event listeners are automatically cleaned up in `disconnectedCallback` via AbortController
|
|
515
|
-
|
|
516
|
-
## CSS Scoping
|
|
517
|
-
|
|
518
|
-
Styles are automatically scoped to the component using tag-name prefixing:
|
|
519
|
-
|
|
520
|
-
```css
|
|
521
|
-
/* Input */
|
|
522
|
-
.counter { display: flex; }
|
|
523
|
-
|
|
524
|
-
/* Output */
|
|
525
|
-
wcc-counter .counter { display: flex; }
|
|
526
|
-
```
|
|
527
|
-
|
|
528
|
-
`@media` rules are recursively scoped. `@keyframes` are preserved without prefixing.
|
|
529
|
-
|
|
530
|
-
## TypeScript
|
|
531
|
-
|
|
532
|
-
Use `<script lang="ts">` in your `.wcc` file for full type support:
|
|
533
|
-
|
|
534
|
-
```html
|
|
535
|
-
<script lang="ts">
|
|
536
|
-
import { defineComponent, defineProps, defineEmits, signal, computed, watch, defineExpose } from 'wcc'
|
|
537
|
-
|
|
538
|
-
export default defineComponent({
|
|
539
|
-
tag: 'wcc-typescript',
|
|
540
|
-
})
|
|
541
|
-
|
|
542
|
-
const props = defineProps<{ title: string, count: number }>({ title: 'Demo', count: 0 })
|
|
543
|
-
const emit = defineEmits<{ (e: 'update', value: number): void }>()
|
|
544
|
-
|
|
545
|
-
const doubled = computed<number>(() => props.count * 2)
|
|
546
|
-
const watchLog = signal<string>('(no changes yet)')
|
|
547
|
-
|
|
548
|
-
watch(() => props.count, (newVal, oldVal) => {
|
|
549
|
-
watchLog.set(`count changed: ${oldVal} → ${newVal}`)
|
|
550
|
-
})
|
|
551
|
-
|
|
552
|
-
function handleUpdate(): void {
|
|
553
|
-
emit('update', doubled())
|
|
554
|
-
}
|
|
555
|
-
|
|
556
|
-
defineExpose({ doubled, handleUpdate, watchLog })
|
|
557
|
-
</script>
|
|
558
|
-
|
|
559
|
-
<template>
|
|
560
|
-
<div class="demo">
|
|
561
|
-
<span>{{title}}: {{count}}</span>
|
|
562
|
-
<span>Doubled: {{doubled()}}</span>
|
|
563
|
-
<span>Watch: {{watchLog()}}</span>
|
|
564
|
-
<button @click="handleUpdate">Update</button>
|
|
565
|
-
</div>
|
|
566
|
-
</template>
|
|
567
|
-
|
|
568
|
-
<style>
|
|
569
|
-
.demo { font-family: sans-serif; }
|
|
570
|
-
</style>
|
|
571
|
-
```
|
|
572
|
-
|
|
573
|
-
`defineExpose()` exposes methods and properties for external access via ref.
|
|
574
|
-
|
|
575
|
-
```js
|
|
576
|
-
// wcc-timer.wcc — exposes start/stop/elapsed
|
|
577
|
-
const elapsed = signal(0)
|
|
578
|
-
let interval = null
|
|
579
|
-
|
|
580
|
-
function start() { interval = setInterval(() => elapsed.set(elapsed() + 1), 1000) }
|
|
581
|
-
function stop() { clearInterval(interval) }
|
|
582
|
-
|
|
583
|
-
defineExpose({ elapsed, start, stop })
|
|
584
|
-
```
|
|
585
|
-
|
|
586
|
-
```html
|
|
587
|
-
<!-- Parent component accessing exposed API -->
|
|
588
|
-
<script>
|
|
589
|
-
import { defineComponent, templateRef, onMount } from 'wcc'
|
|
590
|
-
import './wcc-timer.wcc'
|
|
591
|
-
|
|
592
|
-
export default defineComponent({ tag: 'wcc-app' })
|
|
593
|
-
|
|
594
|
-
const timer = templateRef('timer')
|
|
595
|
-
|
|
596
|
-
onMount(() => {
|
|
597
|
-
timer.value.start() // call exposed method
|
|
598
|
-
console.log(timer.value.elapsed) // read exposed signal
|
|
599
|
-
})
|
|
600
|
-
</script>
|
|
601
|
-
|
|
602
|
-
<template>
|
|
603
|
-
<wcc-timer ref="timer"></wcc-timer>
|
|
604
|
-
</template>
|
|
605
|
-
```
|
|
606
|
-
|
|
607
|
-
The language server automatically generates a typed interface (PascalCase of the tag name) that can be imported by consumers:
|
|
608
|
-
|
|
609
|
-
```ts
|
|
610
|
-
// In the parent component:
|
|
611
|
-
import type { WccTimer } from './wcc-timer.wcc'
|
|
612
|
-
const timer = templateRef<WccTimer>('timer')
|
|
613
|
-
timer.value!.start() // ✅ typed
|
|
614
|
-
```
|
|
615
|
-
|
|
616
|
-
## CLI
|
|
617
|
-
|
|
618
|
-
```bash
|
|
619
|
-
wcc build # Compile all .wcc files from input/ to output/
|
|
620
|
-
wcc build --bundle # Compile + produce a single bundle.js (works from file://)
|
|
621
|
-
wcc build --minify # Compile with minification
|
|
622
|
-
wcc build --bundle --minify # Production bundle (smallest output)
|
|
623
|
-
wcc dev # Build + watch + live-reload dev server
|
|
624
|
-
```
|
|
625
|
-
|
|
626
|
-
The CLI discovers all `.wcc` files in your source directory and compiles each into a standalone `.js` file.
|
|
627
|
-
|
|
628
|
-
### Bundle Mode
|
|
629
|
-
|
|
630
|
-
The `--bundle` flag produces a single `bundle.js` file that includes all components and their dependencies in one IIFE (Immediately Invoked Function Expression). This file:
|
|
631
|
-
|
|
632
|
-
- Works with `<script src="bundle.js">` (no `type="module"` needed)
|
|
633
|
-
- Works from `file://` protocol (no server required)
|
|
634
|
-
- Includes all child component imports resolved and inlined
|
|
635
|
-
- Includes the reactive runtime
|
|
636
|
-
- Supports `--minify` for production
|
|
637
|
-
|
|
638
|
-
```html
|
|
639
|
-
<!-- Works by double-clicking the HTML file — no server needed -->
|
|
640
|
-
<!DOCTYPE html>
|
|
641
|
-
<html>
|
|
642
|
-
<body>
|
|
643
|
-
<wcc-my-app></wcc-my-app>
|
|
644
|
-
<script src="dist/bundle.js"></script>
|
|
645
|
-
</body>
|
|
646
|
-
</html>
|
|
647
|
-
```
|
|
648
|
-
|
|
649
|
-
**When to use `--bundle`:**
|
|
650
|
-
- Static HTML files opened from disk
|
|
651
|
-
- Electron apps loading local files
|
|
652
|
-
- Offline-first applications
|
|
653
|
-
- Quick prototyping without a dev server
|
|
654
|
-
- Distributing a complete app as HTML + JS
|
|
655
|
-
|
|
656
|
-
**When NOT to use `--bundle`:**
|
|
657
|
-
- Apps served via HTTP (use ES modules for better caching)
|
|
658
|
-
- When you need per-component lazy loading
|
|
659
|
-
- When using a bundler like Vite/Webpack (they handle bundling themselves)
|
|
660
|
-
|
|
661
|
-
### Configuration
|
|
662
|
-
|
|
663
|
-
Create `wcc.config.js` in your project root:
|
|
664
|
-
|
|
665
|
-
```js
|
|
666
|
-
export default {
|
|
667
|
-
port: 4100, // dev server port (default: 4100)
|
|
668
|
-
input: 'src', // source directory (default: 'src')
|
|
669
|
-
output: 'dist', // output directory (default: 'dist')
|
|
670
|
-
standalone: false // inline runtime per component (default: false)
|
|
671
|
-
}
|
|
672
|
-
```
|
|
673
|
-
|
|
674
|
-
All options are optional — defaults shown above.
|
|
675
|
-
|
|
676
|
-
| Option | Type | Default | Description |
|
|
677
|
-
|--------|------|---------|-------------|
|
|
678
|
-
| `port` | number | `4100` | Dev server port for `wcc dev` |
|
|
679
|
-
| `input` | string | `'src'` | Source directory containing `.wcc` files |
|
|
680
|
-
| `output` | string | `'dist'` | Output directory for compiled `.js` files |
|
|
681
|
-
| `standalone` | boolean | `false` | Inline reactive runtime in each component |
|
|
682
|
-
|
|
683
|
-
### Standalone Mode
|
|
684
|
-
|
|
685
|
-
Controls whether the reactive runtime is inlined in each component or imported from a shared module.
|
|
686
|
-
|
|
687
|
-
```js
|
|
688
|
-
// wcc.config.js
|
|
689
|
-
export default {
|
|
690
|
-
standalone: true // inline runtime in every component (default: false)
|
|
691
|
-
}
|
|
692
|
-
```
|
|
693
|
-
|
|
694
|
-
- `standalone: false` (default) — Components import the runtime from a shared `__wcc-signals.js` file. Smaller per-component size when using multiple components.
|
|
695
|
-
- `standalone: true` — Each component includes the full reactive runtime inline. Zero external dependencies per component.
|
|
696
|
-
|
|
697
|
-
**Output difference:**
|
|
698
|
-
|
|
699
|
-
```
|
|
700
|
-
Default (false): component.js → imports __wcc-signals.js
|
|
701
|
-
Standalone (true): component.js → runtime inlined, zero imports
|
|
702
|
-
```
|
|
703
|
-
|
|
704
|
-
**When to use standalone:**
|
|
705
|
-
- Publishing components as npm packages
|
|
706
|
-
- Embedding widgets in third-party sites
|
|
707
|
-
- CDN distribution (`<script src="component.js">`)
|
|
708
|
-
- Micro-frontends where you don't control the host
|
|
709
|
-
|
|
710
|
-
**When NOT to use standalone:**
|
|
711
|
-
- Apps with multiple components (runtime would be duplicated in each)
|
|
712
|
-
- Internal projects where you control the build
|
|
713
|
-
|
|
714
|
-
#### Per-Component Override
|
|
715
|
-
|
|
716
|
-
Override the global setting for individual components:
|
|
717
|
-
|
|
718
|
-
```html
|
|
719
|
-
<script>
|
|
720
|
-
import { defineComponent, signal } from 'wcc'
|
|
721
|
-
|
|
722
|
-
export default defineComponent({
|
|
723
|
-
tag: 'wcc-widget',
|
|
724
|
-
standalone: true, // this component is self-contained regardless of global config
|
|
725
|
-
})
|
|
726
|
-
</script>
|
|
727
|
-
```
|
|
728
|
-
|
|
729
|
-
Component-level `standalone` always takes precedence over the global config. This lets you have a project with shared runtime but mark specific components as fully self-contained for distribution.
|
|
730
|
-
|
|
731
|
-
#### Reactive Scope Isolation
|
|
732
|
-
|
|
733
|
-
Each standalone component has its own isolated reactive runtime. Signals from component A cannot be observed by effects in component B — they are completely independent. This is by design for distribution scenarios where components must be self-contained. If you need cross-component reactivity (e.g., shared state), use the default shared mode (`standalone: false`).
|
|
734
|
-
|
|
735
|
-
## Framework Integrations
|
|
736
|
-
|
|
737
|
-
WCC components are native custom elements — they work in any framework. Props, events, and named slots work natively with zero WCC-specific config. Two-way binding is zero-config in Angular; Vue requires a plugin. Scoped slots require a framework plugin or directive for idiomatic syntax.
|
|
738
|
-
|
|
739
|
-
### Feature Support Matrix
|
|
740
|
-
|
|
741
|
-
| Feature | Vue (plugin) | Angular (directive) | React 19 (plugin) |
|
|
742
|
-
|---------|--------------|--------------------|--------------------|
|
|
743
|
-
| Props | ✅ `:count="ref"` | ✅ `[count]="signal()"` | ✅ `count={state}` |
|
|
744
|
-
| Events | ✅ `@count-changed="handler($event.detail)"` | ✅ `(count-changed)="handler($event.detail)"` | ✅ `oncountchanged={(e) => handler(e.detail)}` |
|
|
745
|
-
| Two-way binding | ✅ `v-model:count="ref"` | ✅ `[(count)]="signal"` | ❌ Not applicable |
|
|
746
|
-
| Default slot | ✅ children | ✅ children | ✅ children |
|
|
747
|
-
| Named slots | ✅ `<template #name>` | ✅ `<div slot-name>` | ✅ `<WccCard.Header>` |
|
|
748
|
-
| Scoped slots | ✅ `<template #name="{ prop }">` | ✅ `<ng-template slot="name" let-prop>` | ✅ `<WccList.Item>{(prop) => jsx}</WccList.Item>` |
|
|
749
|
-
|
|
750
|
-
### Vue (with `wccVuePlugin`)
|
|
751
|
-
|
|
752
|
-
```js
|
|
753
|
-
// vite.config.js
|
|
754
|
-
import { wccVuePlugin } from '@sprlab/wccompiler/integrations/vue'
|
|
755
|
-
export default defineConfig({ plugins: [wccVuePlugin()] })
|
|
756
|
-
```
|
|
757
|
-
|
|
758
|
-
```vue
|
|
759
|
-
<script setup>
|
|
760
|
-
import { ref } from 'vue'
|
|
761
|
-
const count = ref(0)
|
|
762
|
-
const text = ref('')
|
|
763
|
-
</script>
|
|
764
|
-
|
|
765
|
-
<template>
|
|
766
|
-
<!-- Props -->
|
|
767
|
-
<wcc-counter :count="count" label="Clicks"></wcc-counter>
|
|
768
|
-
|
|
769
|
-
<!-- Events -->
|
|
770
|
-
<wcc-counter @count-changed="count = $event.detail"></wcc-counter>
|
|
771
|
-
|
|
772
|
-
<!-- Two-way binding (v-model) -->
|
|
773
|
-
<wcc-counter v-model:count="count"></wcc-counter>
|
|
774
|
-
<wcc-input v-model.trim="text"></wcc-input>
|
|
775
|
-
|
|
776
|
-
<!-- Default slot -->
|
|
777
|
-
<wcc-card>
|
|
778
|
-
<p>Body content</p>
|
|
779
|
-
</wcc-card>
|
|
780
|
-
|
|
781
|
-
<!-- Named slots -->
|
|
782
|
-
<wcc-card>
|
|
783
|
-
<template #header><strong>Title</strong></template>
|
|
784
|
-
<p>Body</p>
|
|
785
|
-
<template #footer>Footer text</template>
|
|
786
|
-
</wcc-card>
|
|
787
|
-
|
|
788
|
-
<!-- Scoped slots -->
|
|
789
|
-
<wcc-list>
|
|
790
|
-
<template #item="{ item, index }">
|
|
791
|
-
<li>{{ index }}: {{ item }}</li>
|
|
792
|
-
</template>
|
|
793
|
-
</wcc-list>
|
|
794
|
-
</template>
|
|
795
|
-
```
|
|
796
|
-
|
|
797
|
-
The plugin provides: `isCustomElement` config, `v-model:prop` support, v-model modifiers (`.trim`, `.number`), and scoped slot syntax (`{{prop}}` → `{%prop%}` escape).
|
|
798
|
-
|
|
799
|
-
### Angular (with `WccSlotsDirective`)
|
|
800
|
-
|
|
801
|
-
```ts
|
|
802
|
-
import { Component, CUSTOM_ELEMENTS_SCHEMA } from '@angular/core'
|
|
803
|
-
import { WccSlotsDirective, WccSlotDef } from '@sprlab/wccompiler/adapters/angular'
|
|
804
|
-
|
|
805
|
-
@Component({
|
|
806
|
-
imports: [WccSlotsDirective, WccSlotDef],
|
|
807
|
-
schemas: [CUSTOM_ELEMENTS_SCHEMA],
|
|
808
|
-
template: `
|
|
809
|
-
<!-- Props -->
|
|
810
|
-
<wcc-counter [count]="count" label="Clicks"></wcc-counter>
|
|
811
|
-
|
|
812
|
-
<!-- Events -->
|
|
813
|
-
<wcc-counter (count-changed)="onCount($event.detail)"></wcc-counter>
|
|
814
|
-
|
|
815
|
-
<!-- Two-way binding (banana-box) -->
|
|
816
|
-
<wcc-counter [(count)]="count"></wcc-counter>
|
|
817
|
-
|
|
818
|
-
<!-- Default slot -->
|
|
819
|
-
<wcc-card>
|
|
820
|
-
<p>Body content</p>
|
|
821
|
-
</wcc-card>
|
|
822
|
-
|
|
823
|
-
<!-- Named slots -->
|
|
824
|
-
<wcc-card wccSlots>
|
|
825
|
-
<strong slot-header>Title</strong>
|
|
826
|
-
<p>Body</p>
|
|
827
|
-
<span slot-footer>Footer text</span>
|
|
828
|
-
</wcc-card>
|
|
829
|
-
|
|
830
|
-
<!-- Scoped slots -->
|
|
831
|
-
<wcc-list wccSlots>
|
|
832
|
-
<ng-template slot="item" let-item let-index="index">
|
|
833
|
-
<li>{{ index }}: {{ item }}</li>
|
|
834
|
-
</ng-template>
|
|
835
|
-
</wcc-list>
|
|
836
|
-
`
|
|
837
|
-
})
|
|
838
|
-
export class AppComponent {
|
|
839
|
-
count = 0
|
|
840
|
-
onCount(value: number) { this.count = value }
|
|
841
|
-
}
|
|
842
|
-
```
|
|
843
|
-
|
|
844
|
-
Angular needs no plugin for props, events, or two-way binding — only `CUSTOM_ELEMENTS_SCHEMA`. The directive is only needed for named slots (with `slot-name` syntax) and scoped slots.
|
|
845
|
-
|
|
846
|
-
### React 19 (with `wccReactPlugin`)
|
|
847
|
-
|
|
848
|
-
```js
|
|
849
|
-
// vite.config.js
|
|
850
|
-
import { wccReactPlugin } from '@sprlab/wccompiler/integrations/react'
|
|
851
|
-
import react from '@vitejs/plugin-react'
|
|
852
|
-
export default defineConfig({ plugins: [wccReactPlugin({ prefix: 'wcc-' }), react()] })
|
|
853
|
-
```
|
|
854
|
-
|
|
855
|
-
```jsx
|
|
856
|
-
import { useState } from 'react'
|
|
857
|
-
import { WccCard, WccList } from './dist/wcc-react'
|
|
858
|
-
|
|
859
|
-
export default function App() {
|
|
860
|
-
const [count, setCount] = useState(0)
|
|
861
|
-
|
|
862
|
-
return (
|
|
863
|
-
<>
|
|
864
|
-
{/* Props */}
|
|
865
|
-
<wcc-counter count={count} label="Clicks"></wcc-counter>
|
|
866
|
-
|
|
867
|
-
{/* Events */}
|
|
868
|
-
<wcc-counter oncountchanged={(e) => setCount(e.detail)}></wcc-counter>
|
|
869
|
-
|
|
870
|
-
{/* Default slot */}
|
|
871
|
-
<WccCard>
|
|
872
|
-
<p>Body content</p>
|
|
873
|
-
</WccCard>
|
|
874
|
-
|
|
875
|
-
{/* Named slots (compound pattern) */}
|
|
876
|
-
<WccCard>
|
|
877
|
-
<WccCard.Header><strong>Title</strong></WccCard.Header>
|
|
878
|
-
<p>Body</p>
|
|
879
|
-
<WccCard.Footer>Footer text</WccCard.Footer>
|
|
880
|
-
</WccCard>
|
|
881
|
-
|
|
882
|
-
{/* Named slots (props pattern) */}
|
|
883
|
-
<wcc-card header={<strong>Title</strong>} footer="Footer text">
|
|
884
|
-
<p>Body</p>
|
|
885
|
-
</wcc-card>
|
|
886
|
-
|
|
887
|
-
{/* Scoped slots (compound pattern) */}
|
|
888
|
-
<WccList>
|
|
889
|
-
<WccList.Item>{(item, index) => <li>{index}: {item}</li>}</WccList.Item>
|
|
890
|
-
</WccList>
|
|
891
|
-
|
|
892
|
-
{/* Scoped slots (render prop pattern) */}
|
|
893
|
-
<wcc-list renderItem={(item, index) => <li>{index}: {item}</li>} />
|
|
894
|
-
</>
|
|
895
|
-
)
|
|
896
|
-
}
|
|
897
|
-
```
|
|
898
|
-
|
|
899
|
-
The plugin transforms PascalCase tags, compound components, props-as-slots, and render props at build time. Import stubs from `./dist/wcc-react` (auto-generated by `wcc build`).
|
|
900
|
-
|
|
901
|
-
### Vanilla (no framework)
|
|
902
|
-
|
|
903
|
-
No configuration needed:
|
|
904
|
-
|
|
905
|
-
```html
|
|
906
|
-
<script type="module" src="dist/wcc-counter.js"></script>
|
|
907
|
-
<script type="module" src="dist/wcc-card.js"></script>
|
|
908
|
-
<script type="module" src="dist/wcc-list.js"></script>
|
|
909
|
-
|
|
910
|
-
<!-- Props (attributes) -->
|
|
911
|
-
<wcc-counter count="0" label="Clicks"></wcc-counter>
|
|
912
|
-
|
|
913
|
-
<!-- Events -->
|
|
914
|
-
<script>
|
|
915
|
-
document.querySelector('wcc-counter')
|
|
916
|
-
.addEventListener('count-changed', (e) => console.log(e.detail))
|
|
917
|
-
</script>
|
|
918
|
-
|
|
919
|
-
<!-- Default slot -->
|
|
920
|
-
<wcc-card>
|
|
921
|
-
<p>Body content</p>
|
|
922
|
-
</wcc-card>
|
|
923
|
-
|
|
924
|
-
<!-- Named slots -->
|
|
925
|
-
<wcc-card>
|
|
926
|
-
<strong slot="header">Title</strong>
|
|
927
|
-
<p>Body</p>
|
|
928
|
-
<span slot="footer">Footer text</span>
|
|
929
|
-
</wcc-card>
|
|
930
|
-
|
|
931
|
-
<!-- Scoped slots -->
|
|
932
|
-
<wcc-list>
|
|
933
|
-
<template #item="{ item, index }">
|
|
934
|
-
<li>{{index}}: {{item}}</li>
|
|
935
|
-
</template>
|
|
936
|
-
</wcc-list>
|
|
937
|
-
```
|
|
938
|
-
|
|
939
|
-
### TypeScript Types for Frameworks
|
|
940
|
-
|
|
941
|
-
`wcc build` auto-generates typed stubs for each framework in the `dist/` folder:
|
|
942
|
-
|
|
943
|
-
```
|
|
944
|
-
dist/
|
|
945
|
-
├── wcc-vue.d.ts ← Vue/Volar prop autocompletion
|
|
946
|
-
├── wcc-vue.js ← Vue component stubs
|
|
947
|
-
├── wcc-react.d.ts ← React compound component types
|
|
948
|
-
├── wcc-react.js ← React component stubs
|
|
949
|
-
└── ...
|
|
950
|
-
```
|
|
951
|
-
|
|
952
|
-
**Vue (Volar autocompletion)**
|
|
953
|
-
|
|
954
|
-
Add `dist/wcc-vue.d.ts` to your tsconfig to get prop/event autocompletion in `.vue` templates:
|
|
955
|
-
|
|
956
|
-
```json
|
|
957
|
-
// tsconfig.json
|
|
958
|
-
{
|
|
959
|
-
"include": ["src/**/*", "dist/wcc-vue.d.ts"]
|
|
960
|
-
}
|
|
961
|
-
```
|
|
962
|
-
|
|
963
|
-
After this, Volar provides:
|
|
964
|
-
- Prop autocompletion: `<wcc-counter :la|` → suggests `label`
|
|
965
|
-
- Type-checking: `<wcc-counter :count="'string'">` → type error (expects number)
|
|
966
|
-
- Event types on hover
|
|
967
|
-
|
|
968
|
-
**React**
|
|
969
|
-
|
|
970
|
-
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.
|
|
971
|
-
|
|
972
|
-
**Angular**
|
|
973
|
-
|
|
974
|
-
Angular's `CUSTOM_ELEMENTS_SCHEMA` disables all type-checking on custom elements. No additional type setup possible from the library side.
|
|
975
|
-
|
|
976
|
-
## Editor Support
|
|
977
|
-
|
|
978
|
-
The **wcCompiler (.wcc) Language Support** extension is available on the VS Code Marketplace. It provides syntax highlighting, completions, and diagnostics for `.wcc` files.
|
|
979
|
-
|
|
980
|
-
## Runtime Helper
|
|
981
|
-
|
|
982
|
-
An optional `wcc-runtime.js` is copied to your output directory for declarative host-page bindings:
|
|
983
|
-
|
|
984
|
-
```html
|
|
985
|
-
<wcc-counter :count="count" @change="handleChange"></wcc-counter>
|
|
986
|
-
|
|
987
|
-
<script type="module">
|
|
988
|
-
import './dist/wcc-counter.js'
|
|
989
|
-
import { init, on, set, get } from './dist/wcc-runtime.js'
|
|
990
|
-
|
|
991
|
-
on('handleChange', (e) => set('count', e.detail))
|
|
992
|
-
init({ count: 0 })
|
|
993
|
-
</script>
|
|
994
|
-
```
|
|
995
|
-
|
|
996
|
-
## License
|
|
997
|
-
|
|
998
|
-
MIT
|
|
1
|
+
# wcCompiler
|
|
2
|
+
|
|
3
|
+
Zero-runtime compiler that transforms `.wcc` single-file components into native web components. No framework, no virtual DOM, no runtime — just vanilla JavaScript custom elements with signals-based reactivity.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install -D @sprlab/wccompiler
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Quick Start
|
|
12
|
+
|
|
13
|
+
**1. Create a component**
|
|
14
|
+
|
|
15
|
+
```html
|
|
16
|
+
<!-- src/wcc-counter.wcc -->
|
|
17
|
+
<script>
|
|
18
|
+
import { defineComponent, signal } from 'wcc'
|
|
19
|
+
|
|
20
|
+
export default defineComponent({
|
|
21
|
+
tag: 'wcc-counter',
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
const count = signal(0)
|
|
25
|
+
|
|
26
|
+
function increment() {
|
|
27
|
+
count.set(count() + 1)
|
|
28
|
+
}
|
|
29
|
+
</script>
|
|
30
|
+
|
|
31
|
+
<template>
|
|
32
|
+
<div class="counter">
|
|
33
|
+
<span>{{count()}}</span>
|
|
34
|
+
<button @click="increment">+</button>
|
|
35
|
+
</div>
|
|
36
|
+
</template>
|
|
37
|
+
|
|
38
|
+
<style>
|
|
39
|
+
.counter { display: flex; gap: 8px; align-items: center; }
|
|
40
|
+
</style>
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
**2. Build**
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
npx wcc build
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
**3. Use**
|
|
50
|
+
|
|
51
|
+
```html
|
|
52
|
+
<script type="module" src="dist/wcc-counter.js"></script>
|
|
53
|
+
<wcc-counter></wcc-counter>
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
The compiled output is a single `.js` file with zero dependencies — works in any browser that supports custom elements.
|
|
57
|
+
|
|
58
|
+
## How It Works
|
|
59
|
+
|
|
60
|
+
```
|
|
61
|
+
src/wcc-counter.wcc dist/wcc-counter.js
|
|
62
|
+
┌──────────────────┐ ┌──────────────────────────┐
|
|
63
|
+
│ <script> │ │ // Reactive runtime │
|
|
64
|
+
│ signal, effect │ ───► │ // (inline or imported) │
|
|
65
|
+
│ <template> │ build │ class WccCounter extends │
|
|
66
|
+
│ {{count()}} │ │ HTMLElement { ... } │
|
|
67
|
+
│ <style> │ │ customElements.define(...) │
|
|
68
|
+
└──────────────────┘ └──────────────────────────┘
|
|
69
|
+
+
|
|
70
|
+
dist/__wcc-signals.js (shared mode)
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
The compiler reads your `.wcc` source, extracts script/template/style blocks, analyzes reactive declarations, walks the template DOM for bindings and directives, and generates a self-contained custom element class. CSS is automatically scoped by tag name.
|
|
74
|
+
|
|
75
|
+
## Single File Component (.wcc)
|
|
76
|
+
|
|
77
|
+
wcCompiler uses a single-file component format with the `.wcc` extension. Each file contains three blocks:
|
|
78
|
+
|
|
79
|
+
- `<script>` — Component logic (signals, props, events, lifecycle)
|
|
80
|
+
- `<template>` — HTML template with directives
|
|
81
|
+
- `<style>` — Scoped CSS
|
|
82
|
+
|
|
83
|
+
```html
|
|
84
|
+
<script>
|
|
85
|
+
import { defineComponent, signal } from 'wcc'
|
|
86
|
+
|
|
87
|
+
export default defineComponent({
|
|
88
|
+
tag: 'wcc-my-component',
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
const message = signal('Hello')
|
|
92
|
+
</script>
|
|
93
|
+
|
|
94
|
+
<template>
|
|
95
|
+
<p>{{message()}}</p>
|
|
96
|
+
</template>
|
|
97
|
+
|
|
98
|
+
<style>
|
|
99
|
+
p { color: steelblue; }
|
|
100
|
+
</style>
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
Use `<script lang="ts">` for TypeScript support. The CLI discovers and compiles all `.wcc` files in your source directory.
|
|
104
|
+
|
|
105
|
+
## Coming from Vue?
|
|
106
|
+
|
|
107
|
+
If you're familiar with Vue, here's how wcCompiler maps:
|
|
108
|
+
|
|
109
|
+
| Vue | wcCompiler |
|
|
110
|
+
|-----|------------|
|
|
111
|
+
| `ref(0)` | `signal(0)` |
|
|
112
|
+
| `computed(() => ...)` | `computed(() => ...)` |
|
|
113
|
+
| `watch(source, cb)` | `watch(source, cb)` |
|
|
114
|
+
| `v-if` | `if` |
|
|
115
|
+
| `v-else-if` | `else-if` |
|
|
116
|
+
| `v-else` | `else` |
|
|
117
|
+
| `v-for="item in items"` | `each="item in items()"` |
|
|
118
|
+
| `v-show` | `show` |
|
|
119
|
+
| `v-model` | `model` |
|
|
120
|
+
| `@click` | `@click` |
|
|
121
|
+
| `:prop` | `:prop` |
|
|
122
|
+
| `defineProps()` | `defineProps()` |
|
|
123
|
+
| `defineEmits()` | `defineEmits()` |
|
|
124
|
+
| `onMounted()` | `onMount()` |
|
|
125
|
+
| `onUnmounted()` | `onDestroy()` |
|
|
126
|
+
| `<slot>` | `<slot>` |
|
|
127
|
+
|
|
128
|
+
Key differences: signals use `.set()` to write and `()` to read. Template directives have no `v-` prefix. Output is vanilla JS with no runtime framework.
|
|
129
|
+
|
|
130
|
+
## Reactivity
|
|
131
|
+
|
|
132
|
+
### Signals
|
|
133
|
+
|
|
134
|
+
```js
|
|
135
|
+
const count = signal(0) // create
|
|
136
|
+
count() // read → 0
|
|
137
|
+
count.set(5) // write → 5
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
> **Note:** `.set()` is the public API for writing signals. The compiled output uses direct invocation (`count(5)`) as an internal optimization — both forms are equivalent, but `.set()` is the recommended way to write signals in your source code.
|
|
141
|
+
|
|
142
|
+
### Computed
|
|
143
|
+
|
|
144
|
+
```js
|
|
145
|
+
const doubled = computed(() => count() * 2)
|
|
146
|
+
doubled() // auto-updates when count changes
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
### Effects
|
|
150
|
+
|
|
151
|
+
```js
|
|
152
|
+
effect(() => {
|
|
153
|
+
console.log('Count is:', count()) // re-runs on change
|
|
154
|
+
})
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
Effects support cleanup — return a function to run before re-execution:
|
|
158
|
+
|
|
159
|
+
```js
|
|
160
|
+
effect(() => {
|
|
161
|
+
const id = setInterval(() => tick.set(tick() + 1), 1000)
|
|
162
|
+
return () => clearInterval(id) // called before re-run
|
|
163
|
+
})
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
### Batch
|
|
167
|
+
|
|
168
|
+
Group multiple signal writes into a single update pass:
|
|
169
|
+
|
|
170
|
+
```js
|
|
171
|
+
import { batch } from 'wcc'
|
|
172
|
+
|
|
173
|
+
batch(() => {
|
|
174
|
+
firstName.set('John')
|
|
175
|
+
lastName.set('Doe')
|
|
176
|
+
age.set(30)
|
|
177
|
+
})
|
|
178
|
+
// Effects run once after all three writes, not three times
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
Nested batches are supported — effects flush only when the outermost batch completes.
|
|
182
|
+
|
|
183
|
+
### Watch
|
|
184
|
+
|
|
185
|
+
```js
|
|
186
|
+
// Watch a signal directly
|
|
187
|
+
watch(count, (newVal, oldVal) => {
|
|
188
|
+
console.log(`Changed from ${oldVal} to ${newVal}`)
|
|
189
|
+
})
|
|
190
|
+
|
|
191
|
+
// Watch a getter function (useful for props or derived values)
|
|
192
|
+
watch(() => props.count, (newVal, oldVal) => {
|
|
193
|
+
console.log(`Prop changed: ${oldVal} → ${newVal}`)
|
|
194
|
+
})
|
|
195
|
+
|
|
196
|
+
// Watch a derived expression
|
|
197
|
+
watch(() => count() * 2, (newVal, oldVal) => {
|
|
198
|
+
console.log(`Doubled changed: ${oldVal} → ${newVal}`)
|
|
199
|
+
})
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
`watch` observes a specific signal or getter and provides both old and new values. The callback does not run on initial mount — only on subsequent changes.
|
|
203
|
+
|
|
204
|
+
### Constants
|
|
205
|
+
|
|
206
|
+
```js
|
|
207
|
+
const TAX_RATE = 0.21 // non-reactive, no signal() wrapper
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
## Props
|
|
211
|
+
|
|
212
|
+
```js
|
|
213
|
+
const props = defineProps({ label: 'Click', count: 0 })
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
```html
|
|
217
|
+
<wcc-counter label="Clicks:" count="5"></wcc-counter>
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
You can also call `defineProps` without assignment — the props are available by name in the template:
|
|
221
|
+
|
|
222
|
+
```js
|
|
223
|
+
defineProps({ label: 'Click' })
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
```html
|
|
227
|
+
<span>{{label}}</span>
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
TypeScript generics:
|
|
231
|
+
|
|
232
|
+
```ts
|
|
233
|
+
const props = defineProps<{ label: string, count: number }>({ label: 'Click', count: 0 })
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
Props are reactive — they update when attributes change. Supports boolean and number coercion.
|
|
237
|
+
|
|
238
|
+
## Custom Events
|
|
239
|
+
|
|
240
|
+
```js
|
|
241
|
+
const emit = defineEmits(['change', 'reset'])
|
|
242
|
+
|
|
243
|
+
function handleClick() {
|
|
244
|
+
emit('change', count())
|
|
245
|
+
}
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
TypeScript call signatures:
|
|
249
|
+
|
|
250
|
+
```ts
|
|
251
|
+
const emit = defineEmits<{ (e: 'change', value: number): void }>()
|
|
252
|
+
```
|
|
253
|
+
|
|
254
|
+
The compiler validates emit calls against declared events at compile time.
|
|
255
|
+
|
|
256
|
+
## defineModel (Two-Way Binding)
|
|
257
|
+
|
|
258
|
+
`defineModel` declares a prop that supports two-way binding across frameworks:
|
|
259
|
+
|
|
260
|
+
```js
|
|
261
|
+
import { defineModel } from 'wcc'
|
|
262
|
+
|
|
263
|
+
const count = defineModel({ name: 'count', default: 0 })
|
|
264
|
+
const title = defineModel({ name: 'title', default: 'untitled' })
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
Read and write like a signal:
|
|
268
|
+
```js
|
|
269
|
+
count() // read current value
|
|
270
|
+
count.set(5) // write — updates internal state + emits events
|
|
271
|
+
```
|
|
272
|
+
|
|
273
|
+
**Events emitted on write:**
|
|
274
|
+
|
|
275
|
+
| Event | Purpose |
|
|
276
|
+
|-------|---------|
|
|
277
|
+
| `count-changed` | Kebab-case — for Vue plugin, addEventListener |
|
|
278
|
+
| `countChanged` | camelCase — for Angular, direct binding |
|
|
279
|
+
| `countChange` | Angular `[(count)]` banana-box syntax |
|
|
280
|
+
| `wcc:model` | Generic — for vanilla JS and WCC-to-WCC |
|
|
281
|
+
|
|
282
|
+
**Usage per framework:**
|
|
283
|
+
|
|
284
|
+
```vue
|
|
285
|
+
<!-- Vue (with plugin) -->
|
|
286
|
+
<wcc-counter v-model:count="ref"></wcc-counter>
|
|
287
|
+
|
|
288
|
+
<!-- Vue (without plugin) -->
|
|
289
|
+
<wcc-counter :count="ref" @count-changed="ref = $event.detail"></wcc-counter>
|
|
290
|
+
```
|
|
291
|
+
|
|
292
|
+
```jsx
|
|
293
|
+
// React (with wrapper)
|
|
294
|
+
<WccCounter count={count} onCountChange={(value) => setCount(value)} />
|
|
295
|
+
|
|
296
|
+
// React (native CE)
|
|
297
|
+
<wcc-counter count={count} oncountchanged={(e) => setCount(e.detail)} />
|
|
298
|
+
```
|
|
299
|
+
|
|
300
|
+
```html
|
|
301
|
+
<!-- Angular (zero-config two-way) -->
|
|
302
|
+
<wcc-counter [(count)]="signal"></wcc-counter>
|
|
303
|
+
|
|
304
|
+
<!-- Angular (manual) -->
|
|
305
|
+
<wcc-counter [count]="signal()" (countChange)="signal.set($event.detail)"></wcc-counter>
|
|
306
|
+
```
|
|
307
|
+
|
|
308
|
+
## Template Directives
|
|
309
|
+
|
|
310
|
+
### Text Interpolation
|
|
311
|
+
|
|
312
|
+
Signals and computeds require `()` to read their value in templates:
|
|
313
|
+
|
|
314
|
+
```html
|
|
315
|
+
<span>{{count()}}</span>
|
|
316
|
+
<p>You have {{items().length}} items.</p>
|
|
317
|
+
<span>{{doubled()}}</span>
|
|
318
|
+
```
|
|
319
|
+
|
|
320
|
+
Props accessed without assignment use their name directly (no parentheses):
|
|
321
|
+
|
|
322
|
+
```html
|
|
323
|
+
<span>{{label}}</span>
|
|
324
|
+
<p>Hello, {{name}}!</p>
|
|
325
|
+
```
|
|
326
|
+
|
|
327
|
+
### Event Binding
|
|
328
|
+
|
|
329
|
+
```html
|
|
330
|
+
<button @click="increment">+</button>
|
|
331
|
+
<input @input="handleInput">
|
|
332
|
+
```
|
|
333
|
+
|
|
334
|
+
Event handlers support expressions and inline arguments:
|
|
335
|
+
|
|
336
|
+
```html
|
|
337
|
+
<button @click="removeItem(item)">×</button>
|
|
338
|
+
<button @click="() => doSomething()">Do it</button>
|
|
339
|
+
```
|
|
340
|
+
|
|
341
|
+
### Conditional Rendering
|
|
342
|
+
|
|
343
|
+
```html
|
|
344
|
+
<div if="status() === 'active'">Active</div>
|
|
345
|
+
<div else-if="status() === 'pending'">Pending</div>
|
|
346
|
+
<div else>Inactive</div>
|
|
347
|
+
```
|
|
348
|
+
|
|
349
|
+
### List Rendering
|
|
350
|
+
|
|
351
|
+
```html
|
|
352
|
+
<li each="item in items()">{{item.name}}</li>
|
|
353
|
+
<li each="(item, index) in items()">{{index}}: {{item.name}}</li>
|
|
354
|
+
```
|
|
355
|
+
|
|
356
|
+
The source expression calls the signal (`items()`) to read the current array. Supports keyed rendering with `:key`:
|
|
357
|
+
|
|
358
|
+
```html
|
|
359
|
+
<li each="item in items()" :key="item.id">{{item.name}}</li>
|
|
360
|
+
```
|
|
361
|
+
|
|
362
|
+
Numeric ranges are also supported:
|
|
363
|
+
|
|
364
|
+
```html
|
|
365
|
+
<li each="n in 5">Item {{n}}</li>
|
|
366
|
+
```
|
|
367
|
+
|
|
368
|
+
#### Nested Directives in `each`
|
|
369
|
+
|
|
370
|
+
Directives work inside `each` blocks — including conditionals and nested loops:
|
|
371
|
+
|
|
372
|
+
```html
|
|
373
|
+
<div each="user in users()">
|
|
374
|
+
<span>{{user.name}}</span>
|
|
375
|
+
<span if="user.active" class="badge">Active</span>
|
|
376
|
+
<span else class="badge muted">Inactive</span>
|
|
377
|
+
<ul>
|
|
378
|
+
<li each="role in user.roles">{{role}}</li>
|
|
379
|
+
</ul>
|
|
380
|
+
</div>
|
|
381
|
+
```
|
|
382
|
+
|
|
383
|
+
### Visibility Toggle
|
|
384
|
+
|
|
385
|
+
```html
|
|
386
|
+
<div show="isVisible()">Shown or hidden via CSS display</div>
|
|
387
|
+
```
|
|
388
|
+
|
|
389
|
+
### Two-Way Binding
|
|
390
|
+
|
|
391
|
+
```html
|
|
392
|
+
<input type="text" model="name">
|
|
393
|
+
<input type="number" model="age">
|
|
394
|
+
<input type="checkbox" model="agree">
|
|
395
|
+
<input type="radio" name="color" value="red" model="color">
|
|
396
|
+
<select model="country">...</select>
|
|
397
|
+
<textarea model="bio"></textarea>
|
|
398
|
+
```
|
|
399
|
+
|
|
400
|
+
### Attribute Binding
|
|
401
|
+
|
|
402
|
+
```html
|
|
403
|
+
<a :href="url()">Link</a>
|
|
404
|
+
<button :disabled="isLoading()">Submit</button>
|
|
405
|
+
<div :class="{ active: isActive(), error: hasError() }">...</div>
|
|
406
|
+
<div :style="{ color: textColor() }">...</div>
|
|
407
|
+
```
|
|
408
|
+
|
|
409
|
+
### Template Refs
|
|
410
|
+
|
|
411
|
+
```js
|
|
412
|
+
const canvas = templateRef('myCanvas')
|
|
413
|
+
|
|
414
|
+
onMount(() => {
|
|
415
|
+
const ctx = canvas.value.getContext('2d')
|
|
416
|
+
})
|
|
417
|
+
```
|
|
418
|
+
|
|
419
|
+
```html
|
|
420
|
+
<canvas ref="myCanvas"></canvas>
|
|
421
|
+
```
|
|
422
|
+
|
|
423
|
+
## Slots
|
|
424
|
+
|
|
425
|
+
### Named Slots
|
|
426
|
+
|
|
427
|
+
Component template:
|
|
428
|
+
```html
|
|
429
|
+
<div class="card">
|
|
430
|
+
<slot name="header">Default Header</slot>
|
|
431
|
+
<slot>Default Body</slot>
|
|
432
|
+
<slot name="footer">Default Footer</slot>
|
|
433
|
+
</div>
|
|
434
|
+
```
|
|
435
|
+
|
|
436
|
+
Consumer:
|
|
437
|
+
```html
|
|
438
|
+
<wcc-card>
|
|
439
|
+
<template #header><strong>Custom Header</strong></template>
|
|
440
|
+
<p>Custom body content</p>
|
|
441
|
+
<template #footer>Custom footer</template>
|
|
442
|
+
</wcc-card>
|
|
443
|
+
```
|
|
444
|
+
|
|
445
|
+
### Scoped Slots
|
|
446
|
+
|
|
447
|
+
Component template (passes reactive data to consumer):
|
|
448
|
+
```html
|
|
449
|
+
<slot name="stats" :likes="likes">Likes: {{likes}}</slot>
|
|
450
|
+
```
|
|
451
|
+
|
|
452
|
+
Consumer (receives data via template props):
|
|
453
|
+
```html
|
|
454
|
+
<wcc-card>
|
|
455
|
+
<template #stats="{ likes }">🔥 {{likes}} likes!</template>
|
|
456
|
+
</wcc-card>
|
|
457
|
+
```
|
|
458
|
+
|
|
459
|
+
## Nested Components
|
|
460
|
+
|
|
461
|
+
Components can import and use other components in their templates using PascalCase tags:
|
|
462
|
+
|
|
463
|
+
```html
|
|
464
|
+
<!-- src/nested/wcc-profile.wcc -->
|
|
465
|
+
<script>
|
|
466
|
+
import { defineComponent, signal } from 'wcc'
|
|
467
|
+
import WccBadge from './wcc-badge.wcc'
|
|
468
|
+
|
|
469
|
+
export default defineComponent({ tag: 'wcc-profile' })
|
|
470
|
+
|
|
471
|
+
const count = signal(0)
|
|
472
|
+
|
|
473
|
+
function increment() {
|
|
474
|
+
count.set(count() + 1)
|
|
475
|
+
}
|
|
476
|
+
</script>
|
|
477
|
+
|
|
478
|
+
<template>
|
|
479
|
+
<div class="profile">
|
|
480
|
+
<WccBadge :count="count()" @click="increment"></WccBadge>
|
|
481
|
+
</div>
|
|
482
|
+
</template>
|
|
483
|
+
```
|
|
484
|
+
|
|
485
|
+
- **Named import**: `import WccBadge from './wcc-badge.wcc'` — the PascalCase identifier becomes the tag alias in the template
|
|
486
|
+
- **Side-effect import**: `import './wcc-child.wcc'` — registers the child without using it in the template (for programmatic creation)
|
|
487
|
+
- **Reactive props**: Use `:prop="expr"` to pass reactive data down — updates automatically when the expression changes
|
|
488
|
+
- **Event listening**: Use `@event="handler"` to listen to custom events emitted by the child
|
|
489
|
+
- **Compile-time validation**: Using a PascalCase tag without a matching import throws an error at build time
|
|
490
|
+
- **Hyphenated tags**: Tags like `<my-element>` without a corresponding import are treated as plain custom elements (no import generated)
|
|
491
|
+
|
|
492
|
+
## Lifecycle Hooks
|
|
493
|
+
|
|
494
|
+
```js
|
|
495
|
+
onMount(() => {
|
|
496
|
+
console.log('Component connected to DOM')
|
|
497
|
+
})
|
|
498
|
+
|
|
499
|
+
onMount(async () => {
|
|
500
|
+
const data = await fetch('/api/items').then(r => r.json())
|
|
501
|
+
items.set(data)
|
|
502
|
+
})
|
|
503
|
+
|
|
504
|
+
onDestroy(() => {
|
|
505
|
+
console.log('Component removed from DOM')
|
|
506
|
+
})
|
|
507
|
+
```
|
|
508
|
+
|
|
509
|
+
Async callbacks are wrapped in an IIFE — `connectedCallback` itself stays synchronous.
|
|
510
|
+
|
|
511
|
+
**Details:**
|
|
512
|
+
- Multiple `onMount` / `onDestroy` calls are supported — they all run in declaration order
|
|
513
|
+
- `connectedCallback` is idempotent — re-mounting a component (e.g., moving it in the DOM) re-attaches listeners and effects cleanly
|
|
514
|
+
- All effects and event listeners are automatically cleaned up in `disconnectedCallback` via AbortController
|
|
515
|
+
|
|
516
|
+
## CSS Scoping
|
|
517
|
+
|
|
518
|
+
Styles are automatically scoped to the component using tag-name prefixing:
|
|
519
|
+
|
|
520
|
+
```css
|
|
521
|
+
/* Input */
|
|
522
|
+
.counter { display: flex; }
|
|
523
|
+
|
|
524
|
+
/* Output */
|
|
525
|
+
wcc-counter .counter { display: flex; }
|
|
526
|
+
```
|
|
527
|
+
|
|
528
|
+
`@media` rules are recursively scoped. `@keyframes` are preserved without prefixing.
|
|
529
|
+
|
|
530
|
+
## TypeScript
|
|
531
|
+
|
|
532
|
+
Use `<script lang="ts">` in your `.wcc` file for full type support:
|
|
533
|
+
|
|
534
|
+
```html
|
|
535
|
+
<script lang="ts">
|
|
536
|
+
import { defineComponent, defineProps, defineEmits, signal, computed, watch, defineExpose } from 'wcc'
|
|
537
|
+
|
|
538
|
+
export default defineComponent({
|
|
539
|
+
tag: 'wcc-typescript',
|
|
540
|
+
})
|
|
541
|
+
|
|
542
|
+
const props = defineProps<{ title: string, count: number }>({ title: 'Demo', count: 0 })
|
|
543
|
+
const emit = defineEmits<{ (e: 'update', value: number): void }>()
|
|
544
|
+
|
|
545
|
+
const doubled = computed<number>(() => props.count * 2)
|
|
546
|
+
const watchLog = signal<string>('(no changes yet)')
|
|
547
|
+
|
|
548
|
+
watch(() => props.count, (newVal, oldVal) => {
|
|
549
|
+
watchLog.set(`count changed: ${oldVal} → ${newVal}`)
|
|
550
|
+
})
|
|
551
|
+
|
|
552
|
+
function handleUpdate(): void {
|
|
553
|
+
emit('update', doubled())
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
defineExpose({ doubled, handleUpdate, watchLog })
|
|
557
|
+
</script>
|
|
558
|
+
|
|
559
|
+
<template>
|
|
560
|
+
<div class="demo">
|
|
561
|
+
<span>{{title}}: {{count}}</span>
|
|
562
|
+
<span>Doubled: {{doubled()}}</span>
|
|
563
|
+
<span>Watch: {{watchLog()}}</span>
|
|
564
|
+
<button @click="handleUpdate">Update</button>
|
|
565
|
+
</div>
|
|
566
|
+
</template>
|
|
567
|
+
|
|
568
|
+
<style>
|
|
569
|
+
.demo { font-family: sans-serif; }
|
|
570
|
+
</style>
|
|
571
|
+
```
|
|
572
|
+
|
|
573
|
+
`defineExpose()` exposes methods and properties for external access via ref.
|
|
574
|
+
|
|
575
|
+
```js
|
|
576
|
+
// wcc-timer.wcc — exposes start/stop/elapsed
|
|
577
|
+
const elapsed = signal(0)
|
|
578
|
+
let interval = null
|
|
579
|
+
|
|
580
|
+
function start() { interval = setInterval(() => elapsed.set(elapsed() + 1), 1000) }
|
|
581
|
+
function stop() { clearInterval(interval) }
|
|
582
|
+
|
|
583
|
+
defineExpose({ elapsed, start, stop })
|
|
584
|
+
```
|
|
585
|
+
|
|
586
|
+
```html
|
|
587
|
+
<!-- Parent component accessing exposed API -->
|
|
588
|
+
<script>
|
|
589
|
+
import { defineComponent, templateRef, onMount } from 'wcc'
|
|
590
|
+
import './wcc-timer.wcc'
|
|
591
|
+
|
|
592
|
+
export default defineComponent({ tag: 'wcc-app' })
|
|
593
|
+
|
|
594
|
+
const timer = templateRef('timer')
|
|
595
|
+
|
|
596
|
+
onMount(() => {
|
|
597
|
+
timer.value.start() // call exposed method
|
|
598
|
+
console.log(timer.value.elapsed) // read exposed signal
|
|
599
|
+
})
|
|
600
|
+
</script>
|
|
601
|
+
|
|
602
|
+
<template>
|
|
603
|
+
<wcc-timer ref="timer"></wcc-timer>
|
|
604
|
+
</template>
|
|
605
|
+
```
|
|
606
|
+
|
|
607
|
+
The language server automatically generates a typed interface (PascalCase of the tag name) that can be imported by consumers:
|
|
608
|
+
|
|
609
|
+
```ts
|
|
610
|
+
// In the parent component:
|
|
611
|
+
import type { WccTimer } from './wcc-timer.wcc'
|
|
612
|
+
const timer = templateRef<WccTimer>('timer')
|
|
613
|
+
timer.value!.start() // ✅ typed
|
|
614
|
+
```
|
|
615
|
+
|
|
616
|
+
## CLI
|
|
617
|
+
|
|
618
|
+
```bash
|
|
619
|
+
wcc build # Compile all .wcc files from input/ to output/
|
|
620
|
+
wcc build --bundle # Compile + produce a single bundle.js (works from file://)
|
|
621
|
+
wcc build --minify # Compile with minification
|
|
622
|
+
wcc build --bundle --minify # Production bundle (smallest output)
|
|
623
|
+
wcc dev # Build + watch + live-reload dev server
|
|
624
|
+
```
|
|
625
|
+
|
|
626
|
+
The CLI discovers all `.wcc` files in your source directory and compiles each into a standalone `.js` file.
|
|
627
|
+
|
|
628
|
+
### Bundle Mode
|
|
629
|
+
|
|
630
|
+
The `--bundle` flag produces a single `bundle.js` file that includes all components and their dependencies in one IIFE (Immediately Invoked Function Expression). This file:
|
|
631
|
+
|
|
632
|
+
- Works with `<script src="bundle.js">` (no `type="module"` needed)
|
|
633
|
+
- Works from `file://` protocol (no server required)
|
|
634
|
+
- Includes all child component imports resolved and inlined
|
|
635
|
+
- Includes the reactive runtime
|
|
636
|
+
- Supports `--minify` for production
|
|
637
|
+
|
|
638
|
+
```html
|
|
639
|
+
<!-- Works by double-clicking the HTML file — no server needed -->
|
|
640
|
+
<!DOCTYPE html>
|
|
641
|
+
<html>
|
|
642
|
+
<body>
|
|
643
|
+
<wcc-my-app></wcc-my-app>
|
|
644
|
+
<script src="dist/bundle.js"></script>
|
|
645
|
+
</body>
|
|
646
|
+
</html>
|
|
647
|
+
```
|
|
648
|
+
|
|
649
|
+
**When to use `--bundle`:**
|
|
650
|
+
- Static HTML files opened from disk
|
|
651
|
+
- Electron apps loading local files
|
|
652
|
+
- Offline-first applications
|
|
653
|
+
- Quick prototyping without a dev server
|
|
654
|
+
- Distributing a complete app as HTML + JS
|
|
655
|
+
|
|
656
|
+
**When NOT to use `--bundle`:**
|
|
657
|
+
- Apps served via HTTP (use ES modules for better caching)
|
|
658
|
+
- When you need per-component lazy loading
|
|
659
|
+
- When using a bundler like Vite/Webpack (they handle bundling themselves)
|
|
660
|
+
|
|
661
|
+
### Configuration
|
|
662
|
+
|
|
663
|
+
Create `wcc.config.js` in your project root:
|
|
664
|
+
|
|
665
|
+
```js
|
|
666
|
+
export default {
|
|
667
|
+
port: 4100, // dev server port (default: 4100)
|
|
668
|
+
input: 'src', // source directory (default: 'src')
|
|
669
|
+
output: 'dist', // output directory (default: 'dist')
|
|
670
|
+
standalone: false // inline runtime per component (default: false)
|
|
671
|
+
}
|
|
672
|
+
```
|
|
673
|
+
|
|
674
|
+
All options are optional — defaults shown above.
|
|
675
|
+
|
|
676
|
+
| Option | Type | Default | Description |
|
|
677
|
+
|--------|------|---------|-------------|
|
|
678
|
+
| `port` | number | `4100` | Dev server port for `wcc dev` |
|
|
679
|
+
| `input` | string | `'src'` | Source directory containing `.wcc` files |
|
|
680
|
+
| `output` | string | `'dist'` | Output directory for compiled `.js` files |
|
|
681
|
+
| `standalone` | boolean | `false` | Inline reactive runtime in each component |
|
|
682
|
+
|
|
683
|
+
### Standalone Mode
|
|
684
|
+
|
|
685
|
+
Controls whether the reactive runtime is inlined in each component or imported from a shared module.
|
|
686
|
+
|
|
687
|
+
```js
|
|
688
|
+
// wcc.config.js
|
|
689
|
+
export default {
|
|
690
|
+
standalone: true // inline runtime in every component (default: false)
|
|
691
|
+
}
|
|
692
|
+
```
|
|
693
|
+
|
|
694
|
+
- `standalone: false` (default) — Components import the runtime from a shared `__wcc-signals.js` file. Smaller per-component size when using multiple components.
|
|
695
|
+
- `standalone: true` — Each component includes the full reactive runtime inline. Zero external dependencies per component.
|
|
696
|
+
|
|
697
|
+
**Output difference:**
|
|
698
|
+
|
|
699
|
+
```
|
|
700
|
+
Default (false): component.js → imports __wcc-signals.js
|
|
701
|
+
Standalone (true): component.js → runtime inlined, zero imports
|
|
702
|
+
```
|
|
703
|
+
|
|
704
|
+
**When to use standalone:**
|
|
705
|
+
- Publishing components as npm packages
|
|
706
|
+
- Embedding widgets in third-party sites
|
|
707
|
+
- CDN distribution (`<script src="component.js">`)
|
|
708
|
+
- Micro-frontends where you don't control the host
|
|
709
|
+
|
|
710
|
+
**When NOT to use standalone:**
|
|
711
|
+
- Apps with multiple components (runtime would be duplicated in each)
|
|
712
|
+
- Internal projects where you control the build
|
|
713
|
+
|
|
714
|
+
#### Per-Component Override
|
|
715
|
+
|
|
716
|
+
Override the global setting for individual components:
|
|
717
|
+
|
|
718
|
+
```html
|
|
719
|
+
<script>
|
|
720
|
+
import { defineComponent, signal } from 'wcc'
|
|
721
|
+
|
|
722
|
+
export default defineComponent({
|
|
723
|
+
tag: 'wcc-widget',
|
|
724
|
+
standalone: true, // this component is self-contained regardless of global config
|
|
725
|
+
})
|
|
726
|
+
</script>
|
|
727
|
+
```
|
|
728
|
+
|
|
729
|
+
Component-level `standalone` always takes precedence over the global config. This lets you have a project with shared runtime but mark specific components as fully self-contained for distribution.
|
|
730
|
+
|
|
731
|
+
#### Reactive Scope Isolation
|
|
732
|
+
|
|
733
|
+
Each standalone component has its own isolated reactive runtime. Signals from component A cannot be observed by effects in component B — they are completely independent. This is by design for distribution scenarios where components must be self-contained. If you need cross-component reactivity (e.g., shared state), use the default shared mode (`standalone: false`).
|
|
734
|
+
|
|
735
|
+
## Framework Integrations
|
|
736
|
+
|
|
737
|
+
WCC components are native custom elements — they work in any framework. Props, events, and named slots work natively with zero WCC-specific config. Two-way binding is zero-config in Angular; Vue requires a plugin. Scoped slots require a framework plugin or directive for idiomatic syntax.
|
|
738
|
+
|
|
739
|
+
### Feature Support Matrix
|
|
740
|
+
|
|
741
|
+
| Feature | Vue (plugin) | Angular (directive) | React 19 (plugin) |
|
|
742
|
+
|---------|--------------|--------------------|--------------------|
|
|
743
|
+
| Props | ✅ `:count="ref"` | ✅ `[count]="signal()"` | ✅ `count={state}` |
|
|
744
|
+
| Events | ✅ `@count-changed="handler($event.detail)"` | ✅ `(count-changed)="handler($event.detail)"` | ✅ `oncountchanged={(e) => handler(e.detail)}` |
|
|
745
|
+
| Two-way binding | ✅ `v-model:count="ref"` | ✅ `[(count)]="signal"` | ❌ Not applicable |
|
|
746
|
+
| Default slot | ✅ children | ✅ children | ✅ children |
|
|
747
|
+
| Named slots | ✅ `<template #name>` | ✅ `<div slot-name>` | ✅ `<WccCard.Header>` |
|
|
748
|
+
| Scoped slots | ✅ `<template #name="{ prop }">` | ✅ `<ng-template slot="name" let-prop>` | ✅ `<WccList.Item>{(prop) => jsx}</WccList.Item>` |
|
|
749
|
+
|
|
750
|
+
### Vue (with `wccVuePlugin`)
|
|
751
|
+
|
|
752
|
+
```js
|
|
753
|
+
// vite.config.js
|
|
754
|
+
import { wccVuePlugin } from '@sprlab/wccompiler/integrations/vue'
|
|
755
|
+
export default defineConfig({ plugins: [wccVuePlugin()] })
|
|
756
|
+
```
|
|
757
|
+
|
|
758
|
+
```vue
|
|
759
|
+
<script setup>
|
|
760
|
+
import { ref } from 'vue'
|
|
761
|
+
const count = ref(0)
|
|
762
|
+
const text = ref('')
|
|
763
|
+
</script>
|
|
764
|
+
|
|
765
|
+
<template>
|
|
766
|
+
<!-- Props -->
|
|
767
|
+
<wcc-counter :count="count" label="Clicks"></wcc-counter>
|
|
768
|
+
|
|
769
|
+
<!-- Events -->
|
|
770
|
+
<wcc-counter @count-changed="count = $event.detail"></wcc-counter>
|
|
771
|
+
|
|
772
|
+
<!-- Two-way binding (v-model) -->
|
|
773
|
+
<wcc-counter v-model:count="count"></wcc-counter>
|
|
774
|
+
<wcc-input v-model.trim="text"></wcc-input>
|
|
775
|
+
|
|
776
|
+
<!-- Default slot -->
|
|
777
|
+
<wcc-card>
|
|
778
|
+
<p>Body content</p>
|
|
779
|
+
</wcc-card>
|
|
780
|
+
|
|
781
|
+
<!-- Named slots -->
|
|
782
|
+
<wcc-card>
|
|
783
|
+
<template #header><strong>Title</strong></template>
|
|
784
|
+
<p>Body</p>
|
|
785
|
+
<template #footer>Footer text</template>
|
|
786
|
+
</wcc-card>
|
|
787
|
+
|
|
788
|
+
<!-- Scoped slots -->
|
|
789
|
+
<wcc-list>
|
|
790
|
+
<template #item="{ item, index }">
|
|
791
|
+
<li>{{ index }}: {{ item }}</li>
|
|
792
|
+
</template>
|
|
793
|
+
</wcc-list>
|
|
794
|
+
</template>
|
|
795
|
+
```
|
|
796
|
+
|
|
797
|
+
The plugin provides: `isCustomElement` config, `v-model:prop` support, v-model modifiers (`.trim`, `.number`), and scoped slot syntax (`{{prop}}` → `{%prop%}` escape).
|
|
798
|
+
|
|
799
|
+
### Angular (with `WccSlotsDirective`)
|
|
800
|
+
|
|
801
|
+
```ts
|
|
802
|
+
import { Component, CUSTOM_ELEMENTS_SCHEMA } from '@angular/core'
|
|
803
|
+
import { WccSlotsDirective, WccSlotDef } from '@sprlab/wccompiler/adapters/angular'
|
|
804
|
+
|
|
805
|
+
@Component({
|
|
806
|
+
imports: [WccSlotsDirective, WccSlotDef],
|
|
807
|
+
schemas: [CUSTOM_ELEMENTS_SCHEMA],
|
|
808
|
+
template: `
|
|
809
|
+
<!-- Props -->
|
|
810
|
+
<wcc-counter [count]="count" label="Clicks"></wcc-counter>
|
|
811
|
+
|
|
812
|
+
<!-- Events -->
|
|
813
|
+
<wcc-counter (count-changed)="onCount($event.detail)"></wcc-counter>
|
|
814
|
+
|
|
815
|
+
<!-- Two-way binding (banana-box) -->
|
|
816
|
+
<wcc-counter [(count)]="count"></wcc-counter>
|
|
817
|
+
|
|
818
|
+
<!-- Default slot -->
|
|
819
|
+
<wcc-card>
|
|
820
|
+
<p>Body content</p>
|
|
821
|
+
</wcc-card>
|
|
822
|
+
|
|
823
|
+
<!-- Named slots -->
|
|
824
|
+
<wcc-card wccSlots>
|
|
825
|
+
<strong slot-header>Title</strong>
|
|
826
|
+
<p>Body</p>
|
|
827
|
+
<span slot-footer>Footer text</span>
|
|
828
|
+
</wcc-card>
|
|
829
|
+
|
|
830
|
+
<!-- Scoped slots -->
|
|
831
|
+
<wcc-list wccSlots>
|
|
832
|
+
<ng-template slot="item" let-item let-index="index">
|
|
833
|
+
<li>{{ index }}: {{ item }}</li>
|
|
834
|
+
</ng-template>
|
|
835
|
+
</wcc-list>
|
|
836
|
+
`
|
|
837
|
+
})
|
|
838
|
+
export class AppComponent {
|
|
839
|
+
count = 0
|
|
840
|
+
onCount(value: number) { this.count = value }
|
|
841
|
+
}
|
|
842
|
+
```
|
|
843
|
+
|
|
844
|
+
Angular needs no plugin for props, events, or two-way binding — only `CUSTOM_ELEMENTS_SCHEMA`. The directive is only needed for named slots (with `slot-name` syntax) and scoped slots.
|
|
845
|
+
|
|
846
|
+
### React 19 (with `wccReactPlugin`)
|
|
847
|
+
|
|
848
|
+
```js
|
|
849
|
+
// vite.config.js
|
|
850
|
+
import { wccReactPlugin } from '@sprlab/wccompiler/integrations/react'
|
|
851
|
+
import react from '@vitejs/plugin-react'
|
|
852
|
+
export default defineConfig({ plugins: [wccReactPlugin({ prefix: 'wcc-' }), react()] })
|
|
853
|
+
```
|
|
854
|
+
|
|
855
|
+
```jsx
|
|
856
|
+
import { useState } from 'react'
|
|
857
|
+
import { WccCard, WccList } from './dist/wcc-react'
|
|
858
|
+
|
|
859
|
+
export default function App() {
|
|
860
|
+
const [count, setCount] = useState(0)
|
|
861
|
+
|
|
862
|
+
return (
|
|
863
|
+
<>
|
|
864
|
+
{/* Props */}
|
|
865
|
+
<wcc-counter count={count} label="Clicks"></wcc-counter>
|
|
866
|
+
|
|
867
|
+
{/* Events */}
|
|
868
|
+
<wcc-counter oncountchanged={(e) => setCount(e.detail)}></wcc-counter>
|
|
869
|
+
|
|
870
|
+
{/* Default slot */}
|
|
871
|
+
<WccCard>
|
|
872
|
+
<p>Body content</p>
|
|
873
|
+
</WccCard>
|
|
874
|
+
|
|
875
|
+
{/* Named slots (compound pattern) */}
|
|
876
|
+
<WccCard>
|
|
877
|
+
<WccCard.Header><strong>Title</strong></WccCard.Header>
|
|
878
|
+
<p>Body</p>
|
|
879
|
+
<WccCard.Footer>Footer text</WccCard.Footer>
|
|
880
|
+
</WccCard>
|
|
881
|
+
|
|
882
|
+
{/* Named slots (props pattern) */}
|
|
883
|
+
<wcc-card header={<strong>Title</strong>} footer="Footer text">
|
|
884
|
+
<p>Body</p>
|
|
885
|
+
</wcc-card>
|
|
886
|
+
|
|
887
|
+
{/* Scoped slots (compound pattern) */}
|
|
888
|
+
<WccList>
|
|
889
|
+
<WccList.Item>{(item, index) => <li>{index}: {item}</li>}</WccList.Item>
|
|
890
|
+
</WccList>
|
|
891
|
+
|
|
892
|
+
{/* Scoped slots (render prop pattern) */}
|
|
893
|
+
<wcc-list renderItem={(item, index) => <li>{index}: {item}</li>} />
|
|
894
|
+
</>
|
|
895
|
+
)
|
|
896
|
+
}
|
|
897
|
+
```
|
|
898
|
+
|
|
899
|
+
The plugin transforms PascalCase tags, compound components, props-as-slots, and render props at build time. Import stubs from `./dist/wcc-react` (auto-generated by `wcc build`).
|
|
900
|
+
|
|
901
|
+
### Vanilla (no framework)
|
|
902
|
+
|
|
903
|
+
No configuration needed:
|
|
904
|
+
|
|
905
|
+
```html
|
|
906
|
+
<script type="module" src="dist/wcc-counter.js"></script>
|
|
907
|
+
<script type="module" src="dist/wcc-card.js"></script>
|
|
908
|
+
<script type="module" src="dist/wcc-list.js"></script>
|
|
909
|
+
|
|
910
|
+
<!-- Props (attributes) -->
|
|
911
|
+
<wcc-counter count="0" label="Clicks"></wcc-counter>
|
|
912
|
+
|
|
913
|
+
<!-- Events -->
|
|
914
|
+
<script>
|
|
915
|
+
document.querySelector('wcc-counter')
|
|
916
|
+
.addEventListener('count-changed', (e) => console.log(e.detail))
|
|
917
|
+
</script>
|
|
918
|
+
|
|
919
|
+
<!-- Default slot -->
|
|
920
|
+
<wcc-card>
|
|
921
|
+
<p>Body content</p>
|
|
922
|
+
</wcc-card>
|
|
923
|
+
|
|
924
|
+
<!-- Named slots -->
|
|
925
|
+
<wcc-card>
|
|
926
|
+
<strong slot="header">Title</strong>
|
|
927
|
+
<p>Body</p>
|
|
928
|
+
<span slot="footer">Footer text</span>
|
|
929
|
+
</wcc-card>
|
|
930
|
+
|
|
931
|
+
<!-- Scoped slots -->
|
|
932
|
+
<wcc-list>
|
|
933
|
+
<template #item="{ item, index }">
|
|
934
|
+
<li>{{index}}: {{item}}</li>
|
|
935
|
+
</template>
|
|
936
|
+
</wcc-list>
|
|
937
|
+
```
|
|
938
|
+
|
|
939
|
+
### TypeScript Types for Frameworks
|
|
940
|
+
|
|
941
|
+
`wcc build` auto-generates typed stubs for each framework in the `dist/` folder:
|
|
942
|
+
|
|
943
|
+
```
|
|
944
|
+
dist/
|
|
945
|
+
├── wcc-vue.d.ts ← Vue/Volar prop autocompletion
|
|
946
|
+
├── wcc-vue.js ← Vue component stubs
|
|
947
|
+
├── wcc-react.d.ts ← React compound component types
|
|
948
|
+
├── wcc-react.js ← React component stubs
|
|
949
|
+
└── ...
|
|
950
|
+
```
|
|
951
|
+
|
|
952
|
+
**Vue (Volar autocompletion)**
|
|
953
|
+
|
|
954
|
+
Add `dist/wcc-vue.d.ts` to your tsconfig to get prop/event autocompletion in `.vue` templates:
|
|
955
|
+
|
|
956
|
+
```json
|
|
957
|
+
// tsconfig.json
|
|
958
|
+
{
|
|
959
|
+
"include": ["src/**/*", "dist/wcc-vue.d.ts"]
|
|
960
|
+
}
|
|
961
|
+
```
|
|
962
|
+
|
|
963
|
+
After this, Volar provides:
|
|
964
|
+
- Prop autocompletion: `<wcc-counter :la|` → suggests `label`
|
|
965
|
+
- Type-checking: `<wcc-counter :count="'string'">` → type error (expects number)
|
|
966
|
+
- Event types on hover
|
|
967
|
+
|
|
968
|
+
**React**
|
|
969
|
+
|
|
970
|
+
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.
|
|
971
|
+
|
|
972
|
+
**Angular**
|
|
973
|
+
|
|
974
|
+
Angular's `CUSTOM_ELEMENTS_SCHEMA` disables all type-checking on custom elements. No additional type setup possible from the library side.
|
|
975
|
+
|
|
976
|
+
## Editor Support
|
|
977
|
+
|
|
978
|
+
The **wcCompiler (.wcc) Language Support** extension is available on the VS Code Marketplace. It provides syntax highlighting, completions, and diagnostics for `.wcc` files.
|
|
979
|
+
|
|
980
|
+
## Runtime Helper
|
|
981
|
+
|
|
982
|
+
An optional `wcc-runtime.js` is copied to your output directory for declarative host-page bindings:
|
|
983
|
+
|
|
984
|
+
```html
|
|
985
|
+
<wcc-counter :count="count" @change="handleChange"></wcc-counter>
|
|
986
|
+
|
|
987
|
+
<script type="module">
|
|
988
|
+
import './dist/wcc-counter.js'
|
|
989
|
+
import { init, on, set, get } from './dist/wcc-runtime.js'
|
|
990
|
+
|
|
991
|
+
on('handleChange', (e) => set('count', e.detail))
|
|
992
|
+
init({ count: 0 })
|
|
993
|
+
</script>
|
|
994
|
+
```
|
|
995
|
+
|
|
996
|
+
## License
|
|
997
|
+
|
|
998
|
+
MIT
|