Skip to content
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

Merged
merged 34 commits into from Dec 21, 2023

Conversation

emmatown
Copy link
Contributor

Fixes #49492

This treats package.json#imports as roughly equivalent to compilerOptions#paths in terms of deciding which specifier to use, it also prefers package.json#imports over compilerOptions#paths

While doing this, I found some broken behaviour around extension replacement which was already observable with the package.json#exports field auto-import but is more visible with package.json#imports so this fixes that as well. autoImportPackageJsonExportsSpecifierEndsInTs shows an example of what's fixed for package.json#exports. That fix and test is in a seperate commit if that's easier to review

@typescript-bot typescript-bot added the For Backlog Bug PRs that fix a backlog bug label Jul 14, 2023
@andrewbranch andrewbranch self-assigned this Jul 14, 2023
@sandersn sandersn added this to Not started in PR Backlog Jul 28, 2023
@sandersn sandersn moved this from Not started to Waiting on reviewers in PR Backlog Jul 28, 2023
Copy link
Member

@andrewbranch andrewbranch left a 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!

if (typeof cachedPackageJson !== "object" && cachedPackageJson !== undefined) {
return undefined;
}
const packageJsonContent = cachedPackageJson?.contents.packageJsonContent || readJson(packageJsonPath, host as { readFile(fileName: string): string | undefined });
Copy link
Member

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.

Copy link
Contributor Author

@emmatown emmatown Aug 16, 2023

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 exports field. I was thinking maybe that should be left for a seperate PR to avoid more ad-hoc handling of package.json reading? Sorry, I was thinking of some other code, I've updated it to 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

Copy link
Member

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).

Copy link
Member

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;
     }
PR Backlog automation moved this from Waiting on reviewers to Needs merge Aug 16, 2023
if (typeof cachedPackageJson !== "object" && cachedPackageJson !== undefined || !host.fileExists(packageJsonPath)) {
return undefined;
}
const packageJsonContent = cachedPackageJson?.contents.packageJsonContent || JSON.parse(host.readFile(packageJsonPath)!);
Copy link
Member

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.

Copy link
Member

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.

if (typeof cachedPackageJson !== "object" && cachedPackageJson !== undefined) {
return undefined;
}
const packageJsonContent = cachedPackageJson?.contents.packageJsonContent || readJson(packageJsonPath, host as { readFile(fileName: string): string | undefined });
Copy link
Member

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).

Copy link
Member

@jakebailey jakebailey left a 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...

if (typeof cachedPackageJson !== "object" && cachedPackageJson !== undefined || !host.fileExists(packageJsonPath)) {
return undefined;
}
const packageJsonContent = cachedPackageJson?.contents.packageJsonContent || JSON.parse(host.readFile(packageJsonPath)!);
Copy link
Member

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.

PR Backlog automation moved this from Needs merge to Waiting on author Aug 16, 2023
Copy link
Member

@weswigham weswigham left a 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.

@lenaggar
Copy link

Just wanted to say, I'm excited about this 😍
Thank you 🙏🏻

@MrHBS
Copy link

MrHBS commented Sep 3, 2023

@emmatown Almost there!

@andrewbranch
Copy link
Member

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.

@typescript-bot typescript-bot removed the For Backlog Bug PRs that fix a backlog bug label Sep 11, 2023
src/compiler/moduleSpecifiers.ts Outdated Show resolved Hide resolved
src/compiler/moduleSpecifiers.ts Outdated Show resolved Hide resolved
src/compiler/moduleSpecifiers.ts Outdated Show resolved Hide resolved
src/compiler/moduleSpecifiers.ts Fixed Show resolved Hide resolved
@MrHBS
Copy link

MrHBS commented Nov 15, 2023

So umm what is left with this PR?

@andrewbranch
Copy link
Member

@typescript-bot pack this

@typescript-bot
Copy link
Collaborator

typescript-bot commented Nov 15, 2023

Heya @andrewbranch, I've started to run the tarball bundle task on this PR at 1f9f214. You can monitor the build here.

@typescript-bot
Copy link
Collaborator

typescript-bot commented Nov 15, 2023

Hey @andrewbranch, I've packed this into an installable tgz. You can install it for testing by referencing it in your package.json like so:

{
    "devDependencies": {
        "typescript": "https://typescript.visualstudio.com/cf7ac146-d525-443c-b23c-0d58337efebc/_apis/build/builds/158604/artifacts?artifactName=tgz&fileId=6668D7F09CE1B8B51DFE74B4A90ADB04B9F2C5DCE09CEE086EFFCF435E45884202&fileName=/typescript-5.4.0-insiders.20231115.tgz"
    }
}

and then running npm install.


There is also a playground for this build and an npm module you can use via "typescript": "npm:@typescript-deploys/pr-build@5.4.0-pr-55015-10".;

@andrewbranch
Copy link
Member

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.

src/compiler/moduleSpecifiers.ts Outdated Show resolved Hide resolved
src/compiler/moduleSpecifiers.ts Show resolved Hide resolved
src/compiler/moduleSpecifiers.ts Outdated Show resolved Hide resolved
@Andarist
Copy link
Contributor

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 MatchingMode.Pattern case. To fix it there I propose to tweak startsWith and endsWith to optionally accept ignoreCase, WDYT?

@andrewbranch
Copy link
Member

Sounds good to me 👍

@Andarist
Copy link
Contributor

cool, done! :)

Copy link
Member

@andrewbranch andrewbranch left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Big thanks to @emmatown and @Andarist! 🌟

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)) {
Copy link
Member

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)

Copy link
Contributor

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.

@andrewbranch andrewbranch merged commit fbcdb8c into microsoft:main Dec 21, 2023
19 checks passed
PR Backlog automation moved this from Waiting on author to Done Dec 21, 2023
@Andarist Andarist deleted the auto-import-package-json-imports branch December 21, 2023 17:52
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
For Milestone Bug PRs that fix a bug with a specific milestone
Projects
PR Backlog
  
Done
Development

Successfully merging this pull request may close these issues.

[suggestions] Provide import suggestions for imports from package.json "imports" field
10 participants