From 5a12641f8f6f69cc3899a01f59a892053f146de4 Mon Sep 17 00:00:00 2001 From: Christopher Aubut Date: Tue, 11 Jun 2024 15:24:06 -0600 Subject: [PATCH 01/22] perf(typescript-estree): moves setHostConfiguration into createProjectService Calling on every source causes a full graph update. Relocating to call once per ProjectService. --- .../create-program/createProjectService.ts | 17 +++++++ .../src/parseSettings/createParseSettings.ts | 12 +++-- .../tests/lib/createProjectService.test.ts | 43 +++++++++++++++++ .../lib/useProgramFromProjectService.test.ts | 46 ------------------- 4 files changed, 67 insertions(+), 51 deletions(-) diff --git a/packages/typescript-estree/src/create-program/createProjectService.ts b/packages/typescript-estree/src/create-program/createProjectService.ts index 9f0f44dcec68..751581ca161f 100644 --- a/packages/typescript-estree/src/create-program/createProjectService.ts +++ b/packages/typescript-estree/src/create-program/createProjectService.ts @@ -22,9 +22,14 @@ export interface ProjectServiceSettings { service: TypeScriptProjectService; } +export interface ProjectServiceParseSettings { + extraFileExtensions?: string[]; +} + export function createProjectService( optionsRaw: boolean | ProjectServiceOptions | undefined, jsDocParsingMode: ts.JSDocParsingMode | undefined, + parseSettings?: ProjectServiceParseSettings, ): ProjectServiceSettings { const options = typeof optionsRaw === 'object' ? optionsRaw : {}; validateDefaultProjectForFilesGlob(options); @@ -95,6 +100,18 @@ export function createProjectService( ); } + if (parseSettings?.extraFileExtensions?.length) { + service.setHostConfiguration({ + extraFileExtensions: parseSettings.extraFileExtensions.map( + extension => ({ + extension, + isMixedContent: false, + scriptKind: tsserver.ScriptKind.Deferred, + }), + ), + }); + } + service.setCompilerOptionsForInferredProjects( ( configRead.config as { diff --git a/packages/typescript-estree/src/parseSettings/createParseSettings.ts b/packages/typescript-estree/src/parseSettings/createParseSettings.ts index 98e42d8af36c..ff6537780f02 100644 --- a/packages/typescript-estree/src/parseSettings/createParseSettings.ts +++ b/packages/typescript-estree/src/parseSettings/createParseSettings.ts @@ -61,6 +61,11 @@ export function createParseSettings( return JSDocParsingMode.ParseAll; } })(); + const extraFileExtensions = + Array.isArray(tsestreeOptions.extraFileExtensions) && + tsestreeOptions.extraFileExtensions.every(ext => typeof ext === 'string') + ? tsestreeOptions.extraFileExtensions + : []; const parseSettings: MutableParseSettings = { allowInvalidAST: tsestreeOptions.allowInvalidAST === true, @@ -76,11 +81,7 @@ export function createParseSettings( : new Set(), errorOnTypeScriptSyntacticAndSemanticIssues: false, errorOnUnknownASTType: tsestreeOptions.errorOnUnknownASTType === true, - extraFileExtensions: - Array.isArray(tsestreeOptions.extraFileExtensions) && - tsestreeOptions.extraFileExtensions.every(ext => typeof ext === 'string') - ? tsestreeOptions.extraFileExtensions - : [], + extraFileExtensions, filePath: ensureAbsolutePath( typeof tsestreeOptions.filePath === 'string' && tsestreeOptions.filePath !== '' @@ -110,6 +111,7 @@ export function createParseSettings( ? (TSSERVER_PROJECT_SERVICE ??= createProjectService( tsestreeOptions.projectService, jsDocParsingMode, + { extraFileExtensions }, )) : undefined, range: tsestreeOptions.range === true, diff --git a/packages/typescript-estree/tests/lib/createProjectService.test.ts b/packages/typescript-estree/tests/lib/createProjectService.test.ts index 1159e8625f41..68d1f6ca703f 100644 --- a/packages/typescript-estree/tests/lib/createProjectService.test.ts +++ b/packages/typescript-estree/tests/lib/createProjectService.test.ts @@ -4,6 +4,7 @@ import { createProjectService } from '../../src/create-program/createProjectServ const mockReadConfigFile = jest.fn(); const mockSetCompilerOptionsForInferredProjects = jest.fn(); +const mockSetHostConfiguration = jest.fn(); jest.mock('typescript/lib/tsserverlibrary', () => ({ ...jest.requireActual('typescript/lib/tsserverlibrary'), @@ -12,6 +13,7 @@ jest.mock('typescript/lib/tsserverlibrary', () => ({ ProjectService: class { setCompilerOptionsForInferredProjects = mockSetCompilerOptionsForInferredProjects; + setHostConfiguration = mockSetHostConfiguration; }, }, })); @@ -89,4 +91,45 @@ describe('createProjectService', () => { compilerOptions, ); }); + + it('does not call setHostConfiguration if extraFileExtensions are not provided', () => { + const compilerOptions = { strict: true }; + mockReadConfigFile.mockReturnValue({ config: { compilerOptions } }); + + const { service } = createProjectService( + { + allowDefaultProject: ['file.js'], + defaultProject: './tsconfig.json', + }, + undefined, + ); + + expect(service.setHostConfiguration).not.toHaveBeenCalled(); + }); + + it('calls setHostConfiguration when extraFileExtensions is provided', () => { + const compilerOptions = { strict: true }; + mockReadConfigFile.mockReturnValue({ config: { compilerOptions } }); + + const { service } = createProjectService( + { + allowDefaultProject: ['file.js'], + defaultProject: './tsconfig.json', + }, + undefined, + { + extraFileExtensions: ['.vue'], + }, + ); + + expect(service.setHostConfiguration).toHaveBeenCalledWith({ + extraFileExtensions: [ + { + extension: '.vue', + isMixedContent: false, + scriptKind: ts.ScriptKind.Deferred, + }, + ], + }); + }); }); diff --git a/packages/typescript-estree/tests/lib/useProgramFromProjectService.test.ts b/packages/typescript-estree/tests/lib/useProgramFromProjectService.test.ts index 3ca8ae53aea4..958ab3e96240 100644 --- a/packages/typescript-estree/tests/lib/useProgramFromProjectService.test.ts +++ b/packages/typescript-estree/tests/lib/useProgramFromProjectService.test.ts @@ -1,6 +1,5 @@ /* eslint-disable @typescript-eslint/explicit-function-return-type -- Fancy mocks */ import path from 'path'; -import { ScriptKind } from 'typescript'; import type { ProjectServiceSettings, @@ -23,7 +22,6 @@ const currentDirectory = '/repos/repo'; function createMockProjectService() { const openClientFile = jest.fn(); - const setHostConfiguration = jest.fn(); const service = { getDefaultProjectForFile: () => ({ getLanguageService: () => ({ @@ -35,7 +33,6 @@ function createMockProjectService() { getCurrentDirectory: () => currentDirectory, }, openClientFile, - setHostConfiguration, }; return { @@ -301,47 +298,4 @@ If you absolutely need more files included, set parserOptions.projectService.max expect(actual).toBe(program); }); - - it('does not call setHostConfiguration if extraFileExtensions are not provided', () => { - const { service } = createMockProjectService(); - - useProgramFromProjectService( - createProjectServiceSettings({ - allowDefaultProject: [mockParseSettings.filePath], - service, - }), - mockParseSettings, - false, - new Set(), - ); - - expect(service.setHostConfiguration).not.toHaveBeenCalled(); - }); - - it('calls setHostConfiguration on the service to use extraFileExtensions when it is provided', () => { - const { service } = createMockProjectService(); - - useProgramFromProjectService( - createProjectServiceSettings({ - allowDefaultProject: [mockParseSettings.filePath], - service, - }), - { - ...mockParseSettings, - extraFileExtensions: ['.vue'], - }, - false, - new Set(), - ); - - expect(service.setHostConfiguration).toHaveBeenCalledWith({ - extraFileExtensions: [ - { - extension: '.vue', - isMixedContent: false, - scriptKind: ScriptKind.Deferred, - }, - ], - }); - }); }); From 8ef28ae922fa21b708a3ed7001b62419fa15e74a Mon Sep 17 00:00:00 2001 From: Christopher Aubut Date: Thu, 13 Jun 2024 17:19:44 -0600 Subject: [PATCH 02/22] fix(typescript-estree): fixes extraFileExtensions to allow updates --- .../create-program/createProjectService.ts | 17 --- .../src/parseSettings/createParseSettings.ts | 1 - .../src/useProgramFromProjectService.ts | 50 ++++++-- .../tests/lib/createProjectService.test.ts | 43 ------- .../lib/useProgramFromProjectService.test.ts | 118 ++++++++++++++++++ 5 files changed, 157 insertions(+), 72 deletions(-) diff --git a/packages/typescript-estree/src/create-program/createProjectService.ts b/packages/typescript-estree/src/create-program/createProjectService.ts index 751581ca161f..9f0f44dcec68 100644 --- a/packages/typescript-estree/src/create-program/createProjectService.ts +++ b/packages/typescript-estree/src/create-program/createProjectService.ts @@ -22,14 +22,9 @@ export interface ProjectServiceSettings { service: TypeScriptProjectService; } -export interface ProjectServiceParseSettings { - extraFileExtensions?: string[]; -} - export function createProjectService( optionsRaw: boolean | ProjectServiceOptions | undefined, jsDocParsingMode: ts.JSDocParsingMode | undefined, - parseSettings?: ProjectServiceParseSettings, ): ProjectServiceSettings { const options = typeof optionsRaw === 'object' ? optionsRaw : {}; validateDefaultProjectForFilesGlob(options); @@ -100,18 +95,6 @@ export function createProjectService( ); } - if (parseSettings?.extraFileExtensions?.length) { - service.setHostConfiguration({ - extraFileExtensions: parseSettings.extraFileExtensions.map( - extension => ({ - extension, - isMixedContent: false, - scriptKind: tsserver.ScriptKind.Deferred, - }), - ), - }); - } - service.setCompilerOptionsForInferredProjects( ( configRead.config as { diff --git a/packages/typescript-estree/src/parseSettings/createParseSettings.ts b/packages/typescript-estree/src/parseSettings/createParseSettings.ts index ff6537780f02..5eecd713626e 100644 --- a/packages/typescript-estree/src/parseSettings/createParseSettings.ts +++ b/packages/typescript-estree/src/parseSettings/createParseSettings.ts @@ -111,7 +111,6 @@ export function createParseSettings( ? (TSSERVER_PROJECT_SERVICE ??= createProjectService( tsestreeOptions.projectService, jsDocParsingMode, - { extraFileExtensions }, )) : undefined, range: tsestreeOptions.range === true, diff --git a/packages/typescript-estree/src/useProgramFromProjectService.ts b/packages/typescript-estree/src/useProgramFromProjectService.ts index b0606902b079..415ee5d10787 100644 --- a/packages/typescript-estree/src/useProgramFromProjectService.ts +++ b/packages/typescript-estree/src/useProgramFromProjectService.ts @@ -1,7 +1,7 @@ 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'; @@ -13,6 +13,41 @@ const log = debug( 'typescript-eslint:typescript-estree:useProgramFromProjectService', ); +const union = (self: Set, other: Set): Set => + new Set([...self, ...other]); +const difference = (self: Set, other: Set): Set => + new Set([...self].filter(elem => !other.has(elem))); +const symmetricDifference = (self: Set, other: Set): Set => + union(difference(self, other), difference(other, self)); + +const updateExtraFileExtensions = ( + service: ts.server.ProjectService & { + __extra_file_extensions?: Set; + }, + extraFileExtensions: string[], +): void => { + if (!service.__extra_file_extensions) { + service.__extra_file_extensions = new Set(); + } + if ( + symmetricDifference( + service.__extra_file_extensions, + new Set(extraFileExtensions), + ).size > 0 + ) { + service.__extra_file_extensions = new Set(extraFileExtensions); + log('Updating extra file extensions: %s', extraFileExtensions); + service.setHostConfiguration({ + extraFileExtensions: extraFileExtensions.map(extension => ({ + extension, + isMixedContent: false, + scriptKind: ts.ScriptKind.Deferred, + })), + }); + log('Extra file extensions updated: %o', service.__extra_file_extensions); + } +}; + export function useProgramFromProjectService( { allowDefaultProject, @@ -23,6 +58,9 @@ export function useProgramFromProjectService( hasFullTypeInformation: boolean, defaultProjectMatchedFiles: Set, ): 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); @@ -32,16 +70,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, diff --git a/packages/typescript-estree/tests/lib/createProjectService.test.ts b/packages/typescript-estree/tests/lib/createProjectService.test.ts index 68d1f6ca703f..1159e8625f41 100644 --- a/packages/typescript-estree/tests/lib/createProjectService.test.ts +++ b/packages/typescript-estree/tests/lib/createProjectService.test.ts @@ -4,7 +4,6 @@ import { createProjectService } from '../../src/create-program/createProjectServ const mockReadConfigFile = jest.fn(); const mockSetCompilerOptionsForInferredProjects = jest.fn(); -const mockSetHostConfiguration = jest.fn(); jest.mock('typescript/lib/tsserverlibrary', () => ({ ...jest.requireActual('typescript/lib/tsserverlibrary'), @@ -13,7 +12,6 @@ jest.mock('typescript/lib/tsserverlibrary', () => ({ ProjectService: class { setCompilerOptionsForInferredProjects = mockSetCompilerOptionsForInferredProjects; - setHostConfiguration = mockSetHostConfiguration; }, }, })); @@ -91,45 +89,4 @@ describe('createProjectService', () => { compilerOptions, ); }); - - it('does not call setHostConfiguration if extraFileExtensions are not provided', () => { - const compilerOptions = { strict: true }; - mockReadConfigFile.mockReturnValue({ config: { compilerOptions } }); - - const { service } = createProjectService( - { - allowDefaultProject: ['file.js'], - defaultProject: './tsconfig.json', - }, - undefined, - ); - - expect(service.setHostConfiguration).not.toHaveBeenCalled(); - }); - - it('calls setHostConfiguration when extraFileExtensions is provided', () => { - const compilerOptions = { strict: true }; - mockReadConfigFile.mockReturnValue({ config: { compilerOptions } }); - - const { service } = createProjectService( - { - allowDefaultProject: ['file.js'], - defaultProject: './tsconfig.json', - }, - undefined, - { - extraFileExtensions: ['.vue'], - }, - ); - - expect(service.setHostConfiguration).toHaveBeenCalledWith({ - extraFileExtensions: [ - { - extension: '.vue', - isMixedContent: false, - scriptKind: ts.ScriptKind.Deferred, - }, - ], - }); - }); }); diff --git a/packages/typescript-estree/tests/lib/useProgramFromProjectService.test.ts b/packages/typescript-estree/tests/lib/useProgramFromProjectService.test.ts index 958ab3e96240..94e054d2e864 100644 --- a/packages/typescript-estree/tests/lib/useProgramFromProjectService.test.ts +++ b/packages/typescript-estree/tests/lib/useProgramFromProjectService.test.ts @@ -1,5 +1,6 @@ /* eslint-disable @typescript-eslint/explicit-function-return-type -- Fancy mocks */ import path from 'path'; +import { ScriptKind } from 'typescript'; import type { ProjectServiceSettings, @@ -22,6 +23,7 @@ const currentDirectory = '/repos/repo'; function createMockProjectService() { const openClientFile = jest.fn(); + const setHostConfiguration = jest.fn(); const service = { getDefaultProjectForFile: () => ({ getLanguageService: () => ({ @@ -33,6 +35,7 @@ function createMockProjectService() { getCurrentDirectory: () => currentDirectory, }, openClientFile, + setHostConfiguration, }; return { @@ -298,4 +301,119 @@ If you absolutely need more files included, set parserOptions.projectService.max expect(actual).toBe(program); }); + + it('does not call setHostConfiguration if extraFileExtensions are not provided', () => { + const { service } = createMockProjectService(); + + useProgramFromProjectService( + createProjectServiceSettings({ + allowDefaultProject: [mockParseSettings.filePath], + service, + }), + mockParseSettings, + false, + new Set(), + ); + + expect(service.setHostConfiguration).not.toHaveBeenCalled(); + }); + + it('calls setHostConfiguration on the service to use extraFileExtensions when it is provided', () => { + const { service } = createMockProjectService(); + + useProgramFromProjectService( + createProjectServiceSettings({ + allowDefaultProject: [mockParseSettings.filePath], + service, + }), + { + ...mockParseSettings, + extraFileExtensions: ['.vue'], + }, + false, + new Set(), + ); + + expect(service.setHostConfiguration).toHaveBeenCalledWith({ + extraFileExtensions: [ + { + extension: '.vue', + isMixedContent: false, + scriptKind: ScriptKind.Deferred, + }, + ], + }); + }); + + it('does not call setHostConfiguration on the service to use extraFileExtensions when unchanged', () => { + const { service } = createMockProjectService(); + + useProgramFromProjectService( + createProjectServiceSettings({ + allowDefaultProject: [mockParseSettings.filePath], + service, + }), + { + ...mockParseSettings, + extraFileExtensions: ['.vue'], + }, + false, + new Set(), + ); + + expect(service.setHostConfiguration).toHaveBeenCalledTimes(1); + expect(service.setHostConfiguration).toHaveBeenCalledWith({ + extraFileExtensions: [ + { + extension: '.vue', + isMixedContent: false, + scriptKind: ScriptKind.Deferred, + }, + ], + }); + }); + + it('calls setHostConfiguration on the service to use extraFileExtensions when changed', () => { + const { service } = createMockProjectService(); + const settings = createProjectServiceSettings({ + allowDefaultProject: [mockParseSettings.filePath], + service, + }); + + useProgramFromProjectService( + settings, + { + ...mockParseSettings, + extraFileExtensions: ['.vue'], + }, + false, + new Set(), + ); + + expect(service.setHostConfiguration).toHaveBeenCalledTimes(1); + expect(service.setHostConfiguration).toHaveBeenCalledWith({ + extraFileExtensions: [ + { + extension: '.vue', + isMixedContent: false, + scriptKind: ScriptKind.Deferred, + }, + ], + }); + + useProgramFromProjectService( + settings, + { + ...mockParseSettings, + extraFileExtensions: [], + }, + false, + new Set(), + ); + + expect(service.setHostConfiguration).toHaveBeenCalledTimes(2); + expect(service.setHostConfiguration).toHaveBeenCalledWith({ + extraFileExtensions: [], + }); + }); }); From 14261ca6815ecc0ddbf6fc50348bcfa1a3edc821 Mon Sep 17 00:00:00 2001 From: Christopher Aubut Date: Thu, 13 Jun 2024 18:00:28 -0600 Subject: [PATCH 03/22] refactor(typescript-estree): revert some changes to createParseSettings --- .../src/parseSettings/createParseSettings.ts | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/packages/typescript-estree/src/parseSettings/createParseSettings.ts b/packages/typescript-estree/src/parseSettings/createParseSettings.ts index 5eecd713626e..98e42d8af36c 100644 --- a/packages/typescript-estree/src/parseSettings/createParseSettings.ts +++ b/packages/typescript-estree/src/parseSettings/createParseSettings.ts @@ -61,11 +61,6 @@ export function createParseSettings( return JSDocParsingMode.ParseAll; } })(); - const extraFileExtensions = - Array.isArray(tsestreeOptions.extraFileExtensions) && - tsestreeOptions.extraFileExtensions.every(ext => typeof ext === 'string') - ? tsestreeOptions.extraFileExtensions - : []; const parseSettings: MutableParseSettings = { allowInvalidAST: tsestreeOptions.allowInvalidAST === true, @@ -81,7 +76,11 @@ export function createParseSettings( : new Set(), errorOnTypeScriptSyntacticAndSemanticIssues: false, errorOnUnknownASTType: tsestreeOptions.errorOnUnknownASTType === true, - extraFileExtensions, + extraFileExtensions: + Array.isArray(tsestreeOptions.extraFileExtensions) && + tsestreeOptions.extraFileExtensions.every(ext => typeof ext === 'string') + ? tsestreeOptions.extraFileExtensions + : [], filePath: ensureAbsolutePath( typeof tsestreeOptions.filePath === 'string' && tsestreeOptions.filePath !== '' From 3dff7e09391cb99fe408e97bab2c7d869de98d2c Mon Sep 17 00:00:00 2001 From: Christopher Aubut Date: Thu, 13 Jun 2024 19:21:23 -0600 Subject: [PATCH 04/22] docs(typescript-estree): adds performance information to the documentation --- docs/packages/Parser.mdx | 5 ++ docs/packages/TypeScript_ESTree.mdx | 2 + docs/troubleshooting/FAQ.mdx | 5 ++ docs/troubleshooting/Performance.mdx | 80 +++++++++++++++++++ .../typescript-estree/src/parser-options.ts | 2 + 5 files changed, 94 insertions(+) diff --git a/docs/packages/Parser.mdx b/docs/packages/Parser.mdx index c337e5655967..0aab34880f8e 100644 --- a/docs/packages/Parser.mdx +++ b/docs/packages/Parser.mdx @@ -152,6 +152,11 @@ 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 +When used with [`projectService`](#projectservice) it will trigger a full project reload. +See [Changes to `extraFileExtensions` with `projectService` in your ESLint options](../troubleshooting/Performance.mdx#changes-to-extrafileextensions-with-projectservice-in-your-eslint-options) +::: + ### `jsDocParsingMode` > Default if `parserOptions.project` is set, then `'all'`, otherwise `'none'` diff --git a/docs/packages/TypeScript_ESTree.mdx b/docs/packages/TypeScript_ESTree.mdx index e8a27eb0f012..41afef0f555f 100644 --- a/docs/packages/TypeScript_ESTree.mdx +++ b/docs/packages/TypeScript_ESTree.mdx @@ -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 that when used with {@link projectService} it will trigger a full project reload. */ extraFileExtensions?: string[]; diff --git a/docs/troubleshooting/FAQ.mdx b/docs/troubleshooting/FAQ.mdx index 164ee17c32bc..a372a23e4531 100644 --- a/docs/troubleshooting/FAQ.mdx +++ b/docs/troubleshooting/FAQ.mdx @@ -243,6 +243,11 @@ We don't recommend using `--cache`. You can use `parserOptions.extraFileExtensions` to specify an array of non-TypeScript extensions to allow, for example: +:::note +When used with [`projectService`](../packages/Parser.mdx#projectservice) it will trigger a full project reload. +See [Changes to `extraFileExtensions` with `projectService` in your ESLint options](../troubleshooting/Performance.mdx#changes-to-extrafileextensions-with-projectservice-in-your-eslint-options) +::: + diff --git a/docs/troubleshooting/Performance.mdx b/docs/troubleshooting/Performance.mdx index 71a63da234b2..77ab15d9269d 100644 --- a/docs/troubleshooting/Performance.mdx +++ b/docs/troubleshooting/Performance.mdx @@ -79,6 +79,86 @@ 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` in your ESLint options + +Using different [`extraFileExtensions`](../packages/Parser.mdx#extrafileextensions) for different files with [`projectService`](../packages/Parser.mdx#projectservice) will cause a full project reload. +This incurs a sizeable performance penalty. +The optimal approach is to use the same `extraFileExtensions` across `parserOptions` to avoid the performance cost. + + + + +```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, + }, + }, + }, +]; +``` + + + + +```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, + }, + }, + ], +}; +``` + + + + ## The `indent` / `@typescript-eslint/indent` rules This rule helps ensure your codebase follows a consistent indentation pattern. diff --git a/packages/typescript-estree/src/parser-options.ts b/packages/typescript-estree/src/parser-options.ts index b3f7794133d4..8edb25a14398 100644 --- a/packages/typescript-estree/src/parser-options.ts +++ b/packages/typescript-estree/src/parser-options.ts @@ -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 that when used with {@link projectService} it will trigger a full project reload. */ extraFileExtensions?: string[]; From 85ce13d232fdf1b4dafac394bfaca5b8f9b165c3 Mon Sep 17 00:00:00 2001 From: Christopher Aubut Date: Thu, 13 Jun 2024 20:46:11 -0600 Subject: [PATCH 05/22] docs(cspell): filter words --- .cspell.json | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.cspell.json b/.cspell.json index 1a754cf9b96d..a994be7d5639 100644 --- a/.cspell.json +++ b/.cspell.json @@ -88,6 +88,7 @@ "esquery", "esrecurse", "estree", + "extrafileextensions", "falsiness", "globby", "IDE's", @@ -120,6 +121,7 @@ "preact", "Premade", "prettier's", + "projectservice", "quasis", "Quickstart", "recurse", From dd99286bc85cc4aafc3d925b4983d1736fa996b5 Mon Sep 17 00:00:00 2001 From: Christopher Aubut Date: Thu, 13 Jun 2024 21:37:46 -0600 Subject: [PATCH 06/22] refactor(typescript-estree): fixes test to show host config not updated and clean up impl Fixes the test where it should not update to update twice with the same values to demonstrate the cache worked. Changes the cache to not update until successfully applied and cleans up the implementation a bit. --- .../src/useProgramFromProjectService.ts | 17 +++++++++------- .../lib/useProgramFromProjectService.test.ts | 20 +++++++++++++++---- 2 files changed, 26 insertions(+), 11 deletions(-) diff --git a/packages/typescript-estree/src/useProgramFromProjectService.ts b/packages/typescript-estree/src/useProgramFromProjectService.ts index 415ee5d10787..3ef8e58f4f91 100644 --- a/packages/typescript-estree/src/useProgramFromProjectService.ts +++ b/packages/typescript-estree/src/useProgramFromProjectService.ts @@ -26,24 +26,27 @@ const updateExtraFileExtensions = ( }, extraFileExtensions: string[], ): void => { - if (!service.__extra_file_extensions) { - service.__extra_file_extensions = new Set(); - } + const uniqExtraFileExtensions = new Set(extraFileExtensions); if ( + service.__extra_file_extensions === undefined || symmetricDifference( service.__extra_file_extensions, - new Set(extraFileExtensions), + uniqExtraFileExtensions, ).size > 0 ) { - service.__extra_file_extensions = new Set(extraFileExtensions); - log('Updating extra file extensions: %s', extraFileExtensions); + log( + 'Updating extra file extensions: %s: %s', + extraFileExtensions, + uniqExtraFileExtensions, + ); service.setHostConfiguration({ - extraFileExtensions: extraFileExtensions.map(extension => ({ + extraFileExtensions: [...uniqExtraFileExtensions].map(extension => ({ extension, isMixedContent: false, scriptKind: ts.ScriptKind.Deferred, })), }); + service.__extra_file_extensions = uniqExtraFileExtensions; log('Extra file extensions updated: %o', service.__extra_file_extensions); } }; diff --git a/packages/typescript-estree/tests/lib/useProgramFromProjectService.test.ts b/packages/typescript-estree/tests/lib/useProgramFromProjectService.test.ts index 94e054d2e864..ee2de7594d11 100644 --- a/packages/typescript-estree/tests/lib/useProgramFromProjectService.test.ts +++ b/packages/typescript-estree/tests/lib/useProgramFromProjectService.test.ts @@ -347,12 +347,13 @@ If you absolutely need more files included, set parserOptions.projectService.max it('does not call setHostConfiguration on the service to use extraFileExtensions when unchanged', () => { const { service } = createMockProjectService(); + const settings = createProjectServiceSettings({ + allowDefaultProject: [mockParseSettings.filePath], + service, + }); useProgramFromProjectService( - createProjectServiceSettings({ - allowDefaultProject: [mockParseSettings.filePath], - service, - }), + settings, { ...mockParseSettings, extraFileExtensions: ['.vue'], @@ -371,6 +372,17 @@ If you absolutely need more files included, set parserOptions.projectService.max }, ], }); + + useProgramFromProjectService( + settings, + { + ...mockParseSettings, + extraFileExtensions: ['.vue'], + }, + false, + new Set(), + ); + expect(service.setHostConfiguration).toHaveBeenCalledTimes(1); }); it('calls setHostConfiguration on the service to use extraFileExtensions when changed', () => { From 8487e9d396050ad8853e7663980c67857dd44632 Mon Sep 17 00:00:00 2001 From: Christopher Aubut Date: Thu, 13 Jun 2024 21:44:59 -0600 Subject: [PATCH 07/22] refactor(typescript-estree): fixes broken test --- .../src/useProgramFromProjectService.ts | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/packages/typescript-estree/src/useProgramFromProjectService.ts b/packages/typescript-estree/src/useProgramFromProjectService.ts index 3ef8e58f4f91..f56c660ae3e1 100644 --- a/packages/typescript-estree/src/useProgramFromProjectService.ts +++ b/packages/typescript-estree/src/useProgramFromProjectService.ts @@ -28,11 +28,13 @@ const updateExtraFileExtensions = ( ): void => { const uniqExtraFileExtensions = new Set(extraFileExtensions); if ( - service.__extra_file_extensions === undefined || - symmetricDifference( - service.__extra_file_extensions, - uniqExtraFileExtensions, - ).size > 0 + (service.__extra_file_extensions === undefined && + uniqExtraFileExtensions.size > 0) || + (service.__extra_file_extensions !== undefined && + symmetricDifference( + service.__extra_file_extensions, + uniqExtraFileExtensions, + ).size > 0) ) { log( 'Updating extra file extensions: %s: %s', From 2092269aafd4776107f8d966161411f88e661b26 Mon Sep 17 00:00:00 2001 From: Christopher Aubut Date: Thu, 13 Jun 2024 21:46:14 -0600 Subject: [PATCH 08/22] refactor(typescript-estree): logs before/after updating extraFileExtensions --- packages/typescript-estree/src/useProgramFromProjectService.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/typescript-estree/src/useProgramFromProjectService.ts b/packages/typescript-estree/src/useProgramFromProjectService.ts index f56c660ae3e1..084669bf3961 100644 --- a/packages/typescript-estree/src/useProgramFromProjectService.ts +++ b/packages/typescript-estree/src/useProgramFromProjectService.ts @@ -38,7 +38,7 @@ const updateExtraFileExtensions = ( ) { log( 'Updating extra file extensions: %s: %s', - extraFileExtensions, + service.__extra_file_extensions, uniqExtraFileExtensions, ); service.setHostConfiguration({ From bfc80e69952d19571099ece15329a73b792d9875 Mon Sep 17 00:00:00 2001 From: Christopher Aubut Date: Mon, 17 Jun 2024 10:03:21 -0600 Subject: [PATCH 09/22] test(typescript-estree): [ProjectService] adds additional extraFileExtensions tests Empty or not provided extraFileExtensions are handled differently depending on the state of ProjectService. Clarified test names and added additional tests to reflect this. --- .../lib/useProgramFromProjectService.test.ts | 59 ++++++++++++++++++- 1 file changed, 57 insertions(+), 2 deletions(-) diff --git a/packages/typescript-estree/tests/lib/useProgramFromProjectService.test.ts b/packages/typescript-estree/tests/lib/useProgramFromProjectService.test.ts index ee2de7594d11..b368355a2c04 100644 --- a/packages/typescript-estree/tests/lib/useProgramFromProjectService.test.ts +++ b/packages/typescript-estree/tests/lib/useProgramFromProjectService.test.ts @@ -302,7 +302,7 @@ If you absolutely need more files included, set parserOptions.projectService.max expect(actual).toBe(program); }); - it('does not call setHostConfiguration if extraFileExtensions are not provided', () => { + it('does not call setHostConfiguration on the service with default extensions if extraFileExtensions are not provided', () => { const { service } = createMockProjectService(); useProgramFromProjectService( @@ -318,7 +318,26 @@ If you absolutely need more files included, set parserOptions.projectService.max expect(service.setHostConfiguration).not.toHaveBeenCalled(); }); - it('calls setHostConfiguration on the service to use extraFileExtensions when it is provided', () => { + it('does not call setHostConfiguration on the service with default extensions if extraFileExtensions is empty', () => { + const { service } = createMockProjectService(); + + useProgramFromProjectService( + createProjectServiceSettings({ + allowDefaultProject: [mockParseSettings.filePath], + service, + }), + { + ...mockParseSettings, + extraFileExtensions: [], + }, + false, + new Set(), + ); + + expect(service.setHostConfiguration).not.toHaveBeenCalled(); + }); + + it('calls setHostConfiguration on the service with default extensions to use extraFileExtensions when it is provided', () => { const { service } = createMockProjectService(); useProgramFromProjectService( @@ -428,4 +447,40 @@ If you absolutely need more files included, set parserOptions.projectService.max extraFileExtensions: [], }); }); + + it('calls setHostConfiguration on the service with non-default extensions to use defaults when extraFileExtensions are not provided', () => { + const { service } = createMockProjectService(); + const settings = createProjectServiceSettings({ + allowDefaultProject: [mockParseSettings.filePath], + service, + }); + + useProgramFromProjectService( + settings, + { + ...mockParseSettings, + extraFileExtensions: ['.vue'], + }, + false, + new Set(), + ); + + expect(service.setHostConfiguration).toHaveBeenCalledTimes(1); + expect(service.setHostConfiguration).toHaveBeenCalledWith({ + extraFileExtensions: [ + { + extension: '.vue', + isMixedContent: false, + scriptKind: ScriptKind.Deferred, + }, + ], + }); + + useProgramFromProjectService(settings, mockParseSettings, false, new Set()); + + expect(service.setHostConfiguration).toHaveBeenCalledTimes(2); + expect(service.setHostConfiguration).toHaveBeenCalledWith({ + extraFileExtensions: [], + }); + }); }); From cf09e1e694561fd73622fb07c5b8a094310e917f Mon Sep 17 00:00:00 2001 From: Christopher Aubut Date: Mon, 17 Jun 2024 13:37:53 -0600 Subject: [PATCH 10/22] refactor(typescript-estree): simplifies last set extraFileExtensions caching behavior --- .../src/useProgramFromProjectService.ts | 37 +++++++------------ 1 file changed, 13 insertions(+), 24 deletions(-) diff --git a/packages/typescript-estree/src/useProgramFromProjectService.ts b/packages/typescript-estree/src/useProgramFromProjectService.ts index 084669bf3961..516ad0dbcdb5 100644 --- a/packages/typescript-estree/src/useProgramFromProjectService.ts +++ b/packages/typescript-estree/src/useProgramFromProjectService.ts @@ -1,6 +1,8 @@ +import path from 'node:path'; +import util from 'node:util'; + import debug from 'debug'; import { minimatch } from 'minimatch'; -import path from 'path'; import * as ts from 'typescript'; import { createProjectProgram } from './create-program/createProjectProgram'; @@ -13,43 +15,30 @@ const log = debug( 'typescript-eslint:typescript-estree:useProgramFromProjectService', ); -const union = (self: Set, other: Set): Set => - new Set([...self, ...other]); -const difference = (self: Set, other: Set): Set => - new Set([...self].filter(elem => !other.has(elem))); -const symmetricDifference = (self: Set, other: Set): Set => - union(difference(self, other), difference(other, self)); +const serviceFileExtensions = new WeakMap(); const updateExtraFileExtensions = ( - service: ts.server.ProjectService & { - __extra_file_extensions?: Set; - }, + service: ts.server.ProjectService, extraFileExtensions: string[], ): void => { - const uniqExtraFileExtensions = new Set(extraFileExtensions); + const currentServiceFileExtensions = serviceFileExtensions.get(service) ?? []; if ( - (service.__extra_file_extensions === undefined && - uniqExtraFileExtensions.size > 0) || - (service.__extra_file_extensions !== undefined && - symmetricDifference( - service.__extra_file_extensions, - uniqExtraFileExtensions, - ).size > 0) + !util.isDeepStrictEqual(currentServiceFileExtensions, extraFileExtensions) ) { log( - 'Updating extra file extensions: %s: %s', - service.__extra_file_extensions, - uniqExtraFileExtensions, + 'Updating extra file extensions: before=%s: after=%s', + currentServiceFileExtensions, + extraFileExtensions, ); service.setHostConfiguration({ - extraFileExtensions: [...uniqExtraFileExtensions].map(extension => ({ + extraFileExtensions: extraFileExtensions.map(extension => ({ extension, isMixedContent: false, scriptKind: ts.ScriptKind.Deferred, })), }); - service.__extra_file_extensions = uniqExtraFileExtensions; - log('Extra file extensions updated: %o', service.__extra_file_extensions); + serviceFileExtensions.set(service, extraFileExtensions); + log('Extra file extensions updated: %o', extraFileExtensions); } }; From 113441d469e8bc1471461be363980d575323dbfc Mon Sep 17 00:00:00 2001 From: Christopher Aubut Date: Mon, 17 Jun 2024 13:38:59 -0600 Subject: [PATCH 11/22] docs: adds additional performance details for extraFileExtensions with projectService --- docs/troubleshooting/Performance.mdx | 36 ++++++++++++++++++++++++---- 1 file changed, 31 insertions(+), 5 deletions(-) diff --git a/docs/troubleshooting/Performance.mdx b/docs/troubleshooting/Performance.mdx index 77ab15d9269d..a274c6d67782 100644 --- a/docs/troubleshooting/Performance.mdx +++ b/docs/troubleshooting/Performance.mdx @@ -81,9 +81,35 @@ See [Glob pattern in parser's option "project" slows down linting](https://githu ## Changes to `extraFileExtensions` with `projectService` in your ESLint options -Using different [`extraFileExtensions`](../packages/Parser.mdx#extrafileextensions) for different files with [`projectService`](../packages/Parser.mdx#projectservice) will cause a full project reload. -This incurs a sizeable performance penalty. -The optimal approach is to use the same `extraFileExtensions` across `parserOptions` to avoid the performance cost. +Using different [`extraFileExtensions`](../packages/Parser.mdx#extrafileextensions) for different files with +[`projectService`](../packages/Parser.mdx#projectservice) causes the TypeScript server to perform a full project reload. +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: [ '.vue' ] +... +typescript-estree:useProgramFromProjectService Updating extra file extensions: before=[]: after=[ '.vue' ] +typescript-estree:tsserver:info reload projects. +typescript-estree:useProgramFromProjectService Extra file extensions updated: [ '.vue' ] +``` + +Frequently switching `extraFileExtensions` may cause project reloads to trigger before the last one completes. + +``` +typescript-estree:tsserver:info Scheduled: /path/to/tsconfig.src.json, Cancelled earlier one +0ms +typescript-estree:tsserver:info Scheduled: *ensureProjectForOpenFiles*, Cancelled earlier one +0ms +``` + +The impact of setting `extraFileExtensions` per-package in a project is likely negligible on the CLI. +That said, when using [typescript-eslint-language-service](https://www.npmjs.com/package/typescript-eslint-language-service), +the optimal approach is to use the same `extraFileExtensions` across `parserOptions` to avoid project reloads +when switching between files in different packages. @@ -110,7 +136,7 @@ export default [ }, { files: ['*.vue'], - languageOptions: { + languageOptions: {e parser: vueParser, parserOptions: { projectService: true, @@ -159,7 +185,7 @@ module.exports = { -## The `indent` / `@typescript-eslint/indent` rules +## The `indent` / `@typescripst-eslint/indent` rules This rule helps ensure your codebase follows a consistent indentation pattern. However this involves a _lot_ of computations across every single token in a file. From 1b9887f063a64fd0e31f8f6ec09eb984a868140f Mon Sep 17 00:00:00 2001 From: Christopher Aubut Date: Mon, 17 Jun 2024 13:52:01 -0600 Subject: [PATCH 12/22] docs: fixes typo --- docs/troubleshooting/Performance.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/troubleshooting/Performance.mdx b/docs/troubleshooting/Performance.mdx index a274c6d67782..c232003937e4 100644 --- a/docs/troubleshooting/Performance.mdx +++ b/docs/troubleshooting/Performance.mdx @@ -136,7 +136,7 @@ export default [ }, { files: ['*.vue'], - languageOptions: {e + languageOptions: { parser: vueParser, parserOptions: { projectService: true, From 559d8ea98f657f796fe002c7743f3dc8876ab7fd Mon Sep 17 00:00:00 2001 From: Christopher Aubut Date: Mon, 17 Jun 2024 16:17:21 -0600 Subject: [PATCH 13/22] docs: adds additional clarifying details to performance with extraFileExtensions --- docs/troubleshooting/Performance.mdx | 23 +++++++++++++++-------- 1 file changed, 15 insertions(+), 8 deletions(-) diff --git a/docs/troubleshooting/Performance.mdx b/docs/troubleshooting/Performance.mdx index c232003937e4..3e209593a2f9 100644 --- a/docs/troubleshooting/Performance.mdx +++ b/docs/troubleshooting/Performance.mdx @@ -81,8 +81,8 @@ See [Glob pattern in parser's option "project" slows down linting](https://githu ## Changes to `extraFileExtensions` with `projectService` in your ESLint options -Using different [`extraFileExtensions`](../packages/Parser.mdx#extrafileextensions) for different files with -[`projectService`](../packages/Parser.mdx#projectservice) causes the TypeScript server to perform a full project reload. +When [`extraFileExtensions`](../packages/Parser.mdx#extrafileextensions) changes while using +[`projectService`](../packages/Parser.mdx#projectservice), the TypeScript server must perform a full project reload. Project reloads can be observed using the [debug environment variable](../packages/typescript-estree/#debugging): `DEBUG='typescript-eslint:typescript-estree:*'`. ``` @@ -92,24 +92,31 @@ typescript-estree:useProgramFromProjectService Extra file extensions updated: [ ... typescript-estree:useProgramFromProjectService Updating extra file extensions: before=[ '.vue' ]: after=[] typescript-estree:tsserver:info reload projects. -typescript-estree:useProgramFromProjectService Extra file extensions updated: [ '.vue' ] +typescript-estree:useProgramFromProjectService Extra file extensions updated: [] ... typescript-estree:useProgramFromProjectService Updating extra file extensions: before=[]: after=[ '.vue' ] typescript-estree:tsserver:info reload projects. typescript-estree:useProgramFromProjectService Extra file extensions updated: [ '.vue' ] ``` -Frequently switching `extraFileExtensions` may cause project reloads to trigger before the last one completes. +As files are received, if `extraFileExtensions` differs from the previous file, +the TypeScript server is updated to match your ESLint options. +The TypeScript server incrementally updates the project structure by creating an execution pipeline. +If the ordering of files received from ESLint happens to frequently alternate the value of `extraFileExtensions`, +a project reload may occur before the last pipeline completes. +In turn, this can lead to cascading performance issues. ``` typescript-estree:tsserver:info Scheduled: /path/to/tsconfig.src.json, Cancelled earlier one +0ms typescript-estree:tsserver:info Scheduled: *ensureProjectForOpenFiles*, Cancelled earlier one +0ms ``` -The impact of setting `extraFileExtensions` per-package in a project is likely negligible on the CLI. -That said, when using [typescript-eslint-language-service](https://www.npmjs.com/package/typescript-eslint-language-service), -the optimal approach is to use the same `extraFileExtensions` across `parserOptions` to avoid project reloads -when switching between files in different packages. +The impact of setting `extraFileExtensions` per file type in a project may be negligible depending on the order files are received. +That said, complex ESLint options, cached linting results altering which files are evaluated, +and switching between files with different `extraFileExtensions` while using +[typescript-eslint-language-service](https://www.npmjs.com/package/typescript-eslint-language-service) +may all be sources of unintended project reloads. +The optimal approach to avoid project reloads altogether is to use the same `extraFileExtensions` across `parserOptions`. From 5f83cc3017bf671fa58e240ede24a3a22eb01447 Mon Sep 17 00:00:00 2001 From: Christopher Aubut Date: Mon, 17 Jun 2024 16:38:16 -0600 Subject: [PATCH 14/22] docs: updates sections that refer to performance with extraFileExtensions --- docs/packages/Parser.mdx | 2 +- docs/packages/TypeScript_ESTree.mdx | 2 +- docs/troubleshooting/FAQ.mdx | 2 +- packages/typescript-estree/src/parser-options.ts | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/packages/Parser.mdx b/docs/packages/Parser.mdx index 0aab34880f8e..9d2b4c852023 100644 --- a/docs/packages/Parser.mdx +++ b/docs/packages/Parser.mdx @@ -153,8 +153,8 @@ The default extensions are `['.js', '.mjs', '.cjs', '.jsx', '.ts', '.mts', '.cts Add extensions starting with `.`, followed by the file extension. E.g. for a `.vue` file use `"extraFileExtensions": [".vue"]`. :::note -When used with [`projectService`](#projectservice) it will trigger a full project reload. See [Changes to `extraFileExtensions` with `projectService` in your ESLint options](../troubleshooting/Performance.mdx#changes-to-extrafileextensions-with-projectservice-in-your-eslint-options) +when using with [`projectService`](../packages/Parser.mdx#projectservice) to avoid performance issues. ::: ### `jsDocParsingMode` diff --git a/docs/packages/TypeScript_ESTree.mdx b/docs/packages/TypeScript_ESTree.mdx index 6493939cc74d..7e70ed1624c8 100644 --- a/docs/packages/TypeScript_ESTree.mdx +++ b/docs/packages/TypeScript_ESTree.mdx @@ -204,7 +204,7 @@ 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 that when used with {@link projectService} it will trigger a full project reload. + * NOTE: When used with {@link projectService}, full project reloads may occur. */ extraFileExtensions?: string[]; diff --git a/docs/troubleshooting/FAQ.mdx b/docs/troubleshooting/FAQ.mdx index a372a23e4531..56b6a406f281 100644 --- a/docs/troubleshooting/FAQ.mdx +++ b/docs/troubleshooting/FAQ.mdx @@ -244,8 +244,8 @@ We don't recommend using `--cache`. You can use `parserOptions.extraFileExtensions` to specify an array of non-TypeScript extensions to allow, for example: :::note -When used with [`projectService`](../packages/Parser.mdx#projectservice) it will trigger a full project reload. See [Changes to `extraFileExtensions` with `projectService` in your ESLint options](../troubleshooting/Performance.mdx#changes-to-extrafileextensions-with-projectservice-in-your-eslint-options) +when using with [`projectService`](../packages/Parser.mdx#projectservice) to avoid performance issues. ::: diff --git a/packages/typescript-estree/src/parser-options.ts b/packages/typescript-estree/src/parser-options.ts index 8edb25a14398..8b4496526d5c 100644 --- a/packages/typescript-estree/src/parser-options.ts +++ b/packages/typescript-estree/src/parser-options.ts @@ -152,7 +152,7 @@ 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 that when used with {@link projectService} it will trigger a full project reload. + * NOTE: When used with {@link projectService}, full project reloads may occur. */ extraFileExtensions?: string[]; From ec9d09b380ad63aca5083ae35424c210a1560e99 Mon Sep 17 00:00:00 2001 From: Christopher Aubut Date: Thu, 20 Jun 2024 14:51:45 -0600 Subject: [PATCH 15/22] docs: updates docs with review feedback --- docs/packages/Parser.mdx | 3 +- docs/troubleshooting/FAQ.mdx | 3 +- docs/troubleshooting/Performance.mdx | 63 +++++++++++----------------- 3 files changed, 27 insertions(+), 42 deletions(-) diff --git a/docs/packages/Parser.mdx b/docs/packages/Parser.mdx index 9d2b4c852023..cf8ac653ac30 100644 --- a/docs/packages/Parser.mdx +++ b/docs/packages/Parser.mdx @@ -153,8 +153,7 @@ The default extensions are `['.js', '.mjs', '.cjs', '.jsx', '.ts', '.mts', '.cts 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` in your ESLint options](../troubleshooting/Performance.mdx#changes-to-extrafileextensions-with-projectservice-in-your-eslint-options) -when using with [`projectService`](../packages/Parser.mdx#projectservice) to avoid performance issues. +See [Changes to `extraFileExtensions` with `projectService`](../troubleshooting/Performance.mdx#changes-to-extrafileextensions-with-projectservice) to avoid performance issues. ::: ### `jsDocParsingMode` diff --git a/docs/troubleshooting/FAQ.mdx b/docs/troubleshooting/FAQ.mdx index 56b6a406f281..8a8b3af7feb1 100644 --- a/docs/troubleshooting/FAQ.mdx +++ b/docs/troubleshooting/FAQ.mdx @@ -244,8 +244,7 @@ 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` in your ESLint options](../troubleshooting/Performance.mdx#changes-to-extrafileextensions-with-projectservice-in-your-eslint-options) -when using with [`projectService`](../packages/Parser.mdx#projectservice) to avoid performance issues. +See [Changes to `extraFileExtensions` with `projectService`](../troubleshooting/Performance.mdx#changes-to-extrafileextensions-with-projectservice) to avoid performance issues. ::: diff --git a/docs/troubleshooting/Performance.mdx b/docs/troubleshooting/Performance.mdx index 3e209593a2f9..f608086d8ac3 100644 --- a/docs/troubleshooting/Performance.mdx +++ b/docs/troubleshooting/Performance.mdx @@ -79,44 +79,12 @@ 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` in your ESLint options +## Changes to `extraFileExtensions` with `projectService` -When [`extraFileExtensions`](../packages/Parser.mdx#extrafileextensions) changes while using -[`projectService`](../packages/Parser.mdx#projectservice), the TypeScript server must perform a full project reload. -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:useProgramFromProjectService Updating extra file extensions: before=[]: after=[ '.vue' ] -typescript-estree:tsserver:info reload projects. -typescript-estree:useProgramFromProjectService Extra file extensions updated: [ '.vue' ] -``` - -As files are received, if `extraFileExtensions` differs from the previous file, -the TypeScript server is updated to match your ESLint options. -The TypeScript server incrementally updates the project structure by creating an execution pipeline. -If the ordering of files received from ESLint happens to frequently alternate the value of `extraFileExtensions`, -a project reload may occur before the last pipeline completes. -In turn, this can lead to cascading performance issues. - -``` -typescript-estree:tsserver:info Scheduled: /path/to/tsconfig.src.json, Cancelled earlier one +0ms -typescript-estree:tsserver:info Scheduled: *ensureProjectForOpenFiles*, Cancelled earlier one +0ms -``` - -The impact of setting `extraFileExtensions` per file type in a project may be negligible depending on the order files are received. -That said, complex ESLint options, cached linting results altering which files are evaluated, -and switching between files with different `extraFileExtensions` while using -[typescript-eslint-language-service](https://www.npmjs.com/package/typescript-eslint-language-service) -may all be sources of unintended project reloads. -The optimal approach to avoid project reloads altogether is to use the same `extraFileExtensions` across `parserOptions`. +When [`extraFileExtensions`](../packages/Parser.mdx#extrafileextensions) changes while using [`projectService`](../packages/Parser.mdx#projectservice), +the TypeScript server must perform a full project reload to account for new and removed files from the project. +Configuring `parserOptions.extraFileExtensions` multiple times with different values in the same project is likely to cause unintentional project reloads. +The optimal approach to avoid project reloads altogether is to use the same `extraFileExtensions` value. @@ -192,7 +160,26 @@ module.exports = { -## The `indent` / `@typescripst-eslint/indent` rules +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. However this involves a _lot_ of computations across every single token in a file. From aa142c41d03db1d17e11d5775af05ab41d52c608 Mon Sep 17 00:00:00 2001 From: Christopher Aubut Date: Thu, 20 Jun 2024 15:27:32 -0600 Subject: [PATCH 16/22] docs: reorders content related to this issue in the performance section --- docs/troubleshooting/Performance.mdx | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/docs/troubleshooting/Performance.mdx b/docs/troubleshooting/Performance.mdx index f608086d8ac3..ae2f3df5d1f1 100644 --- a/docs/troubleshooting/Performance.mdx +++ b/docs/troubleshooting/Performance.mdx @@ -81,10 +81,11 @@ See [Glob pattern in parser's option "project" slows down linting](https://githu ## Changes to `extraFileExtensions` with `projectService` -When [`extraFileExtensions`](../packages/Parser.mdx#extrafileextensions) changes while using [`projectService`](../packages/Parser.mdx#projectservice), -the TypeScript server must perform a full project reload to account for new and removed files from the project. -Configuring `parserOptions.extraFileExtensions` multiple times with different values in the same project is likely to cause unintentional project reloads. -The optimal approach to avoid project reloads altogether is to use the same `extraFileExtensions` value. +Changes to [`extraFileExtensions`](../packages/Parser.mdx#extrafileextensions) between files in the same project with +the [`projectService`](../packages/Parser.mdx#projectservice) option is likely to cause more overhead than expected updating the project. +The optimal approach is to use the same `extraFileExtensions` value across all files in the project. +For every file linted, we update the `projectService` whenever `extraFileExtensions` changes to match your ESLint options. +This causes the underlying TypeScript server to perform a full project reload to account for new and removed files from the project. From fa15073d3996b2ce0e29354630df1d3e86fce49e Mon Sep 17 00:00:00 2001 From: Christopher Aubut Date: Sat, 22 Jun 2024 18:01:15 +0000 Subject: [PATCH 17/22] docs: accepts review suggestion in docs/troubleshooting/Performance.mdx MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Josh Goldberg ✨ --- docs/troubleshooting/Performance.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/troubleshooting/Performance.mdx b/docs/troubleshooting/Performance.mdx index ae2f3df5d1f1..1173321b88fb 100644 --- a/docs/troubleshooting/Performance.mdx +++ b/docs/troubleshooting/Performance.mdx @@ -81,7 +81,7 @@ See [Glob pattern in parser's option "project" slows down linting](https://githu ## Changes to `extraFileExtensions` with `projectService` -Changes to [`extraFileExtensions`](../packages/Parser.mdx#extrafileextensions) between files in the same project with +Using a different `extraFileExtensions`](../packages/Parser.mdx#extrafileextensions) between files in the same project with the [`projectService`](../packages/Parser.mdx#projectservice) option is likely to cause more overhead than expected updating the project. The optimal approach is to use the same `extraFileExtensions` value across all files in the project. For every file linted, we update the `projectService` whenever `extraFileExtensions` changes to match your ESLint options. From 1771c270367592ffb9dd3738af221105254c41de Mon Sep 17 00:00:00 2001 From: Christopher Aubut Date: Sat, 22 Jun 2024 18:01:34 +0000 Subject: [PATCH 18/22] docs: accepts review suggestion in docs/troubleshooting/Performance.mdx MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Josh Goldberg ✨ --- docs/troubleshooting/Performance.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/troubleshooting/Performance.mdx b/docs/troubleshooting/Performance.mdx index 1173321b88fb..0c1c192feee1 100644 --- a/docs/troubleshooting/Performance.mdx +++ b/docs/troubleshooting/Performance.mdx @@ -82,7 +82,7 @@ See [Glob pattern in parser's option "project" slows down linting](https://githu ## 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 is likely to cause more overhead than expected updating the project. +the [`projectService`](../packages/Parser.mdx#projectservice) option may cause performance degredations. The optimal approach is to use the same `extraFileExtensions` value across all files in the project. For every file linted, we update the `projectService` whenever `extraFileExtensions` changes to match your ESLint options. This causes the underlying TypeScript server to perform a full project reload to account for new and removed files from the project. From 63992dc040034968c93cad319ac8d8d2a3c433c6 Mon Sep 17 00:00:00 2001 From: Christopher Aubut Date: Sat, 22 Jun 2024 18:02:02 +0000 Subject: [PATCH 19/22] docs: accepts review suggestion in docs/troubleshooting/Performance.mdx MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Josh Goldberg ✨ --- docs/troubleshooting/Performance.mdx | 1 - 1 file changed, 1 deletion(-) diff --git a/docs/troubleshooting/Performance.mdx b/docs/troubleshooting/Performance.mdx index 0c1c192feee1..41bf5ac1e637 100644 --- a/docs/troubleshooting/Performance.mdx +++ b/docs/troubleshooting/Performance.mdx @@ -83,7 +83,6 @@ See [Glob pattern in parser's option "project" slows down linting](https://githu 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 degredations. -The optimal approach is to use the same `extraFileExtensions` value across all files in the project. For every file linted, we update the `projectService` whenever `extraFileExtensions` changes to match your ESLint options. This causes the underlying TypeScript server to perform a full project reload to account for new and removed files from the project. From ddb970c9d62a3cbe7ac6676eaf35150b6fc112d0 Mon Sep 17 00:00:00 2001 From: Christopher Aubut Date: Sat, 22 Jun 2024 18:02:19 +0000 Subject: [PATCH 20/22] docs: accepts review suggestion in docs/troubleshooting/Performance.mdx MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Josh Goldberg ✨ --- docs/troubleshooting/Performance.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/troubleshooting/Performance.mdx b/docs/troubleshooting/Performance.mdx index 41bf5ac1e637..fd40ed7d514b 100644 --- a/docs/troubleshooting/Performance.mdx +++ b/docs/troubleshooting/Performance.mdx @@ -84,7 +84,7 @@ See [Glob pattern in parser's option "project" slows down linting](https://githu 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 degredations. For every file linted, we update the `projectService` whenever `extraFileExtensions` changes to match your ESLint options. -This causes the underlying TypeScript server to perform a full project reload to account for new and removed files from the project. +This causes the underlying TypeScript server to perform a full project reload. From eaaee4f458680ef994fc72f25b0f843de3c75398 Mon Sep 17 00:00:00 2001 From: Christopher Aubut Date: Sat, 22 Jun 2024 18:02:35 +0000 Subject: [PATCH 21/22] docs: accepts review suggestion in docs/troubleshooting/Performance.mdx MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Josh Goldberg ✨ --- docs/troubleshooting/Performance.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/troubleshooting/Performance.mdx b/docs/troubleshooting/Performance.mdx index fd40ed7d514b..796b54cb07b0 100644 --- a/docs/troubleshooting/Performance.mdx +++ b/docs/troubleshooting/Performance.mdx @@ -83,7 +83,7 @@ See [Glob pattern in parser's option "project" slows down linting](https://githu 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 degredations. -For every file linted, we update the `projectService` whenever `extraFileExtensions` changes to match your ESLint options. +For every file linted, we update the `projectService` whenever `extraFileExtensions` changes. This causes the underlying TypeScript server to perform a full project reload. From e081759b39457b2a9532a022285a6d377c81a9e1 Mon Sep 17 00:00:00 2001 From: Christopher Aubut Date: Sat, 22 Jun 2024 12:17:56 -0600 Subject: [PATCH 22/22] docs: fixes broken link and linting errors --- docs/troubleshooting/Performance.mdx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/troubleshooting/Performance.mdx b/docs/troubleshooting/Performance.mdx index 796b54cb07b0..68cafa2b3253 100644 --- a/docs/troubleshooting/Performance.mdx +++ b/docs/troubleshooting/Performance.mdx @@ -81,8 +81,8 @@ See [Glob pattern in parser's option "project" slows down linting](https://githu ## 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 degredations. +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.