nukejs 0.0.25 → 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.
@@ -12,6 +12,7 @@ import {
12
12
  findPageLayouts,
13
13
  buildPerPageRegistry,
14
14
  makePageAdapterSource,
15
+ buildClientComponentTagImports,
15
16
  buildCombinedBundle,
16
17
  copyPublicFiles
17
18
  } from "./build-common.js";
@@ -572,6 +573,7 @@ if (serverPages.length > 0 || ["_404.tsx", "_500.tsx"].some((f) => fs.existsSync
572
573
  pageImport: JSON.stringify("./" + path.basename(page.absPath)),
573
574
  layoutImports,
574
575
  clientComponentNames,
576
+ clientComponentTagImports: buildClientComponentTagImports(registry, adapterDir),
575
577
  allClientIds: [...registry.keys()],
576
578
  layoutArrayItems: layoutPaths.map((_, i) => `__layout_${i}__`).join(", "),
577
579
  prerenderedHtml: prerenderedRecord,
@@ -609,6 +611,7 @@ if (serverPages.length > 0 || ["_404.tsx", "_500.tsx"].some((f) => fs.existsSync
609
611
  pageImport: JSON.stringify("./" + path.basename(src)),
610
612
  layoutImports,
611
613
  clientComponentNames,
614
+ clientComponentTagImports: buildClientComponentTagImports(registry, adapterDir),
612
615
  allClientIds: [...registry.keys()],
613
616
  layoutArrayItems: layoutPaths.map((_, i) => `__layout_${i}__`).join(", "),
614
617
  prerenderedHtml: prerenderedRecord,
@@ -209,8 +209,8 @@ async function buildPages(pagesDir, staticDir, outPagesDir) {
209
209
  const bundleText = await bundlePageHandler({
210
210
  absPath: page.absPath,
211
211
  pagesDir,
212
+ registry,
212
213
  clientComponentNames,
213
- allClientIds: [...registry.keys()],
214
214
  layoutPaths,
215
215
  prerenderedHtml: prerenderedRecord,
216
216
  routeParamNames: page.paramNames,
@@ -292,11 +292,20 @@ export default async function handler(req: IncomingMessage, res: ServerResponse)
292
292
  }
293
293
  `;
294
294
  }
295
+ function buildClientComponentTagImports(registry, adapterDir) {
296
+ return [...registry.entries()].map(([id, absPath], i) => {
297
+ const rel = path.relative(adapterDir, absPath).replace(/\\/g, "/");
298
+ const spec = JSON.stringify(rel.startsWith(".") ? rel : "./" + rel);
299
+ return `import __cc_tag_${i}__ from ${spec};
300
+ if (typeof __cc_tag_${i}__ === 'function') (__cc_tag_${i}__ as any).__nukeClientId = ${JSON.stringify(id)};`;
301
+ }).join("\n");
302
+ }
295
303
  function makePageAdapterSource(opts) {
296
304
  const {
297
305
  pageImport,
298
306
  layoutImports,
299
307
  clientComponentNames,
308
+ clientComponentTagImports,
300
309
  allClientIds,
301
310
  layoutArrayItems,
302
311
  prerenderedHtml,
@@ -309,6 +318,7 @@ import { createElement as __createElement__ } from 'react';
309
318
  import { renderToString as __renderToString__ } from 'react-dom/server';
310
319
  import * as __page__ from ${pageImport};
311
320
  ${layoutImports}
321
+ ${clientComponentTagImports}
312
322
 
313
323
  const CLIENT_COMPONENTS: Record<string, string> = ${JSON.stringify(clientComponentNames)};
314
324
  const ALL_CLIENT_IDS: string[] = ${JSON.stringify(allClientIds)};
@@ -485,27 +495,83 @@ function buildWrapperAttrString(attrs: Record<string, any>): string {
485
495
  return parts.length ? ' ' + parts.join(' ') : '';
486
496
  }
487
497
 
488
- function serializeProps(value: any): any {
489
- if (typeof value === 'function') return undefined; // must come before the object check
490
- if (value == null || typeof value !== 'object') return value;
491
- if (Array.isArray(value)) return value.map(serializeProps).filter((v: any) => v !== undefined);
492
- if ((value as any).$$typeof) {
493
- const { type, props: p } = value as any;
494
- if (typeof type === 'string') return { __re: 'html', tag: type, props: serializeProps(p) };
495
- if (typeof type === 'function') {
496
- const cid = CLIENT_COMPONENTS[type.name];
497
- if (cid) return { __re: 'client', componentId: cid, props: serializeProps(p) };
498
- }
499
- return undefined;
500
- }
501
- const out: any = {};
502
- for (const [k, v] of Object.entries(value as Record<string, any>)) {
503
- const s = serializeProps(v);
504
- 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
+ };
505
523
  }
524
+
525
+ if (value.$$typeof) return prepareElement(value, hydrated);
526
+
527
+ const out = await prepareProps(value, hydrated);
506
528
  return out;
507
529
  }
508
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
+
509
575
  async function renderNode(node: any, hydrated: Set<string>): Promise<string> {
510
576
  if (node == null || typeof node === 'boolean') return '';
511
577
  if (typeof node === 'string') return escapeHtml(node);
@@ -542,15 +608,15 @@ async function renderNode(node: any, hydrated: Set<string>): Promise<string> {
542
608
  }
543
609
 
544
610
  if (typeof type === 'function') {
545
- const clientId = CLIENT_COMPONENTS[type.name];
611
+ const clientId = (type as any).__nukeClientId ?? CLIENT_COMPONENTS[type.name];
546
612
  if (clientId) {
547
613
  hydrated.add(clientId);
548
614
  const { wrapperAttrs, componentProps } = splitWrapperAttrs(props);
549
615
  const wrapperAttrStr = buildWrapperAttrString(wrapperAttrs);
550
- const serializedProps = serializeProps(componentProps ?? {});
616
+ const { real: hydrationSafeProps, json: serializedProps } = await prepareProps(componentProps ?? {}, hydrated);
551
617
  let ssrHtml: string;
552
618
  try {
553
- ssrHtml = __renderToString__(__createElement__(type as any, componentProps || {}));
619
+ ssrHtml = __renderToString__(__createElement__(type as any, hydrationSafeProps || {}));
554
620
  } catch {
555
621
  ssrHtml = PRERENDERED_HTML[clientId] ?? '';
556
622
  }
@@ -715,8 +781,8 @@ async function bundleApiHandler(absPath) {
715
781
  async function bundlePageHandler(opts) {
716
782
  const {
717
783
  absPath,
784
+ registry,
718
785
  clientComponentNames,
719
- allClientIds,
720
786
  layoutPaths,
721
787
  prerenderedHtml,
722
788
  routeParamNames,
@@ -733,7 +799,8 @@ async function bundlePageHandler(opts) {
733
799
  pageImport: JSON.stringify("./" + path.basename(absPath)),
734
800
  layoutImports,
735
801
  clientComponentNames,
736
- allClientIds,
802
+ clientComponentTagImports: buildClientComponentTagImports(registry, adapterDir),
803
+ allClientIds: [...registry.keys()],
737
804
  layoutArrayItems: layoutPaths.map((_, i) => `__layout_${i}__`).join(", "),
738
805
  prerenderedHtml,
739
806
  routeParamNames,
@@ -835,8 +902,8 @@ async function buildErrorPages(pagesDir, outPagesDir, prerenderedHtml) {
835
902
  const bundleText = await bundlePageHandler({
836
903
  absPath: src,
837
904
  pagesDir,
905
+ registry,
838
906
  clientComponentNames,
839
- allClientIds: [...registry.keys()],
840
907
  layoutPaths,
841
908
  prerenderedHtml,
842
909
  routeParamNames: [],
@@ -922,6 +989,7 @@ function copyPublicFiles(publicDir, destDir) {
922
989
  }
923
990
  export {
924
991
  analyzeFile,
992
+ buildClientComponentTagImports,
925
993
  buildCombinedBundle,
926
994
  buildErrorPages,
927
995
  buildPages,
@@ -12,6 +12,7 @@ import {
12
12
  findPageLayouts,
13
13
  buildPerPageRegistry,
14
14
  makePageAdapterSource,
15
+ buildClientComponentTagImports,
15
16
  buildCombinedBundle,
16
17
  copyPublicFiles
17
18
  } from "./build-common.js";
@@ -294,6 +295,7 @@ if (serverPages.length > 0 || hasErrorPages) {
294
295
  pageImport: JSON.stringify("./" + path.basename(page.absPath)),
295
296
  layoutImports,
296
297
  clientComponentNames,
298
+ clientComponentTagImports: buildClientComponentTagImports(registry, adapterDir),
297
299
  allClientIds: [...registry.keys()],
298
300
  layoutArrayItems: layoutPaths.map((_, i) => `__layout_${i}__`).join(", "),
299
301
  prerenderedHtml: prerenderedRecord,
@@ -328,6 +330,7 @@ if (serverPages.length > 0 || hasErrorPages) {
328
330
  pageImport: JSON.stringify("./" + path.basename(src)),
329
331
  layoutImports,
330
332
  clientComponentNames,
333
+ clientComponentTagImports: buildClientComponentTagImports(registry, adapterDir),
331
334
  allClientIds: [...registry.keys()],
332
335
  layoutArrayItems: layoutPaths.map((_, i) => `__layout_${i}__`).join(", "),
333
336
  prerenderedHtml: prerenderedRecord,
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
@@ -81,14 +81,18 @@ async function renderFunctionComponent(type, props, ctx) {
81
81
  for (const [id, filePath] of ctx.registry.entries()) {
82
82
  const info = componentCache.get(filePath);
83
83
  if (!info?.isClientComponent) continue;
84
- if (!info.exportedName || type.name !== info.exportedName) continue;
84
+ if (type.__nukeClientId === id) {
85
+ } else {
86
+ if (!info.exportedName || type.name !== info.exportedName) continue;
87
+ type.__nukeClientId = id;
88
+ }
85
89
  try {
86
90
  ctx.hydrated.add(id);
87
91
  const { wrapperAttrs, componentProps } = splitWrapperAttrs(props);
88
92
  const wrapperAttrStr = buildWrapperAttrString(wrapperAttrs);
89
- const serializedProps = serializePropsForHydration(componentProps, ctx.registry);
93
+ const { real: hydrationSafeProps, json: serializedProps } = await prepareProps(componentProps, ctx);
90
94
  log.verbose(`Client component rendered for hydration: ${id} (${path.basename(filePath)})`);
91
- const html = ctx.skipClientSSR ? "" : renderToString(createElement(type, componentProps));
95
+ const html = ctx.skipClientSSR ? "" : renderToString(createElement(type, hydrationSafeProps));
92
96
  return `<span data-hydrate-id="${id}"${wrapperAttrStr} data-hydrate-props="${escapeHtml(
93
97
  JSON.stringify(serializedProps)
94
98
  )}">${html}</span>`;
@@ -101,50 +105,72 @@ async function renderFunctionComponent(type, props, ctx) {
101
105
  const resolved = result?.then ? await result : result;
102
106
  return renderElementToHtml(resolved, ctx);
103
107
  }
104
- function serializePropsForHydration(props, registry) {
105
- if (!props || typeof props !== "object") return props;
106
- 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 = {};
107
112
  for (const [key, value] of Object.entries(props)) {
108
- const s = serializeValue(value, registry);
109
- 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;
110
116
  }
111
- return out;
117
+ return { real, json };
112
118
  }
113
- function serializeValue(value, registry) {
114
- if (value === null || value === void 0) return value;
115
- if (typeof value === "function") return void 0;
116
- if (typeof value !== "object") return value;
117
- if (Array.isArray(value))
118
- return value.map((v) => serializeValue(v, registry)).filter((v) => v !== void 0);
119
- if (value.$$typeof)
120
- return serializeReactElement(value, registry);
121
- 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 = {};
122
133
  for (const [k, v] of Object.entries(value)) {
123
- const s = serializeValue(v, registry);
124
- 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;
125
137
  }
126
- return out;
138
+ return { real, json };
127
139
  }
128
- function serializeReactElement(element, registry) {
140
+ async function prepareElement(element, ctx) {
129
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
+ }
130
146
  if (typeof type === "string") {
131
- 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 } };
132
149
  }
133
150
  if (typeof type === "function") {
134
151
  const componentCache = getComponentCache();
135
- for (const [id, filePath] of registry.entries()) {
152
+ for (const [id, filePath] of ctx.registry.entries()) {
136
153
  const info = componentCache.get(filePath);
137
154
  if (!info?.isClientComponent) continue;
138
- if (info.exportedName && type.name === info.exportedName) {
155
+ if (type.__nukeClientId === id || info.exportedName && type.name === info.exportedName) {
156
+ type.__nukeClientId = id;
157
+ const p = await prepareProps(props, ctx);
139
158
  return {
140
- __re: "client",
141
- componentId: id,
142
- 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 }
143
162
  };
144
163
  }
145
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 } };
146
172
  }
147
- return void 0;
173
+ return { real: element, json: void 0 };
148
174
  }
149
175
  export {
150
176
  renderElementToHtml
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nukejs",
3
- "version": "0.0.25",
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",