@sprlab/wccompiler 0.13.0 → 0.15.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 +998 -998
- package/adapters/angular-compiled/angular.d.ts +197 -197
- package/adapters/angular-compiled/angular.mjs +488 -488
- package/adapters/angular.js +54 -54
- package/adapters/angular.ts +630 -630
- package/adapters/react.js +114 -114
- package/adapters/vue.js +103 -103
- package/bin/wcc.js +412 -412
- package/bin/wcc.test.js +126 -126
- package/integrations/angular.js +73 -73
- package/integrations/react.js +859 -859
- package/integrations/vue.js +253 -253
- package/lib/codegen.js +2078 -2074
- package/lib/compiler-browser.js +545 -545
- package/lib/compiler.js +483 -479
- package/lib/config.js +71 -71
- package/lib/css-scoper.js +180 -180
- package/lib/dev-server.js +193 -193
- package/lib/import-resolver.js +160 -160
- package/lib/parser-extractors.js +1240 -1169
- package/lib/parser.js +273 -269
- package/lib/reactive-runtime.js +143 -143
- package/lib/sfc-parser.js +333 -333
- package/lib/template-normalizer.js +114 -114
- package/lib/tree-walker.js +1013 -1013
- package/lib/types.js +262 -262
- package/lib/wcc-runtime.js +68 -68
- package/package.json +85 -85
- package/types/wcc.d.ts +28 -28
- package/types/wcc.test.js +46 -46
package/lib/compiler-browser.js
CHANGED
|
@@ -1,545 +1,545 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Browser Compiler — compiles web components from strings using native browser APIs.
|
|
3
|
-
*
|
|
4
|
-
* This is the browser-compatible entry point for wcCompiler.
|
|
5
|
-
* Uses DOMParser instead of jsdom, accepts strings instead of file paths.
|
|
6
|
-
* Reuses codegen and css-scoper directly. Reimplements the tree-walking
|
|
7
|
-
* pipeline using browser-native DOM APIs.
|
|
8
|
-
*
|
|
9
|
-
* Usage:
|
|
10
|
-
* import { compileFromStrings } from '@sprlab/wccompiler/browser'
|
|
11
|
-
*
|
|
12
|
-
* const js = await compileFromStrings({
|
|
13
|
-
* script: 'import { signal } from "wcc"\nconst count = signal(0)',
|
|
14
|
-
* template: '<div>{{count}}</div>',
|
|
15
|
-
* style: '.counter { display: flex; }',
|
|
16
|
-
* tag: 'wcc-counter',
|
|
17
|
-
* lang: 'ts',
|
|
18
|
-
* stripTypes: async (code) => esbuild.transform(code, { loader: 'ts' }).then(r => r.code)
|
|
19
|
-
* })
|
|
20
|
-
*/
|
|
21
|
-
|
|
22
|
-
import {
|
|
23
|
-
stripMacroImport,
|
|
24
|
-
toClassName,
|
|
25
|
-
camelToKebab,
|
|
26
|
-
extractPropsGeneric,
|
|
27
|
-
extractPropsArray,
|
|
28
|
-
extractPropsDefaults,
|
|
29
|
-
extractPropsObjectName,
|
|
30
|
-
extractEmitsFromCallSignatures,
|
|
31
|
-
extractEmits,
|
|
32
|
-
extractEmitsObjectName,
|
|
33
|
-
extractEmitsObjectNameFromGeneric,
|
|
34
|
-
extractSignals,
|
|
35
|
-
extractComputeds,
|
|
36
|
-
extractEffects,
|
|
37
|
-
extractWatchers,
|
|
38
|
-
extractFunctions,
|
|
39
|
-
extractLifecycleHooks,
|
|
40
|
-
extractRefs,
|
|
41
|
-
extractConstants,
|
|
42
|
-
} from './parser-extractors.js';
|
|
43
|
-
|
|
44
|
-
import { generateComponent } from './codegen.js';
|
|
45
|
-
import { BOOLEAN_ATTRIBUTES } from './types.js';
|
|
46
|
-
import { parseSFC } from './sfc-parser.js';
|
|
47
|
-
|
|
48
|
-
// ── Browser-compatible DOM helpers ──────────────────────────────────
|
|
49
|
-
|
|
50
|
-
/**
|
|
51
|
-
* Create a DOM root from HTML using the browser's DOMParser.
|
|
52
|
-
* @param {string} html
|
|
53
|
-
* @returns {Element}
|
|
54
|
-
*/
|
|
55
|
-
function createRoot(html) {
|
|
56
|
-
const doc = new DOMParser().parseFromString(
|
|
57
|
-
`<html><body><div id="__root">${html}</div></body></html>`,
|
|
58
|
-
'text/html'
|
|
59
|
-
);
|
|
60
|
-
return doc.getElementById('__root');
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
// ── Inline tree-walker (browser-compatible, no jsdom import) ────────
|
|
64
|
-
// These are copies of the tree-walker functions that use DOMParser
|
|
65
|
-
// instead of JSDOM for walkBranch. walkTree itself is DOM-agnostic.
|
|
66
|
-
|
|
67
|
-
function walkTree(rootEl, signalNames, computedNames, propNames = new Set()) {
|
|
68
|
-
const bindings = [];
|
|
69
|
-
const events = [];
|
|
70
|
-
const showBindings = [];
|
|
71
|
-
const modelBindings = [];
|
|
72
|
-
const modelPropBindings = [];
|
|
73
|
-
const attrBindings = [];
|
|
74
|
-
const slots = [];
|
|
75
|
-
const childComponents = [];
|
|
76
|
-
let bindIdx = 0, eventIdx = 0, showIdx = 0, modelIdx = 0, modelPropIdx = 0, attrIdx = 0, slotIdx = 0, childIdx = 0;
|
|
77
|
-
|
|
78
|
-
function bindingType(name) {
|
|
79
|
-
if (propNames.has(name)) return 'prop';
|
|
80
|
-
if (signalNames.has(name)) return 'signal';
|
|
81
|
-
if (computedNames.has(name)) return 'computed';
|
|
82
|
-
return 'method';
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
function walk(node, pathParts) {
|
|
86
|
-
if (node.nodeType === 1) {
|
|
87
|
-
const el = node;
|
|
88
|
-
|
|
89
|
-
if (el.tagName === 'SLOT') {
|
|
90
|
-
const slotName = el.getAttribute('name') || '';
|
|
91
|
-
const varName = `__s${slotIdx++}`;
|
|
92
|
-
const defaultContent = el.innerHTML.trim();
|
|
93
|
-
const slotProps = [];
|
|
94
|
-
for (const attr of Array.from(el.attributes)) {
|
|
95
|
-
if (attr.name.startsWith(':')) slotProps.push({ prop: attr.name.slice(1), source: attr.value });
|
|
96
|
-
}
|
|
97
|
-
slots.push({ varName, name: slotName, path: [...pathParts], defaultContent, slotProps });
|
|
98
|
-
const placeholder = el.ownerDocument.createElement('span');
|
|
99
|
-
placeholder.setAttribute('data-slot', slotName || 'default');
|
|
100
|
-
if (defaultContent) placeholder.innerHTML = defaultContent;
|
|
101
|
-
el.parentNode.replaceChild(placeholder, el);
|
|
102
|
-
return;
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
const tagLower = el.tagName.toLowerCase();
|
|
106
|
-
if (tagLower.includes('-') && tagLower !== rootEl.tagName?.toLowerCase()) {
|
|
107
|
-
const propBindings = [];
|
|
108
|
-
for (const attr of Array.from(el.attributes)) {
|
|
109
|
-
if (attr.name.startsWith('@') || attr.name.startsWith(':') || attr.name.startsWith('bind:') || attr.name.startsWith('model:')) continue;
|
|
110
|
-
if (['show', 'model', 'if', 'else-if', 'else', 'each', 'ref'].includes(attr.name)) continue;
|
|
111
|
-
const interpMatch = attr.value.match(/^\{\{([\w.]+)\}\}$/);
|
|
112
|
-
if (interpMatch) {
|
|
113
|
-
const expr = interpMatch[1];
|
|
114
|
-
propBindings.push({
|
|
115
|
-
attr: attr.name, expr,
|
|
116
|
-
type: propNames.has(expr) ? 'prop' : signalNames.has(expr) ? 'signal' : computedNames.has(expr) ? 'computed' : 'method',
|
|
117
|
-
});
|
|
118
|
-
el.setAttribute(attr.name, '');
|
|
119
|
-
}
|
|
120
|
-
}
|
|
121
|
-
if (propBindings.length > 0) {
|
|
122
|
-
childComponents.push({ tag: tagLower, varName: `__child${childIdx++}`, path: [...pathParts], propBindings });
|
|
123
|
-
}
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
const attrsToRemove = [];
|
|
127
|
-
for (const attr of Array.from(el.attributes)) {
|
|
128
|
-
if (attr.name.startsWith('@')) {
|
|
129
|
-
events.push({ varName: `__e${eventIdx++}`, event: attr.name.slice(1), handler: attr.value, path: [...pathParts] });
|
|
130
|
-
attrsToRemove.push(attr.name);
|
|
131
|
-
} else if (attr.name.startsWith(':') || attr.name.startsWith('bind:')) {
|
|
132
|
-
const attrName = attr.name.startsWith(':') ? attr.name.slice(1) : attr.name.slice(5);
|
|
133
|
-
let kind = 'attr';
|
|
134
|
-
if (attrName === 'class') kind = 'class';
|
|
135
|
-
else if (attrName === 'style') kind = 'style';
|
|
136
|
-
else if (BOOLEAN_ATTRIBUTES.has(attrName)) kind = 'bool';
|
|
137
|
-
attrBindings.push({ varName: `__attr${attrIdx++}`, attr: attrName, expression: attr.value, kind, path: [...pathParts] });
|
|
138
|
-
attrsToRemove.push(attr.name);
|
|
139
|
-
}
|
|
140
|
-
}
|
|
141
|
-
attrsToRemove.forEach(a => el.removeAttribute(a));
|
|
142
|
-
|
|
143
|
-
if (el.hasAttribute('show')) {
|
|
144
|
-
showBindings.push({ varName: `__show${showIdx++}`, expression: el.getAttribute('show'), path: [...pathParts] });
|
|
145
|
-
el.removeAttribute('show');
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
if (el.hasAttribute('model')) {
|
|
149
|
-
const signalName = el.getAttribute('model');
|
|
150
|
-
const tag = el.tagName.toLowerCase();
|
|
151
|
-
const type = el.getAttribute('type') || 'text';
|
|
152
|
-
let prop, event, coerce = false, radioValue = null;
|
|
153
|
-
if (tag === 'select') { prop = 'value'; event = 'change'; }
|
|
154
|
-
else if (tag === 'textarea') { prop = 'value'; event = 'input'; }
|
|
155
|
-
else if (type === 'checkbox') { prop = 'checked'; event = 'change'; }
|
|
156
|
-
else if (type === 'radio') { prop = 'checked'; event = 'change'; radioValue = el.getAttribute('value'); }
|
|
157
|
-
else if (type === 'number') { prop = 'value'; event = 'input'; coerce = true; }
|
|
158
|
-
else { prop = 'value'; event = 'input'; }
|
|
159
|
-
modelBindings.push({ varName: `__model${modelIdx++}`, signal: signalName, prop, event, coerce, radioValue, path: [...pathParts] });
|
|
160
|
-
el.removeAttribute('model');
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
// Detect model:propName="signalName" attributes (for custom element binding)
|
|
164
|
-
const modelPropAttrsToRemove = [];
|
|
165
|
-
for (const attr of Array.from(el.attributes)) {
|
|
166
|
-
if (attr.name.startsWith('model:')) {
|
|
167
|
-
const propName = attr.name.slice(6);
|
|
168
|
-
const signal = attr.value;
|
|
169
|
-
const tag = el.tagName.toLowerCase();
|
|
170
|
-
if (!tag.includes('-')) {
|
|
171
|
-
const error = new Error(`model:propName is only valid on custom elements (tag must contain a hyphen)`);
|
|
172
|
-
error.code = 'MODEL_PROP_INVALID_TARGET';
|
|
173
|
-
throw error;
|
|
174
|
-
}
|
|
175
|
-
modelPropBindings.push({ varName: `__modelProp${modelPropIdx++}`, propName, signal, path: [...pathParts] });
|
|
176
|
-
modelPropAttrsToRemove.push(attr.name);
|
|
177
|
-
}
|
|
178
|
-
}
|
|
179
|
-
modelPropAttrsToRemove.forEach(a => el.removeAttribute(a));
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
if (node.nodeType === 3 && /\{\{[\w.]+\}\}/.test(node.textContent)) {
|
|
183
|
-
const text = node.textContent;
|
|
184
|
-
const trimmed = text.trim();
|
|
185
|
-
const soleMatch = trimmed.match(/^\{\{([\w.]+)\}\}$/);
|
|
186
|
-
const parent = node.parentNode;
|
|
187
|
-
|
|
188
|
-
if (soleMatch && parent.childNodes.length === 1) {
|
|
189
|
-
bindings.push({ varName: `__b${bindIdx++}`, name: soleMatch[1], type: bindingType(soleMatch[1]), path: pathParts.slice(0, -1) });
|
|
190
|
-
parent.textContent = '';
|
|
191
|
-
return;
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
const doc = node.ownerDocument;
|
|
195
|
-
const fragment = doc.createDocumentFragment();
|
|
196
|
-
const parts = text.split(/(\{\{[\w.]+\}\})/);
|
|
197
|
-
const parentPath = pathParts.slice(0, -1);
|
|
198
|
-
let baseIndex = 0;
|
|
199
|
-
for (const child of parent.childNodes) { if (child === node) break; baseIndex++; }
|
|
200
|
-
let offset = 0;
|
|
201
|
-
for (const part of parts) {
|
|
202
|
-
const bm = part.match(/^\{\{([\w.]+)\}\}$/);
|
|
203
|
-
if (bm) {
|
|
204
|
-
fragment.appendChild(doc.createElement('span'));
|
|
205
|
-
bindings.push({ varName: `__b${bindIdx++}`, name: bm[1], type: bindingType(bm[1]), path: [...parentPath, `childNodes[${baseIndex + offset}]`] });
|
|
206
|
-
offset++;
|
|
207
|
-
} else if (part) {
|
|
208
|
-
fragment.appendChild(doc.createTextNode(part));
|
|
209
|
-
offset++;
|
|
210
|
-
}
|
|
211
|
-
}
|
|
212
|
-
parent.replaceChild(fragment, node);
|
|
213
|
-
return;
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
const children = Array.from(node.childNodes);
|
|
217
|
-
for (let i = 0; i < children.length; i++) {
|
|
218
|
-
walk(children[i], [...pathParts, `childNodes[${i}]`]);
|
|
219
|
-
}
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
walk(rootEl, []);
|
|
223
|
-
return { bindings, events, showBindings, modelBindings, modelPropBindings, attrBindings, slots, childComponents };
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
function recomputeAnchorPath(rootEl, targetNode) {
|
|
227
|
-
const segments = [];
|
|
228
|
-
let current = targetNode;
|
|
229
|
-
while (current && current !== rootEl) {
|
|
230
|
-
const parent = current.parentNode;
|
|
231
|
-
if (!parent) break;
|
|
232
|
-
const children = Array.from(parent.childNodes);
|
|
233
|
-
const idx = children.indexOf(current);
|
|
234
|
-
segments.unshift(`childNodes[${idx}]`);
|
|
235
|
-
current = parent;
|
|
236
|
-
}
|
|
237
|
-
return segments;
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
function walkBranch(html, signalNames, computedNames, propNames) {
|
|
241
|
-
const branchRoot = createRoot(html);
|
|
242
|
-
const result = walkTree(branchRoot, signalNames, computedNames, propNames);
|
|
243
|
-
const processedHtml = branchRoot.innerHTML;
|
|
244
|
-
|
|
245
|
-
function stripFirstSegment(items) {
|
|
246
|
-
for (const item of items) {
|
|
247
|
-
if (item.path && item.path.length > 0 && item.path[0].startsWith('childNodes[')) {
|
|
248
|
-
item.path = item.path.slice(1);
|
|
249
|
-
}
|
|
250
|
-
}
|
|
251
|
-
}
|
|
252
|
-
stripFirstSegment(result.bindings);
|
|
253
|
-
stripFirstSegment(result.events);
|
|
254
|
-
stripFirstSegment(result.showBindings);
|
|
255
|
-
stripFirstSegment(result.attrBindings);
|
|
256
|
-
stripFirstSegment(result.modelBindings);
|
|
257
|
-
stripFirstSegment(result.slots);
|
|
258
|
-
stripFirstSegment(result.childComponents);
|
|
259
|
-
|
|
260
|
-
return { ...result, processedHtml };
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
// Simplified processIfChains and processForBlocks for browser
|
|
264
|
-
// (same logic as tree-walker.js but using browser DOM)
|
|
265
|
-
|
|
266
|
-
function isChainPredecessor(el) {
|
|
267
|
-
return el.hasAttribute('if') || el.hasAttribute('else-if');
|
|
268
|
-
}
|
|
269
|
-
|
|
270
|
-
function processIfChains(parent, parentPath, signalNames, computedNames, propNames) {
|
|
271
|
-
const ifBlocks = [];
|
|
272
|
-
let ifIdx = 0;
|
|
273
|
-
|
|
274
|
-
function findIfChains(node, currentPath) {
|
|
275
|
-
const children = Array.from(node.childNodes);
|
|
276
|
-
let currentChain = null;
|
|
277
|
-
let prevElement = null;
|
|
278
|
-
|
|
279
|
-
for (const child of children) {
|
|
280
|
-
if (child.nodeType !== 1) continue;
|
|
281
|
-
const el = child;
|
|
282
|
-
const hasIf = el.hasAttribute('if');
|
|
283
|
-
const hasElseIf = el.hasAttribute('else-if');
|
|
284
|
-
const hasElse = el.hasAttribute('else');
|
|
285
|
-
|
|
286
|
-
if (hasIf) {
|
|
287
|
-
if (currentChain) {
|
|
288
|
-
ifBlocks.push(buildIfBlock(currentChain, node, currentPath, ifIdx++, signalNames, computedNames, propNames));
|
|
289
|
-
currentChain = null;
|
|
290
|
-
}
|
|
291
|
-
currentChain = { elements: [el], branches: [{ type: 'if', expression: el.getAttribute('if'), element: el }] };
|
|
292
|
-
} else if (hasElseIf && currentChain) {
|
|
293
|
-
currentChain.elements.push(el);
|
|
294
|
-
currentChain.branches.push({ type: 'else-if', expression: el.getAttribute('else-if'), element: el });
|
|
295
|
-
} else if (hasElse && currentChain) {
|
|
296
|
-
currentChain.elements.push(el);
|
|
297
|
-
currentChain.branches.push({ type: 'else', expression: null, element: el });
|
|
298
|
-
ifBlocks.push(buildIfBlock(currentChain, node, currentPath, ifIdx++, signalNames, computedNames, propNames));
|
|
299
|
-
currentChain = null;
|
|
300
|
-
} else {
|
|
301
|
-
if (currentChain) {
|
|
302
|
-
ifBlocks.push(buildIfBlock(currentChain, node, currentPath, ifIdx++, signalNames, computedNames, propNames));
|
|
303
|
-
currentChain = null;
|
|
304
|
-
}
|
|
305
|
-
const childIdx = Array.from(node.childNodes).indexOf(el);
|
|
306
|
-
findIfChains(el, [...currentPath, `childNodes[${childIdx}]`]);
|
|
307
|
-
}
|
|
308
|
-
prevElement = el;
|
|
309
|
-
}
|
|
310
|
-
if (currentChain) {
|
|
311
|
-
ifBlocks.push(buildIfBlock(currentChain, node, currentPath, ifIdx++, signalNames, computedNames, propNames));
|
|
312
|
-
}
|
|
313
|
-
}
|
|
314
|
-
|
|
315
|
-
findIfChains(parent, parentPath);
|
|
316
|
-
parent.normalize();
|
|
317
|
-
for (const ib of ifBlocks) ib.anchorPath = recomputeAnchorPath(parent, ib._anchorNode);
|
|
318
|
-
return ifBlocks;
|
|
319
|
-
}
|
|
320
|
-
|
|
321
|
-
function buildIfBlock(chain, parent, parentPath, idx, signalNames, computedNames, propNames) {
|
|
322
|
-
const doc = parent.ownerDocument;
|
|
323
|
-
const branches = chain.branches.map(branch => {
|
|
324
|
-
const clone = branch.element.cloneNode(true);
|
|
325
|
-
clone.removeAttribute('if');
|
|
326
|
-
clone.removeAttribute('else-if');
|
|
327
|
-
clone.removeAttribute('else');
|
|
328
|
-
const { bindings, events, showBindings, attrBindings, modelBindings, slots, processedHtml } = walkBranch(clone.outerHTML, signalNames, computedNames, propNames);
|
|
329
|
-
return { type: branch.type, expression: branch.expression, templateHtml: processedHtml, bindings, events, showBindings, attrBindings, modelBindings, slots };
|
|
330
|
-
});
|
|
331
|
-
|
|
332
|
-
const comment = doc.createComment(' if ');
|
|
333
|
-
parent.insertBefore(comment, chain.elements[0]);
|
|
334
|
-
for (const el of chain.elements) parent.removeChild(el);
|
|
335
|
-
|
|
336
|
-
const childNodes = Array.from(parent.childNodes);
|
|
337
|
-
const commentIndex = childNodes.indexOf(comment);
|
|
338
|
-
|
|
339
|
-
return { varName: `__if${idx}`, anchorPath: [...parentPath, `childNodes[${commentIndex}]`], _anchorNode: comment, branches };
|
|
340
|
-
}
|
|
341
|
-
|
|
342
|
-
function processForBlocks(parent, parentPath, signalNames, computedNames, propNames) {
|
|
343
|
-
const forBlocks = [];
|
|
344
|
-
let forIdx = 0;
|
|
345
|
-
const simpleRe = /^\s*(\w+)\s+in\s+(.+)\s*$/;
|
|
346
|
-
const destructuredRe = /^\s*\(\s*(\w+)\s*,\s*(\w+)\s*\)\s+in\s+(.+)\s*$/;
|
|
347
|
-
|
|
348
|
-
function parseEach(expr) {
|
|
349
|
-
const d = destructuredRe.exec(expr);
|
|
350
|
-
if (d) return { itemVar: d[1], indexVar: d[2], source: d[3].trim() };
|
|
351
|
-
const s = simpleRe.exec(expr);
|
|
352
|
-
if (s) return { itemVar: s[1], indexVar: null, source: s[2].trim() };
|
|
353
|
-
throw new Error('Invalid each expression: ' + expr);
|
|
354
|
-
}
|
|
355
|
-
|
|
356
|
-
function find(node, currentPath) {
|
|
357
|
-
const children = Array.from(node.childNodes);
|
|
358
|
-
for (let i = 0; i < children.length; i++) {
|
|
359
|
-
const child = children[i];
|
|
360
|
-
if (child.nodeType !== 1) continue;
|
|
361
|
-
const el = child;
|
|
362
|
-
if (el.hasAttribute('each')) {
|
|
363
|
-
const { itemVar, indexVar, source } = parseEach(el.getAttribute('each'));
|
|
364
|
-
const keyExpr = el.hasAttribute(':key') ? el.getAttribute(':key') : null;
|
|
365
|
-
const clone = el.cloneNode(true);
|
|
366
|
-
clone.removeAttribute('each');
|
|
367
|
-
clone.removeAttribute(':key');
|
|
368
|
-
const { bindings, events, showBindings, attrBindings, modelBindings, slots, processedHtml } = walkBranch(clone.outerHTML, signalNames, computedNames, propNames);
|
|
369
|
-
const doc = node.ownerDocument;
|
|
370
|
-
const comment = doc.createComment(' each ');
|
|
371
|
-
node.replaceChild(comment, el);
|
|
372
|
-
const updatedChildren = Array.from(node.childNodes);
|
|
373
|
-
const commentIndex = updatedChildren.indexOf(comment);
|
|
374
|
-
forBlocks.push({
|
|
375
|
-
varName: `__for${forIdx++}`, itemVar, indexVar, source, keyExpr,
|
|
376
|
-
templateHtml: processedHtml, anchorPath: [...currentPath, `childNodes[${commentIndex}]`],
|
|
377
|
-
_anchorNode: comment, bindings, events, showBindings, attrBindings, modelBindings, slots,
|
|
378
|
-
});
|
|
379
|
-
} else {
|
|
380
|
-
find(el, [...currentPath, `childNodes[${i}]`]);
|
|
381
|
-
}
|
|
382
|
-
}
|
|
383
|
-
}
|
|
384
|
-
|
|
385
|
-
find(parent, parentPath);
|
|
386
|
-
return forBlocks;
|
|
387
|
-
}
|
|
388
|
-
|
|
389
|
-
function detectRefs(rootEl) {
|
|
390
|
-
const refBindings = [];
|
|
391
|
-
const elements = rootEl.querySelectorAll('[ref]');
|
|
392
|
-
for (const el of elements) {
|
|
393
|
-
const refName = el.getAttribute('ref');
|
|
394
|
-
const path = [];
|
|
395
|
-
let current = el;
|
|
396
|
-
while (current && current !== rootEl) {
|
|
397
|
-
const parent = current.parentNode;
|
|
398
|
-
if (!parent) break;
|
|
399
|
-
const children = Array.from(parent.childNodes);
|
|
400
|
-
path.unshift(`childNodes[${children.indexOf(current)}`);
|
|
401
|
-
current = parent;
|
|
402
|
-
}
|
|
403
|
-
el.removeAttribute('ref');
|
|
404
|
-
refBindings.push({ refName, path });
|
|
405
|
-
}
|
|
406
|
-
return refBindings;
|
|
407
|
-
}
|
|
408
|
-
|
|
409
|
-
// ── Main compile function ───────────────────────────────────────────
|
|
410
|
-
|
|
411
|
-
/**
|
|
412
|
-
* @typedef {Object} CompileFromStringsOptions
|
|
413
|
-
* @property {string} script
|
|
414
|
-
* @property {string} template
|
|
415
|
-
* @property {string} [style]
|
|
416
|
-
* @property {string} tag
|
|
417
|
-
* @property {'ts'|'js'} [lang]
|
|
418
|
-
* @property {(code: string) => Promise<string>} [stripTypes]
|
|
419
|
-
*/
|
|
420
|
-
|
|
421
|
-
/**
|
|
422
|
-
* Compile a web component from source strings.
|
|
423
|
-
* Browser-compatible — uses DOMParser instead of jsdom.
|
|
424
|
-
*
|
|
425
|
-
* @param {CompileFromStringsOptions} options
|
|
426
|
-
* @returns {Promise<string>} Compiled JavaScript
|
|
427
|
-
*/
|
|
428
|
-
export async function compileFromStrings({ script, template, style = '', tag, lang = 'js', stripTypes }) {
|
|
429
|
-
const className = toClassName(tag);
|
|
430
|
-
|
|
431
|
-
// 1. Strip macro imports
|
|
432
|
-
let source = stripMacroImport(script);
|
|
433
|
-
|
|
434
|
-
// 2. Extract from generic form BEFORE type stripping
|
|
435
|
-
const propsFromGeneric = extractPropsGeneric(source);
|
|
436
|
-
const propsObjectNameFromGeneric = extractPropsObjectName(source);
|
|
437
|
-
const emitsFromCallSignatures = extractEmitsFromCallSignatures(source);
|
|
438
|
-
const emitsObjectNameGenericMatch = source.match(/(?:const|let|var)\s+([$\w]+)\s*=\s*defineEmits\s*<\s*\{/);
|
|
439
|
-
const emitsObjectNameFromGeneric = emitsObjectNameGenericMatch ? emitsObjectNameGenericMatch[1] : null;
|
|
440
|
-
|
|
441
|
-
// 3. Strip TypeScript types if needed
|
|
442
|
-
if (lang === 'ts' && stripTypes) {
|
|
443
|
-
source = await stripTypes(source);
|
|
444
|
-
}
|
|
445
|
-
|
|
446
|
-
// 4. Extract lifecycle hooks
|
|
447
|
-
const { onMountHooks, onDestroyHooks } = extractLifecycleHooks(source);
|
|
448
|
-
|
|
449
|
-
// 4b. Strip lifecycle + watch blocks
|
|
450
|
-
const hookLinePattern = /\bonMount\s*\(|\bonDestroy\s*\(|\bwatch\s*\(/;
|
|
451
|
-
const sourceLines = source.split('\n');
|
|
452
|
-
const filteredLines = [];
|
|
453
|
-
let skipDepth = 0, skipping = false;
|
|
454
|
-
for (const line of sourceLines) {
|
|
455
|
-
if (!skipping && hookLinePattern.test(line)) {
|
|
456
|
-
skipping = true; skipDepth = 0;
|
|
457
|
-
for (const ch of line) { if (ch === '{') skipDepth++; if (ch === '}') skipDepth--; }
|
|
458
|
-
if (skipDepth <= 0) skipping = false;
|
|
459
|
-
continue;
|
|
460
|
-
}
|
|
461
|
-
if (skipping) {
|
|
462
|
-
for (const ch of line) { if (ch === '{') skipDepth++; if (ch === '}') skipDepth--; }
|
|
463
|
-
if (skipDepth <= 0) skipping = false;
|
|
464
|
-
continue;
|
|
465
|
-
}
|
|
466
|
-
filteredLines.push(line);
|
|
467
|
-
}
|
|
468
|
-
const src = filteredLines.join('\n');
|
|
469
|
-
|
|
470
|
-
// 5. Extract declarations
|
|
471
|
-
const signals = extractSignals(src);
|
|
472
|
-
const computeds = extractComputeds(src);
|
|
473
|
-
const effects = extractEffects(src);
|
|
474
|
-
const watchers = extractWatchers(source);
|
|
475
|
-
const methods = extractFunctions(src);
|
|
476
|
-
const refs = extractRefs(src);
|
|
477
|
-
const constantVars = extractConstants(src);
|
|
478
|
-
|
|
479
|
-
// 6. Props
|
|
480
|
-
const propsFromArray = propsFromGeneric.length > 0 ? [] : extractPropsArray(source);
|
|
481
|
-
let propNames = propsFromGeneric.length > 0 ? propsFromGeneric : propsFromArray;
|
|
482
|
-
const propsDefaults = extractPropsDefaults(source);
|
|
483
|
-
if (propNames.length === 0 && Object.keys(propsDefaults).length > 0) propNames = Object.keys(propsDefaults);
|
|
484
|
-
const propsObjectName = propsObjectNameFromGeneric ?? extractPropsObjectName(source);
|
|
485
|
-
const propDefs = propNames.map(name => ({ name, default: propsDefaults[name] ?? 'undefined', attrName: camelToKebab(name) }));
|
|
486
|
-
|
|
487
|
-
// 7. Emits
|
|
488
|
-
const emitsFromArray = emitsFromCallSignatures.length > 0 ? [] : extractEmits(source);
|
|
489
|
-
const emitNames = emitsFromCallSignatures.length > 0 ? emitsFromCallSignatures : emitsFromArray;
|
|
490
|
-
const emitsObjectName = emitsObjectNameFromGeneric ?? extractEmitsObjectName(source);
|
|
491
|
-
|
|
492
|
-
// 8. Parse template
|
|
493
|
-
const rootEl = createRoot(template);
|
|
494
|
-
|
|
495
|
-
// 9. Name sets
|
|
496
|
-
const signalNameSet = new Set(signals.map(s => s.name));
|
|
497
|
-
const computedNameSet = new Set(computeds.map(c => c.name));
|
|
498
|
-
const propNameSet = new Set(propDefs.map(p => p.name));
|
|
499
|
-
|
|
500
|
-
// 10. Process directives
|
|
501
|
-
const forBlocks = processForBlocks(rootEl, [], signalNameSet, computedNameSet, propNameSet);
|
|
502
|
-
const ifBlocks = processIfChains(rootEl, [], signalNameSet, computedNameSet, propNameSet);
|
|
503
|
-
rootEl.normalize();
|
|
504
|
-
for (const fb of forBlocks) fb.anchorPath = recomputeAnchorPath(rootEl, fb._anchorNode);
|
|
505
|
-
for (const ib of ifBlocks) ib.anchorPath = recomputeAnchorPath(rootEl, ib._anchorNode);
|
|
506
|
-
|
|
507
|
-
// 11. Walk tree
|
|
508
|
-
const { bindings, events, showBindings, modelBindings, modelPropBindings, attrBindings, slots, childComponents } = walkTree(rootEl, signalNameSet, computedNameSet, propNameSet);
|
|
509
|
-
|
|
510
|
-
// 12. Detect refs
|
|
511
|
-
const refBindings = detectRefs(rootEl);
|
|
512
|
-
|
|
513
|
-
// 13. Generate
|
|
514
|
-
return generateComponent({
|
|
515
|
-
tagName: tag, className, template, style,
|
|
516
|
-
signals, computeds, effects, constantVars, watchers, methods,
|
|
517
|
-
propDefs, propsObjectName: propsObjectName ?? null,
|
|
518
|
-
emits: emitNames, emitsObjectName: emitsObjectName ?? null,
|
|
519
|
-
bindings, events, showBindings, modelBindings, attrBindings,
|
|
520
|
-
ifBlocks, forBlocks, slots, onMountHooks, onDestroyHooks,
|
|
521
|
-
refs, refBindings, childComponents, childImports: [],
|
|
522
|
-
processedTemplate: rootEl.innerHTML,
|
|
523
|
-
});
|
|
524
|
-
}
|
|
525
|
-
|
|
526
|
-
/**
|
|
527
|
-
* Compile an SFC component from a source string (browser-compatible).
|
|
528
|
-
* Parses the SFC to extract blocks, then delegates to compileFromStrings.
|
|
529
|
-
*
|
|
530
|
-
* @param {string} source — Full content of the .wcc file
|
|
531
|
-
* @param {{ stripTypes?: (code: string) => Promise<string> }} [options]
|
|
532
|
-
* @returns {Promise<string>} Compiled JavaScript
|
|
533
|
-
*/
|
|
534
|
-
export async function compileFromSFC(source, options) {
|
|
535
|
-
const descriptor = parseSFC(source);
|
|
536
|
-
return compileFromStrings({
|
|
537
|
-
script: descriptor.script,
|
|
538
|
-
template: descriptor.template,
|
|
539
|
-
style: descriptor.style,
|
|
540
|
-
tag: descriptor.tag,
|
|
541
|
-
lang: descriptor.lang,
|
|
542
|
-
stripTypes: options?.stripTypes,
|
|
543
|
-
});
|
|
544
|
-
}
|
|
545
|
-
|
|
1
|
+
/**
|
|
2
|
+
* Browser Compiler — compiles web components from strings using native browser APIs.
|
|
3
|
+
*
|
|
4
|
+
* This is the browser-compatible entry point for wcCompiler.
|
|
5
|
+
* Uses DOMParser instead of jsdom, accepts strings instead of file paths.
|
|
6
|
+
* Reuses codegen and css-scoper directly. Reimplements the tree-walking
|
|
7
|
+
* pipeline using browser-native DOM APIs.
|
|
8
|
+
*
|
|
9
|
+
* Usage:
|
|
10
|
+
* import { compileFromStrings } from '@sprlab/wccompiler/browser'
|
|
11
|
+
*
|
|
12
|
+
* const js = await compileFromStrings({
|
|
13
|
+
* script: 'import { signal } from "wcc"\nconst count = signal(0)',
|
|
14
|
+
* template: '<div>{{count}}</div>',
|
|
15
|
+
* style: '.counter { display: flex; }',
|
|
16
|
+
* tag: 'wcc-counter',
|
|
17
|
+
* lang: 'ts',
|
|
18
|
+
* stripTypes: async (code) => esbuild.transform(code, { loader: 'ts' }).then(r => r.code)
|
|
19
|
+
* })
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
import {
|
|
23
|
+
stripMacroImport,
|
|
24
|
+
toClassName,
|
|
25
|
+
camelToKebab,
|
|
26
|
+
extractPropsGeneric,
|
|
27
|
+
extractPropsArray,
|
|
28
|
+
extractPropsDefaults,
|
|
29
|
+
extractPropsObjectName,
|
|
30
|
+
extractEmitsFromCallSignatures,
|
|
31
|
+
extractEmits,
|
|
32
|
+
extractEmitsObjectName,
|
|
33
|
+
extractEmitsObjectNameFromGeneric,
|
|
34
|
+
extractSignals,
|
|
35
|
+
extractComputeds,
|
|
36
|
+
extractEffects,
|
|
37
|
+
extractWatchers,
|
|
38
|
+
extractFunctions,
|
|
39
|
+
extractLifecycleHooks,
|
|
40
|
+
extractRefs,
|
|
41
|
+
extractConstants,
|
|
42
|
+
} from './parser-extractors.js';
|
|
43
|
+
|
|
44
|
+
import { generateComponent } from './codegen.js';
|
|
45
|
+
import { BOOLEAN_ATTRIBUTES } from './types.js';
|
|
46
|
+
import { parseSFC } from './sfc-parser.js';
|
|
47
|
+
|
|
48
|
+
// ── Browser-compatible DOM helpers ──────────────────────────────────
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Create a DOM root from HTML using the browser's DOMParser.
|
|
52
|
+
* @param {string} html
|
|
53
|
+
* @returns {Element}
|
|
54
|
+
*/
|
|
55
|
+
function createRoot(html) {
|
|
56
|
+
const doc = new DOMParser().parseFromString(
|
|
57
|
+
`<html><body><div id="__root">${html}</div></body></html>`,
|
|
58
|
+
'text/html'
|
|
59
|
+
);
|
|
60
|
+
return doc.getElementById('__root');
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// ── Inline tree-walker (browser-compatible, no jsdom import) ────────
|
|
64
|
+
// These are copies of the tree-walker functions that use DOMParser
|
|
65
|
+
// instead of JSDOM for walkBranch. walkTree itself is DOM-agnostic.
|
|
66
|
+
|
|
67
|
+
function walkTree(rootEl, signalNames, computedNames, propNames = new Set()) {
|
|
68
|
+
const bindings = [];
|
|
69
|
+
const events = [];
|
|
70
|
+
const showBindings = [];
|
|
71
|
+
const modelBindings = [];
|
|
72
|
+
const modelPropBindings = [];
|
|
73
|
+
const attrBindings = [];
|
|
74
|
+
const slots = [];
|
|
75
|
+
const childComponents = [];
|
|
76
|
+
let bindIdx = 0, eventIdx = 0, showIdx = 0, modelIdx = 0, modelPropIdx = 0, attrIdx = 0, slotIdx = 0, childIdx = 0;
|
|
77
|
+
|
|
78
|
+
function bindingType(name) {
|
|
79
|
+
if (propNames.has(name)) return 'prop';
|
|
80
|
+
if (signalNames.has(name)) return 'signal';
|
|
81
|
+
if (computedNames.has(name)) return 'computed';
|
|
82
|
+
return 'method';
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function walk(node, pathParts) {
|
|
86
|
+
if (node.nodeType === 1) {
|
|
87
|
+
const el = node;
|
|
88
|
+
|
|
89
|
+
if (el.tagName === 'SLOT') {
|
|
90
|
+
const slotName = el.getAttribute('name') || '';
|
|
91
|
+
const varName = `__s${slotIdx++}`;
|
|
92
|
+
const defaultContent = el.innerHTML.trim();
|
|
93
|
+
const slotProps = [];
|
|
94
|
+
for (const attr of Array.from(el.attributes)) {
|
|
95
|
+
if (attr.name.startsWith(':')) slotProps.push({ prop: attr.name.slice(1), source: attr.value });
|
|
96
|
+
}
|
|
97
|
+
slots.push({ varName, name: slotName, path: [...pathParts], defaultContent, slotProps });
|
|
98
|
+
const placeholder = el.ownerDocument.createElement('span');
|
|
99
|
+
placeholder.setAttribute('data-slot', slotName || 'default');
|
|
100
|
+
if (defaultContent) placeholder.innerHTML = defaultContent;
|
|
101
|
+
el.parentNode.replaceChild(placeholder, el);
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const tagLower = el.tagName.toLowerCase();
|
|
106
|
+
if (tagLower.includes('-') && tagLower !== rootEl.tagName?.toLowerCase()) {
|
|
107
|
+
const propBindings = [];
|
|
108
|
+
for (const attr of Array.from(el.attributes)) {
|
|
109
|
+
if (attr.name.startsWith('@') || attr.name.startsWith(':') || attr.name.startsWith('bind:') || attr.name.startsWith('model:')) continue;
|
|
110
|
+
if (['show', 'model', 'if', 'else-if', 'else', 'each', 'ref'].includes(attr.name)) continue;
|
|
111
|
+
const interpMatch = attr.value.match(/^\{\{([\w.]+)\}\}$/);
|
|
112
|
+
if (interpMatch) {
|
|
113
|
+
const expr = interpMatch[1];
|
|
114
|
+
propBindings.push({
|
|
115
|
+
attr: attr.name, expr,
|
|
116
|
+
type: propNames.has(expr) ? 'prop' : signalNames.has(expr) ? 'signal' : computedNames.has(expr) ? 'computed' : 'method',
|
|
117
|
+
});
|
|
118
|
+
el.setAttribute(attr.name, '');
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
if (propBindings.length > 0) {
|
|
122
|
+
childComponents.push({ tag: tagLower, varName: `__child${childIdx++}`, path: [...pathParts], propBindings });
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const attrsToRemove = [];
|
|
127
|
+
for (const attr of Array.from(el.attributes)) {
|
|
128
|
+
if (attr.name.startsWith('@')) {
|
|
129
|
+
events.push({ varName: `__e${eventIdx++}`, event: attr.name.slice(1), handler: attr.value, path: [...pathParts] });
|
|
130
|
+
attrsToRemove.push(attr.name);
|
|
131
|
+
} else if (attr.name.startsWith(':') || attr.name.startsWith('bind:')) {
|
|
132
|
+
const attrName = attr.name.startsWith(':') ? attr.name.slice(1) : attr.name.slice(5);
|
|
133
|
+
let kind = 'attr';
|
|
134
|
+
if (attrName === 'class') kind = 'class';
|
|
135
|
+
else if (attrName === 'style') kind = 'style';
|
|
136
|
+
else if (BOOLEAN_ATTRIBUTES.has(attrName)) kind = 'bool';
|
|
137
|
+
attrBindings.push({ varName: `__attr${attrIdx++}`, attr: attrName, expression: attr.value, kind, path: [...pathParts] });
|
|
138
|
+
attrsToRemove.push(attr.name);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
attrsToRemove.forEach(a => el.removeAttribute(a));
|
|
142
|
+
|
|
143
|
+
if (el.hasAttribute('show')) {
|
|
144
|
+
showBindings.push({ varName: `__show${showIdx++}`, expression: el.getAttribute('show'), path: [...pathParts] });
|
|
145
|
+
el.removeAttribute('show');
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
if (el.hasAttribute('model')) {
|
|
149
|
+
const signalName = el.getAttribute('model');
|
|
150
|
+
const tag = el.tagName.toLowerCase();
|
|
151
|
+
const type = el.getAttribute('type') || 'text';
|
|
152
|
+
let prop, event, coerce = false, radioValue = null;
|
|
153
|
+
if (tag === 'select') { prop = 'value'; event = 'change'; }
|
|
154
|
+
else if (tag === 'textarea') { prop = 'value'; event = 'input'; }
|
|
155
|
+
else if (type === 'checkbox') { prop = 'checked'; event = 'change'; }
|
|
156
|
+
else if (type === 'radio') { prop = 'checked'; event = 'change'; radioValue = el.getAttribute('value'); }
|
|
157
|
+
else if (type === 'number') { prop = 'value'; event = 'input'; coerce = true; }
|
|
158
|
+
else { prop = 'value'; event = 'input'; }
|
|
159
|
+
modelBindings.push({ varName: `__model${modelIdx++}`, signal: signalName, prop, event, coerce, radioValue, path: [...pathParts] });
|
|
160
|
+
el.removeAttribute('model');
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// Detect model:propName="signalName" attributes (for custom element binding)
|
|
164
|
+
const modelPropAttrsToRemove = [];
|
|
165
|
+
for (const attr of Array.from(el.attributes)) {
|
|
166
|
+
if (attr.name.startsWith('model:')) {
|
|
167
|
+
const propName = attr.name.slice(6);
|
|
168
|
+
const signal = attr.value;
|
|
169
|
+
const tag = el.tagName.toLowerCase();
|
|
170
|
+
if (!tag.includes('-')) {
|
|
171
|
+
const error = new Error(`model:propName is only valid on custom elements (tag must contain a hyphen)`);
|
|
172
|
+
error.code = 'MODEL_PROP_INVALID_TARGET';
|
|
173
|
+
throw error;
|
|
174
|
+
}
|
|
175
|
+
modelPropBindings.push({ varName: `__modelProp${modelPropIdx++}`, propName, signal, path: [...pathParts] });
|
|
176
|
+
modelPropAttrsToRemove.push(attr.name);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
modelPropAttrsToRemove.forEach(a => el.removeAttribute(a));
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
if (node.nodeType === 3 && /\{\{[\w.]+\}\}/.test(node.textContent)) {
|
|
183
|
+
const text = node.textContent;
|
|
184
|
+
const trimmed = text.trim();
|
|
185
|
+
const soleMatch = trimmed.match(/^\{\{([\w.]+)\}\}$/);
|
|
186
|
+
const parent = node.parentNode;
|
|
187
|
+
|
|
188
|
+
if (soleMatch && parent.childNodes.length === 1) {
|
|
189
|
+
bindings.push({ varName: `__b${bindIdx++}`, name: soleMatch[1], type: bindingType(soleMatch[1]), path: pathParts.slice(0, -1) });
|
|
190
|
+
parent.textContent = '';
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
const doc = node.ownerDocument;
|
|
195
|
+
const fragment = doc.createDocumentFragment();
|
|
196
|
+
const parts = text.split(/(\{\{[\w.]+\}\})/);
|
|
197
|
+
const parentPath = pathParts.slice(0, -1);
|
|
198
|
+
let baseIndex = 0;
|
|
199
|
+
for (const child of parent.childNodes) { if (child === node) break; baseIndex++; }
|
|
200
|
+
let offset = 0;
|
|
201
|
+
for (const part of parts) {
|
|
202
|
+
const bm = part.match(/^\{\{([\w.]+)\}\}$/);
|
|
203
|
+
if (bm) {
|
|
204
|
+
fragment.appendChild(doc.createElement('span'));
|
|
205
|
+
bindings.push({ varName: `__b${bindIdx++}`, name: bm[1], type: bindingType(bm[1]), path: [...parentPath, `childNodes[${baseIndex + offset}]`] });
|
|
206
|
+
offset++;
|
|
207
|
+
} else if (part) {
|
|
208
|
+
fragment.appendChild(doc.createTextNode(part));
|
|
209
|
+
offset++;
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
parent.replaceChild(fragment, node);
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
const children = Array.from(node.childNodes);
|
|
217
|
+
for (let i = 0; i < children.length; i++) {
|
|
218
|
+
walk(children[i], [...pathParts, `childNodes[${i}]`]);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
walk(rootEl, []);
|
|
223
|
+
return { bindings, events, showBindings, modelBindings, modelPropBindings, attrBindings, slots, childComponents };
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
function recomputeAnchorPath(rootEl, targetNode) {
|
|
227
|
+
const segments = [];
|
|
228
|
+
let current = targetNode;
|
|
229
|
+
while (current && current !== rootEl) {
|
|
230
|
+
const parent = current.parentNode;
|
|
231
|
+
if (!parent) break;
|
|
232
|
+
const children = Array.from(parent.childNodes);
|
|
233
|
+
const idx = children.indexOf(current);
|
|
234
|
+
segments.unshift(`childNodes[${idx}]`);
|
|
235
|
+
current = parent;
|
|
236
|
+
}
|
|
237
|
+
return segments;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
function walkBranch(html, signalNames, computedNames, propNames) {
|
|
241
|
+
const branchRoot = createRoot(html);
|
|
242
|
+
const result = walkTree(branchRoot, signalNames, computedNames, propNames);
|
|
243
|
+
const processedHtml = branchRoot.innerHTML;
|
|
244
|
+
|
|
245
|
+
function stripFirstSegment(items) {
|
|
246
|
+
for (const item of items) {
|
|
247
|
+
if (item.path && item.path.length > 0 && item.path[0].startsWith('childNodes[')) {
|
|
248
|
+
item.path = item.path.slice(1);
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
stripFirstSegment(result.bindings);
|
|
253
|
+
stripFirstSegment(result.events);
|
|
254
|
+
stripFirstSegment(result.showBindings);
|
|
255
|
+
stripFirstSegment(result.attrBindings);
|
|
256
|
+
stripFirstSegment(result.modelBindings);
|
|
257
|
+
stripFirstSegment(result.slots);
|
|
258
|
+
stripFirstSegment(result.childComponents);
|
|
259
|
+
|
|
260
|
+
return { ...result, processedHtml };
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// Simplified processIfChains and processForBlocks for browser
|
|
264
|
+
// (same logic as tree-walker.js but using browser DOM)
|
|
265
|
+
|
|
266
|
+
function isChainPredecessor(el) {
|
|
267
|
+
return el.hasAttribute('if') || el.hasAttribute('else-if');
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
function processIfChains(parent, parentPath, signalNames, computedNames, propNames) {
|
|
271
|
+
const ifBlocks = [];
|
|
272
|
+
let ifIdx = 0;
|
|
273
|
+
|
|
274
|
+
function findIfChains(node, currentPath) {
|
|
275
|
+
const children = Array.from(node.childNodes);
|
|
276
|
+
let currentChain = null;
|
|
277
|
+
let prevElement = null;
|
|
278
|
+
|
|
279
|
+
for (const child of children) {
|
|
280
|
+
if (child.nodeType !== 1) continue;
|
|
281
|
+
const el = child;
|
|
282
|
+
const hasIf = el.hasAttribute('if');
|
|
283
|
+
const hasElseIf = el.hasAttribute('else-if');
|
|
284
|
+
const hasElse = el.hasAttribute('else');
|
|
285
|
+
|
|
286
|
+
if (hasIf) {
|
|
287
|
+
if (currentChain) {
|
|
288
|
+
ifBlocks.push(buildIfBlock(currentChain, node, currentPath, ifIdx++, signalNames, computedNames, propNames));
|
|
289
|
+
currentChain = null;
|
|
290
|
+
}
|
|
291
|
+
currentChain = { elements: [el], branches: [{ type: 'if', expression: el.getAttribute('if'), element: el }] };
|
|
292
|
+
} else if (hasElseIf && currentChain) {
|
|
293
|
+
currentChain.elements.push(el);
|
|
294
|
+
currentChain.branches.push({ type: 'else-if', expression: el.getAttribute('else-if'), element: el });
|
|
295
|
+
} else if (hasElse && currentChain) {
|
|
296
|
+
currentChain.elements.push(el);
|
|
297
|
+
currentChain.branches.push({ type: 'else', expression: null, element: el });
|
|
298
|
+
ifBlocks.push(buildIfBlock(currentChain, node, currentPath, ifIdx++, signalNames, computedNames, propNames));
|
|
299
|
+
currentChain = null;
|
|
300
|
+
} else {
|
|
301
|
+
if (currentChain) {
|
|
302
|
+
ifBlocks.push(buildIfBlock(currentChain, node, currentPath, ifIdx++, signalNames, computedNames, propNames));
|
|
303
|
+
currentChain = null;
|
|
304
|
+
}
|
|
305
|
+
const childIdx = Array.from(node.childNodes).indexOf(el);
|
|
306
|
+
findIfChains(el, [...currentPath, `childNodes[${childIdx}]`]);
|
|
307
|
+
}
|
|
308
|
+
prevElement = el;
|
|
309
|
+
}
|
|
310
|
+
if (currentChain) {
|
|
311
|
+
ifBlocks.push(buildIfBlock(currentChain, node, currentPath, ifIdx++, signalNames, computedNames, propNames));
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
findIfChains(parent, parentPath);
|
|
316
|
+
parent.normalize();
|
|
317
|
+
for (const ib of ifBlocks) ib.anchorPath = recomputeAnchorPath(parent, ib._anchorNode);
|
|
318
|
+
return ifBlocks;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
function buildIfBlock(chain, parent, parentPath, idx, signalNames, computedNames, propNames) {
|
|
322
|
+
const doc = parent.ownerDocument;
|
|
323
|
+
const branches = chain.branches.map(branch => {
|
|
324
|
+
const clone = branch.element.cloneNode(true);
|
|
325
|
+
clone.removeAttribute('if');
|
|
326
|
+
clone.removeAttribute('else-if');
|
|
327
|
+
clone.removeAttribute('else');
|
|
328
|
+
const { bindings, events, showBindings, attrBindings, modelBindings, slots, processedHtml } = walkBranch(clone.outerHTML, signalNames, computedNames, propNames);
|
|
329
|
+
return { type: branch.type, expression: branch.expression, templateHtml: processedHtml, bindings, events, showBindings, attrBindings, modelBindings, slots };
|
|
330
|
+
});
|
|
331
|
+
|
|
332
|
+
const comment = doc.createComment(' if ');
|
|
333
|
+
parent.insertBefore(comment, chain.elements[0]);
|
|
334
|
+
for (const el of chain.elements) parent.removeChild(el);
|
|
335
|
+
|
|
336
|
+
const childNodes = Array.from(parent.childNodes);
|
|
337
|
+
const commentIndex = childNodes.indexOf(comment);
|
|
338
|
+
|
|
339
|
+
return { varName: `__if${idx}`, anchorPath: [...parentPath, `childNodes[${commentIndex}]`], _anchorNode: comment, branches };
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
function processForBlocks(parent, parentPath, signalNames, computedNames, propNames) {
|
|
343
|
+
const forBlocks = [];
|
|
344
|
+
let forIdx = 0;
|
|
345
|
+
const simpleRe = /^\s*(\w+)\s+in\s+(.+)\s*$/;
|
|
346
|
+
const destructuredRe = /^\s*\(\s*(\w+)\s*,\s*(\w+)\s*\)\s+in\s+(.+)\s*$/;
|
|
347
|
+
|
|
348
|
+
function parseEach(expr) {
|
|
349
|
+
const d = destructuredRe.exec(expr);
|
|
350
|
+
if (d) return { itemVar: d[1], indexVar: d[2], source: d[3].trim() };
|
|
351
|
+
const s = simpleRe.exec(expr);
|
|
352
|
+
if (s) return { itemVar: s[1], indexVar: null, source: s[2].trim() };
|
|
353
|
+
throw new Error('Invalid each expression: ' + expr);
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
function find(node, currentPath) {
|
|
357
|
+
const children = Array.from(node.childNodes);
|
|
358
|
+
for (let i = 0; i < children.length; i++) {
|
|
359
|
+
const child = children[i];
|
|
360
|
+
if (child.nodeType !== 1) continue;
|
|
361
|
+
const el = child;
|
|
362
|
+
if (el.hasAttribute('each')) {
|
|
363
|
+
const { itemVar, indexVar, source } = parseEach(el.getAttribute('each'));
|
|
364
|
+
const keyExpr = el.hasAttribute(':key') ? el.getAttribute(':key') : null;
|
|
365
|
+
const clone = el.cloneNode(true);
|
|
366
|
+
clone.removeAttribute('each');
|
|
367
|
+
clone.removeAttribute(':key');
|
|
368
|
+
const { bindings, events, showBindings, attrBindings, modelBindings, slots, processedHtml } = walkBranch(clone.outerHTML, signalNames, computedNames, propNames);
|
|
369
|
+
const doc = node.ownerDocument;
|
|
370
|
+
const comment = doc.createComment(' each ');
|
|
371
|
+
node.replaceChild(comment, el);
|
|
372
|
+
const updatedChildren = Array.from(node.childNodes);
|
|
373
|
+
const commentIndex = updatedChildren.indexOf(comment);
|
|
374
|
+
forBlocks.push({
|
|
375
|
+
varName: `__for${forIdx++}`, itemVar, indexVar, source, keyExpr,
|
|
376
|
+
templateHtml: processedHtml, anchorPath: [...currentPath, `childNodes[${commentIndex}]`],
|
|
377
|
+
_anchorNode: comment, bindings, events, showBindings, attrBindings, modelBindings, slots,
|
|
378
|
+
});
|
|
379
|
+
} else {
|
|
380
|
+
find(el, [...currentPath, `childNodes[${i}]`]);
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
find(parent, parentPath);
|
|
386
|
+
return forBlocks;
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
function detectRefs(rootEl) {
|
|
390
|
+
const refBindings = [];
|
|
391
|
+
const elements = rootEl.querySelectorAll('[ref]');
|
|
392
|
+
for (const el of elements) {
|
|
393
|
+
const refName = el.getAttribute('ref');
|
|
394
|
+
const path = [];
|
|
395
|
+
let current = el;
|
|
396
|
+
while (current && current !== rootEl) {
|
|
397
|
+
const parent = current.parentNode;
|
|
398
|
+
if (!parent) break;
|
|
399
|
+
const children = Array.from(parent.childNodes);
|
|
400
|
+
path.unshift(`childNodes[${children.indexOf(current)}`);
|
|
401
|
+
current = parent;
|
|
402
|
+
}
|
|
403
|
+
el.removeAttribute('ref');
|
|
404
|
+
refBindings.push({ refName, path });
|
|
405
|
+
}
|
|
406
|
+
return refBindings;
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
// ── Main compile function ───────────────────────────────────────────
|
|
410
|
+
|
|
411
|
+
/**
|
|
412
|
+
* @typedef {Object} CompileFromStringsOptions
|
|
413
|
+
* @property {string} script
|
|
414
|
+
* @property {string} template
|
|
415
|
+
* @property {string} [style]
|
|
416
|
+
* @property {string} tag
|
|
417
|
+
* @property {'ts'|'js'} [lang]
|
|
418
|
+
* @property {(code: string) => Promise<string>} [stripTypes]
|
|
419
|
+
*/
|
|
420
|
+
|
|
421
|
+
/**
|
|
422
|
+
* Compile a web component from source strings.
|
|
423
|
+
* Browser-compatible — uses DOMParser instead of jsdom.
|
|
424
|
+
*
|
|
425
|
+
* @param {CompileFromStringsOptions} options
|
|
426
|
+
* @returns {Promise<string>} Compiled JavaScript
|
|
427
|
+
*/
|
|
428
|
+
export async function compileFromStrings({ script, template, style = '', tag, lang = 'js', stripTypes }) {
|
|
429
|
+
const className = toClassName(tag);
|
|
430
|
+
|
|
431
|
+
// 1. Strip macro imports
|
|
432
|
+
let source = stripMacroImport(script);
|
|
433
|
+
|
|
434
|
+
// 2. Extract from generic form BEFORE type stripping
|
|
435
|
+
const propsFromGeneric = extractPropsGeneric(source);
|
|
436
|
+
const propsObjectNameFromGeneric = extractPropsObjectName(source);
|
|
437
|
+
const emitsFromCallSignatures = extractEmitsFromCallSignatures(source);
|
|
438
|
+
const emitsObjectNameGenericMatch = source.match(/(?:const|let|var)\s+([$\w]+)\s*=\s*defineEmits\s*<\s*\{/);
|
|
439
|
+
const emitsObjectNameFromGeneric = emitsObjectNameGenericMatch ? emitsObjectNameGenericMatch[1] : null;
|
|
440
|
+
|
|
441
|
+
// 3. Strip TypeScript types if needed
|
|
442
|
+
if (lang === 'ts' && stripTypes) {
|
|
443
|
+
source = await stripTypes(source);
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
// 4. Extract lifecycle hooks
|
|
447
|
+
const { onMountHooks, onDestroyHooks } = extractLifecycleHooks(source);
|
|
448
|
+
|
|
449
|
+
// 4b. Strip lifecycle + watch blocks
|
|
450
|
+
const hookLinePattern = /\bonMount\s*\(|\bonDestroy\s*\(|\bwatch\s*\(/;
|
|
451
|
+
const sourceLines = source.split('\n');
|
|
452
|
+
const filteredLines = [];
|
|
453
|
+
let skipDepth = 0, skipping = false;
|
|
454
|
+
for (const line of sourceLines) {
|
|
455
|
+
if (!skipping && hookLinePattern.test(line)) {
|
|
456
|
+
skipping = true; skipDepth = 0;
|
|
457
|
+
for (const ch of line) { if (ch === '{') skipDepth++; if (ch === '}') skipDepth--; }
|
|
458
|
+
if (skipDepth <= 0) skipping = false;
|
|
459
|
+
continue;
|
|
460
|
+
}
|
|
461
|
+
if (skipping) {
|
|
462
|
+
for (const ch of line) { if (ch === '{') skipDepth++; if (ch === '}') skipDepth--; }
|
|
463
|
+
if (skipDepth <= 0) skipping = false;
|
|
464
|
+
continue;
|
|
465
|
+
}
|
|
466
|
+
filteredLines.push(line);
|
|
467
|
+
}
|
|
468
|
+
const src = filteredLines.join('\n');
|
|
469
|
+
|
|
470
|
+
// 5. Extract declarations
|
|
471
|
+
const signals = extractSignals(src);
|
|
472
|
+
const computeds = extractComputeds(src);
|
|
473
|
+
const effects = extractEffects(src);
|
|
474
|
+
const watchers = extractWatchers(source);
|
|
475
|
+
const methods = extractFunctions(src);
|
|
476
|
+
const refs = extractRefs(src);
|
|
477
|
+
const constantVars = extractConstants(src);
|
|
478
|
+
|
|
479
|
+
// 6. Props
|
|
480
|
+
const propsFromArray = propsFromGeneric.length > 0 ? [] : extractPropsArray(source);
|
|
481
|
+
let propNames = propsFromGeneric.length > 0 ? propsFromGeneric : propsFromArray;
|
|
482
|
+
const propsDefaults = extractPropsDefaults(source);
|
|
483
|
+
if (propNames.length === 0 && Object.keys(propsDefaults).length > 0) propNames = Object.keys(propsDefaults);
|
|
484
|
+
const propsObjectName = propsObjectNameFromGeneric ?? extractPropsObjectName(source);
|
|
485
|
+
const propDefs = propNames.map(name => ({ name, default: propsDefaults[name] ?? 'undefined', attrName: camelToKebab(name) }));
|
|
486
|
+
|
|
487
|
+
// 7. Emits
|
|
488
|
+
const emitsFromArray = emitsFromCallSignatures.length > 0 ? [] : extractEmits(source);
|
|
489
|
+
const emitNames = emitsFromCallSignatures.length > 0 ? emitsFromCallSignatures : emitsFromArray;
|
|
490
|
+
const emitsObjectName = emitsObjectNameFromGeneric ?? extractEmitsObjectName(source);
|
|
491
|
+
|
|
492
|
+
// 8. Parse template
|
|
493
|
+
const rootEl = createRoot(template);
|
|
494
|
+
|
|
495
|
+
// 9. Name sets
|
|
496
|
+
const signalNameSet = new Set(signals.map(s => s.name));
|
|
497
|
+
const computedNameSet = new Set(computeds.map(c => c.name));
|
|
498
|
+
const propNameSet = new Set(propDefs.map(p => p.name));
|
|
499
|
+
|
|
500
|
+
// 10. Process directives
|
|
501
|
+
const forBlocks = processForBlocks(rootEl, [], signalNameSet, computedNameSet, propNameSet);
|
|
502
|
+
const ifBlocks = processIfChains(rootEl, [], signalNameSet, computedNameSet, propNameSet);
|
|
503
|
+
rootEl.normalize();
|
|
504
|
+
for (const fb of forBlocks) fb.anchorPath = recomputeAnchorPath(rootEl, fb._anchorNode);
|
|
505
|
+
for (const ib of ifBlocks) ib.anchorPath = recomputeAnchorPath(rootEl, ib._anchorNode);
|
|
506
|
+
|
|
507
|
+
// 11. Walk tree
|
|
508
|
+
const { bindings, events, showBindings, modelBindings, modelPropBindings, attrBindings, slots, childComponents } = walkTree(rootEl, signalNameSet, computedNameSet, propNameSet);
|
|
509
|
+
|
|
510
|
+
// 12. Detect refs
|
|
511
|
+
const refBindings = detectRefs(rootEl);
|
|
512
|
+
|
|
513
|
+
// 13. Generate
|
|
514
|
+
return generateComponent({
|
|
515
|
+
tagName: tag, className, template, style,
|
|
516
|
+
signals, computeds, effects, constantVars, watchers, methods,
|
|
517
|
+
propDefs, propsObjectName: propsObjectName ?? null,
|
|
518
|
+
emits: emitNames, emitsObjectName: emitsObjectName ?? null,
|
|
519
|
+
bindings, events, showBindings, modelBindings, attrBindings,
|
|
520
|
+
ifBlocks, forBlocks, slots, onMountHooks, onDestroyHooks,
|
|
521
|
+
refs, refBindings, childComponents, childImports: [],
|
|
522
|
+
processedTemplate: rootEl.innerHTML,
|
|
523
|
+
});
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
/**
|
|
527
|
+
* Compile an SFC component from a source string (browser-compatible).
|
|
528
|
+
* Parses the SFC to extract blocks, then delegates to compileFromStrings.
|
|
529
|
+
*
|
|
530
|
+
* @param {string} source — Full content of the .wcc file
|
|
531
|
+
* @param {{ stripTypes?: (code: string) => Promise<string> }} [options]
|
|
532
|
+
* @returns {Promise<string>} Compiled JavaScript
|
|
533
|
+
*/
|
|
534
|
+
export async function compileFromSFC(source, options) {
|
|
535
|
+
const descriptor = parseSFC(source);
|
|
536
|
+
return compileFromStrings({
|
|
537
|
+
script: descriptor.script,
|
|
538
|
+
template: descriptor.template,
|
|
539
|
+
style: descriptor.style,
|
|
540
|
+
tag: descriptor.tag,
|
|
541
|
+
lang: descriptor.lang,
|
|
542
|
+
stripTypes: options?.stripTypes,
|
|
543
|
+
});
|
|
544
|
+
}
|
|
545
|
+
|