utils/ts_config/test/config.test.js
Krzysztof kuhy Rudnicki 128ede438e ts_config: @kuhyx/ts-config, the one lint/TS bar every TypeScript repo extends
Evil Martians' "ten anti-AI-slop moves" as a package: strict tsconfig base
(noUncheckedIndexedAccess, exactOptionalPropertyTypes), type-checked
typescript-eslint strict+stylistic, unicorn/sonarjs/perfectionist, behaviour
rules for tests (expect-expect, no-focused-tests), React (hooks, @eslint-react,
jsx-a11y strict, you-might-not-need-an-effect), Playwright, and layer
boundaries via eslint-plugin-boundaries 7 with the article's default policies.
Knip / jscpd / Stryker bases; Stryker carries no break threshold on purpose.

Plugins are dependencies, not devDependencies: a flat config imports them from
this package's own node_modules, so a consumer installing from the git tag
gets them or the lint cannot run. No build step, so the tag needs no prepare.

test/fixture is a mini repo with one planted violation per rule family;
test/expected.js names the rule that must catch each, and the suite runs the
real CLI over it (in a child process -- v8 coverage merges natively-loaded
and transformed copies of the same module into "uncovered"). 35 tests, 100%
branch coverage on the factories.

eslint/fast.js is the type-free subset for a per-edit hook: 0.52 s cold on
one file, measured against 1.28 s for base() without types -- importing a
plugin is the cost, so it loads none, nor base.js which imports them.

Probed under ESLint 10: jsx-a11y 6.10.2 (peer says <=9) and boundaries 7.2.0
both run; the fixture pins that.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DnAYyNH1Sm4iJNidfYbrZJ
2026-09-16 21:40:08 +02:00

131 lines
5.2 KiB
JavaScript

// The factories: what each option adds, and what a bad call refuses.
import { describe, expect, it } from "vitest";
import { base, DEFAULT_IGNORES } from "../eslint/base.js";
import { boundaries, DEFAULT_ALLOW, folderElements } from "../eslint/boundaries.js";
import fast from "../eslint/fast.js";
import { defineConfig } from "../eslint/index.js";
const ROOT = import.meta.dirname;
/** Rule ids that appear anywhere in a flat config array. */
function ruleIds(config) {
return new Set(config.flatMap((entry) => Object.keys(entry.rules ?? {})));
}
/** Plugin names registered anywhere in a flat config array. */
function pluginNames(config) {
return new Set(config.flatMap((entry) => Object.keys(entry.plugins ?? {})));
}
describe("base()", () => {
it("refuses to run without a tsconfigRootDir", () => {
expect(() => base({})).toThrow(/tsconfigRootDir/);
expect(() => base({ tsconfigRootDir: "" })).toThrow(/tsconfigRootDir/);
});
it("prepends the default ignores and keeps the caller's", () => {
const [ignores] = base({ ignores: ["public"], tsconfigRootDir: ROOT });
expect(ignores.ignores).toEqual([...DEFAULT_IGNORES, "public"]);
});
it("wires the type-aware parser at the given root", () => {
const entry = base({ tsconfigRootDir: ROOT }).find(
(candidate) => candidate.languageOptions?.parserOptions?.projectService === true,
);
expect(entry?.languageOptions.parserOptions.tsconfigRootDir).toBe(ROOT);
});
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(
"no-restricted-imports",
);
});
});
describe("boundaries()", () => {
it("refuses an empty element list", () => {
expect(() => boundaries({ elements: [], tsconfigRootDir: ROOT })).toThrow(/elements/);
expect(() => boundaries({ tsconfigRootDir: ROOT })).toThrow(/elements/);
});
it("refuses to run without a tsconfigRootDir", () => {
expect(() => boundaries({ elements: folderElements(["utils"]) })).toThrow(/tsconfigRootDir/);
});
it("emits a policy only for element types the repo declared", () => {
const [entry] = boundaries({
elements: folderElements(["components", "services"]),
tsconfigRootDir: ROOT,
});
const { policies } = entry.rules["boundaries/dependencies"][1];
const froms = policies.filter((p) => p.from.element).map((p) => p.from.element.type);
expect(froms.toSorted((a, b) => a.localeCompare(b))).toEqual(["components", "services"]);
const components = policies.find((p) => p.from.element?.type === "components");
// utils/types are not declared here, so they are filtered out of the
// allow list rather than left dangling.
expect(components.allow.to.element.types.anyOf).toEqual(["services", "components"]);
});
it("merges a repo's allow override over one layer only", () => {
const [entry] = boundaries({
allow: { services: ["components"] },
elements: folderElements(["components", "services"]),
tsconfigRootDir: ROOT,
});
const { policies } = entry.rules["boundaries/dependencies"][1];
const services = policies.find((p) => p.from.element?.type === "services");
expect(services.allow.to.element.types.anyOf).toEqual(["components"]);
expect(DEFAULT_ALLOW.services).toEqual(["utils", "types", "services"]);
});
it("points the TS resolver at the repo root and marks tests", () => {
const [entry] = boundaries({
elements: folderElements(["utils"], "lib"),
testFiles: ["**/*.spec.ts"],
tsconfigRootDir: ROOT,
});
expect(entry.settings["boundaries/elements"]).toEqual([{ pattern: "lib/utils", type: "utils" }]);
expect(entry.settings["boundaries/files"]).toEqual([{ category: "test", pattern: ["**/*.spec.ts"] }]);
expect(entry.settings["import/resolver"].typescript.project).toBe(ROOT);
expect(entry.settings["boundaries/root-path"]).toBe(ROOT);
});
});
describe("defineConfig()", () => {
it("stacks nothing optional by default except vitest", () => {
const plugins = pluginNames(defineConfig({ tsconfigRootDir: ROOT }));
expect(plugins).toContain("vitest");
expect(plugins).not.toContain("boundaries");
expect(plugins).not.toContain("react-hooks");
expect(plugins).not.toContain("playwright");
});
it("stacks every layer when asked and appends overrides last", () => {
const override = { rules: { "unicorn/no-null": "error" } };
const config = defineConfig({
boundaries: { elements: folderElements(["components"]) },
overrides: [override],
playwright: true,
react: true,
tsconfigRootDir: ROOT,
vitest: false,
});
const plugins = pluginNames(config);
for (const name of ["boundaries", "react-hooks", "playwright", "jsx-a11y"]) {
expect(plugins).toContain(name);
}
expect(plugins).not.toContain("vitest");
expect(config.at(-1)).toBe(override);
});
});
describe("fast", () => {
it("is type-free: no projectService anywhere", () => {
const typeAware = fast.some((entry) => entry.languageOptions?.parserOptions?.projectService);
expect(typeAware).toBe(false);
expect(ruleIds(fast)).toContain("@typescript-eslint/no-explicit-any");
});
});