Skip to content

Igorkowalski94/eslint-plugin-project-structure

Repository files navigation

eslint-plugin-project-structure

Eslint plugin that allows you to enforce rules on project structure to keep your repository consistent even in large teams.

Features

βœ… Validation of project structure (Any files/folders outside the structure will be considered an error).
βœ… Validation of folder and file names.
βœ… Name case validation.
βœ… Name regex validation.
βœ… File extension validation (Support for all extensions).
βœ… Inheriting the parent's name (The child inherits the name of the folder in which it is located).
βœ… Folder recursion (You can nest a given folder structure recursively).
βœ… Forcing a nested/flat structure for a given folder.

Go to:

Installation

$ yarn add -D eslint-plugin-project-structure
$ npm i --dev eslint-plugin-project-structure

Getting started

Step 1 (optional)

If you want to check extensions that are not supported by eslint like .css, .sass, .less, .svg, .png, .jpg, .ico, .yml, .json, read the step below, if not go to the next step.

Add the following script to your package.json. You can extend the list of extensions in the script. After completing Step 2 and Step 3, use this script to check your structure.

{
    "scripts": {
        "projectStructure:check": "eslint --parser ./node_modules/eslint-plugin-project-structure/dist/parser.js --rule project-structure/file-structure:error --ext .js,.jsx,.ts,.tsx,.css,.sass,.less,.svg,.png,.jpg,.ico,.yml,.json ."
    }
}

Step 2

Add the following lines to .eslintrc.

{
    "plugins": ["project-structure"],
    "rules": {
        "project-structure/file-structure": "error" // warn | error
    },
    "settings": {
        "project-structure/config-path": "projectStructure.json" // json | yaml
    }
}

Step 3

Create a projectStructure.json or projectStructure.yaml in the root of your project.

Note You can choose your own file name, just make sure it is the same as in Step 2.

Here you will find an example of the project structure for the framework (CLI) you are using. If it's not on the examples list and you want to help the community, add its configuration here.

If you want to help:
You can leave a ⭐ and share the link with your friends. It will help grow our community.
You can share your project structure in the discussions section and help create standards for given frameworks. Standards will make moving from one project to another much easier and will increase the overall quality and consistency of projects in our industry.

Simple example for the structure below:

.
β”œβ”€β”€ ...
β”œβ”€β”€ πŸ“„ projectStructure.json
β”œβ”€β”€ πŸ“„ .eslintrc.json
└── πŸ“‚ src
    β”œβ”€β”€ πŸ“„ index.tsx
    └── πŸ“‚ components
        β”œβ”€β”€ ...
        └── πŸ“„ ComponentName.tsx

JSON

{
    "structure": {
        "children": [
            {
                "extension": "*"
            },
            {
                "name": "src",
                "children": [
                    {
                        "name": "index",
                        "extension": "tsx"
                    },
                    {
                        "name": "components",
                        "children": [
                            {
                                "name": "/^${{PascalCase}}$/",
                                "extension": "tsx"
                            }
                        ]
                    }
                ]
            }
        ]
    }
}

YAML

structure:
    children:
        - extension: "*"
        - name: src
          children:
              - name: index
                extension: tsx
              - name: components
                children:
                    - name: "/^${{PascalCase}}$/"
                      extension: tsx

Advanced example for the structure below, containing all key features:

.
β”œβ”€β”€ ...
β”œβ”€β”€ πŸ“„ projectStructure.json
β”œβ”€β”€ πŸ“„ .eslintrc.json
└── πŸ“‚ src
    β”œβ”€β”€ πŸ“‚ hooks
    β”‚   β”œβ”€β”€ ...
    β”‚   β”œβ”€β”€ πŸ“„ useSimpleGlobalHook.test.ts
    β”‚   β”œβ”€β”€ πŸ“„ useSimpleGlobalHook.ts
    β”‚   └── πŸ“‚ useComplexGlobalHook
    β”‚       β”œβ”€β”€ πŸ“ hooks (recursion)
    β”‚       β”œβ”€β”€ πŸ“„ useComplexGlobalHook.api.ts
    β”‚       β”œβ”€β”€ πŸ“„ useComplexGlobalHook.types.ts
    β”‚       β”œβ”€β”€ πŸ“„ useComplexGlobalHook.test.ts
    β”‚       └── πŸ“„ useComplexGlobalHook.ts
    └── πŸ“‚ components
        β”œβ”€β”€ ...
        └── πŸ“‚ ParentComponent
            β”œβ”€β”€ πŸ“„ parentComponent.api.ts
            β”œβ”€β”€ πŸ“„ parentComponent.types.ts
            β”œβ”€β”€ πŸ“„ ParentComponent.context.tsx
            β”œβ”€β”€ πŸ“„ ParentComponent.test.tsx
            β”œβ”€β”€ πŸ“„ ParentComponent.tsx
            β”œβ”€β”€ πŸ“‚ components
            β”‚   β”œβ”€β”€ ...
            β”‚   └── πŸ“‚ ChildComponent
            β”‚       β”œβ”€β”€ πŸ“ components (recursion)
            β”‚       β”œβ”€β”€ πŸ“ hooks (recursion)
            β”‚       β”œβ”€β”€ πŸ“„ childComponent.types.ts
            β”‚       β”œβ”€β”€ πŸ“„ childComponent.api.ts
            β”‚       β”œβ”€β”€ πŸ“„ ChildComponent.context.tsx
            β”‚       β”œβ”€β”€ πŸ“„ ChildComponent.test.tsx
            β”‚       └── πŸ“„ ChildComponent.tsx
            └── πŸ“‚ hooks
                β”œβ”€β”€ ...
                β”œβ”€β”€ πŸ“„ useSimpleParentComponentHook.test.ts
                β”œβ”€β”€ πŸ“„ useSimpleParentComponentHook.ts
                └── πŸ“‚ useComplexParentComponentHook
                    β”œβ”€β”€ πŸ“ hooks (recursion)
                    β”œβ”€β”€ πŸ“„ useComplexParentComponentHook.api.ts
                    β”œβ”€β”€ πŸ“„ useComplexParentComponentHook.types.ts
                    β”œβ”€β”€ πŸ“„ useComplexParentComponentHook.test.ts
                    └── πŸ“„ useComplexParentComponentHook.ts

JSON

{
    "$schema": "node_modules/eslint-plugin-project-structure/projectStructure.schema.json",
    "ignorePatterns": ["src/legacy/*"],
    "structure": {
        "children": [
            {
                "extension": "*"
            },
            {
                "name": "src",
                "children": [
                    {
                        "ruleId": "hooks_folder"
                    },
                    {
                        "ruleId": "components_folder"
                    }
                ]
            }
        ]
    },
    "rules": {
        "components_folder": {
            "name": "components",
            "children": [
                {
                    "ruleId": "component_folder"
                }
            ]
        },
        "hooks_folder": {
            "name": "hooks",
            "children": [
                {
                    "name": "/^use${{PascalCase}}$/",
                    "children": [
                        {
                            "ruleId": "hooks_folder"
                        },
                        {
                            "name": "/^${{parentName}}(\\.(test|api|types))?$/",
                            "extension": "ts"
                        }
                    ]
                },
                {
                    "name": "/^use${{PascalCase}}(\\.test)?$/",
                    "extension": "ts"
                }
            ]
        },
        "component_folder": {
            "name": "/^${{PascalCase}}$/",
            "children": [
                {
                    "ruleId": "components_folder"
                },
                {
                    "ruleId": "hooks_folder"
                },
                {
                    "name": "/^${{parentName}}${{yourCustomRegexParameter}}$/",
                    "extension": ".ts"
                },
                {
                    "name": "/^${{ParentName}}(\\.(context|test))?$/",
                    "extension": ".tsx"
                }
            ]
        }
    },
    "regexParameters": {
        "yourCustomRegexParameter": "\\.(types|api)"
    }
}

YAML

ignorePatterns:
    - src/legacy/*
structure:
    children:
        - extension: "*"
        - name: src
          children:
              - ruleId: hooks_folder
              - ruleId: components_folder
rules:
    components_folder:
        name: components
        children:
            - ruleId: component_folder
    hooks_folder:
        name: hooks
        children:
            - name: "/^use${{PascalCase}}$/"
              children:
                  - ruleId: hooks_folder
                  - name: "/^${{parentName}}(\\.(test|api|types))?$/"
                    extension: ts
            - name: "/^use${{PascalCase}}(\\.test)?$/"
              extension: ts
    component_folder:
        name: "/^${{PascalCase}}$/"
        children:
            - ruleId: components_folder
            - ruleId: hooks_folder
            - name: "/^${{parentName}}${{yourCustomRegexParameter}}$/"
              extension: ".ts"
            - name: "/^${{ParentName}}(\\.(context|test))?$/"
              extension: ".tsx"
regexParameters:
    yourCustomRegexParameter: "\\.(types|api)"

API:

"$schema": <string | undefined>

Type checking for your projectStructure.json. It helps to fill configuration correctly.

{
    "$schema": "node_modules/eslint-plugin-project-structure/projectStructure.schema.json"
    // ...
}

"ignorePatterns": <string[] | undefined>

Here you can set the paths you want to ignore.

{
    "ignorePatterns": ["src/legacy/*"]
    // ...
}

"name": <string | undefined>

When used with children this will be the name of folder.
When used with extension this will be the name of file.
If used without children and extension this will be name of folder and file.

Note If you only care about the name of the folder without rules for its children, leave the children as [].

Note If you only care about the name of the file without rules for its extension, leave the extension as "*".

Fixed name

Fixed file/folder name.

{
    "name": "FixedName"
    // ...
}

Regex

Dynamic file/folder name.
Remember that the regular expression must start and end with a /.

{
    "name": "/^Regex logic$/"
    // ...
}

"regexParameters": <Record<string, string> | undefined>

A place where you can add your own regex parameters.
You can use built-in regex parameters. You can overwrite them with your logic, exceptions are parentName and ParentName overwriting them will be ignored.
You can freely mix regex parameters together see example.

{
    "regexParameters": {
        "yourCustomRegexParameter": "(Regex logic)",
        "camelCase": "(Regex logic)", // Override built-in camelCase.
        "parentName": "(Regex logic)", // Overwriting will be ignored.
        "ParentName": "(Regex logic)" // Overwriting will be ignored.
        // ...
    }
    // ...
}

Then you can use them in regex with the following notation ${{yourCustomRegexParameter}}.

{
    "name": "/^${{yourCustomRegexParameter}}$/"
    // ...
}

Note Remember that the regular expression must start and end with a /.

Note If your parameter will only be part of the regex, I recommend wrapping it in parentheses and not adding /^$/.

Built-in regex parameters

${{parentName}}
The child inherits the name of the folder in which it is located and sets its first letter to lowercase.

{
    "name": "/^${{parentName}}$/"
}

${{ParentName}}
The child inherits the name of the folder in which it is located and sets its first letter to uppercase.

{
    "name": "/^${{ParentName}}$/"
}

${{PascalCase}}
Add PascalCase validation to your regex.
The added regex is ((([A-Z]|\d){1}([a-z]|\d)*)*([A-Z]|\d){1}([a-z]|\d)*).

{
    "name": "/^${{PascalCase}}$/"
}

${{camelCase}}
Add camelCase validation to your regex.
The added regex is (([a-z]|\d)+(([A-Z]|\d){1}([a-z]|\d)*)*).

{
    "name": "/^${{camelCase}}$/"
}

${{snake_case}}
Add snake_case validation to your regex.
The added regex is ((([a-z]|\d)+_)*([a-z]|\d)+).

{
    "name": "/^${{snake_case}}$/"
}

${{kebab-case}}
Add kebab-case validation to your regex.
The added regex is ((([a-z]|\d)+-)*([a-z]|\d)+).

{
    "name": "/^${{kebab-case}}$/"
}

${{dash-case}}
Add dash-case validation to your regex.
The added regex is ((([a-z]|\d)+-)*([a-z]|\d)+).

{
    "name": "/^${{dash-case}}$/"
}

Regex parameters mix example

Here are some examples of how easy it is to combine regex parameters.

{
    // useNiceHook
    // useNiceHook.api
    // useNiceHook.test
    "name": "/^use${{PascalCase}}(\\.(test|api))?$/"
}
{
    // YourParentName.hello_world
    // YourParentName.hello_world.test
    // YourParentName.hello_world.api
    "name": "/^${{ParentName}}\\.${{snake_case}}(\\.(test|api))?$/"
}

"extension": <string | string[] | undefined>

Extension of your file.
Not available when children are used.

{
    "extension": ["*", ".ts", ".tsx", "js", "jsx", "..."]
    // ...
}

Warning If you want to check extensions that are not supported by eslint like .css, .sass, .less, .svg, .png, .jpg, .ico, .yml, .json go to Step 1.

Note You don't need to add . it is optional.

Note If you want to include all extensions use *.

"children": <Rule[] | undefined>

Folder children rules.
Not available when extension is used.

{
    "children": [
        {
            "name": "Child"
            // ...
        }
        // ...
    ]
    // ...
}

"structure": <Rule>

The structure of your project and its rules.

.
β”œβ”€β”€ πŸ“‚ libs
β”œβ”€β”€ πŸ“‚ src
β”œβ”€β”€ πŸ“‚ yourCoolFolderName
└── πŸ“„ ...
{
    "structure": {
        "children": [
            {
                "name": "libs",
                "children": [
                    // ...
                ]
            },
            {
                "name": "src",
                "children": [
                    // ...
                ]
            },
            {
                "name": "yourCoolFolderName",
                "children": [
                    // ...
                ]
            },
            {
                "extension": "*" // All files located in the root of your project, like package.json, .eslintrc, etc. You can specify them more precisely.
            }
            // ...
        ]
    }
    // ...
}

Warning Make sure your tsconfig/.eslintrc contains all the files/folders you want to validate. Otherwise eslint will not take them into account.

"rules": <Record<string, Rule> | undefined>

A place where you can add your custom rules. This is useful when you want to avoid a lot of repetition in your structure or use folder recursion feature.
The key in the object will correspond to ruleId, which you can then use in many places.

{
    "rules": {
        "yourCustomRule": {
            "name": "ComponentName",
            "children": [
                // ...
            ]
        }
        // ...
    }
    // ...
}

"ruleId": <string | undefined>

A reference to your custom rule.

{
    "ruleId": "yourCustomRule"
    // ...
}

You can use it with other keys like name, extension and children but remember that they will override the keys from your custom rule.
This is useful if you want to get rid of a lot of repetition in your structure, for example, folders have different name, but the same children.

.
β”œβ”€β”€ ...
└── πŸ“‚ src
    β”œβ”€β”€ πŸ“‚ folder1
    β”‚   β”œβ”€β”€ ...
    β”‚   └── πŸ“‚ NestedFolder
    β”‚       β”œβ”€β”€ ...
    β”‚       β”œβ”€β”€ πŸ“„ File1.tsx
    β”‚       └── πŸ“„ file2.ts
    └── πŸ“‚ folder2
        β”œβ”€β”€ πŸ“‚ subFolder1
        β”‚    β”œβ”€β”€ ...
        β”‚    β”œβ”€β”€ πŸ“„ File1.tsx
        β”‚    └── πŸ“„ file2.ts
        └── πŸ“‚ subFolder2
            β”œβ”€β”€ ...
            β”œβ”€β”€ πŸ“„ File1.tsx
            └── πŸ“„ file2.ts
{
    "structure": {
        "children": [
            {
                "name": "src",
                "children": [
                    {
                        "name": "folder1",
                        "children": [
                            {
                                "name": "/^${{PascalCase}}$/",
                                "ruleId": "shared_children"
                            }
                        ]
                    },
                    {
                        "name": "folder2",
                        "children": [
                            {
                                "name": "/^(subFolder1|subFolder2)$/",
                                "ruleId": "shared_children"
                            }
                        ]
                    }
                ]
            }
            // ...
        ]
    },
    "rules": {
        "shared_children": {
            "children": [
                {
                    "name": "/^${{PascalCase}}$/",
                    "extension": ".tsx"
                },
                {
                    "name": "/^${{camelCase}}$/",
                    "extension": ".ts"
                }
            ]
        }
        // ...
    }
    // ...
}

Folder recursion

You can easily create recursions when you refer to the same ruleId that your rule has.

Suppose your folder is named ComponentFolder which satisfies the rule ${{PascalCase}} and your next folder will be NextComponentFolder which also satisfies the rule ${{PascalCase}}. In this case, the recursion will look like this:

.
β”œβ”€β”€ ...
└── πŸ“‚ src
    └── πŸ“‚ ComponentFolder
        β”œβ”€β”€ ...
        └── πŸ“‚ components
            β”œβ”€β”€ ...
            └── πŸ“ NextComponentFolder
                β”œβ”€β”€ ...
                └── πŸ“‚ components
                    └── ... (recursion)
{
    "structure": {
        "children": [
            {
                "name": "src",
                "children": [
                    {
                        "ruleId": "yourCustomRule"
                    }
                ]
            }
            // ...
        ]
    },
    "rules": {
        "yourCustomRule": {
            "name": "/^${{PascalCase}}$/",
            "children": [
                {
                    "name": "components",
                    "children": [
                        {
                            "ruleId": "yourCustomRule"
                        }
                        // ...
                    ]
                }
                // ...
            ]
        }
        // ...
    }
    // ...
}