@sprlab/wccompiler 0.6.1 → 0.6.4
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 +189 -7
- package/lib/codegen.js +43 -39
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -55,6 +55,23 @@ npx wcc build
|
|
|
55
55
|
|
|
56
56
|
The compiled output is a single `.js` file with zero dependencies — works in any browser that supports custom elements.
|
|
57
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
|
+
|
|
58
75
|
## Single File Component (.wcc)
|
|
59
76
|
|
|
60
77
|
wcCompiler uses a single-file component format with the `.wcc` extension. Each file contains three blocks:
|
|
@@ -85,6 +102,31 @@ p { color: steelblue; }
|
|
|
85
102
|
|
|
86
103
|
Use `<script lang="ts">` for TypeScript support. The CLI discovers and compiles all `.wcc` files in your source directory.
|
|
87
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
|
+
|
|
88
130
|
## Reactivity
|
|
89
131
|
|
|
90
132
|
### Signals
|
|
@@ -95,6 +137,8 @@ count() // read → 0
|
|
|
95
137
|
count.set(5) // write → 5
|
|
96
138
|
```
|
|
97
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
|
+
|
|
98
142
|
### Computed
|
|
99
143
|
|
|
100
144
|
```js
|
|
@@ -119,6 +163,23 @@ effect(() => {
|
|
|
119
163
|
})
|
|
120
164
|
```
|
|
121
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
|
+
|
|
122
183
|
### Watch
|
|
123
184
|
|
|
124
185
|
```js
|
|
@@ -235,7 +296,32 @@ Event handlers support expressions and inline arguments:
|
|
|
235
296
|
<li each="(item, index) in items()">{{index}}: {{item.name}}</li>
|
|
236
297
|
```
|
|
237
298
|
|
|
238
|
-
The source expression calls the signal (`items()`) to read the current array.
|
|
299
|
+
The source expression calls the signal (`items()`) to read the current array. Supports keyed rendering with `:key`:
|
|
300
|
+
|
|
301
|
+
```html
|
|
302
|
+
<li each="item in items()" :key="item.id">{{item.name}}</li>
|
|
303
|
+
```
|
|
304
|
+
|
|
305
|
+
Numeric ranges are also supported:
|
|
306
|
+
|
|
307
|
+
```html
|
|
308
|
+
<li each="n in 5">Item {{n}}</li>
|
|
309
|
+
```
|
|
310
|
+
|
|
311
|
+
#### Nested Directives in `each`
|
|
312
|
+
|
|
313
|
+
Directives work inside `each` blocks — including conditionals and nested loops:
|
|
314
|
+
|
|
315
|
+
```html
|
|
316
|
+
<div each="user in users()">
|
|
317
|
+
<span>{{user.name}}</span>
|
|
318
|
+
<span if="user.active" class="badge">Active</span>
|
|
319
|
+
<span else class="badge muted">Inactive</span>
|
|
320
|
+
<ul>
|
|
321
|
+
<li each="role in user.roles">{{role}}</li>
|
|
322
|
+
</ul>
|
|
323
|
+
</div>
|
|
324
|
+
```
|
|
239
325
|
|
|
240
326
|
### Visibility Toggle
|
|
241
327
|
|
|
@@ -313,6 +399,36 @@ Consumer (receives data via template props):
|
|
|
313
399
|
</wcc-card>
|
|
314
400
|
```
|
|
315
401
|
|
|
402
|
+
## Nested Components
|
|
403
|
+
|
|
404
|
+
Components can use other components in their templates. Import the child `.wcc` file and use its tag in the template:
|
|
405
|
+
|
|
406
|
+
```html
|
|
407
|
+
<script>
|
|
408
|
+
import { defineComponent, signal } from 'wcc'
|
|
409
|
+
import './wcc-badge.wcc'
|
|
410
|
+
|
|
411
|
+
export default defineComponent({ tag: 'wcc-profile' })
|
|
412
|
+
|
|
413
|
+
const count = signal(0)
|
|
414
|
+
|
|
415
|
+
function increment() {
|
|
416
|
+
count.set(count() + 1)
|
|
417
|
+
}
|
|
418
|
+
</script>
|
|
419
|
+
|
|
420
|
+
<template>
|
|
421
|
+
<div class="profile">
|
|
422
|
+
<wcc-badge :count="count()" @click="increment"></wcc-badge>
|
|
423
|
+
</div>
|
|
424
|
+
</template>
|
|
425
|
+
```
|
|
426
|
+
|
|
427
|
+
- **Manual import**: `import './wcc-child.wcc'` — the compiler registers the child component
|
|
428
|
+
- **Auto-detect**: If a custom element tag in the template matches a `.wcc` file in the same directory, it's auto-imported
|
|
429
|
+
- **Reactive props**: Use `:prop="expr"` to pass reactive data down — updates automatically when the expression changes
|
|
430
|
+
- **Event listening**: Use `@event="handler"` to listen to custom events emitted by the child
|
|
431
|
+
|
|
316
432
|
## Lifecycle Hooks
|
|
317
433
|
|
|
318
434
|
```js
|
|
@@ -332,6 +448,11 @@ onDestroy(() => {
|
|
|
332
448
|
|
|
333
449
|
Async callbacks are wrapped in an IIFE — `connectedCallback` itself stays synchronous.
|
|
334
450
|
|
|
451
|
+
**Details:**
|
|
452
|
+
- Multiple `onMount` / `onDestroy` calls are supported — they all run in declaration order
|
|
453
|
+
- `connectedCallback` is idempotent — re-mounting a component (e.g., moving it in the DOM) re-attaches listeners and effects cleanly
|
|
454
|
+
- All effects and event listeners are automatically cleaned up in `disconnectedCallback` via AbortController
|
|
455
|
+
|
|
335
456
|
## CSS Scoping
|
|
336
457
|
|
|
337
458
|
Styles are automatically scoped to the component using tag-name prefixing:
|
|
@@ -391,13 +512,45 @@ defineExpose({ doubled, handleUpdate, watchLog })
|
|
|
391
512
|
|
|
392
513
|
`defineExpose()` exposes methods and properties for external access via ref.
|
|
393
514
|
|
|
515
|
+
```js
|
|
516
|
+
// wcc-timer.wcc — exposes start/stop/elapsed
|
|
517
|
+
const elapsed = signal(0)
|
|
518
|
+
let interval = null
|
|
519
|
+
|
|
520
|
+
function start() { interval = setInterval(() => elapsed.set(elapsed() + 1), 1000) }
|
|
521
|
+
function stop() { clearInterval(interval) }
|
|
522
|
+
|
|
523
|
+
defineExpose({ elapsed, start, stop })
|
|
524
|
+
```
|
|
525
|
+
|
|
526
|
+
```html
|
|
527
|
+
<!-- Parent component accessing exposed API -->
|
|
528
|
+
<script>
|
|
529
|
+
import { defineComponent, templateRef, onMount } from 'wcc'
|
|
530
|
+
import './wcc-timer.wcc'
|
|
531
|
+
|
|
532
|
+
export default defineComponent({ tag: 'wcc-app' })
|
|
533
|
+
|
|
534
|
+
const timer = templateRef('timer')
|
|
535
|
+
|
|
536
|
+
onMount(() => {
|
|
537
|
+
timer.value.start() // call exposed method
|
|
538
|
+
console.log(timer.value.elapsed) // read exposed signal
|
|
539
|
+
})
|
|
540
|
+
</script>
|
|
541
|
+
|
|
542
|
+
<template>
|
|
543
|
+
<wcc-timer ref="timer"></wcc-timer>
|
|
544
|
+
</template>
|
|
545
|
+
```
|
|
546
|
+
|
|
394
547
|
The language server automatically generates a typed interface (PascalCase of the tag name) that can be imported by consumers:
|
|
395
548
|
|
|
396
549
|
```ts
|
|
397
550
|
// In the parent component:
|
|
398
|
-
import type {
|
|
399
|
-
const
|
|
400
|
-
|
|
551
|
+
import type { WccTimer } from './wcc-timer.wcc'
|
|
552
|
+
const timer = templateRef<WccTimer>('timer')
|
|
553
|
+
timer.value!.start() // ✅ typed
|
|
401
554
|
```
|
|
402
555
|
|
|
403
556
|
## CLI
|
|
@@ -415,14 +568,22 @@ Create `wcc.config.js` in your project root:
|
|
|
415
568
|
|
|
416
569
|
```js
|
|
417
570
|
export default {
|
|
418
|
-
port: 4100,
|
|
419
|
-
input: 'src',
|
|
420
|
-
output: 'dist'
|
|
571
|
+
port: 4100, // dev server port (default: 4100)
|
|
572
|
+
input: 'src', // source directory (default: 'src')
|
|
573
|
+
output: 'dist', // output directory (default: 'dist')
|
|
574
|
+
standalone: false // inline runtime per component (default: false)
|
|
421
575
|
}
|
|
422
576
|
```
|
|
423
577
|
|
|
424
578
|
All options are optional — defaults shown above.
|
|
425
579
|
|
|
580
|
+
| Option | Type | Default | Description |
|
|
581
|
+
|--------|------|---------|-------------|
|
|
582
|
+
| `port` | number | `4100` | Dev server port for `wcc dev` |
|
|
583
|
+
| `input` | string | `'src'` | Source directory containing `.wcc` files |
|
|
584
|
+
| `output` | string | `'dist'` | Output directory for compiled `.js` files |
|
|
585
|
+
| `standalone` | boolean | `false` | Inline reactive runtime in each component |
|
|
586
|
+
|
|
426
587
|
### Standalone Mode
|
|
427
588
|
|
|
428
589
|
Controls whether the reactive runtime is inlined in each component or imported from a shared module.
|
|
@@ -437,6 +598,23 @@ export default {
|
|
|
437
598
|
- `standalone: false` (default) — Components import the runtime from a shared `__wcc-signals.js` file. Smaller per-component size when using multiple components.
|
|
438
599
|
- `standalone: true` — Each component includes the full reactive runtime inline. Zero external dependencies per component.
|
|
439
600
|
|
|
601
|
+
**Output difference:**
|
|
602
|
+
|
|
603
|
+
```
|
|
604
|
+
Default (false): component.js → imports __wcc-signals.js
|
|
605
|
+
Standalone (true): component.js → runtime inlined, zero imports
|
|
606
|
+
```
|
|
607
|
+
|
|
608
|
+
**When to use standalone:**
|
|
609
|
+
- Publishing components as npm packages
|
|
610
|
+
- Embedding widgets in third-party sites
|
|
611
|
+
- CDN distribution (`<script src="component.js">`)
|
|
612
|
+
- Micro-frontends where you don't control the host
|
|
613
|
+
|
|
614
|
+
**When NOT to use standalone:**
|
|
615
|
+
- Apps with multiple components (runtime would be duplicated in each)
|
|
616
|
+
- Internal projects where you control the build
|
|
617
|
+
|
|
440
618
|
#### Per-Component Override
|
|
441
619
|
|
|
442
620
|
Override the global setting for individual components:
|
|
@@ -454,6 +632,10 @@ export default defineComponent({
|
|
|
454
632
|
|
|
455
633
|
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.
|
|
456
634
|
|
|
635
|
+
#### Reactive Scope Isolation
|
|
636
|
+
|
|
637
|
+
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`).
|
|
638
|
+
|
|
457
639
|
## Editor Support
|
|
458
640
|
|
|
459
641
|
The **wcCompiler (.wcc) Language Support** extension is available on the VS Code Marketplace. It provides syntax highlighting, completions, and diagnostics for `.wcc` files.
|
package/lib/codegen.js
CHANGED
|
@@ -904,10 +904,51 @@ export function generateComponent(parseResult, options = {}) {
|
|
|
904
904
|
lines.push('');
|
|
905
905
|
}
|
|
906
906
|
|
|
907
|
-
// Constructor
|
|
907
|
+
// Constructor — reactive state only (no DOM manipulation per Custom Elements spec)
|
|
908
908
|
lines.push(' constructor() {');
|
|
909
909
|
lines.push(' super();');
|
|
910
910
|
|
|
911
|
+
// Prop signal initialization (BEFORE user signals)
|
|
912
|
+
for (const p of propDefs) {
|
|
913
|
+
lines.push(` this._s_${p.name} = __signal(${p.default});`);
|
|
914
|
+
}
|
|
915
|
+
|
|
916
|
+
// Signal initialization
|
|
917
|
+
for (const s of signals) {
|
|
918
|
+
lines.push(` this._${s.name} = __signal(${s.value});`);
|
|
919
|
+
}
|
|
920
|
+
|
|
921
|
+
// Constant initialization
|
|
922
|
+
for (const c of constantVars) {
|
|
923
|
+
lines.push(` this._const_${c.name} = ${c.value};`);
|
|
924
|
+
}
|
|
925
|
+
|
|
926
|
+
// Computed initialization
|
|
927
|
+
for (const c of computeds) {
|
|
928
|
+
const body = transformExpr(c.body, signalNames, computedNames, propsObjectName, propNames, emitsObjectName, constantNames, methodNames);
|
|
929
|
+
lines.push(` this._c_${c.name} = __computed(() => ${body});`);
|
|
930
|
+
}
|
|
931
|
+
|
|
932
|
+
// Watcher prev-value initialization
|
|
933
|
+
for (let idx = 0; idx < watchers.length; idx++) {
|
|
934
|
+
const w = watchers[idx];
|
|
935
|
+
if (w.kind === 'signal') {
|
|
936
|
+
lines.push(` this.__prev_${w.target} = undefined;`);
|
|
937
|
+
} else {
|
|
938
|
+
lines.push(` this.__prev_watch${idx} = undefined;`);
|
|
939
|
+
}
|
|
940
|
+
}
|
|
941
|
+
|
|
942
|
+
lines.push(' }');
|
|
943
|
+
lines.push('');
|
|
944
|
+
|
|
945
|
+
// connectedCallback (idempotent — safe for re-mount)
|
|
946
|
+
lines.push(' connectedCallback() {');
|
|
947
|
+
lines.push(' if (this.__connected) return;');
|
|
948
|
+
lines.push(' this.__connected = true;');
|
|
949
|
+
|
|
950
|
+
// ── DOM SETUP (moved from constructor for Custom Elements spec compliance) ──
|
|
951
|
+
|
|
911
952
|
// Slot resolution: read childNodes BEFORE clearing innerHTML (when slots are present)
|
|
912
953
|
if (slots.length > 0) {
|
|
913
954
|
lines.push(' const __slotMap = {};');
|
|
@@ -973,37 +1014,6 @@ export function generateComponent(parseResult, options = {}) {
|
|
|
973
1014
|
}
|
|
974
1015
|
}
|
|
975
1016
|
|
|
976
|
-
// Prop signal initialization (BEFORE user signals)
|
|
977
|
-
for (const p of propDefs) {
|
|
978
|
-
lines.push(` this._s_${p.name} = __signal(${p.default});`);
|
|
979
|
-
}
|
|
980
|
-
|
|
981
|
-
// Signal initialization
|
|
982
|
-
for (const s of signals) {
|
|
983
|
-
lines.push(` this._${s.name} = __signal(${s.value});`);
|
|
984
|
-
}
|
|
985
|
-
|
|
986
|
-
// Constant initialization
|
|
987
|
-
for (const c of constantVars) {
|
|
988
|
-
lines.push(` this._const_${c.name} = ${c.value};`);
|
|
989
|
-
}
|
|
990
|
-
|
|
991
|
-
// Computed initialization
|
|
992
|
-
for (const c of computeds) {
|
|
993
|
-
const body = transformExpr(c.body, signalNames, computedNames, propsObjectName, propNames, emitsObjectName, constantNames, methodNames);
|
|
994
|
-
lines.push(` this._c_${c.name} = __computed(() => ${body});`);
|
|
995
|
-
}
|
|
996
|
-
|
|
997
|
-
// Watcher prev-value initialization
|
|
998
|
-
for (let idx = 0; idx < watchers.length; idx++) {
|
|
999
|
-
const w = watchers[idx];
|
|
1000
|
-
if (w.kind === 'signal') {
|
|
1001
|
-
lines.push(` this.__prev_${w.target} = undefined;`);
|
|
1002
|
-
} else {
|
|
1003
|
-
lines.push(` this.__prev_watch${idx} = undefined;`);
|
|
1004
|
-
}
|
|
1005
|
-
}
|
|
1006
|
-
|
|
1007
1017
|
// ── if: template creation, anchor reference, state init ──
|
|
1008
1018
|
for (const ifBlock of ifBlocks) {
|
|
1009
1019
|
const vn = ifBlock.varName;
|
|
@@ -1055,13 +1065,7 @@ export function generateComponent(parseResult, options = {}) {
|
|
|
1055
1065
|
}
|
|
1056
1066
|
}
|
|
1057
1067
|
|
|
1058
|
-
|
|
1059
|
-
lines.push('');
|
|
1060
|
-
|
|
1061
|
-
// connectedCallback (idempotent — safe for re-mount)
|
|
1062
|
-
lines.push(' connectedCallback() {');
|
|
1063
|
-
lines.push(' if (this.__connected) return;');
|
|
1064
|
-
lines.push(' this.__connected = true;');
|
|
1068
|
+
// ── EFFECTS AND LISTENERS ──
|
|
1065
1069
|
lines.push(' this.__ac = new AbortController();');
|
|
1066
1070
|
lines.push(' this.__disposers = [];');
|
|
1067
1071
|
lines.push('');
|
package/package.json
CHANGED