Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
1 change: 1 addition & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 2 additions & 0 deletions contributors.yml
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,7 @@
- dokeet
- doytch
- Drishtantr
- DucMinhNe
- EdgeOneDev
- edmundhung
- edwin177
Expand Down Expand Up @@ -501,6 +502,7 @@
- yuleicul
- yuri-poliantsev
- zeevick10
- Zelys-DFKH
- zeromask1337
- zeroqs
- zheng-chuang
Expand Down
3 changes: 2 additions & 1 deletion docs/how-to/headers.md
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down
91 changes: 91 additions & 0 deletions integration/fog-of-war-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
<html lang="en">
<head>
<Meta />
<Links />
</head>
<body>
<Outlet />
<Scripts />
</body>
</html>
);
}
`,
"app/routes/_index.tsx": js`
import { Link } from "react-router";
export function meta() {
return [{ title: "Home" }];
}
export default function Index() {
return (
<div>
<h1>Home</h1>
<Link to="/catchall" data-testid="go-catchall">
Go to catchall
</Link>
</div>
);
}
`,
"app/routes/catchall.tsx": js`
import { Link } from "react-router";
export function meta() {
return [{ title: "Catchall" }];
}
export default function Catchall() {
return (
<div>
<h1 data-testid="catchall-heading">Catchall route</h1>
<Link to="#hash" data-testid="hash-link">Hash link</Link>
</div>
);
}
`,
},
});

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({
Expand Down
2 changes: 1 addition & 1 deletion packages/react-router/__tests__/router/fetchers-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
20 changes: 19 additions & 1 deletion scripts/changes/pr.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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;
Expand Down Expand Up @@ -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}`);
}
}
Expand Down Expand Up @@ -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,
Expand Down
2 changes: 1 addition & 1 deletion scripts/pr.ts
Original file line number Diff line number Diff line change
Expand Up @@ -132,7 +132,7 @@ async function runChecks() {
}

async function changeFileCheck(ctx: CheckContext): Promise<Action[]> {
if (ctx.baseBranch !== "dev") return [];
if (ctx.baseBranch !== "main") return [];
if (!["opened", "synchronize", "reopened"].includes(ctx.eventAction)) {
return [];
}
Expand Down
11 changes: 11 additions & 0 deletions scripts/utils/github.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
*/
Expand Down