Skip to content

Commit

Permalink
Add ESLint rule that enforces absolute (@src/...) imports from other …
Browse files Browse the repository at this point in the history
…modules (#1303)

Main motivation was to have nicer import paths for CRUD generator code

It should avoid `import xxx from "../common/xxx";` and change it to
`@src/common/xxx`

For other examples see
packages/eslint-plugin/src/rules/no-other-module-relative-import.test.ts

very basic definition of "module" (a concept that doesn't really exist):
- src/modulea
- src/moduleb

sub-modules or nested modules are not supported by this.
  • Loading branch information
nsams authored Nov 22, 2023
1 parent f6a6c4f commit ec0582e
Show file tree
Hide file tree
Showing 25 changed files with 176 additions and 25 deletions.
9 changes: 9 additions & 0 deletions .changeset/clever-jokes-chew.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
---
"@comet/eslint-plugin": minor
---

Add new ESLint rule to enforce absolute imports when importing from other modules

For instance, an import `import { AThingInModuleA } from "../moduleA/AThingInModuleA"` in module `B` needs to be imported as `import { AThingInModuleA } from "@src/moduleA/AThingInModuleA"`.
The default source root `"./src"` and alias `"@src"` can be changed via the rule's `sourceRoot` and `sourceRootAlias` options.
This rule will be enforced by `@comet/eslint-config` in the next major release.
4 changes: 2 additions & 2 deletions demo/admin/src/pages/PageContentBlock.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
import { createBlocksBlock, SpaceBlock, YouTubeVideoBlock } from "@comet/blocks-admin";
import { AnchorBlock, DamImageBlock, DamVideoBlock } from "@comet/cms-admin";
import { HeadlineBlock } from "@src/common/blocks/HeadlineBlock";
import { LinkListBlock } from "@src/common/blocks/LinkListBlock";
import { RichTextBlock } from "@src/common/blocks/RichTextBlock";
import { TextImageBlock } from "@src/common/blocks/TextImageBlock";
import { userGroupAdditionalItemFields } from "@src/userGroups/userGroupAdditionalItemFields";
import { UserGroupChip } from "@src/userGroups/UserGroupChip";
import { UserGroupContextMenuItem } from "@src/userGroups/UserGroupContextMenuItem";
import * as React from "react";

import { HeadlineBlock } from "../common/blocks/HeadlineBlock";
import { TextImageBlock } from "../common/blocks/TextImageBlock";
import { ColumnsBlock } from "./blocks/ColumnsBlock";
import { FullWidthImageBlock } from "./blocks/FullWidthImageBlock";
import { MediaBlock } from "./blocks/MediaBlock";
Expand Down
3 changes: 1 addition & 2 deletions demo/admin/src/pages/blocks/ColumnsBlock.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,10 @@ import {
SpaceBlock,
} from "@comet/blocks-admin";
import { DamImageBlock } from "@comet/cms-admin";
import { HeadlineBlock } from "@src/common/blocks/HeadlineBlock";
import { RichTextBlock } from "@src/common/blocks/RichTextBlock";
import * as React from "react";

import { HeadlineBlock } from "../../common/blocks/HeadlineBlock";

const ColumnsContentBlock = createBlocksBlock({
name: "ColumnsContent",
supportedBlocks: {
Expand Down
3 changes: 1 addition & 2 deletions demo/admin/src/pages/blocks/TwoListsBlock.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { createCompositeBlock, createListBlock } from "@comet/blocks-admin";

import { HeadlineBlock } from "../../common/blocks/HeadlineBlock";
import { HeadlineBlock } from "@src/common/blocks/HeadlineBlock";

const TwoListsListBlock = createListBlock({
name: "TwoListsList",
Expand Down
2 changes: 1 addition & 1 deletion demo/api/src/config/config.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import cometConfig from "@src/../comet-config.json";
import { plainToClass } from "class-transformer";
import { validateSync } from "class-validator";

import cometConfig from "../../comet-config.json";
import { EnvironmentVariables } from "./environment-variables";

// eslint-disable-next-line @typescript-eslint/explicit-function-return-type
Expand Down
2 changes: 1 addition & 1 deletion demo/api/src/footer/blocks/footer-content.block.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,9 @@ import {
ExtractBlockInput,
inputToData,
} from "@comet/blocks-api";
import { LinkListBlock } from "@src/common/blocks/link-list.block";
import { IsOptional, IsString } from "class-validator";

import { LinkListBlock } from "../../common/blocks/link-list.block";
import { FooterLinkSectionBlock } from "./footer-link-section.block";

export const FooterTopLinksBlock = createListBlock({ block: FooterLinkSectionBlock }, "FooterTopLinks");
Expand Down
3 changes: 1 addition & 2 deletions demo/api/src/footer/blocks/footer-link-section.block.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,9 @@ import {
ExtractBlockInput,
inputToData,
} from "@comet/blocks-api";
import { LinkListBlock } from "@src/common/blocks/link-list.block";
import { IsOptional, IsString, ValidateNested } from "class-validator";

import { LinkListBlock } from "../../common/blocks/link-list.block";

class FooterLinkSectionBlockData extends BlockData {
@BlockField()
title?: string;
Expand Down
5 changes: 4 additions & 1 deletion packages/admin/admin-color-picker/.eslintrc.json
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
{
"extends": "@comet/eslint-config/react",
"ignorePatterns": ["src/*.generated.ts", "lib/**"]
"ignorePatterns": ["src/*.generated.ts", "lib/**"],
"rules": {
"@comet/no-other-module-relative-import": "off"
}
}
5 changes: 4 additions & 1 deletion packages/admin/admin-date-time/.eslintrc.json
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
{
"extends": "@comet/eslint-config/react",
"ignorePatterns": ["src/*.generated.ts", "lib/**"]
"ignorePatterns": ["src/*.generated.ts", "lib/**"],
"rules": {
"@comet/no-other-module-relative-import": "off"
}
}
5 changes: 4 additions & 1 deletion packages/admin/admin-icons/.eslintrc.json
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
{
"extends": "@comet/eslint-config/react",
"ignorePatterns": ["src/*.generated.ts", "src/generated/", "lib/**"]
"ignorePatterns": ["src/*.generated.ts", "src/generated/", "lib/**"],
"rules": {
"@comet/no-other-module-relative-import": "off"
}
}
5 changes: 4 additions & 1 deletion packages/admin/admin-react-select/.eslintrc.json
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
{
"extends": "@comet/eslint-config/react",
"ignorePatterns": ["src/*.generated.ts", "lib/**"]
"ignorePatterns": ["src/*.generated.ts", "lib/**"],
"rules": {
"@comet/no-other-module-relative-import": "off"
}
}
3 changes: 2 additions & 1 deletion packages/admin/admin-rte/.eslintrc.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
"ignorePatterns": ["src/*.generated.ts", "lib/**"],
"rules": {
"@typescript-eslint/no-explicit-any": "off",
"@typescript-eslint/no-non-null-assertion": "off"
"@typescript-eslint/no-non-null-assertion": "off",
"@comet/no-other-module-relative-import": "off"
}
}
3 changes: 2 additions & 1 deletion packages/admin/admin-stories/.eslintrc.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
"@typescript-eslint/no-empty-function": "off",
"@typescript-eslint/no-explicit-any": "off",
"@typescript-eslint/no-non-null-assertion": "off",
"no-console": "off"
"no-console": "off",
"@comet/no-other-module-relative-import": "off"
}
}
5 changes: 4 additions & 1 deletion packages/admin/admin-theme/.eslintrc.json
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
{
"extends": "@comet/eslint-config/react",
"ignorePatterns": ["src/*.generated.ts", "lib/**"]
"ignorePatterns": ["src/*.generated.ts", "lib/**"],
"rules": {
"@comet/no-other-module-relative-import": "off"
}
}
3 changes: 2 additions & 1 deletion packages/admin/admin/.eslintrc.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
"ignorePatterns": ["src/*.generated.ts", "lib/**"],
"rules": {
"@typescript-eslint/no-explicit-any": "off",
"@typescript-eslint/no-non-null-assertion": "off"
"@typescript-eslint/no-non-null-assertion": "off",
"@comet/no-other-module-relative-import": "off"
}
}
5 changes: 4 additions & 1 deletion packages/admin/blocks-admin/.eslintrc.json
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
{
"extends": "@comet/eslint-config/react",
"ignorePatterns": ["src/*.generated.ts", "lib/**"]
"ignorePatterns": ["src/*.generated.ts", "lib/**"],
"rules": {
"@comet/no-other-module-relative-import": "off"
}
}
5 changes: 4 additions & 1 deletion packages/admin/cms-admin/.eslintrc.json
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
{
"extends": "@comet/eslint-config/react",
"ignorePatterns": ["src/**/*.generated.ts", "lib/**"]
"ignorePatterns": ["src/**/*.generated.ts", "lib/**"],
"rules": {
"@comet/no-other-module-relative-import": "off"
}
}
5 changes: 4 additions & 1 deletion packages/api/blocks-api/.eslintrc.json
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
{
"extends": "@comet/eslint-config/nestjs",
"ignorePatterns": ["lib/**"]
"ignorePatterns": ["lib/**"],
"rules": {
"@comet/no-other-module-relative-import": "off"
}
}
5 changes: 4 additions & 1 deletion packages/api/cms-api/.eslintrc.json
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
{
"extends": "@comet/eslint-config/nestjs",
"ignorePatterns": ["src/mikro-orm/migrations/**", "lib/**"]
"ignorePatterns": ["src/mikro-orm/migrations/**", "lib/**"],
"rules": {
"@comet/no-other-module-relative-import": "off"
}
}
5 changes: 4 additions & 1 deletion packages/cli/.eslintrc.json
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
{
"extends": "@comet/eslint-config/core",
"ignorePatterns": ["bin/", "lib/**"]
"ignorePatterns": ["bin/", "lib/**"],
"rules": {
"@comet/no-other-module-relative-import": "off"
}
}
2 changes: 1 addition & 1 deletion packages/eslint-config/core-without-import.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ module.exports = {
"unused-imports/no-unused-imports": "error",
"no-console": ["error", { allow: ["warn", "error"] }],
"no-return-await": "error",
"json-files/sort-package-json": "error",
"json-files/sort-package-json": "error"
},
overrides: [
{
Expand Down
3 changes: 3 additions & 0 deletions packages/eslint-plugin/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
import noOtherModuleRelativeImport from "./rules/no-other-module-relative-import";
import noPrivateSiblingImport from "./rules/no-private-sibling-import";

const plugin = {
rules: {
"no-private-sibling-import": noPrivateSiblingImport,
"no-other-module-relative-import": noOtherModuleRelativeImport,
},
};
export type Plugin = typeof plugin;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { RuleTester } from "eslint";

import noOtherModuleRelativeImport from "./no-other-module-relative-import";

const ruleTester = new RuleTester({
parser: require.resolve("@typescript-eslint/parser"),
});

const errors = [{ message: "Avoid relative import from other module" }];

const options = [{ sourceRoot: "./src", sourceRootAlias: "@src" }];

ruleTester.run("no-other-module-relative-import", noOtherModuleRelativeImport, {
valid: [
{
code: `import Bar from "@src/moduleb/Bar";`,
filename: `${process.cwd()}/src/modulea/Foo.ts`,
options,
},
{ code: `import Bar from "../Bar";`, filename: `${process.cwd()}/src/modulea/sub/Foo.ts`, options },
{ code: `import Bar from "xx/bar";`, filename: `${process.cwd()}/src/modulea/Foo.ts`, options },
],

invalid: [
{
code: `import Bar from "../moduleb/Bar";`,
filename: `${process.cwd()}/src/modulea/Foo.ts`,
options,
errors,
output: `import Bar from "@src/moduleb/Bar";`,
},
{
code: `import Bar from "../../Bar";`,
filename: `${process.cwd()}/src/modulea/sub/Foo.ts`,
options,
errors,
output: `import Bar from "@src/Bar";`,
},
],
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import { Rule } from "eslint";
import path from "path";

function parentDirCount(dir: string) {
const match = dir.match(/^(\.\.\/)*/);
return match[0].length / 3;
}

export default {
meta: {
type: "suggestion",
fixable: "code",
schema: [
{
type: "object",
properties: {
sourceRoot: {
type: "string",
},
sourceRootAlias: {
type: "string",
},
},
additionalProperties: false,
},
],
},
create(context) {
return {
ImportDeclaration: function (node) {
const options = context.options[0] ?? { sourceRoot: "./src", sourceRootAlias: "@src" };

const importParentDirCount = parentDirCount(node.source.value.toString());
if (!importParentDirCount) {
// import is not relative
return;
}

const filePath = context.getPhysicalFilename ? context.getPhysicalFilename() : context.getFilename();
if (filePath == "<text>") return; // If the input is from stdin, this test can't fail
const sourceDir = `${context.getCwd()}/${options.sourceRoot}`;

const fileDir = path.dirname(filePath);

const relativeFileToSourceDir = path.relative(sourceDir, fileDir);
if (!relativeFileToSourceDir || relativeFileToSourceDir.startsWith("..")) {
// file is not in source directory
return;
}

const fileSubdirectoriesCount = relativeFileToSourceDir.split(path.sep).length;

// importParentDirCount is the number of ../ parts in the import path
// fileSubdirectoriesCount is the number of subdirectories in the file path relative to the source directory
if (importParentDirCount >= fileSubdirectoriesCount) {
context.report({
node,
message: "Avoid relative import from other module",
fix: (fixer) => {
const importPathRelativeToSourceDir = path.relative(sourceDir, `${fileDir}/${node.source.value.toString()}`);
return fixer.replaceText(node.source, `"${options.sourceRootAlias}/${importPathRelativeToSourceDir}"`);
},
});
}
},
};
},
} as Rule.RuleModule;
3 changes: 2 additions & 1 deletion packages/site/cms-site/.eslintrc.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
"extends": "@comet/eslint-config/nextjs",
"ignorePatterns": ["src/*.generated.ts", "lib/**"],
"rules": {
"@next/next/no-html-link-for-pages": "off" // disabled because lib has no pages dir
"@next/next/no-html-link-for-pages": "off", // disabled because lib has no pages dir
"@comet/no-other-module-relative-import": "off"
}
}

0 comments on commit ec0582e

Please sign in to comment.