@zeus-js/output-react-wrapper 0.1.0-beta.2 → 0.1.0-beta.4
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.
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* output-react-wrapper v0.1.0-beta.
|
|
2
|
+
* output-react-wrapper v0.1.0-beta.4
|
|
3
3
|
* (c) 2026 baicie
|
|
4
4
|
* Released under the MIT License.
|
|
5
5
|
**/
|
|
@@ -20,160 +20,293 @@ function generateReactIndex(components, options) {
|
|
|
20
20
|
return lines.join("\n");
|
|
21
21
|
}
|
|
22
22
|
//#endregion
|
|
23
|
-
//#region packages/web-c/output-react-wrapper/src/naming.ts
|
|
24
|
-
function toReactEventProp(eventName) {
|
|
25
|
-
return "on" + eventName.split("-").filter(Boolean).map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join("");
|
|
26
|
-
}
|
|
27
|
-
//#endregion
|
|
28
23
|
//#region packages/web-c/output-react-wrapper/src/generateReactWrapper.ts
|
|
29
24
|
function generateReactWrapper(input) {
|
|
25
|
+
return input.mode === "event-bridge" ? generateEventBridgeReactWrapper(input) : generateMinimalReactWrapper(input);
|
|
26
|
+
}
|
|
27
|
+
function generateMinimalReactWrapper(input) {
|
|
30
28
|
const { component, namedSlots, wcModuleId } = input;
|
|
31
|
-
const
|
|
32
|
-
const
|
|
33
|
-
const
|
|
34
|
-
const
|
|
35
|
-
const
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
29
|
+
const slotBindings = createBindings(getNamedSlots(component, namedSlots), "slotValue");
|
|
30
|
+
const omittedKeys = ["children", ...slotBindings.map(({ sourceName }) => sourceName)];
|
|
31
|
+
const slotAssignments = generatePropAssignments(slotBindings);
|
|
32
|
+
const namedSlotLines = generateMinimalNamedSlots(slotBindings);
|
|
33
|
+
const childSetup = slotBindings.length ? `${namedSlotLines}
|
|
34
|
+
const childArgs = [];
|
|
35
|
+
pushAll(childArgs, slotNodes);
|
|
36
|
+
if (children != null) childArgs.push(children);` : "";
|
|
37
|
+
const render = slotBindings.length ? `React.createElement.apply(
|
|
38
|
+
React,
|
|
39
|
+
[${JSON.stringify(component.tag)}, rest].concat(childArgs),
|
|
40
|
+
)` : `React.createElement(${JSON.stringify(component.tag)}, rest, children)`;
|
|
41
41
|
return `
|
|
42
|
-
import
|
|
43
|
-
createElement,
|
|
44
|
-
cloneElement,
|
|
45
|
-
Fragment,
|
|
46
|
-
forwardRef,
|
|
47
|
-
isValidElement,
|
|
48
|
-
useEffect,
|
|
49
|
-
useImperativeHandle,
|
|
50
|
-
useRef,
|
|
51
|
-
} from 'react';
|
|
42
|
+
import * as React from 'react';
|
|
52
43
|
|
|
53
44
|
import ${JSON.stringify(wcModuleId)};
|
|
54
45
|
|
|
55
|
-
const
|
|
56
|
-
const EVENT_MAP = ${JSON.stringify(createReactEventMap(eventNames))};
|
|
57
|
-
const NAMED_SLOTS = ${JSON.stringify(slotNames)};
|
|
58
|
-
|
|
59
|
-
export const ${component.name} = forwardRef(function ${component.name}(props, ref) {
|
|
60
|
-
const {
|
|
61
|
-
children,
|
|
62
|
-
className,
|
|
63
|
-
style,
|
|
64
|
-
${destructuredProps}
|
|
65
|
-
...rest
|
|
66
|
-
} = props;
|
|
46
|
+
const OMITTED_PROPS = new Set(${JSON.stringify(omittedKeys)});
|
|
67
47
|
|
|
68
|
-
|
|
48
|
+
export const ${component.name} = React.forwardRef(
|
|
49
|
+
function ${component.name}(inputProps, ref) {
|
|
50
|
+
const props = inputProps || {};
|
|
51
|
+
const children = props.children;
|
|
52
|
+
${slotAssignments}
|
|
53
|
+
const rest = omitProps(props);
|
|
54
|
+
rest.ref = ref;
|
|
55
|
+
${childSetup}
|
|
69
56
|
|
|
70
|
-
|
|
57
|
+
return ${render};
|
|
58
|
+
},
|
|
59
|
+
);
|
|
71
60
|
|
|
72
|
-
|
|
61
|
+
function omitProps(source) {
|
|
62
|
+
const output = {};
|
|
63
|
+
for (const key in source) {
|
|
64
|
+
if (hasOwn(source, key) && !OMITTED_PROPS.has(key)) {
|
|
65
|
+
output[key] = source[key];
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
return output;
|
|
69
|
+
}
|
|
73
70
|
|
|
74
|
-
|
|
71
|
+
function hasOwn(source, key) {
|
|
72
|
+
return Object.prototype.hasOwnProperty.call(source, key);
|
|
73
|
+
}
|
|
74
|
+
${slotBindings.length ? PUSH_ALL_HELPER : ""}
|
|
75
|
+
`.trimStart();
|
|
76
|
+
}
|
|
77
|
+
function generateEventBridgeReactWrapper(input) {
|
|
78
|
+
const { component, namedSlots, wcModuleId } = input;
|
|
79
|
+
const propBindings = createBindings(Object.keys(component.props), "propValue");
|
|
80
|
+
const eventBindings = createEventBindings(component.events);
|
|
81
|
+
if (!propBindings.length && !eventBindings.length) return generateMinimalReactWrapper(input);
|
|
82
|
+
const slotBindings = createBindings(getNamedSlots(component, namedSlots), "slotValue");
|
|
83
|
+
const destructuredBindings = [
|
|
84
|
+
...propBindings,
|
|
85
|
+
...eventBindings,
|
|
86
|
+
...slotBindings
|
|
87
|
+
];
|
|
88
|
+
const omittedKeys = [
|
|
89
|
+
"children",
|
|
90
|
+
"className",
|
|
91
|
+
"style",
|
|
92
|
+
...destructuredBindings.map(({ sourceName }) => sourceName)
|
|
93
|
+
];
|
|
94
|
+
return `
|
|
95
|
+
import {
|
|
96
|
+
${[
|
|
97
|
+
"createElement",
|
|
98
|
+
...slotBindings.length ? [
|
|
99
|
+
"cloneElement",
|
|
100
|
+
"Fragment",
|
|
101
|
+
"isValidElement"
|
|
102
|
+
] : [],
|
|
103
|
+
"forwardRef",
|
|
104
|
+
...propBindings.length || eventBindings.length ? ["useEffect"] : [],
|
|
105
|
+
"useImperativeHandle",
|
|
106
|
+
"useRef"
|
|
107
|
+
].join(",\n ")},
|
|
108
|
+
} from 'react';
|
|
75
109
|
|
|
76
|
-
|
|
110
|
+
import ${JSON.stringify(wcModuleId)};
|
|
77
111
|
|
|
78
|
-
|
|
112
|
+
const OMITTED_PROPS = new Set(${JSON.stringify(omittedKeys)});
|
|
113
|
+
${eventBindings.length ? `const EVENT_NAMES = ${JSON.stringify(eventBindings.map((binding) => binding.eventName))};` : ""}
|
|
79
114
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
115
|
+
export const ${component.name} = forwardRef(function ${component.name}(inputProps, ref) {
|
|
116
|
+
const props = inputProps || {};
|
|
117
|
+
const children = props.children;
|
|
118
|
+
const className = props.className;
|
|
119
|
+
const style = props.style;
|
|
120
|
+
${generatePropAssignments(destructuredBindings)}
|
|
121
|
+
${generatePropPresenceAssignments(propBindings)}
|
|
122
|
+
const rest = omitProps(props);
|
|
83
123
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
ref: innerRef,
|
|
89
|
-
className,
|
|
90
|
-
style,
|
|
91
|
-
},
|
|
92
|
-
...slotChildren,
|
|
93
|
-
);
|
|
94
|
-
});
|
|
124
|
+
const innerRef = useRef(null);
|
|
125
|
+
${generatePropRefs(propBindings)}
|
|
126
|
+
${generateEventRefs(eventBindings)}
|
|
127
|
+
useImperativeHandle(ref, () => innerRef.current, []);
|
|
95
128
|
|
|
96
|
-
|
|
97
|
-
|
|
129
|
+
${generatePropSyncEffect(propBindings)}
|
|
130
|
+
${generateEventEffect(eventBindings)}
|
|
131
|
+
${generateChildrenSetup(slotBindings)}
|
|
132
|
+
rest.ref = innerRef;
|
|
133
|
+
rest.className = className;
|
|
134
|
+
rest.style = style;
|
|
98
135
|
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
136
|
+
return ${generateReactRender(component.tag, slotBindings)};
|
|
137
|
+
});
|
|
138
|
+
${slotBindings.length ? NAMED_SLOT_HELPER : ""}
|
|
139
|
+
function omitProps(source) {
|
|
140
|
+
const output = {};
|
|
141
|
+
for (const key in source) {
|
|
142
|
+
if (hasOwn(source, key) && !OMITTED_PROPS.has(key)) {
|
|
143
|
+
output[key] = source[key];
|
|
144
|
+
}
|
|
104
145
|
}
|
|
146
|
+
return output;
|
|
147
|
+
}
|
|
105
148
|
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
{
|
|
109
|
-
slot: name,
|
|
110
|
-
style: { display: 'contents' },
|
|
111
|
-
},
|
|
112
|
-
value,
|
|
113
|
-
);
|
|
149
|
+
function hasOwn(source, key) {
|
|
150
|
+
return Object.prototype.hasOwnProperty.call(source, key);
|
|
114
151
|
}
|
|
115
152
|
`.trimStart();
|
|
116
153
|
}
|
|
117
|
-
function
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
154
|
+
function createBindings(names, prefix) {
|
|
155
|
+
return names.map((sourceName, index) => ({
|
|
156
|
+
sourceName,
|
|
157
|
+
localName: `${prefix}${index}`
|
|
158
|
+
}));
|
|
159
|
+
}
|
|
160
|
+
function createEventBindings(events) {
|
|
161
|
+
return Object.entries(events).map(([key, event], index) => {
|
|
162
|
+
var _event$key, _event$name, _event$reactName;
|
|
163
|
+
const sourceEventName = (_event$key = event.key) !== null && _event$key !== void 0 ? _event$key : key;
|
|
164
|
+
return {
|
|
165
|
+
eventName: (_event$name = event.name) !== null && _event$name !== void 0 ? _event$name : toKebabCase(sourceEventName),
|
|
166
|
+
sourceName: (_event$reactName = event.reactName) !== null && _event$reactName !== void 0 ? _event$reactName : toReactEventProp(sourceEventName),
|
|
167
|
+
localName: `eventHandler${index}`
|
|
168
|
+
};
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
function generatePropAssignments(bindings) {
|
|
172
|
+
return bindings.map(({ sourceName, localName }) => ` const ${localName} = props[${JSON.stringify(sourceName)}];`).join("\n");
|
|
173
|
+
}
|
|
174
|
+
function generatePropPresenceAssignments(bindings) {
|
|
175
|
+
return bindings.map(({ sourceName }, index) => ` const propPresent${index} = hasOwn(props, ${JSON.stringify(sourceName)});`).join("\n");
|
|
121
176
|
}
|
|
122
|
-
function
|
|
123
|
-
if (!
|
|
124
|
-
return `
|
|
177
|
+
function generatePropRefs(bindings) {
|
|
178
|
+
if (!bindings.length) return "";
|
|
179
|
+
return ` const previousPropPresenceRef = useRef([]);
|
|
180
|
+
const previousPropValuesRef = useRef([]);`;
|
|
181
|
+
}
|
|
182
|
+
function generateEventRefs(bindings) {
|
|
183
|
+
if (!bindings.length) return "";
|
|
184
|
+
return ` const eventHandlersRef = useRef([]);
|
|
185
|
+
${bindings.map(({ localName }, index) => ` eventHandlersRef.current[${index}] = ${localName};`).join("\n")}`;
|
|
186
|
+
}
|
|
187
|
+
function generatePropSyncEffect(bindings) {
|
|
188
|
+
if (!bindings.length) return "";
|
|
189
|
+
return ` useEffect(() => {
|
|
125
190
|
const el = innerRef.current;
|
|
126
191
|
if (!el) return;
|
|
127
192
|
|
|
128
|
-
|
|
129
|
-
|
|
193
|
+
const previousPropPresence = previousPropPresenceRef.current;
|
|
194
|
+
const previousPropValues = previousPropValuesRef.current;
|
|
195
|
+
|
|
196
|
+
${bindings.map(({ sourceName, localName }, index) => {
|
|
197
|
+
const key = JSON.stringify(sourceName);
|
|
198
|
+
return ` if (propPresent${index}) {
|
|
199
|
+
if (
|
|
200
|
+
!previousPropPresence[${index}] ||
|
|
201
|
+
!Object.is(previousPropValues[${index}], ${localName})
|
|
202
|
+
) {
|
|
203
|
+
el[${key}] = ${localName};
|
|
204
|
+
previousPropValues[${index}] = ${localName};
|
|
205
|
+
}
|
|
206
|
+
previousPropPresence[${index}] = true;
|
|
207
|
+
} else if (previousPropPresence[${index}]) {
|
|
208
|
+
el[${key}] = undefined;
|
|
209
|
+
previousPropPresence[${index}] = false;
|
|
210
|
+
previousPropValues[${index}] = undefined;
|
|
211
|
+
}`;
|
|
212
|
+
}).join("\n\n")}
|
|
213
|
+
}, [${bindings.flatMap((binding, index) => [`propPresent${index}`, binding.localName]).join(", ")}]);
|
|
214
|
+
`;
|
|
130
215
|
}
|
|
131
|
-
function
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
return `
|
|
135
|
-
useEffect(() => {
|
|
216
|
+
function generateEventEffect(bindings) {
|
|
217
|
+
if (!bindings.length) return "";
|
|
218
|
+
return ` useEffect(() => {
|
|
136
219
|
const el = innerRef.current;
|
|
137
|
-
if (!el
|
|
220
|
+
if (!el) return;
|
|
138
221
|
|
|
139
|
-
const
|
|
140
|
-
|
|
141
|
-
|
|
222
|
+
const listeners = EVENT_NAMES.map(
|
|
223
|
+
(_eventName, index) => event => {
|
|
224
|
+
const handler = eventHandlersRef.current[index];
|
|
225
|
+
if (handler) handler(event);
|
|
226
|
+
},
|
|
227
|
+
);
|
|
142
228
|
|
|
143
|
-
|
|
229
|
+
for (let index = 0; index < EVENT_NAMES.length; index += 1) {
|
|
230
|
+
el.addEventListener(EVENT_NAMES[index], listeners[index]);
|
|
231
|
+
}
|
|
144
232
|
|
|
145
233
|
return () => {
|
|
146
|
-
|
|
234
|
+
for (let index = 0; index < EVENT_NAMES.length; index += 1) {
|
|
235
|
+
el.removeEventListener(EVENT_NAMES[index], listeners[index]);
|
|
236
|
+
}
|
|
147
237
|
};
|
|
148
|
-
}, [
|
|
238
|
+
}, []);
|
|
149
239
|
`;
|
|
150
|
-
}).join("");
|
|
151
240
|
}
|
|
152
|
-
function
|
|
153
|
-
if (
|
|
154
|
-
return
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
return namedSlots.map((name) => {
|
|
158
|
-
return `
|
|
159
|
-
{
|
|
160
|
-
const node = createNamedSlot(${JSON.stringify(name)}, ${name});
|
|
241
|
+
function generateChildrenSetup(bindings) {
|
|
242
|
+
if (!bindings.length) return "";
|
|
243
|
+
return ` const slotChildren = [];
|
|
244
|
+
${bindings.map(({ sourceName, localName }) => ` {
|
|
245
|
+
const node = createNamedSlot(${JSON.stringify(sourceName)}, ${localName});
|
|
161
246
|
if (node != null) slotChildren.push(node);
|
|
162
|
-
}
|
|
247
|
+
}`).join("\n")}
|
|
248
|
+
if (children != null) slotChildren.push(children);
|
|
249
|
+
`;
|
|
250
|
+
}
|
|
251
|
+
function generateReactRender(tag, slotBindings) {
|
|
252
|
+
if (!slotBindings.length) return `createElement(${JSON.stringify(tag)}, rest, children)`;
|
|
253
|
+
return `createElement.apply(
|
|
254
|
+
null,
|
|
255
|
+
[${JSON.stringify(tag)}, rest].concat(slotChildren),
|
|
256
|
+
)`;
|
|
257
|
+
}
|
|
258
|
+
function getNamedSlots(component, namedSlots) {
|
|
259
|
+
return namedSlots === "none" ? [] : Object.keys(component.slots).filter((name) => name !== "default");
|
|
260
|
+
}
|
|
261
|
+
function generateMinimalNamedSlots(bindings) {
|
|
262
|
+
if (!bindings.length) return "";
|
|
263
|
+
return `${bindings.map(({ sourceName, localName }, index) => {
|
|
264
|
+
return ` const slotNode${index} = ${localName} != null && ${localName} !== false
|
|
265
|
+
? (React.isValidElement(${localName}) && ${localName}.type !== React.Fragment
|
|
266
|
+
? React.cloneElement(${localName}, { slot: ${JSON.stringify(sourceName)} })
|
|
267
|
+
: React.createElement('span', { slot: ${JSON.stringify(sourceName)}, style: { display: 'contents' } }, ${localName}))
|
|
268
|
+
: null;`;
|
|
269
|
+
}).join("\n")}
|
|
270
|
+
const slotNodes = [${bindings.map((_, index) => `slotNode${index}`).join(", ")}].filter(Boolean);`;
|
|
271
|
+
}
|
|
272
|
+
function toKebabCase(value) {
|
|
273
|
+
return value.replace(/[A-Z]/g, (match) => `-${match.toLowerCase()}`);
|
|
274
|
+
}
|
|
275
|
+
function toReactEventProp(value) {
|
|
276
|
+
return `on${value.split("-").filter(Boolean).map((part) => `${part.charAt(0).toUpperCase()}${part.slice(1)}`).join("")}`;
|
|
277
|
+
}
|
|
278
|
+
const PUSH_ALL_HELPER = `
|
|
279
|
+
function pushAll(target, values) {
|
|
280
|
+
for (const value of values) target.push(value);
|
|
281
|
+
}
|
|
163
282
|
`;
|
|
164
|
-
|
|
283
|
+
const NAMED_SLOT_HELPER = `
|
|
284
|
+
function createNamedSlot(name, value) {
|
|
285
|
+
if (value == null || value === false) return null;
|
|
286
|
+
|
|
287
|
+
if (isValidElement(value) && value.type !== Fragment) {
|
|
288
|
+
return cloneElement(value, { slot: name });
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
return createElement(
|
|
292
|
+
'span',
|
|
293
|
+
{ slot: name, style: { display: 'contents' } },
|
|
294
|
+
value,
|
|
295
|
+
);
|
|
165
296
|
}
|
|
297
|
+
`;
|
|
166
298
|
//#endregion
|
|
167
299
|
//#region packages/web-c/output-react-wrapper/src/index.ts
|
|
168
300
|
function reactWrapper(options = {}) {
|
|
169
|
-
var _options$outDir, _options$stripPrefix, _options$dts, _options$index, _options$namedSlots;
|
|
301
|
+
var _options$outDir, _options$stripPrefix, _options$dts, _options$index, _options$namedSlots, _options$wrapper;
|
|
170
302
|
const normalized = {
|
|
171
303
|
outDir: (_options$outDir = options.outDir) !== null && _options$outDir !== void 0 ? _options$outDir : "react",
|
|
172
304
|
stripPrefix: (_options$stripPrefix = options.stripPrefix) !== null && _options$stripPrefix !== void 0 ? _options$stripPrefix : false,
|
|
173
305
|
fileName: options.fileName,
|
|
174
|
-
dts: (_options$dts = options.dts) !== null && _options$dts !== void 0 ? _options$dts :
|
|
306
|
+
dts: (_options$dts = options.dts) !== null && _options$dts !== void 0 ? _options$dts : true,
|
|
175
307
|
index: (_options$index = options.index) !== null && _options$index !== void 0 ? _options$index : true,
|
|
176
|
-
namedSlots: (_options$namedSlots = options.namedSlots) !== null && _options$namedSlots !== void 0 ? _options$namedSlots : "props"
|
|
308
|
+
namedSlots: (_options$namedSlots = options.namedSlots) !== null && _options$namedSlots !== void 0 ? _options$namedSlots : "props",
|
|
309
|
+
wrapper: (_options$wrapper = options.wrapper) !== null && _options$wrapper !== void 0 ? _options$wrapper : "minimal"
|
|
177
310
|
};
|
|
178
311
|
return {
|
|
179
312
|
name: "zeus-output-react-wrapper",
|
|
@@ -194,7 +327,8 @@ function reactWrapper(options = {}) {
|
|
|
194
327
|
code: generateReactWrapper({
|
|
195
328
|
component,
|
|
196
329
|
namedSlots: normalized.namedSlots,
|
|
197
|
-
wcModuleId: `zeus:wc:${component.tag}
|
|
330
|
+
wcModuleId: `zeus:wc:${component.tag}`,
|
|
331
|
+
mode: normalized.wrapper
|
|
198
332
|
})
|
|
199
333
|
});
|
|
200
334
|
if (normalized.index) modules.push({
|
|
@@ -209,7 +343,7 @@ function reactWrapper(options = {}) {
|
|
|
209
343
|
return [{
|
|
210
344
|
type: "asset",
|
|
211
345
|
fileName: ctx.outputs.join("react", "index.d.ts"),
|
|
212
|
-
source: (0, _zeus_js_component_dts.generateReactDts)(ctx.manifest)
|
|
346
|
+
source: (0, _zeus_js_component_dts.generateReactDts)(ctx.manifest, { namedSlots: normalized.namedSlots })
|
|
213
347
|
}];
|
|
214
348
|
}
|
|
215
349
|
};
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* output-react-wrapper v0.1.0-beta.
|
|
2
|
+
* output-react-wrapper v0.1.0-beta.4
|
|
3
3
|
* (c) 2026 baicie
|
|
4
4
|
* Released under the MIT License.
|
|
5
5
|
**/
|
|
@@ -20,160 +20,293 @@ function generateReactIndex(components, options) {
|
|
|
20
20
|
return lines.join("\n");
|
|
21
21
|
}
|
|
22
22
|
//#endregion
|
|
23
|
-
//#region packages/web-c/output-react-wrapper/src/naming.ts
|
|
24
|
-
function toReactEventProp(eventName) {
|
|
25
|
-
return "on" + eventName.split("-").filter(Boolean).map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join("");
|
|
26
|
-
}
|
|
27
|
-
//#endregion
|
|
28
23
|
//#region packages/web-c/output-react-wrapper/src/generateReactWrapper.ts
|
|
29
24
|
function generateReactWrapper(input) {
|
|
25
|
+
return input.mode === "event-bridge" ? generateEventBridgeReactWrapper(input) : generateMinimalReactWrapper(input);
|
|
26
|
+
}
|
|
27
|
+
function generateMinimalReactWrapper(input) {
|
|
30
28
|
const { component, namedSlots, wcModuleId } = input;
|
|
31
|
-
const
|
|
32
|
-
const
|
|
33
|
-
const
|
|
34
|
-
const
|
|
35
|
-
const
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
29
|
+
const slotBindings = createBindings(getNamedSlots(component, namedSlots), "slotValue");
|
|
30
|
+
const omittedKeys = ["children", ...slotBindings.map(({ sourceName }) => sourceName)];
|
|
31
|
+
const slotAssignments = generatePropAssignments(slotBindings);
|
|
32
|
+
const namedSlotLines = generateMinimalNamedSlots(slotBindings);
|
|
33
|
+
const childSetup = slotBindings.length ? `${namedSlotLines}
|
|
34
|
+
const childArgs = [];
|
|
35
|
+
pushAll(childArgs, slotNodes);
|
|
36
|
+
if (children != null) childArgs.push(children);` : "";
|
|
37
|
+
const render = slotBindings.length ? `React.createElement.apply(
|
|
38
|
+
React,
|
|
39
|
+
[${JSON.stringify(component.tag)}, rest].concat(childArgs),
|
|
40
|
+
)` : `React.createElement(${JSON.stringify(component.tag)}, rest, children)`;
|
|
41
41
|
return `
|
|
42
|
-
import
|
|
43
|
-
createElement,
|
|
44
|
-
cloneElement,
|
|
45
|
-
Fragment,
|
|
46
|
-
forwardRef,
|
|
47
|
-
isValidElement,
|
|
48
|
-
useEffect,
|
|
49
|
-
useImperativeHandle,
|
|
50
|
-
useRef,
|
|
51
|
-
} from 'react';
|
|
42
|
+
import * as React from 'react';
|
|
52
43
|
|
|
53
44
|
import ${JSON.stringify(wcModuleId)};
|
|
54
45
|
|
|
55
|
-
const
|
|
56
|
-
const EVENT_MAP = ${JSON.stringify(createReactEventMap(eventNames))};
|
|
57
|
-
const NAMED_SLOTS = ${JSON.stringify(slotNames)};
|
|
58
|
-
|
|
59
|
-
export const ${component.name} = forwardRef(function ${component.name}(props, ref) {
|
|
60
|
-
const {
|
|
61
|
-
children,
|
|
62
|
-
className,
|
|
63
|
-
style,
|
|
64
|
-
${destructuredProps}
|
|
65
|
-
...rest
|
|
66
|
-
} = props;
|
|
46
|
+
const OMITTED_PROPS = new Set(${JSON.stringify(omittedKeys)});
|
|
67
47
|
|
|
68
|
-
|
|
48
|
+
export const ${component.name} = React.forwardRef(
|
|
49
|
+
function ${component.name}(inputProps, ref) {
|
|
50
|
+
const props = inputProps || {};
|
|
51
|
+
const children = props.children;
|
|
52
|
+
${slotAssignments}
|
|
53
|
+
const rest = omitProps(props);
|
|
54
|
+
rest.ref = ref;
|
|
55
|
+
${childSetup}
|
|
69
56
|
|
|
70
|
-
|
|
57
|
+
return ${render};
|
|
58
|
+
},
|
|
59
|
+
);
|
|
71
60
|
|
|
72
|
-
|
|
61
|
+
function omitProps(source) {
|
|
62
|
+
const output = {};
|
|
63
|
+
for (const key in source) {
|
|
64
|
+
if (hasOwn(source, key) && !OMITTED_PROPS.has(key)) {
|
|
65
|
+
output[key] = source[key];
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
return output;
|
|
69
|
+
}
|
|
73
70
|
|
|
74
|
-
|
|
71
|
+
function hasOwn(source, key) {
|
|
72
|
+
return Object.prototype.hasOwnProperty.call(source, key);
|
|
73
|
+
}
|
|
74
|
+
${slotBindings.length ? PUSH_ALL_HELPER : ""}
|
|
75
|
+
`.trimStart();
|
|
76
|
+
}
|
|
77
|
+
function generateEventBridgeReactWrapper(input) {
|
|
78
|
+
const { component, namedSlots, wcModuleId } = input;
|
|
79
|
+
const propBindings = createBindings(Object.keys(component.props), "propValue");
|
|
80
|
+
const eventBindings = createEventBindings(component.events);
|
|
81
|
+
if (!propBindings.length && !eventBindings.length) return generateMinimalReactWrapper(input);
|
|
82
|
+
const slotBindings = createBindings(getNamedSlots(component, namedSlots), "slotValue");
|
|
83
|
+
const destructuredBindings = [
|
|
84
|
+
...propBindings,
|
|
85
|
+
...eventBindings,
|
|
86
|
+
...slotBindings
|
|
87
|
+
];
|
|
88
|
+
const omittedKeys = [
|
|
89
|
+
"children",
|
|
90
|
+
"className",
|
|
91
|
+
"style",
|
|
92
|
+
...destructuredBindings.map(({ sourceName }) => sourceName)
|
|
93
|
+
];
|
|
94
|
+
return `
|
|
95
|
+
import {
|
|
96
|
+
${[
|
|
97
|
+
"createElement",
|
|
98
|
+
...slotBindings.length ? [
|
|
99
|
+
"cloneElement",
|
|
100
|
+
"Fragment",
|
|
101
|
+
"isValidElement"
|
|
102
|
+
] : [],
|
|
103
|
+
"forwardRef",
|
|
104
|
+
...propBindings.length || eventBindings.length ? ["useEffect"] : [],
|
|
105
|
+
"useImperativeHandle",
|
|
106
|
+
"useRef"
|
|
107
|
+
].join(",\n ")},
|
|
108
|
+
} from 'react';
|
|
75
109
|
|
|
76
|
-
|
|
110
|
+
import ${JSON.stringify(wcModuleId)};
|
|
77
111
|
|
|
78
|
-
|
|
112
|
+
const OMITTED_PROPS = new Set(${JSON.stringify(omittedKeys)});
|
|
113
|
+
${eventBindings.length ? `const EVENT_NAMES = ${JSON.stringify(eventBindings.map((binding) => binding.eventName))};` : ""}
|
|
79
114
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
115
|
+
export const ${component.name} = forwardRef(function ${component.name}(inputProps, ref) {
|
|
116
|
+
const props = inputProps || {};
|
|
117
|
+
const children = props.children;
|
|
118
|
+
const className = props.className;
|
|
119
|
+
const style = props.style;
|
|
120
|
+
${generatePropAssignments(destructuredBindings)}
|
|
121
|
+
${generatePropPresenceAssignments(propBindings)}
|
|
122
|
+
const rest = omitProps(props);
|
|
83
123
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
ref: innerRef,
|
|
89
|
-
className,
|
|
90
|
-
style,
|
|
91
|
-
},
|
|
92
|
-
...slotChildren,
|
|
93
|
-
);
|
|
94
|
-
});
|
|
124
|
+
const innerRef = useRef(null);
|
|
125
|
+
${generatePropRefs(propBindings)}
|
|
126
|
+
${generateEventRefs(eventBindings)}
|
|
127
|
+
useImperativeHandle(ref, () => innerRef.current, []);
|
|
95
128
|
|
|
96
|
-
|
|
97
|
-
|
|
129
|
+
${generatePropSyncEffect(propBindings)}
|
|
130
|
+
${generateEventEffect(eventBindings)}
|
|
131
|
+
${generateChildrenSetup(slotBindings)}
|
|
132
|
+
rest.ref = innerRef;
|
|
133
|
+
rest.className = className;
|
|
134
|
+
rest.style = style;
|
|
98
135
|
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
136
|
+
return ${generateReactRender(component.tag, slotBindings)};
|
|
137
|
+
});
|
|
138
|
+
${slotBindings.length ? NAMED_SLOT_HELPER : ""}
|
|
139
|
+
function omitProps(source) {
|
|
140
|
+
const output = {};
|
|
141
|
+
for (const key in source) {
|
|
142
|
+
if (hasOwn(source, key) && !OMITTED_PROPS.has(key)) {
|
|
143
|
+
output[key] = source[key];
|
|
144
|
+
}
|
|
104
145
|
}
|
|
146
|
+
return output;
|
|
147
|
+
}
|
|
105
148
|
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
{
|
|
109
|
-
slot: name,
|
|
110
|
-
style: { display: 'contents' },
|
|
111
|
-
},
|
|
112
|
-
value,
|
|
113
|
-
);
|
|
149
|
+
function hasOwn(source, key) {
|
|
150
|
+
return Object.prototype.hasOwnProperty.call(source, key);
|
|
114
151
|
}
|
|
115
152
|
`.trimStart();
|
|
116
153
|
}
|
|
117
|
-
function
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
154
|
+
function createBindings(names, prefix) {
|
|
155
|
+
return names.map((sourceName, index) => ({
|
|
156
|
+
sourceName,
|
|
157
|
+
localName: `${prefix}${index}`
|
|
158
|
+
}));
|
|
159
|
+
}
|
|
160
|
+
function createEventBindings(events) {
|
|
161
|
+
return Object.entries(events).map(([key, event], index) => {
|
|
162
|
+
var _event$key, _event$name, _event$reactName;
|
|
163
|
+
const sourceEventName = (_event$key = event.key) !== null && _event$key !== void 0 ? _event$key : key;
|
|
164
|
+
return {
|
|
165
|
+
eventName: (_event$name = event.name) !== null && _event$name !== void 0 ? _event$name : toKebabCase(sourceEventName),
|
|
166
|
+
sourceName: (_event$reactName = event.reactName) !== null && _event$reactName !== void 0 ? _event$reactName : toReactEventProp(sourceEventName),
|
|
167
|
+
localName: `eventHandler${index}`
|
|
168
|
+
};
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
function generatePropAssignments(bindings) {
|
|
172
|
+
return bindings.map(({ sourceName, localName }) => ` const ${localName} = props[${JSON.stringify(sourceName)}];`).join("\n");
|
|
173
|
+
}
|
|
174
|
+
function generatePropPresenceAssignments(bindings) {
|
|
175
|
+
return bindings.map(({ sourceName }, index) => ` const propPresent${index} = hasOwn(props, ${JSON.stringify(sourceName)});`).join("\n");
|
|
121
176
|
}
|
|
122
|
-
function
|
|
123
|
-
if (!
|
|
124
|
-
return `
|
|
177
|
+
function generatePropRefs(bindings) {
|
|
178
|
+
if (!bindings.length) return "";
|
|
179
|
+
return ` const previousPropPresenceRef = useRef([]);
|
|
180
|
+
const previousPropValuesRef = useRef([]);`;
|
|
181
|
+
}
|
|
182
|
+
function generateEventRefs(bindings) {
|
|
183
|
+
if (!bindings.length) return "";
|
|
184
|
+
return ` const eventHandlersRef = useRef([]);
|
|
185
|
+
${bindings.map(({ localName }, index) => ` eventHandlersRef.current[${index}] = ${localName};`).join("\n")}`;
|
|
186
|
+
}
|
|
187
|
+
function generatePropSyncEffect(bindings) {
|
|
188
|
+
if (!bindings.length) return "";
|
|
189
|
+
return ` useEffect(() => {
|
|
125
190
|
const el = innerRef.current;
|
|
126
191
|
if (!el) return;
|
|
127
192
|
|
|
128
|
-
|
|
129
|
-
|
|
193
|
+
const previousPropPresence = previousPropPresenceRef.current;
|
|
194
|
+
const previousPropValues = previousPropValuesRef.current;
|
|
195
|
+
|
|
196
|
+
${bindings.map(({ sourceName, localName }, index) => {
|
|
197
|
+
const key = JSON.stringify(sourceName);
|
|
198
|
+
return ` if (propPresent${index}) {
|
|
199
|
+
if (
|
|
200
|
+
!previousPropPresence[${index}] ||
|
|
201
|
+
!Object.is(previousPropValues[${index}], ${localName})
|
|
202
|
+
) {
|
|
203
|
+
el[${key}] = ${localName};
|
|
204
|
+
previousPropValues[${index}] = ${localName};
|
|
205
|
+
}
|
|
206
|
+
previousPropPresence[${index}] = true;
|
|
207
|
+
} else if (previousPropPresence[${index}]) {
|
|
208
|
+
el[${key}] = undefined;
|
|
209
|
+
previousPropPresence[${index}] = false;
|
|
210
|
+
previousPropValues[${index}] = undefined;
|
|
211
|
+
}`;
|
|
212
|
+
}).join("\n\n")}
|
|
213
|
+
}, [${bindings.flatMap((binding, index) => [`propPresent${index}`, binding.localName]).join(", ")}]);
|
|
214
|
+
`;
|
|
130
215
|
}
|
|
131
|
-
function
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
return `
|
|
135
|
-
useEffect(() => {
|
|
216
|
+
function generateEventEffect(bindings) {
|
|
217
|
+
if (!bindings.length) return "";
|
|
218
|
+
return ` useEffect(() => {
|
|
136
219
|
const el = innerRef.current;
|
|
137
|
-
if (!el
|
|
220
|
+
if (!el) return;
|
|
138
221
|
|
|
139
|
-
const
|
|
140
|
-
|
|
141
|
-
|
|
222
|
+
const listeners = EVENT_NAMES.map(
|
|
223
|
+
(_eventName, index) => event => {
|
|
224
|
+
const handler = eventHandlersRef.current[index];
|
|
225
|
+
if (handler) handler(event);
|
|
226
|
+
},
|
|
227
|
+
);
|
|
142
228
|
|
|
143
|
-
|
|
229
|
+
for (let index = 0; index < EVENT_NAMES.length; index += 1) {
|
|
230
|
+
el.addEventListener(EVENT_NAMES[index], listeners[index]);
|
|
231
|
+
}
|
|
144
232
|
|
|
145
233
|
return () => {
|
|
146
|
-
|
|
234
|
+
for (let index = 0; index < EVENT_NAMES.length; index += 1) {
|
|
235
|
+
el.removeEventListener(EVENT_NAMES[index], listeners[index]);
|
|
236
|
+
}
|
|
147
237
|
};
|
|
148
|
-
}, [
|
|
238
|
+
}, []);
|
|
149
239
|
`;
|
|
150
|
-
}).join("");
|
|
151
240
|
}
|
|
152
|
-
function
|
|
153
|
-
if (
|
|
154
|
-
return
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
return namedSlots.map((name) => {
|
|
158
|
-
return `
|
|
159
|
-
{
|
|
160
|
-
const node = createNamedSlot(${JSON.stringify(name)}, ${name});
|
|
241
|
+
function generateChildrenSetup(bindings) {
|
|
242
|
+
if (!bindings.length) return "";
|
|
243
|
+
return ` const slotChildren = [];
|
|
244
|
+
${bindings.map(({ sourceName, localName }) => ` {
|
|
245
|
+
const node = createNamedSlot(${JSON.stringify(sourceName)}, ${localName});
|
|
161
246
|
if (node != null) slotChildren.push(node);
|
|
162
|
-
}
|
|
247
|
+
}`).join("\n")}
|
|
248
|
+
if (children != null) slotChildren.push(children);
|
|
249
|
+
`;
|
|
250
|
+
}
|
|
251
|
+
function generateReactRender(tag, slotBindings) {
|
|
252
|
+
if (!slotBindings.length) return `createElement(${JSON.stringify(tag)}, rest, children)`;
|
|
253
|
+
return `createElement.apply(
|
|
254
|
+
null,
|
|
255
|
+
[${JSON.stringify(tag)}, rest].concat(slotChildren),
|
|
256
|
+
)`;
|
|
257
|
+
}
|
|
258
|
+
function getNamedSlots(component, namedSlots) {
|
|
259
|
+
return namedSlots === "none" ? [] : Object.keys(component.slots).filter((name) => name !== "default");
|
|
260
|
+
}
|
|
261
|
+
function generateMinimalNamedSlots(bindings) {
|
|
262
|
+
if (!bindings.length) return "";
|
|
263
|
+
return `${bindings.map(({ sourceName, localName }, index) => {
|
|
264
|
+
return ` const slotNode${index} = ${localName} != null && ${localName} !== false
|
|
265
|
+
? (React.isValidElement(${localName}) && ${localName}.type !== React.Fragment
|
|
266
|
+
? React.cloneElement(${localName}, { slot: ${JSON.stringify(sourceName)} })
|
|
267
|
+
: React.createElement('span', { slot: ${JSON.stringify(sourceName)}, style: { display: 'contents' } }, ${localName}))
|
|
268
|
+
: null;`;
|
|
269
|
+
}).join("\n")}
|
|
270
|
+
const slotNodes = [${bindings.map((_, index) => `slotNode${index}`).join(", ")}].filter(Boolean);`;
|
|
271
|
+
}
|
|
272
|
+
function toKebabCase(value) {
|
|
273
|
+
return value.replace(/[A-Z]/g, (match) => `-${match.toLowerCase()}`);
|
|
274
|
+
}
|
|
275
|
+
function toReactEventProp(value) {
|
|
276
|
+
return `on${value.split("-").filter(Boolean).map((part) => `${part.charAt(0).toUpperCase()}${part.slice(1)}`).join("")}`;
|
|
277
|
+
}
|
|
278
|
+
const PUSH_ALL_HELPER = `
|
|
279
|
+
function pushAll(target, values) {
|
|
280
|
+
for (const value of values) target.push(value);
|
|
281
|
+
}
|
|
163
282
|
`;
|
|
164
|
-
|
|
283
|
+
const NAMED_SLOT_HELPER = `
|
|
284
|
+
function createNamedSlot(name, value) {
|
|
285
|
+
if (value == null || value === false) return null;
|
|
286
|
+
|
|
287
|
+
if (isValidElement(value) && value.type !== Fragment) {
|
|
288
|
+
return cloneElement(value, { slot: name });
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
return createElement(
|
|
292
|
+
'span',
|
|
293
|
+
{ slot: name, style: { display: 'contents' } },
|
|
294
|
+
value,
|
|
295
|
+
);
|
|
165
296
|
}
|
|
297
|
+
`;
|
|
166
298
|
//#endregion
|
|
167
299
|
//#region packages/web-c/output-react-wrapper/src/index.ts
|
|
168
300
|
function reactWrapper(options = {}) {
|
|
169
|
-
var _options$outDir, _options$stripPrefix, _options$dts, _options$index, _options$namedSlots;
|
|
301
|
+
var _options$outDir, _options$stripPrefix, _options$dts, _options$index, _options$namedSlots, _options$wrapper;
|
|
170
302
|
const normalized = {
|
|
171
303
|
outDir: (_options$outDir = options.outDir) !== null && _options$outDir !== void 0 ? _options$outDir : "react",
|
|
172
304
|
stripPrefix: (_options$stripPrefix = options.stripPrefix) !== null && _options$stripPrefix !== void 0 ? _options$stripPrefix : false,
|
|
173
305
|
fileName: options.fileName,
|
|
174
|
-
dts: (_options$dts = options.dts) !== null && _options$dts !== void 0 ? _options$dts :
|
|
306
|
+
dts: (_options$dts = options.dts) !== null && _options$dts !== void 0 ? _options$dts : true,
|
|
175
307
|
index: (_options$index = options.index) !== null && _options$index !== void 0 ? _options$index : true,
|
|
176
|
-
namedSlots: (_options$namedSlots = options.namedSlots) !== null && _options$namedSlots !== void 0 ? _options$namedSlots : "props"
|
|
308
|
+
namedSlots: (_options$namedSlots = options.namedSlots) !== null && _options$namedSlots !== void 0 ? _options$namedSlots : "props",
|
|
309
|
+
wrapper: (_options$wrapper = options.wrapper) !== null && _options$wrapper !== void 0 ? _options$wrapper : "minimal"
|
|
177
310
|
};
|
|
178
311
|
return {
|
|
179
312
|
name: "zeus-output-react-wrapper",
|
|
@@ -194,7 +327,8 @@ function reactWrapper(options = {}) {
|
|
|
194
327
|
code: generateReactWrapper({
|
|
195
328
|
component,
|
|
196
329
|
namedSlots: normalized.namedSlots,
|
|
197
|
-
wcModuleId: `zeus:wc:${component.tag}
|
|
330
|
+
wcModuleId: `zeus:wc:${component.tag}`,
|
|
331
|
+
mode: normalized.wrapper
|
|
198
332
|
})
|
|
199
333
|
});
|
|
200
334
|
if (normalized.index) modules.push({
|
|
@@ -209,7 +343,7 @@ function reactWrapper(options = {}) {
|
|
|
209
343
|
return [{
|
|
210
344
|
type: "asset",
|
|
211
345
|
fileName: ctx.outputs.join("react", "index.d.ts"),
|
|
212
|
-
source: (0, _zeus_js_component_dts.generateReactDts)(ctx.manifest)
|
|
346
|
+
source: (0, _zeus_js_component_dts.generateReactDts)(ctx.manifest, { namedSlots: normalized.namedSlots })
|
|
213
347
|
}];
|
|
214
348
|
}
|
|
215
349
|
};
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { DtsMode, ZeusComponentPlugin } from '@zeus-js/bundler-plugin';
|
|
2
2
|
|
|
3
|
+
type ReactWrapperMode = 'minimal' | 'event-bridge';
|
|
3
4
|
export interface OutputReactWrapperOptions {
|
|
4
5
|
/**
|
|
5
6
|
* React wrapper output directory.
|
|
@@ -20,7 +21,7 @@ export interface OutputReactWrapperOptions {
|
|
|
20
21
|
/**
|
|
21
22
|
* Generate react/index.d.ts.
|
|
22
23
|
*
|
|
23
|
-
* @default
|
|
24
|
+
* @default true
|
|
24
25
|
*/
|
|
25
26
|
dts?: DtsMode;
|
|
26
27
|
/**
|
|
@@ -30,7 +31,18 @@ export interface OutputReactWrapperOptions {
|
|
|
30
31
|
*/
|
|
31
32
|
index?: boolean;
|
|
32
33
|
/**
|
|
33
|
-
*
|
|
34
|
+
* minimal:
|
|
35
|
+
* Default. Requires React 19+.
|
|
36
|
+
* React wrapper only renders the custom element tag.
|
|
37
|
+
* No useEffect prop sync, no event listeners.
|
|
38
|
+
*
|
|
39
|
+
* event-bridge:
|
|
40
|
+
* Compatibility mode for React 18 or applications that require explicit
|
|
41
|
+
* CustomEvent bridging and property assignment.
|
|
42
|
+
*/
|
|
43
|
+
wrapper?: ReactWrapperMode;
|
|
44
|
+
/**
|
|
45
|
+
* Named slot strategy (event-bridge mode only).
|
|
34
46
|
*
|
|
35
47
|
* props:
|
|
36
48
|
* <ZCard header={<div />} />
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* output-react-wrapper v0.1.0-beta.
|
|
2
|
+
* output-react-wrapper v0.1.0-beta.4
|
|
3
3
|
* (c) 2026 baicie
|
|
4
4
|
* Released under the MIT License.
|
|
5
5
|
**/
|
|
@@ -16,160 +16,293 @@ function generateReactIndex(components, options) {
|
|
|
16
16
|
return lines.join("\n");
|
|
17
17
|
}
|
|
18
18
|
//#endregion
|
|
19
|
-
//#region packages/web-c/output-react-wrapper/src/naming.ts
|
|
20
|
-
function toReactEventProp(eventName) {
|
|
21
|
-
return "on" + eventName.split("-").filter(Boolean).map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join("");
|
|
22
|
-
}
|
|
23
|
-
//#endregion
|
|
24
19
|
//#region packages/web-c/output-react-wrapper/src/generateReactWrapper.ts
|
|
25
20
|
function generateReactWrapper(input) {
|
|
21
|
+
return input.mode === "event-bridge" ? generateEventBridgeReactWrapper(input) : generateMinimalReactWrapper(input);
|
|
22
|
+
}
|
|
23
|
+
function generateMinimalReactWrapper(input) {
|
|
26
24
|
const { component, namedSlots, wcModuleId } = input;
|
|
27
|
-
const
|
|
28
|
-
const
|
|
29
|
-
const
|
|
30
|
-
const
|
|
31
|
-
const
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
25
|
+
const slotBindings = createBindings(getNamedSlots(component, namedSlots), "slotValue");
|
|
26
|
+
const omittedKeys = ["children", ...slotBindings.map(({ sourceName }) => sourceName)];
|
|
27
|
+
const slotAssignments = generatePropAssignments(slotBindings);
|
|
28
|
+
const namedSlotLines = generateMinimalNamedSlots(slotBindings);
|
|
29
|
+
const childSetup = slotBindings.length ? `${namedSlotLines}
|
|
30
|
+
const childArgs = [];
|
|
31
|
+
pushAll(childArgs, slotNodes);
|
|
32
|
+
if (children != null) childArgs.push(children);` : "";
|
|
33
|
+
const render = slotBindings.length ? `React.createElement.apply(
|
|
34
|
+
React,
|
|
35
|
+
[${JSON.stringify(component.tag)}, rest].concat(childArgs),
|
|
36
|
+
)` : `React.createElement(${JSON.stringify(component.tag)}, rest, children)`;
|
|
37
37
|
return `
|
|
38
|
-
import
|
|
39
|
-
createElement,
|
|
40
|
-
cloneElement,
|
|
41
|
-
Fragment,
|
|
42
|
-
forwardRef,
|
|
43
|
-
isValidElement,
|
|
44
|
-
useEffect,
|
|
45
|
-
useImperativeHandle,
|
|
46
|
-
useRef,
|
|
47
|
-
} from 'react';
|
|
38
|
+
import * as React from 'react';
|
|
48
39
|
|
|
49
40
|
import ${JSON.stringify(wcModuleId)};
|
|
50
41
|
|
|
51
|
-
const
|
|
52
|
-
const EVENT_MAP = ${JSON.stringify(createReactEventMap(eventNames))};
|
|
53
|
-
const NAMED_SLOTS = ${JSON.stringify(slotNames)};
|
|
54
|
-
|
|
55
|
-
export const ${component.name} = forwardRef(function ${component.name}(props, ref) {
|
|
56
|
-
const {
|
|
57
|
-
children,
|
|
58
|
-
className,
|
|
59
|
-
style,
|
|
60
|
-
${destructuredProps}
|
|
61
|
-
...rest
|
|
62
|
-
} = props;
|
|
42
|
+
const OMITTED_PROPS = new Set(${JSON.stringify(omittedKeys)});
|
|
63
43
|
|
|
64
|
-
|
|
44
|
+
export const ${component.name} = React.forwardRef(
|
|
45
|
+
function ${component.name}(inputProps, ref) {
|
|
46
|
+
const props = inputProps || {};
|
|
47
|
+
const children = props.children;
|
|
48
|
+
${slotAssignments}
|
|
49
|
+
const rest = omitProps(props);
|
|
50
|
+
rest.ref = ref;
|
|
51
|
+
${childSetup}
|
|
65
52
|
|
|
66
|
-
|
|
53
|
+
return ${render};
|
|
54
|
+
},
|
|
55
|
+
);
|
|
67
56
|
|
|
68
|
-
|
|
57
|
+
function omitProps(source) {
|
|
58
|
+
const output = {};
|
|
59
|
+
for (const key in source) {
|
|
60
|
+
if (hasOwn(source, key) && !OMITTED_PROPS.has(key)) {
|
|
61
|
+
output[key] = source[key];
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
return output;
|
|
65
|
+
}
|
|
69
66
|
|
|
70
|
-
|
|
67
|
+
function hasOwn(source, key) {
|
|
68
|
+
return Object.prototype.hasOwnProperty.call(source, key);
|
|
69
|
+
}
|
|
70
|
+
${slotBindings.length ? PUSH_ALL_HELPER : ""}
|
|
71
|
+
`.trimStart();
|
|
72
|
+
}
|
|
73
|
+
function generateEventBridgeReactWrapper(input) {
|
|
74
|
+
const { component, namedSlots, wcModuleId } = input;
|
|
75
|
+
const propBindings = createBindings(Object.keys(component.props), "propValue");
|
|
76
|
+
const eventBindings = createEventBindings(component.events);
|
|
77
|
+
if (!propBindings.length && !eventBindings.length) return generateMinimalReactWrapper(input);
|
|
78
|
+
const slotBindings = createBindings(getNamedSlots(component, namedSlots), "slotValue");
|
|
79
|
+
const destructuredBindings = [
|
|
80
|
+
...propBindings,
|
|
81
|
+
...eventBindings,
|
|
82
|
+
...slotBindings
|
|
83
|
+
];
|
|
84
|
+
const omittedKeys = [
|
|
85
|
+
"children",
|
|
86
|
+
"className",
|
|
87
|
+
"style",
|
|
88
|
+
...destructuredBindings.map(({ sourceName }) => sourceName)
|
|
89
|
+
];
|
|
90
|
+
return `
|
|
91
|
+
import {
|
|
92
|
+
${[
|
|
93
|
+
"createElement",
|
|
94
|
+
...slotBindings.length ? [
|
|
95
|
+
"cloneElement",
|
|
96
|
+
"Fragment",
|
|
97
|
+
"isValidElement"
|
|
98
|
+
] : [],
|
|
99
|
+
"forwardRef",
|
|
100
|
+
...propBindings.length || eventBindings.length ? ["useEffect"] : [],
|
|
101
|
+
"useImperativeHandle",
|
|
102
|
+
"useRef"
|
|
103
|
+
].join(",\n ")},
|
|
104
|
+
} from 'react';
|
|
71
105
|
|
|
72
|
-
|
|
106
|
+
import ${JSON.stringify(wcModuleId)};
|
|
73
107
|
|
|
74
|
-
|
|
108
|
+
const OMITTED_PROPS = new Set(${JSON.stringify(omittedKeys)});
|
|
109
|
+
${eventBindings.length ? `const EVENT_NAMES = ${JSON.stringify(eventBindings.map((binding) => binding.eventName))};` : ""}
|
|
75
110
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
111
|
+
export const ${component.name} = forwardRef(function ${component.name}(inputProps, ref) {
|
|
112
|
+
const props = inputProps || {};
|
|
113
|
+
const children = props.children;
|
|
114
|
+
const className = props.className;
|
|
115
|
+
const style = props.style;
|
|
116
|
+
${generatePropAssignments(destructuredBindings)}
|
|
117
|
+
${generatePropPresenceAssignments(propBindings)}
|
|
118
|
+
const rest = omitProps(props);
|
|
79
119
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
ref: innerRef,
|
|
85
|
-
className,
|
|
86
|
-
style,
|
|
87
|
-
},
|
|
88
|
-
...slotChildren,
|
|
89
|
-
);
|
|
90
|
-
});
|
|
120
|
+
const innerRef = useRef(null);
|
|
121
|
+
${generatePropRefs(propBindings)}
|
|
122
|
+
${generateEventRefs(eventBindings)}
|
|
123
|
+
useImperativeHandle(ref, () => innerRef.current, []);
|
|
91
124
|
|
|
92
|
-
|
|
93
|
-
|
|
125
|
+
${generatePropSyncEffect(propBindings)}
|
|
126
|
+
${generateEventEffect(eventBindings)}
|
|
127
|
+
${generateChildrenSetup(slotBindings)}
|
|
128
|
+
rest.ref = innerRef;
|
|
129
|
+
rest.className = className;
|
|
130
|
+
rest.style = style;
|
|
94
131
|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
132
|
+
return ${generateReactRender(component.tag, slotBindings)};
|
|
133
|
+
});
|
|
134
|
+
${slotBindings.length ? NAMED_SLOT_HELPER : ""}
|
|
135
|
+
function omitProps(source) {
|
|
136
|
+
const output = {};
|
|
137
|
+
for (const key in source) {
|
|
138
|
+
if (hasOwn(source, key) && !OMITTED_PROPS.has(key)) {
|
|
139
|
+
output[key] = source[key];
|
|
140
|
+
}
|
|
100
141
|
}
|
|
142
|
+
return output;
|
|
143
|
+
}
|
|
101
144
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
{
|
|
105
|
-
slot: name,
|
|
106
|
-
style: { display: 'contents' },
|
|
107
|
-
},
|
|
108
|
-
value,
|
|
109
|
-
);
|
|
145
|
+
function hasOwn(source, key) {
|
|
146
|
+
return Object.prototype.hasOwnProperty.call(source, key);
|
|
110
147
|
}
|
|
111
148
|
`.trimStart();
|
|
112
149
|
}
|
|
113
|
-
function
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
150
|
+
function createBindings(names, prefix) {
|
|
151
|
+
return names.map((sourceName, index) => ({
|
|
152
|
+
sourceName,
|
|
153
|
+
localName: `${prefix}${index}`
|
|
154
|
+
}));
|
|
155
|
+
}
|
|
156
|
+
function createEventBindings(events) {
|
|
157
|
+
return Object.entries(events).map(([key, event], index) => {
|
|
158
|
+
var _event$key, _event$name, _event$reactName;
|
|
159
|
+
const sourceEventName = (_event$key = event.key) !== null && _event$key !== void 0 ? _event$key : key;
|
|
160
|
+
return {
|
|
161
|
+
eventName: (_event$name = event.name) !== null && _event$name !== void 0 ? _event$name : toKebabCase(sourceEventName),
|
|
162
|
+
sourceName: (_event$reactName = event.reactName) !== null && _event$reactName !== void 0 ? _event$reactName : toReactEventProp(sourceEventName),
|
|
163
|
+
localName: `eventHandler${index}`
|
|
164
|
+
};
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
function generatePropAssignments(bindings) {
|
|
168
|
+
return bindings.map(({ sourceName, localName }) => ` const ${localName} = props[${JSON.stringify(sourceName)}];`).join("\n");
|
|
169
|
+
}
|
|
170
|
+
function generatePropPresenceAssignments(bindings) {
|
|
171
|
+
return bindings.map(({ sourceName }, index) => ` const propPresent${index} = hasOwn(props, ${JSON.stringify(sourceName)});`).join("\n");
|
|
117
172
|
}
|
|
118
|
-
function
|
|
119
|
-
if (!
|
|
120
|
-
return `
|
|
173
|
+
function generatePropRefs(bindings) {
|
|
174
|
+
if (!bindings.length) return "";
|
|
175
|
+
return ` const previousPropPresenceRef = useRef([]);
|
|
176
|
+
const previousPropValuesRef = useRef([]);`;
|
|
177
|
+
}
|
|
178
|
+
function generateEventRefs(bindings) {
|
|
179
|
+
if (!bindings.length) return "";
|
|
180
|
+
return ` const eventHandlersRef = useRef([]);
|
|
181
|
+
${bindings.map(({ localName }, index) => ` eventHandlersRef.current[${index}] = ${localName};`).join("\n")}`;
|
|
182
|
+
}
|
|
183
|
+
function generatePropSyncEffect(bindings) {
|
|
184
|
+
if (!bindings.length) return "";
|
|
185
|
+
return ` useEffect(() => {
|
|
121
186
|
const el = innerRef.current;
|
|
122
187
|
if (!el) return;
|
|
123
188
|
|
|
124
|
-
|
|
125
|
-
|
|
189
|
+
const previousPropPresence = previousPropPresenceRef.current;
|
|
190
|
+
const previousPropValues = previousPropValuesRef.current;
|
|
191
|
+
|
|
192
|
+
${bindings.map(({ sourceName, localName }, index) => {
|
|
193
|
+
const key = JSON.stringify(sourceName);
|
|
194
|
+
return ` if (propPresent${index}) {
|
|
195
|
+
if (
|
|
196
|
+
!previousPropPresence[${index}] ||
|
|
197
|
+
!Object.is(previousPropValues[${index}], ${localName})
|
|
198
|
+
) {
|
|
199
|
+
el[${key}] = ${localName};
|
|
200
|
+
previousPropValues[${index}] = ${localName};
|
|
201
|
+
}
|
|
202
|
+
previousPropPresence[${index}] = true;
|
|
203
|
+
} else if (previousPropPresence[${index}]) {
|
|
204
|
+
el[${key}] = undefined;
|
|
205
|
+
previousPropPresence[${index}] = false;
|
|
206
|
+
previousPropValues[${index}] = undefined;
|
|
207
|
+
}`;
|
|
208
|
+
}).join("\n\n")}
|
|
209
|
+
}, [${bindings.flatMap((binding, index) => [`propPresent${index}`, binding.localName]).join(", ")}]);
|
|
210
|
+
`;
|
|
126
211
|
}
|
|
127
|
-
function
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
return `
|
|
131
|
-
useEffect(() => {
|
|
212
|
+
function generateEventEffect(bindings) {
|
|
213
|
+
if (!bindings.length) return "";
|
|
214
|
+
return ` useEffect(() => {
|
|
132
215
|
const el = innerRef.current;
|
|
133
|
-
if (!el
|
|
216
|
+
if (!el) return;
|
|
134
217
|
|
|
135
|
-
const
|
|
136
|
-
|
|
137
|
-
|
|
218
|
+
const listeners = EVENT_NAMES.map(
|
|
219
|
+
(_eventName, index) => event => {
|
|
220
|
+
const handler = eventHandlersRef.current[index];
|
|
221
|
+
if (handler) handler(event);
|
|
222
|
+
},
|
|
223
|
+
);
|
|
138
224
|
|
|
139
|
-
|
|
225
|
+
for (let index = 0; index < EVENT_NAMES.length; index += 1) {
|
|
226
|
+
el.addEventListener(EVENT_NAMES[index], listeners[index]);
|
|
227
|
+
}
|
|
140
228
|
|
|
141
229
|
return () => {
|
|
142
|
-
|
|
230
|
+
for (let index = 0; index < EVENT_NAMES.length; index += 1) {
|
|
231
|
+
el.removeEventListener(EVENT_NAMES[index], listeners[index]);
|
|
232
|
+
}
|
|
143
233
|
};
|
|
144
|
-
}, [
|
|
234
|
+
}, []);
|
|
145
235
|
`;
|
|
146
|
-
}).join("");
|
|
147
236
|
}
|
|
148
|
-
function
|
|
149
|
-
if (
|
|
150
|
-
return
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
return namedSlots.map((name) => {
|
|
154
|
-
return `
|
|
155
|
-
{
|
|
156
|
-
const node = createNamedSlot(${JSON.stringify(name)}, ${name});
|
|
237
|
+
function generateChildrenSetup(bindings) {
|
|
238
|
+
if (!bindings.length) return "";
|
|
239
|
+
return ` const slotChildren = [];
|
|
240
|
+
${bindings.map(({ sourceName, localName }) => ` {
|
|
241
|
+
const node = createNamedSlot(${JSON.stringify(sourceName)}, ${localName});
|
|
157
242
|
if (node != null) slotChildren.push(node);
|
|
158
|
-
}
|
|
243
|
+
}`).join("\n")}
|
|
244
|
+
if (children != null) slotChildren.push(children);
|
|
245
|
+
`;
|
|
246
|
+
}
|
|
247
|
+
function generateReactRender(tag, slotBindings) {
|
|
248
|
+
if (!slotBindings.length) return `createElement(${JSON.stringify(tag)}, rest, children)`;
|
|
249
|
+
return `createElement.apply(
|
|
250
|
+
null,
|
|
251
|
+
[${JSON.stringify(tag)}, rest].concat(slotChildren),
|
|
252
|
+
)`;
|
|
253
|
+
}
|
|
254
|
+
function getNamedSlots(component, namedSlots) {
|
|
255
|
+
return namedSlots === "none" ? [] : Object.keys(component.slots).filter((name) => name !== "default");
|
|
256
|
+
}
|
|
257
|
+
function generateMinimalNamedSlots(bindings) {
|
|
258
|
+
if (!bindings.length) return "";
|
|
259
|
+
return `${bindings.map(({ sourceName, localName }, index) => {
|
|
260
|
+
return ` const slotNode${index} = ${localName} != null && ${localName} !== false
|
|
261
|
+
? (React.isValidElement(${localName}) && ${localName}.type !== React.Fragment
|
|
262
|
+
? React.cloneElement(${localName}, { slot: ${JSON.stringify(sourceName)} })
|
|
263
|
+
: React.createElement('span', { slot: ${JSON.stringify(sourceName)}, style: { display: 'contents' } }, ${localName}))
|
|
264
|
+
: null;`;
|
|
265
|
+
}).join("\n")}
|
|
266
|
+
const slotNodes = [${bindings.map((_, index) => `slotNode${index}`).join(", ")}].filter(Boolean);`;
|
|
267
|
+
}
|
|
268
|
+
function toKebabCase(value) {
|
|
269
|
+
return value.replace(/[A-Z]/g, (match) => `-${match.toLowerCase()}`);
|
|
270
|
+
}
|
|
271
|
+
function toReactEventProp(value) {
|
|
272
|
+
return `on${value.split("-").filter(Boolean).map((part) => `${part.charAt(0).toUpperCase()}${part.slice(1)}`).join("")}`;
|
|
273
|
+
}
|
|
274
|
+
const PUSH_ALL_HELPER = `
|
|
275
|
+
function pushAll(target, values) {
|
|
276
|
+
for (const value of values) target.push(value);
|
|
277
|
+
}
|
|
159
278
|
`;
|
|
160
|
-
|
|
279
|
+
const NAMED_SLOT_HELPER = `
|
|
280
|
+
function createNamedSlot(name, value) {
|
|
281
|
+
if (value == null || value === false) return null;
|
|
282
|
+
|
|
283
|
+
if (isValidElement(value) && value.type !== Fragment) {
|
|
284
|
+
return cloneElement(value, { slot: name });
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
return createElement(
|
|
288
|
+
'span',
|
|
289
|
+
{ slot: name, style: { display: 'contents' } },
|
|
290
|
+
value,
|
|
291
|
+
);
|
|
161
292
|
}
|
|
293
|
+
`;
|
|
162
294
|
//#endregion
|
|
163
295
|
//#region packages/web-c/output-react-wrapper/src/index.ts
|
|
164
296
|
function reactWrapper(options = {}) {
|
|
165
|
-
var _options$outDir, _options$stripPrefix, _options$dts, _options$index, _options$namedSlots;
|
|
297
|
+
var _options$outDir, _options$stripPrefix, _options$dts, _options$index, _options$namedSlots, _options$wrapper;
|
|
166
298
|
const normalized = {
|
|
167
299
|
outDir: (_options$outDir = options.outDir) !== null && _options$outDir !== void 0 ? _options$outDir : "react",
|
|
168
300
|
stripPrefix: (_options$stripPrefix = options.stripPrefix) !== null && _options$stripPrefix !== void 0 ? _options$stripPrefix : false,
|
|
169
301
|
fileName: options.fileName,
|
|
170
|
-
dts: (_options$dts = options.dts) !== null && _options$dts !== void 0 ? _options$dts :
|
|
302
|
+
dts: (_options$dts = options.dts) !== null && _options$dts !== void 0 ? _options$dts : true,
|
|
171
303
|
index: (_options$index = options.index) !== null && _options$index !== void 0 ? _options$index : true,
|
|
172
|
-
namedSlots: (_options$namedSlots = options.namedSlots) !== null && _options$namedSlots !== void 0 ? _options$namedSlots : "props"
|
|
304
|
+
namedSlots: (_options$namedSlots = options.namedSlots) !== null && _options$namedSlots !== void 0 ? _options$namedSlots : "props",
|
|
305
|
+
wrapper: (_options$wrapper = options.wrapper) !== null && _options$wrapper !== void 0 ? _options$wrapper : "minimal"
|
|
173
306
|
};
|
|
174
307
|
return {
|
|
175
308
|
name: "zeus-output-react-wrapper",
|
|
@@ -190,7 +323,8 @@ function reactWrapper(options = {}) {
|
|
|
190
323
|
code: generateReactWrapper({
|
|
191
324
|
component,
|
|
192
325
|
namedSlots: normalized.namedSlots,
|
|
193
|
-
wcModuleId: `zeus:wc:${component.tag}
|
|
326
|
+
wcModuleId: `zeus:wc:${component.tag}`,
|
|
327
|
+
mode: normalized.wrapper
|
|
194
328
|
})
|
|
195
329
|
});
|
|
196
330
|
if (normalized.index) modules.push({
|
|
@@ -205,7 +339,7 @@ function reactWrapper(options = {}) {
|
|
|
205
339
|
return [{
|
|
206
340
|
type: "asset",
|
|
207
341
|
fileName: ctx.outputs.join("react", "index.d.ts"),
|
|
208
|
-
source: generateReactDts(ctx.manifest)
|
|
342
|
+
source: generateReactDts(ctx.manifest, { namedSlots: normalized.namedSlots })
|
|
209
343
|
}];
|
|
210
344
|
}
|
|
211
345
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zeus-js/output-react-wrapper",
|
|
3
|
-
"version": "0.1.0-beta.
|
|
3
|
+
"version": "0.1.0-beta.4",
|
|
4
4
|
"description": "Zeus React wrapper output plugin",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "index.js",
|
|
@@ -13,14 +13,14 @@
|
|
|
13
13
|
"exports": {
|
|
14
14
|
".": {
|
|
15
15
|
"types": "./dist/output-react-wrapper.d.ts",
|
|
16
|
+
"module": "./dist/output-react-wrapper.esm-bundler.js",
|
|
17
|
+
"import": "./dist/output-react-wrapper.esm-bundler.js",
|
|
18
|
+
"require": "./index.js",
|
|
16
19
|
"node": {
|
|
17
20
|
"production": "./dist/output-react-wrapper.cjs.prod.js",
|
|
18
21
|
"development": "./dist/output-react-wrapper.cjs.js",
|
|
19
22
|
"default": "./index.js"
|
|
20
|
-
}
|
|
21
|
-
"module": "./dist/output-react-wrapper.esm-bundler.js",
|
|
22
|
-
"import": "./dist/output-react-wrapper.esm-bundler.js",
|
|
23
|
-
"require": "./index.js"
|
|
23
|
+
}
|
|
24
24
|
}
|
|
25
25
|
},
|
|
26
26
|
"sideEffects": false,
|
|
@@ -36,12 +36,12 @@
|
|
|
36
36
|
]
|
|
37
37
|
},
|
|
38
38
|
"dependencies": {
|
|
39
|
-
"@zeus-js/
|
|
40
|
-
"@zeus-js/
|
|
41
|
-
"@zeus-js/component-dts": "0.1.0-beta.
|
|
39
|
+
"@zeus-js/bundler-plugin": "0.1.0-beta.4",
|
|
40
|
+
"@zeus-js/component-analyzer": "0.1.0-beta.4",
|
|
41
|
+
"@zeus-js/component-dts": "0.1.0-beta.4"
|
|
42
42
|
},
|
|
43
43
|
"peerDependencies": {
|
|
44
|
-
"react": ">=18
|
|
44
|
+
"react": ">=18"
|
|
45
45
|
},
|
|
46
46
|
"peerDependenciesMeta": {
|
|
47
47
|
"react": {
|