diff --git a/ts_config/eslint/base.js b/ts_config/eslint/base.js index 13440d1..6611f89 100644 --- a/ts_config/eslint/base.js +++ b/ts_config/eslint/base.js @@ -51,9 +51,16 @@ export { DEFAULT_IGNORES } from "./ignores.js"; * @param {boolean} [options.aliasOnly] Ban `../` parent-relative imports so * every cross-directory import goes through the repo's `@/` alias. Default * true; a repo with no alias configured turns it off with a reason. + * @param {string[]} [options.project] tsconfig paths for the type-aware + * rules, relative to tsconfigRootDir. Default is the project service + * (nearest tsconfig.json per file). A package whose tsconfig.json excludes + * its tests so they never land in dist/ passes its lint tsconfig here. */ -export function base({ aliasOnly = true, ignores = [], tsconfigRootDir }) { +export function base({ aliasOnly = true, ignores = [], project, tsconfigRootDir }) { requireNonEmptyString(tsconfigRootDir, "base() tsconfigRootDir"); + const parserOptions = project + ? { project, tsconfigRootDir } + : { projectService: true, tsconfigRootDir }; return tseslint.config( { ignores: [...DEFAULT_IGNORES, ...ignores] }, { linterOptions: { reportUnusedDisableDirectives: "error" } }, @@ -67,9 +74,7 @@ export function base({ aliasOnly = true, ignores = [], tsconfigRootDir }) { { files: TS_FILES, - languageOptions: { - parserOptions: { projectService: true, tsconfigRootDir }, - }, + languageOptions: { parserOptions }, rules: { // `type X = {}` and `interface X {}` are interchangeable for the // shapes this codebase writes; picking one keeps diffs quiet. diff --git a/ts_config/eslint/index.js b/ts_config/eslint/index.js index f43d996..b245964 100644 --- a/ts_config/eslint/index.js +++ b/ts_config/eslint/index.js @@ -24,6 +24,7 @@ export { vitestConfig } from "./vitest.js"; * consumer's eslint.config.js. * @param {string[]} [options.ignores] * @param {boolean} [options.aliasOnly] + * @param {string[]} [options.project] See base(). * @param {boolean} [options.react] Stack the React layer. * @param {boolean} [options.vitest] Stack the vitest rules (default true). * @param {boolean} [options.playwright] Stack the Playwright rules. @@ -38,12 +39,13 @@ export function defineConfig({ ignores, overrides = [], playwright = false, + project, react: withReact = false, tsconfigRootDir, vitest = true, }) { return [ - ...base({ aliasOnly, ignores, tsconfigRootDir }), + ...base({ aliasOnly, ignores, project, tsconfigRootDir }), ...(withReact ? react : []), ...(vitest ? vitestConfig : []), ...(playwright ? playwrightConfig : []), diff --git a/ts_config/eslint/overrides.js b/ts_config/eslint/overrides.js index d828dff..70aad87 100644 --- a/ts_config/eslint/overrides.js +++ b/ts_config/eslint/overrides.js @@ -11,6 +11,13 @@ /** @type {import("eslint").Linter.Config[]} */ export const overrides = [ + { + // A package's entry point IS a barrel: its whole job is to name the + // public API. The rule stays on everywhere else, where a barrel is how + // `import { x } from "@/utils"` ends up pulling in forty modules. + files: ["src/index.{ts,tsx}", "index.{ts,tsx}"], + rules: { "unicorn/no-barrel-files": "off" }, + }, { rules: { // Directly contradicts unicorn/consistent-class-member-order: one wants diff --git a/ts_config/eslint/react.js b/ts_config/eslint/react.js index a4588e4..3c33966 100644 --- a/ts_config/eslint/react.js +++ b/ts_config/eslint/react.js @@ -45,4 +45,12 @@ export const react = [ files: ["**/*.tsx", "**/*.jsx"], ...reactRefresh.configs.vite, }, + { + files: TS_FILES, + rules: { + // `className` is React's own prop name; in a React codebase the rule + // fires on every styled element and cannot be satisfied. + "unicorn/no-keyword-prefix": "off", + }, + }, ]; diff --git a/ts_config/pnpm-workspace.yaml b/ts_config/pnpm-workspace.yaml index cc8a728..1846b62 100644 --- a/ts_config/pnpm-workspace.yaml +++ b/ts_config/pnpm-workspace.yaml @@ -1,4 +1,4 @@ allowBuilds: - unrs-resolver: set this to true or false + unrs-resolver: false minimumReleaseAgeExclude: - eslint-plugin-unicorn@75.0.0 diff --git a/ts_config/test/config.test.js b/ts_config/test/config.test.js index a6c29b6..4c32b50 100644 --- a/ts_config/test/config.test.js +++ b/ts_config/test/config.test.js @@ -36,6 +36,17 @@ describe("base()", () => { expect(entry?.languageOptions.parserOptions.tsconfigRootDir).toBe(ROOT); }); + it("uses an explicit project list instead of the project service when given", () => { + const entry = base({ project: ["./tsconfig.lint.json"], tsconfigRootDir: ROOT }).find( + (candidate) => candidate.languageOptions?.parserOptions?.project, + ); + expect(entry.languageOptions.parserOptions).toEqual({ + project: ["./tsconfig.lint.json"], + tsconfigRootDir: ROOT, + }); + expect(entry.languageOptions.parserOptions.projectService).toBeUndefined(); + }); + it("drops the alias-only import ban when asked", () => { expect(ruleIds(base({ tsconfigRootDir: ROOT }))).toContain("no-restricted-imports"); expect(ruleIds(base({ aliasOnly: false, tsconfigRootDir: ROOT }))).not.toContain(