Skip to content

feat: create standalone project-service, tsconfig-utils packages #11182

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 17 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .github/ISSUE_TEMPLATE/06-bug-report-other.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -40,8 +40,10 @@ body:
- ast-spec
- eslint-plugin
- parser
- project-service
- rule-tester
- scope-manager
- tsconfig-utils
- type-utils
- types
- typescript-eslint
Expand Down
2 changes: 2 additions & 0 deletions .github/ISSUE_TEMPLATE/07-enhancement-other.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,10 @@ body:
- ast-spec
- eslint-plugin
- parser
- project-service
- rule-tester
- scope-manager
- tsconfig-utils
- type-utils
- types
- typescript-eslint
Expand Down
2 changes: 2 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
2 changes: 2 additions & 0 deletions .github/workflows/semantic-pr-titles.yml
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,10 @@ jobs:
eslint-plugin
eslint-plugin-internal
parser
project-service
rule-tester
scope-manager
tsconfig-utils
type-utils
types
typescript-eslint
Expand Down
39 changes: 39 additions & 0 deletions docs/packages/Project_Service.mdx
Original file line number Diff line number Diff line change
@@ -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`

<PackageLink packageName="project-service" scope="@typescript-eslint" />

> 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.

<GeneratedDocs />
17 changes: 17 additions & 0 deletions docs/packages/TSConfig_Utilsx.mdx
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

filename is wrong? TSConfig_Utilsx?

Original file line number Diff line number Diff line change
@@ -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`

<PackageLink packageName="tsconfig-utils" scope="@typescript-eslint" />

> Utilities for collecting TSConfigs for linting scenarios ✨

The following documentation is auto-generated from source code.

<GeneratedDocs />
21 changes: 21 additions & 0 deletions packages/project-service/LICENSE
Original file line number Diff line number Diff line change
@@ -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.
12 changes: 12 additions & 0 deletions packages/project-service/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
# `@typescript-eslint/project-service`

> Standalone TypeScript project service wrapper for linting.
[![NPM Version](https://img.shields.io/npm/v/@typescript-eslint/utils.svg?style=flat-square)](https://www.npmjs.com/package/@typescript-eslint/utils)
[![NPM Downloads](https://img.shields.io/npm/dm/@typescript-eslint/utils.svg?style=flat-square)](https://www.npmjs.com/package/@typescript-eslint/utils)
Comment on lines +5 to +6
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
[![NPM Version](https://img.shields.io/npm/v/@typescript-eslint/utils.svg?style=flat-square)](https://www.npmjs.com/package/@typescript-eslint/utils)
[![NPM Downloads](https://img.shields.io/npm/dm/@typescript-eslint/utils.svg?style=flat-square)](https://www.npmjs.com/package/@typescript-eslint/utils)
[![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.
<!-- Local path for docs: docs/packages/Project_Service.mdx -->
64 changes: 64 additions & 0 deletions packages/project-service/package.json
Original file line number Diff line number Diff line change
@@ -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",
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same comment

"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"
}
}
13 changes: 13 additions & 0 deletions packages/project-service/project.json
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please update this PR to follow the conventions set in main after #11226 you can check out the loom for reference

Original file line number Diff line number Diff line change
@@ -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}"]
}
}
}
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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 },
Expand All @@ -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,
Expand All @@ -178,3 +217,5 @@ export function createProjectService(
service,
};
}

export { type ProjectServiceOptions } from '@typescript-eslint/types';
Loading