@wular/pnext 0.0.19 → 0.0.21
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 +2 -2
- package/package.json +1 -1
- package/reference/compat.md +2 -2
- package/reference/getting-started.md +2 -2
- package/src/cli/adapters/vercel.ts +27 -0
- package/src/cli/analyze.ts +8 -7
- package/src/cli/boot/trace.ts +9 -3
- package/src/cli/build/cache.ts +324 -0
- package/src/cli/build.ts +42 -18
- package/src/cli/create.ts +4 -4
- package/src/cli/dev.ts +23 -0
- package/src/cli/migrate/package-json.ts +3 -2
- package/src/cli/migrate/report.ts +1 -1
- package/src/cli/migrate/run.ts +2 -2
- package/src/cli/migrate/scan.ts +128 -1
- package/src/cli/serve/pipeline.ts +42 -42
- package/src/cli/start.ts +20 -20
- package/src/client/build.ts +64 -35
- package/src/client/entry.ts +228 -16
- package/src/client/router/page-slot.ts +81 -0
- package/src/client/router/runtime.ts +107 -112
- package/src/compat/bundler/cjs-exports.ts +26 -20
- package/src/compat/client/css-order.ts +1 -1
- package/src/compat/client/navigation-scroll.ts +36 -40
- package/src/compat/css/modules.ts +6 -2
- package/src/compat/metadata.ts +0 -9
- package/src/compat/next/font/cache.ts +10 -22
- package/src/compat/next/font/runtime.ts +20 -0
- package/src/compat/next/legacy-request-apis.ts +41 -5
- package/src/compat/next/script.tsx +16 -24
- package/src/compat/react/client-lite.ts +1 -0
- package/src/compat/react/client.ts +11 -0
- package/src/compat/react/dom-client.ts +11 -2
- package/src/compat/react/dom-react-server.ts +4 -0
- package/src/compat/react/dom.ts +11 -2
- package/src/compat/react/internals.ts +65 -0
- package/src/compat/react/parity.ts +47 -0
- package/src/compat/react/preact.ts +31 -1
- package/src/compat/react/react-server.ts +7 -0
- package/src/compat/react/server.ts +16 -2
- package/src/compat/react/style-values.ts +68 -0
- package/src/compat/register/proxy.ts +3 -1
- package/src/compat/register/render.ts +14 -3
- package/src/compat/register/routing.ts +57 -7
- package/src/compat/register/typed-routes.ts +0 -2
- package/src/compat/tsconfig-defaults.ts +4 -0
- package/src/compat/typecheck/check.ts +26 -71
- package/src/compat/typecheck/worker.ts +3 -2
- package/src/compat/validation/validate.ts +2 -0
- package/src/compat-bootstrap.ts +4 -0
- package/src/css/build.ts +153 -14
- package/src/css/postcss.ts +32 -3
- package/src/dev/restart/client-key.ts +1 -1
- package/src/dev/restart/global-css.ts +12 -94
- package/src/dev/restart/record.ts +106 -0
- package/src/dev/restart/route-facts.ts +11 -90
- package/src/dev/server.ts +208 -41
- package/src/extensions.ts +69 -3
- package/src/render/hooks.ts +1 -1
- package/src/render/metadata.ts +89 -17
- package/src/render/renderer.ts +325 -138
- package/src/render/resource-hints.ts +62 -9
- package/src/render/slots.tsx +2 -1
- package/src/resolve/scan-facts.ts +13 -5
- package/src/routing/forwarded.ts +20 -7
- package/src/routing/handler.ts +6 -2
- package/src/routing/href.ts +11 -1
- package/src/routing/metadata-files.ts +16 -3
- package/src/routing/proxy.ts +6 -3
- package/src/routing/routes.ts +253 -34
- package/src/runtime/fetch-host.ts +42 -0
- package/src/runtime/fingerprint.ts +132 -0
- package/src/runtime/loader.ts +7 -5
- package/src/runtime/module-cache.ts +3 -36
- package/src/runtime/modules.ts +333 -77
- package/src/runtime/vendor-build.ts +34 -5
- package/src/runtime/vendor.ts +1 -18
- package/src/types.ts +6 -0
- package/src/utils/asset-hash.ts +22 -0
- package/src/utils/code.ts +41 -0
- package/src/utils/html.ts +4 -1
- package/src/utils/registry.ts +70 -0
- package/src/utils/serialize.ts +66 -7
package/src/runtime/vendor.ts
CHANGED
|
@@ -40,7 +40,7 @@ import { fileURLToPath } from 'node:url'
|
|
|
40
40
|
import { build } from '../utils/esbuild'
|
|
41
41
|
import { isCommonJsModuleSource } from '../resolve/imports'
|
|
42
42
|
import { outputSpecifiers } from './module-transform'
|
|
43
|
-
import {
|
|
43
|
+
import { esbuildEntryExportNames, isIdentifier, uniqueIdentifier } from '../utils/code'
|
|
44
44
|
import { cachedExistsSync } from '../utils/fs-cache'
|
|
45
45
|
import { traceEnabled, traceValue } from '../utils/trace-flags'
|
|
46
46
|
import { writeFileAtomic } from '../utils/fs'
|
|
@@ -771,23 +771,6 @@ export function commonJsExportNames(code: string) {
|
|
|
771
771
|
.sort()
|
|
772
772
|
}
|
|
773
773
|
|
|
774
|
-
// Recover named exports from esbuild's `__export(entry, {...}); module.exports =
|
|
775
|
-
// __toCommonJS(entry)` shape. The entry is emitted last, so the final __toCommonJS.
|
|
776
|
-
function esbuildEntryExportNames(code: string) {
|
|
777
|
-
const toCommonJs = [
|
|
778
|
-
...code.matchAll(/module\.exports\s*=\s*__toCommonJS\w*\(\s*([A-Za-z_$][\w$]*)\s*\)/g),
|
|
779
|
-
]
|
|
780
|
-
const entryVar = toCommonJs.at(-1)?.[1]
|
|
781
|
-
if (!entryVar) return []
|
|
782
|
-
const exportCall = new RegExp(
|
|
783
|
-
`__export\\w*\\(\\s*${escapeRegex(entryVar)}\\s*,\\s*\\{([\\s\\S]*?)\\}\\s*\\)`,
|
|
784
|
-
).exec(code)
|
|
785
|
-
if (!exportCall?.[1]) return []
|
|
786
|
-
return [...exportCall[1].matchAll(/(?:^|,)\s*([A-Za-z_$][\w$]*)\s*:/g)]
|
|
787
|
-
.map(match => match[1])
|
|
788
|
-
.filter((name): name is string => Boolean(name && isIdentifier(name)))
|
|
789
|
-
}
|
|
790
|
-
|
|
791
774
|
/**
|
|
792
775
|
* The gate every plugin-free artifact has to pass before it is published: esbuild re-bundles the emitted
|
|
793
776
|
* entry, so its own chunk graph has to resolve and its syntax has to be real, and every reference the
|
package/src/types.ts
CHANGED
|
@@ -558,6 +558,8 @@ export interface RouteManifestEntry {
|
|
|
558
558
|
childrenDefault?: string
|
|
559
559
|
/** Intercepting-route metadata, when this route is an interceptor. */
|
|
560
560
|
interception?: RouteInterception
|
|
561
|
+
/** First `'use server'` module in the route's closure — core refuses these. */
|
|
562
|
+
serverActionFile?: string
|
|
561
563
|
/**
|
|
562
564
|
* Ship the soft-navigation runtime even without client references. Derived:
|
|
563
565
|
* true exactly when `clientEntryReasons` is non-empty. Kept as its own field
|
|
@@ -609,6 +611,8 @@ export interface StaticMetadataFileEntry {
|
|
|
609
611
|
outputPath: string
|
|
610
612
|
routeSegments: string[]
|
|
611
613
|
contentType: string
|
|
614
|
+
/** Content-derived URL identity for cache-busted metadata image links. */
|
|
615
|
+
cacheIdentity?: string
|
|
612
616
|
width?: number
|
|
613
617
|
height?: number
|
|
614
618
|
sizes?: string
|
|
@@ -642,6 +646,8 @@ export interface BuildManifest {
|
|
|
642
646
|
outDir: string
|
|
643
647
|
routes: RouteManifestEntry[]
|
|
644
648
|
staticFiles?: Record<string, StaticFileMetadata>
|
|
649
|
+
/** Logical build-asset name -> the content-hashed name emitted (`global.css` -> `global-<hash>.css`). */
|
|
650
|
+
assetNames?: Record<string, string>
|
|
645
651
|
staticMetadataFiles?: StaticMetadataFileEntry[]
|
|
646
652
|
staticModuleMetadata?: Record<string, StaticModuleMetadata>
|
|
647
653
|
staticRouteMetadata?: Record<string, StaticRouteMetadata>
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* The content fingerprint a build-output asset name carries so its URL can be
|
|
5
|
+
* served `immutable`. Lowercase hex keeps the emitted name inside the
|
|
6
|
+
* `<name>-<hash>.<ext>` shape the client chunks and Next's own assets use.
|
|
7
|
+
*/
|
|
8
|
+
export function assetContentHash(bytes: Uint8Array | string): string {
|
|
9
|
+
return createHash('sha256').update(bytes).digest('hex').slice(0, 16)
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/** `global.css` + bytes -> `global-1f4a9c2b3d5e6f70.css`. */
|
|
13
|
+
export function hashedAssetName(name: string, bytes: Uint8Array | string): string {
|
|
14
|
+
const hash = assetContentHash(bytes)
|
|
15
|
+
const dot = name.lastIndexOf('.')
|
|
16
|
+
return dot <= 0 ? `${name}-${hash}` : `${name.slice(0, dot)}-${hash}${name.slice(dot)}`
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/** True for a name that already carries an `assetContentHash` fingerprint. */
|
|
20
|
+
export function isHashedAssetName(name: string): boolean {
|
|
21
|
+
return /-[0-9a-f]{16}(?:\.[^.]+)?$/.test(name)
|
|
22
|
+
}
|
package/src/utils/code.ts
CHANGED
|
@@ -37,6 +37,47 @@ export function isIdentifier(value: string) {
|
|
|
37
37
|
return /^[A-Za-z_$][\w$]*$/.test(value)
|
|
38
38
|
}
|
|
39
39
|
|
|
40
|
+
/**
|
|
41
|
+
* Named exports of esbuild's `__export(entry, {...}); module.exports = __toCommonJS(entry)` shape.
|
|
42
|
+
* Both helpers are matched by POSITION, not by name: a dependency published with `--minify` (tsup's
|
|
43
|
+
* default, e.g. react-web-share) renames them to one letter, and a literal `__export`/`__toCommonJS`
|
|
44
|
+
* match left every such package default-only - a hard failure, since the importer's named binding is
|
|
45
|
+
* static. The `() =>` getter value is what pins the object down as an export map, not any other literal.
|
|
46
|
+
*/
|
|
47
|
+
export function esbuildEntryExportNames(code: string): string[] {
|
|
48
|
+
const scope = entryCommonJsScope(code)
|
|
49
|
+
const assignments = [
|
|
50
|
+
...scope.matchAll(
|
|
51
|
+
/module\.exports\s*=\s*(?:[A-Za-z_$][\w$]*\s*\(\s*)?([A-Za-z_$][\w$]*)\s*\)?\s*[;\n]/g,
|
|
52
|
+
),
|
|
53
|
+
]
|
|
54
|
+
const entryVar = assignments.at(-1)?.[1]
|
|
55
|
+
if (!entryVar) return []
|
|
56
|
+
const exportCall = new RegExp(
|
|
57
|
+
`[A-Za-z_$][\\w$]*\\(\\s*${escapeRegex(entryVar)}\\s*,\\s*\\{([\\s\\S]*?)\\}\\s*\\)`,
|
|
58
|
+
).exec(scope)
|
|
59
|
+
if (!exportCall?.[1]) return []
|
|
60
|
+
return [
|
|
61
|
+
...exportCall[1].matchAll(
|
|
62
|
+
/(?:^|,)\s*(?:["']([^"']+)["']|([A-Za-z_$][\w$]*))\s*:\s*\(\s*\)\s*=>/g,
|
|
63
|
+
),
|
|
64
|
+
]
|
|
65
|
+
.map(match => match[1] ?? match[2])
|
|
66
|
+
.filter((name): name is string => Boolean(name && isIdentifier(name)))
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* The entry CommonJS module's own text. esbuild emits a `__commonJS` wrapper per module, dependencies
|
|
71
|
+
* first and the entry's last, so slicing from that declaration keeps a dependency's `module.exports`
|
|
72
|
+
* from being read as the entry's - which would publish names the package does not have.
|
|
73
|
+
*/
|
|
74
|
+
function entryCommonJsScope(code: string) {
|
|
75
|
+
const wrapper = /(^|\n)export default ([A-Za-z_$][\w$]*)\(\)/.exec(code)?.[2]
|
|
76
|
+
if (!wrapper) return code
|
|
77
|
+
const start = code.lastIndexOf(`var ${wrapper} = `)
|
|
78
|
+
return start === -1 ? code : code.slice(start)
|
|
79
|
+
}
|
|
80
|
+
|
|
40
81
|
/** Source with comments removed; the `[^:]` guard keeps `://` inside string URLs intact. */
|
|
41
82
|
export function stripComments(source: string): string {
|
|
42
83
|
return source.replace(/\/\*[\s\S]*?\*\//g, '').replace(/(^|[^:])\/\/.*$/gm, '$1')
|
package/src/utils/html.ts
CHANGED
|
@@ -10,7 +10,10 @@ export function escapeHtml(value: unknown) {
|
|
|
10
10
|
case '"':
|
|
11
11
|
return '"'
|
|
12
12
|
case "'":
|
|
13
|
-
|
|
13
|
+
// React/Next use the hexadecimal spelling in both text and attribute
|
|
14
|
+
// nodes. Keep our hand-written document/metadata serializers on the
|
|
15
|
+
// same wire format rather than relying on browser-equivalent entities.
|
|
16
|
+
return '''
|
|
14
17
|
default:
|
|
15
18
|
return char
|
|
16
19
|
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
// Which @wular/pnext a generated project depends on. `bunx @wular/pnext` reuses a cached copy of the
|
|
2
|
+
// package instead of re-resolving the dist-tag, so a stale laptop runs an old CLI — and an old CLI
|
|
3
|
+
// stamping its own version pins the new project to it. The range comes from the registry instead,
|
|
4
|
+
// with the running version as the offline fallback.
|
|
5
|
+
|
|
6
|
+
import { bold, cyan, dim } from './ansi'
|
|
7
|
+
import { pnextVersionRange } from './fs'
|
|
8
|
+
|
|
9
|
+
const REGISTRY_URL = 'https://registry.npmjs.org/@wular%2Fpnext'
|
|
10
|
+
// Short: create/migrate must not hang behind a slow or captive network.
|
|
11
|
+
const TIMEOUT_MS = 2000
|
|
12
|
+
|
|
13
|
+
async function fetchLatestPublishedVersion() {
|
|
14
|
+
const response = await fetch(REGISTRY_URL, {
|
|
15
|
+
signal: AbortSignal.timeout(TIMEOUT_MS),
|
|
16
|
+
// The abbreviated document — dist-tags plus versions, without the full metadata.
|
|
17
|
+
headers: { accept: 'application/vnd.npm.install-v1+json' },
|
|
18
|
+
})
|
|
19
|
+
if (!response.ok) return undefined
|
|
20
|
+
const body = (await response.json()) as { 'dist-tags'?: { latest?: string } }
|
|
21
|
+
return body['dist-tags']?.latest
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// Test seam: the create/migrate suites assert the stamped range and must never hit the network.
|
|
25
|
+
let fetchLatestVersion: () => Promise<string | undefined> = fetchLatestPublishedVersion
|
|
26
|
+
|
|
27
|
+
/** @internal Test-only. Returns a restore function. */
|
|
28
|
+
export function setLatestVersionFetcher(fetcher: () => Promise<string | undefined>) {
|
|
29
|
+
const previous = fetchLatestVersion
|
|
30
|
+
fetchLatestVersion = fetcher
|
|
31
|
+
return () => {
|
|
32
|
+
fetchLatestVersion = previous
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* '^' + the latest published @wular/pnext, for a project this CLI generates. Never throws and never
|
|
38
|
+
* blocks for long — any failure falls back to the running version. Warns when the CLI itself is
|
|
39
|
+
* stale, which is the symptom the user actually has to fix.
|
|
40
|
+
*/
|
|
41
|
+
export async function latestPnextVersionRange(command: 'create' | 'migrate') {
|
|
42
|
+
const running = pnextVersionRange().slice(1)
|
|
43
|
+
const latest = await fetchLatestVersion().catch(() => undefined)
|
|
44
|
+
if (!latest) return `^${running}`
|
|
45
|
+
if (isNewer(latest, running)) warnStaleCli(command, running, latest)
|
|
46
|
+
return `^${latest}`
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function warnStaleCli(command: string, running: string, latest: string) {
|
|
50
|
+
console.error(
|
|
51
|
+
`\n${bold('⚠ Your pnext CLI is out of date')} — running ${running}, latest is ${latest}.\n` +
|
|
52
|
+
dim(
|
|
53
|
+
` bunx reused a cached copy. This project depends on ^${latest}, but ${command} ran with ${running}.\n`,
|
|
54
|
+
) +
|
|
55
|
+
` Re-run with: ${cyan(`bunx @wular/pnext@latest ${command}`)}\n`,
|
|
56
|
+
)
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** Numeric major.minor.patch only; anything unparseable counts as "not newer". */
|
|
60
|
+
function isNewer(candidate: string, current: string) {
|
|
61
|
+
const parts = (value: string) => value.split('-')[0]!.split('.').map(Number)
|
|
62
|
+
const [a, b] = [parts(candidate), parts(current)]
|
|
63
|
+
for (let index = 0; index < 3; index += 1) {
|
|
64
|
+
const left = a[index] ?? 0
|
|
65
|
+
const right = b[index] ?? 0
|
|
66
|
+
if (!Number.isFinite(left) || !Number.isFinite(right)) return false
|
|
67
|
+
if (left !== right) return left > right
|
|
68
|
+
}
|
|
69
|
+
return false
|
|
70
|
+
}
|
package/src/utils/serialize.ts
CHANGED
|
@@ -109,6 +109,11 @@ function encodeContainer(value: object, ancestors: Map<object, string[]>, path:
|
|
|
109
109
|
// Errors reach `error.tsx` as props and ride the same marker convention (the
|
|
110
110
|
// encoder for this one lives on the render side, with the boundary props).
|
|
111
111
|
const ERROR_MARKER = '$$pnextError'
|
|
112
|
+
// error.tsx's `reset`/`unstable_retry` are FUNCTIONS the framework supplies, so
|
|
113
|
+
// they cross as this marker (renderer serializeConventionProps) and come back as
|
|
114
|
+
// a re-run of the route. Compat's action runtime revives it first and identically;
|
|
115
|
+
// core has no action runtime, so the reviver below is the one that runs.
|
|
116
|
+
const ERROR_RESET_MARKER = '$$pnextErrorReset'
|
|
112
117
|
|
|
113
118
|
/**
|
|
114
119
|
* Revive the wire markers `serializeProps` wrote, IN PLACE:
|
|
@@ -132,7 +137,8 @@ export function reviveSerializedRefs<T>(
|
|
|
132
137
|
const item = node[key]
|
|
133
138
|
if (item === null || typeof item !== 'object' || ArrayBuffer.isView(item)) continue
|
|
134
139
|
const child = item as Record<string, unknown>
|
|
135
|
-
const revived =
|
|
140
|
+
const revived =
|
|
141
|
+
refTarget(root, child) ?? extra?.(child) ?? binaryTarget(child) ?? errorResetTarget(child)
|
|
136
142
|
if (revived !== undefined) {
|
|
137
143
|
node[key] = revived
|
|
138
144
|
continue
|
|
@@ -154,6 +160,14 @@ export function reviveSerializedErrorRefs<T>(root: T): T {
|
|
|
154
160
|
return reviveSerializedRefs(root, errorTarget)
|
|
155
161
|
}
|
|
156
162
|
|
|
163
|
+
function errorResetTarget(node: Record<string, unknown>): (() => void) | undefined {
|
|
164
|
+
return node[ERROR_RESET_MARKER] === true ? reloadRoute : undefined
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function reloadRoute(): void {
|
|
168
|
+
location.reload()
|
|
169
|
+
}
|
|
170
|
+
|
|
157
171
|
function errorTarget(node: Record<string, unknown>): Error | undefined {
|
|
158
172
|
const marker = node[ERROR_MARKER]
|
|
159
173
|
if (marker === null || typeof marker !== 'object') return undefined
|
|
@@ -217,28 +231,73 @@ export function hasPromiseProps(raw: string) {
|
|
|
217
231
|
* Nested containers are rewritten in place (the client just parsed them; the server already
|
|
218
232
|
* serialized the wire bytes before this runs), the root is not.
|
|
219
233
|
*/
|
|
220
|
-
export function revivePromiseMarkers<T>(props: T): T {
|
|
221
|
-
return reviveMarkers(props, new Set()) as T
|
|
234
|
+
export function revivePromiseMarkers<T>(props: T, sync?: SyncPropsRebuild): T {
|
|
235
|
+
return reviveMarkers(props, new Set(), sync) as T
|
|
222
236
|
}
|
|
223
237
|
|
|
224
|
-
|
|
238
|
+
/**
|
|
239
|
+
* Rebuild the legacy sync surface (`withSyncProps`) on a revived promise. The wire carries only the
|
|
240
|
+
* resolved value, so a marker revives into a bare promise and every unawaited `params.creator` read
|
|
241
|
+
* a Next-14-era component makes returns undefined - on the island SSR pass as well as on the client.
|
|
242
|
+
*/
|
|
243
|
+
export type SyncPropsRebuild = <T extends object>(
|
|
244
|
+
promise: Promise<T>,
|
|
245
|
+
kind: 'params' | 'searchParams',
|
|
246
|
+
value: T,
|
|
247
|
+
) => Promise<T>
|
|
248
|
+
|
|
249
|
+
/** Exactly the props Next gives the transitional sync surface - the same scope as the server's. */
|
|
250
|
+
const SYNC_PROP_KEYS = new Set(['params', 'searchParams'])
|
|
251
|
+
|
|
252
|
+
function reviveMarkers(
|
|
253
|
+
value: unknown,
|
|
254
|
+
seen: Set<object>,
|
|
255
|
+
sync?: SyncPropsRebuild,
|
|
256
|
+
key?: string,
|
|
257
|
+
): unknown {
|
|
225
258
|
if (value === null || typeof value !== 'object' || seen.has(value)) return value
|
|
226
259
|
if (PROMISE_MARKER_KEY in value) {
|
|
227
|
-
|
|
260
|
+
const resolved = (value as Record<string, unknown>)[PROMISE_MARKER_KEY]
|
|
261
|
+
const promise = fulfilledPromise(resolved)
|
|
262
|
+
if (!sync || !key || !SYNC_PROP_KEYS.has(key)) return promise
|
|
263
|
+
if (resolved === null || typeof resolved !== 'object') return promise
|
|
264
|
+
return sync(promise as Promise<object>, key as 'params' | 'searchParams', resolved)
|
|
228
265
|
}
|
|
229
266
|
seen.add(value)
|
|
230
267
|
if (Array.isArray(value)) {
|
|
231
268
|
for (let index = 0; index < value.length; index++) {
|
|
232
|
-
value[index] = reviveMarkers(value[index], seen)
|
|
269
|
+
value[index] = reviveMarkers(value[index], seen, sync)
|
|
233
270
|
}
|
|
234
271
|
return value
|
|
235
272
|
}
|
|
236
273
|
if (Object.getPrototypeOf(value) !== Object.prototype) return value
|
|
237
274
|
const target = value as Record<string, unknown>
|
|
238
|
-
for (const
|
|
275
|
+
for (const name of Object.keys(target)) {
|
|
276
|
+
target[name] = reviveMarkers(target[name], seen, sync, name)
|
|
277
|
+
}
|
|
239
278
|
return value
|
|
240
279
|
}
|
|
241
280
|
|
|
281
|
+
/**
|
|
282
|
+
* Client half of `withSyncProps`: give the revived promise the resolved object's own keys, skipping
|
|
283
|
+
* the ones the promise already answers for (then/status/value/...), exactly as the server does.
|
|
284
|
+
*/
|
|
285
|
+
export function rebuildSyncProps<T extends object>(
|
|
286
|
+
promise: Promise<T>,
|
|
287
|
+
_kind: 'params' | 'searchParams',
|
|
288
|
+
value: T,
|
|
289
|
+
): Promise<T> {
|
|
290
|
+
for (const key of Object.keys(value)) {
|
|
291
|
+
if (key in promise) continue
|
|
292
|
+
void Object.defineProperty(promise, key, {
|
|
293
|
+
get: () => (value as Record<string, unknown>)[key],
|
|
294
|
+
enumerable: true,
|
|
295
|
+
configurable: true,
|
|
296
|
+
})
|
|
297
|
+
}
|
|
298
|
+
return promise
|
|
299
|
+
}
|
|
300
|
+
|
|
242
301
|
function fulfilledPromise(value: unknown) {
|
|
243
302
|
const promise = Promise.resolve(value) as Promise<unknown> & { status: string; value: unknown }
|
|
244
303
|
promise.status = 'fulfilled'
|