diff --git a/docs/packages/TypeScript_ESTree.mdx b/docs/packages/TypeScript_ESTree.mdx index f589cb8f6605..df50232716f0 100644 --- a/docs/packages/TypeScript_ESTree.mdx +++ b/docs/packages/TypeScript_ESTree.mdx @@ -277,9 +277,14 @@ interface ParseAndGenerateServicesOptions extends ParseOptions { */ interface ProjectServiceOptions { /** - * Globs of files to allow running with the default inferred project settings. + * Globs of files to allow running with the default project compiler options. */ allowDefaultProjectForFiles?: string[]; + + /** + * Path to a TSConfig to use instead of TypeScript's default project configuration. + */ + defaultProject?: string; } interface ParserServices { diff --git a/packages/typescript-estree/src/create-program/createProjectService.ts b/packages/typescript-estree/src/create-program/createProjectService.ts index 1cf7f9d82992..c355033191c4 100644 --- a/packages/typescript-estree/src/create-program/createProjectService.ts +++ b/packages/typescript-estree/src/create-program/createProjectService.ts @@ -1,4 +1,6 @@ /* eslint-disable @typescript-eslint/no-empty-function -- for TypeScript APIs*/ +import os from 'node:os'; + import type * as ts from 'typescript/lib/tsserverlibrary'; import type { ProjectServiceOptions } from '../parser-options'; @@ -58,6 +60,42 @@ export function createProjectService( jsDocParsingMode, }); + if (typeof options === 'object' && options.defaultProject) { + let configRead; + + try { + configRead = tsserver.readConfigFile( + options.defaultProject, + system.readFile, + ); + } catch (error) { + throw new Error( + `Could not parse default project '${options.defaultProject}': ${(error as Error).message}`, + ); + } + + if (configRead.error) { + throw new Error( + `Could not read default project '${options.defaultProject}': ${tsserver.formatDiagnostic( + configRead.error, + { + getCurrentDirectory: system.getCurrentDirectory, + getCanonicalFileName: fileName => fileName, + getNewLine: () => os.EOL, + }, + )}`, + ); + } + + service.setCompilerOptionsForInferredProjects( + ( + configRead.config as { + compilerOptions: ts.server.protocol.InferredProjectCompilerOptions; + } + ).compilerOptions, + ); + } + return { allowDefaultProjectForFiles: typeof options === 'object' diff --git a/packages/typescript-estree/src/parser-options.ts b/packages/typescript-estree/src/parser-options.ts index 5df5b5b0e271..02c77dfe1a32 100644 --- a/packages/typescript-estree/src/parser-options.ts +++ b/packages/typescript-estree/src/parser-options.ts @@ -106,9 +106,14 @@ interface ParseOptions { */ export interface ProjectServiceOptions { /** - * Globs of files to allow running with the default inferred project settings. + * Globs of files to allow running with the default project compiler options. */ allowDefaultProjectForFiles?: string[]; + + /** + * Path to a TSConfig to use instead of TypeScript's default project configuration. + */ + defaultProject?: string; } interface ParseAndGenerateServicesOptions extends ParseOptions { diff --git a/packages/typescript-estree/tests/lib/createProjectService.test.ts b/packages/typescript-estree/tests/lib/createProjectService.test.ts index 9541dcd43942..a81106d03620 100644 --- a/packages/typescript-estree/tests/lib/createProjectService.test.ts +++ b/packages/typescript-estree/tests/lib/createProjectService.test.ts @@ -1,5 +1,21 @@ +import * as ts from 'typescript'; + import { createProjectService } from '../../src/create-program/createProjectService'; +const mockReadConfigFile = jest.fn(); +const mockSetCompilerOptionsForInferredProjects = jest.fn(); + +jest.mock('typescript/lib/tsserverlibrary', () => ({ + ...jest.requireActual('typescript/lib/tsserverlibrary'), + readConfigFile: mockReadConfigFile, + server: { + ProjectService: class { + setCompilerOptionsForInferredProjects = + mockSetCompilerOptionsForInferredProjects; + }, + }, +})); + describe('createProjectService', () => { it('sets allowDefaultProjectForFiles when options.allowDefaultProjectForFiles is defined', () => { const allowDefaultProjectForFiles = ['./*.js']; @@ -18,4 +34,64 @@ describe('createProjectService', () => { expect(settings.allowDefaultProjectForFiles).toBeUndefined(); }); + + it('throws an error when options.defaultProject is set and readConfigFile returns an error', () => { + mockReadConfigFile.mockReturnValue({ + error: { + category: ts.DiagnosticCategory.Error, + code: 1234, + file: ts.createSourceFile('./tsconfig.json', '', { + languageVersion: ts.ScriptTarget.Latest, + }), + start: 0, + length: 0, + messageText: 'Oh no!', + } satisfies ts.Diagnostic, + }); + + expect(() => + createProjectService( + { + allowDefaultProjectForFiles: ['file.js'], + defaultProject: './tsconfig.json', + }, + undefined, + ), + ).toThrow( + /Could not read default project '\.\/tsconfig.json': .+ error TS1234: Oh no!/, + ); + }); + + it('throws an error when options.defaultProject is set and readConfigFile throws an error', () => { + mockReadConfigFile.mockImplementation(() => { + throw new Error('Oh no!'); + }); + + expect(() => + createProjectService( + { + allowDefaultProjectForFiles: ['file.js'], + defaultProject: './tsconfig.json', + }, + undefined, + ), + ).toThrow("Could not parse default project './tsconfig.json': Oh no!"); + }); + + it('uses the default projects compiler options when options.defaultProject is set and readConfigFile succeeds', () => { + const compilerOptions = { strict: true }; + mockReadConfigFile.mockReturnValue({ config: { compilerOptions } }); + + const { service } = createProjectService( + { + allowDefaultProjectForFiles: ['file.js'], + defaultProject: './tsconfig.json', + }, + undefined, + ); + + expect(service.setCompilerOptionsForInferredProjects).toHaveBeenCalledWith( + compilerOptions, + ); + }); });