Skip to content

Commit

Permalink
src: move package resolver to c++
Browse files Browse the repository at this point in the history
Co-authored-by: Daniel Lemire <daniel@lemire.me>
PR-URL: nodejs#50322
Reviewed-By: Jacob Smith <jacob@frende.me>
Reviewed-By: Matteo Collina <matteo.collina@gmail.com>
Reviewed-By: Antoine du Hamel <duhamelantoine1995@gmail.com>
Reviewed-By: James M Snell <jasnell@gmail.com>
Reviewed-By: Geoffrey Booth <webadmin@geoffreybooth.com>

# Conflicts:
#	lib/internal/modules/package_json_reader.js
#	src/node_file.cc
#	test/parallel/test-module-binding.js
  • Loading branch information
anonrig committed Jun 27, 2024
1 parent 7f4d716 commit 19640ce
Show file tree
Hide file tree
Showing 26 changed files with 797 additions and 399 deletions.
29 changes: 14 additions & 15 deletions lib/internal/modules/cjs/loader.js
Original file line number Diff line number Diff line change
Expand Up @@ -428,7 +428,7 @@ ObjectDefineProperty(Module, '_readPackage', {
* @param {string} originalPath The specifier passed to `require`
*/
function tryPackage(requestPath, exts, isMain, originalPath) {
const pkg = _readPackage(requestPath).main;
const { main: pkg, pjsonPath } = _readPackage(requestPath);

if (!pkg) {
return tryExtensions(path.resolve(requestPath, 'index'), exts, isMain);
Expand All @@ -447,14 +447,13 @@ function tryPackage(requestPath, exts, isMain, originalPath) {
'Please verify that the package.json has a valid "main" entry',
);
err.code = 'MODULE_NOT_FOUND';
err.path = path.resolve(requestPath, 'package.json');
err.path = pjsonPath;
err.requestPath = originalPath;
// TODO(BridgeAR): Add the requireStack as well.
throw err;
} else {
const jsonPath = path.resolve(requestPath, 'package.json');
process.emitWarning(
`Invalid 'main' field in '${jsonPath}' of '${pkg}'. ` +
`Invalid 'main' field in '${pjsonPath}' of '${pkg}'. ` +
'Please either fix that or report it to the module author',
'DeprecationWarning',
'DEP0128',
Expand Down Expand Up @@ -540,28 +539,28 @@ function trySelfParentPath(parent) {
function trySelf(parentPath, request) {
if (!parentPath) { return false; }

const { data: pkg, path: pkgPath } = packageJsonReader.readPackageScope(parentPath);
if (!pkg || pkg.exports == null || pkg.name === undefined) {
const pkg = packageJsonReader.getNearestParentPackageJSON(parentPath);
if (pkg?.data.exports === undefined || pkg.data.name === undefined) {
return false;
}

let expansion;
if (request === pkg.name) {
if (request === pkg.data.name) {
expansion = '.';
} else if (StringPrototypeStartsWith(request, `${pkg.name}/`)) {
expansion = '.' + StringPrototypeSlice(request, pkg.name.length);
} else if (StringPrototypeStartsWith(request, `${pkg.data.name}/`)) {
expansion = '.' + StringPrototypeSlice(request, pkg.data.name.length);
} else {
return false;
}

try {
const { packageExportsResolve } = require('internal/modules/esm/resolve');
return finalizeEsmResolution(packageExportsResolve(
pathToFileURL(pkgPath + '/package.json'), expansion, pkg,
pathToFileURL(parentPath), getCjsConditions()), parentPath, pkgPath);
pathToFileURL(pkg.path + '/package.json'), expansion, pkg.data,
pathToFileURL(parentPath), getCjsConditions()), parentPath, pkg.path);
} catch (e) {
if (e.code === 'ERR_MODULE_NOT_FOUND') {
throw createEsmNotFoundErr(request, pkgPath + '/package.json');
throw createEsmNotFoundErr(request, pkg.path + '/package.json');
}
throw e;
}
Expand Down Expand Up @@ -1100,7 +1099,7 @@ 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.readPackageScope(parentPath) || { __proto__: null };
const pkg = packageJsonReader.getNearestParentPackageJSON(parentPath) || { __proto__: null };
if (pkg.data?.imports != null) {
try {
const { packageImportsResolve } = require('internal/modules/esm/resolve');
Expand Down Expand Up @@ -1378,9 +1377,9 @@ Module._extensions['.js'] = function(module, filename) {
content = fs.readFileSync(filename, 'utf8');
}
if (StringPrototypeEndsWith(filename, '.js')) {
const pkg = packageJsonReader.readPackageScope(filename) || { __proto__: null };
const pkg = packageJsonReader.getNearestParentPackageJSON(filename);
// Function require shouldn't be used in ES modules.
if (pkg.data?.type === 'module') {
if (pkg?.data.type === 'module') {
// This is an error path because `require` of a `.js` file in a `"type": "module"` scope is not allowed.
const parent = moduleParentCache.get(module);
const parentPath = parent?.filename;
Expand Down
2 changes: 1 addition & 1 deletion lib/internal/modules/esm/get_format.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ const {
const experimentalNetworkImports =
getOptionValue('--experimental-network-imports');
const { containsModuleSyntax } = internalBinding('contextify');
const { getPackageType } = require('internal/modules/esm/resolve');
const { getPackageType } = require('internal/modules/esm/package_config');
const { fileURLToPath } = require('internal/url');
const { ERR_UNKNOWN_FILE_EXTENSION } = require('internal/errors').codes;

Expand Down
2 changes: 1 addition & 1 deletion lib/internal/modules/esm/module_job.js
Original file line number Diff line number Diff line change
Expand Up @@ -232,7 +232,7 @@ class ModuleJob {
const packageConfig =
StringPrototypeStartsWith(this.module.url, 'file://') &&
RegExpPrototypeExec(/\.js(\?[^#]*)?(#.*)?$/, this.module.url) !== null &&
require('internal/modules/esm/resolve')
require('internal/modules/esm/package_config')
.getPackageScopeConfig(this.module.url);
if (packageConfig.type === 'module') {
e.message +=
Expand Down
69 changes: 22 additions & 47 deletions lib/internal/modules/esm/package_config.js
Original file line number Diff line number Diff line change
@@ -1,69 +1,44 @@
'use strict';

const {
StringPrototypeEndsWith,
} = primordials;
const { URL, fileURLToPath } = require('internal/url');
const packageJsonReader = require('internal/modules/package_json_reader');
const { ArrayIsArray } = primordials;
const modulesBinding = internalBinding('modules');
const { deserializePackageJSON } = require('internal/modules/package_json_reader');

/**
* @typedef {object} PackageConfig
* @property {string} pjsonPath - The path to the package.json file.
* @property {boolean} exists - Whether the package.json file exists.
* @property {'none' | 'commonjs' | 'module'} type - The type of the package.
* @property {string} [name] - The name of the package.
* @property {string} [main] - The main entry point of the package.
* @property {PackageTarget} [exports] - The exports configuration of the package.
* @property {Record<string, string | Record<string, string>>} [imports] - The imports configuration of the package.
*/
/**
* @typedef {string | string[] | Record<string, string | Record<string, string>>} PackageTarget
*/
// TODO(@anonrig): Merge this file with internal/esm/package_json_reader.js

/**
* Returns the package configuration for the given resolved URL.
* @param {URL | string} resolved - The resolved URL.
* @returns {PackageConfig} - The package configuration.
* @returns {import('typings/internalBinding/modules').PackageConfig} - The package configuration.
*/
function getPackageScopeConfig(resolved) {
let packageJSONUrl = new URL('./package.json', resolved);
while (true) {
const packageJSONPath = packageJSONUrl.pathname;
if (StringPrototypeEndsWith(packageJSONPath, 'node_modules/package.json')) {
break;
}
const packageConfig = packageJsonReader.read(fileURLToPath(packageJSONUrl), {
__proto__: null,
specifier: resolved,
isESM: true,
});
if (packageConfig.exists) {
return packageConfig;
}

const lastPackageJSONUrl = packageJSONUrl;
packageJSONUrl = new URL('../package.json', packageJSONUrl);
const result = modulesBinding.getPackageScopeConfig(`${resolved}`);

// Terminates at root where ../package.json equals ../../package.json
// (can't just check "/package.json" for Windows support).
if (packageJSONUrl.pathname === lastPackageJSONUrl.pathname) {
break;
}
if (ArrayIsArray(result)) {
return deserializePackageJSON(`${resolved}`, result, false /* checkIntegrity */);
}
const packageJSONPath = fileURLToPath(packageJSONUrl);

// This means that the response is a string
// and it is the path to the package.json file
return {
__proto__: null,
pjsonPath: packageJSONPath,
pjsonPath: result,
exists: false,
main: undefined,
name: undefined,
type: 'none',
exports: undefined,
imports: undefined,
};
}

/**
* Returns the package type for a given URL.
* @param {URL} url - The URL to get the package type for.
*/
function getPackageType(url) {
// TODO(@anonrig): Write a C++ function that returns only "type".
return getPackageScopeConfig(url).type;
}


module.exports = {
getPackageScopeConfig,
getPackageType,
};
18 changes: 4 additions & 14 deletions lib/internal/modules/esm/resolve.js
Original file line number Diff line number Diff line change
Expand Up @@ -199,7 +199,7 @@ const legacyMainResolveExtensionsIndexes = {
* 4. TRY(pkg_url/index.js, pkg_url/index.json, pkg_url/index.node)
* 5. NOT_FOUND
* @param {URL} packageJSONUrl
* @param {PackageConfig} packageConfig
* @param {import('typings/internalBinding/modules').PackageConfig} packageConfig
* @param {string | URL | undefined} base
* @returns {URL}
*/
Expand Down Expand Up @@ -503,7 +503,7 @@ function resolvePackageTarget(packageJSONUrl, target, subpath, packageSubpath,
}
return resolveResult;
}
if (lastException === undefined || lastException === null) {
if (lastException == null) {
return lastException;
}
throw lastException;
Expand Down Expand Up @@ -576,7 +576,7 @@ function isConditionalExportsMainSugar(exports, packageJSONUrl, base) {
*/
function packageExportsResolve(
packageJSONUrl, packageSubpath, packageConfig, base, conditions) {
let exports = packageConfig.exports;
let { exports } = packageConfig;
if (isConditionalExportsMainSugar(exports, packageJSONUrl, base)) {
exports = { '.': exports };
}
Expand Down Expand Up @@ -741,15 +741,6 @@ function packageImportsResolve(name, base, conditions) {
throw importNotDefined(name, packageJSONUrl, base);
}

/**
* Returns the package type for a given URL.
* @param {URL} url - The URL to get the package type for.
*/
function getPackageType(url) {
const packageConfig = getPackageScopeConfig(url);
return packageConfig.type;
}

/**
* Parse a package name from a specifier.
* @param {string} specifier - The import specifier.
Expand Down Expand Up @@ -797,6 +788,7 @@ function parsePackageName(specifier, base) {
* @returns {URL} - The resolved URL.
*/
function packageResolve(specifier, base, conditions) {
// TODO(@anonrig): Move this to a C++ function.
if (BuiltinModule.canBeRequiredWithoutScheme(specifier)) {
return new URL('node:' + specifier);
}
Expand Down Expand Up @@ -1206,8 +1198,6 @@ module.exports = {
decorateErrorWithCommonJSHints,
defaultResolve,
encodedSepRegEx,
getPackageScopeConfig,
getPackageType,
packageExportsResolve,
packageImportsResolve,
throwIfInvalidParentURL,
Expand Down
Loading

0 comments on commit 19640ce

Please sign in to comment.