-
-
Notifications
You must be signed in to change notification settings - Fork 35.4k
module: expose getPackageJSON utility
#55229
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
Changes from 13 commits
48ab696
dcddd1e
98c72f4
d4ede54
92f2c62
b83707b
e0beefa
6e715ec
458dcca
903d201
b567e9a
ed4db64
9a4b1d1
c23c8f1
c865d55
a9bf393
190e315
a9e3ded
926b2b0
485b1e3
f824ce9
7deeb7f
cf49f71
358486c
d9bdf23
bf5265d
3e1cda8
da29815
b531497
eef2b60
395cfa6
841848f
945c207
4adac4c
0064379
eed739c
864b98a
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -217,6 +217,39 @@ added: v22.8.0 | |
| * Returns: {string|undefined} Path to the [module compile cache][] directory if it is enabled, | ||
| or `undefined` otherwise. | ||
|
|
||
| ### `module.getPackageJSON(startPath[, everything])` | ||
|
|
||
| <!-- YAML | ||
| added: VERSION | ||
| --> | ||
|
|
||
| > Stability: 1.1 - Active Development | ||
|
|
||
| * `startPath` {URL['pathname']} Where to start looking | ||
| * `everything` {boolean} Whether to return the full contents of the found package.json | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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?
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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.
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. May I recommend a different naming and API surface?
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
@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 Regarding "find" +
I do not like either option though because the shape of the return is changed significantly in a foot-gun way: (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 If we were to do this, I think exposing 2 different utils ( |
||
| * Returns: {undefined | { | ||
| data: { | ||
| name?: string, | ||
| type?: 'commonjs' | 'module' | 'none', | ||
| exports?: string | string[] | Record<string, unknown>, | ||
| imports?: string | string[] | Record<string, unknown>, | ||
| [key: string]?: unknown, | ||
| }, | ||
| path: URL['pathname'], | ||
| }} | ||
|
|
||
| In addition to being available to users, this utility is used internally when | ||
|
JakobJingleheimer marked this conversation as resolved.
Outdated
|
||
| resolving various aspects about a module. | ||
|
|
||
| ```mjs | ||
| import { getPackageJSON } from 'node:module'; | ||
|
|
||
| const pjson = getPackageJSON(import.meta.resolve('some-package'), true)?.data; | ||
|
|
||
| pjson?.name; // 'some-package-real-name' | ||
| pjson?.types; // './index.d.ts' | ||
| ``` | ||
|
|
||
| ### `module.isBuiltin(moduleName)` | ||
|
|
||
| <!-- YAML | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -4,17 +4,26 @@ const { | |
| ArrayIsArray, | ||
| JSONParse, | ||
| ObjectDefineProperty, | ||
| StringPrototypeLastIndexOf, | ||
| StringPrototypeSlice, | ||
| } = primordials; | ||
| const modulesBinding = internalBinding('modules'); | ||
| const { resolve, sep } = require('path'); | ||
| const { resolve } = require('path'); | ||
| const { kEmptyObject } = require('internal/util'); | ||
| const { | ||
| codes: { | ||
| ERR_INVALID_ARG_TYPE, | ||
| }, | ||
| } = require('internal/errors'); | ||
|
|
||
| /** | ||
| * @typedef {import('typings/internalBinding/modules').FullPackageConfig} FullPackageConfig | ||
| * @typedef {import('typings/internalBinding/modules').RecognisedPackageConfig} RecognisedPackageConfig | ||
| * @typedef {import('typings/internalBinding/modules').SerializedPackageConfig} SerializedPackageConfig | ||
| */ | ||
|
|
||
| /** | ||
| * @param {string} path | ||
| * @param {import('typings/internalBinding/modules').SerializedPackageConfig} contents | ||
| * @returns {import('typings/internalBinding/modules').PackageConfig} | ||
| * @param {SerializedPackageConfig} contents | ||
| * @returns {RecognisedPackageConfig} | ||
| */ | ||
| function deserializePackageJSON(path, contents) { | ||
| if (contents === undefined) { | ||
|
|
@@ -51,19 +60,23 @@ function deserializePackageJSON(path, contents) { | |
| exists: true, | ||
| pjsonPath, | ||
| name, | ||
| main, | ||
| type, | ||
| // This getters are used to lazily parse the imports and exports fields. | ||
| get imports() { | ||
| const value = requiresJSONParse(plainImports) ? JSONParse(plainImports) : plainImports; | ||
| ObjectDefineProperty(this, 'imports', { __proto__: null, value }); | ||
| return this.imports; | ||
| }, | ||
| get exports() { | ||
| const value = requiresJSONParse(plainExports) ? JSONParse(plainExports) : plainExports; | ||
| ObjectDefineProperty(this, 'exports', { __proto__: null, value }); | ||
| return this.exports; | ||
| }, | ||
| ...(main != null && { main }), | ||
| ...(type != null && { type }), | ||
|
JakobJingleheimer marked this conversation as resolved.
Outdated
|
||
| ...(plainImports != null && { | ||
| // This getters are used to lazily parse the imports and exports fields. | ||
| get imports() { | ||
| const value = requiresJSONParse(plainImports) ? JSONParse(plainImports) : plainImports; | ||
| ObjectDefineProperty(this, 'imports', { __proto__: null, value }); | ||
| return this.imports; | ||
| }, | ||
| }), | ||
| ...(plainExports != null && { | ||
| get exports() { | ||
| const value = requiresJSONParse(plainExports) ? JSONParse(plainExports) : plainExports; | ||
| ObjectDefineProperty(this, 'exports', { __proto__: null, value }); | ||
| return this.exports; | ||
| }, | ||
| }), | ||
| }; | ||
| } | ||
|
|
||
|
|
@@ -75,7 +88,7 @@ function deserializePackageJSON(path, contents) { | |
| * specifier?: URL | string, | ||
| * isESM?: boolean, | ||
| * }} options | ||
| * @returns {import('typings/internalBinding/modules').PackageConfig} | ||
| * @returns {RecognisedPackageConfig} | ||
| */ | ||
| function read(jsonPath, { base, specifier, isESM } = kEmptyObject) { | ||
| // This function will be called by both CJS and ESM, so we need to make sure | ||
|
|
@@ -94,7 +107,7 @@ function read(jsonPath, { base, specifier, isESM } = kEmptyObject) { | |
| * @deprecated Expected to be removed in favor of `read` in the future. | ||
| * Behaves the same was as `read`, but appends package.json to the path. | ||
| * @param {string} requestPath | ||
| * @return {PackageConfig} | ||
| * @return {RecognisedPackageConfig} | ||
| */ | ||
| function readPackage(requestPath) { | ||
| // TODO(@anonrig): Remove this function. | ||
|
|
@@ -104,29 +117,56 @@ function readPackage(requestPath) { | |
| /** | ||
| * Get the nearest parent package.json file from a given path. | ||
| * Return the package.json data and the path to the package.json file, or undefined. | ||
| * @param {string} checkPath The path to start searching from. | ||
| * @returns {undefined | {data: import('typings/internalBinding/modules').PackageConfig, path: string}} | ||
| * @param {URL['href'] | URL['pathname']} startPath The path to start searching from. | ||
| * @param {boolean} everything Whether to include the full contents of the package.json. | ||
| * @returns {undefined | { | ||
| * data: everything extends true ? FullPackageConfig : RecognisedPackageConfig, | ||
| * path: URL['pathname'], | ||
| * }} | ||
| */ | ||
| function getNearestParentPackageJSON(checkPath) { | ||
| const result = modulesBinding.getNearestParentPackageJSON(checkPath); | ||
| function getNearestParentPackageJSON(startPath, everything = false) { | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Instead of everything, can we change this to
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. And preferably make this an object rather than a plain argument.
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
IntelliSense :) I think an object makes sense when there are (or potentially will be) multiple configuration options. Here it seems verbose. |
||
| if (typeof startPath !== 'string') { | ||
| throw new ERR_INVALID_ARG_TYPE('startPath', 'string', startPath); | ||
| } | ||
|
JakobJingleheimer marked this conversation as resolved.
Outdated
|
||
| if ( | ||
| everything !== undefined && | ||
| typeof everything !== 'boolean' | ||
| ) { | ||
|
JakobJingleheimer marked this conversation as resolved.
Outdated
|
||
| throw new ERR_INVALID_ARG_TYPE('everything', 'boolean', everything); | ||
|
JakobJingleheimer marked this conversation as resolved.
Outdated
|
||
| } | ||
|
|
||
| if (everything) { | ||
| const result = modulesBinding.getNearestRawParentPackageJSON(startPath); | ||
|
JakobJingleheimer marked this conversation as resolved.
|
||
|
|
||
| return { | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Does this also need to be null prototyped
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @aduh95 please advise 🙏
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. no, it's user-facing |
||
| data: { | ||
| __proto__: null, | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Having null-prototype object returned to the object feels weird
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I agree. I did it for consistency between
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. That feels like another reason not to expose
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The reason is because we've already done the work, and handled the caching. Doing it this way also provides better performance. Users having to do exactly the same thing is the whole reason we as engineers create utilities, and there is basically no chance any user would not subsequently parse it. Freezing seems like a good idea. |
||
| ...JSONParse(result[0]), | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This makes an unnecessary deep copy.
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think I need to spread here in order to void the prototype. Or is voiding the prototype here not necessary because it came from c-land? |
||
| }, | ||
| path: result[1], | ||
| }; | ||
| } | ||
|
|
||
| const result = modulesBinding.getNearestParentPackageJSON(startPath); | ||
|
|
||
| if (result === undefined) { | ||
| return undefined; | ||
| } | ||
|
|
||
| const data = deserializePackageJSON(checkPath, result); | ||
| const data = deserializePackageJSON(startPath, result); | ||
|
|
||
| const { pjsonPath: path } = data; | ||
|
|
||
| // Path should be the root folder of the matched package.json | ||
| // For example for ~/path/package.json, it should be ~/path | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Q: Why does this statement doesn't hold anymore?
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
| const path = StringPrototypeSlice(data.pjsonPath, 0, StringPrototypeLastIndexOf(data.pjsonPath, sep)); | ||
| delete data.exists; | ||
| delete data.pjsonPath; | ||
|
JakobJingleheimer marked this conversation as resolved.
Outdated
|
||
|
|
||
| return { data, path }; | ||
| } | ||
|
|
||
| /** | ||
| * Returns the package configuration for the given resolved URL. | ||
| * @param {URL | string} resolved - The resolved URL. | ||
| * @returns {import('typings/internalBinding/modules').PackageConfig} - The package configuration. | ||
| * @returns {RecognisedPackageConfig} - The package configuration. | ||
| */ | ||
| function getPackageScopeConfig(resolved) { | ||
| const result = modulesBinding.getPackageScopeConfig(`${resolved}`); | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,9 @@ | ||
| { | ||
| "name": "package-with-unrecognised-fields", | ||
| "type": "module", | ||
| "exports": { | ||
| "default": "./index.js", | ||
| "types": "./index.d.ts" | ||
| }, | ||
| "unrecognised": true | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| { | ||
| "name": "package-with-unrecognised-fields", | ||
| "type": "module", | ||
| "types": "./index.d.ts" | ||
| } |
Uh oh!
There was an error while loading. Please reload this page.