@ultimat3/render 27.8.1 → 27.9.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/README.md +8 -0
- package/package.json +6 -6
- package/src/index.ts +2 -0
- package/src/prerender-enumerate.ts +108 -0
- package/src/render-static.ts +5 -54
- package/src/route.ts +23 -4
- package/src/server.ts +1 -1
package/README.md
CHANGED
|
@@ -26,6 +26,14 @@ export const config = defineRoute({
|
|
|
26
26
|
export function Page(props: { data: Post }) { /* the SAME object meta was given */ }
|
|
27
27
|
```
|
|
28
28
|
|
|
29
|
+
`prerender()` answers the pages to build: a bare string (a one-param route), a params object, or an
|
|
30
|
+
entry — `{ params, lastmod?, locales? }` (`PrerenderEntry`) — mixed freely. The build reads `params`
|
|
31
|
+
alone and renders every routed locale; `lastmod` (a `Date` or ISO 8601 with a zone) and `locales`
|
|
32
|
+
are what the page says of itself for the sitemap: its `<lastmod>`, and the locales it is listed and
|
|
33
|
+
cross-linked in. `enumeratePrerender()` answers params, `enumeratePrerenderEntries()` the entries.
|
|
34
|
+
A key outside the three, a `lastmod` that is neither a string nor a `Date`, or `locales` that is
|
|
35
|
+
not a list of names is `X_PRERENDER_FAILED`, naming the route file.
|
|
36
|
+
|
|
29
37
|
## `load` is the one server-side data seam
|
|
30
38
|
|
|
31
39
|
Optional, and the only way a page gets data. It runs **once per render** and the result is handed
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ultimat3/render",
|
|
3
|
-
"version": "27.
|
|
3
|
+
"version": "27.9.0",
|
|
4
4
|
"description": "The route primitive and the five render modes: static, isr, ssr, stream, spa.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -40,11 +40,11 @@
|
|
|
40
40
|
"test": "bun test"
|
|
41
41
|
},
|
|
42
42
|
"dependencies": {
|
|
43
|
-
"@ultimat3/cache": "27.
|
|
44
|
-
"@ultimat3/core": "27.
|
|
45
|
-
"@ultimat3/http": "27.
|
|
46
|
-
"@ultimat3/i18n": "27.
|
|
47
|
-
"@ultimat3/seo": "27.
|
|
43
|
+
"@ultimat3/cache": "27.9.0",
|
|
44
|
+
"@ultimat3/core": "27.9.0",
|
|
45
|
+
"@ultimat3/http": "27.9.0",
|
|
46
|
+
"@ultimat3/i18n": "27.9.0",
|
|
47
|
+
"@ultimat3/seo": "27.9.0",
|
|
48
48
|
"sass": "1.105.1"
|
|
49
49
|
}
|
|
50
50
|
}
|
package/src/index.ts
CHANGED
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
// Single responsibility: what a route's `prerender()` answered, normalised — every page as its
|
|
2
|
+
// params, plus what the page said about itself for a sitemap (`lastmod`, `locales`).
|
|
3
|
+
|
|
4
|
+
import { renderCauseValue, renderThrowable } from '@ultimat3/core';
|
|
5
|
+
import { PrerenderFailedError } from './errors';
|
|
6
|
+
import type { RouteEntry } from './registry';
|
|
7
|
+
import type { PrerenderEntry, RouteParams } from './route';
|
|
8
|
+
|
|
9
|
+
const ENTRY_KEYS: readonly string[] = ['params', 'lastmod', 'locales'];
|
|
10
|
+
|
|
11
|
+
const isRecord = (value: unknown): value is Readonly<Record<string, unknown>> =>
|
|
12
|
+
typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Is `item` the ENTRY form — `{ params: { … }, lastmod?, locales? }` — rather than a params object?
|
|
16
|
+
* Decided by `params` holding an object: a param VALUE is always a string, so `{ params: 'a' }` on a
|
|
17
|
+
* route whose own dynamic segment is `[params]` stays the plain params it has always been.
|
|
18
|
+
*/
|
|
19
|
+
const isEntry = (item: Readonly<Record<string, unknown>>): boolean => isRecord(item['params']);
|
|
20
|
+
|
|
21
|
+
function entryOf(entry: RouteEntry, item: Readonly<Record<string, unknown>>): PrerenderEntry {
|
|
22
|
+
const refuse = (problem: string): never => {
|
|
23
|
+
throw new PrerenderFailedError(
|
|
24
|
+
`prerender() for ${entry.path} returned an entry ${problem}`,
|
|
25
|
+
`x build --target static --json # after prerender in ${entry.file} returns { params, lastmod?: Date | ISO 8601 string, locales?: string[] }`,
|
|
26
|
+
);
|
|
27
|
+
};
|
|
28
|
+
const extra = Object.keys(item).find((key) => !ENTRY_KEYS.includes(key));
|
|
29
|
+
if (extra !== undefined) {
|
|
30
|
+
refuse(`with the key ${JSON.stringify(extra)}, which is not one of ${ENTRY_KEYS.join(', ')}`);
|
|
31
|
+
}
|
|
32
|
+
const { lastmod, locales } = item;
|
|
33
|
+
if (lastmod !== undefined && typeof lastmod !== 'string' && !(lastmod instanceof Date)) {
|
|
34
|
+
refuse(`whose lastmod is ${renderCauseValue(lastmod)}, neither a Date nor an ISO 8601 string`);
|
|
35
|
+
}
|
|
36
|
+
if (
|
|
37
|
+
locales !== undefined &&
|
|
38
|
+
!(Array.isArray(locales) && locales.every((locale) => typeof locale === 'string'))
|
|
39
|
+
) {
|
|
40
|
+
refuse(`whose locales is ${renderCauseValue(locales)}, not a list of locale names`);
|
|
41
|
+
}
|
|
42
|
+
return {
|
|
43
|
+
params: item['params'] as RouteParams,
|
|
44
|
+
...(lastmod === undefined ? {} : { lastmod: lastmod as string | Date }),
|
|
45
|
+
...(locales === undefined ? {} : { locales: locales as readonly string[] }),
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Every page `prerender()` enumerates, as an entry. A bare string fills the route's one dynamic
|
|
51
|
+
* param; a params object is an entry with nothing to add. `lastmod` is carried AS WRITTEN — whether
|
|
52
|
+
* it names an instant is the sitemap's question (`@ultimat3/seo`, `X_SITEMAP_ENTRY_INVALID`).
|
|
53
|
+
*/
|
|
54
|
+
export async function enumeratePrerenderEntries(
|
|
55
|
+
entry: RouteEntry,
|
|
56
|
+
): Promise<readonly PrerenderEntry[]> {
|
|
57
|
+
const prerender = entry.config.prerender;
|
|
58
|
+
if (prerender === undefined) {
|
|
59
|
+
return entry.pattern.keys.length === 0 ? [{ params: {} }] : [];
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
let produced: unknown;
|
|
63
|
+
try {
|
|
64
|
+
produced = await prerender();
|
|
65
|
+
} catch (error) {
|
|
66
|
+
throw new PrerenderFailedError(
|
|
67
|
+
// `renderThrowable`, never `.message`/`String()`: `prerender` is app code and may throw a
|
|
68
|
+
// value whose read raises in turn — this frame is what makes the build failure a coded one.
|
|
69
|
+
`prerender() for ${entry.path} threw: ${renderThrowable(error)}`,
|
|
70
|
+
`fix prerender in ${entry.file} — it runs at build time with no request context`,
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
if (!Array.isArray(produced)) {
|
|
75
|
+
throw new PrerenderFailedError(
|
|
76
|
+
`prerender() for ${entry.path} returned ${typeof produced}, expected an array`,
|
|
77
|
+
`return an array of params from prerender in ${entry.file}`,
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const keys = entry.pattern.keys;
|
|
82
|
+
return produced.map((item: unknown): PrerenderEntry => {
|
|
83
|
+
if (typeof item === 'string') {
|
|
84
|
+
const only = keys[0];
|
|
85
|
+
if (keys.length !== 1 || only === undefined) {
|
|
86
|
+
throw new PrerenderFailedError(
|
|
87
|
+
`prerender() for ${entry.path} returned the bare string ${renderCauseValue(item)} ` +
|
|
88
|
+
`but the route has ${keys.length} dynamic params (${keys.join(', ')})`,
|
|
89
|
+
`return objects from prerender in ${entry.file}, e.g. { ${keys.join(': …, ')}: … }`,
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
return { params: { [only]: item } };
|
|
93
|
+
}
|
|
94
|
+
// A `null` reached `Object.hasOwn` in `static-path.ts` and left the build as a bare `TypeError`.
|
|
95
|
+
if (!isRecord(item)) {
|
|
96
|
+
throw new PrerenderFailedError(
|
|
97
|
+
`prerender() for ${entry.path} returned ${renderCauseValue(item)} as one of its items, which is neither a params object nor a string`,
|
|
98
|
+
`return an array of params objects (or strings, for a one-param route) from prerender in ${entry.file}`,
|
|
99
|
+
);
|
|
100
|
+
}
|
|
101
|
+
return isEntry(item) ? entryOf(entry, item) : { params: item as RouteParams };
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/** The params of every page `prerender()` enumerates — what a build renders, one document each. */
|
|
106
|
+
export async function enumeratePrerender(entry: RouteEntry): Promise<readonly RouteParams[]> {
|
|
107
|
+
return (await enumeratePrerenderEntries(entry)).map((page) => page.params);
|
|
108
|
+
}
|
package/src/render-static.ts
CHANGED
|
@@ -4,12 +4,16 @@
|
|
|
4
4
|
* the precache revision in `sw.js`, and the asset filename suffix.
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
-
import { BUILD_ID_HEADER,
|
|
7
|
+
import { BUILD_ID_HEADER, renderThrowable, useContext } from '@ultimat3/core';
|
|
8
8
|
import { PrerenderFailedError, RouteModeInvalidError } from './errors';
|
|
9
|
+
import { enumeratePrerender } from './prerender-enumerate';
|
|
9
10
|
import type { RouteEntry } from './registry';
|
|
10
11
|
import type { RenderResult, RouteParams } from './route';
|
|
11
12
|
import { filePathOf, filledSegments, urlPathOf } from './static-path';
|
|
12
13
|
|
|
14
|
+
// Re-exported where it has always been imported from; the enumeration is `prerender-enumerate.ts`'s.
|
|
15
|
+
export { enumeratePrerender } from './prerender-enumerate';
|
|
16
|
+
|
|
13
17
|
export interface StaticArtifact {
|
|
14
18
|
readonly path: string;
|
|
15
19
|
readonly params: RouteParams;
|
|
@@ -61,59 +65,6 @@ export function assertNoPerRequestState(file: string): void {
|
|
|
61
65
|
}
|
|
62
66
|
}
|
|
63
67
|
|
|
64
|
-
/** Normalize `prerender()` output. A bare string fills the route's one dynamic param. */
|
|
65
|
-
export async function enumeratePrerender(entry: RouteEntry): Promise<readonly RouteParams[]> {
|
|
66
|
-
const prerender = entry.config.prerender;
|
|
67
|
-
if (prerender === undefined) {
|
|
68
|
-
return entry.pattern.keys.length === 0 ? [{}] : [];
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
let produced: readonly (string | RouteParams)[];
|
|
72
|
-
try {
|
|
73
|
-
produced = await prerender();
|
|
74
|
-
} catch (error) {
|
|
75
|
-
throw new PrerenderFailedError(
|
|
76
|
-
// `renderThrowable`, never `.message`/`String()`: `prerender` is app code and may throw a
|
|
77
|
-
// value whose read raises in turn — this frame is what makes the build failure a coded one.
|
|
78
|
-
`prerender() for ${entry.path} threw: ${renderThrowable(error)}`,
|
|
79
|
-
`fix prerender in ${entry.file} — it runs at build time with no request context`,
|
|
80
|
-
);
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
if (!Array.isArray(produced)) {
|
|
84
|
-
throw new PrerenderFailedError(
|
|
85
|
-
`prerender() for ${entry.path} returned ${typeof produced}, expected an array`,
|
|
86
|
-
`return an array of params from prerender in ${entry.file}`,
|
|
87
|
-
);
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
const keys = entry.pattern.keys;
|
|
91
|
-
return produced.map((item: unknown) => {
|
|
92
|
-
if (typeof item !== 'string') return paramsOf(entry, item);
|
|
93
|
-
const only = keys[0];
|
|
94
|
-
if (keys.length !== 1 || only === undefined) {
|
|
95
|
-
throw new PrerenderFailedError(
|
|
96
|
-
`prerender() for ${entry.path} returned the bare string ${renderCauseValue(item)} ` +
|
|
97
|
-
`but the route has ${keys.length} dynamic params (${keys.join(', ')})`,
|
|
98
|
-
`return objects from prerender in ${entry.file}, e.g. { ${keys.join(': …, ')}: … }`,
|
|
99
|
-
);
|
|
100
|
-
}
|
|
101
|
-
return { [only]: item };
|
|
102
|
-
});
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
/**
|
|
106
|
-
* One `prerender()` item that is not a bare string: a params object, or a coded refusal. A `null`
|
|
107
|
-
* reached `Object.hasOwn` in `static-path.ts` and left the build as a bare `TypeError`.
|
|
108
|
-
*/
|
|
109
|
-
function paramsOf(entry: RouteEntry, item: unknown): RouteParams {
|
|
110
|
-
if (typeof item === 'object' && item !== null && !Array.isArray(item)) return item as RouteParams;
|
|
111
|
-
throw new PrerenderFailedError(
|
|
112
|
-
`prerender() for ${entry.path} returned ${renderCauseValue(item)} as one of its items, which is neither a params object nor a string`,
|
|
113
|
-
`return an array of params objects (or strings, for a one-param route) from prerender in ${entry.file}`,
|
|
114
|
-
);
|
|
115
|
-
}
|
|
116
|
-
|
|
117
68
|
export interface StaticBuildOptions {
|
|
118
69
|
readonly buildId: string;
|
|
119
70
|
/** Extension-less output files get `/index.html` appended. */
|
package/src/route.ts
CHANGED
|
@@ -186,10 +186,29 @@ export type RouteMetaAsyncFn<TData = RouteData> = (
|
|
|
186
186
|
ctx: RouteMetaContext<TData>,
|
|
187
187
|
) => Promise<RouteMeta>;
|
|
188
188
|
|
|
189
|
-
/**
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
189
|
+
/**
|
|
190
|
+
* One page `prerender()` answers, with what the page says about itself for the SITEMAP. The build
|
|
191
|
+
* reads `params` alone: every routed locale's document is still rendered.
|
|
192
|
+
*/
|
|
193
|
+
export interface PrerenderEntry {
|
|
194
|
+
readonly params: RouteParams;
|
|
195
|
+
/**
|
|
196
|
+
* When this page last changed — the sitemap's `<lastmod>` for it. A `Date`, or ISO 8601 with an
|
|
197
|
+
* explicit zone (`row.updatedAt`); a moment in the future is written as now.
|
|
198
|
+
*/
|
|
199
|
+
readonly lastmod?: string | Date;
|
|
200
|
+
/**
|
|
201
|
+
* The locales this page is LISTED in: one sitemap `<url>` each, cross-linked with hreflang among
|
|
202
|
+
* themselves only. Omitted: every routed locale. Empty: built, and listed nowhere.
|
|
203
|
+
*/
|
|
204
|
+
readonly locales?: readonly string[];
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/** One item of a `prerender()` answer: a bare string, a params object, or an entry. */
|
|
208
|
+
export type PrerenderItem = string | RouteParams | PrerenderEntry;
|
|
209
|
+
|
|
210
|
+
/** Returns the pages to build at deploy time. Bare strings fill a single dynamic param. */
|
|
211
|
+
export type PrerenderFn = () => readonly PrerenderItem[] | Promise<readonly PrerenderItem[]>;
|
|
193
212
|
|
|
194
213
|
/**
|
|
195
214
|
* The rule that makes the no-`load` fallback true: a route that loads nothing renders
|
package/src/server.ts
CHANGED
|
@@ -36,10 +36,10 @@ export {
|
|
|
36
36
|
stylesheetsRevision,
|
|
37
37
|
transformTsx,
|
|
38
38
|
} from './module-loader';
|
|
39
|
+
export { enumeratePrerenderEntries } from './prerender-enumerate';
|
|
39
40
|
// ---- the render pipeline: one entry point per mode ----------------------------------------------
|
|
40
41
|
export type { RenderHtmlOptions } from './render-html';
|
|
41
42
|
export { ROOT_ELEMENT_ID, renderComponent, renderToHtml } from './render-html';
|
|
42
|
-
|
|
43
43
|
export {
|
|
44
44
|
DEFAULT_ISR_REGENERATE_DEADLINE_MS,
|
|
45
45
|
invalidateAndRevalidate,
|