mirror of
https://github.com/OpenHands/OpenHands.git
synced 2025-12-25 21:36:52 +08:00
chore(frontend): Migrate from Remix to React Router 7 (#5304)
This commit is contained in:
parent
5069a8700a
commit
a378ff0965
1
frontend/.gitignore
vendored
1
frontend/.gitignore
vendored
@ -7,3 +7,4 @@ node_modules/
|
||||
/playwright-report/
|
||||
/blob-report/
|
||||
/playwright/.cache/
|
||||
.react-router/
|
||||
|
||||
@ -26,8 +26,8 @@ describe("Empty state", () => {
|
||||
}));
|
||||
|
||||
beforeAll(() => {
|
||||
vi.mock("@remix-run/react", async (importActual) => ({
|
||||
...(await importActual<typeof import("@remix-run/react")>()),
|
||||
vi.mock("react-router", async (importActual) => ({
|
||||
...(await importActual<typeof import("react-router")>()),
|
||||
useRouteLoaderData: vi.fn(() => ({})),
|
||||
}));
|
||||
|
||||
@ -290,8 +290,8 @@ describe.skip("ChatInterface", () => {
|
||||
});
|
||||
|
||||
it("should render both GitHub buttons initially when ghToken is available", () => {
|
||||
vi.mock("@remix-run/react", async (importActual) => ({
|
||||
...(await importActual<typeof import("@remix-run/react")>()),
|
||||
vi.mock("react-router", async (importActual) => ({
|
||||
...(await importActual<typeof import("react-router")>()),
|
||||
useRouteLoaderData: vi.fn(() => ({ ghToken: "test-token" })),
|
||||
}));
|
||||
|
||||
@ -315,8 +315,8 @@ describe.skip("ChatInterface", () => {
|
||||
});
|
||||
|
||||
it("should render only 'Push changes to PR' button after PR is created", async () => {
|
||||
vi.mock("@remix-run/react", async (importActual) => ({
|
||||
...(await importActual<typeof import("@remix-run/react")>()),
|
||||
vi.mock("react-router", async (importActual) => ({
|
||||
...(await importActual<typeof import("react-router")>()),
|
||||
useRouteLoaderData: vi.fn(() => ({ ghToken: "test-token" })),
|
||||
}));
|
||||
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import { afterEach, beforeAll, describe, expect, it, vi } from "vitest";
|
||||
import { createRemixStub } from "@remix-run/testing";
|
||||
import { createRoutesStub } from "react-router";
|
||||
import { screen, waitFor, within } from "@testing-library/react";
|
||||
import { renderWithProviders } from "test-utils";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
@ -8,7 +8,7 @@ import * as CaptureConsent from "#/utils/handle-capture-consent";
|
||||
import i18n from "#/i18n";
|
||||
|
||||
describe("frontend/routes/_oh", () => {
|
||||
const RemixStub = createRemixStub([{ Component: MainApp, path: "/" }]);
|
||||
const RouteStub = createRoutesStub([{ Component: MainApp, path: "/" }]);
|
||||
|
||||
const { userIsAuthenticatedMock, settingsAreUpToDateMock } = vi.hoisted(
|
||||
() => ({
|
||||
@ -34,26 +34,26 @@ describe("frontend/routes/_oh", () => {
|
||||
});
|
||||
|
||||
it("should render", async () => {
|
||||
renderWithProviders(<RemixStub />);
|
||||
renderWithProviders(<RouteStub />);
|
||||
await screen.findByTestId("root-layout");
|
||||
});
|
||||
|
||||
it("should render the AI config modal if the user is authed", async () => {
|
||||
// Our mock return value is true by default
|
||||
renderWithProviders(<RemixStub />);
|
||||
renderWithProviders(<RouteStub />);
|
||||
await screen.findByTestId("ai-config-modal");
|
||||
});
|
||||
|
||||
it("should render the AI config modal if settings are not up-to-date", async () => {
|
||||
settingsAreUpToDateMock.mockReturnValue(false);
|
||||
renderWithProviders(<RemixStub />);
|
||||
renderWithProviders(<RouteStub />);
|
||||
|
||||
await screen.findByTestId("ai-config-modal");
|
||||
});
|
||||
|
||||
it("should not render the AI config modal if the settings are up-to-date", async () => {
|
||||
settingsAreUpToDateMock.mockReturnValue(true);
|
||||
renderWithProviders(<RemixStub />);
|
||||
renderWithProviders(<RouteStub />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.queryByTestId("ai-config-modal")).not.toBeInTheDocument();
|
||||
@ -67,7 +67,7 @@ describe("frontend/routes/_oh", () => {
|
||||
"handleCaptureConsent",
|
||||
);
|
||||
|
||||
renderWithProviders(<RemixStub />);
|
||||
renderWithProviders(<RouteStub />);
|
||||
|
||||
// The user has not consented to tracking
|
||||
const consentForm = await screen.findByTestId("user-capture-consent-form");
|
||||
@ -89,7 +89,7 @@ describe("frontend/routes/_oh", () => {
|
||||
|
||||
it("should not render the user consent form if the user has already made a decision", async () => {
|
||||
localStorage.setItem("analytics-consent", "true");
|
||||
renderWithProviders(<RemixStub />);
|
||||
renderWithProviders(<RouteStub />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(
|
||||
@ -101,12 +101,12 @@ describe("frontend/routes/_oh", () => {
|
||||
// TODO: Likely failing due to how tokens are now handled in context. Move to e2e tests
|
||||
it.skip("should render a new project button if a token is set", async () => {
|
||||
localStorage.setItem("token", "test-token");
|
||||
const { rerender } = renderWithProviders(<RemixStub />);
|
||||
const { rerender } = renderWithProviders(<RouteStub />);
|
||||
|
||||
await screen.findByTestId("new-project-button");
|
||||
|
||||
localStorage.removeItem("token");
|
||||
rerender(<RemixStub />);
|
||||
rerender(<RouteStub />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(
|
||||
@ -118,17 +118,17 @@ describe("frontend/routes/_oh", () => {
|
||||
// TODO: Move to e2e tests
|
||||
it.skip("should update the i18n language when the language settings change", async () => {
|
||||
const changeLanguageSpy = vi.spyOn(i18n, "changeLanguage");
|
||||
const { rerender } = renderWithProviders(<RemixStub />);
|
||||
const { rerender } = renderWithProviders(<RouteStub />);
|
||||
|
||||
// The default language is English
|
||||
expect(changeLanguageSpy).toHaveBeenCalledWith("en");
|
||||
|
||||
localStorage.setItem("LANGUAGE", "es");
|
||||
|
||||
rerender(<RemixStub />);
|
||||
rerender(<RouteStub />);
|
||||
expect(changeLanguageSpy).toHaveBeenCalledWith("es");
|
||||
|
||||
rerender(<RemixStub />);
|
||||
rerender(<RouteStub />);
|
||||
// The language has not changed, so the spy should not have been called again
|
||||
expect(changeLanguageSpy).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
@ -139,7 +139,7 @@ describe("frontend/routes/_oh", () => {
|
||||
localStorage.setItem("ghToken", "test-token");
|
||||
|
||||
// const logoutCleanupSpy = vi.spyOn(LogoutCleanup, "logoutCleanup");
|
||||
renderWithProviders(<RemixStub />);
|
||||
renderWithProviders(<RouteStub />);
|
||||
|
||||
const userActions = await screen.findByTestId("user-actions");
|
||||
const userAvatar = within(userActions).getByTestId("user-avatar");
|
||||
|
||||
10674
frontend/package-lock.json
generated
10674
frontend/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -9,11 +9,10 @@
|
||||
"dependencies": {
|
||||
"@monaco-editor/react": "^4.6.0",
|
||||
"@nextui-org/react": "^2.4.8",
|
||||
"@react-router/node": "^7.0.1",
|
||||
"@react-router/serve": "^7.0.1",
|
||||
"@react-types/shared": "^3.25.0",
|
||||
"@reduxjs/toolkit": "^2.3.0",
|
||||
"@remix-run/node": "^2.11.2",
|
||||
"@remix-run/react": "^2.11.2",
|
||||
"@remix-run/serve": "^2.11.2",
|
||||
"@tanstack/react-query": "^5.60.5",
|
||||
"@vitejs/plugin-react": "^4.3.2",
|
||||
"@xterm/addon-fit": "^0.10.0",
|
||||
@ -36,7 +35,7 @@
|
||||
"react-icons": "^5.3.0",
|
||||
"react-markdown": "^9.0.1",
|
||||
"react-redux": "^9.1.2",
|
||||
"react-router-dom": "^6.26.1",
|
||||
"react-router": "^7.0.1",
|
||||
"react-syntax-highlighter": "^15.6.1",
|
||||
"react-textarea-autosize": "^8.5.4",
|
||||
"remark-gfm": "^4.0.0",
|
||||
@ -48,9 +47,9 @@
|
||||
"ws": "^8.18.0"
|
||||
},
|
||||
"scripts": {
|
||||
"dev": "npm run make-i18n && cross-env VITE_MOCK_API=false remix vite:dev",
|
||||
"dev:mock": "npm run make-i18n && cross-env VITE_MOCK_API=true remix vite:dev",
|
||||
"build": "npm run make-i18n && tsc && remix vite:build",
|
||||
"dev": "npm run make-i18n && cross-env VITE_MOCK_API=false react-router dev",
|
||||
"dev:mock": "npm run make-i18n && cross-env VITE_MOCK_API=true react-router dev",
|
||||
"build": "npm run make-i18n && tsc && react-router build",
|
||||
"start": "npx sirv-cli build/ --single",
|
||||
"test": "vitest run",
|
||||
"test:e2e": "playwright test",
|
||||
@ -61,7 +60,8 @@
|
||||
"prelint": "npm run make-i18n",
|
||||
"lint": "eslint src --ext .ts,.tsx,.js && prettier --check src/**/*.{ts,tsx}",
|
||||
"lint:fix": "eslint src --ext .ts,.tsx,.js --fix && prettier --write src/**/*.{ts,tsx}",
|
||||
"prepare": "cd .. && husky frontend/.husky"
|
||||
"prepare": "cd .. && husky frontend/.husky",
|
||||
"typecheck": "react-router typegen && tsc"
|
||||
},
|
||||
"husky": {
|
||||
"hooks": {
|
||||
@ -76,8 +76,7 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@playwright/test": "^1.48.2",
|
||||
"@remix-run/dev": "^2.11.2",
|
||||
"@remix-run/testing": "^2.11.2",
|
||||
"@react-router/dev": "^7.0.1",
|
||||
"@tailwindcss/typography": "^0.5.15",
|
||||
"@tanstack/eslint-plugin-query": "^5.60.1",
|
||||
"@testing-library/jest-dom": "^6.6.1",
|
||||
|
||||
35
frontend/react-router.config.ts
Normal file
35
frontend/react-router.config.ts
Normal file
@ -0,0 +1,35 @@
|
||||
import type { Config } from "@react-router/dev/config";
|
||||
|
||||
/**
|
||||
* This script is used to unpack the client directory from the frontend build directory.
|
||||
* Remix SPA mode builds the client directory into the build directory. This function
|
||||
* moves the contents of the client directory to the build directory and then removes the
|
||||
* client directory.
|
||||
*
|
||||
* This script is used in the buildEnd function of the Vite config.
|
||||
*/
|
||||
const unpackClientDirectory = async () => {
|
||||
const fs = await import("fs");
|
||||
const path = await import("path");
|
||||
|
||||
const buildDir = path.resolve(__dirname, "build");
|
||||
const clientDir = path.resolve(buildDir, "client");
|
||||
|
||||
const files = await fs.promises.readdir(clientDir);
|
||||
await Promise.all(
|
||||
files.map((file) =>
|
||||
fs.promises.rename(
|
||||
path.resolve(clientDir, file),
|
||||
path.resolve(buildDir, file),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
await fs.promises.rmdir(clientDir);
|
||||
};
|
||||
|
||||
export default {
|
||||
appDirectory: "src",
|
||||
buildEnd: unpackClientDirectory,
|
||||
ssr: false,
|
||||
} satisfies Config;
|
||||
@ -1,5 +1,5 @@
|
||||
import React from "react";
|
||||
import { useLocation } from "react-router-dom";
|
||||
import { useLocation } from "react-router";
|
||||
import { useAuth } from "#/context/auth-context";
|
||||
import { useUserPrefs } from "#/context/user-prefs-context";
|
||||
import { useGitHubUser } from "#/hooks/query/use-github-user";
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { NavLink } from "react-router-dom";
|
||||
import { NavLink } from "react-router";
|
||||
import { cn } from "#/utils/utils";
|
||||
import { BetaBadge } from "./beta-badge";
|
||||
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { useLocation } from "@remix-run/react";
|
||||
import { useLocation } from "react-router";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import React from "react";
|
||||
import posthog from "posthog-js";
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import React from "react";
|
||||
import { useNavigate, useNavigation } from "@remix-run/react";
|
||||
import { useNavigate, useNavigation } from "react-router";
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
import posthog from "posthog-js";
|
||||
import { RootState } from "#/store";
|
||||
|
||||
@ -5,7 +5,7 @@
|
||||
* For more information, see https://remix.run/file-conventions/entry.client
|
||||
*/
|
||||
|
||||
import { RemixBrowser } from "@remix-run/react";
|
||||
import { HydratedRouter } from "react-router/dom";
|
||||
import React, { startTransition, StrictMode } from "react";
|
||||
import { hydrateRoot } from "react-dom/client";
|
||||
import { Provider } from "react-redux";
|
||||
@ -74,7 +74,7 @@ prepareApp().then(() =>
|
||||
<UserPrefsProvider>
|
||||
<AuthProvider>
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<RemixBrowser />
|
||||
<HydratedRouter />
|
||||
<PosthogInit />
|
||||
</QueryClientProvider>
|
||||
</AuthProvider>
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import { useDispatch } from "react-redux";
|
||||
import { useNavigate } from "@remix-run/react";
|
||||
import { useNavigate } from "react-router";
|
||||
import { useAuth } from "#/context/auth-context";
|
||||
import {
|
||||
initialState as browserInitialState,
|
||||
|
||||
@ -5,7 +5,7 @@ import {
|
||||
Outlet,
|
||||
Scripts,
|
||||
ScrollRestoration,
|
||||
} from "@remix-run/react";
|
||||
} from "react-router";
|
||||
import "./tailwind.css";
|
||||
import "./index.css";
|
||||
import React from "react";
|
||||
|
||||
19
frontend/src/routes.ts
Normal file
19
frontend/src/routes.ts
Normal file
@ -0,0 +1,19 @@
|
||||
import {
|
||||
type RouteConfig,
|
||||
layout,
|
||||
index,
|
||||
route,
|
||||
} from "@react-router/dev/routes";
|
||||
|
||||
export default [
|
||||
layout("routes/_oh/route.tsx", [
|
||||
index("routes/_oh._index/route.tsx"),
|
||||
route("app", "routes/_oh.app/route.tsx", [
|
||||
index("routes/_oh.app._index/route.tsx"),
|
||||
route("browser", "routes/_oh.app.browser.tsx"),
|
||||
route("jupyter", "routes/_oh.app.jupyter.tsx"),
|
||||
]),
|
||||
]),
|
||||
|
||||
route("oauth", "routes/oauth.github.callback.tsx"),
|
||||
] satisfies RouteConfig;
|
||||
@ -1,4 +1,4 @@
|
||||
import { useLocation, useNavigate } from "@remix-run/react";
|
||||
import { useLocation, useNavigate } from "react-router";
|
||||
import React from "react";
|
||||
import { useDispatch } from "react-redux";
|
||||
import { setImportedProjectZip } from "#/state/initial-query-slice";
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import React from "react";
|
||||
import { useSelector } from "react-redux";
|
||||
import { useRouteError } from "@remix-run/react";
|
||||
import { useRouteError } from "react-router";
|
||||
import { editor } from "monaco-editor";
|
||||
import { EditorProps } from "@monaco-editor/react";
|
||||
import { RootState } from "#/store";
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import { useDisclosure } from "@nextui-org/react";
|
||||
import React from "react";
|
||||
import { Outlet } from "@remix-run/react";
|
||||
import { Outlet } from "react-router";
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
import { Controls } from "#/components/features/controls/controls";
|
||||
import { RootState } from "#/store";
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import React from "react";
|
||||
import { useRouteError, isRouteErrorResponse, Outlet } from "@remix-run/react";
|
||||
import { useRouteError, isRouteErrorResponse, Outlet } from "react-router";
|
||||
import i18n from "#/i18n";
|
||||
import { useGitHubAuthUrl } from "#/hooks/use-github-auth-url";
|
||||
import { useIsAuthed } from "#/hooks/query/use-is-authed";
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { useNavigate, useSearchParams } from "@remix-run/react";
|
||||
import { useNavigate, useSearchParams } from "react-router";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import React from "react";
|
||||
import OpenHands from "#/api/open-hands";
|
||||
|
||||
@ -5,7 +5,8 @@
|
||||
"**/.server/**/*.ts",
|
||||
"**/.server/**/*.tsx",
|
||||
"**/.client/**/*.ts",
|
||||
"**/.client/**/*.tsx"
|
||||
"**/.client/**/*.tsx",
|
||||
".react-router/types/**/*",
|
||||
],
|
||||
"compilerOptions": {
|
||||
"lib": [
|
||||
@ -15,9 +16,13 @@
|
||||
],
|
||||
"target": "es2022",
|
||||
"types": [
|
||||
"@remix-run/node",
|
||||
"@react-router/node",
|
||||
"vite/client",
|
||||
],
|
||||
"rootDirs": [
|
||||
".",
|
||||
"./.react-router/types"
|
||||
],
|
||||
"allowJs": true,
|
||||
"skipLibCheck": true,
|
||||
"esModuleInterop": true,
|
||||
|
||||
@ -1,10 +1,9 @@
|
||||
/* eslint-disable import/no-extraneous-dependencies */
|
||||
/// <reference types="vitest" />
|
||||
/// <reference types="vite-plugin-svgr/client" />
|
||||
import { defineConfig, loadEnv } from "vite";
|
||||
import viteTsconfigPaths from "vite-tsconfig-paths";
|
||||
import svgr from "vite-plugin-svgr";
|
||||
import { vitePlugin as remix } from "@remix-run/dev";
|
||||
import { reactRouter } from "@react-router/dev/vite";
|
||||
import { configDefaults } from "vitest/config";
|
||||
|
||||
export default defineConfig(({ mode }) => {
|
||||
@ -24,47 +23,9 @@ export default defineConfig(({ mode }) => {
|
||||
const WS_URL = `${WS_PROTOCOL}://${VITE_BACKEND_HOST}/`;
|
||||
const FE_PORT = Number.parseInt(VITE_FRONTEND_PORT, 10);
|
||||
|
||||
/**
|
||||
* This script is used to unpack the client directory from the frontend build directory.
|
||||
* Remix SPA mode builds the client directory into the build directory. This function
|
||||
* moves the contents of the client directory to the build directory and then removes the
|
||||
* client directory.
|
||||
*
|
||||
* This script is used in the buildEnd function of the Vite config.
|
||||
*/
|
||||
const unpackClientDirectory = async () => {
|
||||
const fs = await import("fs");
|
||||
const path = await import("path");
|
||||
|
||||
const buildDir = path.resolve(__dirname, "build");
|
||||
const clientDir = path.resolve(buildDir, "client");
|
||||
|
||||
const files = await fs.promises.readdir(clientDir);
|
||||
await Promise.all(
|
||||
files.map((file) =>
|
||||
fs.promises.rename(
|
||||
path.resolve(clientDir, file),
|
||||
path.resolve(buildDir, file),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
await fs.promises.rmdir(clientDir);
|
||||
};
|
||||
|
||||
return {
|
||||
plugins: [
|
||||
!process.env.VITEST &&
|
||||
remix({
|
||||
future: {
|
||||
v3_fetcherPersist: true,
|
||||
v3_relativeSplatPath: true,
|
||||
v3_throwAbortReason: true,
|
||||
},
|
||||
appDirectory: "src",
|
||||
buildEnd: unpackClientDirectory,
|
||||
ssr: false,
|
||||
}),
|
||||
!process.env.VITEST && reactRouter(),
|
||||
viteTsconfigPaths(),
|
||||
svgr(),
|
||||
],
|
||||
@ -87,8 +48,8 @@ export default defineConfig(({ mode }) => {
|
||||
ws: true,
|
||||
changeOrigin: true,
|
||||
secure: !INSECURE_SKIP_VERIFY,
|
||||
//rewriteWsOrigin: true,
|
||||
}
|
||||
// rewriteWsOrigin: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
ssr: {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user