Support for `project: true` or `useProjectService: true` ?
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 as EXPERIMENTAL_useProjectService https://typescript-eslint.io/packages/parser/#experimental_useprojectservice ) which solves a few more issues (including performance) with finding the tsconfig.
Is there any guide/docs about how to use/implement projectService in a third-party library (eslint-import-resolver-typescript in this case)?
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 https://github.com/typescript-eslint/typescript-eslint/discussions/8030 ?
Edit: I've forwarded the question: https://github.com/typescript-eslint/typescript-eslint/discussions/8030#discussioncomment-10674745
@SukkaW here's an answer for you. Hopefully it answers the question: https://github.com/typescript-eslint/typescript-eslint/discussions/8030#discussioncomment-10675969
typescript-eslint/typescript-eslint#8030 (reply in thread)
https://github.com/un-ts/eslint-plugin-import-x/issues/40#issuecomment-2293241425 https://github.com/un-ts/eslint-plugin-import-x/issues/40#issuecomment-2294959792
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 typescript-eslint. It is doable, but it does require patching typescript-eslint (@typescript-eslint/typescript-estree specifically) to make the projectService accessible. I'm on a potato right now, but its about ~3 seconds faster linting a single file. More thorough testing is needed to see if it is actually faster.
The easiest way is to change this one line to add the projectService into globalThis. Where node is single-threaded, I don't recommend loading up another instance of tsserver in the same thread. Probably be better to see if typescript-eslint is willing to expose their instance without patching.
https://github.com/typescript-eslint/typescript-eslint/blob/b2ce15840934fb5bf1ad4b1136658be9578ab82c/packages/typescript-estree/src/parseSettings/createParseSettings.ts#L115
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 DEBUG=tsserver-resolver.
tsserver-resolver Resolving 'debug' in '<REDACTED>/packages/eslint-plugin/lib/settings/typescript.ts' +2ms
tsserver-resolver Found '<REDACTED>/node_modules/.pnpm/@[email protected]/node_modules/@types/debug/index.d.ts' +16ms
tsserver-resolver Resolving 'eslint-import-resolver-typescript' in '<REDACTED>/packages/eslint-plugin/lib/settings/typescript.ts' +20ms
tsserver-resolver Found '<REDACTED>/node_modules/.pnpm/[email protected][email protected][email protected][email protected]_gstg3dkhejoefzvjz5ffhaic5y/node_modules/eslint-import-resolver-typescript/lib/index.d.ts' +15ms
tsserver-resolver Resolving 'eslint-plugin-import-x/types.js' in '<REDACTED>/packages/eslint-plugin/lib/settings/typescript.ts' +2ms
tsserver-resolver Found '<REDACTED>/node_modules/.pnpm/[email protected][email protected][email protected][email protected]/node_modules/eslint-plugin-import-x/lib/types.d.ts' +8ms
tsserver-resolver Resolving 'typescript' in '<REDACTED>/packages/eslint-plugin/lib/settings/typescript.ts' +2ms
tsserver-resolver Found '<REDACTED>/node_modules/.pnpm/[email protected]/node_modules/typescript/lib/typescript.d.ts' +8ms
Some timings on my work's CI server for a moderately sized monorepo project with import-x. Seems about 20% faster. Also found some issues in the root of a few packages that have a tsconfig.utils.json for things like my esbuild.config.ts files not part of the main source tree that I forgot to add to my eslint-import-resolver-typescript config.
--------------------------------------------------------------------------------
Language files blank comment code
--------------------------------------------------------------------------------
TypeScript 684 3422 3979 25885
eslint-import-resolver-typescript: 3 minutes 11 seconds
tsserver-resolver: 2 minutes 28 seconds
@higherorderfunctor Thanks for the PoC! I will look into this.
I don't know if typescript-eslint would be willing to expose the projectService through a global variable.
If we are resolving imports for a current linting file, it is possible to get the existing projectService via typed linting (through rule context). However, since both eslint-plugin-import-x and eslint-plugin-import support recursive resolving for transitive dependency (resolve imports for a file under node_modules), we choose not to expose the rule context in the new resolve interface.
@higherorderfunctor Will projectService also work for transitive dependency as well?
@higherorderfunctor Thanks for the PoC! I will look into this.
There is a suggestion by the typescript-eslint devs on a way without patching over at https://discord.com/channels/1026804805894672454/1318413699731423305.
I havent tested it, but if I get some time this week I'll report back.
I don't know if
typescript-eslintwould be willing to expose theprojectServicethrough a global variable.If we are resolving imports for a current linting file, it is possible to get the existing
projectServicevia typed linting (through rule context). However, since botheslint-plugin-import-xandeslint-plugin-importsupport recursive resolving for transitive dependency (resolve imports for a file undernode_modules), we choose not to expose the rule context in the new resolve interface.@higherorderfunctor Will
projectServicealso work for transitive dependency as well?
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, tsserver tends to load practically everything in node_modules.
Here is some general info: https://github.com/microsoft/TypeScript/wiki/Standalone-Server-%28tsserver%29
The server will include the loose file, then includes all other files included by triple slash references and module imports from the original file transitively.
From this issue it looks like they actually filter them out in the language service suggestions, so that tells me yes the tsserver projectService is aware of those.
An edge case I'm not sure of is dynamic imports.
You can also load up a projectService without typescript-eslint for testing.
I often use tsserver directly for tools that don't support modern tsconfig features like ${configDir} or multiple extends.
While that example just computes a tsconfig, its not too hard to make a projectService. Here is where it is done in typescript-eslint.
On my work repo, the only regression I noticed was ~~@types/bun~~ (edit: all @types/*) were expected to be in dependencies instead of devDependencies ~~for one package~~ in my monorepo. I haven't dug into why yet.
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 typescript-eslint, the context would need to be passed through. Also, type-checking needs to be enabled for this to work.
Here is an MVP implementation that does NOT use typescript-eslint (use DEBUG='tsserver-resolver:*' for the logs).
Again, this is much slower than piggy-backing off typescript-eslint as it is causing twice the projectService-related workload in a single process, but I figured it may make exploring the concept more approachable. It forgoes much of the work done by typescript-eslint like handling extra file extensions, updating the projectService in LSP-mode to catch new files, etc.
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 Either pattern as it just made working with error management/logging easier for me. That adds the dependency effect for the Either implementation.
DEBUG='tsserver-resolver:*,-tsserver-resolver:tsserver:*,-tsserver-resolver:resolver:trace' can be used to cut out most of the noise.
There are 4 resolving techniques attempted.
resolveModule(options).pipe(
Either.orElse(() => resolveTypeReference(options)),
Either.orElse(() => resolveAmbientModule(options)),
Either.orElse(() => resolveFallbackRelativePath(options)),
resolveModule is the original method shared.
resolveTypeReference is for inline references like /// <reference types="bun-types" />.
resolveAmbientModule uses the type checker to find imports like bun:test without needing to traverse from bun -> bun-types -> bun-types/test.d.ts -> checking for the ambient module declaration.
resolveFallbackRelativePath fixed an issue for me where I had export type * from 'vitest'; giving the error No named exports found in module 'vitest' import-x/export. vitest.d.cts just had a single export * from './dist/index.js'. For some reason, vitest.d.cts isn't considered part of the projectService. I could do an openClientFile, in which case it will get added to a new inferred project by the compiler, but a simple relative path resolver seemed more efficient than opening up a bunch of files and inferred project's.
Like the case above, at a certain "depth" it does start failing.
2024-12-20T21:49:24.590Z tsserver-resolver:resolver:error Resolved fallback relative path doesn't exist: {
file: '<REDACTED>/node_modules/.pnpm/[email protected]_@[email protected]_@[email protected][email protected][email protected][email protected]/node_modules/vitest/dist/index.js',
source: 'chai',
path: '<REDACTED>/node_modules/.pnpm/[email protected]_@[email protected]_@[email protected][email protected][email protected][email protected]/node_modules/vitest/dist/chai'
}
2024-12-20T21:49:24.590Z tsserver-resolver:resolver:error Result: { found: false }
I don't use chai in my test repo and I'm assuming vitest doesn't export any types from chai, so it is just not loaded into the projectService. This seems like an acceptable natural boundary where further resolution isn't needed if resolveFallbackRelativePath can't resolve.
I still get errors regarding @types/* wanting to be defined in each package in my monorepo and in dependencies not devDependencies. I haven't had much time to dig into that rule to see why.
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 projectService with oxc-resolver enabled in next major.
See #368
And you can try it out today.
https://github.com/import-js/eslint-import-resolver-typescript/pull/368#issuecomment-2711154991
But it's would also good to provide this option, this may need import-x to expose more eslint running context into resolver.
See also https://github.com/import-js/eslint-plugin-import/issues/2108#issuecomment-1205016584
cc @SukkaW
But it's would also good to provide this option, this may need
import-xto expose more eslint running context into resolver.See also import-js/eslint-plugin-import#2108 (comment)
cc @SukkaW
What about adding a new optional method to the resolver interface v3 resolveWithContext(src, mod, context)? And this will not be mandatory.
Or we can simply pass the third parameter to the existing resolve method (from resolve(source, mod) to resolve(source, mod, context)). This won't be a breaking change as well.
But it's would also good to provide this option, this may need
import-xto expose more eslint running context into resolver.See also import-js/eslint-plugin-import#2108 (comment)
cc @SukkaW
What about adding a new optional method to the resolver interface v3
resolveWithContext(src, mod, context)? And this will not be mandatory.
I like this. I've been testing my previous poc but with a patched import-x to expose the context instead of patching eslint.
Or we can simply pass the third parameter to the existing
resolvemethod (fromresolve(source, mod)toresolve(source, mod, context)). This won't be a breaking change as well.
The 3rd param is used for resolver options, and the 4th is used for resolver for v3 adaptors like https://github.com/9romise/eslint-import-resolver-oxc/blob/677c18515c65666a428d5be1f262d4b5ee65fa7d/src/index.ts#L17
I still hope we can benefit v2 resolvers in someway.
I believe we may have a better resolver than depending on
projectServicewithoxc-resolverenabled in next major.See #368
And you can try it out today.
https://github.com/import-js/eslint-import-resolver-typescript/pull/368#issuecomment-2711154991
But it's would also good to provide this option, this may need
import-xto expose more eslint running context into resolver.See also https://github.com/import-js/eslint-plugin-import/issues/2108#issuecomment-1205016584
cc @SukkaW
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/
Or we can simply pass the third parameter to the existing
resolvemethod (fromresolve(source, mod)toresolve(source, mod, context)). This won't be a breaking change as well.
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.
@SukkaW ~~I notice the 3rd param for v3 resolver will not work, because context is a runtime API instead of prepared options/settings.~~
This patch I made a couple months back does it by adding it as a third parameter of the resolve callback.
https://github.com/higherorderfunctor/astal-config/blob/feat%2Fprototype/patches%2Feslint-plugin-import-x%404.6.1.patch
Usage example:
https://github.com/higherorderfunctor/astal-config/blob/ebea4b28ab2acacf666f3331a18cbc36d79280fc/packages/eslint-plugin/src/settings/typescript.ts#L229
Oh, I misremember the v3 resolver factory vs finally resolver.
With https://github.com/un-ts/eslint-import-context, I believe we can already get rule's context now, would you like to raise a PR for this?
@higherorderfunctor
cc @JoshuaKGoldberg since you're an expert on this part.