@rsc-kit/core 0.20.2 → 0.20.4
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/earlyHints.d.ts +17 -0
- package/dist/earlyHints.js +65 -0
- package/dist/earlyHints.js.map +1 -0
- package/dist/host.d.ts +10 -4
- package/dist/host.js +54 -2
- package/dist/host.js.map +1 -1
- package/dist/js/DefaultRouteError.js +8 -3
- package/dist/js/DefaultRouteError.js.map +1 -1
- package/dist/js/Link.d.ts +1 -1
- package/dist/js/Link.js +20 -2
- package/dist/js/Link.js.map +1 -1
- package/dist/js/createViteRscApp.js +13 -4
- package/dist/js/createViteRscApp.js.map +1 -1
- package/dist/js/earlyClicks.d.ts +26 -0
- package/dist/js/earlyClicks.js +46 -0
- package/dist/js/earlyClicks.js.map +1 -0
- package/dist/js/navigate.js +33 -10
- package/dist/js/navigate.js.map +1 -1
- package/dist/js/staleAssets.d.ts +10 -0
- package/dist/js/staleAssets.js +27 -6
- package/dist/js/staleAssets.js.map +1 -1
- package/dist/js/viewportPrefetch.d.ts +21 -0
- package/dist/js/viewportPrefetch.js +67 -0
- package/dist/js/viewportPrefetch.js.map +1 -0
- package/dist/vite.js +97 -3
- package/dist/vite.js.map +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export interface CriticalAssets {
|
|
2
|
+
/** Stylesheets the document links: `<link rel="stylesheet">`. */
|
|
3
|
+
styles: string[];
|
|
4
|
+
/** The client entry: `<script type="module">`'s import. */
|
|
5
|
+
modules: string[];
|
|
6
|
+
/** Fonts the document preloads: `<link rel="preload" as="font">`. */
|
|
7
|
+
fonts: string[];
|
|
8
|
+
}
|
|
9
|
+
/** One `Link` header value for the set. Empty string for an empty set. */
|
|
10
|
+
export declare function linkHeader(assets: CriticalAssets): string;
|
|
11
|
+
/**
|
|
12
|
+
* What a stored document's head names: its stylesheets and preloaded fonts.
|
|
13
|
+
* The first 16 KB, which is the head of any document this build writes.
|
|
14
|
+
*/
|
|
15
|
+
export declare function criticalAssetsOf(html: string): CriticalAssets;
|
|
16
|
+
/** One set over another: the document's own names first, the build's where the document has none. */
|
|
17
|
+
export declare function mergeAssets(primary: CriticalAssets, fallback: CriticalAssets | null): CriticalAssets;
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
// The Link header a document answers with, for a CDN to send ahead as 103
|
|
2
|
+
// Early Hints - so the stylesheet, the client entry and the fonts are on
|
|
3
|
+
// their way while the HTML is still being written. Cloudflare turns the
|
|
4
|
+
// header into hints and caches them at the edge for the next visitor;
|
|
5
|
+
// anything else passes it through, where a browser still reads it as the
|
|
6
|
+
// document arrives.
|
|
7
|
+
//
|
|
8
|
+
// The critical set and nothing more: a hint promotes whatever it names, and
|
|
9
|
+
// naming every chunk demotes the document behind them. The stylesheet and
|
|
10
|
+
// the client entry come from the build's own manifest, the same names the
|
|
11
|
+
// document carries. The fonts are the app's - preloaded in its layout - and
|
|
12
|
+
// are read from a stored document's own head; a rendered document's head is
|
|
13
|
+
// not known until it is rendered, after its headers have gone.
|
|
14
|
+
/** One `Link` header value for the set. Empty string for an empty set. */
|
|
15
|
+
export function linkHeader(assets) {
|
|
16
|
+
const parts = [];
|
|
17
|
+
for (const href of assets.styles)
|
|
18
|
+
parts.push(`<${href}>; rel=preload; as=style`);
|
|
19
|
+
for (const href of assets.modules)
|
|
20
|
+
parts.push(`<${href}>; rel=modulepreload`);
|
|
21
|
+
for (const href of assets.fonts)
|
|
22
|
+
parts.push(`<${href}>; rel=preload; as=font; crossorigin`);
|
|
23
|
+
return parts.join(', ');
|
|
24
|
+
}
|
|
25
|
+
const HEAD_LIMIT = 16_384;
|
|
26
|
+
const LINK = /<link\s[^>]*>/g;
|
|
27
|
+
const ATTR = (name, tag) => {
|
|
28
|
+
const match = new RegExp(`\\b${name}=["']([^"']*)["']`, 'i').exec(tag);
|
|
29
|
+
return match ? match[1] : null;
|
|
30
|
+
};
|
|
31
|
+
/**
|
|
32
|
+
* What a stored document's head names: its stylesheets and preloaded fonts.
|
|
33
|
+
* The first 16 KB, which is the head of any document this build writes.
|
|
34
|
+
*/
|
|
35
|
+
export function criticalAssetsOf(html) {
|
|
36
|
+
const head = html.slice(0, HEAD_LIMIT);
|
|
37
|
+
const found = { styles: [], modules: [], fonts: [] };
|
|
38
|
+
for (const tag of head.match(LINK) ?? []) {
|
|
39
|
+
const rel = ATTR('rel', tag)?.toLowerCase();
|
|
40
|
+
const href = ATTR('href', tag);
|
|
41
|
+
if (!href || !href.startsWith('/'))
|
|
42
|
+
continue;
|
|
43
|
+
if (rel === 'stylesheet')
|
|
44
|
+
found.styles.push(href);
|
|
45
|
+
else if (rel === 'preload' && ATTR('as', tag)?.toLowerCase() === 'font')
|
|
46
|
+
found.fonts.push(href);
|
|
47
|
+
}
|
|
48
|
+
// The bootstrap script is the last thing in the body, after the shell; its
|
|
49
|
+
// import names the entry. The tail, then, and the head for a short page.
|
|
50
|
+
const entry = /import\(["'](\/[^"']+\.js)["']\)/.exec(html.slice(-4096)) ?? /import\(["'](\/[^"']+\.js)["']\)/.exec(head);
|
|
51
|
+
if (entry)
|
|
52
|
+
found.modules.push(entry[1]);
|
|
53
|
+
return found;
|
|
54
|
+
}
|
|
55
|
+
/** One set over another: the document's own names first, the build's where the document has none. */
|
|
56
|
+
export function mergeAssets(primary, fallback) {
|
|
57
|
+
if (!fallback)
|
|
58
|
+
return primary;
|
|
59
|
+
return {
|
|
60
|
+
styles: primary.styles.length ? primary.styles : fallback.styles,
|
|
61
|
+
modules: primary.modules.length ? primary.modules : fallback.modules,
|
|
62
|
+
fonts: primary.fonts.length ? primary.fonts : fallback.fonts,
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
//# sourceMappingURL=earlyHints.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"earlyHints.js","sourceRoot":"","sources":["../src/earlyHints.ts"],"names":[],"mappings":"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","sourcesContent":["// The Link header a document answers with, for a CDN to send ahead as 103\n// Early Hints - so the stylesheet, the client entry and the fonts are on\n// their way while the HTML is still being written. Cloudflare turns the\n// header into hints and caches them at the edge for the next visitor;\n// anything else passes it through, where a browser still reads it as the\n// document arrives.\n//\n// The critical set and nothing more: a hint promotes whatever it names, and\n// naming every chunk demotes the document behind them. The stylesheet and\n// the client entry come from the build's own manifest, the same names the\n// document carries. The fonts are the app's - preloaded in its layout - and\n// are read from a stored document's own head; a rendered document's head is\n// not known until it is rendered, after its headers have gone.\n\nexport interface CriticalAssets {\n /** Stylesheets the document links: `<link rel=\"stylesheet\">`. */\n styles: string[];\n /** The client entry: `<script type=\"module\">`'s import. */\n modules: string[];\n /** Fonts the document preloads: `<link rel=\"preload\" as=\"font\">`. */\n fonts: string[];\n}\n\n/** One `Link` header value for the set. Empty string for an empty set. */\nexport function linkHeader(assets: CriticalAssets): string {\n const parts: string[] = []\n\n for (const href of assets.styles) parts.push(`<${href}>; rel=preload; as=style`)\n for (const href of assets.modules) parts.push(`<${href}>; rel=modulepreload`)\n for (const href of assets.fonts) parts.push(`<${href}>; rel=preload; as=font; crossorigin`)\n\n return parts.join(', ')\n}\n\nconst HEAD_LIMIT = 16_384\nconst LINK = /<link\\s[^>]*>/g\nconst ATTR = (name: string, tag: string): string | null => {\n const match = new RegExp(`\\\\b${name}=[\"']([^\"']*)[\"']`, 'i').exec(tag)\n\n return match ? match[1] : null\n}\n\n/**\n * What a stored document's head names: its stylesheets and preloaded fonts.\n * The first 16 KB, which is the head of any document this build writes.\n */\nexport function criticalAssetsOf(html: string): CriticalAssets {\n const head = html.slice(0, HEAD_LIMIT)\n const found: CriticalAssets = { styles: [], modules: [], fonts: [] }\n\n for (const tag of head.match(LINK) ?? []) {\n const rel = ATTR('rel', tag)?.toLowerCase()\n const href = ATTR('href', tag)\n\n if (!href || !href.startsWith('/')) continue\n\n if (rel === 'stylesheet') found.styles.push(href)\n else if (rel === 'preload' && ATTR('as', tag)?.toLowerCase() === 'font') found.fonts.push(href)\n }\n\n // The bootstrap script is the last thing in the body, after the shell; its\n // import names the entry. The tail, then, and the head for a short page.\n const entry = /import\\([\"'](\\/[^\"']+\\.js)[\"']\\)/.exec(html.slice(-4096)) ?? /import\\([\"'](\\/[^\"']+\\.js)[\"']\\)/.exec(head)\n\n if (entry) found.modules.push(entry[1])\n\n return found\n}\n\n/** One set over another: the document's own names first, the build's where the document has none. */\nexport function mergeAssets(primary: CriticalAssets, fallback: CriticalAssets | null): CriticalAssets {\n if (!fallback) return primary\n\n return {\n styles: primary.styles.length ? primary.styles : fallback.styles,\n modules: primary.modules.length ? primary.modules : fallback.modules,\n fonts: primary.fonts.length ? primary.fonts : fallback.fonts,\n }\n}\n"]}
|
package/dist/host.d.ts
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
* from `@rsc-kit/core/revalidate`.
|
|
4
4
|
*/
|
|
5
5
|
export { revalidate } from "./revalidate.js";
|
|
6
|
+
import { type CriticalAssets } from "./earlyHints.js";
|
|
6
7
|
/**
|
|
7
8
|
* @internal For a host adapter that embeds the engine. An app imports this
|
|
8
9
|
* from `@rsc-kit/core/redirect` - the one the guides teach, and the one an
|
|
@@ -18,6 +19,10 @@ import type { RouteManifest } from "./manifest.js";
|
|
|
18
19
|
export interface RscEngine {
|
|
19
20
|
/** The route table this bundle was built from. */
|
|
20
21
|
manifest?(): RouteManifest;
|
|
22
|
+
/** A short id of this build's client, for the version the host answers with when given none. */
|
|
23
|
+
buildId?(): Promise<string>;
|
|
24
|
+
/** The stylesheet and client entry every document links, for the Link header a CDN sends ahead. */
|
|
25
|
+
criticalAssets?(): CriticalAssets;
|
|
21
26
|
installHostFn(fn: (name: string, ...args: unknown[]) => unknown): void;
|
|
22
27
|
handleRscStream(component: string, props?: Record<string, unknown>, layouts?: {
|
|
23
28
|
component: string;
|
|
@@ -143,10 +148,11 @@ export interface RscHostOptions {
|
|
|
143
148
|
/** Serve a built browser asset. Return null for anything not found. */
|
|
144
149
|
assets?: (pathname: string, request: Request) => Promise<Response | null> | Response | null;
|
|
145
150
|
/**
|
|
146
|
-
* Identifies this build to the client, which
|
|
147
|
-
* navigation and
|
|
148
|
-
*
|
|
149
|
-
*
|
|
151
|
+
* Identifies this build to the client, which says it back on every
|
|
152
|
+
* navigation and is sent to load the document when it differs. For an
|
|
153
|
+
* engine with no `buildId` of its own; the generated one has, and every
|
|
154
|
+
* document it renders says that id, so a version named here would
|
|
155
|
+
* disagree with what the client was told and refuse every navigation.
|
|
150
156
|
*/
|
|
151
157
|
version?: string;
|
|
152
158
|
/**
|
package/dist/host.js
CHANGED
|
@@ -27,6 +27,7 @@ import { currentNotFound, withRedirect } from "./redirect.js";
|
|
|
27
27
|
import { compressed } from "./compress.js";
|
|
28
28
|
import { withCache } from "./cache.js";
|
|
29
29
|
import { takeAfterWork, withRequest, withResponseDraft } from "./request.js";
|
|
30
|
+
import { criticalAssetsOf, linkHeader, mergeAssets } from "./earlyHints.js";
|
|
30
31
|
/**
|
|
31
32
|
* @internal For a host adapter that embeds the engine. An app imports this
|
|
32
33
|
* from `@rsc-kit/core/redirect` - the one the guides teach, and the one an
|
|
@@ -286,7 +287,13 @@ function matchPage(routes, url) {
|
|
|
286
287
|
return matchRoute(routes, url.pathname, hostOf.get(url) ?? null);
|
|
287
288
|
}
|
|
288
289
|
export function createRscHandler(options) {
|
|
289
|
-
const { engine, assets
|
|
290
|
+
const { engine, assets } = options;
|
|
291
|
+
// The build's own id, which is also what every document says it is - the
|
|
292
|
+
// two must agree, or a client's honest claim is a 409 on every request.
|
|
293
|
+
// A version named by the app applies only to an engine with no id of its
|
|
294
|
+
// own. Resolved on the first request: the engine reads it from a build
|
|
295
|
+
// product it only has at runtime.
|
|
296
|
+
let version = engine.buildId ? undefined : options.version;
|
|
290
297
|
const maxActionBody = options.maxActionBody ?? DEFAULT_MAX_ACTION_BODY;
|
|
291
298
|
const compress = options.compress ?? true;
|
|
292
299
|
// Annotated rather than inferred: the narrowing below is lost inside the
|
|
@@ -350,6 +357,10 @@ export function createRscHandler(options) {
|
|
|
350
357
|
const identify = Boolean(manifest.build?.identify);
|
|
351
358
|
/** How a response was answered, for X-RSC-Kit: a file the build wrote, or a shell of one. */
|
|
352
359
|
const servedFrom = new WeakMap();
|
|
360
|
+
/** What a stored document's head names, read once per file, for the Link header. */
|
|
361
|
+
const hinted = new WeakMap();
|
|
362
|
+
const hintsByKey = new Map();
|
|
363
|
+
const EMPTY_ASSETS = { styles: [], modules: [], fonts: [] };
|
|
353
364
|
/**
|
|
354
365
|
* The page a payload url belongs to, if this is one.
|
|
355
366
|
*
|
|
@@ -421,6 +432,8 @@ export function createRscHandler(options) {
|
|
|
421
432
|
return [version ?? "", url.pathname + url.search, ...varies].join("\n");
|
|
422
433
|
}
|
|
423
434
|
return async function handle(request) {
|
|
435
|
+
if (version === undefined && engine.buildId)
|
|
436
|
+
version = await engine.buildId();
|
|
424
437
|
return await withRequest(request, () => withCache(() =>
|
|
425
438
|
// Open for the whole request and sealed the moment an answer exists,
|
|
426
439
|
// so middleware — which runs before any rendering — can put headers on
|
|
@@ -449,6 +462,16 @@ export function createRscHandler(options) {
|
|
|
449
462
|
// Network tab, the way X-Nextjs-Cache is. Always: it names no
|
|
450
463
|
// product, and a CDN rule or a check can key on it.
|
|
451
464
|
response.headers.set("X-RSC-Kit", servedFrom.get(response) ?? "rendered");
|
|
465
|
+
// What the document needs first, as a Link header: a CDN sends it
|
|
466
|
+
// ahead as 103 Early Hints, and the stylesheet, the entry and the
|
|
467
|
+
// fonts download while the HTML is still being written. Every
|
|
468
|
+
// document, stored ones included - a stored document's own head
|
|
469
|
+
// names its fonts; a rendered one's is not known until too late.
|
|
470
|
+
if (response.status === 200 && (response.headers.get("Content-Type") ?? "").startsWith("text/html")) {
|
|
471
|
+
const link = linkHeader(mergeAssets(hinted.get(response) ?? EMPTY_ASSETS, engine.criticalAssets?.() ?? null));
|
|
472
|
+
if (link)
|
|
473
|
+
response.headers.set("Link", link);
|
|
474
|
+
}
|
|
452
475
|
// What built it. The name only, never the version - a version in
|
|
453
476
|
// every response is what a vulnerability scanner filters on - and
|
|
454
477
|
// off for a team whose policy strips every framework identifier.
|
|
@@ -583,6 +606,20 @@ export function createRscHandler(options) {
|
|
|
583
606
|
const formPost = await formPostOf(request, url);
|
|
584
607
|
if (!formPost && request.method !== "GET" && request.method !== "HEAD")
|
|
585
608
|
return null;
|
|
609
|
+
// A client saying which build it runs, and it is not this one: its
|
|
610
|
+
// manifest cannot load what this build's payload names - a client
|
|
611
|
+
// component added since is "client reference not found" and the route's
|
|
612
|
+
// error boundary, on a page that worked a click ago. Under a service
|
|
613
|
+
// worker that serves the last build's document first, that is every
|
|
614
|
+
// returning visitor's first navigation after a deploy, not an open tab.
|
|
615
|
+
// A 409 sends the client to load the document instead, from this build.
|
|
616
|
+
const claimed = request.headers.get(HEADER.version);
|
|
617
|
+
if (claimed !== null && claimed !== "" && version && claimed !== version && request.headers.get(HEADER.rsc) !== null) {
|
|
618
|
+
return new Response(null, {
|
|
619
|
+
status: 409,
|
|
620
|
+
headers: withVersion({ "X-RSC-Location": url.pathname + url.search, "Cache-Control": "no-store" }),
|
|
621
|
+
});
|
|
622
|
+
}
|
|
586
623
|
// One named region of this page, asked for without mutating anything to
|
|
587
624
|
// earn it. What an action invalidated does not come through here — that
|
|
588
625
|
// travels back inside the action's own answer, which is the whole point of
|
|
@@ -936,7 +973,7 @@ export function createRscHandler(options) {
|
|
|
936
973
|
const whole = await read(`${key}.html`);
|
|
937
974
|
// A whole page is finished. Nothing to resume, nothing to render.
|
|
938
975
|
if (whole !== null) {
|
|
939
|
-
|
|
976
|
+
const response = new Response(whole, {
|
|
940
977
|
headers: withVersion({
|
|
941
978
|
"Content-Type": HTML_TYPE,
|
|
942
979
|
Vary: VARY_ON_RSC,
|
|
@@ -945,6 +982,14 @@ export function createRscHandler(options) {
|
|
|
945
982
|
: REVALIDATE,
|
|
946
983
|
}),
|
|
947
984
|
});
|
|
985
|
+
// Its head is in hand, so its fonts are too. Read once per file.
|
|
986
|
+
let found = hintsByKey.get(key);
|
|
987
|
+
if (!found) {
|
|
988
|
+
found = criticalAssetsOf(whole);
|
|
989
|
+
hintsByKey.set(key, found);
|
|
990
|
+
}
|
|
991
|
+
hinted.set(response, found);
|
|
992
|
+
return response;
|
|
948
993
|
}
|
|
949
994
|
// Then a shell, under this url or under the route's pattern. Which of the
|
|
950
995
|
// two answered is tracked, because the state that resumes it sits beside
|
|
@@ -1010,6 +1055,13 @@ export function createRscHandler(options) {
|
|
|
1010
1055
|
}),
|
|
1011
1056
|
});
|
|
1012
1057
|
servedFrom.set(withHoles, "shell");
|
|
1058
|
+
// The shell's head is the document's head; its fonts are known too.
|
|
1059
|
+
let found = hintsByKey.get(shellKey);
|
|
1060
|
+
if (!found) {
|
|
1061
|
+
found = criticalAssetsOf(shell);
|
|
1062
|
+
hintsByKey.set(shellKey, found);
|
|
1063
|
+
}
|
|
1064
|
+
hinted.set(withHoles, found);
|
|
1013
1065
|
return withHoles;
|
|
1014
1066
|
}
|
|
1015
1067
|
// Only the document is ever served frozen for a shell. The payload is what
|