Skip to content

Add auto-import for the package.jsonimports field - #55015

Merged
Andrew Branch (andrewbranch) merged 34 commits into
microsoft:mainfrom
emmatown:auto-import-package-json-imports
Dec 21, 2023
Merged

Add auto-import for the package.jsonimports field#55015
Andrew Branch (andrewbranch) merged 34 commits into
microsoft:mainfrom
emmatown:auto-import-package-json-imports

Conversation

@emmatown

Copy link
Copy Markdown
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-botTypeScript Bot (typescript-bot) added the For Backlog Bug PRs that fix a backlog bug label Jul 14, 2023
Comment threadsrc/compiler/moduleSpecifiers.ts

Copy link
Copy Markdown
Member

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!

Comment threadtests/cases/fourslash/autoImportPackageJsonImportsLength1.ts
Comment threadsrc/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 });

Copy link
Copy Markdown
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.

@emmatownEmma Hamilton (emmatown)Aug 16, 2023

Copy link
Copy Markdown
ContributorAuthor

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
Copy Markdown
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
Copy Markdown
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 4dcf24e..d6022af 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;
}
Comment threadsrc/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)!);

Copy link
Copy Markdown
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
Copy Markdown
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.

Comment threadtests/cases/fourslash/autoImportPackageJsonImportsLength1.ts
Comment threadtests/cases/fourslash/autoImportPackageJsonImportsLength2.ts
Comment threadsrc/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 });

Copy link
Copy Markdown
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).

@jakebaileyJake Bailey (jakebailey) left a comment

Copy link
Copy Markdown
Member

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

Comment threadsrc/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)!);

Copy link
Copy Markdown
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.

Comment threadtests/cases/fourslash/autoImportPackageJsonImportsLength2.ts

Copy link
Copy Markdown
Member

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
Copy Markdown

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

@TKDev7

Copy link
Copy Markdown

Emma Hamilton (@emmatown) Almost there!

@andrewbranch

Copy link
Copy Markdown
Member

I would love to get this into 5.3. Emma Hamilton (@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-botTypeScript Bot (typescript-bot) added For Milestone Bug PRs that fix a bug with a specific milestone and removed For Backlog Bug PRs that fix a backlog bug labels Sep 11, 2023
@emmatown

Copy link
Copy Markdown
ContributorAuthor

Sorry, probably not anytime soon 😔, just pushed a test for conditions and merged main in if that's helpful but if someone on the team could address the other feedback, that would be great!

@andrewbranch
Andrew Branch (andrewbranch) merged commit fbcdb8c into microsoft:mainDec 21, 2023
@Andarist
Mateusz Burzyński (Andarist) deleted the auto-import-package-json-imports branch December 21, 2023 17:52
Chris Krycho (chriskrycho) added a commit to chriskrycho/repro-ts-5.4-glimmer-exports-issue that referenced this pull request Jan 9, 2024
This shows a failure mode around a package which (oddly!) publishes an
`index.ts` file in its root but also specifies both the `types` and
`exports.types` keys in `package.json` as pointing to an emitted `.d.ts`
file in a `dist` directory. If the `index.ts` is removed or renamed,
`tsc` correctly uses `types` to resolve the location.
This appears to have been introduced by microsoft/TypeScript#55015.
@webpro

Copy link
Copy Markdown
Contributor

Inspired by and in anticipation of this feature being released, I wrote https://www.webpro.nl/articles/using-subpath-imports-and-path-aliases (please let me know if there's mistakes). Thanks Emma Hamilton (@emmatown) and Mateusz Burzyński (@Andarist) for the hard work!

// @Filename: /a.ts
//// something/**/

verify.importFixModuleSpecifiers("", ["#something.js"]);

@AndaristMateusz Burzyński (Andarist)Mar 9, 2024

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Andrew Branch (@andrewbranch) should this autocomplete #something.js? I'm investigating some stuff and I concluded the conclusion that perhaps this should actually be just #something. @module: nodenext doesn't prescribe the specifier ending - and since this package.json doesn't have a type it's an implied CJS file so the ending is optional.

note for myself: fixing this would likely depend on using getModuleSpecifierEndingPreference appropriately

Hmm, I still observe some inconsistencies elsewhere but now I concluded that this one is correct - imports/exports always require extensions (regardless of the module format).

Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

For Milestone BugPRs that fix a bug with a specific milestone

Projects

Archived in project

Development

Successfully merging this pull request may close these issues.

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

13 participants

@emmatown@lenaggar@TKDev7@andrewbranch@jakebailey@Andarist@typescript-bot@webpro@weswigham@Jack-Works@sheetalkamat@github-advanced-security@sandersn