@sprlab/wccompiler 0.0.1

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.
@@ -0,0 +1,163 @@
1
+ /**
2
+ * Tree Walker for parsed templates.
3
+ *
4
+ * Walks a jsdom DOM tree to discover:
5
+ * - Text bindings {{var}} with childNodes[n] paths
6
+ * - Event bindings @event="handler"
7
+ * - Slots <slot> (named, default, with slotProps)
8
+ *
9
+ * Produces { bindings, events, slots } arrays with path metadata.
10
+ */
11
+
12
+ import { JSDOM } from 'jsdom';
13
+
14
+ /**
15
+ * Convert a path array to a JS expression string.
16
+ * e.g. pathExpr(['childNodes[0]', 'childNodes[1]'], '__root') => '__root.childNodes[0].childNodes[1]'
17
+ *
18
+ * @param {string[]} parts - Array of childNodes[n] path segments
19
+ * @param {string} rootVar - Root variable name
20
+ * @returns {string}
21
+ */
22
+ export function pathExpr(parts, rootVar) {
23
+ return parts.length === 0 ? rootVar : rootVar + '.' + parts.join('.');
24
+ }
25
+
26
+ /**
27
+ * Walk a DOM tree rooted at rootEl, discovering bindings, events, and slots.
28
+ *
29
+ * @param {Element} rootEl - jsdom DOM element (the parsed template root)
30
+ * @param {Set<string>} propsSet - Set of prop names
31
+ * @param {Set<string>} computedNames - Set of computed property names
32
+ * @param {Set<string>} rootVarNames - Set of root-level reactive variable names
33
+ * @returns {{ bindings: Binding[], events: EventBinding[], slots: SlotDef[] }}
34
+ */
35
+ export function walkTree(rootEl, propsSet, computedNames, rootVarNames) {
36
+ const bindings = [];
37
+ const events = [];
38
+ const slots = [];
39
+ let bindIdx = 0;
40
+ let eventIdx = 0;
41
+ let slotIdx = 0;
42
+
43
+ function bindingType(name) {
44
+ if (propsSet.has(name)) return 'prop';
45
+ if (computedNames.has(name)) return 'computed';
46
+ return 'internal';
47
+ }
48
+
49
+ function walk(node, pathParts) {
50
+ // --- Element node ---
51
+ if (node.nodeType === 1) {
52
+ // Detect <slot> elements
53
+ if (node.tagName === 'SLOT') {
54
+ const slotName = node.getAttribute('name') || '';
55
+ const varName = `__s${slotIdx++}`;
56
+ const defaultContent = node.innerHTML.trim();
57
+
58
+ // Collect slotProps (attributes starting with :)
59
+ const slotProps = [];
60
+ for (const attr of Array.from(node.attributes)) {
61
+ if (attr.name.startsWith(':')) {
62
+ slotProps.push({ prop: attr.name.slice(1), source: attr.value });
63
+ }
64
+ }
65
+
66
+ slots.push({
67
+ varName,
68
+ name: slotName,
69
+ path: [...pathParts],
70
+ defaultContent,
71
+ slotProps,
72
+ });
73
+
74
+ // Replace <slot> with <span data-slot="name">
75
+ const doc = node.ownerDocument;
76
+ const placeholder = doc.createElement('span');
77
+ placeholder.setAttribute('data-slot', slotName || 'default');
78
+ if (defaultContent) placeholder.innerHTML = defaultContent;
79
+ node.parentNode.replaceChild(placeholder, node);
80
+ return;
81
+ }
82
+
83
+ // Check for @event attributes
84
+ const attrsToRemove = [];
85
+ for (const attr of Array.from(node.attributes)) {
86
+ if (attr.name.startsWith('@')) {
87
+ const varName = `__e${eventIdx++}`;
88
+ events.push({
89
+ varName,
90
+ event: attr.name.slice(1),
91
+ handler: attr.value,
92
+ path: [...pathParts],
93
+ });
94
+ attrsToRemove.push(attr.name);
95
+ }
96
+ }
97
+ attrsToRemove.forEach((a) => node.removeAttribute(a));
98
+ }
99
+
100
+ // --- Text node with interpolations ---
101
+ if (node.nodeType === 3 && /\{\{\w+\}\}/.test(node.textContent)) {
102
+ const text = node.textContent;
103
+ const trimmed = text.trim();
104
+ const soleMatch = trimmed.match(/^\{\{(\w+)\}\}$/);
105
+ const parent = node.parentNode;
106
+
107
+ // Case 1: {{var}} is the sole content of the parent element
108
+ if (soleMatch && parent.childNodes.length === 1) {
109
+ const varName = `__b${bindIdx++}`;
110
+ bindings.push({
111
+ varName,
112
+ name: soleMatch[1],
113
+ type: bindingType(soleMatch[1]),
114
+ path: pathParts.slice(0, -1),
115
+ });
116
+ node.textContent = '';
117
+ return;
118
+ }
119
+
120
+ // Case 2: Mixed text and interpolations — split into spans
121
+ const doc = node.ownerDocument;
122
+ const fragment = doc.createDocumentFragment();
123
+ const parts = text.split(/(\{\{\w+\}\})/);
124
+ const parentPath = pathParts.slice(0, -1);
125
+
126
+ // Find the index of this text node among its siblings
127
+ let baseIndex = 0;
128
+ for (const child of parent.childNodes) {
129
+ if (child === node) break;
130
+ baseIndex++;
131
+ }
132
+
133
+ let offset = 0;
134
+ for (const part of parts) {
135
+ const bm = part.match(/^\{\{(\w+)\}\}$/);
136
+ if (bm) {
137
+ fragment.appendChild(doc.createElement('span'));
138
+ const varName = `__b${bindIdx++}`;
139
+ bindings.push({
140
+ varName,
141
+ name: bm[1],
142
+ type: bindingType(bm[1]),
143
+ path: [...parentPath, `childNodes[${baseIndex + offset}]`],
144
+ });
145
+ } else if (part) {
146
+ fragment.appendChild(doc.createTextNode(part));
147
+ }
148
+ offset++;
149
+ }
150
+ parent.replaceChild(fragment, node);
151
+ return;
152
+ }
153
+
154
+ // --- Recurse into children ---
155
+ const children = Array.from(node.childNodes);
156
+ for (let i = 0; i < children.length; i++) {
157
+ walk(children[i], [...pathParts, `childNodes[${i}]`]);
158
+ }
159
+ }
160
+
161
+ walk(rootEl, []);
162
+ return { bindings, events, slots };
163
+ }
@@ -0,0 +1,42 @@
1
+ /**
2
+ * wcc-runtime.js — Optional reactive binding helper for consuming wcc components.
3
+ * This is NOT required. Components are 100% native and work without it.
4
+ * It provides declarative :prop and @event bindings in HTML.
5
+ */
6
+ const state = {};
7
+ const listeners = [];
8
+ const handlers = {};
9
+
10
+ export function init(initialState) {
11
+ Object.assign(state, initialState);
12
+ document.querySelectorAll('*').forEach(el => {
13
+ for (const attr of Array.from(el.attributes)) {
14
+ if (attr.name.startsWith(':')) {
15
+ const prop = attr.name.slice(1);
16
+ const key = attr.value;
17
+ listeners.push({ key, update: (val) => { el[prop] = val; } });
18
+ if (key in state) el[prop] = state[key];
19
+ }
20
+ if (attr.name.startsWith('@')) {
21
+ const event = attr.name.slice(1);
22
+ const handlerName = attr.value;
23
+ el.addEventListener(event, (e) => {
24
+ if (handlers[handlerName]) handlers[handlerName](e);
25
+ });
26
+ }
27
+ }
28
+ });
29
+ }
30
+
31
+ export function set(key, value) {
32
+ state[key] = value;
33
+ listeners.filter(l => l.key === key).forEach(l => l.update(state[key]));
34
+ }
35
+
36
+ export function get(key) {
37
+ return state[key];
38
+ }
39
+
40
+ export function on(name, fn) {
41
+ handlers[name] = fn;
42
+ }
package/package.json ADDED
@@ -0,0 +1,42 @@
1
+ {
2
+ "name": "@sprlab/wccompiler",
3
+ "version": "0.0.1",
4
+ "description": "Zero-runtime compiler that transforms .html files with Vue-like syntax into 100% native web components",
5
+ "type": "module",
6
+ "bin": {
7
+ "wcc": "./bin/wcc.js"
8
+ },
9
+ "files": [
10
+ "bin/",
11
+ "lib/*.js",
12
+ "!lib/*.test.js",
13
+ "README.md"
14
+ ],
15
+ "scripts": {
16
+ "test": "vitest --run"
17
+ },
18
+ "keywords": [
19
+ "web-components",
20
+ "compiler",
21
+ "custom-elements",
22
+ "zero-runtime",
23
+ "native",
24
+ "vue-like"
25
+ ],
26
+ "license": "MIT",
27
+ "repository": {
28
+ "type": "git",
29
+ "url": ""
30
+ },
31
+ "dependencies": {
32
+ "jsdom": "^29.0.2"
33
+ },
34
+ "devDependencies": {
35
+ "fast-check": "^4.1.1",
36
+ "vitest": "^3.2.1"
37
+ },
38
+ "volta": {
39
+ "node": "24.0.0",
40
+ "yarn": "4.12.0"
41
+ }
42
+ }