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 1e4861de3cdf..a31e268f5a16 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -184,8 +184,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_Utils.mdx b/docs/packages/TSConfig_Utils.mdx new file mode 100644 index 000000000000..1ce3361c7fa4 --- /dev/null +++ b/docs/packages/TSConfig_Utils.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..cf1a671681d6 --- /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/project-service.svg?style=flat-square)](https://www.npmjs.com/package/@typescript-eslint/project-service) +[![NPM Downloads](https://img.shields.io/npm/dm/@typescript-eslint/project-service.svg?style=flat-square)](https://www.npmjs.com/package/@typescript-eslint/project-service) + +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..6b84af53ee34 --- /dev/null +++ b/packages/project-service/package.json @@ -0,0 +1,63 @@ +{ + "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": { + "//": "These package scripts are mostly here for convenience. Task running is handled by Nx at the root level.", + "build": "yarn run -BT nx build", + "clean": "rimraf dist/ coverage/", + "format": "yarn run -T format", + "lint": "yarn run -BT nx lint", + "test": "yarn run -BT nx test", + "typecheck": "yarn run -BT nx typecheck" + }, + "dependencies": { + "@typescript-eslint/tsconfig-utils": "^8.32.1", + "@typescript-eslint/types": "^8.32.1", + "debug": "^4.3.4" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "nx": { + "name": "project-service", + "includedScripts": [ + "clean" + ] + } +} 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..32fd4e971d85 --- /dev/null +++ b/packages/project-service/tests/createProjectService.test.ts @@ -0,0 +1,277 @@ +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(() => { + // eslint-disable-next-line @typescript-eslint/no-require-imports + 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..075c3fbb7315 --- /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/tsconfig-utils.svg?style=flat-square)](https://www.npmjs.com/package/@typescript-eslint/tsconfig-utils) +[![NPM Downloads](https://img.shields.io/npm/dm/@typescript-eslint/tsconfig-utils.svg?style=flat-square)](https://www.npmjs.com/package/@typescript-eslint/tsconfig-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..d430af46082a --- /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": { + "//": "These package scripts are mostly here for convenience. Task running is handled by Nx at the root level.", + "build": "yarn run -BT nx build", + "clean": "rimraf dist/ coverage/", + "format": "yarn run -T format", + "lint": "yarn run -BT nx lint", + "test": "yarn run -BT nx test", + "typecheck": "yarn run -BT nx typecheck" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <5.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "nx": { + "name": "tsconfig-utils", + "includedScripts": [ + "clean" + ] + } +} 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 cfd6091e8d62..41b687506a4f 100644 --- a/packages/typescript-estree/package.json +++ b/packages/typescript-estree/package.json @@ -53,6 +53,8 @@ "typecheck": "yarn run -BT 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 83d544efa9b7..09f7355bb869 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5493,6 +5493,16 @@ __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 + debug: ^4.3.4 + 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" @@ -5547,6 +5557,14 @@ __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" + 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" @@ -5648,6 +5666,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 pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy