@rsc-kit/core 0.13.1 → 0.14.0

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
@@ -2245,8 +2245,27 @@ async function renderTree(
2245
2245
  // Icons and share images the build found in app/. Rendered here so React
2246
2246
  // hoists them into <head>, the same way the manifest link is — an app never
2247
2247
  // edits its layout to get a favicon.
2248
- for (const [i, tag] of APP_HEAD.entries()) {
2249
- head.push(createElement(tag.tag, { key: '__a' + i, ...tag.props }))
2248
+ // Where the site lives, for making a relative image or url absolute. Read
2249
+ // here, above the found images, because those are the ones most likely to
2250
+ // be relative: an opengraph-image.png in app/ is emitted as /_app/..., and a
2251
+ // share-card scraper needs the origin in front of it.
2252
+ const base = md?.metadataBase ? String(md.metadataBase) : null
2253
+ const absolute = (value: unknown): string => {
2254
+ const text = String(value instanceof URL ? value.href : value)
2255
+
2256
+ if (!base || /^[a-z][a-z0-9+.-]*:/i.test(text)) return text
2257
+
2258
+ return new URL(text, base).href
2259
+ }
2260
+
2261
+ for (const [i, found] of APP_HEAD.entries()) {
2262
+ const props = { ...found.props }
2263
+
2264
+ if (props.content && /^(og:image|twitter:image)$/.test(props.property ?? props.name ?? '')) {
2265
+ props.content = absolute(props.content)
2266
+ }
2267
+
2268
+ head.push(createElement(found.tag, { key: '__a' + i, ...props }))
2250
2269
  }
2251
2270
 
2252
2271
  if (WEB_MANIFEST) {
@@ -2271,16 +2290,123 @@ async function renderTree(
2271
2290
  if (bootstrap) head.push(createElement(DocumentTitle, { key: '__ts', title: String(md.title) }))
2272
2291
  }
2273
2292
  if (md.description != null) head.push(createElement('meta', { key: '__d', name: 'description', content: String(md.description) }))
2293
+
2294
+ // og: and its relatives are PROPERTY, not name. Facebook's scraper - and
2295
+ // Slack's, and LinkedIn's - reads only property=, so every og tag this
2296
+ // used to emit with name= was invisible to the thing it existed for.
2297
+ // Twitter reads name=, which is why the two are not one rule.
2298
+ const isProperty = (k: string) => /^(og|article|profile|book|music|video|fb):/.test(k)
2299
+ // The keys whose CONTENT is a url. Exact, plus the :url and :secure_url
2300
+ // spellings - not og:image:width, which a looser match turned into
2301
+ // https://site/1200 and no scraper would ever read.
2302
+ const isUrlKey = (k: string) => /^(og:image|og:url|twitter:image)(:(secure_)?url)?$/.test(k)
2303
+ const tag = (k: string, v: unknown) =>
2304
+ createElement('meta', {
2305
+ key: '__m_' + k + '_' + String(v).slice(0, 40),
2306
+ [isProperty(k) ? 'property' : 'name']: k,
2307
+ content: isUrlKey(k) ? absolute(v) : String(v),
2308
+ })
2309
+
2310
+ // One image, or several. Each becomes its own og:image plus the size and
2311
+ // alt tags beside it, which is how a scraper is told which is which.
2312
+ const images = (prefix: string, value: unknown): void => {
2313
+ const list = Array.isArray(value) ? value : [value]
2314
+
2315
+ for (const item of list) {
2316
+ if (item == null) continue
2317
+
2318
+ if (typeof item === 'object' && !(item instanceof URL)) {
2319
+ const image = item as { url: unknown; width?: number; height?: number; alt?: string; type?: string }
2320
+
2321
+ head.push(tag(prefix, image.url))
2322
+ if (image.width) head.push(tag(prefix + ':width', image.width))
2323
+ if (image.height) head.push(tag(prefix + ':height', image.height))
2324
+ if (image.alt) head.push(tag(prefix + ':alt', image.alt))
2325
+ if (image.type) head.push(tag(prefix + ':type', image.type))
2326
+ } else {
2327
+ head.push(tag(prefix, item))
2328
+ }
2329
+ }
2330
+ }
2331
+
2332
+ if (md.openGraph) {
2333
+ const og = md.openGraph as Record<string, unknown>
2334
+
2335
+ if (og.title != null) head.push(tag('og:title', og.title))
2336
+ if (og.description != null) head.push(tag('og:description', og.description))
2337
+ if (og.url != null) head.push(tag('og:url', og.url))
2338
+ if (og.siteName != null) head.push(tag('og:site_name', og.siteName))
2339
+ if (og.type != null) head.push(tag('og:type', og.type))
2340
+ if (og.locale != null) head.push(tag('og:locale', og.locale))
2341
+ if (og.images != null) images('og:image', og.images)
2342
+ }
2343
+
2344
+ if (md.twitter) {
2345
+ const tw = md.twitter as Record<string, unknown>
2346
+
2347
+ if (tw.card != null) head.push(tag('twitter:card', tw.card))
2348
+ if (tw.title != null) head.push(tag('twitter:title', tw.title))
2349
+ if (tw.description != null) head.push(tag('twitter:description', tw.description))
2350
+ if (tw.site != null) head.push(tag('twitter:site', tw.site))
2351
+ if (tw.creator != null) head.push(tag('twitter:creator', tw.creator))
2352
+ if (tw.images != null) images('twitter:image', tw.images)
2353
+ }
2354
+
2355
+ // icons is links, not meta. A string is one icon; an object names which
2356
+ // rel each is for. The ones found in app/ are already in APP_HEAD above,
2357
+ // so this is for an app that wants to say it explicitly.
2358
+ const links = (rel: string, value: unknown): void => {
2359
+ const list = Array.isArray(value) ? value : [value]
2360
+
2361
+ for (const item of list) {
2362
+ if (item == null) continue
2363
+
2364
+ const icon = typeof item === 'object' && !(item instanceof URL)
2365
+ ? (item as Record<string, unknown>)
2366
+ : { url: item }
2367
+
2368
+ head.push(
2369
+ createElement('link', {
2370
+ key: '__i_' + rel + '_' + String(icon.url).slice(0, 40),
2371
+ rel: (icon.rel as string) ?? rel,
2372
+ href: absolute(icon.url),
2373
+ ...(icon.type ? { type: icon.type } : {}),
2374
+ ...(icon.sizes ? { sizes: icon.sizes } : {}),
2375
+ ...(icon.media ? { media: icon.media } : {}),
2376
+ }),
2377
+ )
2378
+ }
2379
+ }
2380
+
2381
+ if (md.icons != null) {
2382
+ const icons = md.icons as Record<string, unknown> | string | unknown[]
2383
+
2384
+ if (typeof icons === 'string' || Array.isArray(icons) || icons instanceof URL) {
2385
+ links('icon', icons)
2386
+ } else {
2387
+ if (icons.icon != null) links('icon', icons.icon)
2388
+ if (icons.apple != null) links('apple-touch-icon', icons.apple)
2389
+ if (icons.shortcut != null) links('shortcut icon', icons.shortcut)
2390
+ if (icons.other != null) links('icon', icons.other)
2391
+ }
2392
+ }
2393
+
2274
2394
  // other is flattened in beside the named keys, because it is a place to put
2275
2395
  // meta tags rather than a meta tag by that name. A key at the top level
2276
- // still renders — the type no longer invites one, but an app written
2396
+ // still renders - the type no longer invites one, but an app written
2277
2397
  // against the old shape must not silently lose its tags.
2278
- const named = Object.entries(md).filter(([k]) => k !== 'other')
2398
+ const structured = new Set(['title', 'description', 'metadataBase', 'openGraph', 'twitter', 'icons', 'other'])
2399
+ const named = Object.entries(md).filter(([k]) => !structured.has(k))
2279
2400
  const extra = Object.entries((md.other ?? {}) as Record<string, unknown>)
2280
2401
 
2281
2402
  for (const [k, v] of [...named, ...extra]) {
2282
- if (k === 'title' || k === 'description' || v == null) continue
2283
- head.push(createElement('meta', { key: '__m_' + k, name: k, content: String(v) }))
2403
+ if (v == null) continue
2404
+
2405
+ if (Array.isArray(v)) {
2406
+ for (const item of v) head.push(tag(k, item))
2407
+ } else {
2408
+ head.push(tag(k, v))
2409
+ }
2284
2410
  }
2285
2411
  }
2286
2412
 
@@ -3931,6 +4057,15 @@ export function rscKit(options = {}) {
3931
4057
  },
3932
4058
  configResolved(config) {
3933
4059
  isWatch = config.build?.watch != null;
4060
+ const nitroMain = config.plugins.find((p) => p.name === 'nitro:main');
4061
+ if (nitroMain?.hotUpdate) {
4062
+ const original = nitroMain.hotUpdate;
4063
+ nitroMain.hotUpdate = function (ctx) {
4064
+ if (this.environment?.name === 'rsc')
4065
+ return;
4066
+ return original.call(this, ctx);
4067
+ };
4068
+ }
3934
4069
  // rsc() splits the module graph into client and server; a JSX transform
3935
4070
  // placed ahead of it sees the wrong graph and fails in ways that are hard
3936
4071
  // to trace back here. Cheaper to refuse than to let it through.