@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.
Files changed (75) hide show
  1. package/README.md +2 -2
  2. package/package.json +1 -1
  3. package/reference/compat.md +2 -2
  4. package/reference/getting-started.md +2 -2
  5. package/src/cli/adapters/vercel.ts +27 -0
  6. package/src/cli/analyze.ts +8 -7
  7. package/src/cli/boot/trace.ts +9 -3
  8. package/src/cli/build.ts +23 -15
  9. package/src/cli/dev.ts +23 -0
  10. package/src/cli/migrate/report.ts +1 -1
  11. package/src/cli/migrate/run.ts +2 -2
  12. package/src/cli/migrate/scan.ts +128 -1
  13. package/src/cli/serve/pipeline.ts +42 -42
  14. package/src/cli/start.ts +4 -2
  15. package/src/client/build.ts +51 -25
  16. package/src/client/entry.ts +27 -4
  17. package/src/client/router/page-slot.ts +81 -0
  18. package/src/client/router/runtime.ts +107 -112
  19. package/src/compat/bundler/cjs-exports.ts +26 -20
  20. package/src/compat/client/css-order.ts +1 -1
  21. package/src/compat/client/navigation-scroll.ts +36 -40
  22. package/src/compat/css/modules.ts +6 -2
  23. package/src/compat/next/font/cache.ts +10 -22
  24. package/src/compat/next/font/runtime.ts +20 -0
  25. package/src/compat/next/legacy-request-apis.ts +41 -5
  26. package/src/compat/next/script.tsx +16 -24
  27. package/src/compat/react/client-lite.ts +1 -0
  28. package/src/compat/react/client.ts +11 -0
  29. package/src/compat/react/dom-client.ts +11 -2
  30. package/src/compat/react/dom-react-server.ts +4 -0
  31. package/src/compat/react/dom.ts +11 -2
  32. package/src/compat/react/internals.ts +65 -0
  33. package/src/compat/react/parity.ts +47 -0
  34. package/src/compat/react/preact.ts +31 -1
  35. package/src/compat/react/react-server.ts +7 -0
  36. package/src/compat/react/server.ts +16 -2
  37. package/src/compat/react/style-values.ts +68 -0
  38. package/src/compat/register/render.ts +14 -3
  39. package/src/compat/register/routing.ts +57 -7
  40. package/src/compat/tsconfig-defaults.ts +4 -0
  41. package/src/compat/typecheck/check.ts +26 -71
  42. package/src/compat/typecheck/worker.ts +3 -2
  43. package/src/compat-bootstrap.ts +4 -0
  44. package/src/css/build.ts +153 -14
  45. package/src/css/postcss.ts +32 -3
  46. package/src/dev/restart/client-key.ts +1 -1
  47. package/src/dev/restart/global-css.ts +12 -94
  48. package/src/dev/restart/record.ts +106 -0
  49. package/src/dev/restart/route-facts.ts +11 -90
  50. package/src/dev/server.ts +208 -41
  51. package/src/extensions.ts +69 -3
  52. package/src/render/hooks.ts +1 -1
  53. package/src/render/metadata.ts +89 -17
  54. package/src/render/renderer.ts +322 -136
  55. package/src/render/resource-hints.ts +62 -9
  56. package/src/render/slots.tsx +2 -1
  57. package/src/resolve/scan-facts.ts +13 -5
  58. package/src/routing/forwarded.ts +20 -7
  59. package/src/routing/handler.ts +6 -2
  60. package/src/routing/href.ts +11 -1
  61. package/src/routing/metadata-files.ts +16 -3
  62. package/src/routing/proxy.ts +6 -3
  63. package/src/routing/routes.ts +253 -34
  64. package/src/runtime/fetch-host.ts +42 -0
  65. package/src/runtime/fingerprint.ts +132 -0
  66. package/src/runtime/loader.ts +7 -5
  67. package/src/runtime/module-cache.ts +3 -36
  68. package/src/runtime/modules.ts +333 -77
  69. package/src/runtime/vendor-build.ts +34 -5
  70. package/src/runtime/vendor.ts +1 -18
  71. package/src/types.ts +6 -0
  72. package/src/utils/asset-hash.ts +22 -0
  73. package/src/utils/code.ts +41 -0
  74. package/src/utils/html.ts +4 -1
  75. 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
- /** Content hash of the font runtime, so an upgrade never serves its output. */
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 = 1
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
- // The CSS a resolution emits is shaped entirely by this directory's sources, so
154
- // editing the font runtime must retire its records the same way editing a font
155
- // does. Four small files, once per process.
156
- let fingerprint: string | undefined
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`. Each sync access
4
- // logs Next's sync-dynamic-apis deprecation warning, deduped by userland callsite.
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
- warnSyncAccess(api, prop)
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
- function warnSyncAccess(api: string, prop: PropertyKey): void {
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
- `\`${api}()\` should be awaited before using its value. ` +
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 { registerResourceHint } from '../../render/resource-hints'
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
- const holder = globalThis as { __PNEXT_DEFERRED_SCRIPTS__?: DeferredScript[] }
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
- ;(globalThis as { __PNEXT_DEFERRED_SCRIPTS__?: DeferredScript[] }).__PNEXT_DEFERRED_SCRIPTS__ = []
207
+ clearRenderBuffer('deferred')
205
208
  }
206
209
 
207
210
  export function takeDeferredScripts() {
208
- const holder = globalThis as { __PNEXT_DEFERRED_SCRIPTS__?: DeferredScript[] }
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 renderer replaces the resource-hints array with a fresh `[]` at the start
252
- // of every render (clearResourceHints). Keying the executed-set off that array
253
- // via a WeakMap gives us a dedup scope that lives exactly one render and is
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 holder = globalThis as { __PNEXT_RESOURCE_HINTS__?: object }
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
- const holder = globalThis as { __PNEXT_HEAD_SCRIPTS__?: HeadScript[] }
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
- ;(globalThis as { __PNEXT_HEAD_SCRIPTS__?: HeadScript[] }).__PNEXT_HEAD_SCRIPTS__ = []
271
+ clearRenderBuffer('head')
277
272
  }
278
273
 
279
274
  export function takeHeadScripts() {
280
- const holder = globalThis as { __PNEXT_HEAD_SCRIPTS__?: HeadScript[] }
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
- ReactCompat.useSyncExternalStore(subscribe, () => storeVersion)
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
@@ -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
- ReactCompat.useSyncExternalStore(subscribe, () => storeVersion)
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
- getSnapshot: () => Snapshot,
103
+ _getSnapshot: () => Snapshot,
95
104
  getServerSnapshot?: () => Snapshot,
96
105
  ): Snapshot {
97
- return getServerSnapshot ? getServerSnapshot() : getSnapshot()
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 {