mirror of
https://github.com/Sendouc/sendou.ink.git
synced 2026-10-29 05:57:34 -05:00
533 lines
32 KiB
Diff
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":
|