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 24 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
17475ae
feat: create standalone project-service, tsconfig-utils packages
JoshuaKGoldberg May 6, 2025
2670553
fix: add missing dependency on tsconfig-utils
JoshuaKGoldberg May 6, 2025
2fe82d9
chore: correct for Knip reports
JoshuaKGoldberg May 6, 2025
7e4902a
lint fixes
JoshuaKGoldberg May 7, 2025
59a1d8d
lint fixes
JoshuaKGoldberg May 7, 2025
e01d316
lint fixes
JoshuaKGoldberg May 7, 2025
8a305c9
Merge branch 'main' into standalone-project-service
JoshuaKGoldberg May 12, 2025
cd550db
Docs and lockfile updates
JoshuaKGoldberg May 12, 2025
f904504
nit: 'programs', not 'projects'
JoshuaKGoldberg May 12, 2025
f151e66
Updated y(a)ml files too
JoshuaKGoldberg May 12, 2025
03a1ccc
chore: correct tsconfig-utils type-utils config entries
JoshuaKGoldberg May 12, 2025
abd2315
chore: correct license year
JoshuaKGoldberg May 12, 2025
c424f73
tests: corrected
JoshuaKGoldberg May 12, 2025
ef91a38
Lint fixes
JoshuaKGoldberg May 12, 2025
caa57bb
Lint fixes
JoshuaKGoldberg May 12, 2025
12f8d47
Merge branch 'main'
JoshuaKGoldberg May 12, 2025
9364664
Merge branch 'main'
JoshuaKGoldberg May 16, 2025
3d0e00e
Apply suggestions from code review
JoshuaKGoldberg May 20, 2025
06f7ae8
filename fix
JoshuaKGoldberg May 20, 2025
94e5552
an inline disable
JoshuaKGoldberg May 20, 2025
ee2001b
Merge branch 'main' into standalone-project-service
JoshuaKGoldberg May 20, 2025
ba8b507
Remove project.json files, and instead add package.json nx entries
JoshuaKGoldberg May 20, 2025
5e52cf8
Align scripts
JoshuaKGoldberg May 20, 2025
572acf5
Remove unnecessary deps
JoshuaKGoldberg May 20, 2025
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 @@ -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',
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_Utils.mdx
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/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 -->
63 changes: 63 additions & 0 deletions packages/project-service/package.json
Original file line number Diff line number Diff line change
@@ -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"
]
}
}
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
Loading