-
-
Notifications
You must be signed in to change notification settings - Fork 64
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
Support for project: true
or useProjectService: true
?
#282
Comments
project: true
or useprojectservice: true
?project: true
or useProjectService: true
?
Is there any guide/docs about how to use/implement |
Sorry I'm not that knowledgeable about how that would work, I suppose the best place to ask would be https://github.com/typescript-eslint/typescript-eslint/discussions , maybe this discussion typescript-eslint/typescript-eslint#8030 ? Edit: I've forwarded the question: typescript-eslint/typescript-eslint#8030 (comment) |
@SukkaW here's an answer for you. Hopefully it answers the question: typescript-eslint/typescript-eslint#8030 (reply in thread) |
un-ts/eslint-plugin-import-x#40 (comment) Due to the previous performance degradation (up to 50% slower) with parsing, I doubt the non-isolated parsing would do any good with module resolution. For now, I'd prefer not to do module resolution with type information unless the typescript-eslint team has other details that I don't know. |
I've done a little work on The easiest way is to change this one line to add the For the transpiled version (e.g., patching with pnpm or something), it would look like this. - ? (TSSERVER_PROJECT_SERVICE ??= (0, createProjectService_1.createProjectService)(tsestreeOptions.projectService, jsDocParsingMode, tsconfigRootDir))
+ ? (global.TSSERVER_PROJECT_SERVICE ??= (TSSERVER_PROJECT_SERVICE ??= (0, createProjectService_1.createProjectService)(tsestreeOptions.projectService, jsDocParsingMode, tsconfigRootDir))) A zero-config working implementation will then look like the following without needing to specify where the tsconfigs are. import debug from 'debug';
import ts from 'typescript';
import type { NewResolver, ResolvedResult } from 'eslint-plugin-import-x/types.js';
// use the same debugger as eslint with a namespace for our resolver
const log = debug('tsserver-resolver');
// declare the projectService in global for the patch if using typescript
declare global {
var TSSERVER_PROJECT_SERVICE: { service: ts.server.ProjectService; } | null;
}
// failure boilerplate
const failure = (message: string): ResolvedResult => {
log(message);
return { found: false };
};
// success boilerplate
const success = (resolvedModule: ts.ResolvedModuleFull): ResolvedResult => {
log('Found', `'${resolvedModule.resolvedFileName}'`);
return { found: true, path: resolvedModule.resolvedFileName };
};
// zero config resolver
const tsserverResolver: NewResolver = {
interfaceVersion: 3,
name: 'tsserver-resolver',
resolve: (source: string, file: string): ResolvedResult => {
log('Resolving', `'${source}'`, 'in', `'${file}'`);
// make sure typescript-eslint has initialized the service
const projectService = global.TSSERVER_PROJECT_SERVICE?.service;
if (!projectService) return failure('No project service found');
// make sure the file is in the projectService
const project = projectService.getDefaultProjectForFile(ts.server.toNormalizedPath(file), false);
if (!project) return failure('No project found');
// resolve the import from the file
const sourceModule = ts.resolveModuleName(source, file, project.getCompilerOptions(), ts.sys);
// not found case
if (!sourceModule.resolvedModule) return failure('No module found');
// found case
return success(sourceModule.resolvedModule);
},
}; Log output linting this file with
|
Some timings on my work's CI server for a moderately sized monorepo project with
|
@higherorderfunctor Thanks for the PoC! I will look into this. |
I don't know if If we are resolving imports for a current linting file, it is possible to get the existing @higherorderfunctor Will |
There is a suggestion by the I havent tested it, but if I get some time this week I'll report back. |
I cannot say yes with any authority, but I'm hopefully optimistic it is a yes. Does either project have unit tests for these scenarios we could use to verify? From the logs, Here is some general info:
From this issue it looks like they actually filter them out in the language service suggestions, so that tells me yes the An edge case I'm not sure of is dynamic imports. You can also load up a I often use tsserver directly for tools that don't support modern While that example just computes a On my work repo, the only regression I noticed was |
Did a little more digging. Here is some general info for customizing module resolution that may or may not be useful: https://github.com/microsoft/TypeScript/wiki/Using-the-Compiler-API#customizing-module-resolution. In order to use the suggestion on discord to avoid patching Here is an MVP implementation that does NOT use Again, this is much slower than piggy-backing off Click to expand...import debug from 'debug';
import type { NewResolver, ResolvedResult } from 'eslint-plugin-import-x/types.js';
import ts from 'typescript';
// noop
const doNothing = (): void => {};
// basic logging facilities
const log = debug('tsserver-resolver:resolver');
const logTsserverErr = debug('tsserver-resolver:tsserver:err');
const logTsserverInfo = debug('tsserver-resolver:tsserver:info');
const logTsserverPerf = debug('tsserver-resolver:tsserver:perf');
const logTsserverEvent = debug('tsserver-resolver:tsserver:event');
const logger: ts.server.Logger = {
close: doNothing,
endGroup: doNothing,
getLogFileName: (): undefined => undefined,
hasLevel: (): boolean => true,
info(s) {
this.msg(s, ts.server.Msg.Info);
},
loggingEnabled: (): boolean => logTsserverInfo.enabled || logTsserverErr.enabled || logTsserverPerf.enabled,
msg: (s, type) => {
switch (type) {
case ts.server.Msg.Err:
logTsserverErr(s);
break;
case ts.server.Msg.Perf:
logTsserverPerf(s);
break;
default:
logTsserverInfo(s);
break;
}
},
perftrc(s) {
this.msg(s, ts.server.Msg.Perf);
},
startGroup: doNothing,
};
// we won't actually watch files, updates come from LSP or are only read once in CLI
const createStubFileWatcher = (): ts.FileWatcher => ({
close: doNothing,
});
// general host capabilities, use node built-ins or noop stubs
const host: ts.server.ServerHost = {
...ts.sys,
clearImmediate,
clearTimeout,
setImmediate,
setTimeout,
watchDirectory: createStubFileWatcher,
watchFile: createStubFileWatcher,
};
// init the project service
const projectService = new ts.server.ProjectService({
cancellationToken: { isCancellationRequested: (): boolean => false },
eventHandler: (e): void => {
if (logTsserverEvent.enabled) logTsserverEvent(e);
},
host,
jsDocParsingMode: ts.JSDocParsingMode.ParseNone,
logger,
session: undefined,
useInferredProjectPerProjectRoot: false,
useSingleInferredProject: false,
});
// original PoC (mostly)
const failure = (message: string): ResolvedResult => {
log(message);
return { found: false };
};
const success = (resolvedModule: ts.ResolvedModuleFull): ResolvedResult => {
log('Found', `'${resolvedModule.resolvedFileName}'`);
return { found: true, path: resolvedModule.resolvedFileName };
};
const tsserverResolver: NewResolver = {
interfaceVersion: 3,
name: 'tsserver-resolver',
resolve: (source: string, file: string): ResolvedResult => {
log('Resolving', `'${source}'`, 'in', `'${file}'`);
// const projectService = global.TSSERVER_PROJECT_SERVICE?.service;
// if (!projectService) return failure('No project service found');
// NOTE: typescript-eslint does this allowing it to be skipped in the original PoC
projectService.openClientFile(file, undefined, undefined, process.cwd());
const project = projectService.getDefaultProjectForFile(ts.server.toNormalizedPath(file), false);
if (!project) return failure('No project found');
const sourceModule = ts.resolveModuleName(source, file, project.getCompilerOptions(), ts.sys);
if (!sourceModule.resolvedModule) return failure('No module found');
return success(sourceModule.resolvedModule);
},
};
const settings = {
'import-x/resolver-next': [tsserverResolver],
}; |
Sharing what is probably my final update before going into vacation mode. Refactored to use an
There are 4 resolving techniques attempted. resolveModule(options).pipe(
Either.orElse(() => resolveTypeReference(options)),
Either.orElse(() => resolveAmbientModule(options)),
Either.orElse(() => resolveFallbackRelativePath(options)),
Like the case above, at a certain "depth" it does start failing.
I don't use I still get errors regarding Click to expand...import path from 'node:path';
import type { Debugger } from 'debug';
import debug from 'debug';
import { Either, flow } from 'effect';
import type * as importX from 'eslint-plugin-import-x/types.js';
import ts from 'typescript';
/**
* TSServer setup.
*/
// eslint-disable-next-line @typescript-eslint/no-empty-function
const doNothing = (): void => {};
const logTsserverErr = debug('tsserver-resolver:tsserver:err');
const logTsserverInfo = debug('tsserver-resolver:tsserver:info');
const logTsserverPerf = debug('tsserver-resolver:tsserver:perf');
const logTsserverEvent = debug('tsserver-resolver:tsserver:event');
const logger: ts.server.Logger = {
close: doNothing,
endGroup: doNothing,
getLogFileName: (): undefined => undefined,
hasLevel: (): boolean => true,
info(s) {
this.msg(s, ts.server.Msg.Info);
},
loggingEnabled: (): boolean => logTsserverInfo.enabled || logTsserverErr.enabled || logTsserverPerf.enabled,
msg: (s, type) => {
// eslint-disable-next-line @typescript-eslint/switch-exhaustiveness-check
switch (type) {
case ts.server.Msg.Err:
logTsserverErr(s);
break;
case ts.server.Msg.Perf:
logTsserverPerf(s);
break;
default:
logTsserverInfo(s);
break;
}
},
perftrc(s) {
this.msg(s, ts.server.Msg.Perf);
},
startGroup: doNothing,
};
const createStubFileWatcher = (): ts.FileWatcher => ({
close: doNothing,
});
// general host capabilities, use node built-ins or noop stubs
const host: ts.server.ServerHost = {
...ts.sys,
clearImmediate,
clearTimeout,
setImmediate,
setTimeout,
watchDirectory: createStubFileWatcher,
watchFile: createStubFileWatcher,
};
// init the project service
const projectService = new ts.server.ProjectService({
cancellationToken: { isCancellationRequested: (): boolean => false },
eventHandler: (e): void => {
if (logTsserverEvent.enabled) logTsserverEvent(e);
},
host,
jsDocParsingMode: ts.JSDocParsingMode.ParseNone,
logger,
session: undefined,
useInferredProjectPerProjectRoot: false,
useSingleInferredProject: false,
});
/**
* Implementation.
*/
const logInfo = debug('tsserver-resolver:resolver:info');
const logError = debug('tsserver-resolver:resolver:error');
const logTrace = debug('tsserver-resolver:resolver:trace');
const logRight: <R>(
message: (args: NoInfer<R>) => Parameters<Debugger>,
log?: Debugger,
) => <L = never>(self: Either.Either<R, L>) => Either.Either<R, L> =
(message, log = logInfo) =>
(self) =>
Either.map(self, (args) => {
log(...message(args));
return args;
});
const logLeft: <L>(
message: (args: NoInfer<L>) => Parameters<Debugger>,
log?: Debugger,
) => <R>(self: Either.Either<R, L>) => Either.Either<R, L> =
(message, log = logError) =>
(self) =>
Either.mapLeft(self, (args) => {
log(...message(args));
return args;
});
const NOT_FOUND: importX.ResultNotFound = { found: false };
const fail: <T extends Array<unknown>>(
message: (...value: NoInfer<T>) => Parameters<Debugger>,
log?: Debugger,
) => (...value: T) => importX.ResultNotFound =
(message, log = logError) =>
(...value) => {
log(...message(...value));
return NOT_FOUND;
};
export const success: (path: string) => importX.ResultFound = (path) => ({ found: true, path });
/**
* Get a `ProjectService` instance.
*/
const getProjectService: () => Either.Either<ts.server.ProjectService, importX.ResultNotFound> = () =>
Either.right(projectService);
/**
* Open's the file with tsserver so it loads the project that includes the file.
*
* @remarks Not necessary if using the `projectService` from `typescript-eslint`.
*/
export const openClientFile: (options: {
file: string;
}) => Either.Either<ts.server.OpenConfiguredProjectResult, importX.ResultNotFound> = flow(
Either.right,
logRight(({ file }) => ['Opening client file:', { file }], logTrace),
Either.bind('projectService', getProjectService),
Either.flatMap(({ file, projectService }) =>
Either.liftPredicate(
projectService.openClientFile(file, undefined, undefined, process.cwd()),
({ configFileErrors }) => configFileErrors === undefined || configFileErrors.length === 0,
fail(({ configFileErrors }) => ['Failed to open:', { diagnostics: configFileErrors, file }], logTrace),
),
),
logRight(({ configFileName }) => ['Opened client file:', { clientFile: configFileName }], logTrace),
);
/**
* Get the `Project` for a given file from a `ProjectService`.
*/
const getProject: (options: { file: string }) => Either.Either<ts.server.Project, importX.ResultNotFound> = flow(
Either.right,
logRight(({ file }) => ['Getting project:', file], logTrace),
Either.bind('projectService', getProjectService),
// Either.bind('clientFile', openClientFile),
Either.bind('project', ({ file, projectService }) =>
Either.fromNullable(
projectService.getDefaultProjectForFile(ts.server.toNormalizedPath(file), false),
fail(() => ['No project found:', file] as const),
),
),
logRight(({ file, project }) => ['Found project:', { file, project: project.getProjectName() }], logTrace),
Either.map(({ project }) => project),
);
/**
* Get the `Program` for a given `Project`.
*/
const getProgram: (options: { project: ts.server.Project }) => Either.Either<ts.Program, importX.ResultNotFound> = flow(
Either.right,
logRight(({ project }) => ['Getting program:', { project: project.getProjectName() }], logTrace),
Either.bind('program', ({ project }) =>
Either.fromNullable(
project.getLanguageService().getProgram(),
fail(() => ['No program found']),
),
),
logRight(({ project }) => ['Found program:', { project: project.getProjectName() }], logTrace),
Either.map(({ program }) => program),
);
/**
* Get the `SourceFile` for a given `Program`.
*
* @remarks The `SourceFile` is used to traverse inline-references or a `TypeChecker`.
*/
const getSourceFile: (options: {
file: string;
program: ts.Program;
}) => Either.Either<ts.SourceFile, importX.ResultNotFound> = flow(
Either.right,
logRight(({ file }) => ['Getting source file:', file], logTrace),
Either.bind('sourceFile', ({ file, program }) =>
Either.fromNullable(
program.getSourceFile(file),
fail(() => ['No source file found:', { file }]),
),
),
logRight(({ file, sourceFile }) => ['Found source file:', { file, sourceFile: sourceFile.fileName }], logTrace),
Either.map(({ sourceFile }) => sourceFile),
);
/**
* Get the `TypeChecker` for a given `Program`.
*
* @remarks The `TypeChecker` is used to find ambient modules.
*/
const getTypeChecker: (options: { program: ts.Program }) => Either.Either<ts.TypeChecker> = flow(
Either.right,
logRight(() => ['Getting type checker'], logTrace),
Either.bind('typeChecker', ({ program }) => Either.right(program.getTypeChecker())),
logRight(() => ['Found type checker'], logTrace),
Either.map(({ typeChecker }) => typeChecker),
);
/**
* Resolve a module.
*/
export const resolveModule: (options: {
file: string;
source: string;
}) => Either.Either<importX.ResultFound, importX.ResultNotFound> = flow(
Either.right,
Either.bind('project', getProject),
logRight(({ file, project, source }) => ['Resolving module:', { file, project: project.getProjectName(), source }]),
Either.bind('resolvedModule', ({ file, project, source }) => {
const { resolvedModule } = ts.resolveModuleName(source, file, project.getCompilerOptions(), ts.sys);
return Either.fromNullable(
resolvedModule,
fail(() => ['No module found:', { file, project: project.getProjectName(), source }]),
);
}),
logRight(({ file, project, resolvedModule, source }) => [
'Resolved module:',
{ file, path: resolvedModule.resolvedFileName, project: project.getProjectName(), source },
]),
Either.map(({ resolvedModule }) => success(resolvedModule.resolvedFileName)),
);
/**
* Resolve a type reference.
*/
export const resolveTypeReference: (options: {
file: string;
source: string;
}) => Either.Either<importX.ResultFound, importX.ResultNotFound> = flow(
Either.right,
Either.bind('project', getProject),
logRight(({ file, project, source }) => [
'Resolving type reference directive:',
{ file, project: project.getProjectName(), source },
]),
Either.bind('resolvedTypeReferenceDirective', ({ file, project, source }) => {
const { resolvedTypeReferenceDirective } = ts.resolveTypeReferenceDirective(
source,
file,
project.getCompilerOptions(),
ts.sys,
);
return Either.fromNullable(
resolvedTypeReferenceDirective?.resolvedFileName,
fail(() => ['No type reference directive found:', { file, project: project.getProjectName(), source }]),
);
}),
logRight(({ file, project, resolvedTypeReferenceDirective, source }) => [
'Resolved type reference directive:',
{ file, path: resolvedTypeReferenceDirective, project: project.getProjectName(), source },
]),
Either.map(({ resolvedTypeReferenceDirective }) => success(resolvedTypeReferenceDirective)),
);
/**
* Resolve an ambient module.
*/
export const resolveAmbientModule: (options: {
file: string;
source: string;
}) => Either.Either<importX.ResultFound, importX.ResultNotFound> = flow(
Either.right,
logRight(({ file, source }) => ['Resolving ambient module:', { file, source }]),
Either.bind('project', getProject),
Either.bind('program', getProgram),
Either.bind('sourceFile', getSourceFile),
Either.bind('typeChecker', getTypeChecker),
Either.bind('resolvedAmbientModule', ({ file, source, typeChecker }) =>
Either.fromNullable(
typeChecker
.getAmbientModules()
.find((_) => _.getName() === `"${source}"`)
?.getDeclarations()?.[0]
.getSourceFile().fileName,
fail(() => ['No ambient module found:', { file, source }]),
),
),
logRight(({ file, project, resolvedAmbientModule, source }) => [
'Resolved ambient module:',
{ file, project: project.getProjectName(), resolvedAmbientModule, source },
]),
Either.map(({ resolvedAmbientModule }) => success(resolvedAmbientModule)),
);
/**
* Resolve a fallback relative path.
*/
export const resolveFallbackRelativePath: (options: {
file: string;
source: string;
}) => Either.Either<importX.ResultFound, importX.ResultNotFound> = flow(
Either.right,
logRight(({ file, source }) => ['Resolving fallback relative path:', { file, source }]),
Either.bind('path', ({ file, source }) =>
Either.try({
catch: fail((error) => ['No fallback relative path found:', { error, file, source }]),
try: () => path.resolve(path.dirname(file), source),
}),
),
Either.flatMap(
Either.liftPredicate(
({ path }) => ts.sys.fileExists(path),
fail(({ file, path, source }) => ["Resolved fallback relative path doesn't exist:", { file, path, source }]),
),
),
logRight(({ file, path, source }) => ['Resolved fallback relative path:', { file, path, source }]),
Either.map(({ path }) => success(path)),
);
/**
* Version 3 resolver.
*/
export const tsserverResolver: importX.NewResolver = {
interfaceVersion: 3,
name: 'tsserver-resolver',
resolve: (source: string, file: string): importX.ResolvedResult =>
Either.right({ file, source }).pipe(
Either.bind('clientFile', openClientFile),
Either.flatMap((options) =>
resolveModule(options).pipe(
Either.orElse(() => resolveTypeReference(options)),
Either.orElse(() => resolveAmbientModule(options)),
Either.orElse(() => resolveFallbackRelativePath(options)),
),
),
logRight((result) => ['Result:', result] as const),
logLeft((result) => ['Result:', result]),
Either.merge,
),
}; |
Hi! I'm wondering if the resolver currently or plans on supporting
project: true
like https://typescript-eslint.io/packages/parser/#project ?As well as plans to support
useProjectService
once out of experimental phase (currently experimental asEXPERIMENTAL_useProjectService
https://typescript-eslint.io/packages/parser/#experimental_useprojectservice ) which solves a few more issues (including performance) with finding the tsconfig.The text was updated successfully, but these errors were encountered: