@wular/pnext 0.0.19 → 0.0.21
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -2
- package/package.json +1 -1
- package/reference/compat.md +2 -2
- package/reference/getting-started.md +2 -2
- package/src/cli/adapters/vercel.ts +27 -0
- package/src/cli/analyze.ts +8 -7
- package/src/cli/boot/trace.ts +9 -3
- package/src/cli/build/cache.ts +324 -0
- package/src/cli/build.ts +42 -18
- package/src/cli/create.ts +4 -4
- package/src/cli/dev.ts +23 -0
- package/src/cli/migrate/package-json.ts +3 -2
- package/src/cli/migrate/report.ts +1 -1
- package/src/cli/migrate/run.ts +2 -2
- package/src/cli/migrate/scan.ts +128 -1
- package/src/cli/serve/pipeline.ts +42 -42
- package/src/cli/start.ts +20 -20
- package/src/client/build.ts +64 -35
- package/src/client/entry.ts +228 -16
- package/src/client/router/page-slot.ts +81 -0
- package/src/client/router/runtime.ts +107 -112
- package/src/compat/bundler/cjs-exports.ts +26 -20
- package/src/compat/client/css-order.ts +1 -1
- package/src/compat/client/navigation-scroll.ts +36 -40
- package/src/compat/css/modules.ts +6 -2
- package/src/compat/metadata.ts +0 -9
- package/src/compat/next/font/cache.ts +10 -22
- package/src/compat/next/font/runtime.ts +20 -0
- package/src/compat/next/legacy-request-apis.ts +41 -5
- package/src/compat/next/script.tsx +16 -24
- package/src/compat/react/client-lite.ts +1 -0
- package/src/compat/react/client.ts +11 -0
- package/src/compat/react/dom-client.ts +11 -2
- package/src/compat/react/dom-react-server.ts +4 -0
- package/src/compat/react/dom.ts +11 -2
- package/src/compat/react/internals.ts +65 -0
- package/src/compat/react/parity.ts +47 -0
- package/src/compat/react/preact.ts +31 -1
- package/src/compat/react/react-server.ts +7 -0
- package/src/compat/react/server.ts +16 -2
- package/src/compat/react/style-values.ts +68 -0
- package/src/compat/register/proxy.ts +3 -1
- package/src/compat/register/render.ts +14 -3
- package/src/compat/register/routing.ts +57 -7
- package/src/compat/register/typed-routes.ts +0 -2
- package/src/compat/tsconfig-defaults.ts +4 -0
- package/src/compat/typecheck/check.ts +26 -71
- package/src/compat/typecheck/worker.ts +3 -2
- package/src/compat/validation/validate.ts +2 -0
- package/src/compat-bootstrap.ts +4 -0
- package/src/css/build.ts +153 -14
- package/src/css/postcss.ts +32 -3
- package/src/dev/restart/client-key.ts +1 -1
- package/src/dev/restart/global-css.ts +12 -94
- package/src/dev/restart/record.ts +106 -0
- package/src/dev/restart/route-facts.ts +11 -90
- package/src/dev/server.ts +208 -41
- package/src/extensions.ts +69 -3
- package/src/render/hooks.ts +1 -1
- package/src/render/metadata.ts +89 -17
- package/src/render/renderer.ts +325 -138
- package/src/render/resource-hints.ts +62 -9
- package/src/render/slots.tsx +2 -1
- package/src/resolve/scan-facts.ts +13 -5
- package/src/routing/forwarded.ts +20 -7
- package/src/routing/handler.ts +6 -2
- package/src/routing/href.ts +11 -1
- package/src/routing/metadata-files.ts +16 -3
- package/src/routing/proxy.ts +6 -3
- package/src/routing/routes.ts +253 -34
- package/src/runtime/fetch-host.ts +42 -0
- package/src/runtime/fingerprint.ts +132 -0
- package/src/runtime/loader.ts +7 -5
- package/src/runtime/module-cache.ts +3 -36
- package/src/runtime/modules.ts +333 -77
- package/src/runtime/vendor-build.ts +34 -5
- package/src/runtime/vendor.ts +1 -18
- package/src/types.ts +6 -0
- package/src/utils/asset-hash.ts +22 -0
- package/src/utils/code.ts +41 -0
- package/src/utils/html.ts +4 -1
- package/src/utils/registry.ts +70 -0
- package/src/utils/serialize.ts +66 -7
package/src/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,7 +4453,24 @@ 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) {
|
|
4473
|
+
if (error.name === 'DynamicServerError') return
|
|
4381
4474
|
const seen = isBuildError(error) ? loggedBuildErrors : loggedRuntimeErrors
|
|
4382
4475
|
if (seen.has(error)) return
|
|
4383
4476
|
seen.add(error)
|
|
@@ -4386,7 +4479,7 @@ function logDevError(options: RenderOptions, error: Error, clientSsrError = fals
|
|
|
4386
4479
|
// rejection guard and the start.ts top-level catch so whichever site fires
|
|
4387
4480
|
// first wins (dedupe-rsc-error-log asserts one log per error).
|
|
4388
4481
|
if (!markErrorLogged(error)) return
|
|
4389
|
-
const trace = error
|
|
4482
|
+
const trace = errorLogTrace(error)
|
|
4390
4483
|
if (clientSsrError) {
|
|
4391
4484
|
// A client-page SSR error is replayed in the browser (no digest attached),
|
|
4392
4485
|
// but Next's prod server log still prints it in the `⨯ <trace>` shape —
|
|
@@ -5153,6 +5246,7 @@ async function resolvePartialSuspense(
|
|
|
5153
5246
|
renderSuspenseReplacement(
|
|
5154
5247
|
String(id),
|
|
5155
5248
|
parts.join(''),
|
|
5249
|
+
state.options,
|
|
5156
5250
|
documentNonce(state.options.request),
|
|
5157
5251
|
dynamic,
|
|
5158
5252
|
),
|
|
@@ -5162,7 +5256,12 @@ async function resolvePartialSuspense(
|
|
|
5162
5256
|
const resolved = await resolveServerTree(node.props.children, opaque)
|
|
5163
5257
|
const html = await renderVNodeToString(h(Fragment, null, resolved), opaque)
|
|
5164
5258
|
partial.chunks.push(
|
|
5165
|
-
renderSuspenseReplacement(
|
|
5259
|
+
renderSuspenseReplacement(
|
|
5260
|
+
String(id),
|
|
5261
|
+
html,
|
|
5262
|
+
state.options,
|
|
5263
|
+
documentNonce(state.options.request),
|
|
5264
|
+
),
|
|
5166
5265
|
)
|
|
5167
5266
|
return null
|
|
5168
5267
|
}
|
|
@@ -5477,6 +5576,7 @@ function deferSuspenseBoundary(
|
|
|
5477
5576
|
renderSuspenseReplacement(
|
|
5478
5577
|
id,
|
|
5479
5578
|
await renderVNodeToString(h(Fragment, null, resolved), state),
|
|
5579
|
+
state.options,
|
|
5480
5580
|
nonce,
|
|
5481
5581
|
),
|
|
5482
5582
|
)
|
|
@@ -5487,12 +5587,18 @@ function deferSuspenseBoundary(
|
|
|
5487
5587
|
// the nearest not-found UI plus a noindex meta, or the redirect meta-refresh and
|
|
5488
5588
|
// location.replace script. Both ride the chunk's own markup rather than the sent head.
|
|
5489
5589
|
if (isNotFoundError(error)) {
|
|
5490
|
-
return renderSuspenseReplacement(
|
|
5590
|
+
return renderSuspenseReplacement(
|
|
5591
|
+
id,
|
|
5592
|
+
await renderStreamNotFoundBoundary(state),
|
|
5593
|
+
state.options,
|
|
5594
|
+
nonce,
|
|
5595
|
+
)
|
|
5491
5596
|
}
|
|
5492
5597
|
if (isRedirectError(error)) {
|
|
5493
5598
|
return renderSuspenseReplacement(
|
|
5494
5599
|
id,
|
|
5495
5600
|
metadataRedirectHtml(error.location, error.status),
|
|
5601
|
+
state.options,
|
|
5496
5602
|
nonce,
|
|
5497
5603
|
)
|
|
5498
5604
|
}
|
|
@@ -5513,6 +5619,7 @@ function deferSuspenseBoundary(
|
|
|
5513
5619
|
needsClientEntry(state.options.route),
|
|
5514
5620
|
),
|
|
5515
5621
|
),
|
|
5622
|
+
state.options,
|
|
5516
5623
|
nonce,
|
|
5517
5624
|
)
|
|
5518
5625
|
}),
|
|
@@ -5840,6 +5947,17 @@ function elementVNode(
|
|
|
5840
5947
|
return h(type as string, props, children)
|
|
5841
5948
|
}
|
|
5842
5949
|
|
|
5950
|
+
/** The one message every core server-action refusal uses; see SERVER_ACTIONS_UNSUPPORTED. */
|
|
5951
|
+
function serverActionUnsupported(options: RenderOptions): Error {
|
|
5952
|
+
return new Error(
|
|
5953
|
+
serverActionsUnsupportedMessage({
|
|
5954
|
+
file: options.route.serverActionFile ?? options.route.file,
|
|
5955
|
+
route: options.route.route,
|
|
5956
|
+
root: options.config.root,
|
|
5957
|
+
}),
|
|
5958
|
+
)
|
|
5959
|
+
}
|
|
5960
|
+
|
|
5843
5961
|
// Progressive enhancement for <form action={serverAction}>: render a real POST
|
|
5844
5962
|
// form back to the current page URL with the id in a hidden field, so a no-JS
|
|
5845
5963
|
// submit still dispatches (serveAction promotes the field to the next-action
|
|
@@ -5854,6 +5972,10 @@ function serverActionFormVNode(
|
|
|
5854
5972
|
const rest = { ...props }
|
|
5855
5973
|
delete rest.action
|
|
5856
5974
|
if (!state) return h('form', rest, children)
|
|
5975
|
+
// Core has no action registry and no dispatch endpoint, so there is nothing to
|
|
5976
|
+
// wire this form to. Dropping the prop rendered a form that silently submits a
|
|
5977
|
+
// GET to the current URL - undebuggable from the browser - so core refuses.
|
|
5978
|
+
if (!nextCompatEnabled(state.options.config)) throw serverActionUnsupported(state.options)
|
|
5857
5979
|
const enhanced = serverActionFormProps(props, state)
|
|
5858
5980
|
if (!enhanced) return h('form', rest, children)
|
|
5859
5981
|
return h('form', enhanced.props, enhanced.hidden, children)
|
|
@@ -5907,6 +6029,7 @@ function actionSubmitterVNode(
|
|
|
5907
6029
|
state: ActionSerializeState,
|
|
5908
6030
|
) {
|
|
5909
6031
|
const { formAction, ...rest } = props
|
|
6032
|
+
if (!nextCompatEnabled(state.options.config)) throw serverActionUnsupported(state.options)
|
|
5910
6033
|
const id = actionIdForFunction(formAction, state)
|
|
5911
6034
|
if (!id) return h(type, rest, children)
|
|
5912
6035
|
return h(
|
|
@@ -5922,6 +6045,25 @@ function actionSubmitterVNode(
|
|
|
5922
6045
|
)
|
|
5923
6046
|
}
|
|
5924
6047
|
|
|
6048
|
+
/**
|
|
6049
|
+
* Convention props the framework itself supplies as FUNCTIONS (error.tsx's
|
|
6050
|
+
* `reset`/`unstable_retry`) are documented API, so a `'use client'` boundary
|
|
6051
|
+
* must be able to receive them. They ride the same `$$pnextErrorReset` marker
|
|
6052
|
+
* compat writes; core reads it back in utils/serialize. Convention props are
|
|
6053
|
+
* always top level, so this is one typeof per prop and no copy when there is
|
|
6054
|
+
* nothing to rewrite.
|
|
6055
|
+
*/
|
|
6056
|
+
function serializeConventionProps(props: Record<string, unknown>): Record<string, unknown> {
|
|
6057
|
+
let out: Record<string, unknown> | undefined
|
|
6058
|
+
for (const [key, value] of Object.entries(props)) {
|
|
6059
|
+
if (typeof value !== 'function') continue
|
|
6060
|
+
if (!(value as { [PROP_ERROR_RESET_MARKER]?: unknown })[PROP_ERROR_RESET_MARKER]) continue
|
|
6061
|
+
out ??= { ...props }
|
|
6062
|
+
out[key] = { [PROP_ERROR_RESET_MARKER]: true }
|
|
6063
|
+
}
|
|
6064
|
+
return out ?? props
|
|
6065
|
+
}
|
|
6066
|
+
|
|
5925
6067
|
/**
|
|
5926
6068
|
* Replace function props (server actions handed to a client island) with the
|
|
5927
6069
|
* serializable action-reference marker the client runtime revives. Non-action
|
|
@@ -5931,7 +6073,7 @@ function serializeActionProps(
|
|
|
5931
6073
|
props: Record<string, unknown>,
|
|
5932
6074
|
state: ActionSerializeState,
|
|
5933
6075
|
): Record<string, unknown> {
|
|
5934
|
-
if (!nextCompatEnabled(state.options.config)) return props
|
|
6076
|
+
if (!nextCompatEnabled(state.options.config)) return serializeConventionProps(props)
|
|
5935
6077
|
// Taint check runs on the RAW props: serializeActionValue copies plain (and
|
|
5936
6078
|
// null-prototype) objects, which would drop the identity `process.env` is
|
|
5937
6079
|
// recognized by.
|
|
@@ -6417,7 +6559,13 @@ async function readRenderModuleMetadata(
|
|
|
6417
6559
|
}
|
|
6418
6560
|
}
|
|
6419
6561
|
|
|
6420
|
-
function renderSuspenseReplacement(
|
|
6562
|
+
function renderSuspenseReplacement(
|
|
6563
|
+
id: string,
|
|
6564
|
+
html: string,
|
|
6565
|
+
options: RenderOptions,
|
|
6566
|
+
nonce?: string,
|
|
6567
|
+
slots?: number[],
|
|
6568
|
+
) {
|
|
6421
6569
|
// Streamed Suspense content rides in a `<div hidden>` (React/Next's own
|
|
6422
6570
|
// streaming markup shape) rather than a `<template>`: tests (and tools like
|
|
6423
6571
|
// cheerio) assert streamed-but-unswapped content sits under a `[hidden]`
|
|
@@ -6427,7 +6575,11 @@ function renderSuspenseReplacement(id: string, html: string, nonce?: string, slo
|
|
|
6427
6575
|
// boundary's dynamic slots (comment-delimited), and promotion merges them with
|
|
6428
6576
|
// the static slots the shell's `<template data-pnext-static>` already holds.
|
|
6429
6577
|
const slotsAttribute = slots ? ` data-pnext-slots="${escapeHtml(slots.join(','))}"` : ''
|
|
6430
|
-
|
|
6578
|
+
// A page whose slot sits under a Suspense (loading.tsx, a streamed leaf) ships the slot HERE
|
|
6579
|
+
// rather than in the shell, so the same mount-target rule the document body applies has to run
|
|
6580
|
+
// on the tail: $RC grafts these bytes into the live DOM, and an inert wrapper would re-enter it.
|
|
6581
|
+
const body = unwrapUnusedPageSlot(html, streamPageSlotMode(options))
|
|
6582
|
+
return `<div hidden data-pnext-stream="${escapeHtml(id)}"${slotsAttribute}>${body}</div><script${nonceAttribute}>${suspenseReplaceScript(id)}</script>`
|
|
6431
6583
|
}
|
|
6432
6584
|
|
|
6433
6585
|
async function renderVNodeToString(vnode: VNode, state?: ActionSerializeState) {
|
|
@@ -6553,8 +6705,11 @@ async function renderSuspendingStreamShell(
|
|
|
6553
6705
|
const refresh = tail.includes('<preact-island')
|
|
6554
6706
|
? clientSuspenseRefreshScript(documentNonce(state.options.request))
|
|
6555
6707
|
: ''
|
|
6556
|
-
|
|
6557
|
-
|
|
6708
|
+
// Same mount-target rule as the out-of-order chunks: an inline-suspense tail carries the page
|
|
6709
|
+
// slot whenever the page itself suspended, and the graft lands it in the live DOM.
|
|
6710
|
+
return unwrapUnusedPageSlot(
|
|
6711
|
+
serializeNeutralClientIslands(`${inserted}${anchorInlineSuspenseHoles(tail)}${refresh}`),
|
|
6712
|
+
streamPageSlotMode(state.options),
|
|
6558
6713
|
)
|
|
6559
6714
|
}),
|
|
6560
6715
|
)
|
|
@@ -6579,14 +6734,19 @@ function anchorInlineSuspenseHoles(html: string): string {
|
|
|
6579
6734
|
.replace(/<!--\/\$s:[^>]+?-->/g, '</pnext-hole>')
|
|
6580
6735
|
}
|
|
6581
6736
|
|
|
6582
|
-
function clientSuspenseRefreshScript(nonce: string | undefined) {
|
|
6737
|
+
export function clientSuspenseRefreshScript(nonce: string | undefined) {
|
|
6583
6738
|
const nonceAttribute = nonce ? ` nonce="${escapeHtml(nonce)}"` : ''
|
|
6584
6739
|
// Graft each streamed island's content over its hole. The hole is a `<pnext-hole>` element rather
|
|
6585
6740
|
// than preact's `$s:ID` comment markers: hydration of an ancestor client layout removes comment
|
|
6586
6741
|
// markers before this script - which streams in the deferred tail - can run, but the element
|
|
6587
6742
|
// survives adoption as the island's static children. Falls back to the comment markers when the
|
|
6588
6743
|
// element is absent.
|
|
6589
|
-
|
|
6744
|
+
//
|
|
6745
|
+
// The grafted content arrives AFTER the document-wide island bootstrap ran, so its islands are
|
|
6746
|
+
// still comment markers no scan can see: materialize and re-mount once the graft is done, the same
|
|
6747
|
+
// pair PROMO_RUNTIME runs for the `<pnext-suspense>` path. Without it every client component inside
|
|
6748
|
+
// a streamed Suspense boundary stays inert server HTML - no hydration, no effects, no data fetch.
|
|
6749
|
+
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
6750
|
}
|
|
6591
6751
|
|
|
6592
6752
|
/**
|
|
@@ -6601,11 +6761,16 @@ function islandMarkerBootstrapTag(mode: PageSlotMarkerMode): string {
|
|
|
6601
6761
|
|
|
6602
6762
|
/**
|
|
6603
6763
|
* 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
|
-
*
|
|
6764
|
+
* slot itself hydrates - and an inert wrapper would break the flat body children Next emits.
|
|
6765
|
+
* Elements wire format: unwrap it. Comment wire format: empty the marker's attribute payload, which
|
|
6766
|
+
* every materializer already reads as "no element to build" - so the slot stays a pure anchor pair
|
|
6767
|
+
* on first paint AND on every soft navigation that lands on this document.
|
|
6606
6768
|
*/
|
|
6607
6769
|
function unwrapUnusedPageSlot(html: string, mode: PageSlotMarkerMode): string {
|
|
6608
|
-
if (mode === 'materialize'
|
|
6770
|
+
if (mode === 'materialize') return html
|
|
6771
|
+
if (getRenderExtensions().islandCommentWireFormat()) {
|
|
6772
|
+
return html.replace(/<!--pnext-page:[^>]*-->/, '<!--pnext-page:-->')
|
|
6773
|
+
}
|
|
6609
6774
|
const slot = pageSlotSpan(html)
|
|
6610
6775
|
return slot ? html.slice(0, slot.start) + slot.children + html.slice(slot.end) : html
|
|
6611
6776
|
}
|
|
@@ -6671,25 +6836,45 @@ function pageSlotSpan(html: string) {
|
|
|
6671
6836
|
* against the live DOM, so `div#pnext-page` may only be rebuilt when something actually mounts on
|
|
6672
6837
|
* it:
|
|
6673
6838
|
*
|
|
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.
|
|
6839
|
+
* - `materialize`: a whole-page client route hydrates on the slot, or a client ROOT layout
|
|
6840
|
+
* re-renders the page through it.
|
|
6841
|
+
* - `keep`: nothing mounts on the slot - the route ships the entry for islands and/or the router
|
|
6842
|
+
* runtime only. The wrapper is not rebuilt, but the comment anchors stay: the page-slot
|
|
6843
|
+
* machinery locates the slot from the comment pair just as well as from the element.
|
|
6679
6844
|
* - `drop`: no client runtime at all, so nothing will ever read the anchors.
|
|
6680
6845
|
*/
|
|
6681
6846
|
type PageSlotMarkerMode = 'materialize' | 'keep' | 'drop'
|
|
6682
6847
|
|
|
6683
|
-
|
|
6848
|
+
/**
|
|
6849
|
+
* Whether anything client-side ever mounts INTO the page slot. Only two things do: a whole-page
|
|
6850
|
+
* `'use client'` route (it hydrates on `#pnext-page` itself) and a `'use client'` ROOT layout (the
|
|
6851
|
+
* entry's client shell re-renders the document through it and reads the slot as its children).
|
|
6852
|
+
* ISLANDS DO NOT - each mounts on its own `pnext-client` host - so an island-only route keeps Next's
|
|
6853
|
+
* wrapper-free body. Mirrors client/build.ts `hasClientRootLayout`, which decides whether the entry
|
|
6854
|
+
* carries the shell path at all.
|
|
6855
|
+
*/
|
|
6856
|
+
function pageSlotIsMountTarget(options: RenderOptions): boolean {
|
|
6857
|
+
if (options.route.client) return true
|
|
6858
|
+
const rootLayout = (options.layoutFiles ??
|
|
6859
|
+
findLayouts(options.config.appPath, options.route.file))[0]
|
|
6860
|
+
return Boolean(rootLayout && options.route.clientReferences.some(ref => ref.file === rootLayout))
|
|
6861
|
+
}
|
|
6862
|
+
|
|
6863
|
+
function pageSlotMarkerMode(page: DocumentPage): PageSlotMarkerMode {
|
|
6684
6864
|
if (!page.clientScript) return 'drop'
|
|
6685
|
-
|
|
6686
|
-
// no island marker of its own); the body scan additionally covers islands
|
|
6687
|
-
// contributed by an ancestor layout outside this route's own references.
|
|
6688
|
-
if (page.hasClientMounts || /<!--pnext-client|<pnext-client\b/.test(body)) return 'materialize'
|
|
6689
|
-
return 'keep'
|
|
6865
|
+
return page.hasClientMounts ? 'materialize' : 'keep'
|
|
6690
6866
|
}
|
|
6691
6867
|
|
|
6692
|
-
|
|
6868
|
+
/**
|
|
6869
|
+
* The same mode for a streamed tail chunk, which is assembled before the document knows whether it
|
|
6870
|
+
* ships a client script. `keep` and `drop` dissolve the slot identically here (only the bootstrap
|
|
6871
|
+
* script distinguishes them), so the mount-target question is the whole decision.
|
|
6872
|
+
*/
|
|
6873
|
+
function streamPageSlotMode(options: RenderOptions): PageSlotMarkerMode {
|
|
6874
|
+
return pageSlotIsMountTarget(options) ? 'materialize' : 'keep'
|
|
6875
|
+
}
|
|
6876
|
+
|
|
6877
|
+
export function clientIslandMarkerBootstrapSource(mode: PageSlotMarkerMode = 'materialize') {
|
|
6693
6878
|
// Pages that mount nothing on #pnext-page never need the wrapper div: the
|
|
6694
6879
|
// page's children stay direct children of <body>, matching Next's document
|
|
6695
6880
|
// structure (tests assert structural selectors like `body > p`). `keep`
|
|
@@ -6747,45 +6932,48 @@ function hStreamError(
|
|
|
6747
6932
|
hasClientRuntime = false,
|
|
6748
6933
|
): VNode<any> {
|
|
6749
6934
|
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
|
|
6935
|
+
// Compat: carry the serialized boundary error (redacted message + digest)
|
|
6936
|
+
// on the marker so the client error runtime can replace this fallback with
|
|
6937
|
+
// the route's error.js boundary (Next renders the nearest error boundary
|
|
6938
|
+
// for a server error inside a streamed Suspense hole; its unstable_retry
|
|
6939
|
+
// soft-refreshes and recovers). Core keeps the plain generic marker.
|
|
6940
|
+
const serialized = compatNext
|
|
6941
|
+
? getRenderExtensions().serializeError({ error: resolvedError, dev })
|
|
6942
|
+
: undefined
|
|
6943
|
+
// The marker's own content is what stays on screen when nothing escalates it. Dev keeps the
|
|
6944
|
+
// diagnostic card there - Next renders the error boundary in dev too, so the marker must be emitted
|
|
6945
|
+
// either way; substituting the card for the marker stranded the route's error.js.
|
|
6946
|
+
const fallback = dev
|
|
6947
|
+
? genericErrorTree(resolvedError, true, compatNext)
|
|
6948
|
+
: ((compatNext
|
|
6771
6949
|
? 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
|
-
|
|
6950
|
+
: undefined) ?? 'Application error')
|
|
6951
|
+
const marker = h(
|
|
6952
|
+
'pnext-error',
|
|
6953
|
+
{
|
|
6954
|
+
'data-pnext-stream-error': true,
|
|
6955
|
+
...(serialized
|
|
6956
|
+
? {
|
|
6957
|
+
'data-pnext-error-message': serialized.message,
|
|
6958
|
+
...(serialized.digest ? { 'data-pnext-error-digest': serialized.digest } : {}),
|
|
6959
|
+
}
|
|
6960
|
+
: {}),
|
|
6961
|
+
},
|
|
6962
|
+
fallback,
|
|
6963
|
+
)
|
|
6964
|
+
// A page with no client components ships no error runtime, so nothing can escalate the marker
|
|
6965
|
+
// client-side. Emit a self-contained fallback that swaps in the built-in global-error document
|
|
6966
|
+
// unless the runtime announces itself (__PNEXT_STREAM_ERROR_RUNTIME__), in which case it owns
|
|
6967
|
+
// the marker instead. Dev needs none: its card is already the marker's visible content.
|
|
6968
|
+
const fallbackScript =
|
|
6969
|
+
!dev && compatNext && !hasClientRuntime ? streamErrorDocumentScript(serialized) : undefined
|
|
6970
|
+
if (!fallbackScript) return marker
|
|
6971
|
+
return h(
|
|
6972
|
+
Fragment,
|
|
6973
|
+
null,
|
|
6974
|
+
marker,
|
|
6975
|
+
h('script', { dangerouslySetInnerHTML: { __html: fallbackScript } }),
|
|
6976
|
+
)
|
|
6789
6977
|
}
|
|
6790
6978
|
|
|
6791
6979
|
// Inline bootstrap flushed after a stream-error marker: replaces the document
|
|
@@ -6982,19 +7170,18 @@ async function applyLayouts(
|
|
|
6982
7170
|
// postpone its boundary too (not read an empty object). Reuse the page-level
|
|
6983
7171
|
// hanging params promise so the whole shell is param-independent.
|
|
6984
7172
|
if (options.fallbackParams) return { ...layoutProps, params: hangingPromise('params') }
|
|
7173
|
+
// The scoped params go through the vary-tracking extension under the LAYOUT
|
|
7174
|
+
// segment kind: compat keys the layout's cache frame on the params this
|
|
7175
|
+
// layout reads, independently of the page's own set. Outside a tracking
|
|
7176
|
+
// scope the extension is the identity, so ordinary renders pay nothing (see
|
|
7177
|
+
// compat/segment/vary-params.ts).
|
|
7178
|
+
const scopedParams = getRenderExtensions().trackVaryParams(
|
|
7179
|
+
layoutParamsForDir(options.config.appPath, dirname(layoutFile), options.params ?? {}),
|
|
7180
|
+
{ kind: 'params', segment: 'layout' },
|
|
7181
|
+
)
|
|
6985
7182
|
return {
|
|
6986
7183
|
...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
|
-
),
|
|
7184
|
+
params: legacySyncProps(Promise.resolve(scopedParams), 'params', scopedParams),
|
|
6998
7185
|
}
|
|
6999
7186
|
}
|
|
7000
7187
|
|