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: add findPackageJSON util #55412

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
133ad16
module: add `findPackageJSON` util
JakobJingleheimer Oct 15, 2024
1c80d6d
fixup!: `DeserializedPackageConfig`
JakobJingleheimer Oct 15, 2024
746a88c
fixup!: handle CJS & relative specifiers
JakobJingleheimer Oct 16, 2024
ec853c7
fixup!: tidy
JakobJingleheimer Oct 16, 2024
1742549
fixup!: enable Antoine's test-case
JakobJingleheimer Oct 16, 2024
0bf17ca
fixup!: throw on unexpected error
JakobJingleheimer Oct 16, 2024
acc2104
fixup!: correct test-case inputs (`parentURL` can't be a dir)
JakobJingleheimer Oct 16, 2024
12a722f
fixup!: correct conflict resolution
JakobJingleheimer Oct 16, 2024
7568d49
fixup!: tidy
JakobJingleheimer Oct 16, 2024
20db90f
fixup!: add `text` to fenced codebock
JakobJingleheimer Oct 17, 2024
b4be705
fixup!: code review improvements
JakobJingleheimer Oct 17, 2024
54c4ad2
fixup!: avoid premature `URL` → string conversion
JakobJingleheimer Oct 17, 2024
913116b
fixup!: document when pjson is not found
JakobJingleheimer Oct 17, 2024
2266258
fixup!: rename test various for clarity
JakobJingleheimer Oct 17, 2024
6cb9ddd
Apply suggestions from code review
aduh95 Oct 17, 2024
10b27b1
add a third argument to get the package scope pjson
aduh95 Oct 18, 2024
ed6e1d1
implement suggestion
aduh95 Oct 19, 2024
8397d22
fixup!: de-lint
JakobJingleheimer Oct 22, 2024
1cd6d65
fixup!: windows
JakobJingleheimer Oct 22, 2024
563cbc0
fixup!: wordsmith test-case description
JakobJingleheimer Oct 22, 2024
a06a1b3
fixup!: de-lint
JakobJingleheimer Oct 22, 2024
08fb0e2
fixup!: correct suggestion
JakobJingleheimer Oct 22, 2024
fb7d379
fixup!: de-lint
JakobJingleheimer Oct 22, 2024
073fcaa
fixup!: windows paths 😩
JakobJingleheimer Oct 23, 2024
6783f64
fixup!: readability
JakobJingleheimer Oct 23, 2024
2a1aad1
fixup!: more windows paths
JakobJingleheimer Oct 23, 2024
ef16d22
fixup!: more windows pathing
JakobJingleheimer Oct 24, 2024
31491f9
fixup!: de-lint
JakobJingleheimer Oct 24, 2024
ba9acf9
fixup!: b0rked conflict resolution
JakobJingleheimer Oct 24, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading