Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
60e382a
Align UNSAFE history exports (#14663)
brophdawg11 Dec 15, 2025
3277886
Merge branch 'release-next' into dev
brophdawg11 Dec 17, 2025
5ce5cd4
chore: format
remix-run-bot Dec 17, 2025
7ad6853
fix: properly handle cyclic nodes with getParentClientNodes (#14522)
BlankParticle Dec 18, 2025
915694e
fix(vite): Skip SSR middleware in preview server for SPA mode (#14673)
andreiborza Dec 18, 2025
1954af6
Preserve hydrate property on client loaders during instrumentation (#…
brophdawg11 Dec 19, 2025
7f140e0
Handle data requests with trailing slash consistently (#14644)
richardkall Dec 19, 2025
30f6c1d
fix(react-router): handle parameters with static suffixes in generate…
joseph0926 Dec 19, 2025
cbcbf30
fix: pass nonce to importmap script when using subResourceIntegrity (…
dimmageiras Dec 23, 2025
7394407
chore: format
remix-run-bot Dec 23, 2025
041c7b1
Accept future object directly in reactRouterConfig test helper (#14683)
brophdawg11 Jan 5, 2026
c89c32c
Escape HTML in scroll restoration keys (#14705)
brophdawg11 Jan 6, 2026
c05ef93
Validate redirect locations (#14706)
brophdawg11 Jan 6, 2026
75b1ef5
Add origin checks for UI route submissions (#14708)
brophdawg11 Jan 6, 2026
e51b518
Merge branch 'main' into release-next
brophdawg11 Jan 6, 2026
69d8361
Enter prerelease mode
brophdawg11 Jan 6, 2026
7ac2346
chore: Update version for release (pre) (#14709)
github-actions[bot] Jan 6, 2026
485cd78
Draft release notes
brophdawg11 Jan 6, 2026
29635f7
Update release notes
brophdawg11 Jan 6, 2026
5e15a93
Update release notes
brophdawg11 Jan 7, 2026
154bdb5
Exit prerelease mode
brophdawg11 Jan 7, 2026
26653a6
chore: Update version for release (#14712)
github-actions[bot] Jan 7, 2026
5d55300
Merge branch 'release-next'
brophdawg11 Jan 7, 2026
bfdc747
chore: format
remix-run-bot Jan 7, 2026
a8f3401
Copy 6.30.3 release notes to changelog
brophdawg11 Jan 7, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
381 changes: 226 additions & 155 deletions CHANGELOG.md

Large diffs are not rendered by default.

4 changes: 4 additions & 0 deletions contributors.yml
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
- amsal
- Andarist
- andreasottosson-polestar
- andreiborza
- andreiduca
- antonmontrezor
- appden
Expand All @@ -55,6 +56,7 @@
- bilalk711
- bjohn465
- black5box
- BlankParticle
- bmsuseluda
- bobziroll
- bravo-kernel
Expand Down Expand Up @@ -105,6 +107,7 @@
- dgrijuela
- DigitalNaut
- DimaAmega
- dimmageiras
- dmitrytarassov
- dogxii
- dokeet
Expand Down Expand Up @@ -338,6 +341,7 @@
- renyu-io
- reyronald
- RFCreate
- richardkall
- richardscarrott
- rifaidev
- rimian
Expand Down
2 changes: 1 addition & 1 deletion integration/client-data-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -149,7 +149,7 @@ test.describe("Client Data", () => {
templateName,
files: {
"react-router.config.ts": reactRouterConfig({
v8_splitRouteModules,
future: { v8_splitRouteModules },
}),
"app/root.tsx": js`
import { Form, Outlet, Scripts } from "react-router"
Expand Down
45 changes: 13 additions & 32 deletions integration/helpers/vite.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,42 +23,23 @@ const __dirname = url.fileURLToPath(new URL(".", import.meta.url));
const root = path.resolve(__dirname, "../..");
const TMP_DIR = path.join(root, ".tmp/integration");

export const reactRouterConfig = ({
ssr,
basename,
prerender,
appDirectory,
v8_middleware,
v8_splitRouteModules,
v8_viteEnvironmentApi,
routeDiscovery,
}: {
ssr?: boolean;
basename?: string;
prerender?: boolean | string[];
appDirectory?: string;
v8_middleware?: boolean;
v8_splitRouteModules?: NonNullable<Config["future"]>["v8_splitRouteModules"];
v8_viteEnvironmentApi?: boolean;
routeDiscovery?: Config["routeDiscovery"];
}) => {
let config: Config = {
ssr,
basename,
prerender,
appDirectory,
routeDiscovery,
future: {
v8_middleware,
v8_splitRouteModules,
v8_viteEnvironmentApi,
},
};
export const reactRouterConfig = (
// Don't support function configs due to JSON.stringify()
config: Omit<Partial<Config>, "buildEnd" | "presets" | "serverBundles">,
) => {
if (
typeof config.prerender === "function" ||
(typeof config.prerender === "object" &&
!Array.isArray(config.prerender) &&
typeof config.prerender.paths === "function")
) {
throw new Error("reactRouterConfig() does not support prerender functions");
}

return dedent`
import type { Config } from "@react-router/dev/config";

export default ${JSON.stringify(config)} satisfies Config;
export default ${JSON.stringify(config, null, 2)} satisfies Config;
`;
};

Expand Down
20 changes: 10 additions & 10 deletions integration/middleware-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ test.describe("Middleware", () => {
// ...existing code...
"react-router.config.ts": reactRouterConfig({
ssr: false,
v8_middleware: true,
future: { v8_middleware: true },
}),
"vite.config.ts": js`
import { defineConfig } from "vite";
Expand Down Expand Up @@ -368,8 +368,7 @@ test.describe("Middleware", () => {
files: {
"react-router.config.ts": reactRouterConfig({
ssr: false,
v8_middleware: true,
v8_splitRouteModules: true,
future: { v8_middleware: true, v8_splitRouteModules: true },
}),
"vite.config.ts": js`
import { defineConfig } from "vite";
Expand Down Expand Up @@ -466,7 +465,7 @@ test.describe("Middleware", () => {
fixture = await createFixture({
files: {
"react-router.config.ts": reactRouterConfig({
v8_middleware: true,
future: { v8_middleware: true },
}),
"vite.config.ts": js`
import { defineConfig } from "vite";
Expand Down Expand Up @@ -773,7 +772,7 @@ test.describe("Middleware", () => {
let fixture = await createFixture({
files: {
"react-router.config.ts": reactRouterConfig({
v8_middleware: true,
future: { v8_middleware: true },
}),
"vite.config.ts": js`
import { defineConfig } from "vite";
Expand Down Expand Up @@ -895,7 +894,7 @@ test.describe("Middleware", () => {
let fixture = await createFixture({
files: {
"react-router.config.ts": reactRouterConfig({
v8_middleware: true,
future: { v8_middleware: true },
}),
"vite.config.ts": js`
import { defineConfig } from "vite";
Expand Down Expand Up @@ -1060,8 +1059,7 @@ test.describe("Middleware", () => {
fixture = await createFixture({
files: {
"react-router.config.ts": reactRouterConfig({
v8_middleware: true,
v8_splitRouteModules: true,
future: { v8_middleware: true, v8_splitRouteModules: true },
}),
"vite.config.ts": js`
import { defineConfig } from "vite";
Expand Down Expand Up @@ -1156,7 +1154,9 @@ test.describe("Middleware", () => {
test.beforeAll(async () => {
fixture = await createFixture({
files: {
"react-router.config.ts": reactRouterConfig({ v8_middleware: true }),
"react-router.config.ts": reactRouterConfig({
future: { v8_middleware: true },
}),
"vite.config.ts": js`
import { defineConfig } from "vite";
import { reactRouter } from "@react-router/dev/vite";
Expand Down Expand Up @@ -1983,7 +1983,7 @@ test.describe("Middleware", () => {
{
files: {
"react-router.config.ts": reactRouterConfig({
v8_middleware: true,
future: { v8_middleware: true },
}),
"vite.config.ts": js`
import { defineConfig } from "vite";
Expand Down
189 changes: 189 additions & 0 deletions integration/single-fetch-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4476,4 +4476,193 @@ test.describe("single-fetch", () => {
await page.waitForSelector("h1");
expect(await app.getHtml("h1")).toMatch("It worked!");
});

test("always uses /{path}.data without future.unstable_trailingSlashAwareDataRequests flag", async ({
page,
}) => {
let fixture = await createFixture({
files: {
...files,
"app/routes/_index.tsx": js`
import { Link } from "react-router";

export default function Index() {
return (
<div>
<h1>Home</h1>
<Link to="/about/">Go to About (with trailing slash)</Link>
<Link to="/about">Go to About (without trailing slash)</Link>
</div>
);
}
`,
"app/routes/about.tsx": js`
import { Link, useLoaderData } from "react-router";

export function loader({ request }) {
let url = new URL(request.url);
return {
pathname: url.pathname,
hasTrailingSlash: url.pathname.endsWith("/"),
};
}

export default function About() {
let { pathname, hasTrailingSlash } = useLoaderData();
return (
<div>
<h1>About</h1>
<p id="pathname">{pathname}</p>
<p id="trailing-slash">{String(hasTrailingSlash)}</p>
<Link to="/">Go back home</Link>
</div>
);
}
`,
},
});
let appFixture = await createAppFixture(fixture);
let app = new PlaywrightFixture(appFixture, page);

let requests: string[] = [];
page.on("request", (req) => {
let url = new URL(req.url());
if (url.pathname.endsWith(".data")) {
requests.push(url.pathname + url.search);
}
});

// Document load without trailing slash
await app.goto("/about");
await page.waitForSelector("#pathname");
expect(await page.locator("#pathname").innerText()).toEqual("/about");
expect(await page.locator("#trailing-slash").innerText()).toEqual("false");

// Client-side navigation without trailing slash
await app.goto("/");
await app.clickLink("/about");
await page.waitForSelector("#pathname");
expect(await page.locator("#pathname").innerText()).toEqual("/about");
expect(await page.locator("#trailing-slash").innerText()).toEqual("false");
expect(requests).toEqual(["/about.data"]);
requests = [];

// Document load with trailing slash
await app.goto("/about/");
await page.waitForSelector("#pathname");
expect(await page.locator("#pathname").innerText()).toEqual("/about/");
expect(await page.locator("#trailing-slash").innerText()).toEqual("true");

// Client-side navigation with trailing slash
await app.goto("/");
await app.clickLink("/about/");
await page.waitForSelector("#pathname");
expect(await page.locator("#pathname").innerText()).toEqual("/about");
expect(await page.locator("#trailing-slash").innerText()).toEqual("false");
expect(requests).toEqual(["/about.data"]);
requests = [];

// Client-side navigation back to /
await app.clickLink("/");
await page.waitForSelector("h1:has-text('Home')");
expect(requests).toEqual(["/_root.data"]);
requests = [];
});

test("uses {path}.data or {path}/_.data depending on trailing slash with future.unstable_trailingSlashAwareDataRequests flag", async ({
page,
}) => {
let fixture = await createFixture({
files: {
...files,
"react-router.config.ts": reactRouterConfig({
future: {
unstable_trailingSlashAwareDataRequests: true,
},
}),
"app/routes/_index.tsx": js`
import { Link } from "react-router";

export default function Index() {
return (
<div>
<h1>Home</h1>
<Link to="/about/">Go to About (with trailing slash)</Link>
<Link to="/about">Go to About (without trailing slash)</Link>
</div>
);
}
`,
"app/routes/about.tsx": js`
import { Link, useLoaderData } from "react-router";

export function loader({ request }) {
let url = new URL(request.url);
return {
pathname: url.pathname,
hasTrailingSlash: url.pathname.endsWith("/"),
};
}

export default function About() {
let { pathname, hasTrailingSlash } = useLoaderData();
return (
<div>
<h1>About</h1>
<p id="pathname">{pathname}</p>
<p id="trailing-slash">{String(hasTrailingSlash)}</p>
<Link to="/">Go back home</Link>
</div>
);
}
`,
},
});
let appFixture = await createAppFixture(fixture);
let app = new PlaywrightFixture(appFixture, page);

let requests: string[] = [];
page.on("request", (req) => {
let url = new URL(req.url());
if (url.pathname.endsWith(".data")) {
requests.push(url.pathname + url.search);
}
});

// Document load without trailing slash
await app.goto("/about");
await page.waitForSelector("#pathname");
expect(await page.locator("#pathname").innerText()).toEqual("/about");
expect(await page.locator("#trailing-slash").innerText()).toEqual("false");

// Client-side navigation without trailing slash
await app.goto("/");
await app.clickLink("/about");
await page.waitForSelector("#pathname");
expect(await page.locator("#pathname").innerText()).toEqual("/about");
expect(await page.locator("#trailing-slash").innerText()).toEqual("false");
expect(requests).toEqual(["/about.data"]);
requests = [];

// Document load with trailing slash
await app.goto("/about/");
await page.waitForSelector("#pathname");
expect(await page.locator("#pathname").innerText()).toEqual("/about/");
expect(await page.locator("#trailing-slash").innerText()).toEqual("true");

// Client-side navigation with trailing slash
await app.goto("/");
await app.clickLink("/about/");
await page.waitForSelector("#pathname");
expect(await page.locator("#pathname").innerText()).toEqual("/about/");
expect(await page.locator("#trailing-slash").innerText()).toEqual("true");
expect(requests).toEqual(["/about/_.data"]);
requests = [];

// Client-side navigation back to /
await app.clickLink("/");
await page.waitForSelector("h1:has-text('Home')");
expect(requests).toEqual(["/_.data"]);
requests = [];
});
});
Loading