@wular/pnext 0.0.7 → 0.0.9
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/package.json +1 -1
- package/src/cli/analyze.ts +103 -48
- package/src/cli/dev.ts +5 -1
- package/src/client/build.ts +94 -119
- package/src/client/entry.ts +87 -66
- package/src/compat/next/font/runtime.ts +3 -6
- package/src/dev/server.ts +27 -54
- package/src/render/renderer.ts +61 -16
- package/src/render/slots.tsx +6 -12
- package/src/render/static-slots-revive.ts +65 -0
- package/src/render/static-slots.ts +3 -54
- package/src/utils/serialize.ts +59 -2
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { h, type VNode } from 'preact'
|
|
2
2
|
import { isElementLike } from '../utils/serialize'
|
|
3
|
+
import { ISLAND_STATIC_SLOT_ATTRIBUTE, ISLAND_STATIC_SLOT_MARKER } from './static-slots-revive'
|
|
3
4
|
|
|
4
|
-
|
|
5
|
-
export
|
|
5
|
+
// The client half lives in ./static-slots-revive (preact-free); re-exported so importers keep one entry point.
|
|
6
|
+
export * from './static-slots-revive'
|
|
6
7
|
|
|
7
8
|
type Props = Record<string, unknown>
|
|
8
9
|
|
|
@@ -52,55 +53,3 @@ function mapSlots(
|
|
|
52
53
|
if (Array.isArray(value)) return mapped.map(([, item]) => item)
|
|
53
54
|
return Object.fromEntries(mapped)
|
|
54
55
|
}
|
|
55
|
-
|
|
56
|
-
/** Cheap gate on the raw props attribute so islands with no element props skip the revive walk. */
|
|
57
|
-
export function hasIslandStaticSlots(raw: string) {
|
|
58
|
-
return raw.includes(ISLAND_STATIC_SLOT_MARKER)
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
/**
|
|
62
|
-
* Client mount: swap each `$$pnext_slot` marker for a `pnext-static-slot` host holding the matching
|
|
63
|
-
* server-rendered DOM, converted to vnodes by the entry's DOM walker (so nested islands inside the
|
|
64
|
-
* adopted subtree become real island vnodes and hydrate on their own). The content is static server
|
|
65
|
-
* markup - it never re-renders, same as element children.
|
|
66
|
-
*/
|
|
67
|
-
export async function reviveIslandStaticSlots(
|
|
68
|
-
props: Props,
|
|
69
|
-
root: ParentNode,
|
|
70
|
-
toChildren: (node: ParentNode) => unknown,
|
|
71
|
-
): Promise<Props> {
|
|
72
|
-
return (await reviveSlots(props, root, toChildren, new Set())) as Props
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
async function reviveSlots(
|
|
76
|
-
value: unknown,
|
|
77
|
-
root: ParentNode,
|
|
78
|
-
toChildren: (node: ParentNode) => unknown,
|
|
79
|
-
seen: Set<object>,
|
|
80
|
-
): Promise<unknown> {
|
|
81
|
-
if (value === null || typeof value !== 'object' || seen.has(value)) return value
|
|
82
|
-
const marker = (value as Props)[ISLAND_STATIC_SLOT_MARKER]
|
|
83
|
-
if (typeof marker === 'string') {
|
|
84
|
-
const node = root.querySelector(`[${ISLAND_STATIC_SLOT_ATTRIBUTE}="${cssEscape(marker)}"]`)
|
|
85
|
-
// No server markup for this slot (the island never rendered the prop, or it was skipped for
|
|
86
|
-
// SSR): nothing to adopt, so the prop arrives null rather than as an empty host.
|
|
87
|
-
if (!node) return null
|
|
88
|
-
return h(
|
|
89
|
-
'pnext-static-slot',
|
|
90
|
-
{ [ISLAND_STATIC_SLOT_ATTRIBUTE]: marker, style: { display: 'contents' } },
|
|
91
|
-
(await toChildren(node)) as VNode,
|
|
92
|
-
)
|
|
93
|
-
}
|
|
94
|
-
const proto = Object.getPrototypeOf(value) as object | null
|
|
95
|
-
if (!Array.isArray(value) && proto !== Object.prototype && proto !== null) return value
|
|
96
|
-
seen.add(value)
|
|
97
|
-
const target = value as Props
|
|
98
|
-
for (const key of Object.keys(target)) {
|
|
99
|
-
target[key] = await reviveSlots(target[key], root, toChildren, seen)
|
|
100
|
-
}
|
|
101
|
-
return value
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
function cssEscape(value: string) {
|
|
105
|
-
return value.replace(/["\\]/g, '\\$&')
|
|
106
|
-
}
|
package/src/utils/serialize.ts
CHANGED
|
@@ -19,6 +19,12 @@ const SET_MARKER = '$$pnext_set'
|
|
|
19
19
|
// restoring identity. Only true cycles are encoded this way - a value that merely appears twice in different
|
|
20
20
|
// branches is still expanded, so every acyclic payload is byte-identical to what it was before.
|
|
21
21
|
const REF_MARKER = '$$pnext_ref'
|
|
22
|
+
// Promise-valued island props ride as `{__pnextPromise: resolved}` so the value keeps its PROMISE
|
|
23
|
+
// IDENTITY across the wire - top level (page params) and nested in plain containers (a react-query
|
|
24
|
+
// dehydrated state carries its pending-query promise inside `state.queries[n].promise`). The render
|
|
25
|
+
// half writes the marker (render/slots promiseMarker); both revive halves live here so the
|
|
26
|
+
// preact-free client entry can import them.
|
|
27
|
+
export const PROMISE_MARKER_KEY = '__pnextPromise'
|
|
22
28
|
|
|
23
29
|
type TypedArray =
|
|
24
30
|
| Int8Array
|
|
@@ -197,6 +203,49 @@ function refTarget(root: unknown, node: Record<string, unknown>): unknown {
|
|
|
197
203
|
return current
|
|
198
204
|
}
|
|
199
205
|
|
|
206
|
+
/** Cheap gate: an island whose raw props carry no marker skips the revive walk entirely. */
|
|
207
|
+
export function hasPromiseProps(raw: string) {
|
|
208
|
+
return raw.includes(PROMISE_MARKER_KEY)
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* Revive every `promiseMarker` in `props` - top level and nested inside plain objects/arrays - into a
|
|
213
|
+
* PRE-FULFILLED promise (React `use()` protocol: status/value readable synchronously). A bare
|
|
214
|
+
* `Promise.resolve` would make `use()` suspend during hydration, and a suspended hydration re-render
|
|
215
|
+
* appends fragment siblings instead of reusing the server DOM.
|
|
216
|
+
*
|
|
217
|
+
* Nested containers are rewritten in place (the client just parsed them; the server already
|
|
218
|
+
* serialized the wire bytes before this runs), the root is not.
|
|
219
|
+
*/
|
|
220
|
+
export function revivePromiseMarkers<T>(props: T): T {
|
|
221
|
+
return reviveMarkers(props, new Set()) as T
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function reviveMarkers(value: unknown, seen: Set<object>): unknown {
|
|
225
|
+
if (value === null || typeof value !== 'object' || seen.has(value)) return value
|
|
226
|
+
if (PROMISE_MARKER_KEY in value) {
|
|
227
|
+
return fulfilledPromise((value as Record<string, unknown>)[PROMISE_MARKER_KEY])
|
|
228
|
+
}
|
|
229
|
+
seen.add(value)
|
|
230
|
+
if (Array.isArray(value)) {
|
|
231
|
+
for (let index = 0; index < value.length; index++) {
|
|
232
|
+
value[index] = reviveMarkers(value[index], seen)
|
|
233
|
+
}
|
|
234
|
+
return value
|
|
235
|
+
}
|
|
236
|
+
if (Object.getPrototypeOf(value) !== Object.prototype) return value
|
|
237
|
+
const target = value as Record<string, unknown>
|
|
238
|
+
for (const key of Object.keys(target)) target[key] = reviveMarkers(target[key], seen)
|
|
239
|
+
return value
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
function fulfilledPromise(value: unknown) {
|
|
243
|
+
const promise = Promise.resolve(value) as Promise<unknown> & { status: string; value: unknown }
|
|
244
|
+
promise.status = 'fulfilled'
|
|
245
|
+
promise.value = value
|
|
246
|
+
return promise
|
|
247
|
+
}
|
|
248
|
+
|
|
200
249
|
/** A preact vnode: `h` sets `constructor` to undefined on every one it makes. */
|
|
201
250
|
export function isElementLike(value: unknown): boolean {
|
|
202
251
|
if (value === null || typeof value !== 'object') return false
|
|
@@ -227,6 +276,14 @@ export function assertSerializable(
|
|
|
227
276
|
if (value instanceof Date || value instanceof URL) {
|
|
228
277
|
throw new Error(`${path} must be a plain JSON value`)
|
|
229
278
|
}
|
|
279
|
+
// A promise the render half could not await: island props are awaited at the top level and inside
|
|
280
|
+
// plain objects/arrays/Maps/Sets, so anything left here sits behind a class instance (or is a
|
|
281
|
+
// server-action argument, where promises are never allowed). Say so instead of "plain object".
|
|
282
|
+
if (typeof (value as { then?: unknown }).then === 'function') {
|
|
283
|
+
throw new Error(
|
|
284
|
+
`${path} is a Promise: a promise prop must be reachable at the top level of the props or nested in plain objects/arrays/Maps/Sets - one held by a class instance cannot be awaited or serialized, and server actions take no promises at all`,
|
|
285
|
+
)
|
|
286
|
+
}
|
|
230
287
|
if (typeof value !== 'object') return
|
|
231
288
|
// A value that contains ITSELF is serializable: encodeBinary writes it as a
|
|
232
289
|
// `$$pnext_ref` back-reference (React flight does the same), so stop walking
|
|
@@ -241,8 +298,8 @@ export function assertSerializable(
|
|
|
241
298
|
return
|
|
242
299
|
}
|
|
243
300
|
|
|
244
|
-
//
|
|
245
|
-
//
|
|
301
|
+
// Elements stay illegal inside Map/Set: the slot walk (render/static-slots) does not enter
|
|
302
|
+
// them, so allowing one would silently ship a JSON-mangled vnode instead of a `$$pnext_slot`.
|
|
246
303
|
if (value instanceof Map) {
|
|
247
304
|
let index = 0
|
|
248
305
|
for (const [key, item] of value.entries()) {
|