Skip to content

fix(typescript-estree): only run projectService setHostConfiguration when needed #9336

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

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
5a12641
perf(typescript-estree): moves setHostConfiguration into createProjec…
higherorderfunctor Jun 11, 2024
8ef28ae
fix(typescript-estree): fixes extraFileExtensions to allow updates
higherorderfunctor Jun 13, 2024
14261ca
refactor(typescript-estree): revert some changes to createParseSettings
higherorderfunctor Jun 14, 2024
3dff7e0
docs(typescript-estree): adds performance information to the document…
higherorderfunctor Jun 14, 2024
85ce13d
docs(cspell): filter words
higherorderfunctor Jun 14, 2024
dd99286
refactor(typescript-estree): fixes test to show host config not updat…
higherorderfunctor Jun 14, 2024
8487e9d
refactor(typescript-estree): fixes broken test
higherorderfunctor Jun 14, 2024
2092269
refactor(typescript-estree): logs before/after updating extraFileExte…
higherorderfunctor Jun 14, 2024
ae73208
Merge branch 'v8' into perf/9312-set-host-config-once-per-project-ser…
higherorderfunctor Jun 17, 2024
bfc80e6
test(typescript-estree): [ProjectService] adds additional extraFileEx…
higherorderfunctor Jun 17, 2024
cf09e1e
refactor(typescript-estree): simplifies last set extraFileExtensions …
higherorderfunctor Jun 17, 2024
113441d
docs: adds additional performance details for extraFileExtensions wit…
higherorderfunctor Jun 17, 2024
1b9887f
docs: fixes typo
higherorderfunctor Jun 17, 2024
559d8ea
docs: adds additional clarifying details to performance with extraFil…
higherorderfunctor Jun 17, 2024
5f83cc3
docs: updates sections that refer to performance with extraFileExtens…
higherorderfunctor Jun 17, 2024
ec9d09b
docs: updates docs with review feedback
higherorderfunctor Jun 20, 2024
aa142c4
docs: reorders content related to this issue in the performance section
higherorderfunctor Jun 20, 2024
fa15073
docs: accepts review suggestion in docs/troubleshooting/Performance.mdx
higherorderfunctor Jun 22, 2024
1771c27
docs: accepts review suggestion in docs/troubleshooting/Performance.mdx
higherorderfunctor Jun 22, 2024
63992dc
docs: accepts review suggestion in docs/troubleshooting/Performance.mdx
higherorderfunctor Jun 22, 2024
ddb970c
docs: accepts review suggestion in docs/troubleshooting/Performance.mdx
higherorderfunctor Jun 22, 2024
eaaee4f
docs: accepts review suggestion in docs/troubleshooting/Performance.mdx
higherorderfunctor Jun 22, 2024
e081759
docs: fixes broken link and linting errors
higherorderfunctor Jun 22, 2024
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
2 changes: 2 additions & 0 deletions .cspell.json
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@
"esquery",
"esrecurse",
"estree",
"extrafileextensions",
"falsiness",
"globby",
"IDE's",
Expand Down Expand Up @@ -120,6 +121,7 @@
"preact",
"Premade",
"prettier's",
"projectservice",
"quasis",
"Quickstart",
"recurse",
Expand Down
4 changes: 4 additions & 0 deletions docs/packages/Parser.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,10 @@ This option allows you to provide one or more additional file extensions which s
The default extensions are `['.js', '.mjs', '.cjs', '.jsx', '.ts', '.mts', '.cts', '.tsx']`.
Add extensions starting with `.`, followed by the file extension. E.g. for a `.vue` file use `"extraFileExtensions": [".vue"]`.

:::note
See [Changes to `extraFileExtensions` with `projectService`](../troubleshooting/Performance.mdx#changes-to-extrafileextensions-with-projectservice) to avoid performance issues.
:::

### `jsDocParsingMode`

> Default if `parserOptions.project` is set, then `'all'`, otherwise `'none'`
Expand Down
2 changes: 2 additions & 0 deletions docs/packages/TypeScript_ESTree.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -203,6 +203,8 @@ interface ParseAndGenerateServicesOptions extends ParseOptions {
/**
* When `project` is provided, this controls the non-standard file extensions which will be parsed.
* It accepts an array of file extensions, each preceded by a `.`.
*
* NOTE: When used with {@link projectService}, full project reloads may occur.
*/
extraFileExtensions?: string[];

Expand Down
4 changes: 4 additions & 0 deletions docs/troubleshooting/FAQ.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -243,6 +243,10 @@ We don't recommend using `--cache`.

You can use `parserOptions.extraFileExtensions` to specify an array of non-TypeScript extensions to allow, for example:

:::note
See [Changes to `extraFileExtensions` with `projectService`](../troubleshooting/Performance.mdx#changes-to-extrafileextensions-with-projectservice) to avoid performance issues.
:::

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

Expand Down
100 changes: 100 additions & 0 deletions docs/troubleshooting/Performance.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,106 @@ module.exports = {

See [Glob pattern in parser's option "project" slows down linting](https://github.com/typescript-eslint/typescript-eslint/issues/2611) for more details.

## Changes to `extraFileExtensions` with `projectService`

Using a different [`extraFileExtensions`](../packages/Parser.mdx#extrafileextensions) between files in the same project with
the [`projectService`](../packages/Parser.mdx#projectservice) option may cause performance degradations.
For every file linted, we update the `projectService` whenever `extraFileExtensions` changes.
This causes the underlying TypeScript server to perform a full project reload.

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

```js title="eslint.config.js"
// @ts-check

import tseslint from 'typescript-eslint';
import vueParser from 'vue-eslint-parser';

// Add this line
const extraFileExtensions = ['.vue'];
export default [
{
files: ['*.ts'],
languageOptions: {
parser: tseslint.parser,
parserOptions: {
projectService: true,
// Add this line
extraFileExtensions,
},
},
},
{
files: ['*.vue'],
languageOptions: {
parser: vueParser,
parserOptions: {
projectService: true,
parser: tseslint.parser,
// Remove this line
extraFileExtensions: ['.vue'],
// Add this line
extraFileExtensions,
},
},
},
];
```

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

```js title=".eslintrc.js"
// Add this line
const extraFileExtensions = ['.vue'];
module.exports = {
files: ['*.ts'],
parser: '@typescript-eslint/parser',
parserOptions: {
projectService: true,
// Add this line
extraFileExtensions,
},
overrides: [
{
files: ['*.vue'],
parser: 'vue-eslint-parser',
parserOptions: {
parser: '@typescript-eslint/parser',
projectService: true,
// Remove this line
extraFileExtensions: ['.vue'],
// Add this line
extraFileExtensions,
},
},
],
};
```

</TabItem>
</Tabs>

Project reloads can be observed using the [debug environment variable](../packages/typescript-estree/#debugging): `DEBUG='typescript-eslint:typescript-estree:*'`.

```
typescript-estree:useProgramFromProjectService Updating extra file extensions: before=[]: after=[ '.vue' ]
typescript-estree:tsserver:info reload projects.
typescript-estree:useProgramFromProjectService Extra file extensions updated: [ '.vue' ]
...
typescript-estree:useProgramFromProjectService Updating extra file extensions: before=[ '.vue' ]: after=[]
typescript-estree:tsserver:info reload projects.
typescript-estree:useProgramFromProjectService Extra file extensions updated: []
...
typescript-estree:tsserver:info Scheduled: /path/to/tsconfig.src.json, Cancelled earlier one +0ms
typescript-estree:tsserver:info Scheduled: *ensureProjectForOpenFiles*, Cancelled earlier one +0ms
...
typescript-estree:useProgramFromProjectService Updating extra file extensions: before=[]: after=[ '.vue' ]
typescript-estree:tsserver:info reload projects.
typescript-estree:useProgramFromProjectService Extra file extensions updated: [ '.vue' ]
```

## The `indent` / `@typescript-eslint/indent` rules

This rule helps ensure your codebase follows a consistent indentation pattern.
Expand Down
2 changes: 2 additions & 0 deletions packages/typescript-estree/src/parser-options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,8 @@ interface ParseAndGenerateServicesOptions extends ParseOptions {
/**
* When `project` is provided, this controls the non-standard file extensions which will be parsed.
* It accepts an array of file extensions, each preceded by a `.`.
*
* NOTE: When used with {@link projectService}, full project reloads may occur.
*/
extraFileExtensions?: string[];

Expand Down
46 changes: 34 additions & 12 deletions packages/typescript-estree/src/useProgramFromProjectService.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import path from 'node:path';
import util from 'node:util';

import debug from 'debug';
import { minimatch } from 'minimatch';
import path from 'path';
import { ScriptKind } from 'typescript';
import * as ts from 'typescript';

import { createProjectProgram } from './create-program/createProjectProgram';
import type { ProjectServiceSettings } from './create-program/createProjectService';
Expand All @@ -13,6 +15,33 @@ const log = debug(
'typescript-eslint:typescript-estree:useProgramFromProjectService',
);

const serviceFileExtensions = new WeakMap<ts.server.ProjectService, string[]>();

const updateExtraFileExtensions = (
service: ts.server.ProjectService,
extraFileExtensions: string[],
): void => {
const currentServiceFileExtensions = serviceFileExtensions.get(service) ?? [];
if (
!util.isDeepStrictEqual(currentServiceFileExtensions, extraFileExtensions)
) {
log(
'Updating extra file extensions: before=%s: after=%s',
currentServiceFileExtensions,
extraFileExtensions,
);
service.setHostConfiguration({
extraFileExtensions: extraFileExtensions.map(extension => ({
extension,
isMixedContent: false,
scriptKind: ts.ScriptKind.Deferred,
})),
});
serviceFileExtensions.set(service, extraFileExtensions);
log('Extra file extensions updated: %o', extraFileExtensions);
}
};

export function useProgramFromProjectService(
{
allowDefaultProject,
Expand All @@ -23,6 +52,9 @@ export function useProgramFromProjectService(
hasFullTypeInformation: boolean,
defaultProjectMatchedFiles: Set<string>,
): ASTAndDefiniteProgram | undefined {
// NOTE: triggers a full project reload when changes are detected
updateExtraFileExtensions(service, parseSettings.extraFileExtensions);

// We don't canonicalize the filename because it caused a performance regression.
// See https://github.com/typescript-eslint/typescript-eslint/issues/8519
const filePathAbsolute = absolutify(parseSettings.filePath);
Expand All @@ -32,16 +64,6 @@ export function useProgramFromProjectService(
filePathAbsolute,
);

if (parseSettings.extraFileExtensions.length) {
service.setHostConfiguration({
extraFileExtensions: parseSettings.extraFileExtensions.map(extension => ({
extension,
isMixedContent: false,
scriptKind: ScriptKind.Deferred,
})),
});
}

const opened = service.openClientFile(
filePathAbsolute,
parseSettings.codeFullText,
Expand Down
Loading
Loading