@rsc-kit/core 0.30.13 → 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/appAssets.d.ts +25 -4
- package/dist/appAssets.js +86 -30
- package/dist/appAssets.js.map +1 -1
- package/dist/metadata.d.ts +4 -0
- package/dist/metadata.js.map +1 -1
- package/dist/shareImages.d.ts +30 -0
- package/dist/shareImages.js +50 -0
- package/dist/shareImages.js.map +1 -0
- package/dist/vite.d.ts +24 -0
- package/dist/vite.js +127 -21
- package/dist/vite.js.map +1 -1
- package/package.json +1 -1
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";
|
|
@@ -157,6 +157,7 @@ let typesDir;
|
|
|
157
157
|
let hotFile;
|
|
158
158
|
let hostCallOptions;
|
|
159
159
|
let packageDir;
|
|
160
|
+
let backendPaths = [];
|
|
160
161
|
let hostGlobal;
|
|
161
162
|
let interceptManifestFile;
|
|
162
163
|
let packageAlias;
|
|
@@ -194,8 +195,7 @@ let foundAssets = {
|
|
|
194
195
|
appleIcon: null,
|
|
195
196
|
appleSplash: [],
|
|
196
197
|
screenshots: [],
|
|
197
|
-
|
|
198
|
-
twitter: null,
|
|
198
|
+
shareImages: {},
|
|
199
199
|
};
|
|
200
200
|
/** Host functions to generate stubs for — see RscKitOptions.hostActions. */
|
|
201
201
|
let hostActions;
|
|
@@ -348,6 +348,34 @@ function aliasEntries() {
|
|
|
348
348
|
});
|
|
349
349
|
return entries;
|
|
350
350
|
}
|
|
351
|
+
/**
|
|
352
|
+
* The prefixes a backend owns, as the matcher reads them: `/auth`, no trailing
|
|
353
|
+
* slash. Refused rather than corrected where a mistake would silently send the
|
|
354
|
+
* whole app, or its own endpoints, to the backend.
|
|
355
|
+
*/
|
|
356
|
+
export function normaliseBackendPaths(paths) {
|
|
357
|
+
const out = [];
|
|
358
|
+
for (const raw of paths ?? []) {
|
|
359
|
+
const given = typeof raw === "string" ? raw.trim() : "";
|
|
360
|
+
if (!given.startsWith("/")) {
|
|
361
|
+
throw new Error(`[rsc-kit] backendPaths: ${JSON.stringify(raw)} is not a path. Each starts with a slash, as in '/auth'.`);
|
|
362
|
+
}
|
|
363
|
+
// After the slash check, so "/" is the root and not "not a path".
|
|
364
|
+
const path = given.replace(/\/+$/, "");
|
|
365
|
+
if (path === "") {
|
|
366
|
+
throw new Error("[rsc-kit] backendPaths: '/' is every url, and nothing would be left for this app. Name the prefixes the backend owns.");
|
|
367
|
+
}
|
|
368
|
+
if (path === "/_rsc" || path.startsWith("/_rsc/")) {
|
|
369
|
+
throw new Error(`[rsc-kit] backendPaths: ${JSON.stringify(raw)} is this package's own endpoint, which the renderer answers.`);
|
|
370
|
+
}
|
|
371
|
+
if (/[?#*]/.test(path)) {
|
|
372
|
+
throw new Error(`[rsc-kit] backendPaths: ${JSON.stringify(raw)} is a prefix, not a pattern: no query, fragment or wildcard.`);
|
|
373
|
+
}
|
|
374
|
+
if (!out.includes(path))
|
|
375
|
+
out.push(path);
|
|
376
|
+
}
|
|
377
|
+
return out;
|
|
378
|
+
}
|
|
351
379
|
function resolvePaths(options) {
|
|
352
380
|
projectRoot = resolve(options.projectRoot || process.env.RSC_PROJECT_ROOT || process.cwd());
|
|
353
381
|
sourceDir = resolve(options.sourceDir || process.env.RSC_SOURCE_DIR || join(projectRoot, "src"));
|
|
@@ -385,6 +413,7 @@ function resolvePaths(options) {
|
|
|
385
413
|
hotFile = options.hotFile || process.env.RSC_HOT_FILE || "";
|
|
386
414
|
hostCallOptions = options.hostCall;
|
|
387
415
|
packageDir = resolve(options.packageDir || process.env.RSC_PACKAGE_DIR || thisDir());
|
|
416
|
+
backendPaths = normaliseBackendPaths(options.backendPaths);
|
|
388
417
|
hostGlobal = options.hostGlobal || process.env.RSC_HOST_GLOBAL || "rpc";
|
|
389
418
|
interceptManifestFile = resolve(options.interceptManifestFile ||
|
|
390
419
|
process.env.RSC_INTERCEPT_MANIFEST ||
|
|
@@ -1363,7 +1392,7 @@ function copyAppAssets(clientDir) {
|
|
|
1363
1392
|
if (all.length === 0)
|
|
1364
1393
|
return;
|
|
1365
1394
|
for (const asset of all) {
|
|
1366
|
-
const to = join(clientDir, asset.href.slice(1));
|
|
1395
|
+
const to = join(clientDir, assetFile(asset.href.slice(1)));
|
|
1367
1396
|
mkdirSync(dirname(to), { recursive: true });
|
|
1368
1397
|
copyFileSync(join(sourceDir, "app", asset.file), to);
|
|
1369
1398
|
}
|
|
@@ -2394,6 +2423,8 @@ const API_METHODS = [
|
|
|
2394
2423
|
];
|
|
2395
2424
|
/** `orders.section.tsx` — a region of a page that can be refreshed by name. */
|
|
2396
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;
|
|
2397
2428
|
/**
|
|
2398
2429
|
* Whether a page or a section declares refreshOn. Read from the source, as
|
|
2399
2430
|
* section names are, because the manifest is written before any bundle exists.
|
|
@@ -2769,17 +2800,40 @@ function fallbackOrigin(): string {
|
|
|
2769
2800
|
const FALLBACK_MARKER = 'x-rsc-renderer-fallback'
|
|
2770
2801
|
const PROXIED_MARKER = 'x-rsc-proxied-by-backend'
|
|
2771
2802
|
`;
|
|
2803
|
+
/**
|
|
2804
|
+
* The urls the app said belong to the backend, tested before any page is
|
|
2805
|
+
* matched. Emitted in both variants: a test's stand-in backend is reached by
|
|
2806
|
+
* the same prefixes as the real one.
|
|
2807
|
+
*/
|
|
2808
|
+
function backendPathConsts() {
|
|
2809
|
+
return `const BACKEND_PATHS: string[] = ${JSON.stringify(backendPaths)}
|
|
2810
|
+
|
|
2811
|
+
// A prefix at a segment boundary: '/auth' is '/auth' and what is under it, not '/authors'.
|
|
2812
|
+
function onBackendPath(pathname: string): boolean {
|
|
2813
|
+
return BACKEND_PATHS.some((prefix) => pathname === prefix || pathname.startsWith(prefix + '/'))
|
|
2814
|
+
}
|
|
2815
|
+
`;
|
|
2816
|
+
}
|
|
2772
2817
|
/** With no backend configured: this app's not-found page, or a test's backend. */
|
|
2773
|
-
const NO_FALLBACK_BODY = `
|
|
2818
|
+
const NO_FALLBACK_BODY = ` // The app named this prefix as the backend's: no page is asked about it, so one
|
|
2819
|
+
// that matches (a dynamic root route) cannot answer for it.
|
|
2820
|
+
const toBackend = onBackendPath(new URL(request.url).pathname)
|
|
2821
|
+
const answer = toBackend ? null : await devHandler(request)
|
|
2774
2822
|
|
|
2775
2823
|
if (answer) return answer
|
|
2776
2824
|
|
|
2777
|
-
if (backendForward && !pageSaidNotFound(request)) return await backendForward(request)
|
|
2825
|
+
if (backendForward && (toBackend || !pageSaidNotFound(request))) return await backendForward(request)
|
|
2778
2826
|
|
|
2779
2827
|
return await notFound(request)
|
|
2780
2828
|
`;
|
|
2781
2829
|
const FALLBACK_BODY = ` const FALLBACK_ORIGIN = fallbackOrigin()
|
|
2782
|
-
|
|
2830
|
+
|
|
2831
|
+
// The app named this prefix as the backend's (rscKit({ backendPaths })): no
|
|
2832
|
+
// page is asked about it. A dynamic root route such as [team]/[app] matches
|
|
2833
|
+
// /gitlab/connect, renders, says notFound() - and a page that said that is not
|
|
2834
|
+
// forwarded, so the backend's own route was the not-found page.
|
|
2835
|
+
const toBackend = onBackendPath(new URL(request.url).pathname)
|
|
2836
|
+
const answer = toBackend ? null : await devHandler(request)
|
|
2783
2837
|
|
|
2784
2838
|
if (answer) return answer
|
|
2785
2839
|
|
|
@@ -2787,7 +2841,7 @@ const FALLBACK_BODY = ` const FALLBACK_ORIGIN = fallbackOrigin()
|
|
|
2787
2841
|
// its guard, or a backend's 404. That is this app's not-found.tsx, not a
|
|
2788
2842
|
// url for the backend: forwarded, the backend answered with its own 404
|
|
2789
2843
|
// and the app's never showed.
|
|
2790
|
-
if (pageSaidNotFound(request)) return await notFound(request)
|
|
2844
|
+
if (!toBackend && pageSaidNotFound(request)) return await notFound(request)
|
|
2791
2845
|
|
|
2792
2846
|
// Nothing here owns this url. In development the backend usually does — a
|
|
2793
2847
|
// Blade page, /login, a webhook, an uploaded file under /storage — so the
|
|
@@ -3093,6 +3147,7 @@ ${
|
|
|
3093
3147
|
import { SegmentBoundary } from ${JSON.stringify(join(packageDir, "js/SegmentBoundary"))}
|
|
3094
3148
|
import { LoadingBoundary } from ${JSON.stringify(join(packageDir, "js/LoadingBoundary"))}
|
|
3095
3149
|
import { DocumentTitle } from ${JSON.stringify(join(packageDir, "js/DocumentTitle"))}
|
|
3150
|
+
import { depthOf, fileWins, folderOf, nearestShareImage } from ${JSON.stringify(join(packageDir, "shareImages"))}
|
|
3096
3151
|
import { SlotBoundary } from ${JSON.stringify(join(packageDir, "js/SlotBoundary"))}
|
|
3097
3152
|
import { RouteErrorBoundary } from ${JSON.stringify(join(packageDir, "js/RouteErrorBoundary"))}
|
|
3098
3153
|
import __rsc_assets from 'virtual:vite-rsc/assets-manifest'
|
|
@@ -3146,6 +3201,9 @@ ${mapEntries.join("\n")}
|
|
|
3146
3201
|
/** Head tags for the icons and share images found in app/. */
|
|
3147
3202
|
const APP_HEAD: { tag: any; props: Record<string, string> }[] = ${JSON.stringify(headTags(foundAssets))}
|
|
3148
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
|
+
|
|
3149
3207
|
const WEB_MANIFEST: { href: string; themeColor?: string } | null = ${JSON.stringify(webManifestOptions
|
|
3150
3208
|
? {
|
|
3151
3209
|
href: MANIFEST_PATH,
|
|
@@ -4146,13 +4204,35 @@ async function renderTree(
|
|
|
4146
4204
|
}
|
|
4147
4205
|
|
|
4148
4206
|
for (const [i, found] of APP_HEAD.entries()) {
|
|
4149
|
-
|
|
4207
|
+
head.push(createElement(found.tag, { key: '__a' + i, ...found.props }))
|
|
4208
|
+
}
|
|
4150
4209
|
|
|
4151
|
-
|
|
4152
|
-
|
|
4153
|
-
|
|
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')
|
|
4154
4226
|
|
|
4155
|
-
|
|
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
|
+
}
|
|
4156
4236
|
}
|
|
4157
4237
|
|
|
4158
4238
|
if (md) {
|
|
@@ -4252,7 +4332,7 @@ async function renderTree(
|
|
|
4252
4332
|
if (og.siteName != null) head.push(tag('og:site_name', og.siteName))
|
|
4253
4333
|
if (og.type != null) head.push(tag('og:type', og.type))
|
|
4254
4334
|
if (og.locale != null) head.push(tag('og:locale', og.locale))
|
|
4255
|
-
if (og.images != null) images('og:image', og.images)
|
|
4335
|
+
if (og.images != null && !ogFile) images('og:image', og.images)
|
|
4256
4336
|
}
|
|
4257
4337
|
|
|
4258
4338
|
if (md.twitter) {
|
|
@@ -4263,7 +4343,7 @@ async function renderTree(
|
|
|
4263
4343
|
if (tw.description != null) head.push(tag('twitter:description', tw.description))
|
|
4264
4344
|
if (tw.site != null) head.push(tag('twitter:site', tw.site))
|
|
4265
4345
|
if (tw.creator != null) head.push(tag('twitter:creator', tw.creator))
|
|
4266
|
-
if (tw.images != null) images('twitter:image', tw.images)
|
|
4346
|
+
if (tw.images != null && !twitterFile) images('twitter:image', tw.images)
|
|
4267
4347
|
}
|
|
4268
4348
|
|
|
4269
4349
|
// icons is links, not meta. A string is one icon; an object names which
|
|
@@ -5156,6 +5236,17 @@ const METADATA_PROBE_GRACE_MS = 20
|
|
|
5156
5236
|
|
|
5157
5237
|
const METADATA_POSTPONED = Symbol('metadata postponed')
|
|
5158
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
|
+
|
|
5159
5250
|
export async function resolveMetadata(
|
|
5160
5251
|
component: string,
|
|
5161
5252
|
props: Record<string, unknown> = {},
|
|
@@ -5220,18 +5311,27 @@ export async function resolveMetadata(
|
|
|
5220
5311
|
// happened to declare one of its own.
|
|
5221
5312
|
const merged: Record<string, unknown> = {}
|
|
5222
5313
|
const other: Record<string, unknown> = {}
|
|
5314
|
+
const sources: ImageSources = {}
|
|
5223
5315
|
|
|
5224
|
-
const take = (from: Record<string, unknown
|
|
5316
|
+
const take = (from: Record<string, unknown>, depth: number) => {
|
|
5225
5317
|
for (const [k, v] of Object.entries(from)) {
|
|
5226
5318
|
if (k === 'title') continue
|
|
5227
5319
|
if (k === 'other') Object.assign(other, v as Record<string, unknown>)
|
|
5228
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
|
+
}
|
|
5229
5329
|
}
|
|
5230
5330
|
}
|
|
5231
5331
|
|
|
5232
|
-
|
|
5332
|
+
layoutMeta.forEach((m, i) => take(m, depthOf(folderOf(layouts[i].component))))
|
|
5233
5333
|
|
|
5234
|
-
take(page)
|
|
5334
|
+
take(page, depthOf(folderOf(component)))
|
|
5235
5335
|
|
|
5236
5336
|
if (Object.keys(other).length > 0) merged.other = other
|
|
5237
5337
|
|
|
@@ -5256,7 +5356,11 @@ export async function resolveMetadata(
|
|
|
5256
5356
|
}
|
|
5257
5357
|
if (title != null) merged.title = title
|
|
5258
5358
|
|
|
5259
|
-
|
|
5359
|
+
if (Object.keys(merged).length === 0) return null
|
|
5360
|
+
|
|
5361
|
+
imageSources.set(merged, sources)
|
|
5362
|
+
|
|
5363
|
+
return merged
|
|
5260
5364
|
}
|
|
5261
5365
|
|
|
5262
5366
|
// Buffered render (worker: rsc / rscWithoutCallbacks — used at prerender time).
|
|
@@ -5707,7 +5811,7 @@ export async function handleRscPprShell(
|
|
|
5707
5811
|
* server rendering live pages it had already frozen. The gate is the mode, not
|
|
5708
5812
|
* the file.
|
|
5709
5813
|
*/
|
|
5710
|
-
${fallbackOrigin === false ? "" : FALLBACK_CONSTS.replace("__ORIGIN__", JSON.stringify(fallbackOrigin))}${NITRO_HOST_CALLS}
|
|
5814
|
+
${backendPathConsts()}${fallbackOrigin === false ? "" : FALLBACK_CONSTS.replace("__ORIGIN__", JSON.stringify(fallbackOrigin))}${NITRO_HOST_CALLS}
|
|
5711
5815
|
let devHandler: ((request: Request) => Promise<Response | null>) | null = null
|
|
5712
5816
|
|
|
5713
5817
|
export default async function handler(request: Request): Promise<Response> {
|
|
@@ -7584,7 +7688,8 @@ export function rscKit(options = {}) {
|
|
|
7584
7688
|
res.end(webManifest(webManifestOptions));
|
|
7585
7689
|
return;
|
|
7586
7690
|
}
|
|
7587
|
-
|
|
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));
|
|
7588
7693
|
const file = asset ? join(sourceDir, "app", asset.file) : null;
|
|
7589
7694
|
if (!asset || !file || !existsSync(file))
|
|
7590
7695
|
return next();
|
|
@@ -7671,6 +7776,7 @@ export function rscKit(options = {}) {
|
|
|
7671
7776
|
// published without any — generic-host.test.ts fails if it reappears.
|
|
7672
7777
|
return ((base !== stem && shapes.has(stem)) ||
|
|
7673
7778
|
SECTION_FILE.test(base) ||
|
|
7779
|
+
SHARE_IMAGE_FILE.test(base) ||
|
|
7674
7780
|
(routeConfig !== null && base === routeConfig.file));
|
|
7675
7781
|
};
|
|
7676
7782
|
const restart = (file) => {
|