Open lifeiscontent opened 3 years ago
The "recommended" config of course doesn't support any typescript functionality - the default is (should always be) JavaScript.
https://github.com/benmosher/eslint-plugin-import#typescript may be helpful.
@ljharb Yeah, I'm specifically talking about the typescript additions.
@lifeiscontent - try using: https://www.npmjs.com/package/eslint-import-resolver-typescript
@ljharb - would there be any appetite for import/extensions
rule to support a typescript option which would allow .ts
and .tsx
to be equivalent to a .js
extension which the Typescript compiler allows us to use as mentioned by @lifeiscontent
// foo.ts
export const foo = 'foo';
// bar.ts
import { foo } from './foo.js';
Currently this errors with Missing file extension "ts" for "./foo.js"
. Maybe we could add an additional option? I'm happy to take a stab at it if it's something that was welcomed?
I don’t think that would really make sense. It would be modifying a general rule with typescript-specific logic to paper over a typescript bug/flaw - one that once they fix, we’d be stuck supporting the workaround for forever.
@ljharb - sorry, do you mind explaining what the Typescript bug/flaw is? Currently I'm struggling to be able to enforce .js
extensions in a project using Typescript using this rule (which will go on to be published as a ESM package)
@leepowelldev my understanding is that typescript explicitly forces you to omit extensions, so that it can conceptually point to .ts
at dev time but .js
at runtime. TS and native ESM are effectively not yet compatible, and can't be used together.
@ljharb Typescript doesn't explicitly force you to omit extensions. It won't let you use .ts
ot .tsx
as an extension, but .js
is perfectly valid (https://github.com/microsoft/TypeScript/issues/16577). This allows me to create valid ESM output from a Typescript source.
TS and native ESM are effectively not yet compatible, and can't be used together.
I'm not sure what you mean by this.
I mean that if your goal is a native ESM package (something i'd strongly discourage for many reasons, unrelated to this), then authoring in TypeScript is not yet an ideal way to get there, because of node's unfortunate "required extensions for ESM" choice.
What I'd suggest is to only use tsc as a typechecker, to use babel as your only transpiler, and then to use a babel plugin to transform the proper omitted-extensions source to whatever output you like, which can include "native ESM with extensions".
I'm migrating a large project written in TypeScript to output native ESM modules for Node. This requires all the import
statements in TypeScript to end in .js
so they get output with the .js
extensions and I would like to validate that behavior.
I don't think TypeScript is going to budge on their treatment of extensions, see https://github.com/microsoft/TypeScript/issues/16577#issuecomment-754941937, neither will Node JS back off of required extensions for ESM so it is perfectly valid to write this:
// foo.ts
export const foo = 'foo';
// bar.ts
import { foo } from './foo.js';
Currently this config produces Missing file extension "ts" for "./foo.js"
which is illogical since TypeScript doesn't actually support .ts
on file paths https://github.com/microsoft/TypeScript/issues/37582.
module.exports = {
root: true,
parser: "@typescript-eslint/parser",
parserOptions: {
ecmaVersion: 2018,
sourceType: "module"
},
plugins: ["@typescript-eslint", "import"],
extends: ["plugin:import/recommended", "plugin:import/typescript"],
rules: {
"import/extensions": ["error", "ignorePackages"]
},
settings: {
"import/extensions": [".js", ".jsx"],
"import/parsers": {
"@typescript-eslint/parser": [".ts", ".tsx"]
},
"import/resolver": {
typescript: {
project: "./"
}
}
}
};
Would a compromise maybe be to introduce a validExentions
option so we could say that .js
is a valid extension in .ts
files?
There is also this issue ineslint-import-resolver-typescript
https://github.com/alexgorbatchev/eslint-import-resolver-typescript/issues/80 that mentions this exact use case.
I think the proper place to do that is the TS resolver. It can, for example, resolve otherwise-missing .js
files to their present .ts
equivalent.
Looking at the src for extensions
rule I believe this plugin would still error even if anything was done in the resolver.
I have to disagree that supporting this feature should be done at the resolver level.
That suggests to me that the extensions rule needs refactoring, if it’s hardcoding node resolution.
I'm not sure about that ... I think it resolves just fine (would need to check), but then simply check that the file extensions match. Obviously it fails as expected when it tries to match .js
with .ts
or .tsx
. We would need additional checks to allow this to be valid.
I just opened https://github.com/alexgorbatchev/eslint-import-resolver-typescript/issues/82 so we can see what might be able to be done in the resolver. Although I do agree with @leepowelldev that it makes more sense to handle this behavior in the rules not the resolver.
This behavior is really specific to TypeScript though so would this be best as a separate rule just for TypeScript so it doesn't conflict?
We don’t have any typescript-specific rules; this is a JavaScript plugin that works for TS as well.
Sure, and I don't think this has to be typescript specific if we can expose a config option to support alternative relationships between extensions.
"import/extensions": [
<severity>,
"never" | "always" | "ignorePackages",
{
ignorePackages: true | false,
pattern: {
<extension>: "never" | "always" | "ignorePackages"
},
mapExtensions: {
"js": "tsx?"
}
}
]
This gives the flexibility without directly supporting Typescript.
This is actually already supported at https://github.com/alexgorbatchev/eslint-import-resolver-typescript/pull/56.
Just remove "import/extensions" this setting.
This resolver resolves the .ts file correctly, but it's not allowed in your configuration.
@ljharb The problem is there is no way to enforce to use .js
extension currently for .ts
files.
and overrides that only apply to .ts files, that set the extension to be “always”, presumably would force .ts, and you want .js?
and overrides that only apply to .ts files, that set the extension to be “always”, presumably would force .ts, and you want .js?
Yep.
Thanks to @JounQin I was able to get this working by excluding eslint-import-resolver-typescript
which seems a but counter initiative but it is working. If anyone wants to take a look at the minimal reproduction I made I can push it up to GitHub.
// src/bar.ts - should pass
import { foo } from "./foo.js";
// src/baz.ts - should fail
import { foo } from "./foo";
// src/foo.ts
export const foo = "foo";
eslint-import-resolver-typescript
- both bar.ts
and baz.ts
fail{
"root": true,
"parser": "@typescript-eslint/parser",
"parserOptions": {
"ecmaVersion": 12,
"sourceType": "module"
},
"plugins": [
"import"
],
"rules": {
"import/extensions": ["error", "ignorePackages"]
},
"settings": {
"import/parsers": {
"@typescript-eslint/parser": [
".ts",
".tsx"
]
},
"import/resolver": {
"typescript": {
"project": "./tsconfig.json"
}
}
}
}
eslint-import-resolver-typescript
- works as intended{
"root": true,
"parser": "@typescript-eslint/parser",
"parserOptions": {
"ecmaVersion": 12,
"sourceType": "module"
},
"plugins": [
"import"
],
"rules": {
"import/extensions": ["error", "ignorePackages"]
},
"settings": {
"import/parsers": {
"@typescript-eslint/parser": [
".ts",
".tsx"
]
}
}
}
@patrickarlt so if we just don't use eslint-import-resolver-typescript
then this works?
@leepowelldev It works unless you also enable the import/no-unresolved
rule. In my quest to make a VERY minimal reproduction I didn't extend plugin:import/recommended
and plugin:import/typescript
. So if you don't use eslint-import-resolver-typescript
and keep import/no-unresolved
disabled it works which I don't really think is intended. I think this is what was discussed in https://github.com/import-js/eslint-plugin-import/issues/2111#issuecomment-916212917. The resolver works but the import/extensions
rule doesn't think the extension is correct.
My minimal reproduction of this is at https://github.com/patrickarlt/minimal-typescript-eslint-with-extensions.
import/no-unresolved
without eslint-import-resolver-typescript
breaks resolving (predictable){
"root": true,
"parser": "@typescript-eslint/parser",
"parserOptions": {
"ecmaVersion": 12,
"sourceType": "module"
},
"plugins": [
"import"
],
"extends": [
"plugin:import/recommended",
"plugin:import/typescript"
],
"rules": {
"import/extensions": ["error", "ignorePackages"]
},
"settings": {
"import/parsers": {
"@typescript-eslint/parser": [
".ts",
".tsx"
]
}
}
}
import/no-unresolved
+ eslint-import-resolver-typescript
breaks (incorrect ./foo.js
is valid TypeScript){
"root": true,
"parser": "@typescript-eslint/parser",
"parserOptions": {
"ecmaVersion": 12,
"sourceType": "module"
},
"plugins": [
"import"
],
"extends": [
"plugin:import/recommended",
"plugin:import/typescript"
],
"rules": {
"import/extensions": ["error", "ignorePackages"]
},
"settings": {
"import/parsers": {
"@typescript-eslint/parser": [
".ts",
".tsx"
]
},
"import/resolver": {
"typescript": {
}
}
}
}
Unfortunately no-unresolved
is necessary to detect missing untyped packages, but perhaps adding an option to no-unresolved
might be the solution here?
@ljharb I think the behavior everyone is asking for is this:
Using this config:
{
"root": true,
"parser": "@typescript-eslint/parser",
"parserOptions": {
"ecmaVersion": 12,
"sourceType": "module"
},
"plugins": [
"import"
],
"extends": [
"plugin:import/recommended",
"plugin:import/typescript"
],
"rules": {
"import/extensions": ["error", "ignorePackages"]
},
"settings": {
"import/parsers": {
"@typescript-eslint/parser": [
".ts",
".tsx"
]
},
"import/resolver": {
"typescript": {
}
}
}
}
And these files
// src/bar.ts - should pass
import { foo } from "./foo.js";
// src/baz.ts - should fail
import { foo } from "./foo";
// src/foo.ts
export const foo = "foo";
The expected output should be:
Currently it is:
This is because import { foo } from "./foo.js";
is valid TypeScript that compiles to import { foo } from "./foo.js";
If you want the extensions to output from the TypeScript compiler you must add them in the source.
It looks like eslint-import-resolver-typescript
resolves to the correct file but imports/extensions
rejects that you can import .js
from a .ts
file.
@patrickarlt Try to add ts: never, tsx: never
in the rule configuration like the PR's example.
@JounQin unfortunately that just seem to make everything pass.
{
"root": true,
"parser": "@typescript-eslint/parser",
"parserOptions": {
"ecmaVersion": 12,
"sourceType": "module"
},
"plugins": [
"import"
],
"extends": [
"plugin:import/recommended",
"plugin:import/typescript"
],
"rules": {
"import/extensions": [
"error",
"always",
{
"ts": "never",
"tsx": "never"
}
]
},
"settings": {
"import/parsers": {
"@typescript-eslint/parser": [
".ts",
".tsx"
]
},
"import/resolver": {
"typescript": {
}
}
}
}
@patrickarlt
I've already said previously.
@ljharb The problem is there is no way to enforce to use
.js
extension currently for.ts
files.
Any movement on this? It would be great if we can get the extension linting working correctly for ESM in TS as well as the no-unresolved
.
I'm experiencing exactly the same issues as described by @patrickarlt. I need the resolver for no-unresolved
to work, but as soon as I enable the resolver, the extensions
check gives me false negatives. Disabling the no-unresolved
is not really a solution and the extensions
check would add a lot of value!
TS recently merged a PR which might address this problem in TS itself.
@ljharb can you elaborate on that please? which PR and how does it solve the problem? The request here is for a linting rule, we already have the ability to import TS files using a .js
extension for ESM build outputs. The linting rule would ensure that the extension isn't forgotten
I mean https://github.com/microsoft/TypeScript/pull/45884 - which I believe will allow TS to do the right thing here, so that your imports in code don't have to have extensions (since source code shouldn't be using them) and they'll still transpile/resolve to the proper extensioned path.
In other words, a solution here would be a hack around a TypeScript flaw; TS seems on track to fix that flaw, obviating the need for any workaround.
From my understanding, the extension is mandatory in both Node and the Browser https://nodejs.org/api/esm.html#esm_mandatory_file_extensions
It's only bundling systems like babel/webpack that have added the feature of omitting the extension.
TS is not automating this extension as it's an assumption of the extension. TS won't modify import statements at compile as it risks changing the behaviour of the code.
Consider the following:
// file.ts
import x from './x';
Given
somedir/
file.ts
x.mjs
x.d.ts
x.ts
x.tsx
So I wouldn't call it a "hack". I don't see them changing this behaviour.
It would make everyone happy to be able to lint against this extension being present in relative imports.
Your understanding is incorrect; the browser has no concept of extensions whatsoever, and the extension in node is only mandatory for relative imports - imports from packages should omit them by setting up extensionless entry points in their “exports” field.
Perhaps I misunderstood this statement about mandatory extensions in native modules. I took "native" here to mean native in the browser.
It states clearly that the server would need to support the mime type, but the browser will infer the content type from the extension.
That is explicitly saying that browsers provide no "lookup" - browsers in NO WAY have the concept of extensions, and the MIME type is only and always explicitly defined by the server and/or by the HTML tag that loaded it.
Not quite sure where this conversation is going. Personally I don't believe the upcoming TS release will address this issue. @ljharb obviously advocates using extensionless imports, while a number of others want to use them (for whatever personal reasons they may have).
@ljharb obviously advocates using extensionless imports
Wait, what? Extensionless imports only work for bare module and explicit declarations in package.json exports
field. Browser argument is rather a little beside the point. Yes, browser does not care for extension per-se but if the module is /foo/bar.js
then it cannot be imported as /foo/bar
.
So unless TS has the option to output explicit extensions in imported paths we are forced to make sure all our TS modules are imported as *.js
@tpluscode - I completely agree, but I think @ljharb made the argument that the .js
extension should be added as part of the build process, either by a babel plugin or as subsequent step to the typescript compiler.
That's not so simple. An import from './foo/bar'
can mean both ./foo/bar.js
as well as ./foo/bar/index.js
and both can exist on disk. Unless the import module is explicit, trying to defer "fixing" paths to the build is bound to be error prone. This is exactly where a import linter seems to fit right in
This plugin should certainly disambiguate those possibilities, but that actually happens very rarely in practice.
TypeScript will not modify the import extension at compile time. They have made this clear in multiple (very long) threads.
https://github.com/microsoft/TypeScript/issues/16577#issuecomment-754941937
@beamery-tomht they already do that with .ts
to .js
on the file; they just don't want to modify your source. So, if your source remains extensionless, then https://github.com/microsoft/TypeScript/pull/45774 (merged in https://github.com/microsoft/TypeScript/pull/45884) i believe should work, unless i'm misunderstanding something.
They aren't modifying the extension at compile time. They are supporting the use of extensions in your source code. When the source is extensionless, the dist is also extensionless, which is invalid ESM.
That is explicitly saying that browsers provide no "lookup"
It's explicitly stating that the extension cannot be omitted in browsers for ESM.
No, it is not. It's saying that the browser will ask the webserver for the exact URL in the source code, whether it has an extension or not, and that it's up to the webserver to handle it. It's exceedingly easy - and in fact common - for webservers to do extension lookup, and certainly to do index
lookup.
Browsers do not have any idea of what an extension is - the word is meaningless in a browser context.
they already do that with
.ts
to.js
on the file; they just don't want to modify your source. So, if your source remains extensionless, then microsoft/TypeScript#45774 (merged in microsoft/TypeScript#45884) i believe should work, unless i'm misunderstanding something.
This is part of the problem - TS doesn’t allow the use of .ts and .tsx extensions in source code. So you either have to be extensionless or use .js
@leepowelldev ok so, to be clear, the problem (assuming the latest TS changes I referenced) is:
.js
or .mjs
or .cjs
, i presume?), but the import specifiers remain extensionless. This works fine with this plugin, and every ESM use case except "native ESM in node" or "native ESM in a browser, and without an import map that maps the extensionless specifier to the desired URL", the latter of which is trivial to fix and thus I don't consider remotely important. The former, however, is important to fix - but, this plugin does not support native node ESM, until resolve
supports exports (node itself doesn't even provide a non-experimental way to resolve ESM specifiers, so it's just not possible for most tooling to support native ESM yet).js
in your source. TS produces output files with a .js
extension. Everything works fine in every ESM use case - you'd still want an import map in the browser - but this plugin will report errors whenever those output files do not exist on disk.Is that accurate?
based off the conversation here: https://github.com/microsoft/TypeScript/issues/16577
if a
.ts
or.tsx
file imports a.js
file it will resolve.ts
.tsx
or.js
but theplugin:import/typescript
doesn't seem to have support for this functionality.the
js
extension is used to reference files that WILL be compiled to, so they technically don't exist, but this is how the TS team is allowing people to write ESM compatible modules in TypeScript.It would be nice to have this eslint plugin to work with this out of the box so I can enforce that all of my modules import
.js
files to make sure I'm not missing anything when exporting my package to the web.