Merged in chore/upgrade-to-next16 (pull request #3305)

chore(SW-3665): Upgrade to Next 16

* Upgrade partner-sas

* Upgrade scandic-web to next 16

* Update peerDep versions

* Fix revalidateTag

* Remove comment

* Merge branch 'master' into chore/upgrade-to-next16

* Update netlify adapter

* Build with webpack instead of turbopack

* Revert from proxy to middleware

* Merge branch 'master' into chore/upgrade-to-next16

* Revert proxy type

* Fix react types versions

* 16.0.9

* Bump to 16.0.10


Approved-by: Linus Flood
This commit is contained in:
Anton Gunnarsson
2025-12-12 09:17:15 +00:00
parent 7d021f8e40
commit e5149846e5
41 changed files with 417 additions and 224 deletions

View File

@@ -1,5 +1,3 @@
"use client"
import { useSession } from "next-auth/react"
import { useEffect, useState } from "react"
import {
@@ -48,6 +46,7 @@ export function UserMenu({
})
useEffect(() => {
// eslint-disable-next-line react-hooks/set-state-in-effect
setLoginLink(`/${lang}/login?redirectTo=${window?.location.href}`)
}, [lang, setLoginLink])

View File

@@ -1,48 +1,34 @@
import { FlatCompat } from "@eslint/eslintrc"
import js from "@eslint/js"
import typescriptEslint from "@typescript-eslint/eslint-plugin"
import tsParser from "@typescript-eslint/parser"
import { defineConfig } from "eslint/config"
import { defineConfig, globalIgnores } from "eslint/config"
import nextVitals from "eslint-config-next/core-web-vitals"
import nextTs from "eslint-config-next/typescript"
import formatjs from "eslint-plugin-formatjs"
import simpleImportSort from "eslint-plugin-simple-import-sort"
const compat = new FlatCompat({
recommendedConfig: js.configs.recommended,
allConfig: js.configs.all,
})
export default defineConfig([
...nextVitals,
...nextTs,
globalIgnores([
".next/**",
"node_modules/**",
"dist/**",
"build/**",
"public/**",
"playwright-report/**",
"test-results/**",
"coverage/**",
"*.config.js",
"*.config.ts",
"*.config.mjs",
"next-env.d.ts",
]),
{
ignores: [
".next/**",
"node_modules/**",
"dist/**",
"build/**",
"public/**",
"playwright-report/**",
"test-results/**",
"coverage/**",
"*.config.js",
"*.config.ts",
"*.config.mjs",
"next-env.d.ts",
],
},
{
extends: compat.extends(
"next/core-web-vitals",
"plugin:@typescript-eslint/recommended"
),
files: ["**/*.{js,jsx,ts,tsx,mts}"],
plugins: {
"simple-import-sort": simpleImportSort,
"@typescript-eslint": typescriptEslint,
formatjs,
},
languageOptions: {
parser: tsParser,
},
rules: {
"no-console": "warn",
"no-unused-vars": "off",

View File

@@ -22,7 +22,7 @@
},
"dependencies": {
"@formatjs/intl": "^3.1.6",
"@netlify/plugin-nextjs": "^5.14.4",
"@netlify/plugin-nextjs": "^5.15.1",
"@scandic-hotels/booking-flow": "workspace:*",
"@scandic-hotels/design-system": "workspace:*",
"@scandic-hotels/tracking": "workspace:*",
@@ -32,30 +32,27 @@
"@tanstack/react-query": "^5.75.5",
"@tanstack/react-query-devtools": "^5.75.5",
"iron-session": "^8.0.4",
"next": "^15.5.7",
"next": "16.0.10",
"next-auth": "5.0.0-beta.29",
"react": "^19.0.0",
"react": "19.2.1",
"react-aria-components": "1.8.0",
"react-dom": "^19.0.0",
"react-dom": "19.2.1",
"react-intl": "^7.1.11",
"server-only": "^0.0.1",
"usehooks-ts": "3.1.1"
},
"devDependencies": {
"@eslint/js": "^9.26.0",
"@playwright/test": "^1.53.1",
"@scandic-hotels/common": "workspace:*",
"@scandic-hotels/typescript-config": "workspace:*",
"@swc/plugin-formatjs": "^3.2.2",
"@types/node": "^20",
"@types/react": "^19.2.3",
"@types/react-dom": "^19.2.3",
"@typescript-eslint/eslint-plugin": "^8.32.0",
"@typescript-eslint/parser": "^8.32.0",
"@types/react": "19.2.7",
"@types/react-dom": "19.2.3",
"@typescript/native-preview": "^7.0.0-dev.20251104.1",
"babel-plugin-formatjs": "^10.5.39",
"eslint": "^9",
"eslint-config-next": "15.3.2",
"eslint-config-next": "16.0.7",
"eslint-plugin-formatjs": "^5.3.1",
"eslint-plugin-import": "^2.31.0",
"eslint-plugin-simple-import-sort": "^12.1.1",

View File

@@ -29,6 +29,7 @@ export default function Error({
defaultMessage: "Breadcrumbs failed for this page ({errorId})",
},
{
// eslint-disable-next-line react-hooks/purity
errorId: `${error.digest}@${Date.now()}`,
}
)}

View File

@@ -29,6 +29,7 @@ export default function Error({
defaultMessage: "Error loading this page ({errorId})",
},
{
// eslint-disable-next-line react-hooks/purity
errorId: `${error.digest}@${Date.now()}`,
}
)}

View File

@@ -0,0 +1,3 @@
export default function Default() {
return null
}

View File

@@ -32,6 +32,7 @@ export default function Error({
defaultMessage: "An error occurred ({errorId})",
},
{
// eslint-disable-next-line react-hooks/purity
errorId: `${error.digest}@${Date.now()}`,
}
)}

View File

@@ -0,0 +1,3 @@
export default function Default() {
return null
}

View File

@@ -67,7 +67,7 @@ export async function POST(request: NextRequest) {
}
revalidateHotelLogger.debug(`Revalidating hotel url tag: ${tag}`)
revalidateTag(tag)
revalidateTag(tag, { expire: 0 })
const cacheClient = await getCacheClient()
await cacheClient.deleteKey(tag, { fuzzy: true })

View File

@@ -84,7 +84,7 @@ export async function POST(request: NextRequest) {
}
loyaltyRevalidateLogger.info(`Revalidating loyalty config tag: ${tag}`)
revalidateTag(tag)
revalidateTag(tag, { expire: 0 })
const cacheClient = await getCacheClient()
await cacheClient.deleteKey(tag, { fuzzy: true })

View File

@@ -48,7 +48,7 @@ export async function POST() {
)
revalidateManuallyLogger.info(`Tag: ${tag}`)
revalidateTag(tag)
revalidateTag(tag, { expire: 0 })
cacheClient.deleteKey(tag, { fuzzy: false, includeGitHashInKey: true })
return Response.json({ revalidated: true, now: Date.now() })

View File

@@ -103,31 +103,31 @@ export async function POST(request: NextRequest) {
revalidateLogger.debug(
`Revalidating tag by content_type_uid: ${contentTypeUidTag}`
)
revalidateTag(contentTypeUidTag)
revalidateTag(contentTypeUidTag, { expire: 0 })
keysToDelete.push(contentTypeUidTag)
revalidateLogger.debug(`Revalidating refsTag: ${refsTag}`)
revalidateTag(refsTag)
revalidateTag(refsTag, { expire: 0 })
keysToDelete.push(refsTag)
revalidateLogger.debug(`Revalidating refTag: ${refTag}`)
revalidateTag(refTag)
revalidateTag(refTag, { expire: 0 })
keysToDelete.push(refTag)
revalidateLogger.debug(`Revalidating tag: ${tag}`)
revalidateTag(tag)
revalidateTag(tag, { expire: 0 })
keysToDelete.push(tag)
revalidateLogger.debug(
`Revalidating language switcher tag: ${languageSwitcherTag}`
)
revalidateTag(languageSwitcherTag)
revalidateTag(languageSwitcherTag, { expire: 0 })
keysToDelete.push(languageSwitcherTag)
revalidateLogger.debug(`Revalidating metadataTag: ${metadataTag}`)
revalidateTag(metadataTag)
revalidateTag(metadataTag, { expire: 0 })
keysToDelete.push(metadataTag)
revalidateLogger.debug(`Revalidating contentEntryTag: ${contentEntryTag}`)
revalidateTag(contentEntryTag)
revalidateTag(contentEntryTag, { expire: 0 })
keysToDelete.push(contentEntryTag)
if (entry.url) {
@@ -151,11 +151,11 @@ export async function POST(request: NextRequest) {
revalidateLogger.debug(
`Revalidating breadcrumbsRefsTag: ${breadcrumbsRefsTag}`
)
revalidateTag(breadcrumbsRefsTag)
revalidateTag(breadcrumbsRefsTag, { expire: 0 })
keysToDelete.push(breadcrumbsRefsTag)
revalidateLogger.debug(`Revalidating breadcrumbsTag: ${breadcrumbsTag}`)
revalidateTag(breadcrumbsTag)
revalidateTag(breadcrumbsTag, { expire: 0 })
keysToDelete.push(breadcrumbsTag)
}
@@ -167,7 +167,7 @@ export async function POST(request: NextRequest) {
)
revalidateLogger.debug(`Revalidating pageSettingsTag: ${pageSettingsTag}`)
revalidateTag(pageSettingsTag)
revalidateTag(pageSettingsTag, { expire: 0 })
keysToDelete.push(pageSettingsTag)
}

View File

@@ -157,7 +157,9 @@ export default function OverviewTableClient({
<div>
<div className={styles.mobileColumns}>
<div className={styles.columnHeaderContainer}>
{/* eslint-disable-next-line react-hooks/static-components */}
<MobileColumnHeader column="A" />
{/* eslint-disable-next-line react-hooks/static-components */}
<MobileColumnHeader column="B" />
</div>
<RewardList

View File

@@ -23,6 +23,7 @@ export function RewardIcon({
if (!IconComponent) return null
return (
// eslint-disable-next-line react-hooks/static-components
<IconComponent
{...props}
width={sizeMap[iconSize].width}

View File

@@ -69,6 +69,7 @@ function Carousel({
useEffect(() => {
if (!api) return
// eslint-disable-next-line react-hooks/set-state-in-effect
onSelect(api)
api.on("reInit", onSelect)
api.on("select", onSelect)

View File

@@ -59,6 +59,7 @@ export default function HotelList() {
return
}
// eslint-disable-next-line react-hooks/set-state-in-effect
setVisibleHotels(getVisibleHotels(activeHotels, map))
}, [map, activeHotels])

View File

@@ -43,6 +43,7 @@ export default function CityMap({
useEffect(() => {
const url = new URL(window.location.href)
// eslint-disable-next-line react-hooks/set-state-in-effect
setIsFromCountryPage(url.searchParams.has("fromCountry"))
}, [params])

View File

@@ -82,6 +82,7 @@ export default function DynamicMap({
bounds.extend(marker.coordinates)
})
map.fitBounds(bounds, boundsPadding)
// eslint-disable-next-line react-hooks/set-state-in-effect
setHasFittedBounds(true)
}
}, [map, fitBounds, markers, hasFittedBounds, boundsPadding])

View File

@@ -86,6 +86,7 @@ export default function Map({
useEffect(() => {
const url = new URL(window.location.href)
// eslint-disable-next-line react-hooks/set-state-in-effect
setFromCountryPage(url.searchParams.has("fromCountry"))
}, [params])

View File

@@ -90,6 +90,7 @@ export default function TabNavigation({
url.hash = activeSectionId
window.history.replaceState(null, "", url.toString())
// eslint-disable-next-line react-hooks/set-state-in-effect
setActiveHash(activeSectionId)
}
}, [activeSectionId])

View File

@@ -70,6 +70,7 @@ export default function ActivateOffer({
(activateCampaign.isSuccess && activateCampaign.data) ? (
<CampaignActivated />
) : (
// eslint-disable-next-line react-hooks/static-components
<ActivateButton />
)}
<ErrorModal

View File

@@ -115,6 +115,7 @@ export default function PromoCampaignHero({
</ul>
) : null}
</div>
{/* eslint-disable-next-line react-hooks/static-components */}
<CampaignCTA />
</div>

View File

@@ -22,14 +22,15 @@ export function FilterAndSortButton({
const searchParams = useSearchParams()
const [isMapView, setIsMapView] = useState(false)
const [isHydrated, setIsHydrated] = useState(false)
useEffect(() => {
const isMapView = searchParams.get("view") === "map"
// eslint-disable-next-line react-hooks/set-state-in-effect
setIsMapView(isMapView)
setIsHydrated(true)
}, [searchParams])
const [isHydrated, setIsHydrated] = useState(false)
const isDesktop = useMediaQuery("(min-width: 950px)")
if (!isHydrated) return null

View File

@@ -96,6 +96,7 @@ export default function Form({ user }: EditFormProps) {
utils.user.get.invalidate()
if (isPasswordChanged) {
// Kept logout out of Next router forcing browser to navigate on logout url
// eslint-disable-next-line react-hooks/immutability
window.location.href = logout[lang]
} else {
const myStayReturnRoute = sessionStorage.getItem("myStayReturnRoute")

View File

@@ -43,6 +43,7 @@ export default function AdditionalInfoForm({
confirmationNumber,
lastName,
}
// eslint-disable-next-line react-hooks/immutability
document.cookie = `bv=${JSON.stringify(value)}; Path=/; Max-Age=600; Secure; SameSite=Strict`
router.refresh()
}

View File

@@ -270,6 +270,7 @@ export default function AddAncillaryFlowModal({
formMethods.reset(updatedFormData)
}
// eslint-disable-next-line react-hooks/set-state-in-effect
setErrorMessage(getErrorMessage(intl, errorCode))
queryParams.delete("ancillary")
queryParams.delete("errorCode")

View File

@@ -57,6 +57,7 @@ export default function GuaranteePaymentFailed() {
? AlertTypeEnum.Warning
: AlertTypeEnum.Alarm
// eslint-disable-next-line react-hooks/set-state-in-effect
setAlert({ type, message })
const newParams = new URLSearchParams(searchParams.toString())

View File

@@ -45,6 +45,7 @@ function usePromptInitialization(memberKey: string | undefined) {
})
const mutationRef = useRef(updateConsentPromptDate)
// eslint-disable-next-line react-hooks/refs
mutationRef.current = updateConsentPromptDate
const [shouldOpenInitially, setShouldOpenInitially] = useState(false)
@@ -99,6 +100,7 @@ export default function ProfilingConsentModal({
useUpdateProfilingConsent()
useEffect(() => {
// eslint-disable-next-line react-hooks/set-state-in-effect
if (shouldOpenInitially) setOpen(true)
}, [shouldOpenInitially])
@@ -112,6 +114,7 @@ export default function ProfilingConsentModal({
}, [memberKey])
useEffect(() => {
// eslint-disable-next-line react-hooks/set-state-in-effect
if (isSuccess) onClose()
}, [isSuccess, onClose])

View File

@@ -1,49 +1,35 @@
import { FlatCompat } from "@eslint/eslintrc"
import js from "@eslint/js"
import typescriptEslint from "@typescript-eslint/eslint-plugin"
import tsParser from "@typescript-eslint/parser"
import { defineConfig } from "eslint/config"
import { defineConfig, globalIgnores } from "eslint/config"
import nextVitals from "eslint-config-next/core-web-vitals"
import nextTs from "eslint-config-next/typescript"
import formatjs from "eslint-plugin-formatjs"
import simpleImportSort from "eslint-plugin-simple-import-sort"
const compat = new FlatCompat({
recommendedConfig: js.configs.recommended,
allConfig: js.configs.all,
})
export default defineConfig([
...nextVitals,
...nextTs,
globalIgnores([
".next/**",
"node_modules/**",
"dist/**",
"build/**",
".netlify/**",
"public/**",
"playwright-report/**",
"test-results/**",
"coverage/**",
"*.config.js",
"*.config.ts",
"*.config.mjs",
"next-env.d.ts",
]),
{
ignores: [
".next/**",
"node_modules/**",
"dist/**",
"build/**",
".netlify/**",
"public/**",
"playwright-report/**",
"test-results/**",
"coverage/**",
"*.config.js",
"*.config.ts",
"*.config.mjs",
"next-env.d.ts",
],
},
{
extends: compat.extends(
"next/core-web-vitals",
"plugin:@typescript-eslint/recommended"
),
files: ["**/*.{js,jsx,ts,tsx,mts}"],
plugins: {
"simple-import-sort": simpleImportSort,
"@typescript-eslint": typescriptEslint,
formatjs,
},
languageOptions: {
parser: tsParser,
},
rules: {
"no-console": "warn",
"no-unused-vars": "off",
@@ -56,6 +42,7 @@ export default defineConfig([
propElementValues: "always",
},
],
"react-hooks/incompatible-library": "off",
"import/no-relative-packages": "error",
"simple-import-sort/imports": [
"warn",

View File

@@ -78,6 +78,7 @@ export function useGuaranteeBooking(
if (bookingStatus?.data?.booking.paymentUrl && isPollingForBookingStatus) {
router.push(bookingStatus.data.booking.paymentUrl)
utils.booking.get.invalidate({ refId })
// eslint-disable-next-line react-hooks/set-state-in-effect
setIsPollingForBookingStatus(false)
} else if (bookingStatus.isTimeout) {
handleGuaranteeError("Timeout")

View File

@@ -46,6 +46,7 @@ async function fetchAndCacheRedirect(lang: Lang, pathname: string) {
"1d"
)
}
const redirectCounter = createCounter("middleware.redirect")
export const middleware: NextMiddleware = async (request) => {
const lang = findLang(request.nextUrl.pathname)!

View File

@@ -30,7 +30,7 @@
"@internationalized/date": "^3.8.0",
"@netlify/blobs": "^8.1.0",
"@netlify/functions": "^3.0.0",
"@netlify/plugin-nextjs": "^5.14.4",
"@netlify/plugin-nextjs": "^5.15.1",
"@radix-ui/react-slot": "^1.2.2",
"@react-aria/ssr": "^3.9.8",
"@scandic-hotels/booking-flow": "workspace:*",
@@ -71,12 +71,12 @@
"md5": "^2.3.0",
"motion": "^12.10.0",
"nanoid": "^5.1.5",
"next": "^15.5.7",
"next": "16.0.10",
"next-auth": "5.0.0-beta.29",
"react": "^19.1.0",
"react": "19.2.1",
"react-aria-components": "1.8.0",
"react-day-picker": "^9.6.7",
"react-dom": "^19.1.0",
"react-dom": "19.2.1",
"react-feather": "^2.0.10",
"react-focus-lock": "^2.13.6",
"react-hook-form": "^7.56.2",
@@ -89,8 +89,6 @@
"zustand": "^4.5.2"
},
"devDependencies": {
"@eslint/compat": "^1.2.9",
"@eslint/js": "^9.26.0",
"@formatjs/cli": "^6.7.1",
"@lokalise/node-api": "^14.0.0",
"@playwright/test": "^1.57.0",
@@ -102,16 +100,14 @@
"@types/json-stable-stringify-without-jsonify": "^1.0.2",
"@types/jsonwebtoken": "^9",
"@types/node": "^20",
"@types/react": "^19.2.3",
"@types/react-dom": "^19.2.3",
"@typescript-eslint/eslint-plugin": "^8.32.0",
"@typescript-eslint/parser": "^8.32.0",
"@types/react": "19.2.7",
"@types/react-dom": "19.2.3",
"@vitejs/plugin-react": "^5.0.0",
"adm-zip": "^0.5.16",
"babel-plugin-formatjs": "^10.5.39",
"dotenv": "^16.5.0",
"eslint": "^9",
"eslint-config-next": "15.3.2",
"eslint-config-next": "16.0.7",
"eslint-plugin-formatjs": "^5.3.1",
"eslint-plugin-import": "^2.31.0",
"eslint-plugin-simple-import-sort": "^12.1.1",

View File

@@ -23,6 +23,7 @@ export function AddAncillaryProvider({
children: React.ReactNode
}) {
const storeRef = useRef<AddAncillaryStore>(undefined)
// eslint-disable-next-line react-hooks/refs
if (!storeRef.current) {
storeRef.current = createAddAncillaryStore(booking, ancillaries)
}
@@ -40,6 +41,7 @@ export function AddAncillaryProvider({
}, [])
return (
// eslint-disable-next-line react-hooks/refs
<AddAncillaryContext.Provider value={storeRef.current}>
{children}
</AddAncillaryContext.Provider>

View File

@@ -23,6 +23,7 @@ export default function DestinationDataProvider({
const storeRef = useRef<DestinationDataStore>(undefined)
const searchParams = useSearchParams()
// eslint-disable-next-line react-hooks/refs
if (!storeRef.current) {
storeRef.current = createDestinationDataStore({
allCities,
@@ -36,6 +37,7 @@ export default function DestinationDataProvider({
}
return (
// eslint-disable-next-line react-hooks/refs
<DestinationDataContext.Provider value={storeRef.current}>
<DestinationDataProviderContent>
{children}

View File

@@ -20,6 +20,7 @@ export default function HotelListingDataProvider({
const storeRef = useRef<HotelListingDataStore>(undefined)
const searchParams = useSearchParams()
// eslint-disable-next-line react-hooks/refs
if (!storeRef.current) {
storeRef.current = createHotelListingDataStore({
allHotels,
@@ -30,6 +31,7 @@ export default function HotelListingDataProvider({
}
return (
// eslint-disable-next-line react-hooks/refs
<HotelListingDataContext.Provider value={storeRef.current}>
<HotelListingDataProviderContent>
{children}

View File

@@ -101,6 +101,7 @@ export default function MyStayProvider({
(isFetchedAfterMount && data) ||
(linkedReservationsIsFetchedAfterMount && linkedReservations)
// eslint-disable-next-line react-hooks/refs
if (!storeRef.current || hasInvalidatedQueryAndRefetched) {
storeRef.current = createMyStayStore({
breakfastPackages,
@@ -116,6 +117,7 @@ export default function MyStayProvider({
}
return (
// eslint-disable-next-line react-hooks/refs
<MyStayContext.Provider value={storeRef.current}>
{children}
</MyStayContext.Provider>