@sprlab/wccompiler 0.8.7 → 0.9.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/integrations/vue.js +79 -0
- package/lib/codegen.js +82 -2
- package/package.json +1 -1
package/integrations/vue.js
CHANGED
|
@@ -90,6 +90,85 @@ export function wccVuePlugin(options = {}) {
|
|
|
90
90
|
)
|
|
91
91
|
}
|
|
92
92
|
|
|
93
|
+
// ── Slot transforms ──
|
|
94
|
+
// Transform <template #name>content</template> inside custom elements
|
|
95
|
+
// → <div slot="name">content</div>
|
|
96
|
+
// This prevents Vue from intercepting the slot syntax and erroring.
|
|
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.
|
|
101
|
+
|
|
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)
|
|
148
|
+
prev = ''
|
|
149
|
+
while (prev !== result) {
|
|
150
|
+
prev = result
|
|
151
|
+
result = result.replace(
|
|
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>`
|
|
155
|
+
}
|
|
156
|
+
)
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// Handle non-scoped <template v-slot:name>...</template> (verbose)
|
|
160
|
+
// Only inside custom elements (tag names with hyphens)
|
|
161
|
+
prev = ''
|
|
162
|
+
while (prev !== result) {
|
|
163
|
+
prev = result
|
|
164
|
+
result = result.replace(
|
|
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>`
|
|
168
|
+
}
|
|
169
|
+
)
|
|
170
|
+
}
|
|
171
|
+
|
|
93
172
|
if (result !== code) return result
|
|
94
173
|
return null
|
|
95
174
|
}
|
package/lib/codegen.js
CHANGED
|
@@ -1039,7 +1039,26 @@ export function generateComponent(parseResult, options = {}) {
|
|
|
1039
1039
|
lines.push(' __slotMap[slotName] = { content: child.innerHTML, propsExpr: attr.value };');
|
|
1040
1040
|
lines.push(' }');
|
|
1041
1041
|
lines.push(' }');
|
|
1042
|
-
lines.push(" } else if (child.nodeType === 1
|
|
1042
|
+
lines.push(" } else if (child.nodeType === 1 && child.getAttribute('slot')) {");
|
|
1043
|
+
// NEW: regular element with slot="name" (cross-framework support)
|
|
1044
|
+
lines.push(" const slotName = child.getAttribute('slot');");
|
|
1045
|
+
lines.push(" const propsExpr = child.getAttribute('slot-props') || '';");
|
|
1046
|
+
lines.push(" child.removeAttribute('slot');");
|
|
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()) {");
|
|
1043
1062
|
lines.push(' __defaultSlotNodes.push(child);');
|
|
1044
1063
|
lines.push(' }');
|
|
1045
1064
|
lines.push(' }');
|
|
@@ -1148,6 +1167,66 @@ export function generateComponent(parseResult, options = {}) {
|
|
|
1148
1167
|
}
|
|
1149
1168
|
}
|
|
1150
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
|
+
// After the first render, Angular projects children as siblings after the template root.
|
|
1174
|
+
// The microtask skips the first child (our rendered template) and parses the rest.
|
|
1175
|
+
if (slots.length > 0) {
|
|
1176
|
+
lines.push(' if (Object.keys(__slotMap).length === 0 && __defaultSlotNodes.length === 0) {');
|
|
1177
|
+
lines.push(' queueMicrotask(() => {');
|
|
1178
|
+
lines.push(' const __sm = {};');
|
|
1179
|
+
lines.push(' const __dn = [];');
|
|
1180
|
+
// Skip the first element child (our rendered template root)
|
|
1181
|
+
lines.push(' const __children = Array.from(this.childNodes).slice(1);');
|
|
1182
|
+
lines.push(' for (const child of __children) {');
|
|
1183
|
+
lines.push(" if (child.nodeName === 'TEMPLATE') {");
|
|
1184
|
+
lines.push(' for (const attr of child.attributes) {');
|
|
1185
|
+
lines.push(" if (attr.name.startsWith('#')) {");
|
|
1186
|
+
lines.push(" __sm[attr.name.slice(1)] = { content: child.innerHTML, propsExpr: attr.value };");
|
|
1187
|
+
lines.push(' }');
|
|
1188
|
+
lines.push(' }');
|
|
1189
|
+
lines.push(" } else if (child.nodeType === 1 && child.getAttribute('slot')) {");
|
|
1190
|
+
lines.push(" const sn = child.getAttribute('slot');");
|
|
1191
|
+
lines.push(" const pe = child.getAttribute('slot-props') || '';");
|
|
1192
|
+
lines.push(" child.removeAttribute('slot');");
|
|
1193
|
+
lines.push(" child.removeAttribute('slot-props');");
|
|
1194
|
+
lines.push(" __sm[sn] = { content: pe ? child.innerHTML : child.outerHTML, propsExpr: pe };");
|
|
1195
|
+
lines.push(" child.remove();");
|
|
1196
|
+
lines.push(" } else if (child.nodeType === 1) {");
|
|
1197
|
+
lines.push(" for (const attr of Array.from(child.attributes)) {");
|
|
1198
|
+
lines.push(" if (attr.name.startsWith('slot-template-')) {");
|
|
1199
|
+
lines.push(" const sn = attr.name.slice('slot-template-'.length);");
|
|
1200
|
+
lines.push(" if (!__sm[sn]) { __sm[sn] = { content: attr.value, propsExpr: '' }; }");
|
|
1201
|
+
lines.push(" child.removeAttribute(attr.name);");
|
|
1202
|
+
lines.push(" }");
|
|
1203
|
+
lines.push(" }");
|
|
1204
|
+
lines.push(" __dn.push(child);");
|
|
1205
|
+
lines.push(" } else if (child.nodeType === 3 && child.textContent.trim()) {");
|
|
1206
|
+
lines.push(" __dn.push(child);");
|
|
1207
|
+
lines.push(' }');
|
|
1208
|
+
lines.push(' }');
|
|
1209
|
+
// Re-inject slots if we found content this time
|
|
1210
|
+
lines.push(' if (Object.keys(__sm).length > 0 || __dn.length > 0) {');
|
|
1211
|
+
for (const s of slots) {
|
|
1212
|
+
if (s.name && s.slotProps.length > 0) {
|
|
1213
|
+
lines.push(` if (__sm['${s.name}']) {`);
|
|
1214
|
+
lines.push(` this.__slotTpl_${s.name} = __sm['${s.name}'].content;`);
|
|
1215
|
+
if (s.slotProps.length > 0 && s.slotProps[0].source) {
|
|
1216
|
+
lines.push(` this._${s.slotProps[0].source}.set(this._${s.slotProps[0].source}());`);
|
|
1217
|
+
}
|
|
1218
|
+
lines.push(` }`);
|
|
1219
|
+
} else if (s.name) {
|
|
1220
|
+
lines.push(` if (__sm['${s.name}']) { this.${s.varName}.innerHTML = __sm['${s.name}'].content; }`);
|
|
1221
|
+
} else {
|
|
1222
|
+
lines.push(` if (__dn.length) { this.${s.varName}.textContent = ''; __dn.forEach(n => this.${s.varName}.appendChild(n.cloneNode(true))); }`);
|
|
1223
|
+
}
|
|
1224
|
+
}
|
|
1225
|
+
lines.push(' }');
|
|
1226
|
+
lines.push(' });');
|
|
1227
|
+
lines.push(' }');
|
|
1228
|
+
}
|
|
1229
|
+
|
|
1151
1230
|
// ── EFFECTS AND LISTENERS ──
|
|
1152
1231
|
lines.push(' this.__ac = new AbortController();');
|
|
1153
1232
|
lines.push(' this.__disposers = [];');
|
|
@@ -1198,7 +1277,7 @@ export function generateComponent(parseResult, options = {}) {
|
|
|
1198
1277
|
lines.push(` const __props = { ${propsObj} };`);
|
|
1199
1278
|
lines.push(` let __html = this.__slotTpl_${s.name};`);
|
|
1200
1279
|
lines.push(" for (const [k, v] of Object.entries(__props)) {");
|
|
1201
|
-
lines.push(` __html = __html.replace(new RegExp('
|
|
1280
|
+
lines.push(` __html = __html.replace(new RegExp('(?:\\\\{\\\\{|\\\\{%)\\\\s*' + k + '(\\\\(\\\\))?\\\\s*(?:\\\\}\\\\}|%\\\\})', 'g'), v ?? '');`);
|
|
1202
1281
|
lines.push(' }');
|
|
1203
1282
|
lines.push(` this.${s.varName}.innerHTML = __html;`);
|
|
1204
1283
|
lines.push(' });');
|
|
@@ -1551,6 +1630,7 @@ export function generateComponent(parseResult, options = {}) {
|
|
|
1551
1630
|
}
|
|
1552
1631
|
}
|
|
1553
1632
|
|
|
1633
|
+
// Close connectedCallback
|
|
1554
1634
|
lines.push(' }');
|
|
1555
1635
|
lines.push('');
|
|
1556
1636
|
|
package/package.json
CHANGED