@zeus-js/output-react-wrapper 0.1.0-beta.3 → 0.1.0-beta.5

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.3
2
+ * output-react-wrapper v0.1.0-beta.5
3
3
  * (c) 2026 baicie
4
4
  * Released under the MIT License.
5
5
  **/
@@ -9,6 +9,7 @@ Object.defineProperties(exports, {
9
9
  });
10
10
  let _zeus_js_bundler_plugin = require("@zeus-js/bundler-plugin");
11
11
  let _zeus_js_component_dts = require("@zeus-js/component-dts");
12
+ let _lit_react = require("@lit/react");
12
13
  //#region packages/web-c/output-react-wrapper/src/generateReactIndex.ts
13
14
  function generateReactIndex(components, options) {
14
15
  const lines = [];
@@ -20,211 +21,362 @@ function generateReactIndex(components, options) {
20
21
  return lines.join("\n");
21
22
  }
22
23
  //#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
24
  //#region packages/web-c/output-react-wrapper/src/generateReactWrapper.ts
29
25
  function generateReactWrapper(input) {
30
- const { mode = "minimal" } = input;
31
- if (mode === "minimal") return generateMinimalReactWrapper(input);
32
- return generateEventBridgeReactWrapper(input);
33
- }
34
- function createBindings(names, prefix) {
35
- return names.map((sourceName, index) => ({
36
- sourceName,
37
- localName: `${prefix}${index}`
38
- }));
39
- }
40
- function createEventBindings(eventNames) {
41
- return eventNames.map((eventName, index) => ({
42
- eventName,
43
- sourceName: toReactEventProp(eventName),
44
- localName: `eventHandler${index}`
45
- }));
46
- }
47
- function generateDestructuredBindings(bindings) {
48
- if (!bindings.length) return "";
49
- return bindings.map(({ sourceName, localName }) => {
50
- return `${JSON.stringify(sourceName)}: ${localName},`;
51
- }).join("\n ");
26
+ return input.mode === "runtime" ? generateRuntimeReactWrapper(input) : input.mode === "event-bridge" ? generateEventBridgeReactWrapper(input) : generateMinimalReactWrapper(input);
52
27
  }
53
28
  function generateMinimalReactWrapper(input) {
54
29
  const { component, namedSlots, wcModuleId } = input;
55
30
  const slotBindings = createBindings(getNamedSlots(component, namedSlots), "slotValue");
56
- const slotDestructure = slotBindings.length ? `\n ${generateDestructuredBindings(slotBindings)}` : "";
31
+ const omittedKeys = ["children", ...slotBindings.map(({ sourceName }) => sourceName)];
32
+ const slotAssignments = generatePropAssignments(slotBindings);
57
33
  const namedSlotLines = generateMinimalNamedSlots(slotBindings);
34
+ const childSetup = slotBindings.length ? `${namedSlotLines}
35
+ const childArgs = [];
36
+ pushAll(childArgs, slotNodes);
37
+ if (children != null) childArgs.push(children);` : "";
38
+ const render = slotBindings.length ? `React.createElement.apply(
39
+ React,
40
+ [${JSON.stringify(component.tag)}, rest].concat(childArgs),
41
+ )` : `React.createElement(${JSON.stringify(component.tag)}, rest, children)`;
58
42
  return `
59
43
  import * as React from 'react';
60
44
 
61
45
  import ${JSON.stringify(wcModuleId)};
62
46
 
47
+ const OMITTED_PROPS = new Set(${JSON.stringify(omittedKeys)});
48
+
63
49
  export const ${component.name} = React.forwardRef(
64
- function ${component.name}({
65
- children,${slotDestructure}
66
- ...rest
67
- } = {}, ref) {
68
- ${namedSlotLines}
69
- return React.createElement(
70
- ${JSON.stringify(component.tag)},
71
- {
72
- ...rest,
73
- ref,
74
- },
75
- ${namedSlotLines ? " ...slotNodes,\n" : ""} children,
76
- );
50
+ function ${component.name}(inputProps, ref) {
51
+ const props = inputProps || {};
52
+ const children = props.children;
53
+ ${slotAssignments}
54
+ const rest = omitProps(props);
55
+ rest.ref = ref;
56
+ ${childSetup}
57
+
58
+ return ${render};
77
59
  },
78
60
  );
61
+
62
+ function omitProps(source) {
63
+ const output = {};
64
+ for (const key in source) {
65
+ if (hasOwn(source, key) && !OMITTED_PROPS.has(key)) {
66
+ output[key] = source[key];
67
+ }
68
+ }
69
+ return output;
70
+ }
71
+
72
+ function hasOwn(source, key) {
73
+ return Object.prototype.hasOwnProperty.call(source, key);
74
+ }
75
+ ${slotBindings.length ? PUSH_ALL_HELPER : ""}
79
76
  `.trimStart();
80
77
  }
81
78
  function generateEventBridgeReactWrapper(input) {
82
79
  const { component, namedSlots, wcModuleId } = input;
83
80
  const propBindings = createBindings(Object.keys(component.props), "propValue");
84
- const eventBindings = createEventBindings(Object.keys(component.events));
81
+ const eventBindings = createEventBindings(component.events);
82
+ if (!propBindings.length && !eventBindings.length) return generateMinimalReactWrapper(input);
85
83
  const slotBindings = createBindings(getNamedSlots(component, namedSlots), "slotValue");
86
- const destructuredProps = generateDestructuredBindings([
84
+ const destructuredBindings = [
87
85
  ...propBindings,
88
86
  ...eventBindings,
89
87
  ...slotBindings
90
- ]);
88
+ ];
89
+ const omittedKeys = [
90
+ "children",
91
+ "className",
92
+ "style",
93
+ ...destructuredBindings.map(({ sourceName }) => sourceName)
94
+ ];
91
95
  return `
92
96
  import {
93
- createElement,
94
- cloneElement,
95
- Fragment,
96
- forwardRef,
97
- isValidElement,
98
- useEffect,
99
- useImperativeHandle,
100
- useRef,
97
+ ${[
98
+ "createElement",
99
+ ...slotBindings.length ? [
100
+ "cloneElement",
101
+ "Fragment",
102
+ "isValidElement"
103
+ ] : [],
104
+ "forwardRef",
105
+ ...propBindings.length || eventBindings.length ? ["useEffect"] : [],
106
+ "useImperativeHandle",
107
+ "useRef"
108
+ ].join(",\n ")},
101
109
  } from 'react';
102
110
 
103
111
  import ${JSON.stringify(wcModuleId)};
104
112
 
105
- export const ${component.name} = forwardRef(function ${component.name}(props, ref) {
106
- const {
107
- children,
108
- className,
109
- style,
110
- ${destructuredProps}
111
- ...rest
112
- } = props;
113
-
114
- const innerRef = useRef(null);
115
- const previousPropKeysRef = useRef(new Set());
116
-
117
- useImperativeHandle(ref, () => innerRef.current);
118
-
119
- ${generatePropSyncLines(propBindings)}
120
-
121
- ${generateEventEffects(eventBindings)}
113
+ const OMITTED_PROPS = new Set(${JSON.stringify(omittedKeys)});
114
+ ${eventBindings.length ? `const EVENT_NAMES = ${JSON.stringify(eventBindings.map((binding) => binding.eventName))};` : ""}
122
115
 
123
- const slotChildren = [];
116
+ export const ${component.name} = forwardRef(function ${component.name}(inputProps, ref) {
117
+ const props = inputProps || {};
118
+ const children = props.children;
119
+ const className = props.className;
120
+ const style = props.style;
121
+ ${generatePropAssignments(destructuredBindings)}
122
+ ${generatePropPresenceAssignments(propBindings)}
123
+ const rest = omitProps(props);
124
124
 
125
- ${generateNamedSlotRenderLines(slotBindings)}
125
+ const innerRef = useRef(null);
126
+ ${generatePropRefs(propBindings)}
127
+ ${generateEventRefs(eventBindings)}
128
+ useImperativeHandle(ref, () => innerRef.current, []);
126
129
 
127
- if (children != null) {
128
- slotChildren.push(children);
129
- }
130
+ ${generatePropSyncEffect(propBindings)}
131
+ ${generateEventEffect(eventBindings)}
132
+ ${generateChildrenSetup(slotBindings)}
133
+ rest.ref = innerRef;
134
+ rest.className = className;
135
+ rest.style = style;
130
136
 
131
- return createElement(
132
- ${JSON.stringify(component.tag)},
133
- {
134
- ...rest,
135
- ref: innerRef,
136
- className,
137
- style,
138
- },
139
- ...slotChildren,
140
- );
137
+ return ${generateReactRender(component.tag, slotBindings)};
141
138
  });
142
-
143
- function createNamedSlot(name, value) {
144
- if (value == null || value === false) return null;
145
-
146
- if (
147
- isValidElement(value) &&
148
- value.type !== Fragment
149
- ) {
150
- return cloneElement(value, { slot: name });
139
+ ${slotBindings.length ? NAMED_SLOT_HELPER : ""}
140
+ function omitProps(source) {
141
+ const output = {};
142
+ for (const key in source) {
143
+ if (hasOwn(source, key) && !OMITTED_PROPS.has(key)) {
144
+ output[key] = source[key];
145
+ }
151
146
  }
147
+ return output;
148
+ }
152
149
 
153
- return createElement(
154
- 'span',
155
- {
156
- slot: name,
157
- style: { display: 'contents' },
158
- },
159
- value,
160
- );
150
+ function hasOwn(source, key) {
151
+ return Object.prototype.hasOwnProperty.call(source, key);
161
152
  }
162
153
  `.trimStart();
163
154
  }
164
- function generatePropSyncLines(bindings) {
165
- if (!bindings.length) return "// no props";
166
- return `useEffect(() => {
155
+ function createBindings(names, prefix) {
156
+ return names.map((sourceName, index) => ({
157
+ sourceName,
158
+ localName: `${prefix}${index}`
159
+ }));
160
+ }
161
+ function createEventBindings(events) {
162
+ return Object.entries(events).map(([key, event], index) => {
163
+ var _event$key, _event$name, _event$reactName;
164
+ const sourceEventName = (_event$key = event.key) !== null && _event$key !== void 0 ? _event$key : key;
165
+ return {
166
+ eventName: (_event$name = event.name) !== null && _event$name !== void 0 ? _event$name : toKebabCase(sourceEventName),
167
+ sourceName: (_event$reactName = event.reactName) !== null && _event$reactName !== void 0 ? _event$reactName : toReactEventProp(sourceEventName),
168
+ localName: `eventHandler${index}`
169
+ };
170
+ });
171
+ }
172
+ function generatePropAssignments(bindings) {
173
+ return bindings.map(({ sourceName, localName }) => ` const ${localName} = props[${JSON.stringify(sourceName)}];`).join("\n");
174
+ }
175
+ function generatePropPresenceAssignments(bindings) {
176
+ return bindings.map(({ sourceName }, index) => ` const propPresent${index} = hasOwn(props, ${JSON.stringify(sourceName)});`).join("\n");
177
+ }
178
+ function generatePropRefs(bindings) {
179
+ if (!bindings.length) return "";
180
+ return ` const previousPropPresenceRef = useRef([]);
181
+ const previousPropValuesRef = useRef([]);`;
182
+ }
183
+ function generateEventRefs(bindings) {
184
+ if (!bindings.length) return "";
185
+ return ` const eventHandlersRef = useRef([]);
186
+ ${bindings.map(({ localName }, index) => ` eventHandlersRef.current[${index}] = ${localName};`).join("\n")}`;
187
+ }
188
+ function generatePropSyncEffect(bindings) {
189
+ if (!bindings.length) return "";
190
+ return ` useEffect(() => {
167
191
  const el = innerRef.current;
168
192
  if (!el) return;
169
193
 
170
- const previousPropKeys = previousPropKeysRef.current;
194
+ const previousPropPresence = previousPropPresenceRef.current;
195
+ const previousPropValues = previousPropValuesRef.current;
171
196
 
172
- ${bindings.map(({ sourceName, localName }) => {
197
+ ${bindings.map(({ sourceName, localName }, index) => {
173
198
  const key = JSON.stringify(sourceName);
174
- return `if (Object.prototype.hasOwnProperty.call(props, ${key})) {
175
- el[${key}] = ${localName};
176
- previousPropKeys.add(${key});
177
- } else if (previousPropKeys.has(${key})) {
199
+ return ` if (propPresent${index}) {
200
+ if (
201
+ !previousPropPresence[${index}] ||
202
+ !Object.is(previousPropValues[${index}], ${localName})
203
+ ) {
204
+ el[${key}] = ${localName};
205
+ previousPropValues[${index}] = ${localName};
206
+ }
207
+ previousPropPresence[${index}] = true;
208
+ } else if (previousPropPresence[${index}]) {
178
209
  el[${key}] = undefined;
179
- previousPropKeys.delete(${key});
210
+ previousPropPresence[${index}] = false;
211
+ previousPropValues[${index}] = undefined;
180
212
  }`;
181
- }).join("\n\n ")}
182
- }, [props, ${bindings.map((binding) => binding.localName).join(", ")}]);`;
213
+ }).join("\n\n")}
214
+ }, [${bindings.flatMap((binding, index) => [`propPresent${index}`, binding.localName]).join(", ")}]);
215
+ `;
183
216
  }
184
- function generateEventEffects(bindings) {
185
- return bindings.map(({ eventName, localName }) => {
186
- return `
187
- useEffect(() => {
217
+ function generateEventEffect(bindings) {
218
+ if (!bindings.length) return "";
219
+ return ` useEffect(() => {
188
220
  const el = innerRef.current;
189
- if (!el || !${localName}) return;
221
+ if (!el) return;
190
222
 
191
- const handler = event => {
192
- ${localName}(event);
193
- };
223
+ const listeners = EVENT_NAMES.map(
224
+ (_eventName, index) => event => {
225
+ const handler = eventHandlersRef.current[index];
226
+ if (handler) handler(event);
227
+ },
228
+ );
194
229
 
195
- el.addEventListener(${JSON.stringify(eventName)}, handler);
230
+ for (let index = 0; index < EVENT_NAMES.length; index += 1) {
231
+ el.addEventListener(EVENT_NAMES[index], listeners[index]);
232
+ }
196
233
 
197
234
  return () => {
198
- el.removeEventListener(${JSON.stringify(eventName)}, handler);
235
+ for (let index = 0; index < EVENT_NAMES.length; index += 1) {
236
+ el.removeEventListener(EVENT_NAMES[index], listeners[index]);
237
+ }
199
238
  };
200
- }, [${localName}]);
239
+ }, []);
201
240
  `;
202
- }).join("");
203
- }
204
- function getNamedSlots(component, namedSlots) {
205
- if (namedSlots === "none") return [];
206
- return Object.keys(component.slots).filter((name) => name !== "default");
207
241
  }
208
- function generateNamedSlotRenderLines(bindings) {
209
- return bindings.map(({ sourceName, localName }) => {
210
- return `
211
- {
242
+ function generateChildrenSetup(bindings) {
243
+ if (!bindings.length) return "";
244
+ return ` const slotChildren = [];
245
+ ${bindings.map(({ sourceName, localName }) => ` {
212
246
  const node = createNamedSlot(${JSON.stringify(sourceName)}, ${localName});
213
247
  if (node != null) slotChildren.push(node);
214
- }
248
+ }`).join("\n")}
249
+ if (children != null) slotChildren.push(children);
215
250
  `;
216
- }).join("");
251
+ }
252
+ function generateReactRender(tag, slotBindings) {
253
+ if (!slotBindings.length) return `createElement(${JSON.stringify(tag)}, rest, children)`;
254
+ return `createElement.apply(
255
+ null,
256
+ [${JSON.stringify(tag)}, rest].concat(slotChildren),
257
+ )`;
258
+ }
259
+ function getNamedSlots(component, namedSlots) {
260
+ return namedSlots === "none" ? [] : Object.keys(component.slots).filter((name) => name !== "default");
217
261
  }
218
262
  function generateMinimalNamedSlots(bindings) {
219
263
  if (!bindings.length) return "";
220
- return bindings.map(({ sourceName, localName }, index) => {
221
- return ` const ${`slotNode${index}`} = ${localName} != null && ${localName} !== false
264
+ return `${bindings.map(({ sourceName, localName }, index) => {
265
+ return ` const slotNode${index} = ${localName} != null && ${localName} !== false
222
266
  ? (React.isValidElement(${localName}) && ${localName}.type !== React.Fragment
223
267
  ? React.cloneElement(${localName}, { slot: ${JSON.stringify(sourceName)} })
224
268
  : React.createElement('span', { slot: ${JSON.stringify(sourceName)}, style: { display: 'contents' } }, ${localName}))
225
- : null;
269
+ : null;`;
270
+ }).join("\n")}
271
+ const slotNodes = [${bindings.map((_, index) => `slotNode${index}`).join(", ")}].filter(Boolean);`;
272
+ }
273
+ function toKebabCase(value) {
274
+ return value.replace(/[A-Z]/g, (match) => `-${match.toLowerCase()}`);
275
+ }
276
+ function toReactEventProp(value) {
277
+ return `on${value.split("-").filter(Boolean).map((part) => `${part.charAt(0).toUpperCase()}${part.slice(1)}`).join("")}`;
278
+ }
279
+ function generateRuntimeReactWrapper(input) {
280
+ const { component, namedSlots } = input;
281
+ const events = createRuntimeEventMap(component);
282
+ const slots = getNamedSlots(component, namedSlots);
283
+ return [
284
+ `import React from 'react'`,
285
+ `import { createComponent } from '@zeus-js/output-react-wrapper/runtime'`,
286
+ `import { defineCustomElement } from '../wc/loader.js'`,
287
+ ``,
288
+ `export const ${component.name} = createComponent({`,
289
+ ` react: React,`,
290
+ ` tagName: ${JSON.stringify(component.tag)},`,
291
+ ` defineCustomElement: () => defineCustomElement(${JSON.stringify(component.tag)}),`,
292
+ ` events: ${formatEventObject(events)},`,
293
+ ` slots: ${JSON.stringify(slots)},`,
294
+ ` displayName: ${JSON.stringify(component.name)},`,
295
+ `})`,
296
+ ``
297
+ ].join("\n");
298
+ }
299
+ function createRuntimeEventMap(component) {
300
+ const events = {};
301
+ for (const [key, event] of Object.entries(component.events)) {
302
+ var _event$key2, _event$name2, _event$reactName2;
303
+ const sourceEventName = (_event$key2 = event.key) !== null && _event$key2 !== void 0 ? _event$key2 : key;
304
+ const domEventName = (_event$name2 = event.name) !== null && _event$name2 !== void 0 ? _event$name2 : toKebabCase(sourceEventName);
305
+ const reactPropName = (_event$reactName2 = event.reactName) !== null && _event$reactName2 !== void 0 ? _event$reactName2 : toReactEventProp(sourceEventName);
306
+ events[reactPropName] = domEventName;
307
+ }
308
+ return events;
309
+ }
310
+ function formatEventObject(value) {
311
+ const entries = Object.entries(value);
312
+ if (!entries.length) return "{}";
313
+ return `{\n${entries.map(([key, item]) => ` ${JSON.stringify(key)}: ${JSON.stringify(item)}`).join(",\n")}\n }`;
314
+ }
315
+ const PUSH_ALL_HELPER = `
316
+ function pushAll(target, values) {
317
+ for (const value of values) target.push(value);
318
+ }
319
+ `;
320
+ const NAMED_SLOT_HELPER = `
321
+ function createNamedSlot(name, value) {
322
+ if (value == null || value === false) return null;
323
+
324
+ if (isValidElement(value) && value.type !== Fragment) {
325
+ return cloneElement(value, { slot: name });
326
+ }
327
+
328
+ return createElement(
329
+ 'span',
330
+ { slot: name, style: { display: 'contents' } },
331
+ value,
332
+ );
333
+ }
226
334
  `;
227
- }).join("") + "\n const slotNodes = [" + bindings.map((_, index) => `slotNode${index}`).join(", ") + "].filter(Boolean);\n";
335
+ //#endregion
336
+ //#region packages/web-c/output-react-wrapper/src/runtime/createComponent.ts
337
+ function createComponent(options) {
338
+ var _customElements$get;
339
+ const { defineCustomElement, react, tagName, transformTag, elementClass, events = {}, slots = [], displayName } = options;
340
+ const finalTagName = transformTag ? transformTag(tagName) : tagName;
341
+ defineCustomElement === null || defineCustomElement === void 0 || defineCustomElement();
342
+ const LitComponent = (0, _lit_react.createComponent)({
343
+ react,
344
+ tagName: finalTagName,
345
+ elementClass: elementClass !== null && elementClass !== void 0 ? elementClass : typeof customElements === "undefined" ? HTMLElement : (_customElements$get = customElements.get(finalTagName)) !== null && _customElements$get !== void 0 ? _customElements$get : HTMLElement,
346
+ events,
347
+ displayName
348
+ });
349
+ if (!slots.length) return LitComponent;
350
+ const slotSet = new Set(slots);
351
+ return react.forwardRef((inputProps, ref) => {
352
+ const rest = {};
353
+ const slottedChildren = [];
354
+ if (inputProps == null) {
355
+ rest.ref = ref;
356
+ return react.createElement(LitComponent, rest);
357
+ }
358
+ const props = inputProps;
359
+ for (const key in props) {
360
+ if (!Object.prototype.hasOwnProperty.call(props, key)) continue;
361
+ const value = props[key];
362
+ if (slotSet.has(key)) {
363
+ const child = createNamedSlot(react, key, value);
364
+ if (child != null) slottedChildren.push(child);
365
+ continue;
366
+ }
367
+ rest[key] = value;
368
+ }
369
+ rest.ref = ref;
370
+ if (props.children != null) slottedChildren.push(props.children);
371
+ return react.createElement(LitComponent, rest, ...slottedChildren);
372
+ });
373
+ }
374
+ function createNamedSlot(react, name, value) {
375
+ if (value == null || value === false) return null;
376
+ return react.createElement("span", {
377
+ slot: name,
378
+ style: { display: "contents" }
379
+ }, value);
228
380
  }
229
381
  //#endregion
230
382
  //#region packages/web-c/output-react-wrapper/src/index.ts
@@ -237,7 +389,7 @@ function reactWrapper(options = {}) {
237
389
  dts: (_options$dts = options.dts) !== null && _options$dts !== void 0 ? _options$dts : true,
238
390
  index: (_options$index = options.index) !== null && _options$index !== void 0 ? _options$index : true,
239
391
  namedSlots: (_options$namedSlots = options.namedSlots) !== null && _options$namedSlots !== void 0 ? _options$namedSlots : "props",
240
- wrapper: (_options$wrapper = options.wrapper) !== null && _options$wrapper !== void 0 ? _options$wrapper : "minimal"
392
+ wrapper: (_options$wrapper = options.wrapper) !== null && _options$wrapper !== void 0 ? _options$wrapper : "runtime"
241
393
  };
242
394
  return {
243
395
  name: "zeus-output-react-wrapper",
@@ -280,4 +432,5 @@ function reactWrapper(options = {}) {
280
432
  };
281
433
  }
282
434
  //#endregion
435
+ exports.createComponent = createComponent;
283
436
  exports.default = reactWrapper;