Skip to content

Commit adda37f

Browse files
JakobJingleheimerRafaelGSS
authored andcommitted
module: add findPackageJSON util
PR-URL: #55412 Reviewed-By: Matteo Collina <matteo.collina@gmail.com> Reviewed-By: Antoine du Hamel <duhamelantoine1995@gmail.com>
1 parent 2d19614 commit adda37f

17 files changed

Lines changed: 516 additions & 151 deletions

File tree

‎doc/api/module.md‎

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -217,6 +217,88 @@ added: v22.8.0
217217
* Returns: {string|undefined} Path to the [module compile cache][] directory if it is enabled,
218218
or `undefined` otherwise.
219219
220+
### `module.findPackageJSON(specifier[, base])`
221+
222+
<!-- YAML
223+
added: REPLACEME
224+
-->
225+
226+
> Stability: 1.1 - Active Development
227+
228+
* `specifier` {string|URL} The specifier for the module whose `package.json` to
229+
retrieve. When passing a _bare specifier_, the `package.json` at the root of
230+
the package is returned. When passing a _relative specifier_ or an _absolute specifier_,
231+
the closest parent `package.json` is returned.
232+
* `base` {string|URL} The absolute location (`file:` URL string or FS path) of the
233+
containing module. For CJS, use `__filename` (not `__dirname`!); for ESM, use
234+
`import.meta.url`. You do not need to pass it if `specifier` is an `absolute specifier`.
235+
* Returns: {string|undefined} A path if the `package.json` is found. When `startLocation`
236+
is a package, the package's root `package.json`; when a relative or unresolved, the closest
237+
`package.json` to the `startLocation`.
238+
239+
> **Caveat**: Do not use this to try to determine module format. There are many things effecting
240+
> that determination; the `type` field of package.json is the _least_ definitive (ex file extension
241+
> superceeds it, and a loader hook superceeds that).
242+
243+
```text
244+
/path/to/project
245+
├ packages/
246+
├ bar/
247+
bar.js
248+
package.json// name = '@foo/bar'
249+
└ qux/
250+
├ node_modules/
251+
└ some-package/
252+
package.json// name = 'some-package'
253+
qux.js
254+
package.json// name = '@foo/qux'
255+
main.js
256+
package.json// name = '@foo'
257+
```
258+
259+
```mjs
260+
// /path/to/project/packages/bar/bar.js
261+
import { findPackageJSON } from'node:module';
262+
263+
findPackageJSON('..', import.meta.url);
264+
// '/path/to/project/package.json'
265+
// Same result when passing an absolute specifier instead:
266+
findPackageJSON(newURL('../', import.meta.url));
267+
findPackageJSON(import.meta.resolve('../'));
268+
269+
findPackageJSON('some-package', import.meta.url);
270+
// '/path/to/project/packages/bar/node_modules/some-package/package.json'
271+
// When passing an absolute specifier, you might get a different result if the
272+
// resolved module is inside a subfolder that has nested `package.json`.
273+
findPackageJSON(import.meta.resolve('some-package'));
274+
// '/path/to/project/packages/bar/node_modules/some-package/some-subfolder/package.json'
275+
276+
findPackageJSON('@foo/qux', import.meta.url);
277+
// '/path/to/project/packages/qux/package.json'
278+
```
279+
280+
```cjs
281+
// /path/to/project/packages/bar/bar.js
282+
const { findPackageJSON } =require('node:module');
283+
const { pathToFileURL } =require('node:url');
284+
constpath=require('node:path');
285+
286+
findPackageJSON('..', __filename);
287+
// '/path/to/project/package.json'
288+
// Same result when passing an absolute specifier instead:
289+
findPackageJSON(pathToFileURL(path.join(__dirname, '..')));
290+
291+
findPackageJSON('some-package', __filename);
292+
// '/path/to/project/packages/bar/node_modules/some-package/package.json'
293+
// When passing an absolute specifier, you might get a different result if the
294+
// resolved module is inside a subfolder that has nested `package.json`.
295+
findPackageJSON(pathToFileURL(require.resolve('some-package')));
296+
// '/path/to/project/packages/bar/node_modules/some-package/some-subfolder/package.json'
297+
298+
findPackageJSON('@foo/qux', __filename);
299+
// '/path/to/project/packages/qux/package.json'
300+
```
301+
220302
### `module.isBuiltin(moduleName)`
221303
222304
<!-- YAML

‎lib/internal/modules/cjs/loader.js‎

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -602,11 +602,11 @@ function trySelf(parentPath, request) {
602602
try{
603603
const{ packageExportsResolve }=require('internal/modules/esm/resolve');
604604
returnfinalizeEsmResolution(packageExportsResolve(
605-
pathToFileURL(pkg.path+'/package.json'),expansion,pkg.data,
605+
pathToFileURL(pkg.path),expansion,pkg.data,
606606
pathToFileURL(parentPath),getCjsConditions()),parentPath,pkg.path);
607607
}catch(e){
608608
if(e.code==='ERR_MODULE_NOT_FOUND'){
609-
throwcreateEsmNotFoundErr(request,pkg.path+'/package.json');
609+
throwcreateEsmNotFoundErr(request,pkg.path);
610610
}
611611
throwe;
612612
}
@@ -1201,14 +1201,15 @@ Module._resolveFilename = function(request, parent, isMain, options) {
12011201

12021202
if(request[0]==='#'&&(parent?.filename||parent?.id==='<repl>')){
12031203
constparentPath=parent?.filename??process.cwd()+path.sep;
1204-
constpkg=packageJsonReader.getNearestParentPackageJSON(parentPath)||{__proto__: null};
1205-
if(pkg.data?.imports!=null){
1204+
constpkg=packageJsonReader.getNearestParentPackageJSON(parentPath);
1205+
if(pkg?.data.imports!=null){
12061206
try{
12071207
const{ packageImportsResolve }=require('internal/modules/esm/resolve');
12081208
returnfinalizeEsmResolution(
1209-
packageImportsResolve(request,pathToFileURL(parentPath),
1210-
getCjsConditions()),parentPath,
1211-
pkg.path);
1209+
packageImportsResolve(request,pathToFileURL(parentPath),getCjsConditions()),
1210+
parentPath,
1211+
pkg.path,
1212+
);
12121213
}catch(e){
12131214
if(e.code==='ERR_MODULE_NOT_FOUND'){
12141215
throwcreateEsmNotFoundErr(request);
@@ -1268,8 +1269,7 @@ function finalizeEsmResolution(resolved, parentPath, pkgPath) {
12681269
if(actual){
12691270
returnactual;
12701271
}
1271-
consterr=createEsmNotFoundErr(filename,
1272-
path.resolve(pkgPath,'package.json'));
1272+
consterr=createEsmNotFoundErr(filename,pkgPath);
12731273
throwerr;
12741274
}
12751275

@@ -1623,7 +1623,7 @@ function loadTS(module, filename) {
16231623

16241624
constparent=module[kModuleParent];
16251625
constparentPath=parent?.filename;
1626-
constpackageJsonPath=path.resolve(pkg.path,'package.json');
1626+
constpackageJsonPath=pkg.path;
16271627
constusesEsm=containsModuleSyntax(content,filename);
16281628
consterr=newERR_REQUIRE_ESM(filename,usesEsm,parentPath,
16291629
packageJsonPath);
@@ -1682,7 +1682,7 @@ Module._extensions['.js'] = function(module, filename) {
16821682
// This is an error path because `require` of a `.js` file in a `"type": "module"` scope is not allowed.
16831683
constparent=module[kModuleParent];
16841684
constparentPath=parent?.filename;
1685-
constpackageJsonPath=path.resolve(pkg.path,'package.json');
1685+
constpackageJsonPath=pkg.path;
16861686
constusesEsm=containsModuleSyntax(content,filename);
16871687
consterr=newERR_REQUIRE_ESM(filename,usesEsm,parentPath,
16881688
packageJsonPath);

‎lib/internal/modules/esm/resolve.js‎

Lines changed: 16 additions & 87 deletions
Original file line numberDiff line numberDiff line change
@@ -356,7 +356,6 @@ function invalidPackageTarget(
356356

357357
constinvalidSegmentRegEx=/(^|\\|\/)((\.|%2e)(\.|%2e)?|(n|%6e|%4e)(o|%6f|%4f)(d|%64|%44)(e|%65|%45)(_|%5f)(m|%6d|%4d)(o|%6f|%4f)(d|%64|%44)(u|%75|%55)(l|%6c|%4c)(e|%65|%45)(s|%73|%53))?(\\|\/|$)/i;
358358
constdeprecatedInvalidSegmentRegEx=/(^|\\|\/)((\.|%2e)(\.|%2e)?|(n|%6e|%4e)(o|%6f|%4f)(d|%64|%44)(e|%65|%45)(_|%5f)(m|%6d|%4d)(o|%6f|%4f)(d|%64|%44)(u|%75|%55)(l|%6c|%4c)(e|%65|%45)(s|%73|%53))(\\|\/|$)/i;
359-
constinvalidPackageNameRegEx=/^\.|%|\\/;
360359
constpatternRegEx=/\*/g;
361360

362361
/**
@@ -752,44 +751,6 @@ function packageImportsResolve(name, base, conditions) {
752751
throwimportNotDefined(name,packageJSONUrl,base);
753752
}
754753

755-
/**
756-
* Parse a package name from a specifier.
757-
* @param {string} specifier - The import specifier.
758-
* @param {string | URL | undefined} base - The parent URL.
759-
*/
760-
functionparsePackageName(specifier,base){
761-
letseparatorIndex=StringPrototypeIndexOf(specifier,'/');
762-
letvalidPackageName=true;
763-
letisScoped=false;
764-
if(specifier[0]==='@'){
765-
isScoped=true;
766-
if(separatorIndex===-1||specifier.length===0){
767-
validPackageName=false;
768-
}else{
769-
separatorIndex=StringPrototypeIndexOf(
770-
specifier,'/',separatorIndex+1);
771-
}
772-
}
773-
774-
constpackageName=separatorIndex===-1 ?
775-
specifier : StringPrototypeSlice(specifier,0,separatorIndex);
776-
777-
// Package name cannot have leading . and cannot have percent-encoding or
778-
// \\ separators.
779-
if(RegExpPrototypeExec(invalidPackageNameRegEx,packageName)!==null){
780-
validPackageName=false;
781-
}
782-
783-
if(!validPackageName){
784-
thrownewERR_INVALID_MODULE_SPECIFIER(
785-
specifier,'is not a valid package name',fileURLToPath(base));
786-
}
787-
788-
constpackageSubpath='.'+(separatorIndex===-1 ? '' :
789-
StringPrototypeSlice(specifier,separatorIndex));
790-
791-
return{ packageName, packageSubpath, isScoped };
792-
}
793754

794755
/**
795756
* Resolves a package specifier to a URL.
@@ -804,57 +765,24 @@ function packageResolve(specifier, base, conditions) {
804765
returnnewURL('node:'+specifier);
805766
}
806767

807-
const{ packageName, packageSubpath, isScoped }=
808-
parsePackageName(specifier,base);
768+
const{ packageJSONUrl, packageJSONPath, packageSubpath }=packageJsonReader.getPackageJSONURL(specifier,base);
809769

810-
// ResolveSelf
811-
constpackageConfig=packageJsonReader.getPackageScopeConfig(base);
812-
if(packageConfig.exists){
813-
if(packageConfig.exports!=null&&packageConfig.name===packageName){
814-
constpackageJSONUrl=pathToFileURL(packageConfig.pjsonPath);
815-
returnpackageExportsResolve(
816-
packageJSONUrl,packageSubpath,packageConfig,base,conditions);
817-
}
818-
}
770+
constpackageConfig=packageJsonReader.read(packageJSONPath,{__proto__: null, specifier, base,isESM: true});
819771

820-
letpackageJSONUrl=
821-
newURL('./node_modules/'+packageName+'/package.json',base);
822-
letpackageJSONPath=fileURLToPath(packageJSONUrl);
823-
letlastPath;
824-
do{
825-
conststat=internalFsBinding.internalModuleStat(
826-
internalFsBinding,
827-
StringPrototypeSlice(packageJSONPath,0,packageJSONPath.length-13),
772+
// Package match.
773+
if(packageConfig.exports!=null){
774+
returnpackageExportsResolve(
775+
packageJSONUrl,packageSubpath,packageConfig,base,conditions);
776+
}
777+
if(packageSubpath==='.'){
778+
returnlegacyMainResolve(
779+
packageJSONUrl,
780+
packageConfig,
781+
base,
828782
);
829-
// Check for !stat.isDirectory()
830-
if(stat!==1){
831-
lastPath=packageJSONPath;
832-
packageJSONUrl=newURL((isScoped ?
833-
'../../../../node_modules/' : '../../../node_modules/')+
834-
packageName+'/package.json',packageJSONUrl);
835-
packageJSONPath=fileURLToPath(packageJSONUrl);
836-
continue;
837-
}
838-
839-
// Package match.
840-
constpackageConfig=packageJsonReader.read(packageJSONPath,{__proto__: null, specifier, base,isESM: true});
841-
if(packageConfig.exports!=null){
842-
returnpackageExportsResolve(
843-
packageJSONUrl,packageSubpath,packageConfig,base,conditions);
844-
}
845-
if(packageSubpath==='.'){
846-
returnlegacyMainResolve(
847-
packageJSONUrl,
848-
packageConfig,
849-
base,
850-
);
851-
}
852-
853-
returnnewURL(packageSubpath,packageJSONUrl);
854-
// Cross-platform root check.
855-
}while(packageJSONPath.length!==lastPath.length);
783+
}
856784

857-
thrownewERR_MODULE_NOT_FOUND(packageName,fileURLToPath(base),null);
785+
returnnewURL(packageSubpath,packageJSONUrl);
858786
}
859787

860788
/**
@@ -1105,10 +1033,11 @@ module.exports = {
11051033
decorateErrorWithCommonJSHints,
11061034
defaultResolve,
11071035
encodedSepRegEx,
1036+
legacyMainResolve,
11081037
packageExportsResolve,
11091038
packageImportsResolve,
1039+
packageResolve,
11101040
throwIfInvalidParentURL,
1111-
legacyMainResolve,
11121041
};
11131042

11141043
// cycle

0 commit comments

Comments
 (0)