@wular/pnext 0.0.19 → 0.0.20
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.ts +23 -15
- package/src/cli/dev.ts +23 -0
- 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 +4 -2
- package/src/client/build.ts +51 -25
- package/src/client/entry.ts +27 -4
- 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/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/render.ts +14 -3
- package/src/compat/register/routing.ts +57 -7
- 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-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 +322 -136
- 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/serialize.ts +66 -7
|
@@ -9,15 +9,17 @@
|
|
|
9
9
|
// Nothing here is used by `pnext build`, which owns the directory it just wiped.
|
|
10
10
|
import { existsSync, readFileSync, writeFileSync } from 'node:fs'
|
|
11
11
|
import { mkdir, stat } from 'node:fs/promises'
|
|
12
|
-
import { createHash } from 'node:crypto'
|
|
13
12
|
import path from 'node:path'
|
|
14
13
|
import type { ResolvedConfig } from '../../../config'
|
|
15
14
|
import { restartCacheEnabled } from '../../../dev/restart/enabled'
|
|
15
|
+
import { frameworkFingerprint } from '../../../runtime/fingerprint'
|
|
16
16
|
import { writeFileAtomic } from '../../../utils/fs'
|
|
17
17
|
import type { MetadataLink } from '../../../types'
|
|
18
18
|
|
|
19
19
|
export interface PersistableFont {
|
|
20
20
|
css: string
|
|
21
|
+
/** Whether the resolution shipped a size-adjusted fallback face (Next's `next-size-adjust` meta). */
|
|
22
|
+
sizeAdjust: boolean
|
|
21
23
|
preloads: MetadataLink[]
|
|
22
24
|
usesFonts: boolean
|
|
23
25
|
files: string[]
|
|
@@ -34,13 +36,13 @@ interface Entry extends PersistableFont {
|
|
|
34
36
|
|
|
35
37
|
interface IndexFile {
|
|
36
38
|
version: number
|
|
37
|
-
/**
|
|
39
|
+
/** pnext's generation, so an upgrade never serves the previous framework's output. */
|
|
38
40
|
runtime: string
|
|
39
41
|
fonts: Record<string, Entry>
|
|
40
42
|
}
|
|
41
43
|
|
|
42
44
|
/** Bumped whenever a record's shape changes; an older index is simply ignored. */
|
|
43
|
-
const INDEX_VERSION =
|
|
45
|
+
const INDEX_VERSION = 2
|
|
44
46
|
|
|
45
47
|
interface FontIndex {
|
|
46
48
|
file: string
|
|
@@ -150,22 +152,8 @@ function serializeIndex(fonts: Map<string, Entry>) {
|
|
|
150
152
|
} satisfies IndexFile)
|
|
151
153
|
}
|
|
152
154
|
|
|
153
|
-
//
|
|
154
|
-
//
|
|
155
|
-
//
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
function runtimeFingerprint() {
|
|
159
|
-
if (fingerprint !== undefined) return fingerprint
|
|
160
|
-
const hash = createHash('sha256')
|
|
161
|
-
for (const entry of ['runtime.ts', 'google.ts', 'local.ts', 'shared.ts']) {
|
|
162
|
-
const file = new URL(entry, import.meta.url).pathname
|
|
163
|
-
try {
|
|
164
|
-
hash.update(`${entry}:${Bun.hash(readFileSync(file)).toString(36)}\0`)
|
|
165
|
-
} catch {
|
|
166
|
-
// ignore unreadable entries
|
|
167
|
-
}
|
|
168
|
-
}
|
|
169
|
-
fingerprint = hash.digest('hex').slice(0, 16)
|
|
170
|
-
return fingerprint
|
|
171
|
-
}
|
|
155
|
+
// A recorded resolution IS framework output - its css, its preloads, its emitted file names - so it
|
|
156
|
+
// follows pnext's generation, the same one every other compiled artifact is keyed on. Never a
|
|
157
|
+
// hand-picked file list: this hashed four files in this directory and was blind to every other
|
|
158
|
+
// source the resolution reads (utils/code, the asset emitter, this file's own record rule).
|
|
159
|
+
const runtimeFingerprint = () => frameworkFingerprint()
|
|
@@ -37,6 +37,8 @@ export type {
|
|
|
37
37
|
|
|
38
38
|
interface FontScope {
|
|
39
39
|
fonts: Map<string, FontDefinition>
|
|
40
|
+
/** Set once a resolved font in this render shipped a size-adjusted fallback face. */
|
|
41
|
+
sizeAdjust?: boolean
|
|
40
42
|
}
|
|
41
43
|
|
|
42
44
|
interface FontDefinition {
|
|
@@ -61,6 +63,8 @@ export interface ResolvedFontAssets {
|
|
|
61
63
|
|
|
62
64
|
interface ResolvedFont {
|
|
63
65
|
css: string
|
|
66
|
+
/** Whether this font emitted a size-adjusted fallback face (drives Next's `next-size-adjust` meta). */
|
|
67
|
+
sizeAdjust: boolean
|
|
64
68
|
/** Preloadable font links (empty when `preload:false`). Delivered via the
|
|
65
69
|
* `Link` response header in prod, not as HTML `<link>` tags. */
|
|
66
70
|
preloads: MetadataLink[]
|
|
@@ -174,6 +178,7 @@ export async function fontAssets(
|
|
|
174
178
|
// tags). Stash them on the request work unit; the response finalizer
|
|
175
179
|
// (registerFontResponseFinalizer) reads them back and sets the header.
|
|
176
180
|
stashHeaderPreloads(preloads)
|
|
181
|
+
if (resolved.some(font => font.sizeAdjust)) scope.sizeAdjust = true
|
|
177
182
|
|
|
178
183
|
const usesFonts = resolved.some(font => font.usesFonts)
|
|
179
184
|
return {
|
|
@@ -211,6 +216,19 @@ export function fontPreloadDomScript(): string {
|
|
|
211
216
|
)
|
|
212
217
|
}
|
|
213
218
|
|
|
219
|
+
/**
|
|
220
|
+
* Next's `<meta name="next-size-adjust">` marker, emitted once per document whenever any next/font in
|
|
221
|
+
* the render shipped a metrics-adjusted fallback face (`appUsingSizeAdjustment`). The flag lives on the
|
|
222
|
+
* render's own font scope, so it can neither leak across requests nor depend on a work unit a
|
|
223
|
+
* build-time render does not have.
|
|
224
|
+
*/
|
|
225
|
+
export function fontSizeAdjustMeta(): string {
|
|
226
|
+
// Byte-for-byte with Next, empty `content` included.
|
|
227
|
+
return fontStorage.getStore()?.sizeAdjust === true
|
|
228
|
+
? '<meta name="next-size-adjust" content=""/>'
|
|
229
|
+
: ''
|
|
230
|
+
}
|
|
231
|
+
|
|
214
232
|
function stashHeaderPreloads(preloads: MetadataLink[]): void {
|
|
215
233
|
if (preloads.length === 0) return
|
|
216
234
|
const unit = getWorkUnit()
|
|
@@ -463,6 +481,7 @@ async function resolveGoogleFont(
|
|
|
463
481
|
|
|
464
482
|
return {
|
|
465
483
|
usesFonts: emitted.length > 0,
|
|
484
|
+
sizeAdjust: adjustFontFallback,
|
|
466
485
|
css: scopedCss,
|
|
467
486
|
preloads: emitted
|
|
468
487
|
.filter(item => item.preloadFontFile)
|
|
@@ -517,6 +536,7 @@ async function resolveLocalFont(
|
|
|
517
536
|
|
|
518
537
|
return {
|
|
519
538
|
usesFonts: emitted.length > 0,
|
|
539
|
+
sizeAdjust: adjustFontFallback,
|
|
520
540
|
css: [
|
|
521
541
|
...faceCss,
|
|
522
542
|
adjustFontFallback ? fallbackFaceCss(request.fontFamily) : '',
|
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
// Next 15's transitional sync request APIs, behind compat.next.legacyRequestAPIs. cookies()/headers()/
|
|
2
2
|
// draftMode() still return a Promise, but that promise also carries the resolved value's surface, so
|
|
3
|
-
// Next-14-era code (`cookies().get('x')`) keeps working while it migrates to `await`.
|
|
4
|
-
//
|
|
3
|
+
// Next-14-era code (`cookies().get('x')`) keeps working while it migrates to `await`. `params` and
|
|
4
|
+
// `searchParams` get the same treatment (`params.creator` without `await`). Each sync access logs
|
|
5
|
+
// Next's sync-dynamic-apis deprecation warning, deduped by userland callsite.
|
|
5
6
|
//
|
|
6
7
|
// Nothing here is reachable — or even allocated — when the option is off.
|
|
7
8
|
|
|
9
|
+
import { legacyRequestAPIs } from '../../request/context'
|
|
8
10
|
import { currentRenderCacheMeta } from '../cache/revalidate'
|
|
9
11
|
|
|
10
12
|
const PROMISE_PROPS = new Set<PropertyKey>(['then', 'catch', 'finally', 'constructor'])
|
|
@@ -29,7 +31,7 @@ export function withSyncAccess<T extends object>(
|
|
|
29
31
|
? (own as (...args: unknown[]) => unknown).bind(target)
|
|
30
32
|
: own
|
|
31
33
|
}
|
|
32
|
-
|
|
34
|
+
warnSyncApiAccess(api, prop)
|
|
33
35
|
const source = resolve() as object
|
|
34
36
|
const read: unknown = Reflect.get(source, prop, source)
|
|
35
37
|
return typeof read === 'function'
|
|
@@ -39,18 +41,52 @@ export function withSyncAccess<T extends object>(
|
|
|
39
41
|
})
|
|
40
42
|
}
|
|
41
43
|
|
|
42
|
-
|
|
44
|
+
/**
|
|
45
|
+
* Give a settled `params`/`searchParams` promise the resolved object's own keys, so Next-14 code
|
|
46
|
+
* (`function Layout({ params }) { params.creator }`) reads them without `await`. Keys the promise
|
|
47
|
+
* already answers for (then/catch/status/value/constructor/...) stay the promise's — Next skips them
|
|
48
|
+
* too — and the promise itself is returned, never a copy or a Proxy: callers hand it to `use()`.
|
|
49
|
+
*/
|
|
50
|
+
export function withSyncProps<T extends object>(
|
|
51
|
+
promise: Promise<T>,
|
|
52
|
+
kind: 'params' | 'searchParams',
|
|
53
|
+
value: T,
|
|
54
|
+
): Promise<T> {
|
|
55
|
+
if (!legacyRequestAPIs()) return promise
|
|
56
|
+
for (const key of Object.keys(value)) {
|
|
57
|
+
if (key in promise) continue
|
|
58
|
+
void Object.defineProperty(promise, key, {
|
|
59
|
+
get() {
|
|
60
|
+
warnSyncPropAccess(kind, key)
|
|
61
|
+
return (value as Record<string, unknown>)[key]
|
|
62
|
+
},
|
|
63
|
+
enumerable: true,
|
|
64
|
+
configurable: true,
|
|
65
|
+
})
|
|
66
|
+
}
|
|
67
|
+
return promise
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function warnSyncApiAccess(api: string, prop: PropertyKey): void {
|
|
43
71
|
const expression =
|
|
44
72
|
prop === Symbol.iterator
|
|
45
73
|
? `\`...${api}()\` or similar iteration`
|
|
46
74
|
: `\`${api}().${String(prop)}\``
|
|
75
|
+
warnSyncAccess(expression, `${api}()`, 'value')
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function warnSyncPropAccess(kind: string, key: string): void {
|
|
79
|
+
warnSyncAccess(`\`${kind}.${key}\``, kind, 'properties')
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function warnSyncAccess(expression: string, subject: string, noun: 'value' | 'properties'): void {
|
|
47
83
|
const route = currentRenderCacheMeta()?.route
|
|
48
84
|
const key = `${route ?? ''}|${expression}|${callsite()}`
|
|
49
85
|
if (warned.has(key)) return
|
|
50
86
|
warned.add(key)
|
|
51
87
|
console.warn(
|
|
52
88
|
`${route ? `Route "${route}" ` : 'This route '}used ${expression}. ` +
|
|
53
|
-
`\`${
|
|
89
|
+
`\`${subject}\` should be awaited before using its ${noun}. ` +
|
|
54
90
|
'Learn more: https://nextjs.org/docs/messages/sync-dynamic-apis',
|
|
55
91
|
)
|
|
56
92
|
}
|
|
@@ -2,7 +2,12 @@
|
|
|
2
2
|
import type { JSX } from 'preact'
|
|
3
3
|
import { escapeHtml } from '../../utils/html'
|
|
4
4
|
import { getNextConfig } from './config-loader'
|
|
5
|
-
import {
|
|
5
|
+
import {
|
|
6
|
+
clearRenderBuffer,
|
|
7
|
+
registerResourceHint,
|
|
8
|
+
renderBuffer,
|
|
9
|
+
takeRenderBuffer,
|
|
10
|
+
} from '../../render/resource-hints'
|
|
6
11
|
|
|
7
12
|
export type ScriptStrategy = 'beforeInteractive' | 'afterInteractive' | 'lazyOnload' | 'worker'
|
|
8
13
|
|
|
@@ -195,20 +200,15 @@ export interface DeferredScript {
|
|
|
195
200
|
}
|
|
196
201
|
|
|
197
202
|
function deferredScripts() {
|
|
198
|
-
|
|
199
|
-
holder.__PNEXT_DEFERRED_SCRIPTS__ ??= []
|
|
200
|
-
return holder.__PNEXT_DEFERRED_SCRIPTS__
|
|
203
|
+
return renderBuffer<DeferredScript>('deferred')
|
|
201
204
|
}
|
|
202
205
|
|
|
203
206
|
export function clearDeferredScripts() {
|
|
204
|
-
|
|
207
|
+
clearRenderBuffer('deferred')
|
|
205
208
|
}
|
|
206
209
|
|
|
207
210
|
export function takeDeferredScripts() {
|
|
208
|
-
|
|
209
|
-
const scripts = holder.__PNEXT_DEFERRED_SCRIPTS__ ?? []
|
|
210
|
-
holder.__PNEXT_DEFERRED_SCRIPTS__ = []
|
|
211
|
-
return scripts
|
|
211
|
+
return takeRenderBuffer<DeferredScript>('deferred')
|
|
212
212
|
}
|
|
213
213
|
|
|
214
214
|
// Like scriptAttrs, but building a raw name -> value map (for
|
|
@@ -248,16 +248,13 @@ export interface HeadScript {
|
|
|
248
248
|
content?: string
|
|
249
249
|
}
|
|
250
250
|
|
|
251
|
-
// The
|
|
252
|
-
//
|
|
253
|
-
//
|
|
254
|
-
// garbage-collected with it — no cross-request leakage.
|
|
251
|
+
// The resource-hints buffer is per render (render/resource-hints). Keying the executed-set off that
|
|
252
|
+
// array via a WeakMap gives us a dedup scope that lives exactly one render and is garbage-collected
|
|
253
|
+
// with it — no cross-request leakage.
|
|
255
254
|
const scriptDedupRegistry = new WeakMap<object, Set<string>>()
|
|
256
255
|
|
|
257
256
|
function executedScriptKeys(): Set<string> {
|
|
258
|
-
const
|
|
259
|
-
holder.__PNEXT_RESOURCE_HINTS__ ??= []
|
|
260
|
-
const arr = holder.__PNEXT_RESOURCE_HINTS__
|
|
257
|
+
const arr = renderBuffer<unknown>('hints')
|
|
261
258
|
let keys = scriptDedupRegistry.get(arr)
|
|
262
259
|
if (!keys) {
|
|
263
260
|
keys = new Set<string>()
|
|
@@ -267,20 +264,15 @@ function executedScriptKeys(): Set<string> {
|
|
|
267
264
|
}
|
|
268
265
|
|
|
269
266
|
function headScripts() {
|
|
270
|
-
|
|
271
|
-
holder.__PNEXT_HEAD_SCRIPTS__ ??= []
|
|
272
|
-
return holder.__PNEXT_HEAD_SCRIPTS__
|
|
267
|
+
return renderBuffer<HeadScript>('head')
|
|
273
268
|
}
|
|
274
269
|
|
|
275
270
|
export function clearHeadScripts() {
|
|
276
|
-
|
|
271
|
+
clearRenderBuffer('head')
|
|
277
272
|
}
|
|
278
273
|
|
|
279
274
|
export function takeHeadScripts() {
|
|
280
|
-
|
|
281
|
-
const scripts = holder.__PNEXT_HEAD_SCRIPTS__ ?? []
|
|
282
|
-
holder.__PNEXT_HEAD_SCRIPTS__ = []
|
|
283
|
-
return scripts
|
|
275
|
+
return takeRenderBuffer<HeadScript>('head')
|
|
284
276
|
}
|
|
285
277
|
|
|
286
278
|
export function renderHeadScripts() {
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
// Everything here is backed by preact core + preact/hooks, so the bundle ships no preact/compat. The
|
|
4
4
|
// parity import wires the same vnode pass (React 19 ref prop, primitive-throw safety) as the full shim.
|
|
5
5
|
import './parity'
|
|
6
|
+
import './style-values'
|
|
6
7
|
import {
|
|
7
8
|
Component,
|
|
8
9
|
Fragment,
|
|
@@ -1,3 +1,14 @@
|
|
|
1
|
+
import './style-values'
|
|
2
|
+
import ReactCompat from './preact'
|
|
3
|
+
import { attachReactInternals } from './internals'
|
|
4
|
+
|
|
5
|
+
attachReactInternals(ReactCompat)
|
|
6
|
+
|
|
7
|
+
export {
|
|
8
|
+
ReactSharedInternals as __SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED,
|
|
9
|
+
ReactClientInternals as __CLIENT_INTERNALS_DO_NOT_USE_OR_WARN_USERS_THEY_CANNOT_UPGRADE,
|
|
10
|
+
} from './internals'
|
|
11
|
+
|
|
1
12
|
export {
|
|
2
13
|
Children,
|
|
3
14
|
Component,
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* server response-finalization pipeline.
|
|
7
7
|
*/
|
|
8
8
|
import ReactCompat from 'preact/compat'
|
|
9
|
-
import { useActionState } from './preact'
|
|
9
|
+
import { useActionState, useSyncExternalStore } from './preact'
|
|
10
10
|
|
|
11
11
|
const ReactDomCompat = Object.assign(ReactCompat, {
|
|
12
12
|
preload,
|
|
@@ -27,6 +27,10 @@ export const flushSync = ReactCompat.flushSync
|
|
|
27
27
|
export const unmountComponentAtNode = ReactCompat.unmountComponentAtNode
|
|
28
28
|
export const unstable_batchedUpdates = ReactCompat.unstable_batchedUpdates
|
|
29
29
|
export const version = ReactCompat.version
|
|
30
|
+
export {
|
|
31
|
+
ReactDOMSharedInternals as __SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED,
|
|
32
|
+
ReactDOMSharedInternals as __DOM_INTERNALS_DO_NOT_USE_OR_WARN_USERS_THEY_CANNOT_UPGRADE,
|
|
33
|
+
} from './internals'
|
|
30
34
|
|
|
31
35
|
function noOpRecordResourceHint() {
|
|
32
36
|
// intentionally empty: client bundles do not build response headers.
|
|
@@ -106,7 +110,12 @@ function subscribe(listener: () => void) {
|
|
|
106
110
|
}
|
|
107
111
|
|
|
108
112
|
export function useFormStatus(): FormStatus {
|
|
109
|
-
|
|
113
|
+
// No submission is ever in flight during a server render, so the counter is its own server value.
|
|
114
|
+
useSyncExternalStore(
|
|
115
|
+
subscribe,
|
|
116
|
+
() => storeVersion,
|
|
117
|
+
() => storeVersion,
|
|
118
|
+
)
|
|
110
119
|
return pendingStack.length > 0 ? pendingStack[pendingStack.length - 1]! : NOT_PENDING
|
|
111
120
|
}
|
|
112
121
|
|
|
@@ -11,4 +11,8 @@ delete ReactDomServer.useFormStatus
|
|
|
11
11
|
delete ReactDomServer.requestFormReset
|
|
12
12
|
|
|
13
13
|
export { preconnect, prefetchDNS, preinit, preload, version }
|
|
14
|
+
export {
|
|
15
|
+
ReactDOMSharedInternals as __SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED,
|
|
16
|
+
ReactDOMSharedInternals as __DOM_INTERNALS_DO_NOT_USE_OR_WARN_USERS_THEY_CANNOT_UPGRADE,
|
|
17
|
+
} from './internals'
|
|
14
18
|
export default ReactDomServer
|
package/src/compat/react/dom.ts
CHANGED
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
* uncontrolled forms after a successful action itself.
|
|
15
15
|
*/
|
|
16
16
|
import ReactCompat from 'preact/compat'
|
|
17
|
-
import { useActionState } from './preact'
|
|
17
|
+
import { useActionState, useSyncExternalStore } from './preact'
|
|
18
18
|
import { registerResourceHint } from '../../render/resource-hints'
|
|
19
19
|
|
|
20
20
|
const ReactDomCompat = Object.assign(ReactCompat, {
|
|
@@ -36,6 +36,10 @@ export const flushSync = ReactCompat.flushSync
|
|
|
36
36
|
export const unmountComponentAtNode = ReactCompat.unmountComponentAtNode
|
|
37
37
|
export const unstable_batchedUpdates = ReactCompat.unstable_batchedUpdates
|
|
38
38
|
export const version = ReactCompat.version
|
|
39
|
+
export {
|
|
40
|
+
ReactDOMSharedInternals as __SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED,
|
|
41
|
+
ReactDOMSharedInternals as __DOM_INTERNALS_DO_NOT_USE_OR_WARN_USERS_THEY_CANNOT_UPGRADE,
|
|
42
|
+
} from './internals'
|
|
39
43
|
|
|
40
44
|
export function preload(
|
|
41
45
|
href: string,
|
|
@@ -143,7 +147,12 @@ function subscribe(listener: () => void) {
|
|
|
143
147
|
}
|
|
144
148
|
|
|
145
149
|
export function useFormStatus(): FormStatus {
|
|
146
|
-
|
|
150
|
+
// No submission is ever in flight during a server render, so the counter is its own server value.
|
|
151
|
+
useSyncExternalStore(
|
|
152
|
+
subscribe,
|
|
153
|
+
() => storeVersion,
|
|
154
|
+
() => storeVersion,
|
|
155
|
+
)
|
|
147
156
|
return pendingStack.length > 0 ? pendingStack[pendingStack.length - 1]! : NOT_PENDING
|
|
148
157
|
}
|
|
149
158
|
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
// React publishes its shared internals as a well-known export, and packages that reach into it read
|
|
2
|
+
// fields at MODULE scope - `var ReactCurrentOwner = React.__SECRET_INTERNALS_….ReactCurrentOwner` in
|
|
3
|
+
// react-reconciler, which every custom renderer (react-konva, react-three-fiber, react-pdf, ink) pulls
|
|
4
|
+
// in. preact has no equivalent, so the whole import crashes with a TypeError before a single component
|
|
5
|
+
// renders. The shim publishes React's shape with inert slots: reads resolve, and nothing pretends to be
|
|
6
|
+
// driving a render (real React's slots are null outside one too).
|
|
7
|
+
|
|
8
|
+
// Field-for-field react@18.3.1 and react@19.2.8, no more: an invented field is weight in every bundle
|
|
9
|
+
// and a lie to whatever reads it.
|
|
10
|
+
export const ReactSharedInternals = {
|
|
11
|
+
ReactCurrentDispatcher: { current: null },
|
|
12
|
+
ReactCurrentBatchConfig: { transition: null },
|
|
13
|
+
ReactCurrentOwner: { current: null },
|
|
14
|
+
ReactCurrentActQueue: { current: null, isBatchingLegacy: false, didScheduleLegacyUpdate: false },
|
|
15
|
+
ReactDebugCurrentFrame: { getCurrentStack: null },
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** React 19 renamed both the export and every field (`__CLIENT_INTERNALS_…`). */
|
|
19
|
+
export const ReactClientInternals = {
|
|
20
|
+
H: null,
|
|
21
|
+
A: null,
|
|
22
|
+
T: null,
|
|
23
|
+
S: null,
|
|
24
|
+
actQueue: null,
|
|
25
|
+
asyncTransitions: 0,
|
|
26
|
+
isBatchingLegacy: false,
|
|
27
|
+
didScheduleLegacyUpdate: false,
|
|
28
|
+
didUsePromise: false,
|
|
29
|
+
thrownErrors: [],
|
|
30
|
+
getCurrentStack: null,
|
|
31
|
+
recentlyCreatedOwnerStacks: 0,
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* react-dom's own internals, under both its React 18 (`usingClientEntryPoint`/`Events`) and React 19
|
|
36
|
+
* (`d`/`p`/`findDOMNode`) shapes. Named exports only: the `react`/`react-dom` shims share ONE
|
|
37
|
+
* preact/compat default object, so attaching a react-dom shape to it would shadow react's. Namespace
|
|
38
|
+
* and CJS-interop reads - how react-dom internals are read in practice - resolve the named export.
|
|
39
|
+
*/
|
|
40
|
+
export const ReactDOMSharedInternals = {
|
|
41
|
+
usingClientEntryPoint: false,
|
|
42
|
+
Events: null,
|
|
43
|
+
d: {},
|
|
44
|
+
p: 0,
|
|
45
|
+
findDOMNode: null,
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const REACT_18_KEY = '__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED'
|
|
49
|
+
const REACT_19_KEY = '__CLIENT_INTERNALS_DO_NOT_USE_OR_WARN_USERS_THEY_CANNOT_UPGRADE'
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Attach both react internals keys to a shim's default export object. preact/compat already publishes
|
|
53
|
+
* a one-field `ReactCurrentDispatcher`-only object under the React 18 key and reads that dispatcher for
|
|
54
|
+
* its own `readContext`, so its live slots are folded IN rather than replaced - and folded into the
|
|
55
|
+
* exported object, keeping one identity for consumers that capture a slot by reference.
|
|
56
|
+
*/
|
|
57
|
+
export function attachReactInternals<Target extends object>(target: Target): Target {
|
|
58
|
+
const slots = target as Record<string, Record<string, unknown> | undefined>
|
|
59
|
+
if (slots[REACT_18_KEY]) Object.assign(ReactSharedInternals, slots[REACT_18_KEY])
|
|
60
|
+
if (slots[REACT_19_KEY]) Object.assign(ReactClientInternals, slots[REACT_19_KEY])
|
|
61
|
+
return Object.assign(target, {
|
|
62
|
+
[REACT_18_KEY]: ReactSharedInternals,
|
|
63
|
+
[REACT_19_KEY]: ReactClientInternals,
|
|
64
|
+
})
|
|
65
|
+
}
|
|
@@ -50,6 +50,7 @@ if (!pnextOptions[react19RefCompatInstalled]) {
|
|
|
50
50
|
suspenseParity?.beforeThrowSafety(vnode)
|
|
51
51
|
applyPrimitiveThrowSafety(vnode)
|
|
52
52
|
suspenseParity?.afterThrowSafety(vnode)
|
|
53
|
+
applyStrayDocumentChildren(vnode)
|
|
53
54
|
applyReactTextSeparators(vnode)
|
|
54
55
|
}
|
|
55
56
|
pnextOptions[react19RefCompatInstalled] = true
|
|
@@ -77,6 +78,52 @@ if (!pnextOptions[react19RefCompatInstalled]) {
|
|
|
77
78
|
}
|
|
78
79
|
}
|
|
79
80
|
|
|
81
|
+
// A root layout may render children directly under `<html>` alongside `<body>` - `<GoogleAnalytics/>`,
|
|
82
|
+
// a `<Script>`, an analytics island. React renders them in place and the browser's parser hoists the
|
|
83
|
+
// result into `<body>`; pnext's document reader keeps only the `<head>`/`<body>` children, so anything
|
|
84
|
+
// else was dropped and never ran (no resource hints, no hydration). Relocate strays to the front of
|
|
85
|
+
// `<body>` - the position the parser would have given them - so the component renders exactly once.
|
|
86
|
+
function applyStrayDocumentChildren(vnode: VNode) {
|
|
87
|
+
// Server-only: the client never re-reads the document layout (see applyReactTextSeparators).
|
|
88
|
+
if (process.browser || typeof window !== 'undefined') return
|
|
89
|
+
if (vnode.type !== 'html') return
|
|
90
|
+
const props = vnode.props as Record<string, unknown>
|
|
91
|
+
const children = props.children
|
|
92
|
+
if (!Array.isArray(children)) return
|
|
93
|
+
|
|
94
|
+
const kept: unknown[] = []
|
|
95
|
+
const stray: unknown[] = []
|
|
96
|
+
let body: VNode | undefined
|
|
97
|
+
for (const child of children as unknown[]) {
|
|
98
|
+
const type = isElementVNode(child) ? child.type : undefined
|
|
99
|
+
if (type === 'body') body = child as VNode
|
|
100
|
+
if (type === 'head' || type === 'body' || !mayRenderContent(child)) {
|
|
101
|
+
kept.push(child)
|
|
102
|
+
continue
|
|
103
|
+
}
|
|
104
|
+
stray.push(child)
|
|
105
|
+
}
|
|
106
|
+
if (!body || stray.length === 0) return
|
|
107
|
+
|
|
108
|
+
const bodyProps = body.props as Record<string, unknown>
|
|
109
|
+
const bodyChildren: unknown = bodyProps.children
|
|
110
|
+
bodyProps.children = [
|
|
111
|
+
...stray,
|
|
112
|
+
...(Array.isArray(bodyChildren) ? (bodyChildren as unknown[]) : [bodyChildren]),
|
|
113
|
+
]
|
|
114
|
+
props.children = kept
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function isElementVNode(value: unknown): value is VNode {
|
|
118
|
+
return typeof value === 'object' && value !== null && 'type' in value
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Nullish/boolean children and JSX whitespace render nothing, so leaving them under `<html>` is free.
|
|
122
|
+
function mayRenderContent(child: unknown) {
|
|
123
|
+
if (child == null || typeof child === 'boolean') return false
|
|
124
|
+
return typeof child === 'string' ? child.trim() !== '' : true
|
|
125
|
+
}
|
|
126
|
+
|
|
80
127
|
function applyReactTextSeparators(vnode: VNode) {
|
|
81
128
|
// Server-only. `process.browser` is a client-bundle define (true) and
|
|
82
129
|
// undefined on the server, so this body folds out of browser builds; the
|
|
@@ -80,11 +80,42 @@ function applyAsyncClientComponent(vnode: VNode) {
|
|
|
80
80
|
vnode.type = wrapper
|
|
81
81
|
}
|
|
82
82
|
|
|
83
|
+
// Captured before the augmentation below overwrites the key, or the wrapper would recurse.
|
|
84
|
+
const preactUseSyncExternalStore = ReactCompat.useSyncExternalStore
|
|
85
|
+
|
|
86
|
+
// preact/compat ignores getServerSnapshot; React uses it for every server render, so without this
|
|
87
|
+
// the client-SSR pass calls the browser snapshot (and app stores touch `window`).
|
|
88
|
+
function serverSnapshotAware<Snapshot>(
|
|
89
|
+
subscribe: (onStoreChange: () => void) => () => void,
|
|
90
|
+
getSnapshot: () => Snapshot,
|
|
91
|
+
getServerSnapshot?: () => Snapshot,
|
|
92
|
+
): Snapshot {
|
|
93
|
+
if (typeof window === 'undefined') {
|
|
94
|
+
if (!getServerSnapshot)
|
|
95
|
+
throw new Error(
|
|
96
|
+
'Missing getServerSnapshot, which is required for server-rendered content. Will revert to client rendering.',
|
|
97
|
+
)
|
|
98
|
+
return getServerSnapshot()
|
|
99
|
+
}
|
|
100
|
+
return preactUseSyncExternalStore(subscribe, getSnapshot)
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// The browser build folds `process.browser` to true, so it ships preact's function verbatim and
|
|
104
|
+
// tree-shakes the wrapper - the server-only text never reaches the client. Purely a size win: the
|
|
105
|
+
// wrapper is already correct in a browser, so an undefined `process.browser` only costs bytes.
|
|
106
|
+
export const useSyncExternalStore: typeof serverSnapshotAware = process.browser
|
|
107
|
+
? preactUseSyncExternalStore
|
|
108
|
+
: serverSnapshotAware
|
|
109
|
+
|
|
83
110
|
// React 19 APIs preact/compat lacks - use(), useActionState, useOptimistic (imported above) - must ALSO
|
|
84
111
|
// live on the default export: app code does `import React from 'react'; React.use(...)`, not only named
|
|
85
112
|
// imports. server.ts spreads this default into ReactServer, so the augmentation covers both layers.
|
|
86
113
|
export default Object.assign(ReactCompat, { use, useActionState, useOptimistic, useTransition })
|
|
87
114
|
|
|
115
|
+
// `React.useSyncExternalStore` needs the wrapper for the same reason the named export does. The
|
|
116
|
+
// browser folds this to `if (false)` and drops it: preact's own property is already correct there.
|
|
117
|
+
if (!process.browser) ReactCompat.useSyncExternalStore = useSyncExternalStore
|
|
118
|
+
|
|
88
119
|
export const Children = ReactCompat.Children
|
|
89
120
|
export const Component = ReactCompat.Component
|
|
90
121
|
export const Fragment = ReactCompat.Fragment
|
|
@@ -130,5 +161,4 @@ export const useMemo = ReactCompat.useMemo
|
|
|
130
161
|
export const useReducer = ReactCompat.useReducer
|
|
131
162
|
export const useRef = ReactCompat.useRef
|
|
132
163
|
export const useState = ReactCompat.useState
|
|
133
|
-
export const useSyncExternalStore = ReactCompat.useSyncExternalStore
|
|
134
164
|
export const version = ReactCompat.version
|
|
@@ -43,6 +43,10 @@ export {
|
|
|
43
43
|
version,
|
|
44
44
|
} from './preact'
|
|
45
45
|
export { cache } from '../../api/cache'
|
|
46
|
+
export {
|
|
47
|
+
ReactSharedInternals as __SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED,
|
|
48
|
+
ReactClientInternals as __CLIENT_INTERNALS_DO_NOT_USE_OR_WARN_USERS_THEY_CANNOT_UPGRADE,
|
|
49
|
+
} from './internals'
|
|
46
50
|
|
|
47
51
|
import { ViewTransition, addTransitionType } from './view-transition'
|
|
48
52
|
export { ViewTransition, addTransitionType }
|
|
@@ -60,7 +64,9 @@ export function cacheSignal(): AbortSignal | null {
|
|
|
60
64
|
return currentCacheSignal()
|
|
61
65
|
}
|
|
62
66
|
|
|
67
|
+
import './style-values'
|
|
63
68
|
import ReactCompat from './preact'
|
|
69
|
+
import { attachReactInternals } from './internals'
|
|
64
70
|
import { cache as serverCache } from '../../api/cache'
|
|
65
71
|
|
|
66
72
|
const ReactServer = { ...ReactCompat } as typeof ReactCompat & {
|
|
@@ -80,5 +86,6 @@ delete (ReactServer as { useSyncExternalStore?: unknown }).useSyncExternalStore
|
|
|
80
86
|
ReactServer.cache = serverCache
|
|
81
87
|
ReactServer.cacheSignal = cacheSignal
|
|
82
88
|
Object.assign(ReactServer, { ViewTransition, addTransitionType })
|
|
89
|
+
attachReactInternals(ReactServer)
|
|
83
90
|
|
|
84
91
|
export default ReactServer
|
|
@@ -1,5 +1,14 @@
|
|
|
1
|
+
import './style-values'
|
|
1
2
|
import ReactCompat from './preact'
|
|
3
|
+
import { attachReactInternals } from './internals'
|
|
2
4
|
import { cache as serverCache } from '../../api/cache'
|
|
5
|
+
|
|
6
|
+
attachReactInternals(ReactCompat)
|
|
7
|
+
|
|
8
|
+
export {
|
|
9
|
+
ReactSharedInternals as __SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED,
|
|
10
|
+
ReactClientInternals as __CLIENT_INTERNALS_DO_NOT_USE_OR_WARN_USERS_THEY_CANNOT_UPGRADE,
|
|
11
|
+
} from './internals'
|
|
3
12
|
import { currentPrerenderScope } from '../../render/ppr'
|
|
4
13
|
import { currentCacheSignal } from '../../request/cache'
|
|
5
14
|
|
|
@@ -91,10 +100,15 @@ function useTransition(): [false, (callback: () => void) => void] {
|
|
|
91
100
|
|
|
92
101
|
function useSyncExternalStore<Snapshot>(
|
|
93
102
|
_subscribe: (onStoreChange: () => void) => () => void,
|
|
94
|
-
|
|
103
|
+
_getSnapshot: () => Snapshot,
|
|
95
104
|
getServerSnapshot?: () => Snapshot,
|
|
96
105
|
): Snapshot {
|
|
97
|
-
|
|
106
|
+
// Falling back to getSnapshot here is the same hazard: this layer only ever renders on the server.
|
|
107
|
+
if (!getServerSnapshot)
|
|
108
|
+
throw new Error(
|
|
109
|
+
'Missing getServerSnapshot, which is required for server-rendered content. Will revert to client rendering.',
|
|
110
|
+
)
|
|
111
|
+
return getServerSnapshot()
|
|
98
112
|
}
|
|
99
113
|
|
|
100
114
|
function useContext<Value>(context: Parameters<typeof ReactCompat.useContext<Value>>[0]): Value {
|