@rsc-kit/core 0.29.8 → 0.29.10
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/dist/bootPayloads.d.ts +18 -0
- package/dist/bootPayloads.js +81 -0
- package/dist/bootPayloads.js.map +1 -0
- package/dist/crawler.d.ts +2 -0
- package/dist/crawler.js +62 -0
- package/dist/crawler.js.map +1 -0
- package/dist/headers.d.ts +5 -0
- package/dist/headers.js +5 -0
- package/dist/headers.js.map +1 -1
- package/dist/host.d.ts +10 -9
- package/dist/host.js +130 -30
- package/dist/host.js.map +1 -1
- package/dist/hostCalls.js +11 -1
- package/dist/hostCalls.js.map +1 -1
- package/dist/js/SegmentBoundary.js +20 -4
- package/dist/js/SegmentBoundary.js.map +1 -1
- package/dist/js/createViteRscApp.js +8 -1
- package/dist/js/createViteRscApp.js.map +1 -1
- package/dist/js/pagePayload.d.ts +1 -1
- package/dist/js/pagePayload.js +2 -2
- package/dist/js/pagePayload.js.map +1 -1
- package/dist/notFound.d.ts +4 -4
- package/dist/notFound.js +6 -4
- package/dist/notFound.js.map +1 -1
- package/dist/redirect.d.ts +1 -1
- package/dist/redirect.js +1 -1
- package/dist/redirect.js.map +1 -1
- package/dist/request.d.ts +5 -0
- package/dist/request.js +9 -0
- package/dist/request.js.map +1 -1
- package/dist/vite.js +268 -197
- package/dist/vite.js.map +1 -1
- package/package.json +1 -1
- package/dist/shellHead.d.ts +0 -22
- package/dist/shellHead.js +0 -45
- package/dist/shellHead.js.map +0 -1
package/dist/vite.js
CHANGED
|
@@ -3023,9 +3023,10 @@ import { isSharedSection, sectionComponent } from ${JSON.stringify(join(packageD
|
|
|
3023
3023
|
import { shareRender } from ${JSON.stringify(join(packageDir, "sharedRenders"))}
|
|
3024
3024
|
import { PathnameProvider } from ${JSON.stringify(join(packageDir, "js/PathnameProvider"))}
|
|
3025
3025
|
import { DefaultRouteError } from ${JSON.stringify(join(packageDir, "js/DefaultRouteError"))}
|
|
3026
|
-
import { requestSearch, searchParams as requestSearchParams, withUrl } from ${JSON.stringify(join(packageDir, "request"))}
|
|
3026
|
+
import { requestFromCrawler, requestSearch, searchParams as requestSearchParams, withUrl } from ${JSON.stringify(join(packageDir, "request"))}
|
|
3027
3027
|
import { parseParams, parseSearchParams, parseBody, isSearchParamsError, isBodyError } from ${JSON.stringify(join(packageDir, "routeSchema"))}
|
|
3028
3028
|
import { notFoundDigest, isNotFoundSignal } from ${JSON.stringify(join(packageDir, "notFound"))}
|
|
3029
|
+
import { bootToken } from ${JSON.stringify(join(packageDir, "bootPayloads"))}
|
|
3029
3030
|
import { noteRequestRead, notePageProps, urlOf } from ${JSON.stringify(join(packageDir, "request"))}
|
|
3030
3031
|
import { redirectDigest } from ${JSON.stringify(join(packageDir, "redirectDigest"))}
|
|
3031
3032
|
import { cancelledByConsumer } from ${JSON.stringify(join(packageDir, "js/fallbackReport"))}
|
|
@@ -3942,6 +3943,22 @@ function buildElement(
|
|
|
3942
3943
|
// Resolve route metadata into React elements. React 19 hoists <title>/<meta>
|
|
3943
3944
|
// rendered anywhere in the tree into <head> — so the "vite way" for metadata is
|
|
3944
3945
|
// to render it as elements, rather than a backend injecting a <head> string.
|
|
3946
|
+
/** Whether any file of this route generates its metadata - then it streams. */
|
|
3947
|
+
function metadataStreams(component: string, layouts: LayoutEntry[]): boolean {
|
|
3948
|
+
return [component, ...layouts.map((l) => l.component)].some((c) => Boolean(metadataMap[c]?.generate))
|
|
3949
|
+
}
|
|
3950
|
+
|
|
3951
|
+
/** The page's generated metadata, rendered when it resolves: its own boundary, so nothing waits for it. */
|
|
3952
|
+
async function StreamedMetadata({
|
|
3953
|
+
load,
|
|
3954
|
+
render,
|
|
3955
|
+
}: {
|
|
3956
|
+
load: () => Promise<Record<string, unknown> | null>
|
|
3957
|
+
render: (md: Record<string, unknown> | null) => unknown[]
|
|
3958
|
+
}) {
|
|
3959
|
+
return render(await load()) as never
|
|
3960
|
+
}
|
|
3961
|
+
|
|
3945
3962
|
async function renderTree(
|
|
3946
3963
|
component: string,
|
|
3947
3964
|
props: Record<string, unknown>,
|
|
@@ -3967,7 +3984,6 @@ async function renderTree(
|
|
|
3967
3984
|
// The params promise travels too: during the pattern probe it never
|
|
3968
3985
|
// settles, and a generateMetadata that reads it is then left out of the
|
|
3969
3986
|
// shell rather than run against the placeholder - see resolveMetadata.
|
|
3970
|
-
const md = await resolveMetadata(component, props, layouts, metadataParams)
|
|
3971
3987
|
const head: unknown[] = []
|
|
3972
3988
|
|
|
3973
3989
|
// Rendered into the tree rather than written into the app's layout: React
|
|
@@ -3981,15 +3997,6 @@ async function renderTree(
|
|
|
3981
3997
|
// here, above the found images, because those are the ones most likely to
|
|
3982
3998
|
// be relative: an opengraph-image.png in app/ is emitted as /_app/..., and a
|
|
3983
3999
|
// share-card scraper needs the origin in front of it.
|
|
3984
|
-
const base = md?.metadataBase ? String(md.metadataBase) : null
|
|
3985
|
-
const absolute = (value: unknown): string => {
|
|
3986
|
-
const text = String(value instanceof URL ? value.href : value)
|
|
3987
|
-
|
|
3988
|
-
if (!base || /^[a-z][a-z0-9+.-]*:/i.test(text)) return text
|
|
3989
|
-
|
|
3990
|
-
return new URL(text, base).href
|
|
3991
|
-
}
|
|
3992
|
-
|
|
3993
4000
|
// What Next wrote into every document without being asked, and a layout
|
|
3994
4001
|
// copied from a Next app therefore never writes: the charset, and the
|
|
3995
4002
|
// viewport - without which a phone lays the page out at desktop width.
|
|
@@ -4017,16 +4024,6 @@ async function renderTree(
|
|
|
4017
4024
|
head.push(createElement('meta', { key: '__vcs', name: 'color-scheme', content: viewport.colorScheme }))
|
|
4018
4025
|
}
|
|
4019
4026
|
|
|
4020
|
-
for (const [i, found] of APP_HEAD.entries()) {
|
|
4021
|
-
const props = { ...found.props }
|
|
4022
|
-
|
|
4023
|
-
if (props.content && /^(og:image|twitter:image)$/.test(props.property ?? props.name ?? '')) {
|
|
4024
|
-
props.content = absolute(props.content)
|
|
4025
|
-
}
|
|
4026
|
-
|
|
4027
|
-
head.push(createElement(found.tag, { key: '__a' + i, ...props }))
|
|
4028
|
-
}
|
|
4029
|
-
|
|
4030
4027
|
if (WEB_MANIFEST) {
|
|
4031
4028
|
head.push(createElement('link', { key: '__mf', rel: 'manifest', href: WEB_MANIFEST.href }))
|
|
4032
4029
|
|
|
@@ -4037,215 +4034,264 @@ async function renderTree(
|
|
|
4037
4034
|
}
|
|
4038
4035
|
}
|
|
4039
4036
|
|
|
4040
|
-
if (md) {
|
|
4041
|
-
if (md.title != null) {
|
|
4042
|
-
// The element is what a server render puts in <head>, and what a route
|
|
4043
|
-
// with no runtime relies on entirely.
|
|
4044
|
-
head.push(createElement('title', { key: '__t' }, String(md.title)))
|
|
4045
|
-
|
|
4046
|
-
// And the effect is what keeps it right once pages are retained — see
|
|
4047
|
-
// DocumentTitle. Only where there is a runtime to run it: a client
|
|
4048
|
-
// component on a route that ships none is refused by the build.
|
|
4049
|
-
if (bootstrap) head.push(createElement(DocumentTitle, { key: '__ts', title: String(md.title) }))
|
|
4050
|
-
}
|
|
4051
|
-
if (md.description != null) head.push(createElement('meta', { key: '__d', name: 'description', content: String(md.description) }))
|
|
4052
|
-
// The name only, never the version - see the identify option.
|
|
4053
|
-
if (manifest().build?.identify) head.push(createElement('meta', { key: '__g', name: 'generator', content: 'rsc-kit' }))
|
|
4054
|
-
}
|
|
4055
|
-
|
|
4056
4037
|
// Which build this document is, for the client to say on every navigation
|
|
4057
4038
|
// - read from the document rather than learned from the first answer,
|
|
4058
4039
|
// because a document a service worker served from its cache is the last
|
|
4059
4040
|
// build's while the answers are this one's. An id, not a version number.
|
|
4060
4041
|
if (bootstrap) head.push(createElement('meta', { key: '__v', name: 'rsc-kit:build', content: await buildId() }))
|
|
4061
4042
|
|
|
4062
|
-
|
|
4043
|
+
// What the page's metadata adds - its title, description, share tags,
|
|
4044
|
+
// robots, icons - and the found icons and share images, made absolute
|
|
4045
|
+
// against its metadataBase.
|
|
4046
|
+
const metadataHead = (md: Record<string, unknown> | null): unknown[] => {
|
|
4047
|
+
const head: unknown[] = []
|
|
4048
|
+
const base = md?.metadataBase ? String(md.metadataBase) : null
|
|
4049
|
+
const absolute = (value: unknown): string => {
|
|
4050
|
+
const text = String(value instanceof URL ? value.href : value)
|
|
4063
4051
|
|
|
4064
|
-
|
|
4065
|
-
// their no- forms, the flags by name, the limits as name:value. googleBot
|
|
4066
|
-
// is the same shape for the googlebot tag. The object used to fall
|
|
4067
|
-
// through to the catch-all below as "[object Object]" - which no crawler
|
|
4068
|
-
// reads, on the one page that asked not to be indexed.
|
|
4069
|
-
const robotsContent = (value: unknown): string => {
|
|
4070
|
-
if (typeof value !== 'object' || value === null) return String(value)
|
|
4052
|
+
if (!base || /^[a-z][a-z0-9+.-]*:/i.test(text)) return text
|
|
4071
4053
|
|
|
4072
|
-
|
|
4073
|
-
|
|
4054
|
+
return new URL(text, base).href
|
|
4055
|
+
}
|
|
4074
4056
|
|
|
4075
|
-
|
|
4076
|
-
|
|
4077
|
-
|
|
4078
|
-
|
|
4079
|
-
|
|
4080
|
-
if (r.unavailable_after != null) parts.push('unavailable_after: ' + String(r.unavailable_after))
|
|
4081
|
-
for (const limit of ['max-video-preview', 'max-image-preview', 'max-snippet']) {
|
|
4082
|
-
if (r[limit] != null) parts.push(limit + ':' + String(r[limit]))
|
|
4057
|
+
for (const [i, found] of APP_HEAD.entries()) {
|
|
4058
|
+
const props = { ...found.props }
|
|
4059
|
+
|
|
4060
|
+
if (props.content && /^(og:image|twitter:image)$/.test(props.property ?? props.name ?? '')) {
|
|
4061
|
+
props.content = absolute(props.content)
|
|
4083
4062
|
}
|
|
4084
4063
|
|
|
4085
|
-
|
|
4064
|
+
head.push(createElement(found.tag, { key: '__a' + i, ...props }))
|
|
4086
4065
|
}
|
|
4087
4066
|
|
|
4088
|
-
if (md
|
|
4089
|
-
|
|
4090
|
-
|
|
4091
|
-
|
|
4067
|
+
if (md) {
|
|
4068
|
+
if (md.title != null) {
|
|
4069
|
+
// The element is what a server render puts in <head>, and what a route
|
|
4070
|
+
// with no runtime relies on entirely.
|
|
4071
|
+
head.push(createElement('title', { key: '__t' }, String(md.title)))
|
|
4092
4072
|
|
|
4093
|
-
|
|
4073
|
+
// And the effect is what keeps it right once pages are retained — see
|
|
4074
|
+
// DocumentTitle. Only where there is a runtime to run it: a client
|
|
4075
|
+
// component on a route that ships none is refused by the build.
|
|
4076
|
+
if (bootstrap) head.push(createElement(DocumentTitle, { key: '__ts', title: String(md.title) }))
|
|
4077
|
+
}
|
|
4078
|
+
if (md.description != null) head.push(createElement('meta', { key: '__d', name: 'description', content: String(md.description) }))
|
|
4079
|
+
// The name only, never the version - see the identify option.
|
|
4080
|
+
if (manifest().build?.identify) head.push(createElement('meta', { key: '__g', name: 'generator', content: 'rsc-kit' }))
|
|
4094
4081
|
}
|
|
4095
4082
|
|
|
4096
|
-
|
|
4097
|
-
// Slack's, and LinkedIn's - reads only property=, so every og tag this
|
|
4098
|
-
// used to emit with name= was invisible to the thing it existed for.
|
|
4099
|
-
// Twitter reads name=, which is why the two are not one rule.
|
|
4100
|
-
const isProperty = (k: string) => /^(og|article|profile|book|music|video|fb):/.test(k)
|
|
4101
|
-
// The keys whose CONTENT is a url. Exact, plus the :url and :secure_url
|
|
4102
|
-
// spellings - not og:image:width, which a looser match turned into
|
|
4103
|
-
// https://site/1200 and no scraper would ever read.
|
|
4104
|
-
const isUrlKey = (k: string) => /^(og:image|og:url|twitter:image)(:(secure_)?url)?$/.test(k)
|
|
4105
|
-
const tag = (k: string, v: unknown) =>
|
|
4106
|
-
createElement('meta', {
|
|
4107
|
-
key: '__m_' + k + '_' + String(v).slice(0, 40),
|
|
4108
|
-
[isProperty(k) ? 'property' : 'name']: k,
|
|
4109
|
-
content: isUrlKey(k) ? absolute(v) : String(v),
|
|
4110
|
-
})
|
|
4111
|
-
|
|
4112
|
-
// One image, or several. Each becomes its own og:image plus the size and
|
|
4113
|
-
// alt tags beside it, which is how a scraper is told which is which.
|
|
4114
|
-
const images = (prefix: string, value: unknown): void => {
|
|
4115
|
-
const list = Array.isArray(value) ? value : [value]
|
|
4083
|
+
if (md) {
|
|
4116
4084
|
|
|
4117
|
-
|
|
4118
|
-
|
|
4085
|
+
// robots is a string, or the object Next takes: index and follow as
|
|
4086
|
+
// their no- forms, the flags by name, the limits as name:value. googleBot
|
|
4087
|
+
// is the same shape for the googlebot tag. The object used to fall
|
|
4088
|
+
// through to the catch-all below as "[object Object]" - which no crawler
|
|
4089
|
+
// reads, on the one page that asked not to be indexed.
|
|
4090
|
+
const robotsContent = (value: unknown): string => {
|
|
4091
|
+
if (typeof value !== 'object' || value === null) return String(value)
|
|
4119
4092
|
|
|
4120
|
-
|
|
4121
|
-
|
|
4093
|
+
const r = value as Record<string, unknown>
|
|
4094
|
+
const parts: string[] = []
|
|
4122
4095
|
|
|
4123
|
-
|
|
4124
|
-
|
|
4125
|
-
|
|
4126
|
-
if (
|
|
4127
|
-
|
|
4128
|
-
|
|
4129
|
-
|
|
4096
|
+
if (r.index != null) parts.push(r.index ? 'index' : 'noindex')
|
|
4097
|
+
if (r.follow != null) parts.push(r.follow ? 'follow' : 'nofollow')
|
|
4098
|
+
for (const flag of ['noarchive', 'nosnippet', 'noimageindex', 'nocache', 'notranslate', 'indexifembedded', 'nositelinkssearchbox']) {
|
|
4099
|
+
if (r[flag]) parts.push(flag)
|
|
4100
|
+
}
|
|
4101
|
+
if (r.unavailable_after != null) parts.push('unavailable_after: ' + String(r.unavailable_after))
|
|
4102
|
+
for (const limit of ['max-video-preview', 'max-image-preview', 'max-snippet']) {
|
|
4103
|
+
if (r[limit] != null) parts.push(limit + ':' + String(r[limit]))
|
|
4130
4104
|
}
|
|
4105
|
+
|
|
4106
|
+
return parts.join(', ')
|
|
4131
4107
|
}
|
|
4132
|
-
}
|
|
4133
4108
|
|
|
4134
|
-
|
|
4135
|
-
|
|
4109
|
+
if (md.robots != null) {
|
|
4110
|
+
head.push(createElement('meta', { key: '__r', name: 'robots', content: robotsContent(md.robots) }))
|
|
4136
4111
|
|
|
4137
|
-
|
|
4138
|
-
if (og.description != null) head.push(tag('og:description', og.description))
|
|
4139
|
-
if (og.url != null) head.push(tag('og:url', og.url))
|
|
4140
|
-
if (og.siteName != null) head.push(tag('og:site_name', og.siteName))
|
|
4141
|
-
if (og.type != null) head.push(tag('og:type', og.type))
|
|
4142
|
-
if (og.locale != null) head.push(tag('og:locale', og.locale))
|
|
4143
|
-
if (og.images != null) images('og:image', og.images)
|
|
4144
|
-
}
|
|
4112
|
+
const bot = typeof md.robots === 'object' ? (md.robots as { googleBot?: unknown }).googleBot : null
|
|
4145
4113
|
|
|
4146
|
-
|
|
4147
|
-
|
|
4114
|
+
if (bot != null) head.push(createElement('meta', { key: '__rg', name: 'googlebot', content: robotsContent(bot) }))
|
|
4115
|
+
}
|
|
4148
4116
|
|
|
4149
|
-
|
|
4150
|
-
|
|
4151
|
-
|
|
4152
|
-
|
|
4153
|
-
|
|
4154
|
-
|
|
4155
|
-
|
|
4117
|
+
// og: and its relatives are PROPERTY, not name. Facebook's scraper - and
|
|
4118
|
+
// Slack's, and LinkedIn's - reads only property=, so every og tag this
|
|
4119
|
+
// used to emit with name= was invisible to the thing it existed for.
|
|
4120
|
+
// Twitter reads name=, which is why the two are not one rule.
|
|
4121
|
+
const isProperty = (k: string) => /^(og|article|profile|book|music|video|fb):/.test(k)
|
|
4122
|
+
// The keys whose CONTENT is a url. Exact, plus the :url and :secure_url
|
|
4123
|
+
// spellings - not og:image:width, which a looser match turned into
|
|
4124
|
+
// https://site/1200 and no scraper would ever read.
|
|
4125
|
+
const isUrlKey = (k: string) => /^(og:image|og:url|twitter:image)(:(secure_)?url)?$/.test(k)
|
|
4126
|
+
const tag = (k: string, v: unknown) =>
|
|
4127
|
+
createElement('meta', {
|
|
4128
|
+
key: '__m_' + k + '_' + String(v).slice(0, 40),
|
|
4129
|
+
[isProperty(k) ? 'property' : 'name']: k,
|
|
4130
|
+
content: isUrlKey(k) ? absolute(v) : String(v),
|
|
4131
|
+
})
|
|
4132
|
+
|
|
4133
|
+
// One image, or several. Each becomes its own og:image plus the size and
|
|
4134
|
+
// alt tags beside it, which is how a scraper is told which is which.
|
|
4135
|
+
const images = (prefix: string, value: unknown): void => {
|
|
4136
|
+
const list = Array.isArray(value) ? value : [value]
|
|
4137
|
+
|
|
4138
|
+
for (const item of list) {
|
|
4139
|
+
if (item == null) continue
|
|
4156
4140
|
|
|
4157
|
-
|
|
4158
|
-
|
|
4159
|
-
|
|
4160
|
-
|
|
4161
|
-
|
|
4162
|
-
|
|
4163
|
-
|
|
4164
|
-
|
|
4165
|
-
|
|
4166
|
-
|
|
4167
|
-
|
|
4168
|
-
|
|
4169
|
-
|
|
4170
|
-
head.push(
|
|
4171
|
-
createElement('link', {
|
|
4172
|
-
key: '__i_' + rel + '_' + String(icon.url).slice(0, 40),
|
|
4173
|
-
rel: (icon.rel as string) ?? rel,
|
|
4174
|
-
href: absolute(icon.url),
|
|
4175
|
-
...(icon.type ? { type: icon.type } : {}),
|
|
4176
|
-
...(icon.sizes ? { sizes: icon.sizes } : {}),
|
|
4177
|
-
...(icon.media ? { media: icon.media } : {}),
|
|
4178
|
-
}),
|
|
4179
|
-
)
|
|
4141
|
+
if (typeof item === 'object' && !(item instanceof URL)) {
|
|
4142
|
+
const image = item as { url: unknown; width?: number; height?: number; alt?: string; type?: string }
|
|
4143
|
+
|
|
4144
|
+
head.push(tag(prefix, image.url))
|
|
4145
|
+
if (image.width) head.push(tag(prefix + ':width', image.width))
|
|
4146
|
+
if (image.height) head.push(tag(prefix + ':height', image.height))
|
|
4147
|
+
if (image.alt) head.push(tag(prefix + ':alt', image.alt))
|
|
4148
|
+
if (image.type) head.push(tag(prefix + ':type', image.type))
|
|
4149
|
+
} else {
|
|
4150
|
+
head.push(tag(prefix, item))
|
|
4151
|
+
}
|
|
4152
|
+
}
|
|
4180
4153
|
}
|
|
4181
|
-
}
|
|
4182
4154
|
|
|
4183
|
-
|
|
4184
|
-
|
|
4155
|
+
if (md.openGraph) {
|
|
4156
|
+
const og = md.openGraph as Record<string, unknown>
|
|
4185
4157
|
|
|
4186
|
-
|
|
4187
|
-
|
|
4188
|
-
|
|
4189
|
-
if (
|
|
4190
|
-
if (
|
|
4191
|
-
if (
|
|
4192
|
-
if (
|
|
4158
|
+
if (og.title != null) head.push(tag('og:title', og.title))
|
|
4159
|
+
if (og.description != null) head.push(tag('og:description', og.description))
|
|
4160
|
+
if (og.url != null) head.push(tag('og:url', og.url))
|
|
4161
|
+
if (og.siteName != null) head.push(tag('og:site_name', og.siteName))
|
|
4162
|
+
if (og.type != null) head.push(tag('og:type', og.type))
|
|
4163
|
+
if (og.locale != null) head.push(tag('og:locale', og.locale))
|
|
4164
|
+
if (og.images != null) images('og:image', og.images)
|
|
4193
4165
|
}
|
|
4194
|
-
}
|
|
4195
4166
|
|
|
4196
|
-
|
|
4197
|
-
|
|
4198
|
-
// and reads the plain one, and Next switched between them in a minor.
|
|
4199
|
-
if (md.appleWebApp != null && md.appleWebApp !== false) {
|
|
4200
|
-
const web = (md.appleWebApp === true ? { capable: true } : md.appleWebApp) as Record<string, unknown>
|
|
4167
|
+
if (md.twitter) {
|
|
4168
|
+
const tw = md.twitter as Record<string, unknown>
|
|
4201
4169
|
|
|
4202
|
-
|
|
4203
|
-
head.push(tag('
|
|
4204
|
-
head.push(tag('
|
|
4170
|
+
if (tw.card != null) head.push(tag('twitter:card', tw.card))
|
|
4171
|
+
if (tw.title != null) head.push(tag('twitter:title', tw.title))
|
|
4172
|
+
if (tw.description != null) head.push(tag('twitter:description', tw.description))
|
|
4173
|
+
if (tw.site != null) head.push(tag('twitter:site', tw.site))
|
|
4174
|
+
if (tw.creator != null) head.push(tag('twitter:creator', tw.creator))
|
|
4175
|
+
if (tw.images != null) images('twitter:image', tw.images)
|
|
4205
4176
|
}
|
|
4206
4177
|
|
|
4207
|
-
|
|
4208
|
-
|
|
4209
|
-
|
|
4210
|
-
|
|
4211
|
-
const
|
|
4178
|
+
// icons is links, not meta. A string is one icon; an object names which
|
|
4179
|
+
// rel each is for. The ones found in app/ are already in APP_HEAD above,
|
|
4180
|
+
// so this is for an app that wants to say it explicitly.
|
|
4181
|
+
const links = (rel: string, value: unknown): void => {
|
|
4182
|
+
const list = Array.isArray(value) ? value : [value]
|
|
4212
4183
|
|
|
4213
|
-
for (const item of
|
|
4184
|
+
for (const item of list) {
|
|
4214
4185
|
if (item == null) continue
|
|
4215
4186
|
|
|
4216
|
-
const
|
|
4187
|
+
const icon = typeof item === 'object' && !(item instanceof URL)
|
|
4217
4188
|
? (item as Record<string, unknown>)
|
|
4218
4189
|
: { url: item }
|
|
4219
4190
|
|
|
4220
4191
|
head.push(
|
|
4221
4192
|
createElement('link', {
|
|
4222
|
-
key: '
|
|
4223
|
-
rel:
|
|
4224
|
-
href: absolute(
|
|
4225
|
-
...(
|
|
4193
|
+
key: '__i_' + rel + '_' + String(icon.url).slice(0, 40),
|
|
4194
|
+
rel: (icon.rel as string) ?? rel,
|
|
4195
|
+
href: absolute(icon.url),
|
|
4196
|
+
...(icon.type ? { type: icon.type } : {}),
|
|
4197
|
+
...(icon.sizes ? { sizes: icon.sizes } : {}),
|
|
4198
|
+
...(icon.media ? { media: icon.media } : {}),
|
|
4226
4199
|
}),
|
|
4227
4200
|
)
|
|
4228
4201
|
}
|
|
4229
4202
|
}
|
|
4230
|
-
}
|
|
4231
4203
|
|
|
4232
|
-
|
|
4233
|
-
|
|
4234
|
-
|
|
4235
|
-
|
|
4236
|
-
|
|
4237
|
-
|
|
4238
|
-
|
|
4239
|
-
|
|
4240
|
-
|
|
4241
|
-
|
|
4242
|
-
|
|
4243
|
-
|
|
4244
|
-
|
|
4245
|
-
|
|
4246
|
-
|
|
4204
|
+
if (md.icons != null) {
|
|
4205
|
+
const icons = md.icons as Record<string, unknown> | string | unknown[]
|
|
4206
|
+
|
|
4207
|
+
if (typeof icons === 'string' || Array.isArray(icons) || icons instanceof URL) {
|
|
4208
|
+
links('icon', icons)
|
|
4209
|
+
} else {
|
|
4210
|
+
if (icons.icon != null) links('icon', icons.icon)
|
|
4211
|
+
if (icons.apple != null) links('apple-touch-icon', icons.apple)
|
|
4212
|
+
if (icons.shortcut != null) links('shortcut icon', icons.shortcut)
|
|
4213
|
+
if (icons.other != null) links('icon', icons.other)
|
|
4214
|
+
}
|
|
4215
|
+
}
|
|
4216
|
+
|
|
4217
|
+
// How the app behaves added to an iPhone's home screen - Next's shape.
|
|
4218
|
+
// Both capable names: Safari reads the apple- one, Chrome warns about it
|
|
4219
|
+
// and reads the plain one, and Next switched between them in a minor.
|
|
4220
|
+
if (md.appleWebApp != null && md.appleWebApp !== false) {
|
|
4221
|
+
const web = (md.appleWebApp === true ? { capable: true } : md.appleWebApp) as Record<string, unknown>
|
|
4222
|
+
|
|
4223
|
+
if (web.capable) {
|
|
4224
|
+
head.push(tag('mobile-web-app-capable', 'yes'))
|
|
4225
|
+
head.push(tag('apple-mobile-web-app-capable', 'yes'))
|
|
4226
|
+
}
|
|
4227
|
+
|
|
4228
|
+
if (web.title != null) head.push(tag('apple-mobile-web-app-title', web.title))
|
|
4229
|
+
if (web.statusBarStyle != null) head.push(tag('apple-mobile-web-app-status-bar-style', web.statusBarStyle))
|
|
4230
|
+
|
|
4231
|
+
if (web.startupImage != null) {
|
|
4232
|
+
const startup = Array.isArray(web.startupImage) ? web.startupImage : [web.startupImage]
|
|
4233
|
+
|
|
4234
|
+
for (const item of startup) {
|
|
4235
|
+
if (item == null) continue
|
|
4236
|
+
|
|
4237
|
+
const image = typeof item === 'object' && !(item instanceof URL)
|
|
4238
|
+
? (item as Record<string, unknown>)
|
|
4239
|
+
: { url: item }
|
|
4240
|
+
|
|
4241
|
+
head.push(
|
|
4242
|
+
createElement('link', {
|
|
4243
|
+
key: '__s_' + String(image.url).slice(0, 60) + '_' + String(image.media ?? ''),
|
|
4244
|
+
rel: 'apple-touch-startup-image',
|
|
4245
|
+
href: absolute(image.url),
|
|
4246
|
+
...(image.media ? { media: image.media } : {}),
|
|
4247
|
+
}),
|
|
4248
|
+
)
|
|
4249
|
+
}
|
|
4250
|
+
}
|
|
4251
|
+
}
|
|
4252
|
+
|
|
4253
|
+
// other is flattened in beside the named keys, because it is a place to put
|
|
4254
|
+
// meta tags rather than a meta tag by that name. A key at the top level
|
|
4255
|
+
// still renders - the type no longer invites one, but an app written
|
|
4256
|
+
// against the old shape must not silently lose its tags.
|
|
4257
|
+
const structured = new Set(['title', 'description', 'robots', 'metadataBase', 'openGraph', 'twitter', 'icons', 'appleWebApp', 'other'])
|
|
4258
|
+
const named = Object.entries(md).filter(([k]) => !structured.has(k))
|
|
4259
|
+
const extra = Object.entries((md.other ?? {}) as Record<string, unknown>)
|
|
4260
|
+
|
|
4261
|
+
for (const [k, v] of [...named, ...extra]) {
|
|
4262
|
+
if (v == null) continue
|
|
4263
|
+
|
|
4264
|
+
if (Array.isArray(v)) {
|
|
4265
|
+
for (const item of v) head.push(tag(k, item))
|
|
4266
|
+
} else {
|
|
4267
|
+
head.push(tag(k, v))
|
|
4268
|
+
}
|
|
4247
4269
|
}
|
|
4248
4270
|
}
|
|
4271
|
+
|
|
4272
|
+
|
|
4273
|
+
return head
|
|
4274
|
+
}
|
|
4275
|
+
|
|
4276
|
+
// Static metadata - export const metadata on every file of the route -
|
|
4277
|
+
// costs nothing to resolve, and is in the shell. A generateMetadata is
|
|
4278
|
+
// data: it streams, under its own boundary, like any other read, so the
|
|
4279
|
+
// page paints at once and the title arrives when it is ready. Never held
|
|
4280
|
+
// up for it. A crawler, which needs the title and the status in what it
|
|
4281
|
+
// reads first, is answered once the whole document is ready - see the host.
|
|
4282
|
+
if (!metadataStreams(component, layouts)) {
|
|
4283
|
+
head.push(...metadataHead(await resolveMetadata(component, props, layouts, metadataParams)))
|
|
4284
|
+
} else {
|
|
4285
|
+
head.push(
|
|
4286
|
+
createElement(
|
|
4287
|
+
Suspense,
|
|
4288
|
+
{ key: '__md', fallback: null },
|
|
4289
|
+
createElement(StreamedMetadata as never, {
|
|
4290
|
+
load: () => resolveMetadata(component, props, layouts, metadataParams, true),
|
|
4291
|
+
render: metadataHead,
|
|
4292
|
+
}),
|
|
4293
|
+
),
|
|
4294
|
+
)
|
|
4249
4295
|
}
|
|
4250
4296
|
|
|
4251
4297
|
// Metadata elements are rendered INSIDE the document tree so React 19 hoists
|
|
@@ -4492,7 +4538,7 @@ export async function handleRscHtmlStream(
|
|
|
4492
4538
|
nonce?: string,
|
|
4493
4539
|
pageKey = '',
|
|
4494
4540
|
bootstrap = true,
|
|
4495
|
-
): Promise<{ htmlStream: ReadableStream;
|
|
4541
|
+
): Promise<{ htmlStream: ReadableStream; rscPayload: ReadableStream; bootToken?: string; clientChunks: unknown }> {
|
|
4496
4542
|
await instrumented()
|
|
4497
4543
|
applyHost()
|
|
4498
4544
|
await runMiddleware(component, props)
|
|
@@ -4500,11 +4546,18 @@ export async function handleRscHtmlStream(
|
|
|
4500
4546
|
await renderTree(component, props, layouts, loadings, parallelSlots, slotOverrides, 0, pageKey, bootstrap),
|
|
4501
4547
|
{ onError: flightOnError },
|
|
4502
4548
|
)
|
|
4503
|
-
|
|
4504
|
-
|
|
4549
|
+
// Written into the bootstrap script, so it is in the document before the
|
|
4550
|
+
// runtime that reads it. Only where there is a runtime: a page shipping
|
|
4551
|
+
// none never comes back for a payload.
|
|
4552
|
+
const token = bootstrap ? bootToken() : undefined
|
|
4553
|
+
// The same tree twice: one copy becomes the HTML, the other is the payload
|
|
4554
|
+
// the browser comes back for to hydrate. The host holds it for that fetch
|
|
4555
|
+
// - see bootPayloads.ts - so a visit renders once. Streamed, not read to a
|
|
4556
|
+
// string: it used to be buffered whole here and never read by anyone.
|
|
4557
|
+
const [forHtml, rscPayload] = flight.tee()
|
|
4505
4558
|
const ssr = await (import.meta as any).viteRsc.loadModule('ssr', 'index')
|
|
4506
|
-
const htmlStream = await ssr.handleSsr(forHtml, nonce, undefined, bootstrap, undefined, requestSearch())
|
|
4507
|
-
return { htmlStream,
|
|
4559
|
+
const htmlStream = await ssr.handleSsr(forHtml, nonce, undefined, bootstrap, undefined, requestSearch(), requestFromCrawler(), token)
|
|
4560
|
+
return { htmlStream, rscPayload, bootToken: token, clientChunks: {} }
|
|
4508
4561
|
}
|
|
4509
4562
|
|
|
4510
4563
|
/**
|
|
@@ -4550,7 +4603,7 @@ export async function handleRscFormPost(
|
|
|
4550
4603
|
{ onError: flightOnError },
|
|
4551
4604
|
)
|
|
4552
4605
|
const ssr = await (import.meta as any).viteRsc.loadModule('ssr', 'index')
|
|
4553
|
-
const htmlStream = await ssr.handleSsr(flight, nonce, undefined, bootstrap, formState, requestSearch())
|
|
4606
|
+
const htmlStream = await ssr.handleSsr(flight, nonce, undefined, bootstrap, formState, requestSearch(), requestFromCrawler())
|
|
4554
4607
|
|
|
4555
4608
|
return { htmlStream }
|
|
4556
4609
|
}
|
|
@@ -4588,7 +4641,7 @@ export async function handleRscResume(
|
|
|
4588
4641
|
// hole by construction: at build the params never settled, so anything
|
|
4589
4642
|
// reading them was postponed.
|
|
4590
4643
|
params?: Record<string, string>,
|
|
4591
|
-
): Promise<{ htmlStream: ReadableStream }> {
|
|
4644
|
+
): Promise<{ htmlStream: ReadableStream; replayed: () => boolean; rscPayload: ReadableStream }> {
|
|
4592
4645
|
await instrumented()
|
|
4593
4646
|
applyHost()
|
|
4594
4647
|
await runMiddleware(component, params ?? props)
|
|
@@ -4620,11 +4673,10 @@ export async function handleRscResume(
|
|
|
4620
4673
|
// rendered here for the url and hydrated against a payload that, like
|
|
4621
4674
|
// the shell, does not know it. The browser fills it in.
|
|
4622
4675
|
pageKey === '' ? null : pathname,
|
|
4623
|
-
//
|
|
4624
|
-
//
|
|
4625
|
-
//
|
|
4626
|
-
|
|
4627
|
-
pageKey === '' ? new Promise<Record<string, unknown>>(() => {}) : undefined,
|
|
4676
|
+
// The metadata is the real params' too. A generateMetadata streams
|
|
4677
|
+
// under its own boundary, which the pattern probe left as a hole; this
|
|
4678
|
+
// is what fills it, with the page's real title.
|
|
4679
|
+
undefined,
|
|
4628
4680
|
),
|
|
4629
4681
|
{ onError: flightOnError },
|
|
4630
4682
|
)
|
|
@@ -4639,8 +4691,12 @@ export async function handleRscResume(
|
|
|
4639
4691
|
// client-rendered. Set before the stream ends, because that is when
|
|
4640
4692
|
// React reports it.
|
|
4641
4693
|
let replayFailed = false
|
|
4694
|
+
// The same tree twice, as handleRscHtmlStream: the holes for the HTML, and
|
|
4695
|
+
// the payload the browser comes back for. The host holds the second for
|
|
4696
|
+
// that fetch - one render per visit.
|
|
4697
|
+
const [forHtml, rscPayload] = flight.tee()
|
|
4642
4698
|
const htmlStream = await ssr.handleSsrResume(
|
|
4643
|
-
|
|
4699
|
+
forHtml,
|
|
4644
4700
|
postponed,
|
|
4645
4701
|
nonce,
|
|
4646
4702
|
() => {
|
|
@@ -4649,7 +4705,7 @@ export async function handleRscResume(
|
|
|
4649
4705
|
requestSearch(),
|
|
4650
4706
|
)
|
|
4651
4707
|
|
|
4652
|
-
return { htmlStream, replayed: () => !replayFailed }
|
|
4708
|
+
return { htmlStream, replayed: () => !replayFailed, rscPayload }
|
|
4653
4709
|
}
|
|
4654
4710
|
|
|
4655
4711
|
// Server action (worker: rsc-action).
|
|
@@ -4984,6 +5040,11 @@ export async function resolveMetadata(
|
|
|
4984
5040
|
// The page's own params promise, when the caller has one. During the
|
|
4985
5041
|
// pattern probe it never settles - see handleRscPprShell.
|
|
4986
5042
|
params?: Promise<Record<string, unknown>>,
|
|
5043
|
+
// Rendered under its own boundary: no grace to race. A generateMetadata
|
|
5044
|
+
// waiting on params that never settle - the build's pattern probe - is a
|
|
5045
|
+
// hole like any other read, filled per request with the real title,
|
|
5046
|
+
// instead of an empty one frozen into the shell.
|
|
5047
|
+
streamed = false,
|
|
4987
5048
|
): Promise<Record<string, unknown> | null> {
|
|
4988
5049
|
await instrumented()
|
|
4989
5050
|
|
|
@@ -5010,7 +5071,7 @@ export async function resolveMetadata(
|
|
|
5010
5071
|
searchParams: pageSearchParams(),
|
|
5011
5072
|
})
|
|
5012
5073
|
|
|
5013
|
-
const settled = params
|
|
5074
|
+
const settled = params && !streamed
|
|
5014
5075
|
? await Promise.race([
|
|
5015
5076
|
Promise.resolve(generated),
|
|
5016
5077
|
new Promise<typeof METADATA_POSTPONED>((resolve) =>
|
|
@@ -5700,6 +5761,12 @@ export async function handleSsr(
|
|
|
5700
5761
|
bootstrap = true,
|
|
5701
5762
|
formState?: unknown,
|
|
5702
5763
|
search?: string | null,
|
|
5764
|
+
// A crawler's request: answered once everything has rendered - see
|
|
5765
|
+
// crawler.ts - so the host reads the page's final word on its status.
|
|
5766
|
+
whole = false,
|
|
5767
|
+
// The boot token: the document's render kept its payload under it, and
|
|
5768
|
+
// the runtime redeems it instead of asking for a second render.
|
|
5769
|
+
bootToken?: string,
|
|
5703
5770
|
): Promise<ReadableStream> {
|
|
5704
5771
|
const root = withSearch(await createFromReadableStream(rscStream), search)
|
|
5705
5772
|
|
|
@@ -5710,7 +5777,9 @@ export async function handleSsr(
|
|
|
5710
5777
|
// seconds before the runtime hydrates is held for it rather than given
|
|
5711
5778
|
// to the browser as a document load. See earlyClicks.ts.
|
|
5712
5779
|
const bootstrapScriptContent = bootstrap
|
|
5713
|
-
?
|
|
5780
|
+
? (bootToken ? 'self.__rsc_boot=' + JSON.stringify(bootToken) + ';' : '') +
|
|
5781
|
+
EARLY +
|
|
5782
|
+
(await (import.meta as any).viteRsc.loadBootstrapScriptContent('index'))
|
|
5714
5783
|
: undefined
|
|
5715
5784
|
|
|
5716
5785
|
// Without an onError handler React rejects each abortable task on its own,
|
|
@@ -5725,6 +5794,8 @@ export async function handleSsr(
|
|
|
5725
5794
|
...(formState !== undefined ? { formState: formState as any } : {}),
|
|
5726
5795
|
})
|
|
5727
5796
|
|
|
5797
|
+
if (whole) await html.allReady
|
|
5798
|
+
|
|
5728
5799
|
return DEV_ORIGIN ? rewriteViteDevUrlStream(html, DEV_ORIGIN) : html
|
|
5729
5800
|
}
|
|
5730
5801
|
|