Two CLAUDE.md conventions had no mechanical gate, so both drifted: entity models shipped without sibling tests, and feature test files imported src modules via `../` instead of the `@/` alias. - `entity-must-have-test` — every entities/models/<x>.ts needs a sibling <x>.test.ts (errors and barrels excluded). - `no-relative-parent-import-in-tests` — feature test files must import src via `@/`, not `../`. Scoped to feature packages; core packages are governed by their own generator templates. Both register at warn level, bringing the conformance rule count to 15.
50 lines
1.7 KiB
JavaScript
50 lines
1.7 KiB
JavaScript
/**
|
|
* Feature test files import from `src/` through the `@/` alias, never via
|
|
* `../` parent traversal (CLAUDE.md "Key Conventions"). A `../` import in a
|
|
* test file is always reaching across `src/` directories — `@/` keeps those
|
|
* imports stable under file moves and makes the test's dependencies legible.
|
|
*
|
|
* Scoped to feature packages (`packages/<name>/src/`, excluding `core-*`):
|
|
* the convention is part of the feature template's contract. Core packages
|
|
* are generated and governed by their own templates, and tooling packages
|
|
* (turbo/generators, scripts) legitimately use relative paths.
|
|
*/
|
|
function isFeatureSrcTestFile(filename) {
|
|
const normalized = filename.replace(/\\/g, "/");
|
|
if (!normalized.endsWith(".test.ts") && !normalized.endsWith(".test.tsx")) {
|
|
return false;
|
|
}
|
|
return /\/packages\/(?!core-)[^/]+\/src\//.test(normalized);
|
|
}
|
|
|
|
/** @type {import("eslint").Rule.RuleModule} */
|
|
export default {
|
|
meta: {
|
|
type: "problem",
|
|
docs: {
|
|
description:
|
|
"Feature test files must import src modules via the @/ alias, not ../ parent paths.",
|
|
},
|
|
schema: [],
|
|
messages: {
|
|
relativeParentImport:
|
|
'Test file imports "{{source}}" with a ../ parent path. Use the "@/" alias for src imports (e.g. "@/application/...").',
|
|
},
|
|
},
|
|
create(context) {
|
|
if (!isFeatureSrcTestFile(context.filename)) return {};
|
|
return {
|
|
ImportDeclaration(node) {
|
|
const source = node.source.value;
|
|
if (typeof source === "string" && source.startsWith("../")) {
|
|
context.report({
|
|
node: node.source,
|
|
messageId: "relativeParentImport",
|
|
data: { source },
|
|
});
|
|
}
|
|
},
|
|
};
|
|
},
|
|
};
|