Skip to content

Commit

Permalink
fix: Incorporate basePath correctly in useRouter functions (#906)
Browse files Browse the repository at this point in the history
Fixes #905 
Fixes #910

---------

Co-authored-by: Jan Amann <jan@amann.me>
  • Loading branch information
Oberwaditzer and amannn committed Mar 5, 2024
1 parent 16b19e8 commit 4abcbeb
Show file tree
Hide file tree
Showing 6 changed files with 104 additions and 19 deletions.
4 changes: 2 additions & 2 deletions packages/next-intl/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -122,11 +122,11 @@
},
{
"path": "dist/production/navigation.react-client.js",
"limit": "2.89 KB"
"limit": "2.94 KB"
},
{
"path": "dist/production/navigation.react-server.js",
"limit": "3.01 KB"
"limit": "3.03 KB"
},
{
"path": "dist/production/server.react-client.js",
Expand Down
13 changes: 7 additions & 6 deletions packages/next-intl/src/navigation/react-client/useBaseRouter.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import useLocale from '../../react-client/useLocale';
import {AllLocales} from '../../shared/types';
import {localizeHref} from '../../shared/utils';
import syncLocaleCookie from '../shared/syncLocaleCookie';
import {getBasePath} from '../shared/utils';

type IntlNavigateOptions<Locales extends AllLocales> = {
locale?: Locales[number];
Expand Down Expand Up @@ -35,12 +36,12 @@ export default function useBaseRouter<Locales extends AllLocales>() {

return useMemo(() => {
function localize(href: string, nextLocale?: string) {
return localizeHref(
href,
nextLocale || locale,
locale,
window.location.pathname
);
let curPathname = window.location.pathname;

const basePath = getBasePath(pathname);
if (basePath) curPathname = curPathname.replace(basePath, '');

return localizeHref(href, nextLocale || locale, locale, curPathname);
}

function createHandler<
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import {
COOKIE_MAX_AGE,
COOKIE_SAME_SITE
} from '../../shared/constants';
import {getBasePath} from './utils';

/**
* We have to keep the cookie value in sync as Next.js might
Expand All @@ -25,7 +26,7 @@ export default function syncLocaleCookie(
return;
}

const basePath = window.location.pathname.replace(pathname, '');
const basePath = getBasePath(pathname);
const hasBasePath = basePath !== '';
const path = hasBasePath ? basePath : '/';

Expand Down
4 changes: 4 additions & 0 deletions packages/next-intl/src/navigation/shared/utils.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -175,3 +175,7 @@ export function getRoute<Locales extends AllLocales>({

return template as keyof Pathnames<Locales>;
}

export function getBasePath(pathname: string) {
return window.location.pathname.replace(pathname, '');
}
8 changes: 4 additions & 4 deletions packages/next-intl/src/shared/utils.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,27 +22,27 @@ export function localizeHref(
href: string,
locale: string,
curLocale: string,
pathname: string
curPathname: string
): string;
export function localizeHref(
href: UrlObject | string,
locale: string,
curLocale: string,
pathname: string
curPathname: string
): UrlObject | string;
export function localizeHref(
href: UrlObject | string,
locale: string,
curLocale: string = locale,
pathname: string
curPathname: string
) {
if (!isLocalHref(href) || isRelativeHref(href)) {
return href;
}

const isSwitchingLocale = locale !== curLocale;
const isPathnamePrefixed =
locale == null || hasPathnamePrefixed(locale, pathname);
locale == null || hasPathnamePrefixed(locale, curPathname);
const shouldPrefix = isSwitchingLocale || isPathnamePrefixed;

if (shouldPrefix && locale != null) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
import {render} from '@testing-library/react';
import {PrefetchKind} from 'next/dist/client/components/router-reducer/router-reducer-types';
import {AppRouterInstance} from 'next/dist/shared/lib/app-router-context.shared-runtime';
import {useRouter as useNextRouter} from 'next/navigation';
import {
useRouter as useNextRouter,
usePathname as useNextPathname
} from 'next/navigation';
import React, {useEffect} from 'react';
import {it, describe, vi, beforeEach, expect} from 'vitest';
import useBaseRouter from '../../../src/navigation/react-client/useBaseRouter';
Expand All @@ -16,9 +19,9 @@ vi.mock('next/navigation', () => {
refresh: vi.fn()
};
return {
useRouter: () => router,
useParams: () => ({locale: 'en'}),
usePathname: () => '/'
useRouter: vi.fn(() => router),
useParams: vi.fn(() => ({locale: 'en'})),
usePathname: vi.fn(() => '/')
};
});

Expand All @@ -34,15 +37,26 @@ function callRouter(cb: (router: ReturnType<typeof useBaseRouter>) => void) {
render(<Component />);
}

function mockLocation(pathname: string) {
function mockLocation(pathname: string, basePath = '') {
vi.mocked(useNextPathname).mockReturnValue(pathname);

delete (global.window as any).location;
global.window ??= Object.create(window);
(global.window as any).location = {pathname};
(global.window as any).location = {pathname: basePath + pathname};
}

function clearNextRouterMocks() {
['push', 'replace', 'prefetch', 'back', 'forward', 'refresh'].forEach(
(fnName) => {
vi.mocked((useNextRouter() as any)[fnName]).mockClear();
}
);
}

describe('unprefixed routing', () => {
beforeEach(() => {
mockLocation('/');
clearNextRouterMocks();
});

it('can push', () => {
Expand Down Expand Up @@ -111,6 +125,71 @@ describe('unprefixed routing', () => {
describe('prefixed routing', () => {
beforeEach(() => {
mockLocation('/en');
clearNextRouterMocks();
});

it('can push', () => {
callRouter((router) => router.push('/test'));
expect(useNextRouter().push).toHaveBeenCalledWith('/en/test');
});

it('can replace', () => {
callRouter((router) => router.replace('/test'));
expect(useNextRouter().replace).toHaveBeenCalledWith('/en/test');
});

it('can prefetch', () => {
callRouter((router) => router.prefetch('/test'));
expect(useNextRouter().prefetch).toHaveBeenCalledWith('/en/test');
});

it('passes through absolute urls', () => {
callRouter((router) => router.push('https://example.com'));
expect(useNextRouter().push).toHaveBeenCalledWith('https://example.com');
});

it('passes through relative urls', () => {
callRouter((router) => router.push('about'));
expect(useNextRouter().push).toHaveBeenCalledWith('about');
});
});

describe('basePath unprefixed routing', () => {
beforeEach(() => {
mockLocation('/', '/base/path');
clearNextRouterMocks();
});

it('can push', () => {
callRouter((router) => router.push('/test'));
expect(useNextRouter().push).toHaveBeenCalledWith('/test');
});

it('can replace', () => {
callRouter((router) => router.replace('/test'));
expect(useNextRouter().replace).toHaveBeenCalledWith('/test');
});

it('can prefetch', () => {
callRouter((router) => router.prefetch('/test'));
expect(useNextRouter().prefetch).toHaveBeenCalledWith('/test');
});

it('passes through absolute urls', () => {
callRouter((router) => router.push('https://example.com'));
expect(useNextRouter().push).toHaveBeenCalledWith('https://example.com');
});

it('passes through relative urls', () => {
callRouter((router) => router.push('about'));
expect(useNextRouter().push).toHaveBeenCalledWith('about');
});
});

describe('basePath prefixed routing', () => {
beforeEach(() => {
mockLocation('/en', '/base/path');
clearNextRouterMocks();
});

it('can push', () => {
Expand Down

0 comments on commit 4abcbeb

Please sign in to comment.