diff --git a/lib/internal/modules/cjs/loader.js b/lib/internal/modules/cjs/loader.js index 7bbd59e16330b5..0d76fa0613b7a9 100644 --- a/lib/internal/modules/cjs/loader.js +++ b/lib/internal/modules/cjs/loader.js @@ -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); @@ -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', @@ -540,16 +539,16 @@ 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; } @@ -557,11 +556,11 @@ function trySelf(parentPath, request) { 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; } @@ -1100,7 +1099,7 @@ Module._resolveFilename = function(request, parent, isMain, options) { if (request[0] === '#' && (parent?.filename || parent?.id === '')) { 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'); @@ -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; diff --git a/lib/internal/modules/esm/get_format.js b/lib/internal/modules/esm/get_format.js index 1931688e85d05e..c029b6c614384f 100644 --- a/lib/internal/modules/esm/get_format.js +++ b/lib/internal/modules/esm/get_format.js @@ -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; diff --git a/lib/internal/modules/esm/module_job.js b/lib/internal/modules/esm/module_job.js index ba62fd4361a9a6..2f04070327f10b 100644 --- a/lib/internal/modules/esm/module_job.js +++ b/lib/internal/modules/esm/module_job.js @@ -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 += diff --git a/lib/internal/modules/esm/package_config.js b/lib/internal/modules/esm/package_config.js index 5da47764c9de2c..6b3847966cb1d3 100644 --- a/lib/internal/modules/esm/package_config.js +++ b/lib/internal/modules/esm/package_config.js @@ -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>} [imports] - The imports configuration of the package. - */ -/** - * @typedef {string | string[] | Record>} 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, }; diff --git a/lib/internal/modules/esm/resolve.js b/lib/internal/modules/esm/resolve.js index b56ad6cc833f00..f4485e6fb38b9c 100644 --- a/lib/internal/modules/esm/resolve.js +++ b/lib/internal/modules/esm/resolve.js @@ -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} */ @@ -503,7 +503,7 @@ function resolvePackageTarget(packageJSONUrl, target, subpath, packageSubpath, } return resolveResult; } - if (lastException === undefined || lastException === null) { + if (lastException == null) { return lastException; } throw lastException; @@ -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 }; } @@ -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. @@ -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); } @@ -1206,8 +1198,6 @@ module.exports = { decorateErrorWithCommonJSHints, defaultResolve, encodedSepRegEx, - getPackageScopeConfig, - getPackageType, packageExportsResolve, packageImportsResolve, throwIfInvalidParentURL, diff --git a/lib/internal/modules/package_json_reader.js b/lib/internal/modules/package_json_reader.js index 88c079d10d1161..232ed35528c5c5 100644 --- a/lib/internal/modules/package_json_reader.js +++ b/lib/internal/modules/package_json_reader.js @@ -2,178 +2,158 @@ const { JSONParse, - ObjectPrototypeHasOwnProperty, - SafeMap, - StringPrototypeEndsWith, - StringPrototypeIndexOf, - StringPrototypeLastIndexOf, StringPrototypeSlice, + StringPrototypeLastIndexOf, + ObjectDefineProperty, } = primordials; -const { - ERR_INVALID_PACKAGE_CONFIG, -} = require('internal/errors').codes; -const { internalModuleReadJSON } = internalBinding('fs'); -const { resolve, sep, toNamespacedPath } = require('path'); -const permission = require('internal/process/permission'); +const modulesBinding = internalBinding('modules'); +const { resolve, sep } = require('path'); const { kEmptyObject } = require('internal/util'); - -const { fileURLToPath, pathToFileURL } = require('internal/url'); - -const cache = new SafeMap(); -const isAIX = process.platform === 'aix'; +const { pathToFileURL } = require('internal/url'); let manifest; /** - * @typedef {{ - * exists: boolean, - * pjsonPath: string, - * exports?: string | string[] | Record, - * imports?: string | string[] | Record, - * name?: string, - * main?: string, - * type: 'commonjs' | 'module' | 'none', - * }} PackageConfig + * @param {string} jsonPath + * @param {string} value The integrity value to check against. */ +function checkPackageJSONIntegrity(jsonPath, value) { + if (manifest === undefined) { + const { getOptionValue } = require('internal/options'); + manifest = getOptionValue('--experimental-policy') ? + require('internal/process/policy').manifest : + null; + } + if (manifest !== null) { + const jsonURL = pathToFileURL(jsonPath); + manifest.assertIntegrity(jsonURL, value); + } +} /** - * @param {string} jsonPath - * @param {{ - * base?: string, - * specifier: string, - * isESM: boolean, - * }} options - * @returns {PackageConfig} + * @param {string} path + * @param {import('typings/internalBinding/modules').SerializedPackageConfig} contents + * @param {boolean} [checkIntegrity=false] Whether to check the integrity of the package.json file. + * @returns {import('typings/internalBinding/modules').PackageConfig} */ -function read(jsonPath, { base, specifier, isESM } = kEmptyObject) { - if (cache.has(jsonPath)) { - return cache.get(jsonPath); +function deserializePackageJSON(path, contents, checkIntegrity = false) { + if (contents === undefined) { + return { + __proto__: null, + exists: false, + pjsonPath: path, + type: 'none', // Ignore unknown types for forwards compatibility + }; } + let pjsonPath = path; const { - 0: string, - 1: containsKeys, - } = internalModuleReadJSON( - toNamespacedPath(jsonPath), - ); - const result = { + 0: name, + 1: main, + 2: type, + 3: plainImports, + 4: plainExports, + 5: manifest, + 6: optionalFilePath, + } = contents; + + // This is required to be used in getPackageScopeConfig. + if (optionalFilePath) { + pjsonPath = optionalFilePath; + } + + if (checkIntegrity) { + // parsed[5] is only available when experimental policy is enabled. + checkPackageJSONIntegrity(pjsonPath, manifest); + } + + // 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] === '{')); + + return { __proto__: null, - exists: false, - pjsonPath: jsonPath, - main: undefined, - name: undefined, - type: 'none', // Ignore unknown types for forwards compatibility - exports: undefined, - imports: undefined, + 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; + }, }; +} - // Folder read operation succeeds in AIX. - // For libuv change, see https://github.com/libuv/libuv/pull/2025. - // https://github.com/nodejs/node/pull/48477#issuecomment-1604586650 - // TODO(anonrig): Follow-up on this change and remove it since it is a - // semver-major change. - const isResultValid = isAIX && !isESM ? containsKeys : string !== undefined; - - if (isResultValid) { - let parsed; - try { - parsed = JSONParse(string); - } catch (error) { - if (isESM) { - throw new ERR_INVALID_PACKAGE_CONFIG( - jsonPath, - (base ? `"${specifier}" from ` : '') + fileURLToPath(base || specifier), - error.message, - ); - } else { - // For backward compat, we modify the error returned by JSON.parse rather than creating a new one. - // TODO(aduh95): make it throw ERR_INVALID_PACKAGE_CONFIG in a semver-major with original error as cause - error.message = 'Error parsing ' + jsonPath + ': ' + error.message; - error.path = jsonPath; - 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 (manifest === undefined) { - const { getOptionValue } = require('internal/options'); - manifest = getOptionValue('--experimental-policy') ? - require('internal/process/policy').manifest : - null; - } - if (manifest !== null) { - const jsonURL = pathToFileURL(jsonPath); - manifest.assertIntegrity(jsonURL, string); - } - } - cache.set(jsonPath, result); - return result; +/** + * Reads a package.json file and returns the parsed contents. + * @param {string} jsonPath + * @param {{ + * base?: URL | string, + * specifier?: URL | string, + * isESM?: boolean, + * }} options + * @returns {import('typings/internalBinding/modules').PackageConfig} + */ +function read(jsonPath, { base, specifier, isESM } = kEmptyObject) { + // This function will be called by both CJS and ESM, so we need to make sure + // non-null attributes are converted to strings. + const parsed = modulesBinding.readPackageJSON( + jsonPath, + isESM, + base == null ? undefined : `${base}`, + specifier == null ? undefined : `${specifier}`, + ); + + return deserializePackageJSON(jsonPath, parsed, true /* checkIntegrity */); } /** + * @deprecated Expected to be removed in favor of `read` in the future. + * Behaves the same was as `read`, but appends package.json to the path. * @param {string} requestPath * @return {PackageConfig} */ function readPackage(requestPath) { + // TODO(@anonrig): Remove this function. return read(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 false. + * 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}} */ -function readPackageScope(checkPath) { - const rootSeparatorIndex = StringPrototypeIndexOf(checkPath, sep); - let separatorIndex; - const enabledPermission = permission.isEnabled(); - do { - separatorIndex = StringPrototypeLastIndexOf(checkPath, sep); - checkPath = StringPrototypeSlice(checkPath, 0, separatorIndex); - // Stop the search when the process doesn't have permissions - // to walk upwards - if (enabledPermission && !permission.has('fs.read', checkPath + sep)) { - return false; - } - if (StringPrototypeEndsWith(checkPath, sep + 'node_modules')) { - return false; - } - const pjson = readPackage(checkPath + sep); - if (pjson.exists) { - return { - data: pjson, - path: checkPath, - }; - } - } while (separatorIndex > rootSeparatorIndex); - return false; +function getNearestParentPackageJSON(checkPath) { + const result = modulesBinding.getNearestParentPackageJSON(checkPath); + + if (result === undefined) { + return undefined; + } + + const data = deserializePackageJSON(checkPath, result, true /* checkIntegrity */); + + // 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 }; } module.exports = { + checkPackageJSONIntegrity, read, readPackage, - readPackageScope, + getNearestParentPackageJSON, + + deserializePackageJSON, }; diff --git a/lib/internal/modules/run_main.js b/lib/internal/modules/run_main.js index ca401044c0178c..6488bedb21cf6c 100644 --- a/lib/internal/modules/run_main.js +++ b/lib/internal/modules/run_main.js @@ -5,7 +5,9 @@ const { } = primordials; const { containsModuleSyntax } = internalBinding('contextify'); +const { getNearestParentPackageJSONType } = internalBinding('modules'); const { getOptionValue } = require('internal/options'); +const { checkPackageJSONIntegrity } = require('internal/modules/package_json_reader'); const path = require('path'); const { pathToFileURL } = require('internal/url'); const { kEmptyObject, getCWDURL } = require('internal/util'); @@ -77,22 +79,27 @@ 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) { - case 'module': - return true; - case 'commonjs': - return false; - default: { // No package.json or no `type` field. - if (getOptionValue('--experimental-detect-module')) { - // If the first argument of `containsModuleSyntax` is undefined, it will read `mainPath` from the file system. - return containsModuleSyntax(undefined, mainPath); - } - return false; + const response = getNearestParentPackageJSONType(mainPath); + + // No package.json or no `type` field. + if (response === undefined || response[0] === 'none') { + if (getOptionValue('--experimental-detect-module')) { + // If the first argument of `containsModuleSyntax` is undefined, it will read `mainPath` from the file system. + return containsModuleSyntax(undefined, mainPath); } + return false; } + + // TODO(@anonrig): Do not return filePath and rawContent if experimental-policy is not used. + const { + 0: type, + 1: filePath, + 2: rawContent, + } = response; + + checkPackageJSONIntegrity(filePath, rawContent); + + return type === 'module'; } /** diff --git a/node.gyp b/node.gyp index e5873af7bc3dd6..6e5306c1f772f1 100644 --- a/node.gyp +++ b/node.gyp @@ -114,6 +114,7 @@ 'src/node_main_instance.cc', 'src/node_messaging.cc', 'src/node_metadata.cc', + 'src/node_modules.cc', 'src/node_options.cc', 'src/node_os.cc', 'src/node_perf.cc', @@ -238,6 +239,7 @@ 'src/node_messaging.h', 'src/node_metadata.h', 'src/node_mutex.h', + 'src/node_modules.h', 'src/node_object_wrap.h', 'src/node_options.h', 'src/node_options-inl.h', diff --git a/src/base_object_types.h b/src/base_object_types.h index cb034f1d62b681..9cfe6a77f71708 100644 --- a/src/base_object_types.h +++ b/src/base_object_types.h @@ -17,7 +17,8 @@ namespace node { V(blob_binding_data, BlobBindingData) \ V(process_binding_data, process::BindingData) \ V(timers_binding_data, timers::BindingData) \ - V(url_binding_data, url::BindingData) + V(url_binding_data, url::BindingData) \ + V(modules_binding_data, modules::BindingData) #define UNSERIALIZABLE_BINDING_TYPES(V) \ V(http2_binding_data, http2::BindingData) \ diff --git a/src/node_binding.cc b/src/node_binding.cc index 6b0297d8984ccb..6b2dc510e8a437 100644 --- a/src/node_binding.cc +++ b/src/node_binding.cc @@ -52,6 +52,7 @@ V(js_stream) \ V(js_udp_wrap) \ V(messaging) \ + V(modules) \ V(module_wrap) \ V(mksnapshot) \ V(options) \ diff --git a/src/node_binding.h b/src/node_binding.h index 7256bf2bbcf732..dfcfe5fe9e1bf4 100644 --- a/src/node_binding.h +++ b/src/node_binding.h @@ -40,6 +40,7 @@ static_assert(static_cast(NM_F_LINKED) == V(fs_dir) \ V(messaging) \ V(mksnapshot) \ + V(modules) \ V(module_wrap) \ V(performance) \ V(process_methods) \ diff --git a/src/node_errors.h b/src/node_errors.h index 30f66a7648bff4..9f94f7104adeaa 100644 --- a/src/node_errors.h +++ b/src/node_errors.h @@ -76,6 +76,7 @@ void OOMErrorHandler(const char* location, const v8::OOMDetails& details); V(ERR_INVALID_ARG_TYPE, TypeError) \ V(ERR_INVALID_FILE_URL_HOST, TypeError) \ V(ERR_INVALID_FILE_URL_PATH, TypeError) \ + V(ERR_INVALID_PACKAGE_CONFIG, Error) \ V(ERR_INVALID_OBJECT_DEFINE_PROPERTY, TypeError) \ V(ERR_INVALID_MODULE, Error) \ V(ERR_INVALID_STATE, Error) \ diff --git a/src/node_external_reference.h b/src/node_external_reference.h index a3317d25ad6a96..8060392f8cc74c 100644 --- a/src/node_external_reference.h +++ b/src/node_external_reference.h @@ -113,6 +113,7 @@ class ExternalReferenceRegistry { V(messaging) \ V(mksnapshot) \ V(module_wrap) \ + V(modules) \ V(options) \ V(os) \ V(performance) \ diff --git a/src/node_file.cc b/src/node_file.cc index b0aa53420c4efb..a6106779b50373 100644 --- a/src/node_file.cc +++ b/src/node_file.cc @@ -50,7 +50,6 @@ namespace fs { using v8::Array; using v8::BigInt; -using v8::Boolean; using v8::Context; using v8::EscapableHandleScope; using v8::Function; @@ -1039,103 +1038,6 @@ static void ExistsSync(const FunctionCallbackInfo& args) { args.GetReturnValue().Set(err == 0); } -// Used to speed up module loading. Returns an array [string, boolean] -static void InternalModuleReadJSON(const FunctionCallbackInfo& args) { - Environment* env = Environment::GetCurrent(args); - Isolate* isolate = env->isolate(); - uv_loop_t* loop = env->event_loop(); - - CHECK(args[0]->IsString()); - node::Utf8Value path(isolate, args[0]); - THROW_IF_INSUFFICIENT_PERMISSIONS( - env, permission::PermissionScope::kFileSystemRead, path.ToStringView()); - - if (strlen(*path) != path.length()) { - args.GetReturnValue().Set(Array::New(isolate)); - return; // Contains a nul byte. - } - uv_fs_t open_req; - const int fd = uv_fs_open(loop, &open_req, *path, O_RDONLY, 0, nullptr); - uv_fs_req_cleanup(&open_req); - - if (fd < 0) { - args.GetReturnValue().Set(Array::New(isolate)); - return; - } - - auto defer_close = OnScopeLeave([fd, loop]() { - uv_fs_t close_req; - CHECK_EQ(0, uv_fs_close(loop, &close_req, fd, nullptr)); - uv_fs_req_cleanup(&close_req); - }); - - const size_t kBlockSize = 32 << 10; - std::vector chars; - int64_t offset = 0; - ssize_t numchars; - do { - const size_t start = chars.size(); - chars.resize(start + kBlockSize); - - uv_buf_t buf; - buf.base = &chars[start]; - buf.len = kBlockSize; - - uv_fs_t read_req; - numchars = uv_fs_read(loop, &read_req, fd, &buf, 1, offset, nullptr); - uv_fs_req_cleanup(&read_req); - - if (numchars < 0) { - args.GetReturnValue().Set(Array::New(isolate)); - return; - } - offset += numchars; - } while (static_cast(numchars) == kBlockSize); - - size_t start = 0; - if (offset >= 3 && 0 == memcmp(chars.data(), "\xEF\xBB\xBF", 3)) { - start = 3; // Skip UTF-8 BOM. - } - const size_t size = offset - start; - - // TODO(anonrig): Follow-up on removing the following changes for AIX. - char* p = &chars[start]; - char* pe = &chars[size]; - char* pos[2]; - char** ppos = &pos[0]; - - while (p < pe) { - char c = *p++; - if (c == '\\' && p < pe && *p == '"') p++; - if (c != '"') continue; - *ppos++ = p; - if (ppos < &pos[2]) continue; - ppos = &pos[0]; - - char* s = &pos[0][0]; - char* se = &pos[1][-1]; // Exclude quote. - size_t n = se - s; - - if (n == 4) { - if (0 == memcmp(s, "main", 4)) break; - if (0 == memcmp(s, "name", 4)) break; - if (0 == memcmp(s, "type", 4)) break; - } else if (n == 7) { - if (0 == memcmp(s, "exports", 7)) break; - if (0 == memcmp(s, "imports", 7)) break; - } - } - - Local return_value[] = { - String::NewFromUtf8( - isolate, &chars[start], v8::NewStringType::kNormal, size) - .ToLocalChecked(), - Boolean::New(isolate, p < pe ? true : false)}; - - args.GetReturnValue().Set( - Array::New(isolate, return_value, arraysize(return_value))); -} - // Used to speed up module loading. Returns 0 if the path refers to // a file, 1 when it's a directory or < 0 on error (usually -ENOENT.) // The speedup comes from not creating thousands of Stat and Error objects. @@ -3379,7 +3281,6 @@ static void CreatePerIsolateProperties(IsolateData* isolate_data, SetMethod(isolate, target, "rmdir", RMDir); SetMethod(isolate, target, "mkdir", MKDir); SetMethod(isolate, target, "readdir", ReadDir); - SetMethod(isolate, target, "internalModuleReadJSON", InternalModuleReadJSON); SetMethod(isolate, target, "internalModuleStat", InternalModuleStat); SetMethod(isolate, target, "stat", Stat); SetMethod(isolate, target, "lstat", LStat); @@ -3500,7 +3401,6 @@ void RegisterExternalReferences(ExternalReferenceRegistry* registry) { registry->Register(RMDir); registry->Register(MKDir); registry->Register(ReadDir); - registry->Register(InternalModuleReadJSON); registry->Register(InternalModuleStat); registry->Register(Stat); registry->Register(LStat); diff --git a/src/node_modules.cc b/src/node_modules.cc new file mode 100644 index 00000000000000..ffe9fdf3f2ccdf --- /dev/null +++ b/src/node_modules.cc @@ -0,0 +1,450 @@ +#include "node_modules.h" +#include +#include "base_object-inl.h" +#include "node_errors.h" +#include "node_external_reference.h" +#include "node_url.h" +#include "permission/permission.h" +#include "permission/permission_base.h" +#include "util-inl.h" +#include "v8-fast-api-calls.h" +#include "v8-function-callback.h" +#include "v8-primitive.h" +#include "v8-value.h" +#include "v8.h" + +#include "simdjson.h" + +namespace node { +namespace modules { + +using v8::Array; +using v8::Context; +using v8::FunctionCallbackInfo; +using v8::HandleScope; +using v8::Isolate; +using v8::Local; +using v8::NewStringType; +using v8::Object; +using v8::ObjectTemplate; +using v8::Primitive; +using v8::String; +using v8::Undefined; +using v8::Value; + +#ifdef __POSIX__ +constexpr char kPathSeparator = '/'; +constexpr std::string_view kNodeModules = "/node_modules"; +#else +constexpr char kPathSeparator = '\\'; +constexpr std::string_view kNodeModules = "\\node_modules"; +#endif + +void BindingData::MemoryInfo(MemoryTracker* tracker) const { + // Do nothing +} + +BindingData::BindingData(Realm* realm, + v8::Local object, + InternalFieldInfo* info) + : SnapshotableObject(realm, object, type_int) {} + +bool BindingData::PrepareForSerialization(v8::Local context, + v8::SnapshotCreator* creator) { + // Return true because we need to maintain the reference to the binding from + // JS land. + return true; +} + +InternalFieldInfoBase* BindingData::Serialize(int index) { + DCHECK_IS_SNAPSHOT_SLOT(index); + InternalFieldInfo* info = + InternalFieldInfoBase::New(type()); + return info; +} + +void BindingData::Deserialize(v8::Local context, + v8::Local holder, + int index, + InternalFieldInfoBase* info) { + DCHECK_IS_SNAPSHOT_SLOT(index); + HandleScope scope(context->GetIsolate()); + Realm* realm = Realm::GetCurrent(context); + BindingData* binding = realm->AddBindingData(holder); + CHECK_NOT_NULL(binding); +} + +Local BindingData::PackageConfig::Serialize(Realm* realm) const { + auto has_manifest = !realm->env()->options()->experimental_policy.empty(); + auto isolate = realm->isolate(); + const auto ToString = [isolate](std::string_view input) -> Local { + return String::NewFromUtf8( + isolate, input.data(), NewStringType::kNormal, input.size()) + .ToLocalChecked(); + }; + Local values[7] = { + name.has_value() ? ToString(*name) : Undefined(isolate), + main.has_value() ? ToString(*main) : Undefined(isolate), + ToString(type), + imports.has_value() ? ToString(*imports) : Undefined(isolate), + exports.has_value() ? ToString(*exports) : Undefined(isolate), + has_manifest ? ToString(raw_json) : Undefined(isolate), + ToString(file_path), + }; + return Array::New(isolate, values, 7); +} + +const BindingData::PackageConfig* BindingData::GetPackageJSON( + Realm* realm, std::string_view path, ErrorContext* error_context) { + auto binding_data = realm->GetBindingData(); + + auto cache_entry = binding_data->package_configs_.find(path.data()); + if (cache_entry != binding_data->package_configs_.end()) { + return &cache_entry->second; + } + + PackageConfig package_config{}; + package_config.file_path = path; + // No need to exclude BOM since simdjson will skip it. + if (ReadFileSync(&package_config.raw_json, path.data()) < 0) { + return nullptr; + } + + simdjson::ondemand::document document; + simdjson::ondemand::object main_object; + simdjson::error_code error = + binding_data->json_parser.iterate(package_config.raw_json).get(document); + + const auto throw_invalid_package_config = [error_context, path, realm]() { + if (error_context == nullptr) { + THROW_ERR_INVALID_PACKAGE_CONFIG( + realm->isolate(), "Invalid package config %s.", path.data()); + } else if (error_context->base.has_value()) { + auto file_url = ada::parse(error_context->base.value()); + CHECK(file_url); + auto file_path = url::FileURLToPath(realm->env(), *file_url); + CHECK(file_path.has_value()); + THROW_ERR_INVALID_PACKAGE_CONFIG( + realm->isolate(), + "Invalid package config %s while importing \"%s\" from %s.", + path.data(), + error_context->specifier.c_str(), + file_path->c_str()); + } else { + THROW_ERR_INVALID_PACKAGE_CONFIG( + realm->isolate(), "Invalid package config %s.", path.data()); + } + + return nullptr; + }; + + if (error || document.get_object().get(main_object)) { + return throw_invalid_package_config(); + } + + simdjson::ondemand::raw_json_string key; + simdjson::ondemand::value value; + std::string_view field_value; + simdjson::ondemand::json_type field_type; + + for (auto field : main_object) { + // Throw error if getting key or value fails. + if (field.key().get(key) || field.value().get(value)) { + return throw_invalid_package_config(); + } + + if (key == "name") { + // Though there is a key "name" with a corresponding value, + // the value may not be a string or could be an invalid JSON string + if (value.get_string(package_config.name)) { + return throw_invalid_package_config(); + } + } else if (key == "main") { + if (value.get_string(package_config.main)) { + return throw_invalid_package_config(); + } + } else if (key == "exports") { + if (value.type().get(field_type)) { + return throw_invalid_package_config(); + } + switch (field_type) { + case simdjson::ondemand::json_type::object: + case simdjson::ondemand::json_type::array: { + if (value.raw_json().get(field_value)) { + return throw_invalid_package_config(); + } + package_config.exports = field_value; + break; + } + case simdjson::ondemand::json_type::string: { + if (value.get_string(package_config.exports)) { + return throw_invalid_package_config(); + } + break; + } + default: + break; + } + } else if (key == "imports") { + if (value.type().get(field_type)) { + return throw_invalid_package_config(); + } + switch (field_type) { + case simdjson::ondemand::json_type::array: + case simdjson::ondemand::json_type::object: { + if (value.raw_json().get(field_value)) { + return throw_invalid_package_config(); + } + package_config.imports = field_value; + break; + } + case simdjson::ondemand::json_type::string: { + if (value.get_string(package_config.imports)) { + return throw_invalid_package_config(); + } + break; + } + default: + break; + } + } else if (key == "type") { + if (value.get_string().get(field_value)) { + return throw_invalid_package_config(); + } + // Only update type if it is "commonjs" or "module" + // The default value is "none" for backward compatibility. + if (field_value == "commonjs" || field_value == "module") { + package_config.type = field_value; + } + } + } + // package_config could be quite large, so we should move it instead of + // copying it. + auto cached = binding_data->package_configs_.insert( + {std::string(path), std::move(package_config)}); + + return &cached.first->second; +} + +void BindingData::ReadPackageJSON(const FunctionCallbackInfo& args) { + CHECK_GE(args.Length(), 1); // path, [is_esm, base, specifier] + CHECK(args[0]->IsString()); // path + + Realm* realm = Realm::GetCurrent(args); + auto isolate = realm->isolate(); + + Utf8Value path(isolate, args[0]); + bool is_esm = args[1]->IsTrue(); + auto error_context = ErrorContext(); + if (is_esm) { + CHECK(args[2]->IsUndefined() || args[2]->IsString()); // base + CHECK(args[3]->IsString()); // specifier + + if (args[2]->IsString()) { + Utf8Value base_value(isolate, args[2]); + error_context.base = base_value.ToString(); + } + Utf8Value specifier(isolate, args[3]); + error_context.specifier = specifier.ToString(); + } + + THROW_IF_INSUFFICIENT_PERMISSIONS( + realm->env(), + permission::PermissionScope::kFileSystemRead, + path.ToStringView()); + + auto package_json = + GetPackageJSON(realm, path.ToString(), is_esm ? &error_context : nullptr); + if (package_json == nullptr) { + return; + } + + args.GetReturnValue().Set(package_json->Serialize(realm)); +} + +const BindingData::PackageConfig* BindingData::TraverseParent( + Realm* realm, std::string_view check_path) { + auto env = realm->env(); + auto root_separator_index = check_path.find_first_of(kPathSeparator); + size_t separator_index = 0; + const bool is_permissions_enabled = env->permission()->enabled(); + + do { + separator_index = check_path.find_last_of(kPathSeparator); + check_path = check_path.substr(0, separator_index); + + // We don't need to try "/" + if (check_path.empty()) { + break; + } + + // Stop the search when the process doesn't have permissions + // to walk upwards + if (UNLIKELY(is_permissions_enabled && + !env->permission()->is_granted( + permission::PermissionScope::kFileSystemRead, + std::string(check_path) + kPathSeparator))) { + return nullptr; + } + + // Check if the path ends with `/node_modules` + if (check_path.size() >= kNodeModules.size() && + std::equal(check_path.end() - kNodeModules.size(), + check_path.end(), + kNodeModules.begin())) { + return nullptr; + } + + auto package_json = GetPackageJSON( + realm, + std::string(check_path) + kPathSeparator + "package.json", + nullptr); + if (package_json != nullptr) { + return package_json; + } + } while (separator_index > root_separator_index); + + return nullptr; +} + +void BindingData::GetNearestParentPackageJSON( + const v8::FunctionCallbackInfo& args) { + CHECK_GE(args.Length(), 1); + CHECK(args[0]->IsString()); + + Realm* realm = Realm::GetCurrent(args); + Utf8Value path_value(realm->isolate(), args[0]); + auto package_json = TraverseParent(realm, path_value.ToStringView()); + + if (package_json != nullptr) { + args.GetReturnValue().Set(package_json->Serialize(realm)); + } +} + +void BindingData::GetNearestParentPackageJSONType( + const FunctionCallbackInfo& args) { + CHECK_GE(args.Length(), 1); + CHECK(args[0]->IsString()); + + Realm* realm = Realm::GetCurrent(args); + Utf8Value path(realm->isolate(), args[0]); + auto package_json = TraverseParent(realm, path.ToStringView()); + + if (package_json == nullptr) { + return; + } + + Local values[3] = { + ToV8Value(realm->context(), package_json->type).ToLocalChecked(), + ToV8Value(realm->context(), package_json->file_path).ToLocalChecked(), + ToV8Value(realm->context(), package_json->raw_json).ToLocalChecked()}; + args.GetReturnValue().Set(Array::New(realm->isolate(), values, 3)); +} + +void BindingData::GetPackageScopeConfig( + const FunctionCallbackInfo& args) { + CHECK_GE(args.Length(), 1); + CHECK(args[0]->IsString()); + + Realm* realm = Realm::GetCurrent(args); + Utf8Value resolved(realm->isolate(), args[0]); + auto package_json_url_base = ada::parse(resolved.ToStringView()); + if (!package_json_url_base) { + url::ThrowInvalidURL(realm->env(), resolved.ToStringView(), std::nullopt); + return; + } + auto package_json_url = + ada::parse("./package.json", &package_json_url_base.value()); + if (!package_json_url) { + url::ThrowInvalidURL(realm->env(), "./package.json", resolved.ToString()); + return; + } + + std::string_view node_modules_package_path = "node_modules/package.json"; + auto error_context = ErrorContext(); + error_context.is_esm = true; + + // TODO(@anonrig): Rewrite this function and avoid calling URL parser. + while (true) { + auto pathname = package_json_url->get_pathname(); + if (pathname.size() >= node_modules_package_path.size() && + pathname.compare(pathname.size() - node_modules_package_path.size(), + node_modules_package_path.size(), + node_modules_package_path) == 0) { + break; + } + + auto file_url = url::FileURLToPath(realm->env(), *package_json_url); + CHECK(file_url); + error_context.specifier = resolved.ToString(); + auto package_json = GetPackageJSON(realm, *file_url, &error_context); + if (package_json != nullptr) { + return args.GetReturnValue().Set(package_json->Serialize(realm)); + } + + auto last_href = std::string(package_json_url->get_href()); + auto last_pathname = std::string(package_json_url->get_pathname()); + package_json_url = ada::parse("../package.json", &package_json_url.value()); + if (!package_json_url) { + url::ThrowInvalidURL(realm->env(), "../package.json", last_href); + return; + } + + // Terminates at root where ../package.json equals ../../package.json + // (can't just check "/package.json" for Windows support). + if (package_json_url->get_pathname() == last_pathname) { + break; + } + } + + auto package_json_url_as_path = + url::FileURLToPath(realm->env(), *package_json_url); + CHECK(package_json_url_as_path); + return args.GetReturnValue().Set( + String::NewFromUtf8(realm->isolate(), + package_json_url_as_path->c_str(), + NewStringType::kNormal, + package_json_url_as_path->size()) + .ToLocalChecked()); +} + +void BindingData::CreatePerIsolateProperties(IsolateData* isolate_data, + Local target) { + Isolate* isolate = isolate_data->isolate(); + SetMethod(isolate, target, "readPackageJSON", ReadPackageJSON); + SetMethod(isolate, + target, + "getNearestParentPackageJSONType", + GetNearestParentPackageJSONType); + SetMethod(isolate, + target, + "getNearestParentPackageJSON", + GetNearestParentPackageJSON); + SetMethod(isolate, target, "getPackageScopeConfig", GetPackageScopeConfig); +} + +void BindingData::CreatePerContextProperties(Local target, + Local unused, + Local context, + void* priv) { + Realm* realm = Realm::GetCurrent(context); + realm->AddBindingData(target); +} + +void BindingData::RegisterExternalReferences( + ExternalReferenceRegistry* registry) { + registry->Register(ReadPackageJSON); + registry->Register(GetNearestParentPackageJSONType); + registry->Register(GetNearestParentPackageJSON); + registry->Register(GetPackageScopeConfig); +} + +} // namespace modules +} // namespace node + +NODE_BINDING_CONTEXT_AWARE_INTERNAL( + modules, node::modules::BindingData::CreatePerContextProperties) +NODE_BINDING_PER_ISOLATE_INIT( + modules, node::modules::BindingData::CreatePerIsolateProperties) +NODE_BINDING_EXTERNAL_REFERENCE( + modules, node::modules::BindingData::RegisterExternalReferences) diff --git a/src/node_modules.h b/src/node_modules.h new file mode 100644 index 00000000000000..27b2bcb11f56b0 --- /dev/null +++ b/src/node_modules.h @@ -0,0 +1,89 @@ +#ifndef SRC_NODE_MODULES_H_ +#define SRC_NODE_MODULES_H_ + +#include "v8-function-callback.h" +#if defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS + +#include "node.h" +#include "node_snapshotable.h" +#include "simdjson.h" +#include "util.h" +#include "v8-fast-api-calls.h" +#include "v8.h" + +#include +#include +#include +#include + +namespace node { +class ExternalReferenceRegistry; + +namespace modules { + +class BindingData : public SnapshotableObject { + public: + using InternalFieldInfo = InternalFieldInfoBase; + + struct PackageConfig { + std::string file_path; + std::optional name; + std::optional main; + std::string type = "none"; + std::optional exports; + std::optional imports; + std::string raw_json; + + v8::Local Serialize(Realm* realm) const; + }; + + struct ErrorContext { + std::optional base; + std::string specifier; + bool is_esm; + }; + + BindingData(Realm* realm, + v8::Local obj, + InternalFieldInfo* info = nullptr); + SERIALIZABLE_OBJECT_METHODS() + SET_BINDING_ID(modules_binding_data) + + void MemoryInfo(MemoryTracker* tracker) const override; + SET_SELF_SIZE(BindingData) + SET_MEMORY_INFO_NAME(BindingData) + + static void ReadPackageJSON(const v8::FunctionCallbackInfo& args); + static void GetNearestParentPackageJSON( + const v8::FunctionCallbackInfo& args); + static void GetNearestParentPackageJSONType( + const v8::FunctionCallbackInfo& args); + static void GetPackageScopeConfig( + const v8::FunctionCallbackInfo& args); + + static void CreatePerIsolateProperties(IsolateData* isolate_data, + v8::Local ctor); + static void CreatePerContextProperties(v8::Local target, + v8::Local unused, + v8::Local context, + void* priv); + static void RegisterExternalReferences(ExternalReferenceRegistry* registry); + + private: + std::unordered_map package_configs_; + simdjson::ondemand::parser json_parser; + // returns null on error + static const PackageConfig* GetPackageJSON( + Realm* realm, + std::string_view path, + ErrorContext* error_context = nullptr); + static const PackageConfig* TraverseParent(Realm* realm, + std::string_view check_path); +}; + +} // namespace modules +} // namespace node + +#endif // defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS + +#endif // SRC_NODE_MODULES_H_ diff --git a/src/node_snapshotable.cc b/src/node_snapshotable.cc index 55a65deeea6e74..683f36839e0de2 100644 --- a/src/node_snapshotable.cc +++ b/src/node_snapshotable.cc @@ -21,6 +21,7 @@ #include "node_internals.h" #include "node_main_instance.h" #include "node_metadata.h" +#include "node_modules.h" #include "node_process.h" #include "node_snapshot_builder.h" #include "node_url.h" diff --git a/src/node_url.cc b/src/node_url.cc index 94510aa1904a00..95d15c78407359 100644 --- a/src/node_url.cc +++ b/src/node_url.cc @@ -229,35 +229,6 @@ void BindingData::Format(const FunctionCallbackInfo& args) { .ToLocalChecked()); } -void BindingData::ThrowInvalidURL(node::Environment* env, - std::string_view input, - std::optional base) { - Local err = ERR_INVALID_URL(env->isolate(), "Invalid URL"); - DCHECK(err->IsObject()); - - auto err_object = err.As(); - - USE(err_object->Set(env->context(), - env->input_string(), - v8::String::NewFromUtf8(env->isolate(), - input.data(), - v8::NewStringType::kNormal, - input.size()) - .ToLocalChecked())); - - if (base.has_value()) { - USE(err_object->Set(env->context(), - env->base_string(), - v8::String::NewFromUtf8(env->isolate(), - base.value().c_str(), - v8::NewStringType::kNormal, - base.value().size()) - .ToLocalChecked())); - } - - env->isolate()->ThrowException(err); -} - void BindingData::Parse(const FunctionCallbackInfo& args) { CHECK_GE(args.Length(), 1); CHECK(args[0]->IsString()); // input @@ -419,6 +390,35 @@ void BindingData::RegisterExternalReferences( } } +void ThrowInvalidURL(node::Environment* env, + std::string_view input, + std::optional base) { + Local err = ERR_INVALID_URL(env->isolate(), "Invalid URL"); + DCHECK(err->IsObject()); + + auto err_object = err.As(); + + USE(err_object->Set(env->context(), + env->input_string(), + v8::String::NewFromUtf8(env->isolate(), + input.data(), + v8::NewStringType::kNormal, + input.size()) + .ToLocalChecked())); + + if (base.has_value()) { + USE(err_object->Set(env->context(), + env->base_string(), + v8::String::NewFromUtf8(env->isolate(), + base.value().c_str(), + v8::NewStringType::kNormal, + base.value().size()) + .ToLocalChecked())); + } + + env->isolate()->ThrowException(err); +} + std::string FromFilePath(std::string_view file_path) { // Avoid unnecessary allocations. size_t pos = file_path.empty() ? std::string_view::npos : file_path.find('%'); diff --git a/src/node_url.h b/src/node_url.h index c106e8245284da..3c77b538b16f8f 100644 --- a/src/node_url.h +++ b/src/node_url.h @@ -77,11 +77,11 @@ class BindingData : public SnapshotableObject { const ada::scheme::type type); static v8::CFunction fast_can_parse_methods_[]; - static void ThrowInvalidURL(Environment* env, - std::string_view input, - std::optional base); }; +void ThrowInvalidURL(Environment* env, + std::string_view input, + std::optional base); std::string FromFilePath(std::string_view file_path); std::optional FileURLToPath(Environment* env, const ada::url_aggregator& file_url); diff --git a/test/es-module/test-esm-invalid-pjson.js b/test/es-module/test-esm-invalid-pjson.js index 9b49f6f1357685..52cef9ea98b40a 100644 --- a/test/es-module/test-esm-invalid-pjson.js +++ b/test/es-module/test-esm-invalid-pjson.js @@ -1,6 +1,6 @@ 'use strict'; -const { checkoutEOL, spawnPromisified } = require('../common'); +const { spawnPromisified } = require('../common'); const fixtures = require('../common/fixtures.js'); const assert = require('node:assert'); const { execPath } = require('node:process'); @@ -14,12 +14,10 @@ describe('ESM: Package.json', { concurrency: true }, () => { const { code, signal, stderr } = await spawnPromisified(execPath, [entry]); + assert.ok(stderr.includes('code: \'ERR_INVALID_PACKAGE_CONFIG\''), stderr); assert.ok( stderr.includes( - `[ERR_INVALID_PACKAGE_CONFIG]: Invalid package config ${invalidJson} ` + - `while importing "invalid-pjson" from ${entry}. ` + - "Expected ':' after property name in JSON at position " + - `${12 + checkoutEOL.length * 2}` + `Invalid package config ${invalidJson} while importing "invalid-pjson" from ${entry}.` ), stderr ); diff --git a/test/fixtures/node_modules/pkgexports-number/package.json b/test/fixtures/node_modules/pkgexports-number/package.json index 315f39a66e32a6..c5807f588ce8f7 100644 --- a/test/fixtures/node_modules/pkgexports-number/package.json +++ b/test/fixtures/node_modules/pkgexports-number/package.json @@ -1,3 +1,3 @@ { - "exports": 42 + "exports": {} } diff --git a/test/parallel/test-bootstrap-modules.js b/test/parallel/test-bootstrap-modules.js index 7b9a05711e170b..6327fbeb2e7e1b 100644 --- a/test/parallel/test-bootstrap-modules.js +++ b/test/parallel/test-bootstrap-modules.js @@ -24,6 +24,7 @@ const expected = {}; expected.beforePreExec = new Set([ 'Internal Binding builtins', 'Internal Binding encoding_binding', + 'Internal Binding modules', 'Internal Binding errors', 'Internal Binding util', 'NativeModule internal/errors', diff --git a/test/parallel/test-module-binding.js b/test/parallel/test-module-binding.js deleted file mode 100644 index 47170785434099..00000000000000 --- a/test/parallel/test-module-binding.js +++ /dev/null @@ -1,29 +0,0 @@ -// Flags: --expose-internals -'use strict'; -require('../common'); -const fixtures = require('../common/fixtures'); -const { internalBinding } = require('internal/test/binding'); -const { filterOwnProperties } = require('internal/util'); -const { internalModuleReadJSON } = internalBinding('fs'); -const { readFileSync } = require('fs'); -const { strictEqual, deepStrictEqual } = require('assert'); - -{ - strictEqual(internalModuleReadJSON('nosuchfile')[0], undefined); -} -{ - strictEqual(internalModuleReadJSON(fixtures.path('empty.txt'))[0], ''); -} -{ - strictEqual(internalModuleReadJSON(fixtures.path('empty-with-bom.txt'))[0], ''); -} -{ - const filename = fixtures.path('require-bin/package.json'); - const returnValue = JSON.parse(internalModuleReadJSON(filename)[0]); - const file = JSON.parse(readFileSync(filename, 'utf-8')); - const expectedValue = filterOwnProperties(file, ['name', 'main', 'exports', 'imports', 'type']); - deepStrictEqual({ - __proto__: null, - ...returnValue, - }, expectedValue); -} diff --git a/typings/globals.d.ts b/typings/globals.d.ts index d72bf937bb75c9..39df64f7ec5bf4 100644 --- a/typings/globals.d.ts +++ b/typings/globals.d.ts @@ -14,6 +14,7 @@ import {TypesBinding} from "./internalBinding/types"; import {URLBinding} from "./internalBinding/url"; import {UtilBinding} from "./internalBinding/util"; import {WorkerBinding} from "./internalBinding/worker"; +import {ModulesBinding} from "./internalBinding/modules"; declare type TypedArray = | Uint8Array @@ -36,6 +37,7 @@ interface InternalBindingMap { fs: FsBinding; http_parser: HttpParserBinding; messaging: MessagingBinding; + modules: ModulesBinding; options: OptionsBinding; os: OSBinding; serdes: SerdesBinding; diff --git a/typings/internalBinding/fs.d.ts b/typings/internalBinding/fs.d.ts index c3fabe7b11e9c8..5a4741e99fa727 100644 --- a/typings/internalBinding/fs.d.ts +++ b/typings/internalBinding/fs.d.ts @@ -111,7 +111,6 @@ declare namespace InternalFSBinding { function futimes(fd: number, atime: number, mtime: number): void; function futimes(fd: number, atime: number, mtime: number, usePromises: typeof kUsePromises): Promise; - function internalModuleReadJSON(path: string): [] | [string, boolean]; function internalModuleStat(path: string): number; function lchown(path: string, uid: number, gid: number, req: FSReqCallback): void; diff --git a/typings/internalBinding/modules.d.ts b/typings/internalBinding/modules.d.ts new file mode 100644 index 00000000000000..ddbb3d02704908 --- /dev/null +++ b/typings/internalBinding/modules.d.ts @@ -0,0 +1,29 @@ +export type PackageType = 'commonjs' | 'module' | 'none' +export type PackageConfig = { + pjsonPath: string + exists: boolean + name?: string + main?: any + type: PackageType + exports?: string | string[] | Record + imports?: string | string[] | Record +} +export type SerializedPackageConfig = [ + PackageConfig['name'], + PackageConfig['main'], + PackageConfig['type'], + string | undefined, // exports + string | undefined, // imports + string | undefined, // raw json available for experimental policy +] + +export interface ModulesBinding { + readPackageJSON(path: string): SerializedPackageConfig | undefined; + getNearestParentPackageJSON(path: string): PackageConfig | undefined + getNearestParentPackageJSONType(path: string): [ + PackageConfig['type'], + string, // package.json path + string, // raw content + ] + getPackageScopeConfig(path: string): SerializedPackageConfig | undefined +}