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) */