@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.
- package/README.md +102 -0
- package/bin/wcc.js +135 -0
- package/lib/codegen.js +324 -0
- package/lib/compiler.js +49 -0
- package/lib/config.js +61 -0
- package/lib/css-scoper.js +167 -0
- package/lib/dev-server.js +107 -0
- package/lib/parser.js +269 -0
- package/lib/printer.js +104 -0
- package/lib/reactive-runtime.js +61 -0
- package/lib/tree-walker.js +163 -0
- package/lib/wcc-runtime.js +42 -0
- package/package.json +42 -0
|
@@ -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
|
+
}
|