Upgrade frontend toolchain to latest majors

Bump React 19.2, Relay 21, React Router 8, Vite 8 with
@vitejs/plugin-react 6, ESLint 10, GraphQL 17, TypeScript 6,
@types/node 24, and Tailwind 4.3 across the workspaces.

vite-plugin-react 6 (Vite 8) no longer runs Babel, so the Relay
tagged-template transform now runs through @rolldown/plugin-babel
in the console and trust Vite configs.

Relay 21 ships first-party types and enables the ambiguous-alias
check by default; disable that flag to preserve existing queries
and add explicit usePreloadedQuery type arguments where the new
types no longer infer the operation. TypeScript 6 deprecations and
stricter inference are addressed in tsconfigs and call sites.

Keep n8n-node on ESLint 9 and eslint-plugin-react on 7.37.5, the
newest releases compatible with their toolchains.

Signed-off-by: Émile Ré <emile@probo.com>
This commit is contained in:
Émile Ré
2026-06-18 19:10:00 +02:00
parent f1fc2dc0e0
commit f98b73f073
130 changed files with 2228 additions and 1763 deletions

View File

@@ -56,6 +56,6 @@
},
"devDependencies": {
"esbuild": "^0.28.1",
"typescript": "~5.8.3"
"typescript": "~6.0.3"
}
}

View File

@@ -113,6 +113,7 @@ export class CookieDetector implements Detector {
this.cookieStoreHandler = (event: CookieChangeEvent) => {
for (const cookie of event.changed) {
if (cookie.name === undefined) continue;
if (this.knownNames.has(cookie.name)) continue;
const maxAge = cookie.expires

View File

@@ -11,7 +11,7 @@
"author": "",
"license": "ISC",
"devDependencies": {
"typescript": "^5.8.3",
"typescript": "~6.0.3",
"vitest": "^4.1.8"
}
}

View File

@@ -9,14 +9,14 @@
"export": "email export"
},
"dependencies": {
"react": "^19.0.0",
"react-dom": "^19.0.0",
"react": "^19.2.7",
"react-dom": "^19.2.7",
"react-email": "^6.6.3"
},
"devDependencies": {
"@types/node": "^22.10.7",
"@types/react": "^19.0.6",
"@types/node": "^24",
"@types/react": "^19.2.17",
"tsx": "^4.21.0",
"typescript": "^5.7.3"
"typescript": "~6.0.3"
}
}

View File

@@ -26,16 +26,17 @@
"author": "",
"license": "ISC",
"devDependencies": {
"@eslint/js": "^9.39.2",
"@eslint/js": "^10.0.1",
"@probo/eslint-plugin-relay-types": "^1.0.0",
"@stylistic/eslint-plugin": "^5.7.0",
"eslint-plugin-import-x": "^4.16.1",
"eslint-plugin-react-hooks": "^7.0.1",
"@stylistic/eslint-plugin": "^5.10.0",
"eslint-plugin-import-x": "^4.16.2",
"eslint-plugin-react-hooks": "^7.1.1",
"eslint-plugin-react": "^7.37.5",
"eslint-plugin-relay": "^2.0.0",
"eslint": "^9.39.2",
"globals": "^17.0.0",
"typescript-eslint": "^8.53.1",
"typescript": "^5.8.3"
"eslint": "^10.5.0",
"globals": "^17.6.0",
"jiti": "^2.2.0",
"typescript-eslint": "^8.61.1",
"typescript": "~6.0.3"
}
}

View File

@@ -31,7 +31,10 @@ export const reactConfigs: FlatConfig.ConfigArray = [
{
settings: {
react: {
version: "detect",
// Pin an explicit version: eslint-plugin-react@7.37.5 predates ESLint 10
// and its "detect" path calls the removed context.getFilename(), which
// crashes under ESLint 10.
version: "19.2",
},
},
},

View File

@@ -11,7 +11,7 @@
"author": "",
"license": "ISC",
"devDependencies": {
"typescript": "^5.8.3",
"typescript": "~6.0.3",
"vitest": "^4.1.8"
},
"dependencies": {

View File

@@ -11,7 +11,7 @@
"author": "",
"license": "ISC",
"devDependencies": {
"typescript": "^5.8.3",
"typescript": "~6.0.3",
"vitest": "^4.1.8"
}
}

View File

@@ -4,7 +4,7 @@
"author": "",
"main": "./TranslatorProvider.tsx",
"devDependencies": {
"@types/react": "^19.1.2"
"@types/react": "^19.2.17"
},
"peerDependencies": {
"react": "^19.0"

View File

@@ -56,6 +56,6 @@
},
"devDependencies": {
"@n8n/node-cli": "^0.24.1",
"eslint": "^9.39.2"
"eslint": "^9.39.4"
}
}

View File

@@ -3,6 +3,7 @@
"strict": true,
"module": "commonjs",
"moduleResolution": "node",
"ignoreDeprecations": "6.0",
"target": "es2019",
"lib": ["es2019", "es2020", "es2022.error"],
"removeComments": true,

View File

@@ -15,6 +15,6 @@
"marked": "^15.0.12"
},
"devDependencies": {
"typescript": "~5.8.3"
"typescript": "~6.0.3"
}
}

View File

@@ -12,16 +12,16 @@
"author": "",
"license": "ISC",
"devDependencies": {
"typescript": "^5.8.3",
"typescript": "~6.0.3",
"vitest": "^4.1.8"
},
"peerDependencies": {
"react": "^19.2.1",
"react-relay": "^20.1.1",
"react-router": "^7.17.0",
"relay-runtime": "^20.1.1"
"react": "^19.2.7",
"react-relay": "^21.0.1",
"react-router": "^8.0.0",
"relay-runtime": "^21.0.1"
},
"dependencies": {
"graphql": "^16.12.0"
"graphql": "^17.0.1"
}
}

View File

@@ -11,13 +11,13 @@
"author": "",
"license": "ISC",
"devDependencies": {
"typescript": "^5.8.3",
"typescript": "~6.0.3",
"vitest": "^4.1.8"
},
"peerDependencies": {
"react": "^19.2.1",
"react-relay": "^20.1.1",
"react-router": "^7.17.0",
"relay-runtime": "^20.1.1"
"react": "^19.2.7",
"react-relay": "^21.0.1",
"react-router": "^8.0.0",
"relay-runtime": "^21.0.1"
}
}

View File

@@ -18,14 +18,19 @@ import { type LoaderFunction, type LoaderFunctionArgs, useLoaderData } from "rea
import { type OperationType } from "relay-runtime";
import { useCleanup } from "@probo/hooks";
// Infer the concrete `queryRef` type from a naked type position. Relay 21's
// first-party types model `PreloadedQuery#variables` as `VariablesOf<TQuery>`,
// which prevents inferring `TQuery` through it, so we infer the whole queryRef.
export function withQueryRef<
TQuery extends OperationType,
TEnvironmentProviderOptions = EnvironmentProviderOptions
TQueryRef extends PreloadedQuery<OperationType>
>(
Component: ComponentType<{ queryRef: PreloadedQuery<TQuery, TEnvironmentProviderOptions> }>,
Component: ComponentType<{ queryRef: TQueryRef }>,
) {
return () => {
const { queryRef, dispose } = useLoaderData();
const { queryRef, dispose } = useLoaderData() as {
queryRef: TQueryRef;
dispose: () => void;
};
useCleanup(dispose, 1000);

View File

@@ -42,7 +42,7 @@
"remark-gfm": "^4.0.1",
"tailwind-merge": "^3.4.0",
"tailwind-variants": "^1.0.0",
"tailwindcss": "^4.1.7",
"tailwindcss": "^4.3.1",
"tw-animate-css": "^1.3.0",
"zustand": "^5.0.4"
},
@@ -54,27 +54,27 @@
"@storybook/react": "^10.2.8",
"@storybook/react-vite": "^10.2.8",
"@tailwindcss/typography": "^0.5.16",
"@tailwindcss/vite": "^4.1.17",
"@types/react": "^19.1.2",
"@types/react-dom": "^19.1.2",
"@vitejs/plugin-react": "^5.1.4",
"@tailwindcss/vite": "^4.3.1",
"@types/react": "^19.2.17",
"@types/react-dom": "^19.2.3",
"@vitejs/plugin-react": "^6.0.2",
"@vitest/browser": "4.1.8",
"@vitest/coverage-v8": "4.1.8",
"eslint": "^9.39.2",
"eslint": "^10.5.0",
"playwright": "^1.55.1",
"react": "^19.1.0",
"react": "^19.2.7",
"react-docgen": "^7.1.1",
"react-dom": "^19.1.0",
"react-router": "^7.17.0",
"react-dom": "^19.2.7",
"react-router": "^8.0.0",
"storybook": "^10.2.10",
"typescript": "~5.8.3",
"vite": "^7.3.2",
"typescript": "~6.0.3",
"vite": "^8.0.16",
"vitest": "^4.1.8"
},
"peerDependencies": {
"react": "^19.1.0",
"react-dom": "^19.1.0",
"react-router": "^7.17.0"
"react": "^19.2.7",
"react-dom": "^19.2.7",
"react-router": "^8.0.0"
},
"volta": {
"node": "24.4.0"

View File

@@ -81,7 +81,7 @@ export function Th({
);
}
const TrContext = createContext({} as { to?: string });
const TrContext = createContext<{ to?: string }>({});
export function Tr({
to,

View File

@@ -14,6 +14,7 @@
import { objectKeys } from "@probo/helpers";
import type { Meta, StoryObj } from "@storybook/react";
import type { ComponentType } from "react";
import * as icons from "./Atoms/Icons";
@@ -183,7 +184,7 @@ export const Icons: Story = {
<div className="flex flex-wrap gap-4">
{objectKeys(icons).map((k) => {
/* eslint import-x/namespace: ['error', { allowComputed: true }] */
const IconComponent = icons[k];
const IconComponent = icons[k] as ComponentType;
return (
<button
title={k}

View File

@@ -36,8 +36,8 @@ type Props = PropsWithChildren<{
sidebar?: ReactNode;
}>;
const LayoutContext = createContext({
setDrawer: (() => {}) as (v: boolean) => void,
const LayoutContext = createContext<{ setDrawer: (v: boolean) => void }>({
setDrawer: () => {},
});
export function Layout({

View File

@@ -38,12 +38,14 @@ type State = {
onConfirm: () => void | Promise<unknown>;
};
const initialConfirmState: State = {
message: null,
onConfirm: () => Promise.resolve(),
};
const useConfirmStore = create(
combine(
{
message: null,
onConfirm: () => Promise.resolve(),
} as State,
initialConfirmState,
set => ({
open: (props: State) => {
set(props);

View File

@@ -25,22 +25,20 @@ export default {
type Story = StoryObj<typeof MeasureImplementation>;
type MeasureState = "IMPLEMENTED" | "IN_PROGRESS" | "NOT_APPLICABLE" | "NOT_STARTED" | "UNKNOWN" | "NOT_IMPLEMENTED";
export const Default: Story = {
args: {
measures: [
...times(15, () => ({
state: "IMPLEMENTED" as MeasureState,
state: "IMPLEMENTED" as const,
})),
...times(10, () => ({
state: "IN_PROGRESS" as MeasureState,
state: "IN_PROGRESS" as const,
})),
...times(5, () => ({
state: "NOT_APPLICABLE" as MeasureState,
state: "NOT_APPLICABLE" as const,
})),
...times(5, () => ({
state: "NOT_STARTED" as MeasureState,
state: "NOT_STARTED" as const,
})),
],
},

View File

@@ -131,7 +131,7 @@ export const SlashCommandExtension = Extension.create<object, SlashCommandStorag
"data-empty": isEmpty
? "true"
: "false",
} as Record<string, string>),
}),
]);
} catch {
deactivateSlashCommand(storage);

View File

@@ -14,5 +14,5 @@ export function cellDomElement(
while (el && !(el instanceof HTMLTableCellElement)) {
el = (el as HTMLElement).parentElement;
}
return el as HTMLElement | null;
return el;
}