@sprlab/wccompiler 0.2.0 → 0.3.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.
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Template string containing the mini reactive runtime (~40 lines).
2
+ * Template string containing the mini reactive runtime.
3
3
  * This gets inlined at the top of each compiled component so the output
4
4
  * is fully self-contained with zero imports.
5
5
  *
@@ -7,12 +7,16 @@
7
7
  * - __signal(initialValue): getter/setter function with subscriber tracking
8
8
  * - __computed(fn): cached derived value that auto-invalidates
9
9
  * - __effect(fn): runs fn immediately and re-runs when dependencies change
10
+ * - __batch(fn): batch multiple signal writes, flush effects once at the end
10
11
  *
11
12
  * Dependency tracking uses a global stack (__currentEffect).
13
+ * Batching uses a depth counter — nested batches are supported.
12
14
  */
13
15
  /** @type {string} */
14
16
  export const reactiveRuntime = `
15
17
  let __currentEffect = null;
18
+ let __batchDepth = 0;
19
+ const __pendingEffects = [];
16
20
 
17
21
  function __signal(initial) {
18
22
  let _value = initial;
@@ -25,7 +29,13 @@ function __signal(initial) {
25
29
  const old = _value;
26
30
  _value = args[0];
27
31
  if (old !== _value) {
28
- for (const fn of [..._subs]) fn();
32
+ if (__batchDepth > 0) {
33
+ for (const fn of _subs) {
34
+ if (!__pendingEffects.includes(fn)) __pendingEffects.push(fn);
35
+ }
36
+ } else {
37
+ for (const fn of [..._subs]) fn();
38
+ }
29
39
  }
30
40
  };
31
41
  }
@@ -35,7 +45,13 @@ function __computed(fn) {
35
45
  const _subs = new Set();
36
46
  const recompute = () => {
37
47
  _dirty = true;
38
- for (const fn of [..._subs]) fn();
48
+ if (__batchDepth > 0) {
49
+ for (const fn of _subs) {
50
+ if (!__pendingEffects.includes(fn)) __pendingEffects.push(fn);
51
+ }
52
+ } else {
53
+ for (const fn of [..._subs]) fn();
54
+ }
39
55
  };
40
56
  return () => {
41
57
  if (__currentEffect) _subs.add(__currentEffect);
@@ -51,12 +67,27 @@ function __computed(fn) {
51
67
  }
52
68
 
53
69
  function __effect(fn) {
70
+ let _cleanup = null;
54
71
  const run = () => {
72
+ if (typeof _cleanup === 'function') _cleanup();
55
73
  const prev = __currentEffect;
56
74
  __currentEffect = run;
57
- fn();
75
+ _cleanup = fn();
58
76
  __currentEffect = prev;
59
77
  };
60
78
  run();
61
79
  }
80
+
81
+ function __batch(fn) {
82
+ __batchDepth++;
83
+ try {
84
+ fn();
85
+ } finally {
86
+ __batchDepth--;
87
+ if (__batchDepth === 0) {
88
+ const pending = __pendingEffects.splice(0);
89
+ for (const f of pending) f();
90
+ }
91
+ }
92
+ }
62
93
  `;
@@ -15,7 +15,7 @@
15
15
  import { JSDOM } from 'jsdom';
16
16
  import { BOOLEAN_ATTRIBUTES } from './types.js';
17
17
 
18
- /** @import { Binding, EventBinding, IfBlock, IfBranch, ShowBinding, AttrBinding, ForBlock, ModelBinding, SlotBinding, SlotProp, RefBinding } from './types.js' */
18
+ /** @import { Binding, EventBinding, IfBlock, IfBranch, ShowBinding, AttrBinding, ForBlock, ModelBinding, SlotBinding, SlotProp, RefBinding, ChildComponentBinding, ChildPropBinding } from './types.js' */
19
19
 
20
20
  /**
21
21
  * Walk a DOM tree rooted at rootEl, discovering bindings and events.
@@ -24,7 +24,7 @@ import { BOOLEAN_ATTRIBUTES } from './types.js';
24
24
  * @param {Set<string>} signalNames — Set of signal variable names
25
25
  * @param {Set<string>} computedNames — Set of computed variable names
26
26
  * @param {Set<string>} [propNames] — Set of prop names from defineProps
27
- * @returns {{ bindings: Binding[], events: EventBinding[], showBindings: ShowBinding[], modelBindings: ModelBinding[], attrBindings: AttrBinding[], slots: SlotBinding[] }}
27
+ * @returns {{ bindings: Binding[], events: EventBinding[], showBindings: ShowBinding[], modelBindings: ModelBinding[], attrBindings: AttrBinding[], slots: SlotBinding[], childComponents: ChildComponentBinding[] }}
28
28
  */
29
29
  export function walkTree(rootEl, signalNames, computedNames, propNames = new Set()) {
30
30
  /** @type {Binding[]} */
@@ -39,12 +39,15 @@ export function walkTree(rootEl, signalNames, computedNames, propNames = new Set
39
39
  const attrBindings = [];
40
40
  /** @type {SlotBinding[]} */
41
41
  const slots = [];
42
+ /** @type {ChildComponentBinding[]} */
43
+ const childComponents = [];
42
44
  let bindIdx = 0;
43
45
  let eventIdx = 0;
44
46
  let showIdx = 0;
45
47
  let modelIdx = 0;
46
48
  let attrIdx = 0;
47
49
  let slotIdx = 0;
50
+ let childIdx = 0;
48
51
 
49
52
  /**
50
53
  * Determine the binding type for a variable name.
@@ -103,6 +106,40 @@ export function walkTree(rootEl, signalNames, computedNames, propNames = new Set
103
106
  return; // Don't recurse into the replaced element
104
107
  }
105
108
 
109
+ // Detect child custom elements (tag name contains a hyphen)
110
+ const tagLower = el.tagName.toLowerCase();
111
+ if (tagLower.includes('-') && tagLower !== rootEl.tagName?.toLowerCase()) {
112
+ /** @type {ChildPropBinding[]} */
113
+ const propBindings = [];
114
+ for (const attr of Array.from(el.attributes)) {
115
+ // Skip directive attributes (@event, :bind, show, model, etc.)
116
+ if (attr.name.startsWith('@') || attr.name.startsWith(':') || attr.name.startsWith('bind:')) continue;
117
+ if (['show', 'model', 'if', 'else-if', 'else', 'each', 'ref'].includes(attr.name)) continue;
118
+
119
+ // Check for {{interpolation}} in attribute value
120
+ const interpMatch = attr.value.match(/^\{\{([\w.]+)\}\}$/);
121
+ if (interpMatch) {
122
+ const expr = interpMatch[1];
123
+ propBindings.push({
124
+ attr: attr.name,
125
+ expr,
126
+ type: propNames.has(expr) ? 'prop' : signalNames.has(expr) ? 'signal' : computedNames.has(expr) ? 'computed' : 'method',
127
+ });
128
+ // Clear the interpolation from the attribute — the effect sets it at runtime
129
+ el.setAttribute(attr.name, '');
130
+ }
131
+ }
132
+
133
+ if (propBindings.length > 0) {
134
+ childComponents.push({
135
+ tag: tagLower,
136
+ varName: `__child${childIdx++}`,
137
+ path: [...pathParts],
138
+ propBindings,
139
+ });
140
+ }
141
+ }
142
+
106
143
  // Check for @event attributes
107
144
  const attrsToRemove = [];
108
145
  for (const attr of Array.from(el.attributes)) {
@@ -265,7 +302,7 @@ export function walkTree(rootEl, signalNames, computedNames, propNames = new Set
265
302
  }
266
303
 
267
304
  walk(rootEl, []);
268
- return { bindings, events, showBindings, modelBindings, attrBindings, slots };
305
+ return { bindings, events, showBindings, modelBindings, attrBindings, slots, childComponents };
269
306
  }
270
307
 
271
308
  // ── Conditional chain processing (if / else-if / else) ──────────────
@@ -338,6 +375,7 @@ export function walkBranch(html, signalNames, computedNames, propNames) {
338
375
  stripFirstSegment(result.attrBindings);
339
376
  stripFirstSegment(result.modelBindings);
340
377
  stripFirstSegment(result.slots);
378
+ stripFirstSegment(result.childComponents);
341
379
 
342
380
  return {
343
381
  bindings: result.bindings,
@@ -346,6 +384,7 @@ export function walkBranch(html, signalNames, computedNames, propNames) {
346
384
  attrBindings: result.attrBindings,
347
385
  modelBindings: result.modelBindings,
348
386
  slots: result.slots,
387
+ childComponents: result.childComponents,
349
388
  processedHtml,
350
389
  };
351
390
  }
package/lib/types.js CHANGED
@@ -26,6 +26,15 @@
26
26
  /**
27
27
  * @typedef {Object} LifecycleHook
28
28
  * @property {string} body — The callback body (JavaScript code)
29
+ * @property {boolean} async — Whether the callback is async
30
+ */
31
+
32
+ /**
33
+ * @typedef {Object} WatcherDef
34
+ * @property {string} target — Signal/prop/computed name to watch (e.g., 'count')
35
+ * @property {string} newParam — Parameter name for new value (e.g., 'newVal')
36
+ * @property {string} oldParam — Parameter name for old value (e.g., 'oldVal')
37
+ * @property {string} body — Callback body
29
38
  */
30
39
 
31
40
  /**
@@ -68,6 +77,7 @@
68
77
  * @property {ComputedDef[]} computeds — computed() declarations
69
78
  * @property {EffectDef[]} effects — effect() declarations
70
79
  * @property {ConstantVar[]} constantVars — Plain const declarations (non-reactive)
80
+ * @property {WatcherDef[]} watchers — watch() declarations
71
81
  * @property {MethodDef[]} methods — function declarations
72
82
  * @property {PropDef[]} propDefs — Prop definitions with names and defaults
73
83
  * @property {string|null} propsObjectName — Variable name from `const X = defineProps(...)`
@@ -86,6 +96,8 @@
86
96
  * @property {SlotBinding[]} slots — Slot bindings (empty array if no slots)
87
97
  * @property {RefDeclaration[]} refs — templateRef declarations from script (empty array if none)
88
98
  * @property {RefBinding[]} refBindings — ref attribute bindings from template (empty array if none)
99
+ * @property {ChildComponentBinding[]} childComponents — Child component bindings (empty array if none)
100
+ * @property {ChildComponentImport[]} childImports — Resolved child component imports (empty array if none)
89
101
  */
90
102
 
91
103
  /**
@@ -179,6 +191,27 @@
179
191
  * @property {SlotProp[]} slotProps — Array of :prop="expr" bindings on the slot
180
192
  */
181
193
 
194
+ /**
195
+ * @typedef {Object} ChildPropBinding
196
+ * @property {string} attr — Attribute name on the child element (e.g., 'label')
197
+ * @property {string} expr — Expression from {{expr}} (e.g., 'role')
198
+ * @property {string} type — Binding source type: 'signal' | 'computed' | 'prop' | 'constant' | 'method'
199
+ */
200
+
201
+ /**
202
+ * @typedef {Object} ChildComponentBinding
203
+ * @property {string} tag — Child component tag name (e.g., 'wcc-badge')
204
+ * @property {string} varName — Internal ref name (e.g., '__child0')
205
+ * @property {string[]} path — DOM path from __root
206
+ * @property {ChildPropBinding[]} propBindings — Reactive attribute bindings
207
+ */
208
+
209
+ /**
210
+ * @typedef {Object} ChildComponentImport
211
+ * @property {string} tag — Child component tag name
212
+ * @property {string} importPath — Relative import path (e.g., './wcc-badge.js')
213
+ */
214
+
182
215
  /**
183
216
  * Set of HTML attributes that use property assignment instead of setAttribute.
184
217
  * @type {Set<string>}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sprlab/wccompiler",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "description": "Zero-runtime compiler that transforms .ts/.js component files into native web components with signals-based reactivity",
5
5
  "type": "module",
6
6
  "bin": {
package/types/wcc.d.ts CHANGED
@@ -7,6 +7,7 @@ declare module 'wcc' {
7
7
  export function signal<T>(value: T): Signal<T>;
8
8
  export function computed<T>(fn: () => T): () => T;
9
9
  export function effect(fn: () => void): void;
10
+ export function watch<T>(target: string, fn: (newVal: T, oldVal: T) => void): void;
10
11
  export function defineComponent(options: {
11
12
  tag: string;
12
13
  template: string;
@@ -21,7 +22,7 @@ declare module 'wcc' {
21
22
 
22
23
  export function templateRef(name: string): { value: HTMLElement | null };
23
24
 
24
- export function onMount(fn: () => void): void;
25
- export function onDestroy(fn: () => void): void;
25
+ export function onMount(fn: () => void | Promise<void>): void;
26
+ export function onDestroy(fn: () => void | Promise<void>): void;
26
27
  export function templateBindings(bindings: Record<string, any>): void;
27
28
  }