eslint-plugin-import: `import/order` fails to recognize internal import groups

I’m using babel-plugin-module-resolver with eslint-import-resolver-babel-module to map imports from ~/<path> to src/<path>:

In my ESLint config, the import/order rule is turned on using the following configuration:

module.exports = {
  rules: {
    'import/order': [
      2,
      {
        groups: [
          'builtin',
          'external',
          'internal',
          ['parent', 'sibling', 'index'],
        ],
        'newlines-between': 'always',
      },
    ],
  },
};

Imports from ~/<path>, I believe, should be classified as an internal import group separated from other groups by a newline. I’m not sure what the behaviour is here, but given the following code:

import someNpmModule from 'some-npm-module';

import SomeComponent from '~/components/SomeComponent';
import someUtil from '~/utils/someUtil';

I’m receiving the following error:

   3:1   error  There should be at least one empty line between import groups  import/order

About this issue

  • Original URL
  • State: closed
  • Created 7 years ago
  • Reactions: 37
  • Comments: 44 (13 by maintainers)

Commits related to this issue

Most upvoted comments

Maybe the actual fix is just to allow a setting which gives flexibility to what is considered an internal module.

Maybe something like this:

function isInternalModule(name, settings, path) {
  const folders = settings && settings['import/internal-module-folders']
  const customInternalModule = folders && folders.some(folder => name.startsWith(folder))
  return customInternalModule || externalModuleRegExp.test(name) && !isExternalPath(path, name, settings)
}

FYI, my use-case is a yarn workspace with namespaced packages. So, I have several packages in packages/, all of them are prefixed with a namespace like @x-client/ or @x-server/. Using yarn workspace, I can just import like that, e.g. import { getBaseUrl } from '@x-client/api'; without relative paths.

When using import/order, all my actually internal packages are treated as external, and it would be great to have some control over that treatment.

Hi there, I resolved this issue the following way. I hope this can help someone.

For JavaScript:

settings: {
  'import/resolver': {
    node: {
      paths: ['./src'],
    },
  },
},

And overrides for TypeScript files:

settings: {
  'import/parsers': {
    '@typescript-eslint/parser': ['.ts', '.tsx'],
  },
  'import/resolver': {
    node: {},
    ts: {
      directory: tsconfigPath,
    },
  },
},

I want to clarify the approach I highlighted above also worked within a monorepo, with a root ESLint config.

However, for us, internal means inside a package, and that may be a distinction. We don’t count other packages in the monorepo as internal.

I used the ts resolver over the typescript resolver, but it solved all problems and correctly identified internal modules. However, I haven’t updated this config since that time and the situation may have changed.

On a new project I’m working on, which is not a monorepo, I just used the below and internal groups are working as expected:

module.exports = {
  extends: [
    // ...
    'plugin:import/typescript',
  ],
  // ...
  settings: {
    'import/resolver': {
      node: {
        paths: 'src',
      },
    },
  },
};

I think what is missing is an option for custom resolver to define to which group a resolved file belongs to.

I was able to add the alias resolver, and it does seem to improve the behavior.

In my case, I don’t have specific aliases set up, but I do have “absolute imports” set up for a Create-React-App + TS project. My tsconfig.json has:

  "include": [
    "src"
  ]

For my app specifically, the source folder structure is:

- /src
  - /app
  - /common
  - /features

and so typical imports might look like:

import {RootState} from "app/reducers/rootReducer";
import {showDialog} from "common/dialogs/dialogsRegistry";
import {logout} from "features/auth/authSlice";

Those are imports I would want classified as “internal”.

I added these settings to my ESLint config:

    "settings": {
        "import/resolver": {
            "typescript": {
                "directory": "."
            },
            "alias": {
                "map": [
                    ["app", "./src/app"],
                    ["common", "./src/common"],
                    ["features", "./src/features"]
                ],
                "extensions": [".ts", ".js", ".jsx", ".json"]
            }
        }
    },
    "rules": {
        "import/order": [
            "warn",
            {
                "groups": ["builtin", "external", "internal", "parent", "sibling", "index"],
                "newlines-between": "always-and-inside-groups"
            }
        ]
    }

That appears to have them mostly sorting how I want - after 3rd-party libs, and before local relative imports.

@ljharb Thanks. I guess that wasn’t clear to me, that statement didn’t seem to imply that internal is nothing by default. Also, it’s true that any file within the project is technically a sibling or parent, but I thought that was defined by the use of the relative path, and I’m using an absolute path.

import/internal-regex doesn’t work for me because I’d have to list every subfolder under src/ for the regex to pick it up. For example if my paths are src/routes/user and src/models/user, my imports are routes/user and models/user and there’s no common prefix to use as the regex.

The solution I posted above solves my use case and treats any import within the monorepo as internal, I just don’t understand how it’s working. @mrmckeb’s last response also works if you want monorepo imports outside of the local project to be treated as external instead of internal.

@stramel There’s a PR (#914) to address this, but it needs changes which I’m unable to commit to doing at this time.

If you like I could add you as a collaborator on my fork.

In that case, I’ll repeat:

I’m not clear on what’s actionable here.

What would help the most is a PR with failing test cases (and even better, also a fix! but i can handle that part) so it’s clear what needs fixing.

At this point, I’m going to close the thread, but will immediately reopen it once it becomes clear what the remaining issue is 😃

Unfortunately, I drove the discussion off-topic… Workspaces are a different use case entirely. Totally worth supporting, but I don’t see how that relates to the OP. Apologies.

internal is nothing by default. This is expected (and documented).

This is a long thread and I’m not clear on what’s actionable here.

@ljharb I believe that missing distinction is what is actionable here: most of us are asking for a new feature and concept of local workspace packages vs registry-installed packages. It doesn’t help that no such concept exists and is documented or not.

In the end, from a project’s codebase perspective this is a major distinction between “dependencies” and actual “source code”.

In modern projects with workspaces, you just can’t tell the difference by looking at an imported package name, e.g. import utils from 'utils' could be either a utils package from npm, or a local one.

By the way, workspaces are not just a yarn feature anymore - npm 7 embraces and introduces the exact same concept of workspaces and local packages, see https://github.com/npm/rfcs/blob/latest/accepted/0026-workspaces.md

So what’s needed is a conceptual decision for the plugin first: support local packages and allow some control over the handling, or not.

The implementation could be anything from a simple yet good solution where users configure the plugin and tell it “what is local”, to more complex solutions that use the actual npm/yarn APIs and inherently know which packages are from local workspaces and which aren’t.

I initially tried the typescript resolver, but it kept counting modules that were included via a @types/_whatever package as an internal modules, which messed up the grouping. I setup the alias map just like you did.

If you’re using .tsx files be sure to include that in the extensions.

Heres my config tsconfig.json

{
  "compilerOptions": {
    "paths": {
      "src/*": ["src/*"]
    }
  }
}

.eslintrc.js

const root = (...args) => path.resolve(__dirname, ...args)
{
      settings: {
        'import/resolver': {
          alias: {
            map: [['src', root('client/src')]],
            extensions: ['.ts', '.tsx', '.js', '.jsx', '.json'],
          },
        },
      },
    },
}

@Whoaa512 I could try to take a look on it. Could give some initial guidance/context? Thank you

Maybe a bit off topic but… Can’t we just configure certain top-level paths to be explicitly treated as internal, like shared/? I guess given all possibilities of aliasing and module resolving, it would be much easier that way than trying to determine everything heuristically… Also, using yarn workspaces and lerna, I started to use use namespaces like “@client/” or “@server/”, but I might also introduce some namespace that actually exists on the public registry, but is a working local package nonetheless.