@pracht/core 0.3.0 → 0.5.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 +2 -2
- package/dist/index.d.mts +22 -9
- package/dist/index.mjs +328 -146
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -28,8 +28,8 @@ Route modules may export the page as a function default export or as a named
|
|
|
28
28
|
`handlePrachtRequest()` sanitizes unexpected 5xx errors by default so raw server
|
|
29
29
|
messages do not leak into SSR HTML or route-state JSON. Explicit
|
|
30
30
|
`PrachtHttpError` 4xx messages are preserved. Pass `debugErrors: true` to expose
|
|
31
|
-
raw details intentionally during debugging;
|
|
32
|
-
|
|
31
|
+
raw details intentionally during debugging; the flag is ignored when
|
|
32
|
+
`NODE_ENV=production`. Debug responses also attach `error.diagnostics`
|
|
33
33
|
metadata for the failure phase and matched framework files when available.
|
|
34
34
|
|
|
35
35
|
### Client
|
package/dist/index.d.mts
CHANGED
|
@@ -78,10 +78,11 @@ interface ApiConfig {
|
|
|
78
78
|
middleware?: string[];
|
|
79
79
|
/**
|
|
80
80
|
* When `true` (the default), state-changing API requests
|
|
81
|
-
* (POST/PUT/PATCH/DELETE) are rejected unless the browser signals
|
|
82
|
-
* same-origin
|
|
83
|
-
* matches the request URL's origin.
|
|
84
|
-
*
|
|
81
|
+
* (POST/PUT/PATCH/DELETE) are rejected unless the browser signals an
|
|
82
|
+
* exact same-origin fetch (`Sec-Fetch-Site: same-origin`) or the request
|
|
83
|
+
* Origin/Referer matches the request URL's origin. `same-site` is not
|
|
84
|
+
* accepted by default because sibling subdomains can be attacker-controlled.
|
|
85
|
+
* Set to `false` to opt out if you build your own CSRF protection into middleware.
|
|
85
86
|
*/
|
|
86
87
|
requireSameOrigin?: boolean;
|
|
87
88
|
}
|
|
@@ -123,7 +124,7 @@ interface ParamRouteSegment {
|
|
|
123
124
|
}
|
|
124
125
|
interface CatchAllRouteSegment {
|
|
125
126
|
type: "catchall";
|
|
126
|
-
name:
|
|
127
|
+
name: string;
|
|
127
128
|
}
|
|
128
129
|
type RouteSegment = StaticRouteSegment | ParamRouteSegment | CatchAllRouteSegment;
|
|
129
130
|
interface ResolvedRoute extends Omit<RouteMeta, "middleware"> {
|
|
@@ -155,11 +156,16 @@ interface BaseRouteArgs<TContext = RegisteredContext> {
|
|
|
155
156
|
}
|
|
156
157
|
interface LoaderArgs<TContext = RegisteredContext> extends BaseRouteArgs<TContext> {}
|
|
157
158
|
interface MiddlewareArgs<TContext = RegisteredContext> extends BaseRouteArgs<TContext> {}
|
|
159
|
+
type HeadAttributes = Record<string, string | undefined>;
|
|
160
|
+
interface HeadScriptDescriptor extends HeadAttributes {
|
|
161
|
+
children?: string;
|
|
162
|
+
}
|
|
158
163
|
interface HeadMetadata {
|
|
159
164
|
title?: string;
|
|
160
165
|
lang?: string;
|
|
161
|
-
meta?:
|
|
162
|
-
link?:
|
|
166
|
+
meta?: HeadAttributes[];
|
|
167
|
+
link?: HeadAttributes[];
|
|
168
|
+
script?: HeadScriptDescriptor[];
|
|
163
169
|
}
|
|
164
170
|
type MaybePromise<T> = T | Promise<T>;
|
|
165
171
|
type LoaderLike = ((args: LoaderArgs<any>) => unknown) | undefined;
|
|
@@ -175,7 +181,10 @@ interface RouteComponentProps<TLoader extends LoaderLike = undefined> {
|
|
|
175
181
|
params: RouteParams;
|
|
176
182
|
}
|
|
177
183
|
interface ErrorBoundaryProps {
|
|
178
|
-
error: Error
|
|
184
|
+
error: Error & {
|
|
185
|
+
diagnostics?: unknown;
|
|
186
|
+
status?: number;
|
|
187
|
+
};
|
|
179
188
|
}
|
|
180
189
|
interface ShellProps {
|
|
181
190
|
children: ComponentChildren;
|
|
@@ -237,6 +246,7 @@ declare function buildPathFromSegments(segments: RouteSegment[], params: RoutePa
|
|
|
237
246
|
*
|
|
238
247
|
* Example: `"/src/api/health.ts"` → path `/api/health`
|
|
239
248
|
* `"/src/api/users/[id].ts"` → path `/api/users/:id`
|
|
249
|
+
* `"/src/api/files/[...path].ts"` → path `/api/files/*`
|
|
240
250
|
* `"/src/api/index.ts"` → path `/api`
|
|
241
251
|
*/
|
|
242
252
|
declare function resolveApiRoutes(files: string[], apiDir?: string): ResolvedApiRoute[];
|
|
@@ -338,6 +348,7 @@ declare function PrachtRuntimeProvider<TData>({
|
|
|
338
348
|
}>;
|
|
339
349
|
declare function startApp<TData = unknown>(options?: StartAppOptions<TData>): TData | undefined;
|
|
340
350
|
declare function readHydrationState<TData = unknown>(): PrachtHydrationState<TData> | undefined;
|
|
351
|
+
declare function useRouteData<TLoader extends LoaderLike>(): LoaderData<TLoader>;
|
|
341
352
|
declare function useRouteData<TData = unknown>(): TData;
|
|
342
353
|
declare function useLocation(): Location;
|
|
343
354
|
declare function useParams(): RouteParams;
|
|
@@ -394,6 +405,8 @@ interface PrerenderAppOptions {
|
|
|
394
405
|
cssManifest?: Record<string, string[]>;
|
|
395
406
|
/** Per-source-file JS map produced by the vite plugin for modulepreload hints. */
|
|
396
407
|
jsManifest?: Record<string, string[]>;
|
|
408
|
+
/** Maximum number of pages rendered concurrently. Defaults to 10. */
|
|
409
|
+
concurrency?: number;
|
|
397
410
|
}
|
|
398
411
|
declare function prerenderApp(options: PrerenderAppOptions): Promise<PrerenderResult[]>;
|
|
399
412
|
declare function prerenderApp(options: PrerenderAppOptions & {
|
|
@@ -422,4 +435,4 @@ interface InitClientRouterOptions {
|
|
|
422
435
|
}
|
|
423
436
|
declare function initClientRouter(options: InitClientRouterOptions): Promise<void>;
|
|
424
437
|
//#endregion
|
|
425
|
-
export { type ApiConfig, type ApiRouteArgs, type ApiRouteHandler, type ApiRouteMatch, type ApiRouteModule, type BaseRouteArgs, type DataModule, type ErrorBoundaryProps, Form, type FormProps, type GroupDefinition, type GroupMeta, type HandlePrachtRequestOptions, type HeadArgs, type HeadMetadata, type HeadersArgs, type HttpMethod, type ISGManifestEntry, type InitClientRouterOptions, type LoaderArgs, type LoaderData, type LoaderFn, type Location, type MiddlewareArgs, type MiddlewareFn, type MiddlewareModule, type MiddlewareResult, type ModuleImporter, type ModuleRef, type ModuleRegistry, type NavigateFn, type PrachtApp, type PrachtAppConfig, PrachtHttpError, type PrachtHydrationState, type PrachtRuntimeDiagnosticPhase, type PrachtRuntimeDiagnostics, PrachtRuntimeProvider, type PrefetchStrategy, type PrerenderAppOptions, type PrerenderAppResult, type PrerenderResult, type Register, type RenderMode, type ResolvedApiRoute, type ResolvedPrachtApp, type ResolvedRoute, type RouteComponentProps, type RouteConfig, type RouteDefinition, type RouteMatch, type RouteMeta, type RouteModule, type RouteParams, type RouteRevalidate, type RouteStateResult, type RouteTreeNode, type SerializedRouteError, type ShellModule, type ShellProps, type StartAppOptions, Suspense, type TimeRevalidatePolicy, applyDefaultSecurityHeaders, buildPathFromSegments, defineApp, forwardRef, group, handlePrachtRequest, initClientRouter, lazy, matchApiRoute, matchAppRoute, prerenderApp, readHydrationState, resolveApiRoutes, resolveApp, route, startApp, timeRevalidate, useIsHydrated, useLocation, useNavigate, useParams, useRevalidate, useRouteData };
|
|
438
|
+
export { type ApiConfig, type ApiRouteArgs, type ApiRouteHandler, type ApiRouteMatch, type ApiRouteModule, type BaseRouteArgs, type DataModule, type ErrorBoundaryProps, Form, type FormProps, type GroupDefinition, type GroupMeta, type HandlePrachtRequestOptions, type HeadArgs, type HeadAttributes, type HeadMetadata, type HeadScriptDescriptor, type HeadersArgs, type HttpMethod, type ISGManifestEntry, type InitClientRouterOptions, type LoaderArgs, type LoaderData, type LoaderFn, type Location, type MiddlewareArgs, type MiddlewareFn, type MiddlewareModule, type MiddlewareResult, type ModuleImporter, type ModuleRef, type ModuleRegistry, type NavigateFn, type PrachtApp, type PrachtAppConfig, PrachtHttpError, type PrachtHydrationState, type PrachtRuntimeDiagnosticPhase, type PrachtRuntimeDiagnostics, PrachtRuntimeProvider, type PrefetchStrategy, type PrerenderAppOptions, type PrerenderAppResult, type PrerenderResult, type Register, type RenderMode, type ResolvedApiRoute, type ResolvedPrachtApp, type ResolvedRoute, type RouteComponentProps, type RouteConfig, type RouteDefinition, type RouteMatch, type RouteMeta, type RouteModule, type RouteParams, type RouteRevalidate, type RouteStateResult, type RouteTreeNode, type SerializedRouteError, type ShellModule, type ShellProps, type StartAppOptions, Suspense, type TimeRevalidatePolicy, applyDefaultSecurityHeaders, buildPathFromSegments, defineApp, forwardRef, group, handlePrachtRequest, initClientRouter, lazy, matchApiRoute, matchAppRoute, prerenderApp, readHydrationState, resolveApiRoutes, resolveApp, route, startApp, timeRevalidate, useIsHydrated, useLocation, useNavigate, useParams, useRevalidate, useRouteData };
|
package/dist/index.mjs
CHANGED
|
@@ -119,7 +119,11 @@ function matchRouteSegments(routeSegments, targetSegments) {
|
|
|
119
119
|
while (routeIndex < routeSegments.length) {
|
|
120
120
|
const currentSegment = routeSegments[routeIndex];
|
|
121
121
|
if (currentSegment.type === "catchall") {
|
|
122
|
-
|
|
122
|
+
try {
|
|
123
|
+
params[currentSegment.name] = targetSegments.slice(targetIndex).map(decodeURIComponent).join("/");
|
|
124
|
+
} catch {
|
|
125
|
+
return null;
|
|
126
|
+
}
|
|
123
127
|
return params;
|
|
124
128
|
}
|
|
125
129
|
const targetSegment = targetSegments[targetIndex];
|
|
@@ -142,6 +146,10 @@ function parseRouteSegments(path) {
|
|
|
142
146
|
type: "catchall",
|
|
143
147
|
name: "*"
|
|
144
148
|
};
|
|
149
|
+
if (segment.startsWith(":") && segment.endsWith("*")) return {
|
|
150
|
+
type: "catchall",
|
|
151
|
+
name: segment.slice(1, -1) || "*"
|
|
152
|
+
};
|
|
145
153
|
if (segment.startsWith(":")) return {
|
|
146
154
|
type: "param",
|
|
147
155
|
name: segment.slice(1)
|
|
@@ -172,7 +180,7 @@ function buildPathFromSegments(segments, params) {
|
|
|
172
180
|
return normalizeRoutePath("/" + segments.map((segment) => {
|
|
173
181
|
if (segment.type === "static") return segment.value;
|
|
174
182
|
if (segment.type === "param") return encodeURIComponent(params[segment.name] ?? "");
|
|
175
|
-
return (params["*"] ?? "").split("/").map((part) => encodeCatchAllSegment(part)).join("/");
|
|
183
|
+
return (params[segment.name] ?? params["*"] ?? "").split("/").map((part) => encodeCatchAllSegment(part)).join("/");
|
|
176
184
|
}).join("/"));
|
|
177
185
|
}
|
|
178
186
|
/**
|
|
@@ -191,6 +199,7 @@ function encodeCatchAllSegment(part) {
|
|
|
191
199
|
*
|
|
192
200
|
* Example: `"/src/api/health.ts"` → path `/api/health`
|
|
193
201
|
* `"/src/api/users/[id].ts"` → path `/api/users/:id`
|
|
202
|
+
* `"/src/api/files/[...path].ts"` → path `/api/files/*`
|
|
194
203
|
* `"/src/api/index.ts"` → path `/api`
|
|
195
204
|
*/
|
|
196
205
|
function resolveApiRoutes(files, apiDir = "/src/api") {
|
|
@@ -200,6 +209,7 @@ function resolveApiRoutes(files, apiDir = "/src/api") {
|
|
|
200
209
|
if (relative.startsWith(normalizedDir)) relative = relative.slice(normalizedDir.length);
|
|
201
210
|
relative = relative.replace(/\.(ts|tsx|js|jsx)$/, "");
|
|
202
211
|
if (relative.endsWith("/index")) relative = relative.slice(0, -6) || "/";
|
|
212
|
+
relative = relative.replace(/\[\.\.\.[^\]]+\]/g, "*");
|
|
203
213
|
relative = relative.replace(/\[([^\]]+)\]/g, ":$1");
|
|
204
214
|
const path = normalizeRoutePath(`/api${relative}`);
|
|
205
215
|
return {
|
|
@@ -528,7 +538,8 @@ async function fetchPrachtRouteState(url, options) {
|
|
|
528
538
|
[ROUTE_STATE_REQUEST_HEADER]: "1",
|
|
529
539
|
"Cache-Control": "no-cache"
|
|
530
540
|
},
|
|
531
|
-
redirect: "manual"
|
|
541
|
+
redirect: "manual",
|
|
542
|
+
signal: options?.signal
|
|
532
543
|
});
|
|
533
544
|
if (response.type === "opaqueredirect" || response.status >= 300 && response.status < 400) return {
|
|
534
545
|
location: response.headers.get("location") ?? url,
|
|
@@ -570,6 +581,153 @@ async function navigateToClientLocation(location, options) {
|
|
|
570
581
|
window.location.href = targetUrl.toString();
|
|
571
582
|
}
|
|
572
583
|
//#endregion
|
|
584
|
+
//#region src/prefetch.ts
|
|
585
|
+
const CACHE_TTL_MS = 3e4;
|
|
586
|
+
const MAX_PREFETCH_CACHE_ENTRIES = 100;
|
|
587
|
+
const MAX_MATCH_CACHE_ENTRIES = 250;
|
|
588
|
+
const prefetchCache = /* @__PURE__ */ new Map();
|
|
589
|
+
function clearPrefetchCache() {
|
|
590
|
+
prefetchCache.clear();
|
|
591
|
+
}
|
|
592
|
+
function getCachedRouteState(url) {
|
|
593
|
+
const entry = prefetchCache.get(url);
|
|
594
|
+
if (!entry) return null;
|
|
595
|
+
if (Date.now() - entry.timestamp > CACHE_TTL_MS) {
|
|
596
|
+
prefetchCache.delete(url);
|
|
597
|
+
return null;
|
|
598
|
+
}
|
|
599
|
+
prefetchCache.delete(url);
|
|
600
|
+
prefetchCache.set(url, entry);
|
|
601
|
+
return entry.promise;
|
|
602
|
+
}
|
|
603
|
+
function prefetchRouteState(url) {
|
|
604
|
+
const cached = getCachedRouteState(url);
|
|
605
|
+
if (cached) return cached;
|
|
606
|
+
sweepPrefetchCache();
|
|
607
|
+
const promise = fetchPrachtRouteState(url);
|
|
608
|
+
prefetchCache.set(url, {
|
|
609
|
+
promise,
|
|
610
|
+
timestamp: Date.now()
|
|
611
|
+
});
|
|
612
|
+
trimMapToSize(prefetchCache, MAX_PREFETCH_CACHE_ENTRIES);
|
|
613
|
+
return promise;
|
|
614
|
+
}
|
|
615
|
+
function setupPrefetching(app, warmModules) {
|
|
616
|
+
let hoverTimer = null;
|
|
617
|
+
const observedViewportAnchors = /* @__PURE__ */ new WeakSet();
|
|
618
|
+
const matchCache = /* @__PURE__ */ new Map();
|
|
619
|
+
function getRoutePathname(url) {
|
|
620
|
+
try {
|
|
621
|
+
return new URL(url, window.location.origin).pathname;
|
|
622
|
+
} catch {
|
|
623
|
+
return null;
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
function getInternalHref(anchor) {
|
|
627
|
+
const href = anchor.getAttribute("href");
|
|
628
|
+
if (!href || href.startsWith("#")) return null;
|
|
629
|
+
let url;
|
|
630
|
+
try {
|
|
631
|
+
url = new URL(href, window.location.origin);
|
|
632
|
+
} catch {
|
|
633
|
+
return null;
|
|
634
|
+
}
|
|
635
|
+
if (url.origin !== window.location.origin) return null;
|
|
636
|
+
return url.pathname + url.search;
|
|
637
|
+
}
|
|
638
|
+
function getMatchEntry(href) {
|
|
639
|
+
const cached = matchCache.get(href);
|
|
640
|
+
if (cached) {
|
|
641
|
+
matchCache.delete(href);
|
|
642
|
+
matchCache.set(href, cached);
|
|
643
|
+
return cached;
|
|
644
|
+
}
|
|
645
|
+
const routePathname = getRoutePathname(href);
|
|
646
|
+
const match = routePathname ? matchAppRoute(app, routePathname) ?? null : null;
|
|
647
|
+
const entry = {
|
|
648
|
+
match,
|
|
649
|
+
strategy: match ? match.route.prefetch ?? "intent" : "none"
|
|
650
|
+
};
|
|
651
|
+
matchCache.set(href, entry);
|
|
652
|
+
trimMapToSize(matchCache, MAX_MATCH_CACHE_ENTRIES);
|
|
653
|
+
return entry;
|
|
654
|
+
}
|
|
655
|
+
function prefetchHref(href) {
|
|
656
|
+
prefetchRouteState(href);
|
|
657
|
+
if (!warmModules) return;
|
|
658
|
+
const match = getMatchEntry(href).match;
|
|
659
|
+
if (match) warmModules(match);
|
|
660
|
+
}
|
|
661
|
+
document.addEventListener("mouseenter", (e) => {
|
|
662
|
+
const anchor = e.target.closest?.("a");
|
|
663
|
+
if (!anchor) return;
|
|
664
|
+
const href = getInternalHref(anchor);
|
|
665
|
+
if (!href) return;
|
|
666
|
+
const strategy = getMatchEntry(href).strategy;
|
|
667
|
+
if (strategy !== "hover" && strategy !== "intent") return;
|
|
668
|
+
if (hoverTimer) clearTimeout(hoverTimer);
|
|
669
|
+
hoverTimer = setTimeout(() => {
|
|
670
|
+
prefetchHref(href);
|
|
671
|
+
}, 50);
|
|
672
|
+
}, true);
|
|
673
|
+
document.addEventListener("mouseleave", (e) => {
|
|
674
|
+
if (!e.target.closest?.("a")) return;
|
|
675
|
+
if (hoverTimer) {
|
|
676
|
+
clearTimeout(hoverTimer);
|
|
677
|
+
hoverTimer = null;
|
|
678
|
+
}
|
|
679
|
+
}, true);
|
|
680
|
+
document.addEventListener("focusin", (e) => {
|
|
681
|
+
const anchor = e.target.closest?.("a");
|
|
682
|
+
if (!anchor) return;
|
|
683
|
+
const href = getInternalHref(anchor);
|
|
684
|
+
if (!href) return;
|
|
685
|
+
const strategy = getMatchEntry(href).strategy;
|
|
686
|
+
if (strategy !== "hover" && strategy !== "intent") return;
|
|
687
|
+
prefetchHref(href);
|
|
688
|
+
}, true);
|
|
689
|
+
if (typeof IntersectionObserver === "undefined") return;
|
|
690
|
+
const observer = new IntersectionObserver((entries) => {
|
|
691
|
+
for (const entry of entries) {
|
|
692
|
+
if (!entry.isIntersecting) continue;
|
|
693
|
+
const anchor = entry.target;
|
|
694
|
+
const href = getInternalHref(anchor);
|
|
695
|
+
if (!href) continue;
|
|
696
|
+
prefetchHref(href);
|
|
697
|
+
observer.unobserve(anchor);
|
|
698
|
+
}
|
|
699
|
+
}, { rootMargin: "200px" });
|
|
700
|
+
function observeAnchor(anchor) {
|
|
701
|
+
if (observedViewportAnchors.has(anchor)) return;
|
|
702
|
+
const href = getInternalHref(anchor);
|
|
703
|
+
if (!href) return;
|
|
704
|
+
if (getMatchEntry(href).strategy !== "viewport") return;
|
|
705
|
+
observedViewportAnchors.add(anchor);
|
|
706
|
+
observer.observe(anchor);
|
|
707
|
+
}
|
|
708
|
+
function observeViewportLinks(root) {
|
|
709
|
+
if (root instanceof HTMLAnchorElement) observeAnchor(root);
|
|
710
|
+
for (const anchor of root.querySelectorAll("a[href]")) observeAnchor(anchor);
|
|
711
|
+
}
|
|
712
|
+
observeViewportLinks(document.body);
|
|
713
|
+
new MutationObserver((records) => {
|
|
714
|
+
for (const record of records) for (const node of record.addedNodes) if (node instanceof HTMLElement || node instanceof DocumentFragment) observeViewportLinks(node);
|
|
715
|
+
}).observe(document.body, {
|
|
716
|
+
childList: true,
|
|
717
|
+
subtree: true
|
|
718
|
+
});
|
|
719
|
+
}
|
|
720
|
+
function sweepPrefetchCache(now = Date.now()) {
|
|
721
|
+
for (const [url, entry] of prefetchCache) if (now - entry.timestamp > CACHE_TTL_MS) prefetchCache.delete(url);
|
|
722
|
+
}
|
|
723
|
+
function trimMapToSize(map, maxEntries) {
|
|
724
|
+
while (map.size > maxEntries) {
|
|
725
|
+
const first = map.keys().next();
|
|
726
|
+
if (first.done) return;
|
|
727
|
+
map.delete(first.value);
|
|
728
|
+
}
|
|
729
|
+
}
|
|
730
|
+
//#endregion
|
|
573
731
|
//#region src/runtime-hooks.ts
|
|
574
732
|
const RouteDataContext = createContext(void 0);
|
|
575
733
|
function PrachtRuntimeProvider({ children, data, params = EMPTY_ROUTE_PARAMS, routeId, stateVersion = 0, url }) {
|
|
@@ -662,6 +820,7 @@ function Form(props) {
|
|
|
662
820
|
const formMethod = (method ?? form.method ?? "post").toUpperCase();
|
|
663
821
|
if (SAFE_METHODS.has(formMethod)) return;
|
|
664
822
|
event.preventDefault();
|
|
823
|
+
clearPrefetchCache();
|
|
665
824
|
const response = await fetch(props.action ?? form.action, {
|
|
666
825
|
method: formMethod,
|
|
667
826
|
body: new FormData(form),
|
|
@@ -691,13 +850,75 @@ function escapeHtml(str) {
|
|
|
691
850
|
return str.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
692
851
|
}
|
|
693
852
|
function serializeJsonForHtml(value) {
|
|
694
|
-
return JSON.stringify(value)
|
|
853
|
+
return escapeScriptText(JSON.stringify(value) ?? "null");
|
|
854
|
+
}
|
|
855
|
+
function escapeScriptText(value) {
|
|
856
|
+
return value.replace(/</g, "\\u003c").replace(/>/g, "\\u003e").replace(/&/g, "\\u0026").replace(/\u2028/g, "\\u2028").replace(/\u2029/g, "\\u2029");
|
|
857
|
+
}
|
|
858
|
+
const SAFE_ATTRIBUTE_NAME_RE = /^[A-Za-z_:][A-Za-z0-9:._-]*$/;
|
|
859
|
+
const GLOBAL_HEAD_ATTRIBUTE_PREFIXES = ["data-", "aria-"];
|
|
860
|
+
const META_ATTRIBUTES = new Set([
|
|
861
|
+
"charset",
|
|
862
|
+
"content",
|
|
863
|
+
"http-equiv",
|
|
864
|
+
"itemprop",
|
|
865
|
+
"media",
|
|
866
|
+
"name",
|
|
867
|
+
"property"
|
|
868
|
+
]);
|
|
869
|
+
const LINK_ATTRIBUTES = new Set([
|
|
870
|
+
"as",
|
|
871
|
+
"blocking",
|
|
872
|
+
"color",
|
|
873
|
+
"crossorigin",
|
|
874
|
+
"disabled",
|
|
875
|
+
"fetchpriority",
|
|
876
|
+
"href",
|
|
877
|
+
"hreflang",
|
|
878
|
+
"imagesizes",
|
|
879
|
+
"imagesrcset",
|
|
880
|
+
"integrity",
|
|
881
|
+
"media",
|
|
882
|
+
"referrerpolicy",
|
|
883
|
+
"rel",
|
|
884
|
+
"sizes",
|
|
885
|
+
"title",
|
|
886
|
+
"type"
|
|
887
|
+
]);
|
|
888
|
+
const SCRIPT_ATTRIBUTES = new Set([
|
|
889
|
+
"async",
|
|
890
|
+
"blocking",
|
|
891
|
+
"class",
|
|
892
|
+
"crossorigin",
|
|
893
|
+
"defer",
|
|
894
|
+
"fetchpriority",
|
|
895
|
+
"id",
|
|
896
|
+
"integrity",
|
|
897
|
+
"nomodule",
|
|
898
|
+
"nonce",
|
|
899
|
+
"referrerpolicy",
|
|
900
|
+
"src",
|
|
901
|
+
"type"
|
|
902
|
+
]);
|
|
903
|
+
function renderAttributes(attributes, allowedAttributes) {
|
|
904
|
+
return Object.entries(attributes).filter(([key, value]) => isAllowedHeadAttribute(key, value, allowedAttributes)).map(([key, value]) => `${key}="${escapeHtml(value ?? "")}"`).join(" ");
|
|
905
|
+
}
|
|
906
|
+
function isAllowedHeadAttribute(key, value, allowedAttributes) {
|
|
907
|
+
if (key === "children" || typeof value === "undefined" || !SAFE_ATTRIBUTE_NAME_RE.test(key)) return false;
|
|
908
|
+
const normalized = key.toLowerCase();
|
|
909
|
+
if (normalized.startsWith("on")) return false;
|
|
910
|
+
return allowedAttributes.has(normalized) || GLOBAL_HEAD_ATTRIBUTE_PREFIXES.some((prefix) => normalized.startsWith(prefix));
|
|
695
911
|
}
|
|
696
912
|
function buildHtmlDocument(options) {
|
|
697
913
|
const { head, body, hydrationState, clientEntryUrl, cssUrls = [], modulePreloadUrls = [], routeStatePreloadUrl } = options;
|
|
698
914
|
const titleTag = head.title ? `<title>${escapeHtml(head.title)}</title>` : "";
|
|
699
|
-
const metaTags = (head.meta ?? []).map((m) =>
|
|
700
|
-
const linkTags = (head.link ?? []).map((l) =>
|
|
915
|
+
const metaTags = (head.meta ?? []).map((m) => renderAttributes(m, META_ATTRIBUTES)).filter(Boolean).map((attrs) => `<meta ${attrs}>`).join("\n ");
|
|
916
|
+
const linkTags = (head.link ?? []).map((l) => renderAttributes(l, LINK_ATTRIBUTES)).filter(Boolean).map((attrs) => `<link ${attrs}>`).join("\n ");
|
|
917
|
+
const scriptTags = (head.script ?? []).map((script) => {
|
|
918
|
+
const attrs = renderAttributes(script, SCRIPT_ATTRIBUTES);
|
|
919
|
+
const children = script.children ? escapeScriptText(script.children) : "";
|
|
920
|
+
return attrs ? `<script ${attrs}>${children}<\/script>` : `<script>${children}<\/script>`;
|
|
921
|
+
}).join("\n ");
|
|
701
922
|
const cssTags = cssUrls.map((url) => `<link rel="stylesheet" href="${escapeHtml(url)}">`).join("\n ");
|
|
702
923
|
const modulePreloadTags = modulePreloadUrls.map((url) => `<link rel="modulepreload" href="${escapeHtml(url)}">`).join("\n ");
|
|
703
924
|
const routeStatePreloadTag = routeStatePreloadUrl ? `<link rel="preload" as="fetch" href="${escapeHtml(routeStatePreloadUrl)}" crossorigin="anonymous">` : "";
|
|
@@ -710,6 +931,7 @@ function buildHtmlDocument(options) {
|
|
|
710
931
|
${titleTag}
|
|
711
932
|
${metaTags}
|
|
712
933
|
${linkTags}
|
|
934
|
+
${scriptTags}
|
|
713
935
|
${cssTags}
|
|
714
936
|
${modulePreloadTags}
|
|
715
937
|
${routeStatePreloadTag}
|
|
@@ -868,7 +1090,8 @@ async function mergeHeadMetadata(shellModule, routeModule, routeArgs, data) {
|
|
|
868
1090
|
title: routeHead.title ?? shellHead.title,
|
|
869
1091
|
lang: routeHead.lang ?? shellHead.lang,
|
|
870
1092
|
meta: [...shellHead.meta ?? [], ...routeHead.meta ?? []],
|
|
871
|
-
link: [...shellHead.link ?? [], ...routeHead.link ?? []]
|
|
1093
|
+
link: [...shellHead.link ?? [], ...routeHead.link ?? []],
|
|
1094
|
+
script: [...shellHead.script ?? [], ...routeHead.script ?? []]
|
|
872
1095
|
};
|
|
873
1096
|
}
|
|
874
1097
|
async function mergeDocumentHeaders(shellModule, routeModule, routeArgs, data) {
|
|
@@ -1032,25 +1255,25 @@ function markdownResponse(source) {
|
|
|
1032
1255
|
}
|
|
1033
1256
|
//#endregion
|
|
1034
1257
|
//#region src/runtime.ts
|
|
1035
|
-
const
|
|
1258
|
+
const SAME_ORIGIN_FETCH_SITE = "same-origin";
|
|
1036
1259
|
/**
|
|
1037
1260
|
* Stricter variant of first-party detection used for CSRF protection on
|
|
1038
|
-
* state-changing API requests.
|
|
1039
|
-
*
|
|
1040
|
-
*
|
|
1041
|
-
*
|
|
1042
|
-
*
|
|
1043
|
-
* explicitly or pre-flight via middleware.
|
|
1261
|
+
* state-changing API requests. It rejects any browser signal that points
|
|
1262
|
+
* outside this exact origin — a cross-origin form POST will send `Origin`
|
|
1263
|
+
* from the attacker, and `Sec-Fetch-Site: same-site` is not enough because
|
|
1264
|
+
* sibling subdomains can be attacker-controlled. Requests with no browser
|
|
1265
|
+
* provenance headers are treated as non-browser callers.
|
|
1044
1266
|
*/
|
|
1045
1267
|
function isSameOriginMutation(request, url) {
|
|
1046
1268
|
const site = request.headers.get("sec-fetch-site");
|
|
1047
|
-
if (site) return
|
|
1269
|
+
if (site && site !== SAME_ORIGIN_FETCH_SITE) return false;
|
|
1048
1270
|
const origin = request.headers.get("origin");
|
|
1049
1271
|
if (origin) try {
|
|
1050
1272
|
return new URL(origin).origin === url.origin;
|
|
1051
1273
|
} catch {
|
|
1052
1274
|
return false;
|
|
1053
1275
|
}
|
|
1276
|
+
if (site === SAME_ORIGIN_FETCH_SITE) return true;
|
|
1054
1277
|
const referer = request.headers.get("referer");
|
|
1055
1278
|
if (referer) try {
|
|
1056
1279
|
return new URL(referer).origin === url.origin;
|
|
@@ -1065,27 +1288,31 @@ function isSameOriginMutation(request, url) {
|
|
|
1065
1288
|
* otherwise reachable via any cross-origin `<a href>` / redirect.
|
|
1066
1289
|
*
|
|
1067
1290
|
* Accepts a request as first-party when:
|
|
1068
|
-
* - Sec-Fetch-Site is `same-origin`
|
|
1291
|
+
* - Sec-Fetch-Site is `same-origin` (modern browsers),
|
|
1069
1292
|
* - OR Sec-Fetch-Site is absent AND the Origin header matches the
|
|
1070
1293
|
* request URL's origin (older clients that still send Origin),
|
|
1071
|
-
* - OR
|
|
1072
|
-
*
|
|
1073
|
-
*
|
|
1074
|
-
*
|
|
1075
|
-
*
|
|
1076
|
-
* or `none` (for user-typed URLs Sec-Fetch-Site: none, Referer absent,
|
|
1077
|
-
* Origin absent — handled by the "no headers → allow" branch since that
|
|
1078
|
-
* matches a first-party typed URL too).
|
|
1294
|
+
* - OR Sec-Fetch-Site/Origin are absent AND Referer matches the request
|
|
1295
|
+
* URL's origin,
|
|
1296
|
+
* - OR no Origin/Sec-Fetch-Site/Referer is present (non-browser clients like
|
|
1297
|
+
* curl — CSRF is not the threat model there; blocking would break
|
|
1298
|
+
* tests and CLIs).
|
|
1079
1299
|
*/
|
|
1080
1300
|
function isFirstPartyFetch(request) {
|
|
1081
1301
|
const site = request.headers.get("sec-fetch-site");
|
|
1082
|
-
if (site) return
|
|
1302
|
+
if (site && site !== SAME_ORIGIN_FETCH_SITE) return false;
|
|
1083
1303
|
const origin = request.headers.get("origin");
|
|
1084
1304
|
if (origin) try {
|
|
1085
1305
|
return new URL(origin).origin === new URL(request.url).origin;
|
|
1086
1306
|
} catch {
|
|
1087
1307
|
return false;
|
|
1088
1308
|
}
|
|
1309
|
+
if (site === SAME_ORIGIN_FETCH_SITE) return true;
|
|
1310
|
+
const referer = request.headers.get("referer");
|
|
1311
|
+
if (referer) try {
|
|
1312
|
+
return new URL(referer).origin === new URL(request.url).origin;
|
|
1313
|
+
} catch {
|
|
1314
|
+
return false;
|
|
1315
|
+
}
|
|
1089
1316
|
return true;
|
|
1090
1317
|
}
|
|
1091
1318
|
async function handlePrachtRequest(options) {
|
|
@@ -1316,9 +1543,10 @@ async function prerenderApp(options) {
|
|
|
1316
1543
|
revalidate: route.revalidate
|
|
1317
1544
|
});
|
|
1318
1545
|
}
|
|
1319
|
-
const
|
|
1320
|
-
|
|
1321
|
-
|
|
1546
|
+
const concurrency = options.concurrency ?? 10;
|
|
1547
|
+
if (!Number.isInteger(concurrency) || concurrency <= 0) throw new Error("prerenderApp({ concurrency }) expects a positive integer.");
|
|
1548
|
+
for (let i = 0; i < work.length; i += concurrency) {
|
|
1549
|
+
const batch = work.slice(i, i + concurrency);
|
|
1322
1550
|
const batchResults = await Promise.all(batch.map(async (item) => {
|
|
1323
1551
|
const url = new URL(item.pathname, "http://localhost");
|
|
1324
1552
|
const request = new Request(url, { method: "GET" });
|
|
@@ -1367,127 +1595,70 @@ async function collectSSGPaths(route, registry) {
|
|
|
1367
1595
|
return (await routeModule.getStaticPaths()).map((params) => buildPathFromSegments(route.segments, params));
|
|
1368
1596
|
}
|
|
1369
1597
|
//#endregion
|
|
1370
|
-
//#region src/
|
|
1371
|
-
const
|
|
1372
|
-
|
|
1373
|
-
function
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
}
|
|
1382
|
-
function prefetchRouteState(url) {
|
|
1383
|
-
const cached = getCachedRouteState(url);
|
|
1384
|
-
if (cached) return cached;
|
|
1385
|
-
const promise = fetchPrachtRouteState(url);
|
|
1386
|
-
prefetchCache.set(url, {
|
|
1387
|
-
promise,
|
|
1388
|
-
timestamp: Date.now()
|
|
1389
|
-
});
|
|
1390
|
-
return promise;
|
|
1598
|
+
//#region src/hydration-mismatch.ts
|
|
1599
|
+
const HYDRATION_BANNER_ID = "__pracht_hydration_mismatch__";
|
|
1600
|
+
let installed = false;
|
|
1601
|
+
function installHydrationMismatchWarning() {
|
|
1602
|
+
if (installed) return;
|
|
1603
|
+
installed = true;
|
|
1604
|
+
const prev = options.__m;
|
|
1605
|
+
options.__m = function(vnode) {
|
|
1606
|
+
appendHydrationWarning(vnode);
|
|
1607
|
+
if (prev) prev(vnode);
|
|
1608
|
+
};
|
|
1391
1609
|
}
|
|
1392
|
-
function
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
if (!href || href.startsWith("#")) return null;
|
|
1404
|
-
let url;
|
|
1405
|
-
try {
|
|
1406
|
-
url = new URL(href, window.location.origin);
|
|
1407
|
-
} catch {
|
|
1408
|
-
return null;
|
|
1610
|
+
function appendHydrationWarning(vnode) {
|
|
1611
|
+
if (typeof document === "undefined") return;
|
|
1612
|
+
const componentName = getVNodeName(vnode);
|
|
1613
|
+
let banner = document.getElementById(HYDRATION_BANNER_ID);
|
|
1614
|
+
const message = `Hydration mismatch detected on <${componentName}>. The server-rendered HTML did not match the client.`;
|
|
1615
|
+
if (banner) {
|
|
1616
|
+
const list = banner.querySelector(`[data-pracht-mismatch-list]`);
|
|
1617
|
+
if (list) {
|
|
1618
|
+
const item = document.createElement("li");
|
|
1619
|
+
item.textContent = message;
|
|
1620
|
+
list.appendChild(item);
|
|
1409
1621
|
}
|
|
1410
|
-
|
|
1411
|
-
return url.pathname + url.search;
|
|
1622
|
+
return;
|
|
1412
1623
|
}
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1624
|
+
banner = document.createElement("div");
|
|
1625
|
+
banner.id = HYDRATION_BANNER_ID;
|
|
1626
|
+
banner.setAttribute("role", "alert");
|
|
1627
|
+
banner.style.cssText = [
|
|
1628
|
+
"position:fixed",
|
|
1629
|
+
"top:0",
|
|
1630
|
+
"left:0",
|
|
1631
|
+
"right:0",
|
|
1632
|
+
"z-index:2147483647",
|
|
1633
|
+
"background:#1a1a2e",
|
|
1634
|
+
"color:#ff6b6b",
|
|
1635
|
+
"padding:12px 16px",
|
|
1636
|
+
"font:12px/1.5 ui-monospace,Menlo,Consolas,monospace",
|
|
1637
|
+
"border-bottom:2px solid #e74c3c",
|
|
1638
|
+
"box-shadow:0 2px 8px rgba(0,0,0,0.3)"
|
|
1639
|
+
].join(";");
|
|
1640
|
+
const title = document.createElement("strong");
|
|
1641
|
+
title.textContent = "pracht: hydration mismatch";
|
|
1642
|
+
title.style.cssText = "display:block;margin-bottom:4px;color:#fff";
|
|
1643
|
+
banner.appendChild(title);
|
|
1644
|
+
const list = document.createElement("ul");
|
|
1645
|
+
list.setAttribute("data-pracht-mismatch-list", "");
|
|
1646
|
+
list.style.cssText = "margin:0;padding-left:18px";
|
|
1647
|
+
const item = document.createElement("li");
|
|
1648
|
+
item.textContent = message;
|
|
1649
|
+
list.appendChild(item);
|
|
1650
|
+
banner.appendChild(list);
|
|
1651
|
+
document.body.appendChild(banner);
|
|
1652
|
+
}
|
|
1653
|
+
function getVNodeName(vnode) {
|
|
1654
|
+
if (!vnode) return "Unknown";
|
|
1655
|
+
const type = vnode.type;
|
|
1656
|
+
if (typeof type === "string") return type;
|
|
1657
|
+
if (typeof type === "function") {
|
|
1658
|
+
const fn = type;
|
|
1659
|
+
return fn.displayName || fn.name || "Component";
|
|
1420
1660
|
}
|
|
1421
|
-
|
|
1422
|
-
const anchor = e.target.closest?.("a");
|
|
1423
|
-
if (!anchor) return;
|
|
1424
|
-
const href = getInternalHref(anchor);
|
|
1425
|
-
if (!href) return;
|
|
1426
|
-
const strategy = getPrefetchStrategy(href);
|
|
1427
|
-
if (strategy !== "hover" && strategy !== "intent") return;
|
|
1428
|
-
if (hoverTimer) clearTimeout(hoverTimer);
|
|
1429
|
-
hoverTimer = setTimeout(() => {
|
|
1430
|
-
prefetchRouteState(href);
|
|
1431
|
-
if (warmModules) {
|
|
1432
|
-
const pathname = getRoutePathname(href);
|
|
1433
|
-
const m = pathname ? matchAppRoute(app, pathname) : void 0;
|
|
1434
|
-
if (m) warmModules(m);
|
|
1435
|
-
}
|
|
1436
|
-
}, 50);
|
|
1437
|
-
}, true);
|
|
1438
|
-
document.addEventListener("mouseleave", (e) => {
|
|
1439
|
-
if (!e.target.closest?.("a")) return;
|
|
1440
|
-
if (hoverTimer) {
|
|
1441
|
-
clearTimeout(hoverTimer);
|
|
1442
|
-
hoverTimer = null;
|
|
1443
|
-
}
|
|
1444
|
-
}, true);
|
|
1445
|
-
document.addEventListener("focusin", (e) => {
|
|
1446
|
-
const anchor = e.target.closest?.("a");
|
|
1447
|
-
if (!anchor) return;
|
|
1448
|
-
const href = getInternalHref(anchor);
|
|
1449
|
-
if (!href) return;
|
|
1450
|
-
const strategy = getPrefetchStrategy(href);
|
|
1451
|
-
if (strategy !== "hover" && strategy !== "intent") return;
|
|
1452
|
-
prefetchRouteState(href);
|
|
1453
|
-
if (warmModules) {
|
|
1454
|
-
const pathname = getRoutePathname(href);
|
|
1455
|
-
const m = pathname ? matchAppRoute(app, pathname) : void 0;
|
|
1456
|
-
if (m) warmModules(m);
|
|
1457
|
-
}
|
|
1458
|
-
}, true);
|
|
1459
|
-
if (typeof IntersectionObserver === "undefined") return;
|
|
1460
|
-
const observer = new IntersectionObserver((entries) => {
|
|
1461
|
-
for (const entry of entries) {
|
|
1462
|
-
if (!entry.isIntersecting) continue;
|
|
1463
|
-
const anchor = entry.target;
|
|
1464
|
-
const href = getInternalHref(anchor);
|
|
1465
|
-
if (!href) continue;
|
|
1466
|
-
prefetchRouteState(href);
|
|
1467
|
-
if (warmModules) {
|
|
1468
|
-
const pathname = getRoutePathname(href);
|
|
1469
|
-
const m = pathname ? matchAppRoute(app, pathname) : void 0;
|
|
1470
|
-
if (m) warmModules(m);
|
|
1471
|
-
}
|
|
1472
|
-
observer.unobserve(anchor);
|
|
1473
|
-
}
|
|
1474
|
-
}, { rootMargin: "200px" });
|
|
1475
|
-
function observeViewportLinks() {
|
|
1476
|
-
const anchors = document.querySelectorAll("a[href]");
|
|
1477
|
-
for (const anchor of anchors) {
|
|
1478
|
-
const href = getInternalHref(anchor);
|
|
1479
|
-
if (!href) continue;
|
|
1480
|
-
if (getPrefetchStrategy(href) !== "viewport") continue;
|
|
1481
|
-
observer.observe(anchor);
|
|
1482
|
-
}
|
|
1483
|
-
}
|
|
1484
|
-
observeViewportLinks();
|
|
1485
|
-
new MutationObserver(() => {
|
|
1486
|
-
observeViewportLinks();
|
|
1487
|
-
}).observe(document.body, {
|
|
1488
|
-
childList: true,
|
|
1489
|
-
subtree: true
|
|
1490
|
-
});
|
|
1661
|
+
return "Unknown";
|
|
1491
1662
|
}
|
|
1492
1663
|
//#endregion
|
|
1493
1664
|
//#region src/router.ts
|
|
@@ -1497,6 +1668,7 @@ function useNavigate() {
|
|
|
1497
1668
|
}
|
|
1498
1669
|
async function initClientRouter(options) {
|
|
1499
1670
|
const { app, routeModules, shellModules, root, findModuleKey } = options;
|
|
1671
|
+
if (import.meta.env?.DEV) installHydrationMismatchWarning();
|
|
1500
1672
|
const moduleCache = /* @__PURE__ */ new Map();
|
|
1501
1673
|
function loadModule(modules, key) {
|
|
1502
1674
|
let cached = moduleCache.get(key);
|
|
@@ -1519,6 +1691,8 @@ async function initClientRouter(options) {
|
|
|
1519
1691
|
}
|
|
1520
1692
|
let updateRouteState = null;
|
|
1521
1693
|
let routeStateVersion = 0;
|
|
1694
|
+
let latestNavigationId = 0;
|
|
1695
|
+
let activeNavigationAbort = null;
|
|
1522
1696
|
function RouterRoot({ initialState }) {
|
|
1523
1697
|
const [routeState, setRouteState] = useState(initialState);
|
|
1524
1698
|
updateRouteState = setRouteState;
|
|
@@ -1605,6 +1779,10 @@ async function initClientRouter(options) {
|
|
|
1605
1779
|
};
|
|
1606
1780
|
}
|
|
1607
1781
|
async function navigate(to, opts) {
|
|
1782
|
+
const navigationId = ++latestNavigationId;
|
|
1783
|
+
activeNavigationAbort?.abort();
|
|
1784
|
+
const abortController = new AbortController();
|
|
1785
|
+
activeNavigationAbort = abortController;
|
|
1608
1786
|
const target = resolveBrowserRouteTarget(to);
|
|
1609
1787
|
if (!target) {
|
|
1610
1788
|
window.location.href = to;
|
|
@@ -1615,7 +1793,7 @@ async function initClientRouter(options) {
|
|
|
1615
1793
|
window.location.href = target.browserUrl;
|
|
1616
1794
|
return;
|
|
1617
1795
|
}
|
|
1618
|
-
const statePromise = getCachedRouteState(target.requestUrl) ?? fetchPrachtRouteState(target.requestUrl);
|
|
1796
|
+
const statePromise = getCachedRouteState(target.requestUrl) ?? fetchPrachtRouteState(target.requestUrl, { signal: abortController.signal });
|
|
1619
1797
|
const routeModPromise = startRouteImport(match);
|
|
1620
1798
|
const shellModPromise = startShellImport(match);
|
|
1621
1799
|
let state = {
|
|
@@ -1624,6 +1802,7 @@ async function initClientRouter(options) {
|
|
|
1624
1802
|
};
|
|
1625
1803
|
try {
|
|
1626
1804
|
const result = await statePromise;
|
|
1805
|
+
if (navigationId !== latestNavigationId) return;
|
|
1627
1806
|
if (result.type === "redirect") {
|
|
1628
1807
|
if (result.location) {
|
|
1629
1808
|
const redirect = resolveRedirectTarget(result.location);
|
|
@@ -1659,12 +1838,15 @@ async function initClientRouter(options) {
|
|
|
1659
1838
|
error: null
|
|
1660
1839
|
};
|
|
1661
1840
|
} catch {
|
|
1841
|
+
if (abortController.signal.aborted || navigationId !== latestNavigationId) return;
|
|
1662
1842
|
window.location.href = target.browserUrl;
|
|
1663
1843
|
return;
|
|
1664
1844
|
}
|
|
1845
|
+
if (navigationId !== latestNavigationId) return;
|
|
1665
1846
|
if (!opts?._popstate) if (opts?.replace) history.replaceState(null, "", target.browserUrl);
|
|
1666
1847
|
else history.pushState(null, "", target.browserUrl);
|
|
1667
1848
|
const routeState = await resolveRouteState(match, state, target.requestUrl, routeModPromise, shellModPromise);
|
|
1849
|
+
if (navigationId !== latestNavigationId) return;
|
|
1668
1850
|
if (routeState) {
|
|
1669
1851
|
applyRouteState(routeState);
|
|
1670
1852
|
window.scrollTo(0, 0);
|