Skip to content

feat(typescript-estree): exposes ProjectService logs through the plugin #9337

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

Merged
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
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`.
Original file line number Diff line number Diff line change
@@ -1,13 +1,28 @@
/* eslint-disable @typescript-eslint/no-empty-function -- for TypeScript APIs*/
import os from 'node:os';

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

import type { ProjectServiceOptions } from '../parser-options';
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 => ({
Expand Down Expand Up @@ -48,27 +63,60 @@ export function createProjectService(
watchFile: createStubFileWatcher,
};

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,
jsDocParsingMode,
});

if (options.defaultProject) {
log('Enabling default project: %s', options.defaultProject);
let configRead;

try {
Expand Down
134 changes: 134 additions & 0 deletions packages/typescript-estree/tests/lib/createProjectService.test.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import debug from 'debug';
import * as ts from 'typescript';

import { createProjectService } from '../../src/create-program/createProjectService';
Expand All @@ -9,14 +10,32 @@ jest.mock('typescript/lib/tsserverlibrary', () => ({
...jest.requireActual('typescript/lib/tsserverlibrary'),
readConfigFile: mockReadConfigFile,
server: {
...jest.requireActual('typescript/lib/tsserverlibrary').server,
ProjectService: class {
logger: ts.server.Logger;
eventHandler: ts.server.ProjectServiceEventHandler | undefined;
constructor(
...args: ConstructorParameters<typeof ts.server.ProjectService>
) {
this.logger = args[0].logger;
this.eventHandler = args[0].eventHandler;
if (this.eventHandler) {
this.eventHandler({
eventName: 'projectLoadingStart',
} as ts.server.ProjectLoadingStartEvent);
}
}
setCompilerOptionsForInferredProjects =
mockSetCompilerOptionsForInferredProjects;
},
},
}));

describe('createProjectService', () => {
afterEach(() => {
jest.resetAllMocks();
});

it('sets allowDefaultProject when options.allowDefaultProject is defined', () => {
const allowDefaultProject = ['./*.js'];
const settings = createProjectService({ allowDefaultProject }, undefined);
Expand Down Expand Up @@ -89,4 +108,119 @@ describe('createProjectService', () => {
compilerOptions,
);
});

it('uses the default projects error debugger for error messages when enabled', () => {
jest.spyOn(process.stderr, 'write').mockImplementation();
const { service } = createProjectService(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(process.stderr.write).toHaveBeenCalledWith(
expect.stringMatching(
/^.*typescript-eslint:typescript-estree:tsserver:err foo\n$/,
),
);
});

it('does not use the default projects error debugger for error messages when disabled', () => {
jest.spyOn(process.stderr, 'write').mockImplementation();
const { service } = createProjectService(undefined, undefined);
const enabled = service.logger.loggingEnabled();
service.logger.msg('foo', ts.server.Msg.Err);

expect(enabled).toBe(false);
expect(process.stderr.write).toHaveBeenCalledTimes(0);
});

it('uses the default projects info debugger for info messages when enabled', () => {
jest.spyOn(process.stderr, 'write').mockImplementation();
const { service } = createProjectService(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(process.stderr.write).toHaveBeenCalledWith(
expect.stringMatching(
/^.*typescript-eslint:typescript-estree:tsserver:info foo\n$/,
),
);
});

it('does not use the default projects info debugger for info messages when disabled', () => {
jest.spyOn(process.stderr, 'write').mockImplementation();
const { service } = createProjectService(undefined, undefined);
const enabled = service.logger.loggingEnabled();
service.logger.info('foo');

expect(enabled).toBe(false);
expect(process.stderr.write).toHaveBeenCalledTimes(0);
});

it('uses the default projects perf debugger for perf messages when enabled', () => {
jest.spyOn(process.stderr, 'write').mockImplementation();
const { service } = createProjectService(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(process.stderr.write).toHaveBeenCalledWith(
expect.stringMatching(
/^.*typescript-eslint:typescript-estree:tsserver:perf foo\n$/,
),
);
});

it('does not use the default projects perf debugger for perf messages when disabled', () => {
jest.spyOn(process.stderr, 'write').mockImplementation();
const { service } = createProjectService(undefined, undefined);
const enabled = service.logger.loggingEnabled();
service.logger.perftrc('foo');

expect(enabled).toBe(false);
expect(process.stderr.write).toHaveBeenCalledTimes(0);
});

it('enables all log levels for the default projects logger', () => {
const { service } = createProjectService(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);

expect(service.logger.getLogFileName()).toBeUndefined();
});

it('uses the default projects event debugger for event handling when enabled', () => {
jest.spyOn(process.stderr, 'write').mockImplementation();

debug.enable('typescript-eslint:typescript-estree:tsserver:event');
createProjectService(undefined, undefined);
debug.disable();

expect(process.stderr.write).toHaveBeenCalledWith(
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', () => {
jest.spyOn(process.stderr, 'write').mockImplementation();

createProjectService(undefined, undefined);

expect(process.stderr.write).toHaveBeenCalledTimes(0);
});
});
Loading