@rsc-kit/core 0.16.3 → 0.18.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/barrelImports.d.ts +6 -0
- package/dist/barrelImports.js +93 -0
- package/dist/barrelImports.js.map +1 -0
- package/dist/buildReport.d.ts +6 -1
- package/dist/buildReport.js +16 -18
- package/dist/buildReport.js.map +1 -1
- package/dist/cache.js +8 -4
- package/dist/cache.js.map +1 -1
- package/dist/clientEntries.d.ts +32 -0
- package/dist/clientEntries.js +107 -0
- package/dist/clientEntries.js.map +1 -0
- package/dist/clientImports.d.ts +36 -0
- package/dist/clientImports.js +36 -0
- package/dist/clientImports.js.map +1 -0
- package/dist/devReload.d.ts +19 -0
- package/dist/devReload.js +32 -0
- package/dist/devReload.js.map +1 -0
- package/dist/events.d.ts +57 -0
- package/dist/events.js +113 -0
- package/dist/events.js.map +1 -0
- package/dist/host.d.ts +16 -6
- package/dist/host.js +219 -124
- package/dist/host.js.map +1 -1
- package/dist/hostCalls.d.ts +27 -0
- package/dist/hostCalls.js +127 -12
- package/dist/hostCalls.js.map +1 -1
- package/dist/hostRouting.d.ts +53 -0
- package/dist/hostRouting.js +100 -0
- package/dist/hostRouting.js.map +1 -0
- package/dist/js/DefaultRouteError.d.ts +3 -0
- package/dist/js/DefaultRouteError.js +61 -0
- package/dist/js/DefaultRouteError.js.map +1 -0
- package/dist/js/Form.js +54 -6
- package/dist/js/Form.js.map +1 -1
- package/dist/js/Link.d.ts +0 -3
- package/dist/js/Link.js +3 -6
- package/dist/js/Link.js.map +1 -1
- package/dist/js/SegmentBoundary.d.ts +38 -2
- package/dist/js/SegmentBoundary.js +51 -13
- package/dist/js/SegmentBoundary.js.map +1 -1
- package/dist/js/createViteRscApp.js +32 -17
- package/dist/js/createViteRscApp.js.map +1 -1
- package/dist/js/devNotice.d.ts +2 -2
- package/dist/js/devNotice.js +17 -7
- package/dist/js/devNotice.js.map +1 -1
- package/dist/js/fallbackReport.d.ts +2 -0
- package/dist/js/fallbackReport.js +51 -0
- package/dist/js/fallbackReport.js.map +1 -0
- package/dist/js/navigate.d.ts +1 -1
- package/dist/js/navigate.js +55 -23
- package/dist/js/navigate.js.map +1 -1
- package/dist/js/queryClient.d.ts +2 -1
- package/dist/js/queryClient.js +0 -12
- package/dist/js/queryClient.js.map +1 -1
- package/dist/js/router.d.ts +2 -2
- package/dist/js/router.js.map +1 -1
- package/dist/js/segmentStore.d.ts +2 -0
- package/dist/js/segmentStore.js +21 -1
- package/dist/js/segmentStore.js.map +1 -1
- package/dist/js/staleAssets.js +10 -1
- package/dist/js/staleAssets.js.map +1 -1
- package/dist/js/standardSchema.d.ts +6 -0
- package/dist/js/standardSchema.js +19 -1
- package/dist/js/standardSchema.js.map +1 -1
- package/dist/js/useEvents.d.ts +25 -0
- package/dist/js/useEvents.js +78 -0
- package/dist/js/useEvents.js.map +1 -0
- package/dist/js/useLinkStatus.d.ts +14 -1
- package/dist/js/useLinkStatus.js +15 -1
- package/dist/js/useLinkStatus.js.map +1 -1
- package/dist/js/usePolling.d.ts +29 -0
- package/dist/js/usePolling.js +141 -0
- package/dist/js/usePolling.js.map +1 -0
- package/dist/manifest.d.ts +18 -1
- package/dist/manifest.js.map +1 -1
- package/dist/metadata.d.ts +94 -15
- package/dist/metadata.js.map +1 -1
- package/dist/metadataRoutes.d.ts +61 -0
- package/dist/metadataRoutes.js +173 -0
- package/dist/metadataRoutes.js.map +1 -0
- package/dist/prerender.js +44 -10
- package/dist/prerender.js.map +1 -1
- package/dist/reactCache.d.ts +1 -0
- package/dist/reactCache.js +52 -0
- package/dist/reactCache.js.map +1 -0
- package/dist/redirect.d.ts +16 -2
- package/dist/redirect.js +10 -19
- package/dist/redirect.js.map +1 -1
- package/dist/request.d.ts +18 -1
- package/dist/request.js +91 -5
- package/dist/request.js.map +1 -1
- package/dist/revalidate.d.ts +6 -4
- package/dist/revalidate.js +6 -5
- package/dist/revalidate.js.map +1 -1
- package/dist/routes.d.ts +16 -3
- package/dist/routes.js +5 -5
- package/dist/routes.js.map +1 -1
- package/dist/routing.d.ts +2 -2
- package/dist/routing.js +47 -25
- package/dist/routing.js.map +1 -1
- package/dist/useSsr.d.ts +19 -0
- package/dist/useSsr.js +104 -0
- package/dist/useSsr.js.map +1 -0
- package/dist/vite.d.ts +86 -9
- package/dist/vite.js +827 -24
- package/dist/vite.js.map +1 -1
- package/package.json +13 -1
|
@@ -1,4 +1,22 @@
|
|
|
1
|
-
|
|
1
|
+
/**
|
|
2
|
+
* Validating a form before it is submitted, with whatever library you use.
|
|
3
|
+
*
|
|
4
|
+
* Not a client module, though a client component is its first caller: the
|
|
5
|
+
* action pipeline validates with it on the server too, and a "use client"
|
|
6
|
+
* directive here turned that call into a client-reference stub - "client
|
|
7
|
+
* reference export 'validateWith' is called on server", on every action
|
|
8
|
+
* with a schema. Pure functions, no React; importable from either side.
|
|
9
|
+
*
|
|
10
|
+
* Not with an adapter per library: Standard Schema is the contract Zod,
|
|
11
|
+
* Valibot, ArkType and others already implement, so this speaks to the schema
|
|
12
|
+
* itself and never imports one. The types below are the spec, restated here
|
|
13
|
+
* rather than depended on — it is an interface, and a package for it would be
|
|
14
|
+
* a dependency that ships nothing.
|
|
15
|
+
*
|
|
16
|
+
* Validation on the client is a courtesy, never a control. The server still
|
|
17
|
+
* has to check: a form is one way to reach an action, and the action is a
|
|
18
|
+
* public endpoint reachable without it.
|
|
19
|
+
*/
|
|
2
20
|
/**
|
|
3
21
|
* The field an issue belongs to.
|
|
4
22
|
*
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"standardSchema.js","sourceRoot":"","sources":["../../src/js/standardSchema.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"standardSchema.js","sourceRoot":"","sources":["../../src/js/standardSchema.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAqBH;;;;;;;GAOG;AACH,SAAS,OAAO,CAAC,IAA8I;IAC7J,IAAI,CAAC,IAAI,IAAI,IAAI,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,EAAE,CAAA;IAEzC,OAAO,IAAI;SACR,GAAG,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC,CAAC,OAAO,OAAO,KAAK,QAAQ,IAAI,OAAO,KAAK,IAAI,CAAC,CAAC,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC;SAC3F,IAAI,CAAC,GAAG,CAAC,CAAA;AACd,CAAC;AAED,sEAAsE;AACtE,MAAM,UAAU,cAAc,CAC5B,MAA4D;IAE5D,MAAM,MAAM,GAA6B,EAAE,CAAA;IAE3C,KAAK,MAAM,KAAK,IAAI,MAAM,EAAE,CAAC;QAC3B,MAAM,KAAK,GAAG,OAAO,CAAC,KAAK,CAAC,IAAI,CAAC,CAEhC;QAAA,CAAC,MAAM,CAAC,KAAK,CAAC,KAAK,EAAE,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,CAAA;IAC7C,CAAC;IAED,OAAO,MAAM,CAAA;AACf,CAAC;AAED;;;;;;;;GAQG;AACH,MAAM,CAAC,KAAK,UAAU,YAAY,CAChC,MAAoC,EACpC,KAAc;IAEd,IAAI,CAAC,MAAM;QAAE,OAAO,IAAI,CAAA;IAExB,MAAM,MAAM,GAAG,MAAM,MAAM,CAAC,WAAW,CAAC,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAA;IAExD,OAAO,MAAM,CAAC,MAAM,IAAI,MAAM,CAAC,MAAM,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,cAAc,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,IAAI,CAAA;AACzF,CAAC","sourcesContent":["/**\n * Validating a form before it is submitted, with whatever library you use.\n *\n * Not a client module, though a client component is its first caller: the\n * action pipeline validates with it on the server too, and a \"use client\"\n * directive here turned that call into a client-reference stub - \"client\n * reference export 'validateWith' is called on server\", on every action\n * with a schema. Pure functions, no React; importable from either side.\n *\n * Not with an adapter per library: Standard Schema is the contract Zod,\n * Valibot, ArkType and others already implement, so this speaks to the schema\n * itself and never imports one. The types below are the spec, restated here\n * rather than depended on — it is an interface, and a package for it would be\n * a dependency that ships nothing.\n *\n * Validation on the client is a courtesy, never a control. The server still\n * has to check: a form is one way to reach an action, and the action is a\n * public endpoint reachable without it.\n */\n\nexport interface StandardSchemaV1<Input = unknown, Output = Input> {\n readonly '~standard': {\n readonly version: 1\n readonly vendor: string\n readonly validate: (\n value: unknown,\n ) => StandardSchemaResult<Output> | Promise<StandardSchemaResult<Output>>\n readonly types?: { readonly input: Input; readonly output: Output }\n }\n}\n\ninterface StandardSchemaResult<Output> {\n readonly value?: Output\n readonly issues?: ReadonlyArray<{\n readonly message: string\n readonly path?: ReadonlyArray<PropertyKey | { readonly key: PropertyKey }>\n }>\n}\n\n/**\n * The field an issue belongs to.\n *\n * Segments are joined with dots so a nested field reads the way it was named —\n * `address.city`, not `address`. An issue with no path is about the form\n * rather than any field, and goes under the empty string, which is where a\n * component that wants to render it can look.\n */\nfunction fieldOf(path: StandardSchemaResult<unknown>['issues'] extends undefined ? never : NonNullable<StandardSchemaResult<unknown>['issues']>[number]['path']): string {\n if (!path || path.length === 0) return ''\n\n return path\n .map((segment) => (typeof segment === 'object' && segment !== null ? segment.key : segment))\n .join('.')\n}\n\n/** Issues grouped by field, in the shape the form already reports. */\nexport function issuesToErrors(\n issues: NonNullable<StandardSchemaResult<unknown>['issues']>,\n): Record<string, string[]> {\n const errors: Record<string, string[]> = {}\n\n for (const issue of issues) {\n const field = fieldOf(issue.path)\n\n ;(errors[field] ??= []).push(issue.message)\n }\n\n return errors\n}\n\n/**\n * Run a schema, or nothing if there is not one.\n *\n * Returns the errors to show, or null when the value is acceptable. The parsed\n * value is deliberately discarded: what gets submitted is the FormData the\n * browser built, and quietly sending something else — a coerced number, a\n * trimmed string — would mean the action receives what the schema decided\n * rather than what the field said.\n */\nexport async function validateWith(\n schema: StandardSchemaV1 | undefined,\n value: unknown,\n): Promise<Record<string, string[]> | null> {\n if (!schema) return null\n\n const result = await schema['~standard'].validate(value)\n\n return result.issues && result.issues.length > 0 ? issuesToErrors(result.issues) : null\n}\n"]}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
export type EventsStatus = "connecting" | "open" | "closed";
|
|
2
|
+
export interface EventsOptions<T> {
|
|
3
|
+
/** Off, and nothing connects. For a stream that waits on an id. */
|
|
4
|
+
enabled?: boolean;
|
|
5
|
+
/** Listen to one named event rather than the unnamed stream. */
|
|
6
|
+
event?: string;
|
|
7
|
+
/** Every message, as it arrives. */
|
|
8
|
+
onMessage?: (message: T) => void;
|
|
9
|
+
/**
|
|
10
|
+
* The connection failed or dropped. EventSource reconnects on its own, so
|
|
11
|
+
* this is for a notice, not a retry; `status` says whether it gave up.
|
|
12
|
+
*/
|
|
13
|
+
onError?: (error: Event) => void;
|
|
14
|
+
/** How many messages `all` keeps. Default 100; 0 keeps none. */
|
|
15
|
+
keep?: number;
|
|
16
|
+
}
|
|
17
|
+
export interface EventsState<T> {
|
|
18
|
+
latest: T | null;
|
|
19
|
+
all: T[];
|
|
20
|
+
status: EventsStatus;
|
|
21
|
+
error: Event | null;
|
|
22
|
+
/** Close the stream for good; the hook will not reconnect. */
|
|
23
|
+
close: () => void;
|
|
24
|
+
}
|
|
25
|
+
export declare function useEvents<T = unknown>(url: string, options?: EventsOptions<T>): EventsState<T>;
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
/**
|
|
3
|
+
* A stream of server-sent events, as state.
|
|
4
|
+
*
|
|
5
|
+
* const { latest, status } = useEvents(`/api/orders/${id}/events`)
|
|
6
|
+
*
|
|
7
|
+
* Subscribes in an effect, so it is safe in a component that also renders on
|
|
8
|
+
* the server. The browser's EventSource reconnects on its own and resumes
|
|
9
|
+
* with Last-Event-ID when the route yielded ids; `status` says where it is.
|
|
10
|
+
* `onMessage` is for a store that already holds the value - TanStack's
|
|
11
|
+
* setQueryData, SWR's mutate, a setState - and `latest` for when the hook is
|
|
12
|
+
* the store.
|
|
13
|
+
*/
|
|
14
|
+
import { useEffect, useRef, useState } from "react";
|
|
15
|
+
export function useEvents(url, options = {}) {
|
|
16
|
+
const { enabled = true, event, keep = 100 } = options;
|
|
17
|
+
const onMessage = useRef(options.onMessage);
|
|
18
|
+
const onError = useRef(options.onError);
|
|
19
|
+
onMessage.current = options.onMessage;
|
|
20
|
+
onError.current = options.onError;
|
|
21
|
+
const [latest, setLatest] = useState(null);
|
|
22
|
+
const [all, setAll] = useState([]);
|
|
23
|
+
const [status, setStatus] = useState(enabled ? "connecting" : "closed");
|
|
24
|
+
const [error, setError] = useState(null);
|
|
25
|
+
const source = useRef(null);
|
|
26
|
+
useEffect(() => {
|
|
27
|
+
if (!enabled || typeof EventSource === "undefined") {
|
|
28
|
+
setStatus("closed");
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
const es = new EventSource(url);
|
|
32
|
+
source.current = es;
|
|
33
|
+
setStatus("connecting");
|
|
34
|
+
setError(null);
|
|
35
|
+
const receive = (e) => {
|
|
36
|
+
let message;
|
|
37
|
+
try {
|
|
38
|
+
message = JSON.parse(e.data);
|
|
39
|
+
}
|
|
40
|
+
catch {
|
|
41
|
+
message = e.data;
|
|
42
|
+
}
|
|
43
|
+
setLatest(message);
|
|
44
|
+
if (keep > 0)
|
|
45
|
+
setAll((prev) => prev.length >= keep
|
|
46
|
+
? [...prev.slice(1), message]
|
|
47
|
+
: [...prev, message]);
|
|
48
|
+
onMessage.current?.(message);
|
|
49
|
+
};
|
|
50
|
+
es.onopen = () => setStatus("open");
|
|
51
|
+
es.onerror = (e) => {
|
|
52
|
+
setError(e);
|
|
53
|
+
onError.current?.(e);
|
|
54
|
+
// EventSource reconnects on its own; CLOSED means it gave up.
|
|
55
|
+
setStatus(es.readyState === EventSource.CLOSED ? "closed" : "connecting");
|
|
56
|
+
};
|
|
57
|
+
if (event)
|
|
58
|
+
es.addEventListener(event, receive);
|
|
59
|
+
else
|
|
60
|
+
es.onmessage = receive;
|
|
61
|
+
return () => {
|
|
62
|
+
es.close();
|
|
63
|
+
source.current = null;
|
|
64
|
+
};
|
|
65
|
+
}, [url, enabled, event, keep]);
|
|
66
|
+
return {
|
|
67
|
+
latest,
|
|
68
|
+
all,
|
|
69
|
+
status,
|
|
70
|
+
error,
|
|
71
|
+
close: () => {
|
|
72
|
+
source.current?.close();
|
|
73
|
+
source.current = null;
|
|
74
|
+
setStatus("closed");
|
|
75
|
+
},
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
//# sourceMappingURL=useEvents.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"useEvents.js","sourceRoot":"","sources":["../../src/js/useEvents.ts"],"names":[],"mappings":"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","sourcesContent":["\"use client\";\n\n/**\n * A stream of server-sent events, as state.\n *\n * const { latest, status } = useEvents(`/api/orders/${id}/events`)\n *\n * Subscribes in an effect, so it is safe in a component that also renders on\n * the server. The browser's EventSource reconnects on its own and resumes\n * with Last-Event-ID when the route yielded ids; `status` says where it is.\n * `onMessage` is for a store that already holds the value - TanStack's\n * setQueryData, SWR's mutate, a setState - and `latest` for when the hook is\n * the store.\n */\n\nimport { useEffect, useRef, useState } from \"react\";\n\nexport type EventsStatus = \"connecting\" | \"open\" | \"closed\";\n\nexport interface EventsOptions<T> {\n /** Off, and nothing connects. For a stream that waits on an id. */\n enabled?: boolean;\n /** Listen to one named event rather than the unnamed stream. */\n event?: string;\n /** Every message, as it arrives. */\n onMessage?: (message: T) => void;\n /**\n * The connection failed or dropped. EventSource reconnects on its own, so\n * this is for a notice, not a retry; `status` says whether it gave up.\n */\n onError?: (error: Event) => void;\n /** How many messages `all` keeps. Default 100; 0 keeps none. */\n keep?: number;\n}\n\nexport interface EventsState<T> {\n latest: T | null;\n all: T[];\n status: EventsStatus;\n error: Event | null;\n /** Close the stream for good; the hook will not reconnect. */\n close: () => void;\n}\n\nexport function useEvents<T = unknown>(\n url: string,\n options: EventsOptions<T> = {},\n): EventsState<T> {\n const { enabled = true, event, keep = 100 } = options;\n const onMessage = useRef(options.onMessage);\n const onError = useRef(options.onError);\n\n onMessage.current = options.onMessage;\n onError.current = options.onError;\n\n const [latest, setLatest] = useState<T | null>(null);\n const [all, setAll] = useState<T[]>([]);\n const [status, setStatus] = useState<EventsStatus>(\n enabled ? \"connecting\" : \"closed\",\n );\n const [error, setError] = useState<Event | null>(null);\n const source = useRef<EventSource | null>(null);\n\n useEffect(() => {\n if (!enabled || typeof EventSource === \"undefined\") {\n setStatus(\"closed\");\n\n return;\n }\n\n const es = new EventSource(url);\n\n source.current = es;\n setStatus(\"connecting\");\n setError(null);\n\n const receive = (e: MessageEvent) => {\n let message: T;\n\n try {\n message = JSON.parse(e.data) as T;\n } catch {\n message = e.data as T;\n }\n\n setLatest(message);\n if (keep > 0)\n setAll((prev) =>\n prev.length >= keep\n ? [...prev.slice(1), message]\n : [...prev, message],\n );\n onMessage.current?.(message);\n };\n\n es.onopen = () => setStatus(\"open\");\n es.onerror = (e) => {\n setError(e);\n onError.current?.(e);\n // EventSource reconnects on its own; CLOSED means it gave up.\n setStatus(es.readyState === EventSource.CLOSED ? \"closed\" : \"connecting\");\n };\n\n if (event) es.addEventListener(event, receive as EventListener);\n else es.onmessage = receive;\n\n return () => {\n es.close();\n source.current = null;\n };\n }, [url, enabled, event, keep]);\n\n return {\n latest,\n all,\n status,\n error,\n close: () => {\n source.current?.close();\n source.current = null;\n setStatus(\"closed\");\n },\n };\n}\n"]}
|
|
@@ -1 +1,14 @@
|
|
|
1
|
-
|
|
1
|
+
/**
|
|
2
|
+
* Whether the enclosing <Link> is mid-navigation, for the thing inside it
|
|
3
|
+
* that wants to show as much.
|
|
4
|
+
*
|
|
5
|
+
* In its own module rather than beside Link: a module that exports a
|
|
6
|
+
* component and a hook cannot be fast-refreshed, and every change to Link
|
|
7
|
+
* became a full page reload in development for whoever had it open.
|
|
8
|
+
*/
|
|
9
|
+
export declare const LinkStatusContext: import("react").Context<{
|
|
10
|
+
pending: boolean;
|
|
11
|
+
}>;
|
|
12
|
+
export declare function useLinkStatus(): {
|
|
13
|
+
pending: boolean;
|
|
14
|
+
};
|
package/dist/js/useLinkStatus.js
CHANGED
|
@@ -1,3 +1,17 @@
|
|
|
1
1
|
"use client";
|
|
2
|
-
|
|
2
|
+
import { createContext, useContext } from "react";
|
|
3
|
+
/**
|
|
4
|
+
* Whether the enclosing <Link> is mid-navigation, for the thing inside it
|
|
5
|
+
* that wants to show as much.
|
|
6
|
+
*
|
|
7
|
+
* In its own module rather than beside Link: a module that exports a
|
|
8
|
+
* component and a hook cannot be fast-refreshed, and every change to Link
|
|
9
|
+
* became a full page reload in development for whoever had it open.
|
|
10
|
+
*/
|
|
11
|
+
export const LinkStatusContext = createContext({
|
|
12
|
+
pending: false,
|
|
13
|
+
});
|
|
14
|
+
export function useLinkStatus() {
|
|
15
|
+
return useContext(LinkStatusContext);
|
|
16
|
+
}
|
|
3
17
|
//# sourceMappingURL=useLinkStatus.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"useLinkStatus.js","sourceRoot":"","sources":["../../src/js/useLinkStatus.ts"],"names":[],"mappings":"AAAA,YAAY,CAAC;AAEb,OAAO,EAAE,aAAa,EAAE,MAAM,
|
|
1
|
+
{"version":3,"file":"useLinkStatus.js","sourceRoot":"","sources":["../../src/js/useLinkStatus.ts"],"names":[],"mappings":"AAAA,YAAY,CAAC;AAEb,OAAO,EAAE,aAAa,EAAE,UAAU,EAAE,MAAM,OAAO,CAAC;AAElD;;;;;;;GAOG;AACH,MAAM,CAAC,MAAM,iBAAiB,GAAG,aAAa,CAAuB;IACnE,OAAO,EAAE,KAAK;CACf,CAAC,CAAC;AAEH,MAAM,UAAU,aAAa;IAC3B,OAAO,UAAU,CAAC,iBAAiB,CAAC,CAAC;AACvC,CAAC","sourcesContent":["\"use client\";\n\nimport { createContext, useContext } from \"react\";\n\n/**\n * Whether the enclosing <Link> is mid-navigation, for the thing inside it\n * that wants to show as much.\n *\n * In its own module rather than beside Link: a module that exports a\n * component and a hook cannot be fast-refreshed, and every change to Link\n * became a full page reload in development for whoever had it open.\n */\nexport const LinkStatusContext = createContext<{ pending: boolean }>({\n pending: false,\n});\n\nexport function useLinkStatus(): { pending: boolean } {\n return useContext(LinkStatusContext);\n}\n"]}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
export interface PollingOptions<T> {
|
|
2
|
+
/** Milliseconds between reads. */
|
|
3
|
+
every: number;
|
|
4
|
+
enabled?: boolean;
|
|
5
|
+
/** Every answer, as it arrives. */
|
|
6
|
+
onData?: (data: T) => void;
|
|
7
|
+
/** When a read is the last one: polling stops, and the answer is settled. */
|
|
8
|
+
until?: (data: T) => boolean;
|
|
9
|
+
/** Once, with the answer `until` accepted. */
|
|
10
|
+
onSettled?: (data: T) => void;
|
|
11
|
+
/**
|
|
12
|
+
* Every read that failed. The next interval still reads; `error` is the
|
|
13
|
+
* state. `failures` counts the failed reads in a row, so a third one can be
|
|
14
|
+
* a toast where the first was a blip — a success resets it.
|
|
15
|
+
*/
|
|
16
|
+
onError?: (error: unknown, info: {
|
|
17
|
+
failures: number;
|
|
18
|
+
}) => void;
|
|
19
|
+
/** Keep reading while the tab is hidden. Off by default. */
|
|
20
|
+
whenHidden?: boolean;
|
|
21
|
+
}
|
|
22
|
+
export interface PollingState<T> {
|
|
23
|
+
data: T | null;
|
|
24
|
+
error: unknown;
|
|
25
|
+
status: "idle" | "reading" | "paused" | "settled";
|
|
26
|
+
/** Read now, outside the interval - and start again after settling. */
|
|
27
|
+
refresh: () => Promise<void>;
|
|
28
|
+
}
|
|
29
|
+
export declare function usePolling<T>(read: () => Promise<T>, options: PollingOptions<T>): PollingState<T>;
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
/**
|
|
3
|
+
* A value read again on an interval, as state - until it settles, if asked.
|
|
4
|
+
*
|
|
5
|
+
* const { data } = usePolling(() => fetchQuery(getSeats), { every: 2_000 })
|
|
6
|
+
*
|
|
7
|
+
* // Until a job is done, then re-render the page that showed it.
|
|
8
|
+
* usePolling(() => fetchQuery(jobStatus, [id]), {
|
|
9
|
+
* every: 2_000,
|
|
10
|
+
* until: (job) => job.state === 'done' || job.state === 'failed',
|
|
11
|
+
* onSettled: () => refresh('page'),
|
|
12
|
+
* })
|
|
13
|
+
*
|
|
14
|
+
* The result is the data: whatever `read` returns is what `data` holds, and
|
|
15
|
+
* a query's Cache-Control lets a CDN absorb a thousand tabs polling the same
|
|
16
|
+
* thing into one origin request per interval. Settling is separate and
|
|
17
|
+
* explicit: `until` says when a read is the last one, and `onSettled` fires
|
|
18
|
+
* once on that read - so a page that wants to re-render through the server
|
|
19
|
+
* path that built it calls refresh('page') there, and a page that wants the
|
|
20
|
+
* value in hand reads `data`. Which of those is the page's to decide, not
|
|
21
|
+
* the hook's.
|
|
22
|
+
*
|
|
23
|
+
* Pauses while the tab is hidden, never overlaps two reads, and `refresh()`
|
|
24
|
+
* reads now - including after it settled, which starts it again.
|
|
25
|
+
*
|
|
26
|
+
* Polling against server-sent events: a stream sends bytes only when
|
|
27
|
+
* something changed and arrives at once, but holds a connection per tab
|
|
28
|
+
* and needs a source of change to yield from. Polling needs neither - it
|
|
29
|
+
* reuses the query you wrote - and is cacheable. Start here when you have
|
|
30
|
+
* no change feed yet; see useEvents when you do.
|
|
31
|
+
*/
|
|
32
|
+
import { useCallback, useEffect, useRef, useState } from "react";
|
|
33
|
+
export function usePolling(read, options) {
|
|
34
|
+
const { every, enabled = true, whenHidden = false } = options;
|
|
35
|
+
const latest = useRef({
|
|
36
|
+
read,
|
|
37
|
+
onData: options.onData,
|
|
38
|
+
until: options.until,
|
|
39
|
+
onSettled: options.onSettled,
|
|
40
|
+
onError: options.onError,
|
|
41
|
+
});
|
|
42
|
+
latest.current = {
|
|
43
|
+
read,
|
|
44
|
+
onData: options.onData,
|
|
45
|
+
until: options.until,
|
|
46
|
+
onSettled: options.onSettled,
|
|
47
|
+
onError: options.onError,
|
|
48
|
+
};
|
|
49
|
+
const [data, setData] = useState(null);
|
|
50
|
+
const [error, setError] = useState(null);
|
|
51
|
+
const failures = useRef(0);
|
|
52
|
+
const [status, setStatus] = useState(enabled ? "reading" : "idle");
|
|
53
|
+
const inFlight = useRef(null);
|
|
54
|
+
// Settled is a stop: the interval is cleared and stays cleared until
|
|
55
|
+
// refresh() or a change of inputs. Kept in a ref as well as state, so the
|
|
56
|
+
// interval callback sees it without a re-render in between.
|
|
57
|
+
const isSettled = useRef(false);
|
|
58
|
+
const refresh = useCallback(async () => {
|
|
59
|
+
// Never two at once: a slow answer and a fast interval would otherwise
|
|
60
|
+
// pile reads up, and the last to land wins whether or not it was newest.
|
|
61
|
+
if (inFlight.current)
|
|
62
|
+
return inFlight.current;
|
|
63
|
+
// A read after settling is asked for: refresh() starts the clock again.
|
|
64
|
+
if (isSettled.current) {
|
|
65
|
+
isSettled.current = false;
|
|
66
|
+
setStatus("reading");
|
|
67
|
+
}
|
|
68
|
+
inFlight.current = (async () => {
|
|
69
|
+
try {
|
|
70
|
+
const next = await latest.current.read();
|
|
71
|
+
setData(next);
|
|
72
|
+
setError(null);
|
|
73
|
+
failures.current = 0;
|
|
74
|
+
latest.current.onData?.(next);
|
|
75
|
+
if (latest.current.until?.(next)) {
|
|
76
|
+
isSettled.current = true;
|
|
77
|
+
setStatus("settled");
|
|
78
|
+
latest.current.onSettled?.(next);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
catch (e) {
|
|
82
|
+
failures.current += 1;
|
|
83
|
+
setError(e);
|
|
84
|
+
latest.current.onError?.(e, { failures: failures.current });
|
|
85
|
+
}
|
|
86
|
+
finally {
|
|
87
|
+
inFlight.current = null;
|
|
88
|
+
}
|
|
89
|
+
})();
|
|
90
|
+
return inFlight.current;
|
|
91
|
+
}, []);
|
|
92
|
+
useEffect(() => {
|
|
93
|
+
if (!enabled) {
|
|
94
|
+
setStatus("idle");
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
let timer = null;
|
|
98
|
+
isSettled.current = false;
|
|
99
|
+
const start = () => {
|
|
100
|
+
if (timer)
|
|
101
|
+
return;
|
|
102
|
+
setStatus("reading");
|
|
103
|
+
void refresh();
|
|
104
|
+
timer = setInterval(() => {
|
|
105
|
+
if (!isSettled.current)
|
|
106
|
+
void refresh();
|
|
107
|
+
}, every);
|
|
108
|
+
};
|
|
109
|
+
const stop = () => {
|
|
110
|
+
if (timer)
|
|
111
|
+
clearInterval(timer);
|
|
112
|
+
timer = null;
|
|
113
|
+
};
|
|
114
|
+
const onVisibility = () => {
|
|
115
|
+
if (document.visibilityState === "hidden") {
|
|
116
|
+
stop();
|
|
117
|
+
setStatus("paused");
|
|
118
|
+
}
|
|
119
|
+
else {
|
|
120
|
+
start();
|
|
121
|
+
}
|
|
122
|
+
};
|
|
123
|
+
if (whenHidden ||
|
|
124
|
+
typeof document === "undefined" ||
|
|
125
|
+
document.visibilityState !== "hidden")
|
|
126
|
+
start();
|
|
127
|
+
else
|
|
128
|
+
setStatus("paused");
|
|
129
|
+
if (!whenHidden && typeof document !== "undefined") {
|
|
130
|
+
document.addEventListener("visibilitychange", onVisibility);
|
|
131
|
+
}
|
|
132
|
+
return () => {
|
|
133
|
+
stop();
|
|
134
|
+
if (!whenHidden && typeof document !== "undefined") {
|
|
135
|
+
document.removeEventListener("visibilitychange", onVisibility);
|
|
136
|
+
}
|
|
137
|
+
};
|
|
138
|
+
}, [enabled, every, whenHidden, refresh]);
|
|
139
|
+
return { data, error, status, refresh };
|
|
140
|
+
}
|
|
141
|
+
//# sourceMappingURL=usePolling.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"usePolling.js","sourceRoot":"","sources":["../../src/js/usePolling.ts"],"names":[],"mappings":"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","sourcesContent":["\"use client\";\n\n/**\n * A value read again on an interval, as state - until it settles, if asked.\n *\n * const { data } = usePolling(() => fetchQuery(getSeats), { every: 2_000 })\n *\n * // Until a job is done, then re-render the page that showed it.\n * usePolling(() => fetchQuery(jobStatus, [id]), {\n * every: 2_000,\n * until: (job) => job.state === 'done' || job.state === 'failed',\n * onSettled: () => refresh('page'),\n * })\n *\n * The result is the data: whatever `read` returns is what `data` holds, and\n * a query's Cache-Control lets a CDN absorb a thousand tabs polling the same\n * thing into one origin request per interval. Settling is separate and\n * explicit: `until` says when a read is the last one, and `onSettled` fires\n * once on that read - so a page that wants to re-render through the server\n * path that built it calls refresh('page') there, and a page that wants the\n * value in hand reads `data`. Which of those is the page's to decide, not\n * the hook's.\n *\n * Pauses while the tab is hidden, never overlaps two reads, and `refresh()`\n * reads now - including after it settled, which starts it again.\n *\n * Polling against server-sent events: a stream sends bytes only when\n * something changed and arrives at once, but holds a connection per tab\n * and needs a source of change to yield from. Polling needs neither - it\n * reuses the query you wrote - and is cacheable. Start here when you have\n * no change feed yet; see useEvents when you do.\n */\n\nimport { useCallback, useEffect, useRef, useState } from \"react\";\n\nexport interface PollingOptions<T> {\n /** Milliseconds between reads. */\n every: number;\n enabled?: boolean;\n /** Every answer, as it arrives. */\n onData?: (data: T) => void;\n /** When a read is the last one: polling stops, and the answer is settled. */\n until?: (data: T) => boolean;\n /** Once, with the answer `until` accepted. */\n onSettled?: (data: T) => void;\n /**\n * Every read that failed. The next interval still reads; `error` is the\n * state. `failures` counts the failed reads in a row, so a third one can be\n * a toast where the first was a blip — a success resets it.\n */\n onError?: (error: unknown, info: { failures: number }) => void;\n /** Keep reading while the tab is hidden. Off by default. */\n whenHidden?: boolean;\n}\n\nexport interface PollingState<T> {\n data: T | null;\n error: unknown;\n status: \"idle\" | \"reading\" | \"paused\" | \"settled\";\n /** Read now, outside the interval - and start again after settling. */\n refresh: () => Promise<void>;\n}\n\nexport function usePolling<T>(\n read: () => Promise<T>,\n options: PollingOptions<T>,\n): PollingState<T> {\n const { every, enabled = true, whenHidden = false } = options;\n const latest = useRef({\n read,\n onData: options.onData,\n until: options.until,\n onSettled: options.onSettled,\n onError: options.onError,\n });\n\n latest.current = {\n read,\n onData: options.onData,\n until: options.until,\n onSettled: options.onSettled,\n onError: options.onError,\n };\n\n const [data, setData] = useState<T | null>(null);\n const [error, setError] = useState<unknown>(null);\n const failures = useRef(0);\n const [status, setStatus] = useState<PollingState<T>[\"status\"]>(\n enabled ? \"reading\" : \"idle\",\n );\n const inFlight = useRef<Promise<void> | null>(null);\n // Settled is a stop: the interval is cleared and stays cleared until\n // refresh() or a change of inputs. Kept in a ref as well as state, so the\n // interval callback sees it without a re-render in between.\n const isSettled = useRef(false);\n\n const refresh = useCallback(async () => {\n // Never two at once: a slow answer and a fast interval would otherwise\n // pile reads up, and the last to land wins whether or not it was newest.\n if (inFlight.current) return inFlight.current;\n\n // A read after settling is asked for: refresh() starts the clock again.\n if (isSettled.current) {\n isSettled.current = false;\n setStatus(\"reading\");\n }\n\n inFlight.current = (async () => {\n try {\n const next = await latest.current.read();\n\n setData(next);\n setError(null);\n failures.current = 0;\n latest.current.onData?.(next);\n\n if (latest.current.until?.(next)) {\n isSettled.current = true;\n setStatus(\"settled\");\n latest.current.onSettled?.(next);\n }\n } catch (e) {\n failures.current += 1;\n setError(e);\n latest.current.onError?.(e, { failures: failures.current });\n } finally {\n inFlight.current = null;\n }\n })();\n\n return inFlight.current;\n }, []);\n\n useEffect(() => {\n if (!enabled) {\n setStatus(\"idle\");\n\n return;\n }\n\n let timer: ReturnType<typeof setInterval> | null = null;\n\n isSettled.current = false;\n\n const start = () => {\n if (timer) return;\n\n setStatus(\"reading\");\n void refresh();\n timer = setInterval(() => {\n if (!isSettled.current) void refresh();\n }, every);\n };\n const stop = () => {\n if (timer) clearInterval(timer);\n\n timer = null;\n };\n const onVisibility = () => {\n if (document.visibilityState === \"hidden\") {\n stop();\n setStatus(\"paused\");\n } else {\n start();\n }\n };\n\n if (\n whenHidden ||\n typeof document === \"undefined\" ||\n document.visibilityState !== \"hidden\"\n )\n start();\n else setStatus(\"paused\");\n\n if (!whenHidden && typeof document !== \"undefined\") {\n document.addEventListener(\"visibilitychange\", onVisibility);\n }\n\n return () => {\n stop();\n if (!whenHidden && typeof document !== \"undefined\") {\n document.removeEventListener(\"visibilitychange\", onVisibility);\n }\n };\n }, [enabled, every, whenHidden, refresh]);\n\n return { data, error, status, refresh };\n}\n"]}
|
package/dist/manifest.d.ts
CHANGED
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
export interface RouteSegment {
|
|
2
|
-
|
|
2
|
+
/**
|
|
3
|
+
* `host`: a `[name]` directory at the top of app/. Bound from the request's
|
|
4
|
+
* host, never from a path segment - so `example.com/nope` is a 404 rather
|
|
5
|
+
* than a tenant called "nope". See hostRouting.
|
|
6
|
+
*/
|
|
7
|
+
type: "static" | "param" | "catchAll" | "host";
|
|
3
8
|
value: string;
|
|
4
9
|
}
|
|
5
10
|
export interface ManifestRoute {
|
|
@@ -96,6 +101,18 @@ export interface RouteManifest {
|
|
|
96
101
|
output: string;
|
|
97
102
|
exportPath: string;
|
|
98
103
|
payloadName: string;
|
|
104
|
+
/**
|
|
105
|
+
* The site's own hosts, bare and lower-case. A request from any other
|
|
106
|
+
* host is matched with that host's segment in front of its path - see
|
|
107
|
+
* hostRouting. Absent or empty: every request is the site's own.
|
|
108
|
+
*/
|
|
109
|
+
hosts?: string[];
|
|
110
|
+
/**
|
|
111
|
+
* Whether every response says what built it: `X-Powered-By: rsc-kit` and
|
|
112
|
+
* a generator meta tag in the document. The name only, never the version.
|
|
113
|
+
* `X-RSC-Kit` (how a response was served) is sent regardless.
|
|
114
|
+
*/
|
|
115
|
+
identify?: boolean;
|
|
99
116
|
};
|
|
100
117
|
routes: ManifestRoute[];
|
|
101
118
|
intercepts: ManifestIntercept[];
|
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 type:
|
|
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 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"]}
|
package/dist/metadata.d.ts
CHANGED
|
@@ -5,7 +5,7 @@ export interface IconDescriptor {
|
|
|
5
5
|
color?: string;
|
|
6
6
|
rel?: string;
|
|
7
7
|
media?: string;
|
|
8
|
-
fetchPriority?:
|
|
8
|
+
fetchPriority?: "high" | "low" | "auto";
|
|
9
9
|
}
|
|
10
10
|
export type IconURL = string | URL;
|
|
11
11
|
export interface Icons {
|
|
@@ -40,13 +40,13 @@ export interface OpenGraph {
|
|
|
40
40
|
/** Absolute, or relative to `metadataBase`. */
|
|
41
41
|
url?: string | URL;
|
|
42
42
|
siteName?: string;
|
|
43
|
-
type?:
|
|
43
|
+
type?: "website" | "article" | "profile" | "book" | (string & {});
|
|
44
44
|
locale?: string;
|
|
45
45
|
images?: string | URL | OpenGraphImage | (string | URL | OpenGraphImage)[];
|
|
46
46
|
}
|
|
47
47
|
/** The same card for X, which reads `name=` rather than `property=`. */
|
|
48
48
|
export interface Twitter {
|
|
49
|
-
card?:
|
|
49
|
+
card?: "summary" | "summary_large_image" | "app" | "player";
|
|
50
50
|
title?: string;
|
|
51
51
|
description?: string;
|
|
52
52
|
/** The site's account, `@handle`. */
|
|
@@ -55,13 +55,35 @@ export interface Twitter {
|
|
|
55
55
|
creator?: string;
|
|
56
56
|
images?: string | URL | OpenGraphImage | (string | URL | OpenGraphImage)[];
|
|
57
57
|
}
|
|
58
|
+
export interface Robots {
|
|
59
|
+
index?: boolean;
|
|
60
|
+
follow?: boolean;
|
|
61
|
+
noarchive?: boolean;
|
|
62
|
+
nosnippet?: boolean;
|
|
63
|
+
noimageindex?: boolean;
|
|
64
|
+
nocache?: boolean;
|
|
65
|
+
notranslate?: boolean;
|
|
66
|
+
indexifembedded?: boolean;
|
|
67
|
+
nositelinkssearchbox?: boolean;
|
|
68
|
+
unavailable_after?: string;
|
|
69
|
+
"max-video-preview"?: number | string;
|
|
70
|
+
"max-image-preview"?: "none" | "standard" | "large";
|
|
71
|
+
"max-snippet"?: number;
|
|
72
|
+
googleBot?: string | Omit<Robots, "googleBot">;
|
|
73
|
+
}
|
|
58
74
|
export interface Metadata {
|
|
59
75
|
/** A string on a page; a template on a layout, applied to the pages below it. */
|
|
60
76
|
title?: string | TitleTemplate;
|
|
61
77
|
description?: string;
|
|
62
78
|
keywords?: string | string[];
|
|
63
79
|
author?: string;
|
|
64
|
-
|
|
80
|
+
/**
|
|
81
|
+
* A string, or the object Next takes: `{ index: false, follow: false }`
|
|
82
|
+
* becomes `<meta name="robots" content="noindex, nofollow">`, the flags
|
|
83
|
+
* by name, the limits as `name:value`; `googleBot` is the same shape for
|
|
84
|
+
* `<meta name="googlebot">`.
|
|
85
|
+
*/
|
|
86
|
+
robots?: string | Robots;
|
|
65
87
|
/**
|
|
66
88
|
* Where the site lives, so a relative image or url can be made absolute.
|
|
67
89
|
*
|
|
@@ -77,27 +99,27 @@ export interface Metadata {
|
|
|
77
99
|
openGraph?: OpenGraph;
|
|
78
100
|
twitter?: Twitter;
|
|
79
101
|
/** @deprecated Use `openGraph.title`. Still rendered, correctly, as `property=`. */
|
|
80
|
-
|
|
102
|
+
"og:title"?: string;
|
|
81
103
|
/** @deprecated Use `openGraph.description`. */
|
|
82
|
-
|
|
104
|
+
"og:description"?: string;
|
|
83
105
|
/** @deprecated Use `openGraph.images`. */
|
|
84
|
-
|
|
106
|
+
"og:image"?: string;
|
|
85
107
|
/** @deprecated Use `openGraph.url`. */
|
|
86
|
-
|
|
108
|
+
"og:url"?: string;
|
|
87
109
|
/** @deprecated Use `openGraph.type`. */
|
|
88
|
-
|
|
110
|
+
"og:type"?: string;
|
|
89
111
|
/** @deprecated Use `openGraph.siteName`. */
|
|
90
|
-
|
|
112
|
+
"og:site_name"?: string;
|
|
91
113
|
/** @deprecated Use `twitter.card`. */
|
|
92
|
-
|
|
114
|
+
"twitter:card"?: string;
|
|
93
115
|
/** @deprecated Use `twitter.title`. */
|
|
94
|
-
|
|
116
|
+
"twitter:title"?: string;
|
|
95
117
|
/** @deprecated Use `twitter.description`. */
|
|
96
|
-
|
|
118
|
+
"twitter:description"?: string;
|
|
97
119
|
/** @deprecated Use `twitter.images`. */
|
|
98
|
-
|
|
120
|
+
"twitter:image"?: string;
|
|
99
121
|
/** @deprecated Use `twitter.site`. */
|
|
100
|
-
|
|
122
|
+
"twitter:site"?: string;
|
|
101
123
|
/**
|
|
102
124
|
* Any other meta tag, by name.
|
|
103
125
|
*
|
|
@@ -125,3 +147,60 @@ export type GenerateMetadata<P = Record<string, string>> = (args: {
|
|
|
125
147
|
params: Promise<P>;
|
|
126
148
|
searchParams: Promise<URLSearchParams>;
|
|
127
149
|
}) => Metadata | Promise<Metadata>;
|
|
150
|
+
/** One rule block of a robots.txt: which agents, what they may and may not fetch. */
|
|
151
|
+
export interface RobotsRule {
|
|
152
|
+
userAgent?: string | string[];
|
|
153
|
+
allow?: string | string[];
|
|
154
|
+
disallow?: string | string[];
|
|
155
|
+
crawlDelay?: number;
|
|
156
|
+
}
|
|
157
|
+
/** One url of a sitemap. `url` may be relative when the root layout has a metadataBase. */
|
|
158
|
+
export interface SitemapEntry {
|
|
159
|
+
url: string;
|
|
160
|
+
lastModified?: string | Date;
|
|
161
|
+
changeFrequency?: "always" | "hourly" | "daily" | "weekly" | "monthly" | "yearly" | "never";
|
|
162
|
+
priority?: number;
|
|
163
|
+
/** Image urls on this page, for image search. */
|
|
164
|
+
images?: string[];
|
|
165
|
+
/** Translations of this page: language tag to url. */
|
|
166
|
+
alternates?: {
|
|
167
|
+
languages?: Record<string, string>;
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
/** A link in an llms.txt section. */
|
|
171
|
+
export interface LlmsLink {
|
|
172
|
+
title: string;
|
|
173
|
+
url: string;
|
|
174
|
+
description?: string;
|
|
175
|
+
}
|
|
176
|
+
export interface LlmsSection {
|
|
177
|
+
title: string;
|
|
178
|
+
links: LlmsLink[];
|
|
179
|
+
}
|
|
180
|
+
/**
|
|
181
|
+
* The files a site describes itself with, each from a file beside the root
|
|
182
|
+
* layout and written the way Next writes them:
|
|
183
|
+
*
|
|
184
|
+
* app/robots.ts -> /robots.txt default export returns MetadataRoute.Robots
|
|
185
|
+
* app/sitemap.ts -> /sitemap.xml default export returns MetadataRoute.Sitemap
|
|
186
|
+
* app/llms.ts -> /llms.txt default export returns MetadataRoute.Llms
|
|
187
|
+
*
|
|
188
|
+
* Each may return a string instead, served as written. A relative url in any
|
|
189
|
+
* of them is made absolute with the root layout's metadataBase.
|
|
190
|
+
*/
|
|
191
|
+
export declare namespace MetadataRoute {
|
|
192
|
+
type Robots = {
|
|
193
|
+
rules: RobotsRule | RobotsRule[];
|
|
194
|
+
sitemap?: string | string[];
|
|
195
|
+
host?: string;
|
|
196
|
+
};
|
|
197
|
+
type Sitemap = SitemapEntry[];
|
|
198
|
+
/** The llms.txt shape at llmstxt.org: a title, a summary, then sections of links. */
|
|
199
|
+
type Llms = {
|
|
200
|
+
title: string;
|
|
201
|
+
summary?: string;
|
|
202
|
+
/** Paragraphs after the summary, before the sections. */
|
|
203
|
+
details?: string | string[];
|
|
204
|
+
sections?: LlmsSection[];
|
|
205
|
+
};
|
|
206
|
+
}
|