utils/ts_config/test/config.test.js
Krzysztof kuhy Rudnicki 0f9699d4f3 ts_config: project option, entry-point barrel and className carve-outs
Surfaced by making ts_core and web_ui the first consumers:

- base({ project }) uses an explicit tsconfig list instead of the project
  service. Both packages exclude tests from tsconfig.json so they never land
  in dist/, and widen the include in tsconfig.lint.json for lint only; the
  project service would have reported every test file as "not found".
- unicorn/no-barrel-files is off for src/index.ts: a package entry point IS
  a barrel, that is its job. Stays on everywhere else.
- unicorn/no-keyword-prefix is off in the React layer: `className` is
  React's prop name and fires on every styled element.
- pnpm-workspace.yaml: allowBuilds for unrs-resolver set to false explicitly
  (pnpm 12 had written a placeholder); the resolver's prebuilt binding is
  used, no install script runs.

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

142 lines
5.7 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("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(
"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");
});
});