From 81694a5ae39067093266df15f8b7d8b67b3d64c4 Mon Sep 17 00:00:00 2001
From: Jacob Ebey <jacob.ebey@live.com>
Date: Wed, 5 Mar 2025 11:46:16 -0800
Subject: [PATCH 01/10] feat: Sub Resource Integrity POC

---
 packages/react-router-dev/vite/plugin.ts      | 49 +++++++++++++++++++
 .../react-router/lib/dom/ssr/components.tsx   |  8 ++-
 playground/framework/app/root.tsx             | 12 ++++-
 3 files changed, 66 insertions(+), 3 deletions(-)

diff --git a/packages/react-router-dev/vite/plugin.ts b/packages/react-router-dev/vite/plugin.ts
index 7a24bced7b..12b78831a4 100644
--- a/packages/react-router-dev/vite/plugin.ts
+++ b/packages/react-router-dev/vite/plugin.ts
@@ -289,6 +289,7 @@ let virtual = {
   serverBuild: VirtualModule.create("server-build"),
   serverManifest: VirtualModule.create("server-manifest"),
   browserManifest: VirtualModule.create("browser-manifest"),
+  sriManifest: VirtualModule.create("sri-manifest"),
 };
 
 let invalidateVirtualModules = (viteDevServer: Vite.ViteDevServer) => {
@@ -1863,6 +1864,36 @@ export const reactRouterVitePlugin: ReactRouterVitePlugin = () => {
             let routeIds = getServerBundleRouteIds(this, ctx);
             return await getServerEntry({ routeIds });
           }
+          case virtual.sriManifest.resolvedId: {
+            let environmentName = ctx.reactRouterConfig.future
+              .unstable_viteEnvironmentApi
+              ? this.environment.name
+              : ctx.environmentBuildContext?.name;
+            if (
+              viteCommand !== "build" ||
+              !environmentName ||
+              (environmentName !== "ssr" &&
+                !isSsrBundleEnvironmentName(environmentName))
+            ) {
+              return `export default {};`;
+            }
+
+            let viteManifest = await loadViteManifest(
+              getClientBuildDirectory(ctx.reactRouterConfig)
+            );
+            let clientBuildDirectory = getClientBuildDirectory(
+              ctx.reactRouterConfig
+            );
+
+            let integrityMap = createSubResourceIntegrityMap(
+              viteManifest,
+              "sha384",
+              clientBuildDirectory,
+              ctx.publicPath
+            );
+
+            return `export default ${jsesc(integrityMap, { es6: true })};`;
+          }
           case virtual.serverManifest.resolvedId: {
             let routeIds = getServerBundleRouteIds(this, ctx);
             let reactRouterManifest =
@@ -3497,3 +3528,21 @@ async function getEnvironmentsOptions(
 function isNonNullable<T>(x: T): x is NonNullable<T> {
   return x != null;
 }
+
+function createSubResourceIntegrityMap(
+  viteManifest: Vite.Manifest,
+  algorithm: string,
+  outdir: string,
+  publicPath: string
+) {
+  let map: Record<string, string> = {};
+  for (let value of Object.values(viteManifest)) {
+    let file = path.resolve(outdir, value.file);
+    if (!fse.existsSync(file)) continue;
+    let source = fse.readFileSync(file);
+    let hash = createHash("sha384").update(source).digest().toString("base64");
+    let url = `${publicPath}${value.file}`;
+    map[url] = `${algorithm.toLowerCase()}-${hash}`;
+  }
+  return map;
+}
diff --git a/packages/react-router/lib/dom/ssr/components.tsx b/packages/react-router/lib/dom/ssr/components.tsx
index bb2cad8aef..4ffee08858 100644
--- a/packages/react-router/lib/dom/ssr/components.tsx
+++ b/packages/react-router/lib/dom/ssr/components.tsx
@@ -618,7 +618,9 @@ export type ScriptsProps = Omit<
   | "noModule"
   | "dangerouslySetInnerHTML"
   | "suppressHydrationWarning"
->;
+> & {
+  integrity?: Record<string, string>;
+};
 
 /**
   Renders the client runtime of your app. It should be rendered inside the `<body>` of the document.
@@ -642,7 +644,7 @@ export type ScriptsProps = Omit<
 
   @category Components
  */
-export function Scripts(props: ScriptsProps) {
+export function Scripts({ integrity, ...props }: ScriptsProps) {
   let { manifest, serverHandoffString, isSpaMode, ssr, renderMeta } =
     useFrameworkContext();
   let { router, static: isStatic, staticContext } = useDataRouterContext();
@@ -795,6 +797,7 @@ import(${JSON.stringify(manifest.entry.module)});`;
         rel="modulepreload"
         href={manifest.entry.module}
         crossOrigin={props.crossOrigin}
+        integrity={integrity?.[manifest.entry.module]}
       />
       {dedupe(preloads).map((path) => (
         <link
@@ -802,6 +805,7 @@ import(${JSON.stringify(manifest.entry.module)});`;
           rel="modulepreload"
           href={path}
           crossOrigin={props.crossOrigin}
+          integrity={integrity?.[path]}
         />
       ))}
       {initialScripts}
diff --git a/playground/framework/app/root.tsx b/playground/framework/app/root.tsx
index 11d5972955..4adfe5a28c 100644
--- a/playground/framework/app/root.tsx
+++ b/playground/framework/app/root.tsx
@@ -1,18 +1,28 @@
 import { Links, Meta, Outlet, Scripts, ScrollRestoration } from "react-router";
 
+import integrity from "virtual:react-router/sri-manifest";
+
 export function Layout({ children }: { children: React.ReactNode }) {
   return (
     <html lang="en">
       <head>
         <meta charSet="utf-8" />
         <meta name="viewport" content="width=device-width, initial-scale=1" />
+        <script
+          type="importmap"
+          dangerouslySetInnerHTML={{
+            __html: JSON.stringify({
+              integrity,
+            }),
+          }}
+        />
         <Meta />
         <Links />
       </head>
       <body>
         {children}
         <ScrollRestoration />
-        <Scripts />
+        <Scripts integrity={integrity} />
       </body>
     </html>
   );

From 974e42f57345099dca36b2a50f32f3bd0e21cff5 Mon Sep 17 00:00:00 2001
From: Jacob Ebey <jacob.ebey@live.com>
Date: Wed, 5 Mar 2025 13:33:15 -0800
Subject: [PATCH 02/10] refactor to be a part of the server build manifest and
 hidden behind `<Scripts>` and `unstable_subResourceIntegrity` future flag.

---
 .changeset/late-falcons-sort.md               |  6 ++
 packages/react-router-dev/config/config.ts    |  3 +
 packages/react-router-dev/manifest.ts         |  1 +
 packages/react-router-dev/vite/plugin.ts      | 75 +++++++++++--------
 .../react-router/lib/dom/ssr/components.tsx   | 21 ++++--
 packages/react-router/lib/dom/ssr/entry.ts    |  1 +
 playground/framework/app/root.tsx             | 13 +---
 playground/framework/react-router.config.ts   |  6 +-
 8 files changed, 77 insertions(+), 49 deletions(-)
 create mode 100644 .changeset/late-falcons-sort.md

diff --git a/.changeset/late-falcons-sort.md b/.changeset/late-falcons-sort.md
new file mode 100644
index 0000000000..da5c5c09c5
--- /dev/null
+++ b/.changeset/late-falcons-sort.md
@@ -0,0 +1,6 @@
+---
+"@react-router/dev": patch
+"react-router": patch
+---
+
+Introduce `unstable_subResourceIntegrity` future flag that enables generation of an importmap with integrity for the scripts that will be loaded by the browser.
diff --git a/packages/react-router-dev/config/config.ts b/packages/react-router-dev/config/config.ts
index 86b6833cd0..999e19eec3 100644
--- a/packages/react-router-dev/config/config.ts
+++ b/packages/react-router-dev/config/config.ts
@@ -92,6 +92,7 @@ interface FutureConfig {
    * Automatically split route modules into multiple chunks when possible.
    */
   unstable_splitRouteModules: boolean | "enforce";
+  unstable_subResourceIntegrity: boolean;
   /**
    * Use Vite Environment API (experimental)
    */
@@ -497,6 +498,8 @@ async function resolveConfig({
       reactRouterUserConfig.future?.unstable_optimizeDeps ?? false,
     unstable_splitRouteModules:
       reactRouterUserConfig.future?.unstable_splitRouteModules ?? false,
+    unstable_subResourceIntegrity:
+      reactRouterUserConfig.future?.unstable_subResourceIntegrity ?? false,
     unstable_viteEnvironmentApi:
       reactRouterUserConfig.future?.unstable_viteEnvironmentApi ?? false,
   };
diff --git a/packages/react-router-dev/manifest.ts b/packages/react-router-dev/manifest.ts
index 06a1bb80b6..f501d12e55 100644
--- a/packages/react-router-dev/manifest.ts
+++ b/packages/react-router-dev/manifest.ts
@@ -26,6 +26,7 @@ export type Manifest = {
   routes: {
     [routeId: string]: ManifestRoute;
   };
+  sri: Record<string, string> | undefined;
   hmr?: {
     timestamp?: number;
     runtime: string;
diff --git a/packages/react-router-dev/vite/plugin.ts b/packages/react-router-dev/vite/plugin.ts
index 12b78831a4..b56dc54057 100644
--- a/packages/react-router-dev/vite/plugin.ts
+++ b/packages/react-router-dev/vite/plugin.ts
@@ -2,6 +2,7 @@
 // context but want to use Vite's ESM build to avoid deprecation warnings
 import type * as Vite from "vite";
 import { type BinaryLike, createHash } from "node:crypto";
+import * as fs from "node:fs";
 import * as path from "node:path";
 import * as url from "node:url";
 import * as fse from "fs-extra";
@@ -289,7 +290,6 @@ let virtual = {
   serverBuild: VirtualModule.create("server-build"),
   serverManifest: VirtualModule.create("server-manifest"),
   browserManifest: VirtualModule.create("browser-manifest"),
-  sriManifest: VirtualModule.create("sri-manifest"),
 };
 
 let invalidateVirtualModules = (viteDevServer: Vite.ViteDevServer) => {
@@ -817,6 +817,39 @@ export const reactRouterVitePlugin: ReactRouterVitePlugin = () => {
     return new Set([...cssUrlPaths, ...chunkAssetPaths]);
   };
 
+  let generateSriManifest = async (ctx: ReactRouterPluginContext) => {
+    let clientBuildDirectory = getClientBuildDirectory(ctx.reactRouterConfig);
+    // walk the client build directory and generate SRI hashes for all .js files
+    let entries = fs.readdirSync(clientBuildDirectory, {
+      withFileTypes: true,
+      recursive: true,
+    });
+    let sriManifest: ReactRouterManifest["sri"] = {};
+    for (const entry of entries) {
+      if (entry.isFile() && entry.name.endsWith(".js")) {
+        let contents;
+        try {
+          contents = await fse.readFile(
+            path.join(entry.path, entry.name),
+            "utf-8"
+          );
+        } catch (e) {
+          logger.error(`Failed to read file for SRI generation: ${entry.name}`);
+          throw e;
+        }
+        let hash = createHash("sha384")
+          .update(contents)
+          .digest()
+          .toString("base64");
+        let filepath = getVite().normalizePath(
+          path.relative(clientBuildDirectory, path.join(entry.path, entry.name))
+        );
+        sriManifest[`${ctx.publicPath}${filepath}`] = `sha384-${hash}`;
+      }
+    }
+    return sriManifest;
+  };
+
   let generateReactRouterManifestsForBuild = async ({
     routeIds,
   }: {
@@ -939,6 +972,7 @@ export const reactRouterVitePlugin: ReactRouterVitePlugin = () => {
     let reactRouterBrowserManifest: ReactRouterManifest = {
       ...fingerprintedValues,
       ...nonFingerprintedValues,
+      sri: undefined,
     };
 
     // Write the browser manifest to disk as part of the build process
@@ -949,12 +983,18 @@ export const reactRouterVitePlugin: ReactRouterVitePlugin = () => {
       )};`
     );
 
+    let sri: ReactRouterManifest["sri"] = undefined;
+    if (ctx.reactRouterConfig.future.unstable_subResourceIntegrity) {
+      sri = await generateSriManifest(ctx);
+    }
+
     // The server manifest is the same as the browser manifest, except for
     // server bundle builds which only includes routes for the current bundle,
     // otherwise the server and client have the same routes
     let reactRouterServerManifest: ReactRouterManifest = {
       ...reactRouterBrowserManifest,
       routes: serverRoutes,
+      sri,
     };
 
     return {
@@ -1032,6 +1072,8 @@ export const reactRouterVitePlugin: ReactRouterVitePlugin = () => {
       };
     }
 
+    let sri: ReactRouterManifest["sri"] = undefined;
+
     let reactRouterManifestForDev = {
       version: String(Math.random()),
       url: combineURLs(ctx.publicPath, virtual.browserManifest.url),
@@ -1045,6 +1087,7 @@ export const reactRouterVitePlugin: ReactRouterVitePlugin = () => {
         ),
         imports: [],
       },
+      sri,
       routes,
     };
 
@@ -1864,36 +1907,6 @@ export const reactRouterVitePlugin: ReactRouterVitePlugin = () => {
             let routeIds = getServerBundleRouteIds(this, ctx);
             return await getServerEntry({ routeIds });
           }
-          case virtual.sriManifest.resolvedId: {
-            let environmentName = ctx.reactRouterConfig.future
-              .unstable_viteEnvironmentApi
-              ? this.environment.name
-              : ctx.environmentBuildContext?.name;
-            if (
-              viteCommand !== "build" ||
-              !environmentName ||
-              (environmentName !== "ssr" &&
-                !isSsrBundleEnvironmentName(environmentName))
-            ) {
-              return `export default {};`;
-            }
-
-            let viteManifest = await loadViteManifest(
-              getClientBuildDirectory(ctx.reactRouterConfig)
-            );
-            let clientBuildDirectory = getClientBuildDirectory(
-              ctx.reactRouterConfig
-            );
-
-            let integrityMap = createSubResourceIntegrityMap(
-              viteManifest,
-              "sha384",
-              clientBuildDirectory,
-              ctx.publicPath
-            );
-
-            return `export default ${jsesc(integrityMap, { es6: true })};`;
-          }
           case virtual.serverManifest.resolvedId: {
             let routeIds = getServerBundleRouteIds(this, ctx);
             let reactRouterManifest =
diff --git a/packages/react-router/lib/dom/ssr/components.tsx b/packages/react-router/lib/dom/ssr/components.tsx
index 4ffee08858..8a38787f9e 100644
--- a/packages/react-router/lib/dom/ssr/components.tsx
+++ b/packages/react-router/lib/dom/ssr/components.tsx
@@ -618,9 +618,7 @@ export type ScriptsProps = Omit<
   | "noModule"
   | "dangerouslySetInnerHTML"
   | "suppressHydrationWarning"
-> & {
-  integrity?: Record<string, string>;
-};
+>;
 
 /**
   Renders the client runtime of your app. It should be rendered inside the `<body>` of the document.
@@ -644,7 +642,7 @@ export type ScriptsProps = Omit<
 
   @category Components
  */
-export function Scripts({ integrity, ...props }: ScriptsProps) {
+export function Scripts(props: ScriptsProps) {
   let { manifest, serverHandoffString, isSpaMode, ssr, renderMeta } =
     useFrameworkContext();
   let { router, static: isStatic, staticContext } = useDataRouterContext();
@@ -786,18 +784,29 @@ import(${JSON.stringify(manifest.entry.module)});`;
 
   return isHydrated ? null : (
     <>
+      {manifest.sri ? (
+        <script
+          type="importmap"
+          dangerouslySetInnerHTML={{
+            __html: JSON.stringify({
+              integrity: manifest.sri,
+            }),
+          }}
+        />
+      ) : null}
       {!enableFogOfWar ? (
         <link
           rel="modulepreload"
           href={manifest.url}
           crossOrigin={props.crossOrigin}
+          integrity={manifest.sri?.[manifest.url]}
         />
       ) : null}
       <link
         rel="modulepreload"
         href={manifest.entry.module}
         crossOrigin={props.crossOrigin}
-        integrity={integrity?.[manifest.entry.module]}
+        integrity={manifest.sri?.[manifest.entry.module]}
       />
       {dedupe(preloads).map((path) => (
         <link
@@ -805,7 +814,7 @@ import(${JSON.stringify(manifest.entry.module)});`;
           rel="modulepreload"
           href={path}
           crossOrigin={props.crossOrigin}
-          integrity={integrity?.[path]}
+          integrity={manifest.sri?.[path]}
         />
       ))}
       {initialScripts}
diff --git a/packages/react-router/lib/dom/ssr/entry.ts b/packages/react-router/lib/dom/ssr/entry.ts
index 1bde4c5f0c..d5302f5af4 100644
--- a/packages/react-router/lib/dom/ssr/entry.ts
+++ b/packages/react-router/lib/dom/ssr/entry.ts
@@ -59,4 +59,5 @@ export interface AssetsManifest {
     timestamp?: number;
     runtime: string;
   };
+  sri?: Record<string, string>;
 }
diff --git a/playground/framework/app/root.tsx b/playground/framework/app/root.tsx
index 4adfe5a28c..3e25dac0ba 100644
--- a/playground/framework/app/root.tsx
+++ b/playground/framework/app/root.tsx
@@ -1,28 +1,19 @@
 import { Links, Meta, Outlet, Scripts, ScrollRestoration } from "react-router";
 
-import integrity from "virtual:react-router/sri-manifest";
-
 export function Layout({ children }: { children: React.ReactNode }) {
   return (
     <html lang="en">
       <head>
         <meta charSet="utf-8" />
         <meta name="viewport" content="width=device-width, initial-scale=1" />
-        <script
-          type="importmap"
-          dangerouslySetInnerHTML={{
-            __html: JSON.stringify({
-              integrity,
-            }),
-          }}
-        />
+        
         <Meta />
         <Links />
       </head>
       <body>
         {children}
         <ScrollRestoration />
-        <Scripts integrity={integrity} />
+        <Scripts />
       </body>
     </html>
   );
diff --git a/playground/framework/react-router.config.ts b/playground/framework/react-router.config.ts
index 8fcafa6494..d0da26c798 100644
--- a/playground/framework/react-router.config.ts
+++ b/playground/framework/react-router.config.ts
@@ -1,3 +1,7 @@
 import type { Config } from "@react-router/dev/config";
 
-export default {} satisfies Config;
+export default {
+  future: {
+    unstable_subResourceIntegrity: true,
+  },
+} satisfies Config;

From dcaf200234ad7e1e9fbf8a91536d408359e01b24 Mon Sep 17 00:00:00 2001
From: Jacob Ebey <jacob.ebey@live.com>
Date: Wed, 5 Mar 2025 13:34:42 -0800
Subject: [PATCH 03/10] fix lockfile

---
 pnpm-lock.yaml | 15 ++++-----------
 1 file changed, 4 insertions(+), 11 deletions(-)

diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index e5d6860472..af64508cc4 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -884,7 +884,7 @@ importers:
         version: 2.4.7
       esbuild-register:
         specifier: ^3.6.0
-        version: 3.6.0(esbuild@0.17.19)
+        version: 3.6.0(esbuild@0.25.0)
       execa:
         specifier: 5.1.1
         version: 5.1.1
@@ -1437,7 +1437,7 @@ importers:
     devDependencies:
       '@cloudflare/vite-plugin':
         specifier: ^0.1.1
-        version: 0.1.1(vite@6.1.1(@types/node@20.11.30)(jiti@1.21.0)(yaml@2.6.0))(workerd@1.20241230.0)(wrangler@3.109.2(@cloudflare/workers-types@4.20250214.0))
+        version: 0.1.1(vite@6.1.1(@types/node@20.11.30)(jiti@1.21.0)(tsx@4.19.3)(yaml@2.6.0))(workerd@1.20241230.0)(wrangler@3.109.2(@cloudflare/workers-types@4.20250214.0))
       '@cloudflare/workers-types':
         specifier: ^4.20250214.0
         version: 4.20250214.0
@@ -1464,10 +1464,10 @@ importers:
         version: 5.4.5
       vite:
         specifier: ^6.1.0
-        version: 6.1.1(@types/node@20.11.30)(jiti@1.21.0)(yaml@2.6.0)
+        version: 6.1.1(@types/node@20.11.30)(jiti@1.21.0)(tsx@4.19.3)(yaml@2.6.0)
       vite-tsconfig-paths:
         specifier: ^4.2.1
-        version: 4.3.2(typescript@5.4.5)(vite@6.1.1(@types/node@20.11.30)(jiti@1.21.0)(yaml@2.6.0))
+        version: 4.3.2(typescript@5.4.5)(vite@6.1.1(@types/node@20.11.30)(jiti@1.21.0)(tsx@4.19.3)(yaml@2.6.0))
       wrangler:
         specifier: ^3.109.2
         version: 3.109.2(@cloudflare/workers-types@4.20250214.0)
@@ -12705,13 +12705,6 @@ snapshots:
       is-date-object: 1.0.5
       is-symbol: 1.0.4
 
-  esbuild-register@3.6.0(esbuild@0.17.19):
-    dependencies:
-      debug: 4.4.0
-      esbuild: 0.17.19
-    transitivePeerDependencies:
-      - supports-color
-
   esbuild-register@3.6.0(esbuild@0.25.0):
     dependencies:
       debug: 4.4.0

From 35027c3befce4ce3d07ba1d5d517fb2e43e8bd03 Mon Sep 17 00:00:00 2001
From: Jacob Ebey <jacob.ebey@live.com>
Date: Wed, 5 Mar 2025 14:42:33 -0800
Subject: [PATCH 04/10] update types

---
 packages/react-router/lib/dom/ssr/entry.ts             | 1 +
 packages/react-router/lib/dom/ssr/routes-test-stub.tsx | 1 +
 2 files changed, 2 insertions(+)

diff --git a/packages/react-router/lib/dom/ssr/entry.ts b/packages/react-router/lib/dom/ssr/entry.ts
index d5302f5af4..2da6ba5864 100644
--- a/packages/react-router/lib/dom/ssr/entry.ts
+++ b/packages/react-router/lib/dom/ssr/entry.ts
@@ -42,6 +42,7 @@ export interface EntryContext extends FrameworkContextObject {
 }
 
 export interface FutureConfig {
+  unstable_subResourceIntegrity: boolean;
   unstable_middleware: boolean;
 }
 
diff --git a/packages/react-router/lib/dom/ssr/routes-test-stub.tsx b/packages/react-router/lib/dom/ssr/routes-test-stub.tsx
index 1babdd989b..9736d914d0 100644
--- a/packages/react-router/lib/dom/ssr/routes-test-stub.tsx
+++ b/packages/react-router/lib/dom/ssr/routes-test-stub.tsx
@@ -100,6 +100,7 @@ export function createRoutesStub(
     if (routerRef.current == null) {
       remixContextRef.current = {
         future: {
+          unstable_subResourceIntegrity: future?.unstable_subResourceIntegrity === true,
           unstable_middleware: future?.unstable_middleware === true,
         },
         manifest: {

From f3849b238b6dbfe8321df536ef4bd73368b94e52 Mon Sep 17 00:00:00 2001
From: Jacob Ebey <jacob.ebey@live.com>
Date: Wed, 5 Mar 2025 20:21:38 -0800
Subject: [PATCH 05/10] remove unused function

---
 packages/react-router-dev/vite/plugin.ts | 18 ------------------
 1 file changed, 18 deletions(-)

diff --git a/packages/react-router-dev/vite/plugin.ts b/packages/react-router-dev/vite/plugin.ts
index b56dc54057..3a3a5e7270 100644
--- a/packages/react-router-dev/vite/plugin.ts
+++ b/packages/react-router-dev/vite/plugin.ts
@@ -3541,21 +3541,3 @@ async function getEnvironmentsOptions(
 function isNonNullable<T>(x: T): x is NonNullable<T> {
   return x != null;
 }
-
-function createSubResourceIntegrityMap(
-  viteManifest: Vite.Manifest,
-  algorithm: string,
-  outdir: string,
-  publicPath: string
-) {
-  let map: Record<string, string> = {};
-  for (let value of Object.values(viteManifest)) {
-    let file = path.resolve(outdir, value.file);
-    if (!fse.existsSync(file)) continue;
-    let source = fse.readFileSync(file);
-    let hash = createHash("sha384").update(source).digest().toString("base64");
-    let url = `${publicPath}${value.file}`;
-    map[url] = `${algorithm.toLowerCase()}-${hash}`;
-  }
-  return map;
-}

From 1f21bdfb5a3d3a6771941c25929630f7bb8e7e57 Mon Sep 17 00:00:00 2001
From: Jacob Ebey <jacob.ebey@live.com>
Date: Thu, 6 Mar 2025 10:07:17 -0800
Subject: [PATCH 06/10] do not transport SRI info in manifest, only in the
 importmap

---
 .../react-router/lib/dom/ssr/components.tsx   | 29 ++++++++++++-------
 packages/react-router/lib/dom/ssr/entry.ts    |  2 +-
 .../react-router/lib/dom/ssr/fog-of-war.ts    |  3 +-
 playground/framework/app/root.tsx             | 23 +++++++++++++--
 4 files changed, 42 insertions(+), 15 deletions(-)

diff --git a/packages/react-router/lib/dom/ssr/components.tsx b/packages/react-router/lib/dom/ssr/components.tsx
index 8a38787f9e..ca1c2d0dcb 100644
--- a/packages/react-router/lib/dom/ssr/components.tsx
+++ b/packages/react-router/lib/dom/ssr/components.tsx
@@ -776,21 +776,28 @@ import(${JSON.stringify(manifest.entry.module)});`;
 
   let preloads = isHydrated
     ? []
-    : manifest.entry.imports.concat(
-        getModuleLinkHrefs(matches, manifest, {
-          includeHydrateFallback: true,
-        })
+    : dedupe(
+        manifest.entry.imports.concat(
+          getModuleLinkHrefs(matches, manifest, {
+            includeHydrateFallback: true,
+          })
+        )
       );
 
+  let sri = typeof manifest.sri === "object" ? manifest.sri : {};
+
   return isHydrated ? null : (
     <>
       {manifest.sri ? (
         <script
           type="importmap"
+          suppressHydrationWarning
           dangerouslySetInnerHTML={{
-            __html: JSON.stringify({
-              integrity: manifest.sri,
-            }),
+            __html: sri
+              ? JSON.stringify({
+                  integrity: sri,
+                })
+              : "",
           }}
         />
       ) : null}
@@ -799,22 +806,22 @@ import(${JSON.stringify(manifest.entry.module)});`;
           rel="modulepreload"
           href={manifest.url}
           crossOrigin={props.crossOrigin}
-          integrity={manifest.sri?.[manifest.url]}
+          integrity={sri[manifest.url]}
         />
       ) : null}
       <link
         rel="modulepreload"
         href={manifest.entry.module}
         crossOrigin={props.crossOrigin}
-        integrity={manifest.sri?.[manifest.entry.module]}
+        integrity={sri[manifest.entry.module]}
       />
-      {dedupe(preloads).map((path) => (
+      {preloads.map((path) => (
         <link
           key={path}
           rel="modulepreload"
           href={path}
           crossOrigin={props.crossOrigin}
-          integrity={manifest.sri?.[path]}
+          integrity={sri[path]}
         />
       ))}
       {initialScripts}
diff --git a/packages/react-router/lib/dom/ssr/entry.ts b/packages/react-router/lib/dom/ssr/entry.ts
index 2da6ba5864..995cf50b0d 100644
--- a/packages/react-router/lib/dom/ssr/entry.ts
+++ b/packages/react-router/lib/dom/ssr/entry.ts
@@ -60,5 +60,5 @@ export interface AssetsManifest {
     timestamp?: number;
     runtime: string;
   };
-  sri?: Record<string, string>;
+  sri?: Record<string, string> | true;
 }
diff --git a/packages/react-router/lib/dom/ssr/fog-of-war.ts b/packages/react-router/lib/dom/ssr/fog-of-war.ts
index a6b7cf5b52..7222b15215 100644
--- a/packages/react-router/lib/dom/ssr/fog-of-war.ts
+++ b/packages/react-router/lib/dom/ssr/fog-of-war.ts
@@ -31,7 +31,7 @@ export function isFogOfWarEnabled(ssr: boolean) {
 }
 
 export function getPartialManifest(
-  manifest: AssetsManifest,
+  { sri, ...manifest }: AssetsManifest,
   router: DataRouter
 ) {
   // Start with our matches for this pathname
@@ -64,6 +64,7 @@ export function getPartialManifest(
   return {
     ...manifest,
     routes: initialRoutes,
+    sri: true,
   };
 }
 
diff --git a/playground/framework/app/root.tsx b/playground/framework/app/root.tsx
index 3e25dac0ba..46bce69a48 100644
--- a/playground/framework/app/root.tsx
+++ b/playground/framework/app/root.tsx
@@ -1,4 +1,11 @@
-import { Links, Meta, Outlet, Scripts, ScrollRestoration } from "react-router";
+import {
+  Link,
+  Links,
+  Meta,
+  Outlet,
+  Scripts,
+  ScrollRestoration,
+} from "react-router";
 
 export function Layout({ children }: { children: React.ReactNode }) {
   return (
@@ -6,11 +13,23 @@ export function Layout({ children }: { children: React.ReactNode }) {
       <head>
         <meta charSet="utf-8" />
         <meta name="viewport" content="width=device-width, initial-scale=1" />
-        
+
         <Meta />
         <Links />
       </head>
       <body>
+        <ul>
+          <li>
+            <Link prefetch="intent" to="/">
+              Home
+            </Link>
+          </li>
+          <li>
+            <Link prefetch="intent" to="/products/abc">
+              Product
+            </Link>
+          </li>
+        </ul>
         {children}
         <ScrollRestoration />
         <Scripts />

From 5a1c79193d7e8a4e6fc606c81c4ec79db1f34e41 Mon Sep 17 00:00:00 2001
From: Jacob Ebey <jacob.ebey@live.com>
Date: Tue, 1 Apr 2025 11:40:18 -0700
Subject: [PATCH 07/10] parse import map from document to avoid transporting it
 twice

---
 .../lib/dom-export/hydrated-router.tsx             | 13 +++++++++++++
 packages/react-router/lib/dom/ssr/components.tsx   | 14 ++++++++------
 2 files changed, 21 insertions(+), 6 deletions(-)

diff --git a/packages/react-router/lib/dom-export/hydrated-router.tsx b/packages/react-router/lib/dom-export/hydrated-router.tsx
index df0d1bca29..388820ccd7 100644
--- a/packages/react-router/lib/dom-export/hydrated-router.tsx
+++ b/packages/react-router/lib/dom-export/hydrated-router.tsx
@@ -50,6 +50,19 @@ function initSsrInfo(): void {
     window.__reactRouterManifest &&
     window.__reactRouterRouteModules
   ) {
+    if (window.__reactRouterManifest.sri === true) {
+      const importMap = document.querySelector("script[rr-importmap]");
+      if (importMap?.textContent) {
+        try {
+          window.__reactRouterManifest.sri = JSON.parse(
+            importMap.textContent
+          ).integrity;
+        } catch (err) {
+          console.error("Failed to parse import map", err);
+        }
+      }
+    }
+
     ssrInfo = {
       context: window.__reactRouterContext,
       manifest: window.__reactRouterManifest,
diff --git a/packages/react-router/lib/dom/ssr/components.tsx b/packages/react-router/lib/dom/ssr/components.tsx
index dcda217ee3..0f4e39c7e9 100644
--- a/packages/react-router/lib/dom/ssr/components.tsx
+++ b/packages/react-router/lib/dom/ssr/components.tsx
@@ -797,16 +797,15 @@ import(${JSON.stringify(manifest.entry.module)});`;
 
   return isHydrated ? null : (
     <>
-      {manifest.sri ? (
+      {typeof manifest.sri === "object" ? (
         <script
+          rr-importmap=""
           type="importmap"
           suppressHydrationWarning
           dangerouslySetInnerHTML={{
-            __html: sri
-              ? JSON.stringify({
-                  integrity: sri,
-                })
-              : "",
+            __html: JSON.stringify({
+              integrity: sri,
+            }),
           }}
         />
       ) : null}
@@ -816,6 +815,7 @@ import(${JSON.stringify(manifest.entry.module)});`;
           href={manifest.url}
           crossOrigin={props.crossOrigin}
           integrity={sri[manifest.url]}
+          suppressHydrationWarning
         />
       ) : null}
       <link
@@ -823,6 +823,7 @@ import(${JSON.stringify(manifest.entry.module)});`;
         href={manifest.entry.module}
         crossOrigin={props.crossOrigin}
         integrity={sri[manifest.entry.module]}
+        suppressHydrationWarning
       />
       {preloads.map((path) => (
         <link
@@ -831,6 +832,7 @@ import(${JSON.stringify(manifest.entry.module)});`;
           href={path}
           crossOrigin={props.crossOrigin}
           integrity={sri[path]}
+          suppressHydrationWarning
         />
       ))}
       {initialScripts}

From 1fc59ba51a810d3975bcc992288825f7008ae66d Mon Sep 17 00:00:00 2001
From: Jacob Ebey <jacob.ebey@live.com>
Date: Tue, 1 Apr 2025 11:52:06 -0700
Subject: [PATCH 08/10] disable cache for now

---
 .github/workflows/shared-build.yml       | 3 ++-
 .github/workflows/shared-integration.yml | 3 ++-
 2 files changed, 4 insertions(+), 2 deletions(-)

diff --git a/.github/workflows/shared-build.yml b/.github/workflows/shared-build.yml
index 50bec16d99..6de9e0c61f 100644
--- a/.github/workflows/shared-build.yml
+++ b/.github/workflows/shared-build.yml
@@ -22,7 +22,8 @@ jobs:
           node-version-file: ".nvmrc"
           cache: "pnpm"
 
-      - uses: google/wireit@setup-github-actions-caching/v2
+      # TODO: Track and renable once this has been fixed: https://github.com/google/wireit/issues/1297
+      # - uses: google/wireit@setup-github-actions-caching/v2
 
       - name: Disable GitHub Actions Annotations
         run: |
diff --git a/.github/workflows/shared-integration.yml b/.github/workflows/shared-integration.yml
index ee0d86a340..2438029450 100644
--- a/.github/workflows/shared-integration.yml
+++ b/.github/workflows/shared-integration.yml
@@ -45,7 +45,8 @@ jobs:
           node-version: ${{ matrix.node }}
           cache: "pnpm"
 
-      - uses: google/wireit@setup-github-actions-caching/v2
+      # TODO: Track and renable once this has been fixed: https://github.com/google/wireit/issues/1297
+      # - uses: google/wireit@setup-github-actions-caching/v2
 
       - name: Disable GitHub Actions Annotations
         run: |

From a23ca0e28ba502874af0f576fb35b6601465b9f3 Mon Sep 17 00:00:00 2001
From: Jacob Ebey <jacob.ebey@live.com>
Date: Tue, 1 Apr 2025 11:54:29 -0700
Subject: [PATCH 09/10] disable cache for now

---
 .github/workflows/test.yml | 3 ++-
 1 file changed, 2 insertions(+), 1 deletion(-)

diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
index 7a364dd55c..af709fdaaa 100644
--- a/.github/workflows/test.yml
+++ b/.github/workflows/test.yml
@@ -45,7 +45,8 @@ jobs:
           cache: pnpm
           check-latest: true
 
-      - uses: google/wireit@setup-github-actions-caching/v2
+      # TODO: Track and renable once this has been fixed: https://github.com/google/wireit/issues/1297
+      # - uses: google/wireit@setup-github-actions-caching/v2
 
       - name: Disable GitHub Actions Annotations
         run: |

From 48084169da56beb1d800418e4f29cf4c48355caf Mon Sep 17 00:00:00 2001
From: Jacob Ebey <jacob.ebey@live.com>
Date: Tue, 1 Apr 2025 12:28:50 -0700
Subject: [PATCH 10/10] forward sri value through partial manifest

---
 packages/react-router/lib/dom/ssr/fog-of-war.ts | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/packages/react-router/lib/dom/ssr/fog-of-war.ts b/packages/react-router/lib/dom/ssr/fog-of-war.ts
index 7222b15215..c929bf1c13 100644
--- a/packages/react-router/lib/dom/ssr/fog-of-war.ts
+++ b/packages/react-router/lib/dom/ssr/fog-of-war.ts
@@ -64,7 +64,7 @@ export function getPartialManifest(
   return {
     ...manifest,
     routes: initialRoutes,
-    sri: true,
+    sri: sri ? true : undefined,
   };
 }