@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.
Files changed (3) hide show
  1. package/README.md +189 -7
  2. package/lib/codegen.js +43 -39
  3. 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 { WccTypescript } from './wcc-typescript.wcc'
399
- const child = templateRef<WccTypescript>('myRef')
400
- child.value!.handleUpdate() // ✅ typed
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, // dev server port
419
- input: 'src', // source directory
420
- output: 'dist' // output directory
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
- lines.push(' }');
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sprlab/wccompiler",
3
- "version": "0.6.1",
3
+ "version": "0.6.4",
4
4
  "description": "Zero-runtime compiler that transforms .wcc single-file components into native web components with signals-based reactivity",
5
5
  "type": "module",
6
6
  "bin": {