@sprlab/wccompiler 0.12.1 → 0.13.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/lib/codegen.js +46 -1
- package/lib/compiler.js +7 -1
- package/lib/template-normalizer.js +5 -0
- package/lib/tree-walker.js +91 -1
- package/lib/types.js +22 -0
- package/package.json +1 -1
package/lib/codegen.js
CHANGED
|
@@ -877,6 +877,7 @@ export function generateComponent(parseResult, options = {}) {
|
|
|
877
877
|
childImports = [],
|
|
878
878
|
exposeNames = [],
|
|
879
879
|
modelDefs = [],
|
|
880
|
+
dynamicComponents = [],
|
|
880
881
|
} = parseResult;
|
|
881
882
|
|
|
882
883
|
const signalNames = signals.map(s => s.name);
|
|
@@ -903,7 +904,7 @@ export function generateComponent(parseResult, options = {}) {
|
|
|
903
904
|
// ── 1. Reactive runtime (shared import or inline) ──
|
|
904
905
|
if (options.comments) lines.push('// ── Runtime ──────────────────────────────────────────');
|
|
905
906
|
// Determine which runtime functions this component needs
|
|
906
|
-
const needsEffect = effects.length > 0 || bindings.length > 0 || showBindings.length > 0 || modelBindings.length > 0 || modelPropBindings.length > 0 || attrBindings.length > 0 || ifBlocks.length > 0 || forBlocks.length > 0 || watchers.length > 0 || childComponents.length > 0 || slots.some(s => s.slotProps.length > 0);
|
|
907
|
+
const needsEffect = effects.length > 0 || bindings.length > 0 || showBindings.length > 0 || modelBindings.length > 0 || modelPropBindings.length > 0 || attrBindings.length > 0 || ifBlocks.length > 0 || forBlocks.length > 0 || watchers.length > 0 || childComponents.length > 0 || dynamicComponents.length > 0 || slots.some(s => s.slotProps.length > 0);
|
|
907
908
|
const needsComputed = computeds.length > 0;
|
|
908
909
|
const needsUntrack = watchers.length > 0;
|
|
909
910
|
|
|
@@ -1186,6 +1187,15 @@ export function generateComponent(parseResult, options = {}) {
|
|
|
1186
1187
|
lines.push(` this.${vn}_nodes = [];`);
|
|
1187
1188
|
}
|
|
1188
1189
|
|
|
1190
|
+
// ── dynamic component: anchor reference, state init ──
|
|
1191
|
+
for (const dyn of dynamicComponents) {
|
|
1192
|
+
const vn = dyn.varName;
|
|
1193
|
+
lines.push(` this.${vn}_anchor = ${pathExpr(dyn.anchorPath, '__root')};`);
|
|
1194
|
+
lines.push(` this.${vn}_current = null;`);
|
|
1195
|
+
lines.push(` this.${vn}_tag = null;`);
|
|
1196
|
+
lines.push(` this.${vn}_propDisposers = [];`);
|
|
1197
|
+
}
|
|
1198
|
+
|
|
1189
1199
|
// ── Ref DOM reference assignments (before appendChild moves nodes) ──
|
|
1190
1200
|
for (const rb of refBindings) {
|
|
1191
1201
|
lines.push(` this._ref_${rb.refName} = ${pathExpr(rb.path, '__root')};`);
|
|
@@ -1671,6 +1681,41 @@ export function generateComponent(parseResult, options = {}) {
|
|
|
1671
1681
|
}
|
|
1672
1682
|
}
|
|
1673
1683
|
|
|
1684
|
+
// ── dynamic component effects ──
|
|
1685
|
+
for (const dyn of dynamicComponents) {
|
|
1686
|
+
const vn = dyn.varName;
|
|
1687
|
+
const isExpr = transformExpr(dyn.isExpression, signalNames, computedNames, propsObjectName, propNames, emitsObjectName, constantNames, methodNames, modelVarMap);
|
|
1688
|
+
lines.push(' this.__disposers.push(__effect(() => {');
|
|
1689
|
+
lines.push(` const __tag = ${isExpr};`);
|
|
1690
|
+
lines.push(` if (__tag === this.${vn}_tag) return;`);
|
|
1691
|
+
lines.push(` if (this.${vn}_current) {`);
|
|
1692
|
+
lines.push(` this.${vn}_propDisposers.forEach(d => d());`);
|
|
1693
|
+
lines.push(` this.${vn}_propDisposers = [];`);
|
|
1694
|
+
lines.push(` this.${vn}_current.remove();`);
|
|
1695
|
+
lines.push(` this.${vn}_current = null;`);
|
|
1696
|
+
lines.push(' }');
|
|
1697
|
+
lines.push(' if (__tag) {');
|
|
1698
|
+
lines.push(' const el = document.createElement(__tag);');
|
|
1699
|
+
// Emit nested prop effects
|
|
1700
|
+
for (const prop of dyn.props) {
|
|
1701
|
+
const propExprTransformed = transformExpr(prop.expression, signalNames, computedNames, propsObjectName, propNames, emitsObjectName, constantNames, methodNames, modelVarMap);
|
|
1702
|
+
lines.push(` this.${vn}_propDisposers.push(__effect(() => {`);
|
|
1703
|
+
lines.push(` el.setAttribute('${prop.attr}', ${propExprTransformed});`);
|
|
1704
|
+
lines.push(' }));');
|
|
1705
|
+
}
|
|
1706
|
+
// Emit event listeners
|
|
1707
|
+
for (const evt of dyn.events) {
|
|
1708
|
+
const handlerExpr = generateEventHandler(evt.handler, signalNames, computedNames, propsObjectName, propNames, emitsObjectName, constantNames, modelVarMap);
|
|
1709
|
+
lines.push(` el.addEventListener('${evt.event}', ${handlerExpr});`);
|
|
1710
|
+
}
|
|
1711
|
+
lines.push(` this.${vn}_anchor.parentNode.insertBefore(el, this.${vn}_anchor);`);
|
|
1712
|
+
lines.push(' customElements.upgrade(el);');
|
|
1713
|
+
lines.push(` this.${vn}_current = el;`);
|
|
1714
|
+
lines.push(' }');
|
|
1715
|
+
lines.push(` this.${vn}_tag = __tag;`);
|
|
1716
|
+
lines.push(' }));');
|
|
1717
|
+
}
|
|
1718
|
+
|
|
1674
1719
|
// Lifecycle: onMount hooks (at the very end of connectedCallback)
|
|
1675
1720
|
for (const hook of onMountHooks) {
|
|
1676
1721
|
const body = transformMethodBody(hook.body, signalNames, computedNames, propsObjectName, propNames, emitsObjectName, refVarNames, constantNames, modelVarMap);
|
package/lib/compiler.js
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
import { parseHTML } from 'linkedom';
|
|
10
10
|
import { readFileSync } from 'node:fs';
|
|
11
11
|
import { basename } from 'node:path';
|
|
12
|
-
import { walkTree, processIfChains, processForBlocks, recomputeAnchorPath, detectRefs } from './tree-walker.js';
|
|
12
|
+
import { walkTree, processIfChains, processForBlocks, processDynamicComponents, recomputeAnchorPath, detectRefs } from './tree-walker.js';
|
|
13
13
|
import { generateComponent } from './codegen.js';
|
|
14
14
|
import { parseSFC } from './sfc-parser.js';
|
|
15
15
|
import {
|
|
@@ -290,6 +290,7 @@ async function compileSFC(filePath, config) {
|
|
|
290
290
|
childImports: [],
|
|
291
291
|
exposeNames,
|
|
292
292
|
modelDefs,
|
|
293
|
+
dynamicComponents: [],
|
|
293
294
|
};
|
|
294
295
|
|
|
295
296
|
// 16. Process template through linkedom → tree-walker → codegen
|
|
@@ -307,6 +308,7 @@ async function compileSFC(filePath, config) {
|
|
|
307
308
|
|
|
308
309
|
const forBlocks = processForBlocks(rootEl, [], signalNames, computedNames, propNamesSet);
|
|
309
310
|
const ifBlocks = processIfChains(rootEl, [], signalNames, computedNames, propNamesSet);
|
|
311
|
+
const dynamicComponents = processDynamicComponents(rootEl, []);
|
|
310
312
|
|
|
311
313
|
rootEl.normalize();
|
|
312
314
|
|
|
@@ -316,6 +318,9 @@ async function compileSFC(filePath, config) {
|
|
|
316
318
|
for (const ib of ifBlocks) {
|
|
317
319
|
ib.anchorPath = recomputeAnchorPath(rootEl, ib._anchorNode);
|
|
318
320
|
}
|
|
321
|
+
for (const dc of dynamicComponents) {
|
|
322
|
+
dc.anchorPath = recomputeAnchorPath(rootEl, dc._anchorNode);
|
|
323
|
+
}
|
|
319
324
|
|
|
320
325
|
const { bindings, events, showBindings, modelBindings, modelPropBindings, attrBindings, slots, childComponents } = walkTree(rootEl, signalNames, computedNames, propNamesSet);
|
|
321
326
|
|
|
@@ -412,6 +417,7 @@ async function compileSFC(filePath, config) {
|
|
|
412
417
|
parseResult.slots = slots;
|
|
413
418
|
parseResult.refBindings = refBindings;
|
|
414
419
|
parseResult.childComponents = childComponents;
|
|
420
|
+
parseResult.dynamicComponents = dynamicComponents;
|
|
415
421
|
|
|
416
422
|
parseResult.childImports = childImports;
|
|
417
423
|
parseResult.processedTemplate = rootEl.innerHTML;
|
|
@@ -71,6 +71,11 @@ export function normalizeTemplate(html, options) {
|
|
|
71
71
|
const TAG_RE = /<(\/?)([A-Za-z][\w-]*)((?:\s[^>]*?)?)(\/?)>/g;
|
|
72
72
|
|
|
73
73
|
return html.replace(TAG_RE, (match, closingSlash, tagName, attrs, selfClosing) => {
|
|
74
|
+
// Guard: preserve <component> tags as-is — this is a compiler directive, not a custom element
|
|
75
|
+
if (tagName.toLowerCase() === 'component') {
|
|
76
|
+
return match;
|
|
77
|
+
}
|
|
78
|
+
|
|
74
79
|
let normalizedTag = tagName;
|
|
75
80
|
|
|
76
81
|
// Step 1: Convert PascalCase to kebab-case
|
package/lib/tree-walker.js
CHANGED
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
import { parseHTML } from 'linkedom';
|
|
16
16
|
import { BOOLEAN_ATTRIBUTES } from './types.js';
|
|
17
17
|
|
|
18
|
-
/** @import { Binding, EventBinding, IfBlock, IfBranch, ShowBinding, AttrBinding, ForBlock, ModelBinding, ModelPropBinding, SlotBinding, SlotProp, RefBinding, ChildComponentBinding, ChildPropBinding } from './types.js' */
|
|
18
|
+
/** @import { Binding, EventBinding, IfBlock, IfBranch, ShowBinding, AttrBinding, ForBlock, ModelBinding, ModelPropBinding, SlotBinding, SlotProp, RefBinding, ChildComponentBinding, ChildPropBinding, DynamicComponentBinding, DynPropBinding, DynEventBinding } from './types.js' */
|
|
19
19
|
|
|
20
20
|
/**
|
|
21
21
|
* Walk a DOM tree rooted at rootEl, discovering bindings and events.
|
|
@@ -871,6 +871,96 @@ export function processForBlocks(parent, parentPath, signalNames, computedNames,
|
|
|
871
871
|
}
|
|
872
872
|
|
|
873
873
|
|
|
874
|
+
// ── Dynamic component processing ────────────────────────────────────
|
|
875
|
+
|
|
876
|
+
/**
|
|
877
|
+
* Process dynamic component elements (`<component :is="expr">`) in descendants of a parent element.
|
|
878
|
+
* Recursively detects `<component>` elements, validates the `:is` attribute,
|
|
879
|
+
* extracts prop/event bindings, and replaces them with comment anchors.
|
|
880
|
+
*
|
|
881
|
+
* @param {Element} parent - Root element to search
|
|
882
|
+
* @param {string[]} parentPath - DOM path to parent from __root
|
|
883
|
+
* @returns {DynamicComponentBinding[]}
|
|
884
|
+
*/
|
|
885
|
+
export function processDynamicComponents(parent, parentPath) {
|
|
886
|
+
/** @type {DynamicComponentBinding[]} */
|
|
887
|
+
const dynamicComponents = [];
|
|
888
|
+
let dynIdx = 0;
|
|
889
|
+
|
|
890
|
+
/**
|
|
891
|
+
* Recursively search for <component> elements in the subtree.
|
|
892
|
+
* @param {Element} node
|
|
893
|
+
* @param {string[]} currentPath
|
|
894
|
+
*/
|
|
895
|
+
function findDynamicComponents(node, currentPath) {
|
|
896
|
+
const children = Array.from(node.childNodes);
|
|
897
|
+
for (let i = 0; i < children.length; i++) {
|
|
898
|
+
const child = children[i];
|
|
899
|
+
if (child.nodeType !== 1) continue;
|
|
900
|
+
const el = /** @type {Element} */ (child);
|
|
901
|
+
|
|
902
|
+
if (el.tagName === 'COMPONENT') {
|
|
903
|
+
// Validate :is attribute is present
|
|
904
|
+
const isExpr = el.getAttribute(':is');
|
|
905
|
+
if (!isExpr) {
|
|
906
|
+
const error = new Error(':is attribute is required on <component> elements');
|
|
907
|
+
/** @ts-expect-error — custom error code */
|
|
908
|
+
error.code = 'MISSING_IS_ATTRIBUTE';
|
|
909
|
+
throw error;
|
|
910
|
+
}
|
|
911
|
+
|
|
912
|
+
// Collect prop bindings (:attr="expr", excluding :is)
|
|
913
|
+
/** @type {DynPropBinding[]} */
|
|
914
|
+
const props = [];
|
|
915
|
+
// Collect event bindings (@event="handler")
|
|
916
|
+
/** @type {DynEventBinding[]} */
|
|
917
|
+
const events = [];
|
|
918
|
+
|
|
919
|
+
for (const attr of Array.from(el.attributes)) {
|
|
920
|
+
if (attr.name.startsWith(':') && attr.name !== ':is') {
|
|
921
|
+
props.push({
|
|
922
|
+
attr: attr.name.slice(1),
|
|
923
|
+
expression: attr.value,
|
|
924
|
+
});
|
|
925
|
+
} else if (attr.name.startsWith('@')) {
|
|
926
|
+
events.push({
|
|
927
|
+
event: attr.name.slice(1),
|
|
928
|
+
handler: attr.value,
|
|
929
|
+
});
|
|
930
|
+
}
|
|
931
|
+
}
|
|
932
|
+
|
|
933
|
+
// Replace <component> with a comment node <!-- dynamic -->
|
|
934
|
+
const doc = node.ownerDocument;
|
|
935
|
+
const comment = doc.createComment(' dynamic ');
|
|
936
|
+
node.replaceChild(comment, el);
|
|
937
|
+
|
|
938
|
+
// Calculate anchorPath
|
|
939
|
+
const updatedChildren = Array.from(node.childNodes);
|
|
940
|
+
const commentIndex = updatedChildren.indexOf(comment);
|
|
941
|
+
const anchorPath = [...currentPath, `childNodes[${commentIndex}]`];
|
|
942
|
+
|
|
943
|
+
// Create DynamicComponentBinding
|
|
944
|
+
dynamicComponents.push({
|
|
945
|
+
varName: `__dyn${dynIdx++}`,
|
|
946
|
+
isExpression: isExpr,
|
|
947
|
+
props,
|
|
948
|
+
events,
|
|
949
|
+
anchorPath,
|
|
950
|
+
_anchorNode: comment,
|
|
951
|
+
});
|
|
952
|
+
} else {
|
|
953
|
+
// Recurse into non-component elements to find nested dynamic components
|
|
954
|
+
const childPath = [...currentPath, `childNodes[${i}]`];
|
|
955
|
+
findDynamicComponents(el, childPath);
|
|
956
|
+
}
|
|
957
|
+
}
|
|
958
|
+
}
|
|
959
|
+
|
|
960
|
+
findDynamicComponents(parent, parentPath);
|
|
961
|
+
return dynamicComponents;
|
|
962
|
+
}
|
|
963
|
+
|
|
874
964
|
// ── Ref detection ───────────────────────────────────────────────────
|
|
875
965
|
|
|
876
966
|
/**
|
package/lib/types.js
CHANGED
|
@@ -100,6 +100,7 @@
|
|
|
100
100
|
* @property {RefBinding[]} refBindings — ref attribute bindings from template (empty array if none)
|
|
101
101
|
* @property {ChildComponentBinding[]} childComponents — Child component bindings (empty array if none)
|
|
102
102
|
* @property {ChildComponentImport[]} childImports — Resolved child component imports (empty array if none)
|
|
103
|
+
* @property {DynamicComponentBinding[]} dynamicComponents — Dynamic component bindings (empty array if none)
|
|
103
104
|
* @property {string[]} exposeNames — Property names from defineExpose (empty array if none)
|
|
104
105
|
*/
|
|
105
106
|
|
|
@@ -227,6 +228,27 @@
|
|
|
227
228
|
* @property {boolean} sideEffect — true if this is a side-effect import (no identifier)
|
|
228
229
|
*/
|
|
229
230
|
|
|
231
|
+
/**
|
|
232
|
+
* @typedef {Object} DynPropBinding
|
|
233
|
+
* @property {string} attr — Attribute name (e.g., 'label', 'count')
|
|
234
|
+
* @property {string} expression — Expression string (e.g., "name()", "props.title")
|
|
235
|
+
*/
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* @typedef {Object} DynEventBinding
|
|
239
|
+
* @property {string} event — Event name (e.g., 'click', 'change')
|
|
240
|
+
* @property {string} handler — Handler expression (e.g., "handleClick", "handleClick($event)")
|
|
241
|
+
*/
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* @typedef {Object} DynamicComponentBinding
|
|
245
|
+
* @property {string} varName — Unique name: '__dyn0', '__dyn1', ...
|
|
246
|
+
* @property {string} isExpression — The :is attribute expression (e.g., "currentTag()")
|
|
247
|
+
* @property {DynPropBinding[]} props — Prop bindings from :attr="expr" attributes
|
|
248
|
+
* @property {DynEventBinding[]} events — Event bindings from @event="handler" attributes
|
|
249
|
+
* @property {string[]} anchorPath — DOM path to comment anchor from __root
|
|
250
|
+
*/
|
|
251
|
+
|
|
230
252
|
/**
|
|
231
253
|
* Set of HTML attributes that use property assignment instead of setAttribute.
|
|
232
254
|
* @type {Set<string>}
|
package/package.json
CHANGED