@rsc-kit/core 0.20.3 → 0.20.5
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 +3 -0
- package/dist/host.js +31 -1
- package/dist/host.js.map +1 -1
- package/dist/js/Link.js +1 -1
- package/dist/js/Link.js.map +1 -1
- package/dist/js/PathnameProvider.js +14 -1
- package/dist/js/PathnameProvider.js.map +1 -1
- package/dist/js/createViteRscApp.js +6 -0
- package/dist/js/createViteRscApp.js.map +1 -1
- package/dist/js/earlyClicks.d.ts +35 -0
- package/dist/js/earlyClicks.js +67 -0
- package/dist/js/earlyClicks.js.map +1 -0
- package/dist/js/navigate.js +95 -11
- package/dist/js/navigate.js.map +1 -1
- package/dist/js/updateStore.d.ts +6 -0
- package/dist/js/updateStore.js +48 -2
- package/dist/js/updateStore.js.map +1 -1
- package/dist/vite.js +113 -11
- 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
|
|
@@ -20,6 +21,8 @@ export interface RscEngine {
|
|
|
20
21
|
manifest?(): RouteManifest;
|
|
21
22
|
/** A short id of this build's client, for the version the host answers with when given none. */
|
|
22
23
|
buildId?(): Promise<string>;
|
|
24
|
+
/** The stylesheet and client entry every document links, for the Link header a CDN sends ahead. */
|
|
25
|
+
criticalAssets?(): CriticalAssets;
|
|
23
26
|
installHostFn(fn: (name: string, ...args: unknown[]) => unknown): void;
|
|
24
27
|
handleRscStream(component: string, props?: Record<string, unknown>, layouts?: {
|
|
25
28
|
component: string;
|
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
|
|
@@ -356,6 +357,10 @@ export function createRscHandler(options) {
|
|
|
356
357
|
const identify = Boolean(manifest.build?.identify);
|
|
357
358
|
/** How a response was answered, for X-RSC-Kit: a file the build wrote, or a shell of one. */
|
|
358
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: [] };
|
|
359
364
|
/**
|
|
360
365
|
* The page a payload url belongs to, if this is one.
|
|
361
366
|
*
|
|
@@ -457,6 +462,16 @@ export function createRscHandler(options) {
|
|
|
457
462
|
// Network tab, the way X-Nextjs-Cache is. Always: it names no
|
|
458
463
|
// product, and a CDN rule or a check can key on it.
|
|
459
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
|
+
}
|
|
460
475
|
// What built it. The name only, never the version - a version in
|
|
461
476
|
// every response is what a vulnerability scanner filters on - and
|
|
462
477
|
// off for a team whose policy strips every framework identifier.
|
|
@@ -958,7 +973,7 @@ export function createRscHandler(options) {
|
|
|
958
973
|
const whole = await read(`${key}.html`);
|
|
959
974
|
// A whole page is finished. Nothing to resume, nothing to render.
|
|
960
975
|
if (whole !== null) {
|
|
961
|
-
|
|
976
|
+
const response = new Response(whole, {
|
|
962
977
|
headers: withVersion({
|
|
963
978
|
"Content-Type": HTML_TYPE,
|
|
964
979
|
Vary: VARY_ON_RSC,
|
|
@@ -967,6 +982,14 @@ export function createRscHandler(options) {
|
|
|
967
982
|
: REVALIDATE,
|
|
968
983
|
}),
|
|
969
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;
|
|
970
993
|
}
|
|
971
994
|
// Then a shell, under this url or under the route's pattern. Which of the
|
|
972
995
|
// two answered is tracked, because the state that resumes it sits beside
|
|
@@ -1032,6 +1055,13 @@ export function createRscHandler(options) {
|
|
|
1032
1055
|
}),
|
|
1033
1056
|
});
|
|
1034
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);
|
|
1035
1065
|
return withHoles;
|
|
1036
1066
|
}
|
|
1037
1067
|
// Only the document is ever served frozen for a shell. The payload is what
|