@rsc-kit/core 0.30.14 → 0.30.15

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/vite.js CHANGED
@@ -24,7 +24,7 @@ import { loadEnv, parseAst } from "vite";
24
24
  import { stampDisplayNames } from "./stableNames.js";
25
25
  import { REPORT_FILE, buildReport } from "./buildReport.js";
26
26
  import { MANIFEST_PATH, manifestWarning, webManifest } from "./webManifest.js";
27
- import { ASSET_BASE, allAppAssets, appAssets, headTags, typeOf, } from "./appAssets.js";
27
+ import { ASSET_BASE, allAppAssets, assetFile, appAssets, headTags, shareImageUrls, typeOf, } from "./appAssets.js";
28
28
  import { reactCacheImports } from "./reactCache.js";
29
29
  import { clientEntries, clientScanPlugin, engineClientEntries, } from "./clientEntries.js";
30
30
  import { serverImportsOfClientPackages } from "./clientImports.js";
@@ -195,8 +195,7 @@ let foundAssets = {
195
195
  appleIcon: null,
196
196
  appleSplash: [],
197
197
  screenshots: [],
198
- openGraph: null,
199
- twitter: null,
198
+ shareImages: {},
200
199
  };
201
200
  /** Host functions to generate stubs for — see RscKitOptions.hostActions. */
202
201
  let hostActions;
@@ -1393,7 +1392,7 @@ function copyAppAssets(clientDir) {
1393
1392
  if (all.length === 0)
1394
1393
  return;
1395
1394
  for (const asset of all) {
1396
- const to = join(clientDir, asset.href.slice(1));
1395
+ const to = join(clientDir, assetFile(asset.href.slice(1)));
1397
1396
  mkdirSync(dirname(to), { recursive: true });
1398
1397
  copyFileSync(join(sourceDir, "app", asset.file), to);
1399
1398
  }
@@ -2424,6 +2423,8 @@ const API_METHODS = [
2424
2423
  ];
2425
2424
  /** `orders.section.tsx` — a region of a page that can be refreshed by name. */
2426
2425
  const SECTION_FILE = /\.section\.(tsx|jsx|ts|js)$/;
2426
+ /** A share image file: the pages beneath its folder change what they say about themselves. */
2427
+ const SHARE_IMAGE_FILE = /^(?:opengraph|twitter)-image[-\w]*\.(?:png|svg|jpe?g|webp|gif|avif)$/i;
2427
2428
  /**
2428
2429
  * Whether a page or a section declares refreshOn. Read from the source, as
2429
2430
  * section names are, because the manifest is written before any bundle exists.
@@ -3146,6 +3147,7 @@ ${
3146
3147
  import { SegmentBoundary } from ${JSON.stringify(join(packageDir, "js/SegmentBoundary"))}
3147
3148
  import { LoadingBoundary } from ${JSON.stringify(join(packageDir, "js/LoadingBoundary"))}
3148
3149
  import { DocumentTitle } from ${JSON.stringify(join(packageDir, "js/DocumentTitle"))}
3150
+ import { depthOf, fileWins, folderOf, nearestShareImage } from ${JSON.stringify(join(packageDir, "shareImages"))}
3149
3151
  import { SlotBoundary } from ${JSON.stringify(join(packageDir, "js/SlotBoundary"))}
3150
3152
  import { RouteErrorBoundary } from ${JSON.stringify(join(packageDir, "js/RouteErrorBoundary"))}
3151
3153
  import __rsc_assets from 'virtual:vite-rsc/assets-manifest'
@@ -3199,6 +3201,9 @@ ${mapEntries.join("\n")}
3199
3201
  /** Head tags for the icons and share images found in app/. */
3200
3202
  const APP_HEAD: { tag: any; props: Record<string, string> }[] = ${JSON.stringify(headTags(foundAssets))}
3201
3203
 
3204
+ /** The share images found under app/, by the folder each is in - see shareImages.ts. */
3205
+ const SHARE_IMAGES: Record<string, { openGraph?: string; twitter?: string }> = ${JSON.stringify(shareImageUrls(foundAssets))}
3206
+
3202
3207
  const WEB_MANIFEST: { href: string; themeColor?: string } | null = ${JSON.stringify(webManifestOptions
3203
3208
  ? {
3204
3209
  href: MANIFEST_PATH,
@@ -4199,13 +4204,35 @@ async function renderTree(
4199
4204
  }
4200
4205
 
4201
4206
  for (const [i, found] of APP_HEAD.entries()) {
4202
- const props = { ...found.props }
4207
+ head.push(createElement(found.tag, { key: '__a' + i, ...found.props }))
4208
+ }
4203
4209
 
4204
- if (props.content && /^(og:image|twitter:image)$/.test(props.property ?? props.name ?? '')) {
4205
- props.content = absolute(props.content)
4206
- }
4210
+ // The share image files found in app/. The nearest to this page is its
4211
+ // card - unless its metadata exports images from a segment nearer still,
4212
+ // in which case the file is left out and the exported ones render, so a
4213
+ // page that names its own image replaces the site's instead of following
4214
+ // it. Scrapers read the first og:image they meet; a second after the
4215
+ // site's would be a card that never changes.
4216
+ const folder = folderOf(component)
4217
+ const sources = md ? imageSources.get(md) : undefined
4218
+ const shared = (kind: 'openGraph' | 'twitter') => {
4219
+ const file = nearestShareImage(SHARE_IMAGES, folder, kind)
4220
+ const source = sources?.[kind]
4221
+
4222
+ return fileWins(file, source?.exports ? source : null) ? file : null
4223
+ }
4224
+ const ogFile = shared('openGraph')
4225
+ const twitterFile = shared('twitter')
4207
4226
 
4208
- head.push(createElement(found.tag, { key: '__a' + i, ...props }))
4227
+ if (ogFile) head.push(createElement('meta', { key: '__og', property: 'og:image', content: absolute(ogFile.href) }))
4228
+
4229
+ if (twitterFile) {
4230
+ head.push(createElement('meta', { key: '__tw', name: 'twitter:image', content: absolute(twitterFile.href) }))
4231
+
4232
+ // The card a twitter image implies, unless the page named its own.
4233
+ if ((md?.twitter as { card?: unknown } | undefined)?.card == null) {
4234
+ head.push(createElement('meta', { key: '__twc', name: 'twitter:card', content: 'summary_large_image' }))
4235
+ }
4209
4236
  }
4210
4237
 
4211
4238
  if (md) {
@@ -4305,7 +4332,7 @@ async function renderTree(
4305
4332
  if (og.siteName != null) head.push(tag('og:site_name', og.siteName))
4306
4333
  if (og.type != null) head.push(tag('og:type', og.type))
4307
4334
  if (og.locale != null) head.push(tag('og:locale', og.locale))
4308
- if (og.images != null) images('og:image', og.images)
4335
+ if (og.images != null && !ogFile) images('og:image', og.images)
4309
4336
  }
4310
4337
 
4311
4338
  if (md.twitter) {
@@ -4316,7 +4343,7 @@ async function renderTree(
4316
4343
  if (tw.description != null) head.push(tag('twitter:description', tw.description))
4317
4344
  if (tw.site != null) head.push(tag('twitter:site', tw.site))
4318
4345
  if (tw.creator != null) head.push(tag('twitter:creator', tw.creator))
4319
- if (tw.images != null) images('twitter:image', tw.images)
4346
+ if (tw.images != null && !twitterFile) images('twitter:image', tw.images)
4320
4347
  }
4321
4348
 
4322
4349
  // icons is links, not meta. A string is one icon; an object names which
@@ -5209,6 +5236,17 @@ const METADATA_PROBE_GRACE_MS = 20
5209
5236
 
5210
5237
  const METADATA_POSTPONED = Symbol('metadata postponed')
5211
5238
 
5239
+ /**
5240
+ * For each of openGraph and twitter, how deep the segment that supplied the
5241
+ * merged object is, and whether it exports images - the other half of the
5242
+ * comparison a share image file is held to. Beside the metadata rather than
5243
+ * inside it, so the object a page's tags are rendered from stays only what
5244
+ * the app wrote.
5245
+ */
5246
+ type ImageSources = { openGraph?: { depth: number; exports: boolean }; twitter?: { depth: number; exports: boolean } }
5247
+
5248
+ const imageSources = new WeakMap<object, ImageSources>()
5249
+
5212
5250
  export async function resolveMetadata(
5213
5251
  component: string,
5214
5252
  props: Record<string, unknown> = {},
@@ -5273,18 +5311,27 @@ export async function resolveMetadata(
5273
5311
  // happened to declare one of its own.
5274
5312
  const merged: Record<string, unknown> = {}
5275
5313
  const other: Record<string, unknown> = {}
5314
+ const sources: ImageSources = {}
5276
5315
 
5277
- const take = (from: Record<string, unknown>) => {
5316
+ const take = (from: Record<string, unknown>, depth: number) => {
5278
5317
  for (const [k, v] of Object.entries(from)) {
5279
5318
  if (k === 'title') continue
5280
5319
  if (k === 'other') Object.assign(other, v as Record<string, unknown>)
5281
5320
  else merged[k] = v
5321
+
5322
+ // openGraph and twitter are replaced whole by a nearer segment's, so
5323
+ // whichever segment took them last is the one whose images - or lack of
5324
+ // them - the merged object carries. That segment's depth is what a file
5325
+ // in app/ is weighed against; see shareImages.ts.
5326
+ if (k === 'openGraph' || k === 'twitter') {
5327
+ sources[k] = { depth, exports: typeof v === 'object' && v !== null && (v as { images?: unknown }).images != null }
5328
+ }
5282
5329
  }
5283
5330
  }
5284
5331
 
5285
- for (const m of layoutMeta) take(m)
5332
+ layoutMeta.forEach((m, i) => take(m, depthOf(folderOf(layouts[i].component))))
5286
5333
 
5287
- take(page)
5334
+ take(page, depthOf(folderOf(component)))
5288
5335
 
5289
5336
  if (Object.keys(other).length > 0) merged.other = other
5290
5337
 
@@ -5309,7 +5356,11 @@ export async function resolveMetadata(
5309
5356
  }
5310
5357
  if (title != null) merged.title = title
5311
5358
 
5312
- return Object.keys(merged).length ? merged : null
5359
+ if (Object.keys(merged).length === 0) return null
5360
+
5361
+ imageSources.set(merged, sources)
5362
+
5363
+ return merged
5313
5364
  }
5314
5365
 
5315
5366
  // Buffered render (worker: rsc / rscWithoutCallbacks — used at prerender time).
@@ -7637,7 +7688,8 @@ export function rscKit(options = {}) {
7637
7688
  res.end(webManifest(webManifestOptions));
7638
7689
  return;
7639
7690
  }
7640
- const asset = allAppAssets(foundAssets).find((a) => a.href === url);
7691
+ // Either spelling of a bracket: the tag says %5B, a hand-typed url says [.
7692
+ const asset = allAppAssets(foundAssets).find((a) => assetFile(a.href) === assetFile(url));
7641
7693
  const file = asset ? join(sourceDir, "app", asset.file) : null;
7642
7694
  if (!asset || !file || !existsSync(file))
7643
7695
  return next();
@@ -7724,6 +7776,7 @@ export function rscKit(options = {}) {
7724
7776
  // published without any — generic-host.test.ts fails if it reappears.
7725
7777
  return ((base !== stem && shapes.has(stem)) ||
7726
7778
  SECTION_FILE.test(base) ||
7779
+ SHARE_IMAGE_FILE.test(base) ||
7727
7780
  (routeConfig !== null && base === routeConfig.file));
7728
7781
  };
7729
7782
  const restart = (file) => {