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
Add auto-import for the package.json imports field
#55015
Add auto-import for the package.json imports field
#55015
Conversation
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.
Thank you for adding this!
src/compiler/moduleSpecifiers.ts
Outdated
| if (typeof cachedPackageJson !== "object" && cachedPackageJson !== undefined) { | ||
| return undefined; | ||
| } | ||
| const packageJsonContent = cachedPackageJson?.contents.packageJsonContent || readJson(packageJsonPath, host as { readFile(fileName: string): string | undefined }); |
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.
Use JSON.parse for package.json files. readJson uses the TypeScript parser to construct an AST and then converts that to an object 😵 I’ve been meaning to audit existing uses of readJson and then rename it; I know some can be replaced.
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 saw that 😅. I was aiming to consistent with the implementation of auto-import for the Sorry, I was thinking of some other code, I've updated it to exports field. I was thinking maybe that should be left for a seperate PR to avoid more ad-hoc handling of package.json reading?JSON.parse.
Use JSON.parse for package.json files. readJson uses the TypeScript parser to construct an AST and then converts that to an object 😵 I’ve been meaning to audit existing uses of readJson and then rename it; I know some can be replaced.
I've actually checked btw and all the usages of readJson are for reading package.jsons and the readJsonOrUndefined function that's used by readJson is only used for build info so all of them can really be replaced with JSON.parse
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'm not sure that replacing everything with JSON.parse is generally correct, because it'll throw if the file has malformed JSON. The services project has a helper, tryParseJson, which catches this, and that's used by its createPackageJsonInfo (which then makes its package.json cache).
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.
Yes, JSON.parse should be in a try/catch, particularly when reading a local package.json as opposed to one in node_modules. It’s virtually impossible for a node_modules package.json to be malformed so I don’t care as much there. My point was only that we shouldn’t be creating TypeScript AST nodes and issuing parse diagnostics just to get an object representation of a JSON file.
diff --git a/src/compiler/moduleSpecifiers.ts b/src/compiler/moduleSpecifiers.ts
index 4dcf24ea4b..d6022afe4b 100644
--- a/src/compiler/moduleSpecifiers.ts
+++ b/src/compiler/moduleSpecifiers.ts
@@ -90,7 +90,6 @@ import {
pathIsBareSpecifier,
pathIsRelative,
PropertyAccessExpression,
- readJson,
removeExtension,
removeFileExtension,
removeSuffix,
@@ -894,11 +893,11 @@ function tryGetModuleNameFromPackageJsonImports(moduleFileName: string, sourceDi
}
const packageJsonPath = combinePaths(ancestorDirectoryWithPackageJson, "package.json");
const cachedPackageJson = host.getPackageJsonInfoCache?.()?.getPackageJsonInfo(packageJsonPath);
- if (typeof cachedPackageJson !== "object" && cachedPackageJson !== undefined) {
+ if (typeof cachedPackageJson !== "object" && cachedPackageJson !== undefined || !host.fileExists(packageJsonPath)) {
return undefined;
}
- const packageJsonContent = cachedPackageJson?.contents.packageJsonContent || readJson(packageJsonPath, host as { readFile(fileName: string): string | undefined });
- const imports = (packageJsonContent as any).imports;
+ const packageJsonContent = cachedPackageJson?.contents.packageJsonContent || JSON.parse(host.readFile(packageJsonPath)!);
+ const imports = packageJsonContent?.imports;
if (!imports) {
return undefined;
}
src/compiler/moduleSpecifiers.ts
Outdated
| if (typeof cachedPackageJson !== "object" && cachedPackageJson !== undefined || !host.fileExists(packageJsonPath)) { | ||
| return undefined; | ||
| } | ||
| const packageJsonContent = cachedPackageJson?.contents.packageJsonContent || JSON.parse(host.readFile(packageJsonPath)!); |
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 don't think this is a new problem as this file already has code that looks exactly like this, but I'm somewhat certain that we should be using readJson for this; if the JSON is malformed, we'll throw here and then I don't know what will happen. readJson is what's used to get stuff into the cache, and generally speaking other raw JSON.parse calls all appear to at least try/catch.
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 see that there's another thread about this at #55015 (comment); if we want to leave this JSON.parse in here given we already do that, that's fine, but overall if we're avoiding readJson because it calls the wrong JSON parser, but the alternatiive is to call JSON.parse unguarded... that feels spooky to me.
src/compiler/moduleSpecifiers.ts
Outdated
| if (typeof cachedPackageJson !== "object" && cachedPackageJson !== undefined) { | ||
| return undefined; | ||
| } | ||
| const packageJsonContent = cachedPackageJson?.contents.packageJsonContent || readJson(packageJsonPath, host as { readFile(fileName: string): string | undefined }); |
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'm not sure that replacing everything with JSON.parse is generally correct, because it'll throw if the file has malformed JSON. The services project has a helper, tryParseJson, which catches this, and that's used by its createPackageJsonInfo (which then makes its package.json cache).
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.
Reading the tests, I'm a little confused about the import conditions which map to .ts files...
src/compiler/moduleSpecifiers.ts
Outdated
| if (typeof cachedPackageJson !== "object" && cachedPackageJson !== undefined || !host.fileExists(packageJsonPath)) { | ||
| return undefined; | ||
| } | ||
| const packageJsonContent = cachedPackageJson?.contents.packageJsonContent || JSON.parse(host.readFile(packageJsonPath)!); |
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 see that there's another thread about this at #55015 (comment); if we want to leave this JSON.parse in here given we already do that, that's fine, but overall if we're avoiding readJson because it calls the wrong JSON parser, but the alternatiive is to call JSON.parse unguarded... that feels spooky to me.
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.
None of the new tests test conditional imports - those probably need to be checked. Complex examples of nested conditions like
{
"imports": {
"#thing": {
"types": { "import": "./types-esm/thing.d.mts", "require": "./types/thing.d.ts" },
"default": { "import": "./esm/thing.mjs", "require": "./dist/thing.js" }
}
}
}are actually more common in the wild than you'd think for exports, and likely have just as much utility for imports.
|
Just wanted to say, I'm excited about this 😍 |
|
@emmatown Almost there! |
|
I would love to get this into 5.3. @emmatown, are you interested in continuing? If not, someone on the team can probably address the remaining feedback and get it ready to merge. |
|
So umm what is left with this PR? |
|
@typescript-bot pack this |
|
Heya @andrewbranch, I've started to run the tarball bundle task on this PR at 1f9f214. You can monitor the build here. |
|
Hey @andrewbranch, I've packed this into an installable tgz. You can install it for testing by referencing it in your and then running There is also a playground for this build and an npm module you can use via |
|
I deduplicated the emitter code per @sheetalkamat’s suggestion and found a bug by testing on a real project. I added a failing test but I’m out of time until the end of the day or maybe tomorrow. Will pick it back up and finish it unless @Andarist tells me he’s going to beat me to it. |
I pushed out a fix for this particular issue :P but I believe that a similar case is still broken within |
|
Sounds good to me 👍 |
|
cool, done! :) |
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.
| break; | ||
| case MatchingMode.Pattern: | ||
| const starPos = pathOrPattern.indexOf("*"); | ||
| const leadingSlice = pathOrPattern.slice(0, starPos); | ||
| const trailingSlice = pathOrPattern.slice(starPos + 1); | ||
| if (startsWith(targetFilePath, leadingSlice) && endsWith(targetFilePath, trailingSlice)) { | ||
| if (extensionSwappedTarget && startsWith(extensionSwappedTarget, leadingSlice, ignoreCase) && endsWith(extensionSwappedTarget, trailingSlice, ignoreCase)) { |
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 think instead of changing starts with and endsWith, may be we need helper that uses getCanonicalFileName intead for startsWith(extensionSwappedTarget, leadingSlice, ignoreCase) && endsWith(extensionSwappedTarget, trailingSlice, ignoreCase) type of pattern (similar to tryRemovePrefix)
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.
Is there a test case that I should add and cover for? Code style-wise, I think that I prefer the current approach since it feels simpler to me. But I, of course, know way less about potential edge cases of this whole section and overall coding guidelines in this codebase.
Fixes #49492
This treats
package.json#importsas roughly equivalent tocompilerOptions#pathsin terms of deciding which specifier to use, it also preferspackage.json#importsovercompilerOptions#pathsWhile doing this, I found some broken behaviour around extension replacement which was already observable with the
package.json#exportsfield auto-import but is more visible withpackage.json#importsso this fixes that as well.autoImportPackageJsonExportsSpecifierEndsInTsshows an example of what's fixed forpackage.json#exports. That fix and test is in a seperate commit if that's easier to review