rfcs
rfcs copied to clipboard
feat: Add support for TypeScript config files
Summary
Add support for TypeScript config files (eslint.config.ts
, eslint.config.mts
, eslint.config.cts
).
Related Issues
This PR is related to this RFC.
Worth linking the prior art in this area: https://github.com/eslint/rfcs/pull/50
I think it makes sense and is low cost to support this in a runtime that supports ts natively. Like deno and bun, we can just import them.
@bradzacher Yes thank you!!!
Which version of TypeScript can the config file be written in? With what tsconfig settings?
Does this mean eslint would need to depend on typescript, causing it to be installed for non-TS users? if not, then would eslint be able to consume typescript in pnpm and yarn pnp in order to transpile the eslint config?
if not, then would eslint be able to consume typescript in pnpm and yarn pnp in order to transpile the eslint config?
It's possible to declare an implicit, optional peer dependency in a way that both yarn and pnpm will respect.
{
"peerDependenciesMeta": {
"typescript": {
"optional": true
}
},
}
You don't even need to declare an explicit peer dependency with this config as it implicitly declares a dep on typescript: "*"
.
For context this is how the @typescript-eslint
packages depend on TypeScript.
@ljharb
Which version of TypeScript can the config file be written in?
We are going to be using jiti
to transpile the TypeScript config files. It has been used by frameworks such as Nuxt, Tailwind CSS and Docusaurus for this exact purpose. As far as I'm aware of, the only limitation it has, is that it doesn't yet support top-level await. The latest TypeScript syntax that I can think of that would also be relevant to our situation is the satisfies
operator which I have made sure jiti
has no problem with as I used it recently to do the exact same thing with size-limit
.
With what tsconfig settings?
As far as I know, jiti
does not care about tsconfig
settings. It doesn't work like ts-node
. I believe it uses babel
internally to do the transpilling and it provides an interopDefault
option to allow for using either export default
and module.exports
which in this situation would be very helpful as users will not have to worry about ESM/CJS compatibility issues.
Does this mean eslint would need to depend on typescript, causing it to be installed for non-TS users? if not, then would eslint be able to consume typescript in pnpm and yarn pnp in order to transpile the eslint config?
I think jiti
will have to become either a dependency or an optional dependency. And we could make TypeScript
an optional peer dependency as to not force the non-TS users to install it.
Thanks for this RFC @aryaemami59! Could it be that Jiti doesn't transpile top-level await
expressions properly? For example, the following config is throwing an error for me:
// eslint.config.mts
const { default: globals } = await import('globals');
export default {
languageOptions: {
globals: globals.node
},
rules: {
'no-undef': 'error'
}
};
SyntaxError: await is only valid in async functions and the top level bodies of modules
Would it be possible to add a note to the RFC to state this limitation - or if I'm overlooking something, explain how to make top-level await
work?
It would be good to have this noted either way to make sure that we don't forget to add unit tests during the implementation.
Thanks for this RFC @aryaemami59! Could it be that Jiti doesn't transpile top-level
await
expressions properly? Would it be possible to add a note to the RFC to state this limitation - or if I'm overlooking something, explain how to make top-levelawait
work?
@fasttime Yeah I mentioned it in this comment but I can go ahead and include it in the RFC as well. To my knowledge top level awaits are pretty much the only limitation jiti
has. The library author did hint at a jiti.import
method but it doesn't seem to be ready yet.
@fasttime Yeah I mentioned it in this comment but I can go ahead and include it in the RFC as well. To my knowledge top level awaits are pretty much the only limitation
jiti
has. The library author did hint at ajiti.import
method but it doesn't seem to be ready yet.
Yes, the lack of support for top-level await
should be included in the RFC since it is an important limitation. If the RFC is accepted, we will also need to include it as a note in the documentation, along with other inconsistencies we may find while adding unit tests.
I agree. As a user, I wouldn't want a TS compiler installed in non-TS projects, and I wouldn't want a second TS compiler installed if I already have one installed.
As a reference in postcss-load-config
, tsx
and jiti
are optional peer dependencies so it's up to the user to provide:
https://github.com/postcss/postcss-load-config/blob/62d325c7c51fa536c433b2a2517e59d8f1ed101d/src/req.js#L18-L57
@privatenumber I think that's good feedback. We could definitely specify TS-related stuff as an optional dependency and ask folks to manually install.
Seems like a good call to require opt-in to these dependencies. Some runtimes like bun and deno support typescript out of the box so won't need this compiler. I think it's also likely that Node.js will gain native typescript support eventually, see https://github.com/nodejs/node/issues/43816.
Some runtimes like bun and deno support typescript out of the box so won't need this compiler.
That's true. In those runtimes the .ts
file would just be loaded without any other dependencies.
This is consistent with what I mentioned in https://github.com/eslint/rfcs/pull/117#issuecomment-1987209637. We can check the runtime and import it directly, otherwise keep looking for the ts loader
In the case of using alternative runtimes that support TS by default (Bun, Deno), which solution will prevail between:
- the native support of the runtime
- the solution implemented in ESLint to support
eslint.config.ts
Or is it possible that the implementation of this support could conflict with these runtimes and prevent their use or impair their proper functioning?
@Bluzzi I guess we could try catch
native importing a tiny ts file to see if it's supported? Ideally it should work with ts-node
and tsx
's CLI where they registered global loader of Node as well - I will try to put up a tiny library for that maybe
I'm really glad this is getting the traction it needs, I was gone for less than a week, and already we have a counter proposal! Now here is something we could do. I'm actually really liking what @antfu has done with eslint-ts-patch
by adding support for all the TS loaders we've discussed so far. So what we can do (or rather what I'm probably going to do) is create a giant testing matrix to figure out which one is going to cover the most ground.
Keep in mind when I made the initial proposal tsx
did not yet have a tsImport
API. So jiti
seemed like it would be more suited for something like this. Though I still think jiti
is probably the way to go, @antfu is correct in that because jiti
handles everything synchronously (uses babel
to transpile to CJS), enabling support for top-level-await
is going to be difficult. Another approach we could consider is what postcss-load-config
has done with peerDependencies
like @privatenumber previously mentioned. And while bundle-require
does write tmp
files to disk, it uses esbuild
which is quite fast and efficient. So it's still a viable option. Not to mention bundle-require
is written by @egoist who is also the author of tsup
which is somewhat relevant to what we're trying to do here.
I ended up creating a wrapper library importx
, which supports all the solutions we mention and will smartly decide one based on the user env or allow manual swap the loader if one doesn't work out. In runtime that supports importing TS directly (Deno/Bun), it will use native imports for the best performance (as @Bluzzi mentioned). Also has a matrix here
If we don't want to be coupled with one solution and its limitation, I'd say we could probably use importx
to manage the underlying implementation and swallow the complexity, so ESLint no longer needs to concern it.
@antfu I like it! 👍
importx looks good. But based on the discussion above we seem to prefer letting the user install the optional loader themselves.
Yes, we could make importx
optional and let users install it explicitly. Same as how I implement and documented as https://github.com/eslint/eslint/pull/18440
Just to reiterate: temp files are not something we can consider. We need to think not just about people using the CLI, but also people who are using the API and how may be doing so in an environment where writes are not allowed.
It's important to note that if a TS-specific tool is a runtime dep, then it's extra weight for non-TS users; and if it's a peer dep, even an optional one, some users may be forced to install it (not every tool considers optionality); and if it's an optional dep, it will always be installed (but if it fails to compile it won't fail the overarching install); and if it's none of those, then it's a hidden dep that users won't know is needed until runtime.
In terms of maintainability for eslint, and correctness for users, an optional peer dep is probably the best bet - but, note that adding peer deps, even optional ones, is a breaking change.
It's important to note that if a TS-specific tool is a runtime dep, then it's extra weight for non-TS users
While Jiti would add a little under two MB for users, this is a significantly valuable feature, and adopting it in a way that is a breaking change would mean that it'd have to wait until ESLint 10. If ESLint 9's timeline was not an outlier, that will be quite a long time for users to wait. It may be preferable to add it as a dependency now, and make it a peer dependency for the next major version, so users will only be waiting for that potential 2 MB savings.
And, either way, I was under the impression that node_modules size is irrelevant.
@miscellaneo i don't think your snark is helpful or in line with the CoC.
In general it's more that I don't want the ick of TS-related things in a project that doesn't suffer the burden of using TS, but I tried to express that in a way that wouldn't trigger those who love TS.
even an optional one, some users may be forced to install it (not every tool considers optionality)
Which package managers don't honor peerDependenciesMeta.<dep>.optional
? I think at least npm honors it.
And yes, i agree it must remain optional, keep eslint slim please.
peerDependenciesMeta.<dep>.optional
is supported by these package managers:
npm: https://docs.npmjs.com/cli/v10/configuring-npm/package-json#peerdependenciesmeta pnpm: https://pnpm.io/package_json#peerdependenciesmeta yarn: https://yarnpkg.com/configuration/manifest#peerDependenciesMeta.optional
Personally, I think this is enough to move forward without a breaking change. But to be extra safe, I guess we should also investigate which versions of these package managers support for optional
was added in, and see if those versions are still maintained / using a version of Node that's in LTS.
@privatenumber it's still always a breaking change, because someone who has a version installed that doesn't satisfy the optional range will still get a failed install. Restating, adding a peer dep is always a breaking change, without exception, optional or not, forever.
doesn't satisfy the optional range will still get a failed install
@ljharb won't all the package managers either log a warning if it isn't satisfied (not fail the build) or just install the peer dep as specified?
I didn't realise that one of them hard failed if the peer dep wasn't satisfied.
If you wanted to avoid any logging etc whilst still satisfying pnp et al constraints then you can just not list the peer dep and only list the optional peer dep.
As I mentioned above - that's what we do for the TS dep for ts-eslint. We did that because of the install logs we caused for users who installed us as a transient dependency (specifically for create-react-app
which always added our packages even if the user didn't need it).
They should all fail - a peer del requirement is hoisted and necessary. Optional means it can be absent, not that it can be out of range.