diff --git a/.changeset/beige-clouds-whisper.md b/.changeset/beige-clouds-whisper.md new file mode 100644 index 000000000..1e1799334 --- /dev/null +++ b/.changeset/beige-clouds-whisper.md @@ -0,0 +1,5 @@ +--- +"@bigcommerce/catalyst-core": patch +--- + +Enforce use of next-intl's wrapper navigation APIs. diff --git a/core/.eslintrc.cjs b/core/.eslintrc.cjs index fb0ee4071..6bc34066c 100644 --- a/core/.eslintrc.cjs +++ b/core/.eslintrc.cjs @@ -27,6 +27,21 @@ const config = { name: 'next/link', message: "Please import 'Link' from '~/components/Link' instead.", }, + { + name: '~/i18n/routing', + importNames: ['Link'], + message: "Please import 'Link' from '~/components/Link' instead.", + }, + { + name: 'next/router', + importNames: ['useRouter'], + message: 'Please import from `~/i18n/routing` instead.', + }, + { + name: 'next/navigation', + importNames: ['redirect', 'permanentRedirect', 'useRouter', 'usePathname'], + message: 'Please import from `~/i18n/routing` instead.', + }, ], }, ], @@ -37,7 +52,12 @@ const config = { }, ], }, - ignorePatterns: ['client/generated/**/*.ts', 'playwright-report/**', 'test-results/**', '**/google_analytics4.js'], + ignorePatterns: [ + 'client/generated/**/*.ts', + 'playwright-report/**', + 'test-results/**', + '**/google_analytics4.js', + ], }; module.exports = config; diff --git a/core/app/[locale]/(default)/(auth)/change-password/page.tsx b/core/app/[locale]/(default)/(auth)/change-password/page.tsx index 04a86b50b..e490282c0 100644 --- a/core/app/[locale]/(default)/(auth)/change-password/page.tsx +++ b/core/app/[locale]/(default)/(auth)/change-password/page.tsx @@ -1,6 +1,7 @@ -import { redirect } from 'next/navigation'; import { useTranslations } from 'next-intl'; +import { redirect } from '~/i18n/routing'; + import { ChangePasswordForm } from './_components/change-password-form'; export const metadata = { diff --git a/core/app/[locale]/(default)/(auth)/login/_actions/login.ts b/core/app/[locale]/(default)/(auth)/login/_actions/login.ts index 24436c06f..ec5830fdf 100644 --- a/core/app/[locale]/(default)/(auth)/login/_actions/login.ts +++ b/core/app/[locale]/(default)/(auth)/login/_actions/login.ts @@ -1,9 +1,9 @@ 'use server'; import { isRedirectError } from 'next/dist/client/components/redirect'; -import { redirect } from 'next/navigation'; import { Credentials, signIn } from '~/auth'; +import { redirect } from '~/i18n/routing'; export const login = async (_previousState: unknown, formData: FormData) => { try { diff --git a/core/app/[locale]/(default)/(auth)/login/_components/login-form.tsx b/core/app/[locale]/(default)/(auth)/login/_components/login-form.tsx index bc24001fa..e8c8f4b73 100644 --- a/core/app/[locale]/(default)/(auth)/login/_components/login-form.tsx +++ b/core/app/[locale]/(default)/(auth)/login/_components/login-form.tsx @@ -44,7 +44,7 @@ export const LoginForm = () => { const [state, formAction] = useFormState(login, { status: 'idle' }); const { accountState } = useAccountStatusContext(); - const isFormInvalid = state.status === 'error'; + const isFormInvalid = state?.status === 'error'; const handleInputValidation = (e: ChangeEvent) => { const validationStatus = e.target.validity.valueMissing; diff --git a/core/app/[locale]/(default)/(auth)/register/_actions/login.ts b/core/app/[locale]/(default)/(auth)/register/_actions/login.ts index b295b4d61..434282291 100644 --- a/core/app/[locale]/(default)/(auth)/register/_actions/login.ts +++ b/core/app/[locale]/(default)/(auth)/register/_actions/login.ts @@ -1,9 +1,9 @@ 'use server'; import { isRedirectError } from 'next/dist/client/components/redirect'; -import { redirect } from 'next/navigation'; import { Credentials, signIn } from '~/auth'; +import { redirect } from '~/i18n/routing'; export const login = async (formData: FormData) => { try { diff --git a/core/app/[locale]/(default)/(auth)/reset/_components/reset-password-form/index.tsx b/core/app/[locale]/(default)/(auth)/reset/_components/reset-password-form/index.tsx index d917312f7..c1e662ec1 100644 --- a/core/app/[locale]/(default)/(auth)/reset/_components/reset-password-form/index.tsx +++ b/core/app/[locale]/(default)/(auth)/reset/_components/reset-password-form/index.tsx @@ -1,6 +1,5 @@ 'use client'; -import { useRouter } from 'next/navigation'; import { useTranslations } from 'next-intl'; import { ChangeEvent, useEffect, useRef, useState } from 'react'; import { useFormStatus } from 'react-dom'; @@ -19,6 +18,7 @@ import { Input, } from '~/components/ui/form'; import { Message } from '~/components/ui/message'; +import { useRouter } from '~/i18n/routing'; import { resetPassword } from '../../_actions/reset-password'; diff --git a/core/app/[locale]/(default)/(faceted)/_components/facets.tsx b/core/app/[locale]/(default)/(faceted)/_components/facets.tsx index e125a89b6..96cc0a0f9 100644 --- a/core/app/[locale]/(default)/(faceted)/_components/facets.tsx +++ b/core/app/[locale]/(default)/(faceted)/_components/facets.tsx @@ -1,6 +1,6 @@ 'use client'; -import { usePathname, useRouter, useSearchParams } from 'next/navigation'; +import { useSearchParams } from 'next/navigation'; import { useTranslations } from 'next-intl'; import { FormEvent, useRef, useTransition } from 'react'; @@ -9,6 +9,7 @@ import { Accordions } from '~/components/ui/accordions'; import { Button } from '~/components/ui/button'; import { Checkbox, Input, Label } from '~/components/ui/form'; import { Rating } from '~/components/ui/rating'; +import { usePathname, useRouter } from '~/i18n/routing'; import { cn } from '~/lib/utils'; import type { Facet, PageType } from '../types'; diff --git a/core/app/[locale]/(default)/(faceted)/_components/refine-by.tsx b/core/app/[locale]/(default)/(faceted)/_components/refine-by.tsx index 1c0a7fdec..15a55475c 100644 --- a/core/app/[locale]/(default)/(faceted)/_components/refine-by.tsx +++ b/core/app/[locale]/(default)/(faceted)/_components/refine-by.tsx @@ -1,10 +1,11 @@ 'use client'; -import { usePathname, useRouter, useSearchParams } from 'next/navigation'; +import { useSearchParams } from 'next/navigation'; import { useTranslations } from 'next-intl'; import { useTransition } from 'react'; import { Tag } from '~/components/ui/tag'; +import { usePathname, useRouter } from '~/i18n/routing'; import type { Facet, PageType, PublicParamKeys } from '../types'; diff --git a/core/app/[locale]/(default)/(faceted)/_components/sort-by.tsx b/core/app/[locale]/(default)/(faceted)/_components/sort-by.tsx index df33127fe..33adc1f93 100644 --- a/core/app/[locale]/(default)/(faceted)/_components/sort-by.tsx +++ b/core/app/[locale]/(default)/(faceted)/_components/sort-by.tsx @@ -1,10 +1,11 @@ 'use client'; -import { usePathname, useRouter, useSearchParams } from 'next/navigation'; +import { useSearchParams } from 'next/navigation'; import { useTranslations } from 'next-intl'; import { useTransition } from 'react'; import { Select } from '~/components/ui/form'; +import { usePathname, useRouter } from '~/i18n/routing'; export function SortBy() { const router = useRouter(); diff --git a/core/app/[locale]/(default)/account/(tabs)/_components/account-status-provider.tsx b/core/app/[locale]/(default)/account/(tabs)/_components/account-status-provider.tsx index dace7d7d2..6683b9778 100644 --- a/core/app/[locale]/(default)/account/(tabs)/_components/account-status-provider.tsx +++ b/core/app/[locale]/(default)/account/(tabs)/_components/account-status-provider.tsx @@ -1,8 +1,9 @@ 'use client'; -import { usePathname } from 'next/navigation'; import { createContext, ReactNode, useContext, useEffect, useState } from 'react'; +import { usePathname } from '~/i18n/routing'; + import { State as AccountState } from '../settings/change-password/_actions/change-password'; const defaultState: AccountState = { status: 'idle', message: '' }; diff --git a/core/app/[locale]/(default)/account/(tabs)/addresses/add/_components/add-address-form.tsx b/core/app/[locale]/(default)/account/(tabs)/addresses/add/_components/add-address-form.tsx index 4cf7b1df1..6a09a14de 100644 --- a/core/app/[locale]/(default)/account/(tabs)/addresses/add/_components/add-address-form.tsx +++ b/core/app/[locale]/(default)/account/(tabs)/addresses/add/_components/add-address-form.tsx @@ -1,6 +1,5 @@ 'use client'; -import { useRouter } from 'next/navigation'; import { useTranslations } from 'next-intl'; import { MouseEvent, useEffect, useRef, useState } from 'react'; import { useFormStatus } from 'react-dom'; @@ -35,6 +34,7 @@ import { Link } from '~/components/link'; import { Button } from '~/components/ui/button'; import { Field, Form, FormSubmit } from '~/components/ui/form'; import { Message } from '~/components/ui/message'; +import { useRouter } from '~/i18n/routing'; import { useAccountStatusContext } from '../../../_components/account-status-provider'; import { addAddress } from '../_actions/add-address'; diff --git a/core/app/[locale]/(default)/account/(tabs)/addresses/edit/[slug]/_components/edit-address-form.tsx b/core/app/[locale]/(default)/account/(tabs)/addresses/edit/[slug]/_components/edit-address-form.tsx index 4fe5edbfd..b6c2570b5 100644 --- a/core/app/[locale]/(default)/account/(tabs)/addresses/edit/[slug]/_components/edit-address-form.tsx +++ b/core/app/[locale]/(default)/account/(tabs)/addresses/edit/[slug]/_components/edit-address-form.tsx @@ -1,6 +1,5 @@ 'use client'; -import { useRouter } from 'next/navigation'; import { useTranslations } from 'next-intl'; import { MouseEvent, useEffect, useRef, useState } from 'react'; import { useFormStatus } from 'react-dom'; @@ -36,6 +35,7 @@ import { Link } from '~/components/link'; import { Button } from '~/components/ui/button'; import { Field, Form, FormSubmit } from '~/components/ui/form'; import { Message } from '~/components/ui/message'; +import { useRouter } from '~/i18n/routing'; import { useAccountStatusContext } from '../../../../_components/account-status-provider'; import { Modal } from '../../../../_components/modal'; diff --git a/core/app/[locale]/(default)/account/layout.tsx b/core/app/[locale]/(default)/account/layout.tsx index dc5298fa6..318dd8a6f 100644 --- a/core/app/[locale]/(default)/account/layout.tsx +++ b/core/app/[locale]/(default)/account/layout.tsx @@ -1,7 +1,7 @@ -import { redirect } from 'next/navigation'; import { PropsWithChildren } from 'react'; import { auth } from '~/auth'; +import { redirect } from '~/i18n/routing'; export default async function AccountLayout({ children }: PropsWithChildren) { const session = await auth(); diff --git a/core/app/[locale]/(default)/cart/_actions/redirect-to-checkout.ts b/core/app/[locale]/(default)/cart/_actions/redirect-to-checkout.ts index 195ace117..e3b129c60 100644 --- a/core/app/[locale]/(default)/cart/_actions/redirect-to-checkout.ts +++ b/core/app/[locale]/(default)/cart/_actions/redirect-to-checkout.ts @@ -1,11 +1,11 @@ 'use server'; -import { redirect } from 'next/navigation'; import { z } from 'zod'; import { getSessionCustomerId } from '~/auth'; import { client } from '~/client'; import { graphql } from '~/client/graphql'; +import { redirect } from '~/i18n/routing'; const CheckoutRedirectMutation = graphql(` mutation CheckoutRedirectMutation($cartId: String!) { diff --git a/core/app/[locale]/(default)/product/[slug]/_components/product-form/fields/multiple-choice-field/index.tsx b/core/app/[locale]/(default)/product/[slug]/_components/product-form/fields/multiple-choice-field/index.tsx index e51044811..426f4e277 100644 --- a/core/app/[locale]/(default)/product/[slug]/_components/product-form/fields/multiple-choice-field/index.tsx +++ b/core/app/[locale]/(default)/product/[slug]/_components/product-form/fields/multiple-choice-field/index.tsx @@ -1,8 +1,9 @@ import { removeEdgesAndNodes } from '@bigcommerce/catalyst-client'; -import { usePathname, useRouter, useSearchParams } from 'next/navigation'; +import { useSearchParams } from 'next/navigation'; import { FragmentOf } from '~/client/graphql'; import { Label, PickList, RadioGroup, RectangleList, Select, Swatch } from '~/components/ui/form'; +import { usePathname, useRouter } from '~/i18n/routing'; import { useProductFieldController } from '../../use-product-form'; import { ErrorMessage } from '../shared/error-message'; diff --git a/core/app/admin/route.ts b/core/app/admin/route.ts index d6e6c55ba..457ae77a4 100644 --- a/core/app/admin/route.ts +++ b/core/app/admin/route.ts @@ -1,4 +1,4 @@ -import { redirect } from 'next/navigation'; +import { redirect } from '~/i18n/routing'; const canonicalDomain: string = process.env.BIGCOMMERCE_GRAPHQL_API_DOMAIN ?? 'mybigcommerce.com'; const BIGCOMMERCE_STORE_HASH = process.env.BIGCOMMERCE_STORE_HASH; diff --git a/core/app/xmlsitemap.php/route.ts b/core/app/xmlsitemap.php/route.ts index 0dad84a09..42036d2b0 100644 --- a/core/app/xmlsitemap.php/route.ts +++ b/core/app/xmlsitemap.php/route.ts @@ -1,5 +1,5 @@ /* eslint-disable check-file/folder-naming-convention */ -import { permanentRedirect } from 'next/navigation'; +import { permanentRedirect } from '~/i18n/routing'; /* * This route is used to redirect the legacy Stencil sitemap that lives on /xmlsitemap.php diff --git a/core/components/header/_actions/logout.ts b/core/components/header/_actions/logout.ts index 9093686f5..5e5bae528 100644 --- a/core/components/header/_actions/logout.ts +++ b/core/components/header/_actions/logout.ts @@ -1,8 +1,7 @@ 'use server'; -import { redirect } from 'next/navigation'; - import { signOut } from '~/auth'; +import { redirect } from '~/i18n/routing'; export const logout = async () => { await signOut({ redirect: false }); diff --git a/core/components/ui/pagination/pagination.tsx b/core/components/ui/pagination/pagination.tsx index 95a9e17e8..5538d1cd4 100644 --- a/core/components/ui/pagination/pagination.tsx +++ b/core/components/ui/pagination/pagination.tsx @@ -1,7 +1,8 @@ import { ChevronLeft, ChevronRight } from 'lucide-react'; -import { usePathname, useSearchParams } from 'next/navigation'; +import { useSearchParams } from 'next/navigation'; import { Link as CustomLink } from '~/components/link'; +import { usePathname } from '~/i18n/routing'; import { cn } from '~/lib/utils'; interface Props { diff --git a/core/i18n/routing.ts b/core/i18n/routing.ts index c38de3e3e..20a53b99b 100644 --- a/core/i18n/routing.ts +++ b/core/i18n/routing.ts @@ -84,4 +84,7 @@ export const routing = defineRouting({ // Lightweight wrappers around Next.js' navigation APIs // that will consider the routing configuration -export const { Link, redirect, usePathname, useRouter } = createSharedPathnamesNavigation(routing); +// Redirect will append locale prefix even when in default locale +// More info: https://github.com/amannn/next-intl/issues/1335 +export const { Link, redirect, usePathname, useRouter, permanentRedirect } = + createSharedPathnamesNavigation(routing); diff --git a/core/tests/ui/desktop/e2e/login.spec.ts b/core/tests/ui/desktop/e2e/login.spec.ts index 21e67abbb..3719a7673 100644 --- a/core/tests/ui/desktop/e2e/login.spec.ts +++ b/core/tests/ui/desktop/e2e/login.spec.ts @@ -27,7 +27,7 @@ test('Account login and logout', async ({ page }) => { await page.getByRole('menuitem', { name: 'Log out' }).click(); - await page.waitForURL('/login/'); + await page.waitForURL('/en/login/'); await expect(page.getByRole('heading', { name: 'Log in' })).toBeVisible(); }); diff --git a/core/tests/ui/desktop/e2e/register.spec.ts b/core/tests/ui/desktop/e2e/register.spec.ts index f885b597c..febcf01fb 100644 --- a/core/tests/ui/desktop/e2e/register.spec.ts +++ b/core/tests/ui/desktop/e2e/register.spec.ts @@ -27,6 +27,6 @@ test('Account register', async ({ page }) => { await page.getByRole('button', { name: 'Create account' }).click(); - await expect(page).toHaveURL('/account/'); + await expect(page).toHaveURL('/en/account/'); await expect(page.getByText('Your account has been successfully created')).toBeVisible(); });