@sprlab/wccompiler 0.8.8 → 0.9.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/integrations/vue.js +57 -8
- package/lib/codegen.js +80 -3
- package/package.json +1 -1
package/integrations/vue.js
CHANGED
|
@@ -95,27 +95,76 @@ export function wccVuePlugin(options = {}) {
|
|
|
95
95
|
// → <div slot="name">content</div>
|
|
96
96
|
// This prevents Vue from intercepting the slot syntax and erroring.
|
|
97
97
|
// The WCC component's runtime slot parser detects slot="name" on regular elements.
|
|
98
|
+
//
|
|
99
|
+
// IMPORTANT: Only transform templates inside custom elements (tags with hyphens).
|
|
100
|
+
// This ensures we don't interfere with Vue's own slot/template handling on native elements.
|
|
98
101
|
|
|
99
|
-
//
|
|
102
|
+
// Helper: transform scoped slot content — escape {{prop}} → {%prop%} for declared props only
|
|
103
|
+
function transformScopedContent(content, propsExpr) {
|
|
104
|
+
const props = propsExpr.split(',').map(p => p.trim()).filter(Boolean)
|
|
105
|
+
let transformed = content
|
|
106
|
+
for (const prop of props) {
|
|
107
|
+
// Replace {{propName}} and {{ propName }} with {%propName%} / {% propName %}
|
|
108
|
+
transformed = transformed.replace(
|
|
109
|
+
new RegExp('\\{\\{(\\s*)' + prop + '(\\s*)\\}\\}', 'g'),
|
|
110
|
+
(m, ws1, ws2) => `{%${ws1}${prop}${ws2}%}`
|
|
111
|
+
)
|
|
112
|
+
}
|
|
113
|
+
return { transformed, props }
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Handle scoped slots: <template #name="{ prop1, prop2 }">...</template>
|
|
117
|
+
// → <div slot="name" slot-props="prop1, prop2">content with {%prop%}</div>
|
|
118
|
+
// Only inside custom elements (tag names with hyphens)
|
|
119
|
+
prev = ''
|
|
120
|
+
while (prev !== result) {
|
|
121
|
+
prev = result
|
|
122
|
+
result = result.replace(
|
|
123
|
+
/(<[\w]+-[\w-]*[^>]*>)([\s\S]*?)<template\s+#(\w+)="\{\s*([^}]*)\s*\}">([\s\S]*?)<\/template>/,
|
|
124
|
+
(match, openTag, before, slotName, propsExpr, content) => {
|
|
125
|
+
const { transformed, props } = transformScopedContent(content, propsExpr)
|
|
126
|
+
return `${openTag}${before}<div slot="${slotName}" slot-props="${props.join(', ')}">${transformed}</div>`
|
|
127
|
+
}
|
|
128
|
+
)
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Handle scoped slots: <template v-slot:name="{ prop1, prop2 }">...</template>
|
|
132
|
+
// → <div slot="name" slot-props="prop1, prop2">content with {%prop%}</div>
|
|
133
|
+
// Only inside custom elements (tag names with hyphens)
|
|
134
|
+
prev = ''
|
|
135
|
+
while (prev !== result) {
|
|
136
|
+
prev = result
|
|
137
|
+
result = result.replace(
|
|
138
|
+
/(<[\w]+-[\w-]*[^>]*>)([\s\S]*?)<template\s+v-slot:(\w+)="\{\s*([^}]*)\s*\}">([\s\S]*?)<\/template>/,
|
|
139
|
+
(match, openTag, before, slotName, propsExpr, content) => {
|
|
140
|
+
const { transformed, props } = transformScopedContent(content, propsExpr)
|
|
141
|
+
return `${openTag}${before}<div slot="${slotName}" slot-props="${props.join(', ')}">${transformed}</div>`
|
|
142
|
+
}
|
|
143
|
+
)
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// Handle non-scoped <template #name>...</template> (shorthand)
|
|
147
|
+
// Only inside custom elements (tag names with hyphens)
|
|
100
148
|
prev = ''
|
|
101
149
|
while (prev !== result) {
|
|
102
150
|
prev = result
|
|
103
151
|
result = result.replace(
|
|
104
|
-
|
|
105
|
-
(match, slotName, content) => {
|
|
106
|
-
return
|
|
152
|
+
/(<[\w]+-[\w-]*[^>]*>)([\s\S]*?)<template\s+#(\w+)>([\s\S]*?)<\/template>/,
|
|
153
|
+
(match, openTag, before, slotName, content) => {
|
|
154
|
+
return `${openTag}${before}<div slot="${slotName}">${content}</div>`
|
|
107
155
|
}
|
|
108
156
|
)
|
|
109
157
|
}
|
|
110
158
|
|
|
111
|
-
// Handle <template v-slot:name>...</template> (verbose)
|
|
159
|
+
// Handle non-scoped <template v-slot:name>...</template> (verbose)
|
|
160
|
+
// Only inside custom elements (tag names with hyphens)
|
|
112
161
|
prev = ''
|
|
113
162
|
while (prev !== result) {
|
|
114
163
|
prev = result
|
|
115
164
|
result = result.replace(
|
|
116
|
-
|
|
117
|
-
(match, slotName, content) => {
|
|
118
|
-
return
|
|
165
|
+
/(<[\w]+-[\w-]*[^>]*>)([\s\S]*?)<template\s+v-slot:(\w+)>([\s\S]*?)<\/template>/,
|
|
166
|
+
(match, openTag, before, slotName, content) => {
|
|
167
|
+
return `${openTag}${before}<div slot="${slotName}">${content}</div>`
|
|
119
168
|
}
|
|
120
169
|
)
|
|
121
170
|
}
|
package/lib/codegen.js
CHANGED
|
@@ -1042,9 +1042,23 @@ export function generateComponent(parseResult, options = {}) {
|
|
|
1042
1042
|
lines.push(" } else if (child.nodeType === 1 && child.getAttribute('slot')) {");
|
|
1043
1043
|
// NEW: regular element with slot="name" (cross-framework support)
|
|
1044
1044
|
lines.push(" const slotName = child.getAttribute('slot');");
|
|
1045
|
+
lines.push(" const propsExpr = child.getAttribute('slot-props') || '';");
|
|
1045
1046
|
lines.push(" child.removeAttribute('slot');");
|
|
1046
|
-
lines.push(
|
|
1047
|
-
lines.push("
|
|
1047
|
+
lines.push(" child.removeAttribute('slot-props');");
|
|
1048
|
+
lines.push(" __slotMap[slotName] = { content: propsExpr ? child.innerHTML : child.outerHTML, propsExpr };");
|
|
1049
|
+
lines.push(" } else if (child.nodeType === 1) {");
|
|
1050
|
+
// NEW: check for slot-template-<name> attributes (React/Angular string attribute pattern)
|
|
1051
|
+
lines.push(" for (const attr of Array.from(child.attributes)) {");
|
|
1052
|
+
lines.push(" if (attr.name.startsWith('slot-template-')) {");
|
|
1053
|
+
lines.push(" const slotName = attr.name.slice('slot-template-'.length);");
|
|
1054
|
+
lines.push(" if (!__slotMap[slotName]) {");
|
|
1055
|
+
lines.push(" __slotMap[slotName] = { content: attr.value, propsExpr: '' };");
|
|
1056
|
+
lines.push(" }");
|
|
1057
|
+
lines.push(" child.removeAttribute(attr.name);");
|
|
1058
|
+
lines.push(" }");
|
|
1059
|
+
lines.push(" }");
|
|
1060
|
+
lines.push(" __defaultSlotNodes.push(child);");
|
|
1061
|
+
lines.push(" } else if (child.nodeType === 3 && child.textContent.trim()) {");
|
|
1048
1062
|
lines.push(' __defaultSlotNodes.push(child);');
|
|
1049
1063
|
lines.push(' }');
|
|
1050
1064
|
lines.push(' }');
|
|
@@ -1153,6 +1167,68 @@ export function generateComponent(parseResult, options = {}) {
|
|
|
1153
1167
|
}
|
|
1154
1168
|
}
|
|
1155
1169
|
|
|
1170
|
+
// ── Deferred slot re-check (Angular compatibility) ──
|
|
1171
|
+
// Angular connects custom elements to DOM BEFORE projecting children.
|
|
1172
|
+
// If no slot content was found on first pass, schedule a microtask retry.
|
|
1173
|
+
// We save a reference to the rendered root node so the microtask can filter it out
|
|
1174
|
+
// and only process children that were projected by the framework after connectedCallback.
|
|
1175
|
+
if (slots.length > 0) {
|
|
1176
|
+
lines.push(' if (Object.keys(__slotMap).length === 0 && __defaultSlotNodes.length === 0) {');
|
|
1177
|
+
lines.push(' const __renderedRoot = this.firstElementChild;');
|
|
1178
|
+
lines.push(' queueMicrotask(() => {');
|
|
1179
|
+
lines.push(' const __sm = {};');
|
|
1180
|
+
lines.push(' const __dn = [];');
|
|
1181
|
+
lines.push(' for (const child of Array.from(this.childNodes)) {');
|
|
1182
|
+
// Skip the rendered template root and any whitespace text nodes that were there before
|
|
1183
|
+
lines.push(' if (child === __renderedRoot) continue;');
|
|
1184
|
+
lines.push(' if (child.nodeType === 3 && !child.textContent.trim()) continue;');
|
|
1185
|
+
lines.push(" if (child.nodeName === 'TEMPLATE') {");
|
|
1186
|
+
lines.push(' for (const attr of child.attributes) {');
|
|
1187
|
+
lines.push(" if (attr.name.startsWith('#')) {");
|
|
1188
|
+
lines.push(" __sm[attr.name.slice(1)] = { content: child.innerHTML, propsExpr: attr.value };");
|
|
1189
|
+
lines.push(' }');
|
|
1190
|
+
lines.push(' }');
|
|
1191
|
+
lines.push(" } else if (child.nodeType === 1 && child.getAttribute('slot')) {");
|
|
1192
|
+
lines.push(" const sn = child.getAttribute('slot');");
|
|
1193
|
+
lines.push(" const pe = child.getAttribute('slot-props') || '';");
|
|
1194
|
+
lines.push(" child.removeAttribute('slot');");
|
|
1195
|
+
lines.push(" child.removeAttribute('slot-props');");
|
|
1196
|
+
lines.push(" __sm[sn] = { content: pe ? child.innerHTML : child.outerHTML, propsExpr: pe };");
|
|
1197
|
+
lines.push(" child.remove();");
|
|
1198
|
+
lines.push(" } else if (child.nodeType === 1) {");
|
|
1199
|
+
lines.push(" for (const attr of Array.from(child.attributes)) {");
|
|
1200
|
+
lines.push(" if (attr.name.startsWith('slot-template-')) {");
|
|
1201
|
+
lines.push(" const sn = attr.name.slice('slot-template-'.length);");
|
|
1202
|
+
lines.push(" if (!__sm[sn]) { __sm[sn] = { content: attr.value, propsExpr: '' }; }");
|
|
1203
|
+
lines.push(" child.removeAttribute(attr.name);");
|
|
1204
|
+
lines.push(" }");
|
|
1205
|
+
lines.push(" }");
|
|
1206
|
+
lines.push(" __dn.push(child);");
|
|
1207
|
+
lines.push(" } else if (child.nodeType === 3 && child.textContent.trim()) {");
|
|
1208
|
+
lines.push(" __dn.push(child);");
|
|
1209
|
+
lines.push(' }');
|
|
1210
|
+
lines.push(' }');
|
|
1211
|
+
// Re-inject slots if we found content this time
|
|
1212
|
+
lines.push(' if (Object.keys(__sm).length > 0 || __dn.length > 0) {');
|
|
1213
|
+
for (const s of slots) {
|
|
1214
|
+
if (s.name && s.slotProps.length > 0) {
|
|
1215
|
+
lines.push(` if (__sm['${s.name}']) {`);
|
|
1216
|
+
lines.push(` this.__slotTpl_${s.name} = __sm['${s.name}'].content;`);
|
|
1217
|
+
if (s.slotProps.length > 0 && s.slotProps[0].source) {
|
|
1218
|
+
lines.push(` this._${s.slotProps[0].source}.set(this._${s.slotProps[0].source}());`);
|
|
1219
|
+
}
|
|
1220
|
+
lines.push(` }`);
|
|
1221
|
+
} else if (s.name) {
|
|
1222
|
+
lines.push(` if (__sm['${s.name}']) { this.${s.varName}.innerHTML = __sm['${s.name}'].content; }`);
|
|
1223
|
+
} else {
|
|
1224
|
+
lines.push(` if (__dn.length) { this.${s.varName}.textContent = ''; __dn.forEach(n => this.${s.varName}.appendChild(n.cloneNode(true))); }`);
|
|
1225
|
+
}
|
|
1226
|
+
}
|
|
1227
|
+
lines.push(' }');
|
|
1228
|
+
lines.push(' });');
|
|
1229
|
+
lines.push(' }');
|
|
1230
|
+
}
|
|
1231
|
+
|
|
1156
1232
|
// ── EFFECTS AND LISTENERS ──
|
|
1157
1233
|
lines.push(' this.__ac = new AbortController();');
|
|
1158
1234
|
lines.push(' this.__disposers = [];');
|
|
@@ -1203,7 +1279,7 @@ export function generateComponent(parseResult, options = {}) {
|
|
|
1203
1279
|
lines.push(` const __props = { ${propsObj} };`);
|
|
1204
1280
|
lines.push(` let __html = this.__slotTpl_${s.name};`);
|
|
1205
1281
|
lines.push(" for (const [k, v] of Object.entries(__props)) {");
|
|
1206
|
-
lines.push(` __html = __html.replace(new RegExp('
|
|
1282
|
+
lines.push(` __html = __html.replace(new RegExp('(?:\\\\{\\\\{|\\\\{%)\\\\s*' + k + '(\\\\(\\\\))?\\\\s*(?:\\\\}\\\\}|%\\\\})', 'g'), v ?? '');`);
|
|
1207
1283
|
lines.push(' }');
|
|
1208
1284
|
lines.push(` this.${s.varName}.innerHTML = __html;`);
|
|
1209
1285
|
lines.push(' });');
|
|
@@ -1556,6 +1632,7 @@ export function generateComponent(parseResult, options = {}) {
|
|
|
1556
1632
|
}
|
|
1557
1633
|
}
|
|
1558
1634
|
|
|
1635
|
+
// Close connectedCallback
|
|
1559
1636
|
lines.push(' }');
|
|
1560
1637
|
lines.push('');
|
|
1561
1638
|
|
package/package.json
CHANGED