bosia 1.2.7 → 1.2.8
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 +1 -1
- package/src/core/cache.ts +20 -8
- package/src/core/client/loaderCache.ts +14 -3
- package/src/core/cookies.ts +15 -12
- package/src/core/dedup.ts +3 -2
- package/src/core/hooks.ts +1 -1
- package/src/core/proxy.ts +8 -7
- package/src/core/renderer.ts +41 -22
- package/src/core/routeFile.ts +0 -2
- package/src/core/routeTypes.ts +3 -2
- package/src/core/scanner.ts +0 -17
- package/src/core/server.ts +9 -12
- package/src/core/types.ts +0 -8
package/package.json
CHANGED
package/src/core/cache.ts
CHANGED
|
@@ -275,7 +275,9 @@ export function computeIdentityHash(req: Request, cookies: Pick<CookieJar, "peek
|
|
|
275
275
|
}
|
|
276
276
|
|
|
277
277
|
export function computeCacheKey(url: URL, req: Request, cookies: Pick<CookieJar, "peek">): string {
|
|
278
|
-
|
|
278
|
+
// Origin is in the key: `url` follows the Host / X-Forwarded-* headers, and a
|
|
279
|
+
// page that renders `url.origin` must not be served to another host.
|
|
280
|
+
return `${dedupKey(url)}|o=${url.origin}|i=${computeIdentityHash(req, cookies)}`;
|
|
279
281
|
}
|
|
280
282
|
|
|
281
283
|
// ─── Tracking parameters (CACHE_IGNORE_PARAMS) ───────────
|
|
@@ -316,11 +318,19 @@ export function stripIgnoredParams(url: URL): URL {
|
|
|
316
318
|
}
|
|
317
319
|
|
|
318
320
|
/** Loaders that read the whole query string (`url.search`, `url.href`, iterating
|
|
319
|
-
* `url.searchParams`): their output may depend on any param.
|
|
320
|
-
|
|
321
|
-
|
|
321
|
+
* `url.searchParams`): their output may depend on any param. Recorded as a `"*"`
|
|
322
|
+
* search param (like the `"*"` cookie) so the client router sees it too. A real
|
|
323
|
+
* `?*=` param read by name counts as a whole-query read, which only re-runs more. */
|
|
322
324
|
export function markWholeQueryRead(deps: LoaderDeps): void {
|
|
323
|
-
|
|
325
|
+
if (!deps.searchParams.includes("*")) deps.searchParams.push("*");
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
/** Server-only: the entry is query-aware (a left-out layout above read an ignored
|
|
329
|
+
* param), without telling the client router to re-run on every query change. */
|
|
330
|
+
const queryAwareDeps = new WeakSet<LoaderDeps>();
|
|
331
|
+
|
|
332
|
+
export function markQueryAware(deps: LoaderDeps): void {
|
|
333
|
+
queryAwareDeps.add(deps);
|
|
324
334
|
}
|
|
325
335
|
|
|
326
336
|
/** Whether any loader read an ignored param — its entry must not be served to a
|
|
@@ -330,16 +340,18 @@ export function depsReadIgnoredParams(
|
|
|
330
340
|
pageDeps: LoaderDeps | null,
|
|
331
341
|
): boolean {
|
|
332
342
|
const check = (d: LoaderDeps | null) =>
|
|
333
|
-
!!d && (
|
|
343
|
+
!!d && (queryAwareDeps.has(d) || d.searchParams.some((k) => k === "*" || isIgnoredParam(k)));
|
|
334
344
|
if (check(pageDeps)) return true;
|
|
335
345
|
return !!layoutDeps && layoutDeps.some(check);
|
|
336
346
|
}
|
|
337
347
|
|
|
338
348
|
/** Extract pathname portion of a cacheKey for path-based invalidation. */
|
|
339
349
|
function pathOfKey(key: string): string {
|
|
350
|
+
// A path keeps a raw `|` (`/files/a|b`) but never a raw `?`; the query,
|
|
351
|
+
// origin and 0/1 mask never hold `?` or `|o=`. Storing the path on the
|
|
352
|
+
// entry would drop this parsing if keys grow free-text parts.
|
|
340
353
|
const qIdx = key.indexOf("?");
|
|
341
|
-
const
|
|
342
|
-
const end = qIdx === -1 ? pIdx : Math.min(qIdx, pIdx);
|
|
354
|
+
const end = qIdx !== -1 ? qIdx : key.lastIndexOf("|o=");
|
|
343
355
|
return end === -1 ? key : key.slice(0, end);
|
|
344
356
|
}
|
|
345
357
|
|
|
@@ -15,6 +15,8 @@ export type CacheSnapshot = {
|
|
|
15
15
|
pathname: string;
|
|
16
16
|
params: Record<string, string>;
|
|
17
17
|
searchParams: Record<string, string | null>;
|
|
18
|
+
/** The full query, for loaders that read all of it (`"*"` in deps.searchParams). */
|
|
19
|
+
search: string;
|
|
18
20
|
cookies: Record<string, string | undefined>;
|
|
19
21
|
};
|
|
20
22
|
|
|
@@ -64,15 +66,21 @@ export function liveContext(
|
|
|
64
66
|
};
|
|
65
67
|
}
|
|
66
68
|
|
|
69
|
+
/** Every value of a query param, so `getAll`/`has(k, v)` see a change past the first. */
|
|
70
|
+
function paramValues(url: URL, k: string): string | null {
|
|
71
|
+
const all = url.searchParams.getAll(k);
|
|
72
|
+
return all.length ? JSON.stringify(all) : null;
|
|
73
|
+
}
|
|
74
|
+
|
|
67
75
|
/** Capture only the values relevant to a loader's tracked deps. */
|
|
68
76
|
export function captureSnapshot(deps: LoaderDeps, ctx: EvalContext): CacheSnapshot {
|
|
69
77
|
const params: Record<string, string> = {};
|
|
70
78
|
for (const k of deps.params) params[k] = ctx.params[k] ?? "";
|
|
71
79
|
const searchParams: Record<string, string | null> = {};
|
|
72
|
-
for (const k of deps.searchParams) searchParams[k] = ctx.url
|
|
80
|
+
for (const k of deps.searchParams) if (k !== "*") searchParams[k] = paramValues(ctx.url, k);
|
|
73
81
|
const cookies: Record<string, string | undefined> = {};
|
|
74
82
|
for (const k of deps.cookies) cookies[k] = ctx.cookies[k];
|
|
75
|
-
return { pathname: ctx.pathname, params, searchParams, cookies };
|
|
83
|
+
return { pathname: ctx.pathname, params, searchParams, search: ctx.url.search, cookies };
|
|
76
84
|
}
|
|
77
85
|
|
|
78
86
|
export type DirtyState = {
|
|
@@ -108,7 +116,10 @@ export function shouldRerun(entry: CacheEntry, dirty: DirtyState, next: EvalCont
|
|
|
108
116
|
}
|
|
109
117
|
// Search param value changes
|
|
110
118
|
for (const k of deps.searchParams) {
|
|
111
|
-
if (
|
|
119
|
+
if (k === "*") {
|
|
120
|
+
// Whole query read: any change re-runs (a reorder too, which is harmless).
|
|
121
|
+
if (snapshot.search !== next.url.search) return true;
|
|
122
|
+
} else if (snapshot.searchParams[k] !== paramValues(next.url, k)) return true;
|
|
112
123
|
}
|
|
113
124
|
// Cookie value changes
|
|
114
125
|
for (const k of deps.cookies) {
|
package/src/core/cookies.ts
CHANGED
|
@@ -6,16 +6,17 @@ import { currentBase } from "./appBase.ts";
|
|
|
6
6
|
const UNSAFE_COOKIE_VALUE = /[;\r\n]/;
|
|
7
7
|
/**
|
|
8
8
|
* Accept both casings (matches SvelteKit/Express convention) and write the
|
|
9
|
-
* canonical capitalized form into the Set-Cookie header.
|
|
9
|
+
* canonical capitalized form into the Set-Cookie header. A Map, so names like
|
|
10
|
+
* "constructor" are rejected rather than inherited.
|
|
10
11
|
*/
|
|
11
|
-
const SAMESITE_NORMALIZE
|
|
12
|
-
strict
|
|
13
|
-
lax
|
|
14
|
-
none
|
|
15
|
-
Strict
|
|
16
|
-
Lax
|
|
17
|
-
None
|
|
18
|
-
|
|
12
|
+
const SAMESITE_NORMALIZE = new Map<string, "Strict" | "Lax" | "None">([
|
|
13
|
+
["strict", "Strict"],
|
|
14
|
+
["lax", "Lax"],
|
|
15
|
+
["none", "None"],
|
|
16
|
+
["Strict", "Strict"],
|
|
17
|
+
["Lax", "Lax"],
|
|
18
|
+
["None", "None"],
|
|
19
|
+
]);
|
|
19
20
|
|
|
20
21
|
/**
|
|
21
22
|
* RFC 6265 §4.1.1: cookie-name is an HTTP token (RFC 2616 §2.2).
|
|
@@ -37,13 +38,15 @@ const COOKIE_DEFAULTS: CookieOptions = {
|
|
|
37
38
|
// ─── Cookie Helpers ──────────────────────────────────────
|
|
38
39
|
|
|
39
40
|
function parseCookies(header: string): Record<string, string> {
|
|
40
|
-
|
|
41
|
+
// No prototype, so `get("constructor")` is undefined. `__proto__` is dropped
|
|
42
|
+
// (as a plain `{}` did): kept, getAll() would hand serializers that key.
|
|
43
|
+
const result: Record<string, string> = Object.create(null);
|
|
41
44
|
for (const pair of header.split(";")) {
|
|
42
45
|
const idx = pair.indexOf("=");
|
|
43
46
|
if (idx === -1) continue;
|
|
44
47
|
const name = pair.slice(0, idx).trim();
|
|
45
48
|
const value = pair.slice(idx + 1).trim();
|
|
46
|
-
if (name) {
|
|
49
|
+
if (name && name !== "__proto__") {
|
|
47
50
|
try {
|
|
48
51
|
result[name] = decodeURIComponent(value);
|
|
49
52
|
} catch {
|
|
@@ -144,7 +147,7 @@ export class CookieJar implements Cookies {
|
|
|
144
147
|
if (opts.httpOnly) header += "; HttpOnly";
|
|
145
148
|
if (opts.secure) header += "; Secure";
|
|
146
149
|
if (opts.sameSite) {
|
|
147
|
-
const canonical = SAMESITE_NORMALIZE
|
|
150
|
+
const canonical = SAMESITE_NORMALIZE.get(opts.sameSite as string);
|
|
148
151
|
if (!canonical) throw new Error(`Invalid cookie sameSite: ${opts.sameSite}`);
|
|
149
152
|
header += `; SameSite=${canonical}`;
|
|
150
153
|
}
|
package/src/core/dedup.ts
CHANGED
|
@@ -4,8 +4,9 @@
|
|
|
4
4
|
// Settled responses are NOT cached — once the promise resolves, the entry is
|
|
5
5
|
// dropped, so the next request runs the loader again.
|
|
6
6
|
//
|
|
7
|
-
// The caller's key
|
|
8
|
-
//
|
|
7
|
+
// The caller's key (computeCacheKey in cache.ts) adds the request origin and
|
|
8
|
+
// the CACHE_KEYS identity hash, so different hosts and users never share a
|
|
9
|
+
// loader result. Custom session cookie names
|
|
9
10
|
// must be listed in CACHE_KEYS. See docs/guides/request-deduplication.md.
|
|
10
11
|
|
|
11
12
|
type Inflight = { promise: Promise<unknown>; start: number };
|
package/src/core/hooks.ts
CHANGED
|
@@ -155,7 +155,7 @@ export type LoaderDeps = {
|
|
|
155
155
|
urls: string[];
|
|
156
156
|
/** Route params the loader read (`params.X`). */
|
|
157
157
|
params: string[];
|
|
158
|
-
/** Search params the loader read (`url.searchParams.get(X)` / `.has(X)`). */
|
|
158
|
+
/** Search params the loader read (`url.searchParams.get(X)` / `.has(X)`); `"*"` = the whole query. */
|
|
159
159
|
searchParams: string[];
|
|
160
160
|
/** Cookies the loader read (`cookies.get(X)`). */
|
|
161
161
|
cookies: string[];
|
package/src/core/proxy.ts
CHANGED
|
@@ -11,13 +11,14 @@ function firstForwarded(request: Request, name: string): string | undefined {
|
|
|
11
11
|
return request.headers.get(name)?.split(",")[0]?.trim() || undefined;
|
|
12
12
|
}
|
|
13
13
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
14
|
+
// A Map, not an object: "constructor" or "__proto__" must read as unknown.
|
|
15
|
+
const SCHEMES = new Map([
|
|
16
|
+
["http", "http:"],
|
|
17
|
+
["https", "https:"],
|
|
17
18
|
// Traefik sends ws/wss on upgrades; the origin a browser sends is http(s).
|
|
18
|
-
ws
|
|
19
|
-
wss
|
|
20
|
-
|
|
19
|
+
["ws", "http:"],
|
|
20
|
+
["wss", "https:"],
|
|
21
|
+
]);
|
|
21
22
|
|
|
22
23
|
/**
|
|
23
24
|
* Point `url` at the public origin from X-Forwarded-Host/-Proto, in place, so
|
|
@@ -27,7 +28,7 @@ const SCHEMES: Record<string, string> = {
|
|
|
27
28
|
*/
|
|
28
29
|
export function applyForwarded(request: Request, url: URL, trustProxy: boolean): void {
|
|
29
30
|
if (!trustProxy) return;
|
|
30
|
-
const proto = SCHEMES
|
|
31
|
+
const proto = SCHEMES.get(firstForwarded(request, "x-forwarded-proto")?.toLowerCase() ?? "");
|
|
31
32
|
const host = firstForwarded(request, "x-forwarded-host");
|
|
32
33
|
let parsed: URL | undefined;
|
|
33
34
|
if (host) {
|
package/src/core/renderer.ts
CHANGED
|
@@ -16,6 +16,7 @@ import {
|
|
|
16
16
|
collectTags,
|
|
17
17
|
depsReadIgnoredParams,
|
|
18
18
|
markWholeQueryRead,
|
|
19
|
+
markQueryAware,
|
|
19
20
|
LOAD_TIMEOUT,
|
|
20
21
|
METADATA_TIMEOUT,
|
|
21
22
|
stripIgnoredParams,
|
|
@@ -246,6 +247,25 @@ function stampErrorContext(
|
|
|
246
247
|
e.partialLayoutData ??= [...partialLayoutData];
|
|
247
248
|
}
|
|
248
249
|
|
|
250
|
+
// A module that fails to import isn't cached; the loader path imports it
|
|
251
|
+
// again and turns the failure into the error page.
|
|
252
|
+
const uncached = () => ({ cache: false });
|
|
253
|
+
|
|
254
|
+
/** A page route's `export const cache`: `false` in either +page.svelte or
|
|
255
|
+
* +page.server.ts opts it out. Read from the real modules each time, so no
|
|
256
|
+
* export form or later `cache = false` is missed. Cheap because the generated
|
|
257
|
+
* `once()` wrapper (routeFile.ts) hands back the settled import promise. */
|
|
258
|
+
export async function routeCacheFlag(route: {
|
|
259
|
+
pageModule: () => Promise<any>;
|
|
260
|
+
pageServer: (() => Promise<any>) | null;
|
|
261
|
+
}): Promise<boolean> {
|
|
262
|
+
const [pageMod, serverMod] = await Promise.all([
|
|
263
|
+
route.pageModule().catch(uncached),
|
|
264
|
+
route.pageServer?.().catch(uncached),
|
|
265
|
+
]);
|
|
266
|
+
return pageMod.cache !== false && serverMod?.cache !== false;
|
|
267
|
+
}
|
|
268
|
+
|
|
249
269
|
// ─── Per-Loader Dependency Tracking ──────────────────────
|
|
250
270
|
// Wraps `params`, `url`, `cookies`, and `fetch` with proxies/closures
|
|
251
271
|
// that record every key read during a single loader run. The client
|
|
@@ -274,7 +294,16 @@ function trackedParams(params: Record<string, string>, deps: LoaderDeps): Record
|
|
|
274
294
|
});
|
|
275
295
|
}
|
|
276
296
|
|
|
277
|
-
const URL_TRACKED_PROPS = new Set([
|
|
297
|
+
const URL_TRACKED_PROPS = new Set([
|
|
298
|
+
"pathname",
|
|
299
|
+
"origin",
|
|
300
|
+
"hash",
|
|
301
|
+
"href",
|
|
302
|
+
"host",
|
|
303
|
+
"hostname",
|
|
304
|
+
"toString",
|
|
305
|
+
"toJSON",
|
|
306
|
+
]);
|
|
278
307
|
|
|
279
308
|
function trackedUrl(url: URL, deps: LoaderDeps): URL {
|
|
280
309
|
const trackedSearch = new Proxy(url.searchParams, {
|
|
@@ -303,6 +332,10 @@ function trackedUrl(url: URL, deps: LoaderDeps): URL {
|
|
|
303
332
|
markWholeQueryRead(deps);
|
|
304
333
|
return target.search;
|
|
305
334
|
}
|
|
335
|
+
// Known cost: `new URL("/api/x", url)` reads `toString` too, so a loader that
|
|
336
|
+
// only needs the origin re-runs on every query change. Accepted: a full-URL
|
|
337
|
+
// read can depend on the query, and stale data is worse than an extra run.
|
|
338
|
+
// Use `url.origin` in such loaders to keep them cached.
|
|
306
339
|
if (prop === "href" || prop === "toString" || prop === "toJSON") {
|
|
307
340
|
markWholeQueryRead(deps);
|
|
308
341
|
}
|
|
@@ -549,7 +582,7 @@ export async function loadRouteData(
|
|
|
549
582
|
// A left-out layout still feeds the layers below it via parent(): its
|
|
550
583
|
// ignored-param reads count for every one that is sent.
|
|
551
584
|
if (omit && depsReadIgnoredParams(null, deps)) omittedReadIgnored = true;
|
|
552
|
-
else if (!omit && omittedReadIgnored)
|
|
585
|
+
else if (!omit && omittedReadIgnored) markQueryAware(deps);
|
|
553
586
|
} catch (err) {
|
|
554
587
|
// Loaders below this one already started; dev warns about the guard.
|
|
555
588
|
const thrown = isDev ? guardLabel(err) : null;
|
|
@@ -616,7 +649,7 @@ export async function loadRouteData(
|
|
|
616
649
|
}
|
|
617
650
|
}
|
|
618
651
|
|
|
619
|
-
if (omittedReadIgnored && pageDeps)
|
|
652
|
+
if (omittedReadIgnored && pageDeps) markQueryAware(pageDeps);
|
|
620
653
|
|
|
621
654
|
// `params` are always attached to pageData for client-side router consumption.
|
|
622
655
|
// When pageData is skipped, the client merges its cached pageData with current
|
|
@@ -701,18 +734,6 @@ export async function renderSSRStream(
|
|
|
701
734
|
// from CACHE_KEYS), so per-user pages stay isolated. Routes opt out via
|
|
702
735
|
// `export const cache = false`. See cache.ts and docs/guides/response-cache.md.
|
|
703
736
|
const cacheBypass = url.searchParams.has("_invalidated");
|
|
704
|
-
// `route.cache` is the build-time static read of `export const cache` in
|
|
705
|
-
// +page.svelte, letting a cache hit skip the page-module import entirely:
|
|
706
|
-
// true → cacheable, false → opted out, null → import to read the real value.
|
|
707
|
-
let pageMod: any = null;
|
|
708
|
-
const staticCache = (route as any).cache as boolean | null | undefined;
|
|
709
|
-
let routeCacheable: boolean;
|
|
710
|
-
if (staticCache === true || staticCache === false) {
|
|
711
|
-
routeCacheable = staticCache;
|
|
712
|
-
} else {
|
|
713
|
-
pageMod = await route.pageModule();
|
|
714
|
-
routeCacheable = pageMod.cache !== false;
|
|
715
|
-
}
|
|
716
737
|
// CSP is incompatible with response cache — the per-request nonce is baked
|
|
717
738
|
// into the cached HTML but the CSP header is re-derived each request, so a
|
|
718
739
|
// cached page would ship with a dead nonce and the browser would block its
|
|
@@ -723,8 +744,8 @@ export async function renderSSRStream(
|
|
|
723
744
|
CACHE_ENABLED &&
|
|
724
745
|
!CSP_ENABLED &&
|
|
725
746
|
!warmingUp &&
|
|
726
|
-
|
|
727
|
-
(
|
|
747
|
+
(req.method === "GET" || req.method === "HEAD") &&
|
|
748
|
+
(await routeCacheFlag(route));
|
|
728
749
|
const cacheable = cacheRead && req.method === "GET";
|
|
729
750
|
let cacheKey: string | null = null;
|
|
730
751
|
let releaseMiss: (() => void) | null = null;
|
|
@@ -779,8 +800,7 @@ export async function renderSSRStream(
|
|
|
779
800
|
match,
|
|
780
801
|
),
|
|
781
802
|
Promise.all(route.layoutModules.map((l: () => Promise<any>) => l())),
|
|
782
|
-
|
|
783
|
-
pageMod ?? route.pageModule(),
|
|
803
|
+
route.pageModule(),
|
|
784
804
|
]);
|
|
785
805
|
// Settled by the early returns below without being read.
|
|
786
806
|
dataP.catch(() => {});
|
|
@@ -830,11 +850,10 @@ export async function renderSSRStream(
|
|
|
830
850
|
|
|
831
851
|
let data: Awaited<ReturnType<typeof loadRouteData>>;
|
|
832
852
|
let layoutMods: any[];
|
|
853
|
+
let pageMod: any;
|
|
833
854
|
|
|
834
855
|
try {
|
|
835
|
-
|
|
836
|
-
[data, layoutMods, pm] = await dataP;
|
|
837
|
-
pageMod = pm;
|
|
856
|
+
[data, layoutMods, pageMod] = await dataP;
|
|
838
857
|
} catch (err) {
|
|
839
858
|
if (isRedirect(err))
|
|
840
859
|
return new Response(null, { status: err.status, headers: { Location: err.location } });
|
package/src/core/routeFile.ts
CHANGED
|
@@ -129,7 +129,6 @@ export function generateRoutesFile(manifest: RouteManifest, defaultTitle: string
|
|
|
129
129
|
lines.push(" layoutServers: { loader: () => Promise<any>; depth: number }[];");
|
|
130
130
|
lines.push(" errorPages: { loader: () => Promise<any>; depth: number }[];");
|
|
131
131
|
lines.push(' trailingSlash: "never" | "always" | "ignore";');
|
|
132
|
-
lines.push(" cache: boolean | null;");
|
|
133
132
|
lines.push("}> = [");
|
|
134
133
|
for (const r of pages) {
|
|
135
134
|
const layoutImports = r.layouts.map((l) => serverImport(l)).join(", ");
|
|
@@ -148,7 +147,6 @@ export function generateRoutesFile(manifest: RouteManifest, defaultTitle: string
|
|
|
148
147
|
lines.push(` layoutServers: [${layoutServerImports}],`);
|
|
149
148
|
lines.push(` errorPages: [${errorPageImports}],`);
|
|
150
149
|
lines.push(` trailingSlash: ${JSON.stringify(r.trailingSlash)},`);
|
|
151
|
-
lines.push(` cache: ${JSON.stringify(r.cache ?? null)},`);
|
|
152
150
|
lines.push(" },");
|
|
153
151
|
}
|
|
154
152
|
lines.push("];\n");
|
package/src/core/routeTypes.ts
CHANGED
|
@@ -90,8 +90,9 @@ export function generateRouteTypes(manifest: RouteManifest): void {
|
|
|
90
90
|
lines.push(``);
|
|
91
91
|
lines.push(`export type Params = ${paramsType};`);
|
|
92
92
|
lines.push(``);
|
|
93
|
-
lines.push(`/** Set \`export const cache: CacheOption = false;\` in +page
|
|
94
|
-
lines.push(` * or +server.ts to opt the route out of
|
|
93
|
+
lines.push(`/** Set \`export const cache: CacheOption = false;\` in +page.server.ts, the`);
|
|
94
|
+
lines.push(` * <script module> of +page.svelte, or +server.ts to opt the route out of`);
|
|
95
|
+
lines.push(` * the server response cache. Layouts don't read it. */`);
|
|
95
96
|
lines.push(`export type CacheOption = false;`);
|
|
96
97
|
lines.push(``);
|
|
97
98
|
lines.push(
|
package/src/core/scanner.ts
CHANGED
|
@@ -19,21 +19,6 @@ import { segmentName, segmentRank } from "./matcher.ts";
|
|
|
19
19
|
|
|
20
20
|
const ROUTES_DIR = "./src/routes";
|
|
21
21
|
|
|
22
|
-
/**
|
|
23
|
-
* Read `export const cache` from +page.svelte's `<script module>` at scan time.
|
|
24
|
-
* Conservative on purpose — the flag lets the renderer skip caching, so a wrong
|
|
25
|
-
* "cacheable" answer would leak a per-user page. Returns `true` ONLY when there
|
|
26
|
-
* is no `cache` export at all; a literal `= false` returns `false`; anything
|
|
27
|
-
* else (dynamic expression, unreadable file) returns `null` so the renderer
|
|
28
|
-
* imports the module and reads the real value.
|
|
29
|
-
*/
|
|
30
|
-
function pageCache(src: string | null): boolean | null {
|
|
31
|
-
if (src === null) return null;
|
|
32
|
-
if (!/export\s+const\s+cache\b/.test(src)) return true;
|
|
33
|
-
if (/export\s+const\s+cache\s*(?::[^=]+)?=\s*false\b/.test(src)) return false;
|
|
34
|
-
return null;
|
|
35
|
-
}
|
|
36
|
-
|
|
37
22
|
/** Literal `export const prerender = true` in a server module's source. */
|
|
38
23
|
export function wantsPrerender(src: string): boolean {
|
|
39
24
|
return /export\s+const\s+prerender\s*=\s*true/.test(src);
|
|
@@ -154,7 +139,6 @@ export function scanRoutes(): RouteManifest {
|
|
|
154
139
|
const effectiveTs: TrailingSlash = pageTs ?? currentTrailingSlash;
|
|
155
140
|
|
|
156
141
|
const pageFile = rel("+page.svelte");
|
|
157
|
-
const pageSrc = readSource(join(ROUTES_DIR, pageFile));
|
|
158
142
|
pages.push({
|
|
159
143
|
pattern: toUrlPath(urlSegments),
|
|
160
144
|
id: `/${dir}`,
|
|
@@ -165,7 +149,6 @@ export function scanRoutes(): RouteManifest {
|
|
|
165
149
|
layoutServers: [...currentLayoutServers],
|
|
166
150
|
errorPages: [...currentErrorPages],
|
|
167
151
|
trailingSlash: effectiveTs,
|
|
168
|
-
cache: pageCache(pageSrc),
|
|
169
152
|
prerender: prerenderOf(pageServerSrc),
|
|
170
153
|
});
|
|
171
154
|
}
|
package/src/core/server.ts
CHANGED
|
@@ -73,6 +73,7 @@ import {
|
|
|
73
73
|
renderSSRStream,
|
|
74
74
|
renderErrorPage,
|
|
75
75
|
renderPageWithFormData,
|
|
76
|
+
routeCacheFlag,
|
|
76
77
|
} from "./renderer.ts";
|
|
77
78
|
import { getServerTime } from "../lib/utils.ts";
|
|
78
79
|
import {
|
|
@@ -223,7 +224,7 @@ function parseDataRequest(url: URL): DataRequest | "invalid" | null {
|
|
|
223
224
|
let fresh = false;
|
|
224
225
|
for (const [key, val] of url.searchParams.entries()) {
|
|
225
226
|
if (key === "_invalidated") {
|
|
226
|
-
invalidatedBits = val;
|
|
227
|
+
invalidatedBits = val; // last wins: the client appends its mask after the page's query
|
|
227
228
|
continue;
|
|
228
229
|
}
|
|
229
230
|
// Set by the client router when the fetch follows an invalidate():
|
|
@@ -234,6 +235,11 @@ function parseDataRequest(url: URL): DataRequest | "invalid" | null {
|
|
|
234
235
|
}
|
|
235
236
|
routeUrl.searchParams.append(key, val);
|
|
236
237
|
}
|
|
238
|
+
// Only a 0/1 mask counts: it goes into the cache key, where a stray `?` or
|
|
239
|
+
// `|o=` would file the entry under the wrong path for invalidate(). Anything
|
|
240
|
+
// else runs and sends every loader, as having no mask does. A page's own 0/1
|
|
241
|
+
// `?_invalidated=` with no client mask after it is still read as the mask.
|
|
242
|
+
if (invalidatedBits !== null && !/^[01]*$/.test(invalidatedBits)) invalidatedBits = null;
|
|
237
243
|
return { routeUrl, invalidatedBits, fresh };
|
|
238
244
|
}
|
|
239
245
|
|
|
@@ -291,17 +297,9 @@ function routesFor(
|
|
|
291
297
|
|
|
292
298
|
const jsonEncoder = new TextEncoder();
|
|
293
299
|
|
|
294
|
-
/** A page route's `export const cache` flag: the build-time read when it has
|
|
295
|
-
* one, otherwise the compiled page module's own export (see renderer.ts). */
|
|
296
|
-
async function pageRouteCacheable(route: (typeof serverRoutes)[number]): Promise<boolean> {
|
|
297
|
-
const flag = (route as { cache?: boolean | null }).cache;
|
|
298
|
-
if (flag === true || flag === false) return flag;
|
|
299
|
-
return (await route.pageModule()).cache !== false;
|
|
300
|
-
}
|
|
301
|
-
|
|
302
300
|
/**
|
|
303
301
|
* Decode an `_invalidated` bitmask string. Char 0 = page, char i+1 = layout
|
|
304
|
-
* depth i. Only
|
|
302
|
+
* depth i. Only 0/1 reach here: parseDataRequest drops any other mask.
|
|
305
303
|
* - Page '0': skip the page loader.
|
|
306
304
|
* - Layout '0': omit its data from the response. The loader still runs,
|
|
307
305
|
* since it may be a guard.
|
|
@@ -488,7 +486,7 @@ async function resolve(event: RequestEvent): Promise<Response> {
|
|
|
488
486
|
!warmingUp &&
|
|
489
487
|
method === "GET" &&
|
|
490
488
|
pageMatch !== null &&
|
|
491
|
-
(await
|
|
489
|
+
(await routeCacheFlag(pageMatch.route));
|
|
492
490
|
if (dataCacheable && !fresh) {
|
|
493
491
|
// Tracking params (CACHE_IGNORE_PARAMS): the plain URL's entry
|
|
494
492
|
// first, then this URL's own, as before — see cache.ts.
|
|
@@ -1560,7 +1558,6 @@ function loadBuiltManifest(): RouteManifest {
|
|
|
1560
1558
|
layoutServers: [],
|
|
1561
1559
|
errorPages: [],
|
|
1562
1560
|
trailingSlash: r.trailingSlash,
|
|
1563
|
-
cache: r.cache ?? null,
|
|
1564
1561
|
prerender: false,
|
|
1565
1562
|
})),
|
|
1566
1563
|
apis: apiRoutes.map((r: any) => ({ pattern: r.pattern, id: r.id, server: "" })),
|
package/src/core/types.ts
CHANGED
|
@@ -28,14 +28,6 @@ export interface PageRoute {
|
|
|
28
28
|
errorPages: { path: string; depth: number }[];
|
|
29
29
|
/** Effective trailing-slash mode (page wins over layout chain). Defaults to "never". */
|
|
30
30
|
trailingSlash: TrailingSlash;
|
|
31
|
-
/**
|
|
32
|
-
* Build-time read of `export const cache` in +page.svelte, so the renderer can
|
|
33
|
-
* decide cacheability on a cache hit without importing the page module:
|
|
34
|
-
* `true` — no `cache` export → cacheable
|
|
35
|
-
* `false` — statically `cache = false` → opted out
|
|
36
|
-
* `null` — a `cache` export exists but isn't a literal `false` (dynamic) → import at runtime
|
|
37
|
-
*/
|
|
38
|
-
cache: boolean | null;
|
|
39
31
|
/** `export const prerender = true` in +page.server.ts — its data is a static file at runtime. */
|
|
40
32
|
prerender: boolean;
|
|
41
33
|
}
|