@rsc-kit/core 0.29.7 → 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 +256 -191
- 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
|
@@ -351,7 +351,11 @@ function aliasEntries() {
|
|
|
351
351
|
function resolvePaths(options) {
|
|
352
352
|
projectRoot = resolve(options.projectRoot || process.env.RSC_PROJECT_ROOT || process.cwd());
|
|
353
353
|
sourceDir = resolve(options.sourceDir || process.env.RSC_SOURCE_DIR || join(projectRoot, "src"));
|
|
354
|
-
outDir = resolve(
|
|
354
|
+
outDir = resolve(
|
|
355
|
+
// .rsc-kit/: the one folder rsc-kit generates into - the types beside
|
|
356
|
+
// the route table, the build report and the server entries - rather
|
|
357
|
+
// than a second one beside it.
|
|
358
|
+
options.outDir || process.env.RSC_OUT_DIR || join(projectRoot, ".rsc-kit"));
|
|
355
359
|
appDir = join(sourceDir, "app");
|
|
356
360
|
// Generated entries live under the (in-project) out dir so module resolution
|
|
357
361
|
// can walk up to the project's node_modules (@vitejs/plugin-rsc, react, ...).
|
|
@@ -3019,7 +3023,7 @@ import { isSharedSection, sectionComponent } from ${JSON.stringify(join(packageD
|
|
|
3019
3023
|
import { shareRender } from ${JSON.stringify(join(packageDir, "sharedRenders"))}
|
|
3020
3024
|
import { PathnameProvider } from ${JSON.stringify(join(packageDir, "js/PathnameProvider"))}
|
|
3021
3025
|
import { DefaultRouteError } from ${JSON.stringify(join(packageDir, "js/DefaultRouteError"))}
|
|
3022
|
-
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"))}
|
|
3023
3027
|
import { parseParams, parseSearchParams, parseBody, isSearchParamsError, isBodyError } from ${JSON.stringify(join(packageDir, "routeSchema"))}
|
|
3024
3028
|
import { notFoundDigest, isNotFoundSignal } from ${JSON.stringify(join(packageDir, "notFound"))}
|
|
3025
3029
|
import { noteRequestRead, notePageProps, urlOf } from ${JSON.stringify(join(packageDir, "request"))}
|
|
@@ -3938,6 +3942,22 @@ function buildElement(
|
|
|
3938
3942
|
// Resolve route metadata into React elements. React 19 hoists <title>/<meta>
|
|
3939
3943
|
// rendered anywhere in the tree into <head> — so the "vite way" for metadata is
|
|
3940
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
|
+
|
|
3941
3961
|
async function renderTree(
|
|
3942
3962
|
component: string,
|
|
3943
3963
|
props: Record<string, unknown>,
|
|
@@ -3963,7 +3983,6 @@ async function renderTree(
|
|
|
3963
3983
|
// The params promise travels too: during the pattern probe it never
|
|
3964
3984
|
// settles, and a generateMetadata that reads it is then left out of the
|
|
3965
3985
|
// shell rather than run against the placeholder - see resolveMetadata.
|
|
3966
|
-
const md = await resolveMetadata(component, props, layouts, metadataParams)
|
|
3967
3986
|
const head: unknown[] = []
|
|
3968
3987
|
|
|
3969
3988
|
// Rendered into the tree rather than written into the app's layout: React
|
|
@@ -3977,15 +3996,6 @@ async function renderTree(
|
|
|
3977
3996
|
// here, above the found images, because those are the ones most likely to
|
|
3978
3997
|
// be relative: an opengraph-image.png in app/ is emitted as /_app/..., and a
|
|
3979
3998
|
// share-card scraper needs the origin in front of it.
|
|
3980
|
-
const base = md?.metadataBase ? String(md.metadataBase) : null
|
|
3981
|
-
const absolute = (value: unknown): string => {
|
|
3982
|
-
const text = String(value instanceof URL ? value.href : value)
|
|
3983
|
-
|
|
3984
|
-
if (!base || /^[a-z][a-z0-9+.-]*:/i.test(text)) return text
|
|
3985
|
-
|
|
3986
|
-
return new URL(text, base).href
|
|
3987
|
-
}
|
|
3988
|
-
|
|
3989
3999
|
// What Next wrote into every document without being asked, and a layout
|
|
3990
4000
|
// copied from a Next app therefore never writes: the charset, and the
|
|
3991
4001
|
// viewport - without which a phone lays the page out at desktop width.
|
|
@@ -4013,16 +4023,6 @@ async function renderTree(
|
|
|
4013
4023
|
head.push(createElement('meta', { key: '__vcs', name: 'color-scheme', content: viewport.colorScheme }))
|
|
4014
4024
|
}
|
|
4015
4025
|
|
|
4016
|
-
for (const [i, found] of APP_HEAD.entries()) {
|
|
4017
|
-
const props = { ...found.props }
|
|
4018
|
-
|
|
4019
|
-
if (props.content && /^(og:image|twitter:image)$/.test(props.property ?? props.name ?? '')) {
|
|
4020
|
-
props.content = absolute(props.content)
|
|
4021
|
-
}
|
|
4022
|
-
|
|
4023
|
-
head.push(createElement(found.tag, { key: '__a' + i, ...props }))
|
|
4024
|
-
}
|
|
4025
|
-
|
|
4026
4026
|
if (WEB_MANIFEST) {
|
|
4027
4027
|
head.push(createElement('link', { key: '__mf', rel: 'manifest', href: WEB_MANIFEST.href }))
|
|
4028
4028
|
|
|
@@ -4033,215 +4033,264 @@ async function renderTree(
|
|
|
4033
4033
|
}
|
|
4034
4034
|
}
|
|
4035
4035
|
|
|
4036
|
-
if (md) {
|
|
4037
|
-
if (md.title != null) {
|
|
4038
|
-
// The element is what a server render puts in <head>, and what a route
|
|
4039
|
-
// with no runtime relies on entirely.
|
|
4040
|
-
head.push(createElement('title', { key: '__t' }, String(md.title)))
|
|
4041
|
-
|
|
4042
|
-
// And the effect is what keeps it right once pages are retained — see
|
|
4043
|
-
// DocumentTitle. Only where there is a runtime to run it: a client
|
|
4044
|
-
// component on a route that ships none is refused by the build.
|
|
4045
|
-
if (bootstrap) head.push(createElement(DocumentTitle, { key: '__ts', title: String(md.title) }))
|
|
4046
|
-
}
|
|
4047
|
-
if (md.description != null) head.push(createElement('meta', { key: '__d', name: 'description', content: String(md.description) }))
|
|
4048
|
-
// The name only, never the version - see the identify option.
|
|
4049
|
-
if (manifest().build?.identify) head.push(createElement('meta', { key: '__g', name: 'generator', content: 'rsc-kit' }))
|
|
4050
|
-
}
|
|
4051
|
-
|
|
4052
4036
|
// Which build this document is, for the client to say on every navigation
|
|
4053
4037
|
// - read from the document rather than learned from the first answer,
|
|
4054
4038
|
// because a document a service worker served from its cache is the last
|
|
4055
4039
|
// build's while the answers are this one's. An id, not a version number.
|
|
4056
4040
|
if (bootstrap) head.push(createElement('meta', { key: '__v', name: 'rsc-kit:build', content: await buildId() }))
|
|
4057
4041
|
|
|
4058
|
-
|
|
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
|
|
4059
4052
|
|
|
4060
|
-
|
|
4061
|
-
|
|
4062
|
-
// is the same shape for the googlebot tag. The object used to fall
|
|
4063
|
-
// through to the catch-all below as "[object Object]" - which no crawler
|
|
4064
|
-
// reads, on the one page that asked not to be indexed.
|
|
4065
|
-
const robotsContent = (value: unknown): string => {
|
|
4066
|
-
if (typeof value !== 'object' || value === null) return String(value)
|
|
4053
|
+
return new URL(text, base).href
|
|
4054
|
+
}
|
|
4067
4055
|
|
|
4068
|
-
|
|
4069
|
-
const
|
|
4056
|
+
for (const [i, found] of APP_HEAD.entries()) {
|
|
4057
|
+
const props = { ...found.props }
|
|
4070
4058
|
|
|
4071
|
-
if (
|
|
4072
|
-
|
|
4073
|
-
for (const flag of ['noarchive', 'nosnippet', 'noimageindex', 'nocache', 'notranslate', 'indexifembedded', 'nositelinkssearchbox']) {
|
|
4074
|
-
if (r[flag]) parts.push(flag)
|
|
4075
|
-
}
|
|
4076
|
-
if (r.unavailable_after != null) parts.push('unavailable_after: ' + String(r.unavailable_after))
|
|
4077
|
-
for (const limit of ['max-video-preview', 'max-image-preview', 'max-snippet']) {
|
|
4078
|
-
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)
|
|
4079
4061
|
}
|
|
4080
4062
|
|
|
4081
|
-
|
|
4063
|
+
head.push(createElement(found.tag, { key: '__a' + i, ...props }))
|
|
4082
4064
|
}
|
|
4083
4065
|
|
|
4084
|
-
if (md
|
|
4085
|
-
|
|
4086
|
-
|
|
4087
|
-
|
|
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)))
|
|
4088
4071
|
|
|
4089
|
-
|
|
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' }))
|
|
4090
4080
|
}
|
|
4091
4081
|
|
|
4092
|
-
|
|
4093
|
-
// Slack's, and LinkedIn's - reads only property=, so every og tag this
|
|
4094
|
-
// used to emit with name= was invisible to the thing it existed for.
|
|
4095
|
-
// Twitter reads name=, which is why the two are not one rule.
|
|
4096
|
-
const isProperty = (k: string) => /^(og|article|profile|book|music|video|fb):/.test(k)
|
|
4097
|
-
// The keys whose CONTENT is a url. Exact, plus the :url and :secure_url
|
|
4098
|
-
// spellings - not og:image:width, which a looser match turned into
|
|
4099
|
-
// https://site/1200 and no scraper would ever read.
|
|
4100
|
-
const isUrlKey = (k: string) => /^(og:image|og:url|twitter:image)(:(secure_)?url)?$/.test(k)
|
|
4101
|
-
const tag = (k: string, v: unknown) =>
|
|
4102
|
-
createElement('meta', {
|
|
4103
|
-
key: '__m_' + k + '_' + String(v).slice(0, 40),
|
|
4104
|
-
[isProperty(k) ? 'property' : 'name']: k,
|
|
4105
|
-
content: isUrlKey(k) ? absolute(v) : String(v),
|
|
4106
|
-
})
|
|
4082
|
+
if (md) {
|
|
4107
4083
|
|
|
4108
|
-
|
|
4109
|
-
|
|
4110
|
-
|
|
4111
|
-
|
|
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)
|
|
4112
4091
|
|
|
4113
|
-
|
|
4114
|
-
|
|
4092
|
+
const r = value as Record<string, unknown>
|
|
4093
|
+
const parts: string[] = []
|
|
4115
4094
|
|
|
4116
|
-
if (
|
|
4117
|
-
|
|
4118
|
-
|
|
4119
|
-
|
|
4120
|
-
if (image.width) head.push(tag(prefix + ':width', image.width))
|
|
4121
|
-
if (image.height) head.push(tag(prefix + ':height', image.height))
|
|
4122
|
-
if (image.alt) head.push(tag(prefix + ':alt', image.alt))
|
|
4123
|
-
if (image.type) head.push(tag(prefix + ':type', image.type))
|
|
4124
|
-
} else {
|
|
4125
|
-
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)
|
|
4126
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(', ')
|
|
4127
4106
|
}
|
|
4128
|
-
}
|
|
4129
4107
|
|
|
4130
|
-
|
|
4131
|
-
|
|
4108
|
+
if (md.robots != null) {
|
|
4109
|
+
head.push(createElement('meta', { key: '__r', name: 'robots', content: robotsContent(md.robots) }))
|
|
4132
4110
|
|
|
4133
|
-
|
|
4134
|
-
if (og.description != null) head.push(tag('og:description', og.description))
|
|
4135
|
-
if (og.url != null) head.push(tag('og:url', og.url))
|
|
4136
|
-
if (og.siteName != null) head.push(tag('og:site_name', og.siteName))
|
|
4137
|
-
if (og.type != null) head.push(tag('og:type', og.type))
|
|
4138
|
-
if (og.locale != null) head.push(tag('og:locale', og.locale))
|
|
4139
|
-
if (og.images != null) images('og:image', og.images)
|
|
4140
|
-
}
|
|
4111
|
+
const bot = typeof md.robots === 'object' ? (md.robots as { googleBot?: unknown }).googleBot : null
|
|
4141
4112
|
|
|
4142
|
-
|
|
4143
|
-
|
|
4113
|
+
if (bot != null) head.push(createElement('meta', { key: '__rg', name: 'googlebot', content: robotsContent(bot) }))
|
|
4114
|
+
}
|
|
4144
4115
|
|
|
4145
|
-
|
|
4146
|
-
|
|
4147
|
-
|
|
4148
|
-
|
|
4149
|
-
|
|
4150
|
-
|
|
4151
|
-
|
|
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
|
|
4152
4139
|
|
|
4153
|
-
|
|
4154
|
-
|
|
4155
|
-
|
|
4156
|
-
|
|
4157
|
-
|
|
4158
|
-
|
|
4159
|
-
|
|
4160
|
-
|
|
4161
|
-
|
|
4162
|
-
|
|
4163
|
-
|
|
4164
|
-
|
|
4165
|
-
|
|
4166
|
-
head.push(
|
|
4167
|
-
createElement('link', {
|
|
4168
|
-
key: '__i_' + rel + '_' + String(icon.url).slice(0, 40),
|
|
4169
|
-
rel: (icon.rel as string) ?? rel,
|
|
4170
|
-
href: absolute(icon.url),
|
|
4171
|
-
...(icon.type ? { type: icon.type } : {}),
|
|
4172
|
-
...(icon.sizes ? { sizes: icon.sizes } : {}),
|
|
4173
|
-
...(icon.media ? { media: icon.media } : {}),
|
|
4174
|
-
}),
|
|
4175
|
-
)
|
|
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
|
+
}
|
|
4176
4152
|
}
|
|
4177
|
-
}
|
|
4178
4153
|
|
|
4179
|
-
|
|
4180
|
-
|
|
4154
|
+
if (md.openGraph) {
|
|
4155
|
+
const og = md.openGraph as Record<string, unknown>
|
|
4181
4156
|
|
|
4182
|
-
|
|
4183
|
-
|
|
4184
|
-
|
|
4185
|
-
if (
|
|
4186
|
-
if (
|
|
4187
|
-
if (
|
|
4188
|
-
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)
|
|
4189
4164
|
}
|
|
4190
|
-
}
|
|
4191
4165
|
|
|
4192
|
-
|
|
4193
|
-
|
|
4194
|
-
// and reads the plain one, and Next switched between them in a minor.
|
|
4195
|
-
if (md.appleWebApp != null && md.appleWebApp !== false) {
|
|
4196
|
-
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>
|
|
4197
4168
|
|
|
4198
|
-
|
|
4199
|
-
head.push(tag('
|
|
4200
|
-
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)
|
|
4201
4175
|
}
|
|
4202
4176
|
|
|
4203
|
-
|
|
4204
|
-
|
|
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]
|
|
4205
4182
|
|
|
4206
|
-
|
|
4207
|
-
const startup = Array.isArray(web.startupImage) ? web.startupImage : [web.startupImage]
|
|
4208
|
-
|
|
4209
|
-
for (const item of startup) {
|
|
4183
|
+
for (const item of list) {
|
|
4210
4184
|
if (item == null) continue
|
|
4211
4185
|
|
|
4212
|
-
const
|
|
4186
|
+
const icon = typeof item === 'object' && !(item instanceof URL)
|
|
4213
4187
|
? (item as Record<string, unknown>)
|
|
4214
4188
|
: { url: item }
|
|
4215
4189
|
|
|
4216
4190
|
head.push(
|
|
4217
4191
|
createElement('link', {
|
|
4218
|
-
key: '
|
|
4219
|
-
rel:
|
|
4220
|
-
href: absolute(
|
|
4221
|
-
...(
|
|
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 } : {}),
|
|
4222
4198
|
}),
|
|
4223
4199
|
)
|
|
4224
4200
|
}
|
|
4225
4201
|
}
|
|
4226
|
-
}
|
|
4227
4202
|
|
|
4228
|
-
|
|
4229
|
-
|
|
4230
|
-
|
|
4231
|
-
|
|
4232
|
-
|
|
4233
|
-
|
|
4234
|
-
|
|
4235
|
-
|
|
4236
|
-
|
|
4237
|
-
|
|
4238
|
-
|
|
4239
|
-
|
|
4240
|
-
|
|
4241
|
-
|
|
4242
|
-
|
|
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
|
+
}
|
|
4243
4268
|
}
|
|
4244
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
|
+
)
|
|
4245
4294
|
}
|
|
4246
4295
|
|
|
4247
4296
|
// Metadata elements are rendered INSIDE the document tree so React 19 hoists
|
|
@@ -4499,7 +4548,7 @@ export async function handleRscHtmlStream(
|
|
|
4499
4548
|
const [forHtml, forPayload] = flight.tee()
|
|
4500
4549
|
const rscPayloadPromise = new Response(forPayload).text()
|
|
4501
4550
|
const ssr = await (import.meta as any).viteRsc.loadModule('ssr', 'index')
|
|
4502
|
-
const htmlStream = await ssr.handleSsr(forHtml, nonce, undefined, bootstrap, undefined, requestSearch())
|
|
4551
|
+
const htmlStream = await ssr.handleSsr(forHtml, nonce, undefined, bootstrap, undefined, requestSearch(), requestFromCrawler())
|
|
4503
4552
|
return { htmlStream, rscPayloadPromise, clientChunks: {} }
|
|
4504
4553
|
}
|
|
4505
4554
|
|
|
@@ -4546,7 +4595,7 @@ export async function handleRscFormPost(
|
|
|
4546
4595
|
{ onError: flightOnError },
|
|
4547
4596
|
)
|
|
4548
4597
|
const ssr = await (import.meta as any).viteRsc.loadModule('ssr', 'index')
|
|
4549
|
-
const htmlStream = await ssr.handleSsr(flight, nonce, undefined, bootstrap, formState, requestSearch())
|
|
4598
|
+
const htmlStream = await ssr.handleSsr(flight, nonce, undefined, bootstrap, formState, requestSearch(), requestFromCrawler())
|
|
4550
4599
|
|
|
4551
4600
|
return { htmlStream }
|
|
4552
4601
|
}
|
|
@@ -4616,11 +4665,10 @@ export async function handleRscResume(
|
|
|
4616
4665
|
// rendered here for the url and hydrated against a payload that, like
|
|
4617
4666
|
// the shell, does not know it. The browser fills it in.
|
|
4618
4667
|
pageKey === '' ? null : pathname,
|
|
4619
|
-
//
|
|
4620
|
-
//
|
|
4621
|
-
//
|
|
4622
|
-
|
|
4623
|
-
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,
|
|
4624
4672
|
),
|
|
4625
4673
|
{ onError: flightOnError },
|
|
4626
4674
|
)
|
|
@@ -4980,6 +5028,11 @@ export async function resolveMetadata(
|
|
|
4980
5028
|
// The page's own params promise, when the caller has one. During the
|
|
4981
5029
|
// pattern probe it never settles - see handleRscPprShell.
|
|
4982
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,
|
|
4983
5036
|
): Promise<Record<string, unknown> | null> {
|
|
4984
5037
|
await instrumented()
|
|
4985
5038
|
|
|
@@ -5006,7 +5059,7 @@ export async function resolveMetadata(
|
|
|
5006
5059
|
searchParams: pageSearchParams(),
|
|
5007
5060
|
})
|
|
5008
5061
|
|
|
5009
|
-
const settled = params
|
|
5062
|
+
const settled = params && !streamed
|
|
5010
5063
|
? await Promise.race([
|
|
5011
5064
|
Promise.resolve(generated),
|
|
5012
5065
|
new Promise<typeof METADATA_POSTPONED>((resolve) =>
|
|
@@ -5696,6 +5749,9 @@ export async function handleSsr(
|
|
|
5696
5749
|
bootstrap = true,
|
|
5697
5750
|
formState?: unknown,
|
|
5698
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,
|
|
5699
5755
|
): Promise<ReadableStream> {
|
|
5700
5756
|
const root = withSearch(await createFromReadableStream(rscStream), search)
|
|
5701
5757
|
|
|
@@ -5721,6 +5777,8 @@ export async function handleSsr(
|
|
|
5721
5777
|
...(formState !== undefined ? { formState: formState as any } : {}),
|
|
5722
5778
|
})
|
|
5723
5779
|
|
|
5780
|
+
if (whole) await html.allReady
|
|
5781
|
+
|
|
5724
5782
|
return DEV_ORIGIN ? rewriteViteDevUrlStream(html, DEV_ORIGIN) : html
|
|
5725
5783
|
}
|
|
5726
5784
|
|
|
@@ -6746,7 +6804,14 @@ export function rscKit(options = {}) {
|
|
|
6746
6804
|
return [
|
|
6747
6805
|
join(projectRoot, HOST_FILE),
|
|
6748
6806
|
join(sourceDir, HOST_ACTIONS_MODULE),
|
|
6749
|
-
|
|
6807
|
+
// The type declarations only: the folder also holds the build's
|
|
6808
|
+
// own output - the route table, its report, the server entries -
|
|
6809
|
+
// which nobody commits and --check has no business comparing.
|
|
6810
|
+
...(existsSync(typesDir)
|
|
6811
|
+
? readdirSync(typesDir)
|
|
6812
|
+
.filter((name) => name.endsWith(".d.ts"))
|
|
6813
|
+
.map((name) => join(typesDir, name))
|
|
6814
|
+
: []),
|
|
6750
6815
|
];
|
|
6751
6816
|
},
|
|
6752
6817
|
},
|