@rsc-kit/core 0.13.0 → 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
 
@@ -3722,7 +3848,16 @@ export function rscKit(options = {}) {
3722
3848
  // routing bug rather than as this line. In dev the pages are the root;
3723
3849
  // the assets come from the same origin either way.
3724
3850
  base: '/',
3725
- root: outDir,
3851
+ // Where the generated entries live, so Vite resolves them as its own
3852
+ // source. A build concern only.
3853
+ //
3854
+ // Not during preview. `vite preview` serves what was built, and Nitro's
3855
+ // preview reads its build info from `<vite root>/node_modules/.nitro` —
3856
+ // so pointing the root at this directory sent it looking somewhere no
3857
+ // build ever writes, and every preview failed with "Cannot load nitro
3858
+ // build info. Make sure to build first." after a build that had just
3859
+ // succeeded.
3860
+ ...(env.isPreview ? {} : { root: outDir }),
3726
3861
  // Force single instances of React/RSC runtime — critical when the
3727
3862
  // package is symlinked (local dev / monorepo), else "use client"
3728
3863
  // components SSR against a second React copy and hooks throw.
@@ -3922,6 +4057,15 @@ export function rscKit(options = {}) {
3922
4057
  },
3923
4058
  configResolved(config) {
3924
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
+ }
3925
4069
  // rsc() splits the module graph into client and server; a JSX transform
3926
4070
  // placed ahead of it sees the wrong graph and fails in ways that are hard
3927
4071
  // to trace back here. Cheaper to refuse than to let it through.