lint.config.ts
import { antipattern, correctness } from "@effect/tsgo/oxlint-presets";
import solidV2Strict from "eslint-plugin-solid/configs/v2-strict";
import { defineConfig } from "oxlint";
import type { OxlintConfig } from "oxlint";
import antiSlop from "ultracite/oxlint/anti-slop";
import core from "ultracite/oxlint/core";
const generatedIgnores = ["**/file-routes.d.ts", "docs/design/**", "**/styled-system/**"];
const effectErrorRules = {
"effecttsgo/floating-effect": "error",
"effecttsgo/floating-effect-in-vitest": "error",
"effecttsgo/global-error-in-effect-catch": "error",
"effecttsgo/missing-effect-context": "error",
"effecttsgo/missing-effect-error": "error",
"effecttsgo/missing-layer-context": "error",
"effecttsgo/missing-star-in-yield-effect-gen": "error",
"effecttsgo/outdated-api": "error",
"effecttsgo/return-effect-in-gen": "error",
"effecttsgo/run-effect-inside-effect": "error",
"effecttsgo/try-catch-in-effect-gen": "error",
"effecttsgo/unknown-in-effect-catch": "error",
} as const;
const vitePlusPlugin = {
name: "vite-plus",
specifier: "vite-plus/oxlint-plugin",
};
const oxlintFromEslintLevels = (rules: Record<string, 0 | 1 | 2>) =>
Object.fromEntries(
Object.entries(rules).map(([name, level]) => {
if (level === 2) {
return [name, "error"];
}
if (level === 1) {
return [name, "warn"];
}
return [name, "off"];
}),
);
export const lintConfig = (
paths: {
api: string[];
components: string[];
effect: string[];
web: string[];
},
opts?: { root?: boolean },
): OxlintConfig => {
const effectOverride =
paths.api.length === 0
? []
: [
{
files: paths.api,
rules: {
...antipattern.rules,
...correctness.rules,
...effectErrorRules,
},
},
];
const config: OxlintConfig = defineConfig({
extends: [core, antiSlop],
ignorePatterns: [...(core.ignorePatterns ?? []), ...generatedIgnores],
jsPlugins: [vitePlusPlugin, ...(antiSlop.jsPlugins ?? [])],
overrides: [
{
files: paths.web,
jsPlugins: ["eslint-plugin-solid"],
rules: {
...oxlintFromEslintLevels(solidV2Strict.rules),
"solid/reactivity": "error",
// String styles carry CSS variables. The object autofix does not
// type-check against those props.
"solid/style-prop": "off",
},
},
{
files: paths.effect,
rules: {
// Effect's catch/tapError/flatMap/forEach take callbacks and are neither promises nor arrays; these rules match on names.
"promise/prefer-await-to-callbacks": "off",
"promise/prefer-await-to-then": "off",
"unicorn/no-array-for-each": "off",
"unicorn/no-array-method-this-argument": "off",
},
},
{
files: paths.components,
rules: {
"func-style": ["error", "declaration", { allowArrowFunctions: true }],
"unicorn/filename-case": ["error", { cases: { kebabCase: true, pascalCase: true } }],
},
},
...effectOverride,
{
files: ["packages/contracts/**", "**/*-error.ts", "**/errors/*.ts"],
rules: {
// `Schema.TaggedError<E>()(...)` is a class factory, not a thrown
// error; the rule matches on the callee name alone.
"unicorn/throw-new-error": "off",
},
},
{
files: ["packages/*/src/index.ts"],
rules: {
// A package's public surface is one import path by design.
"oxc/no-barrel-file": "off",
},
},
{
files: ["packages/db/src/schema.ts"],
rules: {
// Column order in drizzle table objects defines DDL column order;
// keep the better-auth table layout.
"sort-keys": "off",
},
},
],
plugins: [...(core.plugins ?? []), "effecttsgo"],
rules: {
"import/no-namespace": "off",
// Effect idioms these generic rules misread: a domain's tagged errors
// share a file, `const X = Schema.Struct(...)` pairs with
// `interface X extends Schema.Schema.Type<typeof X> {}`.
"max-classes-per-file": "off",
"no-redeclare": "off",
"typescript/no-empty-interface": "off",
"typescript/no-empty-object-type": "off",
"vite-plus/prefer-vite-plus-imports": "error",
},
settings: solidV2Strict.settings,
});
if (opts?.root === true) {
config.options = { typeAware: true, typeCheck: true };
}
return config;
};
export const rootLint = lintConfig(
{
api: ["apps/api/**", "e2e/**", "packages/contracts/**"],
components: ["apps/web/**/*.tsx"],
effect: [
"apps/api/**",
"e2e/**",
"packages/**",
"apps/web/src/api/**",
"apps/web/src/uploads/**",
],
web: ["apps/web/**"],
},
{ root: true },
);
export const webLint = lintConfig({
api: [],
components: ["**/*.tsx"],
effect: ["src/api/**", "src/uploads/**"],
web: ["**/*.{js,jsx,ts,tsx}"],
});