@sprlab/wccompiler 0.11.0 → 0.11.2

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.
@@ -76,12 +76,14 @@ export function wccVuePlugin(options = {}) {
76
76
 
77
77
  // Transform v-model:propName="expr" on custom elements (tags with hyphens)
78
78
  // Also handles modifiers: v-model:propName.trim.number="expr"
79
- // → :propName="expr" @wcc:model="$event.detail.prop === 'propName' && (expr = value)"
79
+ // → :propName="expr" + merged @wcc:model handler
80
80
  // with modifiers applied to the extracted value:
81
81
  // .trim → value.trim() (for string values)
82
82
  // .number → Number(value)
83
83
  // .lazy → no-op for custom elements
84
- // Run in a loop to handle multiple v-model on the same element
84
+ //
85
+ // Multiple v-model:prop on the same element are merged into a single
86
+ // @wcc:model handler with semicolons (avoids Vue "Duplicate attribute" error).
85
87
  let prev = ''
86
88
  while (prev !== result) {
87
89
  prev = result
@@ -99,14 +101,23 @@ export function wccVuePlugin(options = {}) {
99
101
  }
100
102
  // .lazy is a no-op for custom elements (they already use change events)
101
103
  }
102
- return `${prefix}:${prop}="${expr}" @wcc:model="$event.detail.prop === '${prop}' && (${expr} = ${value})"`
104
+ const handler = `$event.detail.prop === '${prop}' && (${expr} = ${value})`
105
+ // Check if there's already a @wcc:model on this element — append to it
106
+ if (prefix.includes('@wcc:model="')) {
107
+ const merged = prefix.replace(
108
+ /@wcc:model="([^"]*)"/,
109
+ (_, existing) => `@wcc:model="${existing}; ${handler}"`
110
+ )
111
+ return `${merged}:${prop}="${expr}"`
112
+ }
113
+ return `${prefix}:${prop}="${expr}" @wcc:model="${handler}"`
103
114
  }
104
115
  )
105
116
  }
106
117
 
107
118
  // Transform v-model="expr" (without argument) on custom elements
108
119
  // Also handles modifiers: v-model.trim.lazy="expr"
109
- // → :model-value="expr" @wcc:model="$event.detail.prop === 'modelValue' && (expr = value)"
120
+ // → :model-value="expr" + merged @wcc:model handler
110
121
  prev = ''
111
122
  while (prev !== result) {
112
123
  prev = result
@@ -122,11 +133,29 @@ export function wccVuePlugin(options = {}) {
122
133
  value = `Number(${value})`
123
134
  }
124
135
  }
125
- return `${prefix}:model-value="${expr}" @wcc:model="$event.detail.prop === 'modelValue' && (${expr} = ${value})"`
136
+ const handler = `$event.detail.prop === 'modelValue' && (${expr} = ${value})`
137
+ // Check if there's already a @wcc:model on this element — append to it
138
+ if (prefix.includes('@wcc:model="')) {
139
+ const merged = prefix.replace(
140
+ /@wcc:model="([^"]*)"/,
141
+ (_, existing) => `@wcc:model="${existing}; ${handler}"`
142
+ )
143
+ return `${merged}:model-value="${expr}"`
144
+ }
145
+ return `${prefix}:model-value="${expr}" @wcc:model="${handler}"`
126
146
  }
127
147
  )
128
148
  }
129
149
 
150
+ // Post-process: merge any duplicate @wcc:model attributes on the same element
151
+ // This handles the case where v-model (no arg) was before v-model:prop in source order
152
+ result = result.replace(
153
+ /<([\w]+-[\w-]*)((?:\s[^>]*?)?)@wcc:model="([^"]*)"((?:\s[^>]*?)?)@wcc:model="([^"]*)"([^>]*?)>/g,
154
+ (match, tag, before, handler1, middle, handler2, after) => {
155
+ return `<${tag}${before}@wcc:model="${handler1}; ${handler2}"${middle}${after}>`
156
+ }
157
+ )
158
+
130
159
  // ── Slot transforms ──
131
160
  // Transform <template #name>content</template> inside custom elements
132
161
  // → <div slot="name">content</div>
@@ -88,7 +88,9 @@ export function walkTree(rootEl, signalNames, computedNames, propNames = new Set
88
88
  // Detect <slot> elements — replace with <span data-slot="..."> placeholder
89
89
  if (el.tagName === 'SLOT') {
90
90
  const slotName = el.getAttribute('name') || '';
91
- const varName = `__s${slotIdx++}`;
91
+ const safeName = slotName ? slotName.replace(/[^a-zA-Z0-9_]/g, '_') : 'default';
92
+ const varName = `__slot_${safeName}_${slotIdx}`;
93
+ slotIdx++;
92
94
  const defaultContent = el.innerHTML.trim();
93
95
 
94
96
  // Collect :prop="expr" attributes (slot props for scoped slots)
@@ -155,10 +157,13 @@ export function walkTree(rootEl, signalNames, computedNames, propNames = new Set
155
157
  const attrsToRemove = [];
156
158
  for (const attr of Array.from(el.attributes)) {
157
159
  if (attr.name.startsWith('@')) {
158
- const varName = `__e${eventIdx++}`;
160
+ const eventName = attr.name.slice(1);
161
+ const handlerName = attr.value.replace(/[^a-zA-Z0-9_]/g, '_').slice(0, 20);
162
+ const varName = `__evt_${eventName.replace(/-/g, '_')}_${handlerName}`;
163
+ eventIdx++;
159
164
  events.push({
160
165
  varName,
161
- event: attr.name.slice(1),
166
+ event: eventName,
162
167
  handler: attr.value,
163
168
  path: [...pathParts],
164
169
  });
@@ -180,7 +185,8 @@ export function walkTree(rootEl, signalNames, computedNames, propNames = new Set
180
185
  kind = 'attr';
181
186
  }
182
187
 
183
- const varName = `__attr${attrIdx++}`;
188
+ const varName = `__attr_${attrName.replace(/-/g, '_')}_${attrIdx}`;
189
+ attrIdx++;
184
190
  attrBindings.push({
185
191
  varName,
186
192
  attr: attrName,
@@ -195,7 +201,8 @@ export function walkTree(rootEl, signalNames, computedNames, propNames = new Set
195
201
 
196
202
  // Detect show attribute
197
203
  if (el.hasAttribute('show')) {
198
- const varName = `__show${showIdx++}`;
204
+ const varName = `__show_${showIdx}`;
205
+ showIdx++;
199
206
  showBindings.push({
200
207
  varName,
201
208
  expression: el.getAttribute('show'),
@@ -244,7 +251,8 @@ export function walkTree(rootEl, signalNames, computedNames, propNames = new Set
244
251
  prop = 'value'; event = 'input';
245
252
  }
246
253
 
247
- const varName = `__model${modelIdx++}`;
254
+ const varName = `__model_${signalName}`;
255
+ modelIdx++;
248
256
  modelBindings.push({ varName, signal: signalName, prop, event, coerce, radioValue, path: [...pathParts] });
249
257
  el.removeAttribute('model');
250
258
  }
@@ -265,7 +273,8 @@ export function walkTree(rootEl, signalNames, computedNames, propNames = new Set
265
273
  throw error;
266
274
  }
267
275
 
268
- const varName = `__modelProp${modelPropIdx++}`;
276
+ const varName = `__modelProp_${propName}`;
277
+ modelPropIdx++;
269
278
  modelPropBindings.push({ varName, propName, signal, path: [...pathParts] });
270
279
  modelPropAttrsToRemove.push(attr.name);
271
280
  }
@@ -287,8 +296,10 @@ export function walkTree(rootEl, signalNames, computedNames, propNames = new Set
287
296
 
288
297
  // Case 1: {{var}} is the sole content of the parent element and parent has only one child text node
289
298
  if (soleMatch && parent.childNodes.length === 1) {
290
- const varName = `__b${bindIdx++}`;
291
299
  const name = baseName(soleMatch[1]);
300
+ const safeName = name.replace(/[^a-zA-Z0-9_]/g, '_').slice(0, 30);
301
+ const varName = `__text_${safeName}`;
302
+ bindIdx++;
292
303
  bindings.push({
293
304
  varName,
294
305
  name,
@@ -317,8 +328,10 @@ export function walkTree(rootEl, signalNames, computedNames, propNames = new Set
317
328
  const bm = part.match(/^\{\{((?:[^}]|\}(?!\}))+)\}\}$/);
318
329
  if (bm) {
319
330
  fragment.appendChild(doc.createElement('span'));
320
- const varName = `__b${bindIdx++}`;
321
331
  const name = baseName(bm[1]);
332
+ const safeName = name.replace(/[^a-zA-Z0-9_]/g, '_').slice(0, 30);
333
+ const varName = `__text_${safeName}_${bindIdx}`;
334
+ bindIdx++;
322
335
  bindings.push({
323
336
  varName,
324
337
  name,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sprlab/wccompiler",
3
- "version": "0.11.0",
3
+ "version": "0.11.2",
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": {