@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
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 propNames = Object.keys(component.props);
32
- const eventNames = Object.keys(component.events);
33
- const slotNames = getNamedSlots(component, namedSlots);
34
- const eventPropNames = eventNames.map(toReactEventProp);
35
- const destructuredPropNames = [
36
- ...propNames,
37
- ...eventPropNames,
38
- ...slotNames
39
- ];
40
- const destructuredProps = destructuredPropNames.length ? `${destructuredPropNames.join(",\n ")},` : "";
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 PROP_KEYS = ${JSON.stringify(propNames)};
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
- const innerRef = useRef(null);
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
- useImperativeHandle(ref, () => innerRef.current);
57
+ return ${render};
58
+ },
59
+ );
71
60
 
72
- ${generatePropSyncLines(propNames)}
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
- ${generateEventEffects(eventNames)}
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
- const slotChildren = [];
110
+ import ${JSON.stringify(wcModuleId)};
77
111
 
78
- ${generateNamedSlotRenderLines(slotNames)}
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
- if (children != null) {
81
- slotChildren.push(children);
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
- return createElement(
85
- ${JSON.stringify(component.tag)},
86
- {
87
- ...rest,
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
- function createNamedSlot(name, value) {
97
- if (value == null || value === false) return null;
129
+ ${generatePropSyncEffect(propBindings)}
130
+ ${generateEventEffect(eventBindings)}
131
+ ${generateChildrenSetup(slotBindings)}
132
+ rest.ref = innerRef;
133
+ rest.className = className;
134
+ rest.style = style;
98
135
 
99
- if (
100
- isValidElement(value) &&
101
- value.type !== Fragment
102
- ) {
103
- return cloneElement(value, { slot: name });
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
- return createElement(
107
- 'span',
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 createReactEventMap(eventNames) {
118
- const map = {};
119
- for (const eventName of eventNames) map[toReactEventProp(eventName)] = eventName;
120
- return map;
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 generatePropSyncLines(propNames) {
123
- if (!propNames.length) return "// no props";
124
- return `useEffect(() => {
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
- ${propNames.map((name) => `el.${name} = ${name};`).join("\n ")}
129
- }, [${propNames.join(", ")}]);`;
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 generateEventEffects(eventNames) {
132
- return eventNames.map((eventName) => {
133
- const propName = toReactEventProp(eventName);
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 || !${propName}) return;
220
+ if (!el) return;
138
221
 
139
- const handler = event => {
140
- ${propName}(event);
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
- el.addEventListener(${JSON.stringify(eventName)}, handler);
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
- el.removeEventListener(${JSON.stringify(eventName)}, handler);
234
+ for (let index = 0; index < EVENT_NAMES.length; index += 1) {
235
+ el.removeEventListener(EVENT_NAMES[index], listeners[index]);
236
+ }
147
237
  };
148
- }, [${propName}]);
238
+ }, []);
149
239
  `;
150
- }).join("");
151
240
  }
152
- function getNamedSlots(component, namedSlots) {
153
- if (namedSlots === "none") return [];
154
- return Object.keys(component.slots).filter((name) => name !== "default");
155
- }
156
- function generateNamedSlotRenderLines(namedSlots) {
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
- }).join("");
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 : "auto",
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
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 propNames = Object.keys(component.props);
32
- const eventNames = Object.keys(component.events);
33
- const slotNames = getNamedSlots(component, namedSlots);
34
- const eventPropNames = eventNames.map(toReactEventProp);
35
- const destructuredPropNames = [
36
- ...propNames,
37
- ...eventPropNames,
38
- ...slotNames
39
- ];
40
- const destructuredProps = destructuredPropNames.length ? `${destructuredPropNames.join(",\n ")},` : "";
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 PROP_KEYS = ${JSON.stringify(propNames)};
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
- const innerRef = useRef(null);
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
- useImperativeHandle(ref, () => innerRef.current);
57
+ return ${render};
58
+ },
59
+ );
71
60
 
72
- ${generatePropSyncLines(propNames)}
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
- ${generateEventEffects(eventNames)}
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
- const slotChildren = [];
110
+ import ${JSON.stringify(wcModuleId)};
77
111
 
78
- ${generateNamedSlotRenderLines(slotNames)}
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
- if (children != null) {
81
- slotChildren.push(children);
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
- return createElement(
85
- ${JSON.stringify(component.tag)},
86
- {
87
- ...rest,
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
- function createNamedSlot(name, value) {
97
- if (value == null || value === false) return null;
129
+ ${generatePropSyncEffect(propBindings)}
130
+ ${generateEventEffect(eventBindings)}
131
+ ${generateChildrenSetup(slotBindings)}
132
+ rest.ref = innerRef;
133
+ rest.className = className;
134
+ rest.style = style;
98
135
 
99
- if (
100
- isValidElement(value) &&
101
- value.type !== Fragment
102
- ) {
103
- return cloneElement(value, { slot: name });
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
- return createElement(
107
- 'span',
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 createReactEventMap(eventNames) {
118
- const map = {};
119
- for (const eventName of eventNames) map[toReactEventProp(eventName)] = eventName;
120
- return map;
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 generatePropSyncLines(propNames) {
123
- if (!propNames.length) return "// no props";
124
- return `useEffect(() => {
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
- ${propNames.map((name) => `el.${name} = ${name};`).join("\n ")}
129
- }, [${propNames.join(", ")}]);`;
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 generateEventEffects(eventNames) {
132
- return eventNames.map((eventName) => {
133
- const propName = toReactEventProp(eventName);
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 || !${propName}) return;
220
+ if (!el) return;
138
221
 
139
- const handler = event => {
140
- ${propName}(event);
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
- el.addEventListener(${JSON.stringify(eventName)}, handler);
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
- el.removeEventListener(${JSON.stringify(eventName)}, handler);
234
+ for (let index = 0; index < EVENT_NAMES.length; index += 1) {
235
+ el.removeEventListener(EVENT_NAMES[index], listeners[index]);
236
+ }
147
237
  };
148
- }, [${propName}]);
238
+ }, []);
149
239
  `;
150
- }).join("");
151
240
  }
152
- function getNamedSlots(component, namedSlots) {
153
- if (namedSlots === "none") return [];
154
- return Object.keys(component.slots).filter((name) => name !== "default");
155
- }
156
- function generateNamedSlotRenderLines(namedSlots) {
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
- }).join("");
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 : "auto",
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 'auto'
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
- * Named slot strategy.
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
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 propNames = Object.keys(component.props);
28
- const eventNames = Object.keys(component.events);
29
- const slotNames = getNamedSlots(component, namedSlots);
30
- const eventPropNames = eventNames.map(toReactEventProp);
31
- const destructuredPropNames = [
32
- ...propNames,
33
- ...eventPropNames,
34
- ...slotNames
35
- ];
36
- const destructuredProps = destructuredPropNames.length ? `${destructuredPropNames.join(",\n ")},` : "";
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 PROP_KEYS = ${JSON.stringify(propNames)};
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
- const innerRef = useRef(null);
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
- useImperativeHandle(ref, () => innerRef.current);
53
+ return ${render};
54
+ },
55
+ );
67
56
 
68
- ${generatePropSyncLines(propNames)}
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
- ${generateEventEffects(eventNames)}
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
- const slotChildren = [];
106
+ import ${JSON.stringify(wcModuleId)};
73
107
 
74
- ${generateNamedSlotRenderLines(slotNames)}
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
- if (children != null) {
77
- slotChildren.push(children);
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
- return createElement(
81
- ${JSON.stringify(component.tag)},
82
- {
83
- ...rest,
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
- function createNamedSlot(name, value) {
93
- if (value == null || value === false) return null;
125
+ ${generatePropSyncEffect(propBindings)}
126
+ ${generateEventEffect(eventBindings)}
127
+ ${generateChildrenSetup(slotBindings)}
128
+ rest.ref = innerRef;
129
+ rest.className = className;
130
+ rest.style = style;
94
131
 
95
- if (
96
- isValidElement(value) &&
97
- value.type !== Fragment
98
- ) {
99
- return cloneElement(value, { slot: name });
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
- return createElement(
103
- 'span',
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 createReactEventMap(eventNames) {
114
- const map = {};
115
- for (const eventName of eventNames) map[toReactEventProp(eventName)] = eventName;
116
- return map;
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 generatePropSyncLines(propNames) {
119
- if (!propNames.length) return "// no props";
120
- return `useEffect(() => {
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
- ${propNames.map((name) => `el.${name} = ${name};`).join("\n ")}
125
- }, [${propNames.join(", ")}]);`;
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 generateEventEffects(eventNames) {
128
- return eventNames.map((eventName) => {
129
- const propName = toReactEventProp(eventName);
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 || !${propName}) return;
216
+ if (!el) return;
134
217
 
135
- const handler = event => {
136
- ${propName}(event);
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
- el.addEventListener(${JSON.stringify(eventName)}, handler);
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
- el.removeEventListener(${JSON.stringify(eventName)}, handler);
230
+ for (let index = 0; index < EVENT_NAMES.length; index += 1) {
231
+ el.removeEventListener(EVENT_NAMES[index], listeners[index]);
232
+ }
143
233
  };
144
- }, [${propName}]);
234
+ }, []);
145
235
  `;
146
- }).join("");
147
236
  }
148
- function getNamedSlots(component, namedSlots) {
149
- if (namedSlots === "none") return [];
150
- return Object.keys(component.slots).filter((name) => name !== "default");
151
- }
152
- function generateNamedSlotRenderLines(namedSlots) {
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
- }).join("");
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 : "auto",
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.2",
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/component-analyzer": "0.1.0-beta.2",
40
- "@zeus-js/bundler-plugin": "0.1.0-beta.2",
41
- "@zeus-js/component-dts": "0.1.0-beta.2"
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 || >=19"
44
+ "react": ">=18"
45
45
  },
46
46
  "peerDependenciesMeta": {
47
47
  "react": {