Skip to content

Commit

Permalink
src: move package resolver to c++
Browse files Browse the repository at this point in the history
  • Loading branch information
anonrig committed Oct 30, 2023
1 parent 9b0d60d commit cab29f9
Show file tree
Hide file tree
Showing 26 changed files with 670 additions and 276 deletions.
1 change: 1 addition & 0 deletions lib/internal/modules/cjs/loader.js
Original file line number Diff line number Diff line change
Expand Up @@ -1099,6 +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;
// TODO(@anonrig): Move this to C++.
const pkg = packageJsonReader.readPackageScope(parentPath) || { __proto__: null };
if (pkg.data?.imports != null) {
try {
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 @@ -228,7 +228,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
87 changes: 40 additions & 47 deletions lib/internal/modules/esm/package_config.js
Original file line number Diff line number Diff line change
@@ -1,69 +1,62 @@
'use strict';

const {
StringPrototypeEndsWith,
} = primordials;
const { URL, fileURLToPath } = require('internal/url');
const packageJsonReader = 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
*/
const { ArrayIsArray } = primordials;
const { fileURLToPath } = require('internal/url');
const { toNamespacedPath } = require('path');
const modulesBinding = internalBinding('modules');

/**
* 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);

// Terminates at root where ../package.json equals ../../package.json
// (can't just check "/package.json" for Windows support).
if (packageJSONUrl.pathname === lastPackageJSONUrl.pathname) {
break;
}
}
const packageJSONPath = fileURLToPath(packageJSONUrl);
return {
const packageScopeConfig = modulesBinding.getPackageScopeConfig(toNamespacedPath(`${resolved}`));
const response = {
__proto__: null,
pjsonPath: packageJSONPath,
pjsonPath: undefined,
exists: false,
main: undefined,
name: undefined,
type: 'none',
exports: undefined,
imports: undefined,
};
if (ArrayIsArray(packageScopeConfig)) {
const {
0: name,
1: main,
2: type,
3: imports,
4: exports,
} = packageScopeConfig;
response.name = name;
response.main = main;
response.type = type;
response.imports = imports;
response.exports = exports;
response.exists = true;
response.pjsonPath = fileURLToPath(resolved);
} else {
// This means that the response is a string
// and it is the path to the package.json file
response.pjsonPath = packageScopeConfig;
}

return response;
}

/**
* 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;
}


module.exports = {
getPackageScopeConfig,
getPackageType,
};
27 changes: 14 additions & 13 deletions lib/internal/modules/esm/resolve.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ const {
ArrayIsArray,
ArrayPrototypeJoin,
ArrayPrototypeShift,
JSONParse,
JSONStringify,
ObjectGetOwnPropertyNames,
ObjectPrototypeHasOwnProperty,
Expand Down Expand Up @@ -189,7 +190,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 @@ -567,6 +568,11 @@ function isConditionalExportsMainSugar(exports, packageJSONUrl, base) {
function packageExportsResolve(
packageJSONUrl, packageSubpath, packageConfig, base, conditions) {
let exports = packageConfig.exports;
// JSONParse is required because we don't parse imports and exports
// fields in package.json files.
if (exports[0] === '[' || exports[0] === '{') {
exports = JSONParse(exports);
}
if (isConditionalExportsMainSugar(exports, packageJSONUrl, base)) {
exports = { '.': exports };
}
Expand Down Expand Up @@ -681,8 +687,13 @@ function packageImportsResolve(name, base, conditions) {
const packageConfig = getPackageScopeConfig(base);
if (packageConfig.exists) {
packageJSONUrl = pathToFileURL(packageConfig.pjsonPath);
const imports = packageConfig.imports;
let imports = packageConfig.imports;
if (imports) {
// JSONParse is required because we don't parse imports and exports
// fields in package.json files.
if (imports[0] === '[' || imports[0] === '{') {
imports = JSONParse(imports);
}
if (ObjectPrototypeHasOwnProperty(imports, name) &&
!StringPrototypeIncludes(name, '*')) {
const resolveResult = resolvePackageTarget(
Expand Down Expand Up @@ -731,15 +742,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 @@ -787,6 +789,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 @@ -1170,8 +1173,6 @@ module.exports = {
decorateErrorWithCommonJSHints,
defaultResolve,
encodedSepRegEx,
getPackageScopeConfig,
getPackageType,
packageExportsResolve,
packageImportsResolve,
throwIfInvalidParentURL,
Expand Down
109 changes: 32 additions & 77 deletions lib/internal/modules/package_json_reader.js
Original file line number Diff line number Diff line change
@@ -1,60 +1,37 @@
'use strict';

const {
JSONParse,
ObjectPrototypeHasOwnProperty,
SafeMap,
StringPrototypeEndsWith,
StringPrototypeIndexOf,
StringPrototypeLastIndexOf,
StringPrototypeSlice,
} = primordials;
const {
ERR_INVALID_PACKAGE_CONFIG,
} = require('internal/errors').codes;
const { internalModuleReadJSON } = internalBinding('fs');
const modulesBinding = internalBinding('modules');
const { resolve, sep, toNamespacedPath } = require('path');
const permission = require('internal/process/permission');
const { kEmptyObject, setOwnProperty } = require('internal/util');

const { fileURLToPath, pathToFileURL } = require('internal/url');

const cache = new SafeMap();
const { kEmptyObject } = require('internal/util');

let manifest;

/**
* @typedef {{
* exists: boolean,
* pjsonPath: string,
* exports?: string | string[] | Record<string, unknown>,
* imports?: string | string[] | Record<string, unknown>,
* name?: string,
* main?: string,
* type: 'commonjs' | 'module' | 'none',
* }} PackageConfig
*/

/**
* @param {string} jsonPath
* @param {{
* base?: string,
* specifier: string,
* isESM: boolean,
* base?: URL | string,
* specifier?: URL | string,
* isESM?: boolean,
* }} options
* @returns {PackageConfig}
* @returns {import('typings/internalBinding/modules').PackageConfig}
*/
function read(jsonPath, { base, specifier, isESM } = kEmptyObject) {
if (cache.has(jsonPath)) {
return cache.get(jsonPath);
}

const string = internalModuleReadJSON(
// TODO(@anonrig): Understand why specifier and base is sometimes URL and sometimes string.
const parsed = modulesBinding.readPackageJSON(
toNamespacedPath(jsonPath),
isESM,
base == null ? undefined : `${base}`,
specifier == null ? undefined : `${specifier}`,
);
const result = {
const response = {
__proto__: null,
exists: false,
exists: parsed !== undefined,
pjsonPath: jsonPath,
main: undefined,
name: undefined,
Expand All @@ -63,43 +40,19 @@ function read(jsonPath, { base, specifier, isESM } = kEmptyObject) {
imports: undefined,
};

if (string !== undefined) {
let parsed;
try {
parsed = JSONParse(string);
} catch (cause) {
const error = new ERR_INVALID_PACKAGE_CONFIG(
jsonPath,
isESM && (base ? `"${specifier}" from ` : '') + fileURLToPath(base || specifier),
cause.message,
);
setOwnProperty(error, 'cause', cause);
throw error;
}

result.exists = true;

// ObjectPrototypeHasOwnProperty is used to avoid prototype pollution.
if (ObjectPrototypeHasOwnProperty(parsed, 'name') && typeof parsed.name === 'string') {
result.name = parsed.name;
}

if (ObjectPrototypeHasOwnProperty(parsed, 'main') && typeof parsed.main === 'string') {
result.main = parsed.main;
}

if (ObjectPrototypeHasOwnProperty(parsed, 'exports')) {
result.exports = parsed.exports;
}

if (ObjectPrototypeHasOwnProperty(parsed, 'imports')) {
result.imports = parsed.imports;
}

// Ignore unknown types for forwards compatibility
if (ObjectPrototypeHasOwnProperty(parsed, 'type') && (parsed.type === 'commonjs' || parsed.type === 'module')) {
result.type = parsed.type;
}
if (response.exists) {
const {
0: name,
1: main,
2: type,
3: imports,
4: exports,
} = parsed;
response.name = name;
response.main = main;
response.type = type;
response.imports = imports;
response.exports = exports;

if (manifest === undefined) {
const { getOptionValue } = require('internal/options');
Expand All @@ -108,19 +61,21 @@ function read(jsonPath, { base, specifier, isESM } = kEmptyObject) {
null;
}
if (manifest !== null) {
const jsonURL = pathToFileURL(jsonPath);
manifest.assertIntegrity(jsonURL, string);
// TODO(@anonrig): Find a way to assert integrity without returning string.
// const jsonURL = pathToFileURL(jsonPath);
// manifest.assertIntegrity(jsonURL, string);
}
}
cache.set(jsonPath, result);
return result;

return response;
}

/**
* @param {string} requestPath
* @return {PackageConfig}
*/
function readPackage(requestPath) {
// TODO(@anonrig): Remove this function.
return read(resolve(requestPath, 'package.json'));
}

Expand Down
8 changes: 4 additions & 4 deletions lib/internal/modules/run_main.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ const {
} = primordials;

const { containsModuleSyntax } = internalBinding('contextify');
const { getClosestPackageJSONType } = internalBinding('modules');
const { getOptionValue } = require('internal/options');
const path = require('path');

Expand Down Expand Up @@ -68,10 +69,9 @@ function shouldUseESMLoader(mainPath) {
if (mainPath && StringPrototypeEndsWith(mainPath, '.mjs')) { return true; }
if (!mainPath || StringPrototypeEndsWith(mainPath, '.cjs')) { return false; }

const { readPackageScope } = require('internal/modules/package_json_reader');
const pkg = readPackageScope(mainPath);
// No need to guard `pkg` as it can only be an object or `false`.
switch (pkg.data?.type) {
const type = getClosestPackageJSONType(path.toNamespacedPath(mainPath));

switch (type) {
case 'module':
return true;
case 'commonjs':
Expand Down
Loading

0 comments on commit cab29f9

Please sign in to comment.