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, }; }