Skip to content

Commit

Permalink
module: add findPackageJSON util
Browse files Browse the repository at this point in the history
PR-URL: nodejs#55412
Reviewed-By: Matteo Collina <[email protected]>
Reviewed-By: Antoine du Hamel <[email protected]>
  • Loading branch information
JakobJingleheimer authored and tpoisseau committed Nov 21, 2024
1 parent 73bd365 commit 787783a
Show file tree
Hide file tree
Showing 17 changed files with 516 additions and 151 deletions.
82 changes: 82 additions & 0 deletions doc/api/module.md
Original file line number Diff line number Diff line change
Expand Up @@ -217,6 +217,88 @@ added: v22.8.0
* Returns: {string|undefined} Path to the [module compile cache][] directory if it is enabled,
or `undefined` otherwise.
### `module.findPackageJSON(specifier[, base])`
<!-- YAML
added: REPLACEME
-->
> Stability: 1.1 - Active Development
* `specifier` {string|URL} The specifier for the module whose `package.json` to
retrieve. When passing a _bare specifier_, the `package.json` at the root of
the package is returned. When passing a _relative specifier_ or an _absolute specifier_,
the closest parent `package.json` is returned.
* `base` {string|URL} The absolute location (`file:` URL string or FS path) of the
containing module. For CJS, use `__filename` (not `__dirname`!); for ESM, use
`import.meta.url`. You do not need to pass it if `specifier` is an `absolute specifier`.
* Returns: {string|undefined} A path if the `package.json` is found. When `startLocation`
is a package, the package's root `package.json`; when a relative or unresolved, the closest
`package.json` to the `startLocation`.
> **Caveat**: Do not use this to try to determine module format. There are many things effecting
> that determination; the `type` field of package.json is the _least_ definitive (ex file extension
> superceeds it, and a loader hook superceeds that).
```text
/path/to/project
├ packages/
├ bar/
bar.js
package.json // name = '@foo/bar'
└ qux/
├ node_modules/
└ some-package/
package.json // name = 'some-package'
qux.js
package.json // name = '@foo/qux'
main.js
package.json // name = '@foo'
```
```mjs
// /path/to/project/packages/bar/bar.js
import { findPackageJSON } from 'node:module';

findPackageJSON('..', import.meta.url);
// '/path/to/project/package.json'
// Same result when passing an absolute specifier instead:
findPackageJSON(new URL('../', import.meta.url));
findPackageJSON(import.meta.resolve('../'));

findPackageJSON('some-package', import.meta.url);
// '/path/to/project/packages/bar/node_modules/some-package/package.json'
// When passing an absolute specifier, you might get a different result if the
// resolved module is inside a subfolder that has nested `package.json`.
findPackageJSON(import.meta.resolve('some-package'));
// '/path/to/project/packages/bar/node_modules/some-package/some-subfolder/package.json'

findPackageJSON('@foo/qux', import.meta.url);
// '/path/to/project/packages/qux/package.json'
```
```cjs
// /path/to/project/packages/bar/bar.js
const { findPackageJSON } = require('node:module');
const { pathToFileURL } = require('node:url');
const path = require('node:path');

findPackageJSON('..', __filename);
// '/path/to/project/package.json'
// Same result when passing an absolute specifier instead:
findPackageJSON(pathToFileURL(path.join(__dirname, '..')));

findPackageJSON('some-package', __filename);
// '/path/to/project/packages/bar/node_modules/some-package/package.json'
// When passing an absolute specifier, you might get a different result if the
// resolved module is inside a subfolder that has nested `package.json`.
findPackageJSON(pathToFileURL(require.resolve('some-package')));
// '/path/to/project/packages/bar/node_modules/some-package/some-subfolder/package.json'

findPackageJSON('@foo/qux', __filename);
// '/path/to/project/packages/qux/package.json'
```
### `module.isBuiltin(moduleName)`
<!-- YAML
Expand Down
22 changes: 11 additions & 11 deletions lib/internal/modules/cjs/loader.js
Original file line number Diff line number Diff line change
Expand Up @@ -602,11 +602,11 @@ function trySelf(parentPath, request) {
try {
const { packageExportsResolve } = require('internal/modules/esm/resolve');
return finalizeEsmResolution(packageExportsResolve(
pathToFileURL(pkg.path + '/package.json'), expansion, pkg.data,
pathToFileURL(pkg.path), expansion, pkg.data,
pathToFileURL(parentPath), getCjsConditions()), parentPath, pkg.path);
} catch (e) {
if (e.code === 'ERR_MODULE_NOT_FOUND') {
throw createEsmNotFoundErr(request, pkg.path + '/package.json');
throw createEsmNotFoundErr(request, pkg.path);
}
throw e;
}
Expand Down Expand Up @@ -1201,14 +1201,15 @@ Module._resolveFilename = function(request, parent, isMain, options) {

if (request[0] === '#' && (parent?.filename || parent?.id === '<repl>')) {
const parentPath = parent?.filename ?? process.cwd() + path.sep;
const pkg = packageJsonReader.getNearestParentPackageJSON(parentPath) || { __proto__: null };
if (pkg.data?.imports != null) {
const pkg = packageJsonReader.getNearestParentPackageJSON(parentPath);
if (pkg?.data.imports != null) {
try {
const { packageImportsResolve } = require('internal/modules/esm/resolve');
return finalizeEsmResolution(
packageImportsResolve(request, pathToFileURL(parentPath),
getCjsConditions()), parentPath,
pkg.path);
packageImportsResolve(request, pathToFileURL(parentPath), getCjsConditions()),
parentPath,
pkg.path,
);
} catch (e) {
if (e.code === 'ERR_MODULE_NOT_FOUND') {
throw createEsmNotFoundErr(request);
Expand Down Expand Up @@ -1268,8 +1269,7 @@ function finalizeEsmResolution(resolved, parentPath, pkgPath) {
if (actual) {
return actual;
}
const err = createEsmNotFoundErr(filename,
path.resolve(pkgPath, 'package.json'));
const err = createEsmNotFoundErr(filename, pkgPath);
throw err;
}

Expand Down Expand Up @@ -1623,7 +1623,7 @@ function loadTS(module, filename) {

const parent = module[kModuleParent];
const parentPath = parent?.filename;
const packageJsonPath = path.resolve(pkg.path, 'package.json');
const packageJsonPath = pkg.path;
const usesEsm = containsModuleSyntax(content, filename);
const err = new ERR_REQUIRE_ESM(filename, usesEsm, parentPath,
packageJsonPath);
Expand Down Expand Up @@ -1682,7 +1682,7 @@ Module._extensions['.js'] = function(module, filename) {
// This is an error path because `require` of a `.js` file in a `"type": "module"` scope is not allowed.
const parent = module[kModuleParent];
const parentPath = parent?.filename;
const packageJsonPath = path.resolve(pkg.path, 'package.json');
const packageJsonPath = pkg.path;
const usesEsm = containsModuleSyntax(content, filename);
const err = new ERR_REQUIRE_ESM(filename, usesEsm, parentPath,
packageJsonPath);
Expand Down
103 changes: 16 additions & 87 deletions lib/internal/modules/esm/resolve.js
Original file line number Diff line number Diff line change
Expand Up @@ -356,7 +356,6 @@ function invalidPackageTarget(

const invalidSegmentRegEx = /(^|\\|\/)((\.|%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;
const deprecatedInvalidSegmentRegEx = /(^|\\|\/)((\.|%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;
const invalidPackageNameRegEx = /^\.|%|\\/;
const patternRegEx = /\*/g;

/**
Expand Down Expand Up @@ -752,44 +751,6 @@ function packageImportsResolve(name, base, conditions) {
throw importNotDefined(name, packageJSONUrl, base);
}

/**
* Parse a package name from a specifier.
* @param {string} specifier - The import specifier.
* @param {string | URL | undefined} base - The parent URL.
*/
function parsePackageName(specifier, base) {
let separatorIndex = StringPrototypeIndexOf(specifier, '/');
let validPackageName = true;
let isScoped = false;
if (specifier[0] === '@') {
isScoped = true;
if (separatorIndex === -1 || specifier.length === 0) {
validPackageName = false;
} else {
separatorIndex = StringPrototypeIndexOf(
specifier, '/', separatorIndex + 1);
}
}

const packageName = separatorIndex === -1 ?
specifier : StringPrototypeSlice(specifier, 0, separatorIndex);

// Package name cannot have leading . and cannot have percent-encoding or
// \\ separators.
if (RegExpPrototypeExec(invalidPackageNameRegEx, packageName) !== null) {
validPackageName = false;
}

if (!validPackageName) {
throw new ERR_INVALID_MODULE_SPECIFIER(
specifier, 'is not a valid package name', fileURLToPath(base));
}

const packageSubpath = '.' + (separatorIndex === -1 ? '' :
StringPrototypeSlice(specifier, separatorIndex));

return { packageName, packageSubpath, isScoped };
}

/**
* Resolves a package specifier to a URL.
Expand All @@ -804,57 +765,24 @@ function packageResolve(specifier, base, conditions) {
return new URL('node:' + specifier);
}

const { packageName, packageSubpath, isScoped } =
parsePackageName(specifier, base);
const { packageJSONUrl, packageJSONPath, packageSubpath } = packageJsonReader.getPackageJSONURL(specifier, base);

// ResolveSelf
const packageConfig = packageJsonReader.getPackageScopeConfig(base);
if (packageConfig.exists) {
if (packageConfig.exports != null && packageConfig.name === packageName) {
const packageJSONUrl = pathToFileURL(packageConfig.pjsonPath);
return packageExportsResolve(
packageJSONUrl, packageSubpath, packageConfig, base, conditions);
}
}
const packageConfig = packageJsonReader.read(packageJSONPath, { __proto__: null, specifier, base, isESM: true });

let packageJSONUrl =
new URL('./node_modules/' + packageName + '/package.json', base);
let packageJSONPath = fileURLToPath(packageJSONUrl);
let lastPath;
do {
const stat = internalFsBinding.internalModuleStat(
internalFsBinding,
StringPrototypeSlice(packageJSONPath, 0, packageJSONPath.length - 13),
// Package match.
if (packageConfig.exports != null) {
return packageExportsResolve(
packageJSONUrl, packageSubpath, packageConfig, base, conditions);
}
if (packageSubpath === '.') {
return legacyMainResolve(
packageJSONUrl,
packageConfig,
base,
);
// Check for !stat.isDirectory()
if (stat !== 1) {
lastPath = packageJSONPath;
packageJSONUrl = new URL((isScoped ?
'../../../../node_modules/' : '../../../node_modules/') +
packageName + '/package.json', packageJSONUrl);
packageJSONPath = fileURLToPath(packageJSONUrl);
continue;
}

// Package match.
const packageConfig = packageJsonReader.read(packageJSONPath, { __proto__: null, specifier, base, isESM: true });
if (packageConfig.exports != null) {
return packageExportsResolve(
packageJSONUrl, packageSubpath, packageConfig, base, conditions);
}
if (packageSubpath === '.') {
return legacyMainResolve(
packageJSONUrl,
packageConfig,
base,
);
}

return new URL(packageSubpath, packageJSONUrl);
// Cross-platform root check.
} while (packageJSONPath.length !== lastPath.length);
}

throw new ERR_MODULE_NOT_FOUND(packageName, fileURLToPath(base), null);
return new URL(packageSubpath, packageJSONUrl);
}

/**
Expand Down Expand Up @@ -1105,10 +1033,11 @@ module.exports = {
decorateErrorWithCommonJSHints,
defaultResolve,
encodedSepRegEx,
legacyMainResolve,
packageExportsResolve,
packageImportsResolve,
packageResolve,
throwIfInvalidParentURL,
legacyMainResolve,
};

// cycle
Expand Down
Loading

0 comments on commit 787783a

Please sign in to comment.