'use strict'; const { ArrayIsArray, JSONParse, ObjectDefineProperty, RegExpPrototypeExec, StringPrototypeIndexOf, StringPrototypeSlice, } = primordials; 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'); /** * @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 { data: { __proto__: null, type: 'none', // Ignore unknown types for forwards compatibility }, exists: false, path, }; } const { 0: name, 1: main, 2: type, 3: plainImports, 4: plainExports, 5: optionalFilePath, } = contents; const pjsonPath = optionalFilePath ?? path; return { 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, 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 * @param {{ * base?: URL | string, * specifier?: URL | string, * isESM?: boolean, * }} options * @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 // non-null attributes are converted to strings. const parsed = modulesBinding.readPackageJSON( jsonPath, isESM, base == null ? undefined : `${base}`, specifier == null ? undefined : `${specifier}`, ); const result = deserializePackageJSON(jsonPath, parsed); return { __proto__: null, ...result.data, exists: result.exists, pjsonPath: result.path, }; } /** * @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(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 | DeserializedPackageConfig} */ function getNearestParentPackageJSON(checkPath) { const result = modulesBinding.getNearestParentPackageJSON(checkPath); if (result === undefined) { return undefined; } return deserializePackageJSON(checkPath, result); } /** * Returns the package configuration for the given resolved URL. * @param {URL | string} resolved - The resolved URL. * @returns {import('typings/internalBinding/modules').PackageConfig} - The package configuration. */ function getPackageScopeConfig(resolved) { const result = modulesBinding.getPackageScopeConfig(`${resolved}`); if (ArrayIsArray(result)) { const { data, exists, path } = deserializePackageJSON(`${resolved}`, result); return { __proto__: null, ...data, exists, pjsonPath: path, }; } // This means that the response is a string // and it is the path to the package.json file return { __proto__: null, pjsonPath: result, exists: false, type: 'none', }; } /** * 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; } 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/${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); } /** @type {import('./esm/resolve.js').defaultResolve} */ let defaultResolve; /** * @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 = base; if (!isURL(base)) { validateString(base, 'base'); parentURL = path.isAbsolute(base) ? pathToFileURL(base) : new URL(base); } if (specifier && specifier[0] !== '.' && specifier[0] !== '/' && !URLCanParse(specifier)) { // If `specifier` is a bare specifier. const { packageJSONPath } = getPackageJSONURL(specifier, parentURL); return packageJSONPath; } let resolvedTarget; defaultResolve ??= require('internal/modules/esm/resolve').defaultResolve; try { // TODO(@JakobJingleheimer): Detect whether findPackageJSON is being used within a loader // (possibly piggyback on `allowImportMetaResolve`) // - When inside, use the default resolve // - (I think it's impossible to use the chain because of re-entry & a deadlock from atomics). // - When outside, use cascadedLoader.resolveSync (not implemented yet, but the pieces exist). resolvedTarget = defaultResolve(specifier, { parentURL: `${parentURL}` }).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, };