javierbrea / eslint-plugin-boundaries

Eslint plugin checking architecture boundaries between elements
MIT License
473 stars 9 forks source link

Work with the tree structure of the project #313

Closed budarin closed 7 months ago

budarin commented 7 months ago

Now the plugin works with a flat project structure - folders in the root of src, but sometimes not all projects have such a flat structure.

For example, we have the following structure that reflects the vision of a clean architecture:

src/
├── core/
│   ├── contracts
│   ├── use_cases
│   └── domain/
│       ├── entities
│       └── store
│
├── ui
├── services
├── shared
├── utils
└── index.ts

Described each folder in boundaries/elements and tried to write rules for them

module.exports = {
    settings: {
        'boundaries/elements': [
            {
                type: 'root',
                pattern: 'src/index.ts',
                mode: 'full',
            },
            {
                type: 'core',
                pattern: 'src/core/*',
            },
            {
                type: 'contracts',
                pattern: 'src/core/contracts/*',
            },

            {
                type: 'domain',
                pattern: 'src/core/domain/*',
            },
            {
                type: 'entities',
                pattern: 'src/core/domain/entities/*',
            },
            {
                type: 'store',
                pattern: 'src/core/domain/store/*',
            },

            {
                type: 'use_cases',
                pattern: 'src/core/use_cases/*',
            },

            {
                type: 'services',
                pattern: 'src/services/*',
            },
            {
                type: 'ui',
                pattern: 'src/ui/*',
            },
            {
                type: 'shared',
                pattern: 'src/shared/*',
            },
            {
                type: 'utils',
                pattern: 'src/utils/*',
            },
        ],
    },
    rules: {
        'boundaries/element-types': [
            2,
            {
                default: 'disallow',
                rules: [
                    {
                        from: 'root',
                        allow: ['utils'],
                    },
                    {
                        from: 'contracts',
                        allow: ['domain'],
                        importKind: 'type',
                    },
                    {
                        from: 'entities',
                        allow: ['shared'],
                    },
                    {
                        from: 'store',
                        allow: ['contracts', 'entities', 'shared'],
                    },
                    {
                        from: 'ui',
                        allow: ['contracts', 'entities', 'use_cases', 'shared'],
                    },
                    {
                        from: 'services',
                        allow: ['contracts', 'shared'],
                    },
                    {
                        from: 'use_cases',
                        allow: ['contracts', 'entities', 'store', 'services', 'shared'],
                    },
                    {
                        from: 'utils',
                        allow: '*',
                    },
                ],
            },
        ],
    },
};

But unfortunately it didn't work (

// src/core/contracts/api.ts

import type { StoreState } from 'store/types.ts';
...

Rule for contracts is not working

    {
        from: 'contracts',
        allow: ['domain'],
        importKind: 'type',
    }

Plugin shows the error:

No rule allowing this dependency was found. File is of type 'core'. Dependency is of type 'core'eslint[boundaries/element-types](https://github.com/javierbrea/eslint-plugin-boundaries/blob/master/docs/rules/element-types.md)

Info from debugger:

[boundaries]: 'src/core/contracts/api.ts' is of type 'core'
{
  "path": "src/core/contracts/api.ts",
  "isIgnored": false,
  "type": "core",
  "elementPath": "src/core/contracts",
  "capture": [
    "contracts",
    "",
    "api.ts"
  ],
  "capturedValues": null,
  "internalPath": "api.ts",
  "parents": []
}
...
[boundaries]: 'src/core/domain/store/types.ts' is of type 'core'
{
  "source": "store/types.ts",
  "path": "src/core/domain/store/types.ts",
  "isIgnored": false,
  "isLocal": true,
  "isBuiltIn": false,
  "isExternal": false,
  "baseModule": null,
  "type": "core",
  "elementPath": "src/core/domain",        
  "capture": [
    "domain",
    "store",
    "types.ts"
  ],
  "capturedValues": null,
  "internalPath": "store/types.ts",        
  "parents": []
}

We are using aliases for known paths in tsconfig.json

{
    "compilerOptions": {
        "baseUrl": "./src",
        "paths": {
            "domain/*": ["core/domain/*"],
            "contracts/*": ["core/contracts/*"],
            "entities/*": ["core/domain/entities/*"],
            "store/*": ["core/domain/store/*"],
            "use_cases/*": ["core/use_cases/*"],
            "ui/*": ["ui/*"],
            "services/*": ["services/*"],
            "shared/*": ["shared/*"],
            "utils/*": ["utils/*"]
        }
    },
}
budarin commented 7 months ago

also have strange error for the file from src/utils/initApplication.ts:

File is not of any known element typeeslint[boundaries/no-unknown-files](https://github.com/javierbrea/eslint-plugin-boundaries/blob/master/docs/rules/no-unknown-files.md)

debug info

[boundaries]: 'src/utils/initApplication.ts' is of unknown type
{
  "source": "utils/initApplication.ts",
  "path": "src/utils/initApplication.ts",
  "isIgnored": false,
  "isLocal": true,
  "isBuiltIn": false,
  "isExternal": false,
  "baseModule": null,
  "type": null,
  "elementPath": null,
  "capture": null,
  "capturedValues": null,
  "internalPath": null,
  "parents": []
}
settings: {
 'boundaries/elements': [
     ...
     {
        type: 'utils',
        pattern: 'src/utils/*',
     },
     ...
  ]
}
rules: {
    'boundaries/element-types': [
        ...
        {
            from: 'utils',
            allow: '*',
        },
        ...
   ],
}
javierbrea commented 7 months ago

Hi @budarin , the plugin is able to work with structures of any level. The problem in your configuration seems to be that you are trying to assign multiple types to the same files, and this is not supported. The type of each file is unique, and the plugin will assign to it the first one that matches. The key is that you can also use the "capture" feature to assign more custom categories to each file. Afterwards you can also use those categories in your rules.

For example:

module.exports = {
    settings: {
        'boundaries/elements': [
            {
                type: 'root',
                pattern: 'src/index.ts',
                mode: 'full',
            },
            {
                type: 'contracts',
                pattern: 'src/*/contracts/*',
                capture: ["category"],
            },

            {
                type: 'domain',
                pattern: 'src/*/domain/*',
                capture: ["category"],
            },
      ],
   },
};

In this example, I'm capturing the first folder as "category". So, you can now have "contracts" and "domain" types in any "category".

Then, you could define the rules as in:

module.exports = {
rules: {
        'boundaries/element-types': [
            2,
            {
                default: 'disallow',
                rules: [
                    {
                        from: [['contracts', { category: "core" }]],
                        allow: [['domain', { category: "core" }]],
                        importKind: 'type',
                    },
                    {
                        from: [['entities', { category: "core" }]],
                        allow: [['shared', { category: "core" }]],
                    },
                ],
            },
        ],
    },
};

Here I'm defining that contracts with category "core" can import "domain" with category core, etc. You could use even templates for defining more generic rules, for example allowing elements to import other elements, but always having same "category":

module.exports = {
rules: {
        'boundaries/element-types': [
            2,
            {
                default: 'disallow',
                rules: [
                    {
                        from: "contracts",
                        allow: [['domain', { category: "${from.category}" }]],
                        importKind: 'type',
                    },
                    {
                        from: "entities",
                        allow: [['shared', { category: "${from.category}" }]],
                    },
                ],
            },
        ],
    },
};

So, you can use multiple levels both when categorizing elements, and when defining rules, making the plugin very customizable to any structure.

I recommend you to read the documentation about this respect at:

Maybe it can be interesting for you also to read the advanced example: https://github.com/javierbrea/eslint-plugin-boundaries#advanced-example-of-a-rule-configuration

About your second comment, the problem is that the file is not recognized as any "element type". So, it does not match any pattern in the settings. If you want to have "unrecognized" types in your project, you can disable the rule "no-unknown-files".

budarin commented 7 months ago

Hi @javierbrea ! Thanks for the response!

I 'm sorry for the issue. I could not catch the basic idea of the plugin from the documentation - there is still something to improve in the documentation :)

The idea of capturing is quite non-trivial and difficult to understand from the first reading.

I will try to study capture and try to apply it, but it seems to me that it is probably possible to change the algorithm of work a little to simplify the use cases.

budarin commented 7 months ago

My 2nd comment was because I don't understand why the plugin decided that the files in src/utils and src/services are unknown - I described them

    settings: {
        'boundaries/elements': [
            ...
             {
                type: 'services',
                pattern: 'src/services/*',
            },
            {
                type: 'utils',
                pattern: 'src/utils/*',
            },
        ],
    },

from debugger output

[boundaries]: 'src/services/index.ts' is of unknown type
{
  "source": "services/index.ts",
  "path": "src/services/index.ts",
  "isIgnored": false,
  "isLocal": true,
  "isBuiltIn": false,
  "isExternal": false,
  "baseModule": null,
  "type": null,
  "elementPath": null,
  "capture": null,
  "capturedValues": null,
  "internalPath": null,
  "parents": []
}

[boundaries]: 'src/utils/initialStoreData.ts' is of unknown type
{
  "source": "utils/initialStoreData.ts",
  "path": "src/utils/initialStoreData.ts",
  "isIgnored": false,
  "isLocal": true,
  "isBuiltIn": false,
  "isExternal": false,
  "baseModule": null,
  "type": null,
  "elementPath": null,
  "capture": null,
  "capturedValues": null,
  "internalPath": null,
  "parents": []
}
javierbrea commented 7 months ago

@budarin , You're right, It could probably be simplified, but then it probably won't cover the million of different use cases that users require. About improving the documentation, you're also right, for sure it can be improved as well. Anyway, this is an opensource project, and any contribution will be welcome, so, feel free to open PRs with any kind of specific suggestion or improvement, they will be welcome 😃 .

budarin commented 7 months ago

@javierbrea

I'm trying to debug the configuration you suggested and I can't match it with the debugging information

module.exports = {
    settings: {
        'boundaries/elements': [
            {
                type: 'contracts',
                // src/core/contracts
                pattern: 'src/*/contracts/*',
                capture: ["category"],  // category === 'core'
            },
            {
                type: 'domain',
                // src/core/domain
                pattern: 'src/*/domain/*',
                capture: ['category'],  // category === 'core'
            },
            {
                type: 'store',
                // src/core/domain/store
                pattern: 'src/*/*/store/*',
                capture: ['category', 'subcategory'],  // category === 'core' subcategory === 'domain`
            },
      ],
   },
};
module.exports = {
rules: {
        'boundaries/element-types': [
            2,
            {
                default: 'disallow',
                rules: [
                    {
                        from: [['contracts', { category: "core" }]],
                        allow: [['domain', { category: "core" }]],
                        importKind: 'type',
                    },
                ],
            },
        ],
    },
};

there is no reflection of the keys from capture in it

[boundaries]: 'src/core/contracts/api.ts' is of type 'core'
{
  "path": "src/core/contracts/api.ts",
  "isIgnored": false,
  "type": "core",
  "elementPath": "src/core/contracts",
  "capture": [
    "contracts",
    "",
    "api.ts"
  ],
  "capturedValues": null,
  "internalPath": "api.ts",
  "parents": []
}

[boundaries]: 'src/core/domain/store/types.ts' is of type 'core'
{
  "source": "store/types.ts",
  "path": "src/core/domain/store/types.ts",
  "isIgnored": false,
  "isLocal": true,
  "isBuiltIn": false,
  "isExternal": false,
  "baseModule": null,
  "type": "core",
  "elementPath": "src/core/domain",        
  "capture": [
    "domain",
    "store",
    "types.ts"
  ],
  "capturedValues": null,
  "internalPath": "store/types.ts",        
  "parents": []
}

so I still have the errors in code

image

budarin commented 7 months ago

and it's unclear for me why files from src/utils counted as unknow

  'boundaries/elements': [
      ...
      {
          type: 'utils',
          pattern: 'src/utils/*',
      },
  ],
[boundaries]: 'src/utils/initialStoreData.ts' is of unknown type
{
  "source": "utils/initialStoreData.ts",
  "path": "src/utils/initialStoreData.ts",
  "isIgnored": false,
  "isLocal": true,
  "isBuiltIn": false,
  "isExternal": false,
  "baseModule": null,
  "type": null,
  "elementPath": null,
  "capture": null,
  "capturedValues": null,
  "internalPath": null,
  "parents": []
}

It's a bug!

budarin commented 7 months ago

Here is the dummy project for testing

You can take the project as a real example of using a plugin on a tree structure