nukejs 0.0.26 → 0.0.28
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/README.md +68 -0
- package/dist/build-common.js +116 -20
- package/dist/bundle.js +14 -0
- package/dist/cache-store.js +48 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +2 -0
- package/dist/renderer.js +48 -27
- package/dist/ssr.js +3 -2
- package/dist/use-router.js +5 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -25,6 +25,7 @@ npm create nuke@latest
|
|
|
25
25
|
- [Configuration](#configuration)
|
|
26
26
|
- [Link Component & Navigation](#link-component--navigation)
|
|
27
27
|
- [useRequest() — URL Params, Query & Headers](#userequest--url-params-query--headers)
|
|
28
|
+
- [cache() — Request-Scoped Data Caching](#cache--request-scoped-data-caching)
|
|
28
29
|
- [Error Pages](#error-pages)
|
|
29
30
|
- [Building & Deploying](#building--deploying)
|
|
30
31
|
|
|
@@ -1104,6 +1105,73 @@ Changing `?lang=fr` in the URL re-renders client components automatically.
|
|
|
1104
1105
|
|
|
1105
1106
|
---
|
|
1106
1107
|
|
|
1108
|
+
## cache() — Request-Scoped Data Caching
|
|
1109
|
+
|
|
1110
|
+
Server components render independently, so if two components in the same page tree both need the same data, NukeJS has no way to know they're asking for the same thing — each one triggers its own database call, fetch, or computation, even though they're part of the same request:
|
|
1111
|
+
|
|
1112
|
+
```tsx
|
|
1113
|
+
// app/components/Nav.tsx
|
|
1114
|
+
export default async function Nav() {
|
|
1115
|
+
const user = await db.getUser(useRequest().params.id); // DB call #1
|
|
1116
|
+
return <nav>{user.name}</nav>;
|
|
1117
|
+
}
|
|
1118
|
+
|
|
1119
|
+
// app/pages/dashboard.tsx
|
|
1120
|
+
export default async function Dashboard() {
|
|
1121
|
+
const user = await db.getUser(useRequest().params.id); // DB call #2 — same data
|
|
1122
|
+
return <main>{user.email}</main>;
|
|
1123
|
+
}
|
|
1124
|
+
```
|
|
1125
|
+
|
|
1126
|
+
`cache()` wraps an async function so repeated calls with the same arguments, **within the same request**, share a single call:
|
|
1127
|
+
|
|
1128
|
+
```ts
|
|
1129
|
+
// lib/data.ts
|
|
1130
|
+
import { cache } from 'nukejs';
|
|
1131
|
+
|
|
1132
|
+
export const getUser = cache(async (id: string) => {
|
|
1133
|
+
return db.getUser(id); // only runs once per request, however many
|
|
1134
|
+
// components call getUser(id) with that id
|
|
1135
|
+
});
|
|
1136
|
+
```
|
|
1137
|
+
|
|
1138
|
+
```tsx
|
|
1139
|
+
// app/components/Nav.tsx
|
|
1140
|
+
import { getUser } from '../../lib/data';
|
|
1141
|
+
|
|
1142
|
+
export default async function Nav() {
|
|
1143
|
+
const user = await getUser(useRequest().params.id as string);
|
|
1144
|
+
return <nav>{user.name}</nav>;
|
|
1145
|
+
}
|
|
1146
|
+
|
|
1147
|
+
// app/pages/dashboard.tsx
|
|
1148
|
+
import { getUser } from '../../lib/data';
|
|
1149
|
+
|
|
1150
|
+
export default async function Dashboard() {
|
|
1151
|
+
const user = await getUser(useRequest().params.id as string); // same call → no extra DB hit
|
|
1152
|
+
return <main>{user.email}</main>;
|
|
1153
|
+
}
|
|
1154
|
+
```
|
|
1155
|
+
|
|
1156
|
+
Whichever component calls `getUser('42')` first triggers the real call; every other call with `'42'` in that same request — even ones that fire concurrently, before the first has resolved — awaits that same in-flight promise instead of starting a new one.
|
|
1157
|
+
|
|
1158
|
+
### Why not just use React's `cache()`?
|
|
1159
|
+
|
|
1160
|
+
React ships its own `cache()`, but it relies on the React Server Components renderer to signal "this is a new request, reset the memoization table." NukeJS doesn't run the RSC protocol — it renders each request to a single HTML string (see [Pages & Routing](#pages--routing)) — so nothing would ever tell React's `cache()` to reset, risking one request's cached data leaking into another's response. NukeJS's `cache()` has the same signature and mental model as React's, but is tied to NukeJS's own per-request lifecycle instead, so it resets correctly on every request.
|
|
1161
|
+
|
|
1162
|
+
### Scope & behaviour
|
|
1163
|
+
|
|
1164
|
+
- **One request, one cache.** The cache is created fresh before rendering starts and discarded once the response is sent. There's no cross-request caching and nothing to invalidate — the next request simply starts with an empty cache.
|
|
1165
|
+
- **Keyed by function + arguments.** Two different `cache()`-wrapped functions never share entries, even if called with identical arguments. Arguments are compared by value (via JSON serialization), so `getUser('42')` and `getUser('42')` hit the same entry, but `getUser('42')` and `getUser('43')` don't.
|
|
1166
|
+
- **Concurrent calls are deduped too.** If two components call the same cached function before the first call has settled, both await the same pending promise — only one underlying call is made.
|
|
1167
|
+
- **Failed calls aren't stuck.** If a cached call rejects, that entry is removed immediately, so a later call with the same arguments in the same request retries instead of replaying the same error.
|
|
1168
|
+
- **Non-serializable arguments skip caching.** Arguments that can't be safely compared by value (functions, Symbols, cyclic objects) just call straight through, unmemoized, rather than being cached under an incorrect key.
|
|
1169
|
+
- **Safe to call anywhere.** Outside of an active request — a script, a test, client-side code — a `cache()`-wrapped function simply calls straight through with no memoization. It's always safe to import and call.
|
|
1170
|
+
|
|
1171
|
+
`cache()` is a pure optimization: removing it (or calling the wrapped function directly) never changes what your app returns, only how many times the underlying work runs.
|
|
1172
|
+
|
|
1173
|
+
---
|
|
1174
|
+
|
|
1107
1175
|
## Error Pages
|
|
1108
1176
|
|
|
1109
1177
|
NukeJS supports custom error pages for **404 Not Found** and **500 Internal Server Error**. Place them directly in `app/pages/` — they are standard server components and support everything regular pages do: layouts, `useHtml()`, client components, and HMR in dev.
|
package/dist/build-common.js
CHANGED
|
@@ -387,6 +387,46 @@ async function runWithRequestStore<T>(ctx: any, fn: () => Promise<T>): Promise<T
|
|
|
387
387
|
try { return await fn(); } finally { __setReq(null); }
|
|
388
388
|
}
|
|
389
389
|
|
|
390
|
+
// \u2500\u2500\u2500 cache-store (inlined) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
391
|
+
// See src/cache-store.ts for the full explanation. Request-scoped memoisation
|
|
392
|
+
// for async data loaders, keyed on the wrapped function + its (serialisable)
|
|
393
|
+
// arguments. Reset per request exactly like the request/html stores above.
|
|
394
|
+
const __CACHE_KEY__ = Symbol.for('__nukejs_cache_store__');
|
|
395
|
+
const __getCache = (): { entries: Map<string, Promise<unknown>> } | null =>
|
|
396
|
+
(globalThis as any)[__CACHE_KEY__] ?? null;
|
|
397
|
+
const __setCache = (v: { entries: Map<string, Promise<unknown>> } | null): void => {
|
|
398
|
+
(globalThis as any)[__CACHE_KEY__] = v;
|
|
399
|
+
};
|
|
400
|
+
async function runWithCacheStore<T>(fn: () => Promise<T>): Promise<T> {
|
|
401
|
+
__setCache({ entries: new Map() });
|
|
402
|
+
try { return await fn(); } finally { __setCache(null); }
|
|
403
|
+
}
|
|
404
|
+
let __cacheIdCounter = 0;
|
|
405
|
+
function __serialiseArgs(args: unknown[]): string | null {
|
|
406
|
+
try {
|
|
407
|
+
return JSON.stringify(args, (_k, v) => {
|
|
408
|
+
if (typeof v === 'function' || typeof v === 'symbol') throw new Error('non-serialisable');
|
|
409
|
+
return v;
|
|
410
|
+
});
|
|
411
|
+
} catch { return null; }
|
|
412
|
+
}
|
|
413
|
+
function cache<Args extends unknown[], R>(fn: (...args: Args) => Promise<R>): (...args: Args) => Promise<R> {
|
|
414
|
+
const id = \`c\${++__cacheIdCounter}\`;
|
|
415
|
+
return function cached(...args: Args): Promise<R> {
|
|
416
|
+
const store = __getCache();
|
|
417
|
+
if (!store) return fn(...args);
|
|
418
|
+
const argsKey = __serialiseArgs(args);
|
|
419
|
+
if (argsKey === null) return fn(...args);
|
|
420
|
+
const key = \`\${id}:\${argsKey}\`;
|
|
421
|
+
const existing = store.entries.get(key);
|
|
422
|
+
if (existing) return existing as Promise<R>;
|
|
423
|
+
const result = fn(...args);
|
|
424
|
+
store.entries.set(key, result);
|
|
425
|
+
result.catch(() => { store.entries.delete(key); });
|
|
426
|
+
return result;
|
|
427
|
+
};
|
|
428
|
+
}
|
|
429
|
+
|
|
390
430
|
// \u2500\u2500\u2500 HTML helpers \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
391
431
|
function escapeHtml(s: string): string {
|
|
392
432
|
return String(s)
|
|
@@ -495,27 +535,83 @@ function buildWrapperAttrString(attrs: Record<string, any>): string {
|
|
|
495
535
|
return parts.length ? ' ' + parts.join(' ') : '';
|
|
496
536
|
}
|
|
497
537
|
|
|
498
|
-
function
|
|
499
|
-
if (typeof
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
const
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
}
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
538
|
+
function prepareProps(props, hydrated) {
|
|
539
|
+
if (!props || typeof props !== 'object') return Promise.resolve({ real: props, json: props });
|
|
540
|
+
const entries = Object.entries(props);
|
|
541
|
+
return Promise.all(entries.map(([, v]) => prepareValue(v, hydrated))).then((results) => {
|
|
542
|
+
const real = {};
|
|
543
|
+
const json = {};
|
|
544
|
+
entries.forEach(([key], i) => {
|
|
545
|
+
real[key] = results[i].real;
|
|
546
|
+
if (results[i].json !== undefined) json[key] = results[i].json;
|
|
547
|
+
});
|
|
548
|
+
return { real, json };
|
|
549
|
+
});
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
async function prepareValue(value, hydrated) {
|
|
553
|
+
if (value === null || value === undefined) return { real: value, json: value };
|
|
554
|
+
if (typeof value === 'function') return { real: value, json: undefined };
|
|
555
|
+
if (typeof value !== 'object') return { real: value, json: value };
|
|
556
|
+
|
|
557
|
+
if (Array.isArray(value)) {
|
|
558
|
+
const items = await Promise.all(value.map((v) => prepareValue(v, hydrated)));
|
|
559
|
+
return {
|
|
560
|
+
real: items.map((i) => i.real),
|
|
561
|
+
json: items.map((i) => i.json).filter((i) => i !== undefined),
|
|
562
|
+
};
|
|
515
563
|
}
|
|
564
|
+
|
|
565
|
+
if (value.$$typeof) return prepareElement(value, hydrated);
|
|
566
|
+
|
|
567
|
+
const out = await prepareProps(value, hydrated);
|
|
516
568
|
return out;
|
|
517
569
|
}
|
|
518
570
|
|
|
571
|
+
// Resolves a single React element found inside a client component's props.
|
|
572
|
+
// Native elements and fragments recurse into their own props. Client
|
|
573
|
+
// components are left untouched for 'real' (react-dom/server renders them
|
|
574
|
+
// normally within the boundary's own renderToString call below) and wired
|
|
575
|
+
// through as { __re: 'client', componentId, props } for the browser to
|
|
576
|
+
// mount for real. Server components can't run in the browser at all, so
|
|
577
|
+
// they're rendered once with this same file's renderNode() (handles async
|
|
578
|
+
// components and any nested client boundaries inside them) and wrapped in
|
|
579
|
+
// an inert <span style="display:contents"> carrying that HTML \u2014
|
|
580
|
+
// identically on both 'real' and 'json', so the SSR markup and the
|
|
581
|
+
// browser's reconstructed tree have the same shape and hydrateRoot() can
|
|
582
|
+
// reconcile them without a mismatch.
|
|
583
|
+
async function prepareElement(element, hydrated) {
|
|
584
|
+
const { type, props } = element;
|
|
585
|
+
|
|
586
|
+
if (type === Symbol.for('react.fragment')) {
|
|
587
|
+
const p = await prepareProps(props, hydrated);
|
|
588
|
+
return { real: __createElement__(Symbol.for('react.fragment'), p.real), json: { __re: 'fragment', props: p.json } };
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
if (typeof type === 'string') {
|
|
592
|
+
const p = await prepareProps(props, hydrated);
|
|
593
|
+
return { real: __createElement__(type, p.real), json: { __re: 'html', tag: type, props: p.json } };
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
if (typeof type === 'function') {
|
|
597
|
+
const cid = type.__nukeClientId ?? CLIENT_COMPONENTS[type.name];
|
|
598
|
+
if (cid) {
|
|
599
|
+
const p = await prepareProps(props, hydrated);
|
|
600
|
+
return { real: element, json: { __re: 'client', componentId: cid, props: p.json } };
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
const html = await renderNode(element, hydrated);
|
|
604
|
+
const wrapperProps = {
|
|
605
|
+
style: { display: 'contents' },
|
|
606
|
+
'data-n-static': true,
|
|
607
|
+
dangerouslySetInnerHTML: { __html: html },
|
|
608
|
+
};
|
|
609
|
+
return { real: __createElement__('span', wrapperProps), json: { __re: 'static', html } };
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
return { real: element, json: undefined };
|
|
613
|
+
}
|
|
614
|
+
|
|
519
615
|
async function renderNode(node: any, hydrated: Set<string>): Promise<string> {
|
|
520
616
|
if (node == null || typeof node === 'boolean') return '';
|
|
521
617
|
if (typeof node === 'string') return escapeHtml(node);
|
|
@@ -557,10 +653,10 @@ async function renderNode(node: any, hydrated: Set<string>): Promise<string> {
|
|
|
557
653
|
hydrated.add(clientId);
|
|
558
654
|
const { wrapperAttrs, componentProps } = splitWrapperAttrs(props);
|
|
559
655
|
const wrapperAttrStr = buildWrapperAttrString(wrapperAttrs);
|
|
560
|
-
const serializedProps =
|
|
656
|
+
const { real: hydrationSafeProps, json: serializedProps } = await prepareProps(componentProps ?? {}, hydrated);
|
|
561
657
|
let ssrHtml: string;
|
|
562
658
|
try {
|
|
563
|
-
ssrHtml = __renderToString__(__createElement__(type as any,
|
|
659
|
+
ssrHtml = __renderToString__(__createElement__(type as any, hydrationSafeProps || {}));
|
|
564
660
|
} catch {
|
|
565
661
|
ssrHtml = PRERENDERED_HTML[clientId] ?? '';
|
|
566
662
|
}
|
|
@@ -632,7 +728,7 @@ export default async function handler(req: IncomingMessage, res: ServerResponse)
|
|
|
632
728
|
let appHtml = '';
|
|
633
729
|
const store = await runWithRequestStore(
|
|
634
730
|
{ url, pathname, params, query, headers: normHeaders },
|
|
635
|
-
() => runWithHtmlStore(async () => { appHtml = await renderNode(wrapped, hydrated); }),
|
|
731
|
+
() => runWithCacheStore(() => runWithHtmlStore(async () => { appHtml = await renderNode(wrapped, hydrated); })),
|
|
636
732
|
);
|
|
637
733
|
|
|
638
734
|
const pageTitle = resolveTitle(store.titleOps, 'NukeJS');
|
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) {
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
const KEY = /* @__PURE__ */ Symbol.for("__nukejs_cache_store__");
|
|
2
|
+
const getGlobal = () => globalThis[KEY] ?? null;
|
|
3
|
+
const setGlobal = (store) => {
|
|
4
|
+
globalThis[KEY] = store;
|
|
5
|
+
};
|
|
6
|
+
async function runWithCacheStore(fn) {
|
|
7
|
+
setGlobal({ entries: /* @__PURE__ */ new Map() });
|
|
8
|
+
try {
|
|
9
|
+
return await fn();
|
|
10
|
+
} finally {
|
|
11
|
+
setGlobal(null);
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
let cacheIdCounter = 0;
|
|
15
|
+
function serialiseArgs(args) {
|
|
16
|
+
try {
|
|
17
|
+
return JSON.stringify(args, (_key, value) => {
|
|
18
|
+
if (typeof value === "function" || typeof value === "symbol") {
|
|
19
|
+
throw new Error("non-serialisable value");
|
|
20
|
+
}
|
|
21
|
+
return value;
|
|
22
|
+
});
|
|
23
|
+
} catch {
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
function cache(fn) {
|
|
28
|
+
const cacheId = `c${++cacheIdCounter}`;
|
|
29
|
+
return function cached(...args) {
|
|
30
|
+
const store = getGlobal();
|
|
31
|
+
if (!store) return fn(...args);
|
|
32
|
+
const argsKey = serialiseArgs(args);
|
|
33
|
+
if (argsKey === null) return fn(...args);
|
|
34
|
+
const key = `${cacheId}:${argsKey}`;
|
|
35
|
+
const existing = store.entries.get(key);
|
|
36
|
+
if (existing) return existing;
|
|
37
|
+
const result = fn(...args);
|
|
38
|
+
store.entries.set(key, result);
|
|
39
|
+
result.catch(() => {
|
|
40
|
+
store.entries.delete(key);
|
|
41
|
+
});
|
|
42
|
+
return result;
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
export {
|
|
46
|
+
cache,
|
|
47
|
+
runWithCacheStore
|
|
48
|
+
};
|
package/dist/index.d.ts
CHANGED
|
@@ -7,6 +7,7 @@ export { default as useRouter } from './use-router';
|
|
|
7
7
|
export { useRequest } from './use-request';
|
|
8
8
|
export type { RequestContext } from './use-request';
|
|
9
9
|
export { normaliseHeaders, sanitiseHeaders, getRequestStore } from './request-store';
|
|
10
|
+
export { cache } from './cache-store';
|
|
10
11
|
export { default as Link } from './Link';
|
|
11
12
|
export { setupLocationChangeMonitor, initRuntime } from './bundle';
|
|
12
13
|
export type { RuntimeData } from './bundle';
|
package/dist/index.js
CHANGED
|
@@ -3,6 +3,7 @@ import { useHtml } from "./use-html.js";
|
|
|
3
3
|
import { default as default2 } from "./use-router.js";
|
|
4
4
|
import { useRequest } from "./use-request.js";
|
|
5
5
|
import { normaliseHeaders, sanitiseHeaders, getRequestStore } from "./request-store.js";
|
|
6
|
+
import { cache } from "./cache-store.js";
|
|
6
7
|
import { default as default3 } from "./Link.js";
|
|
7
8
|
import { setupLocationChangeMonitor, initRuntime } from "./bundle.js";
|
|
8
9
|
import { escapeHtml } from "./utils.js";
|
|
@@ -11,6 +12,7 @@ export {
|
|
|
11
12
|
default3 as Link,
|
|
12
13
|
ansi,
|
|
13
14
|
c,
|
|
15
|
+
cache,
|
|
14
16
|
createPersistedStore,
|
|
15
17
|
createStore,
|
|
16
18
|
escapeHtml,
|
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 =
|
|
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,
|
|
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
|
|
109
|
-
if (!props || typeof props !== "object") return props;
|
|
110
|
-
const
|
|
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
|
|
113
|
-
|
|
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
|
|
117
|
+
return { real, json };
|
|
116
118
|
}
|
|
117
|
-
function
|
|
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
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
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
|
|
128
|
-
|
|
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
|
|
138
|
+
return { real, json };
|
|
131
139
|
}
|
|
132
|
-
function
|
|
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
|
-
|
|
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
|
-
|
|
146
|
-
|
|
147
|
-
|
|
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/dist/ssr.js
CHANGED
|
@@ -8,6 +8,7 @@ import { matchRoute, findLayoutsForRoute } from "./router.js";
|
|
|
8
8
|
import { findClientComponentsInTree } from "./component-analyzer.js";
|
|
9
9
|
import { renderElementToHtml } from "./renderer.js";
|
|
10
10
|
import { runWithRequestStore, normaliseHeaders, sanitiseHeaders } from "./request-store.js";
|
|
11
|
+
import { runWithCacheStore } from "./cache-store.js";
|
|
11
12
|
import {
|
|
12
13
|
runWithHtmlStore,
|
|
13
14
|
resolveTitle
|
|
@@ -130,9 +131,9 @@ async function renderFile(filePath, params, url, pagesDir, isDev, res, req, stat
|
|
|
130
131
|
query: queryParams,
|
|
131
132
|
headers: normHeaders
|
|
132
133
|
},
|
|
133
|
-
() => runWithHtmlStore(async () => {
|
|
134
|
+
() => runWithCacheStore(() => runWithHtmlStore(async () => {
|
|
134
135
|
appHtml = await renderElementToHtml(wrappedElement, ctx);
|
|
135
|
-
})
|
|
136
|
+
}))
|
|
136
137
|
);
|
|
137
138
|
const pageTitle = resolveTitle(store.titleOps, "NukeJS");
|
|
138
139
|
const headLines = [
|
package/dist/use-router.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { useCallback, useEffect, useState } from "react";
|
|
1
|
+
import { useCallback, useEffect, useMemo, useState } from "react";
|
|
2
2
|
const SSR_ROUTER = { push: () => {
|
|
3
3
|
}, replace: () => {
|
|
4
4
|
}, back: () => {
|
|
@@ -28,7 +28,10 @@ function useRouter() {
|
|
|
28
28
|
const refresh = useCallback(() => {
|
|
29
29
|
window.dispatchEvent(new Event("locationchange"));
|
|
30
30
|
}, []);
|
|
31
|
-
return
|
|
31
|
+
return useMemo(
|
|
32
|
+
() => ({ path, push, replace, back, refresh }),
|
|
33
|
+
[path, push, replace, back, refresh]
|
|
34
|
+
);
|
|
32
35
|
}
|
|
33
36
|
export {
|
|
34
37
|
useRouter as default
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nukejs",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.28",
|
|
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",
|