mirror of
https://github.com/nodejs/node.git
synced 2024-11-21 13:09:21 +01:00
module: add findPackageJSON
util
PR-URL: https://github.com/nodejs/node/pull/55412 Reviewed-By: Matteo Collina <matteo.collina@gmail.com> Reviewed-By: Antoine du Hamel <duhamelantoine1995@gmail.com>
This commit is contained in:
parent
1ba8fbe4b8
commit
e312d60e3f
@ -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
|
||||
|
@ -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;
|
||||
}
|
||||
@ -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);
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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);
|
||||
@ -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);
|
||||
|
@ -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;
|
||||
|
||||
/**
|
||||
@ -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.
|
||||
@ -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 });
|
||||
|
||||
// Package match.
|
||||
if (packageConfig.exports != null) {
|
||||
return packageExportsResolve(
|
||||
packageJSONUrl, packageSubpath, packageConfig, base, conditions);
|
||||
}
|
||||
if (packageSubpath === '.') {
|
||||
return legacyMainResolve(
|
||||
packageJSONUrl,
|
||||
packageConfig,
|
||||
base,
|
||||
);
|
||||
}
|
||||
|
||||
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),
|
||||
);
|
||||
// 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);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -1105,10 +1033,11 @@ module.exports = {
|
||||
decorateErrorWithCommonJSHints,
|
||||
defaultResolve,
|
||||
encodedSepRegEx,
|
||||
legacyMainResolve,
|
||||
packageExportsResolve,
|
||||
packageImportsResolve,
|
||||
packageResolve,
|
||||
throwIfInvalidParentURL,
|
||||
legacyMainResolve,
|
||||
};
|
||||
|
||||
// cycle
|
||||
|
@ -4,29 +4,54 @@ const {
|
||||
ArrayIsArray,
|
||||
JSONParse,
|
||||
ObjectDefineProperty,
|
||||
StringPrototypeLastIndexOf,
|
||||
RegExpPrototypeExec,
|
||||
StringPrototypeIndexOf,
|
||||
StringPrototypeSlice,
|
||||
StringPrototypeStartsWith,
|
||||
} = primordials;
|
||||
const modulesBinding = internalBinding('modules');
|
||||
const { resolve, sep } = require('path');
|
||||
const {
|
||||
fileURLToPath,
|
||||
isURL,
|
||||
pathToFileURL,
|
||||
URL,
|
||||
} = require('internal/url');
|
||||
const { canParse: URLCanParse } = internalBinding('url');
|
||||
const {
|
||||
codes: {
|
||||
ERR_INVALID_MODULE_SPECIFIER,
|
||||
ERR_MISSING_ARGS,
|
||||
ERR_MODULE_NOT_FOUND,
|
||||
},
|
||||
} = require('internal/errors');
|
||||
const { kEmptyObject } = require('internal/util');
|
||||
const modulesBinding = internalBinding('modules');
|
||||
const path = require('path');
|
||||
const { validateString } = require('internal/validators');
|
||||
const internalFsBinding = internalBinding('fs');
|
||||
|
||||
/**
|
||||
* @param {string} path
|
||||
* @param {import('typings/internalBinding/modules').SerializedPackageConfig} contents
|
||||
* @returns {import('typings/internalBinding/modules').PackageConfig}
|
||||
* @typedef {import('typings/internalBinding/modules').DeserializedPackageConfig} DeserializedPackageConfig
|
||||
* @typedef {import('typings/internalBinding/modules').PackageConfig} PackageConfig
|
||||
* @typedef {import('typings/internalBinding/modules').SerializedPackageConfig} SerializedPackageConfig
|
||||
*/
|
||||
|
||||
/**
|
||||
* @param {URL['pathname']} path
|
||||
* @param {SerializedPackageConfig} contents
|
||||
* @returns {DeserializedPackageConfig}
|
||||
*/
|
||||
function deserializePackageJSON(path, contents) {
|
||||
if (contents === undefined) {
|
||||
return {
|
||||
__proto__: null,
|
||||
data: {
|
||||
__proto__: null,
|
||||
type: 'none', // Ignore unknown types for forwards compatibility
|
||||
},
|
||||
exists: false,
|
||||
pjsonPath: path,
|
||||
type: 'none', // Ignore unknown types for forwards compatibility
|
||||
path,
|
||||
};
|
||||
}
|
||||
|
||||
let pjsonPath = path;
|
||||
const {
|
||||
0: name,
|
||||
1: main,
|
||||
@ -36,37 +61,40 @@ function deserializePackageJSON(path, contents) {
|
||||
5: optionalFilePath,
|
||||
} = contents;
|
||||
|
||||
// This is required to be used in getPackageScopeConfig.
|
||||
if (optionalFilePath) {
|
||||
pjsonPath = optionalFilePath;
|
||||
}
|
||||
|
||||
// The imports and exports fields can be either undefined or a string.
|
||||
// - If it's a string, it's either plain string or a stringified JSON string.
|
||||
// - If it's a stringified JSON string, it starts with either '[' or '{'.
|
||||
const requiresJSONParse = (value) => (value !== undefined && (value[0] === '[' || value[0] === '{'));
|
||||
const pjsonPath = optionalFilePath ?? path;
|
||||
|
||||
return {
|
||||
__proto__: null,
|
||||
data: {
|
||||
__proto__: null,
|
||||
...(name != null && { name }),
|
||||
...(main != null && { main }),
|
||||
...(type != null && { type }),
|
||||
...(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;
|
||||
},
|
||||
}),
|
||||
},
|
||||
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;
|
||||
},
|
||||
path: pjsonPath,
|
||||
};
|
||||
}
|
||||
|
||||
// The imports and exports fields can be either undefined or a string.
|
||||
// - If it's a string, it's either plain string or a stringified JSON string.
|
||||
// - If it's a stringified JSON string, it starts with either '[' or '{'.
|
||||
const requiresJSONParse = (value) => (value !== undefined && (value[0] === '[' || value[0] === '{'));
|
||||
|
||||
/**
|
||||
* Reads a package.json file and returns the parsed contents.
|
||||
* @param {string} jsonPath
|
||||
@ -75,7 +103,7 @@ function deserializePackageJSON(path, contents) {
|
||||
* specifier?: URL | string,
|
||||
* isESM?: boolean,
|
||||
* }} options
|
||||
* @returns {import('typings/internalBinding/modules').PackageConfig}
|
||||
* @returns {PackageConfig}
|
||||
*/
|
||||
function read(jsonPath, { base, specifier, isESM } = kEmptyObject) {
|
||||
// This function will be called by both CJS and ESM, so we need to make sure
|
||||
@ -87,7 +115,14 @@ function read(jsonPath, { base, specifier, isESM } = kEmptyObject) {
|
||||
specifier == null ? undefined : `${specifier}`,
|
||||
);
|
||||
|
||||
return deserializePackageJSON(jsonPath, parsed);
|
||||
const result = deserializePackageJSON(jsonPath, parsed);
|
||||
|
||||
return {
|
||||
__proto__: null,
|
||||
...result.data,
|
||||
exists: result.exists,
|
||||
pjsonPath: result.path,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
@ -98,14 +133,14 @@ function read(jsonPath, { base, specifier, isESM } = kEmptyObject) {
|
||||
*/
|
||||
function readPackage(requestPath) {
|
||||
// TODO(@anonrig): Remove this function.
|
||||
return read(resolve(requestPath, 'package.json'));
|
||||
return read(path.resolve(requestPath, 'package.json'));
|
||||
}
|
||||
|
||||
/**
|
||||
* 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}}
|
||||
* @returns {undefined | DeserializedPackageConfig}
|
||||
*/
|
||||
function getNearestParentPackageJSON(checkPath) {
|
||||
const result = modulesBinding.getNearestParentPackageJSON(checkPath);
|
||||
@ -114,13 +149,7 @@ function getNearestParentPackageJSON(checkPath) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const data = deserializePackageJSON(checkPath, result);
|
||||
|
||||
// Path should be the root folder of the matched package.json
|
||||
// For example for ~/path/package.json, it should be ~/path
|
||||
const path = StringPrototypeSlice(data.pjsonPath, 0, StringPrototypeLastIndexOf(data.pjsonPath, sep));
|
||||
|
||||
return { data, path };
|
||||
return deserializePackageJSON(checkPath, result);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -132,7 +161,14 @@ function getPackageScopeConfig(resolved) {
|
||||
const result = modulesBinding.getPackageScopeConfig(`${resolved}`);
|
||||
|
||||
if (ArrayIsArray(result)) {
|
||||
return deserializePackageJSON(`${resolved}`, result);
|
||||
const { data, exists, path } = deserializePackageJSON(`${resolved}`, result);
|
||||
|
||||
return {
|
||||
__proto__: null,
|
||||
...data,
|
||||
exists,
|
||||
pjsonPath: path,
|
||||
};
|
||||
}
|
||||
|
||||
// This means that the response is a string
|
||||
@ -154,10 +190,145 @@ function getPackageType(url) {
|
||||
return getPackageScopeConfig(url).type;
|
||||
}
|
||||
|
||||
const invalidPackageNameRegEx = /^\.|%|\\/;
|
||||
/**
|
||||
* 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 };
|
||||
}
|
||||
|
||||
function getPackageJSONURL(specifier, base) {
|
||||
const { packageName, packageSubpath, isScoped } =
|
||||
parsePackageName(specifier, base);
|
||||
|
||||
// ResolveSelf
|
||||
const packageConfig = getPackageScopeConfig(base);
|
||||
if (packageConfig.exists) {
|
||||
if (packageConfig.exports != null && packageConfig.name === packageName) {
|
||||
const packageJSONPath = packageConfig.pjsonPath;
|
||||
return { packageJSONUrl: pathToFileURL(packageJSONPath), packageJSONPath, packageSubpath };
|
||||
}
|
||||
}
|
||||
|
||||
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),
|
||||
);
|
||||
// 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.
|
||||
return { packageJSONUrl, packageJSONPath, packageSubpath };
|
||||
} while (packageJSONPath.length !== lastPath.length);
|
||||
|
||||
throw new ERR_MODULE_NOT_FOUND(packageName, fileURLToPath(base), null);
|
||||
}
|
||||
|
||||
const pjsonImportAttributes = { __proto__: null, type: 'json' };
|
||||
let cascadedLoader;
|
||||
/**
|
||||
* @param {URL['href'] | string | URL} specifier The location for which to get the "root" package.json
|
||||
* @param {URL['href'] | string | URL} [base] The location of the current module (ex file://tmp/foo.js).
|
||||
*/
|
||||
function findPackageJSON(specifier, base = 'data:') {
|
||||
if (arguments.length === 0) {
|
||||
throw new ERR_MISSING_ARGS('specifier');
|
||||
}
|
||||
try {
|
||||
specifier = `${specifier}`;
|
||||
} catch {
|
||||
validateString(specifier, 'specifier');
|
||||
}
|
||||
|
||||
let parentURL;
|
||||
if (isURL(base)) {
|
||||
parentURL = new URL(base);
|
||||
} else {
|
||||
validateString(base, 'base');
|
||||
if (
|
||||
path.isAbsolute(base) ||
|
||||
(URLCanParse(base) && !StringPrototypeStartsWith(base, 'file:'))
|
||||
) {
|
||||
parentURL = pathToFileURL(path.toNamespacedPath(base));
|
||||
} else {
|
||||
parentURL = URL.parse(base) || pathToFileURL(base);
|
||||
}
|
||||
}
|
||||
|
||||
if (specifier && specifier[0] !== '.' && specifier[0] !== '/' && !URLCanParse(specifier)) {
|
||||
const { packageJSONPath } = getPackageJSONURL(specifier, parentURL);
|
||||
return packageJSONPath;
|
||||
}
|
||||
|
||||
let resolvedTarget;
|
||||
cascadedLoader ??= require('internal/modules/esm/loader').getOrInitializeCascadedLoader();
|
||||
|
||||
try {
|
||||
resolvedTarget = cascadedLoader.resolve(specifier, `${parentURL}`, pjsonImportAttributes).url;
|
||||
} catch (err) {
|
||||
if (err.code === 'ERR_UNSUPPORTED_DIR_IMPORT') {
|
||||
resolvedTarget = err.url;
|
||||
} else {
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
const pkg = getNearestParentPackageJSON(fileURLToPath(resolvedTarget));
|
||||
|
||||
return pkg?.path;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
read,
|
||||
readPackage,
|
||||
getNearestParentPackageJSON,
|
||||
getPackageScopeConfig,
|
||||
getPackageType,
|
||||
getPackageJSONURL,
|
||||
findPackageJSON,
|
||||
};
|
||||
|
@ -10,6 +10,9 @@ const {
|
||||
flushCompileCache,
|
||||
getCompileCacheDir,
|
||||
} = require('internal/modules/helpers');
|
||||
const {
|
||||
findPackageJSON,
|
||||
} = require('internal/modules/package_json_reader');
|
||||
const { stripTypeScriptTypes } = require('internal/modules/typescript');
|
||||
|
||||
Module.findSourceMap = findSourceMap;
|
||||
@ -17,7 +20,9 @@ Module.register = register;
|
||||
Module.SourceMap = SourceMap;
|
||||
Module.constants = constants;
|
||||
Module.enableCompileCache = enableCompileCache;
|
||||
Module.findPackageJSON = findPackageJSON;
|
||||
Module.flushCompileCache = flushCompileCache;
|
||||
Module.stripTypeScriptTypes = stripTypeScriptTypes;
|
||||
Module.getCompileCacheDir = getCompileCacheDir;
|
||||
Module.stripTypeScriptTypes = stripTypeScriptTypes;
|
||||
|
||||
module.exports = Module;
|
||||
|
1
test/fixtures/packages/cjs-main-no-index/main.js
vendored
Normal file
1
test/fixtures/packages/cjs-main-no-index/main.js
vendored
Normal file
@ -0,0 +1 @@
|
||||
module.exports = 42;
|
3
test/fixtures/packages/cjs-main-no-index/other.js
vendored
Normal file
3
test/fixtures/packages/cjs-main-no-index/other.js
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
const answer = require('./');
|
||||
|
||||
module.exports = answer+1;
|
5
test/fixtures/packages/cjs-main-no-index/package.json
vendored
Normal file
5
test/fixtures/packages/cjs-main-no-index/package.json
vendored
Normal file
@ -0,0 +1,5 @@
|
||||
{
|
||||
"name": "main-no-index",
|
||||
"main": "./main.js",
|
||||
"type":"commonjs"
|
||||
}
|
1
test/fixtures/packages/nested/package.json
vendored
Normal file
1
test/fixtures/packages/nested/package.json
vendored
Normal file
@ -0,0 +1 @@
|
||||
{"name": "package-with-sub-package"}
|
3
test/fixtures/packages/nested/sub-pkg-cjs/index.js
vendored
Normal file
3
test/fixtures/packages/nested/sub-pkg-cjs/index.js
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
const { findPackageJSON } = require('node:module');
|
||||
|
||||
module.exports = findPackageJSON('..', __filename);
|
1
test/fixtures/packages/nested/sub-pkg-cjs/package.json
vendored
Normal file
1
test/fixtures/packages/nested/sub-pkg-cjs/package.json
vendored
Normal file
@ -0,0 +1 @@
|
||||
{"name": "sub-package", "type": "commonjs"}
|
3
test/fixtures/packages/nested/sub-pkg-esm/index.js
vendored
Normal file
3
test/fixtures/packages/nested/sub-pkg-esm/index.js
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
import { findPackageJSON } from 'node:module';
|
||||
|
||||
export default findPackageJSON('..', import.meta.url);
|
1
test/fixtures/packages/nested/sub-pkg-esm/package.json
vendored
Normal file
1
test/fixtures/packages/nested/sub-pkg-esm/package.json
vendored
Normal file
@ -0,0 +1 @@
|
||||
{"name": "sub-package", "type": "module"}
|
0
test/fixtures/packages/root-types-field/index.js
vendored
Normal file
0
test/fixtures/packages/root-types-field/index.js
vendored
Normal file
5
test/fixtures/packages/root-types-field/package.json
vendored
Normal file
5
test/fixtures/packages/root-types-field/package.json
vendored
Normal file
@ -0,0 +1,5 @@
|
||||
{
|
||||
"name": "package-with-unrecognised-fields",
|
||||
"type": "module",
|
||||
"types": "./index.d.ts"
|
||||
}
|
152
test/parallel/test-find-package-json.js
Normal file
152
test/parallel/test-find-package-json.js
Normal file
@ -0,0 +1,152 @@
|
||||
'use strict';
|
||||
|
||||
const common = require('../common');
|
||||
const fixtures = require('../common/fixtures');
|
||||
const tmpdir = require('../common/tmpdir');
|
||||
const assert = require('node:assert');
|
||||
const fs = require('node:fs');
|
||||
const { findPackageJSON } = require('node:module');
|
||||
const path = require('node:path');
|
||||
const { describe, it } = require('node:test');
|
||||
|
||||
|
||||
describe('findPackageJSON', () => { // Throws when no arguments are provided
|
||||
it('should throw when no arguments are provided', () => {
|
||||
assert.throws(
|
||||
() => findPackageJSON(),
|
||||
{ code: 'ERR_MISSING_ARGS' }
|
||||
);
|
||||
});
|
||||
|
||||
it('should throw when parentLocation is invalid', () => {
|
||||
for (const invalid of [null, {}, [], Symbol(), () => {}, true, false, 1, 0]) {
|
||||
assert.throws(
|
||||
() => findPackageJSON('', invalid),
|
||||
{ code: 'ERR_INVALID_ARG_TYPE' },
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
it('should accept a file URL (string), like from `import.meta.resolve()`', () => {
|
||||
const importMetaUrl = `${fixtures.fileURL('whatever.ext')}`;
|
||||
const specifierBase = './packages/root-types-field';
|
||||
assert.strictEqual(
|
||||
findPackageJSON(`${specifierBase}/index.js`, importMetaUrl),
|
||||
path.toNamespacedPath(fixtures.path(specifierBase, 'package.json')),
|
||||
);
|
||||
});
|
||||
|
||||
it('should accept a file URL instance', () => {
|
||||
const importMetaUrl = fixtures.fileURL('whatever.ext');
|
||||
const specifierBase = './packages/root-types-field';
|
||||
assert.strictEqual(
|
||||
findPackageJSON(
|
||||
new URL(`${specifierBase}/index.js`, importMetaUrl),
|
||||
importMetaUrl,
|
||||
),
|
||||
path.toNamespacedPath(fixtures.path(specifierBase, 'package.json')),
|
||||
);
|
||||
});
|
||||
|
||||
it('should be able to crawl up (CJS)', () => {
|
||||
const pathToMod = fixtures.path('packages/nested/sub-pkg-cjs/index.js');
|
||||
const parentPkg = require(pathToMod);
|
||||
|
||||
const pathToParent = path.toNamespacedPath(fixtures.path('packages/nested/package.json'));
|
||||
assert.strictEqual(parentPkg, pathToParent);
|
||||
});
|
||||
|
||||
it('should be able to crawl up (ESM)', () => {
|
||||
const pathToMod = fixtures.path('packages/nested/sub-pkg-esm/index.js');
|
||||
const parentPkg = require(pathToMod).default; // This test is a CJS file
|
||||
|
||||
const pathToParent = path.toNamespacedPath(fixtures.path('packages/nested/package.json'));
|
||||
assert.strictEqual(parentPkg, pathToParent);
|
||||
});
|
||||
|
||||
it('can require via package.json', () => {
|
||||
const pathToMod = fixtures.path('packages/cjs-main-no-index/other.js');
|
||||
// `require()` falls back to package.json values like "main" to resolve when there is no index
|
||||
const answer = require(pathToMod);
|
||||
|
||||
assert.strictEqual(answer, 43);
|
||||
});
|
||||
|
||||
it('should be able to resolve both root and closest package.json', () => {
|
||||
tmpdir.refresh();
|
||||
|
||||
fs.writeFileSync(tmpdir.resolve('entry.mjs'), `
|
||||
import { findPackageJSON } from 'node:module';
|
||||
import fs from 'node:fs';
|
||||
|
||||
const readPJSON = (locus) => JSON.parse(fs.readFileSync(locus, 'utf8'));
|
||||
|
||||
const { secretNumberPkgRoot } = readPJSON(findPackageJSON('pkg', import.meta.url));
|
||||
const { secretNumberSubfolder } = readPJSON(findPackageJSON(import.meta.resolve('pkg')));
|
||||
const { secretNumberSubfolder2 } = readPJSON(findPackageJSON(import.meta.resolve('pkg2')));
|
||||
const { secretNumberPkg2 } = readPJSON(findPackageJSON('pkg2', import.meta.url));
|
||||
|
||||
console.log(secretNumberPkgRoot, secretNumberSubfolder, secretNumberSubfolder2, secretNumberPkg2);
|
||||
`);
|
||||
|
||||
const secretNumberPkgRoot = Math.ceil(Math.random() * 999);
|
||||
const secretNumberSubfolder = Math.ceil(Math.random() * 999);
|
||||
const secretNumberSubfolder2 = Math.ceil(Math.random() * 999);
|
||||
const secretNumberPkg2 = Math.ceil(Math.random() * 999);
|
||||
|
||||
fs.mkdirSync(tmpdir.resolve('node_modules/pkg/subfolder'), { recursive: true });
|
||||
fs.writeFileSync(
|
||||
tmpdir.resolve('node_modules/pkg/subfolder/index.js'),
|
||||
'',
|
||||
);
|
||||
fs.writeFileSync(
|
||||
tmpdir.resolve('node_modules/pkg/subfolder/package.json'),
|
||||
JSON.stringify({
|
||||
type: 'module',
|
||||
secretNumberSubfolder,
|
||||
}),
|
||||
);
|
||||
fs.writeFileSync(
|
||||
tmpdir.resolve('node_modules/pkg/package.json'),
|
||||
JSON.stringify({
|
||||
name: 'pkg',
|
||||
exports: './subfolder/index.js',
|
||||
secretNumberPkgRoot,
|
||||
}),
|
||||
);
|
||||
|
||||
fs.mkdirSync(tmpdir.resolve('node_modules/pkg/subfolder2'));
|
||||
fs.writeFileSync(
|
||||
tmpdir.resolve('node_modules/pkg/subfolder2/package.json'),
|
||||
JSON.stringify({
|
||||
type: 'module',
|
||||
secretNumberSubfolder2,
|
||||
}),
|
||||
);
|
||||
fs.writeFileSync(
|
||||
tmpdir.resolve('node_modules/pkg/subfolder2/index.js'),
|
||||
'',
|
||||
);
|
||||
|
||||
fs.mkdirSync(tmpdir.resolve('node_modules/pkg2'));
|
||||
fs.writeFileSync(
|
||||
tmpdir.resolve('node_modules/pkg2/package.json'),
|
||||
JSON.stringify({
|
||||
name: 'pkg',
|
||||
main: tmpdir.resolve('node_modules/pkg/subfolder2/index.js'),
|
||||
secretNumberPkg2,
|
||||
}),
|
||||
);
|
||||
|
||||
common.spawnPromisified(process.execPath, [tmpdir.resolve('entry.mjs')]).then(common.mustCall((result) => {
|
||||
console.error(result.stderr);
|
||||
console.log(result.stdout);
|
||||
assert.deepStrictEqual(result, {
|
||||
stdout: `${secretNumberPkgRoot} ${secretNumberSubfolder} ${secretNumberSubfolder2} ${secretNumberPkg2}\n`,
|
||||
stderr: '',
|
||||
code: 0,
|
||||
signal: null,
|
||||
});
|
||||
}));
|
||||
});
|
||||
});
|
13
typings/internalBinding/modules.d.ts
vendored
13
typings/internalBinding/modules.d.ts
vendored
@ -1,27 +1,30 @@
|
||||
export type PackageType = 'commonjs' | 'module' | 'none'
|
||||
export type PackageConfig = {
|
||||
pjsonPath: string
|
||||
exists: boolean
|
||||
name?: string
|
||||
main?: any
|
||||
type: PackageType
|
||||
exports?: string | string[] | Record<string, unknown>
|
||||
imports?: string | string[] | Record<string, unknown>
|
||||
}
|
||||
export type DeserializedPackageConfig = {
|
||||
data: PackageConfig,
|
||||
exists: boolean,
|
||||
path: URL['pathname'],
|
||||
}
|
||||
export type SerializedPackageConfig = [
|
||||
PackageConfig['name'],
|
||||
PackageConfig['main'],
|
||||
PackageConfig['type'],
|
||||
string | undefined, // exports
|
||||
string | undefined, // imports
|
||||
string | undefined, // raw json available for experimental policy
|
||||
DeserializedPackageConfig['path'], // pjson file path
|
||||
]
|
||||
|
||||
export interface ModulesBinding {
|
||||
readPackageJSON(path: string): SerializedPackageConfig | undefined;
|
||||
getNearestParentPackageJSON(path: string): PackageConfig | undefined
|
||||
getNearestParentPackageJSON(path: string): SerializedPackageConfig | undefined
|
||||
getNearestRawParentPackageJSON(origin: URL['pathname']): [ReturnType<JSON['stringify']>, DeserializedPackageConfig['path']] | undefined
|
||||
getNearestParentPackageJSONType(path: string): PackageConfig['type']
|
||||
getPackageScopeConfig(path: string): SerializedPackageConfig | undefined
|
||||
getPackageJSONScripts(): string | undefined
|
||||
flushCompileCache(keepDeserializedCache?: boolean): void
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user