diff --git a/.github/ISSUE_TEMPLATE/06-bug-report-other.yaml b/.github/ISSUE_TEMPLATE/06-bug-report-other.yaml index 59b8783d1e75..69fc7d6ef7d7 100644 --- a/.github/ISSUE_TEMPLATE/06-bug-report-other.yaml +++ b/.github/ISSUE_TEMPLATE/06-bug-report-other.yaml @@ -40,8 +40,10 @@ body: - ast-spec - eslint-plugin - parser + - project-service - rule-tester - scope-manager + - tsconfig-utils - type-utils - types - typescript-eslint diff --git a/.github/ISSUE_TEMPLATE/07-enhancement-other.yaml b/.github/ISSUE_TEMPLATE/07-enhancement-other.yaml index 604ce5468161..7250ea66e0f3 100644 --- a/.github/ISSUE_TEMPLATE/07-enhancement-other.yaml +++ b/.github/ISSUE_TEMPLATE/07-enhancement-other.yaml @@ -26,8 +26,10 @@ body: - ast-spec - eslint-plugin - parser + - project-service - rule-tester - scope-manager + - tsconfig-utils - type-utils - types - typescript-eslint diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 398c05e17baa..cfcc5d5409df 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -170,8 +170,10 @@ jobs: 'eslint-plugin', 'eslint-plugin-internal', 'parser', + 'project-service', 'rule-tester', 'scope-manager', + 'tsconfig-utils', 'type-utils', 'typescript-eslint', 'typescript-estree', diff --git a/.github/workflows/semantic-pr-titles.yml b/.github/workflows/semantic-pr-titles.yml index 3a3c4a2c3164..b206297b3896 100644 --- a/.github/workflows/semantic-pr-titles.yml +++ b/.github/workflows/semantic-pr-titles.yml @@ -30,8 +30,10 @@ jobs: eslint-plugin eslint-plugin-internal parser + project-service rule-tester scope-manager + tsconfig-utils type-utils types typescript-eslint diff --git a/docs/packages/Project_Service.mdx b/docs/packages/Project_Service.mdx new file mode 100644 index 000000000000..7f841f3560b1 --- /dev/null +++ b/docs/packages/Project_Service.mdx @@ -0,0 +1,39 @@ +--- +id: project-service +sidebar_label: project-service +toc_max_heading_level: 3 +--- + +import GeneratedDocs from './project-service/generated/index.md'; + +# `@typescript-eslint/project-service` + + + +> Standalone TypeScript project service wrapper for linting ✨ + +The typescript-eslint Project Service is a wrapper around TypeScript's "project service" APIs. +These APIs are what editors such as VS Code use to programmatically "open" files and generate TypeScript programs for type information. + +:::note +See [Announcing typescript-eslint v8 > Project Service](/blog/announcing-typescript-eslint-v8#project-service) for more details on how lint users interact with the Project Service. +::: + +```ts +import { createProjectService } from '@typescript-eslint/project-service'; + +const filePathAbsolute = '/path/to/your/project/index.ts'; +const { service } = createProjectService(); + +service.openClientFile(filePathAbsolute); + +const scriptInfo = service.getScriptInfo(filePathAbsolute)!; +const program = service + .getDefaultProjectForFile(scriptInfo.fileName, true)! + .getLanguageService(true) + .getProgram()!; +``` + +The following documentation is auto-generated from source code. + + diff --git a/docs/packages/TSConfig_Utilsx.mdx b/docs/packages/TSConfig_Utilsx.mdx new file mode 100644 index 000000000000..1ce3361c7fa4 --- /dev/null +++ b/docs/packages/TSConfig_Utilsx.mdx @@ -0,0 +1,17 @@ +--- +id: tsconfig-utils +sidebar_label: tsconfig-utils +toc_max_heading_level: 3 +--- + +import GeneratedDocs from './tsconfig-utils/generated/index.md'; + +# `@typescript-eslint/tsconfig-utils` + + + +> Utilities for collecting TSConfigs for linting scenarios ✨ + +The following documentation is auto-generated from source code. + + diff --git a/packages/project-service/LICENSE b/packages/project-service/LICENSE new file mode 100644 index 000000000000..310a18f8a6cb --- /dev/null +++ b/packages/project-service/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 typescript-eslint and other contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/packages/project-service/README.md b/packages/project-service/README.md new file mode 100644 index 000000000000..916084c3df70 --- /dev/null +++ b/packages/project-service/README.md @@ -0,0 +1,12 @@ +# `@typescript-eslint/project-service` + +> Standalone TypeScript project service wrapper for linting. + +[![NPM Version](https://img.shields.io/npm/v/@typescript-eslint/utils.svg?style=flat-square)](https://www.npmjs.com/package/@typescript-eslint/utils) +[![NPM Downloads](https://img.shields.io/npm/dm/@typescript-eslint/utils.svg?style=flat-square)](https://www.npmjs.com/package/@typescript-eslint/utils) + +A standalone export of the "Project Service" that powers typed linting for typescript-eslint. + +> See https://typescript-eslint.io for general documentation on typescript-eslint, the tooling that allows you to run ESLint and Prettier on TypeScript code. + + diff --git a/packages/project-service/package.json b/packages/project-service/package.json new file mode 100644 index 000000000000..1752e7b2a449 --- /dev/null +++ b/packages/project-service/package.json @@ -0,0 +1,64 @@ +{ + "name": "@typescript-eslint/project-service", + "version": "8.32.1", + "description": "Standalone TypeScript project service wrapper for linting.", + "files": [ + "dist", + "!*.tsbuildinfo", + "package.json", + "README.md", + "LICENSE" + ], + "type": "commonjs", + "exports": { + ".": { + "types": "./dist/index.d.ts", + "default": "./dist/index.js" + }, + "./package.json": "./package.json" + }, + "types": "./dist/index.d.ts", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "repository": { + "type": "git", + "url": "https://github.com/typescript-eslint/typescript-eslint.git", + "directory": "packages/project-service" + }, + "bugs": { + "url": "https://github.com/typescript-eslint/typescript-eslint/issues" + }, + "homepage": "https://typescript-eslint.io", + "license": "MIT", + "keywords": [ + "eslint", + "typescript", + "estree" + ], + "scripts": { + "build": "tsc -b tsconfig.build.json", + "clean": "tsc -b tsconfig.build.json --clean", + "postclean": "rimraf dist/ src/generated/ coverage/", + "format": "prettier --write \"./**/*.{ts,mts,cts,tsx,js,mjs,cjs,jsx,json,md,css}\" --ignore-path ../../.prettierignore", + "lint": "npx nx lint", + "test": "vitest --run --config=$INIT_CWD/vitest.config.mts", + "check-types": "npx nx typecheck" + }, + "dependencies": { + "@typescript-eslint/tsconfig-utils": "^8.32.1", + "@typescript-eslint/types": "^8.32.1", + "debug": "^4.3.4" + }, + "devDependencies": { + "@vitest/coverage-v8": "^3.1.2", + "prettier": "^3.2.5", + "rimraf": "*", + "typescript": "*", + "vitest": "^3.1.2" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } +} diff --git a/packages/project-service/project.json b/packages/project-service/project.json new file mode 100644 index 000000000000..7d029ba93a59 --- /dev/null +++ b/packages/project-service/project.json @@ -0,0 +1,13 @@ +{ + "name": "project-service", + "$schema": "../../node_modules/nx/schemas/project-schema.json", + "projectType": "library", + "root": "packages/project-service", + "sourceRoot": "packages/project-service/src", + "targets": { + "lint": { + "executor": "@nx/eslint:lint", + "outputs": ["{options.outputFile}"] + } + } +} diff --git a/packages/typescript-estree/src/create-program/createProjectService.ts b/packages/project-service/src/createProjectService.ts similarity index 62% rename from packages/typescript-estree/src/create-program/createProjectService.ts rename to packages/project-service/src/createProjectService.ts index 26b2c5421612..ecf075ef7c5f 100644 --- a/packages/typescript-estree/src/create-program/createProjectService.ts +++ b/packages/project-service/src/createProjectService.ts @@ -1,57 +1,104 @@ -/* eslint-disable @typescript-eslint/no-empty-function -- for TypeScript APIs*/ +import type { ProjectServiceOptions } from '@typescript-eslint/types'; import type * as ts from 'typescript/lib/tsserverlibrary'; import debug from 'debug'; -import type { ProjectServiceOptions } from '../parser-options'; - -import { getParsedConfigFile } from './getParsedConfigFile'; -import { validateDefaultProjectForFilesGlob } from './validateDefaultProjectForFilesGlob'; +import { getParsedConfigFileFromTSServer } from './getParsedConfigFileFromTSServer.js'; const DEFAULT_PROJECT_MATCHED_FILES_THRESHOLD = 8; -const log = debug( - 'typescript-eslint:typescript-estree:create-program:createProjectService', -); -const logTsserverErr = debug( - 'typescript-eslint:typescript-estree:tsserver:err', -); +const log = debug('typescript-eslint:project-service:createProjectService'); +const logTsserverErr = debug('typescript-eslint:project-service:tsserver:err'); const logTsserverInfo = debug( - 'typescript-eslint:typescript-estree:tsserver:info', + 'typescript-eslint:project-service:tsserver:info', ); const logTsserverPerf = debug( - 'typescript-eslint:typescript-estree:tsserver:perf', + 'typescript-eslint:project-service:tsserver:perf', ); const logTsserverEvent = debug( - 'typescript-eslint:typescript-estree:tsserver:event', + 'typescript-eslint:project-service:tsserver:event', ); +// For TypeScript APIs that expect a function to be passed in +// eslint-disable-next-line @typescript-eslint/no-empty-function const doNothing = (): void => {}; const createStubFileWatcher = (): ts.FileWatcher => ({ close: doNothing, }); +/** + * Shortcut type to refer to TypeScript's server ProjectService. + */ export type TypeScriptProjectService = ts.server.ProjectService; -export interface ProjectServiceSettings { +/** + * A created Project Service instance, as well as metadata on its creation. + */ +export interface ProjectServiceAndMetadata { + /** + * Files allowed to be loaded from the default project, if any were specified. + */ allowDefaultProject: string[] | undefined; + + /** + * The performance.now() timestamp of the last reload of the project service. + */ lastReloadTimestamp: number; + + /** + * The maximum number of files that can be matched by the default project. + */ maximumDefaultProjectFileMatchCount: number; + + /** + * The created TypeScript Project Service instance. + */ service: TypeScriptProjectService; } -export function createProjectService( - optionsRaw: boolean | ProjectServiceOptions | undefined, - jsDocParsingMode: ts.JSDocParsingMode | undefined, - tsconfigRootDir: string | undefined, -): ProjectServiceSettings { - const optionsRawObject = typeof optionsRaw === 'object' ? optionsRaw : {}; +/** + * Settings to create a new Project Service instance with {@link createProjectService}. + */ +export interface CreateProjectServiceSettings { + /** + * Granular options to configure the project service. + */ + options?: ProjectServiceOptions; + + /** + * How aggressively (and slowly) to parse JSDoc comments. + */ + jsDocParsingMode?: ts.JSDocParsingMode; + + /** + * Root directory for the tsconfig.json file, if not the current directory. + */ + tsconfigRootDir?: string; +} + +/** + * Creates a new Project Service instance, as well as metadata on its creation. + * @param settings Settings to create a new Project Service instance. + * @returns A new Project Service instance, as well as metadata on its creation. + * @example + * ```ts + * import { createProjectService } from '@typescript-eslint/project-service'; + * + * const { service } = createProjectService(); + * + * service.openClientFile('index.ts'); + * ``` + */ +export function createProjectService({ + jsDocParsingMode, + options: optionsRaw = {}, + tsconfigRootDir, +}: CreateProjectServiceSettings = {}): ProjectServiceAndMetadata { const options = { defaultProject: 'tsconfig.json', - ...optionsRawObject, + ...optionsRaw, }; - validateDefaultProjectForFilesGlob(options.allowDefaultProject); // We import this lazily to avoid its cost for users who don't use the service // TODO: Once we drop support for TS<5.3 we can import from "typescript" directly @@ -119,7 +166,7 @@ export function createProjectService( startGroup: doNothing, }; - log('Creating project service with: %o', options); + log('Creating Project Service with: %o', options); const service = new tsserver.server.ProjectService({ cancellationToken: { isCancellationRequested: (): boolean => false }, @@ -143,26 +190,18 @@ export function createProjectService( }); log('Enabling default project: %s', options.defaultProject); - let configFile: ts.ParsedCommandLine | undefined; - try { - configFile = getParsedConfigFile( - tsserver, - options.defaultProject, - tsconfigRootDir, - ); - } catch (error) { - if (optionsRawObject.defaultProject) { - throw new Error( - `Could not read project service default project '${options.defaultProject}': ${(error as Error).message}`, - ); - } - } + const configFile = getParsedConfigFileFromTSServer( + tsserver, + options.defaultProject, + !!optionsRaw.defaultProject, + tsconfigRootDir, + ); if (configFile) { service.setCompilerOptionsForInferredProjects( // NOTE: The inferred projects API is not intended for source files when a tsconfig - // exists. There is no API that generates an InferredProjectCompilerOptions suggesting + // exists. There is no API that generates an InferredProjectCompilerOptions suggesting // it is meant for hard coded options passed in. Hard asserting as a work around. // See https://github.com/microsoft/TypeScript/blob/27bcd4cb5a98bce46c9cdd749752703ead021a4b/src/server/protocol.ts#L1904 configFile.options as ts.server.protocol.InferredProjectCompilerOptions, @@ -178,3 +217,5 @@ export function createProjectService( service, }; } + +export { type ProjectServiceOptions } from '@typescript-eslint/types'; diff --git a/packages/project-service/src/getParsedConfigFileFromTSServer.ts b/packages/project-service/src/getParsedConfigFileFromTSServer.ts new file mode 100644 index 000000000000..c2e2f83612c4 --- /dev/null +++ b/packages/project-service/src/getParsedConfigFileFromTSServer.ts @@ -0,0 +1,22 @@ +import type * as ts from 'typescript/lib/tsserverlibrary'; + +import { getParsedConfigFile } from '@typescript-eslint/tsconfig-utils'; + +export function getParsedConfigFileFromTSServer( + tsserver: typeof ts, + defaultProject: string, + throwOnFailure: boolean, + tsconfigRootDir?: string, +): ts.ParsedCommandLine | undefined { + try { + return getParsedConfigFile(tsserver, defaultProject, tsconfigRootDir); + } catch (error) { + if (throwOnFailure) { + throw new Error( + `Could not read Project Service default project '${defaultProject}': ${(error as Error).message}`, + ); + } + } + + return undefined; +} diff --git a/packages/project-service/src/index.ts b/packages/project-service/src/index.ts new file mode 100644 index 000000000000..943c04088b69 --- /dev/null +++ b/packages/project-service/src/index.ts @@ -0,0 +1 @@ +export * from './createProjectService'; diff --git a/packages/project-service/tests/createProjectService.test.ts b/packages/project-service/tests/createProjectService.test.ts new file mode 100644 index 000000000000..abc329108416 --- /dev/null +++ b/packages/project-service/tests/createProjectService.test.ts @@ -0,0 +1,277 @@ +/* eslint-disable @typescript-eslint/no-require-imports */ +import debug from 'debug'; +import * as ts from 'typescript'; + +import { createProjectService } from '../src/createProjectService.js'; + +const mockGetParsedConfigFileFromTSServer = vi.fn(); + +vi.mock('../src/getParsedConfigFileFromTSServer.js', () => ({ + get getParsedConfigFileFromTSServer() { + return mockGetParsedConfigFileFromTSServer; + }, +})); + +const mockSetCompilerOptionsForInferredProjects = vi.fn(); +const mockSetHostConfiguration = vi.fn(); + +vi.mock(import('../src/createProjectService.js'), async importOriginal => { + const actual = await importOriginal(); + + return { + ...actual, + createProjectService: vi + .fn(actual.createProjectService) + .mockImplementation((...args) => { + const projectServiceSettings = actual.createProjectService(...args); + const service = + projectServiceSettings.service as typeof projectServiceSettings.service & { + eventHandler: ts.server.ProjectServiceEventHandler | undefined; + }; + + if (service.eventHandler) { + service.eventHandler({ + eventName: ts.server.ProjectLoadingStartEvent, + } as ts.server.ProjectLoadingStartEvent); + } + + return projectServiceSettings; + }), + }; +}); + +describe(createProjectService, () => { + const processStderrWriteSpy = vi + .spyOn(process.stderr, 'write') + .mockImplementation(() => true); + + beforeEach(() => { + const { ProjectService } = require('typescript/lib/tsserverlibrary').server; + + ProjectService.prototype.setCompilerOptionsForInferredProjects = + mockSetCompilerOptionsForInferredProjects; + ProjectService.prototype.setHostConfiguration = mockSetHostConfiguration; + }); + + afterEach(() => { + debug.disable(); + vi.clearAllMocks(); + }); + + describe('defaultProject', () => { + it('sets allowDefaultProject when options.allowDefaultProject is defined', () => { + const allowDefaultProject = ['./*.js']; + + const settings = createProjectService({ + options: { allowDefaultProject }, + }); + + expect(settings.allowDefaultProject).toBe(allowDefaultProject); + }); + + it('does not set allowDefaultProject when options.allowDefaultProject is not defined', () => { + const settings = createProjectService(); + + assert.isUndefined(settings.allowDefaultProject); + }); + + it('uses the default project compiler options when options.defaultProject is set', () => { + const compilerOptions: ts.CompilerOptions = { strict: true }; + mockGetParsedConfigFileFromTSServer.mockReturnValueOnce({ + errors: [], + fileNames: [], + options: compilerOptions, + }); + + const defaultProject = 'tsconfig.eslint.json'; + + createProjectService({ + options: { + allowDefaultProject: ['file.js'], + defaultProject, + }, + }); + + expect(mockSetCompilerOptionsForInferredProjects).toHaveBeenCalledWith( + compilerOptions, + ); + + expect(mockGetParsedConfigFileFromTSServer).toHaveBeenCalledWith( + expect.any(Object), + defaultProject, + true, + undefined, + ); + }); + }); + + it('uses tsconfigRootDir as getParsedConfigFile projectDirectory when provided', async () => { + const compilerOptions: ts.CompilerOptions = { strict: true }; + const tsconfigRootDir = 'path/to/repo'; + mockGetParsedConfigFileFromTSServer.mockReturnValueOnce({ + errors: [], + fileNames: [], + options: compilerOptions, + }); + + const { service } = createProjectService({ + options: { allowDefaultProject: ['file.js'] }, + tsconfigRootDir, + }); + + expect(service.setCompilerOptionsForInferredProjects).toHaveBeenCalledWith( + compilerOptions, + ); + + expect(mockGetParsedConfigFileFromTSServer).toHaveBeenCalledWith( + (await import('typescript/lib/tsserverlibrary.js')).default, + 'tsconfig.json', + false, + tsconfigRootDir, + ); + }); + + it('uses the default projects error debugger for error messages when enabled', () => { + debug.enable('typescript-eslint:project-service:tsserver:err'); + const { service } = createProjectService(); + + service.logger.msg('foo', ts.server.Msg.Err); + + const newLocal = service.logger.loggingEnabled(); + expect(newLocal).toBe(true); + expect(processStderrWriteSpy).toHaveBeenCalledWith( + expect.stringMatching( + /^.*typescript-eslint:project-service:tsserver:err foo\n$/, + ), + ); + }); + + it('does not use the default projects error debugger for error messages when disabled', () => { + const { service } = createProjectService(); + + service.logger.msg('foo', ts.server.Msg.Err); + + expect(service.logger.loggingEnabled()).toBe(false); + expect(processStderrWriteSpy).not.toHaveBeenCalled(); + }); + + it('uses the default projects info debugger for info messages when enabled', () => { + debug.enable('typescript-eslint:project-service:tsserver:info'); + + const { service } = createProjectService(); + + service.logger.info('foo'); + + expect(service.logger.loggingEnabled()).toBe(true); + expect(processStderrWriteSpy).toHaveBeenCalledWith( + expect.stringMatching( + /^.*typescript-eslint:project-service:tsserver:info foo\n$/, + ), + ); + }); + + it('does not use the default projects info debugger for info messages when disabled', () => { + const { service } = createProjectService(); + + service.logger.info('foo'); + + expect(service.logger.loggingEnabled()).toBe(false); + expect(processStderrWriteSpy).not.toHaveBeenCalled(); + }); + + it('uses the default projects perf debugger for perf messages when enabled', () => { + debug.enable('typescript-eslint:project-service:tsserver:perf'); + const { service } = createProjectService(); + + service.logger.perftrc('foo'); + + expect(service.logger.loggingEnabled()).toBe(true); + expect(processStderrWriteSpy).toHaveBeenCalledWith( + expect.stringMatching( + /^.*typescript-eslint:project-service:tsserver:perf foo\n$/, + ), + ); + }); + + it('does not use the default projects perf debugger for perf messages when disabled', () => { + const { service } = createProjectService(); + + service.logger.perftrc('foo'); + + expect(service.logger.loggingEnabled()).toBe(false); + expect(processStderrWriteSpy).not.toHaveBeenCalled(); + }); + + it('enables all log levels for the default projects logger', () => { + const { service } = createProjectService(); + + expect(service.logger.hasLevel(ts.server.LogLevel.terse)).toBe(true); + expect(service.logger.hasLevel(ts.server.LogLevel.normal)).toBe(true); + expect(service.logger.hasLevel(ts.server.LogLevel.requestTime)).toBe(true); + expect(service.logger.hasLevel(ts.server.LogLevel.verbose)).toBe(true); + }); + + it('does not return a log filename with the default projects logger', () => { + const { service } = createProjectService(); + + assert.isUndefined(service.logger.getLogFileName()); + }); + + it('uses the default projects event debugger for event handling when enabled', () => { + debug.enable('typescript-eslint:project-service:tsserver:event'); + + createProjectService(); + + expect(processStderrWriteSpy).toHaveBeenCalledWith( + expect.stringMatching( + /^.*typescript-eslint:project-service:tsserver:event { eventName: 'projectLoadingStart' }\n$/, + ), + ); + }); + + it('does not use the default projects event debugger for event handling when disabled', () => { + createProjectService(); + + expect(processStderrWriteSpy).not.toHaveBeenCalled(); + }); + + it('provides a stub require to the host system when loadTypeScriptPlugins is falsy', () => { + const { service } = createProjectService({}); + + const required = service.host.require?.('', ''); + + expect(required).toStrictEqual({ + error: { + message: + 'TypeScript plugins are not required when using parserOptions.projectService.', + }, + module: undefined, + }); + }); + + it('does not provide a require to the host system when loadTypeScriptPlugins is truthy', async () => { + const { service } = createProjectService({ + options: { loadTypeScriptPlugins: true }, + }); + + expect(service.host.require).toBe( + ( + await vi.importActual>>( + 'typescript/lib/tsserverlibrary.js', + ) + ).sys.require, + ); + }); + + it('sets a host configuration', () => { + createProjectService({ + options: { allowDefaultProject: ['file.js'] }, + }); + + expect(mockSetHostConfiguration).toHaveBeenCalledWith({ + preferences: { + includePackageJsonAutoImports: 'off', + }, + }); + }); +}); diff --git a/packages/project-service/tests/getParsedConfigFileFromTSServer.test.ts b/packages/project-service/tests/getParsedConfigFileFromTSServer.test.ts new file mode 100644 index 000000000000..f6f868bd0096 --- /dev/null +++ b/packages/project-service/tests/getParsedConfigFileFromTSServer.test.ts @@ -0,0 +1,61 @@ +import type * as ts from 'typescript/lib/tsserverlibrary'; + +import { getParsedConfigFileFromTSServer } from '../src/getParsedConfigFileFromTSServer'; + +const mockGetParsedConfigFile = vi.fn(); + +vi.mock('@typescript-eslint/tsconfig-utils', () => ({ + get getParsedConfigFile() { + return mockGetParsedConfigFile; + }, +})); + +const mockConfigFile = { + fileNames: [], +}; + +const mockTSServer = {} as unknown as typeof ts; +const mockTSConfigRootDir = '/mock/tsconfig/root/dir'; + +describe(getParsedConfigFileFromTSServer, () => { + it('returns the parsed config file when getParsedConfigFile succeeds', () => { + mockGetParsedConfigFile.mockReturnValueOnce(mockConfigFile); + + const actual = getParsedConfigFileFromTSServer( + mockTSServer, + 'tsconfig.json', + false, + mockTSConfigRootDir, + ); + + expect(actual).toBe(mockConfigFile); + }); + + it('returns undefined when getParsedConfigFile fails and throwOnFailure is false', () => { + mockGetParsedConfigFile.mockImplementationOnce(() => { + throw new Error('Oh no!'); + }); + + const actual = getParsedConfigFileFromTSServer( + mockTSServer, + 'tsconfig.json', + false, + ); + + expect(actual).toBeUndefined(); + }); + + it('throws the error when getParsedConfigFile fails and throwOnFailure is true', () => { + mockGetParsedConfigFile.mockImplementationOnce(() => { + throw new Error('Oh no!'); + }); + + expect(() => + getParsedConfigFileFromTSServer(mockTSServer, 'tsconfig.json', true), + ).toThrow( + new Error( + `Could not read Project Service default project 'tsconfig.json': Oh no!`, + ), + ); + }); +}); diff --git a/packages/project-service/tsconfig.build.json b/packages/project-service/tsconfig.build.json new file mode 100644 index 000000000000..d9cc02f3ba7c --- /dev/null +++ b/packages/project-service/tsconfig.build.json @@ -0,0 +1,20 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "baseUrl": ".", + "rootDir": "src", + "outDir": "dist", + "tsBuildInfoFile": "dist/tsconfig.build.tsbuildinfo", + "emitDeclarationOnly": false, + "types": ["node"] + }, + "include": ["src/**/*.ts"], + "references": [ + { + "path": "../tsconfig-utils/tsconfig.build.json" + }, + { + "path": "../types/tsconfig.build.json" + } + ] +} diff --git a/packages/project-service/tsconfig.json b/packages/project-service/tsconfig.json new file mode 100644 index 000000000000..7d8d748caa17 --- /dev/null +++ b/packages/project-service/tsconfig.json @@ -0,0 +1,22 @@ +{ + "extends": "../../tsconfig.base.json", + "files": [], + "include": [], + "references": [ + { + "path": "../tsconfig-utils" + }, + { + "path": "../types" + }, + { + "path": "./tsconfig.build.json" + }, + { + "path": "./tsconfig.tools.json" + }, + { + "path": "./tsconfig.spec.json" + } + ] +} diff --git a/packages/project-service/tsconfig.spec.json b/packages/project-service/tsconfig.spec.json new file mode 100644 index 000000000000..872d3c655b27 --- /dev/null +++ b/packages/project-service/tsconfig.spec.json @@ -0,0 +1,18 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "outDir": "../../dist/out-tsc/packages/types/vitest", + "resolveJsonModule": true, + "types": ["vitest/globals", "vitest/importMeta"] + }, + "include": ["vitest.config.mts", "package.json", "tests"], + "exclude": ["**/fixtures/**"], + "references": [ + { + "path": "./tsconfig.build.json" + }, + { + "path": "../../tsconfig.spec.json" + } + ] +} diff --git a/packages/project-service/tsconfig.tools.json b/packages/project-service/tsconfig.tools.json new file mode 100644 index 000000000000..cf7f584666a9 --- /dev/null +++ b/packages/project-service/tsconfig.tools.json @@ -0,0 +1,10 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "outDir": "../../dist/out-tsc/packages/types/tools", + "module": "NodeNext", + "types": ["node"] + }, + "include": ["tools"], + "references": [] +} diff --git a/packages/project-service/vitest.config.mts b/packages/project-service/vitest.config.mts new file mode 100644 index 000000000000..136e4f77b392 --- /dev/null +++ b/packages/project-service/vitest.config.mts @@ -0,0 +1,22 @@ +import * as path from 'node:path'; +import { defineConfig, mergeConfig } from 'vitest/config'; + +import { vitestBaseConfig } from '../../vitest.config.base.mjs'; +import packageJson from './package.json' with { type: 'json' }; + +const vitestConfig = mergeConfig( + vitestBaseConfig, + + defineConfig({ + root: import.meta.dirname, + + test: { + dir: path.join(import.meta.dirname, 'tests'), + name: packageJson.name.replace('@typescript-eslint/', ''), + passWithNoTests: true, + root: import.meta.dirname, + }, + }), +); + +export default vitestConfig; diff --git a/packages/tsconfig-utils/LICENSE b/packages/tsconfig-utils/LICENSE new file mode 100644 index 000000000000..310a18f8a6cb --- /dev/null +++ b/packages/tsconfig-utils/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 typescript-eslint and other contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/packages/tsconfig-utils/README.md b/packages/tsconfig-utils/README.md new file mode 100644 index 000000000000..c9eb2fdff88e --- /dev/null +++ b/packages/tsconfig-utils/README.md @@ -0,0 +1,12 @@ +# `@typescript-eslint/tsconfig-utils` + +> Utilities for collecting TSConfigs for linting scenarios. + +[![NPM Version](https://img.shields.io/npm/v/@typescript-eslint/utils.svg?style=flat-square)](https://www.npmjs.com/package/@typescript-eslint/utils) +[![NPM Downloads](https://img.shields.io/npm/dm/@typescript-eslint/utils.svg?style=flat-square)](https://www.npmjs.com/package/@typescript-eslint/utils) + +The utilities in this package are separated from `@typescript-eslint/utils` so that they do not have a dependency on `eslint` or `@typescript-eslint/typescript-estree`. + +> See https://typescript-eslint.io for general documentation on typescript-eslint, the tooling that allows you to run ESLint and Prettier on TypeScript code. + + diff --git a/packages/tsconfig-utils/package.json b/packages/tsconfig-utils/package.json new file mode 100644 index 000000000000..9b7498f1cf14 --- /dev/null +++ b/packages/tsconfig-utils/package.json @@ -0,0 +1,60 @@ +{ + "name": "@typescript-eslint/tsconfig-utils", + "version": "8.32.1", + "description": "Utilities for collecting TSConfigs for linting scenarios.", + "files": [ + "dist", + "!*.tsbuildinfo", + "package.json", + "README.md", + "LICENSE" + ], + "type": "commonjs", + "exports": { + ".": { + "types": "./dist/index.d.ts", + "default": "./dist/index.js" + }, + "./package.json": "./package.json" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "repository": { + "type": "git", + "url": "https://github.com/typescript-eslint/typescript-eslint.git", + "directory": "packages/tsconfig-utils" + }, + "bugs": { + "url": "https://github.com/typescript-eslint/typescript-eslint/issues" + }, + "homepage": "https://typescript-eslint.io", + "license": "MIT", + "keywords": [ + "eslint", + "typescript", + "estree" + ], + "scripts": { + "build": "tsc -b tsconfig.build.json", + "clean": "rimraf dist/ coverage/", + "format": "prettier --write \"./**/*.{ts,mts,cts,tsx,js,mjs,cjs,jsx,json,md,css}\" --ignore-path ../../.prettierignore", + "lint": "npx nx lint", + "test": "vitest --run --config=$INIT_CWD/vitest.config.mts", + "check-types": "npx nx typecheck" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <5.9.0" + }, + "devDependencies": { + "@vitest/coverage-v8": "^3.1.2", + "prettier": "^3.2.5", + "rimraf": "*", + "typescript": "*", + "vitest": "^3.1.2" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } +} diff --git a/packages/tsconfig-utils/project.json b/packages/tsconfig-utils/project.json new file mode 100644 index 000000000000..b435c1f765f9 --- /dev/null +++ b/packages/tsconfig-utils/project.json @@ -0,0 +1,16 @@ +{ + "name": "tsconfig-utils", + "$schema": "../../node_modules/nx/schemas/project-schema.json", + "projectType": "library", + "root": "packages/tsconfig-utils", + "sourceRoot": "packages/tsconfig-utils/src", + "targets": { + "lint": { + "executor": "@nx/eslint:lint", + "outputs": ["{options.outputFile}"] + }, + "test": { + "executor": "@nx/vite:test" + } + } +} diff --git a/packages/tsconfig-utils/src/compilerOptions.ts b/packages/tsconfig-utils/src/compilerOptions.ts new file mode 100644 index 000000000000..1183ce149911 --- /dev/null +++ b/packages/tsconfig-utils/src/compilerOptions.ts @@ -0,0 +1,13 @@ +import type * as ts from 'typescript'; + +/** + * Compiler options required to avoid critical functionality issues + */ +export const CORE_COMPILER_OPTIONS = { + // Required to avoid parse from causing emit to occur + noEmit: true, + + // Flags required to make no-unused-vars work + noUnusedLocals: true, + noUnusedParameters: true, +} satisfies ts.CompilerOptions; diff --git a/packages/typescript-estree/src/create-program/getParsedConfigFile.ts b/packages/tsconfig-utils/src/getParsedConfigFile.ts similarity index 94% rename from packages/typescript-estree/src/create-program/getParsedConfigFile.ts rename to packages/tsconfig-utils/src/getParsedConfigFile.ts index 6efebadf48d2..a6dafe5b1daa 100644 --- a/packages/typescript-estree/src/create-program/getParsedConfigFile.ts +++ b/packages/tsconfig-utils/src/getParsedConfigFile.ts @@ -3,10 +3,10 @@ import type * as ts from 'typescript/lib/tsserverlibrary'; import * as fs from 'node:fs'; import * as path from 'node:path'; -import { CORE_COMPILER_OPTIONS } from './shared'; +import { CORE_COMPILER_OPTIONS } from './compilerOptions'; /** - * Utility offered by parser to help consumers parse a config file. + * Parses a TSConfig file using the same logic as tsserver. * * @param configFile the path to the tsconfig.json file, relative to `projectDirectory` * @param projectDirectory the project directory to use as the CWD, defaults to `process.cwd()` diff --git a/packages/tsconfig-utils/src/index.ts b/packages/tsconfig-utils/src/index.ts new file mode 100644 index 000000000000..3213fc4e40a1 --- /dev/null +++ b/packages/tsconfig-utils/src/index.ts @@ -0,0 +1,2 @@ +export * from './compilerOptions'; +export * from './getParsedConfigFile'; diff --git a/packages/typescript-estree/tests/lib/getParsedConfigFile.test.ts b/packages/tsconfig-utils/tests/lib/getParsedConfigFile.test.ts similarity index 97% rename from packages/typescript-estree/tests/lib/getParsedConfigFile.test.ts rename to packages/tsconfig-utils/tests/lib/getParsedConfigFile.test.ts index aa013abef444..d26919b52b5c 100644 --- a/packages/typescript-estree/tests/lib/getParsedConfigFile.test.ts +++ b/packages/tsconfig-utils/tests/lib/getParsedConfigFile.test.ts @@ -1,7 +1,7 @@ import path from 'node:path'; import * as ts from 'typescript'; -import { getParsedConfigFile } from '../../src/create-program/getParsedConfigFile'; +import { getParsedConfigFile } from '../../src/getParsedConfigFile'; const mockGetParsedCommandLineOfConfigFile = vi.fn(); diff --git a/packages/tsconfig-utils/tsconfig.build.json b/packages/tsconfig-utils/tsconfig.build.json new file mode 100644 index 000000000000..2250b25126e4 --- /dev/null +++ b/packages/tsconfig-utils/tsconfig.build.json @@ -0,0 +1,14 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "baseUrl": ".", + "rootDir": "src", + "outDir": "dist", + "tsBuildInfoFile": "dist/tsconfig.build.tsbuildinfo", + "emitDeclarationOnly": false, + "types": ["node"] + }, + "include": ["src/**/*.ts", "typings"], + "exclude": ["vitest.config.mts", "src/**/*.spec.ts", "src/**/*.test.ts"], + "references": [] +} diff --git a/packages/tsconfig-utils/tsconfig.json b/packages/tsconfig-utils/tsconfig.json new file mode 100644 index 000000000000..d4d0929e1955 --- /dev/null +++ b/packages/tsconfig-utils/tsconfig.json @@ -0,0 +1,13 @@ +{ + "extends": "../../tsconfig.base.json", + "files": [], + "include": [], + "references": [ + { + "path": "./tsconfig.build.json" + }, + { + "path": "./tsconfig.spec.json" + } + ] +} diff --git a/packages/tsconfig-utils/tsconfig.spec.json b/packages/tsconfig-utils/tsconfig.spec.json new file mode 100644 index 000000000000..21fcf7e24f3f --- /dev/null +++ b/packages/tsconfig-utils/tsconfig.spec.json @@ -0,0 +1,26 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "outDir": "../../dist/out-tsc/packages/tsconfig-utils", + "module": "NodeNext", + "resolveJsonModule": true, + "types": ["node", "vitest/globals", "vitest/importMeta"] + }, + "include": [ + "vitest.config.mts", + "package.json", + "src/**/*.test.ts", + "src/**/*.spec.ts", + "src/**/*.d.ts", + "tests" + ], + "exclude": ["**/fixtures/**"], + "references": [ + { + "path": "./tsconfig.build.json" + }, + { + "path": "../../tsconfig.spec.json" + } + ] +} diff --git a/packages/tsconfig-utils/vitest.config.mts b/packages/tsconfig-utils/vitest.config.mts new file mode 100644 index 000000000000..9cadb9dea2bb --- /dev/null +++ b/packages/tsconfig-utils/vitest.config.mts @@ -0,0 +1,26 @@ +import * as path from 'node:path'; +import { defineProject, mergeConfig } from 'vitest/config'; + +import { vitestBaseConfig } from '../../vitest.config.base.mjs'; +import packageJson from './package.json' with { type: 'json' }; + +const vitestConfig = mergeConfig( + vitestBaseConfig, + + defineProject({ + root: import.meta.dirname, + + test: { + diff: { + maxDepth: 1, + }, + + dir: path.join(import.meta.dirname, 'tests'), + name: packageJson.name.replace('@typescript-eslint/', ''), + root: import.meta.dirname, + testTimeout: 10_000, + }, + }), +); + +export default vitestConfig; diff --git a/packages/typescript-estree/package.json b/packages/typescript-estree/package.json index 958ba78b17ae..9018458119fb 100644 --- a/packages/typescript-estree/package.json +++ b/packages/typescript-estree/package.json @@ -52,6 +52,8 @@ "check-types": "npx nx typecheck" }, "dependencies": { + "@typescript-eslint/project-service": "8.32.1", + "@typescript-eslint/tsconfig-utils": "8.32.1", "@typescript-eslint/types": "8.32.1", "@typescript-eslint/visitor-keys": "8.32.1", "debug": "^4.3.4", diff --git a/packages/typescript-estree/src/create-program/shared.ts b/packages/typescript-estree/src/create-program/shared.ts index 07a67cbb9b38..088e5ce1c7dd 100644 --- a/packages/typescript-estree/src/create-program/shared.ts +++ b/packages/typescript-estree/src/create-program/shared.ts @@ -1,5 +1,6 @@ import type { Program } from 'typescript'; +import { CORE_COMPILER_OPTIONS } from '@typescript-eslint/tsconfig-utils'; import path from 'node:path'; import * as ts from 'typescript'; @@ -15,19 +16,6 @@ export interface ASTAndDefiniteProgram { } export type ASTAndProgram = ASTAndDefiniteProgram | ASTAndNoProgram; -/** - * Compiler options required to avoid critical functionality issues - */ -export const CORE_COMPILER_OPTIONS: ts.CompilerOptions = { - noEmit: true, // required to avoid parse from causing emit to occur - - /** - * Flags required to make no-unused-vars work - */ - noUnusedLocals: true, - noUnusedParameters: true, -}; - /** * Default compiler options for program generation */ diff --git a/packages/typescript-estree/src/create-program/useProvidedPrograms.ts b/packages/typescript-estree/src/create-program/useProvidedPrograms.ts index f84767ea19c7..b8e958ed34f2 100644 --- a/packages/typescript-estree/src/create-program/useProvidedPrograms.ts +++ b/packages/typescript-estree/src/create-program/useProvidedPrograms.ts @@ -1,3 +1,4 @@ +import { getParsedConfigFile } from '@typescript-eslint/tsconfig-utils'; import debug from 'debug'; import * as path from 'node:path'; import * as ts from 'typescript'; @@ -5,7 +6,6 @@ import * as ts from 'typescript'; import type { ParseSettings } from '../parseSettings'; import type { ASTAndDefiniteProgram } from './shared'; -import { getParsedConfigFile } from './getParsedConfigFile'; import { getAstFromProgram } from './shared'; const log = debug( diff --git a/packages/typescript-estree/src/parseSettings/createParseSettings.ts b/packages/typescript-estree/src/parseSettings/createParseSettings.ts index 6a1c42008c6b..678b771ac3a5 100644 --- a/packages/typescript-estree/src/parseSettings/createParseSettings.ts +++ b/packages/typescript-estree/src/parseSettings/createParseSettings.ts @@ -1,13 +1,19 @@ +import type { + CreateProjectServiceSettings, + ProjectServiceAndMetadata, +} from '@typescript-eslint/project-service'; +import type { ProjectServiceOptions } from '@typescript-eslint/types'; + +import { createProjectService } from '@typescript-eslint/project-service'; import debug from 'debug'; import path from 'node:path'; import * as ts from 'typescript'; -import type { ProjectServiceSettings } from '../create-program/createProjectService'; import type { TSESTreeOptions } from '../parser-options'; import type { MutableParseSettings } from './index'; -import { createProjectService } from '../create-program/createProjectService'; import { ensureAbsolutePath } from '../create-program/shared'; +import { validateDefaultProjectForFilesGlob } from '../create-program/validateDefaultProjectForFilesGlob'; import { isSourceFile } from '../source-files'; import { DEFAULT_TSCONFIG_CACHE_DURATION_SECONDS, @@ -23,7 +29,7 @@ const log = debug( ); let TSCONFIG_MATCH_CACHE: ExpiringCache | null; -let TSSERVER_PROJECT_SERVICE: ProjectServiceSettings | null = null; +let TSSERVER_PROJECT_SERVICE: ProjectServiceAndMetadata | null = null; // NOTE - we intentionally use "unnecessary" `?.` here because in TS<5.3 this enum doesn't exist // This object exists so we can centralize these for tracking and so we don't proliferate these across the file @@ -112,11 +118,10 @@ export function createParseSettings( (tsestreeOptions.project && tsestreeOptions.projectService !== false && process.env.TYPESCRIPT_ESLINT_PROJECT_SERVICE === 'true') - ? (TSSERVER_PROJECT_SERVICE ??= createProjectService( - tsestreeOptions.projectService, + ? populateProjectService(tsestreeOptions.projectService, { jsDocParsingMode, tsconfigRootDir, - )) + }) : undefined, setExternalModuleIndicator: tsestreeOptions.sourceType === 'module' || @@ -223,3 +228,19 @@ function enforceCodeString(code: unknown): string { function getFileName(jsx?: boolean): string { return jsx ? 'estree.tsx' : 'estree.ts'; } + +function populateProjectService( + optionsRaw: ProjectServiceOptions | true | undefined, + settings: CreateProjectServiceSettings, +) { + const options = typeof optionsRaw === 'object' ? optionsRaw : {}; + + validateDefaultProjectForFilesGlob(options.allowDefaultProject); + + TSSERVER_PROJECT_SERVICE ??= createProjectService({ + options, + ...settings, + }); + + return TSSERVER_PROJECT_SERVICE; +} diff --git a/packages/typescript-estree/src/parseSettings/index.ts b/packages/typescript-estree/src/parseSettings/index.ts index 1ea208210e0a..f4d771a8a4bb 100644 --- a/packages/typescript-estree/src/parseSettings/index.ts +++ b/packages/typescript-estree/src/parseSettings/index.ts @@ -1,6 +1,6 @@ +import type { ProjectServiceAndMetadata } from '@typescript-eslint/project-service'; import type * as ts from 'typescript'; -import type { ProjectServiceSettings } from '../create-program/createProjectService'; import type { CanonicalPath } from '../create-program/shared'; import type { TSESTree } from '../ts-estree'; import type { CacheLike } from './ExpiringCache'; @@ -120,7 +120,7 @@ export interface MutableParseSettings { /** * TypeScript server to power program creation. */ - projectService: ProjectServiceSettings | undefined; + projectService: ProjectServiceAndMetadata | undefined; /** * Whether to add the `range` property to AST nodes. diff --git a/packages/typescript-estree/src/parser-options.ts b/packages/typescript-estree/src/parser-options.ts index d3f8c8649fc5..a1841a828d48 100644 --- a/packages/typescript-estree/src/parser-options.ts +++ b/packages/typescript-estree/src/parser-options.ts @@ -9,8 +9,6 @@ import type * as ts from 'typescript'; import type { TSESTree, TSESTreeToTSNode, TSNode, TSToken } from './ts-estree'; -export type { ProjectServiceOptions } from '@typescript-eslint/types'; - ////////////////////////////////////////////////////////// // MAKE SURE THIS IS KEPT IN SYNC WITH THE WEBSITE DOCS // ////////////////////////////////////////////////////////// diff --git a/packages/typescript-estree/src/useProgramFromProjectService.ts b/packages/typescript-estree/src/useProgramFromProjectService.ts index cf98e5523921..9a046ee2c907 100644 --- a/packages/typescript-estree/src/useProgramFromProjectService.ts +++ b/packages/typescript-estree/src/useProgramFromProjectService.ts @@ -1,10 +1,11 @@ +import type { ProjectServiceAndMetadata as ProjectServiceAndMetadata } from '@typescript-eslint/project-service'; + import debug from 'debug'; import { minimatch } from 'minimatch'; import path from 'node:path'; import util from 'node:util'; import * as ts from 'typescript'; -import type { ProjectServiceSettings } from './create-program/createProjectService'; import type { ASTAndDefiniteProgram, ASTAndNoProgram, @@ -55,7 +56,7 @@ function openClientFileFromProjectService( isDefaultProjectAllowed: boolean, filePathAbsolute: string, parseSettings: Readonly, - serviceSettings: ProjectServiceSettings, + serviceAndSettings: ProjectServiceAndMetadata, ): ts.server.OpenConfiguredProjectResult { const opened = openClientFileAndMaybeReload(); @@ -107,7 +108,7 @@ function openClientFileFromProjectService( defaultProjectMatchedFiles.add(filePathAbsolute); if ( defaultProjectMatchedFiles.size > - serviceSettings.maximumDefaultProjectFileMatchCount + serviceAndSettings.maximumDefaultProjectFileMatchCount ) { const filePrintLimit = 20; const filesToPrint = [...defaultProjectMatchedFiles].slice( @@ -118,7 +119,7 @@ function openClientFileFromProjectService( defaultProjectMatchedFiles.size - filesToPrint.length; throw new Error( - `Too many files (>${serviceSettings.maximumDefaultProjectFileMatchCount}) have matched the default project.${DEFAULT_PROJECT_FILES_ERROR_EXPLANATION} + `Too many files (>${serviceAndSettings.maximumDefaultProjectFileMatchCount}) have matched the default project.${DEFAULT_PROJECT_FILES_ERROR_EXPLANATION} Matching files: ${filesToPrint.map(file => `- ${file}`).join('\n')} ${truncatedFileCount ? `...and ${truncatedFileCount} more files\n` : ''} @@ -131,7 +132,7 @@ If you absolutely need more files included, set parserOptions.projectService.max return opened; function openClientFile(): ts.server.OpenConfiguredProjectResult { - return serviceSettings.service.openClientFile( + return serviceAndSettings.service.openClientFile( filePathAbsolute, parseSettings.codeFullText, /* scriptKind */ undefined, @@ -152,13 +153,13 @@ If you absolutely need more files included, set parserOptions.projectService.max !opened.configFileName && !parseSettings.singleRun && !isDefaultProjectAllowed && - performance.now() - serviceSettings.lastReloadTimestamp > + performance.now() - serviceAndSettings.lastReloadTimestamp > RELOAD_THROTTLE_MS ) { log('No config file found; reloading project service and retrying.'); - serviceSettings.service.reloadProjects(); + serviceAndSettings.service.reloadProjects(); opened = openClientFile(); - serviceSettings.lastReloadTimestamp = performance.now(); + serviceAndSettings.lastReloadTimestamp = performance.now(); } return opened; @@ -192,13 +193,13 @@ function createNoProgramWithProjectService( function retrieveASTAndProgramFor( filePathAbsolute: string, parseSettings: Readonly, - serviceSettings: ProjectServiceSettings, + serviceAndSettings: ProjectServiceAndMetadata, ): ASTAndDefiniteProgram | undefined { log('Retrieving script info and then program for: %s', filePathAbsolute); - const scriptInfo = serviceSettings.service.getScriptInfo(filePathAbsolute); + const scriptInfo = serviceAndSettings.service.getScriptInfo(filePathAbsolute); /* eslint-disable @typescript-eslint/no-non-null-assertion */ - const program = serviceSettings.service + const program = serviceAndSettings.service .getDefaultProjectForFile(scriptInfo!.fileName, true)! .getLanguageService(/*ensureSynchronized*/ true) .getProgram(); @@ -215,38 +216,41 @@ function retrieveASTAndProgramFor( } export function useProgramFromProjectService( - settings: ProjectServiceSettings, + serviceAndSettings: ProjectServiceAndMetadata, parseSettings: Readonly, hasFullTypeInformation: boolean, defaultProjectMatchedFiles: Set, ): ASTAndProgram | undefined; export function useProgramFromProjectService( - settings: ProjectServiceSettings, + serviceAndSettings: ProjectServiceAndMetadata, parseSettings: Readonly, hasFullTypeInformation: true, defaultProjectMatchedFiles: Set, ): ASTAndDefiniteProgram | undefined; export function useProgramFromProjectService( - settings: ProjectServiceSettings, + serviceAndSettings: ProjectServiceAndMetadata, parseSettings: Readonly, hasFullTypeInformation: false, defaultProjectMatchedFiles: Set, ): ASTAndNoProgram | undefined; export function useProgramFromProjectService( - serviceSettings: ProjectServiceSettings, + serviceAndSettings: ProjectServiceAndMetadata, parseSettings: Readonly, hasFullTypeInformation: boolean, defaultProjectMatchedFiles: Set, ): ASTAndProgram | undefined { // NOTE: triggers a full project reload when changes are detected updateExtraFileExtensions( - serviceSettings.service, + serviceAndSettings.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, serviceSettings); + const filePathAbsolute = absolutify( + parseSettings.filePath, + serviceAndSettings, + ); log( 'Opening project service file for: %s at absolute path %s', parseSettings.filePath, @@ -259,7 +263,7 @@ export function useProgramFromProjectService( ); const isDefaultProjectAllowed = filePathMatchedBy( filePathRelative, - serviceSettings.allowDefaultProject, + serviceAndSettings.allowDefaultProject, ); // Type-aware linting is disabled for this file. @@ -268,7 +272,7 @@ export function useProgramFromProjectService( return createNoProgramWithProjectService( filePathAbsolute, parseSettings, - serviceSettings.service, + serviceAndSettings.service, ); } @@ -284,7 +288,7 @@ export function useProgramFromProjectService( isDefaultProjectAllowed, filePathAbsolute, parseSettings, - serviceSettings, + serviceAndSettings, ); log('Opened project service file: %o', opened); @@ -292,17 +296,20 @@ export function useProgramFromProjectService( return retrieveASTAndProgramFor( filePathAbsolute, parseSettings, - serviceSettings, + serviceAndSettings, ); } function absolutify( filePath: string, - serviceSettings: ProjectServiceSettings, + serviceAndSettings: ProjectServiceAndMetadata, ): string { return path.isAbsolute(filePath) ? filePath - : path.join(serviceSettings.service.host.getCurrentDirectory(), filePath); + : path.join( + serviceAndSettings.service.host.getCurrentDirectory(), + filePath, + ); } function filePathMatchedBy( diff --git a/packages/typescript-estree/tests/lib/createParseSettings.test.ts b/packages/typescript-estree/tests/lib/createParseSettings.test.ts index 846210391076..ccb210fd8dbd 100644 --- a/packages/typescript-estree/tests/lib/createParseSettings.test.ts +++ b/packages/typescript-estree/tests/lib/createParseSettings.test.ts @@ -2,8 +2,8 @@ import { createParseSettings } from '../../src/parseSettings/createParseSettings const projectService = { service: true }; -vi.mock('../../src/create-program/createProjectService.js', () => ({ - createProjectService: (): typeof projectService => projectService, +vi.mock('@typescript-eslint/project-service', () => ({ + createProjectService: () => projectService, })); describe(createParseSettings, () => { diff --git a/packages/typescript-estree/tests/lib/createProjectService.test.ts b/packages/typescript-estree/tests/lib/createProjectService.test.ts deleted file mode 100644 index af67dd38a024..000000000000 --- a/packages/typescript-estree/tests/lib/createProjectService.test.ts +++ /dev/null @@ -1,379 +0,0 @@ -import debug from 'debug'; -import * as ts from 'typescript'; - -import { createProjectService } from '../../src/create-program/createProjectService.js'; -import { getParsedConfigFile } from '../../src/create-program/getParsedConfigFile.js'; - -const mockGetParsedConfigFile = vi.mocked(getParsedConfigFile); - -vi.mock( - import('../../src/create-program/getParsedConfigFile.js'), - async importOriginal => { - const actual = await importOriginal(); - - return { - ...actual, - getParsedConfigFile: vi.fn(actual.getParsedConfigFile), - }; - }, -); - -vi.mock( - import('../../src/create-program/createProjectService.js'), - async importOriginal => { - const actual = await importOriginal(); - - vi.spyOn( - ts.server.ProjectService.prototype, - 'setCompilerOptionsForInferredProjects', - ); - - vi.spyOn(ts.server.ProjectService.prototype, 'setHostConfiguration'); - - return { - ...actual, - createProjectService: vi - .fn(actual.createProjectService) - .mockImplementation((...args) => { - const projectServiceSettings = actual.createProjectService(...args); - const service = - projectServiceSettings.service as typeof projectServiceSettings.service & { - eventHandler: ts.server.ProjectServiceEventHandler | undefined; - }; - - if (service.eventHandler) { - service.eventHandler({ - eventName: ts.server.ProjectLoadingStartEvent, - } as ts.server.ProjectLoadingStartEvent); - } - - return projectServiceSettings; - }), - }; - }, -); - -describe(createProjectService, () => { - const processStderrWriteSpy = vi - .spyOn(process.stderr, 'write') - .mockImplementation(() => true); - - beforeEach(() => { - mockGetParsedConfigFile.mockReturnValue({ - errors: [], - fileNames: [], - options: {}, - }); - }); - - afterEach(() => { - vi.clearAllMocks(); - }); - - afterAll(() => { - vi.restoreAllMocks(); - }); - - it('sets allowDefaultProject when options.allowDefaultProject is defined', () => { - const allowDefaultProject = ['./*.js']; - const settings = createProjectService( - { allowDefaultProject }, - undefined, - undefined, - ); - - expect(settings.allowDefaultProject).toBe(allowDefaultProject); - }); - - it('does not set allowDefaultProject when options.allowDefaultProject is not defined', () => { - const settings = createProjectService(undefined, undefined, undefined); - - assert.isUndefined(settings.allowDefaultProject); - }); - - it('does not throw an error when options.defaultProject is not provided and getParsedConfigFile throws a diagnostic error', () => { - mockGetParsedConfigFile.mockImplementation(() => { - throw new Error('tsconfig.json(1,1): error TS1234: Oh no!'); - }); - - expect(() => - createProjectService( - { - allowDefaultProject: ['file.js'], - }, - undefined, - undefined, - ), - ).not.toThrow(); - }); - - it('throws an error with a relative path when options.defaultProject is set to a relative path and getParsedConfigFile throws a diagnostic error', () => { - mockGetParsedConfigFile.mockImplementation(() => { - throw new Error('./tsconfig.eslint.json(1,1): error TS1234: Oh no!'); - }); - - expect(() => - createProjectService( - { - allowDefaultProject: ['file.js'], - defaultProject: './tsconfig.eslint.json', - }, - undefined, - undefined, - ), - ).toThrow( - /Could not read project service default project '\.\/tsconfig.eslint.json': .+ error TS1234: Oh no!/, - ); - }); - - it('throws an error with a local path when options.defaultProject is set to a local path and getParsedConfigFile throws a diagnostic error', () => { - mockGetParsedConfigFile.mockImplementation(() => { - throw new Error('./tsconfig.eslint.json(1,1): error TS1234: Oh no!'); - }); - - expect(() => - createProjectService( - { - allowDefaultProject: ['file.js'], - defaultProject: 'tsconfig.eslint.json', - }, - undefined, - undefined, - ), - ).toThrow( - /Could not read project service default project 'tsconfig.eslint.json': .+ error TS1234: Oh no!/, - ); - }); - - it('throws an error when options.defaultProject is set and getParsedConfigFile throws an environment error', () => { - mockGetParsedConfigFile.mockImplementation(() => { - throw new Error( - '`getParsedConfigFile` is only supported in a Node-like environment.', - ); - }); - - expect(() => - createProjectService( - { - allowDefaultProject: ['file.js'], - defaultProject: 'tsconfig.json', - }, - undefined, - undefined, - ), - ).toThrow( - "Could not read project service default project 'tsconfig.json': `getParsedConfigFile` is only supported in a Node-like environment.", - ); - }); - - it('uses the default project compiler options when options.defaultProject is set and getParsedConfigFile succeeds', async () => { - const compilerOptions: ts.CompilerOptions = { strict: true }; - mockGetParsedConfigFile.mockReturnValueOnce({ - errors: [], - fileNames: [], - options: compilerOptions, - }); - - const defaultProject = 'tsconfig.eslint.json'; - - const { service } = createProjectService( - { - allowDefaultProject: ['file.js'], - defaultProject, - }, - undefined, - undefined, - ); - - expect( - service.setCompilerOptionsForInferredProjects, - ).toHaveBeenCalledExactlyOnceWith(compilerOptions); - - expect(mockGetParsedConfigFile).toHaveBeenCalledExactlyOnceWith( - (await import('typescript/lib/tsserverlibrary.js')).default, - defaultProject, - undefined, - ); - }); - - it('uses tsconfigRootDir as getParsedConfigFile projectDirectory when provided', async () => { - const compilerOptions: ts.CompilerOptions = { strict: true }; - const tsconfigRootDir = 'path/to/repo'; - mockGetParsedConfigFile.mockReturnValueOnce({ - errors: [], - fileNames: [], - options: compilerOptions, - }); - - const { service } = createProjectService( - { - allowDefaultProject: ['file.js'], - }, - undefined, - tsconfigRootDir, - ); - - expect( - service.setCompilerOptionsForInferredProjects, - ).toHaveBeenCalledExactlyOnceWith(compilerOptions); - - expect(mockGetParsedConfigFile).toHaveBeenCalledExactlyOnceWith( - (await import('typescript/lib/tsserverlibrary.js')).default, - 'tsconfig.json', - tsconfigRootDir, - ); - }); - - it('uses the default projects error debugger for error messages when enabled', () => { - const { service } = createProjectService(undefined, undefined, undefined); - debug.enable('typescript-eslint:typescript-estree:tsserver:err'); - const enabled = service.logger.loggingEnabled(); - service.logger.msg('foo', ts.server.Msg.Err); - debug.disable(); - - expect(enabled).toBe(true); - expect(processStderrWriteSpy).toHaveBeenCalledExactlyOnceWith( - expect.stringMatching( - /^.*typescript-eslint:typescript-estree:tsserver:err foo\n$/, - ), - ); - }); - - it('does not use the default projects error debugger for error messages when disabled', () => { - const { service } = createProjectService(undefined, undefined, undefined); - const enabled = service.logger.loggingEnabled(); - service.logger.msg('foo', ts.server.Msg.Err); - - expect(enabled).toBe(false); - expect(processStderrWriteSpy).not.toHaveBeenCalled(); - }); - - it('uses the default projects info debugger for info messages when enabled', () => { - const { service } = createProjectService(undefined, undefined, undefined); - debug.enable('typescript-eslint:typescript-estree:tsserver:info'); - const enabled = service.logger.loggingEnabled(); - service.logger.info('foo'); - debug.disable(); - - expect(enabled).toBe(true); - expect(processStderrWriteSpy).toHaveBeenCalledExactlyOnceWith( - expect.stringMatching( - /^.*typescript-eslint:typescript-estree:tsserver:info foo\n$/, - ), - ); - }); - - it('does not use the default projects info debugger for info messages when disabled', () => { - const { service } = createProjectService(undefined, undefined, undefined); - const enabled = service.logger.loggingEnabled(); - service.logger.info('foo'); - - expect(enabled).toBe(false); - expect(processStderrWriteSpy).not.toHaveBeenCalled(); - }); - - it('uses the default projects perf debugger for perf messages when enabled', () => { - const { service } = createProjectService(undefined, undefined, undefined); - debug.enable('typescript-eslint:typescript-estree:tsserver:perf'); - const enabled = service.logger.loggingEnabled(); - service.logger.perftrc('foo'); - debug.disable(); - - expect(enabled).toBe(true); - expect(processStderrWriteSpy).toHaveBeenCalledExactlyOnceWith( - expect.stringMatching( - /^.*typescript-eslint:typescript-estree:tsserver:perf foo\n$/, - ), - ); - }); - - it('does not use the default projects perf debugger for perf messages when disabled', () => { - const { service } = createProjectService(undefined, undefined, undefined); - const enabled = service.logger.loggingEnabled(); - service.logger.perftrc('foo'); - - expect(enabled).toBe(false); - expect(processStderrWriteSpy).not.toHaveBeenCalled(); - }); - - it('enables all log levels for the default projects logger', () => { - const { service } = createProjectService(undefined, undefined, undefined); - - expect(service.logger.hasLevel(ts.server.LogLevel.terse)).toBe(true); - expect(service.logger.hasLevel(ts.server.LogLevel.normal)).toBe(true); - expect(service.logger.hasLevel(ts.server.LogLevel.requestTime)).toBe(true); - expect(service.logger.hasLevel(ts.server.LogLevel.verbose)).toBe(true); - }); - - it('does not return a log filename with the default projects logger', () => { - const { service } = createProjectService(undefined, undefined, undefined); - - assert.isUndefined(service.logger.getLogFileName()); - }); - - it('uses the default projects event debugger for event handling when enabled', () => { - debug.enable('typescript-eslint:typescript-estree:tsserver:event'); - createProjectService(undefined, undefined, undefined); - debug.disable(); - - expect(processStderrWriteSpy).toHaveBeenCalledExactlyOnceWith( - expect.stringMatching( - /^.*typescript-eslint:typescript-estree:tsserver:event { eventName: 'projectLoadingStart' }\n$/, - ), - ); - }); - - it('does not use the default projects event debugger for event handling when disabled', () => { - createProjectService(undefined, undefined, undefined); - - expect(processStderrWriteSpy).not.toHaveBeenCalled(); - }); - - it('provides a stub require to the host system when loadTypeScriptPlugins is falsy', () => { - const { service } = createProjectService({}, undefined, undefined); - - const required = service.host.require?.('', ''); - - expect(required).toStrictEqual({ - error: { - message: - 'TypeScript plugins are not required when using parserOptions.projectService.', - }, - module: undefined, - }); - }); - - it('does not provide a require to the host system when loadTypeScriptPlugins is truthy', async () => { - const { service } = createProjectService( - { - loadTypeScriptPlugins: true, - }, - undefined, - undefined, - ); - - expect(service.host.require).toBe( - ( - await vi.importActual>>( - 'typescript/lib/tsserverlibrary.js', - ) - ).sys.require, - ); - }); - - it('sets a host configuration', () => { - const { service } = createProjectService( - { - allowDefaultProject: ['file.js'], - }, - undefined, - undefined, - ); - - expect(service.setHostConfiguration).toHaveBeenCalledExactlyOnceWith({ - preferences: { - includePackageJsonAutoImports: 'off', - }, - }); - }); -}); diff --git a/packages/typescript-estree/tests/lib/useProgramFromProjectService.test.ts b/packages/typescript-estree/tests/lib/useProgramFromProjectService.test.ts index c156392d3693..e07add99d30e 100644 --- a/packages/typescript-estree/tests/lib/useProgramFromProjectService.test.ts +++ b/packages/typescript-estree/tests/lib/useProgramFromProjectService.test.ts @@ -1,10 +1,11 @@ +import type { + ProjectServiceAndMetadata, + TypeScriptProjectService, +} from '@typescript-eslint/project-service'; + import path from 'node:path'; import * as ts from 'typescript'; -import type { - ProjectServiceSettings, - TypeScriptProjectService, -} from '../../src/create-program/createProjectService'; import type { ParseSettings } from '../../src/parseSettings'; import { useProgramFromProjectService } from '../../src/useProgramFromProjectService'; @@ -65,7 +66,7 @@ const mockParseSettings = { } as ParseSettings; const createProjectServiceSettings = < - T extends Partial, + T extends Partial, >( settings: T, ) => ({ diff --git a/packages/typescript-estree/tests/lib/validateDefaultProjectForFilesGlob.test.ts b/packages/typescript-estree/tests/lib/validateDefaultProjectForFilesGlob.test.ts index 9eccb60f8cc1..043724755b7b 100644 --- a/packages/typescript-estree/tests/lib/validateDefaultProjectForFilesGlob.test.ts +++ b/packages/typescript-estree/tests/lib/validateDefaultProjectForFilesGlob.test.ts @@ -1,4 +1,4 @@ -import { validateDefaultProjectForFilesGlob } from '../../src/create-program/validateDefaultProjectForFilesGlob'; +import { validateDefaultProjectForFilesGlob } from '../../src/create-program/validateDefaultProjectForFilesGlob.js'; describe(validateDefaultProjectForFilesGlob, () => { it('does not throw when options.allowDefaultProject is an empty array', () => { diff --git a/packages/typescript-estree/tsconfig.build.json b/packages/typescript-estree/tsconfig.build.json index e935e6c4e028..53a73a2cf5ec 100644 --- a/packages/typescript-estree/tsconfig.build.json +++ b/packages/typescript-estree/tsconfig.build.json @@ -2,6 +2,12 @@ "extends": "../../tsconfig.build.json", "compilerOptions": {}, "references": [ + { + "path": "../project-service/tsconfig.build.json" + }, + { + "path": "../tsconfig-utils/tsconfig.build.json" + }, { "path": "../visitor-keys/tsconfig.build.json" }, diff --git a/packages/typescript-estree/tsconfig.json b/packages/typescript-estree/tsconfig.json index 7b899170369f..77cec8e6ba10 100644 --- a/packages/typescript-estree/tsconfig.json +++ b/packages/typescript-estree/tsconfig.json @@ -3,6 +3,12 @@ "files": [], "include": [], "references": [ + { + "path": "../project-service" + }, + { + "path": "../tsconfig-utils" + }, { "path": "../visitor-keys" }, diff --git a/packages/website-eslint/build.mts b/packages/website-eslint/build.mts index 6a98bbb0556f..ccb6606e34d8 100644 --- a/packages/website-eslint/build.mts +++ b/packages/website-eslint/build.mts @@ -93,6 +93,7 @@ async function buildPackage(name: string, file: string): Promise { setup(build): void { build.onLoad( makeFilter([ + '/getParsedConfigFile.ts', '/ts-eslint/ESLint.ts', '/ts-eslint/RuleTester.ts', '/ts-eslint/CLIEngine.ts', diff --git a/packages/website/docusaurus.config.mts b/packages/website/docusaurus.config.mts index 3d66c81baa90..3fb7fe565efa 100644 --- a/packages/website/docusaurus.config.mts +++ b/packages/website/docusaurus.config.mts @@ -368,28 +368,32 @@ const config: Config = { onBrokenMarkdownLinks: 'throw', organizationName: 'typescript-eslint', plugins: [ - ...['ast-spec', 'type-utils'].map(packageName => [ - 'docusaurus-plugin-typedoc', - { - entryPoints: [`../${packageName}/src/index.ts`], - enumMembersFormat: 'table', - exclude: '**/*.d.ts', - excludeExternals: true, - groupOrder: ['Functions', 'Variables', '*'], - hidePageTitle: true, - id: `typedoc-generated-${packageName}`, - indexFormat: 'table', - out: `../../docs/packages/${packageName}/generated`, - outputFileStrategy: 'modules', - parametersFormat: 'table', - plugin: [require.resolve('./tools/typedoc-plugin-no-inherit-fork.mjs')], - propertiesFormat: 'table', - readme: 'none', - tsconfig: `../${packageName}/tsconfig.json`, - typeDeclarationFormat: 'table', - useCodeBlocks: true, - }, - ]), + ...['ast-spec', 'project-service', 'tsconfig-utils', 'type-utils'].map( + packageName => [ + 'docusaurus-plugin-typedoc', + { + entryPoints: [`../${packageName}/src/index.ts`], + enumMembersFormat: 'table', + exclude: '**/*.d.ts', + excludeExternals: true, + groupOrder: ['Functions', 'Variables', '*'], + hidePageTitle: true, + id: `typedoc-generated-${packageName}`, + indexFormat: 'table', + out: `../../docs/packages/${packageName}/generated`, + outputFileStrategy: 'modules', + parametersFormat: 'table', + plugin: [ + require.resolve('./tools/typedoc-plugin-no-inherit-fork.mjs'), + ], + propertiesFormat: 'table', + readme: 'none', + tsconfig: `../${packageName}/tsconfig.json`, + typeDeclarationFormat: 'table', + useCodeBlocks: true, + }, + ], + ), require.resolve('./webpack.plugin'), ['@docusaurus/plugin-content-docs', pluginContentDocsOptions], ['@docusaurus/plugin-pwa', pluginPwaOptions], diff --git a/packages/website/sidebars/sidebar.base.js b/packages/website/sidebars/sidebar.base.js index b0d55ed91ffb..689a5bb14202 100644 --- a/packages/website/sidebars/sidebar.base.js +++ b/packages/website/sidebars/sidebar.base.js @@ -103,8 +103,10 @@ module.exports = { 'packages/eslint-plugin', 'packages/eslint-plugin-tslint', 'packages/parser', + 'packages/project-service', 'packages/rule-tester', 'packages/scope-manager', + 'packages/tsconfig-utils', { collapsible: false, items: ['packages/type-utils/type-or-value-specifier'], diff --git a/tsconfig.json b/tsconfig.json index 5a3d4d28b9e8..ef9f265fd580 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -11,6 +11,7 @@ { "path": "./packages/eslint-plugin-internal" }, { "path": "./packages/integration-tests" }, { "path": "./packages/parser" }, + { "path": "./packages/project-service" }, { "path": "./packages/rule-schema-to-typescript-types" }, { "path": "./packages/rule-tester" }, { "path": "./packages/scope-manager" }, @@ -22,6 +23,7 @@ { "path": "./packages/visitor-keys" }, { "path": "./packages/website" }, { "path": "./packages/website-eslint" }, + { "path": "./packages/tsconfig-utils" }, { "path": "./tsconfig.repo-config-files.json" } ] } diff --git a/yarn.lock b/yarn.lock index eb4c70f161e8..9a07859bcfad 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5594,6 +5594,21 @@ __metadata: languageName: unknown linkType: soft +"@typescript-eslint/project-service@8.32.1, @typescript-eslint/project-service@workspace:packages/project-service": + version: 0.0.0-use.local + resolution: "@typescript-eslint/project-service@workspace:packages/project-service" + dependencies: + "@typescript-eslint/tsconfig-utils": ^8.32.1 + "@typescript-eslint/types": ^8.32.1 + "@vitest/coverage-v8": ^3.1.2 + debug: ^4.3.4 + prettier: ^3.2.5 + rimraf: "*" + typescript: "*" + vitest: ^3.1.2 + languageName: unknown + linkType: soft + "@typescript-eslint/rule-schema-to-typescript-types@8.32.1, @typescript-eslint/rule-schema-to-typescript-types@workspace:*, @typescript-eslint/rule-schema-to-typescript-types@workspace:packages/rule-schema-to-typescript-types": version: 0.0.0-use.local resolution: "@typescript-eslint/rule-schema-to-typescript-types@workspace:packages/rule-schema-to-typescript-types" @@ -5653,6 +5668,20 @@ __metadata: languageName: unknown linkType: soft +"@typescript-eslint/tsconfig-utils@8.32.1, @typescript-eslint/tsconfig-utils@^8.32.1, @typescript-eslint/tsconfig-utils@workspace:packages/tsconfig-utils": + version: 0.0.0-use.local + resolution: "@typescript-eslint/tsconfig-utils@workspace:packages/tsconfig-utils" + dependencies: + "@vitest/coverage-v8": ^3.1.2 + prettier: ^3.2.5 + rimraf: "*" + typescript: "*" + vitest: ^3.1.2 + peerDependencies: + typescript: ">=4.8.4 <5.9.0" + languageName: unknown + linkType: soft + "@typescript-eslint/type-utils@8.32.1, @typescript-eslint/type-utils@workspace:*, @typescript-eslint/type-utils@workspace:packages/type-utils": version: 0.0.0-use.local resolution: "@typescript-eslint/type-utils@workspace:packages/type-utils" @@ -5755,6 +5784,8 @@ __metadata: resolution: "@typescript-eslint/typescript-estree@workspace:packages/typescript-estree" dependencies: "@types/is-glob": ^4.0.4 + "@typescript-eslint/project-service": 8.32.1 + "@typescript-eslint/tsconfig-utils": 8.32.1 "@typescript-eslint/types": 8.32.1 "@typescript-eslint/visitor-keys": 8.32.1 "@vitest/coverage-v8": ^3.1.3 @@ -5845,7 +5876,7 @@ __metadata: languageName: node linkType: hard -"@vitest/coverage-v8@npm:^3.1.3": +"@vitest/coverage-v8@npm:^3.1.2, @vitest/coverage-v8@npm:^3.1.3": version: 3.1.3 resolution: "@vitest/coverage-v8@npm:3.1.3" dependencies: @@ -19428,7 +19459,7 @@ __metadata: languageName: node linkType: hard -"vitest@npm:^3.1.3": +"vitest@npm:^3.1.2, vitest@npm:^3.1.3": version: 3.1.3 resolution: "vitest@npm:3.1.3" dependencies: