-
-
Notifications
You must be signed in to change notification settings - Fork 7
project‑structure independent‑modules
A key principle of a healthy project is to prevent the creation of a massive dependency tree, where removing or editing one feature triggers a chain reaction that impacts the entire project.
Create modules where you control what can be imported into them. Eliminate unnecessary dependencies between folders or files to build truly independent functionalities.
- Creating modules in which you control what can be imported (e.g. types, functions, components of one functionality cannot be imported into another functionality).
- The ability to create very detailed rules, even for nested folder structures. Whether it's a large module, a sub-module, or a single file, there are no limitations.
- Support for all types of imports, including
require()
,import()
,jest.mock()
, andjest.requireActual()
, as well asExportAllDeclaration
andExportNamedDeclaration
. - Disabling external imports (node_modules) for a given module (Option to add exceptions).
- Non-relative/relative imports support.
- Built-in import resolver, so you don’t need to install any additional plugins. It also includes built-in configuration for the most popular file extensions, so you don’t have to configure anything manually.
- Reusable import patterns.
- Support for path aliases. The plugin will automatically detect your tsconfig.json and use your settings. There is also an option to enter them manually.
- An option to create a separate configuration file with TypeScript support.
🎮 Playground for eslint-plugin-project-structure rules. Check the latest releases and stay updated with new features and changes.
Become part of the community!
Leave a ⭐ and share the link with your friends.
- If you have any questions or need help creating a configuration that meets your requirements, help.
- If you have found a bug or an error in the documentation, report issues.
- If you have an idea for a new feature or an improvement to an existing one, ideas.
- If you're interested in discussing project structures across different frameworks or want to vote on a proposed idea, discussions.
npm install --save-dev eslint-plugin-project-structure
yarn add --dev eslint-plugin-project-structure
pnpm add --save-dev eslint-plugin-project-structure
Add the following lines to eslint.config.mjs
.
Note
The examples in the documentation refer to ESLint's new config system. If you're interested in examples for the old ESLint config, you can find them in the 🎮 Playground for eslint-plugin-project-structure rules.
// @ts-check
import eslint from "@eslint/js";
import tseslint from "typescript-eslint";
import { projectStructurePlugin } from "eslint-plugin-project-structure";
import { independentModulesConfig } from "./independentModules.mjs";
export default tseslint.config(
eslint.configs.recommended,
tseslint.configs.recommended,
{
files: ["**/*.ts", "**/*.tsx", "**/*.js", "**/*.jsx"],
plugins: {
"project-structure": projectStructurePlugin,
},
rules: {
// If you have many rules in a separate file.
"project-structure/independent-modules": [
"error",
independentModulesConfig,
],
// If you have only a few rules.
"project-structure/independent-modules": [
"error",
{
// Config
},
],
},
}
);
Create a independentModules.mjs
in the root of your project.
Warning
Remember to include // @ts-check
, otherwise type checking won't be enabled.
Note
independentModules.json
, independentModules.jsonc
and independentModules.yaml
are also supported. See an example in the 🎮 Playground for eslint-plugin-project-structure rules.
.
├── ...
├── 📄 independentModules.mjs
└── 📂 src
├── 📂 shared
└── 📂 features
├── ...
├── 📂 Feature1 Feature1 family.
│ ├── 📁 components
│ │ └── 📄 SimpleComponent.tsx Private / Public for Feature1 family.
│ ├── 📄 feature1.api.ts Private / Public for Feature1 family.
│ ├── 📄 feature1.types.ts Private / Public for Feature1 family.
│ └── 📄 Feature1.tsx Public.
│
└── 📂 Feature2 Feature2 family.
├── 📁 components
│ └── 📄 SimpleComponent.tsx Private / Public for Feature2 family.
├── 📄 feature2.api.ts Private / Public for Feature2 family.
├── 📄 feature2.types.ts Private / Public for Feature2 family.
└── 📄 Feature2.tsx Public.
// @ts-check
import { createIndependentModules } from "eslint-plugin-project-structure";
export const independentModulesConfig = createIndependentModules({
modules: [
{
name: "Shared folder",
pattern: "src/shared/**",
errorMessage:
"🔥 The `shared` folder cannot import items from the `features` folder. 🔥",
allowImportsFrom: ["src/shared/**"],
},
{
name: "Features",
pattern: "src/features/**",
errorMessage:
"🔥 A feature may only import items from... Importing items from another feature is prohibited. 🔥",
allowImportsFrom: [
// /* = wildcard.
// /*/ = wildcard for current directory.
// /**/ = wildcard for nested directories.
"src/features/*/*.tsx",
// Let's assume we are in the "features/Feature1/Feature1.tsx".
// In this case we will be able to import:
// "features/Feature2/Feature2.tsx"
// But we won't be able to import Feature1 private files and folders.
// {family} reference finds the common part between the import and the current file.
// By default, at least two common path parts are required, baseUrl is not taken into account.
// This will make your rule work recursively/apply to all nestings.
// You can change the number of common path parts required, {family_1} at least 1, {family_3} at least 3 common part etc.
"{family}/**",
// Let's assume we are in the "features/Feature1/Feature1.tsx".
// In this case we will be able to import:
// "features/Feature1/feature1.types.ts" ({family} === "features/Feature1")
// "features/Feature1/feature1.api.ts" ({family} === "features/Feature1")
// "features/Feature1/components/SimpleComponent.tsx" ({family} === "features/Feature1")
"src/shared/**",
],
},
// All files not specified in the rules are not allowed to import anything.
// Ignore all non-nested files in the `src` folder.
{
name: "Unknown files",
pattern: [["**", "!src/*"]],
allowImportsFrom: [],
allowExternalImports: false,
errorMessage:
"🔥 This file is not specified as an independent module in `independentModules.mjs`. 🔥",
},
],
});
.
├── ...
├── 📄 independentModules.mjs
└── 📂 src
├── 📂 shared
└── 📂 features
├── ...
├── 📂 Feature1 Feature1 family. Same structure as Feature2.
└── 📂 Feature2 Feature2 family.
├── 📄 feature2.api.ts Private / Public for Feature2 family.
├── 📄 feature2.types.ts Private / Public for Feature2 family.
├── 📄 Feature2.tsx Public.
└── 📂 components
├── 📄 SimpleComponent.tsx Private / Public for Feature2 family / Public for ComplexComponent family.
└── 📂 ComplexComponent ComplexComponent family.
├── 📁 components Private / Public for ComplexComponent family.
├── 📄 complexComponent.api.ts Private / Public for ComplexComponent family.
├── 📄 complexComponent.types.ts Private / Public for ComplexComponent family.
└── 📄 ComplexComponent.tsx Private / Public for ComplexComponent family / Public for Feature2 family / Public for SimpleComponent.tsx.
// @ts-check
import { createIndependentModules } from "eslint-plugin-project-structure";
export const independentModulesConfig = createIndependentModules({
modules: [
{
name: "Shared folder",
pattern: "src/shared/**",
errorMessage:
"🔥 The `shared` folder cannot import items from the `features` folder. 🔥",
allowImportsFrom: ["src/shared/**"],
},
{
name: "Features",
pattern: "src/features/**",
errorMessage:
"🔥 A feature may only import items from... Importing items from another feature is prohibited. 🔥",
allowImportsFrom: [
// /* = wildcard.
// /*/ = wildcard for current directory.
// /**/ = wildcard for nested directories.
"src/features/*/*.tsx",
// Let's assume we are in the "features/Feature2/Feature2.tsx"
// In this case we will be able to import:
// "feature/Feature1/Feature1.tsx"
// But we won't be able to import Feature1 private files and folders.
// {family} reference finds the common part between the import and the current file.
// By default, at least two common path parts are required, baseUrl is not taken into account.
// This will make your rule work recursively/apply to all nestings.
// You can change the number of common path parts required, {family_1} at least 1, {family_3} at least 3 common part etc.
"{family}/*",
// Let's assume we are in the "features/Feature2/Feature2.tsx"
// In this case we will be able to import:
// "features/Feature2/feature2.api.ts" ({family} === "features/Feature2")
// "features/Feature2/feature2.types.ts" ({family} === "features/Feature2")
[
"{family}/components/*/*",
"!{family}/components/*/*.(types|api|types).ts",
],
// Let's assume we are in the "features/Feature2/Feature2.tsx"
// In this case we will be able to import:
// "features/Feature2/components/ComplexComponent/ComplexComponent.tsx" ({family} === "features/Feature2")
// But we won't be able to import ComplexComponent private files.
["{family}/*/*", "!{family}/*/*.(types|api|types).ts"],
// Let's assume we are in the "features/Feature2/Feature2.tsx"
// In this case we will be able to import:
// "features/Feature2/components/SimpleComponent.tsx" ({family} === "features/Feature2")
"src/shared/**",
],
},
// All files not specified in the rules are not allowed to import anything.
// Ignore all non-nested files in the `src` folder.
{
name: "Unknown files",
pattern: [["**", "!src/*"]],
allowImportsFrom: [],
allowExternalImports: false,
errorMessage:
"🔥 This file is not specified as an independent module in `independentModules.mjs`. 🔥",
},
],
});
A place where you can add your modules.
After creation, each module will not be able to import anything except external imports (these are allowed by default, can also be disabled).
Warning
The order of modules matters! Module patterns are checked in order from top to bottom.
{
"modules": [
{
"name": "Module 1",
"pattern": "**",
"allowImportsFrom": []
},
// Module 2 with `src/features/**` will not be taken into account because Module 1 with `**` meets the condition.
{
"name": "Module 2",
"pattern": "src/features/**",
"allowImportsFrom": []
}
]
}
{
"modules": [
// Module 2 with `src/features/**` will be taken into account because Module 1 with `**` is below Module 2.
{
"name": "Module 2",
"pattern": "src/features/**",
"allowImportsFrom": []
},
{
"name": "Module 1",
"pattern": "**",
"allowImportsFrom": []
}
]
}
{
"modules": [
{
"name": "Module 1",
"pattern": [["**", "!src/features/**"]],
"allowImportsFrom": []
},
// Module 2 with `src/features/**` will be taken into account because Module 1 with [["**", "!src/features/**"]] ignores `features/**` pattern.
{
"name": "Module 2",
"pattern": "src/features/**",
"allowImportsFrom": []
}
]
}
The name of your module.
{ "modules": [{ "name": "features" }] }
Your module's pattern.
The outer array checks if any pattern meets the requirements. The inner array checks if all patterns meet the requirements.
You can use all micromatch functionalities.
{
"modules": [
// All files from the `features` folder.
{ "pattern": "src/features/**" }, // If your `baseUrl` is `.` (default).
{ "pattern": "packages/package-name/src/features/**" }, // If your `baseUrl` is `.` (default) and you use monorepo.
{ "pattern": "features/**" }, // If your `baseUrl` is `src`.
// All files from the `components` folder or all files from the `globalComponents` folder .
{ "pattern": ["src/components/**", "src/globalComponents/**"] },
// All files from the `globalHelpers` folder or all files from the `helpers` folder except the `index.ts` and `.js` files.
{
"pattern": [
"src/globalHelpers/**",
["src/helpers/**", "!(**/index.ts)", "!(**/*.js)"]
]
},
// If your folder name uses micromatch special characters () [] {} ! , + ? etc.
{
"pattern": [
"src/app/hello/\\(components\\)/**",
"src/app/hello/\\[\\[\\.\\.\\.slug\\]\\]/**"
]
}
]
}
The place where you specify what can be imported into your module.
The outer array checks if any pattern meets the requirements. The inner array checks if all patterns meet the requirements.
If at least one pattern in allowImportsFrom
meets the condition, the import is considered allowed.
You can use all micromatch functionalities.
Warning
The order of patterns matters! They are checked from top to bottom.
Warning
Provide the import pattern without the path alias. If you have baseUrl
set in your tsconfig.json
or another tool, do not include it in the pattern.
{
"modules": [
{
"allowImportsFrom": [
// All files from the first directory of the `helpers` folder.
"src/helpers/*", // If your `baseUrl` is `.` (default).
"packages/package-name/src/helpers/*", // If your `baseUrl` is `.` (default) and you use monorepo.
"helpers/*", // If your `baseUrl` is `src`.
// All files from the `types` folder. All directories/nestings.
"src/types/**",
// All nested files in `hooks` folder except *.types.ts
["src/hooks/**", "!src/hooks/**/*.types.ts"],
// All nested files in the `components` folder except files in the helpers folders.
["src/components/**", "!src/components/**/helpers/**"],
// All nested .js files in the `helpers` folder except index.js
["src/helpers/**/*.js", "!src/helpers/**/index.js"],
// If your folder name uses micromatch special characters () [] {} ! , + ? etc.
{
"pattern": [
"src/app/hello/\\(components\\)/**",
"src/app/hello/\\[\\[\\.\\.\\.slug\\]\\]/**"
]
}
]
},
{
"allowImportsFrom": [
// All nested `.js` files in the `helpers` folder
"src/helpers/**/*.js",
// It will not be taken into account because `helpers/**/*.js` met the condition.
// Not `index.js` files in the `helpers` folder.
"!src/helpers/**/index.js"
]
}
]
}
Here you can enable/disable the ability to import external imports (node_modules) in a given module.
The default value is true
.
{
"modules": [
{
"allowExternalImports": false,
//You can specify exceptions via `allowImportsFrom`.
"allowImportsFrom": [
"react/**", // import x from "react"
"lodash/**", // import x from "lodash", import x from "lodash/omit", import x from "lodash/set"
"lodash.*/**" // import x from "lodash.omit", import x from "lodash.set"
]
}
]
}
Here, you can set your custom error for a given module.
{ "modules": [{ "errorMessage": "My custom module error." }] }
To avoid repetitions, you can create reusable import patterns.
By writing {yourKey}
you refer to a particular key in the reusableImportPatterns
object, you can use this reference in reusableImportPatterns
and in allowImportsFrom
.
The library will automatically inform you about all usage errors such as: Infinite recursion, too many array nests. e.t.c.
{
"reusableImportPatterns": {
"pattern1": ["pattern1_a"],
"pattern2": ["pattern2_a", "pattern2_b"],
"pattern3": ["pattern3_a", "pattern3_b", ["pattern3_c", "pattern3_d"]],
"pattern4": ["pattern4_a", "{pattern1}"],
// ["pattern4_a", "pattern1_a"]
"pattern5": ["pattern5_a", ["{pattern1}"]],
// ["pattern5_a", ["pattern1_a"]]
"pattern6": ["pattern6_a", "**/{pattern1}/**"],
// ["pattern6_a", "**/pattern1_a/**"]
"pattern7": ["pattern7_a", "{pattern2}"],
// ["pattern7_a", "pattern2_a", "pattern2_b"]
"pattern8": ["pattern8_a", ["{pattern2}"]],
// ["pattern8_a", ["pattern2_a", "pattern2_b"]]
"pattern9": ["pattern9_a", "{pattern3}"]
// ["pattern9_a", "pattern3_a", "pattern3_b", ["pattern3_c", "pattern3_d"]]
},
"modules": [
{
"name": "Some Module",
"pattern": "**",
"allowImportsFrom": ["{pattern1}/*.ts", "{pattern9}"]
// ["pattern1_a/*.ts", "pattern9_a", "pattern3_a", "pattern3_b", ["pattern3_c", "pattern3_d"]]
}
]
}
{family}
reference finds the common part between the import and the current file.
By default, at least two common parts are required, baseUrl
is not taken into account.
This will make your rule work recursively/apply to all nestings.
You can change the number of parts required, {family_1}
at least one, {family_3}
at least three common part etc.
baseUrl = "src"
Example 1
Current file = "features/Feature1/Feature1.tsx"
Import = "features/Feature1/Feature1.types.ts"
{family} = "features/Feature1"
Example 2
Current file = "features/Feature1/Feature1.tsx"
Import = "features/Feature1/Feature1.types.ts"
{family_3} = "NO_FAMILY"
Example 3
Current file = "features/Feature1/Child1/Child1.tsx"
Import = "features/Feature1/Child1/hooks/useComplexHook/useComplexHook.ts"
{family} = "features/Feature1/Child1"
Example 4
Current file = "features/Feature1/Child1/hooks/useComplexHook1/useComplexHook1.ts"
import = "features/Feature1/Child1/hooks/useComplexHook2/useComplexHook2.ts"
{family} = "features/Feature1/Child1/hooks"
Example 5
Current file = "features/Feature1/Feature1.tsx"
Import = "features/Feature2/Feature2.types.ts"
{family} = "NO_FAMILY"
Example 6
Current file = "features/Feature1/Feature1.tsx"
Import = "features/Feature2/Feature2.types.ts"
{family_1} = "features"
Reference to the directory of the file you are currently in.
By default, {dirname}
will refer to the nearest directory of the given file you are in.
You can determine the directory lvl by the following notation {dirname_2}
, {dirname_3}
and so on.
baseUrl = "src"
Example 1
Current file = "features/Feature1/Feature1.tsx"
{dirname} = "features/Feature1"
Example 2
Current file = "features/Feature1/Child1/Child1.tsx"
{dirname} = "features/Feature1/Child1"
Example 3
Current file = "features/Feature1/Feature1.tsx"
{dirname_2} = "features"
Example 4
Current file = "features/Feature1/Child1/hooks/useComplexHook1/useComplexHook1.ts"
{dirname_5} = "features"
The path to your tsconfig.json
.
If your tsconfig
is located in the root of your project, the plugin will automatically detect it.
If your tsconfig
is located elsewhere, you can specify its location here.
{ "tsconfigPath": "./tsconfig.json" }
The plugin automatically takes baseUrl
and paths
from your tsconfig.json
.
However, if you are using another tool for path aliases, such as Webpack
or Babel
, you can configure the appropriate path aliases here.
// "src/components/Component.tsx"
import { Component } from "@components/Component";
{
"pathAliases": {
"baseUrl": ".",
"paths": {
"@components/*": ["src/components/*"]
}
}
}
If you use shortened imports without a file extension, the plugin will automatically assign the correct extension from the list of available extensions.
// "helpers/myHelper.ts"
import { myHelper } from "helpers/myHelper"; // The plugin will recognize the file as a .ts file and consider this when validating the glob pattern.
Available extensions: ".js"
, ".jsx"
, ".mjs"
, ".cjs"
".d.ts"
, ".ts"
, ".tsx"
, ".vue"
, ".svelte"
, ".json"
, ".jsonc"
, ".yml"
, ".yaml"
, ".svg"
, ".png"
, ".jpg"
, ".ico"
, ".css"
, ".sass"
, ".scss"
, ".less"
, ".html"
,
If the extension you are using is not on the list, you can extend it.
{ "extensions": [".yourFancyExtension"] }
Debug mode showing the current allowImportsFrom
, {family}
, and {dirname}
for a given import.
The default value is false
.
{ "debugMode": true }
A big thank you to all the sponsors for your support! You give me the strength and motivation to keep going!
Thanks to you, I can help others create their ideal projects!