Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
41 commits
Select commit Hold shift + click to select a range
69aa3c9
refactor(typescript-estree): refactors default project config and add…
higherorderfunctor Jun 8, 2024
d32c1e5
test(typescript-estree): fix original failing unit tests
higherorderfunctor Jun 8, 2024
148a9d0
chore(typescript-estree): cleans up PR submission
higherorderfunctor Jun 8, 2024
8bc4b40
chore: merge remote-tracking branch 'upstream/v8' into fix/9205-proje…
higherorderfunctor Jun 8, 2024
b6718a1
chore(typescript-estree): fixes test names to use the new function name
higherorderfunctor Jun 8, 2024
bde1a26
test(typescript-estree): adds additional tests for coverage
higherorderfunctor Jun 8, 2024
3315345
test(typescript-estree): fixes test for windows
higherorderfunctor Jun 8, 2024
2859ef9
test(typescript-estree): fixes test name typo
higherorderfunctor Jun 8, 2024
f01fe0e
chore(typescript-estree): minor cleanup to comments and variables
higherorderfunctor Jun 9, 2024
73e2c78
style(typescript-estree): unifies how compiler options are defined in…
higherorderfunctor Jun 9, 2024
5a12641
perf(typescript-estree): moves setHostConfiguration into createProjec…
higherorderfunctor Jun 11, 2024
b60001d
feat(typescript-estree): exposes tsserver logs with ProjectService
higherorderfunctor Jun 11, 2024
6c99fe5
chore(typescript-estree): fixes comment typo
higherorderfunctor Jun 11, 2024
f124343
chore(typescript-estree): fixes another comment typo
higherorderfunctor Jun 11, 2024
111453a
Merge branch 'feat/9321-expose-ts-project-service-logs' into testing2
higherorderfunctor Jun 11, 2024
31f08d5
Merge branch 'fix/9205-project-service-extends-issue' into testing2
higherorderfunctor Jun 12, 2024
7f498ee
chore: links for testingh
higherorderfunctor Jun 12, 2024
08a8498
chore: remove un-needed param
higherorderfunctor Jun 12, 2024
5c07f0c
chore: mvp?
higherorderfunctor Jun 12, 2024
0469917
chore: save
higherorderfunctor Jun 12, 2024
248c95a
chore: merge upstream
higherorderfunctor Jun 12, 2024
67ed18d
chore: pin internal deps to won repo
higherorderfunctor Jun 12, 2024
cd19ebd
chore: adds dist folder to install from github
higherorderfunctor Jun 12, 2024
363d6ae
chore: adds back lru to control max open files
higherorderfunctor Jun 12, 2024
2015312
chore: revert open file cache as it breaks vue
higherorderfunctor Jun 12, 2024
53d44c9
chore: testing incremental edits
higherorderfunctor Jun 12, 2024
e461cfa
fix: adjust settings for inc edits
higherorderfunctor Jun 12, 2024
23a0119
fix: adding build artifacts
higherorderfunctor Jun 12, 2024
1cb7669
chore: simplify applying edits and revert to old edit func
higherorderfunctor Jun 12, 2024
dab900b
chore: watches and new simpler edit logic
higherorderfunctor Jun 12, 2024
92d7b72
chore: update some logging
higherorderfunctor Jun 13, 2024
9b71f46
chore: cached file extensions
higherorderfunctor Jun 13, 2024
06923d6
Merge remote-tracking branch 'upstream/v8' into patches2upstream
higherorderfunctor Jun 13, 2024
8f80b47
chore: apply updates from 9337
higherorderfunctor Jun 13, 2024
e2a6e7c
chore: revert gitignore
higherorderfunctor Jun 13, 2024
0c3095b
chore: missed one in the git ignore
higherorderfunctor Jun 13, 2024
b5e9d6c
chore: remove hacks for pnpm testing
higherorderfunctor Jun 13, 2024
0b4ea55
chore: merge upstream
higherorderfunctor Jun 13, 2024
3687bd4
chore: merge upstream (missed one)
higherorderfunctor Jun 13, 2024
e0eeaed
chore: apply update from 9312
higherorderfunctor Jun 13, 2024
9be1c45
chore: apply update from 9312
higherorderfunctor Jun 14, 2024
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
4 changes: 4 additions & 0 deletions docs/packages/TypeScript_ESTree.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -367,3 +367,7 @@ const { ast, services } = parseAndGenerateServices(code, {

If you encounter a bug with the parser that you want to investigate, you can turn on the debug logging via setting the environment variable: `DEBUG=typescript-eslint:*`.
I.e. in this repo you can run: `DEBUG=typescript-eslint:* yarn lint`.

This will include TypeScript server logs.
To turn off these logs, include `-typescript-eslint:typescript-estree:tsserver:*` when setting the environment variable.
I.e. for this repo change to: `DEBUG='typescript-eslint:*,-typescript-eslint:typescript-estree:tsserver:*' yarn lint`.
14 changes: 12 additions & 2 deletions packages/types/src/parser-options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,12 +44,22 @@ interface ProjectServiceOptions {
* Globs of files to allow running with the default project compiler options
* despite not being matched by the project service.
*/
allowDefaultProject?: string[];
allowDefaultProject?: string[] | undefined;

/**
* Path to a TSConfig to use instead of TypeScript's default project configuration.
*/
defaultProject?: string;
defaultProject?: string | undefined | null;

/**
* Maximum number of files to keep open with the project service.
*/
maximumOpenFiles?: number;

/**
* Send changes to files as diffs instead of replacing the entire files.
*/
incremental?: boolean;

/**
* The maximum number of files {@link allowDefaultProject} may match.
Expand Down
3 changes: 3 additions & 0 deletions packages/typescript-estree/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -57,14 +57,17 @@
"@typescript-eslint/types": "7.13.0",
"@typescript-eslint/visitor-keys": "7.13.0",
"debug": "^4.3.4",
"diff": "^5.2.0",
"globby": "^11.1.0",
"is-glob": "^4.0.3",
"lru-cache": "^10.2.2",
"minimatch": "^9.0.4",
"semver": "^7.6.0",
"ts-api-utils": "^1.3.0"
},
"devDependencies": {
"@jest/types": "29.6.3",
"@types/diff": "^5.2.1",
"glob": "*",
"jest": "29.7.0",
"prettier": "^3.2.5",
Expand Down
135 changes: 83 additions & 52 deletions packages/typescript-estree/src/create-program/createProjectService.ts
Original file line number Diff line number Diff line change
@@ -1,114 +1,145 @@
/* eslint-disable @typescript-eslint/no-empty-function -- for TypeScript APIs*/
import os from 'node:os';
import path from 'node:path';

import debug from 'debug';
import type * as ts from 'typescript/lib/tsserverlibrary';

import type { ProjectServiceOptions } from '../parser-options';
import { getParsedConfigFile } from './getParsedConfigFile';
import {
saveDirectoryWatchCallback,
saveFileWatchCallback,
} from './getWatchesForProjectService';
import { validateDefaultProjectForFilesGlob } from './validateDefaultProjectForFilesGlob';

const DEFAULT_PROJECT_MATCHED_FILES_THRESHOLD = 8;
const log = debug('typescript-eslint:typescript-estree:createProjectService');
const logTsserverErr = debug(
'typescript-eslint:typescript-estree:tsserver:err',
);
const logTsserverInfo = debug(
'typescript-eslint:typescript-estree:tsserver:info',
);
const logTsserverPerf = debug(
'typescript-eslint:typescript-estree:tsserver:perf',
);
const logTsserverEvent = debug(
'typescript-eslint:typescript-estree:tsserver:event',
);

const doNothing = (): void => {};

const createStubFileWatcher = (): ts.FileWatcher => ({
close: doNothing,
});

export type TypeScriptProjectService = ts.server.ProjectService;

export interface ProjectServiceSettings {
allowDefaultProject: string[] | undefined;
maximumDefaultProjectFileMatchCount: number;
service: TypeScriptProjectService;
maximumOpenFiles: number;
incremental: boolean;
}

export function createProjectService(
optionsRaw: boolean | ProjectServiceOptions | undefined,
options: Required<ProjectServiceOptions>,
jsDocParsingMode: ts.JSDocParsingMode | undefined,
): ProjectServiceSettings {
const options = typeof optionsRaw === 'object' ? optionsRaw : {};
validateDefaultProjectForFilesGlob(options);

// 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
// eslint-disable-next-line @typescript-eslint/no-require-imports
const tsserver = require('typescript/lib/tsserverlibrary') as typeof ts;

// TODO: see getWatchProgramsForProjects
// We don't watch the disk, we just refer to these when ESLint calls us
// there's a whole separate update pass in maybeInvalidateProgram at the bottom of getWatchProgramsForProjects
// (this "goes nuclear on TypeScript")
const system: ts.server.ServerHost = {
...tsserver.sys,
clearImmediate,
clearTimeout,
setImmediate,
setTimeout,
watchDirectory: createStubFileWatcher,
watchFile: createStubFileWatcher,
watchDirectory: saveDirectoryWatchCallback,
watchFile: saveFileWatchCallback,
};

const logger: ts.server.Logger = {
close: doNothing,
endGroup: doNothing,
getLogFileName: (): undefined => undefined,
// The debug library doesn't use levels without creating a namespace for each.
// Log levels are not passed to the writer so we wouldn't be able to forward
// to a respective namespace. Supporting would require an additional flag for
// granular control. Defaulting to all levels for now.
hasLevel: (): boolean => true,
info(s) {
this.msg(s, tsserver.server.Msg.Info);
},
loggingEnabled: (): boolean =>
// if none of the debug namespaces are enabled, then don't enable logging in tsserver
logTsserverInfo.enabled ||
logTsserverErr.enabled ||
logTsserverPerf.enabled,
msg: (s, type) => {
switch (type) {
case tsserver.server.Msg.Err:
logTsserverErr(s);
break;
case tsserver.server.Msg.Perf:
logTsserverPerf(s);
break;
default:
logTsserverInfo(s);
}
},
perftrc(s) {
this.msg(s, tsserver.server.Msg.Perf);
},
startGroup: doNothing,
};

log('Creating project service with: %o', options);

const service = new tsserver.server.ProjectService({
host: system,
cancellationToken: { isCancellationRequested: (): boolean => false },
useSingleInferredProject: false,
useInferredProjectPerProjectRoot: false,
logger: {
close: doNothing,
endGroup: doNothing,
getLogFileName: (): undefined => undefined,
hasLevel: (): boolean => false,
info: doNothing,
loggingEnabled: (): boolean => false,
msg: doNothing,
perftrc: doNothing,
startGroup: doNothing,
},
logger,
eventHandler: logTsserverEvent.enabled
? (e): void => {
logTsserverEvent(e);
}
: undefined,
session: undefined,
canUseWatchEvents: true,
jsDocParsingMode,
});

if (options.defaultProject) {
let configRead;

log('Enabling default project: %s', options.defaultProject);
try {
configRead = tsserver.readConfigFile(
const configFile = getParsedConfigFile(
options.defaultProject,
system.readFile,
path.dirname(options.defaultProject),
);
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
// it is meant for hard coded options passed in. Hard casting as a work around.
// See https://github.com/microsoft/TypeScript/blob/27bcd4cb5a98bce46c9cdd749752703ead021a4b/src/server/protocol.ts#L1904
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument, @typescript-eslint/no-explicit-any
configFile.options as ts.server.protocol.InferredProjectCompilerOptions,
);
} catch (error) {
throw new Error(
`Could not parse default project '${options.defaultProject}': ${(error as Error).message}`,
);
}

if (configRead.error) {
throw new Error(
`Could not read default project '${options.defaultProject}': ${tsserver.formatDiagnostic(
configRead.error,
{
getCurrentDirectory: system.getCurrentDirectory,
getCanonicalFileName: fileName => fileName,
getNewLine: () => os.EOL,
},
)}`,
);
}

service.setCompilerOptionsForInferredProjects(
(
configRead.config as {
compilerOptions: ts.server.protocol.InferredProjectCompilerOptions;
}
).compilerOptions,
);
}

return {
allowDefaultProject: options.allowDefaultProject,
maximumOpenFiles: options.maximumOpenFiles,
incremental: options.incremental,
maximumDefaultProjectFileMatchCount:
options.maximumDefaultProjectFileMatchCount_THIS_WILL_SLOW_DOWN_LINTING ??
DEFAULT_PROJECT_MATCHED_FILES_THRESHOLD,
options.maximumDefaultProjectFileMatchCount_THIS_WILL_SLOW_DOWN_LINTING,
service,
};
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import * as fs from 'fs';
import * as path from 'path';
import type * as ts from 'typescript/lib/tsserverlibrary';

import { CORE_COMPILER_OPTIONS } from './shared';

/**
* Utility offered by parser to help consumers parse a config file.
*
* @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()`
*/
function getParsedConfigFile(
configFile: string,
projectDirectory?: string,
): ts.ParsedCommandLine {
// 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
// eslint-disable-next-line @typescript-eslint/no-require-imports
const tsserver = require('typescript/lib/tsserverlibrary') as typeof ts;

// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
if (tsserver.sys === undefined) {
throw new Error(
'`createProgramFromConfigFile` is only supported in a Node-like environment.',
);
}

const parsed = tsserver.getParsedCommandLineOfConfigFile(
configFile,
CORE_COMPILER_OPTIONS,
{
onUnRecoverableConfigFileDiagnostic: diag => {
throw new Error(formatDiagnostics([diag])); // ensures that `parsed` is defined.
},
fileExists: fs.existsSync,
getCurrentDirectory: () =>
(projectDirectory && path.resolve(projectDirectory)) || process.cwd(),
readDirectory: tsserver.sys.readDirectory,
readFile: file => fs.readFileSync(file, 'utf-8'),
useCaseSensitiveFileNames: tsserver.sys.useCaseSensitiveFileNames,
},
);

// parsed is not undefined, since we throw on failure.
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const result = parsed!;
if (result.errors.length) {
throw new Error(formatDiagnostics(result.errors));
}

return result;

// scoped to parent function to use lazy typescript import
function formatDiagnostics(diagnostics: ts.Diagnostic[]): string | undefined {
return tsserver.formatDiagnostics(diagnostics, {
getCanonicalFileName: f => f,
getCurrentDirectory: process.cwd,
getNewLine: () => '\n',
});
}
}

export { getParsedConfigFile };
Loading