mirror of
https://github.com/kuhyx/utils.git
synced 2026-11-15 08:50:59 +01:00
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
131 lines
5.2 KiB
JavaScript
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");
|
|
});
|
|
});
|