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.
+
+[](https://www.npmjs.com/package/@typescript-eslint/utils)
+[](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.
+
+[](https://www.npmjs.com/package/@typescript-eslint/utils)
+[](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: