diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index 49d25fe4be..f4c76e5aef 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -72,6 +72,7 @@ jobs:
runs-on: ubuntu-latest
permissions:
contents: write # enable pushing changes to the origin
+ issues: write # enable applying labels to the release PR
pull-requests: write # enable opening a PR for the release
steps:
- name: Checkout
diff --git a/contributors.yml b/contributors.yml
index 06bbf577e4..c7d2621cd0 100644
--- a/contributors.yml
+++ b/contributors.yml
@@ -121,6 +121,7 @@
- dokeet
- doytch
- Drishtantr
+- DucMinhNe
- EdgeOneDev
- edmundhung
- edwin177
@@ -501,6 +502,7 @@
- yuleicul
- yuri-poliantsev
- zeevick10
+- Zelys-DFKH
- zeromask1337
- zeroqs
- zheng-chuang
diff --git a/docs/how-to/headers.md b/docs/how-to/headers.md
index c04c144080..4501951099 100644
--- a/docs/how-to/headers.md
+++ b/docs/how-to/headers.md
@@ -110,7 +110,8 @@ The easiest way is to simply append to the parent headers. This avoids overwriti
```tsx
export function headers({ parentHeaders }: HeadersArgs) {
parentHeaders.append(
- "Permissions-Policy: geolocation=()",
+ "Permissions-Policy",
+ "geolocation=()",
);
return parentHeaders;
}
diff --git a/integration/fog-of-war-test.ts b/integration/fog-of-war-test.ts
index e2ed5f57c9..5c415455e4 100644
--- a/integration/fog-of-war-test.ts
+++ b/integration/fog-of-war-test.ts
@@ -1531,6 +1531,97 @@ test.describe("Fog of War", () => {
expect(currentUrl).toContain("#section1");
});
+ test("Preserves meta tags on hash links in splat routes", async ({
+ page,
+ }) => {
+ let fixture = await createFixture({
+ files: {
+ "app/routes.ts": js`
+ import { type RouteConfig, index, route } from "@react-router/dev/routes";
+ export default [
+ index("routes/_index.tsx"),
+ route("*", "routes/catchall.tsx"),
+ ] satisfies RouteConfig;
+ `,
+ "app/root.tsx": js`
+ import { Links, Meta, Outlet, Scripts } from "react-router";
+ export default function Root() {
+ return (
+
+
+
+
+
+
+
+
+
+
+ );
+ }
+ `,
+ "app/routes/_index.tsx": js`
+ import { Link } from "react-router";
+ export function meta() {
+ return [{ title: "Home" }];
+ }
+ export default function Index() {
+ return (
+
+
Home
+
+ Go to catchall
+
+
+ );
+ }
+ `,
+ "app/routes/catchall.tsx": js`
+ import { Link } from "react-router";
+ export function meta() {
+ return [{ title: "Catchall" }];
+ }
+ export default function Catchall() {
+ return (
+
+
Catchall route
+ Hash link
+
+ );
+ }
+ `,
+ },
+ });
+
+ let appFixture = await createAppFixture(fixture);
+ let app = new PlaywrightFixture(appFixture, page);
+
+ // / => /catch-all => /catch-all#hash
+ await app.goto("/");
+ expect(await page.title()).toBe("Home");
+ await page.waitForSelector("[data-testid='go-catchall']");
+ await page.click("[data-testid='go-catchall']");
+ await page.waitForSelector("[data-testid='catchall-heading']");
+ expect(await page.title()).toBe("Catchall");
+
+ await page.click("[data-testid='hash-link']");
+ // Hash navigation doesn't trigger a load event; waitForFunction polls the DOM directly
+ await page.waitForFunction(() => window.location.hash === "#hash");
+ expect(await page.title()).toBe("Catchall");
+
+ // /catch-all => /catch-all#hash
+ await app.goto("/catchall");
+ await page.waitForSelector("[data-testid='catchall-heading']");
+ expect(await page.title()).toBe("Catchall");
+
+ await page.click("[data-testid='hash-link']");
+ // Hash navigation doesn't trigger a load event; waitForFunction polls the DOM directly
+ await page.waitForFunction(() => window.location.hash === "#hash");
+ expect(await page.title()).toBe("Catchall");
+
+ appFixture.close();
+ });
+
test.describe("routeDiscovery=initial", () => {
test("loads full manifest on initial load", async ({ page }) => {
let fixture = await createFixture({
diff --git a/packages/react-router/__tests__/router/fetchers-test.ts b/packages/react-router/__tests__/router/fetchers-test.ts
index fea1fc24b4..ba9db91f08 100644
--- a/packages/react-router/__tests__/router/fetchers-test.ts
+++ b/packages/react-router/__tests__/router/fetchers-test.ts
@@ -3807,7 +3807,7 @@ describe("fetchers", () => {
});
it("does not mutate the Map reference handed to subscribers (fetcher revalidation during navigation)", async () => {
- // getUpdatedRevalidatingFetchers() (dev branch) calls state.fetchers.set()
+ // getUpdatedRevalidatingFetchers() calls state.fetchers.set()
// on the current Map before returning a copy. This mutates MapPrev.
// Later, processLoaderData mutates the Map that subscribers received for
// the "loading" revalidation state. Test that the subscriber's loading
diff --git a/scripts/changes/pr.ts b/scripts/changes/pr.ts
index 875032e908..17cef96c93 100644
--- a/scripts/changes/pr.ts
+++ b/scripts/changes/pr.ts
@@ -7,7 +7,13 @@
* Environment:
* GITHUB_TOKEN - Required (unless --preview)
*/
-import { closePr, createPr, findOpenPr, updatePr } from "../utils/github.ts";
+import {
+ addPrLabels,
+ closePr,
+ createPr,
+ findOpenPr,
+ updatePr,
+} from "../utils/github.ts";
import { logAndExec } from "../utils/process.ts";
import type { PackageRelease } from "./changes.ts";
import {
@@ -25,6 +31,7 @@ if (!preview && !["main", "hotfix"].includes(baseBranch)) {
}
let prBranch = baseBranch === "hotfix" ? "hotfix-pr" : "release-pr";
+let prLabels = ["pkg:react-router"];
// GitHub has a 65,536 character limit for PR body. We use 60,000 to be safe.
let maxBodyLength = 60_000;
@@ -135,6 +142,13 @@ async function main() {
head: prBranch,
base: baseBranch,
});
+ try {
+ await addPrLabels(newPr.number, prLabels);
+ } catch (error) {
+ console.warn(
+ `⚠️ Unable to add labels (${prLabels.join(", ")}) to PR #${newPr.number}: ${getErrorMessage(error)}`,
+ );
+ }
console.log(`\n✅ Created PR #${newPr.number}: ${newPr.html_url}`);
}
}
@@ -213,6 +227,10 @@ function generatePackageChangelog(release: PackageRelease): string {
});
}
+function getErrorMessage(error: unknown): string {
+ return error instanceof Error ? error.message : String(error);
+}
+
function truncateChangelogs(
releases: PackageRelease[],
maxLength: number,
diff --git a/scripts/pr.ts b/scripts/pr.ts
index 42b5339630..a0e31a4ef2 100644
--- a/scripts/pr.ts
+++ b/scripts/pr.ts
@@ -132,7 +132,7 @@ async function runChecks() {
}
async function changeFileCheck(ctx: CheckContext): Promise {
- if (ctx.baseBranch !== "dev") return [];
+ if (ctx.baseBranch !== "main") return [];
if (!["opened", "synchronize", "reopened"].includes(ctx.eventAction)) {
return [];
}
diff --git a/scripts/utils/github.ts b/scripts/utils/github.ts
index 4338937465..594b2578fd 100644
--- a/scripts/utils/github.ts
+++ b/scripts/utils/github.ts
@@ -238,6 +238,17 @@ export async function deletePrComment(commentId: number) {
});
}
+/**
+ * Add labels to a PR (or issue)
+ */
+export async function addPrLabels(prNumber: number, labels: string[]) {
+ await request("POST /repos/{owner}/{repo}/issues/{issue_number}/labels", {
+ ...requestOptions(),
+ issue_number: prNumber,
+ labels,
+ });
+}
+
/**
* Remove a label from a PR (or issue)
*/