@rangojs/router 0.0.0-experimental.002d056c
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/AGENTS.md +9 -0
- package/README.md +899 -0
- package/dist/bin/rango.js +1606 -0
- package/dist/vite/index.js +5153 -0
- package/package.json +177 -0
- package/skills/breadcrumbs/SKILL.md +250 -0
- package/skills/cache-guide/SKILL.md +262 -0
- package/skills/caching/SKILL.md +253 -0
- package/skills/composability/SKILL.md +172 -0
- package/skills/debug-manifest/SKILL.md +112 -0
- package/skills/document-cache/SKILL.md +182 -0
- package/skills/fonts/SKILL.md +167 -0
- package/skills/hooks/SKILL.md +704 -0
- package/skills/host-router/SKILL.md +218 -0
- package/skills/intercept/SKILL.md +313 -0
- package/skills/layout/SKILL.md +310 -0
- package/skills/links/SKILL.md +239 -0
- package/skills/loader/SKILL.md +596 -0
- package/skills/middleware/SKILL.md +339 -0
- package/skills/mime-routes/SKILL.md +128 -0
- package/skills/parallel/SKILL.md +305 -0
- package/skills/prerender/SKILL.md +643 -0
- package/skills/rango/SKILL.md +118 -0
- package/skills/response-routes/SKILL.md +411 -0
- package/skills/route/SKILL.md +385 -0
- package/skills/router-setup/SKILL.md +439 -0
- package/skills/tailwind/SKILL.md +129 -0
- package/skills/theme/SKILL.md +79 -0
- package/skills/typesafety/SKILL.md +623 -0
- package/skills/use-cache/SKILL.md +324 -0
- package/src/__internal.ts +273 -0
- package/src/bin/rango.ts +321 -0
- package/src/browser/action-coordinator.ts +97 -0
- package/src/browser/action-response-classifier.ts +99 -0
- package/src/browser/event-controller.ts +899 -0
- package/src/browser/history-state.ts +80 -0
- package/src/browser/index.ts +18 -0
- package/src/browser/intercept-utils.ts +52 -0
- package/src/browser/link-interceptor.ts +141 -0
- package/src/browser/logging.ts +55 -0
- package/src/browser/merge-segment-loaders.ts +134 -0
- package/src/browser/navigation-bridge.ts +638 -0
- package/src/browser/navigation-client.ts +261 -0
- package/src/browser/navigation-store.ts +806 -0
- package/src/browser/navigation-transaction.ts +297 -0
- package/src/browser/network-error-handler.ts +61 -0
- package/src/browser/partial-update.ts +582 -0
- package/src/browser/prefetch/cache.ts +206 -0
- package/src/browser/prefetch/fetch.ts +145 -0
- package/src/browser/prefetch/observer.ts +65 -0
- package/src/browser/prefetch/policy.ts +48 -0
- package/src/browser/prefetch/queue.ts +128 -0
- package/src/browser/rango-state.ts +112 -0
- package/src/browser/react/Link.tsx +368 -0
- package/src/browser/react/NavigationProvider.tsx +413 -0
- package/src/browser/react/ScrollRestoration.tsx +94 -0
- package/src/browser/react/context.ts +59 -0
- package/src/browser/react/filter-segment-order.ts +11 -0
- package/src/browser/react/index.ts +52 -0
- package/src/browser/react/location-state-shared.ts +162 -0
- package/src/browser/react/location-state.ts +107 -0
- package/src/browser/react/mount-context.ts +37 -0
- package/src/browser/react/nonce-context.ts +23 -0
- package/src/browser/react/shallow-equal.ts +27 -0
- package/src/browser/react/use-action.ts +218 -0
- package/src/browser/react/use-client-cache.ts +58 -0
- package/src/browser/react/use-handle.ts +162 -0
- package/src/browser/react/use-href.tsx +40 -0
- package/src/browser/react/use-link-status.ts +135 -0
- package/src/browser/react/use-mount.ts +31 -0
- package/src/browser/react/use-navigation.ts +99 -0
- package/src/browser/react/use-params.ts +65 -0
- package/src/browser/react/use-pathname.ts +47 -0
- package/src/browser/react/use-router.ts +63 -0
- package/src/browser/react/use-search-params.ts +56 -0
- package/src/browser/react/use-segments.ts +171 -0
- package/src/browser/response-adapter.ts +73 -0
- package/src/browser/rsc-router.tsx +464 -0
- package/src/browser/scroll-restoration.ts +397 -0
- package/src/browser/segment-reconciler.ts +216 -0
- package/src/browser/segment-structure-assert.ts +83 -0
- package/src/browser/server-action-bridge.ts +667 -0
- package/src/browser/shallow.ts +40 -0
- package/src/browser/types.ts +547 -0
- package/src/browser/validate-redirect-origin.ts +29 -0
- package/src/build/generate-manifest.ts +438 -0
- package/src/build/generate-route-types.ts +36 -0
- package/src/build/index.ts +35 -0
- package/src/build/route-trie.ts +265 -0
- package/src/build/route-types/ast-helpers.ts +25 -0
- package/src/build/route-types/ast-route-extraction.ts +98 -0
- package/src/build/route-types/codegen.ts +102 -0
- package/src/build/route-types/include-resolution.ts +411 -0
- package/src/build/route-types/param-extraction.ts +48 -0
- package/src/build/route-types/per-module-writer.ts +128 -0
- package/src/build/route-types/router-processing.ts +479 -0
- package/src/build/route-types/scan-filter.ts +78 -0
- package/src/build/runtime-discovery.ts +231 -0
- package/src/cache/background-task.ts +34 -0
- package/src/cache/cache-key-utils.ts +44 -0
- package/src/cache/cache-policy.ts +125 -0
- package/src/cache/cache-runtime.ts +338 -0
- package/src/cache/cache-scope.ts +382 -0
- package/src/cache/cf/cf-cache-store.ts +982 -0
- package/src/cache/cf/index.ts +29 -0
- package/src/cache/document-cache.ts +369 -0
- package/src/cache/handle-capture.ts +81 -0
- package/src/cache/handle-snapshot.ts +41 -0
- package/src/cache/index.ts +44 -0
- package/src/cache/memory-segment-store.ts +328 -0
- package/src/cache/profile-registry.ts +73 -0
- package/src/cache/read-through-swr.ts +134 -0
- package/src/cache/segment-codec.ts +256 -0
- package/src/cache/taint.ts +98 -0
- package/src/cache/types.ts +342 -0
- package/src/client.rsc.tsx +85 -0
- package/src/client.tsx +601 -0
- package/src/component-utils.ts +76 -0
- package/src/components/DefaultDocument.tsx +27 -0
- package/src/context-var.ts +86 -0
- package/src/debug.ts +243 -0
- package/src/default-error-boundary.tsx +88 -0
- package/src/deps/browser.ts +8 -0
- package/src/deps/html-stream-client.ts +2 -0
- package/src/deps/html-stream-server.ts +2 -0
- package/src/deps/rsc.ts +10 -0
- package/src/deps/ssr.ts +2 -0
- package/src/errors.ts +365 -0
- package/src/handle.ts +135 -0
- package/src/handles/MetaTags.tsx +246 -0
- package/src/handles/breadcrumbs.ts +66 -0
- package/src/handles/index.ts +7 -0
- package/src/handles/meta.ts +264 -0
- package/src/host/cookie-handler.ts +165 -0
- package/src/host/errors.ts +97 -0
- package/src/host/index.ts +53 -0
- package/src/host/pattern-matcher.ts +214 -0
- package/src/host/router.ts +352 -0
- package/src/host/testing.ts +79 -0
- package/src/host/types.ts +146 -0
- package/src/host/utils.ts +25 -0
- package/src/href-client.ts +222 -0
- package/src/index.rsc.ts +233 -0
- package/src/index.ts +277 -0
- package/src/internal-debug.ts +11 -0
- package/src/loader.rsc.ts +89 -0
- package/src/loader.ts +64 -0
- package/src/network-error-thrower.tsx +23 -0
- package/src/outlet-context.ts +15 -0
- package/src/outlet-provider.tsx +45 -0
- package/src/prerender/param-hash.ts +37 -0
- package/src/prerender/store.ts +185 -0
- package/src/prerender.ts +463 -0
- package/src/reverse.ts +330 -0
- package/src/root-error-boundary.tsx +289 -0
- package/src/route-content-wrapper.tsx +196 -0
- package/src/route-definition/dsl-helpers.ts +934 -0
- package/src/route-definition/helper-factories.ts +200 -0
- package/src/route-definition/helpers-types.ts +430 -0
- package/src/route-definition/index.ts +52 -0
- package/src/route-definition/redirect.ts +93 -0
- package/src/route-definition.ts +1 -0
- package/src/route-map-builder.ts +281 -0
- package/src/route-name.ts +53 -0
- package/src/route-types.ts +259 -0
- package/src/router/content-negotiation.ts +116 -0
- package/src/router/debug-manifest.ts +72 -0
- package/src/router/error-handling.ts +287 -0
- package/src/router/find-match.ts +160 -0
- package/src/router/handler-context.ts +451 -0
- package/src/router/intercept-resolution.ts +397 -0
- package/src/router/lazy-includes.ts +236 -0
- package/src/router/loader-resolution.ts +420 -0
- package/src/router/logging.ts +251 -0
- package/src/router/manifest.ts +269 -0
- package/src/router/match-api.ts +620 -0
- package/src/router/match-context.ts +266 -0
- package/src/router/match-handlers.ts +440 -0
- package/src/router/match-middleware/background-revalidation.ts +223 -0
- package/src/router/match-middleware/cache-lookup.ts +634 -0
- package/src/router/match-middleware/cache-store.ts +295 -0
- package/src/router/match-middleware/index.ts +81 -0
- package/src/router/match-middleware/intercept-resolution.ts +306 -0
- package/src/router/match-middleware/segment-resolution.ts +193 -0
- package/src/router/match-pipelines.ts +179 -0
- package/src/router/match-result.ts +219 -0
- package/src/router/metrics.ts +282 -0
- package/src/router/middleware-cookies.ts +55 -0
- package/src/router/middleware-types.ts +222 -0
- package/src/router/middleware.ts +749 -0
- package/src/router/pattern-matching.ts +563 -0
- package/src/router/prerender-match.ts +402 -0
- package/src/router/preview-match.ts +170 -0
- package/src/router/revalidation.ts +289 -0
- package/src/router/router-context.ts +320 -0
- package/src/router/router-interfaces.ts +452 -0
- package/src/router/router-options.ts +592 -0
- package/src/router/router-registry.ts +24 -0
- package/src/router/segment-resolution/fresh.ts +570 -0
- package/src/router/segment-resolution/helpers.ts +263 -0
- package/src/router/segment-resolution/loader-cache.ts +198 -0
- package/src/router/segment-resolution/revalidation.ts +1242 -0
- package/src/router/segment-resolution/static-store.ts +67 -0
- package/src/router/segment-resolution.ts +21 -0
- package/src/router/segment-wrappers.ts +291 -0
- package/src/router/telemetry-otel.ts +299 -0
- package/src/router/telemetry.ts +300 -0
- package/src/router/timeout.ts +148 -0
- package/src/router/trie-matching.ts +239 -0
- package/src/router/types.ts +170 -0
- package/src/router.ts +1006 -0
- package/src/rsc/handler-context.ts +45 -0
- package/src/rsc/handler.ts +1089 -0
- package/src/rsc/helpers.ts +198 -0
- package/src/rsc/index.ts +36 -0
- package/src/rsc/loader-fetch.ts +209 -0
- package/src/rsc/manifest-init.ts +86 -0
- package/src/rsc/nonce.ts +32 -0
- package/src/rsc/origin-guard.ts +141 -0
- package/src/rsc/progressive-enhancement.ts +379 -0
- package/src/rsc/response-error.ts +37 -0
- package/src/rsc/response-route-handler.ts +347 -0
- package/src/rsc/rsc-rendering.ts +237 -0
- package/src/rsc/runtime-warnings.ts +42 -0
- package/src/rsc/server-action.ts +348 -0
- package/src/rsc/ssr-setup.ts +128 -0
- package/src/rsc/types.ts +263 -0
- package/src/search-params.ts +230 -0
- package/src/segment-system.tsx +454 -0
- package/src/server/context.ts +591 -0
- package/src/server/cookie-store.ts +190 -0
- package/src/server/fetchable-loader-store.ts +37 -0
- package/src/server/handle-store.ts +308 -0
- package/src/server/loader-registry.ts +133 -0
- package/src/server/request-context.ts +920 -0
- package/src/server/root-layout.tsx +10 -0
- package/src/server/tsconfig.json +14 -0
- package/src/server.ts +51 -0
- package/src/ssr/index.tsx +365 -0
- package/src/static-handler.ts +114 -0
- package/src/theme/ThemeProvider.tsx +297 -0
- package/src/theme/ThemeScript.tsx +61 -0
- package/src/theme/constants.ts +62 -0
- package/src/theme/index.ts +48 -0
- package/src/theme/theme-context.ts +44 -0
- package/src/theme/theme-script.ts +155 -0
- package/src/theme/types.ts +182 -0
- package/src/theme/use-theme.ts +44 -0
- package/src/types/boundaries.ts +158 -0
- package/src/types/cache-types.ts +198 -0
- package/src/types/error-types.ts +192 -0
- package/src/types/global-namespace.ts +100 -0
- package/src/types/handler-context.ts +687 -0
- package/src/types/index.ts +88 -0
- package/src/types/loader-types.ts +183 -0
- package/src/types/route-config.ts +170 -0
- package/src/types/route-entry.ts +109 -0
- package/src/types/segments.ts +148 -0
- package/src/types.ts +1 -0
- package/src/urls/include-helper.ts +197 -0
- package/src/urls/index.ts +53 -0
- package/src/urls/path-helper-types.ts +339 -0
- package/src/urls/path-helper.ts +329 -0
- package/src/urls/pattern-types.ts +95 -0
- package/src/urls/response-types.ts +106 -0
- package/src/urls/type-extraction.ts +372 -0
- package/src/urls/urls-function.ts +98 -0
- package/src/urls.ts +1 -0
- package/src/use-loader.tsx +354 -0
- package/src/vite/discovery/bundle-postprocess.ts +184 -0
- package/src/vite/discovery/discover-routers.ts +344 -0
- package/src/vite/discovery/prerender-collection.ts +385 -0
- package/src/vite/discovery/route-types-writer.ts +258 -0
- package/src/vite/discovery/self-gen-tracking.ts +47 -0
- package/src/vite/discovery/state.ts +108 -0
- package/src/vite/discovery/virtual-module-codegen.ts +203 -0
- package/src/vite/index.ts +16 -0
- package/src/vite/plugin-types.ts +48 -0
- package/src/vite/plugins/cjs-to-esm.ts +93 -0
- package/src/vite/plugins/client-ref-dedup.ts +115 -0
- package/src/vite/plugins/client-ref-hashing.ts +105 -0
- package/src/vite/plugins/expose-action-id.ts +363 -0
- package/src/vite/plugins/expose-id-utils.ts +287 -0
- package/src/vite/plugins/expose-ids/export-analysis.ts +296 -0
- package/src/vite/plugins/expose-ids/handler-transform.ts +179 -0
- package/src/vite/plugins/expose-ids/loader-transform.ts +74 -0
- package/src/vite/plugins/expose-ids/router-transform.ts +110 -0
- package/src/vite/plugins/expose-ids/types.ts +45 -0
- package/src/vite/plugins/expose-internal-ids.ts +569 -0
- package/src/vite/plugins/refresh-cmd.ts +65 -0
- package/src/vite/plugins/use-cache-transform.ts +323 -0
- package/src/vite/plugins/version-injector.ts +83 -0
- package/src/vite/plugins/version-plugin.ts +266 -0
- package/src/vite/plugins/version.d.ts +12 -0
- package/src/vite/plugins/virtual-entries.ts +123 -0
- package/src/vite/plugins/virtual-stub-plugin.ts +29 -0
- package/src/vite/rango.ts +445 -0
- package/src/vite/router-discovery.ts +777 -0
- package/src/vite/utils/ast-handler-extract.ts +517 -0
- package/src/vite/utils/banner.ts +36 -0
- package/src/vite/utils/bundle-analysis.ts +137 -0
- package/src/vite/utils/manifest-utils.ts +70 -0
- package/src/vite/utils/package-resolution.ts +121 -0
- package/src/vite/utils/prerender-utils.ts +189 -0
- package/src/vite/utils/shared-utils.ts +169 -0
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import {
|
|
2
|
+
isLocationStateEntry,
|
|
3
|
+
resolveLocationStateEntries,
|
|
4
|
+
} from "./react/location-state-shared.js";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Check if state is from typed LocationStateEntry[] (has __rsc_ls_ keys)
|
|
8
|
+
*/
|
|
9
|
+
function isTypedLocationState(
|
|
10
|
+
state: unknown,
|
|
11
|
+
): state is Record<string, unknown> {
|
|
12
|
+
if (state === null || typeof state !== "object") return false;
|
|
13
|
+
return Object.keys(state).some((key) => key.startsWith("__rsc_ls_"));
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Resolve navigation state - handles both LocationStateEntry[] and plain formats
|
|
18
|
+
*/
|
|
19
|
+
export function resolveNavigationState(state: unknown): unknown {
|
|
20
|
+
if (
|
|
21
|
+
Array.isArray(state) &&
|
|
22
|
+
state.length > 0 &&
|
|
23
|
+
isLocationStateEntry(state[0])
|
|
24
|
+
) {
|
|
25
|
+
return resolveLocationStateEntries(state);
|
|
26
|
+
}
|
|
27
|
+
return state;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Build history state object from user state
|
|
32
|
+
* - Typed state: spread directly into history.state
|
|
33
|
+
* - Plain state: store in history.state.state
|
|
34
|
+
*/
|
|
35
|
+
export function buildHistoryState(
|
|
36
|
+
userState: unknown,
|
|
37
|
+
routerState?: { intercept?: boolean; sourceUrl?: string },
|
|
38
|
+
serverState?: Record<string, unknown>,
|
|
39
|
+
): Record<string, unknown> | null {
|
|
40
|
+
const result: Record<string, unknown> = {};
|
|
41
|
+
|
|
42
|
+
if (routerState?.intercept) {
|
|
43
|
+
result.intercept = true;
|
|
44
|
+
if (routerState.sourceUrl) {
|
|
45
|
+
result.sourceUrl = routerState.sourceUrl;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if (userState !== undefined) {
|
|
50
|
+
if (isTypedLocationState(userState)) {
|
|
51
|
+
Object.assign(result, userState);
|
|
52
|
+
} else {
|
|
53
|
+
result.state = userState;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (serverState) {
|
|
58
|
+
Object.assign(result, serverState);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return Object.keys(result).length > 0 ? result : null;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Merge server-set location state into the current history entry.
|
|
66
|
+
* Replaces the current history state and dispatches notification event
|
|
67
|
+
* so useLocationState hooks re-read from history.state.
|
|
68
|
+
*/
|
|
69
|
+
export function mergeLocationState(
|
|
70
|
+
locationState: Record<string, unknown>,
|
|
71
|
+
): void {
|
|
72
|
+
const merged = {
|
|
73
|
+
...window.history.state,
|
|
74
|
+
...locationState,
|
|
75
|
+
};
|
|
76
|
+
window.history.replaceState(merged, "", window.location.href);
|
|
77
|
+
if (Object.keys(locationState).some((k) => k.startsWith("__rsc_ls_"))) {
|
|
78
|
+
window.dispatchEvent(new Event("__rsc_locationstate"));
|
|
79
|
+
}
|
|
80
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
// ============================================================================
|
|
2
|
+
// Browser Module - Browser entry point for RSC Router
|
|
3
|
+
// ============================================================================
|
|
4
|
+
//
|
|
5
|
+
// Usage:
|
|
6
|
+
// import { initBrowserApp, RSCRouter } from "rsc-router/browser";
|
|
7
|
+
//
|
|
8
|
+
// For React components (Link, useNavigation, etc.):
|
|
9
|
+
// import { Link, useNavigation, useAction, href } from "rsc-router/client";
|
|
10
|
+
//
|
|
11
|
+
// ============================================================================
|
|
12
|
+
|
|
13
|
+
// Browser app initialization
|
|
14
|
+
export {
|
|
15
|
+
initBrowserApp,
|
|
16
|
+
RSCRouter,
|
|
17
|
+
type InitBrowserAppOptions,
|
|
18
|
+
} from "./rsc-router.js";
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import type { ResolvedSegment } from "./types.js";
|
|
2
|
+
import type { SlotState } from "../types.js";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Check if a segment is an intercept segment.
|
|
6
|
+
* Intercept segments have namespace starting with "intercept:" — both the
|
|
7
|
+
* parallel container (@modal) and its content children receive this namespace
|
|
8
|
+
* from intercept-resolution.ts. Regular parallel segments like @sidebar do not.
|
|
9
|
+
*/
|
|
10
|
+
export function isInterceptSegment(s: ResolvedSegment): boolean {
|
|
11
|
+
return s.namespace?.startsWith("intercept:") === true;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Split an array of segments into main and intercept groups.
|
|
16
|
+
* Intercept segments are separated for explicit injection into the render tree
|
|
17
|
+
* via the interceptSegments render option.
|
|
18
|
+
*/
|
|
19
|
+
export function splitInterceptSegments(segments: ResolvedSegment[]): {
|
|
20
|
+
main: ResolvedSegment[];
|
|
21
|
+
intercept: ResolvedSegment[];
|
|
22
|
+
} {
|
|
23
|
+
const main: ResolvedSegment[] = [];
|
|
24
|
+
const intercept: ResolvedSegment[] = [];
|
|
25
|
+
for (const s of segments) {
|
|
26
|
+
if (isInterceptSegment(s)) {
|
|
27
|
+
intercept.push(s);
|
|
28
|
+
} else {
|
|
29
|
+
main.push(s);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
return { main, intercept };
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Check if any slot is currently active (has content to render).
|
|
37
|
+
* Active slots indicate an intercept response where a parallel segment
|
|
38
|
+
* (e.g., @modal) has matched and should be rendered.
|
|
39
|
+
*/
|
|
40
|
+
export function hasActiveIntercept(slots?: Record<string, SlotState>): boolean {
|
|
41
|
+
if (!slots) return false;
|
|
42
|
+
return Object.values(slots).some((slot) => slot.active);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Check if cached segments contain any intercept segments.
|
|
47
|
+
* Intercept caches shouldn't be used for cached SWR rendering since
|
|
48
|
+
* whether interception happens depends on the current page context.
|
|
49
|
+
*/
|
|
50
|
+
export function isInterceptOnlyCache(segments: ResolvedSegment[]): boolean {
|
|
51
|
+
return segments.some(isInterceptSegment);
|
|
52
|
+
}
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import type { LinkInterceptorOptions, NavigateOptions } from "./types.js";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Check if an anchor points to the same page with only a hash change.
|
|
5
|
+
* Used by both Link component and link-interceptor to let the browser
|
|
6
|
+
* handle anchor scrolling natively.
|
|
7
|
+
*/
|
|
8
|
+
export function isHashOnlyNavigation(anchor: HTMLAnchorElement): boolean {
|
|
9
|
+
return (
|
|
10
|
+
anchor.pathname === window.location.pathname &&
|
|
11
|
+
anchor.search === window.location.search &&
|
|
12
|
+
!!anchor.hash
|
|
13
|
+
);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Default link interception predicate
|
|
18
|
+
*
|
|
19
|
+
* Returns true if the link should be intercepted for SPA navigation.
|
|
20
|
+
* Filters out:
|
|
21
|
+
* - Cross-origin links
|
|
22
|
+
* - Links with download attribute
|
|
23
|
+
* - Links with target other than _self
|
|
24
|
+
* - Links with data-no-intercept attribute
|
|
25
|
+
*
|
|
26
|
+
* @param link - The anchor element to check
|
|
27
|
+
* @returns true if the link should be intercepted
|
|
28
|
+
*/
|
|
29
|
+
export function defaultShouldIntercept(link: HTMLAnchorElement): boolean {
|
|
30
|
+
// Only intercept same-origin links
|
|
31
|
+
if (link.origin !== window.location.origin) {
|
|
32
|
+
return false;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Don't intercept if it has download attribute
|
|
36
|
+
if (link.hasAttribute("download")) {
|
|
37
|
+
return false;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Don't intercept if target is set to something other than _self
|
|
41
|
+
if (link.target && link.target !== "_self") {
|
|
42
|
+
return false;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Don't intercept if explicitly disabled
|
|
46
|
+
if (link.getAttribute("data-no-intercept") === "true") {
|
|
47
|
+
return false;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Don't intercept Link component anchors - they handle their own navigation
|
|
51
|
+
if (link.hasAttribute("data-link-component")) {
|
|
52
|
+
return false;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Don't intercept external links
|
|
56
|
+
if (link.hasAttribute("data-external")) {
|
|
57
|
+
return false;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Don't intercept hash-only navigation (same path, only fragment changes).
|
|
61
|
+
// Let the browser handle anchor scrolling natively.
|
|
62
|
+
if (isHashOnlyNavigation(link)) {
|
|
63
|
+
return false;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
return true;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Set up link interception for SPA navigation
|
|
71
|
+
*
|
|
72
|
+
* Attaches a global click handler to intercept clicks on anchor elements
|
|
73
|
+
* and call the onNavigate callback instead of performing a full page load.
|
|
74
|
+
*
|
|
75
|
+
* @param onNavigate - Callback when a link should navigate via SPA
|
|
76
|
+
* @param options - Configuration options
|
|
77
|
+
* @returns Cleanup function to remove the event listener
|
|
78
|
+
*
|
|
79
|
+
* @example
|
|
80
|
+
* ```typescript
|
|
81
|
+
* const cleanup = setupLinkInterception((url) => {
|
|
82
|
+
* window.history.pushState({}, "", url);
|
|
83
|
+
* fetchPartialUpdate(url);
|
|
84
|
+
* });
|
|
85
|
+
*
|
|
86
|
+
* // Later, to clean up:
|
|
87
|
+
* cleanup();
|
|
88
|
+
* ```
|
|
89
|
+
*/
|
|
90
|
+
export function setupLinkInterception(
|
|
91
|
+
onNavigate: (url: string, options?: NavigateOptions) => void,
|
|
92
|
+
options?: LinkInterceptorOptions,
|
|
93
|
+
): () => void {
|
|
94
|
+
const shouldIntercept = options?.shouldIntercept ?? defaultShouldIntercept;
|
|
95
|
+
|
|
96
|
+
const handleClick = (event: MouseEvent) => {
|
|
97
|
+
// If event was already handled by Link component (or other handler), skip
|
|
98
|
+
if (event.defaultPrevented) {
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const target = event.target as HTMLElement;
|
|
103
|
+
const link = target.closest("a");
|
|
104
|
+
|
|
105
|
+
if (!link || !shouldIntercept(link)) {
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Don't intercept if modifier keys are pressed (open in new tab, etc.)
|
|
110
|
+
if (event.metaKey || event.ctrlKey || event.shiftKey || event.altKey) {
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
event.preventDefault();
|
|
115
|
+
const href = link.href;
|
|
116
|
+
|
|
117
|
+
// Read navigation options from data attributes (set by Link component)
|
|
118
|
+
const scrollAttr = link.getAttribute("data-scroll");
|
|
119
|
+
const replaceAttr = link.getAttribute("data-replace");
|
|
120
|
+
const revalidateAttr = link.getAttribute("data-revalidate");
|
|
121
|
+
|
|
122
|
+
const navigateOptions: NavigateOptions = {};
|
|
123
|
+
if (scrollAttr === "false") {
|
|
124
|
+
navigateOptions.scroll = false;
|
|
125
|
+
}
|
|
126
|
+
if (replaceAttr === "true") {
|
|
127
|
+
navigateOptions.replace = true;
|
|
128
|
+
}
|
|
129
|
+
if (revalidateAttr === "false") {
|
|
130
|
+
navigateOptions.revalidate = false;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
onNavigate(href, navigateOptions);
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
document.addEventListener("click", handleClick);
|
|
137
|
+
|
|
138
|
+
return () => {
|
|
139
|
+
document.removeEventListener("click", handleClick);
|
|
140
|
+
};
|
|
141
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { INTERNAL_RANGO_DEBUG } from "../internal-debug.js";
|
|
2
|
+
|
|
3
|
+
interface BrowserLogContext {
|
|
4
|
+
requestId: string;
|
|
5
|
+
txId: string;
|
|
6
|
+
operation: string;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
let txCounter = 0;
|
|
10
|
+
let requestCounter = 0;
|
|
11
|
+
|
|
12
|
+
export function isBrowserDebugEnabled(): boolean {
|
|
13
|
+
return INTERNAL_RANGO_DEBUG;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function nextId(prefix: string, counter: number): string {
|
|
17
|
+
return `${prefix}${counter.toString(36)}`;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function startBrowserTransaction(operation: string): BrowserLogContext {
|
|
21
|
+
txCounter += 1;
|
|
22
|
+
requestCounter += 1;
|
|
23
|
+
return {
|
|
24
|
+
operation,
|
|
25
|
+
txId: nextId("ctx-", txCounter),
|
|
26
|
+
requestId: nextId("creq-", requestCounter),
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function browserDebugLog(
|
|
31
|
+
ctx: BrowserLogContext,
|
|
32
|
+
message: string,
|
|
33
|
+
details?: Record<string, unknown>,
|
|
34
|
+
): void {
|
|
35
|
+
if (!INTERNAL_RANGO_DEBUG) return;
|
|
36
|
+
|
|
37
|
+
const prefix = `[Browser][req:${ctx.requestId}][tx:${ctx.operation}-${ctx.txId}]`;
|
|
38
|
+
if (details) {
|
|
39
|
+
console.log(`${prefix} ${message}`, details);
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
console.log(`${prefix} ${message}`);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Simple gated console.log for browser-side debug output.
|
|
48
|
+
* Unlike browserDebugLog, this doesn't require a transaction context -
|
|
49
|
+
* use it for standalone debug messages in partial-update, navigation-bridge, etc.
|
|
50
|
+
*/
|
|
51
|
+
export function debugLog(msg: string, ...args: unknown[]): void {
|
|
52
|
+
if (INTERNAL_RANGO_DEBUG) {
|
|
53
|
+
console.log(msg, ...args);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import type { ResolvedSegment } from "./types.js";
|
|
2
|
+
import { debugLog } from "./logging.js";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Merge partial loader data from server with cached loader data.
|
|
6
|
+
*
|
|
7
|
+
* During partial revalidation (stale or action), the server may return only
|
|
8
|
+
* some loaders that pass the revalidation check. The component still needs
|
|
9
|
+
* all loader data, so we merge fresh data with cached data.
|
|
10
|
+
*
|
|
11
|
+
* @param fromServer - Segment returned from server with partial loaders
|
|
12
|
+
* @param fromCache - Cached segment with full loader data
|
|
13
|
+
* @returns Merged segment with complete loader data
|
|
14
|
+
*/
|
|
15
|
+
export function mergeSegmentLoaders(
|
|
16
|
+
fromServer: ResolvedSegment,
|
|
17
|
+
fromCache: ResolvedSegment,
|
|
18
|
+
): ResolvedSegment {
|
|
19
|
+
const serverLoaderIds = fromServer.loaderIds || [];
|
|
20
|
+
const cachedLoaderIds = fromCache.loaderIds || [];
|
|
21
|
+
|
|
22
|
+
debugLog(
|
|
23
|
+
`[Browser] Merging partial loaders: server has ${serverLoaderIds.join(", ")}, cache has ${cachedLoaderIds.join(", ")}`,
|
|
24
|
+
);
|
|
25
|
+
|
|
26
|
+
return {
|
|
27
|
+
...fromCache,
|
|
28
|
+
// Keep cached component (server's might be a fresh Promise that needs the loaders)
|
|
29
|
+
component: fromCache.component,
|
|
30
|
+
// Merge loader data - await both and combine
|
|
31
|
+
loaderDataPromise: Promise.all([
|
|
32
|
+
fromServer.loaderDataPromise!,
|
|
33
|
+
fromCache.loaderDataPromise!,
|
|
34
|
+
]).then(([newData, cachedData]) => {
|
|
35
|
+
// Build merged array: use new data for updated loaders, cached for rest
|
|
36
|
+
return cachedLoaderIds.map((id: string, i: number) => {
|
|
37
|
+
const newIndex = serverLoaderIds.indexOf(id);
|
|
38
|
+
if (newIndex !== -1) {
|
|
39
|
+
return (newData as any[])[newIndex]; // Use fresh data
|
|
40
|
+
}
|
|
41
|
+
return (cachedData as any[])[i]; // Use cached data
|
|
42
|
+
});
|
|
43
|
+
}),
|
|
44
|
+
// Keep all loader IDs from cache
|
|
45
|
+
loaderIds: fromCache.loaderIds,
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Check if segments need loader merging during partial revalidation.
|
|
51
|
+
*
|
|
52
|
+
* Returns true when:
|
|
53
|
+
* - Server returned fewer loaders than cached (partial revalidation)
|
|
54
|
+
* - Both segments have loader data promises
|
|
55
|
+
*/
|
|
56
|
+
export function needsLoaderMerge(
|
|
57
|
+
fromServer: ResolvedSegment,
|
|
58
|
+
fromCache: ResolvedSegment | undefined,
|
|
59
|
+
): fromCache is ResolvedSegment {
|
|
60
|
+
return !!(
|
|
61
|
+
fromCache &&
|
|
62
|
+
fromServer.loaderIds &&
|
|
63
|
+
fromCache.loaderIds &&
|
|
64
|
+
fromServer.loaderIds.length < fromCache.loaderIds.length &&
|
|
65
|
+
fromServer.loaderDataPromise &&
|
|
66
|
+
fromCache.loaderDataPromise
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Insert diff segments that aren't in the matched array into allSegments.
|
|
72
|
+
*
|
|
73
|
+
* During consolidation fetch for concurrent actions, loader segments may be
|
|
74
|
+
* excluded from the request. The server returns them in the diff but not in
|
|
75
|
+
* the matched array. This function inserts them at the correct position
|
|
76
|
+
* (after their parent layout segment).
|
|
77
|
+
*
|
|
78
|
+
* Loader segment IDs follow the pattern: {parentLayoutId}D{index}.{loaderId}
|
|
79
|
+
* Example: M9L0L1D0.actionCounter has parent layout M9L0L1
|
|
80
|
+
*
|
|
81
|
+
* @param allSegments - Mutable array of segments to insert into
|
|
82
|
+
* @param diff - Array of segment IDs that changed (from server response)
|
|
83
|
+
* @param matchedIdSet - Set of segment IDs from matched array
|
|
84
|
+
* @param newSegmentMap - Map of segment ID to segment data from server
|
|
85
|
+
*/
|
|
86
|
+
export function insertMissingDiffSegments(
|
|
87
|
+
allSegments: ResolvedSegment[],
|
|
88
|
+
diff: string[] | undefined,
|
|
89
|
+
matchedIdSet: Set<string>,
|
|
90
|
+
newSegmentMap: Map<string, ResolvedSegment>,
|
|
91
|
+
): void {
|
|
92
|
+
if (!diff || diff.length === 0) return;
|
|
93
|
+
|
|
94
|
+
// Track how many siblings have been inserted per parent so each new
|
|
95
|
+
// sibling goes after the last one rather than always at parentIndex + 1
|
|
96
|
+
// (which would reverse the server order).
|
|
97
|
+
const insertedPerParent = new Map<string, number>();
|
|
98
|
+
|
|
99
|
+
diff.forEach((diffId: string) => {
|
|
100
|
+
if (!matchedIdSet.has(diffId)) {
|
|
101
|
+
const fromServer = newSegmentMap.get(diffId);
|
|
102
|
+
if (fromServer) {
|
|
103
|
+
// Loader segment IDs have pattern like M9L0L1D0.actionCounter
|
|
104
|
+
// Parent layout ID is the prefix before D\d+ (e.g., M9L0L1)
|
|
105
|
+
const loaderMatch = diffId.match(/^(.+?)D\d+\./);
|
|
106
|
+
if (loaderMatch) {
|
|
107
|
+
const parentLayoutId = loaderMatch[1];
|
|
108
|
+
const parentIndex = allSegments.findIndex(
|
|
109
|
+
(s) => s.id === parentLayoutId,
|
|
110
|
+
);
|
|
111
|
+
if (parentIndex !== -1) {
|
|
112
|
+
const alreadyInserted = insertedPerParent.get(parentLayoutId) ?? 0;
|
|
113
|
+
const insertAt = parentIndex + 1 + alreadyInserted;
|
|
114
|
+
allSegments.splice(insertAt, 0, fromServer);
|
|
115
|
+
insertedPerParent.set(parentLayoutId, alreadyInserted + 1);
|
|
116
|
+
debugLog(
|
|
117
|
+
`[Browser] Inserted diff segment ${diffId} after ${parentLayoutId}`,
|
|
118
|
+
);
|
|
119
|
+
} else {
|
|
120
|
+
// Fallback: append to end if parent not found
|
|
121
|
+
allSegments.push(fromServer);
|
|
122
|
+
console.warn(
|
|
123
|
+
`[Browser] Appended diff segment ${diffId} (parent ${parentLayoutId} not found)`,
|
|
124
|
+
);
|
|
125
|
+
}
|
|
126
|
+
} else {
|
|
127
|
+
// Non-loader diff segment not in matched - append to end
|
|
128
|
+
allSegments.push(fromServer);
|
|
129
|
+
debugLog(`[Browser] Appended diff segment ${diffId}`);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
});
|
|
134
|
+
}
|