Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

New resolver design #40

Closed
JounQin opened this issue Mar 12, 2024 · 28 comments
Closed

New resolver design #40

JounQin opened this issue Mar 12, 2024 · 28 comments
Assignees
Labels

Comments

@JounQin
Copy link
Member

JounQin commented Mar 12, 2024

I haven't remove the resolver concept in eslint-plugin-import-x<=0.2 because I found that the webpack resolver may be still useful for those webpack users, and although we can use enhanced-resolve directly, but the settings can not fit all projects, I need to figure out how to set the resolver correctly for specific projects.

For example:

  1. plain js
  2. plain typescript
  3. webpack specific syntaxes

I'm thinking the interface of import-x/resolver setting should be:

import type { ResolveOptions } from 'enhanced-resolve'

interface ResolverSettings {
  typescript?: boolean
  webpack?: boolean
  options?: ResolveOptions
}

By default, eslint-plugin-import-x should use enhanced-resolve directly to simulate native node resolve algorithm.

@silverwind
Copy link

Why not:

{
  resolver: "webpack",
  resolverOptions: {/* resolver-specific options */},
}

@SukkaW
Copy link
Collaborator

SukkaW commented Mar 22, 2024

I don't like this idea of resolver: 'webpack' | 'typescript'.

Let's do resolver: require('@eslint-import/webpack') and resolver: import('@eslint-import/webpack').

@JounQin
Copy link
Member Author

JounQin commented Mar 22, 2024

Let's do resolver: require('@eslint-import/webpack') and resolver: import('@eslint-import/webpack').

.eslintrc won't work is this case? Maybe you mean resolver: '@eslint-import/webpack' which is how eslint-plugin-import(-x) work currently?

I don't quite want to support custom resolver actually, that's why I proposed webpack?: boolean option, there should only a few resolvers for usage, ideally there should be no resolver concept except TypeScript, but webpack users may want to resolve webpack config automatically (I'm thinking if there are really eslint users using it nowadays because it does not work for webpack.cofig.ts/webpack.config.mjs, etc).

image

@SukkaW
Copy link
Collaborator

SukkaW commented Mar 22, 2024

I don't quite want to support custom resolver actually, that's why I proposed webpack?: boolean option, there should only a few resolvers for usage, ideally there should be no resolver concept except TypeScript, but webpack users may want to resolve webpack config automatically (I'm thinking if there are really eslint users using it nowadays because it does not work for webpack.cofig.ts/webpack.config.mjs, etc).

Since we have multiple resolvers, and each one of them will have its own unique set of options. Perhaps we could approach it like this instead:

{
  webpack?: boolean | WebpackResolverOption | null
  typescript?: boolean | TypeScriptResolverOption | null
  node?: boolean | NodeResolverOption | null
}

@JounQin
Copy link
Member Author

JounQin commented Mar 22, 2024

Since we have multiple resolvers, and each one of them will have its own unique set of options. Perhaps we could approach it like this instead:

That's how it's working nowadays except it requires specific npm packages installed. 🥹

But if we are going to adopt using enhanced-resolve instead, I think no node resolver should remain because it should be the default behavior. Should'd it?

For resolver options, could there be any other options except enhanced-resolve's?

I'm asking because I want to make a breaking change to drop support for previous resolvers instead of continuing.

@silverwind
Copy link

silverwind commented Mar 22, 2024

I don't like this idea of resolver: 'webpack' | 'typescript'.

Let's do resolver: require('@eslint-import/webpack') and resolver: import('@eslint-import/webpack').

That would work and be preferred in flat config format. Nonflat config has to support the package name as string.

@SukkaW
Copy link
Collaborator

SukkaW commented Mar 22, 2024

For resolver options, could there be any other options except enhanced-resolve's?

Yeah, I want to do the same. There are many resolve libraries out there trying to mimic Node.js-like resolving algorithm:

@silverwind
Copy link

silverwind commented Mar 22, 2024

I think the only modern resolution mechanisms one needs are these two (names based on tsconfig moduleResolution):

  • node-16: Node's resolution that requires file extensions. Likely should use import.meta.resolve and require.resolve, so should work without additional dependencies.
  • bundler: Similar to node, but does not require file extensions, maybe this could cover the webpack case as well.

@SukkaW
Copy link
Collaborator

SukkaW commented Mar 22, 2024

We will still need a library as long as we are publishing ESM/CJS dual packages and want to have a unified behavior across eslint.config.mjs and eslint.config.cjs.

@silverwind
Copy link

silverwind commented Mar 22, 2024

Is CJS support still desired? CJS packages could just remain on using eslint-plugin-import.

Non-flat configs load as CJS but if I recall correctly, import.meta.resolve is also available there.

@SukkaW
Copy link
Collaborator

SukkaW commented Mar 22, 2024

Non-flat configs load as CJS but if I recall correctly, import.meta.resolve is also available there.

Flat config supports both CJS and ESM, so there is no reason to make eslint-plugin-import-x a pure ESM module.

@silverwind
Copy link

silverwind commented Mar 22, 2024

Non-flat configs load as CJS but if I recall correctly, import.meta.resolve is also available there.

Flat config supports both CJS and ESM, so there is no reason to make eslint-plugin-import-x a pure ESM module.

Ah, you are right. I wasn't aware that it's possible to support CJS in flat config as I head read this, so transpilation to CJS will be required if non-flat config is to be supported.

@SukkaW
Copy link
Collaborator

SukkaW commented Apr 13, 2024

@JounQin Was reading this blog post: How we made Vite 4.3 faaaaster 🚀, and here I quote:

Vite 4.2 heavily depends on the resolve package to resolve the dependency's package.json, when we looked into the source code of resolve, there was much useless logic while resolving package.json. Vite 4.3 abandons resolve and follows the simpler resolve logic: directly checks whether package.json exists in the nested parents' directories.

IMHO should we build our own resolving algorithm as well?

@ehoogeveen-medweb
Copy link

Regarding typescript, it would be great if this could somehow piggyback off of typescript's own module resolution.

https://github.com/import-js/eslint-import-resolver-typescript mostly matches typescript, but I don't think it supports the project service, so you still have to tell it about all the relevant projects manually (in dependency order).

typescript-eslint does support the project service through EXPERIMENTAL_useProjectService and they intend to stabilize it as projectService for the next major release: typescript-eslint/typescript-eslint#9084

@n0099
Copy link

n0099 commented Jun 5, 2024

eslint-import-resolver-vite is used to resolve path alias that defined in vite.config.*: pzmosquito/eslint-import-resolver-vite#12

@Samuel-Therrien-Beslogic

import-js/eslint-import-resolver-typescript mostly matches typescript, but I don't think it supports the project service, so you still have to tell it about all the relevant projects manually (in dependency order).

Here's a request for that: import-js/eslint-import-resolver-typescript#282

@xsjcTony
Copy link

So is there any official way currently to support projectService? eslint-import-resolver-typescript doesn't support it at the moment and I don't think so it will be implemented in short. Any alternative way that can make it work?

@SukkaW
Copy link
Collaborator

SukkaW commented Aug 16, 2024

So is there any official way currently to support projectService?

eslint-import-resolver-typescript performs isolated file parsing and deliberately disables project and projectService. Unless typescript-eslint provides a document or a guide, it is impossible for a third-party library to use projectService.

@xsjcTony
Copy link

For anyone who wants to know, if you explicitly provide project to the resolver configuration, then everything will still work, since it's not using projectService at all. I didn't look into the code but I'm assuming it's trying to get the previous project setting in the TSESlint configuration.

@SukkaW
Copy link
Collaborator

SukkaW commented Aug 17, 2024

For anyone who wants to know, if you explicitly provide project to the resolver configuration, then everything will still work, since it's not using projectService at all. I didn't look into the code but I'm assuming it's trying to get the previous project setting in the TSESlint configuration.

If you do so, the typescript-eslint will become 1.5x slower, see typescript-eslint/typescript-eslint#8424

That's why project and projectService are deliberately disabled (as I mentioned) in eslint-plugin-import and eslint-plugin-import-x, see:

@xsjcTony
Copy link

Yeah seems so, but currently I can't really find a way to make things work without specifying the project option in eslint-import-resolver-typescript, since the import/order rule cannot recognise the paths settings in tsconfig.json.

I seems have some idea how the resolver works, but I'm not too sure. Anyway as a result, previously when I'm using the project option for typescript-eslint v7, it works all good, but once I switched to projectService, it breaks.

I tried my best to read through those issues, but this is just a bit hard for someone have no insights to understand well😥.

At least for now I have a way to make it work, so I'm satisfied with it. I guess if one day typescript-eslint provides the guide to take advantage of projectService for third-party libs, things should be straight forward and easy to understand for everyone.

@Samuel-Therrien-Beslogic

eslint-import-resolver-typescript performs isolated file parsing and deliberately disables project and projectService. Unless typescript-eslint provides a document or a guide, it is impossible for a third-party library to use projectService.

Does this help at all? typescript-eslint/typescript-eslint#8030 (reply in thread)

Has there been communications directly between eslint-plugin-import-x and typescript-eslint devs ?

@SukkaW SukkaW pinned this issue Sep 29, 2024
@SukkaW

This comment was marked as outdated.

@9romise
Copy link

9romise commented Oct 12, 2024

I agree with your design but have a few questions.
What is the difference between resolve and resolveImport, since in v2 there is only resolve.

@SukkaW
Copy link
Collaborator

SukkaW commented Oct 12, 2024

What is the difference between resolve and resolveImport, since in v2 there is only resolve.

resolveImport is from v1. Currently, eslint-plugin-import-x supports both v1 and v2, but we will drop both of them in the future. There are many v1 custom resolvers out there and I want to minimize the migration efforts for them.

But of course, I'd recommend all new resolvers implement resolve instead of resolveImport.


The original proposal updated.

@error-four-o-four
Copy link

Hey there,
regarding the upcoming changes I was wondering if there are any plans to support asynchronous resolvers in the future or if it would make sense or be feasible at all?

@SukkaW
Copy link
Collaborator

SukkaW commented Oct 31, 2024

feature request: FlatConfig support import-js/eslint-import-resolver-typescript#318

All resolves are happening inside the rules. So, unless ESLint supports async rules, we can only use a sync resolver for now.

renovate bot added a commit to mmkal/eslint-plugin-mmkal that referenced this issue Dec 3, 2024
##### [v4.5.0](https://github.com/un-ts/eslint-plugin-import-x/blob/HEAD/CHANGELOG.md#450)

##### Minor Changes

-   [#192](un-ts/eslint-plugin-import-x#192) [`fbf639b`](un-ts/eslint-plugin-import-x@fbf639b) Thanks [@SukkaW](https://github.com/SukkaW)! - The PR implements the new resolver design proposed in un-ts/eslint-plugin-import-x#40 (comment)

##### For `eslint-plugin-import-x` users

Like the ESLint flat config allows you to use js objects (e.g. import and require) as ESLint plugins, the new `eslint-plugin-import-x` resolver settings allow you to use js objects as custom resolvers through the new setting `import-x/resolver-next`:

```js
// eslint.config.js
import { createTsResolver } from '#custom-resolver';
const { createOxcResolver } = require('path/to/a/custom/resolver');

const resolverInstance = new ResolverFactory({});
const customResolverObject = {
  interfaceVersion: 3,
  name: 'my-custom-eslint-import-resolver',
  resolve(modPath, sourcePath) {
    const path = resolverInstance.resolve(modPath, sourcePath);
    if (path) {
      return {
        found: true,
        path
      };
    }

    return {
      found: false,
      path: null
    }
  };
};

module.exports = {
  settings: {
    // multiple resolvers
    'import-x/resolver-next': [
      customResolverObject,
      createTsResolver(enhancedResolverOptions),
      createOxcResolver(oxcOptions),
    ],
    // single resolver:
    'import-x/resolver-next': [createOxcResolver(oxcOptions)]
  }
}
```

The new `import-x/resolver-next` no longer accepts strings as the resolver, thus will not be compatible with the ESLint legacy config (a.k.a. `.eslintrc`). Those who are still using the ESLint legacy config should stick with `import-x/resolver`.

In the next major version of `eslint-plugin-import-x` (v5), we will rename the currently existing `import-x/resolver` to `import-x/resolver-legacy` (which allows the existing ESLint legacy config users to use their existing resolver settings), and `import-x/resolver-next` will become the new `import-x/resolver`. When ESLint v9 (the last ESLint version with ESLint legacy config support) reaches EOL in the future, we will remove `import-x/resolver-legacy`.

We have also made a few breaking changes to the new resolver API design, so you can't use existing custom resolvers directly with `import-x/resolver-next`:

```js
// When migrating to `import-x/resolver-next`, you CAN'T use legacy versions of resolvers directly:
module.exports = {
  settings: {
    // THIS WON'T WORK, the resolver interface required for `import-x/resolver-next` is different.
    'import-x/resolver-next': [
       require('eslint-import-resolver-node'),
       require('eslint-import-resolver-webpack'),
       require('some-custom-resolver')
    ];
  }
}
```

For easier migration, the PR also introduces a compat utility `importXResolverCompat` that you can use in your `eslint.config.js`:

```js
// eslint.config.js
import eslintPluginImportX, { importXResolverCompat } from 'eslint-plugin-import-x';
// or
const eslintPluginImportX = require('eslint-plugin-import-x');
const { importXResolverCompat } = eslintPluginImportX;

module.exports = {
  settings: {
    // THIS WILL WORK as you have wrapped the previous version of resolvers with the `importXResolverCompat`
    'import-x/resolver-next': [
       importXResolverCompat(require('eslint-import-resolver-node'), nodeResolveOptions),
       importXResolverCompat(require('eslint-import-resolver-webpack'), webpackResolveOptions),
       importXResolverCompat(require('some-custom-resolver'), { option1: true, option2: '' })
    ];
  }
}
```

##### For custom import resolver developers

This is the new API design of the resolver interface:

```ts
export interface NewResolver {
  interfaceVersion: 3;
  name?: string; // This will be included in the debug log
  resolve: (modulePath: string, sourceFile: string) => ResolvedResult;
}

// The `ResultNotFound` (returned when not resolved) is the same, no changes
export interface ResultNotFound {
  found: false;
  path?: undefined;
}

// The `ResultFound` (returned resolve result) is also the same, no changes
export interface ResultFound {
  found: true;
  path: string | null;
}

export type ResolvedResult = ResultNotFound | ResultFound;
```

You will be able to import `NewResolver` from `eslint-plugin-import-x/types`.

The most notable change is that `eslint-plugin-import-x` no longer passes the third argument (`options`) to the `resolve` function.

We encourage custom resolvers' authors to consume the options outside the actual `resolve` function implementation. You can export a factory function to accept the options, this factory function will then be called inside the `eslint.config.js` to get the actual resolver:

```js
// custom-resolver.js
exports.createCustomResolver = (options) => {
  // The options are consumed outside the `resolve` function.
  const resolverInstance = new ResolverFactory(options);

  return {
    name: 'custom-resolver',
    interfaceVersion: 3,
    resolve(mod, source) {
      const found = resolverInstance.resolve(mod, {});

      // Of course, you still have access to the `options` variable here inside
      // the `resolve` function. That's the power of JavaScript Closures~
    }
  }
};

// eslint.config.js
const { createCustomResolver } = require('custom-resolver')

module.exports = {
  settings: {
    'import-x/resolver-next': [
       createCustomResolver(options)
    ];
  }
}
```

This allows you to create a reusable resolver instance to improve the performance. With the existing version of the resolver interface, because the options are passed to the `resolver` function, you will have to create a resolver instance every time the `resolve` function is called:

```js
module.exports = {
  interfaceVersion: 2,
  resolve(mod, source) {
    // every time the `resolve` function is called, a new instance is created
    // This is very slow
    const resolverInstance = ResolverFactory.createResolver({});
    const found = resolverInstance.resolve(mod, {});
  },
};
```

With the factory function pattern, you can create a resolver instance beforehand:

```js
exports.createCustomResolver = (options) => {
  // `enhance-resolve` allows you to create a reusable instance:
  const resolverInstance = ResolverFactory.createResolver({});
  const resolverInstance = enhanceResolve.create({});

  // `oxc-resolver` also allows you to create a reusable instance:
  const resolverInstance = new ResolverFactory({});

  return {
    name: "custom-resolver",
    interfaceVersion: 3,
    resolve(mod, source) {
      // the same re-usable instance is shared across `resolve` invocations.
      // more performant
      const found = resolverInstance.resolve(mod, {});
    },
  };
};
```

##### Patch Changes

-   [#184](un-ts/eslint-plugin-import-x#184) [`bc4de89`](un-ts/eslint-plugin-import-x@bc4de89) Thanks [@marcalexiei](https://github.com/marcalexiei)! - fix(no-cycle): improves the type declaration of the rule `no-cycle`’s `maxDepth` option

-   [#184](un-ts/eslint-plugin-import-x#184) [`bc4de89`](un-ts/eslint-plugin-import-x@bc4de89) Thanks [@marcalexiei](https://github.com/marcalexiei)! - fix(first): improves the type declaration of the rule `first`'s option

-   [#184](un-ts/eslint-plugin-import-x#184) [`bc4de89`](un-ts/eslint-plugin-import-x@bc4de89) Thanks [@marcalexiei](https://github.com/marcalexiei)! - fix(no-unused-modules): improves the type declaration of the rule `no-unused-modules`’s `missingExports` option

-   [#184](un-ts/eslint-plugin-import-x#184) [`bc4de89`](un-ts/eslint-plugin-import-x@bc4de89) Thanks [@marcalexiei](https://github.com/marcalexiei)! - fix(no-deprecated): improve error message when no description is available
@SukkaW
Copy link
Collaborator

SukkaW commented Dec 11, 2024

I am closing the issue now. [email protected] has introduced the new resolver interface. See changelog for 4.5.0 and the relatedPR.

The popular eslint import resolvers adoption process is tracked here: #192 (comment).

@SukkaW SukkaW closed this as completed Dec 11, 2024
@SukkaW SukkaW unpinned this issue Dec 11, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

9 participants