@ultimat3/render 22.13.0 → 22.15.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ultimat3/render",
3
- "version": "22.13.0",
3
+ "version": "22.15.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",
@@ -38,11 +38,11 @@
38
38
  "test": "bun test"
39
39
  },
40
40
  "dependencies": {
41
- "@ultimat3/cache": "22.13.0",
42
- "@ultimat3/core": "22.13.0",
43
- "@ultimat3/http": "22.13.0",
44
- "@ultimat3/i18n": "22.13.0",
45
- "@ultimat3/seo": "22.13.0",
41
+ "@ultimat3/cache": "22.15.0",
42
+ "@ultimat3/core": "22.15.0",
43
+ "@ultimat3/http": "22.15.0",
44
+ "@ultimat3/i18n": "22.15.0",
45
+ "@ultimat3/seo": "22.15.0",
46
46
  "sass": "1.104.0"
47
47
  }
48
48
  }
package/src/index.ts CHANGED
@@ -200,6 +200,13 @@ export type { RouteComponent } from './route-component';
200
200
  export { pageComponentOf } from './route-component';
201
201
  export { metaContextFor, routeDataFor } from './route-data';
202
202
  export { routeStatusOf, withStatus } from './route-status';
203
+ export type { SpeculationRules } from './speculation-rules';
204
+ export {
205
+ SPECULATION_RULES_TYPE,
206
+ speculationPattern,
207
+ speculationRulesBody,
208
+ speculationRulesTag,
209
+ } from './speculation-rules';
203
210
  export { STREAM_REVEAL_BODIES } from './stream-scripts';
204
211
  export type {
205
212
  BoundaryRule,
@@ -0,0 +1,76 @@
1
+ /**
2
+ * Speculation Rules for a document that carries NO client router: the browser fetches a link's
3
+ * document before the click, so a full-page navigation paints from memory — at zero JavaScript.
4
+ * Pure functions over patterns; which pages are candidates is the caller's (`@ultimat3/cli`).
5
+ * PREFETCH only: a prerender would run the next page's scripts for a page nobody opened.
6
+ */
7
+
8
+ import type { HeadTag } from './head';
9
+
10
+ /** The `<script type>` a browser reads as rules and never executes. */
11
+ export const SPECULATION_RULES_TYPE = 'speculationrules';
12
+
13
+ export interface SpeculationRules {
14
+ /** `moderate`: pointer rest or pointer down. `conservative`: pointer down only. */
15
+ readonly eagerness: 'moderate' | 'conservative';
16
+ /** URL patterns of the documents that may be fetched early. Empty: no rules at all. */
17
+ readonly include: readonly string[];
18
+ /** URL patterns removed from `include` — the app's own `navigation.speculation.exclude`. */
19
+ readonly exclude: readonly string[];
20
+ }
21
+
22
+ /** Characters with a meaning in a URL pattern's pathname, escaped in a literal segment. */
23
+ const PATTERN_SYNTAX = /[\\{}()*+?:]/g;
24
+
25
+ /** A name a URL pattern accepts after `:` — anything else is matched as an unnamed segment. */
26
+ const GROUP_NAME = /^[A-Za-z_][A-Za-z0-9_]*$/;
27
+
28
+ function patternSegment(segment: string): string {
29
+ if (segment.startsWith('*')) return '*';
30
+ if (segment.startsWith(':')) return GROUP_NAME.test(segment.slice(1)) ? segment : '([^/]+)';
31
+ return segment.replace(PATTERN_SYNTAX, (char) => `\\${char}`);
32
+ }
33
+
34
+ /**
35
+ * A route-table path as the URL pattern that matches it in every routed locale: `/blog/:slug` with
36
+ * `['en']` → `{/en}?/blog/:slug`. `localeSegments` are the NON-default locales' URL segments; the
37
+ * default locale is the unprefixed path. A catch-all (`/docs/*path`) becomes a wildcard.
38
+ */
39
+ export function speculationPattern(path: string, localeSegments: readonly string[] = []): string {
40
+ const pattern = path.split('/').map(patternSegment).join('/');
41
+ if (localeSegments.length === 0) return pattern;
42
+ const [only] = localeSegments;
43
+ const group = localeSegments.length === 1 ? only : `(${localeSegments.join('|')})`;
44
+ return `{/${group}}?${pattern}`;
45
+ }
46
+
47
+ /**
48
+ * The exact text between the tags, or `undefined` when nothing may be fetched early. Sorted and
49
+ * deduped, so one route table is one string — the CSP source is hashed from THIS string, and a body
50
+ * that moved with registration order would be a policy that admits yesterday's document. `<` is
51
+ * written `<` (the same character to a JSON parser), so no pattern can close the element and
52
+ * `renderHead`'s raw-text escaper has nothing left to rewrite.
53
+ */
54
+ export function speculationRulesBody(rules: SpeculationRules): string | undefined {
55
+ const sorted = (patterns: readonly string[]): readonly string[] => [...new Set(patterns)].sort();
56
+ const include = sorted(rules.include);
57
+ if (include.length === 0) return undefined;
58
+ const exclude = sorted(rules.exclude);
59
+ const matches = { href_matches: include };
60
+ const where =
61
+ exclude.length === 0 ? matches : { and: [matches, { not: { href_matches: exclude } }] };
62
+ return JSON.stringify({ prefetch: [{ where, eagerness: rules.eagerness }] }).replaceAll(
63
+ '<',
64
+ '\\u003c',
65
+ );
66
+ }
67
+
68
+ /** The tag for a body `speculationRulesBody` wrote. No attribute but `type`: the hash is the body's. */
69
+ export function speculationRulesTag(body: string): HeadTag {
70
+ return {
71
+ kind: 'script',
72
+ key: 'script:speculation-rules',
73
+ attrs: { type: SPECULATION_RULES_TYPE },
74
+ content: body,
75
+ };
76
+ }