Skip to content
Closed
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
95 changes: 95 additions & 0 deletions .github/workflows/release-nightly.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
name: 🌒 Nightly Release

on:
workflow_dispatch:
schedule:
- cron: "0 7 * * *" # every day at 12AM PST

concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true

env:
CI: true

jobs:
# HEADS UP! this "nightly" job will only ever run on the `main` branch due to
# it being a cron job, and the last commit on main will be what github shows
# as the trigger however in the checkout below we specify the `v7` branch,
# so all the scripts in this job will be ran from that, confusing i know, so
# in some cases we'll need to create multiple PRs when modifying nightly
# release processes
nightly:
name: 🌒 Nightly Release
if: github.repository == 'remix-run/react-router'
runs-on: ubuntu-latest
outputs:
# allows this to be used in the `comment` job below - will be undefined
# if there's no release necessary
NEXT_VERSION: ${{ steps.version.outputs.NEXT_VERSION }}
steps:
- name: ⬇️ Checkout repo
uses: actions/checkout@v4
with:
ref: v7
# checkout using a custom token so that we can push later on
token: ${{ secrets.GITHUB_TOKEN }}
fetch-depth: 0

- name: 📦 Setup pnpm
uses: pnpm/[email protected]

- name: ⎔ Setup node
uses: actions/setup-node@v4
with:
node-version-file: ".nvmrc"
cache: "pnpm"

- name: 📥 Install deps
run: pnpm install --frozen-lockfile

- name: 🕵️ Check for changes
id: version
run: |
SHORT_SHA=$(git rev-parse --short HEAD)

# get latest nightly tag
LATEST_NIGHTLY_TAG=$(git tag -l v0.0.0-nightly-\* --sort=-creatordate | head -n 1)

# check if last commit to v7 starts with the nightly tag we're about
# to create (minus the date)
# if it is, we'll skip the nightly creation
# if not, we'll create a new nightly tag
if [[ ${LATEST_NIGHTLY_TAG} == v0.0.0-nightly-${SHORT_SHA}-* ]]; then
echo "🛑 Latest nightly tag is the same as the latest commit sha, skipping nightly release"
else
# yyyyMMdd format (e.g. 20221207)
DATE=$(date '+%Y%m%d')
# v0.0.0-nightly-<short sha>-<date>
NEXT_VERSION=0.0.0-nightly-${SHORT_SHA}-${DATE}
# set output so it can be used in other jobs
echo "NEXT_VERSION=${NEXT_VERSION}" >> $GITHUB_OUTPUT
fi

- name: ⤴️ Update version
if: steps.version.outputs.NEXT_VERSION
run: |
git config --local user.email "[email protected]"
git config --local user.name "Remix Run Bot"
git checkout -b nightly/${{ steps.version.outputs.NEXT_VERSION }}
pnpm run version ${{steps.version.outputs.NEXT_VERSION}}
git push origin --tags

- name: 🏗 Build
if: steps.version.outputs.NEXT_VERSION
run: pnpm build

- name: 🔐 Setup npm auth
if: steps.version.outputs.NEXT_VERSION
run: |
echo "registry=https://registry.npmjs.org" >> ~/.npmrc
echo "//registry.npmjs.org/:_authToken=${{ secrets.NPM_TOKEN }}" >> ~/.npmrc

- name: 🚀 Publish
if: steps.version.outputs.NEXT_VERSION
run: pnpm run publish
4 changes: 2 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -491,7 +491,7 @@ Date: 2023-10-16

#### View Transitions 🚀

We're excited to release experimental support for the the [View Transitions API](https://developer.mozilla.org/en-US/docs/Web/API/ViewTransition) in React Router! You can now trigger navigational DOM updates to be wrapped in `document.startViewTransition` to enable CSS animated transitions on SPA navigations in your application.
We're excited to release experimental support for the [View Transitions API](https://developer.mozilla.org/en-US/docs/Web/API/ViewTransition) in React Router! You can now trigger navigational DOM updates to be wrapped in `document.startViewTransition` to enable CSS animated transitions on SPA navigations in your application.

The simplest approach to enabling a View Transition in your React Router app is via the new [`<Link unstable_viewTransition>`](https://reactrouter.com/components/link#unstable_viewtransition) prop. This will cause the navigation DOM update to be wrapped in `document.startViewTransition` which will enable transitions for the DOM update. Without any additional CSS styles, you'll get a basic cross-fade animation for your page.

Expand Down Expand Up @@ -1046,7 +1046,7 @@ Support absolute URLs in `<Link to>`. If the URL is for the current origin, it w

- Fixes 2 separate issues for revalidating fetcher `shouldRevalidate` calls ([#9948](https://github.com/remix-run/react-router/pull/9948))
- The `shouldRevalidate` function was only being called for _explicit_ revalidation scenarios (after a mutation, manual `useRevalidator` call, or an `X-Remix-Revalidate` header used for cookie setting in Remix). It was not properly being called on _implicit_ revalidation scenarios that also apply to navigation `loader` revalidation, such as a change in search params or clicking a link for the page we're already on. It's now correctly called in those additional scenarios.
- The parameters being passed were incorrect and inconsistent with one another since the `current*`/`next*` parameters reflected the static `fetcher.load` URL (and thus were identical). Instead, they should have reflected the the navigation that triggered the revalidation (as the `form*` parameters did). These parameters now correctly reflect the triggering navigation.
- The parameters being passed were incorrect and inconsistent with one another since the `current*`/`next*` parameters reflected the static `fetcher.load` URL (and thus were identical). Instead, they should have reflected the navigation that triggered the revalidation (as the `form*` parameters did). These parameters now correctly reflect the triggering navigation.
- Fix bug with search params removal via `useSearchParams` ([#9969](https://github.com/remix-run/react-router/pull/9969))
- Respect `preventScrollReset` on `<fetcher.Form>` ([#9963](https://github.com/remix-run/react-router/pull/9963))
- Fix navigation for hash routers on manual URL changes ([#9980](https://github.com/remix-run/react-router/pull/9980))
Expand Down
2 changes: 2 additions & 0 deletions contributors.yml
Original file line number Diff line number Diff line change
Expand Up @@ -190,6 +190,7 @@
- nnhjs
- noisypigeon
- Obi-Dann
- omahs
- omar-moquete
- p13i
- parched
Expand Down Expand Up @@ -263,3 +264,4 @@
- yracnet
- yuleicul
- zheng-chuang
- modi98
2 changes: 1 addition & 1 deletion docs/components/form.md
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ function ProjectsPage() {
</DataBrowserRouter>;
```

If the the current URL is `"/projects/123"`, the form inside the child
If the current URL is `"/projects/123"`, the form inside the child
route, `ProjectsPage`, will have a default action as you might expect: `"/projects/123"`. In this case, where the route is the deepest matching route, both `<Form>` and plain HTML forms have the same result.

But the form inside of `ProjectsLayout` will point to `"/projects"`, not the full URL. In other words, it points to the matching segment of the URL for the route in which the form is rendered.
Expand Down
2 changes: 1 addition & 1 deletion docs/hooks/use-navigate.md
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@ function EditContact() {
}
```

Please note that `relative: "path"` only impacts the resolution of a relative path. It does not change the the "starting" location for that relative path resolution. This resolution is always relative to the current location in the Route hierarchy (i.e., the route `useNavigate` is called in).
Please note that `relative: "path"` only impacts the resolution of a relative path. It does not change the "starting" location for that relative path resolution. This resolution is always relative to the current location in the Route hierarchy (i.e., the route `useNavigate` is called in).

If you wish to use path-relative routing against the current URL instead of the route hierarchy, you can do that with the current [`location`][use-location] and the `URL` constructor (note the trailing slash behavior):

Expand Down
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -108,10 +108,10 @@
"none": "52.8 kB"
},
"packages/react-router/dist/react-router.production.min.js": {
"none": "14.8 kB"
"none": "14.82 kB"
},
"packages/react-router/dist/umd/react-router.production.min.js": {
"none": "17.21 kB"
"none": "17.24 kB"
},
"packages/react-router-dom/dist/react-router-dom.production.min.js": {
"none": "17.1 kB"
Expand Down
28 changes: 28 additions & 0 deletions packages/react-router/__tests__/Routes-test.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import * as React from "react";
import * as TestRenderer from "react-test-renderer";
import { MemoryRouter, Routes, Route } from "react-router";
import type { PathPattern} from '@remix-run/router';
import { matchPath } from '@remix-run/router';

describe("<Routes>", () => {
let consoleWarn: jest.SpyInstance;
Expand Down Expand Up @@ -145,4 +147,30 @@ describe("<Routes>", () => {

expect(consoleError).toHaveBeenCalledTimes(1);
});

it("matches a route based on the customMatchPath prop", () => {
let renderer: TestRenderer.ReactTestRenderer;

const customMatchPath = <Path extends string>
(pattern: PathPattern | Path, pathname: string) => {
if (pathname.length > 5) return matchPath(pattern, pathname);
return null;
};

TestRenderer.act(() => {
renderer = TestRenderer.create(
<MemoryRouter initialEntries={["/slug"]}>
<Routes customMatchPath={customMatchPath}>
<Route path="/:slug" element={<h1>Home</h1>} />
</Routes>
</MemoryRouter>
);
});

expect(renderer.toJSON()).toBeNull();
expect(consoleWarn).toHaveBeenCalledTimes(1);
expect(consoleWarn).toHaveBeenCalledWith(
expect.stringContaining("No routes matched location")
);
});
});
10 changes: 9 additions & 1 deletion packages/react-router/lib/components.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@ import type {
LazyRouteFunction,
Location,
MemoryHistory,
ParamParseKey,
PathMatch,
PathPattern,
RelativeRoutingType,
Router as RemixRouter,
RouterState,
Expand Down Expand Up @@ -492,6 +495,10 @@ export function Router({
export interface RoutesProps {
children?: React.ReactNode;
location?: Partial<Location> | string;
customMatchPath?: <ParamKey extends ParamParseKey<Path>, Path extends string>(
pattern: PathPattern<Path> | Path,
pathname: string
) => PathMatch<ParamKey> | null
}

/**
Expand All @@ -503,8 +510,9 @@ export interface RoutesProps {
export function Routes({
children,
location,
customMatchPath,
}: RoutesProps): React.ReactElement | null {
return useRoutes(createRoutesFromChildren(children), location);
return useRoutes(createRoutesFromChildren(children), location, customMatchPath);
}

export interface AwaitResolveRenderFunction {
Expand Down
16 changes: 12 additions & 4 deletions packages/react-router/lib/hooks.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -338,17 +338,25 @@ export function useResolvedPath(
*/
export function useRoutes(
routes: RouteObject[],
locationArg?: Partial<Location> | string
locationArg?: Partial<Location> | string,
customMatchPath?: <ParamKey extends ParamParseKey<Path>, Path extends string>(
pattern: PathPattern<Path> | Path,
pathname: string
) => PathMatch<ParamKey> | null
): React.ReactElement | null {
return useRoutesImpl(routes, locationArg);
return useRoutesImpl(routes, locationArg, undefined, undefined, customMatchPath);
}

// Internal implementation with accept optional param for RouterProvider usage
export function useRoutesImpl(
routes: RouteObject[],
locationArg?: Partial<Location> | string,
dataRouterState?: RemixRouter["state"],
future?: RemixRouter["future"]
future?: RemixRouter["future"],
customMatchPath?: <ParamKey extends ParamParseKey<Path>, Path extends string>(
pattern: PathPattern<Path> | Path,
pathname: string
) => PathMatch<ParamKey> | null
): React.ReactElement | null {
invariant(
useInRouterContext(),
Expand Down Expand Up @@ -444,7 +452,7 @@ export function useRoutesImpl(
remainingPathname = "/" + segments.slice(parentSegments.length).join("/");
}

let matches = matchRoutes(routes, { pathname: remainingPathname });
let matches = matchRoutes(routes, { pathname: remainingPathname }, undefined, customMatchPath);

if (__DEV__) {
warning(
Expand Down
28 changes: 21 additions & 7 deletions packages/router/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -501,7 +501,11 @@ export function matchRoutes<
>(
routes: RouteObjectType[],
locationArg: Partial<Location> | string,
basename = "/"
basename = "/",
customMatchPath?: <ParamKey extends ParamParseKey<Path>, Path extends string>(
pattern: PathPattern<Path> | Path,
pathname: string
) => PathMatch<ParamKey> | null
): AgnosticRouteMatch<string, RouteObjectType>[] | null {
let location =
typeof locationArg === "string" ? parsePath(locationArg) : locationArg;
Expand All @@ -524,7 +528,7 @@ export function matchRoutes<
// should be a safe operation. This avoids needing matchRoutes to be
// history-aware.
let decoded = decodePath(pathname);
matches = matchRouteBranch<string, RouteObjectType>(branches[i], decoded);
matches = matchRouteBranch<string, RouteObjectType>(branches[i], decoded, customMatchPath);
}

return matches;
Expand Down Expand Up @@ -768,7 +772,11 @@ function matchRouteBranch<
RouteObjectType extends AgnosticRouteObject = AgnosticRouteObject
>(
branch: RouteBranch<RouteObjectType>,
pathname: string
pathname: string,
customMatchPath?: <ParamKey extends ParamParseKey<Path>, Path extends string>(
pattern: PathPattern<Path> | Path,
pathname: string
) => PathMatch<ParamKey> | null
): AgnosticRouteMatch<ParamKey, RouteObjectType>[] | null {
let { routesMeta } = branch;

Expand All @@ -782,10 +790,16 @@ function matchRouteBranch<
matchedPathname === "/"
? pathname
: pathname.slice(matchedPathname.length) || "/";
let match = matchPath(
{ path: meta.relativePath, caseSensitive: meta.caseSensitive, end },
remainingPathname
);
let match =
typeof customMatchPath === "function"
? customMatchPath(
{ path: meta.relativePath, caseSensitive: meta.caseSensitive, end },
remainingPathname
)
: matchPath(
{ path: meta.relativePath, caseSensitive: meta.caseSensitive, end },
remainingPathname
);

if (!match) return null;

Expand Down