UNPKG

nx

Version:

The core Nx plugin contains the core functionality of Nx like the project graph, nx commands and task orchestration.

306 lines (305 loc) • 14.6 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.TargetProjectLocator = void 0; exports.isBuiltinModuleImport = isBuiltinModuleImport; const node_module_1 = require("node:module"); const node_path_1 = require("node:path"); const semver_1 = require("semver"); const find_project_for_path_1 = require("../../../../project-graph/utils/find-project-for-path"); const fileutils_1 = require("../../../../utils/fileutils"); const get_package_name_from_import_path_1 = require("../../../../utils/get-package-name-from-import-path"); const workspace_root_1 = require("../../../../utils/workspace-root"); const packages_1 = require("../../utils/packages"); const resolve_relative_to_dir_1 = require("../../utils/resolve-relative-to-dir"); const typescript_1 = require("../../utils/typescript"); /** * Use a shared cache to avoid repeated npm package resolution work within the TargetProjectLocator. */ const defaultNpmResolutionCache = new Map(); const experimentalNodeModules = new Set(['node:sqlite']); function isBuiltinModuleImport(importExpr) { const packageName = (0, get_package_name_from_import_path_1.getPackageNameFromImportPath)(importExpr); return (0, node_module_1.isBuiltin)(packageName) || experimentalNodeModules.has(packageName); } class TargetProjectLocator { constructor(nodes, externalNodes = {}, npmResolutionCache = defaultNpmResolutionCache) { this.nodes = nodes; this.externalNodes = externalNodes; this.npmResolutionCache = npmResolutionCache; this.projectRootMappings = (0, find_project_for_path_1.createProjectRootMappings)(this.nodes); this.tsConfig = this.getRootTsConfig(); this.paths = this.tsConfig.config?.compilerOptions?.paths; this.typescriptResolutionCache = new Map(); /** * Only the npm external nodes should be included. * * Unlike the raw externalNodes, ensure that there is always copy of the node where the version * is set in the key for optimal lookup. */ this.npmProjects = Object.values(this.externalNodes).reduce((acc, node) => { if (node.type === 'npm') { const keyWithVersion = `npm:${node.data.packageName}@${node.data.version}`; if (!acc[node.name]) { acc[node.name] = node; } // The node.name may have already contained the version if (!acc[keyWithVersion]) { acc[keyWithVersion] = node; } } return acc; }, {}); } /** * Resolve any workspace or external project that matches the given import expression, * originating from the given filePath. * * @param importExpr * @param filePath */ findProjectFromImport(importExpr, filePath) { if ((0, fileutils_1.isRelativePath)(importExpr)) { const resolvedModule = node_path_1.posix.join((0, node_path_1.dirname)(filePath), importExpr); return this.findProjectOfResolvedModule(resolvedModule); } // find project using tsconfig paths const results = this.findPaths(importExpr); if (results) { const [path, paths] = results; for (let p of paths) { const r = p.endsWith('/*') ? (0, node_path_1.join)((0, node_path_1.dirname)(p), (0, node_path_1.relative)(path.replace(/\*$/, ''), importExpr)) : p; const maybeResolvedProject = this.findProjectOfResolvedModule(r); if (maybeResolvedProject) { return maybeResolvedProject; } } } if (isBuiltinModuleImport(importExpr)) { this.npmResolutionCache.set(importExpr, null); return null; } // try to find npm package before using expensive typescript resolution const externalProject = this.findNpmProjectFromImport(importExpr, filePath); if (externalProject) { return externalProject; } if (this.tsConfig.config) { // TODO(meeroslav): this block is probably obsolete // and existed only because of the incomplete `paths` matching // if import cannot be matched using tsconfig `paths` the compilation would fail anyway const resolvedProject = this.resolveImportWithTypescript(importExpr, filePath); if (resolvedProject) { return resolvedProject; } } try { const resolvedModule = this.resolveImportWithRequire(importExpr, filePath); return this.findProjectOfResolvedModule(resolvedModule); } catch { } // fall back to see if it's a locally linked workspace project where the // output might not exist yet const localProject = this.findImportInWorkspaceProjects(importExpr); if (localProject) { return localProject; } // nothing found, cache for later this.npmResolutionCache.set(importExpr, null); return null; } /** * Resolve any external project that matches the given import expression, * relative to the given file path. * * @param importExpr * @param projectRoot */ findNpmProjectFromImport(importExpr, fromFilePath) { const packageName = (0, get_package_name_from_import_path_1.getPackageNameFromImportPath)(importExpr); let fullFilePath = fromFilePath; let workspaceRelativeFilePath = fromFilePath; if (fromFilePath.startsWith(workspace_root_1.workspaceRoot)) { workspaceRelativeFilePath = fromFilePath.replace(workspace_root_1.workspaceRoot, ''); } else { fullFilePath = (0, node_path_1.join)(workspace_root_1.workspaceRoot, fromFilePath); } const fullDirPath = (0, node_path_1.dirname)(fullFilePath); const workspaceRelativeDirPath = (0, node_path_1.dirname)(workspaceRelativeFilePath); const npmImportForProject = `${packageName}__${workspaceRelativeDirPath}`; if (this.npmResolutionCache.has(npmImportForProject)) { return this.npmResolutionCache.get(npmImportForProject); } try { // package.json refers to an external package, we do not match against the version found in there, we instead try and resolve the relevant package how node would const externalPackageJson = this.readPackageJson(packageName, fullDirPath); // The external package.json path might be not be resolvable, e.g. if a reference has been added to a project package.json, but the install command has not been run yet. if (!externalPackageJson) { // Try and fall back to resolving an external node from the graph by name const externalNode = this.npmProjects[`npm:${packageName}`]; const externalNodeName = externalNode?.name || null; this.npmResolutionCache.set(npmImportForProject, externalNodeName); return externalNodeName; } const version = (0, semver_1.clean)(externalPackageJson.version); let matchingExternalNode = this.npmProjects[`npm:${externalPackageJson.name}@${version}`]; if (!matchingExternalNode) { // check if it's a package alias, where the resolved package key is used as the version const aliasNpmProjectKey = `npm:${packageName}@npm:${externalPackageJson.name}@${version}`; matchingExternalNode = this.npmProjects[aliasNpmProjectKey]; } if (!matchingExternalNode) { // Fallback to package name as key. This can happen if the version in project graph is not the same as in the resolved package.json. // e.g. Version in project graph is a git remote, but the resolved version is semver. matchingExternalNode = this.npmProjects[`npm:${externalPackageJson.name}`]; } if (!matchingExternalNode) { return null; } this.npmResolutionCache.set(npmImportForProject, matchingExternalNode.name); return matchingExternalNode.name; } catch (e) { if (process.env.NX_VERBOSE_LOGGING === 'true') { console.error(e); } return null; } } /** * Return file paths matching the import relative to the repo root * @param normalizedImportExpr * @returns */ findPaths(normalizedImportExpr) { if (!this.paths) { return undefined; } if (this.paths[normalizedImportExpr]) { return [normalizedImportExpr, this.paths[normalizedImportExpr]]; } const wildcardPath = Object.keys(this.paths).find((path) => path.endsWith('/*') && (normalizedImportExpr.startsWith(path.replace(/\*$/, '')) || normalizedImportExpr === path.replace(/\/\*$/, ''))); if (wildcardPath) { return [wildcardPath, this.paths[wildcardPath]]; } return undefined; } findImportInWorkspaceProjects(importPath) { this.packagesMetadata ??= (0, packages_1.getWorkspacePackagesMetadata)(this.nodes); if (this.packagesMetadata.entryPointsToProjectMap[importPath]) { return this.packagesMetadata.entryPointsToProjectMap[importPath].name; } const project = (0, packages_1.matchImportToWildcardEntryPointsToProjectMap)(this.packagesMetadata.wildcardEntryPointsToProjectMap, importPath); return project?.name; } findDependencyInWorkspaceProjects(dep) { this.packagesMetadata ??= (0, packages_1.getWorkspacePackagesMetadata)(this.nodes); return this.packagesMetadata.packageToProjectMap[dep]?.name; } resolveImportWithTypescript(normalizedImportExpr, filePath) { let resolvedModule; if (this.typescriptResolutionCache.has(normalizedImportExpr)) { resolvedModule = this.typescriptResolutionCache.get(normalizedImportExpr); } else { resolvedModule = (0, typescript_1.resolveModuleByImport)(normalizedImportExpr, filePath, this.tsConfig.absolutePath); this.typescriptResolutionCache.set(normalizedImportExpr, resolvedModule ? resolvedModule : null); } // TODO: vsavkin temporary workaround. Remove it once we reworking handling of npm packages. if (resolvedModule && resolvedModule.indexOf('node_modules/') === -1) { const resolvedProject = this.findProjectOfResolvedModule(resolvedModule); if (resolvedProject) { return resolvedProject; } } return; } resolveImportWithRequire(normalizedImportExpr, filePath) { return node_path_1.posix.relative(workspace_root_1.workspaceRoot, require.resolve(normalizedImportExpr, { paths: [(0, node_path_1.dirname)(filePath)], })); } findProjectOfResolvedModule(resolvedModule) { if (resolvedModule.startsWith('node_modules/') || resolvedModule.includes('/node_modules/')) { return undefined; } const normalizedResolvedModule = resolvedModule.startsWith('./') ? resolvedModule.substring(2) : resolvedModule; const importedProject = this.findMatchingProjectFiles(normalizedResolvedModule); return importedProject ? importedProject.name : void 0; } getAbsolutePath(path) { return (0, node_path_1.join)(workspace_root_1.workspaceRoot, path); } getRootTsConfig() { const path = (0, typescript_1.getRootTsConfigFileName)(); if (!path) { return { path: null, absolutePath: null, config: null, }; } const absolutePath = this.getAbsolutePath(path); return { absolutePath, path, config: (0, fileutils_1.readJsonFile)(absolutePath), }; } findMatchingProjectFiles(file) { const project = (0, find_project_for_path_1.findProjectForPath)(file, this.projectRootMappings); return this.nodes[project]; } /** * In many cases the package.json will be directly resolvable, so we try that first. * If, however, package exports are used and the package.json is not defined, we will * need to resolve the main entry point of the package and traverse upwards to find the * package.json. * * In some cases, such as when multiple module formats are published, the resolved package.json * might only contain the "type" field - no "name" or "version", so in such cases we keep traversing * until we find a package.json that contains the "name" and "version" fields. */ readPackageJson(packageName, relativeToDir) { // The package.json is directly resolvable const packageJsonPath = (0, resolve_relative_to_dir_1.resolveRelativeToDir)((0, node_path_1.join)(packageName, 'package.json'), relativeToDir); if (packageJsonPath) { const parsedPackageJson = (0, fileutils_1.readJsonFile)(packageJsonPath); if (parsedPackageJson.name && parsedPackageJson.version) { return parsedPackageJson; } } try { // Resolve the main entry point of the package const pathOfFileInPackage = packageJsonPath ?? (0, resolve_relative_to_dir_1.resolveRelativeToDir)(packageName, relativeToDir); let dir = (0, node_path_1.dirname)(pathOfFileInPackage); while (dir !== (0, node_path_1.dirname)(dir)) { const packageJsonPath = (0, node_path_1.join)(dir, 'package.json'); try { const parsedPackageJson = (0, fileutils_1.readJsonFile)(packageJsonPath); // Ensure the package.json contains the "name" and "version" fields if (parsedPackageJson.name && parsedPackageJson.version) { return parsedPackageJson; } } catch { // Package.json doesn't exist, keep traversing } dir = (0, node_path_1.dirname)(dir); } return null; } catch { return null; } } } exports.TargetProjectLocator = TargetProjectLocator;