-
-
Notifications
You must be signed in to change notification settings - Fork 69
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,
),
}; |
I believe we may have a better resolver than depending on See #368 And you can try it out today. But it's would also good to provide this option, this may need See also import-js/eslint-plugin-import#2108 (comment) cc @SukkaW |
What about adding a new optional method to the resolver interface v3 Or we can simply pass the third parameter to the existing |
I like this. I've been testing my previous poc but with a patched import-x to expose the context instead of patching eslint. |
The 3rd param is used for resolver I still hope we can benefit |
Microsoft will also be (eventually) releasing golang tooling. Though I've been quite happy with oxc's stack as I migrate more and more over to it. https://devblogs.microsoft.com/typescript/typescript-native-port/ |
Third parameter from the edit is probably the best approach. Simply don't access it if not needed. |
@JounQin @higherorderfunctor So we all agree that adding a new parameter (for the v2 interface it would be the fourth, for the v3 interface it would be the third) would be the best approach? With the new parameter, the v2 interface can also benefit from this. But the v3 interface will always be more performant (we can share the resolver instance without needing to hash options object and maintain a cache map) and we should recommend the v3 interface over the v2. |
This comment has been minimized.
This comment has been minimized.
This patch I made a couple months back does it by adding it as a third parameter of the resolve callback. Usage example: |
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: