@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 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