mirror of
https://github.com/Termix-SSH/Termix.git
synced 2026-10-28 06:05:24 +00:00
204 lines
8.0 KiB
JavaScript
204 lines
8.0 KiB
JavaScript
import js from "@eslint/js";
|
|
import globals from "globals";
|
|
import reactHooks from "eslint-plugin-react-hooks";
|
|
import reactRefresh from "eslint-plugin-react-refresh";
|
|
import unusedImports from "eslint-plugin-unused-imports";
|
|
import tseslint from "typescript-eslint";
|
|
import { globalIgnores } from "eslint/config";
|
|
|
|
export default tseslint.config([
|
|
globalIgnores(["dist", "release", "Mobile", "src/mcp-server/node_modules"]),
|
|
{
|
|
files: ["**/*.{ts,tsx}"],
|
|
extends: [
|
|
js.configs.recommended,
|
|
tseslint.configs.recommended,
|
|
reactRefresh.configs.vite,
|
|
],
|
|
plugins: {
|
|
"react-hooks": reactHooks,
|
|
"unused-imports": unusedImports,
|
|
},
|
|
languageOptions: {
|
|
ecmaVersion: 2020,
|
|
globals: globals.browser,
|
|
},
|
|
rules: {
|
|
"unused-imports/no-unused-imports": "error",
|
|
"unused-imports/no-unused-vars": [
|
|
"warn",
|
|
{
|
|
vars: "all",
|
|
varsIgnorePattern: "^_",
|
|
args: "after-used",
|
|
argsIgnorePattern: "^_",
|
|
},
|
|
],
|
|
"@typescript-eslint/no-unused-vars": "off",
|
|
"@typescript-eslint/no-explicit-any": "warn",
|
|
"@typescript-eslint/no-unused-expressions": "warn",
|
|
"no-empty": "warn",
|
|
"no-control-regex": "off",
|
|
"no-useless-assignment": "off",
|
|
"preserve-caught-error": "off",
|
|
"react-hooks/exhaustive-deps": "warn",
|
|
"react-hooks/rules-of-hooks": "error",
|
|
"react-refresh/only-export-components": "warn",
|
|
},
|
|
},
|
|
{
|
|
// MySQL has no RETURNING clause, and drizzle's mysql-core does not expose
|
|
// the method at all — a bare .returning() is a TypeError there, not a bad
|
|
// query, and it only fails on the engine no test in this repo runs against.
|
|
//
|
|
// 175 call sites were migrated off it. This is what stops number 176.
|
|
// Writes that need rows back go through repositories/returning.ts, which
|
|
// picks one statement or a read-then-write transaction per dialect.
|
|
files: ["src/backend/database/repositories/**/*.ts"],
|
|
ignores: [
|
|
// The two files whose job is to absorb these differences.
|
|
"src/backend/database/repositories/returning.ts",
|
|
"src/backend/database/repositories/mutation-result.ts",
|
|
],
|
|
rules: {
|
|
"no-restricted-syntax": [
|
|
"error",
|
|
{
|
|
selector: "CallExpression[callee.property.name='returning']",
|
|
message:
|
|
"MySQL has no RETURNING. Use insertReturning/updateReturning/deleteReturning from ./returning.js, or rowsAffected() if you only need a count. Inside a proven sqlite-only branch, disable this rule with a comment saying so.",
|
|
},
|
|
{
|
|
// `||` concatenates on SQLite and Postgres. On MySQL it is logical OR
|
|
// unless the server runs with PIPES_AS_CONCAT, so a folder path built
|
|
// this way silently became 0. Use CONCAT, which all three agree on.
|
|
selector:
|
|
"TaggedTemplateExpression[tag.name='sql'] TemplateElement[value.raw=/\\|\\|/]",
|
|
message:
|
|
"`||` is logical OR on MySQL, not concatenation. Use CONCAT(...).",
|
|
},
|
|
{
|
|
// Postgres and SQLite spell it ON CONFLICT; MySQL spells it ON
|
|
// DUPLICATE KEY and names no columns, so drizzle's mysql-core has no
|
|
// onConflictDoUpdate at all — another TypeError, not a bad query.
|
|
selector: "CallExpression[callee.property.name='onConflictDoUpdate']",
|
|
message:
|
|
"MySQL has no ON CONFLICT. Use upsert() from ./returning.js.",
|
|
},
|
|
{
|
|
// better-sqlite3 puts these on a write result; node-postgres and
|
|
// mysql2 do not, so reading them directly yields undefined — and
|
|
// Number(undefined) is NaN, which reaches the database as the string
|
|
// "NaN" and fails an integer column. Three call sites did exactly
|
|
// this and only broke on Postgres.
|
|
selector:
|
|
"MemberExpression[property.name=/^(lastInsertRowid|changes)$/]",
|
|
message:
|
|
"lastInsertRowid and changes are better-sqlite3 only. Use insertedId() or rowsAffected() from ./mutation-result.js.",
|
|
},
|
|
],
|
|
},
|
|
},
|
|
{
|
|
// Core must not reach into a plugin. A plugin can be disabled, upgraded
|
|
// or removed, so an import from core turns "disabled" into a broken
|
|
// build rather than a missing feature. Core talks to plugins through the
|
|
// runtime in src/backend/plugins/ and the dispatchers in
|
|
// database/routes/*-dispatch.ts, both of which handle absence.
|
|
//
|
|
// Tests are exempt: they import a plugin's modules on purpose to test
|
|
// them, and until A2 gives each plugin its own suite they live under
|
|
// src/*/tests/plugins/.
|
|
files: ["src/**/*.{ts,tsx}"],
|
|
ignores: ["src/**/tests/**"],
|
|
rules: {
|
|
// An error for the backend, which has dispatchers to register through
|
|
// and no remaining offenders.
|
|
"no-restricted-imports": [
|
|
"error",
|
|
{
|
|
patterns: [
|
|
{
|
|
group: ["**/plugins/*/src/backend/**"],
|
|
message:
|
|
"Core must not import a plugin backend. Register a dispatcher or an SDK service instead, so disabling the plugin degrades cleanly.",
|
|
},
|
|
],
|
|
},
|
|
],
|
|
},
|
|
},
|
|
{
|
|
// The shell never imports a plugin. It reaches plugins only through the
|
|
// registries their frontends fill via the app object (A7). This block
|
|
// also carries the backend rule for src/ui files, because flat config
|
|
// replaces a rule's options rather than merging them.
|
|
files: ["src/ui/**/*.{ts,tsx}", "src/main.tsx"],
|
|
ignores: ["src/ui/tests/**"],
|
|
rules: {
|
|
"no-restricted-imports": [
|
|
"error",
|
|
{
|
|
patterns: [
|
|
{
|
|
group: ["**/plugins/*/src/**", "**/plugins/*/dist/**"],
|
|
message:
|
|
"The shell does not import plugins. Register the surface through the app object and read it from a registry.",
|
|
},
|
|
],
|
|
},
|
|
],
|
|
},
|
|
},
|
|
{
|
|
// The other direction. A plugin is supposed to talk to core only through
|
|
// @termix/plugin-sdk, which is the contract D1 finishes enforcing.
|
|
//
|
|
// Only the backend is fenced today. Plugin backends reach core by
|
|
// relative path (../../../src/backend/...) and plugin frontends use the
|
|
// "@/" alias for shared UI, both of which are the "legacy core imports"
|
|
// debt recorded in packages/plugin-sdk/ARCHITECTURE.md: the SDK does not
|
|
// yet expose the SSH pool, host resolution, repositories or the
|
|
// component library. A7 moves the frontends onto the app object and D1
|
|
// moves the backends onto SDK APIs, and this rule tightens to match.
|
|
//
|
|
// What it already catches: a plugin backend importing the frontend tree,
|
|
// which is not debt in any direction, just a mistake.
|
|
files: ["plugins/*/src/backend/**/*.{ts,mjs}"],
|
|
rules: {
|
|
"no-restricted-imports": [
|
|
"error",
|
|
{
|
|
patterns: [
|
|
{
|
|
group: ["@/*", "**/src/ui/**"],
|
|
message:
|
|
"A plugin backend cannot import frontend code. Import from @termix/plugin-sdk, or use a relative path within the plugin.",
|
|
},
|
|
],
|
|
},
|
|
],
|
|
// A second rule name rather than a second pattern, because flat config
|
|
// replaces a rule's options rather than merging them: one rule cannot
|
|
// carry both severities, and folding this into the error above would
|
|
// fail the build on debt that has nowhere to go yet.
|
|
//
|
|
// scripts/plugin-boundary-allowlist.json holds today's offenders and
|
|
// scripts/check-plugin-boundaries.cjs fails on a new one. D1 empties
|
|
// the list and this becomes an error.
|
|
"@typescript-eslint/no-restricted-imports": [
|
|
"warn",
|
|
{
|
|
patterns: [
|
|
{
|
|
group: ["**/src/backend/**", "**/src/types/**"],
|
|
message:
|
|
"A plugin should reach core through @termix/plugin-sdk. This relative import is legacy debt tracked in scripts/plugin-boundary-allowlist.json.",
|
|
},
|
|
],
|
|
},
|
|
],
|
|
},
|
|
},
|
|
]);
|