From 81694a5ae39067093266df15f8b7d8b67b3d64c4 Mon Sep 17 00:00:00 2001 From: Jacob Ebey 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(x: T): x is NonNullable { return x != null; } + +function createSubResourceIntegrityMap( + viteManifest: Vite.Manifest, + algorithm: string, + outdir: string, + publicPath: string +) { + let map: Record = {}; + 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; +}; /** Renders the client runtime of your app. It should be rendered inside the `` 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) => ( ))} {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 ( +