nukejs 0.0.26 → 0.0.27

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.
@@ -495,27 +495,83 @@ function buildWrapperAttrString(attrs: Record<string, any>): string {
495
495
  return parts.length ? ' ' + parts.join(' ') : '';
496
496
  }
497
497
 
498
- function serializeProps(value: any): any {
499
- if (typeof value === 'function') return undefined; // must come before the object check
500
- if (value == null || typeof value !== 'object') return value;
501
- if (Array.isArray(value)) return value.map(serializeProps).filter((v: any) => v !== undefined);
502
- if ((value as any).$$typeof) {
503
- const { type, props: p } = value as any;
504
- if (typeof type === 'string') return { __re: 'html', tag: type, props: serializeProps(p) };
505
- if (typeof type === 'function') {
506
- const cid = (type as any).__nukeClientId ?? CLIENT_COMPONENTS[type.name];
507
- if (cid) return { __re: 'client', componentId: cid, props: serializeProps(p) };
508
- }
509
- return undefined;
510
- }
511
- const out: any = {};
512
- for (const [k, v] of Object.entries(value as Record<string, any>)) {
513
- const s = serializeProps(v);
514
- if (s !== undefined) out[k] = s;
498
+ function prepareProps(props, hydrated) {
499
+ if (!props || typeof props !== 'object') return Promise.resolve({ real: props, json: props });
500
+ const entries = Object.entries(props);
501
+ return Promise.all(entries.map(([, v]) => prepareValue(v, hydrated))).then((results) => {
502
+ const real = {};
503
+ const json = {};
504
+ entries.forEach(([key], i) => {
505
+ real[key] = results[i].real;
506
+ if (results[i].json !== undefined) json[key] = results[i].json;
507
+ });
508
+ return { real, json };
509
+ });
510
+ }
511
+
512
+ async function prepareValue(value, hydrated) {
513
+ if (value === null || value === undefined) return { real: value, json: value };
514
+ if (typeof value === 'function') return { real: value, json: undefined };
515
+ if (typeof value !== 'object') return { real: value, json: value };
516
+
517
+ if (Array.isArray(value)) {
518
+ const items = await Promise.all(value.map((v) => prepareValue(v, hydrated)));
519
+ return {
520
+ real: items.map((i) => i.real),
521
+ json: items.map((i) => i.json).filter((i) => i !== undefined),
522
+ };
515
523
  }
524
+
525
+ if (value.$$typeof) return prepareElement(value, hydrated);
526
+
527
+ const out = await prepareProps(value, hydrated);
516
528
  return out;
517
529
  }
518
530
 
531
+ // Resolves a single React element found inside a client component's props.
532
+ // Native elements and fragments recurse into their own props. Client
533
+ // components are left untouched for 'real' (react-dom/server renders them
534
+ // normally within the boundary's own renderToString call below) and wired
535
+ // through as { __re: 'client', componentId, props } for the browser to
536
+ // mount for real. Server components can't run in the browser at all, so
537
+ // they're rendered once with this same file's renderNode() (handles async
538
+ // components and any nested client boundaries inside them) and wrapped in
539
+ // an inert <span style="display:contents"> carrying that HTML \u2014
540
+ // identically on both 'real' and 'json', so the SSR markup and the
541
+ // browser's reconstructed tree have the same shape and hydrateRoot() can
542
+ // reconcile them without a mismatch.
543
+ async function prepareElement(element, hydrated) {
544
+ const { type, props } = element;
545
+
546
+ if (type === Symbol.for('react.fragment')) {
547
+ const p = await prepareProps(props, hydrated);
548
+ return { real: __createElement__(Symbol.for('react.fragment'), p.real), json: { __re: 'fragment', props: p.json } };
549
+ }
550
+
551
+ if (typeof type === 'string') {
552
+ const p = await prepareProps(props, hydrated);
553
+ return { real: __createElement__(type, p.real), json: { __re: 'html', tag: type, props: p.json } };
554
+ }
555
+
556
+ if (typeof type === 'function') {
557
+ const cid = type.__nukeClientId ?? CLIENT_COMPONENTS[type.name];
558
+ if (cid) {
559
+ const p = await prepareProps(props, hydrated);
560
+ return { real: element, json: { __re: 'client', componentId: cid, props: p.json } };
561
+ }
562
+
563
+ const html = await renderNode(element, hydrated);
564
+ const wrapperProps = {
565
+ style: { display: 'contents' },
566
+ 'data-n-static': true,
567
+ dangerouslySetInnerHTML: { __html: html },
568
+ };
569
+ return { real: __createElement__('span', wrapperProps), json: { __re: 'static', html } };
570
+ }
571
+
572
+ return { real: element, json: undefined };
573
+ }
574
+
519
575
  async function renderNode(node: any, hydrated: Set<string>): Promise<string> {
520
576
  if (node == null || typeof node === 'boolean') return '';
521
577
  if (typeof node === 'string') return escapeHtml(node);
@@ -557,10 +613,10 @@ async function renderNode(node: any, hydrated: Set<string>): Promise<string> {
557
613
  hydrated.add(clientId);
558
614
  const { wrapperAttrs, componentProps } = splitWrapperAttrs(props);
559
615
  const wrapperAttrStr = buildWrapperAttrString(wrapperAttrs);
560
- const serializedProps = serializeProps(componentProps ?? {});
616
+ const { real: hydrationSafeProps, json: serializedProps } = await prepareProps(componentProps ?? {}, hydrated);
561
617
  let ssrHtml: string;
562
618
  try {
563
- ssrHtml = __renderToString__(__createElement__(type as any, componentProps || {}));
619
+ ssrHtml = __renderToString__(__createElement__(type as any, hydrationSafeProps || {}));
564
620
  } catch {
565
621
  ssrHtml = PRERENDERED_HTML[clientId] ?? '';
566
622
  }
package/dist/bundle.js CHANGED
@@ -50,6 +50,20 @@ async function reconstructElement(node, mods) {
50
50
  const React = await import("react");
51
51
  return React.default.createElement(n.tag, await reconstructProps(n.props, mods));
52
52
  }
53
+ if (node.__re === "fragment") {
54
+ const n = node;
55
+ const React = await import("react");
56
+ return React.default.createElement(React.default.Fragment, await reconstructProps(n.props, mods));
57
+ }
58
+ if (node.__re === "static") {
59
+ const n = node;
60
+ const React = await import("react");
61
+ return React.default.createElement("span", {
62
+ style: { display: "contents" },
63
+ "data-n-static": true,
64
+ dangerouslySetInnerHTML: { __html: n.html }
65
+ });
66
+ }
53
67
  return node;
54
68
  }
55
69
  async function reconstructProps(props, mods) {
package/dist/renderer.js CHANGED
@@ -90,9 +90,9 @@ async function renderFunctionComponent(type, props, ctx) {
90
90
  ctx.hydrated.add(id);
91
91
  const { wrapperAttrs, componentProps } = splitWrapperAttrs(props);
92
92
  const wrapperAttrStr = buildWrapperAttrString(wrapperAttrs);
93
- const serializedProps = serializePropsForHydration(componentProps, ctx.registry);
93
+ const { real: hydrationSafeProps, json: serializedProps } = await prepareProps(componentProps, ctx);
94
94
  log.verbose(`Client component rendered for hydration: ${id} (${path.basename(filePath)})`);
95
- const html = ctx.skipClientSSR ? "" : renderToString(createElement(type, componentProps));
95
+ const html = ctx.skipClientSSR ? "" : renderToString(createElement(type, hydrationSafeProps));
96
96
  return `<span data-hydrate-id="${id}"${wrapperAttrStr} data-hydrate-props="${escapeHtml(
97
97
  JSON.stringify(serializedProps)
98
98
  )}">${html}</span>`;
@@ -105,51 +105,72 @@ async function renderFunctionComponent(type, props, ctx) {
105
105
  const resolved = result?.then ? await result : result;
106
106
  return renderElementToHtml(resolved, ctx);
107
107
  }
108
- function serializePropsForHydration(props, registry) {
109
- if (!props || typeof props !== "object") return props;
110
- const out = {};
108
+ async function prepareProps(props, ctx) {
109
+ if (!props || typeof props !== "object") return { real: props, json: props };
110
+ const real = {};
111
+ const json = {};
111
112
  for (const [key, value] of Object.entries(props)) {
112
- const s = serializeValue(value, registry);
113
- if (s !== void 0) out[key] = s;
113
+ const p = await prepareValue(value, ctx);
114
+ real[key] = p.real;
115
+ if (p.json !== void 0) json[key] = p.json;
114
116
  }
115
- return out;
117
+ return { real, json };
116
118
  }
117
- function serializeValue(value, registry) {
118
- if (value === null || value === void 0) return value;
119
- if (typeof value === "function") return void 0;
120
- if (typeof value !== "object") return value;
121
- if (Array.isArray(value))
122
- return value.map((v) => serializeValue(v, registry)).filter((v) => v !== void 0);
123
- if (value.$$typeof)
124
- return serializeReactElement(value, registry);
125
- const out = {};
119
+ async function prepareValue(value, ctx) {
120
+ if (value === null || value === void 0) return { real: value, json: value };
121
+ if (typeof value === "function") return { real: value, json: void 0 };
122
+ if (typeof value !== "object") return { real: value, json: value };
123
+ if (Array.isArray(value)) {
124
+ const items = await Promise.all(value.map((v) => prepareValue(v, ctx)));
125
+ return {
126
+ real: items.map((i) => i.real),
127
+ json: items.map((i) => i.json).filter((i) => i !== void 0)
128
+ };
129
+ }
130
+ if (value.$$typeof) return prepareElement(value, ctx);
131
+ const real = {};
132
+ const json = {};
126
133
  for (const [k, v] of Object.entries(value)) {
127
- const s = serializeValue(v, registry);
128
- if (s !== void 0) out[k] = s;
134
+ const p = await prepareValue(v, ctx);
135
+ real[k] = p.real;
136
+ if (p.json !== void 0) json[k] = p.json;
129
137
  }
130
- return out;
138
+ return { real, json };
131
139
  }
132
- function serializeReactElement(element, registry) {
140
+ async function prepareElement(element, ctx) {
133
141
  const { type, props } = element;
142
+ if (type === Fragment) {
143
+ const p = await prepareProps(props, ctx);
144
+ return { real: createElement(Fragment, p.real), json: { __re: "fragment", props: p.json } };
145
+ }
134
146
  if (typeof type === "string") {
135
- return { __re: "html", tag: type, props: serializePropsForHydration(props, registry) };
147
+ const p = await prepareProps(props, ctx);
148
+ return { real: createElement(type, p.real), json: { __re: "html", tag: type, props: p.json } };
136
149
  }
137
150
  if (typeof type === "function") {
138
151
  const componentCache = getComponentCache();
139
- for (const [id, filePath] of registry.entries()) {
152
+ for (const [id, filePath] of ctx.registry.entries()) {
140
153
  const info = componentCache.get(filePath);
141
154
  if (!info?.isClientComponent) continue;
142
155
  if (type.__nukeClientId === id || info.exportedName && type.name === info.exportedName) {
143
156
  type.__nukeClientId = id;
157
+ const p = await prepareProps(props, ctx);
144
158
  return {
145
- __re: "client",
146
- componentId: id,
147
- props: serializePropsForHydration(props, registry)
159
+ real: element,
160
+ // untouched — React renders it directly within the outer renderToString call
161
+ json: { __re: "client", componentId: id, props: p.json }
148
162
  };
149
163
  }
150
164
  }
165
+ const html = await renderElementToHtml(element, ctx);
166
+ const wrapperProps = {
167
+ style: { display: "contents" },
168
+ "data-n-static": true,
169
+ dangerouslySetInnerHTML: { __html: html }
170
+ };
171
+ return { real: createElement("span", wrapperProps), json: { __re: "static", html } };
151
172
  }
152
- return void 0;
173
+ return { real: element, json: void 0 };
153
174
  }
154
175
  export {
155
176
  renderElementToHtml
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nukejs",
3
- "version": "0.0.26",
3
+ "version": "0.0.27",
4
4
  "description": "A minimal, opinionated full-stack React framework on Node.js that server-renders everything and hydrates only interactive parts.",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",