diff --git a/.changeset/little-timers-occur.md b/.changeset/little-timers-occur.md new file mode 100644 index 0000000000..1626984a85 --- /dev/null +++ b/.changeset/little-timers-occur.md @@ -0,0 +1,5 @@ +--- +"react-router": patch +--- + +[UNSTABLE] Pass `` value through to the underlying `importmap` `script` tag when using `future.unstable_subResourceIntegrity` diff --git a/contributors.yml b/contributors.yml index e8a8cd30dd..0a88bcb7ab 100644 --- a/contributors.yml +++ b/contributors.yml @@ -106,6 +106,7 @@ - developit - dgrijuela - DigitalNaut +- dimmageiras - DimaAmega - dmitrytarassov - dogxii diff --git a/integration/helpers/vite.ts b/integration/helpers/vite.ts index 9ad644d43b..1f985a433f 100644 --- a/integration/helpers/vite.ts +++ b/integration/helpers/vite.ts @@ -31,6 +31,7 @@ export const reactRouterConfig = ({ v8_middleware, v8_splitRouteModules, v8_viteEnvironmentApi, + unstable_subResourceIntegrity, routeDiscovery, }: { ssr?: boolean; @@ -40,6 +41,7 @@ export const reactRouterConfig = ({ v8_middleware?: boolean; v8_splitRouteModules?: NonNullable["v8_splitRouteModules"]; v8_viteEnvironmentApi?: boolean; + unstable_subResourceIntegrity?: boolean; routeDiscovery?: Config["routeDiscovery"]; }) => { let config: Config = { @@ -52,6 +54,7 @@ export const reactRouterConfig = ({ v8_middleware, v8_splitRouteModules, v8_viteEnvironmentApi, + unstable_subResourceIntegrity, }, }; diff --git a/integration/sri-test.ts b/integration/sri-test.ts new file mode 100644 index 0000000000..20a1fa477d --- /dev/null +++ b/integration/sri-test.ts @@ -0,0 +1,74 @@ +import { test, expect } from "@playwright/test"; + +import { + createAppFixture, + createFixture, + js, +} from "./helpers/create-fixture.js"; +import type { AppFixture, Fixture } from "./helpers/create-fixture.js"; +import { reactRouterConfig } from "./helpers/vite.js"; +import { PlaywrightFixture } from "./helpers/playwright-fixture.js"; + +test.describe("CSub-Resource Integrity", () => { + test.use({ javaScriptEnabled: false }); + + let fixture: Fixture; + let appFixture: AppFixture; + + test.beforeAll(async () => { + fixture = await createFixture({ + files: { + "react-router.config.ts": reactRouterConfig({ + unstable_subResourceIntegrity: true, + }), + "app/root.tsx": js` + import { Links, Meta, Outlet, Scripts } from "react-router"; + + export default function Root() { + return ( + + + + + + + + + + + ); + } + `, + }, + }); + appFixture = await createAppFixture(fixture); + }); + + test("includes an importmap", async ({ page }) => { + let app = new PlaywrightFixture(appFixture, page); + await app.goto("/"); + let json = await page.locator('script[type="importmap"]').innerText(); + let importMap = JSON.parse(json); + expect(Object.keys(importMap.integrity).length).toBeGreaterThan(0); + for (let key in importMap.integrity) { + if (key.includes("manifest")) continue; + let value = importMap.integrity[key]; + expect(value).toMatch(/^sha384-/); + + let linkEl = page.locator(`link[rel="modulepreload"][href="${key}"]`); + expect(await linkEl.getAttribute("href")).toBe(key); + expect(await linkEl.getAttribute("integrity")).toBe(value); + + let scriptEl = page.locator(`script[type="module"]`); + expect(await scriptEl.innerText()).toContain(key); + } + }); + + test("includes a nonce on the importmap script", async ({ page }) => { + let app = new PlaywrightFixture(appFixture, page); + await app.goto("/"); + expect( + await page.locator('script[type="importmap"]').getAttribute("nonce"), + ).toBe("test-nonce-123"); + }); +}); diff --git a/packages/react-router/lib/dom/ssr/components.tsx b/packages/react-router/lib/dom/ssr/components.tsx index 57edd93007..3e9349a015 100644 --- a/packages/react-router/lib/dom/ssr/components.tsx +++ b/packages/react-router/lib/dom/ssr/components.tsx @@ -901,6 +901,7 @@ import(${JSON.stringify(manifest.entry.module)});`; <> {typeof manifest.sri === "object" ? (