0
0
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:
Jacob Smith 2024-10-25 22:40:54 +02:00 committed by GitHub
parent 1ba8fbe4b8
commit e312d60e3f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
17 changed files with 517 additions and 152 deletions

View File

@ -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

View File

@ -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);

View File

@ -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

View File

@ -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,
};

View File

@ -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;

View File

@ -0,0 +1 @@
module.exports = 42;

View File

@ -0,0 +1,3 @@
const answer = require('./');
module.exports = answer+1;

View File

@ -0,0 +1,5 @@
{
"name": "main-no-index",
"main": "./main.js",
"type":"commonjs"
}

View File

@ -0,0 +1 @@
{"name": "package-with-sub-package"}

View File

@ -0,0 +1,3 @@
const { findPackageJSON } = require('node:module');
module.exports = findPackageJSON('..', __filename);

View File

@ -0,0 +1 @@
{"name": "sub-package", "type": "commonjs"}

View File

@ -0,0 +1,3 @@
import { findPackageJSON } from 'node:module';
export default findPackageJSON('..', import.meta.url);

View File

@ -0,0 +1 @@
{"name": "sub-package", "type": "module"}

View File

View File

@ -0,0 +1,5 @@
{
"name": "package-with-unrecognised-fields",
"type": "module",
"types": "./index.d.ts"
}

View 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,
});
}));
});
});

View File

@ -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
}