@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.
- package/README.md +27 -0
- package/lib/codegen.js +245 -101
- package/lib/compiler-browser.js +505 -0
- package/lib/compiler.js +79 -2
- package/lib/dev-server.js +55 -17
- package/lib/parser-extractors.js +1030 -0
- package/lib/parser.js +36 -929
- package/lib/reactive-runtime.js +35 -4
- package/lib/tree-walker.js +42 -3
- package/lib/types.js +33 -0
- package/package.json +1 -1
- package/types/wcc.d.ts +3 -2
package/lib/reactive-runtime.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Template string containing the mini reactive runtime
|
|
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
|
-
|
|
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
|
-
|
|
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
|
`;
|
package/lib/tree-walker.js
CHANGED
|
@@ -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
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
|
}
|