diff --git a/frontend/.eslintrc.cjs b/frontend/.eslintrc.cjs
index 524119d0..c508a804 100644
--- a/frontend/.eslintrc.cjs
+++ b/frontend/.eslintrc.cjs
@@ -30,9 +30,25 @@ module.exports = {
"prettier",
"plugin:prettier/recommended",
"plugin:jsx-a11y/recommended",
+ "plugin:import/recommended",
+ "plugin:import/typescript",
],
plugins: ["jsx-a11y"],
files: ["*.ts", "*.tsx", "*.cjs", "*.js"],
+ rules: {
+ "import/order": [
+ "error",
+ {
+ "newlines-between": "always",
+ alphabetize: { order: "asc" },
+ },
+ ],
+ },
+ settings: {
+ "import/resolver": {
+ typescript: true,
+ },
+ },
},
// Processor to extract GraphQL operations embedded in TS files
diff --git a/frontend/package-lock.json b/frontend/package-lock.json
index 4a89186b..036f470e 100644
--- a/frontend/package-lock.json
+++ b/frontend/package-lock.json
@@ -49,6 +49,8 @@
"eslint": "^8.39.0",
"eslint-config-prettier": "^8.8.0",
"eslint-config-react-app": "^7.0.1",
+ "eslint-import-resolver-typescript": "^3.5.5",
+ "eslint-plugin-import": "^2.27.5",
"eslint-plugin-prettier": "^4.2.1",
"postcss": "^8.4.23",
"prettier": "^2.8.8",
@@ -4187,6 +4189,26 @@
"node": ">=10.12.0"
}
},
+ "node_modules/@pkgr/utils": {
+ "version": "2.3.1",
+ "resolved": "https://registry.npmjs.org/@pkgr/utils/-/utils-2.3.1.tgz",
+ "integrity": "sha512-wfzX8kc1PMyUILA+1Z/EqoE4UCXGy0iRGMhPwdfae1+f0OXlLqCk+By+aMzgJBzR9AzS4CDizioG6Ss1gvAFJw==",
+ "dev": true,
+ "dependencies": {
+ "cross-spawn": "^7.0.3",
+ "is-glob": "^4.0.3",
+ "open": "^8.4.0",
+ "picocolors": "^1.0.0",
+ "tiny-glob": "^0.2.9",
+ "tslib": "^2.4.0"
+ },
+ "engines": {
+ "node": "^12.20.0 || ^14.18.0 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/unts"
+ }
+ },
"node_modules/@radix-ui/number": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/@radix-ui/number/-/number-1.0.0.tgz",
@@ -8973,6 +8995,19 @@
"once": "^1.4.0"
}
},
+ "node_modules/enhanced-resolve": {
+ "version": "5.13.0",
+ "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.13.0.tgz",
+ "integrity": "sha512-eyV8f0y1+bzyfh8xAwW/WTSZpLbjhqc4ne9eGSH4Zo2ejdyiNG9pU6mf9DG8a7+Auk6MFTlNOT4Y2y/9k8GKVg==",
+ "dev": true,
+ "dependencies": {
+ "graceful-fs": "^4.2.4",
+ "tapable": "^2.2.0"
+ },
+ "engines": {
+ "node": ">=10.13.0"
+ }
+ },
"node_modules/envinfo": {
"version": "7.8.1",
"resolved": "https://registry.npmjs.org/envinfo/-/envinfo-7.8.1.tgz",
@@ -9333,6 +9368,63 @@
"ms": "^2.1.1"
}
},
+ "node_modules/eslint-import-resolver-typescript": {
+ "version": "3.5.5",
+ "resolved": "https://registry.npmjs.org/eslint-import-resolver-typescript/-/eslint-import-resolver-typescript-3.5.5.tgz",
+ "integrity": "sha512-TdJqPHs2lW5J9Zpe17DZNQuDnox4xo2o+0tE7Pggain9Rbc19ik8kFtXdxZ250FVx2kF4vlt2RSf4qlUpG7bhw==",
+ "dev": true,
+ "dependencies": {
+ "debug": "^4.3.4",
+ "enhanced-resolve": "^5.12.0",
+ "eslint-module-utils": "^2.7.4",
+ "get-tsconfig": "^4.5.0",
+ "globby": "^13.1.3",
+ "is-core-module": "^2.11.0",
+ "is-glob": "^4.0.3",
+ "synckit": "^0.8.5"
+ },
+ "engines": {
+ "node": "^14.18.0 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/unts/projects/eslint-import-resolver-ts"
+ },
+ "peerDependencies": {
+ "eslint": "*",
+ "eslint-plugin-import": "*"
+ }
+ },
+ "node_modules/eslint-import-resolver-typescript/node_modules/globby": {
+ "version": "13.1.4",
+ "resolved": "https://registry.npmjs.org/globby/-/globby-13.1.4.tgz",
+ "integrity": "sha512-iui/IiiW+QrJ1X1hKH5qwlMQyv34wJAYwH1vrf8b9kBA4sNiif3gKsMHa+BrdnOpEudWjpotfa7LrTzB1ERS/g==",
+ "dev": true,
+ "dependencies": {
+ "dir-glob": "^3.0.1",
+ "fast-glob": "^3.2.11",
+ "ignore": "^5.2.0",
+ "merge2": "^1.4.1",
+ "slash": "^4.0.0"
+ },
+ "engines": {
+ "node": "^12.20.0 || ^14.13.1 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/eslint-import-resolver-typescript/node_modules/slash": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/slash/-/slash-4.0.0.tgz",
+ "integrity": "sha512-3dOsAHXXUkQTpOYcoAxLIorMTp4gIQr5IW3iVb7A7lFIp0VHhnynm9izx6TssdrIcVIESAlVjtnO2K8bg+Coew==",
+ "dev": true,
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
"node_modules/eslint-module-utils": {
"version": "2.8.0",
"resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.8.0.tgz",
@@ -10616,6 +10708,15 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/get-tsconfig": {
+ "version": "4.5.0",
+ "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.5.0.tgz",
+ "integrity": "sha512-MjhiaIWCJ1sAU4pIQ5i5OfOuHHxVo1oYeNsWTON7jxYkod8pHocXeh+SSbmu5OZZZK73B6cbJ2XADzXehLyovQ==",
+ "dev": true,
+ "funding": {
+ "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1"
+ }
+ },
"node_modules/giget": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/giget/-/giget-1.1.2.tgz",
@@ -10721,6 +10822,12 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/globalyzer": {
+ "version": "0.1.0",
+ "resolved": "https://registry.npmjs.org/globalyzer/-/globalyzer-0.1.0.tgz",
+ "integrity": "sha512-40oNTM9UfG6aBmuKxk/giHn5nQ8RVz/SS4Ir6zgzOv9/qC3kKZ9v4etGTcJbEl/NyVQH7FGU7d+X1egr57Md2Q==",
+ "dev": true
+ },
"node_modules/globby": {
"version": "11.1.0",
"resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz",
@@ -10741,6 +10848,12 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
+ "node_modules/globrex": {
+ "version": "0.1.2",
+ "resolved": "https://registry.npmjs.org/globrex/-/globrex-0.1.2.tgz",
+ "integrity": "sha512-uHJgbwAMwNFf5mLst7IWLNg14x1CkeqglJb/K3doi4dw6q2IvAAmM/Y81kevy83wP+Sst+nutFTYOGg3d1lsxg==",
+ "dev": true
+ },
"node_modules/gopd": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz",
@@ -15744,6 +15857,22 @@
"integrity": "sha512-AsS729u2RHUfEra9xJrE39peJcc2stq2+poBXX8bcM08Y6g9j/i/PUzwNQqkaJde7Ntg1TO7bSREbR5sdosQ+g==",
"dev": true
},
+ "node_modules/synckit": {
+ "version": "0.8.5",
+ "resolved": "https://registry.npmjs.org/synckit/-/synckit-0.8.5.tgz",
+ "integrity": "sha512-L1dapNV6vu2s/4Sputv8xGsCdAVlb5nRDMFU/E27D44l5U6cw1g0dGd45uLc+OXjNMmF4ntiMdCimzcjFKQI8Q==",
+ "dev": true,
+ "dependencies": {
+ "@pkgr/utils": "^2.3.1",
+ "tslib": "^2.5.0"
+ },
+ "engines": {
+ "node": "^14.18.0 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/unts"
+ }
+ },
"node_modules/tabbable": {
"version": "6.1.1",
"resolved": "https://registry.npmjs.org/tabbable/-/tabbable-6.1.1.tgz",
@@ -15787,6 +15916,15 @@
"node": ">=14.0.0"
}
},
+ "node_modules/tapable": {
+ "version": "2.2.1",
+ "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.1.tgz",
+ "integrity": "sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==",
+ "dev": true,
+ "engines": {
+ "node": ">=6"
+ }
+ },
"node_modules/tar": {
"version": "6.1.13",
"resolved": "https://registry.npmjs.org/tar/-/tar-6.1.13.tgz",
@@ -16031,6 +16169,16 @@
"node": ">=4"
}
},
+ "node_modules/tiny-glob": {
+ "version": "0.2.9",
+ "resolved": "https://registry.npmjs.org/tiny-glob/-/tiny-glob-0.2.9.tgz",
+ "integrity": "sha512-g/55ssRPUjShh+xkfx9UPDXqhckHEsHr4Vd9zX55oSdGZc/MD0m3sferOkwWtp98bv+kcVfEHtRJgBVJzelrzg==",
+ "dev": true,
+ "dependencies": {
+ "globalyzer": "0.1.0",
+ "globrex": "^0.1.2"
+ }
+ },
"node_modules/tinybench": {
"version": "2.4.0",
"resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.4.0.tgz",
diff --git a/frontend/package.json b/frontend/package.json
index 6c4ac603..692d5034 100644
--- a/frontend/package.json
+++ b/frontend/package.json
@@ -57,6 +57,8 @@
"eslint": "^8.39.0",
"eslint-config-prettier": "^8.8.0",
"eslint-config-react-app": "^7.0.1",
+ "eslint-import-resolver-typescript": "^3.5.5",
+ "eslint-plugin-import": "^2.27.5",
"eslint-plugin-prettier": "^4.2.1",
"postcss": "^8.4.23",
"prettier": "^2.8.8",
diff --git a/frontend/src/Router.tsx b/frontend/src/Router.tsx
index c1026633..a4c9044f 100644
--- a/frontend/src/Router.tsx
+++ b/frontend/src/Router.tsx
@@ -12,9 +12,9 @@
// See the License for the specific language governing permissions and
// limitations under the License.
-import { lazy, Suspense, useTransition } from "react";
-import { atomWithLocation } from "jotai-location";
import { atom, useAtomValue, useSetAtom } from "jotai";
+import { atomWithLocation } from "jotai-location";
+import { lazy, Suspense, useTransition } from "react";
import Layout from "./components/Layout";
import LoadingSpinner from "./components/LoadingSpinner";
diff --git a/frontend/src/atoms.ts b/frontend/src/atoms.ts
index d702102f..bf54e0ae 100644
--- a/frontend/src/atoms.ts
+++ b/frontend/src/atoms.ts
@@ -12,13 +12,13 @@
// See the License for the specific language governing permissions and
// limitations under the License.
-import type { ReactElement } from "react";
import { atom } from "jotai";
import { useHydrateAtoms } from "jotai/utils";
import { atomWithQuery, clientAtom } from "jotai-urql";
+import type { ReactElement } from "react";
-import { client } from "./graphql";
import { graphql } from "./gql";
+import { client } from "./graphql";
export const HydrateAtoms = ({ children }: { children: ReactElement }) => {
useHydrateAtoms([[clientAtom, client]]);
diff --git a/frontend/src/components/AddEmailForm.tsx b/frontend/src/components/AddEmailForm.tsx
index 01c6e854..796f57d6 100644
--- a/frontend/src/components/AddEmailForm.tsx
+++ b/frontend/src/components/AddEmailForm.tsx
@@ -12,14 +12,16 @@
// See the License for the specific language governing permissions and
// limitations under the License.
-import React, { useRef, useTransition } from "react";
-import { atomWithMutation } from "jotai-urql";
import { useAtom, useSetAtom } from "jotai";
+import { atomWithMutation } from "jotai-urql";
+import { useRef, useTransition } from "react";
+
import { graphql } from "../gql";
+
import Button from "./Button";
-import UserEmail from "./UserEmail";
import Input from "./Input";
import Typography from "./Typography";
+import UserEmail from "./UserEmail";
import { emailPageResultFamily } from "./UserEmailList";
const ADD_EMAIL_MUTATION = graphql(/* GraphQL */ `
diff --git a/frontend/src/components/BrowserSession.tsx b/frontend/src/components/BrowserSession.tsx
index cee9983b..9ce83f68 100644
--- a/frontend/src/components/BrowserSession.tsx
+++ b/frontend/src/components/BrowserSession.tsx
@@ -12,12 +12,13 @@
// See the License for the specific language governing permissions and
// limitations under the License.
-import Block from "./Block";
-import { Body, Subtitle } from "./Typography";
-import DateTime from "./DateTime";
import { Link } from "../Router";
import { FragmentType, graphql, useFragment } from "../gql";
+import Block from "./Block";
+import DateTime from "./DateTime";
+import { Body, Subtitle } from "./Typography";
+
const FRAGMENT = graphql(/* GraphQL */ `
fragment BrowserSession_session on BrowserSession {
id
diff --git a/frontend/src/components/BrowserSessionList.tsx b/frontend/src/components/BrowserSessionList.tsx
index 2ed8d9fe..dad39b1d 100644
--- a/frontend/src/components/BrowserSessionList.tsx
+++ b/frontend/src/components/BrowserSessionList.tsx
@@ -12,19 +12,20 @@
// See the License for the specific language governing permissions and
// limitations under the License.
-import { useTransition } from "react";
+import { atom, useAtomValue, useSetAtom } from "jotai";
import { atomFamily, atomWithDefault } from "jotai/utils";
import { atomWithQuery } from "jotai-urql";
-import { atom, useAtomValue, useSetAtom } from "jotai";
+import { useTransition } from "react";
+
+import { currentBrowserSessionIdAtom } from "../atoms";
+import { graphql } from "../gql";
+import { PageInfo } from "../gql/graphql";
+import { atomWithPagination, pageSizeAtom, Pagination } from "../pagination";
import BlockList from "./BlockList";
import BrowserSession from "./BrowserSession";
-import { Title } from "./Typography";
import PaginationControls from "./PaginationControls";
-import { graphql } from "../gql";
-import { currentBrowserSessionIdAtom } from "../atoms";
-import { atomWithPagination, pageSizeAtom, Pagination } from "../pagination";
-import { PageInfo } from "../gql/graphql";
+import { Title } from "./Typography";
const QUERY = graphql(/* GraphQL */ `
query BrowserSessionList(
diff --git a/frontend/src/components/CompatSsoLogin.tsx b/frontend/src/components/CompatSsoLogin.tsx
index 5ca70e6a..673dee9b 100644
--- a/frontend/src/components/CompatSsoLogin.tsx
+++ b/frontend/src/components/CompatSsoLogin.tsx
@@ -12,11 +12,12 @@
// See the License for the specific language governing permissions and
// limitations under the License.
-import Block from "./Block";
-import { Body, Bold, Code } from "./Typography";
-import DateTime from "./DateTime";
import { FragmentType, graphql, useFragment } from "../gql";
+import Block from "./Block";
+import DateTime from "./DateTime";
+import { Body, Bold, Code } from "./Typography";
+
const FRAGMENT = graphql(/* GraphQL */ `
fragment CompatSsoLogin_login on CompatSsoLogin {
id
diff --git a/frontend/src/components/CompatSsoLoginList.tsx b/frontend/src/components/CompatSsoLoginList.tsx
index 5f078c36..585e0bd8 100644
--- a/frontend/src/components/CompatSsoLoginList.tsx
+++ b/frontend/src/components/CompatSsoLoginList.tsx
@@ -12,18 +12,19 @@
// See the License for the specific language governing permissions and
// limitations under the License.
-import { atomWithQuery } from "jotai-urql";
import { atom, useSetAtom, useAtomValue } from "jotai";
import { atomFamily, atomWithDefault } from "jotai/utils";
+import { atomWithQuery } from "jotai-urql";
import { useTransition } from "react";
+import { graphql } from "../gql";
+import { PageInfo } from "../gql/graphql";
+import { atomWithPagination, pageSizeAtom, Pagination } from "../pagination";
+
import BlockList from "./BlockList";
import CompatSsoLogin from "./CompatSsoLogin";
-import { Title } from "./Typography";
-import { graphql } from "../gql";
-import { atomWithPagination, pageSizeAtom, Pagination } from "../pagination";
-import { PageInfo } from "../gql/graphql";
import PaginationControls from "./PaginationControls";
+import { Title } from "./Typography";
const QUERY = graphql(/* GraphQL */ `
query CompatSsoLoginList(
diff --git a/frontend/src/components/LoadingScreen.test.tsx b/frontend/src/components/LoadingScreen.test.tsx
index b2b03763..bb2c4ea9 100644
--- a/frontend/src/components/LoadingScreen.test.tsx
+++ b/frontend/src/components/LoadingScreen.test.tsx
@@ -12,13 +12,13 @@
// See the License for the specific language governing permissions and
// limitations under the License.
+import { create } from "react-test-renderer";
import { expect, it } from "vitest";
-import renderer from "react-test-renderer";
import LoadingScreen from "./LoadingScreen";
it("render