@sprlab/wccompiler 0.10.2 → 0.10.3
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 +36 -11
- package/integrations/react.js +117 -0
- package/package.json +1 -1
package/adapters/react.js
CHANGED
|
@@ -105,11 +105,33 @@ export function useWccModel(propName, value, setValue, existingRef) {
|
|
|
105
105
|
}
|
|
106
106
|
|
|
107
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
|
+
|
|
108
129
|
/**
|
|
109
130
|
* Creates a React wrapper component for a WCC custom element.
|
|
110
131
|
*
|
|
111
132
|
* The wrapper provides idiomatic React DX:
|
|
112
|
-
* - Event props: `onChange`, `
|
|
133
|
+
* - Event props: `onChange`, `onCountChange` → automatically wired via addEventListener
|
|
134
|
+
* Handlers receive the unwrapped value (event.detail), not the raw CustomEvent.
|
|
113
135
|
* - Model props: two-way binding via attribute + event listener
|
|
114
136
|
* - Regular props: passed as attributes on the custom element
|
|
115
137
|
* - Children: passed through as-is (use `<div slot="name">` for named slots)
|
|
@@ -118,9 +140,9 @@ export function useWccModel(propName, value, setValue, existingRef) {
|
|
|
118
140
|
* @param {string} tagName - The custom element tag name (e.g., 'wcc-card')
|
|
119
141
|
* @param {Object} [config] - Configuration for the wrapper
|
|
120
142
|
* @param {string[]} [config.events] - Custom event names to expose as onEventName props
|
|
121
|
-
* Event names are converted: 'count-changed' →
|
|
143
|
+
* Event names are converted: 'count-changed' → onCountChange prop (React convention)
|
|
122
144
|
* @param {string[]} [config.models] - Model prop names for two-way binding
|
|
123
|
-
* Each model 'name' creates: `name` prop (sets attribute) + `
|
|
145
|
+
* Each model 'name' creates: `name` prop (sets attribute) + `onNameChange` event
|
|
124
146
|
* @returns {import('react').ForwardRefExoticComponent} A React component
|
|
125
147
|
*
|
|
126
148
|
* @example
|
|
@@ -134,8 +156,8 @@ export function useWccModel(propName, value, setValue, existingRef) {
|
|
|
134
156
|
* return (
|
|
135
157
|
* <WccCounter
|
|
136
158
|
* count={count}
|
|
137
|
-
*
|
|
138
|
-
* onChange={(
|
|
159
|
+
* onCountChange={(value) => setCount(value)}
|
|
160
|
+
* onChange={(value) => console.log('changed', value)}
|
|
139
161
|
* label="Clicks"
|
|
140
162
|
* >
|
|
141
163
|
* <div slot="footer">Footer content</div>
|
|
@@ -147,19 +169,21 @@ export function createWccWrapper(tagName, config = {}) {
|
|
|
147
169
|
const { events = [], models = [] } = config
|
|
148
170
|
|
|
149
171
|
// Build a set of event prop names for quick lookup
|
|
150
|
-
//
|
|
172
|
+
// Convention: kebab-case event → React onCamelCase (without trailing 'd' from 'changed')
|
|
173
|
+
// 'count-changed' → 'onCountChange' (not 'onCountChanged')
|
|
151
174
|
// 'change' → 'onChange'
|
|
175
|
+
// 'value-updated' → 'onValueUpdate' (strips trailing 'd' from past tense)
|
|
152
176
|
const eventPropMap = new Map()
|
|
153
177
|
for (const eventName of events) {
|
|
154
|
-
const propName =
|
|
178
|
+
const propName = toReactEventProp(eventName)
|
|
155
179
|
eventPropMap.set(propName, eventName)
|
|
156
180
|
}
|
|
157
181
|
|
|
158
|
-
// Model events: 'count' → 'count-changed' → '
|
|
182
|
+
// Model events: 'count' → 'count-changed' → 'onCountChange'
|
|
159
183
|
const modelEventMap = new Map()
|
|
160
184
|
for (const modelName of models) {
|
|
161
185
|
const eventName = `${modelName}-changed`
|
|
162
|
-
const propName =
|
|
186
|
+
const propName = toReactEventProp(eventName)
|
|
163
187
|
eventPropMap.set(propName, eventName)
|
|
164
188
|
modelEventMap.set(modelName, eventName)
|
|
165
189
|
}
|
|
@@ -207,7 +231,7 @@ export function createWccWrapper(tagName, config = {}) {
|
|
|
207
231
|
for (const eventName of allEvents) {
|
|
208
232
|
const listener = (e) => {
|
|
209
233
|
const handler = handlersRef.current[eventName]
|
|
210
|
-
if (handler) handler(e)
|
|
234
|
+
if (handler) handler(e instanceof CustomEvent ? e.detail : e)
|
|
211
235
|
}
|
|
212
236
|
el.addEventListener(eventName, listener)
|
|
213
237
|
listeners.push([eventName, listener])
|
|
@@ -268,7 +292,8 @@ export function createWccWrapper(tagName, config = {}) {
|
|
|
268
292
|
* const WccCounter = wrapWccComponent(customElements.get('wcc-counter'))
|
|
269
293
|
*
|
|
270
294
|
* // Use idiomatically — no manual config needed
|
|
271
|
-
*
|
|
295
|
+
* // Handlers receive the value directly (not the event)
|
|
296
|
+
* <WccCounter count={count} onCountChange={setCount} onChange={(val) => console.log(val)} />
|
|
272
297
|
*/
|
|
273
298
|
export function wrapWccComponent(WccClass) {
|
|
274
299
|
const meta = WccClass?.__meta
|
package/integrations/react.js
CHANGED
|
@@ -745,3 +745,120 @@ export function wccReactPlugin(options = {}) {
|
|
|
745
745
|
}
|
|
746
746
|
}
|
|
747
747
|
}
|
|
748
|
+
|
|
749
|
+
|
|
750
|
+
/**
|
|
751
|
+
* Vite plugin that generates a virtual module `@wcc/react` (or custom id)
|
|
752
|
+
* exporting pre-built React wrapper components for all WCC components found
|
|
753
|
+
* in the project's compiled output directory.
|
|
754
|
+
*
|
|
755
|
+
* This enables the ideal import experience:
|
|
756
|
+
* import { WccCounter, WccCard } from '@wcc/react'
|
|
757
|
+
*
|
|
758
|
+
* The plugin scans the output directory for compiled .js files, reads their
|
|
759
|
+
* `static __meta` declarations, and generates wrapper code using createWccWrapper.
|
|
760
|
+
*
|
|
761
|
+
* @param {Object} [options]
|
|
762
|
+
* @param {string} [options.moduleId='@wcc/react'] - Virtual module ID for imports
|
|
763
|
+
* @param {string} [options.componentsDir='./dist'] - Directory containing compiled WCC .js files
|
|
764
|
+
* @param {string} [options.prefix='wcc-'] - Tag prefix filter
|
|
765
|
+
* @returns {import('vite').Plugin}
|
|
766
|
+
*
|
|
767
|
+
* @example vite.config.js
|
|
768
|
+
* ```js
|
|
769
|
+
* import { wccReactPlugin, wccReactComponents } from '@sprlab/wccompiler/integrations/react'
|
|
770
|
+
* export default {
|
|
771
|
+
* plugins: [
|
|
772
|
+
* wccReactPlugin(),
|
|
773
|
+
* wccReactComponents({ componentsDir: './src/wcc' })
|
|
774
|
+
* ]
|
|
775
|
+
* }
|
|
776
|
+
* ```
|
|
777
|
+
*
|
|
778
|
+
* @example Component.jsx
|
|
779
|
+
* ```jsx
|
|
780
|
+
* import { WccCounter, WccCard } from '@wcc/react'
|
|
781
|
+
*
|
|
782
|
+
* <WccCounter count={count} onCountChange={setCount} />
|
|
783
|
+
* <WccCard><div slot="header">Title</div></WccCard>
|
|
784
|
+
* ```
|
|
785
|
+
*/
|
|
786
|
+
export function wccReactComponents(options = {}) {
|
|
787
|
+
const {
|
|
788
|
+
moduleId = '@wcc/react',
|
|
789
|
+
componentsDir = './dist',
|
|
790
|
+
prefix = 'wcc-'
|
|
791
|
+
} = options
|
|
792
|
+
|
|
793
|
+
const resolvedId = '\0' + moduleId
|
|
794
|
+
|
|
795
|
+
return {
|
|
796
|
+
name: 'vite-plugin-wcc-react-components',
|
|
797
|
+
resolveId(id) {
|
|
798
|
+
if (id === moduleId) return resolvedId
|
|
799
|
+
return null
|
|
800
|
+
},
|
|
801
|
+
async load(id) {
|
|
802
|
+
if (id !== resolvedId) return null
|
|
803
|
+
|
|
804
|
+
// Scan componentsDir for .js files and extract __meta
|
|
805
|
+
const fs = await import('fs')
|
|
806
|
+
const path = await import('path')
|
|
807
|
+
|
|
808
|
+
const dir = path.default.resolve(componentsDir)
|
|
809
|
+
if (!fs.default.existsSync(dir)) {
|
|
810
|
+
this.warn(`[wcc-react-components] Directory not found: ${dir}`)
|
|
811
|
+
return 'export {}'
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
const files = fs.default.readdirSync(dir).filter(f => f.endsWith('.js'))
|
|
815
|
+
const components = []
|
|
816
|
+
|
|
817
|
+
for (const file of files) {
|
|
818
|
+
const content = fs.default.readFileSync(path.default.join(dir, file), 'utf-8')
|
|
819
|
+
const metaMatch = content.match(/static __meta\s*=\s*(\{[^}]+\})/)
|
|
820
|
+
if (!metaMatch) continue
|
|
821
|
+
|
|
822
|
+
try {
|
|
823
|
+
// Parse the meta object (it's a JS object literal, evaluate safely)
|
|
824
|
+
const metaStr = metaMatch[1]
|
|
825
|
+
.replace(/'/g, '"')
|
|
826
|
+
.replace(/(\w+):/g, '"$1":')
|
|
827
|
+
.replace(/,\s*}/g, '}')
|
|
828
|
+
.replace(/,\s*]/g, ']')
|
|
829
|
+
const meta = JSON.parse(metaStr)
|
|
830
|
+
|
|
831
|
+
if (!meta.tag || !meta.tag.startsWith(prefix)) continue
|
|
832
|
+
|
|
833
|
+
const pascalName = meta.tag.split('-').map(s => s[0].toUpperCase() + s.slice(1)).join('')
|
|
834
|
+
components.push({ meta, pascalName, file })
|
|
835
|
+
} catch (e) {
|
|
836
|
+
// Skip files with unparseable meta
|
|
837
|
+
}
|
|
838
|
+
}
|
|
839
|
+
|
|
840
|
+
if (components.length === 0) {
|
|
841
|
+
return 'export {}'
|
|
842
|
+
}
|
|
843
|
+
|
|
844
|
+
// Generate the virtual module code
|
|
845
|
+
let code = `import { createWccWrapper } from '@sprlab/wccompiler/adapters/react';\n`
|
|
846
|
+
|
|
847
|
+
// Import each component file to ensure registration
|
|
848
|
+
for (const comp of components) {
|
|
849
|
+
code += `import '${path.default.resolve(dir, comp.file)}';\n`
|
|
850
|
+
}
|
|
851
|
+
|
|
852
|
+
code += '\n'
|
|
853
|
+
|
|
854
|
+
// Generate wrapper exports
|
|
855
|
+
for (const comp of components) {
|
|
856
|
+
const eventsArr = JSON.stringify(comp.meta.events || [])
|
|
857
|
+
const modelsArr = JSON.stringify(comp.meta.models || [])
|
|
858
|
+
code += `export const ${comp.pascalName} = createWccWrapper('${comp.meta.tag}', { events: ${eventsArr}, models: ${modelsArr} });\n`
|
|
859
|
+
}
|
|
860
|
+
|
|
861
|
+
return code
|
|
862
|
+
}
|
|
863
|
+
}
|
|
864
|
+
}
|
package/package.json
CHANGED