Files
sendou.ink/patches/react-router@8.4.0.patch
2026-10-04 17:47:08 +03:00

533 lines
32 KiB
Diff

diff --git a/dist/development/lib/dom/ssr/server.js b/dist/development/lib/dom/ssr/server.js
index a2b1aca6bfc6cb9a663596780167b58102beb361..2b29018c3bd23ba9bbb307debcf5b456324bf160 100644
--- a/dist/development/lib/dom/ssr/server.js
+++ b/dist/development/lib/dom/ssr/server.js
@@ -32,7 +32,7 @@ import * as React$1 from "react";
function ServerRouter({ context, url, nonce }) {
if (typeof url === "string") url = new URL(url);
let { manifest, routeModules, criticalCss, serverHandoffString } = context;
- let routes = createServerRoutes(manifest.routes, routeModules, context.future, context.isSpaMode);
+ let routes = cachedServerRoutes(manifest.routes, routeModules, context.future, context.isSpaMode);
context.staticHandlerContext.loaderData = { ...context.staticHandlerContext.loaderData };
for (let match of context.staticHandlerContext.matches) {
let routeId = match.route.id;
@@ -65,5 +65,14 @@ function ServerRouter({ context, url, nonce }) {
nonce
})) : null);
}
+// patched: the routes only depend on the build, so they are created once instead of on every document request
+const serverRoutesCache = /* @__PURE__ */ new WeakMap();
+function cachedServerRoutes(manifestRoutes, routeModules, future, isSpaMode) {
+ let cached = serverRoutesCache.get(manifestRoutes);
+ if (cached && cached.routeModules === routeModules && cached.future === future && cached.isSpaMode === isSpaMode) return cached.routes;
+ let routes = createServerRoutes(manifestRoutes, routeModules, future, isSpaMode);
+ serverRoutesCache.set(manifestRoutes, { routeModules, future, isSpaMode, routes });
+ return routes;
+}
//#endregion
export { ServerRouter };
diff --git a/dist/development/lib/dom-export/hydrated-router.js b/dist/development/lib/dom-export/hydrated-router.js
index 4d0817136a6b1398fd80a0ea1cb864a01796f203..0e91eece494e1ac17a344670fd08404a89d63f1c 100644
--- a/dist/development/lib/dom-export/hydrated-router.js
+++ b/dist/development/lib/dom-export/hydrated-router.js
@@ -133,7 +133,8 @@ function HydratedRouter(props) {
}, [location]);
UNSAFE_invariant(ssrInfo, "ssrInfo unavailable for HydratedRouter");
UNSAFE_useFogOFWarDiscovery(router, ssrInfo.manifest, ssrInfo.routeModules, ssrInfo.context.ssr, ssrInfo.context.routeDiscovery, ssrInfo.context.isSpaMode);
- return /* @__PURE__ */ React$1.createElement(React$1.Fragment, null, /* @__PURE__ */ React$1.createElement(UNSAFE_FrameworkContext.Provider, { value: {
+ // patched: a stable value so every <Link> doesn't re-render whenever the location changes
+ let frameworkContext = React$1.useMemo(() => ({
manifest: ssrInfo.manifest,
routeModules: ssrInfo.routeModules,
future: ssrInfo.context.future,
@@ -141,7 +142,13 @@ function HydratedRouter(props) {
ssr: ssrInfo.context.ssr,
isSpaMode: ssrInfo.context.isSpaMode,
routeDiscovery: ssrInfo.context.routeDiscovery
- } }, /* @__PURE__ */ React$1.createElement(UNSAFE_RemixErrorBoundary, { location }, /* @__PURE__ */ React$1.createElement(RouterProvider$1, {
+ }), [
+ ssrInfo.manifest,
+ ssrInfo.routeModules,
+ ssrInfo.context,
+ criticalCss
+ ]);
+ return /* @__PURE__ */ React$1.createElement(React$1.Fragment, null, /* @__PURE__ */ React$1.createElement(UNSAFE_FrameworkContext.Provider, { value: frameworkContext }, /* @__PURE__ */ React$1.createElement(UNSAFE_RemixErrorBoundary, { location }, /* @__PURE__ */ React$1.createElement(RouterProvider$1, {
router,
useTransitions: props.useTransitions,
onError: props.onError
diff --git a/dist/development/lib/hooks.js b/dist/development/lib/hooks.js
index 18c69d42e36059faae18922046ea3cd7e9817722..f2c806583352aa61c38ec113c159f0b4a84cac8e 100644
--- a/dist/development/lib/hooks.js
+++ b/dist/development/lib/hooks.js
@@ -527,10 +527,11 @@ function useParams() {
* Set to `"path"` to make relative routing operate against path segments.
* @returns The resolved {@link Path} object with `pathname`, `search`, and `hash`
*/
+// patched: the resolved-path inputs as a string, so links don't re-render when a router state change recreates the route context
+const RoutePathnamesContext = /* @__PURE__ */ React$1.createContext("[]");
function useResolvedPath(to, { relative } = {}) {
- let { matches } = React$1.useContext(RouteContext);
+ let routePathnamesJson = React$1.useContext(RoutePathnamesContext);
let { pathname: locationPathname } = useLocation();
- let routePathnamesJson = JSON.stringify(getResolveToMatches(matches));
return React$1.useMemo(() => resolveTo(to, JSON.parse(routePathnamesJson), locationPathname, relative === "path"), [
to,
routePathnamesJson,
@@ -681,10 +682,10 @@ var RenderErrorBoundary = class extends React$1.Component {
const decoded = decodeRouteErrorResponseDigest(error.digest);
if (decoded) error = decoded;
}
- let result = error !== void 0 ? /* @__PURE__ */ React$1.createElement(RouteContext.Provider, { value: this.props.routeContext }, /* @__PURE__ */ React$1.createElement(IsDataRouteContext.Provider, { value: this.props.routeContext.isDataRoute }, /* @__PURE__ */ React$1.createElement(RouteIdContext.Provider, { value: this.props.routeContext.matches[this.props.routeContext.matches.length - 1]?.route.id }, /* @__PURE__ */ React$1.createElement(RouteErrorContext.Provider, {
+ let result = error !== void 0 ? /* @__PURE__ */ React$1.createElement(RouteContext.Provider, { value: this.props.routeContext }, /* @__PURE__ */ React$1.createElement(RoutePathnamesContext.Provider, { value: JSON.stringify(getResolveToMatches(this.props.routeContext.matches)) }, /* @__PURE__ */ React$1.createElement(IsDataRouteContext.Provider, { value: this.props.routeContext.isDataRoute }, /* @__PURE__ */ React$1.createElement(RouteIdContext.Provider, { value: this.props.routeContext.matches[this.props.routeContext.matches.length - 1]?.route.id }, /* @__PURE__ */ React$1.createElement(RouteErrorContext.Provider, {
value: error,
children: this.props.component
- })))) : this.props.children;
+ }))))) : this.props.children;
if (this.context) return /* @__PURE__ */ React$1.createElement(RSCErrorHandler, { error }, result);
return result;
}
@@ -718,7 +719,7 @@ function RSCErrorHandler({ children, error }) {
function RenderedRoute({ routeContext, match, children }) {
let dataRouterContext = React$1.useContext(DataRouterContext);
if (dataRouterContext && dataRouterContext.static && dataRouterContext.staticContext && (match.route.errorElement || match.route.ErrorBoundary)) dataRouterContext.staticContext._deepestRenderedBoundaryId = match.route.id;
- return /* @__PURE__ */ React$1.createElement(RouteContext.Provider, { value: routeContext }, /* @__PURE__ */ React$1.createElement(IsDataRouteContext.Provider, { value: routeContext.isDataRoute }, /* @__PURE__ */ React$1.createElement(RouteIdContext.Provider, { value: match.route.id }, children)));
+ return /* @__PURE__ */ React$1.createElement(RouteContext.Provider, { value: routeContext }, /* @__PURE__ */ React$1.createElement(RoutePathnamesContext.Provider, { value: JSON.stringify(getResolveToMatches(routeContext.matches)) }, /* @__PURE__ */ React$1.createElement(IsDataRouteContext.Provider, { value: routeContext.isDataRoute }, /* @__PURE__ */ React$1.createElement(RouteIdContext.Provider, { value: match.route.id }, children))));
}
function _renderMatches(matches, parentMatches = [], dataRouterOpts) {
let dataRouterState = dataRouterOpts?.state;
diff --git a/dist/development/lib/router/matcher.js b/dist/development/lib/router/matcher.js
index 135d8da979b2a7c88c43f2f85c43de0ac6a49ad0..5eb2b3edfda349e5de531559c3313e4d0f4ac8e3 100644
--- a/dist/development/lib/router/matcher.js
+++ b/dist/development/lib/router/matcher.js
@@ -8,11 +8,14 @@
*
* @license MIT
*/
-import { flattenAndRankRoutes, matchRoutesImpl } from "./utils.js";
+import { parsePath } from "./history.js";
+import { flattenAndRankRoutes, matchRoutesImpl, stripBasename } from "./utils.js";
//#region lib/router/matcher.ts
var V6RegExMatcher = class {
#routes = [];
#branches = [];
+ #branchesByFirstSegment = /* @__PURE__ */ new Map();
+ #dynamicFirstSegmentBranches = [];
#basename;
constructor(basename) {
this.#basename = basename;
@@ -20,10 +23,31 @@ var V6RegExMatcher = class {
update(routes) {
this.#routes = routes;
this.#branches = flattenAndRankRoutes(routes);
+ // patched: index branches by their static first segment so a match only scans the branches that can match
+ let firstSegments = this.#branches.map(staticFirstSegment);
+ this.#dynamicFirstSegmentBranches = this.#branches.filter((_, i) => firstSegments[i] === null);
+ this.#branchesByFirstSegment = /* @__PURE__ */ new Map();
+ for (let segment of new Set(firstSegments)) if (segment !== null) this.#branchesByFirstSegment.set(segment, this.#branches.filter((_, i) => firstSegments[i] === null || firstSegments[i] === segment));
}
match(locationArg, allowPartial = false) {
- return matchRoutesImpl(this.#routes, locationArg, this.#basename, allowPartial, this.#branches);
+ return matchRoutesImpl(this.#routes, locationArg, this.#basename, allowPartial, this.#candidateBranches(locationArg));
+ }
+ #candidateBranches(locationArg) {
+ let pathname = stripBasename((typeof locationArg === "string" ? parsePath(locationArg) : locationArg).pathname || "/", this.#basename);
+ if (pathname == null) return this.#branches;
+ let firstSegment;
+ try {
+ firstSegment = decodeURIComponent(pathname.split("/", 2)[1] ?? "").toLowerCase();
+ } catch {
+ return this.#branches;
+ }
+ return this.#branchesByFirstSegment.get(firstSegment) ?? this.#dynamicFirstSegmentBranches;
}
};
+const STATIC_SEGMENT_RE = /^[A-Za-z0-9_.~-]+$/;
+function staticFirstSegment(branch) {
+ let segment = branch.path.split("/").find((s) => s !== "");
+ return segment !== void 0 && STATIC_SEGMENT_RE.test(segment) ? segment.toLowerCase() : null;
+}
//#endregion
export { V6RegExMatcher };
diff --git a/dist/development/lib/router/router.js b/dist/development/lib/router/router.js
index b6faa6059480c74780c475ebdcf2f1061056328c..c4c2e41142500ffd6f6378daffdea7b98182647e 100644
--- a/dist/development/lib/router/router.js
+++ b/dist/development/lib/router/router.js
@@ -2725,7 +2725,7 @@ function processLoaderData(state, matches, results, pendingActionResult, revalid
}
function mergeLoaderData(loaderData, newLoaderData, matches, errors) {
let mergedLoaderData = Object.entries(newLoaderData).filter(([, v]) => v !== ResetLoaderDataSymbol).reduce((merged, [k, v]) => {
- merged[k] = v;
+ merged[k] = loaderData.hasOwnProperty(k) ? replaceEqualDeep(loaderData[k], v) : v;
return merged;
}, {});
let preservedCount = 0;
@@ -2739,6 +2739,30 @@ function mergeLoaderData(loaderData, newLoaderData, matches, errors) {
}
return Object.keys(newLoaderData).length === 0 && preservedCount === Object.keys(loaderData).length ? loaderData : mergedLoaderData;
}
+// patched: a revalidation returning equal data keeps the previous data's identity (per unchanged subtree), so the UI reading it doesn't re-render
+function replaceEqualDeep(prev, next) {
+ if (prev === next) return prev;
+ if (prev instanceof Date && next instanceof Date) return prev.getTime() === next.getTime() ? prev : next;
+ let prevIsArray = Array.isArray(prev);
+ if (prevIsArray !== Array.isArray(next)) return next;
+ if (!prevIsArray && !(isPlainObject(prev) && isPlainObject(next))) return next;
+ let nextKeys = prevIsArray ? null : Object.keys(next);
+ let length = prevIsArray ? next.length : nextKeys.length;
+ let equal = length === (prevIsArray ? prev.length : Object.keys(prev).length);
+ let result = prevIsArray ? new Array(length) : {};
+ for (let i = 0; i < length; i++) {
+ let key = prevIsArray ? i : nextKeys[i];
+ let hasPrev = prevIsArray ? i < prev.length : Object.prototype.hasOwnProperty.call(prev, key);
+ result[key] = hasPrev ? replaceEqualDeep(prev[key], next[key]) : next[key];
+ if (!hasPrev || result[key] !== prev[key]) equal = false;
+ }
+ return equal ? prev : result;
+}
+function isPlainObject(value) {
+ if (value === null || typeof value !== "object") return false;
+ let proto = Object.getPrototypeOf(value);
+ return proto === Object.prototype || proto === null;
+}
function getActionDataForCommit(pendingActionResult) {
if (!pendingActionResult) return {};
return isErrorResult(pendingActionResult[1]) ? { actionData: {} } : { actionData: { [pendingActionResult[0]]: pendingActionResult[1].data } };
diff --git a/dist/development/lib/server-runtime/crypto.js b/dist/development/lib/server-runtime/crypto.js
index a4f6c3f43084bfccf5d9ebe4908868502ce70b58..c0619093ba8a2763d523d5a868896626636123f4 100644
--- a/dist/development/lib/server-runtime/crypto.js
+++ b/dist/development/lib/server-runtime/crypto.js
@@ -10,7 +10,10 @@
*/
//#region lib/server-runtime/crypto.ts
const encoder = /* @__PURE__ */ new TextEncoder();
+// patched: node:crypto HMAC is synchronous and ~10x faster than WebCrypto's importKey + sign/verify per cookie
+const nodeCrypto = typeof process !== "undefined" ? process.getBuiltinModule?.("node:crypto") : void 0;
const sign = async (value, secret) => {
+ if (nodeCrypto) return value + "." + nodeCrypto.createHmac("sha256", secret).update(value).digest("base64").replace(/=+$/, "");
let data = encoder.encode(value);
let key = await createKey(secret, ["sign"]);
let signature = await crypto.subtle.sign("HMAC", key, data);
@@ -21,6 +24,13 @@ const unsign = async (cookie, secret) => {
let index = cookie.lastIndexOf(".");
let value = cookie.slice(0, index);
let hash = cookie.slice(index + 1);
+ if (nodeCrypto) try {
+ let signature = byteStringToUint8Array(atob(hash));
+ let expected = nodeCrypto.createHmac("sha256", secret).update(value).digest();
+ return signature.length === expected.length && nodeCrypto.timingSafeEqual(signature, expected) ? value : false;
+ } catch {
+ return false;
+ }
let data = encoder.encode(value);
let key = await createKey(secret, ["verify"]);
try {
diff --git a/dist/development/lib/server-runtime/entry.js b/dist/development/lib/server-runtime/entry.js
index e863c7243768adc4b6a73da7e485895249358bab..26aec6dfce410872389037706052ccaa8f71d570 100644
--- a/dist/development/lib/server-runtime/entry.js
+++ b/dist/development/lib/server-runtime/entry.js
@@ -9,7 +9,14 @@
* @license MIT
*/
//#region lib/server-runtime/entry.ts
+// patched: memoized per build so the document render can reuse the server routes built from it
+const entryRouteModulesCache = /* @__PURE__ */ new WeakMap();
function createEntryRouteModules(manifest) {
+ let cached = entryRouteModulesCache.get(manifest);
+ if (!cached) entryRouteModulesCache.set(manifest, cached = createEntryRouteModulesUncached(manifest));
+ return cached;
+}
+function createEntryRouteModulesUncached(manifest) {
return Object.keys(manifest).reduce((memo, routeId) => {
let route = manifest[routeId];
if (route) memo[routeId] = route.module;
diff --git a/dist/development/vendor/turbo-stream-v2/flatten.js b/dist/development/vendor/turbo-stream-v2/flatten.js
index 61b8dbf73570da26728aada21ecdd5f3ed4a75cd..f27872d6b326e691eb00f9a5ecd3eb9bbf83d675 100644
--- a/dist/development/vendor/turbo-stream-v2/flatten.js
+++ b/dist/development/vendor/turbo-stream-v2/flatten.js
@@ -46,15 +46,16 @@ async function stringify(stack) {
stack.push([value, index]);
return index;
};
+ // patched: built once instead of once per value
+ const partsForObj = (obj) => Object.keys(obj).map((k) => `"_${flattenValue(k)}":${flattenValue(obj[k])}`).join(",");
let i = 0;
while (stack.length > 0) {
- const now = getNow();
- if (++i % 6e3 === 0 && now - lastYieldTime >= TIME_LIMIT_MS) {
+ // patched: reads the clock only when a yield is due instead of once per value
+ if (++i % 6e3 === 0 && getNow() - lastYieldTime >= TIME_LIMIT_MS) {
await yieldToMain();
lastYieldTime = getNow();
}
const [input, index] = stack.pop();
- const partsForObj = (obj) => Object.keys(obj).map((k) => `"_${flattenValue(k)}":${flattenValue(obj[k])}`).join(",");
let error = null;
switch (typeof input) {
case "boolean":
diff --git a/dist/production/lib/dom/ssr/server.js b/dist/production/lib/dom/ssr/server.js
index a2b1aca6bfc6cb9a663596780167b58102beb361..2b29018c3bd23ba9bbb307debcf5b456324bf160 100644
--- a/dist/production/lib/dom/ssr/server.js
+++ b/dist/production/lib/dom/ssr/server.js
@@ -32,7 +32,7 @@ import * as React$1 from "react";
function ServerRouter({ context, url, nonce }) {
if (typeof url === "string") url = new URL(url);
let { manifest, routeModules, criticalCss, serverHandoffString } = context;
- let routes = createServerRoutes(manifest.routes, routeModules, context.future, context.isSpaMode);
+ let routes = cachedServerRoutes(manifest.routes, routeModules, context.future, context.isSpaMode);
context.staticHandlerContext.loaderData = { ...context.staticHandlerContext.loaderData };
for (let match of context.staticHandlerContext.matches) {
let routeId = match.route.id;
@@ -65,5 +65,14 @@ function ServerRouter({ context, url, nonce }) {
nonce
})) : null);
}
+// patched: the routes only depend on the build, so they are created once instead of on every document request
+const serverRoutesCache = /* @__PURE__ */ new WeakMap();
+function cachedServerRoutes(manifestRoutes, routeModules, future, isSpaMode) {
+ let cached = serverRoutesCache.get(manifestRoutes);
+ if (cached && cached.routeModules === routeModules && cached.future === future && cached.isSpaMode === isSpaMode) return cached.routes;
+ let routes = createServerRoutes(manifestRoutes, routeModules, future, isSpaMode);
+ serverRoutesCache.set(manifestRoutes, { routeModules, future, isSpaMode, routes });
+ return routes;
+}
//#endregion
export { ServerRouter };
diff --git a/dist/production/lib/dom-export/hydrated-router.js b/dist/production/lib/dom-export/hydrated-router.js
index 4d0817136a6b1398fd80a0ea1cb864a01796f203..0e91eece494e1ac17a344670fd08404a89d63f1c 100644
--- a/dist/production/lib/dom-export/hydrated-router.js
+++ b/dist/production/lib/dom-export/hydrated-router.js
@@ -133,7 +133,8 @@ function HydratedRouter(props) {
}, [location]);
UNSAFE_invariant(ssrInfo, "ssrInfo unavailable for HydratedRouter");
UNSAFE_useFogOFWarDiscovery(router, ssrInfo.manifest, ssrInfo.routeModules, ssrInfo.context.ssr, ssrInfo.context.routeDiscovery, ssrInfo.context.isSpaMode);
- return /* @__PURE__ */ React$1.createElement(React$1.Fragment, null, /* @__PURE__ */ React$1.createElement(UNSAFE_FrameworkContext.Provider, { value: {
+ // patched: a stable value so every <Link> doesn't re-render whenever the location changes
+ let frameworkContext = React$1.useMemo(() => ({
manifest: ssrInfo.manifest,
routeModules: ssrInfo.routeModules,
future: ssrInfo.context.future,
@@ -141,7 +142,13 @@ function HydratedRouter(props) {
ssr: ssrInfo.context.ssr,
isSpaMode: ssrInfo.context.isSpaMode,
routeDiscovery: ssrInfo.context.routeDiscovery
- } }, /* @__PURE__ */ React$1.createElement(UNSAFE_RemixErrorBoundary, { location }, /* @__PURE__ */ React$1.createElement(RouterProvider$1, {
+ }), [
+ ssrInfo.manifest,
+ ssrInfo.routeModules,
+ ssrInfo.context,
+ criticalCss
+ ]);
+ return /* @__PURE__ */ React$1.createElement(React$1.Fragment, null, /* @__PURE__ */ React$1.createElement(UNSAFE_FrameworkContext.Provider, { value: frameworkContext }, /* @__PURE__ */ React$1.createElement(UNSAFE_RemixErrorBoundary, { location }, /* @__PURE__ */ React$1.createElement(RouterProvider$1, {
router,
useTransitions: props.useTransitions,
onError: props.onError
diff --git a/dist/production/lib/hooks.js b/dist/production/lib/hooks.js
index d83812a300d04b1301221b9f2650dc8197257cbb..4ebb1dd7321b19ab2aa1cd4f1784cb1b60be723e 100644
--- a/dist/production/lib/hooks.js
+++ b/dist/production/lib/hooks.js
@@ -527,10 +527,11 @@ function useParams() {
* Set to `"path"` to make relative routing operate against path segments.
* @returns The resolved {@link Path} object with `pathname`, `search`, and `hash`
*/
+// patched: the resolved-path inputs as a string, so links don't re-render when a router state change recreates the route context
+const RoutePathnamesContext = /* @__PURE__ */ React$1.createContext("[]");
function useResolvedPath(to, { relative } = {}) {
- let { matches } = React$1.useContext(RouteContext);
+ let routePathnamesJson = React$1.useContext(RoutePathnamesContext);
let { pathname: locationPathname } = useLocation();
- let routePathnamesJson = JSON.stringify(getResolveToMatches(matches));
return React$1.useMemo(() => resolveTo(to, JSON.parse(routePathnamesJson), locationPathname, relative === "path"), [
to,
routePathnamesJson,
@@ -666,10 +667,10 @@ var RenderErrorBoundary = class extends React$1.Component {
const decoded = decodeRouteErrorResponseDigest(error.digest);
if (decoded) error = decoded;
}
- let result = error !== void 0 ? /* @__PURE__ */ React$1.createElement(RouteContext.Provider, { value: this.props.routeContext }, /* @__PURE__ */ React$1.createElement(IsDataRouteContext.Provider, { value: this.props.routeContext.isDataRoute }, /* @__PURE__ */ React$1.createElement(RouteIdContext.Provider, { value: this.props.routeContext.matches[this.props.routeContext.matches.length - 1]?.route.id }, /* @__PURE__ */ React$1.createElement(RouteErrorContext.Provider, {
+ let result = error !== void 0 ? /* @__PURE__ */ React$1.createElement(RouteContext.Provider, { value: this.props.routeContext }, /* @__PURE__ */ React$1.createElement(RoutePathnamesContext.Provider, { value: JSON.stringify(getResolveToMatches(this.props.routeContext.matches)) }, /* @__PURE__ */ React$1.createElement(IsDataRouteContext.Provider, { value: this.props.routeContext.isDataRoute }, /* @__PURE__ */ React$1.createElement(RouteIdContext.Provider, { value: this.props.routeContext.matches[this.props.routeContext.matches.length - 1]?.route.id }, /* @__PURE__ */ React$1.createElement(RouteErrorContext.Provider, {
value: error,
children: this.props.component
- })))) : this.props.children;
+ }))))) : this.props.children;
if (this.context) return /* @__PURE__ */ React$1.createElement(RSCErrorHandler, { error }, result);
return result;
}
@@ -703,7 +704,7 @@ function RSCErrorHandler({ children, error }) {
function RenderedRoute({ routeContext, match, children }) {
let dataRouterContext = React$1.useContext(DataRouterContext);
if (dataRouterContext && dataRouterContext.static && dataRouterContext.staticContext && (match.route.errorElement || match.route.ErrorBoundary)) dataRouterContext.staticContext._deepestRenderedBoundaryId = match.route.id;
- return /* @__PURE__ */ React$1.createElement(RouteContext.Provider, { value: routeContext }, /* @__PURE__ */ React$1.createElement(IsDataRouteContext.Provider, { value: routeContext.isDataRoute }, /* @__PURE__ */ React$1.createElement(RouteIdContext.Provider, { value: match.route.id }, children)));
+ return /* @__PURE__ */ React$1.createElement(RouteContext.Provider, { value: routeContext }, /* @__PURE__ */ React$1.createElement(RoutePathnamesContext.Provider, { value: JSON.stringify(getResolveToMatches(routeContext.matches)) }, /* @__PURE__ */ React$1.createElement(IsDataRouteContext.Provider, { value: routeContext.isDataRoute }, /* @__PURE__ */ React$1.createElement(RouteIdContext.Provider, { value: match.route.id }, children))));
}
function _renderMatches(matches, parentMatches = [], dataRouterOpts) {
let dataRouterState = dataRouterOpts?.state;
diff --git a/dist/production/lib/router/matcher.js b/dist/production/lib/router/matcher.js
index 135d8da979b2a7c88c43f2f85c43de0ac6a49ad0..5eb2b3edfda349e5de531559c3313e4d0f4ac8e3 100644
--- a/dist/production/lib/router/matcher.js
+++ b/dist/production/lib/router/matcher.js
@@ -8,11 +8,14 @@
*
* @license MIT
*/
-import { flattenAndRankRoutes, matchRoutesImpl } from "./utils.js";
+import { parsePath } from "./history.js";
+import { flattenAndRankRoutes, matchRoutesImpl, stripBasename } from "./utils.js";
//#region lib/router/matcher.ts
var V6RegExMatcher = class {
#routes = [];
#branches = [];
+ #branchesByFirstSegment = /* @__PURE__ */ new Map();
+ #dynamicFirstSegmentBranches = [];
#basename;
constructor(basename) {
this.#basename = basename;
@@ -20,10 +23,31 @@ var V6RegExMatcher = class {
update(routes) {
this.#routes = routes;
this.#branches = flattenAndRankRoutes(routes);
+ // patched: index branches by their static first segment so a match only scans the branches that can match
+ let firstSegments = this.#branches.map(staticFirstSegment);
+ this.#dynamicFirstSegmentBranches = this.#branches.filter((_, i) => firstSegments[i] === null);
+ this.#branchesByFirstSegment = /* @__PURE__ */ new Map();
+ for (let segment of new Set(firstSegments)) if (segment !== null) this.#branchesByFirstSegment.set(segment, this.#branches.filter((_, i) => firstSegments[i] === null || firstSegments[i] === segment));
}
match(locationArg, allowPartial = false) {
- return matchRoutesImpl(this.#routes, locationArg, this.#basename, allowPartial, this.#branches);
+ return matchRoutesImpl(this.#routes, locationArg, this.#basename, allowPartial, this.#candidateBranches(locationArg));
+ }
+ #candidateBranches(locationArg) {
+ let pathname = stripBasename((typeof locationArg === "string" ? parsePath(locationArg) : locationArg).pathname || "/", this.#basename);
+ if (pathname == null) return this.#branches;
+ let firstSegment;
+ try {
+ firstSegment = decodeURIComponent(pathname.split("/", 2)[1] ?? "").toLowerCase();
+ } catch {
+ return this.#branches;
+ }
+ return this.#branchesByFirstSegment.get(firstSegment) ?? this.#dynamicFirstSegmentBranches;
}
};
+const STATIC_SEGMENT_RE = /^[A-Za-z0-9_.~-]+$/;
+function staticFirstSegment(branch) {
+ let segment = branch.path.split("/").find((s) => s !== "");
+ return segment !== void 0 && STATIC_SEGMENT_RE.test(segment) ? segment.toLowerCase() : null;
+}
//#endregion
export { V6RegExMatcher };
diff --git a/dist/production/lib/router/router.js b/dist/production/lib/router/router.js
index b6faa6059480c74780c475ebdcf2f1061056328c..c4c2e41142500ffd6f6378daffdea7b98182647e 100644
--- a/dist/production/lib/router/router.js
+++ b/dist/production/lib/router/router.js
@@ -2725,7 +2725,7 @@ function processLoaderData(state, matches, results, pendingActionResult, revalid
}
function mergeLoaderData(loaderData, newLoaderData, matches, errors) {
let mergedLoaderData = Object.entries(newLoaderData).filter(([, v]) => v !== ResetLoaderDataSymbol).reduce((merged, [k, v]) => {
- merged[k] = v;
+ merged[k] = loaderData.hasOwnProperty(k) ? replaceEqualDeep(loaderData[k], v) : v;
return merged;
}, {});
let preservedCount = 0;
@@ -2739,6 +2739,30 @@ function mergeLoaderData(loaderData, newLoaderData, matches, errors) {
}
return Object.keys(newLoaderData).length === 0 && preservedCount === Object.keys(loaderData).length ? loaderData : mergedLoaderData;
}
+// patched: a revalidation returning equal data keeps the previous data's identity (per unchanged subtree), so the UI reading it doesn't re-render
+function replaceEqualDeep(prev, next) {
+ if (prev === next) return prev;
+ if (prev instanceof Date && next instanceof Date) return prev.getTime() === next.getTime() ? prev : next;
+ let prevIsArray = Array.isArray(prev);
+ if (prevIsArray !== Array.isArray(next)) return next;
+ if (!prevIsArray && !(isPlainObject(prev) && isPlainObject(next))) return next;
+ let nextKeys = prevIsArray ? null : Object.keys(next);
+ let length = prevIsArray ? next.length : nextKeys.length;
+ let equal = length === (prevIsArray ? prev.length : Object.keys(prev).length);
+ let result = prevIsArray ? new Array(length) : {};
+ for (let i = 0; i < length; i++) {
+ let key = prevIsArray ? i : nextKeys[i];
+ let hasPrev = prevIsArray ? i < prev.length : Object.prototype.hasOwnProperty.call(prev, key);
+ result[key] = hasPrev ? replaceEqualDeep(prev[key], next[key]) : next[key];
+ if (!hasPrev || result[key] !== prev[key]) equal = false;
+ }
+ return equal ? prev : result;
+}
+function isPlainObject(value) {
+ if (value === null || typeof value !== "object") return false;
+ let proto = Object.getPrototypeOf(value);
+ return proto === Object.prototype || proto === null;
+}
function getActionDataForCommit(pendingActionResult) {
if (!pendingActionResult) return {};
return isErrorResult(pendingActionResult[1]) ? { actionData: {} } : { actionData: { [pendingActionResult[0]]: pendingActionResult[1].data } };
diff --git a/dist/production/lib/server-runtime/crypto.js b/dist/production/lib/server-runtime/crypto.js
index a4f6c3f43084bfccf5d9ebe4908868502ce70b58..c0619093ba8a2763d523d5a868896626636123f4 100644
--- a/dist/production/lib/server-runtime/crypto.js
+++ b/dist/production/lib/server-runtime/crypto.js
@@ -10,7 +10,10 @@
*/
//#region lib/server-runtime/crypto.ts
const encoder = /* @__PURE__ */ new TextEncoder();
+// patched: node:crypto HMAC is synchronous and ~10x faster than WebCrypto's importKey + sign/verify per cookie
+const nodeCrypto = typeof process !== "undefined" ? process.getBuiltinModule?.("node:crypto") : void 0;
const sign = async (value, secret) => {
+ if (nodeCrypto) return value + "." + nodeCrypto.createHmac("sha256", secret).update(value).digest("base64").replace(/=+$/, "");
let data = encoder.encode(value);
let key = await createKey(secret, ["sign"]);
let signature = await crypto.subtle.sign("HMAC", key, data);
@@ -21,6 +24,13 @@ const unsign = async (cookie, secret) => {
let index = cookie.lastIndexOf(".");
let value = cookie.slice(0, index);
let hash = cookie.slice(index + 1);
+ if (nodeCrypto) try {
+ let signature = byteStringToUint8Array(atob(hash));
+ let expected = nodeCrypto.createHmac("sha256", secret).update(value).digest();
+ return signature.length === expected.length && nodeCrypto.timingSafeEqual(signature, expected) ? value : false;
+ } catch {
+ return false;
+ }
let data = encoder.encode(value);
let key = await createKey(secret, ["verify"]);
try {
diff --git a/dist/production/lib/server-runtime/entry.js b/dist/production/lib/server-runtime/entry.js
index e863c7243768adc4b6a73da7e485895249358bab..26aec6dfce410872389037706052ccaa8f71d570 100644
--- a/dist/production/lib/server-runtime/entry.js
+++ b/dist/production/lib/server-runtime/entry.js
@@ -9,7 +9,14 @@
* @license MIT
*/
//#region lib/server-runtime/entry.ts
+// patched: memoized per build so the document render can reuse the server routes built from it
+const entryRouteModulesCache = /* @__PURE__ */ new WeakMap();
function createEntryRouteModules(manifest) {
+ let cached = entryRouteModulesCache.get(manifest);
+ if (!cached) entryRouteModulesCache.set(manifest, cached = createEntryRouteModulesUncached(manifest));
+ return cached;
+}
+function createEntryRouteModulesUncached(manifest) {
return Object.keys(manifest).reduce((memo, routeId) => {
let route = manifest[routeId];
if (route) memo[routeId] = route.module;
diff --git a/dist/production/vendor/turbo-stream-v2/flatten.js b/dist/production/vendor/turbo-stream-v2/flatten.js
index 61b8dbf73570da26728aada21ecdd5f3ed4a75cd..f27872d6b326e691eb00f9a5ecd3eb9bbf83d675 100644
--- a/dist/production/vendor/turbo-stream-v2/flatten.js
+++ b/dist/production/vendor/turbo-stream-v2/flatten.js
@@ -46,15 +46,16 @@ async function stringify(stack) {
stack.push([value, index]);
return index;
};
+ // patched: built once instead of once per value
+ const partsForObj = (obj) => Object.keys(obj).map((k) => `"_${flattenValue(k)}":${flattenValue(obj[k])}`).join(",");
let i = 0;
while (stack.length > 0) {
- const now = getNow();
- if (++i % 6e3 === 0 && now - lastYieldTime >= TIME_LIMIT_MS) {
+ // patched: reads the clock only when a yield is due instead of once per value
+ if (++i % 6e3 === 0 && getNow() - lastYieldTime >= TIME_LIMIT_MS) {
await yieldToMain();
lastYieldTime = getNow();
}
const [input, index] = stack.pop();
- const partsForObj = (obj) => Object.keys(obj).map((k) => `"_${flattenValue(k)}":${flattenValue(obj[k])}`).join(",");
let error = null;
switch (typeof input) {
case "boolean":