@rsc-kit/core 0.20.12 → 0.21.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/apiPrerender.js +1 -1
- package/dist/apiPrerender.js.map +1 -1
- package/dist/appAssets.d.ts +6 -0
- package/dist/appAssets.js +34 -0
- package/dist/appAssets.js.map +1 -1
- package/dist/appleSplash.d.ts +10 -0
- package/dist/appleSplash.js +74 -0
- package/dist/appleSplash.js.map +1 -0
- package/dist/earlyHints.js +7 -2
- package/dist/earlyHints.js.map +1 -1
- package/dist/export.js +3 -1
- package/dist/export.js.map +1 -1
- package/dist/files.d.ts +0 -8
- package/dist/files.js +7 -18
- package/dist/files.js.map +1 -1
- package/dist/headers.d.ts +8 -0
- package/dist/headers.js +8 -0
- package/dist/headers.js.map +1 -1
- package/dist/host.d.ts +15 -0
- package/dist/host.js +133 -27
- package/dist/host.js.map +1 -1
- package/dist/js/Form.d.ts +47 -54
- package/dist/js/Form.js +41 -48
- package/dist/js/Form.js.map +1 -1
- package/dist/js/chunkPreload.d.ts +26 -0
- package/dist/js/chunkPreload.js +66 -0
- package/dist/js/chunkPreload.js.map +1 -0
- package/dist/js/installCapture.d.ts +15 -0
- package/dist/js/installCapture.js +17 -0
- package/dist/js/installCapture.js.map +1 -0
- package/dist/js/navigate.js +4 -0
- package/dist/js/navigate.js.map +1 -1
- package/dist/js/useInstall.d.ts +15 -0
- package/dist/js/useInstall.js +102 -0
- package/dist/js/useInstall.js.map +1 -0
- package/dist/manifest.d.ts +2 -0
- package/dist/manifest.js.map +1 -1
- package/dist/metadata.d.ts +31 -0
- package/dist/metadata.js.map +1 -1
- package/dist/prerender.d.ts +24 -5
- package/dist/prerender.js +107 -19
- package/dist/prerender.js.map +1 -1
- package/dist/request.js +3 -2
- package/dist/request.js.map +1 -1
- package/dist/shellHead.js +4 -2
- package/dist/shellHead.js.map +1 -1
- package/dist/stableNames.d.ts +52 -0
- package/dist/stableNames.js +91 -0
- package/dist/stableNames.js.map +1 -0
- package/dist/vite.d.ts +5 -3
- package/dist/vite.js +215 -13
- package/dist/vite.js.map +1 -1
- package/dist/webManifest.d.ts +37 -0
- package/dist/webManifest.js +51 -6
- package/dist/webManifest.js.map +1 -1
- package/package.json +8 -4
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
/**
|
|
3
|
+
* Installing the app, from a button of its own.
|
|
4
|
+
*
|
|
5
|
+
* const { canInstall, install, installed, ios } = useInstall()
|
|
6
|
+
*
|
|
7
|
+
* {canInstall && <button onClick={install}>Install</button>}
|
|
8
|
+
* {ios && !installed && <p>Share, then Add to Home Screen</p>}
|
|
9
|
+
*
|
|
10
|
+
* `canInstall` - the browser has offered, and `install()` will show its
|
|
11
|
+
* dialog. Chrome, Edge and Android. The offer is caught by the page's inline
|
|
12
|
+
* bootstrap before anything else runs (see installCapture.ts), so a button
|
|
13
|
+
* that mounts late still gets it.
|
|
14
|
+
*
|
|
15
|
+
* `ios` - Safari on an iPhone or iPad, where there is no offer to catch and
|
|
16
|
+
* installing is Share, then Add to Home Screen. The app says so; nothing can
|
|
17
|
+
* do it for the visitor.
|
|
18
|
+
*
|
|
19
|
+
* `installed` - running as the installed app, or installed while this page
|
|
20
|
+
* was open. For hiding the button, or for anything that should differ in the
|
|
21
|
+
* app's own window.
|
|
22
|
+
*/
|
|
23
|
+
import { useSyncExternalStore } from "react";
|
|
24
|
+
const listeners = new Set();
|
|
25
|
+
const SERVER = { canInstall: false, installed: false, ios: false };
|
|
26
|
+
let current = SERVER;
|
|
27
|
+
function standalone() {
|
|
28
|
+
return (window.matchMedia?.("(display-mode: standalone)").matches === true ||
|
|
29
|
+
navigator.standalone === true);
|
|
30
|
+
}
|
|
31
|
+
function onIos() {
|
|
32
|
+
return (/iPhone|iPad|iPod/.test(navigator.userAgent) ||
|
|
33
|
+
// iPadOS asks for the desktop site and says it is a Mac.
|
|
34
|
+
(navigator.platform === "MacIntel" && navigator.maxTouchPoints > 1));
|
|
35
|
+
}
|
|
36
|
+
/** The snapshot, replaced only when something changed - useSyncExternalStore compares by identity. */
|
|
37
|
+
function read() {
|
|
38
|
+
const w = window;
|
|
39
|
+
const installed = w.__rsc_installed === true || standalone();
|
|
40
|
+
const next = {
|
|
41
|
+
canInstall: !installed && w.__rsc_install_prompt != null,
|
|
42
|
+
installed,
|
|
43
|
+
ios: !installed && onIos(),
|
|
44
|
+
};
|
|
45
|
+
if (next.canInstall !== current.canInstall || next.installed !== current.installed || next.ios !== current.ios) {
|
|
46
|
+
current = next;
|
|
47
|
+
}
|
|
48
|
+
return current;
|
|
49
|
+
}
|
|
50
|
+
function notify() {
|
|
51
|
+
for (const listener of listeners)
|
|
52
|
+
listener();
|
|
53
|
+
}
|
|
54
|
+
let listening = false;
|
|
55
|
+
/**
|
|
56
|
+
* The browser's own events, from the first subscriber on. Anything that came
|
|
57
|
+
* before that is already on the window - the page's bootstrap kept it - so
|
|
58
|
+
* listening from here misses nothing, and a page that never renders the hook
|
|
59
|
+
* never listens.
|
|
60
|
+
*/
|
|
61
|
+
function listen() {
|
|
62
|
+
if (listening)
|
|
63
|
+
return;
|
|
64
|
+
listening = true;
|
|
65
|
+
const w = window;
|
|
66
|
+
window.addEventListener("beforeinstallprompt", (event) => {
|
|
67
|
+
w.__rsc_install_prompt = event;
|
|
68
|
+
notify();
|
|
69
|
+
});
|
|
70
|
+
window.addEventListener("appinstalled", () => {
|
|
71
|
+
w.__rsc_install_prompt = null;
|
|
72
|
+
w.__rsc_installed = true;
|
|
73
|
+
notify();
|
|
74
|
+
});
|
|
75
|
+
window.matchMedia?.("(display-mode: standalone)").addEventListener?.("change", notify);
|
|
76
|
+
}
|
|
77
|
+
function subscribe(listener) {
|
|
78
|
+
listen();
|
|
79
|
+
listeners.add(listener);
|
|
80
|
+
return () => listeners.delete(listener);
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Show the browser's install dialog. Resolves with what the visitor chose,
|
|
84
|
+
* or `unavailable` when there is no offer to act on - on iOS, in a browser
|
|
85
|
+
* that makes none, or once it has been used: an offer can be shown once.
|
|
86
|
+
*/
|
|
87
|
+
async function install() {
|
|
88
|
+
const w = window;
|
|
89
|
+
const offer = w.__rsc_install_prompt;
|
|
90
|
+
if (!offer)
|
|
91
|
+
return "unavailable";
|
|
92
|
+
w.__rsc_install_prompt = null;
|
|
93
|
+
notify();
|
|
94
|
+
await offer.prompt();
|
|
95
|
+
return (await offer.userChoice).outcome;
|
|
96
|
+
}
|
|
97
|
+
export function useInstall() {
|
|
98
|
+
const state = useSyncExternalStore(subscribe, read, () => SERVER);
|
|
99
|
+
return { ...state, install };
|
|
100
|
+
}
|
|
101
|
+
export default useInstall;
|
|
102
|
+
//# sourceMappingURL=useInstall.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"useInstall.js","sourceRoot":"","sources":["../../src/js/useInstall.ts"],"names":[],"mappings":"AAAA,YAAY,CAAC;AAEb;;;;;;;;;;;;;;;;;;;;GAoBG;AAEH,OAAO,EAAE,oBAAoB,EAAE,MAAM,OAAO,CAAC;AAkB7C,MAAM,SAAS,GAAG,IAAI,GAAG,EAAc,CAAC;AACxC,MAAM,MAAM,GAAiB,EAAE,UAAU,EAAE,KAAK,EAAE,SAAS,EAAE,KAAK,EAAE,GAAG,EAAE,KAAK,EAAE,CAAC;AACjF,IAAI,OAAO,GAAiB,MAAM,CAAC;AAEnC,SAAS,UAAU;IACjB,OAAO,CACL,MAAM,CAAC,UAAU,EAAE,CAAC,4BAA4B,CAAC,CAAC,OAAO,KAAK,IAAI;QACjE,SAAsC,CAAC,UAAU,KAAK,IAAI,CAC5D,CAAC;AACJ,CAAC;AAED,SAAS,KAAK;IACZ,OAAO,CACL,kBAAkB,CAAC,IAAI,CAAC,SAAS,CAAC,SAAS,CAAC;QAC5C,yDAAyD;QACzD,CAAC,SAAS,CAAC,QAAQ,KAAK,UAAU,IAAI,SAAS,CAAC,cAAc,GAAG,CAAC,CAAC,CACpE,CAAC;AACJ,CAAC;AAED,sGAAsG;AACtG,SAAS,IAAI;IACX,MAAM,CAAC,GAAG,MAA6B,CAAC;IACxC,MAAM,SAAS,GAAG,CAAC,CAAC,eAAe,KAAK,IAAI,IAAI,UAAU,EAAE,CAAC;IAC7D,MAAM,IAAI,GAAiB;QACzB,UAAU,EAAE,CAAC,SAAS,IAAI,CAAC,CAAC,oBAAoB,IAAI,IAAI;QACxD,SAAS;QACT,GAAG,EAAE,CAAC,SAAS,IAAI,KAAK,EAAE;KAC3B,CAAC;IAEF,IAAI,IAAI,CAAC,UAAU,KAAK,OAAO,CAAC,UAAU,IAAI,IAAI,CAAC,SAAS,KAAK,OAAO,CAAC,SAAS,IAAI,IAAI,CAAC,GAAG,KAAK,OAAO,CAAC,GAAG,EAAE,CAAC;QAC/G,OAAO,GAAG,IAAI,CAAC;IACjB,CAAC;IAED,OAAO,OAAO,CAAC;AACjB,CAAC;AAED,SAAS,MAAM;IACb,KAAK,MAAM,QAAQ,IAAI,SAAS;QAAE,QAAQ,EAAE,CAAC;AAC/C,CAAC;AAED,IAAI,SAAS,GAAG,KAAK,CAAC;AAEtB;;;;;GAKG;AACH,SAAS,MAAM;IACb,IAAI,SAAS;QAAE,OAAO;IAEtB,SAAS,GAAG,IAAI,CAAC;IAEjB,MAAM,CAAC,GAAG,MAA6B,CAAC;IAExC,MAAM,CAAC,gBAAgB,CAAC,qBAAqB,EAAE,CAAC,KAAK,EAAE,EAAE;QACvD,CAAC,CAAC,oBAAoB,GAAG,KAAsB,CAAC;QAChD,MAAM,EAAE,CAAC;IACX,CAAC,CAAC,CAAC;IACH,MAAM,CAAC,gBAAgB,CAAC,cAAc,EAAE,GAAG,EAAE;QAC3C,CAAC,CAAC,oBAAoB,GAAG,IAAI,CAAC;QAC9B,CAAC,CAAC,eAAe,GAAG,IAAI,CAAC;QACzB,MAAM,EAAE,CAAC;IACX,CAAC,CAAC,CAAC;IACH,MAAM,CAAC,UAAU,EAAE,CAAC,4BAA4B,CAAC,CAAC,gBAAgB,EAAE,CAAC,QAAQ,EAAE,MAAM,CAAC,CAAC;AACzF,CAAC;AAED,SAAS,SAAS,CAAC,QAAoB;IACrC,MAAM,EAAE,CAAC;IACT,SAAS,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC;IAExB,OAAO,GAAG,EAAE,CAAC,SAAS,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC;AAC1C,CAAC;AAED;;;;GAIG;AACH,KAAK,UAAU,OAAO;IACpB,MAAM,CAAC,GAAG,MAA6B,CAAC;IACxC,MAAM,KAAK,GAAG,CAAC,CAAC,oBAAoB,CAAC;IAErC,IAAI,CAAC,KAAK;QAAE,OAAO,aAAa,CAAC;IAEjC,CAAC,CAAC,oBAAoB,GAAG,IAAI,CAAC;IAC9B,MAAM,EAAE,CAAC;IACT,MAAM,KAAK,CAAC,MAAM,EAAE,CAAC;IAErB,OAAO,CAAC,MAAM,KAAK,CAAC,UAAU,CAAC,CAAC,OAAO,CAAC;AAC1C,CAAC;AAED,MAAM,UAAU,UAAU;IACxB,MAAM,KAAK,GAAG,oBAAoB,CAAC,SAAS,EAAE,IAAI,EAAE,GAAG,EAAE,CAAC,MAAM,CAAC,CAAC;IAElE,OAAO,EAAE,GAAG,KAAK,EAAE,OAAO,EAAE,CAAC;AAC/B,CAAC;AAED,eAAe,UAAU,CAAC","sourcesContent":["\"use client\";\n\n/**\n * Installing the app, from a button of its own.\n *\n * const { canInstall, install, installed, ios } = useInstall()\n *\n * {canInstall && <button onClick={install}>Install</button>}\n * {ios && !installed && <p>Share, then Add to Home Screen</p>}\n *\n * `canInstall` - the browser has offered, and `install()` will show its\n * dialog. Chrome, Edge and Android. The offer is caught by the page's inline\n * bootstrap before anything else runs (see installCapture.ts), so a button\n * that mounts late still gets it.\n *\n * `ios` - Safari on an iPhone or iPad, where there is no offer to catch and\n * installing is Share, then Add to Home Screen. The app says so; nothing can\n * do it for the visitor.\n *\n * `installed` - running as the installed app, or installed while this page\n * was open. For hiding the button, or for anything that should differ in the\n * app's own window.\n */\n\nimport { useSyncExternalStore } from \"react\";\n\ninterface InstallPrompt extends Event {\n prompt(): Promise<void>;\n userChoice: Promise<{ outcome: \"accepted\" | \"dismissed\" }>;\n}\n\ninterface InstallState {\n canInstall: boolean;\n installed: boolean;\n ios: boolean;\n}\n\ntype Captured = {\n __rsc_install_prompt?: InstallPrompt | null;\n __rsc_installed?: boolean;\n};\n\nconst listeners = new Set<() => void>();\nconst SERVER: InstallState = { canInstall: false, installed: false, ios: false };\nlet current: InstallState = SERVER;\n\nfunction standalone(): boolean {\n return (\n window.matchMedia?.(\"(display-mode: standalone)\").matches === true ||\n (navigator as { standalone?: boolean }).standalone === true\n );\n}\n\nfunction onIos(): boolean {\n return (\n /iPhone|iPad|iPod/.test(navigator.userAgent) ||\n // iPadOS asks for the desktop site and says it is a Mac.\n (navigator.platform === \"MacIntel\" && navigator.maxTouchPoints > 1)\n );\n}\n\n/** The snapshot, replaced only when something changed - useSyncExternalStore compares by identity. */\nfunction read(): InstallState {\n const w = window as unknown as Captured;\n const installed = w.__rsc_installed === true || standalone();\n const next: InstallState = {\n canInstall: !installed && w.__rsc_install_prompt != null,\n installed,\n ios: !installed && onIos(),\n };\n\n if (next.canInstall !== current.canInstall || next.installed !== current.installed || next.ios !== current.ios) {\n current = next;\n }\n\n return current;\n}\n\nfunction notify(): void {\n for (const listener of listeners) listener();\n}\n\nlet listening = false;\n\n/**\n * The browser's own events, from the first subscriber on. Anything that came\n * before that is already on the window - the page's bootstrap kept it - so\n * listening from here misses nothing, and a page that never renders the hook\n * never listens.\n */\nfunction listen(): void {\n if (listening) return;\n\n listening = true;\n\n const w = window as unknown as Captured;\n\n window.addEventListener(\"beforeinstallprompt\", (event) => {\n w.__rsc_install_prompt = event as InstallPrompt;\n notify();\n });\n window.addEventListener(\"appinstalled\", () => {\n w.__rsc_install_prompt = null;\n w.__rsc_installed = true;\n notify();\n });\n window.matchMedia?.(\"(display-mode: standalone)\").addEventListener?.(\"change\", notify);\n}\n\nfunction subscribe(listener: () => void): () => void {\n listen();\n listeners.add(listener);\n\n return () => listeners.delete(listener);\n}\n\n/**\n * Show the browser's install dialog. Resolves with what the visitor chose,\n * or `unavailable` when there is no offer to act on - on iOS, in a browser\n * that makes none, or once it has been used: an offer can be shown once.\n */\nasync function install(): Promise<\"accepted\" | \"dismissed\" | \"unavailable\"> {\n const w = window as unknown as Captured;\n const offer = w.__rsc_install_prompt;\n\n if (!offer) return \"unavailable\";\n\n w.__rsc_install_prompt = null;\n notify();\n await offer.prompt();\n\n return (await offer.userChoice).outcome;\n}\n\nexport function useInstall(): InstallState & { install: typeof install } {\n const state = useSyncExternalStore(subscribe, read, () => SERVER);\n\n return { ...state, install };\n}\n\nexport default useInstall;\n"]}
|
package/dist/manifest.d.ts
CHANGED
|
@@ -100,6 +100,8 @@ export interface ManifestApiRoute {
|
|
|
100
100
|
* opened a hole in it.
|
|
101
101
|
*/
|
|
102
102
|
middleware: string[];
|
|
103
|
+
/** Host middleware above this route (`export const middleware = [...]`), outermost first. */
|
|
104
|
+
hostMiddleware?: string[];
|
|
103
105
|
}
|
|
104
106
|
export interface RouteManifest {
|
|
105
107
|
version: number;
|
package/dist/manifest.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"manifest.js","sourceRoot":"","sources":["../src/manifest.ts"],"names":[],"mappings":"AAAA,4EAA4E;AAC5E,EAAE;AACF,6EAA6E;AAC7E,6EAA6E;AAC7E,+EAA+E;AAC/E,gFAAgF;AAChF,uCAAuC;AACvC,EAAE;AACF,6EAA6E;AAC7E,+EAA+E;AAC/E,oBAAoB","sourcesContent":["// The shape of routes.json — what the build discovered, for a host to read.\n//\n// The build already walks app/ to generate its entries, and every host needs\n// the same facts: which url a component answers, what layouts wrap it, which\n// slots and sections belong to it. Laravel used to scan the tree a second time\n// to work that out; a JS host would have had to write a third walk. This is the\n// one answer, and these are its types.\n//\n// Urls are segments rather than a pattern string, because the pattern is the\n// host's dialect: Laravel writes {slug}, Hono writes :slug, and neither is the\n// build's business.\n\nexport interface RouteSegment {\n /**\n * `host`: a `[name]` directory at the top of app/. Bound from the request's\n * host, never from a path segment - so `example.com/nope` is a 404 rather\n * than a tenant called \"nope\". See hostRouting.\n */\n type: \"static\" | \"param\" | \"catchAll\" | \"host\";\n value: string;\n}\n\nexport interface ManifestRoute {\n component: string;\n segments: RouteSegment[];\n layouts: string[];\n loadings: string[];\n /**\n * `error.tsx` files above this route, outermost first.\n *\n * The nearest one to a failure catches it, the same way the nearest\n * `loading.tsx` is the fallback. Optional: a manifest from a build before\n * error boundaries existed has none.\n */\n errors?: string[];\n /**\n * `middleware.ts` files above this route, outermost first.\n *\n * Run before anything at or below them renders, on every path. A check is\n * not UI, and making it a layout meant the client could decline it: layouts\n * are skipped on a partial navigation, and what gets skipped is named in a\n * header nothing can verify.\n */\n middleware: string[];\n slots: Record<string, string>;\n sections: string[];\n /**\n * The host's route-config file beside this page, if it named one, and the\n * ancestor ones that also apply — outermost first, this page's excluded.\n *\n * Relative to the project root: an absolute path is true only on the machine\n * that produced it, and building in a container is ordinary.\n */\n config: string | null;\n ancestorConfigs: string[];\n /**\n * Host middleware names for this route, outermost first.\n *\n * Declared in a route.ts beside or above the page. The engine does not know\n * what they mean — they are the host's own vocabulary — it only runs them\n * past the host before anything at or below this route renders.\n *\n * Empty on a route that named none, and on every route in an app that never\n * wrote a route.ts, which is why this needs no flag.\n *\n * Optional because registration boots from the PREVIOUS build's manifest: a\n * shape change takes two builds to settle, and a required field would make\n * the first of those a hard failure rather than a route with no guards.\n */\n hostMiddleware?: string[];\n /**\n * Whether the page exports generateStaticParams.\n *\n * Recorded here so a host can plan a build — which routes to ask for urls,\n * which to leave on demand — without loading the server bundle first. The\n * function itself is reached through the bundle's getStaticParams(), because\n * only the bundle can run it.\n */\n staticParams: boolean;\n}\n\nexport interface ManifestIntercept {\n component: string;\n slot: string;\n segments: RouteSegment[];\n /** (.) same level, (..) one up, (...) from the root. */\n marker: string;\n}\n\n/**\n * A `route.ts` — an api endpoint rather than a page.\n *\n * Separate from `routes` because it is matched before them and answered\n * without rendering anything: no layouts, no payload, no client. A url cannot\n * be both, and the build refuses one that is.\n */\nexport interface ManifestApiRoute {\n /** The module name, as the engine's registry keys it. */\n name: string;\n /**\n * The file that answers, relative to the source directory - `app/api/x/route.ts`,\n * or `app/sitemap.ts` for a route the build synthesised from a metadata file\n * - so a line about the route can say where to look.\n */\n source?: string;\n segments: RouteSegment[];\n /** Which methods the file exports, so a 405 can name the rest. */\n methods: string[];\n /**\n * `middleware.ts` files above this route, outermost first.\n *\n * The same chain a page in that directory runs. A route.ts sits among the\n * pages it belongs with, so a guard on the directory covers it too —\n * anything else would mean adding an endpoint under a guarded path silently\n * opened a hole in it.\n */\n middleware: string[];\n}\n\nexport interface RouteManifest {\n version: number;\n build: {\n output: string;\n exportPath: string;\n payloadName: string;\n /**\n * The site's own hosts, bare and lower-case. A request from any other\n * host is matched with that host's segment in front of its path - see\n * hostRouting. Absent or empty: every request is the site's own.\n */\n hosts?: string[];\n /**\n * Whether every response says what built it: `X-Powered-By: rsc-kit` and\n * a generator meta tag in the document. The name only, never the version.\n * `X-RSC-Kit` (how a response was served) is sent regardless.\n */\n identify?: boolean;\n };\n routes: ManifestRoute[];\n intercepts: ManifestIntercept[];\n /** Optional: a manifest from a build before api routes existed has none. */\n apis?: ManifestApiRoute[];\n}\n"]}
|
|
1
|
+
{"version":3,"file":"manifest.js","sourceRoot":"","sources":["../src/manifest.ts"],"names":[],"mappings":"AAAA,4EAA4E;AAC5E,EAAE;AACF,6EAA6E;AAC7E,6EAA6E;AAC7E,+EAA+E;AAC/E,gFAAgF;AAChF,uCAAuC;AACvC,EAAE;AACF,6EAA6E;AAC7E,+EAA+E;AAC/E,oBAAoB","sourcesContent":["// The shape of routes.json — what the build discovered, for a host to read.\n//\n// The build already walks app/ to generate its entries, and every host needs\n// the same facts: which url a component answers, what layouts wrap it, which\n// slots and sections belong to it. Laravel used to scan the tree a second time\n// to work that out; a JS host would have had to write a third walk. This is the\n// one answer, and these are its types.\n//\n// Urls are segments rather than a pattern string, because the pattern is the\n// host's dialect: Laravel writes {slug}, Hono writes :slug, and neither is the\n// build's business.\n\nexport interface RouteSegment {\n /**\n * `host`: a `[name]` directory at the top of app/. Bound from the request's\n * host, never from a path segment - so `example.com/nope` is a 404 rather\n * than a tenant called \"nope\". See hostRouting.\n */\n type: \"static\" | \"param\" | \"catchAll\" | \"host\";\n value: string;\n}\n\nexport interface ManifestRoute {\n component: string;\n segments: RouteSegment[];\n layouts: string[];\n loadings: string[];\n /**\n * `error.tsx` files above this route, outermost first.\n *\n * The nearest one to a failure catches it, the same way the nearest\n * `loading.tsx` is the fallback. Optional: a manifest from a build before\n * error boundaries existed has none.\n */\n errors?: string[];\n /**\n * `middleware.ts` files above this route, outermost first.\n *\n * Run before anything at or below them renders, on every path. A check is\n * not UI, and making it a layout meant the client could decline it: layouts\n * are skipped on a partial navigation, and what gets skipped is named in a\n * header nothing can verify.\n */\n middleware: string[];\n slots: Record<string, string>;\n sections: string[];\n /**\n * The host's route-config file beside this page, if it named one, and the\n * ancestor ones that also apply — outermost first, this page's excluded.\n *\n * Relative to the project root: an absolute path is true only on the machine\n * that produced it, and building in a container is ordinary.\n */\n config: string | null;\n ancestorConfigs: string[];\n /**\n * Host middleware names for this route, outermost first.\n *\n * Declared in a route.ts beside or above the page. The engine does not know\n * what they mean — they are the host's own vocabulary — it only runs them\n * past the host before anything at or below this route renders.\n *\n * Empty on a route that named none, and on every route in an app that never\n * wrote a route.ts, which is why this needs no flag.\n *\n * Optional because registration boots from the PREVIOUS build's manifest: a\n * shape change takes two builds to settle, and a required field would make\n * the first of those a hard failure rather than a route with no guards.\n */\n hostMiddleware?: string[];\n /**\n * Whether the page exports generateStaticParams.\n *\n * Recorded here so a host can plan a build — which routes to ask for urls,\n * which to leave on demand — without loading the server bundle first. The\n * function itself is reached through the bundle's getStaticParams(), because\n * only the bundle can run it.\n */\n staticParams: boolean;\n}\n\nexport interface ManifestIntercept {\n component: string;\n slot: string;\n segments: RouteSegment[];\n /** (.) same level, (..) one up, (...) from the root. */\n marker: string;\n}\n\n/**\n * A `route.ts` — an api endpoint rather than a page.\n *\n * Separate from `routes` because it is matched before them and answered\n * without rendering anything: no layouts, no payload, no client. A url cannot\n * be both, and the build refuses one that is.\n */\nexport interface ManifestApiRoute {\n /** The module name, as the engine's registry keys it. */\n name: string;\n /**\n * The file that answers, relative to the source directory - `app/api/x/route.ts`,\n * or `app/sitemap.ts` for a route the build synthesised from a metadata file\n * - so a line about the route can say where to look.\n */\n source?: string;\n segments: RouteSegment[];\n /** Which methods the file exports, so a 405 can name the rest. */\n methods: string[];\n /**\n * `middleware.ts` files above this route, outermost first.\n *\n * The same chain a page in that directory runs. A route.ts sits among the\n * pages it belongs with, so a guard on the directory covers it too —\n * anything else would mean adding an endpoint under a guarded path silently\n * opened a hole in it.\n */\n middleware: string[];\n /** Host middleware above this route (`export const middleware = [...]`), outermost first. */\n hostMiddleware?: string[];\n}\n\nexport interface RouteManifest {\n version: number;\n build: {\n output: string;\n exportPath: string;\n payloadName: string;\n /**\n * The site's own hosts, bare and lower-case. A request from any other\n * host is matched with that host's segment in front of its path - see\n * hostRouting. Absent or empty: every request is the site's own.\n */\n hosts?: string[];\n /**\n * Whether every response says what built it: `X-Powered-By: rsc-kit` and\n * a generator meta tag in the document. The name only, never the version.\n * `X-RSC-Kit` (how a response was served) is sent regardless.\n */\n identify?: boolean;\n };\n routes: ManifestRoute[];\n intercepts: ManifestIntercept[];\n /** Optional: a manifest from a build before api routes existed has none. */\n apis?: ManifestApiRoute[];\n}\n"]}
|
package/dist/metadata.d.ts
CHANGED
|
@@ -71,6 +71,21 @@ export interface Robots {
|
|
|
71
71
|
"max-snippet"?: number;
|
|
72
72
|
googleBot?: string | Omit<Robots, "googleBot">;
|
|
73
73
|
}
|
|
74
|
+
/** An iOS launch screen: an image, and the media query naming the device it is for. */
|
|
75
|
+
export interface AppleStartupImage {
|
|
76
|
+
url: string | URL;
|
|
77
|
+
media?: string;
|
|
78
|
+
}
|
|
79
|
+
export interface AppleWebApp {
|
|
80
|
+
/** Opens as an app from the home screen - no browser chrome. */
|
|
81
|
+
capable?: boolean;
|
|
82
|
+
/** The name under the home screen icon, when it should differ from the page title. */
|
|
83
|
+
title?: string;
|
|
84
|
+
/** The status bar over the app: `default`, `black`, or `black-translucent` to draw under it. */
|
|
85
|
+
statusBarStyle?: "default" | "black" | "black-translucent";
|
|
86
|
+
/** Launch screens. A string or url is one image for every device. */
|
|
87
|
+
startupImage?: string | URL | (string | URL | AppleStartupImage)[];
|
|
88
|
+
}
|
|
74
89
|
export interface Metadata {
|
|
75
90
|
/** A string on a page; a template on a layout, applied to the pages below it. */
|
|
76
91
|
title?: string | TitleTemplate;
|
|
@@ -98,6 +113,22 @@ export interface Metadata {
|
|
|
98
113
|
icons?: IconURL | (IconURL | IconDescriptor)[] | Icons | null;
|
|
99
114
|
openGraph?: OpenGraph;
|
|
100
115
|
twitter?: Twitter;
|
|
116
|
+
/**
|
|
117
|
+
* How the app behaves added to an iPhone's home screen. Next's shape, so a
|
|
118
|
+
* port carries it across unchanged.
|
|
119
|
+
*
|
|
120
|
+
* appleWebApp: {
|
|
121
|
+
* capable: true,
|
|
122
|
+
* title: 'Orders',
|
|
123
|
+
* statusBarStyle: 'black-translucent',
|
|
124
|
+
* startupImage: [{ url: '/splash-1179x2556.png', media: '...' }],
|
|
125
|
+
* }
|
|
126
|
+
*
|
|
127
|
+
* `true` is `{ capable: true }`. Launch screens are simpler as files: an
|
|
128
|
+
* `apple-splash-1179x2556.png` in `app/` is linked with the right media
|
|
129
|
+
* query for the device its size names - see the PWA guide.
|
|
130
|
+
*/
|
|
131
|
+
appleWebApp?: boolean | AppleWebApp;
|
|
101
132
|
/** @deprecated Use `openGraph.title`. Still rendered, correctly, as `property=`. */
|
|
102
133
|
"og:title"?: string;
|
|
103
134
|
/** @deprecated Use `openGraph.description`. */
|
package/dist/metadata.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"metadata.js","sourceRoot":"","sources":["../src/metadata.ts"],"names":[],"mappings":"AAAA,sDAAsD;AACtD,EAAE;AACF,6DAA6D;AAC7D,EAAE;AACF,4DAA4D;AAC5D,EAAE;AACF,4EAA4E;AAC5E,+EAA+E;AAC/E,+EAA+E;AAC/E,4EAA4E;AAC5E,2EAA2E;AAC3E,EAAE;AACF,4EAA4E;AAC5E,+EAA+E;AAC/E,MAAM","sourcesContent":["// What a page says about itself, as importable types.\n//\n// import type { Metadata } from '@rsc-kit/core/metadata'\n//\n// export const metadata: Metadata = { title: 'Orders' }\n//\n// Imported rather than ambient, and that is the whole point of the move. An\n// ambient declaration has to be COPIED into the project, which means it is not\n// there until the build has run once — so a freshly cloned app reports \"Cannot\n// find name 'Metadata'\" on every page until someone runs the dev server. An\n// import resolves from node_modules the moment dependencies are installed.\n//\n// The ambient names still work. `.rsc-kit/rsc-types.d.ts` now aliases these\n// rather than restating them, so there is one definition and two ways to reach\n// it.\n\nexport interface IconDescriptor {\n url: string | URL;\n type?: string;\n sizes?: string;\n color?: string;\n rel?: string;\n media?: string;\n fetchPriority?: \"high\" | \"low\" | \"auto\";\n}\n\nexport type IconURL = string | URL;\n\nexport interface Icons {\n icon?: IconURL | IconDescriptor | (IconURL | IconDescriptor)[];\n apple?: IconURL | IconDescriptor | (IconURL | IconDescriptor)[];\n shortcut?: IconURL | IconDescriptor | (IconURL | IconDescriptor)[];\n other?: IconDescriptor | IconDescriptor[];\n}\n\n/** A layout's title, wrapping the titles of the pages beneath it. */\nexport interface TitleTemplate {\n /** `%s` stands in for the page's own title. */\n template?: string;\n /** Used by a page that exports no title of its own. */\n default?: string;\n}\n\n/** One image a share card may show. A string is its url. */\nexport interface OpenGraphImage {\n url: string | URL;\n width?: number;\n height?: number;\n alt?: string;\n type?: string;\n}\n\n/**\n * The card a link to this page unfurls into on Facebook, Slack, LinkedIn and\n * most of the rest. Rendered with `property=`, which is what those scrapers\n * read — a `name=` attribute is ignored by every one of them.\n */\nexport interface OpenGraph {\n title?: string;\n description?: string;\n /** Absolute, or relative to `metadataBase`. */\n url?: string | URL;\n siteName?: string;\n type?: \"website\" | \"article\" | \"profile\" | \"book\" | (string & {});\n locale?: string;\n images?: string | URL | OpenGraphImage | (string | URL | OpenGraphImage)[];\n}\n\n/** The same card for X, which reads `name=` rather than `property=`. */\nexport interface Twitter {\n card?: \"summary\" | \"summary_large_image\" | \"app\" | \"player\";\n title?: string;\n description?: string;\n /** The site's account, `@handle`. */\n site?: string;\n /** The author's account, `@handle`. */\n creator?: string;\n images?: string | URL | OpenGraphImage | (string | URL | OpenGraphImage)[];\n}\n\nexport interface Robots {\n index?: boolean;\n follow?: boolean;\n noarchive?: boolean;\n nosnippet?: boolean;\n noimageindex?: boolean;\n nocache?: boolean;\n notranslate?: boolean;\n indexifembedded?: boolean;\n nositelinkssearchbox?: boolean;\n unavailable_after?: string;\n \"max-video-preview\"?: number | string;\n \"max-image-preview\"?: \"none\" | \"standard\" | \"large\";\n \"max-snippet\"?: number;\n googleBot?: string | Omit<Robots, \"googleBot\">;\n}\n\nexport interface Metadata {\n /** A string on a page; a template on a layout, applied to the pages below it. */\n title?: string | TitleTemplate;\n description?: string;\n keywords?: string | string[];\n author?: string;\n /**\n * A string, or the object Next takes: `{ index: false, follow: false }`\n * becomes `<meta name=\"robots\" content=\"noindex, nofollow\">`, the flags\n * by name, the limits as `name:value`; `googleBot` is the same shape for\n * `<meta name=\"googlebot\">`.\n */\n robots?: string | Robots;\n /**\n * Where the site lives, so a relative image or url can be made absolute.\n *\n * metadataBase: new URL('https://example.com')\n *\n * On the root layout, once. A share-card scraper needs an absolute url and\n * several refuse a relative one; without this, `og:image` for an image in\n * `app/` is emitted relative and works in some places and not others. The\n * same name as Next, so a port carries it across unchanged.\n */\n metadataBase?: string | URL;\n icons?: IconURL | (IconURL | IconDescriptor)[] | Icons | null;\n openGraph?: OpenGraph;\n twitter?: Twitter;\n /** @deprecated Use `openGraph.title`. Still rendered, correctly, as `property=`. */\n \"og:title\"?: string;\n /** @deprecated Use `openGraph.description`. */\n \"og:description\"?: string;\n /** @deprecated Use `openGraph.images`. */\n \"og:image\"?: string;\n /** @deprecated Use `openGraph.url`. */\n \"og:url\"?: string;\n /** @deprecated Use `openGraph.type`. */\n \"og:type\"?: string;\n /** @deprecated Use `openGraph.siteName`. */\n \"og:site_name\"?: string;\n /** @deprecated Use `twitter.card`. */\n \"twitter:card\"?: string;\n /** @deprecated Use `twitter.title`. */\n \"twitter:title\"?: string;\n /** @deprecated Use `twitter.description`. */\n \"twitter:description\"?: string;\n /** @deprecated Use `twitter.images`. */\n \"twitter:image\"?: string;\n /** @deprecated Use `twitter.site`. */\n \"twitter:site\"?: string;\n\n /**\n * Any other meta tag, by name.\n *\n * other: { 'fb:app_id': '123', 'theme-color': '#000' }\n *\n * Here rather than alongside the named keys, and that is what makes the rest\n * of this interface worth annotating. An index signature on the interface\n * itself made every key legal — so `titel` was accepted in silence, and an\n * editor offered no completions at all, because with any identifier valid\n * TypeScript reads an unfinished key as a shorthand property and goes looking\n * for a variable by that name.\n */\n other?: Record<string, string | string[] | null | undefined>;\n}\n\n/**\n * Metadata that depends on the request.\n *\n * Receives the same awaitables a page does, so one shape is learned rather\n * than two:\n *\n * export const generateMetadata: GenerateMetadata<{ slug: string }> =\n * async ({ params }) => ({ title: (await params).slug })\n */\nexport type GenerateMetadata<P = Record<string, string>> = (args: {\n params: Promise<P>;\n searchParams: Promise<URLSearchParams>;\n}) => Metadata | Promise<Metadata>;\n\n/** One rule block of a robots.txt: which agents, what they may and may not fetch. */\nexport interface RobotsRule {\n userAgent?: string | string[];\n allow?: string | string[];\n disallow?: string | string[];\n crawlDelay?: number;\n}\n\n/** One url of a sitemap. `url` may be relative when the root layout has a metadataBase. */\nexport interface SitemapEntry {\n url: string;\n lastModified?: string | Date;\n changeFrequency?:\n \"always\" | \"hourly\" | \"daily\" | \"weekly\" | \"monthly\" | \"yearly\" | \"never\";\n priority?: number;\n /** Image urls on this page, for image search. */\n images?: string[];\n /** Translations of this page: language tag to url. */\n alternates?: { languages?: Record<string, string> };\n}\n\n/** A link in an llms.txt section. */\nexport interface LlmsLink {\n title: string;\n url: string;\n description?: string;\n}\n\nexport interface LlmsSection {\n title: string;\n links: LlmsLink[];\n}\n\n/**\n * The files a site describes itself with, each from a file beside the root\n * layout and written the way Next writes them:\n *\n * app/robots.ts -> /robots.txt default export returns MetadataRoute.Robots\n * app/sitemap.ts -> /sitemap.xml default export returns MetadataRoute.Sitemap\n * app/llms.ts -> /llms.txt default export returns MetadataRoute.Llms\n *\n * Each may return a string instead, served as written. A relative url in any\n * of them is made absolute with the root layout's metadataBase.\n */\nexport namespace MetadataRoute {\n export type Robots = {\n rules: RobotsRule | RobotsRule[];\n sitemap?: string | string[];\n host?: string;\n };\n export type Sitemap = SitemapEntry[];\n /** The llms.txt shape at llmstxt.org: a title, a summary, then sections of links. */\n export type Llms = {\n title: string;\n summary?: string;\n /** Paragraphs after the summary, before the sections. */\n details?: string | string[];\n sections?: LlmsSection[];\n };\n}\n\n/**\n * `export const viewport`, in Next's shape, on a layout or a page: layouts\n * outer to inner, then the page, merged per key. What is not set is\n * `width=device-width, initial-scale=1`, written into every document the way\n * Next wrote it — a layout ported from Next never wrote the tag, and a page\n * without one is the desktop layout on a phone. A layout that renders\n * `<meta name=\"viewport\">` itself is left alone.\n */\nexport interface Viewport {\n width?: string | number;\n height?: string | number;\n initialScale?: number;\n minimumScale?: number;\n maximumScale?: number;\n userScalable?: boolean;\n viewportFit?: \"auto\" | \"cover\" | \"contain\";\n interactiveWidget?: \"resizes-visual\" | \"resizes-content\" | \"overlays-content\";\n /** One colour, or one per media query. Wins over the web manifest's. */\n themeColor?: string | { media?: string; color: string }[];\n colorScheme?: \"normal\" | \"light\" | \"dark\" | \"light dark\" | \"dark light\" | \"only light\";\n}\n"]}
|
|
1
|
+
{"version":3,"file":"metadata.js","sourceRoot":"","sources":["../src/metadata.ts"],"names":[],"mappings":"AAAA,sDAAsD;AACtD,EAAE;AACF,6DAA6D;AAC7D,EAAE;AACF,4DAA4D;AAC5D,EAAE;AACF,4EAA4E;AAC5E,+EAA+E;AAC/E,+EAA+E;AAC/E,4EAA4E;AAC5E,2EAA2E;AAC3E,EAAE;AACF,4EAA4E;AAC5E,+EAA+E;AAC/E,MAAM","sourcesContent":["// What a page says about itself, as importable types.\n//\n// import type { Metadata } from '@rsc-kit/core/metadata'\n//\n// export const metadata: Metadata = { title: 'Orders' }\n//\n// Imported rather than ambient, and that is the whole point of the move. An\n// ambient declaration has to be COPIED into the project, which means it is not\n// there until the build has run once — so a freshly cloned app reports \"Cannot\n// find name 'Metadata'\" on every page until someone runs the dev server. An\n// import resolves from node_modules the moment dependencies are installed.\n//\n// The ambient names still work. `.rsc-kit/rsc-types.d.ts` now aliases these\n// rather than restating them, so there is one definition and two ways to reach\n// it.\n\nexport interface IconDescriptor {\n url: string | URL;\n type?: string;\n sizes?: string;\n color?: string;\n rel?: string;\n media?: string;\n fetchPriority?: \"high\" | \"low\" | \"auto\";\n}\n\nexport type IconURL = string | URL;\n\nexport interface Icons {\n icon?: IconURL | IconDescriptor | (IconURL | IconDescriptor)[];\n apple?: IconURL | IconDescriptor | (IconURL | IconDescriptor)[];\n shortcut?: IconURL | IconDescriptor | (IconURL | IconDescriptor)[];\n other?: IconDescriptor | IconDescriptor[];\n}\n\n/** A layout's title, wrapping the titles of the pages beneath it. */\nexport interface TitleTemplate {\n /** `%s` stands in for the page's own title. */\n template?: string;\n /** Used by a page that exports no title of its own. */\n default?: string;\n}\n\n/** One image a share card may show. A string is its url. */\nexport interface OpenGraphImage {\n url: string | URL;\n width?: number;\n height?: number;\n alt?: string;\n type?: string;\n}\n\n/**\n * The card a link to this page unfurls into on Facebook, Slack, LinkedIn and\n * most of the rest. Rendered with `property=`, which is what those scrapers\n * read — a `name=` attribute is ignored by every one of them.\n */\nexport interface OpenGraph {\n title?: string;\n description?: string;\n /** Absolute, or relative to `metadataBase`. */\n url?: string | URL;\n siteName?: string;\n type?: \"website\" | \"article\" | \"profile\" | \"book\" | (string & {});\n locale?: string;\n images?: string | URL | OpenGraphImage | (string | URL | OpenGraphImage)[];\n}\n\n/** The same card for X, which reads `name=` rather than `property=`. */\nexport interface Twitter {\n card?: \"summary\" | \"summary_large_image\" | \"app\" | \"player\";\n title?: string;\n description?: string;\n /** The site's account, `@handle`. */\n site?: string;\n /** The author's account, `@handle`. */\n creator?: string;\n images?: string | URL | OpenGraphImage | (string | URL | OpenGraphImage)[];\n}\n\nexport interface Robots {\n index?: boolean;\n follow?: boolean;\n noarchive?: boolean;\n nosnippet?: boolean;\n noimageindex?: boolean;\n nocache?: boolean;\n notranslate?: boolean;\n indexifembedded?: boolean;\n nositelinkssearchbox?: boolean;\n unavailable_after?: string;\n \"max-video-preview\"?: number | string;\n \"max-image-preview\"?: \"none\" | \"standard\" | \"large\";\n \"max-snippet\"?: number;\n googleBot?: string | Omit<Robots, \"googleBot\">;\n}\n\n/** An iOS launch screen: an image, and the media query naming the device it is for. */\nexport interface AppleStartupImage {\n url: string | URL;\n media?: string;\n}\n\nexport interface AppleWebApp {\n /** Opens as an app from the home screen - no browser chrome. */\n capable?: boolean;\n /** The name under the home screen icon, when it should differ from the page title. */\n title?: string;\n /** The status bar over the app: `default`, `black`, or `black-translucent` to draw under it. */\n statusBarStyle?: \"default\" | \"black\" | \"black-translucent\";\n /** Launch screens. A string or url is one image for every device. */\n startupImage?: string | URL | (string | URL | AppleStartupImage)[];\n}\n\nexport interface Metadata {\n /** A string on a page; a template on a layout, applied to the pages below it. */\n title?: string | TitleTemplate;\n description?: string;\n keywords?: string | string[];\n author?: string;\n /**\n * A string, or the object Next takes: `{ index: false, follow: false }`\n * becomes `<meta name=\"robots\" content=\"noindex, nofollow\">`, the flags\n * by name, the limits as `name:value`; `googleBot` is the same shape for\n * `<meta name=\"googlebot\">`.\n */\n robots?: string | Robots;\n /**\n * Where the site lives, so a relative image or url can be made absolute.\n *\n * metadataBase: new URL('https://example.com')\n *\n * On the root layout, once. A share-card scraper needs an absolute url and\n * several refuse a relative one; without this, `og:image` for an image in\n * `app/` is emitted relative and works in some places and not others. The\n * same name as Next, so a port carries it across unchanged.\n */\n metadataBase?: string | URL;\n icons?: IconURL | (IconURL | IconDescriptor)[] | Icons | null;\n openGraph?: OpenGraph;\n twitter?: Twitter;\n /**\n * How the app behaves added to an iPhone's home screen. Next's shape, so a\n * port carries it across unchanged.\n *\n * appleWebApp: {\n * capable: true,\n * title: 'Orders',\n * statusBarStyle: 'black-translucent',\n * startupImage: [{ url: '/splash-1179x2556.png', media: '...' }],\n * }\n *\n * `true` is `{ capable: true }`. Launch screens are simpler as files: an\n * `apple-splash-1179x2556.png` in `app/` is linked with the right media\n * query for the device its size names - see the PWA guide.\n */\n appleWebApp?: boolean | AppleWebApp;\n /** @deprecated Use `openGraph.title`. Still rendered, correctly, as `property=`. */\n \"og:title\"?: string;\n /** @deprecated Use `openGraph.description`. */\n \"og:description\"?: string;\n /** @deprecated Use `openGraph.images`. */\n \"og:image\"?: string;\n /** @deprecated Use `openGraph.url`. */\n \"og:url\"?: string;\n /** @deprecated Use `openGraph.type`. */\n \"og:type\"?: string;\n /** @deprecated Use `openGraph.siteName`. */\n \"og:site_name\"?: string;\n /** @deprecated Use `twitter.card`. */\n \"twitter:card\"?: string;\n /** @deprecated Use `twitter.title`. */\n \"twitter:title\"?: string;\n /** @deprecated Use `twitter.description`. */\n \"twitter:description\"?: string;\n /** @deprecated Use `twitter.images`. */\n \"twitter:image\"?: string;\n /** @deprecated Use `twitter.site`. */\n \"twitter:site\"?: string;\n\n /**\n * Any other meta tag, by name.\n *\n * other: { 'fb:app_id': '123', 'theme-color': '#000' }\n *\n * Here rather than alongside the named keys, and that is what makes the rest\n * of this interface worth annotating. An index signature on the interface\n * itself made every key legal — so `titel` was accepted in silence, and an\n * editor offered no completions at all, because with any identifier valid\n * TypeScript reads an unfinished key as a shorthand property and goes looking\n * for a variable by that name.\n */\n other?: Record<string, string | string[] | null | undefined>;\n}\n\n/**\n * Metadata that depends on the request.\n *\n * Receives the same awaitables a page does, so one shape is learned rather\n * than two:\n *\n * export const generateMetadata: GenerateMetadata<{ slug: string }> =\n * async ({ params }) => ({ title: (await params).slug })\n */\nexport type GenerateMetadata<P = Record<string, string>> = (args: {\n params: Promise<P>;\n searchParams: Promise<URLSearchParams>;\n}) => Metadata | Promise<Metadata>;\n\n/** One rule block of a robots.txt: which agents, what they may and may not fetch. */\nexport interface RobotsRule {\n userAgent?: string | string[];\n allow?: string | string[];\n disallow?: string | string[];\n crawlDelay?: number;\n}\n\n/** One url of a sitemap. `url` may be relative when the root layout has a metadataBase. */\nexport interface SitemapEntry {\n url: string;\n lastModified?: string | Date;\n changeFrequency?:\n \"always\" | \"hourly\" | \"daily\" | \"weekly\" | \"monthly\" | \"yearly\" | \"never\";\n priority?: number;\n /** Image urls on this page, for image search. */\n images?: string[];\n /** Translations of this page: language tag to url. */\n alternates?: { languages?: Record<string, string> };\n}\n\n/** A link in an llms.txt section. */\nexport interface LlmsLink {\n title: string;\n url: string;\n description?: string;\n}\n\nexport interface LlmsSection {\n title: string;\n links: LlmsLink[];\n}\n\n/**\n * The files a site describes itself with, each from a file beside the root\n * layout and written the way Next writes them:\n *\n * app/robots.ts -> /robots.txt default export returns MetadataRoute.Robots\n * app/sitemap.ts -> /sitemap.xml default export returns MetadataRoute.Sitemap\n * app/llms.ts -> /llms.txt default export returns MetadataRoute.Llms\n *\n * Each may return a string instead, served as written. A relative url in any\n * of them is made absolute with the root layout's metadataBase.\n */\nexport namespace MetadataRoute {\n export type Robots = {\n rules: RobotsRule | RobotsRule[];\n sitemap?: string | string[];\n host?: string;\n };\n export type Sitemap = SitemapEntry[];\n /** The llms.txt shape at llmstxt.org: a title, a summary, then sections of links. */\n export type Llms = {\n title: string;\n summary?: string;\n /** Paragraphs after the summary, before the sections. */\n details?: string | string[];\n sections?: LlmsSection[];\n };\n}\n\n/**\n * `export const viewport`, in Next's shape, on a layout or a page: layouts\n * outer to inner, then the page, merged per key. What is not set is\n * `width=device-width, initial-scale=1`, written into every document the way\n * Next wrote it — a layout ported from Next never wrote the tag, and a page\n * without one is the desktop layout on a phone. A layout that renders\n * `<meta name=\"viewport\">` itself is left alone.\n */\nexport interface Viewport {\n width?: string | number;\n height?: string | number;\n initialScale?: number;\n minimumScale?: number;\n maximumScale?: number;\n userScalable?: boolean;\n viewportFit?: \"auto\" | \"cover\" | \"contain\";\n interactiveWidget?: \"resizes-visual\" | \"resizes-content\" | \"overlays-content\";\n /** One colour, or one per media query. Wins over the web manifest's. */\n themeColor?: string | { media?: string; color: string }[];\n colorScheme?: \"normal\" | \"light\" | \"dark\" | \"light dark\" | \"dark light\" | \"only light\";\n}\n"]}
|
package/dist/prerender.d.ts
CHANGED
|
@@ -3,8 +3,16 @@ import type { ManifestRoute, RouteManifest } from "./manifest.js";
|
|
|
3
3
|
* Every `<link rel="stylesheet">` whose source the reader answers for,
|
|
4
4
|
* replaced by a `<style>` holding it. A link the reader declines - too big,
|
|
5
5
|
* or not the build's - is left as it was.
|
|
6
|
+
*
|
|
7
|
+
* A page that hydrates keeps the link as well, turned to `media="print"`.
|
|
8
|
+
* React looks its stylesheets up by `link[rel="stylesheet"][href]` and, not
|
|
9
|
+
* finding one, inserts it - fetching the file it was just spared, and
|
|
10
|
+
* holding a navigation that needs it until it lands. Kept for print, the
|
|
11
|
+
* link is what React finds; the browser fetches it at the lowest priority
|
|
12
|
+
* without blocking a paint, and never applies it to the screen, so the
|
|
13
|
+
* cascade is the `<style>`'s alone.
|
|
6
14
|
*/
|
|
7
|
-
export declare function withInlineStylesheets(html: string, read: (href: string) => string | null): string;
|
|
15
|
+
export declare function withInlineStylesheets(html: string, read: (href: string) => string | null, hydrates?: boolean): string;
|
|
8
16
|
export declare function withWorkerRegistration(html: string): string;
|
|
9
17
|
export interface PrerenderEngine {
|
|
10
18
|
manifest?(): RouteManifest;
|
|
@@ -19,7 +27,15 @@ export interface PrerenderEngine {
|
|
|
19
27
|
props: Record<string, unknown>;
|
|
20
28
|
}[], loadings?: string[], parallelSlots?: Record<string, string>, pageKey?: string,
|
|
21
29
|
/** How long to render before taking what has flushed. Defaults to the full budget. */
|
|
22
|
-
budgetMs?: number
|
|
30
|
+
budgetMs?: number,
|
|
31
|
+
/** Whether a host is installed to answer rpc(). False at build. */
|
|
32
|
+
canReachHost?: boolean,
|
|
33
|
+
/**
|
|
34
|
+
* Called once, when the render has stopped producing anything and is only
|
|
35
|
+
* waiting. The budget still runs; this is so the caller can start other
|
|
36
|
+
* work meanwhile. An engine built before this existed never calls it.
|
|
37
|
+
*/
|
|
38
|
+
onQuiet?: () => void): Promise<{
|
|
23
39
|
shellHtml: string;
|
|
24
40
|
timedOut: boolean;
|
|
25
41
|
usedDynamicApis: boolean;
|
|
@@ -117,10 +133,13 @@ export interface PrerenderOptions {
|
|
|
117
133
|
*/
|
|
118
134
|
props?: (route: ManifestRoute, params: Record<string, string>) => Record<string, unknown>;
|
|
119
135
|
/**
|
|
120
|
-
* How many routes to render at once. Defaults to
|
|
136
|
+
* How many routes to render at once. Defaults to 4; 1 renders sequentially.
|
|
121
137
|
*
|
|
122
|
-
*
|
|
123
|
-
*
|
|
138
|
+
* Counts renders doing work. One that has gone quiet - waiting out the
|
|
139
|
+
* budget on something that may never answer - steps aside for the next, so
|
|
140
|
+
* a slow query already in flight can overlap with new ones. Worth lowering
|
|
141
|
+
* when the pages talk to something that will not enjoy many concurrent
|
|
142
|
+
* callers — a local database, a rate-limited API.
|
|
124
143
|
*/
|
|
125
144
|
concurrency?: number;
|
|
126
145
|
/** Identifies the build in what gets written, so a stale page can be spotted. */
|
package/dist/prerender.js
CHANGED
|
@@ -41,6 +41,15 @@ const ROOT_FALLBACK_BUDGET_MS = 200;
|
|
|
41
41
|
* fixtures, where six was enough to turn a two-second budget into a timeout.
|
|
42
42
|
*/
|
|
43
43
|
const DEFAULT_PRERENDER_CONCURRENCY = 4;
|
|
44
|
+
/**
|
|
45
|
+
* How many routes may be in flight at all, working or waiting.
|
|
46
|
+
*
|
|
47
|
+
* A route whose render has gone quiet gives its slot back and waits out the
|
|
48
|
+
* rest of its budget beside the others (see the loop). Waiting costs a timer,
|
|
49
|
+
* not a core, but each is still a render held in memory, so there is a
|
|
50
|
+
* ceiling - high enough that a build is never queued behind its own waits.
|
|
51
|
+
*/
|
|
52
|
+
const PRERENDER_IN_FLIGHT_LIMIT = 256;
|
|
44
53
|
/**
|
|
45
54
|
* The client components the engine itself puts around every page when the
|
|
46
55
|
* bootstrap is on - the segment and slot boundaries, the title, the pathname
|
|
@@ -68,12 +77,26 @@ const WORKER_REGISTRATION = "<script>'serviceWorker'in navigator&&addEventListen
|
|
|
68
77
|
* Every `<link rel="stylesheet">` whose source the reader answers for,
|
|
69
78
|
* replaced by a `<style>` holding it. A link the reader declines - too big,
|
|
70
79
|
* or not the build's - is left as it was.
|
|
80
|
+
*
|
|
81
|
+
* A page that hydrates keeps the link as well, turned to `media="print"`.
|
|
82
|
+
* React looks its stylesheets up by `link[rel="stylesheet"][href]` and, not
|
|
83
|
+
* finding one, inserts it - fetching the file it was just spared, and
|
|
84
|
+
* holding a navigation that needs it until it lands. Kept for print, the
|
|
85
|
+
* link is what React finds; the browser fetches it at the lowest priority
|
|
86
|
+
* without blocking a paint, and never applies it to the screen, so the
|
|
87
|
+
* cascade is the `<style>`'s alone.
|
|
71
88
|
*/
|
|
72
|
-
export function withInlineStylesheets(html, read) {
|
|
89
|
+
export function withInlineStylesheets(html, read, hydrates = false) {
|
|
73
90
|
return html.replace(/<link\b[^>]*\brel="stylesheet"[^>]*>/g, (tag) => {
|
|
74
91
|
const href = /\bhref="([^"]+)"/.exec(tag)?.[1];
|
|
75
|
-
|
|
76
|
-
|
|
92
|
+
// A link with a media query of its own applies only sometimes; inlined,
|
|
93
|
+
// it would apply always.
|
|
94
|
+
const css = href && !/\bmedia=/.test(tag) ? read(href) : null;
|
|
95
|
+
if (css === null)
|
|
96
|
+
return tag;
|
|
97
|
+
if (!hydrates)
|
|
98
|
+
return `<style>${css}</style>`;
|
|
99
|
+
return `<style>${css}</style>` + tag.replace(/\s*\/?>$/, ' media="print">');
|
|
77
100
|
});
|
|
78
101
|
}
|
|
79
102
|
export function withWorkerRegistration(html) {
|
|
@@ -268,7 +291,15 @@ export function summary(results) {
|
|
|
268
291
|
return parts.join(", ") || "nothing to store";
|
|
269
292
|
}
|
|
270
293
|
export function pathKey(url) {
|
|
271
|
-
|
|
294
|
+
// Trimmed by hand: the regex this was, /^\/+|\/+$/, is quadratic on a run
|
|
295
|
+
// of slashes, and it runs on every request's path.
|
|
296
|
+
let start = 0;
|
|
297
|
+
let end = url.length;
|
|
298
|
+
while (start < end && url.charCodeAt(start) === 47)
|
|
299
|
+
start++;
|
|
300
|
+
while (end > start && url.charCodeAt(end - 1) === 47)
|
|
301
|
+
end--;
|
|
302
|
+
const key = url.slice(start, end) || "index";
|
|
272
303
|
if (key.split("/").some((segment) => segment === ".." || segment === ".")) {
|
|
273
304
|
throw new Error("Refusing to store " +
|
|
274
305
|
JSON.stringify(url) +
|
|
@@ -369,31 +400,61 @@ export async function prerender(options) {
|
|
|
369
400
|
//
|
|
370
401
|
// Results are placed by index, so what a build reports does not depend on
|
|
371
402
|
// which page happened to finish first.
|
|
403
|
+
//
|
|
404
|
+
// The bound is on renders doing work, not on renders waiting. A page with a
|
|
405
|
+
// hole the build can never fill - a cookie, a pattern's params, a host call -
|
|
406
|
+
// sits out the whole budget with nothing left to do, and holding a slot for
|
|
407
|
+
// that queued every such page behind four others doing the same: 574 of
|
|
408
|
+
// them took 294 s, of which the rendering was about four. So a render that
|
|
409
|
+
// goes quiet hands its slot back and keeps its budget, and the waits
|
|
410
|
+
// overlap. What a page is classified as does not change - it is given
|
|
411
|
+
// exactly as long as before - only what else runs while it waits.
|
|
372
412
|
const concurrency = Math.max(1, options.concurrency ?? DEFAULT_PRERENDER_CONCURRENCY);
|
|
373
|
-
|
|
374
|
-
const
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
if (index >= entries.length)
|
|
378
|
-
return;
|
|
379
|
-
const { route, params, url } = entries[index];
|
|
380
|
-
results[index] = await prerenderOne(route, params, url);
|
|
381
|
-
options.onResult?.(results[index]);
|
|
382
|
-
}
|
|
383
|
-
};
|
|
413
|
+
const working = gate(concurrency);
|
|
414
|
+
const inFlight = gate(Math.max(concurrency, PRERENDER_IN_FLIGHT_LIMIT));
|
|
415
|
+
const runs = [];
|
|
416
|
+
let failure = null;
|
|
384
417
|
const unwatch = watchNondeterminism();
|
|
385
418
|
try {
|
|
386
|
-
|
|
419
|
+
for (let index = 0; index < entries.length && !failure; index++) {
|
|
420
|
+
await inFlight.take();
|
|
421
|
+
await working.take();
|
|
422
|
+
const { route, params, url } = entries[index];
|
|
423
|
+
let holding = true;
|
|
424
|
+
const release = () => {
|
|
425
|
+
if (holding) {
|
|
426
|
+
holding = false;
|
|
427
|
+
working.give();
|
|
428
|
+
}
|
|
429
|
+
};
|
|
430
|
+
runs.push(prerenderOne(route, params, url, release)
|
|
431
|
+
.then((result) => {
|
|
432
|
+
results[index] = result;
|
|
433
|
+
options.onResult?.(result);
|
|
434
|
+
})
|
|
435
|
+
.catch((error) => {
|
|
436
|
+
failure ??= { error };
|
|
437
|
+
})
|
|
438
|
+
.finally(() => {
|
|
439
|
+
release();
|
|
440
|
+
inFlight.give();
|
|
441
|
+
}));
|
|
442
|
+
}
|
|
443
|
+
await Promise.all(runs);
|
|
387
444
|
}
|
|
388
445
|
finally {
|
|
389
446
|
unwatch();
|
|
390
447
|
}
|
|
448
|
+
if (failure)
|
|
449
|
+
throw failure.error;
|
|
391
450
|
const refused = results.filter((r) => r.type === "blocked");
|
|
392
451
|
if (refused.length > 0) {
|
|
393
452
|
throw new NotPrerenderable(refused);
|
|
394
453
|
}
|
|
395
454
|
return results;
|
|
396
|
-
async function prerenderOne(route, params, url
|
|
455
|
+
async function prerenderOne(route, params, url,
|
|
456
|
+
// Hands this route's slot back once its render is only waiting.
|
|
457
|
+
quiet = () => { }) {
|
|
397
458
|
const props = options.props ? options.props(route, params) : params;
|
|
398
459
|
const layouts = route.layouts.map((component) => ({
|
|
399
460
|
component,
|
|
@@ -455,7 +516,7 @@ export async function prerender(options) {
|
|
|
455
516
|
// Only when this shell serves one url. A parameterised route's
|
|
456
517
|
// shell is shared, so baking a url into it would put the wrong one
|
|
457
518
|
// on every page but the one that happened to be built.
|
|
458
|
-
unlistedNow ? "" : url),
|
|
519
|
+
unlistedNow ? "" : url, undefined, undefined, quiet),
|
|
459
520
|
redirected: taken(),
|
|
460
521
|
readRequest: requestWasRead(),
|
|
461
522
|
// What reached for it, so the build can name the call rather than
|
|
@@ -710,6 +771,9 @@ export async function prerender(options) {
|
|
|
710
771
|
"no client components, so ships no javascript" +
|
|
711
772
|
(inlined ? "; stylesheet inlined" : "");
|
|
712
773
|
}
|
|
774
|
+
else if (options.stylesheet) {
|
|
775
|
+
body = withInlineStylesheets(body, options.stylesheet, true);
|
|
776
|
+
}
|
|
713
777
|
const key = pathKey(url);
|
|
714
778
|
await write(`${key}.html`, body);
|
|
715
779
|
await write(`${key}.flight`, rendered.rscPayload);
|
|
@@ -742,7 +806,10 @@ export async function prerender(options) {
|
|
|
742
806
|
async function writeShell(route, url, body, postponed) {
|
|
743
807
|
const parameterised = route.segments.some((s) => s.type !== "static");
|
|
744
808
|
const key = parameterised && !route.staticParams ? patternKey(route) : pathKey(url);
|
|
745
|
-
|
|
809
|
+
// A shell hydrates, so its link stays for React - see withInlineStylesheets.
|
|
810
|
+
await write(`${key}.ppr.html`, options.stylesheet
|
|
811
|
+
? withInlineStylesheets(body, options.stylesheet, true)
|
|
812
|
+
: body);
|
|
746
813
|
// Written only when there is something to resume from. Its absence is
|
|
747
814
|
// meaningful rather than incidental: a host that finds no postponed state
|
|
748
815
|
// serves the shell and lets the client fill it, which is what every build
|
|
@@ -758,4 +825,25 @@ export async function prerender(options) {
|
|
|
758
825
|
}, null, 2));
|
|
759
826
|
}
|
|
760
827
|
}
|
|
828
|
+
/** A counting semaphore: take() waits for a unit, give() returns one. */
|
|
829
|
+
function gate(size) {
|
|
830
|
+
let free = size;
|
|
831
|
+
const waiting = [];
|
|
832
|
+
return {
|
|
833
|
+
take() {
|
|
834
|
+
if (free > 0) {
|
|
835
|
+
free--;
|
|
836
|
+
return Promise.resolve();
|
|
837
|
+
}
|
|
838
|
+
return new Promise((resolve) => waiting.push(resolve));
|
|
839
|
+
},
|
|
840
|
+
give() {
|
|
841
|
+
const next = waiting.shift();
|
|
842
|
+
if (next)
|
|
843
|
+
next();
|
|
844
|
+
else
|
|
845
|
+
free++;
|
|
846
|
+
},
|
|
847
|
+
};
|
|
848
|
+
}
|
|
761
849
|
//# sourceMappingURL=prerender.js.map
|