@sprlab/wccompiler 0.8.8 → 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.
@@ -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
- // Handle <template #name>...</template> (shorthand)
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
- /<template\s+#(\w+)>([\s\S]*?)<\/template>/,
105
- (match, slotName, content) => {
106
- return `<div slot="${slotName}">${content}</div>`
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
- /<template\s+v-slot:(\w+)>([\s\S]*?)<\/template>/,
117
- (match, slotName, content) => {
118
- return `<div slot="${slotName}">${content}</div>`
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(' __slotMap[slotName] = { content: child.outerHTML, propsExpr: \'\' };');
1047
- lines.push(" } else if (child.nodeType === 1 || (child.nodeType === 3 && child.textContent.trim())) {");
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,66 @@ 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
+ // 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
+
1156
1230
  // ── EFFECTS AND LISTENERS ──
1157
1231
  lines.push(' this.__ac = new AbortController();');
1158
1232
  lines.push(' this.__disposers = [];');
@@ -1203,7 +1277,7 @@ export function generateComponent(parseResult, options = {}) {
1203
1277
  lines.push(` const __props = { ${propsObj} };`);
1204
1278
  lines.push(` let __html = this.__slotTpl_${s.name};`);
1205
1279
  lines.push(" for (const [k, v] of Object.entries(__props)) {");
1206
- lines.push(` __html = __html.replace(new RegExp('\\\\{\\\\{\\\\s*' + k + '(\\\\(\\\\))?\\\\s*\\\\}\\\\}', 'g'), v ?? '');`);
1280
+ lines.push(` __html = __html.replace(new RegExp('(?:\\\\{\\\\{|\\\\{%)\\\\s*' + k + '(\\\\(\\\\))?\\\\s*(?:\\\\}\\\\}|%\\\\})', 'g'), v ?? '');`);
1207
1281
  lines.push(' }');
1208
1282
  lines.push(` this.${s.varName}.innerHTML = __html;`);
1209
1283
  lines.push(' });');
@@ -1556,6 +1630,7 @@ export function generateComponent(parseResult, options = {}) {
1556
1630
  }
1557
1631
  }
1558
1632
 
1633
+ // Close connectedCallback
1559
1634
  lines.push(' }');
1560
1635
  lines.push('');
1561
1636
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sprlab/wccompiler",
3
- "version": "0.8.8",
3
+ "version": "0.9.0",
4
4
  "description": "Zero-runtime compiler that transforms .wcc single-file components into native web components with signals-based reactivity",
5
5
  "type": "module",
6
6
  "exports": {