From 159db9e542a34de80a84069444dd3cf9fb80492f Mon Sep 17 00:00:00 2001 From: Danijel Martinek Date: Wed, 13 May 2026 07:41:01 +0200 Subject: [PATCH] feat(core-eslint): atomic-tier-import-direction rule Co-Authored-By: Claude Sonnet 4.6 --- .../rules/atomic-tier-import-direction.js | 47 ++++++++++++++++ .../atomic-tier-import-direction.test.js | 55 +++++++++++++++++++ 2 files changed, 102 insertions(+) create mode 100644 packages/core-eslint/rules/atomic-tier-import-direction.js create mode 100644 packages/core-eslint/rules/atomic-tier-import-direction.test.js diff --git a/packages/core-eslint/rules/atomic-tier-import-direction.js b/packages/core-eslint/rules/atomic-tier-import-direction.js new file mode 100644 index 0000000..7b8e032 --- /dev/null +++ b/packages/core-eslint/rules/atomic-tier-import-direction.js @@ -0,0 +1,47 @@ +const TIERS = ["atoms", "molecules", "organisms", "templates", "pages"]; + +function tierOf(filepath) { + for (let i = 0; i < TIERS.length; i++) { + const tier = TIERS[i]; + if (filepath.includes(`/${tier}/`) || filepath.includes(`\\${tier}\\`)) { + return { name: tier, index: i }; + } + } + return null; +} + +/** @type {import("eslint").Rule.RuleModule} */ +export default { + meta: { + type: "problem", + docs: { + description: + "Atomic-design tier imports respect direction: atoms ← molecules ← organisms ← templates ← pages.", + }, + schema: [], + messages: { + wrongDirection: + "{{fromTier}} cannot import from {{toTier}} ({{importPath}}). Tier direction: atoms ← molecules ← organisms ← templates ← pages.", + }, + }, + create(context) { + const filename = context.filename; + const from = tierOf(filename); + if (!from) return {}; + return { + ImportDeclaration(node) { + const source = node.source.value; + if (typeof source !== "string") return; + const to = tierOf(source); + if (!to) return; + if (to.index > from.index) { + context.report({ + node, + messageId: "wrongDirection", + data: { fromTier: from.name, toTier: to.name, importPath: source }, + }); + } + }, + }; + }, +}; diff --git a/packages/core-eslint/rules/atomic-tier-import-direction.test.js b/packages/core-eslint/rules/atomic-tier-import-direction.test.js new file mode 100644 index 0000000..c2c67e6 --- /dev/null +++ b/packages/core-eslint/rules/atomic-tier-import-direction.test.js @@ -0,0 +1,55 @@ +import { describe, it } from "vitest"; +import { RuleTester } from "eslint"; +import rule from "./atomic-tier-import-direction.js"; + +const tester = new RuleTester({ + languageOptions: { + parser: await import("@typescript-eslint/parser"), + ecmaVersion: "latest", + sourceType: "module", + parserOptions: { ecmaFeatures: { jsx: true } }, + }, +}); + +describe("atomic-tier-import-direction", () => { + it("passes when an organism imports from atoms", () => { + tester.run("atomic-tier-import-direction", rule, { + valid: [{ + filename: "/repo/packages/core-ui/src/organisms/Card/Card.tsx", + code: `import { Button } from "../../atoms/Button/Button";`, + }], + invalid: [], + }); + }); + + it("fires when an atom imports from organisms", () => { + tester.run("atomic-tier-import-direction", rule, { + valid: [], + invalid: [{ + filename: "/repo/packages/core-ui/src/atoms/Button/Button.tsx", + code: `import { Card } from "../../organisms/Card/Card";`, + errors: [{ messageId: "wrongDirection", data: { fromTier: "atoms", toTier: "organisms", importPath: "../../organisms/Card/Card" } }], + }], + }); + }); + + it("is a no-op for files outside any tier folder", () => { + tester.run("atomic-tier-import-direction", rule, { + valid: [{ + filename: "/repo/packages/auth/src/application/use-cases/sign-in.use-case.ts", + code: `export const x = 1;`, + }], + invalid: [], + }); + }); + + it("passes for same-tier imports", () => { + tester.run("atomic-tier-import-direction", rule, { + valid: [{ + filename: "/repo/packages/core-ui/src/molecules/SearchBar/SearchBar.tsx", + code: `import { FormField } from "../FormField/FormField";`, + }], + invalid: [], + }); + }); +});