@rsc-kit/core 0.29.8 → 0.29.9
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/crawler.d.ts +2 -0
- package/dist/crawler.js +62 -0
- package/dist/crawler.js.map +1 -0
- package/dist/host.d.ts +0 -9
- package/dist/host.js +50 -26
- package/dist/host.js.map +1 -1
- package/dist/hostCalls.js +11 -1
- package/dist/hostCalls.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 +243 -189
- 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,7 +3023,7 @@ 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
3029
|
import { noteRequestRead, notePageProps, urlOf } from ${JSON.stringify(join(packageDir, "request"))}
|
|
@@ -3942,6 +3942,22 @@ function buildElement(
|
|
|
3942
3942
|
// Resolve route metadata into React elements. React 19 hoists <title>/<meta>
|
|
3943
3943
|
// rendered anywhere in the tree into <head> — so the "vite way" for metadata is
|
|
3944
3944
|
// to render it as elements, rather than a backend injecting a <head> string.
|
|
3945
|
+
/** Whether any file of this route generates its metadata - then it streams. */
|
|
3946
|
+
function metadataStreams(component: string, layouts: LayoutEntry[]): boolean {
|
|
3947
|
+
return [component, ...layouts.map((l) => l.component)].some((c) => Boolean(metadataMap[c]?.generate))
|
|
3948
|
+
}
|
|
3949
|
+
|
|
3950
|
+
/** The page's generated metadata, rendered when it resolves: its own boundary, so nothing waits for it. */
|
|
3951
|
+
async function StreamedMetadata({
|
|
3952
|
+
load,
|
|
3953
|
+
render,
|
|
3954
|
+
}: {
|
|
3955
|
+
load: () => Promise<Record<string, unknown> | null>
|
|
3956
|
+
render: (md: Record<string, unknown> | null) => unknown[]
|
|
3957
|
+
}) {
|
|
3958
|
+
return render(await load()) as never
|
|
3959
|
+
}
|
|
3960
|
+
|
|
3945
3961
|
async function renderTree(
|
|
3946
3962
|
component: string,
|
|
3947
3963
|
props: Record<string, unknown>,
|
|
@@ -3967,7 +3983,6 @@ async function renderTree(
|
|
|
3967
3983
|
// The params promise travels too: during the pattern probe it never
|
|
3968
3984
|
// settles, and a generateMetadata that reads it is then left out of the
|
|
3969
3985
|
// shell rather than run against the placeholder - see resolveMetadata.
|
|
3970
|
-
const md = await resolveMetadata(component, props, layouts, metadataParams)
|
|
3971
3986
|
const head: unknown[] = []
|
|
3972
3987
|
|
|
3973
3988
|
// Rendered into the tree rather than written into the app's layout: React
|
|
@@ -3981,15 +3996,6 @@ async function renderTree(
|
|
|
3981
3996
|
// here, above the found images, because those are the ones most likely to
|
|
3982
3997
|
// be relative: an opengraph-image.png in app/ is emitted as /_app/..., and a
|
|
3983
3998
|
// 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
3999
|
// What Next wrote into every document without being asked, and a layout
|
|
3994
4000
|
// copied from a Next app therefore never writes: the charset, and the
|
|
3995
4001
|
// viewport - without which a phone lays the page out at desktop width.
|
|
@@ -4017,16 +4023,6 @@ async function renderTree(
|
|
|
4017
4023
|
head.push(createElement('meta', { key: '__vcs', name: 'color-scheme', content: viewport.colorScheme }))
|
|
4018
4024
|
}
|
|
4019
4025
|
|
|
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
4026
|
if (WEB_MANIFEST) {
|
|
4031
4027
|
head.push(createElement('link', { key: '__mf', rel: 'manifest', href: WEB_MANIFEST.href }))
|
|
4032
4028
|
|
|
@@ -4037,215 +4033,264 @@ async function renderTree(
|
|
|
4037
4033
|
}
|
|
4038
4034
|
}
|
|
4039
4035
|
|
|
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
4036
|
// Which build this document is, for the client to say on every navigation
|
|
4057
4037
|
// - read from the document rather than learned from the first answer,
|
|
4058
4038
|
// because a document a service worker served from its cache is the last
|
|
4059
4039
|
// build's while the answers are this one's. An id, not a version number.
|
|
4060
4040
|
if (bootstrap) head.push(createElement('meta', { key: '__v', name: 'rsc-kit:build', content: await buildId() }))
|
|
4061
4041
|
|
|
4062
|
-
|
|
4042
|
+
// What the page's metadata adds - its title, description, share tags,
|
|
4043
|
+
// robots, icons - and the found icons and share images, made absolute
|
|
4044
|
+
// against its metadataBase.
|
|
4045
|
+
const metadataHead = (md: Record<string, unknown> | null): unknown[] => {
|
|
4046
|
+
const head: unknown[] = []
|
|
4047
|
+
const base = md?.metadataBase ? String(md.metadataBase) : null
|
|
4048
|
+
const absolute = (value: unknown): string => {
|
|
4049
|
+
const text = String(value instanceof URL ? value.href : value)
|
|
4050
|
+
|
|
4051
|
+
if (!base || /^[a-z][a-z0-9+.-]*:/i.test(text)) return text
|
|
4063
4052
|
|
|
4064
|
-
|
|
4065
|
-
|
|
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)
|
|
4053
|
+
return new URL(text, base).href
|
|
4054
|
+
}
|
|
4071
4055
|
|
|
4072
|
-
|
|
4073
|
-
const
|
|
4056
|
+
for (const [i, found] of APP_HEAD.entries()) {
|
|
4057
|
+
const props = { ...found.props }
|
|
4074
4058
|
|
|
4075
|
-
if (
|
|
4076
|
-
|
|
4077
|
-
for (const flag of ['noarchive', 'nosnippet', 'noimageindex', 'nocache', 'notranslate', 'indexifembedded', 'nositelinkssearchbox']) {
|
|
4078
|
-
if (r[flag]) parts.push(flag)
|
|
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]))
|
|
4059
|
+
if (props.content && /^(og:image|twitter:image)$/.test(props.property ?? props.name ?? '')) {
|
|
4060
|
+
props.content = absolute(props.content)
|
|
4083
4061
|
}
|
|
4084
4062
|
|
|
4085
|
-
|
|
4063
|
+
head.push(createElement(found.tag, { key: '__a' + i, ...props }))
|
|
4086
4064
|
}
|
|
4087
4065
|
|
|
4088
|
-
if (md
|
|
4089
|
-
|
|
4090
|
-
|
|
4091
|
-
|
|
4066
|
+
if (md) {
|
|
4067
|
+
if (md.title != null) {
|
|
4068
|
+
// The element is what a server render puts in <head>, and what a route
|
|
4069
|
+
// with no runtime relies on entirely.
|
|
4070
|
+
head.push(createElement('title', { key: '__t' }, String(md.title)))
|
|
4092
4071
|
|
|
4093
|
-
|
|
4072
|
+
// And the effect is what keeps it right once pages are retained — see
|
|
4073
|
+
// DocumentTitle. Only where there is a runtime to run it: a client
|
|
4074
|
+
// component on a route that ships none is refused by the build.
|
|
4075
|
+
if (bootstrap) head.push(createElement(DocumentTitle, { key: '__ts', title: String(md.title) }))
|
|
4076
|
+
}
|
|
4077
|
+
if (md.description != null) head.push(createElement('meta', { key: '__d', name: 'description', content: String(md.description) }))
|
|
4078
|
+
// The name only, never the version - see the identify option.
|
|
4079
|
+
if (manifest().build?.identify) head.push(createElement('meta', { key: '__g', name: 'generator', content: 'rsc-kit' }))
|
|
4094
4080
|
}
|
|
4095
4081
|
|
|
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
|
-
})
|
|
4082
|
+
if (md) {
|
|
4111
4083
|
|
|
4112
|
-
|
|
4113
|
-
|
|
4114
|
-
|
|
4115
|
-
|
|
4084
|
+
// robots is a string, or the object Next takes: index and follow as
|
|
4085
|
+
// their no- forms, the flags by name, the limits as name:value. googleBot
|
|
4086
|
+
// is the same shape for the googlebot tag. The object used to fall
|
|
4087
|
+
// through to the catch-all below as "[object Object]" - which no crawler
|
|
4088
|
+
// reads, on the one page that asked not to be indexed.
|
|
4089
|
+
const robotsContent = (value: unknown): string => {
|
|
4090
|
+
if (typeof value !== 'object' || value === null) return String(value)
|
|
4116
4091
|
|
|
4117
|
-
|
|
4118
|
-
|
|
4092
|
+
const r = value as Record<string, unknown>
|
|
4093
|
+
const parts: string[] = []
|
|
4119
4094
|
|
|
4120
|
-
if (
|
|
4121
|
-
|
|
4122
|
-
|
|
4123
|
-
|
|
4124
|
-
if (image.width) head.push(tag(prefix + ':width', image.width))
|
|
4125
|
-
if (image.height) head.push(tag(prefix + ':height', image.height))
|
|
4126
|
-
if (image.alt) head.push(tag(prefix + ':alt', image.alt))
|
|
4127
|
-
if (image.type) head.push(tag(prefix + ':type', image.type))
|
|
4128
|
-
} else {
|
|
4129
|
-
head.push(tag(prefix, item))
|
|
4095
|
+
if (r.index != null) parts.push(r.index ? 'index' : 'noindex')
|
|
4096
|
+
if (r.follow != null) parts.push(r.follow ? 'follow' : 'nofollow')
|
|
4097
|
+
for (const flag of ['noarchive', 'nosnippet', 'noimageindex', 'nocache', 'notranslate', 'indexifembedded', 'nositelinkssearchbox']) {
|
|
4098
|
+
if (r[flag]) parts.push(flag)
|
|
4130
4099
|
}
|
|
4100
|
+
if (r.unavailable_after != null) parts.push('unavailable_after: ' + String(r.unavailable_after))
|
|
4101
|
+
for (const limit of ['max-video-preview', 'max-image-preview', 'max-snippet']) {
|
|
4102
|
+
if (r[limit] != null) parts.push(limit + ':' + String(r[limit]))
|
|
4103
|
+
}
|
|
4104
|
+
|
|
4105
|
+
return parts.join(', ')
|
|
4131
4106
|
}
|
|
4132
|
-
}
|
|
4133
4107
|
|
|
4134
|
-
|
|
4135
|
-
|
|
4108
|
+
if (md.robots != null) {
|
|
4109
|
+
head.push(createElement('meta', { key: '__r', name: 'robots', content: robotsContent(md.robots) }))
|
|
4136
4110
|
|
|
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
|
-
}
|
|
4111
|
+
const bot = typeof md.robots === 'object' ? (md.robots as { googleBot?: unknown }).googleBot : null
|
|
4145
4112
|
|
|
4146
|
-
|
|
4147
|
-
|
|
4113
|
+
if (bot != null) head.push(createElement('meta', { key: '__rg', name: 'googlebot', content: robotsContent(bot) }))
|
|
4114
|
+
}
|
|
4148
4115
|
|
|
4149
|
-
|
|
4150
|
-
|
|
4151
|
-
|
|
4152
|
-
|
|
4153
|
-
|
|
4154
|
-
|
|
4155
|
-
|
|
4116
|
+
// og: and its relatives are PROPERTY, not name. Facebook's scraper - and
|
|
4117
|
+
// Slack's, and LinkedIn's - reads only property=, so every og tag this
|
|
4118
|
+
// used to emit with name= was invisible to the thing it existed for.
|
|
4119
|
+
// Twitter reads name=, which is why the two are not one rule.
|
|
4120
|
+
const isProperty = (k: string) => /^(og|article|profile|book|music|video|fb):/.test(k)
|
|
4121
|
+
// The keys whose CONTENT is a url. Exact, plus the :url and :secure_url
|
|
4122
|
+
// spellings - not og:image:width, which a looser match turned into
|
|
4123
|
+
// https://site/1200 and no scraper would ever read.
|
|
4124
|
+
const isUrlKey = (k: string) => /^(og:image|og:url|twitter:image)(:(secure_)?url)?$/.test(k)
|
|
4125
|
+
const tag = (k: string, v: unknown) =>
|
|
4126
|
+
createElement('meta', {
|
|
4127
|
+
key: '__m_' + k + '_' + String(v).slice(0, 40),
|
|
4128
|
+
[isProperty(k) ? 'property' : 'name']: k,
|
|
4129
|
+
content: isUrlKey(k) ? absolute(v) : String(v),
|
|
4130
|
+
})
|
|
4131
|
+
|
|
4132
|
+
// One image, or several. Each becomes its own og:image plus the size and
|
|
4133
|
+
// alt tags beside it, which is how a scraper is told which is which.
|
|
4134
|
+
const images = (prefix: string, value: unknown): void => {
|
|
4135
|
+
const list = Array.isArray(value) ? value : [value]
|
|
4136
|
+
|
|
4137
|
+
for (const item of list) {
|
|
4138
|
+
if (item == null) continue
|
|
4156
4139
|
|
|
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
|
-
)
|
|
4140
|
+
if (typeof item === 'object' && !(item instanceof URL)) {
|
|
4141
|
+
const image = item as { url: unknown; width?: number; height?: number; alt?: string; type?: string }
|
|
4142
|
+
|
|
4143
|
+
head.push(tag(prefix, image.url))
|
|
4144
|
+
if (image.width) head.push(tag(prefix + ':width', image.width))
|
|
4145
|
+
if (image.height) head.push(tag(prefix + ':height', image.height))
|
|
4146
|
+
if (image.alt) head.push(tag(prefix + ':alt', image.alt))
|
|
4147
|
+
if (image.type) head.push(tag(prefix + ':type', image.type))
|
|
4148
|
+
} else {
|
|
4149
|
+
head.push(tag(prefix, item))
|
|
4150
|
+
}
|
|
4151
|
+
}
|
|
4180
4152
|
}
|
|
4181
|
-
}
|
|
4182
4153
|
|
|
4183
|
-
|
|
4184
|
-
|
|
4154
|
+
if (md.openGraph) {
|
|
4155
|
+
const og = md.openGraph as Record<string, unknown>
|
|
4185
4156
|
|
|
4186
|
-
|
|
4187
|
-
|
|
4188
|
-
|
|
4189
|
-
if (
|
|
4190
|
-
if (
|
|
4191
|
-
if (
|
|
4192
|
-
if (
|
|
4157
|
+
if (og.title != null) head.push(tag('og:title', og.title))
|
|
4158
|
+
if (og.description != null) head.push(tag('og:description', og.description))
|
|
4159
|
+
if (og.url != null) head.push(tag('og:url', og.url))
|
|
4160
|
+
if (og.siteName != null) head.push(tag('og:site_name', og.siteName))
|
|
4161
|
+
if (og.type != null) head.push(tag('og:type', og.type))
|
|
4162
|
+
if (og.locale != null) head.push(tag('og:locale', og.locale))
|
|
4163
|
+
if (og.images != null) images('og:image', og.images)
|
|
4193
4164
|
}
|
|
4194
|
-
}
|
|
4195
4165
|
|
|
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>
|
|
4166
|
+
if (md.twitter) {
|
|
4167
|
+
const tw = md.twitter as Record<string, unknown>
|
|
4201
4168
|
|
|
4202
|
-
|
|
4203
|
-
head.push(tag('
|
|
4204
|
-
head.push(tag('
|
|
4169
|
+
if (tw.card != null) head.push(tag('twitter:card', tw.card))
|
|
4170
|
+
if (tw.title != null) head.push(tag('twitter:title', tw.title))
|
|
4171
|
+
if (tw.description != null) head.push(tag('twitter:description', tw.description))
|
|
4172
|
+
if (tw.site != null) head.push(tag('twitter:site', tw.site))
|
|
4173
|
+
if (tw.creator != null) head.push(tag('twitter:creator', tw.creator))
|
|
4174
|
+
if (tw.images != null) images('twitter:image', tw.images)
|
|
4205
4175
|
}
|
|
4206
4176
|
|
|
4207
|
-
|
|
4208
|
-
|
|
4177
|
+
// icons is links, not meta. A string is one icon; an object names which
|
|
4178
|
+
// rel each is for. The ones found in app/ are already in APP_HEAD above,
|
|
4179
|
+
// so this is for an app that wants to say it explicitly.
|
|
4180
|
+
const links = (rel: string, value: unknown): void => {
|
|
4181
|
+
const list = Array.isArray(value) ? value : [value]
|
|
4209
4182
|
|
|
4210
|
-
|
|
4211
|
-
const startup = Array.isArray(web.startupImage) ? web.startupImage : [web.startupImage]
|
|
4212
|
-
|
|
4213
|
-
for (const item of startup) {
|
|
4183
|
+
for (const item of list) {
|
|
4214
4184
|
if (item == null) continue
|
|
4215
4185
|
|
|
4216
|
-
const
|
|
4186
|
+
const icon = typeof item === 'object' && !(item instanceof URL)
|
|
4217
4187
|
? (item as Record<string, unknown>)
|
|
4218
4188
|
: { url: item }
|
|
4219
4189
|
|
|
4220
4190
|
head.push(
|
|
4221
4191
|
createElement('link', {
|
|
4222
|
-
key: '
|
|
4223
|
-
rel:
|
|
4224
|
-
href: absolute(
|
|
4225
|
-
...(
|
|
4192
|
+
key: '__i_' + rel + '_' + String(icon.url).slice(0, 40),
|
|
4193
|
+
rel: (icon.rel as string) ?? rel,
|
|
4194
|
+
href: absolute(icon.url),
|
|
4195
|
+
...(icon.type ? { type: icon.type } : {}),
|
|
4196
|
+
...(icon.sizes ? { sizes: icon.sizes } : {}),
|
|
4197
|
+
...(icon.media ? { media: icon.media } : {}),
|
|
4226
4198
|
}),
|
|
4227
4199
|
)
|
|
4228
4200
|
}
|
|
4229
4201
|
}
|
|
4230
|
-
}
|
|
4231
4202
|
|
|
4232
|
-
|
|
4233
|
-
|
|
4234
|
-
|
|
4235
|
-
|
|
4236
|
-
|
|
4237
|
-
|
|
4238
|
-
|
|
4239
|
-
|
|
4240
|
-
|
|
4241
|
-
|
|
4242
|
-
|
|
4243
|
-
|
|
4244
|
-
|
|
4245
|
-
|
|
4246
|
-
|
|
4203
|
+
if (md.icons != null) {
|
|
4204
|
+
const icons = md.icons as Record<string, unknown> | string | unknown[]
|
|
4205
|
+
|
|
4206
|
+
if (typeof icons === 'string' || Array.isArray(icons) || icons instanceof URL) {
|
|
4207
|
+
links('icon', icons)
|
|
4208
|
+
} else {
|
|
4209
|
+
if (icons.icon != null) links('icon', icons.icon)
|
|
4210
|
+
if (icons.apple != null) links('apple-touch-icon', icons.apple)
|
|
4211
|
+
if (icons.shortcut != null) links('shortcut icon', icons.shortcut)
|
|
4212
|
+
if (icons.other != null) links('icon', icons.other)
|
|
4213
|
+
}
|
|
4214
|
+
}
|
|
4215
|
+
|
|
4216
|
+
// How the app behaves added to an iPhone's home screen - Next's shape.
|
|
4217
|
+
// Both capable names: Safari reads the apple- one, Chrome warns about it
|
|
4218
|
+
// and reads the plain one, and Next switched between them in a minor.
|
|
4219
|
+
if (md.appleWebApp != null && md.appleWebApp !== false) {
|
|
4220
|
+
const web = (md.appleWebApp === true ? { capable: true } : md.appleWebApp) as Record<string, unknown>
|
|
4221
|
+
|
|
4222
|
+
if (web.capable) {
|
|
4223
|
+
head.push(tag('mobile-web-app-capable', 'yes'))
|
|
4224
|
+
head.push(tag('apple-mobile-web-app-capable', 'yes'))
|
|
4225
|
+
}
|
|
4226
|
+
|
|
4227
|
+
if (web.title != null) head.push(tag('apple-mobile-web-app-title', web.title))
|
|
4228
|
+
if (web.statusBarStyle != null) head.push(tag('apple-mobile-web-app-status-bar-style', web.statusBarStyle))
|
|
4229
|
+
|
|
4230
|
+
if (web.startupImage != null) {
|
|
4231
|
+
const startup = Array.isArray(web.startupImage) ? web.startupImage : [web.startupImage]
|
|
4232
|
+
|
|
4233
|
+
for (const item of startup) {
|
|
4234
|
+
if (item == null) continue
|
|
4235
|
+
|
|
4236
|
+
const image = typeof item === 'object' && !(item instanceof URL)
|
|
4237
|
+
? (item as Record<string, unknown>)
|
|
4238
|
+
: { url: item }
|
|
4239
|
+
|
|
4240
|
+
head.push(
|
|
4241
|
+
createElement('link', {
|
|
4242
|
+
key: '__s_' + String(image.url).slice(0, 60) + '_' + String(image.media ?? ''),
|
|
4243
|
+
rel: 'apple-touch-startup-image',
|
|
4244
|
+
href: absolute(image.url),
|
|
4245
|
+
...(image.media ? { media: image.media } : {}),
|
|
4246
|
+
}),
|
|
4247
|
+
)
|
|
4248
|
+
}
|
|
4249
|
+
}
|
|
4250
|
+
}
|
|
4251
|
+
|
|
4252
|
+
// other is flattened in beside the named keys, because it is a place to put
|
|
4253
|
+
// meta tags rather than a meta tag by that name. A key at the top level
|
|
4254
|
+
// still renders - the type no longer invites one, but an app written
|
|
4255
|
+
// against the old shape must not silently lose its tags.
|
|
4256
|
+
const structured = new Set(['title', 'description', 'robots', 'metadataBase', 'openGraph', 'twitter', 'icons', 'appleWebApp', 'other'])
|
|
4257
|
+
const named = Object.entries(md).filter(([k]) => !structured.has(k))
|
|
4258
|
+
const extra = Object.entries((md.other ?? {}) as Record<string, unknown>)
|
|
4259
|
+
|
|
4260
|
+
for (const [k, v] of [...named, ...extra]) {
|
|
4261
|
+
if (v == null) continue
|
|
4262
|
+
|
|
4263
|
+
if (Array.isArray(v)) {
|
|
4264
|
+
for (const item of v) head.push(tag(k, item))
|
|
4265
|
+
} else {
|
|
4266
|
+
head.push(tag(k, v))
|
|
4267
|
+
}
|
|
4247
4268
|
}
|
|
4248
4269
|
}
|
|
4270
|
+
|
|
4271
|
+
|
|
4272
|
+
return head
|
|
4273
|
+
}
|
|
4274
|
+
|
|
4275
|
+
// Static metadata - export const metadata on every file of the route -
|
|
4276
|
+
// costs nothing to resolve, and is in the shell. A generateMetadata is
|
|
4277
|
+
// data: it streams, under its own boundary, like any other read, so the
|
|
4278
|
+
// page paints at once and the title arrives when it is ready. Never held
|
|
4279
|
+
// up for it. A crawler, which needs the title and the status in what it
|
|
4280
|
+
// reads first, is answered once the whole document is ready - see the host.
|
|
4281
|
+
if (!metadataStreams(component, layouts)) {
|
|
4282
|
+
head.push(...metadataHead(await resolveMetadata(component, props, layouts, metadataParams)))
|
|
4283
|
+
} else {
|
|
4284
|
+
head.push(
|
|
4285
|
+
createElement(
|
|
4286
|
+
Suspense,
|
|
4287
|
+
{ key: '__md', fallback: null },
|
|
4288
|
+
createElement(StreamedMetadata as never, {
|
|
4289
|
+
load: () => resolveMetadata(component, props, layouts, metadataParams, true),
|
|
4290
|
+
render: metadataHead,
|
|
4291
|
+
}),
|
|
4292
|
+
),
|
|
4293
|
+
)
|
|
4249
4294
|
}
|
|
4250
4295
|
|
|
4251
4296
|
// Metadata elements are rendered INSIDE the document tree so React 19 hoists
|
|
@@ -4503,7 +4548,7 @@ export async function handleRscHtmlStream(
|
|
|
4503
4548
|
const [forHtml, forPayload] = flight.tee()
|
|
4504
4549
|
const rscPayloadPromise = new Response(forPayload).text()
|
|
4505
4550
|
const ssr = await (import.meta as any).viteRsc.loadModule('ssr', 'index')
|
|
4506
|
-
const htmlStream = await ssr.handleSsr(forHtml, nonce, undefined, bootstrap, undefined, requestSearch())
|
|
4551
|
+
const htmlStream = await ssr.handleSsr(forHtml, nonce, undefined, bootstrap, undefined, requestSearch(), requestFromCrawler())
|
|
4507
4552
|
return { htmlStream, rscPayloadPromise, clientChunks: {} }
|
|
4508
4553
|
}
|
|
4509
4554
|
|
|
@@ -4550,7 +4595,7 @@ export async function handleRscFormPost(
|
|
|
4550
4595
|
{ onError: flightOnError },
|
|
4551
4596
|
)
|
|
4552
4597
|
const ssr = await (import.meta as any).viteRsc.loadModule('ssr', 'index')
|
|
4553
|
-
const htmlStream = await ssr.handleSsr(flight, nonce, undefined, bootstrap, formState, requestSearch())
|
|
4598
|
+
const htmlStream = await ssr.handleSsr(flight, nonce, undefined, bootstrap, formState, requestSearch(), requestFromCrawler())
|
|
4554
4599
|
|
|
4555
4600
|
return { htmlStream }
|
|
4556
4601
|
}
|
|
@@ -4620,11 +4665,10 @@ export async function handleRscResume(
|
|
|
4620
4665
|
// rendered here for the url and hydrated against a payload that, like
|
|
4621
4666
|
// the shell, does not know it. The browser fills it in.
|
|
4622
4667
|
pageKey === '' ? null : pathname,
|
|
4623
|
-
//
|
|
4624
|
-
//
|
|
4625
|
-
//
|
|
4626
|
-
|
|
4627
|
-
pageKey === '' ? new Promise<Record<string, unknown>>(() => {}) : undefined,
|
|
4668
|
+
// The metadata is the real params' too. A generateMetadata streams
|
|
4669
|
+
// under its own boundary, which the pattern probe left as a hole; this
|
|
4670
|
+
// is what fills it, with the page's real title.
|
|
4671
|
+
undefined,
|
|
4628
4672
|
),
|
|
4629
4673
|
{ onError: flightOnError },
|
|
4630
4674
|
)
|
|
@@ -4984,6 +5028,11 @@ export async function resolveMetadata(
|
|
|
4984
5028
|
// The page's own params promise, when the caller has one. During the
|
|
4985
5029
|
// pattern probe it never settles - see handleRscPprShell.
|
|
4986
5030
|
params?: Promise<Record<string, unknown>>,
|
|
5031
|
+
// Rendered under its own boundary: no grace to race. A generateMetadata
|
|
5032
|
+
// waiting on params that never settle - the build's pattern probe - is a
|
|
5033
|
+
// hole like any other read, filled per request with the real title,
|
|
5034
|
+
// instead of an empty one frozen into the shell.
|
|
5035
|
+
streamed = false,
|
|
4987
5036
|
): Promise<Record<string, unknown> | null> {
|
|
4988
5037
|
await instrumented()
|
|
4989
5038
|
|
|
@@ -5010,7 +5059,7 @@ export async function resolveMetadata(
|
|
|
5010
5059
|
searchParams: pageSearchParams(),
|
|
5011
5060
|
})
|
|
5012
5061
|
|
|
5013
|
-
const settled = params
|
|
5062
|
+
const settled = params && !streamed
|
|
5014
5063
|
? await Promise.race([
|
|
5015
5064
|
Promise.resolve(generated),
|
|
5016
5065
|
new Promise<typeof METADATA_POSTPONED>((resolve) =>
|
|
@@ -5700,6 +5749,9 @@ export async function handleSsr(
|
|
|
5700
5749
|
bootstrap = true,
|
|
5701
5750
|
formState?: unknown,
|
|
5702
5751
|
search?: string | null,
|
|
5752
|
+
// A crawler's request: answered once everything has rendered - see
|
|
5753
|
+
// crawler.ts - so the host reads the page's final word on its status.
|
|
5754
|
+
whole = false,
|
|
5703
5755
|
): Promise<ReadableStream> {
|
|
5704
5756
|
const root = withSearch(await createFromReadableStream(rscStream), search)
|
|
5705
5757
|
|
|
@@ -5725,6 +5777,8 @@ export async function handleSsr(
|
|
|
5725
5777
|
...(formState !== undefined ? { formState: formState as any } : {}),
|
|
5726
5778
|
})
|
|
5727
5779
|
|
|
5780
|
+
if (whole) await html.allReady
|
|
5781
|
+
|
|
5728
5782
|
return DEV_ORIGIN ? rewriteViteDevUrlStream(html, DEV_ORIGIN) : html
|
|
5729
5783
|
}
|
|
5730
5784
|
|