UNPKG

@ui5/project

Version:
185 lines (169 loc) 5.84 kB
import path from "node:path"; import {readPackageUp} from "read-package-up"; import {readPackage} from "read-pkg"; import {promisify} from "node:util"; import fs from "graceful-fs"; const realpath = promisify(fs.realpath); import resolve from "resolve"; const resolveModulePath = promisify(resolve); import {getLogger} from "@ui5/logger"; const log = getLogger("graph:providers:NodePackageDependencies"); // Packages to consider: // * https://github.com/npm/read-package-json-fast // * https://github.com/npm/name-from-folder ? /** * @public * @class * @alias @ui5/project/graph/providers/NodePackageDependencies */ class NodePackageDependencies { /** * Generates a project graph from npm modules * * @public * @param {object} options * @param {string} options.cwd Directory to start searching for the root module * @param {object} [options.rootConfiguration] * Configuration object to use for the root module instead of reading from a configuration file * @param {string} [options.rootConfigPath] * Configuration file to use for the root module instead the default ui5.yaml */ constructor({cwd, rootConfiguration, rootConfigPath}) { this._cwd = cwd; this._rootConfiguration = rootConfiguration; this._rootConfigPath = rootConfigPath; } async getRootNode() { const rootPkg = await readPackageUp({ cwd: this._cwd, normalize: false }); if (!rootPkg || !rootPkg.packageJson) { throw new Error( `Failed to locate package.json for directory ${path.resolve(this._cwd)}`); } const modulePath = path.dirname(rootPkg.path); if (!rootPkg.packageJson.name) { throw new Error(`Missing or empty 'name' attribute in package.json at ${modulePath}`); } if (!rootPkg.packageJson.version) { throw new Error(`Missing or empty 'version' attribute in package.json at ${modulePath}`); } return { id: rootPkg.packageJson.name, version: rootPkg.packageJson.version, path: modulePath, configuration: this._rootConfiguration, configPath: this._rootConfigPath, _dependencies: await this._getDependencies(modulePath, rootPkg.packageJson, true) }; } async getDependencies(node, workspace) { log.verbose(`Resolving dependencies of ${node.id}...`); if (!node._dependencies) { return []; } return Promise.all(node._dependencies.map(async ({name, optional}) => { const modulePath = await this._resolveModulePath(node.path, name, workspace); return this._getNode(modulePath, optional, name); })); } async _resolveModulePath(baseDir, moduleName, workspace) { log.verbose(`Resolving module path for '${moduleName}'...`); if (workspace) { // Check whether node can be resolved via the provided Workspace instance // If yes, replace the node from NodeProvider with the one from Workspace const workspaceNode = await workspace.getModuleByNodeId(moduleName); if (workspaceNode) { log.info(`Resolved module ${moduleName} via ${workspace.getName()} workspace ` + `to version ${workspaceNode.getVersion()}`); log.verbose(` Resolved module ${moduleName} to path ${workspaceNode.getPath()}`); return workspaceNode.getPath(); } } try { let packageJsonPath = await resolveModulePath(moduleName + "/package.json", { basedir: baseDir, preserveSymlinks: false }); packageJsonPath = await realpath(packageJsonPath); const modulePath = path.dirname(packageJsonPath); log.verbose(`Resolved module ${moduleName} to path ${modulePath}`); return modulePath; } catch (err) { throw new Error( `Unable to locate module ${moduleName} via resolve logic: ${err.message}`); } } /** * Resolves a Node module by reading its package.json * * @param {string} modulePath Path to the module. * @param {boolean} optional Whether this dependency is optional. * @param {string} [nameAlias] The name of the module. It's usually the same as the name definfed * in package.json and could easily be skipped. Useful when defining dependency as an alias: * {@link https://github.com/npm/rfcs/blob/main/implemented/0001-package-aliases.md} * @returns {Promise<object>} */ async _getNode(modulePath, optional, nameAlias) { log.verbose(`Reading package.json in directory ${modulePath}...`); const packageJson = await readPackage({ cwd: modulePath, normalize: false }); return { id: nameAlias || packageJson.name, version: packageJson.version, path: modulePath, optional, _dependencies: await this._getDependencies(modulePath, packageJson) }; } async _getDependencies(modulePath, packageJson, rootModule = false) { const dependencies = []; if (packageJson.dependencies) { Object.keys(packageJson.dependencies).forEach((depName) => { dependencies.push({ name: depName, optional: false }); }); } if (rootModule && packageJson.devDependencies) { Object.keys(packageJson.devDependencies).forEach((depName) => { dependencies.push({ name: depName, optional: false }); }); } if (!rootModule && packageJson.devDependencies) { await Promise.all(Object.keys(packageJson.devDependencies).map(async (depName) => { try { await this._resolveModulePath(modulePath, depName); dependencies.push({ name: depName, optional: true }); } catch { // Ignore error since it's a development dependency of a non-root module } })); } if (packageJson.optionalDependencies) { await Promise.all(Object.keys(packageJson.optionalDependencies).map(async (depName) => { try { await this._resolveModulePath(modulePath, depName); dependencies.push({ name: depName, optional: false }); } catch { // Ignore error since it's an optional dependency } })); } return dependencies; } } export default NodePackageDependencies;