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.
- package/dist/build-cloudflare.js +3 -0
- package/dist/build-common.js +92 -24
- package/dist/build-vercel.js +3 -0
- package/dist/bundle.js +14 -0
- package/dist/renderer.js +55 -29
- package/package.json +1 -1
package/dist/build-cloudflare.js
CHANGED
|
@@ -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,
|
package/dist/build-common.js
CHANGED
|
@@ -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
|
|
489
|
-
if (typeof
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
const
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
}
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
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 =
|
|
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,
|
|
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
|
-
|
|
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,
|
package/dist/build-vercel.js
CHANGED
|
@@ -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 (
|
|
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 =
|
|
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,
|
|
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
|
|
105
|
-
if (!props || typeof props !== "object") return props;
|
|
106
|
-
const
|
|
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
|
|
109
|
-
|
|
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
|
|
117
|
+
return { real, json };
|
|
112
118
|
}
|
|
113
|
-
function
|
|
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
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
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
|
|
124
|
-
|
|
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
|
|
138
|
+
return { real, json };
|
|
127
139
|
}
|
|
128
|
-
function
|
|
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
|
-
|
|
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
|
-
|
|
141
|
-
|
|
142
|
-
|
|
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.
|
|
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",
|