-
Notifications
You must be signed in to change notification settings - Fork 12.8k
Refactor module resolution Extensions
, fix lookup priorities
#51471
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
Refactor module resolution Extensions
, fix lookup priorities
#51471
Conversation
579a4a7
to
ba45a79
Compare
enum Extensions { | ||
TypeScript, /** '.ts', '.tsx', or '.d.ts' */ | ||
JavaScript, /** '.js' or '.jsx' */ | ||
Json, /** '.json' */ | ||
TSConfig, /** '.json' with `tsconfig` used instead of `index` */ | ||
DtsOnly, /** Only '.d.ts' */ | ||
TsOnly, /** '.[cm]tsx?' but not .d.ts variants */ | ||
const enum Extensions { | ||
TypeScript = 1 << 0, // '.ts', '.tsx', '.mts', '.cts' | ||
JavaScript = 1 << 1, // '.js', '.jsx', '.mjs', '.cjs' | ||
Declaration = 1 << 2, // '.d.ts', etc. | ||
Json = 1 << 3, // '.json' | ||
|
||
ImplementationFiles = TypeScript | JavaScript, | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Extensions
is bit flags now, and TSConfig
is changed to be an isConfigLookup
on ModuleResolutionState
.
// ./foo.js -> ./foo.ts | ||
const resolvedByReplacingExtension = loadModuleFromFileNoImplicitExtensions(extensions, candidate, onlyRecordFailures, state); | ||
if (resolvedByReplacingExtension) { | ||
return resolvedByReplacingExtension; | ||
} | ||
|
||
// esm mode resolutions don't include automatic extension lookup (without additional flags, at least) | ||
// ./foo -> ./foo.ts | ||
if (!(state.features & NodeResolutionFeatures.EsmMode)) { | ||
// First, try adding an extension. An import of "foo" could be matched by a file "foo.ts", or "foo.js" by "foo.js.ts" | ||
const resolvedByAddingExtension = tryAddingExtensions(candidate, extensions, "", onlyRecordFailures, state); | ||
if (resolvedByAddingExtension) { | ||
return resolvedByAddingExtension; | ||
} | ||
} | ||
|
||
return loadModuleFromFileNoImplicitExtensions(extensions, candidate, onlyRecordFailures, state); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is the extension adding/replacing order swap
export default 0; | ||
|
||
// @Filename: /a.ts | ||
import b from "./b"; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This test asserted the incorrect behavior. It’s replaced by the two new tests.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM but I'll leave it for Wesley/Sheetal to look at tomorrow.
ImplementationFiles = TypeScript | JavaScript, | ||
} | ||
|
||
function formatExtensions(extensions: Extensions) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
FWIW you can get a pretty similar result with Debug.formatEnum
. Post-modules that's something I want to yank out into a module for use outside of Debug.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I didn’t use it mostly because it was in Debug
and this is production code... I know it would work without any issue but it felt wrong.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, I wouldn't have mentioned it but this seemed like it was just in tracing which straddles the line a little.
I think this might be a problem for monorepos where |
@typescript-bot test top100 |
Heya @andrewbranch, I've started to run the diff-based top-repos suite on this PR at ba45a79. You can monitor the build here. Update: The results are in! |
@typescript-bot perf test faster |
Heya @andrewbranch, I've started to run the abridged perf test suite on this PR at ba45a79. You can monitor the build here. Update: The results are in! |
if this is breaking change anyway why not correct it for all algorithms.. Havent looked at changes yet and will do soon but does it simplify code even further or it makes it worse. Apart from being breaking change does it have any other reasoning not to break these two modes. Will it not do right thing. |
I intend for |
@andrewbranch Here they are:Comparison Report - main..51471
System
Hosts
Scenarios
Developer Information: |
@andrewbranch Here are the results of running the top-repos suite comparing Everything looks good! |
I really expected a couple monorepos to fail there. Interesting. |
Ah, the lack of failures could be due to low usage of |
@typescript-bot test top100 |
Heya @andrewbranch, I've started to run the diff-based top-repos suite on this PR at 4014f30. You can monitor the build here. Update: The results are in! |
extensions = [Extensions.TsOnly]; | ||
if (compilerOptions.allowJs) extensions.push(Extensions.JavaScript); | ||
if (compilerOptions.resolveJsonModule) extensions.push(Extensions.Json); | ||
extensions = Extensions.ImplementationFiles; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this is the part where we are saying always looks for js and report error if allowJs is not on correct.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is actually only used by Find Source Definition, which always passes allowJs
. But I removed the condition to be consistent with other resolution modes which always resolve JS, yes.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
And to be clear, normal resolution has resolved JS and reported errors afterwards for like 6 years; that’s not new here 😄
@typescript-bot perf test this |
Heya @andrewbranch, I've started to run the perf test suite on this PR at 4014f30. You can monitor the build here. Update: The results are in! |
@typescript-bot test top100 again, even though you haven’t finished the last one |
Heya @andrewbranch, I've started to run the diff-based top-repos suite on this PR at 53f0a60. You can monitor the build here. Update: The results are in! |
@andrewbranch Here are the results of running the top-repos suite comparing Something interesting changed - please have a look. Details
|
@andrewbranch Here they are:
CompilerComparison Report - main..51471
System
Hosts
Scenarios
TSServerComparison Report - main..51471
System
Hosts
Scenarios
StartupComparison Report - main..51471
System
Hosts
Scenarios
Developer Information: |
^ Yeah, that’s what I expected. I’ve reverted the node_modules searching algorithm back to two full passes, so I expect the second top100 run to come back clean. Updated the PR description:
|
@andrewbranch Here are the results of running the top-repos suite comparing Everything looks good! |
#51973 found an example of a break (a hypothetical break—the project uses "paths": {
"*": [
"node_modules/*",
"types/*"
]
} If we evaluate the resolution of a package name, But in the new algorithm, "paths": {
"*": ["types/*"]
} There’s no need to put |
Fixes two forever-standing resolution bugs:
"./foo.js"
, we would look up"./foo.js.ts"
at a higher priority than"./foo.ts"
. This has been fixed in every module resolution mode..ts
or.d.ts
files, and then another full pass looking for.js
files if the former returned nothing. This results in some priorities that are obviously wrong underallowJs
, and sinceallowJs
doesn’t actually affect resolution results (only whether JS resolutions are errors after the fact), probably always wrong by extension. An example:import "./dir"
should resolve todir.js
, but previously resolved todir/index.ts
in every module resolution algorithm. This has been fixed only innode16
andnodenext
in an attempt to limit breaking changes toclassic
andnode
. Those algorithms now try all valid extensions in priority order for each lookup location. The exception is that node_modules lookups still happen in two passes: one that searches the implementation package followed by the@types
package looking for.ts
/.d.ts
files for eachnode_modules
folder found up the directory spine, followed (if necessary) by another pass up the directory spine looking for.js
files in implementation packages. Making this change required a pretty extensive refactor of how the algorithms deal with extensions, and I think the result is much easier to reason about.For file system hits and watches, this should be a wash—depending on what files are where, a given resolution might have a few more lookups, a few less, or the same number but in a different order.
Like most bug fixes, this is technically a breaking change, but only very strange edge cases that are already misbehaving should be affected.