1.4.7 • Published 9 months ago

eslint-plugin-project-structure v1.4.7

Weekly downloads
-
License
MIT
Repository
github
Last release
9 months ago

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. ✅ Validation of folder and file names. ✅ Name case validation. ✅ Name regex validation. ✅ File extension validation. ✅ Inheriting the parent's name (the child inherits the name of the folder in which it is located). ✅ Folder recursion. ✅ Forcing a nested/flat structure.

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 have a well-thought-out and proven project structure and want to share it with others, you can add it with a description in the discussions section.

JSON 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
{
    "$schema": "node_modules/eslint-plugin-project-structure/projectStructure.schema.json",
    "ignorePatterns": ["src/legacy/*"],
    "structure": {
        "children": [
            {
                "name": "src",
                "children": [
                    {
                        "ruleId": "components_folder"
                    },
                    {
                        "ruleId": "hooks_folder"
                    }
                ]
            },
            {
                "extension": "*"
            }
        ]
    },
    "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 example

ignorePatterns:
    - src/legacy/*
structure:
    children:
        - name: src
          children:
              - ruleId: components_folder
              - ruleId: hooks_folder
        - extension: "*"
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": "/^(Your 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": "/^(Your regex logic)$/",
        "camelCase": "/^(Your regex logic)$/", // Override built-in camelCase.
        "parentName": "/^(Your regex logic)$/", // Overwriting will be ignored.
        "ParentName": "/^(Your 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.

{
    "structure": {
        "children": [
            {
                "name": "libs"
                // ...
            },
            {
                "name": "apps"
                // ...
            },
            {
                "name": "src"
                // ...
            },
            {
                "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.

{
    "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/features/ComponentFolder/components/NextComponentFolder/components... (recursion).

{
    "structure": {
        "children": [
            {
                "name": "src",
                "children": [
                    {
                        "name": "features",
                        "children": [
                            {
                                "ruleId": "yourCustomRule"
                                // ...
                            }
                            // ...
                        ]
                    }
                    // ...
                ]
            }
            // ...
        ]
    },
    "rules": {
        "yourCustomRule": {
            "name": "/^${{PascalCase}}$/",
            "children": [
                {
                    "name": "components",
                    "children": [
                        {
                            "ruleId": "yourCustomRule"
                            // ...
                        }
                        // ...
                    ]
                }
                // ...
            ]
        }
        // ...
    }
    // ...
}
1.4.6

9 months ago

1.3.7

9 months ago

1.4.5

9 months ago

1.3.6

9 months ago

1.4.4

9 months ago

1.3.5

9 months ago

1.4.3

9 months ago

1.3.4

9 months ago

1.4.2

9 months ago

1.3.3

9 months ago

1.4.1

9 months ago

1.4.0

9 months ago

1.3.9

9 months ago

1.4.7

9 months ago

1.3.8

9 months ago

1.3.2

10 months ago

1.3.1

10 months ago

1.3.0

10 months ago

1.2.0

10 months ago

1.1.1

10 months ago

1.1.0

10 months ago

1.0.10

10 months ago

1.0.9

10 months ago

1.0.8

10 months ago

1.0.7

10 months ago

1.0.6

10 months ago

1.0.5

10 months ago

1.0.4

10 months ago

1.0.3

10 months ago

1.0.2

10 months ago

1.0.1

10 months ago

1.0.0

10 months ago

0.0.1

10 months ago