diff --git a/.github/dependabot.yml b/.github/dependabot.yml
index 3ab69765a9..c584ea1f7c 100644
--- a/.github/dependabot.yml
+++ b/.github/dependabot.yml
@@ -73,14 +73,6 @@ updates:
- "react-dom"
- "@types/react"
- "@types/react-dom"
- typescript-eslint:
- patterns:
- - "@typescript-eslint/*"
- - "typescript-eslint"
- eslint:
- patterns:
- - "eslint"
- - "@eslint/*"
vite:
patterns:
- "vite"
diff --git a/.taskfiles/frontend.yml b/.taskfiles/frontend.yml
index d3ce0d86dd..edbeb9f14d 100644
--- a/.taskfiles/frontend.yml
+++ b/.taskfiles/frontend.yml
@@ -260,8 +260,7 @@ tasks:
desc: "Run linting"
deps: [install]
cmds:
- - task: lint:eslint
- - task: lint:dpdm
+ - task: lint:oxlint
- task: lint:colors
- task: lint:css
@@ -290,25 +289,17 @@ tasks:
cmds:
- node editor/scripts/lint/theme-lint.mjs contrast
- lint:eslint:
- desc: "Run ESLint linting"
+ lint:oxlint:
+ desc: "Run oxlint linting"
deps: [install]
cmds:
- - npx eslint --max-warnings=0
-
- lint:dpdm:
- desc: "Run circular import linting"
- deps: [install]
- cmds:
- # Globs so dpdm walks the whole tree. dpdm expands the braces itself, so this is
- # shell-agnostic. Covers the whole editor tree, including the portal layer.
- - npx dpdm "editor/src/**/*.{ts,tsx}" --circular --no-warning --no-tree --exit-code circular:1
+ - npx oxlint --config oxlint.config.ts --max-warnings=0
lint:fix:
desc: "Auto-fix lint issues"
deps: [install]
cmds:
- - npx eslint --fix
+ - npx oxlint --config oxlint.config.ts --fix
format:
desc: "Auto-fix code formatting"
diff --git a/.vscode/extensions.json b/.vscode/extensions.json
index a02437abe7..752208565c 100644
--- a/.vscode/extensions.json
+++ b/.vscode/extensions.json
@@ -19,6 +19,6 @@
"yzhang.markdown-all-in-one", // Markdown All-in-One extension for enhanced Markdown editing
"stylelint.vscode-stylelint", // Stylelint extension for CSS and SCSS linting
"redhat.vscode-yaml", // YAML extension for Visual Studio Code
- "dbaeumer.vscode-eslint", // ESLint extension for TypeScript linting
+ "oxc.oxc-vscode", // Oxc (oxlint) extension for JavaScript/TypeScript linting
]
}
diff --git a/AGENTS.md b/AGENTS.md
index 259729bc04..7383ab3c3d 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -192,7 +192,7 @@ What goes where:
- **saas** — web-only: Supabase web auth, AuthCallback, avatar canvas, `window.location`.
- **desktop** — Tauri-only: keyring authService, tauriHttpClient, native files/windows, backend routing.
-`cloud/` MUST NOT import `@supabase/*`, `@tauri-apps/*`, raw `fetch`, `window.location`, `localStorage`, `sessionStorage`, or `import.meta.env.VITE_*` (enforced by ESLint). It reaches platform-specific things only via `@app/*` seams: `services/apiClient`, `auth/session.getAccessToken`, `auth/supabase`, `platform/openExternal`, `services/billing`, `hooks/useSaaSMode` — each provided per-platform in `saas/` and `desktop/`.
+`cloud/` MUST NOT import `@supabase/*`, `@tauri-apps/*`, raw `fetch`, `window.location`, `localStorage`, `sessionStorage`, or `import.meta.env.VITE_*` (all enforced by the linter). It reaches platform-specific things only via `@app/*` seams: `services/apiClient`, `auth/session.getAccessToken`, `auth/supabase`, `platform/openExternal`, `services/billing`, `hooks/useSaaSMode` — each provided per-platform in `saas/` and `desktop/`.
Rule of thumb — **move, don't copy**: share via `cloud/`, override by shadowing the same `@app/*` path in a leaf (`saas/` or `desktop/`).
diff --git a/DeveloperGuide.md b/DeveloperGuide.md
index 286bf47fa5..7c2e21d5ca 100644
--- a/DeveloperGuide.md
+++ b/DeveloperGuide.md
@@ -158,7 +158,7 @@ Stirling-PDF/
│ │ │ └── locales/ # Internationalization files (JSON)
│ │ └── vite.config.ts # Vite configuration
│ ├── package.json # Shared workspace dependencies
-│ └── eslint.config.mjs # Shared lint config
+│ └── oxlint.config.ts # Shared lint config
├── customFiles/ # Custom static files and templates (generated at runtime used to replace existing files)
├── docs/ # Documentation files
├── exampleYmlFiles/ # Example YAML configuration files
diff --git a/frontend/.storybook/vitest.setup.ts b/frontend/.storybook/vitest.setup.ts
index 22fa4c027e..c6fe770b55 100644
--- a/frontend/.storybook/vitest.setup.ts
+++ b/frontend/.storybook/vitest.setup.ts
@@ -1,7 +1,7 @@
import { beforeAll } from "vitest";
import { setProjectAnnotations } from "@storybook/react-vite";
import * as a11yAddonAnnotations from "@storybook/addon-a11y/preview";
-// eslint-disable-next-line no-restricted-imports -- Storybook-only: the sibling preview config has no @-alias.
+// oxlint-disable-next-line no-restricted-imports -- Storybook-only: the sibling preview config has no @-alias.
import * as projectAnnotations from "./preview";
// Include addon-a11y's annotations so its axe checks run under Vitest, not only
diff --git a/frontend/README.md b/frontend/README.md
index fafed9bede..ea759538e6 100644
--- a/frontend/README.md
+++ b/frontend/README.md
@@ -6,7 +6,7 @@ All frontend commands are run from the repository root using [Task](https://task
- `task frontend:build` — production build
- `task frontend:test` — run tests
- `task frontend:test:watch` — run tests in watch mode
-- `task frontend:lint` — run ESLint + cycle detection
+- `task frontend:lint` — run linting
- `task frontend:typecheck` — run TypeScript type checking
- `task frontend:check` — run typecheck + lint + test
- `task frontend:install` — install npm dependencies
@@ -18,7 +18,7 @@ For desktop app development, see the [Tauri](#tauri) section below.
`frontend/` is a workspace containing one or more apps. Today it holds the
PDF editor under `frontend/editor/`; new apps (the developer portal, etc.)
will sit alongside it as siblings. Shared tooling — `package.json`, `node_modules`,
-`.storybook/`, ESLint, Prettier — lives at `frontend/` so every app installs
+`.storybook/`, oxlint, Prettier — lives at `frontend/` so every app installs
once and lints with the same config.
## Environment Variables
diff --git a/frontend/editor/scripts/sync-portal-docs.mts b/frontend/editor/scripts/sync-portal-docs.mts
index 08ba00137b..96f616eb46 100644
--- a/frontend/editor/scripts/sync-portal-docs.mts
+++ b/frontend/editor/scripts/sync-portal-docs.mts
@@ -13,7 +13,7 @@ import { mkdirSync, writeFileSync } from "node:fs";
import { dirname, resolve } from "node:path";
import { fileURLToPath } from "node:url";
// tsx/node16 can't resolve the @portal alias here, so import by relative .ts path.
-// eslint-disable-next-line no-restricted-imports
+// oxlint-disable-next-line no-restricted-imports
import {
buildManifest,
type CategoryMap,
diff --git a/frontend/editor/scripts/tsconfig.json b/frontend/editor/scripts/tsconfig.json
index d80666471b..9377b2c7f3 100644
--- a/frontend/editor/scripts/tsconfig.json
+++ b/frontend/editor/scripts/tsconfig.json
@@ -8,5 +8,5 @@
// sync-portal-docs.mts imports the shared transform by its .ts path (run via tsx).
"allowImportingTsExtensions": true
},
- "include": ["./**/*.ts", "./**/*.mts"]
+ "include": ["./**/*.ts", "./**/*.mts", "../../oxlint.config.ts"]
}
diff --git a/frontend/editor/src/cloud/components/shared/config/configSections/Payg.tsx b/frontend/editor/src/cloud/components/shared/config/configSections/Payg.tsx
index 7774b6bc72..da6db6e46c 100644
--- a/frontend/editor/src/cloud/components/shared/config/configSections/Payg.tsx
+++ b/frontend/editor/src/cloud/components/shared/config/configSections/Payg.tsx
@@ -32,9 +32,9 @@ import {
prepaidSnapshotFromWallet,
} from "@app/components/shared/config/configSections/usageMeters";
// Relative (not @app/*) so the co-located CSS + sibling component resolve directly.
-// eslint-disable-next-line no-restricted-imports
+// oxlint-disable-next-line no-restricted-imports
import "./Payg.css";
-// eslint-disable-next-line no-restricted-imports
+// oxlint-disable-next-line no-restricted-imports
import SpendCapControl from "./SpendCapControl";
import { useTranslation } from "react-i18next";
import type { Wallet } from "@app/hooks/useWallet";
diff --git a/frontend/editor/src/cloud/components/shared/config/configSections/PaygFree.tsx b/frontend/editor/src/cloud/components/shared/config/configSections/PaygFree.tsx
index ae3738bd5f..0df1f56d64 100644
--- a/frontend/editor/src/cloud/components/shared/config/configSections/PaygFree.tsx
+++ b/frontend/editor/src/cloud/components/shared/config/configSections/PaygFree.tsx
@@ -33,13 +33,13 @@ import LockIcon from "@mui/icons-material/LockOutlined";
import { useTranslation } from "react-i18next";
import { useRenderCount } from "@app/hooks/useRenderCount";
import { useWallet } from "@app/hooks/useWallet";
-// eslint-disable-next-line no-restricted-imports
+// oxlint-disable-next-line no-restricted-imports
import "./Payg.css";
-// eslint-disable-next-line no-restricted-imports
+// oxlint-disable-next-line no-restricted-imports
import "./PaygFree.css";
-// eslint-disable-next-line no-restricted-imports
+// oxlint-disable-next-line no-restricted-imports
import UpgradeModal from "./UpgradeModal";
-// eslint-disable-next-line no-restricted-imports
+// oxlint-disable-next-line no-restricted-imports
import { DocHelp } from "./Payg";
import {
FreeMeterPanel,
diff --git a/frontend/editor/src/cloud/components/shared/config/configSections/SpendCapControl.tsx b/frontend/editor/src/cloud/components/shared/config/configSections/SpendCapControl.tsx
index d6b8b336f4..6cd00b1b3e 100644
--- a/frontend/editor/src/cloud/components/shared/config/configSections/SpendCapControl.tsx
+++ b/frontend/editor/src/cloud/components/shared/config/configSections/SpendCapControl.tsx
@@ -12,7 +12,7 @@ import {
DEFAULT_CAP_PRESETS,
SpendCapControl as SharedSpendCapControl,
} from "@app/billing";
-// eslint-disable-next-line no-restricted-imports
+// oxlint-disable-next-line no-restricted-imports
import "./SpendCapControl.css";
export { DEFAULT_CAP_PRESETS };
diff --git a/frontend/editor/src/cloud/components/shared/config/configSections/UpgradeModal.tsx b/frontend/editor/src/cloud/components/shared/config/configSections/UpgradeModal.tsx
index bdbd84c6bc..f5ee5054fc 100644
--- a/frontend/editor/src/cloud/components/shared/config/configSections/UpgradeModal.tsx
+++ b/frontend/editor/src/cloud/components/shared/config/configSections/UpgradeModal.tsx
@@ -25,9 +25,9 @@ import ArrowBackIcon from "@mui/icons-material/ArrowBackRounded";
import ShieldIcon from "@mui/icons-material/ShieldOutlined";
import CheckCircleIcon from "@mui/icons-material/CheckCircleRounded";
import { useTranslation } from "react-i18next";
-// eslint-disable-next-line no-restricted-imports
+// oxlint-disable-next-line no-restricted-imports
import "./UpgradeModal.css";
-// eslint-disable-next-line no-restricted-imports
+// oxlint-disable-next-line no-restricted-imports
import SpendCapControl from "./SpendCapControl";
/**
diff --git a/frontend/editor/src/cloud/hooks/walletDevPreview.ts b/frontend/editor/src/cloud/hooks/walletDevPreview.ts
index 7a1477cb4f..25bd9d261a 100644
--- a/frontend/editor/src/cloud/hooks/walletDevPreview.ts
+++ b/frontend/editor/src/cloud/hooks/walletDevPreview.ts
@@ -4,7 +4,7 @@
* The cloud/ layer is the SHARED hosted experience consumed by BOTH the saas
* (web) and desktop (Tauri) leaves, so it must stay platform-portable: it can't
* read {@code import.meta.env}, {@code window.location} or web storage directly
- * (the cloud ESLint guardrail enforces this). The PAYG dev-preview route
+ * (the cloud oxlint guardrail enforces this). The PAYG dev-preview route
* ({@code /dev/payg-preview}) is a saas-only local-design affordance that
* synthesises a wallet from {@code localStorage} when the real backend isn't
* mounted — all three of those banned reads. {@link useWallet} reaches that
diff --git a/frontend/editor/src/core/components/shared/LocalIcon.tsx b/frontend/editor/src/core/components/shared/LocalIcon.tsx
index fc51cb419b..cd278ff95d 100644
--- a/frontend/editor/src/core/components/shared/LocalIcon.tsx
+++ b/frontend/editor/src/core/components/shared/LocalIcon.tsx
@@ -1,6 +1,6 @@
import React from "react";
import { addCollection, Icon } from "@iconify/react";
-import iconSet from "../../../assets/material-symbols-icons.json"; // eslint-disable-line no-restricted-imports -- Outside app paths
+import iconSet from "../../../assets/material-symbols-icons.json"; // oxlint-disable-line no-restricted-imports -- Outside app paths
// Load icons synchronously at import time - guaranteed to be ready on first render
let iconsLoaded = false;
diff --git a/frontend/editor/src/core/components/shared/config/configSections/ThirdPartyLicensesSection.tsx b/frontend/editor/src/core/components/shared/config/configSections/ThirdPartyLicensesSection.tsx
index 334e769a74..e3f7b6b990 100644
--- a/frontend/editor/src/core/components/shared/config/configSections/ThirdPartyLicensesSection.tsx
+++ b/frontend/editor/src/core/components/shared/config/configSections/ThirdPartyLicensesSection.tsx
@@ -12,7 +12,7 @@ import {
} from "@mantine/core";
import { isAxiosError } from "axios";
import apiClient from "@app/services/apiClient";
-import frontendLicenses from "../../../../../assets/3rdPartyLicenses.json"; // eslint-disable-line no-restricted-imports -- asset lives outside @app alias root
+import frontendLicenses from "../../../../../assets/3rdPartyLicenses.json"; // oxlint-disable-line no-restricted-imports -- asset lives outside @app alias root
interface Dependency {
moduleName?: string;
diff --git a/frontend/editor/src/core/data/ogMetadata.test.ts b/frontend/editor/src/core/data/ogMetadata.test.ts
index e43ef11b9e..dd19737d46 100644
--- a/frontend/editor/src/core/data/ogMetadata.test.ts
+++ b/frontend/editor/src/core/data/ogMetadata.test.ts
@@ -4,7 +4,7 @@ import path from "node:path";
import { describe, expect, it } from "vitest";
import { getToolOgImage } from "@app/data/ogImage";
// Build tooling (plain ESM, node:fs only) - import the helpers for coverage.
-// eslint-disable-next-line no-restricted-imports -- build script lives outside the @app alias root
+// oxlint-disable-next-line no-restricted-imports -- build script lives outside the @app alias root
import {
buildOgTags,
injectOg,
diff --git a/frontend/editor/src/core/hooks/tools/compare/useCompareOperation.ts b/frontend/editor/src/core/hooks/tools/compare/useCompareOperation.ts
index 7a43a443e4..3a40d9b699 100644
--- a/frontend/editor/src/core/hooks/tools/compare/useCompareOperation.ts
+++ b/frontend/editor/src/core/hooks/tools/compare/useCompareOperation.ts
@@ -665,7 +665,7 @@ export const useCompareOperation = (): CompareOperationHook => {
if (workerRef.current) {
try {
workerRef.current.terminate();
- // eslint-disable-next-line no-empty
+ // oxlint-disable-next-line no-empty
} catch {}
workerRef.current = null;
}
diff --git a/frontend/editor/src/desktop/services/tauriHttpClient.ts b/frontend/editor/src/desktop/services/tauriHttpClient.ts
index f3d674e7e8..ae226685cb 100644
--- a/frontend/editor/src/desktop/services/tauriHttpClient.ts
+++ b/frontend/editor/src/desktop/services/tauriHttpClient.ts
@@ -1,4 +1,4 @@
-/* eslint-disable @typescript-eslint/no-explicit-any -- Axios-compatible API requires matching axios's `any` signatures */
+/* oxlint-disable typescript/no-explicit-any -- Axios-compatible API requires matching axios's `any` signatures */
import { fetch } from "@tauri-apps/plugin-http";
import {
shouldUseFastLocalTransport,
diff --git a/frontend/editor/src/index.tsx b/frontend/editor/src/index.tsx
index 6ee0f6df51..d66f092bab 100644
--- a/frontend/editor/src/index.tsx
+++ b/frontend/editor/src/index.tsx
@@ -5,7 +5,7 @@
import "@app/utils/patchDomForTranslators";
import "@mantine/core/styles.css";
import "@mantine/dates/styles.css";
-import "../vite-env.d.ts"; // eslint-disable-line no-restricted-imports -- Outside app paths
+import "../vite-env.d.ts"; // oxlint-disable-line no-restricted-imports -- Outside app paths
import "@app/styles/index.css"; // Import global styles
import React from "react";
import ReactDOM from "react-dom/client";
diff --git a/frontend/editor/src/portal/api/usersBackend.saas.test.ts b/frontend/editor/src/portal/api/usersBackend.saas.test.ts
index d8facade5b..aa03ae15a8 100644
--- a/frontend/editor/src/portal/api/usersBackend.saas.test.ts
+++ b/frontend/editor/src/portal/api/usersBackend.saas.test.ts
@@ -31,7 +31,7 @@ vi.mock("@portal/auth/saasSupabase", () => ({ ensureSaasSupabase: vi.fn() }));
// The SaaS usersBackend lives under src/saas; the portal vitest project resolves
// @app to proprietary (there's no @saas alias here), so the SaaS impl can only be
// exercised by importing it directly by path.
-// eslint-disable-next-line no-restricted-imports
+// oxlint-disable-next-line no-restricted-imports
import { usersBackend } from "../../saas/portal/usersBackend";
const server = setupServer(...teamSaasHandlers);
diff --git a/frontend/editor/src/portal/components/users/UsersDirectory.test.tsx b/frontend/editor/src/portal/components/users/UsersDirectory.test.tsx
index 2c047c74e2..21e68f24d4 100644
--- a/frontend/editor/src/portal/components/users/UsersDirectory.test.tsx
+++ b/frontend/editor/src/portal/components/users/UsersDirectory.test.tsx
@@ -19,7 +19,7 @@ import type { Team } from "@portal/api/teams";
// Prove the gating against the real flavor capability files. The portal vitest
// project resolves @app to proprietary and has no @saas alias, so the SaaS set is
// reached by path; the self-hosted set uses the @proprietary alias.
-// eslint-disable-next-line no-restricted-imports
+// oxlint-disable-next-line no-restricted-imports
import { usersCapabilities as saasCaps } from "../../../saas/portal/usersCapabilities";
import { usersCapabilities as selfHostedCaps } from "@proprietary/portal/usersCapabilities";
diff --git a/frontend/editor/src/portal/views/Users.caching.test.tsx b/frontend/editor/src/portal/views/Users.caching.test.tsx
index a43cd8a835..64d839ac62 100644
--- a/frontend/editor/src/portal/views/Users.caching.test.tsx
+++ b/frontend/editor/src/portal/views/Users.caching.test.tsx
@@ -37,10 +37,12 @@ vi.mock("@app/auth/supabase/supabaseClient", () => ({
vi.mock("@portal/auth/saasSupabase", () => ({ ensureSaasSupabase: vi.fn() }));
vi.mock("@app/portal/usersCapabilities", async () => ({
+ // oxlint-disable-next-line no-restricted-imports -- resolve the real SaaS module past the mocked @app alias
usersCapabilities: (await import("../../saas/portal/usersCapabilities"))
.usersCapabilities,
}));
vi.mock("@app/portal/usersBackend", async () => ({
+ // oxlint-disable-next-line no-restricted-imports -- resolve the real SaaS module past the mocked @app alias
usersBackend: (await import("../../saas/portal/usersBackend")).usersBackend,
}));
diff --git a/frontend/editor/src/portal/views/Users.saas.test.tsx b/frontend/editor/src/portal/views/Users.saas.test.tsx
index 3b63171c5d..a81dcf1c28 100644
--- a/frontend/editor/src/portal/views/Users.saas.test.tsx
+++ b/frontend/editor/src/portal/views/Users.saas.test.tsx
@@ -45,10 +45,12 @@ vi.mock("@portal/auth/saasSupabase", () => ({ ensureSaasSupabase: vi.fn() }));
// Force the SaaS flavor: the portal vitest project resolves @app to proprietary,
// so redirect the two flavor seams to their real SaaS implementations.
vi.mock("@app/portal/usersCapabilities", async () => ({
+ // oxlint-disable-next-line no-restricted-imports -- resolve the real SaaS module past the mocked @app alias
usersCapabilities: (await import("../../saas/portal/usersCapabilities"))
.usersCapabilities,
}));
vi.mock("@app/portal/usersBackend", async () => ({
+ // oxlint-disable-next-line no-restricted-imports -- resolve the real SaaS module past the mocked @app alias
usersBackend: (await import("../../saas/portal/usersBackend")).usersBackend,
}));
diff --git a/frontend/editor/src/portal/views/teamMyCache.saas.test.ts b/frontend/editor/src/portal/views/teamMyCache.saas.test.ts
index f18c70dd0a..a4ba296535 100644
--- a/frontend/editor/src/portal/views/teamMyCache.saas.test.ts
+++ b/frontend/editor/src/portal/views/teamMyCache.saas.test.ts
@@ -35,6 +35,7 @@ vi.mock("@app/auth/supabase/supabaseClient", () => ({
// The portal test project's @app points at proprietary; resolve the flavor seam
// to the real SaaS backend (same approach as Users.saas.test).
vi.mock("@app/portal/usersBackend", async () => ({
+ // oxlint-disable-next-line no-restricted-imports -- resolve the real SaaS module past the mocked @app alias
usersBackend: (await import("../../saas/portal/usersBackend")).usersBackend,
}));
diff --git a/frontend/editor/src/proprietary/components/shared/config/configSections/ApiKeys.tsx b/frontend/editor/src/proprietary/components/shared/config/configSections/ApiKeys.tsx
index e5ccfdd727..8a3175303f 100644
--- a/frontend/editor/src/proprietary/components/shared/config/configSections/ApiKeys.tsx
+++ b/frontend/editor/src/proprietary/components/shared/config/configSections/ApiKeys.tsx
@@ -1,10 +1,10 @@
import React, { useState } from "react";
import { Anchor, Group, Stack, Text, Paper, Skeleton } from "@mantine/core";
-// eslint-disable-next-line no-restricted-imports
+// oxlint-disable-next-line no-restricted-imports
import ApiKeySection from "./apiKeys/ApiKeySection";
-// eslint-disable-next-line no-restricted-imports
+// oxlint-disable-next-line no-restricted-imports
import RefreshModal from "./apiKeys/RefreshModal";
-// eslint-disable-next-line no-restricted-imports
+// oxlint-disable-next-line no-restricted-imports
import useApiKey from "./apiKeys/hooks/useApiKey";
import { useTranslation } from "react-i18next";
import LocalIcon from "@app/components/shared/LocalIcon";
diff --git a/frontend/editor/src/proprietary/services/heuristic/heuristicEngine.ts b/frontend/editor/src/proprietary/services/heuristic/heuristicEngine.ts
index e030a9e93a..ed01b9542e 100644
--- a/frontend/editor/src/proprietary/services/heuristic/heuristicEngine.ts
+++ b/frontend/editor/src/proprietary/services/heuristic/heuristicEngine.ts
@@ -282,7 +282,7 @@ const LATIN_LETTER = /[a-z]/gi;
const WORD = /[\p{L}']+/gu;
// ASCII whitespace plus the no-break spaces pdf.js extraction commonly emits.
-// eslint-disable-next-line no-control-regex -- vertical tab is intentional ASCII whitespace
+// oxlint-disable-next-line no-control-regex -- vertical tab is intentional ASCII whitespace
const WHITESPACE = /[\t\n\x0B\f\r \u00A0\u2007\u202F]+/g;
// Structural signal patterns. Boolean-presence ones stay non-global (safe .test()),
diff --git a/frontend/editor/src/saas/auth/AuthProvider.test.tsx b/frontend/editor/src/saas/auth/AuthProvider.test.tsx
index 588507d59c..ffeca5433d 100644
--- a/frontend/editor/src/saas/auth/AuthProvider.test.tsx
+++ b/frontend/editor/src/saas/auth/AuthProvider.test.tsx
@@ -52,7 +52,7 @@ vi.mock("@app/services/userService", () => ({
}));
// Imported after the mocks so the provider picks them up.
-const { AuthProvider, useAuth } = await import("./UseSession");
+const { AuthProvider, useAuth } = await import("@app/auth/UseSession");
/** Surfaces `loading` so a test can assert on it rather than on the container. */
function LoadingProbe() {
diff --git a/frontend/editor/vite.config.ts b/frontend/editor/vite.config.ts
index 2e72d1efec..3dc3272d59 100644
--- a/frontend/editor/vite.config.ts
+++ b/frontend/editor/vite.config.ts
@@ -98,6 +98,7 @@ function prerenderOgPlugin(isSaas: boolean): PluginOption {
name: "prerender-og",
apply: "build" as const,
async closeBundle() {
+ // oxlint-disable-next-line no-restricted-imports -- vite config runs before path aliases resolve, so a relative import is required here
const { prerenderOg } = await import("./scripts/og-prerender.mjs");
const ogBase = (
process.env.VITE_OG_BASE_URL ||
diff --git a/frontend/eslint.config.mjs b/frontend/eslint.config.mjs
deleted file mode 100644
index 43fe732a4e..0000000000
--- a/frontend/eslint.config.mjs
+++ /dev/null
@@ -1,327 +0,0 @@
-// @ts-check
-
-import eslint from "@eslint/js";
-import globals from "globals";
-import { defineConfig } from "eslint/config";
-import tseslint from "typescript-eslint";
-
-const srcGlobs = [
- // The portal layers live under editor/src/portal (base) and
- // editor/src/portal-saas (saas override), so editor/src/** covers them.
- "editor/src/**/*.{js,mjs,jsx,ts,tsx}",
-];
-const nodeGlobs = [
- "scripts/**/*.{js,ts,mjs,mts}",
- "editor/scripts/**/*.{js,ts,mjs,mts}",
- // Covers editor/vite.config.ts and editor/vitest.config.ts.
- "editor/*.config.{js,ts,mjs}",
- "*.config.{js,ts,mjs}",
- ".storybook/*.{js,ts,mjs,mts,tsx}",
-];
-
-const baseRestrictedImportPatterns = [
- {
- regex: "^\\.",
- message:
- "Use a workspace alias (@app/* for editor, @portal/* for portal) instead of relative imports.",
- },
- {
- regex: "^src/",
- message: "Use a workspace alias instead of absolute src/ imports.",
- },
-];
-
-// Button/SegmentedControl/Chip must come from the shared DS (@app/ui), not Mantine.
-// If no variant fits, extend @app/ui — that layer (editor/src/core/ui) is exempt below.
-const mantineComponentImportRestrictions = [
- {
- selector:
- "ImportDeclaration[source.value='@mantine/core'] > ImportSpecifier[imported.name=/^(Button|ActionIcon|UnstyledButton|CloseButton|FileButton)$/]",
- message:
- 'Use the shared Button (@app/ui/Button) instead of the Mantine button family. variant=primary|secondary|tertiary, accent=default|neutral|brand|ai|premium|danger|success|warning; an icon-only button is ``. If no variant fits, extend the shared Button rather than importing Mantine.',
- },
- {
- selector:
- "ImportDeclaration[source.value='@mantine/core'] > ImportSpecifier[imported.name='SegmentedControl']",
- message:
- "Use the shared SegmentedControl (@app/ui/SegmentedControl) instead of Mantine's.",
- },
- {
- selector:
- "ImportDeclaration[source.value='@mantine/core'] > ImportSpecifier[imported.name=/^(Chip|Pill)$/]",
- message:
- "Use the shared Chip (@app/ui/Chip) instead of Mantine's Chip/Pill.",
- },
-];
-
-// Raw