@sprlab/wccompiler 0.10.6 → 0.10.8
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/adapters/react.js +23 -297
- package/integrations/react.js +126 -20
- package/package.json +1 -1
package/adapters/react.js
CHANGED
|
@@ -4,25 +4,24 @@
|
|
|
4
4
|
*
|
|
5
5
|
* @module @sprlab/wccompiler/adapters/react
|
|
6
6
|
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
7
|
+
* NOTE: These hooks are optional utilities for React 18 or edge cases.
|
|
8
|
+
* With React 19 + wccReactPlugin, WCC components work natively:
|
|
9
9
|
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
10
|
+
* <WccCounter count={state} oncountchanged={(e) => setState(e.detail)} />
|
|
11
|
+
* <WccCard>
|
|
12
|
+
* <WccCard.Header><strong>Title</strong></WccCard.Header>
|
|
13
|
+
* </WccCard>
|
|
12
14
|
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
* <wcc-counter ref={ref}></wcc-counter>
|
|
15
|
+
* The plugin handles PascalCase → kebab-case, compound components, and
|
|
16
|
+
* scoped slots at build time. No runtime wrappers needed.
|
|
16
17
|
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
* })
|
|
22
|
-
* <WccCounter onChange={handler} count={count} onCountChanged={setCount} />
|
|
18
|
+
* These hooks are for cases where you need manual event bridging
|
|
19
|
+
* (e.g., React 18, non-Vite builds, or dynamic event names):
|
|
20
|
+
*
|
|
21
|
+
* import { useWccEvent, useWccModel } from '@sprlab/wccompiler/adapters/react'
|
|
23
22
|
*/
|
|
24
23
|
|
|
25
|
-
import
|
|
24
|
+
import { useRef, useEffect } from 'react'
|
|
26
25
|
|
|
27
26
|
/**
|
|
28
27
|
* Hook that attaches a CustomEvent listener to a DOM element via ref.
|
|
@@ -35,6 +34,16 @@ import React, { useRef, useEffect } from 'react'
|
|
|
35
34
|
* @param {string | ((event: CustomEvent) => void)} eventNameOrHandler
|
|
36
35
|
* @param {((event: CustomEvent) => void)} [handler]
|
|
37
36
|
* @returns {import('react').RefObject<HTMLElement> | void}
|
|
37
|
+
*
|
|
38
|
+
* @example
|
|
39
|
+
* // Form 1: Let the hook create the ref
|
|
40
|
+
* const ref = useWccEvent('count-changed', (e) => setCount(e.detail))
|
|
41
|
+
* <wcc-counter ref={ref}></wcc-counter>
|
|
42
|
+
*
|
|
43
|
+
* // Form 2: Pass an existing ref
|
|
44
|
+
* const myRef = useRef(null)
|
|
45
|
+
* useWccEvent(myRef, 'count-changed', (e) => setCount(e.detail))
|
|
46
|
+
* <wcc-counter ref={myRef}></wcc-counter>
|
|
38
47
|
*/
|
|
39
48
|
export function useWccEvent(refOrEventName, eventNameOrHandler, handler) {
|
|
40
49
|
const isRefForm = typeof refOrEventName !== 'string'
|
|
@@ -103,286 +112,3 @@ export function useWccModel(propName, value, setValue, existingRef) {
|
|
|
103
112
|
|
|
104
113
|
return elementRef
|
|
105
114
|
}
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
/**
|
|
109
|
-
* Converts a kebab-case event name to a React-idiomatic prop name.
|
|
110
|
-
*
|
|
111
|
-
* Rules:
|
|
112
|
-
* - 'change' → 'onChange'
|
|
113
|
-
* - 'count-changed' → 'onCountChange' (strips trailing 'd' from past tense)
|
|
114
|
-
* - 'value-updated' → 'onValueUpdate' (strips trailing 'd')
|
|
115
|
-
* - 'reset' → 'onReset'
|
|
116
|
-
* - 'item-click' → 'onItemClick'
|
|
117
|
-
*
|
|
118
|
-
* @param {string} eventName - kebab-case event name
|
|
119
|
-
* @returns {string} React prop name (onCamelCase)
|
|
120
|
-
*/
|
|
121
|
-
function toReactEventProp(eventName) {
|
|
122
|
-
const parts = eventName.split('-')
|
|
123
|
-
const camel = parts.map(s => s[0].toUpperCase() + s.slice(1)).join('')
|
|
124
|
-
// Strip trailing 'd' from past tense verbs (changed→Change, updated→Update)
|
|
125
|
-
const normalized = camel.replace(/(Changed|Updated|Removed|Added|Closed|Opened|Submitted|Cancelled)$/, (m) => m.slice(0, -1))
|
|
126
|
-
return 'on' + normalized
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
/**
|
|
130
|
-
* Creates a React wrapper component for a WCC custom element.
|
|
131
|
-
*
|
|
132
|
-
* The wrapper provides idiomatic React DX:
|
|
133
|
-
* - Event props: `onChange`, `onCountChange` → automatically wired via addEventListener
|
|
134
|
-
* Handlers receive the unwrapped value (event.detail), not the raw CustomEvent.
|
|
135
|
-
* - Model props: two-way binding via attribute + event listener
|
|
136
|
-
* - Regular props: passed as attributes on the custom element
|
|
137
|
-
* - Children: passed through as-is (use `<div slot="name">` for named slots)
|
|
138
|
-
* - Ref forwarding: supports React refs via forwardRef
|
|
139
|
-
*
|
|
140
|
-
* @param {string} tagName - The custom element tag name (e.g., 'wcc-card')
|
|
141
|
-
* @param {Object} [config] - Configuration for the wrapper
|
|
142
|
-
* @param {string[]} [config.events] - Custom event names to expose as onEventName props
|
|
143
|
-
* Event names are converted: 'count-changed' → onCountChange prop (React convention)
|
|
144
|
-
* @param {string[]} [config.models] - Model prop names for two-way binding
|
|
145
|
-
* Each model 'name' creates: `name` prop (sets attribute) + `onNameChange` event
|
|
146
|
-
* @param {string[]} [config.slots] - Named slot names for compound sub-components
|
|
147
|
-
* Each slot 'name' creates a `.Name` sub-component that renders `<div slot="name">`
|
|
148
|
-
* @returns {import('react').ForwardRefExoticComponent} A React component with compound sub-components
|
|
149
|
-
*
|
|
150
|
-
* @example
|
|
151
|
-
* const WccCounter = createWccWrapper('wcc-counter', {
|
|
152
|
-
* events: ['change'],
|
|
153
|
-
* models: ['count'],
|
|
154
|
-
* slots: ['header', 'footer']
|
|
155
|
-
* })
|
|
156
|
-
*
|
|
157
|
-
* function App() {
|
|
158
|
-
* const [count, setCount] = useState(0)
|
|
159
|
-
* return (
|
|
160
|
-
* <WccCounter
|
|
161
|
-
* count={count}
|
|
162
|
-
* onCountChange={(value) => setCount(value)}
|
|
163
|
-
* onChange={(value) => console.log('changed', value)}
|
|
164
|
-
* label="Clicks"
|
|
165
|
-
* >
|
|
166
|
-
* <WccCounter.Header><strong>Title</strong></WccCounter.Header>
|
|
167
|
-
* <p>Body content</p>
|
|
168
|
-
* <WccCounter.Footer>Footer text</WccCounter.Footer>
|
|
169
|
-
* </WccCounter>
|
|
170
|
-
* )
|
|
171
|
-
* }
|
|
172
|
-
*/
|
|
173
|
-
export function createWccWrapper(tagName, config = {}) {
|
|
174
|
-
const { events = [], models = [], slots = [] } = config
|
|
175
|
-
|
|
176
|
-
// Build a set of event prop names for quick lookup
|
|
177
|
-
// Convention: kebab-case event → React onCamelCase (without trailing 'd' from 'changed')
|
|
178
|
-
// 'count-changed' → 'onCountChange' (not 'onCountChanged')
|
|
179
|
-
// 'change' → 'onChange'
|
|
180
|
-
// 'value-updated' → 'onValueUpdate' (strips trailing 'd' from past tense)
|
|
181
|
-
const eventPropMap = new Map()
|
|
182
|
-
for (const eventName of events) {
|
|
183
|
-
const propName = toReactEventProp(eventName)
|
|
184
|
-
eventPropMap.set(propName, eventName)
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
// Model events: 'count' → 'count-changed' → 'onCountChange'
|
|
188
|
-
const modelEventMap = new Map()
|
|
189
|
-
for (const modelName of models) {
|
|
190
|
-
const eventName = `${modelName}-changed`
|
|
191
|
-
const propName = toReactEventProp(eventName)
|
|
192
|
-
eventPropMap.set(propName, eventName)
|
|
193
|
-
modelEventMap.set(modelName, eventName)
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
// Reserved prop names that should not be passed as attributes
|
|
197
|
-
const SKIP_PROPS = new Set(['children', 'key', 'ref', 'style', 'className', 'dangerouslySetInnerHTML'])
|
|
198
|
-
|
|
199
|
-
const WccWrapper = React.forwardRef(function WccWrapper(props, externalRef) {
|
|
200
|
-
const internalRef = useRef(null)
|
|
201
|
-
const ref = externalRef || internalRef
|
|
202
|
-
|
|
203
|
-
// Store event handlers in a ref to avoid re-subscribing on every render
|
|
204
|
-
const handlersRef = useRef({})
|
|
205
|
-
|
|
206
|
-
// Collect event handlers and regular props
|
|
207
|
-
const regularProps = {}
|
|
208
|
-
const eventHandlers = {}
|
|
209
|
-
|
|
210
|
-
for (const [key, value] of Object.entries(props)) {
|
|
211
|
-
if (SKIP_PROPS.has(key)) continue
|
|
212
|
-
|
|
213
|
-
if (eventPropMap.has(key)) {
|
|
214
|
-
eventHandlers[eventPropMap.get(key)] = value
|
|
215
|
-
} else if (key.startsWith('on') && key.length > 2 && key[2] >= 'A' && key[2] <= 'Z') {
|
|
216
|
-
// Generic React event handler pattern: onClick, onFocus, etc.
|
|
217
|
-
// Convert onSomething → 'something' (lowercase first char)
|
|
218
|
-
const nativeEvent = key[2].toLowerCase() + key.slice(3)
|
|
219
|
-
eventHandlers[nativeEvent] = value
|
|
220
|
-
} else {
|
|
221
|
-
regularProps[key] = value
|
|
222
|
-
}
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
// Update handlers ref
|
|
226
|
-
handlersRef.current = eventHandlers
|
|
227
|
-
|
|
228
|
-
// Subscribe to custom events
|
|
229
|
-
useEffect(() => {
|
|
230
|
-
const el = typeof ref === 'function' ? null : ref?.current
|
|
231
|
-
if (!el) return
|
|
232
|
-
|
|
233
|
-
const listeners = []
|
|
234
|
-
const allEvents = new Set([...eventPropMap.values(), ...Object.keys(eventHandlers)])
|
|
235
|
-
|
|
236
|
-
for (const eventName of allEvents) {
|
|
237
|
-
const listener = (e) => {
|
|
238
|
-
const handler = handlersRef.current[eventName]
|
|
239
|
-
if (handler) handler(e instanceof CustomEvent ? e.detail : e)
|
|
240
|
-
}
|
|
241
|
-
el.addEventListener(eventName, listener)
|
|
242
|
-
listeners.push([eventName, listener])
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
return () => {
|
|
246
|
-
for (const [name, listener] of listeners) {
|
|
247
|
-
el.removeEventListener(name, listener)
|
|
248
|
-
}
|
|
249
|
-
}
|
|
250
|
-
}, []) // eslint-disable-line react-hooks/exhaustive-deps
|
|
251
|
-
|
|
252
|
-
// Sync regular props as attributes
|
|
253
|
-
useEffect(() => {
|
|
254
|
-
const el = typeof ref === 'function' ? null : ref?.current
|
|
255
|
-
if (!el) return
|
|
256
|
-
|
|
257
|
-
for (const [key, value] of Object.entries(regularProps)) {
|
|
258
|
-
if (value == null || value === false) {
|
|
259
|
-
el.removeAttribute(key)
|
|
260
|
-
} else if (value === true) {
|
|
261
|
-
el.setAttribute(key, '')
|
|
262
|
-
} else {
|
|
263
|
-
el.setAttribute(key, String(value))
|
|
264
|
-
}
|
|
265
|
-
}
|
|
266
|
-
})
|
|
267
|
-
|
|
268
|
-
// Build the element props for React's createElement
|
|
269
|
-
const elementProps = { ref }
|
|
270
|
-
if (props.style) elementProps.style = props.style
|
|
271
|
-
if (props.className) elementProps.className = props.className
|
|
272
|
-
|
|
273
|
-
return React.createElement(tagName, elementProps, props.children)
|
|
274
|
-
})
|
|
275
|
-
|
|
276
|
-
WccWrapper.displayName = tagName.split('-').map(s => s[0].toUpperCase() + s.slice(1)).join('')
|
|
277
|
-
|
|
278
|
-
// Compound components: generate .SlotName sub-components for each named slot
|
|
279
|
-
// This enables the pattern: <WccLayout.Header>content</WccLayout.Header>
|
|
280
|
-
// which renders as: <div slot="header">content</div>
|
|
281
|
-
for (const slotName of slots) {
|
|
282
|
-
if (!slotName) continue // skip default slot
|
|
283
|
-
const pascalSlot = slotName[0].toUpperCase() + slotName.slice(1)
|
|
284
|
-
const SlotComponent = function WccSlot({ children, ...rest }) {
|
|
285
|
-
const slotProps = { slot: slotName, style: { display: 'contents' } }
|
|
286
|
-
// Pass through any extra props as attributes on the wrapper div
|
|
287
|
-
for (const [key, value] of Object.entries(rest)) {
|
|
288
|
-
slotProps[key] = value
|
|
289
|
-
}
|
|
290
|
-
return React.createElement('div', slotProps, children)
|
|
291
|
-
}
|
|
292
|
-
SlotComponent.displayName = `${WccWrapper.displayName}.${pascalSlot}`
|
|
293
|
-
WccWrapper[pascalSlot] = SlotComponent
|
|
294
|
-
}
|
|
295
|
-
|
|
296
|
-
return WccWrapper
|
|
297
|
-
}
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
/**
|
|
302
|
-
* Creates a React wrapper from a WCC component class that has `static __meta`.
|
|
303
|
-
*
|
|
304
|
-
* Unlike `createWccWrapper` which requires manual event/model configuration,
|
|
305
|
-
* this function reads the metadata directly from the compiled component class.
|
|
306
|
-
*
|
|
307
|
-
* @param {Function} WccClass - The WCC custom element class (must have static __meta)
|
|
308
|
-
* @returns {import('react').ForwardRefExoticComponent} A React component
|
|
309
|
-
*
|
|
310
|
-
* @example
|
|
311
|
-
* import { wrapWccComponent } from '@sprlab/wccompiler/adapters/react'
|
|
312
|
-
* import '../wcc-components/wcc-counter.js' // registers the custom element
|
|
313
|
-
*
|
|
314
|
-
* // Read metadata directly from the registered class
|
|
315
|
-
* const WccCounter = wrapWccComponent(customElements.get('wcc-counter'))
|
|
316
|
-
*
|
|
317
|
-
* // Use idiomatically — no manual config needed
|
|
318
|
-
* // Handlers receive the value directly (not the event)
|
|
319
|
-
* <WccCounter count={count} onCountChange={setCount} onChange={(val) => console.log(val)} />
|
|
320
|
-
*/
|
|
321
|
-
export function wrapWccComponent(WccClass) {
|
|
322
|
-
const meta = WccClass?.__meta
|
|
323
|
-
if (!meta) {
|
|
324
|
-
throw new Error(`wrapWccComponent: class does not have static __meta. Is it a compiled WCC component?`)
|
|
325
|
-
}
|
|
326
|
-
|
|
327
|
-
return createWccWrapper(meta.tag, {
|
|
328
|
-
events: meta.events || [],
|
|
329
|
-
models: meta.models || [],
|
|
330
|
-
slots: meta.slots || [],
|
|
331
|
-
})
|
|
332
|
-
}
|
|
333
|
-
|
|
334
|
-
/**
|
|
335
|
-
* Creates React wrappers for all registered WCC custom elements matching a prefix.
|
|
336
|
-
*
|
|
337
|
-
* Scans the custom elements registry for components with `static __meta` and
|
|
338
|
-
* generates typed wrapper components for each one.
|
|
339
|
-
*
|
|
340
|
-
* @param {Object} [options]
|
|
341
|
-
* @param {string} [options.prefix='wcc-'] - Tag prefix to filter components
|
|
342
|
-
* @returns {Record<string, import('react').ForwardRefExoticComponent>} Map of PascalCase name → React component
|
|
343
|
-
*
|
|
344
|
-
* @example
|
|
345
|
-
* // In your app entry point, after importing all WCC components:
|
|
346
|
-
* import '../wcc-components/wcc-counter.js'
|
|
347
|
-
* import '../wcc-components/wcc-card.js'
|
|
348
|
-
* import { createWccWrappers } from '@sprlab/wccompiler/adapters/react'
|
|
349
|
-
*
|
|
350
|
-
* export const { WccCounter, WccCard } = createWccWrappers()
|
|
351
|
-
*
|
|
352
|
-
* // Then use anywhere:
|
|
353
|
-
* <WccCounter count={count} onCountChange={setCount} />
|
|
354
|
-
* <WccCard>
|
|
355
|
-
* <WccCard.Header><strong>Title</strong></WccCard.Header>
|
|
356
|
-
* <p>Body</p>
|
|
357
|
-
* <WccCard.Footer>Footer</WccCard.Footer>
|
|
358
|
-
* </WccCard>
|
|
359
|
-
*/
|
|
360
|
-
export function createWccWrappers(options = {}) {
|
|
361
|
-
const { prefix = 'wcc-' } = options
|
|
362
|
-
const wrappers = {}
|
|
363
|
-
|
|
364
|
-
// Note: customElements registry doesn't have a list API,
|
|
365
|
-
// so we need the component files to be imported first (which registers them).
|
|
366
|
-
// This function is meant to be called after all component imports.
|
|
367
|
-
|
|
368
|
-
// We'll use a Proxy that lazily creates wrappers on first access
|
|
369
|
-
return new Proxy(wrappers, {
|
|
370
|
-
get(target, prop) {
|
|
371
|
-
if (typeof prop !== 'string') return undefined
|
|
372
|
-
if (prop in target) return target[prop]
|
|
373
|
-
|
|
374
|
-
// Convert PascalCase to kebab-case: WccCounter → wcc-counter
|
|
375
|
-
const kebab = prop.replace(/([A-Z])/g, '-$1').toLowerCase().replace(/^-/, '')
|
|
376
|
-
|
|
377
|
-
// Check if it starts with the prefix
|
|
378
|
-
if (!kebab.startsWith(prefix)) return undefined
|
|
379
|
-
|
|
380
|
-
const ctor = customElements.get(kebab)
|
|
381
|
-
if (!ctor || !(ctor).__meta) return undefined
|
|
382
|
-
|
|
383
|
-
const wrapper = wrapWccComponent(ctor)
|
|
384
|
-
target[prop] = wrapper
|
|
385
|
-
return wrapper
|
|
386
|
-
}
|
|
387
|
-
})
|
|
388
|
-
}
|
package/integrations/react.js
CHANGED
|
@@ -643,9 +643,102 @@ export function wccReactPlugin(options = {}) {
|
|
|
643
643
|
const openingElement = path.node.openingElement
|
|
644
644
|
const nameNode = openingElement.name
|
|
645
645
|
|
|
646
|
+
// ── Compound component transform ──
|
|
647
|
+
// <WccCard.Header>children</WccCard.Header> → <div slot="header" style={{display:'contents'}}>children</div>
|
|
648
|
+
// <WccCard.Stats>{(likes) => <span>{likes}</span>}</WccCard.Stats> → scoped slot div
|
|
649
|
+
if (nameNode.type === 'JSXMemberExpression') {
|
|
650
|
+
const objectName = nameNode.object?.name // e.g., 'WccCard'
|
|
651
|
+
const propName = nameNode.property?.name // e.g., 'Header'
|
|
652
|
+
if (!objectName || !propName) return
|
|
653
|
+
|
|
654
|
+
// Convert PascalCase object to kebab-case and check if it's a custom element
|
|
655
|
+
const kebab = objectName.replace(/([A-Z])/g, '-$1').toLowerCase().replace(/^-/, '')
|
|
656
|
+
if (!kebab.includes('-')) return
|
|
657
|
+
if (prefix && !kebab.startsWith(prefix)) return
|
|
658
|
+
|
|
659
|
+
// Derive slot name: Header → header, FooterNav → footerNav (lcfirst)
|
|
660
|
+
const slotName = propName[0].toLowerCase() + propName.slice(1)
|
|
661
|
+
|
|
662
|
+
// Check if children is a function (scoped slot / render prop pattern)
|
|
663
|
+
const children = path.node.children
|
|
664
|
+
const isScopedSlot = children.length === 1
|
|
665
|
+
&& children[0].type === 'JSXExpressionContainer'
|
|
666
|
+
&& children[0].expression.type === 'ArrowFunctionExpression'
|
|
667
|
+
|
|
668
|
+
if (isScopedSlot) {
|
|
669
|
+
// Scoped slot: <WccCard.Stats>{(likes) => <span>{likes}</span>}</WccCard.Stats>
|
|
670
|
+
const arrowFn = children[0].expression
|
|
671
|
+
const params = (arrowFn.params || []).map(p => p.name || '')
|
|
672
|
+
const body = arrowFn.body
|
|
673
|
+
|
|
674
|
+
// Warn on unsupported expressions
|
|
675
|
+
const renderWarnings = []
|
|
676
|
+
serializeJsxToHtml(body, params, renderWarnings)
|
|
677
|
+
if (renderWarnings.length > 0) {
|
|
678
|
+
pluginCtx.warn(`[wcc-react] ${id} — ${objectName}.${propName}: ${renderWarnings[0]}`)
|
|
679
|
+
return
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
// Replace with scoped slot element
|
|
683
|
+
const scopedEl = generateScopedSlotElement(slotName, params, body)
|
|
684
|
+
path.replaceWith(scopedEl)
|
|
685
|
+
transformed = true
|
|
686
|
+
} else {
|
|
687
|
+
// Named slot: <WccCard.Header><strong>Title</strong></WccCard.Header>
|
|
688
|
+
// → <div slot="header" style={{display:'contents'}}>children</div>
|
|
689
|
+
const slotAttr = {
|
|
690
|
+
type: 'JSXAttribute',
|
|
691
|
+
name: { type: 'JSXIdentifier', name: 'slot' },
|
|
692
|
+
value: { type: 'StringLiteral', value: slotName }
|
|
693
|
+
}
|
|
694
|
+
const styleAttr = {
|
|
695
|
+
type: 'JSXAttribute',
|
|
696
|
+
name: { type: 'JSXIdentifier', name: 'style' },
|
|
697
|
+
value: {
|
|
698
|
+
type: 'JSXExpressionContainer',
|
|
699
|
+
expression: {
|
|
700
|
+
type: 'ObjectExpression',
|
|
701
|
+
properties: [{
|
|
702
|
+
type: 'ObjectProperty',
|
|
703
|
+
key: { type: 'Identifier', name: 'display' },
|
|
704
|
+
value: { type: 'StringLiteral', value: 'contents' },
|
|
705
|
+
computed: false,
|
|
706
|
+
shorthand: false
|
|
707
|
+
}]
|
|
708
|
+
}
|
|
709
|
+
}
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
openingElement.name = { type: 'JSXIdentifier', name: 'div' }
|
|
713
|
+
openingElement.attributes = [...openingElement.attributes, slotAttr, styleAttr]
|
|
714
|
+
if (path.node.closingElement) {
|
|
715
|
+
path.node.closingElement.name = { type: 'JSXIdentifier', name: 'div' }
|
|
716
|
+
}
|
|
717
|
+
transformed = true
|
|
718
|
+
}
|
|
719
|
+
return
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
// ── PascalCase custom element transform ──
|
|
723
|
+
// <WccCard> → <wcc-card> (only if it maps to a hyphenated tag)
|
|
724
|
+
if (nameNode.type === 'JSXIdentifier' && /^[A-Z]/.test(nameNode.name)) {
|
|
725
|
+
const kebab = nameNode.name.replace(/([A-Z])/g, '-$1').toLowerCase().replace(/^-/, '')
|
|
726
|
+
if (!kebab.includes('-')) return
|
|
727
|
+
if (prefix && !kebab.startsWith(prefix)) return
|
|
728
|
+
|
|
729
|
+
// Rewrite tag name to kebab-case
|
|
730
|
+
openingElement.name = { type: 'JSXIdentifier', name: kebab }
|
|
731
|
+
if (path.node.closingElement) {
|
|
732
|
+
path.node.closingElement.name = { type: 'JSXIdentifier', name: kebab }
|
|
733
|
+
}
|
|
734
|
+
transformed = true
|
|
735
|
+
// Fall through to process props on this element
|
|
736
|
+
}
|
|
737
|
+
|
|
646
738
|
// Only process elements with hyphenated tag names (custom elements)
|
|
647
|
-
|
|
648
|
-
|
|
739
|
+
const currentName = openingElement.name
|
|
740
|
+
if (currentName.type !== 'JSXIdentifier') return
|
|
741
|
+
const tagName = currentName.name
|
|
649
742
|
if (!tagName.includes('-')) return
|
|
650
743
|
|
|
651
744
|
// Apply prefix filtering if set
|
|
@@ -748,15 +841,15 @@ export function wccReactPlugin(options = {}) {
|
|
|
748
841
|
|
|
749
842
|
|
|
750
843
|
/**
|
|
751
|
-
* Vite plugin that generates a virtual module
|
|
752
|
-
*
|
|
753
|
-
*
|
|
844
|
+
* Vite plugin that generates a virtual module with component stubs for
|
|
845
|
+
* PascalCase imports. These stubs satisfy the linter/IDE (component is "defined")
|
|
846
|
+
* and the wccReactPlugin transforms them to native custom elements at build time.
|
|
754
847
|
*
|
|
755
|
-
* This enables the
|
|
848
|
+
* This enables the standard React import pattern:
|
|
756
849
|
* import { WccCounter, WccCard } from '@wcc/react'
|
|
757
850
|
*
|
|
758
|
-
* The
|
|
759
|
-
*
|
|
851
|
+
* The stubs are zero-runtime — they're just tag name strings with slot name
|
|
852
|
+
* properties. The wccReactPlugin handles the actual JSX transformation.
|
|
760
853
|
*
|
|
761
854
|
* @param {Object} [options]
|
|
762
855
|
* @param {string} [options.moduleId='@wcc/react'] - Virtual module ID for imports
|
|
@@ -769,18 +862,25 @@ export function wccReactPlugin(options = {}) {
|
|
|
769
862
|
* import { wccReactPlugin, wccReactComponents } from '@sprlab/wccompiler/integrations/react'
|
|
770
863
|
* export default {
|
|
771
864
|
* plugins: [
|
|
865
|
+
* wccReactComponents({ componentsDir: './src/wcc' }),
|
|
772
866
|
* wccReactPlugin(),
|
|
773
|
-
*
|
|
867
|
+
* react()
|
|
774
868
|
* ]
|
|
775
869
|
* }
|
|
776
870
|
* ```
|
|
777
871
|
*
|
|
778
872
|
* @example Component.jsx
|
|
779
873
|
* ```jsx
|
|
780
|
-
* import {
|
|
874
|
+
* import { WccCard, WccList } from '@wcc/react'
|
|
781
875
|
*
|
|
782
|
-
* <
|
|
783
|
-
*
|
|
876
|
+
* <WccCard>
|
|
877
|
+
* <WccCard.Header><strong>Title</strong></WccCard.Header>
|
|
878
|
+
* <p>Body</p>
|
|
879
|
+
* </WccCard>
|
|
880
|
+
*
|
|
881
|
+
* <WccList>
|
|
882
|
+
* <WccList.Item>{(item) => <li>{item}</li>}</WccList.Item>
|
|
883
|
+
* </WccList>
|
|
784
884
|
* ```
|
|
785
885
|
*/
|
|
786
886
|
export function wccReactComponents(options = {}) {
|
|
@@ -820,7 +920,6 @@ export function wccReactComponents(options = {}) {
|
|
|
820
920
|
if (!metaMatch) continue
|
|
821
921
|
|
|
822
922
|
try {
|
|
823
|
-
// Parse the meta object (it's a JS object literal, evaluate safely)
|
|
824
923
|
const metaStr = metaMatch[1]
|
|
825
924
|
.replace(/'/g, '"')
|
|
826
925
|
.replace(/(\w+):/g, '"$1":')
|
|
@@ -841,21 +940,28 @@ export function wccReactComponents(options = {}) {
|
|
|
841
940
|
return 'export {}'
|
|
842
941
|
}
|
|
843
942
|
|
|
844
|
-
// Generate
|
|
845
|
-
|
|
943
|
+
// Generate lightweight stubs (zero runtime)
|
|
944
|
+
// The wccReactPlugin transforms these at build time
|
|
945
|
+
let code = '// Auto-generated WCC component stubs (transformed by wccReactPlugin at build time)\n'
|
|
846
946
|
|
|
847
|
-
// Import each component file to ensure registration
|
|
947
|
+
// Import each component file to ensure custom element registration
|
|
848
948
|
for (const comp of components) {
|
|
849
949
|
code += `import '${path.default.resolve(dir, comp.file)}';\n`
|
|
850
950
|
}
|
|
851
951
|
|
|
852
952
|
code += '\n'
|
|
853
953
|
|
|
854
|
-
// Generate
|
|
954
|
+
// Generate stub exports with compound slot properties
|
|
955
|
+
// Use Object.assign to create an object that holds the tag name and slot sub-properties
|
|
855
956
|
for (const comp of components) {
|
|
856
|
-
const
|
|
857
|
-
const
|
|
858
|
-
|
|
957
|
+
const slots = comp.meta.slots || []
|
|
958
|
+
code += `export const ${comp.pascalName} = Object.assign(() => '${comp.meta.tag}', { __tag: '${comp.meta.tag}'`
|
|
959
|
+
for (const slot of slots) {
|
|
960
|
+
if (!slot) continue
|
|
961
|
+
const pascalSlot = slot[0].toUpperCase() + slot.slice(1)
|
|
962
|
+
code += `, ${pascalSlot}: '${slot}'`
|
|
963
|
+
}
|
|
964
|
+
code += ` });\n`
|
|
859
965
|
}
|
|
860
966
|
|
|
861
967
|
return code
|
package/package.json
CHANGED