@ultimat3/render 27.8.0 → 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/CLAUDE.md +1 -1
- package/README.md +9 -1
- package/package.json +6 -6
- package/src/index.ts +2 -0
- package/src/prerender-enumerate.ts +108 -0
- package/src/render-isr-key.ts +9 -1
- package/src/render-isr.ts +25 -2
- package/src/render-static.ts +5 -54
- package/src/route.ts +23 -4
- package/src/server.ts +2 -1
package/CLAUDE.md
CHANGED
|
@@ -77,7 +77,7 @@ axiom 6). Never `cli` (upward).
|
|
|
77
77
|
| "Is this a TTL?" has one reader | `parseTtlMs` in `duration.ts`, below `modes.ts` and `render-isr.ts`. |
|
|
78
78
|
| A build-time frame reads a throw with `renderThrowable` | `render-html.ts`, `render-static.ts`, `css-modules.ts`, `module-loader.ts`, `route-data.ts`. `bun run error-render` cannot see a value laundered through a local helper. |
|
|
79
79
|
| `X_ROUTE_LOAD_FAILED` computes its pathname BEFORE the try | `pathnameOf` never throws on a relative `ctx.url`. |
|
|
80
|
-
| The ISR key | `isrKey(url, locale)` —
|
|
80
|
+
| The ISR key | `isrKey(url, locale)` — the path in http's `canonicalPath` spelling (no base path), the locale in the reserved `__x_locale` parameter, the sorted query. A key no route owns is never stored. The locale is REQUIRED. The time zone is deliberately not a dimension. `toResult` emits `vary: accept-language`, and `surrogate-key`/`cache-tag` from `@ultimat3/cache`'s `surrogateKeys()` over the route's `revalidateTags` — kept only while the response stays shared: http's `applyCacheHeaders` strips both on a rewrite to `private`/`no-store`. `registry.ts`'s `assertPurgeableTags` screens the tags at registration (`X_ROUTE_MODE_INVALID`); the rest of the shared key is `@ultimat3/http`'s `cache-headers` stage. File and mounted routes are both screened |
|
|
81
81
|
| A bust that lands MID-render | fenced with `@ultimat3/cache`'s `sampleFence({ key, tags })`, taken before `render()` and asked before `store.set`. `registerPath` runs BEFORE the render. |
|
|
82
82
|
| ISR regeneration | `@ultimat3/core`'s `singleFlight`, `deadlineMs` = `regenerateDeadlineMs` (`DEFAULT_ISR_REGENERATE_DEADLINE_MS`, 30 s; `finiteCount` ≥ 1, no opt-out), `schedule` injectable. Past it the PATH is free; the hung render runs on, and `latest` (by identity) drops its result once a newer run started. `render-isr-flight.test.ts`, on a hand-fired scheduler. |
|
|
83
83
|
| Which route a stored ISR path is | exact path first, else the MOST SPECIFIC pattern: `route-pattern.ts`'s `compilePattern` (never a second compiler), sorted on `CompiledPattern.specificity`. Never table order: `describePages()` sorts `*` before `:`. |
|
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
|
|
@@ -557,7 +565,7 @@ a job boundary the class is gone and the `code` is what survives — match on th
|
|
|
557
565
|
| `checkSurfaceBoundary`, `assertSurfaceBoundary`, `surfaceOf` | the hard boundary |
|
|
558
566
|
| `renderStatic`†, `enumeratePrerender`† | build-time render, content hashing |
|
|
559
567
|
| `isrController`†, `invalidateAndRevalidate`† | SWR + single-flight + tag triggers |
|
|
560
|
-
| `isrKey`†, `isrRequestUrl`†, `undeclaredQuery`† | the one derivation of an `isr` store key,
|
|
568
|
+
| `isrKey`†, `isrRequestUrl`†, `undeclaredQuery`†, `isrRouteFor`† | the one derivation of an `isr` store key — the path in `@ultimat3/http`'s `canonicalPath` spelling, so every spelling the router answers with a page is that page's one entry — the request URL narrowed to `revalidate.query`, and the route a key reads back as (a key no route owns is never stored) |
|
|
561
569
|
| `noStore`, `routeNoStoreOf` | a loader's "do not keep this answer" — a mark on the data, like `withStatus`; read by `isr` |
|
|
562
570
|
| `renderSsr`†, `streamResult`† | the per-request modes |
|
|
563
571
|
| `renderToHtml`†, `renderComponent`†, `stylesFor`† | the server JSX writer and the surface's css |
|
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-isr-key.ts
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
* the controller, which only ever holds the key this file derived.
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
+
import { canonicalPath } from '@ultimat3/http';
|
|
7
8
|
import { unlocalizedPath } from '@ultimat3/i18n';
|
|
8
9
|
|
|
9
10
|
/**
|
|
@@ -57,6 +58,13 @@ export function undeclaredQuery(url: URL, query: readonly string[] | null): read
|
|
|
57
58
|
* The ISR store key for one URL: pathname, the negotiated LOCALE, and the query, **params
|
|
58
59
|
* sorted**. The URL is `isrRequestUrl`'s — already narrowed to the route's declared parameters.
|
|
59
60
|
*
|
|
61
|
+
* The pathname is keyed in its ONE spelling — `@ultimat3/http`'s `canonicalPath`, built from the
|
|
62
|
+
* functions the router segments and decodes a request with, never a second normaliser. The router
|
|
63
|
+
* answers `/blog//a/`, `/blog/%61` and `/blog/a` with one page; keyed raw, each was an entry of
|
|
64
|
+
* its own (a 20-letter slug has a million percent-spellings, and the store holds 1,000), and one
|
|
65
|
+
* the route table could not read back (`/blog//a`) was stored with no ttl and no tags, which no
|
|
66
|
+
* bust reached: a withdrawn page stayed up at that spelling for the life of the process.
|
|
67
|
+
*
|
|
60
68
|
* Exported because deriving it is the caller's job and there may only be ONE derivation — a
|
|
61
69
|
* server that keyed on `url.pathname` while the store believed it held a whole URL is the shape
|
|
62
70
|
* of #171. Sorting makes `?a=1&b=2` and `?b=2&a=1` one entry rather than two renders of one page.
|
|
@@ -72,7 +80,7 @@ export function isrKey(url: URL, locale: string): string {
|
|
|
72
80
|
const params = new URLSearchParams(url.search);
|
|
73
81
|
params.set(ISR_LOCALE_PARAM, locale);
|
|
74
82
|
params.sort();
|
|
75
|
-
return `${url.pathname}?${params.toString()}`;
|
|
83
|
+
return `${canonicalPath(url.pathname)}?${params.toString()}`;
|
|
76
84
|
}
|
|
77
85
|
|
|
78
86
|
/**
|
package/src/render-isr.ts
CHANGED
|
@@ -108,6 +108,24 @@ export function isrController(options: IsrControllerOptions = {}): IsrController
|
|
|
108
108
|
|
|
109
109
|
const descriptorFor = (key: string): RouteDescriptor | undefined => isrRouteFor(routes(), key);
|
|
110
110
|
|
|
111
|
+
/**
|
|
112
|
+
* A key NO route owns is rendered for the request that asked and never stored. Its ttl and tags
|
|
113
|
+
* are its route's, so stored it had neither: fresh for ever, on no tag edge, and found by no
|
|
114
|
+
* bust — the one page a purge could not take down. `@ultimat3/cli` keys every request on the
|
|
115
|
+
* path the router matched, so this is a caller that derived a key some other way. Told once.
|
|
116
|
+
*/
|
|
117
|
+
let toldUnowned = false;
|
|
118
|
+
function tellUnowned(path: string): void {
|
|
119
|
+
if (toldUnowned) return;
|
|
120
|
+
toldUnowned = true;
|
|
121
|
+
logger.warn('isr.key.unowned', {
|
|
122
|
+
path,
|
|
123
|
+
cause:
|
|
124
|
+
'no route in the table matches this key, so it has no ttl and no tags to be revalidated by — it was rendered and not stored',
|
|
125
|
+
fix: 'isrKey(url, locale) // key on the URL the router matched, base path removed; never on a hand-built string',
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
|
|
111
129
|
/**
|
|
112
130
|
* A rendered page joins the invalidation graph under its route's tags, so `/blog/a` and
|
|
113
131
|
* `/blog/b` are separately addressable and a `tag.post` bust does not touch `/team`.
|
|
@@ -192,7 +210,8 @@ export function isrController(options: IsrControllerOptions = {}): IsrController
|
|
|
192
210
|
status: finiteStatus('IsrRenderFn', rendered.status),
|
|
193
211
|
};
|
|
194
212
|
const current = latest.get(path) === run;
|
|
195
|
-
const unstorable =
|
|
213
|
+
const unstorable =
|
|
214
|
+
descriptor === undefined || isServerError(rendered.status) || rendered.noStore === true;
|
|
196
215
|
// Kept only when nothing invalidated the page while it rendered — in this process (the
|
|
197
216
|
// fence) or in any other (the store's) — and no newer run started. Refused, never
|
|
198
217
|
// published stale-flagged: the next request re-renders from rows that include the write.
|
|
@@ -203,6 +222,8 @@ export function isrController(options: IsrControllerOptions = {}): IsrController
|
|
|
203
222
|
// store holds stays (still stale, so the next request tries again); a miss has none, and
|
|
204
223
|
// answers this 5xx to its own request only.
|
|
205
224
|
logger.warn('isr.render.unstored', { path, status: String(rendered.status) });
|
|
225
|
+
} else if (descriptor === undefined) {
|
|
226
|
+
tellUnowned(path);
|
|
206
227
|
} else if (rendered.noStore === true) {
|
|
207
228
|
// The loader's answer is not a failure, so the copy held is no longer the page.
|
|
208
229
|
if (current) store.delete(path);
|
|
@@ -331,7 +352,9 @@ export function isrController(options: IsrControllerOptions = {}): IsrController
|
|
|
331
352
|
ageMs: Math.max(0, now() - entry.generatedAt),
|
|
332
353
|
});
|
|
333
354
|
let cached = store.get(path);
|
|
334
|
-
|
|
355
|
+
// A copy under a key no route owns (a shared store an earlier release wrote) is not served:
|
|
356
|
+
// nothing could have invalidated it since.
|
|
357
|
+
if (cached !== undefined && (descriptor === undefined || pastMaxStale(cached, policy))) {
|
|
335
358
|
store.delete(path);
|
|
336
359
|
cached = undefined;
|
|
337
360
|
}
|
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,
|
|
@@ -47,6 +47,7 @@ export {
|
|
|
47
47
|
} from './render-isr';
|
|
48
48
|
export { DEFAULT_ISR_FAILURE_COOLDOWN_MS } from './render-isr-cooldown';
|
|
49
49
|
export { ISR_LOCALE_PARAM, isrKey, isrRequestUrl, undeclaredQuery } from './render-isr-key';
|
|
50
|
+
export { isrRouteFor } from './render-isr-routes';
|
|
50
51
|
export type {
|
|
51
52
|
IsrEntry,
|
|
52
53
|
IsrState,
|