Skip to content

docs: blog post on parserOptions.projectService #8031

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

Draft
wants to merge 12 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 18 additions & 2 deletions docs/packages/Parser.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -285,6 +285,8 @@ For an option that allows linting files outside of your TSConfig file(s), see [`
Specifies using TypeScript APIs to generate type information for rules.
It will automatically detect the TSConfig for each file (like `project: true`), and will also allow type information to be computed for JavaScript files without the `allowJs` compiler option (unlike `project: true`).

See [Typed Linting with `parserOptions.projectService`](/blog/parser-options-project-service) for more context.

<Tabs groupId="eslint-config">
<TabItem value="Flat Config">

Expand Down Expand Up @@ -321,7 +323,6 @@ This option brings two main benefits over the older `project`:

- Simpler configurations: most projects shouldn't need to explicitly configure `project` paths or create `tsconfig.eslint.json`s
- Improved performance: this API is optimized on the TypeScript side for speed
- Initial versions of this option demonstrated performance changes in subsets of the typescript-eslint monorepo ranging from 11% slower to 70% faster

For more information, see:

Expand All @@ -333,11 +334,12 @@ For more information, see:
The behavior of `parserOptions.projectService` can be customized by setting it to an object.

```js
module.exports = {
{
parser: '@typescript-eslint/parser',
parserOptions: {
projectService: {
allowDefaultProject: ['*.js'],
defaultProject: "tsconfig.json"
},
},
};
Expand All @@ -346,10 +348,24 @@ module.exports = {
##### `allowDefaultProject`

Globs of files to allow running with the default project compiler options despite not being matched by the project service.
It takes in an array of string paths that will be resolved relative to the [`tsconfigRootDir`](#tsconfigrootdir).
When set, [`projectService.defaultProject`](#defaultproject) must be set as well.

This is intended to produce type information for in root-level config files such as `eslint.config.js` that aren't included in their sibling `tsconfig.json`.
Every file with type information retrieved from the default project incurs a non-trivial performance overhead to linting.
Use this option sparingly.

There are several restrictions on this option to prevent it from being overused:

- `**` is not allowed in globs passed to it
- Files that match `allowDefaultProject` may not also be included in their nearest `tsconfig.json`

##### `defaultProject`

Path to a TSConfig to use instead of TypeScript's default project configuration.
It takes in an array of string paths that will be resolved relative to the [`tsconfigRootDir`](#tsconfigrootdir).

This is required to specify which TSConfig file on disk will be used for [`projectService.allowDefaultProject`](#allowdefaultproject).

##### `maximumDefaultProjectFileMatchCount_THIS_WILL_SLOW_DOWN_LINTING`

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ describe('createProjectService', () => {
createProjectService(
{
allowDefaultProject: ['file.js'],
defaultProject: './tsconfig.json',
defaultProject: 'tsconfig.json',
},
undefined,
),
Expand All @@ -86,7 +86,7 @@ describe('createProjectService', () => {
createProjectService(
{
allowDefaultProject: ['file.js'],
defaultProject: './tsconfig.json',
defaultProject: 'tsconfig.json',
},
undefined,
),
Expand All @@ -102,7 +102,7 @@ describe('createProjectService', () => {
const { service } = createProjectService(
{
allowDefaultProject: ['file.js'],
defaultProject: './tsconfig.json',
defaultProject: 'tsconfig.json',
},
undefined,
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ authors:
name: Josh Goldberg
title: typescript-eslint Maintainer
url: https://github.com/JoshuaKGoldberg
description: Simplifying how many projects resolve their
description: Simplifying how many projects resolve their TSConfigs for typed linting.
slug: parser-options-project-true
tags: [parser, parser options, project, tsconfig]
title: Relative TSConfig Projects with `parserOptions.project = true`
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -133,7 +133,7 @@ export default tseslint.config(
// Added lines start
projectService: {
allowDefaultProject: ['*.js'],
defaultProject: './tsconfig.json',
defaultProject: 'tsconfig.json',
},
// Added lines end
tsconfigRootDir: import.meta.dirname,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,7 @@ export default tseslint.config(
// Added lines start
projectService: {
allowDefaultProject: ['*.js'],
defaultProject: './tsconfig.json',
defaultProject: 'tsconfig.json',
},
// Added lines end
tsconfigRootDir: import.meta.dirname,
Expand Down
266 changes: 266 additions & 0 deletions packages/website/blog/2024-08-25-parser-options-project-service.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,266 @@
---
authors:
- image_url: https://www.joshuakgoldberg.com/img/josh.jpg
name: Josh Goldberg
title: typescript-eslint Maintainer
url: https://github.com/JoshuaKGoldberg
description: Using a faster and more convenient "Project Service" API for configuring typed linting.
slug: parser-options-project-service
tags: [parser, parser options, project, project service, tsconfig]
title: Typed Linting with `parserOptions.projectService`
---

import Tabs from '@theme/Tabs';
import TabItem from '@theme/TabItem';

["Typed linting"](/linting/typed-linting), or enabling ESLint rules to tap into the power of the TypeScript type checker, is one of the best parts of typescript-eslint.
It enables a slew of more powerful lint rules that check for nuanced bugs, best practice violations, and other code issues.
But typed linting hasn't always been straightforward to configure or performant at runtime.

With typescript-eslint 8.0, we marked as stable a **`parserOptions.projectService`** option that uses more powerful TypeScript APIs than previous typed linting implementations.
We've found it to bring the following benefits:

- ✍️ **Configuration**: simpler ESLint configs for typed linting and no ESLint-specific TSConfig file
- 🧠 **Predictability**: uses the same type information services as editors, including more reliability
- ⚡️ **Speed**: faster linting times out-of-the-box both in CLIs and in editors in many cases

This blog post will cover how `parserOptions.projectService` simplifies configurations and brings linting type information much closer to what editors such as VS Code run with.

<!-- truncate -->

## Introducing the Project Service

Back in [Relative TSConfig Projects with `parserOptions.project = true` > Project Services](2023-09-18-parser-options-project-true.md#project-services), we'd mentioned a replacement for `parserOptions.project`:

> The downside of having users specify `parserOptions.project` at all is that `@typescript-eslint/parser` needs manual logic to create TypeScript Programs and associate them with linted files.
> Manual Program creation logic comes with a few issues:
>
> - Complex project setups can be difficult to get right.
> - For example, [typescript-eslint does not yet support Project References](https://github.com/typescript-eslint/typescript-eslint/issues/2094).
> - The TypeScript compiler options used in the user's editor might differ from the compiler options in the TSConfigs they specified on disk.
> - Files not included in created Programs can't be linted with type information, even though editors still typically surface type information when editing those files.
> - Most commonly, `.eslintrc.(c)js` files can be tricky to lint, resulting in the dreaded [_TSConfig does not include this file_ error](/linting/troubleshooting#i-get-errors-telling-me-eslint-was-configured-to-run--however-that-tsconfig-does-not--none-of-those-tsconfigs-include-this-file).
>
> We're working on an option to instead call the same TypeScript "Project Service" APIs that editors such as VS Code use to create Programs for us instead.
> Project Services will automatically detect the TSConfig for each file (like `project: true`), and will also allow type information to be computed for JavaScript files without the `allowJs` compiler option (unlike `project: true`).

Following a year of discussion and testing, we believe the new Project Service API is ready to be used by real-world projects.
We've found it to be generally faster at runtime and more straightforward to configure.

We're therefore promoting the `parserOptions.EXPERIMENTAL_useProjectService` option to the stable name **`parserOptions.projectService`** in typescript-eslint v8.

## ✍️ Onboarding to the Project Service

You can change over to the new Project Service API by replacing `project` with `projectService` in your ESLint configuration:

<Tabs groupId="eslint-config">
<TabItem value="Flat Config">

```js title="eslint.config.js"
export default tseslint.config({
// ...
languageOptions: {
parserOptions: {
// Remove this line
project: true,
// Add this line
projectService: true,
tsconfigRootDir: import.meta.dirname,
},
},
// ...
});
```

:::note
[`import.meta.dirname`](https://nodejs.org/api/esm.html#importmetadirname) is only present for ESM files in Node.js >=20.11.0 / >= 21.2.0.<br />
For CommonJS modules and/or older versions of Node.js, [use `__dirname` or an alternative](https://stackoverflow.com/questions/46745014/alternative-for-dirname-in-node-js-when-using-es6-modules).
:::

</TabItem>
<TabItem value="Legacy Config">

```js title=".eslintrc.cjs"
module.exports = {
// ...
parser: '@typescript-eslint/parser',
parserOptions: {
// Remove this line
project: true,
// Add this line
projectService: true,
tsconfigRootDir: __dirname,
},
// ...
};
```

</TabItem>
</Tabs>

Other settings, including how you run ESLint and configure rules, should work the same.

See [Packages > Parser > `projectService`](/packages/parser#projectservice) for more details.

### Including Additional Files

One long-standing pain point of typed linting is enabling type information for files not included in the project's `tsconfig.json`.
Common solutions in the traditional Program API were to either skip type checking for those files or to create a separate `tsconfig.eslint.json` that enabled `compilerOptions.allowJs = true`.

Now, the new Project Service API allows for a configuration object specifying:

- `allowDefaultProjectForFiles`: a glob of "out-of-project" files to lint with type information
- `defaultProject`: path to a TSConfig to use for out-of-project file type information

For example, the following config solves the common case of projects that have root-level files like `eslint.config.js` and `vitest.config.ts`:

<Tabs groupId="eslint-config">
<TabItem value="Flat Config">

```js title="eslint.config.js"
export default tseslint.config({
// ...
languageOptions: {
parserOptions: {
projectService: {
allowDefaultProjectForFiles: ['*.js'],
defaultProject: 'tsconfig.json',
},
tsconfigRootDir: import.meta.dirname,
},
},
// ...
});
```

</TabItem>
<TabItem value="Legacy Config">

```js title=".eslintrc.cjs"
module.exports = {
// ...
parser: '@typescript-eslint/parser',
parserOptions: {
projectService: {
allowDefaultProjectForFiles: ['*.js'],
defaultProject: 'tsconfig.json',
},
tsconfigRootDir: __dirname,
},
// ...
};
```

</TabItem>
</Tabs>

:::tip
This means most projects should be able to remove lint-only `tsconfig.eslint.json` files!
🥳
:::

See [Packages > Parser > `projectService` > `ProjectServiceOptions`](/packages/parser#projectserviceOptions) for more details.

## 🧠 Predictability and Reliability

One of the challenges of typed linting is using TypeScript APIs designed for TypeScript projects in ESLint's runtime.
[ESLint does not provide parsers session information](https://github.com/eslint/rfcs/pull/102).
As a result, `@typescript-eslint/parser` has to use approximations to guess whether it was in the more performance-friendly "single-run" mode (rather than in ESLint's multi-pass `--fix` mode or a long-lived editor session).
Single-run mode comes with several bugs.
For example:

- ESLint's `--fix` mode breaks type information after the first run ([#9577](https://github.com/typescript-eslint/typescript-eslint/pull/9577))
- Extra file extensions, such as those used by `.svelte` and `.vue`, are not supported ([#9504](https://github.com/typescript-eslint/typescript-eslint/issues/9504))

typescript-eslint's single-run inference enables uses common heuristics such as checking for `'--fix'` in `process.argv`, the presence of `process.env.CI`, and the presence of `parserOptions.extraFileExtensions`.
Copy link

Choose a reason for hiding this comment

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

nit: typo, I made a guess at the intended wording:

Suggested change
typescript-eslint's single-run inference enables uses common heuristics such as checking for `'--fix'` in `process.argv`, the presence of `process.env.CI`, and the presence of `parserOptions.extraFileExtensions`.
typescript-eslint's single-run inference uses common heuristics such as checking for `'--fix'` in `process.argv`, the presence of `process.env.CI`, and the presence of `parserOptions.extraFileExtensions`.

It can be disabled with [`parserOptions.disallowAutomaticSingleRunInference`](/packages/parser#disallowautomaticsingleruninference).

Enabling single-run mode generally improves performance by 10-20%.
typescript-eslint@v8 enables inference of single-run mode by default.
If your project is stuck on `parserOptions.project`, we recommend keeping single-run inference on if possible.

`parserOptions.projectService` does not suffer from the same bugs as `parserOptions.project` with single-run mode.
It supports extra file extensions out-of-the-box and does not slow down when used with ESLint's `--fix`.

We recommend switching to `parserOptions.projectService` if possible.

## ⚡️ Performance Comparisons

In addition to simplifying configuration, we have also found the new Project Service API to result in _roughly equivalent or faster_ lint times compared to the equivalent traditional program APIs.

To prove this, we measured the average of 10 lint times on three real repositories:

- [Babel](https://github.com/babel/babel):
- [create-t3-app](https://github.com/t3-oss/create-t3-app):
- [tRPC](https://github.com/trpc/trpc):

> TODO: Reproduce these findings locally!

<table>
<thead>
<tr>
<th>Repository</th>
<th>📜 Traditional Projects</th>
<th>🆕 Project Service</th>
<th>𝚫 Delta</th>
<th>Source</th>
</tr>
</thead>
<tbody>
<tr>
<th>Babel</th>
<td>~38 seconds</td>
<td>~36 seconds</td>
<td>~5% faster</td>
<td>[^1]</td>
</tr>
<tr>
<th>create-t3-app</th>
<td>6.662 s ± 0.235 s</td>
<td>5.344 s ± 0.126 s</td>
<td>20% faster</td>
<td>[^2]</td>
</tr>
<tr>
<th>tRPC</th>
<td></td>
<td></td>
<td></td>
<td>[^3]</td>
</tr>
</tbody>
</table>

In summary, we've found linting _some_ large real-world repositories **can be 5% to of 20% faster**. 🚀

### Performance Caveats

Note that using Project Service APIs for linting is still a relatively new use case.
Neither TypeScript nor typescript-eslint has had much time to optimize internally for the new usage.
Some repositories structured differently from the ones we've tested on may be slower.

See [typescript-eslint/performance](https://github.com/typescript-eslint/performance) for details on cases where the project service is slower than traditional projects.

## Next Steps

### Giving Feedback

We'd love to hear from you on how this option works for you.
Does it live up to what we've promised, and/or does it have bugs we haven't fixed yet?
Please do post in the [Community Feedback: Project Service APIs](https://github.com/typescript-eslint/typescript-eslint/discussions/8030) GitHub Discussion on how it goes for you.

For support in setting up the new APIs, feel free to ask on [the typescript-eslint Discord](https://discord.gg/FSxKq8Tdyg)'s `#project-service` channel.
We'd be happy to help you try out `parserOptions.projectService`.

### Long Term Vision

Our hope is that the Project Service API becomes the standard way to work with typed linting over the next few major versions.
Our priority will be to improve the new Project Service API so that it works in all places the traditional project program behavior does.
We won't remove the traditional project program behavior unless and until the new Project Service API is able to fully replace it.

So, please, try out the new Project Service API.
It should help make your typed linting faster and more straightforward to configure. 💜

[^1]: https://github.com/babel/babel/pull/16192#issue-2054613116

[^2]: https://github.com/t3-oss/create-t3-app/pull/1936/#discussion_r1667389041
5 changes: 4 additions & 1 deletion packages/website/docusaurus.config.mts
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,11 @@ const remarkPlugins: MDXPlugin[] = [[npm2yarnPlugin, { sync: true }]];

const githubUrl = 'https://github.com/typescript-eslint/typescript-eslint';

const beforeDefaultRemarkPlugins = [generatedRuleDocs];

const presetClassicOptions: PresetClassicOptions = {
blog: {
beforeDefaultRemarkPlugins,
blogSidebarCount: 'ALL',
// Allow Docusaurus TOC remark plugin to pick up the injected H2
beforeDefaultRemarkPlugins: [blogFooter],
Expand All @@ -30,7 +33,7 @@ const presetClassicOptions: PresetClassicOptions = {
sidebarPath: require.resolve('./sidebars/sidebar.rules.js'),
routeBasePath: 'rules',
editUrl: `${githubUrl}/edit/main/packages/website/`,
beforeDefaultRemarkPlugins: [generatedRuleDocs],
beforeDefaultRemarkPlugins: beforeDefaultRemarkPlugins,
remarkPlugins,
exclude: ['TEMPLATE.md'],
breadcrumbs: false,
Expand Down
Loading