@wular/pnext 0.0.18 → 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/config.ts +21 -12
- 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
package/src/render/renderer.ts
CHANGED
|
@@ -54,7 +54,16 @@ import {
|
|
|
54
54
|
reportRequestError,
|
|
55
55
|
type SerializedError,
|
|
56
56
|
} from '../extensions'
|
|
57
|
-
import {
|
|
57
|
+
import {
|
|
58
|
+
assetHref,
|
|
59
|
+
emitFontCssStylesheet,
|
|
60
|
+
emittedAssetName,
|
|
61
|
+
globalCssHref,
|
|
62
|
+
registerCssRuntime,
|
|
63
|
+
routeCssAssetNames,
|
|
64
|
+
routeCssHref,
|
|
65
|
+
withAssetPrefix,
|
|
66
|
+
} from '../css/build'
|
|
58
67
|
import { dynamicReferenceSymbol, type DynamicReference } from '../api/dynamic'
|
|
59
68
|
import {
|
|
60
69
|
isForbiddenError,
|
|
@@ -92,6 +101,7 @@ import {
|
|
|
92
101
|
findConventionFiles,
|
|
93
102
|
findGlobalError,
|
|
94
103
|
findLayouts,
|
|
104
|
+
serverActionsUnsupportedMessage,
|
|
95
105
|
} from '../routing/routes'
|
|
96
106
|
import { matchSegments, pathnameSegments, slotDirectoriesIn } from '../routing/slots'
|
|
97
107
|
import { getRequestRuntime } from '../routing/request-environment'
|
|
@@ -119,10 +129,12 @@ import {
|
|
|
119
129
|
} from '../utils/dev-profile'
|
|
120
130
|
import {
|
|
121
131
|
applyStaticMetadata,
|
|
132
|
+
assetLinkGroups,
|
|
122
133
|
coerceString,
|
|
123
134
|
headLinks,
|
|
124
135
|
metadataIconLinks,
|
|
125
136
|
metadataContext,
|
|
137
|
+
metadataOnlyLinks,
|
|
126
138
|
metadataTags,
|
|
127
139
|
mergeMetadataEntries,
|
|
128
140
|
mergeViewport,
|
|
@@ -136,7 +148,15 @@ import {
|
|
|
136
148
|
type ViewportExport,
|
|
137
149
|
type MetadataEntry,
|
|
138
150
|
} from './metadata'
|
|
139
|
-
import {
|
|
151
|
+
import {
|
|
152
|
+
clearRenderBuffer,
|
|
153
|
+
clearResourceHints,
|
|
154
|
+
newRenderBufferFrame,
|
|
155
|
+
RENDER_BUFFER_SCOPE,
|
|
156
|
+
takeRenderBuffer,
|
|
157
|
+
takeResourceHints,
|
|
158
|
+
type RenderBufferFrame,
|
|
159
|
+
} from './resource-hints'
|
|
140
160
|
import {
|
|
141
161
|
staticMetadataForPath,
|
|
142
162
|
staticMetadataForPathFromFiles,
|
|
@@ -230,6 +250,9 @@ function runWithFontScope<T>(callback: () => T): T {
|
|
|
230
250
|
|
|
231
251
|
const rawTextRenderingInstalled = Symbol.for('pnext.raw-text-rendering-installed')
|
|
232
252
|
const actionSerializeScope = new AsyncLocalStorage<ActionSerializeState>()
|
|
253
|
+
// Owns the per-render buffer frame the compat bundles write into; see render/resource-hints.
|
|
254
|
+
const renderBufferScope = ((globalThis as Record<PropertyKey, unknown>)[RENDER_BUFFER_SCOPE] ??=
|
|
255
|
+
new AsyncLocalStorage<RenderBufferFrame>()) as AsyncLocalStorage<RenderBufferFrame>
|
|
233
256
|
/** True while a render that SUSPENDS in place is running; see installRawTextRendering. */
|
|
234
257
|
const suspendingStreamScope = new AsyncLocalStorage<boolean>()
|
|
235
258
|
|
|
@@ -402,7 +425,6 @@ interface RenderOptions {
|
|
|
402
425
|
staticMetadataFiles?: StaticMetadataFile[]
|
|
403
426
|
staticModuleMetadata?: Record<string, StaticModuleMetadata>
|
|
404
427
|
staticRouteMetadata?: Record<string, StaticMetadataForPath>
|
|
405
|
-
resolveDynamicMetadataRoutes?: boolean
|
|
406
428
|
status?: number
|
|
407
429
|
partial?: PartialState
|
|
408
430
|
/**
|
|
@@ -506,6 +528,8 @@ interface PageRender {
|
|
|
506
528
|
fontCss: string
|
|
507
529
|
bodyIconLinks?: string
|
|
508
530
|
compatBodyScript?: string
|
|
531
|
+
/** Inlined CSS (`<style>`); sits with the stylesheet links, ahead of every head script. */
|
|
532
|
+
headStyles?: string
|
|
509
533
|
headScripts?: string
|
|
510
534
|
documentHeadTags?: string
|
|
511
535
|
metadataUrl?: string
|
|
@@ -735,9 +759,10 @@ interface DocumentPage extends MetadataRenderPage {
|
|
|
735
759
|
fontCss: string
|
|
736
760
|
bodyIconLinks?: string
|
|
737
761
|
bodyMetadata?: string
|
|
738
|
-
/**
|
|
739
|
-
|
|
762
|
+
/** Document metadata lives below the fold (streamed or deferred), so the head must not carry it. */
|
|
763
|
+
metadataInBody?: boolean
|
|
740
764
|
compatBodyScript?: string
|
|
765
|
+
headStyles?: string
|
|
741
766
|
headScripts?: string
|
|
742
767
|
documentHeadTags?: string
|
|
743
768
|
deferredScripts?: string
|
|
@@ -775,29 +800,53 @@ function titleTag(title: unknown): string {
|
|
|
775
800
|
return value ? `<title>${escapeHtml(value)}</title>` : ''
|
|
776
801
|
}
|
|
777
802
|
|
|
803
|
+
// Head order, taken from React's Fizz preamble (Next inherits it verbatim): charset, preconnects,
|
|
804
|
+
// the viewport meta, font/high-priority-image preloads, the precedence-ordered stylesheets, the bulk
|
|
805
|
+
// preloads, then the bootstrap scripts. Everything registered AFTER that preamble flushes - late
|
|
806
|
+
// preloads, and every ordinary <meta>/<title>/metadata <link>, which React hoists rather than writes
|
|
807
|
+
// into the preamble - follows in encounter order.
|
|
808
|
+
//
|
|
809
|
+
// This only reorders the asset links; the head-vs-body split is untouched, so
|
|
810
|
+
// metadataOnlyLinks still travel with the metadata and never appear when it streams into the body.
|
|
778
811
|
function renderDocumentStart(page: DocumentPage) {
|
|
779
812
|
const rendered = page.shell === 'layout' ? page.body : `<main data-pnext-root>${page.body}</main>`
|
|
780
|
-
const slotMode = pageSlotMarkerMode(page
|
|
813
|
+
const slotMode = pageSlotMarkerMode(page)
|
|
781
814
|
const body = unwrapUnusedPageSlot(rendered, slotMode)
|
|
782
815
|
const viewport = viewportContent(page.viewport)
|
|
783
816
|
const context = metadataContext(page)
|
|
784
817
|
const headPage = page.bodyIconLinks
|
|
785
818
|
? { ...page, metadata: { ...page.metadata, icons: undefined }, suppressIconLinks: true }
|
|
786
819
|
: page
|
|
820
|
+
const [connects, preloads, stylesheets, bulkPreloads, lateHints] = assetLinkGroups(headPage)
|
|
821
|
+
// Joined with the empty slots dropped: a head with no fonts/hints/inline CSS used to ship a blank
|
|
822
|
+
// line per unused slot, and the bucket split multiplies them.
|
|
823
|
+
const head = [
|
|
824
|
+
'<meta charSet="utf-8"/>',
|
|
825
|
+
connects,
|
|
826
|
+
`<meta name="viewport" content="${escapeHtml(viewport)}"/>`,
|
|
827
|
+
preloads,
|
|
828
|
+
page.fontCss ? `<style data-pnext-font>${page.fontCss}</style>` : '',
|
|
829
|
+
page.headStyles,
|
|
830
|
+
stylesheets,
|
|
831
|
+
bulkPreloads,
|
|
832
|
+
page.headScripts,
|
|
833
|
+
lateHints,
|
|
834
|
+
page.documentHeadTags,
|
|
835
|
+
page.viewport.colorScheme
|
|
836
|
+
? `<meta name="color-scheme" content="${escapeHtml(page.viewport.colorScheme)}"/>`
|
|
837
|
+
: '',
|
|
838
|
+
themeColorTags(page.viewport.themeColor),
|
|
839
|
+
page.metadataInBody ? '' : titleTag(page.metadata.title),
|
|
840
|
+
page.metadataInBody ? '' : metadataTags(page.metadata, context),
|
|
841
|
+
page.metadataInBody ? '' : metadataOnlyLinks(headPage, context),
|
|
842
|
+
page.head,
|
|
843
|
+
]
|
|
844
|
+
.filter(Boolean)
|
|
845
|
+
.join('\n ')
|
|
787
846
|
// Next emits the doctype and <html> on one line; tests match /^<!DOCTYPE html><html/.
|
|
788
847
|
const html = `<!DOCTYPE html><html${documentAttrs(page.htmlProps, page.metadata.lang ? { lang: page.metadata.lang } : {})}>
|
|
789
848
|
<head>
|
|
790
|
-
|
|
791
|
-
<meta name="viewport" content="${escapeHtml(viewport)}"/>
|
|
792
|
-
${page.viewport.colorScheme ? `<meta name="color-scheme" content="${escapeHtml(page.viewport.colorScheme)}"/>` : ''}
|
|
793
|
-
${themeColorTags(page.viewport.themeColor)}
|
|
794
|
-
${page.bodyMetadata || page.bodyMetadataDeferred ? '' : titleTag(page.metadata.title)}
|
|
795
|
-
${page.bodyMetadata || page.bodyMetadataDeferred ? '' : metadataTags(page.metadata, context)}
|
|
796
|
-
${page.bodyMetadata || page.bodyMetadataDeferred ? '' : headLinks(headPage, context)}
|
|
797
|
-
${page.headScripts ?? ''}
|
|
798
|
-
${page.documentHeadTags ?? ''}
|
|
799
|
-
${page.fontCss ? `<style data-pnext-font>${page.fontCss}</style>` : ''}
|
|
800
|
-
${page.head}
|
|
849
|
+
${head}
|
|
801
850
|
</head>
|
|
802
851
|
<body${documentAttrs(page.bodyProps)}>
|
|
803
852
|
${body}
|
|
@@ -944,14 +993,11 @@ interface CollectedHeadScript {
|
|
|
944
993
|
}
|
|
945
994
|
|
|
946
995
|
function clearHeadScripts() {
|
|
947
|
-
|
|
996
|
+
clearRenderBuffer('head')
|
|
948
997
|
}
|
|
949
998
|
|
|
950
999
|
function takeHeadScripts() {
|
|
951
|
-
|
|
952
|
-
const scripts = holder.__PNEXT_HEAD_SCRIPTS__ ?? []
|
|
953
|
-
holder.__PNEXT_HEAD_SCRIPTS__ = []
|
|
954
|
-
return scripts
|
|
1000
|
+
return takeRenderBuffer<CollectedHeadScript>('head')
|
|
955
1001
|
}
|
|
956
1002
|
|
|
957
1003
|
function renderCollectedHeadScripts() {
|
|
@@ -990,16 +1036,11 @@ interface CollectedDeferredScript {
|
|
|
990
1036
|
}
|
|
991
1037
|
|
|
992
1038
|
function clearDeferredScripts() {
|
|
993
|
-
|
|
994
|
-
globalThis as { __PNEXT_DEFERRED_SCRIPTS__?: CollectedDeferredScript[] }
|
|
995
|
-
).__PNEXT_DEFERRED_SCRIPTS__ = []
|
|
1039
|
+
clearRenderBuffer('deferred')
|
|
996
1040
|
}
|
|
997
1041
|
|
|
998
1042
|
function takeDeferredScripts(): CollectedDeferredScript[] {
|
|
999
|
-
|
|
1000
|
-
const scripts = holder.__PNEXT_DEFERRED_SCRIPTS__ ?? []
|
|
1001
|
-
holder.__PNEXT_DEFERRED_SCRIPTS__ = []
|
|
1002
|
-
return scripts
|
|
1043
|
+
return takeRenderBuffer<CollectedDeferredScript>('deferred')
|
|
1003
1044
|
}
|
|
1004
1045
|
|
|
1005
1046
|
// A `type="module"` script, deferred like the hydration entry and ordered after it, so any
|
|
@@ -1221,18 +1262,29 @@ function createPageProps(options: RenderRequestOptions): PageProps {
|
|
|
1221
1262
|
}
|
|
1222
1263
|
}
|
|
1223
1264
|
}
|
|
1265
|
+
const params = trackedParams(options)
|
|
1266
|
+
const searchParams = trackedSearchParams(pageSearchParams(childrenSourceSearchParams(options)))
|
|
1224
1267
|
return {
|
|
1225
1268
|
// markRuntimeApiOnAwait is the identity outside a runtime-prefetch
|
|
1226
1269
|
// prerender; inside one, awaiting params/searchParams arms the sync-IO
|
|
1227
1270
|
// abort exactly as awaiting cookies()/headers() does.
|
|
1228
|
-
params: markRuntimeApiOnAwait(settledPromise(
|
|
1271
|
+
params: markRuntimeApiOnAwait(legacySyncProps(settledPromise(params), 'params', params)),
|
|
1229
1272
|
searchParams: markRuntimeApiOnAwait(
|
|
1230
|
-
settledPromise(
|
|
1273
|
+
legacySyncProps(settledPromise(searchParams), 'searchParams', searchParams),
|
|
1231
1274
|
),
|
|
1232
1275
|
request: options.request ? pageRequest(options.request) : undefined,
|
|
1233
1276
|
}
|
|
1234
1277
|
}
|
|
1235
1278
|
|
|
1279
|
+
/** Next 15's transitional sync surface on params/searchParams; identity unless compat enables it. */
|
|
1280
|
+
function legacySyncProps<T extends object>(
|
|
1281
|
+
promise: Promise<T>,
|
|
1282
|
+
kind: 'params' | 'searchParams',
|
|
1283
|
+
value: T,
|
|
1284
|
+
): Promise<T> {
|
|
1285
|
+
return getRenderExtensions().legacySyncProps(promise, kind, value)
|
|
1286
|
+
}
|
|
1287
|
+
|
|
1236
1288
|
/**
|
|
1237
1289
|
* The render's concrete params, wrapped in the compat vary-params tracking view
|
|
1238
1290
|
* when one is active (segment-prefetch renders). Outside a tracking scope the
|
|
@@ -1569,9 +1621,6 @@ export async function renderPageResponse(options: RenderOptions) {
|
|
|
1569
1621
|
headers: {
|
|
1570
1622
|
'content-type': 'text/html; charset=utf-8',
|
|
1571
1623
|
...(truncated ? { 'x-nextjs-postponed': '1' } : {}),
|
|
1572
|
-
// Error pages must never outlive the failure that produced them
|
|
1573
|
-
// in any cache (browser, prefetch, or intermediary).
|
|
1574
|
-
...((page.status ?? 200) >= 400 ? { 'cache-control': 'no-store' } : {}),
|
|
1575
1624
|
},
|
|
1576
1625
|
},
|
|
1577
1626
|
)
|
|
@@ -2544,7 +2593,7 @@ export async function renderGlobalNotFoundResponse(
|
|
|
2544
2593
|
)
|
|
2545
2594
|
response = new Response(pageStream(page), {
|
|
2546
2595
|
status: 404,
|
|
2547
|
-
headers: { 'content-type': 'text/html; charset=utf-8'
|
|
2596
|
+
headers: { 'content-type': 'text/html; charset=utf-8' },
|
|
2548
2597
|
})
|
|
2549
2598
|
}
|
|
2550
2599
|
// Next always marks the not-found page noindex.
|
|
@@ -2587,7 +2636,7 @@ export async function renderNotFoundForRoute(
|
|
|
2587
2636
|
)
|
|
2588
2637
|
const response = new Response(pageStream(page), {
|
|
2589
2638
|
status: 404,
|
|
2590
|
-
headers: { 'content-type': 'text/html; charset=utf-8'
|
|
2639
|
+
headers: { 'content-type': 'text/html; charset=utf-8' },
|
|
2591
2640
|
})
|
|
2592
2641
|
const html = await response.text()
|
|
2593
2642
|
const withRobots = html.includes('name="robots"')
|
|
@@ -2926,7 +2975,6 @@ async function pageRender(options: RenderOptions, stream: boolean): Promise<Page
|
|
|
2926
2975
|
layoutFiles,
|
|
2927
2976
|
clientComponents,
|
|
2928
2977
|
clientComponentsPreload,
|
|
2929
|
-
resolveDynamicMetadataRoutes: options.resolveDynamicMetadataRoutes,
|
|
2930
2978
|
},
|
|
2931
2979
|
loadedPage,
|
|
2932
2980
|
stream,
|
|
@@ -3172,9 +3220,9 @@ function withGlobalErrorReportScript(html: string, error: SerializedError): stri
|
|
|
3172
3220
|
}
|
|
3173
3221
|
|
|
3174
3222
|
function withGlobalErrorStylesheet(html: string, config: ResolvedConfig): string {
|
|
3175
|
-
const asset = `${config.outPath}/public/assets
|
|
3223
|
+
const asset = `${config.outPath}/public/assets/${emittedAssetName(config, 'global-error.css')}`
|
|
3176
3224
|
if (!existsSync(asset)) return html
|
|
3177
|
-
const link = `<link rel="stylesheet" href="${
|
|
3225
|
+
const link = `<link rel="stylesheet" href="${assetHref(config, 'global-error.css')}">`
|
|
3178
3226
|
if (html.includes('</head>')) return html.replace('</head>', `${link}</head>`)
|
|
3179
3227
|
return html.replace(/<html(?:\s[^>]*)?>/i, tag => `${tag}<head>${link}</head>`)
|
|
3180
3228
|
}
|
|
@@ -3287,7 +3335,16 @@ async function resolveSelectedSlotMetadata(
|
|
|
3287
3335
|
return { entries: [], runtime: false }
|
|
3288
3336
|
}
|
|
3289
3337
|
|
|
3290
|
-
|
|
3338
|
+
/**
|
|
3339
|
+
* One render, one buffer frame: the resource hints, head scripts and deferred scripts this render
|
|
3340
|
+
* collects are its own, so a render running alongside it (a build's next route, a not-found
|
|
3341
|
+
* document, another request) can neither drain them nor add to them.
|
|
3342
|
+
*/
|
|
3343
|
+
function renderTree(...args: Parameters<typeof renderTreeInFrame>): Promise<PageRender> {
|
|
3344
|
+
return renderBufferScope.run(newRenderBufferFrame(), () => renderTreeInFrame(...args))
|
|
3345
|
+
}
|
|
3346
|
+
|
|
3347
|
+
async function renderTreeInFrame(
|
|
3291
3348
|
options: RenderOptions,
|
|
3292
3349
|
tree: ComponentChildren,
|
|
3293
3350
|
stream: boolean,
|
|
@@ -3488,19 +3545,22 @@ async function renderTree(
|
|
|
3488
3545
|
const prebuiltRouteMetadata = hasMetadataExtensions
|
|
3489
3546
|
? undefined
|
|
3490
3547
|
: options.staticRouteMetadata?.[staticRouteMetadataKey(options.url.pathname)]
|
|
3548
|
+
// No prebuilt entry means nobody has resolved app/manifest.ts & friends for this
|
|
3549
|
+
// pathname yet: dev never prebuilds, and the build only prebuilds paths it knows
|
|
3550
|
+
// (so every parameterised route arrives here). Resolving is the only way those
|
|
3551
|
+
// documents carry the same metadata-file links as the paramless ones. An app with
|
|
3552
|
+
// no dynamic metadata files short-circuits inside withDynamicMetadataRoutes.
|
|
3491
3553
|
const staticMetadata =
|
|
3492
3554
|
prebuiltRouteMetadata ??
|
|
3493
|
-
(
|
|
3494
|
-
|
|
3495
|
-
|
|
3496
|
-
|
|
3497
|
-
|
|
3498
|
-
|
|
3499
|
-
|
|
3500
|
-
|
|
3501
|
-
|
|
3502
|
-
)
|
|
3503
|
-
: staticMetadataBase)
|
|
3555
|
+
(await profileRenderStep(profile, 'dynamic metadata routes', () =>
|
|
3556
|
+
withDynamicMetadataRoutes(
|
|
3557
|
+
staticMetadataBase,
|
|
3558
|
+
options.config.appPath,
|
|
3559
|
+
options.route.file,
|
|
3560
|
+
options.url.pathname,
|
|
3561
|
+
file => importModule(file, options),
|
|
3562
|
+
),
|
|
3563
|
+
))
|
|
3504
3564
|
const composeMetadata = (pageMeta: Metadata) => {
|
|
3505
3565
|
const merged = supersededSlotPage
|
|
3506
3566
|
? mergeMetadataEntries(layoutRender.metadata, slotMetadata.entries)
|
|
@@ -3614,17 +3674,18 @@ async function renderTree(
|
|
|
3614
3674
|
getDocumentScriptExtensions().stylesheetPrecedence(options.config),
|
|
3615
3675
|
)
|
|
3616
3676
|
const cssHrefStart = profileRenderMark(profile)
|
|
3617
|
-
|
|
3677
|
+
let globalStylesheet =
|
|
3618
3678
|
// global-not-found replaces the document without the root layout, so the
|
|
3619
3679
|
// root layout's global sheet must not load (its CSS ships in the route's
|
|
3620
3680
|
// own chunk, built with includeGlobalCss).
|
|
3621
3681
|
options.route.id === 'global-not-found' ? undefined : globalCssHref(options.config)
|
|
3622
|
-
|
|
3682
|
+
let routeStylesheet = routeCssHref(options.route, options.config)
|
|
3683
|
+
// File names, not logical ones: the inline path READS these off disk, so it has
|
|
3684
|
+
// to spell them the way the build emitted them (content-hashed in production).
|
|
3623
3685
|
const stylesheetAssets = [
|
|
3624
3686
|
...(globalStylesheet ? ['global.css'] : []),
|
|
3625
|
-
...(options.route
|
|
3626
|
-
|
|
3627
|
-
]
|
|
3687
|
+
...routeCssAssetNames(options.route),
|
|
3688
|
+
].map(name => emittedAssetName(options.config, name))
|
|
3628
3689
|
profileRenderSince(profile, 'css hrefs', cssHrefStart)
|
|
3629
3690
|
const inlineStylesheets = profileRenderSyncStep(profile, 'inline stylesheets', () =>
|
|
3630
3691
|
getCssExtensions().inlineStylesheets(options.config, {
|
|
@@ -3634,6 +3695,18 @@ async function renderTree(
|
|
|
3634
3695
|
...(nonce ? { nonce } : {}),
|
|
3635
3696
|
}),
|
|
3636
3697
|
)
|
|
3698
|
+
if (!inlineStylesheets && fonts.css) {
|
|
3699
|
+
const fontAsset = await profileRenderStep(profile, 'font stylesheet', () =>
|
|
3700
|
+
emitFontCssStylesheet(options.config, options.route.id, fonts.css, stylesheetAssets[0], {
|
|
3701
|
+
dev: Boolean(options.dev),
|
|
3702
|
+
}),
|
|
3703
|
+
)
|
|
3704
|
+
const fontHref = assetHref(options.config, fontAsset)
|
|
3705
|
+
if (globalStylesheet) globalStylesheet = fontHref
|
|
3706
|
+
else if (Array.isArray(routeStylesheet))
|
|
3707
|
+
routeStylesheet = [fontHref, ...routeStylesheet.slice(1)]
|
|
3708
|
+
else routeStylesheet = fontHref
|
|
3709
|
+
}
|
|
3637
3710
|
const stylesheetsStart = profileRenderMark(profile)
|
|
3638
3711
|
const stylesheets = (
|
|
3639
3712
|
inlineStylesheets
|
|
@@ -3672,9 +3745,11 @@ async function renderTree(
|
|
|
3672
3745
|
nextCompatEnabled(options.config) && !options.dev
|
|
3673
3746
|
? `<script src="${escapeHtml(withAssetPrefix(options.config, '/_next/static/pnext/_buildManifest.js'))}"></script>`
|
|
3674
3747
|
: ''
|
|
3748
|
+
// Inlined CSS is a stylesheet, not a script: it belongs at the stylesheet slot so the cascade
|
|
3749
|
+
// (and the emitted order) matches the linked path.
|
|
3750
|
+
const headStyles = inlineStylesheets?.join('\n') ?? ''
|
|
3675
3751
|
const headScripts = profileRenderSyncStep(profile, 'head scripts', () =>
|
|
3676
3752
|
[
|
|
3677
|
-
inlineStylesheets?.join('\n'),
|
|
3678
3753
|
renderCollectedHeadScripts(),
|
|
3679
3754
|
bootstrapScripts,
|
|
3680
3755
|
compatBuildManifestScript,
|
|
@@ -3730,25 +3805,23 @@ async function renderTree(
|
|
|
3730
3805
|
resourceHints,
|
|
3731
3806
|
staticMetadata,
|
|
3732
3807
|
fontPreloads: fonts.preloads,
|
|
3808
|
+
headStyles,
|
|
3733
3809
|
headScripts,
|
|
3734
3810
|
deferredScripts,
|
|
3735
3811
|
nonce,
|
|
3736
3812
|
propsScript: extra.propsScript ?? '',
|
|
3737
3813
|
routeScript,
|
|
3738
3814
|
clientScript,
|
|
3739
|
-
|
|
3740
|
-
// hydrates on `#pnext-page` itself, and islands are adopted through it. A route that ships the
|
|
3741
|
-
// entry only for the router runtime mounts nothing there.
|
|
3742
|
-
hasClientMounts: options.route.client || options.route.clientReferences.length > 0,
|
|
3815
|
+
hasClientMounts: pageSlotIsMountTarget(options),
|
|
3743
3816
|
devScript,
|
|
3744
3817
|
streamChunks: streamState.deferred,
|
|
3745
3818
|
lateChunks,
|
|
3746
3819
|
status: extra.status,
|
|
3747
3820
|
viewport,
|
|
3748
|
-
fontCss:
|
|
3821
|
+
fontCss: '',
|
|
3749
3822
|
...(bodyIconLinks ? { bodyIconLinks } : {}),
|
|
3750
3823
|
...(bodyMetadata ? { bodyMetadata } : {}),
|
|
3751
|
-
...(pendingPageMetadata ? {
|
|
3824
|
+
...(bodyMetadata || pendingPageMetadata ? { metadataInBody: true } : {}),
|
|
3752
3825
|
compatBodyScript: getDocumentScriptExtensions().documentBodyScripts(options.config),
|
|
3753
3826
|
// The shell's server-inserted HTML was drained when the stream shell
|
|
3754
3827
|
// finished (streamState.insertedHead); anything still registered is emitted
|
|
@@ -4246,7 +4319,10 @@ async function renderErrorPage(options: RenderOptions, error: unknown, stream: b
|
|
|
4246
4319
|
nextCompatEnabled(options.config),
|
|
4247
4320
|
)
|
|
4248
4321
|
const reset = errorResetCallback()
|
|
4249
|
-
return
|
|
4322
|
+
// Awaited inside the try on purpose: a bare `return` hands the rejection to
|
|
4323
|
+
// the caller, and an error boundary that fails to render answered with a
|
|
4324
|
+
// bodiless 500 instead of the generic error document.
|
|
4325
|
+
return await renderTree(
|
|
4250
4326
|
options,
|
|
4251
4327
|
h(
|
|
4252
4328
|
'div',
|
|
@@ -4377,6 +4453,22 @@ function resolveThrownError(error: unknown, compatNext: boolean): Error {
|
|
|
4377
4453
|
return wrapped ?? new Error(String(error))
|
|
4378
4454
|
}
|
|
4379
4455
|
|
|
4456
|
+
/**
|
|
4457
|
+
* The logged trace, always carrying the error's identity. Bun drops the `Name: message` header when
|
|
4458
|
+
* it source-maps a stack, so a bare frame list would log as `⨯ Error` with nothing to diagnose.
|
|
4459
|
+
*/
|
|
4460
|
+
export function errorLogTrace(error: Error): string {
|
|
4461
|
+
const name = error.name || 'Error'
|
|
4462
|
+
const header = error.message ? `${name}: ${error.message}` : name
|
|
4463
|
+
const stack = error.stack
|
|
4464
|
+
if (!stack) return header
|
|
4465
|
+
if (stack.startsWith(header)) return stack
|
|
4466
|
+
// Source-mapping keeps the name and drops `: <message>`, so a stack that opens with the bare name
|
|
4467
|
+
// is a header to complete, not one to prepend - matching on the name alone would lose the message.
|
|
4468
|
+
if (stack.startsWith(`${name}\n`)) return `${header}${stack.slice(name.length)}`
|
|
4469
|
+
return `${header}\n${stack}`
|
|
4470
|
+
}
|
|
4471
|
+
|
|
4380
4472
|
function logDevError(options: RenderOptions, error: Error, clientSsrError = false) {
|
|
4381
4473
|
const seen = isBuildError(error) ? loggedBuildErrors : loggedRuntimeErrors
|
|
4382
4474
|
if (seen.has(error)) return
|
|
@@ -4386,7 +4478,7 @@ function logDevError(options: RenderOptions, error: Error, clientSsrError = fals
|
|
|
4386
4478
|
// rejection guard and the start.ts top-level catch so whichever site fires
|
|
4387
4479
|
// first wins (dedupe-rsc-error-log asserts one log per error).
|
|
4388
4480
|
if (!markErrorLogged(error)) return
|
|
4389
|
-
const trace = error
|
|
4481
|
+
const trace = errorLogTrace(error)
|
|
4390
4482
|
if (clientSsrError) {
|
|
4391
4483
|
// A client-page SSR error is replayed in the browser (no digest attached),
|
|
4392
4484
|
// but Next's prod server log still prints it in the `⨯ <trace>` shape —
|
|
@@ -5153,6 +5245,7 @@ async function resolvePartialSuspense(
|
|
|
5153
5245
|
renderSuspenseReplacement(
|
|
5154
5246
|
String(id),
|
|
5155
5247
|
parts.join(''),
|
|
5248
|
+
state.options,
|
|
5156
5249
|
documentNonce(state.options.request),
|
|
5157
5250
|
dynamic,
|
|
5158
5251
|
),
|
|
@@ -5162,7 +5255,12 @@ async function resolvePartialSuspense(
|
|
|
5162
5255
|
const resolved = await resolveServerTree(node.props.children, opaque)
|
|
5163
5256
|
const html = await renderVNodeToString(h(Fragment, null, resolved), opaque)
|
|
5164
5257
|
partial.chunks.push(
|
|
5165
|
-
renderSuspenseReplacement(
|
|
5258
|
+
renderSuspenseReplacement(
|
|
5259
|
+
String(id),
|
|
5260
|
+
html,
|
|
5261
|
+
state.options,
|
|
5262
|
+
documentNonce(state.options.request),
|
|
5263
|
+
),
|
|
5166
5264
|
)
|
|
5167
5265
|
return null
|
|
5168
5266
|
}
|
|
@@ -5477,6 +5575,7 @@ function deferSuspenseBoundary(
|
|
|
5477
5575
|
renderSuspenseReplacement(
|
|
5478
5576
|
id,
|
|
5479
5577
|
await renderVNodeToString(h(Fragment, null, resolved), state),
|
|
5578
|
+
state.options,
|
|
5480
5579
|
nonce,
|
|
5481
5580
|
),
|
|
5482
5581
|
)
|
|
@@ -5487,12 +5586,18 @@ function deferSuspenseBoundary(
|
|
|
5487
5586
|
// the nearest not-found UI plus a noindex meta, or the redirect meta-refresh and
|
|
5488
5587
|
// location.replace script. Both ride the chunk's own markup rather than the sent head.
|
|
5489
5588
|
if (isNotFoundError(error)) {
|
|
5490
|
-
return renderSuspenseReplacement(
|
|
5589
|
+
return renderSuspenseReplacement(
|
|
5590
|
+
id,
|
|
5591
|
+
await renderStreamNotFoundBoundary(state),
|
|
5592
|
+
state.options,
|
|
5593
|
+
nonce,
|
|
5594
|
+
)
|
|
5491
5595
|
}
|
|
5492
5596
|
if (isRedirectError(error)) {
|
|
5493
5597
|
return renderSuspenseReplacement(
|
|
5494
5598
|
id,
|
|
5495
5599
|
metadataRedirectHtml(error.location, error.status),
|
|
5600
|
+
state.options,
|
|
5496
5601
|
nonce,
|
|
5497
5602
|
)
|
|
5498
5603
|
}
|
|
@@ -5513,6 +5618,7 @@ function deferSuspenseBoundary(
|
|
|
5513
5618
|
needsClientEntry(state.options.route),
|
|
5514
5619
|
),
|
|
5515
5620
|
),
|
|
5621
|
+
state.options,
|
|
5516
5622
|
nonce,
|
|
5517
5623
|
)
|
|
5518
5624
|
}),
|
|
@@ -5840,6 +5946,17 @@ function elementVNode(
|
|
|
5840
5946
|
return h(type as string, props, children)
|
|
5841
5947
|
}
|
|
5842
5948
|
|
|
5949
|
+
/** The one message every core server-action refusal uses; see SERVER_ACTIONS_UNSUPPORTED. */
|
|
5950
|
+
function serverActionUnsupported(options: RenderOptions): Error {
|
|
5951
|
+
return new Error(
|
|
5952
|
+
serverActionsUnsupportedMessage({
|
|
5953
|
+
file: options.route.serverActionFile ?? options.route.file,
|
|
5954
|
+
route: options.route.route,
|
|
5955
|
+
root: options.config.root,
|
|
5956
|
+
}),
|
|
5957
|
+
)
|
|
5958
|
+
}
|
|
5959
|
+
|
|
5843
5960
|
// Progressive enhancement for <form action={serverAction}>: render a real POST
|
|
5844
5961
|
// form back to the current page URL with the id in a hidden field, so a no-JS
|
|
5845
5962
|
// submit still dispatches (serveAction promotes the field to the next-action
|
|
@@ -5854,6 +5971,10 @@ function serverActionFormVNode(
|
|
|
5854
5971
|
const rest = { ...props }
|
|
5855
5972
|
delete rest.action
|
|
5856
5973
|
if (!state) return h('form', rest, children)
|
|
5974
|
+
// Core has no action registry and no dispatch endpoint, so there is nothing to
|
|
5975
|
+
// wire this form to. Dropping the prop rendered a form that silently submits a
|
|
5976
|
+
// GET to the current URL - undebuggable from the browser - so core refuses.
|
|
5977
|
+
if (!nextCompatEnabled(state.options.config)) throw serverActionUnsupported(state.options)
|
|
5857
5978
|
const enhanced = serverActionFormProps(props, state)
|
|
5858
5979
|
if (!enhanced) return h('form', rest, children)
|
|
5859
5980
|
return h('form', enhanced.props, enhanced.hidden, children)
|
|
@@ -5907,6 +6028,7 @@ function actionSubmitterVNode(
|
|
|
5907
6028
|
state: ActionSerializeState,
|
|
5908
6029
|
) {
|
|
5909
6030
|
const { formAction, ...rest } = props
|
|
6031
|
+
if (!nextCompatEnabled(state.options.config)) throw serverActionUnsupported(state.options)
|
|
5910
6032
|
const id = actionIdForFunction(formAction, state)
|
|
5911
6033
|
if (!id) return h(type, rest, children)
|
|
5912
6034
|
return h(
|
|
@@ -5922,6 +6044,25 @@ function actionSubmitterVNode(
|
|
|
5922
6044
|
)
|
|
5923
6045
|
}
|
|
5924
6046
|
|
|
6047
|
+
/**
|
|
6048
|
+
* Convention props the framework itself supplies as FUNCTIONS (error.tsx's
|
|
6049
|
+
* `reset`/`unstable_retry`) are documented API, so a `'use client'` boundary
|
|
6050
|
+
* must be able to receive them. They ride the same `$$pnextErrorReset` marker
|
|
6051
|
+
* compat writes; core reads it back in utils/serialize. Convention props are
|
|
6052
|
+
* always top level, so this is one typeof per prop and no copy when there is
|
|
6053
|
+
* nothing to rewrite.
|
|
6054
|
+
*/
|
|
6055
|
+
function serializeConventionProps(props: Record<string, unknown>): Record<string, unknown> {
|
|
6056
|
+
let out: Record<string, unknown> | undefined
|
|
6057
|
+
for (const [key, value] of Object.entries(props)) {
|
|
6058
|
+
if (typeof value !== 'function') continue
|
|
6059
|
+
if (!(value as { [PROP_ERROR_RESET_MARKER]?: unknown })[PROP_ERROR_RESET_MARKER]) continue
|
|
6060
|
+
out ??= { ...props }
|
|
6061
|
+
out[key] = { [PROP_ERROR_RESET_MARKER]: true }
|
|
6062
|
+
}
|
|
6063
|
+
return out ?? props
|
|
6064
|
+
}
|
|
6065
|
+
|
|
5925
6066
|
/**
|
|
5926
6067
|
* Replace function props (server actions handed to a client island) with the
|
|
5927
6068
|
* serializable action-reference marker the client runtime revives. Non-action
|
|
@@ -5931,7 +6072,7 @@ function serializeActionProps(
|
|
|
5931
6072
|
props: Record<string, unknown>,
|
|
5932
6073
|
state: ActionSerializeState,
|
|
5933
6074
|
): Record<string, unknown> {
|
|
5934
|
-
if (!nextCompatEnabled(state.options.config)) return props
|
|
6075
|
+
if (!nextCompatEnabled(state.options.config)) return serializeConventionProps(props)
|
|
5935
6076
|
// Taint check runs on the RAW props: serializeActionValue copies plain (and
|
|
5936
6077
|
// null-prototype) objects, which would drop the identity `process.env` is
|
|
5937
6078
|
// recognized by.
|
|
@@ -6417,7 +6558,13 @@ async function readRenderModuleMetadata(
|
|
|
6417
6558
|
}
|
|
6418
6559
|
}
|
|
6419
6560
|
|
|
6420
|
-
function renderSuspenseReplacement(
|
|
6561
|
+
function renderSuspenseReplacement(
|
|
6562
|
+
id: string,
|
|
6563
|
+
html: string,
|
|
6564
|
+
options: RenderOptions,
|
|
6565
|
+
nonce?: string,
|
|
6566
|
+
slots?: number[],
|
|
6567
|
+
) {
|
|
6421
6568
|
// Streamed Suspense content rides in a `<div hidden>` (React/Next's own
|
|
6422
6569
|
// streaming markup shape) rather than a `<template>`: tests (and tools like
|
|
6423
6570
|
// cheerio) assert streamed-but-unswapped content sits under a `[hidden]`
|
|
@@ -6427,7 +6574,11 @@ function renderSuspenseReplacement(id: string, html: string, nonce?: string, slo
|
|
|
6427
6574
|
// boundary's dynamic slots (comment-delimited), and promotion merges them with
|
|
6428
6575
|
// the static slots the shell's `<template data-pnext-static>` already holds.
|
|
6429
6576
|
const slotsAttribute = slots ? ` data-pnext-slots="${escapeHtml(slots.join(','))}"` : ''
|
|
6430
|
-
|
|
6577
|
+
// A page whose slot sits under a Suspense (loading.tsx, a streamed leaf) ships the slot HERE
|
|
6578
|
+
// rather than in the shell, so the same mount-target rule the document body applies has to run
|
|
6579
|
+
// on the tail: $RC grafts these bytes into the live DOM, and an inert wrapper would re-enter it.
|
|
6580
|
+
const body = unwrapUnusedPageSlot(html, streamPageSlotMode(options))
|
|
6581
|
+
return `<div hidden data-pnext-stream="${escapeHtml(id)}"${slotsAttribute}>${body}</div><script${nonceAttribute}>${suspenseReplaceScript(id)}</script>`
|
|
6431
6582
|
}
|
|
6432
6583
|
|
|
6433
6584
|
async function renderVNodeToString(vnode: VNode, state?: ActionSerializeState) {
|
|
@@ -6553,8 +6704,11 @@ async function renderSuspendingStreamShell(
|
|
|
6553
6704
|
const refresh = tail.includes('<preact-island')
|
|
6554
6705
|
? clientSuspenseRefreshScript(documentNonce(state.options.request))
|
|
6555
6706
|
: ''
|
|
6556
|
-
|
|
6557
|
-
|
|
6707
|
+
// Same mount-target rule as the out-of-order chunks: an inline-suspense tail carries the page
|
|
6708
|
+
// slot whenever the page itself suspended, and the graft lands it in the live DOM.
|
|
6709
|
+
return unwrapUnusedPageSlot(
|
|
6710
|
+
serializeNeutralClientIslands(`${inserted}${anchorInlineSuspenseHoles(tail)}${refresh}`),
|
|
6711
|
+
streamPageSlotMode(state.options),
|
|
6558
6712
|
)
|
|
6559
6713
|
}),
|
|
6560
6714
|
)
|
|
@@ -6586,7 +6740,12 @@ function clientSuspenseRefreshScript(nonce: string | undefined) {
|
|
|
6586
6740
|
// markers before this script - which streams in the deferred tail - can run, but the element
|
|
6587
6741
|
// survives adoption as the island's static children. Falls back to the comment markers when the
|
|
6588
6742
|
// element is absent.
|
|
6589
|
-
|
|
6743
|
+
//
|
|
6744
|
+
// The grafted content arrives AFTER the document-wide island bootstrap ran, so its islands are
|
|
6745
|
+
// still comment markers no scan can see: materialize and re-mount once the graft is done, the same
|
|
6746
|
+
// pair PROMO_RUNTIME runs for the `<pnext-suspense>` path. Without it every client component inside
|
|
6747
|
+
// a streamed Suspense boundary stays inert server HTML - no hydration, no effects, no data fetch.
|
|
6748
|
+
return `<script${nonceAttribute}>$RC=function(i,d){var h=document.querySelector('pnext-hole[data-pnext-hole="'+i+'"]');if(h){while(h.firstChild)h.removeChild(h.firstChild);while(d.firstChild)h.appendChild(d.firstChild);d.remove();return}var s,e,w=document.createTreeWalker(document,128);while(w.nextNode()){var n=w.currentNode;if(n.data==='$s:'+i)s=n;else if(n.data==='/$s:'+i)e=n;if(s&&e)break}if(!s||!e)return;var p=e.previousSibling;while(p&&p!==s){var x=p;p=p.previousSibling;x.remove()}while(d.firstChild)s.after(d.firstChild),s=s.nextSibling;d.remove()};document.querySelectorAll('preact-island[data-target]').forEach(function(d){$RC(d.getAttribute('data-target'),d)});window.__PNEXT_MATERIALIZE_CLIENT_ISLANDS__&&window.__PNEXT_MATERIALIZE_CLIENT_ISLANDS__(document);window.__PNEXT_MOUNT_ISLANDS__&&window.__PNEXT_MOUNT_ISLANDS__()</script>`
|
|
6590
6749
|
}
|
|
6591
6750
|
|
|
6592
6751
|
/**
|
|
@@ -6601,11 +6760,16 @@ function islandMarkerBootstrapTag(mode: PageSlotMarkerMode): string {
|
|
|
6601
6760
|
|
|
6602
6761
|
/**
|
|
6603
6762
|
* Elements-as-final leaves `div#pnext-page` in the document, but nothing mounts on it unless the
|
|
6604
|
-
* slot itself hydrates - and an inert wrapper would break the flat body children
|
|
6605
|
-
*
|
|
6763
|
+
* slot itself hydrates - and an inert wrapper would break the flat body children Next emits.
|
|
6764
|
+
* Elements wire format: unwrap it. Comment wire format: empty the marker's attribute payload, which
|
|
6765
|
+
* every materializer already reads as "no element to build" - so the slot stays a pure anchor pair
|
|
6766
|
+
* on first paint AND on every soft navigation that lands on this document.
|
|
6606
6767
|
*/
|
|
6607
6768
|
function unwrapUnusedPageSlot(html: string, mode: PageSlotMarkerMode): string {
|
|
6608
|
-
if (mode === 'materialize'
|
|
6769
|
+
if (mode === 'materialize') return html
|
|
6770
|
+
if (getRenderExtensions().islandCommentWireFormat()) {
|
|
6771
|
+
return html.replace(/<!--pnext-page:[^>]*-->/, '<!--pnext-page:-->')
|
|
6772
|
+
}
|
|
6609
6773
|
const slot = pageSlotSpan(html)
|
|
6610
6774
|
return slot ? html.slice(0, slot.start) + slot.children + html.slice(slot.end) : html
|
|
6611
6775
|
}
|
|
@@ -6671,22 +6835,42 @@ function pageSlotSpan(html: string) {
|
|
|
6671
6835
|
* against the live DOM, so `div#pnext-page` may only be rebuilt when something actually mounts on
|
|
6672
6836
|
* it:
|
|
6673
6837
|
*
|
|
6674
|
-
* - `materialize`: a whole-page client route hydrates on the slot, or
|
|
6675
|
-
* it.
|
|
6676
|
-
* - `keep`:
|
|
6677
|
-
* but the comment anchors stay
|
|
6678
|
-
* just as well as from the element.
|
|
6838
|
+
* - `materialize`: a whole-page client route hydrates on the slot, or a client ROOT layout
|
|
6839
|
+
* re-renders the page through it.
|
|
6840
|
+
* - `keep`: nothing mounts on the slot - the route ships the entry for islands and/or the router
|
|
6841
|
+
* runtime only. The wrapper is not rebuilt, but the comment anchors stay: the page-slot
|
|
6842
|
+
* machinery locates the slot from the comment pair just as well as from the element.
|
|
6679
6843
|
* - `drop`: no client runtime at all, so nothing will ever read the anchors.
|
|
6680
6844
|
*/
|
|
6681
6845
|
type PageSlotMarkerMode = 'materialize' | 'keep' | 'drop'
|
|
6682
6846
|
|
|
6683
|
-
|
|
6847
|
+
/**
|
|
6848
|
+
* Whether anything client-side ever mounts INTO the page slot. Only two things do: a whole-page
|
|
6849
|
+
* `'use client'` route (it hydrates on `#pnext-page` itself) and a `'use client'` ROOT layout (the
|
|
6850
|
+
* entry's client shell re-renders the document through it and reads the slot as its children).
|
|
6851
|
+
* ISLANDS DO NOT - each mounts on its own `pnext-client` host - so an island-only route keeps Next's
|
|
6852
|
+
* wrapper-free body. Mirrors client/build.ts `hasClientRootLayout`, which decides whether the entry
|
|
6853
|
+
* carries the shell path at all.
|
|
6854
|
+
*/
|
|
6855
|
+
function pageSlotIsMountTarget(options: RenderOptions): boolean {
|
|
6856
|
+
if (options.route.client) return true
|
|
6857
|
+
const rootLayout = (options.layoutFiles ??
|
|
6858
|
+
findLayouts(options.config.appPath, options.route.file))[0]
|
|
6859
|
+
return Boolean(rootLayout && options.route.clientReferences.some(ref => ref.file === rootLayout))
|
|
6860
|
+
}
|
|
6861
|
+
|
|
6862
|
+
function pageSlotMarkerMode(page: DocumentPage): PageSlotMarkerMode {
|
|
6684
6863
|
if (!page.clientScript) return 'drop'
|
|
6685
|
-
|
|
6686
|
-
|
|
6687
|
-
|
|
6688
|
-
|
|
6689
|
-
|
|
6864
|
+
return page.hasClientMounts ? 'materialize' : 'keep'
|
|
6865
|
+
}
|
|
6866
|
+
|
|
6867
|
+
/**
|
|
6868
|
+
* The same mode for a streamed tail chunk, which is assembled before the document knows whether it
|
|
6869
|
+
* ships a client script. `keep` and `drop` dissolve the slot identically here (only the bootstrap
|
|
6870
|
+
* script distinguishes them), so the mount-target question is the whole decision.
|
|
6871
|
+
*/
|
|
6872
|
+
function streamPageSlotMode(options: RenderOptions): PageSlotMarkerMode {
|
|
6873
|
+
return pageSlotIsMountTarget(options) ? 'materialize' : 'keep'
|
|
6690
6874
|
}
|
|
6691
6875
|
|
|
6692
6876
|
function clientIslandMarkerBootstrapSource(mode: PageSlotMarkerMode = 'materialize') {
|
|
@@ -6747,45 +6931,48 @@ function hStreamError(
|
|
|
6747
6931
|
hasClientRuntime = false,
|
|
6748
6932
|
): VNode<any> {
|
|
6749
6933
|
const resolvedError = resolveThrownError(error, compatNext)
|
|
6750
|
-
|
|
6751
|
-
|
|
6752
|
-
|
|
6753
|
-
|
|
6754
|
-
|
|
6755
|
-
|
|
6756
|
-
|
|
6757
|
-
|
|
6758
|
-
|
|
6759
|
-
|
|
6760
|
-
|
|
6761
|
-
|
|
6762
|
-
|
|
6763
|
-
|
|
6764
|
-
? {
|
|
6765
|
-
'data-pnext-error-message': serialized.message,
|
|
6766
|
-
...(serialized.digest ? { 'data-pnext-error-digest': serialized.digest } : {}),
|
|
6767
|
-
}
|
|
6768
|
-
: {}),
|
|
6769
|
-
},
|
|
6770
|
-
(compatNext
|
|
6934
|
+
// Compat: carry the serialized boundary error (redacted message + digest)
|
|
6935
|
+
// on the marker so the client error runtime can replace this fallback with
|
|
6936
|
+
// the route's error.js boundary (Next renders the nearest error boundary
|
|
6937
|
+
// for a server error inside a streamed Suspense hole; its unstable_retry
|
|
6938
|
+
// soft-refreshes and recovers). Core keeps the plain generic marker.
|
|
6939
|
+
const serialized = compatNext
|
|
6940
|
+
? getRenderExtensions().serializeError({ error: resolvedError, dev })
|
|
6941
|
+
: undefined
|
|
6942
|
+
// The marker's own content is what stays on screen when nothing escalates it. Dev keeps the
|
|
6943
|
+
// diagnostic card there - Next renders the error boundary in dev too, so the marker must be emitted
|
|
6944
|
+
// either way; substituting the card for the marker stranded the route's error.js.
|
|
6945
|
+
const fallback = dev
|
|
6946
|
+
? genericErrorTree(resolvedError, true, compatNext)
|
|
6947
|
+
: ((compatNext
|
|
6771
6948
|
? getRenderExtensions().genericErrorTitle({ error: resolvedError, dev })
|
|
6772
|
-
: undefined) ?? 'Application error'
|
|
6773
|
-
|
|
6774
|
-
|
|
6775
|
-
|
|
6776
|
-
|
|
6777
|
-
|
|
6778
|
-
|
|
6779
|
-
|
|
6780
|
-
|
|
6781
|
-
|
|
6782
|
-
|
|
6783
|
-
|
|
6784
|
-
|
|
6785
|
-
|
|
6786
|
-
|
|
6787
|
-
|
|
6788
|
-
|
|
6949
|
+
: undefined) ?? 'Application error')
|
|
6950
|
+
const marker = h(
|
|
6951
|
+
'pnext-error',
|
|
6952
|
+
{
|
|
6953
|
+
'data-pnext-stream-error': true,
|
|
6954
|
+
...(serialized
|
|
6955
|
+
? {
|
|
6956
|
+
'data-pnext-error-message': serialized.message,
|
|
6957
|
+
...(serialized.digest ? { 'data-pnext-error-digest': serialized.digest } : {}),
|
|
6958
|
+
}
|
|
6959
|
+
: {}),
|
|
6960
|
+
},
|
|
6961
|
+
fallback,
|
|
6962
|
+
)
|
|
6963
|
+
// A page with no client components ships no error runtime, so nothing can escalate the marker
|
|
6964
|
+
// client-side. Emit a self-contained fallback that swaps in the built-in global-error document
|
|
6965
|
+
// unless the runtime announces itself (__PNEXT_STREAM_ERROR_RUNTIME__), in which case it owns
|
|
6966
|
+
// the marker instead. Dev needs none: its card is already the marker's visible content.
|
|
6967
|
+
const fallbackScript =
|
|
6968
|
+
!dev && compatNext && !hasClientRuntime ? streamErrorDocumentScript(serialized) : undefined
|
|
6969
|
+
if (!fallbackScript) return marker
|
|
6970
|
+
return h(
|
|
6971
|
+
Fragment,
|
|
6972
|
+
null,
|
|
6973
|
+
marker,
|
|
6974
|
+
h('script', { dangerouslySetInnerHTML: { __html: fallbackScript } }),
|
|
6975
|
+
)
|
|
6789
6976
|
}
|
|
6790
6977
|
|
|
6791
6978
|
// Inline bootstrap flushed after a stream-error marker: replaces the document
|
|
@@ -6982,19 +7169,18 @@ async function applyLayouts(
|
|
|
6982
7169
|
// postpone its boundary too (not read an empty object). Reuse the page-level
|
|
6983
7170
|
// hanging params promise so the whole shell is param-independent.
|
|
6984
7171
|
if (options.fallbackParams) return { ...layoutProps, params: hangingPromise('params') }
|
|
7172
|
+
// The scoped params go through the vary-tracking extension under the LAYOUT
|
|
7173
|
+
// segment kind: compat keys the layout's cache frame on the params this
|
|
7174
|
+
// layout reads, independently of the page's own set. Outside a tracking
|
|
7175
|
+
// scope the extension is the identity, so ordinary renders pay nothing (see
|
|
7176
|
+
// compat/segment/vary-params.ts).
|
|
7177
|
+
const scopedParams = getRenderExtensions().trackVaryParams(
|
|
7178
|
+
layoutParamsForDir(options.config.appPath, dirname(layoutFile), options.params ?? {}),
|
|
7179
|
+
{ kind: 'params', segment: 'layout' },
|
|
7180
|
+
)
|
|
6985
7181
|
return {
|
|
6986
7182
|
...layoutProps,
|
|
6987
|
-
|
|
6988
|
-
// LAYOUT segment kind: compat keys the layout's cache frame on the params
|
|
6989
|
-
// this layout reads, independently of the page's own set. Outside a
|
|
6990
|
-
// tracking scope the extension is the identity, so ordinary renders pay
|
|
6991
|
-
// nothing (see compat/segment/vary-params.ts).
|
|
6992
|
-
params: Promise.resolve(
|
|
6993
|
-
getRenderExtensions().trackVaryParams(
|
|
6994
|
-
layoutParamsForDir(options.config.appPath, dirname(layoutFile), options.params ?? {}),
|
|
6995
|
-
{ kind: 'params', segment: 'layout' },
|
|
6996
|
-
),
|
|
6997
|
-
),
|
|
7183
|
+
params: legacySyncProps(Promise.resolve(scopedParams), 'params', scopedParams),
|
|
6998
7184
|
}
|
|
6999
7185
|
}
|
|
7000
7186
|
|