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

feat: add enforceEsmExtensions option to import/extensions rule #2701

Closed
wants to merge 12 commits into from

Conversation

SgtPooki
Copy link

@SgtPooki SgtPooki commented Feb 1, 2023

This rule can help you use the proper full path to an import, with extension. Supports --fix.

note: No tests have been added by me so anyone willing to help add those, please feel free. I won't be able to work on tests for this PR for a bit

You can see this rule in effect (via patch-package) in the commit ipfs/ipfs-webui@5aa8035

Copy link
Member

@ljharb ljharb left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

With native ESM in node, only relative paths should use extensions, as you indicated.

Why does this need a separate rule instead of being an option (or a configuration) on import/extensions?

@SgtPooki
Copy link
Author

SgtPooki commented Feb 2, 2023

Why does this need a separate rule instead of being an option (or a configuration) on import/extensions?

I guess it doesn't. I can look into moving it over to a configuration on import/extensions

@SgtPooki
Copy link
Author

SgtPooki commented Feb 8, 2023

@ljharb updated. lmk if this is reasonable. we could update the rule config to accept an array of replaceable extensions instead of hardcoding them. I will do that later if reasonable.

@SgtPooki SgtPooki changed the title feat: add import/esm-extensions rule feat: add enforceEsmExtensions option to import/extensions rule Feb 8, 2023
@SgtPooki SgtPooki requested a review from ljharb February 8, 2023 18:38
This rule provides additional options for configuration:

- `ignorePackages` follows the same logic as setting the string value of the rule config to `ignorePackages`. Useful when using `always` as the string config.
- `enforceEsmExtensions` will flag any relative import paths that do not resolve to a file. (supports --fix)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maybe instead of hardcoding ESM here (or java-ly capitalizing it "Esm"), this could be an option that takes always or never, and solely applies to relative paths?

Meaning, relativePaths: "always" would be the node native ESM approach (since that and Deno are the only environments where extensions are required for native ESM - browsers don't, and any transpiled ESM shouldn't).

Copy link
Author

@SgtPooki SgtPooki Feb 8, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

java-ly..

hah.. "ESME xtensions" isn't any better ;)

yea it should only apply to relative paths. I am fairly certain that I plugged it into the existing rule where that is true but let me know if I missed something.

Since we're already talking about extensions an option with extensions in the name is superfluous. I'm fine with 'relativePaths', but also thought about using 'fullySpecified' similar to webpack's resolver naming: https://webpack.js.org/configuration/module/#resolvefullyspecified

@@ -75,7 +75,7 @@ This plugin intends to support linting of ES2015+ (ES6+) import/export syntax, a
| [consistent-type-specifier-style](docs/rules/consistent-type-specifier-style.md) | Enforce or ban the use of inline type-only markers for named imports. | | | | 🔧 | | |
| [dynamic-import-chunkname](docs/rules/dynamic-import-chunkname.md) | Enforce a leading comment with the webpackChunkName for dynamic imports. | | | | | | |
| [exports-last](docs/rules/exports-last.md) | Ensure all exports appear after other statements. | | | | | | |
| [extensions](docs/rules/extensions.md) | Ensure consistent use of file extension within the import path. | | | | | | |
| [extensions](docs/rules/extensions.md) | Ensure consistent use of file extension within the import path. | | | | 🔧 | 💡 | |
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if we're going to make it autofixable, let's make sure it can autofix everything, not just this new option?

it might be nice to have a separate PR that makes the rule autofixable?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What I saw from adding the tests for this option is that all the "valid" usecases where imports are fixed will need updating, so the PR could quickly become very large.

I would much rather open a separate PR for autofixing everything.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

to be fair though, many of the failures don't have valid fixes. i.e. import not found... how do we want to handle those? remove the import?

src/rules/extensions.js Outdated Show resolved Hide resolved
src/rules/extensions.js Outdated Show resolved Hide resolved
Comment on lines +195 to +197

const esmExtensions = ['.js', '.ts', '.mjs', '.cjs'];
esmExtensions.push(...esmExtensions.map((ext) => `/index${ext}`));
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
const esmExtensions = ['.js', '.ts', '.mjs', '.cjs'];
esmExtensions.push(...esmExtensions.map((ext) => `/index${ext}`));
const esmExtensions = [].concat(
'.js',
'.mjs',
'.cjs',
esmExtensions.map((ext) => `/index${ext}`)
);

the .ts extension should definitely not be here since it's not ESM.

also, the .js extension isn't always ESM, only when type: module is present, so this might need to be more complex.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Your code isn't valid, but I can remove the 'ts' extension

w.r.t typescript extensions, here's the gotcha: in Typescript packages, you have to import foo from './foo.js' instead of import foo from './foo.ts'. These extensions are the extensions to check to validate that the path is real, but it should still be replacing a valid .ts with .js. Currently, the rule will actually set the import to '.ts' instead of '.js'. Which needs fixed...

For this option to be robust in the current landscape, we should really accept a mapping of expected fileExtensions, and the extension to use in the source. something like:

{
"import/extensions": ["error", "always", { 
    "esm": {
        "extensionOverride": { 
            "ts": "js", 
            "tsx": "jsx"
        }, 
        "extensions": ["js", "ts", "cjs", "mjs"]
    }}]
}

where extensions without overrides are replaced with themselves. What do you think about this.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

also, the .js extension isn't always ESM, only when type: module is present, so this might need to be more complex.

I think for this rule, we can assume when someone enables it that they are using ESM for their non cjs files. Would a comment in the doc cover this usecase?

maybe changing the name to 'fullyResolved' would help communicate more accurately what it's fixing

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

relevant issue: microsoft/TypeScript#49083 (comment)

allowing overrides would let the consumers leave 'ts' replacements as 'ts' (deno migrations) or swapping ts for js (esm migration in typescript project)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what is invalid about that code? it works perfectly fine.

I understand about the TS-specific quirks but that should come from the TS resolver and not be hardcoded into a rule that might not be running on TS at all.

I do not think we should ever assume that someone is using type:module, which is harmful and should be avoided anyways (altho i believe TS's implementation requires it, not everyone uses TS).

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

rofl ok fair enough, good catch. in that case, let's keep it as is, but still without TS.

Suggested change
const esmExtensions = ['.js', '.ts', '.mjs', '.cjs'];
esmExtensions.push(...esmExtensions.map((ext) => `/index${ext}`));
const esmExtensions = ['.js', '.mjs', '.cjs'];
esmExtensions.push(...esmExtensions.map((ext) => `/index${ext}`));

however we'll still need to handle that .cjs isn't ESM, and .js is only ESM in type:module.

Unless this is "extensions that might exist in ESM files", and then i'm not sure why it wouldn't include wasm and json?

Copy link
Author

@SgtPooki SgtPooki Feb 16, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right, thats why im thinking we should change the rule to "fullyResolved" and then allow users to indicate the extensions they care about, as well as whatever mapping they need.

For my usecase (typescript) i have to check for the existence of both .js and .ts, and replace both with .js

What do you think of that?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

tbh this really seems like a uniquely typescript use case, and it probably should live in the TS eslint plugin (cc @bradzacher).

Copy link
Contributor

@bradzacher bradzacher Feb 17, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The philosophy of TS is and always has been that you write the import paths that you want at runtime. TS purposely does not transform import paths at all - it will always emit the exact same string after transpilation.

In fact currently it is an error to attempt to import a file using .ts because as far as TS was concerned - there's no "TS runtime", so importing a .ts file makes no sense! EG nodejs can't parse or understand TS, so importing a .ts file will crash things.

Because of this, when they added ESM support they added module resolution logic in the typechecker so that when TS sees ./foo.[cm]?jsx?, it will look for the file ./foo.[cm]?tsx? and will use that.

TS 5.0 includes a flag which makes it so that import './foo.ts' will no longer error for environments where they do understand TS syntax (eg deno or some bundlers). In this instance it will mean that TS will look for that exact file+extension.

To summarise:

  • ./foo will cause TS to do the standard node resolution logic, preferring ts extensions
    • eg it'll look for ./foo.ts, then ./foo.tsx, then ./foo.js, then ./foo/index.ts, etc
  • ./foo.[cm]?jsx? will cause TS to first look for the corresponding ./foo.[cm]?tsx? file first, then look for the originally specified file.
  • ./foo.[cm]?tsx? will cause TS to look for that exact file (with the correct config)

I think this is the priority order of how TS looks up extensions in the ambiguous cases:
https://github.com/microsoft/TypeScript/blob/ddfec78f55bfedd690708c429f803346555e31c4/src/compiler/utilities.ts#L9233-L9234

.d.ts, .d.mts, .d.cts, .mjs, .mts, .cjs, .cts, .ts, .js, .tsx, .jsx, .json

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@bradzacher ok, so i'm not really sure what that means here.

It really seems like it should be an option to the TS resolver, not to any particular rule, but it's pretty complex :-)

SgtPooki and others added 2 commits February 8, 2023 11:59
Co-authored-by: Jordan Harband <ljharb@gmail.com>
Co-authored-by: Jordan Harband <ljharb@gmail.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Development

Successfully merging this pull request may close these issues.

3 participants