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

module: expose getPackageJSON utility #55229

Open
wants to merge 26 commits into
base: main
Choose a base branch
from

Conversation

JakobJingleheimer
Copy link
Contributor

@JakobJingleheimer JakobJingleheimer commented Oct 1, 2024

Finally got around to exposing one of the utilities we've long-discussed providing to users.

Currently, users (particularly library authors like yarn, who I believe originally requested this) have to re-implement this functionality.

I ran into this issue when building a codemod that needs to consume pjson.types / pjson.exports[…].types

JakobJingleheimer/correct-ts-specifiers#6

References:

@nodejs-github-bot
Copy link
Collaborator

Review requested:

  • @nodejs/loaders

@nodejs-github-bot nodejs-github-bot added lib / src Issues and PRs related to general changes in the lib or src directory. needs-ci PRs that need a full CI run. labels Oct 1, 2024
@JakobJingleheimer JakobJingleheimer added c++ Issues and PRs that require attention from people who are familiar with C++. semver-minor PRs that contain new features and should be released in the next minor version. labels Oct 1, 2024
doc/api/module.md Outdated Show resolved Hide resolved
@RedYetiDev RedYetiDev added module Issues and PRs related to the module subsystem. commit-queue-squash Add this label to instruct the Commit Queue to squash all the PR commits into the first one. and removed lib / src Issues and PRs related to general changes in the lib or src directory. labels Oct 1, 2024
Copy link

codecov bot commented Oct 2, 2024

Codecov Report

Attention: Patch coverage is 96.07843% with 4 lines in your changes missing coverage. Please review.

Project coverage is 88.40%. Comparing base (103b843) to head (a9e3ded).
Report is 40 commits behind head on main.

Files with missing lines Patch % Lines
src/node_modules.cc 84.21% 0 Missing and 3 partials ⚠️
lib/internal/modules/cjs/loader.js 88.88% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main   #55229      +/-   ##
==========================================
+ Coverage   88.23%   88.40%   +0.17%     
==========================================
  Files         651      652       +1     
  Lines      183863   186639    +2776     
  Branches    35824    36069     +245     
==========================================
+ Hits       162235   165005    +2770     
+ Misses      14932    14899      -33     
- Partials     6696     6735      +39     
Files with missing lines Coverage Δ
lib/internal/modules/esm/resolve.js 96.53% <100.00%> (ø)
lib/internal/modules/package_json_reader.js 100.00% <100.00%> (ø)
lib/module.js 100.00% <100.00%> (ø)
src/node_modules.h 100.00% <ø> (ø)
lib/internal/modules/cjs/loader.js 97.31% <88.88%> (-0.11%) ⬇️
src/node_modules.cc 78.96% <84.21%> (+0.85%) ⬆️

... and 45 files with indirect coverage changes

@JakobJingleheimer

This comment was marked as resolved.

@panva

This comment was marked as resolved.

doc/api/module.md Outdated Show resolved Hide resolved
doc/api/module.md Outdated Show resolved Hide resolved

return {
data: {
__proto__: null,
Copy link
Contributor

Choose a reason for hiding this comment

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

Having null-prototype object returned to the object feels weird

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I agree. I did it for consistency between everything (not everything is used internally and needs the null prototype).

> Stability: 1.1 - Active Development
* `startPath` {string} Where to start looking
* `everything` {boolean} Whether to return the full contents of the found package.json
Copy link
Contributor

@aduh95 aduh95 Oct 4, 2024

Choose a reason for hiding this comment

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

I'm not sure it is a good idea to introduce yet-another way of loading a JSON file, wouldn't it be simpler to return the path and let the user deal with reading/parsing the file?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I considered that (there's another PR I opened and closed that exposed findPackageJSON, which did exactly that). We're already reading the pjson and caching it, so IMO that would force a de-op as well as force the user to manually do what we're already doing. So, why?

Copy link
Member

@joyeecheung joyeecheung Oct 4, 2024

Choose a reason for hiding this comment

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

I think returning the parsed data means every time we consider parsing another field in the package.json, it needs to be surfaced into this API, even though that field may not even be useful for tools, but we'll end up paying the serialization/deserialization cost even though internally only selected places need selected fields. IMO even just returning the string would be better than this. Users need this for the lookup algorithm, they tend to have other fields to parse anyway.

Copy link
Member

Choose a reason for hiding this comment

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

May I recommend a different naming and API surface?

module.findPackageJson(path, { includeContents: true })

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Sorry, @joyeecheung I'm not sure I follow in your message. I think you've misunderstood the behaviour here: internally, we do not use the full version—we do use getNearestParentPackageJSON but do not set everything to true. When everything is not true, only the select fields we use internally are parsed. That has not changed in this PR.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

May I recommend a different naming and API surface?

module.findPackageJson(path, { includeContents: true })

@anonrig if you recall, when we were working on this a few days ago, that did not make sense due to how the C++ worked, which is what caused me to abandon the findPackageJSON + getPackageJSON route in the first place.

Regarding "find" + includeContents, this seems counter-intuitive to me: I expect something called "find" to only locate, not retrieve. I could perhaps see the inverse

module.getPackageJSON(path, { includeContents: false })

I do not like either option though because the shape of the return is changed significantly in a foot-gun way:

getPackageJSON(path, false) // '…/package.json'

getPackageJSON(path, true)  // { data: {…}, path: '…/package.json' }

(I simplified the 2nd arg for brevity, not necessarily to say we shouldn't use an object)

I suppose it could always return an object for consistency, but that seems like a clumsy compromise

getPackageJSON(path, false) // { path: '…/package.json' }

getPackageJSON(path, true)  // { data: {…}, path: '…/package.json' }

If we were to do this, I think exposing 2 different utils (findPackageJSON + getPackageJSON) would be better.

doc/api/module.md Outdated Show resolved Hide resolved
Comment on lines 63 to 64
...(main != null && { main }),
...(type != null && { type }),
Copy link
Member

Choose a reason for hiding this comment

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

Why did you change these lines? Meaning anything after the highlighted lines included 63-64.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Because as it was before, empty fields are forced to be present but with no value:

{
  "name": "foo",
  "exports": "./index.js"
}
{
  name: 'foo'
  main: undefined,
  exports: './index.js',
}

The only result is hassle and gotchas: ex 'main' in pkgtrue, which is a lie ("main" is not in the package.json).

And in tests, it forces you to add extra properties that don't matter:

assert.deepStrictEqual(pkg, {
  name: 'foo'
  exports: './index.js',
});

Pedantically fails because main: undefined is not explicitly specified.

lib/internal/modules/package_json_reader.js Outdated Show resolved Hide resolved
lib/internal/modules/package_json_reader.js Show resolved Hide resolved
lib/internal/modules/package_json_reader.js Outdated Show resolved Hide resolved
*/
function getNearestParentPackageJSON(checkPath) {
const result = modulesBinding.getNearestParentPackageJSON(checkPath);
function getNearestParentPackageJSON(startPath, everything = false) {
Copy link
Member

Choose a reason for hiding this comment

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

Instead of everything, can we change this to includeContents?

Copy link
Member

Choose a reason for hiding this comment

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

And preferably make this an object rather than a plain argument. getPackageJSON(path, true) does not help the developer, unless they look into the documentation.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

includeContents does not make sense: it will include some of the contents regardless.

does not help the developer, unless they look into the documentation.

IntelliSense :)

I think an object makes sense when there are (or potentially will be) multiple configuration options. Here it seems verbose.

PackageConfig['name'],
PackageConfig['main'],
PackageConfig['type'],
RecognisedPackageConfig['name'],
Copy link
Member

Choose a reason for hiding this comment

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

These changes seems to be unnecessary

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I disagree: PackageConfig sounds like the superset of all config options. That's not what it actually is. The potential for confusion is now much more because there are now both options (subset and superset). This rename removes the ambiguity to preclude confusion.

export type PackageConfig = {
pjsonPath: string
export type RecognisedPackageConfig = {
pjsonPath: URL['pathname']
Copy link
Member

Choose a reason for hiding this comment

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

This would resolve to string, and variable name also includes path suffix. I recommend just using string

Suggested change
pjsonPath: URL['pathname']
pjsonPath: string

Copy link
Contributor Author

Choose a reason for hiding this comment

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

We talked about this on our screenshare a few days ago, and I showed why what I did is better: Whilst ultimate, yes, it does resolve to string, not any string is acceptable, so the extra human-friendly context provides valuable info; also, IntelliSense displays it as URL['pathname'].

If we really wanted to make it correct, we could create a template literal type to properly validate via typescript.

Comment on lines 11 to 13
export type FullPackageConfig = RecognisedPackageConfig & {
[key: string]: unknown,
}
Copy link
Member

Choose a reason for hiding this comment

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

This would make every value of FullPackageConfig unknown since unknown has higher precedence than any literal type, and I don't think we need this type in here, since it is a return value and depends on the contents of the package.json. Since we are not accessing them in node.js core, we don't need to add a type for them.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

This would make every value of FullPackageConfig unknown

That's not correct: TS Playground

I don't think we need this type in here […] since we are not accessing them in node.js core

I disagree: Failing to do this causes it to lie. A lying tool is worse than no tool. Even if we're not consuming them, the extra info could still be valuable, and a lie could cause an unaware dev to make a mistake. There is no drawback to doing it correctly, so we should do it correctly.

string | undefined, // exports
string | undefined, // imports
string | undefined, // raw json available for experimental policy
RecognisedPackageConfig['pjsonPath'], // pjson file path
Copy link
Member

Choose a reason for hiding this comment

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

This change seems unrelated. experimental policy doesn't exist anymore, so we might just replace this line with raw json.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

so we might just replace this line with raw json.

I'm not sure what you're talking about. The type dec was merely wrong—this element was already pjsonPath.

> Stability: 1.1 - Active Development
* `startPath` {string} Where to start looking
* `everything` {boolean} Whether to return the full contents of the found package.json
Copy link
Member

Choose a reason for hiding this comment

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

May I recommend a different naming and API surface?

module.findPackageJson(path, { includeContents: true })

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
c++ Issues and PRs that require attention from people who are familiar with C++. commit-queue-squash Add this label to instruct the Commit Queue to squash all the PR commits into the first one. module Issues and PRs related to the module subsystem. needs-ci PRs that need a full CI run. semver-minor PRs that contain new features and should be released in the next minor version.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

7 participants