UNPKG

nx

Version:

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

404 lines (403 loc) • 18.8 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; }, {}); if (this.tsConfig.config?.compilerOptions?.paths) { this.parsePaths(this.tsConfig.config.compilerOptions.paths); } } /** * 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.findMatchingPaths(importExpr); if (results) { const [path, paths] = results; const matchedStar = typeof path === 'string' ? undefined : importExpr.substring(path.prefix.length, importExpr.length - path.suffix.length); for (let p of paths) { const path = matchedStar ? p.replace('*', matchedStar) : p; const maybeResolvedProject = this.findProjectOfResolvedModule(path); 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: this can be removed once we rework resolveImportWithRequire below // to properly handle ESM (exports, imports, conditions) 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 * @deprecated Use `findMatchingPaths` instead. It will be removed in Nx v22. */ 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; } findMatchingPaths(importExpr) { if (!this.parsedPathPatterns) { return undefined; } const { matchableStrings, patterns } = this.parsedPathPatterns; if (matchableStrings.has(importExpr)) { return [importExpr, this.paths[importExpr]]; } // https://github.com/microsoft/TypeScript/blob/29e6d6689dfb422e4f1395546c1917d07e1f664d/src/compiler/core.ts#L2410 let matchedValue; let longestMatchPrefixLength = -1; for (let i = 0; i < patterns.length; i++) { const pattern = patterns[i]; if (pattern.prefix.length > longestMatchPrefixLength && this.isPatternMatch(pattern, importExpr)) { longestMatchPrefixLength = pattern.prefix.length; matchedValue = pattern; } } return matchedValue ? [matchedValue, this.paths[matchedValue.pattern]] : 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(packageJsonPath, dep, packageVersion) { this.packagesMetadata ??= (0, packages_1.getWorkspacePackagesMetadata)(this.nodes); const maybeDep = this.packagesMetadata.packageToProjectMap[dep]; const maybeDepMetadata = maybeDep?.data.metadata.js; if (!maybeDepMetadata) { return null; } const workspaceRegex = /^workspace:/; const hasWorkspaceProtocol = workspaceRegex.test(packageVersion); const normalizedRange = packageVersion.replace(workspaceRegex, ''); /** * Regex is needed to test for workspace: protocol because following options are all valid: * - workspace:* * - workspace:^ * - workspace:~ * - workspace:foo@* */ if (hasWorkspaceProtocol || normalizedRange === '*') { return maybeDep?.name; } if (normalizedRange.startsWith('file:')) { const targetPath = maybeDep?.data.root; const normalizedPath = normalizedRange.replace('file:', ''); const resolvedPath = node_path_1.posix.join((0, node_path_1.dirname)(packageJsonPath), normalizedPath); if (targetPath === resolvedPath) { return maybeDep?.name; } } if ((0, semver_1.satisfies)(maybeDepMetadata.packageVersion, normalizedRange, { includePrerelease: true, })) { return maybeDep?.name; } return null; } isPatternMatch({ prefix, suffix }, candidate) { return (candidate.length >= prefix.length + suffix.length && candidate.startsWith(prefix) && candidate.endsWith(suffix)); } parsePaths(paths) { this.parsedPathPatterns = { matchableStrings: new Set(), patterns: [], }; for (const key of Object.keys(paths)) { const parts = key.split('*'); if (parts.length > 2) { continue; } if (parts.length === 1) { this.parsedPathPatterns.matchableStrings.add(key); continue; } this.parsedPathPatterns.patterns.push({ pattern: key, prefix: parts[0], suffix: parts[1], }); } } resolveImportWithTypescript(normalizedImportExpr, filePath) { let resolvedModule; const projectName = (0, find_project_for_path_1.findProjectForPath)(filePath, this.projectRootMappings); const cacheScope = projectName ? // fall back to the project name if the project root can't be determined this.nodes[projectName]?.data?.root || projectName : // fall back to the file path if the project can't be determined filePath; const cacheKey = `${normalizedImportExpr}__${cacheScope}`; if (this.typescriptResolutionCache.has(cacheKey)) { resolvedModule = this.typescriptResolutionCache.get(cacheKey); } else { resolvedModule = (0, typescript_1.resolveModuleByImport)(normalizedImportExpr, filePath, this.tsConfig.absolutePath); this.typescriptResolutionCache.set(cacheKey, resolvedModule ? resolvedModule : null); } if (!resolvedModule) { return; } const nodeModulesIndex = resolvedModule.lastIndexOf('node_modules/'); if (nodeModulesIndex === -1) { const resolvedProject = this.findProjectOfResolvedModule(resolvedModule); return resolvedProject; } // strip the node_modules/ prefix from the resolved module path const packagePath = resolvedModule.substring(nodeModulesIndex + 'node_modules/'.length); const externalProject = this.findNpmProjectFromImport(packagePath, filePath); return externalProject; } resolveImportWithRequire(normalizedImportExpr, filePath) { return (0, node_path_1.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; } let normalizedResolvedModule = resolvedModule.startsWith('./') ? resolvedModule.substring(2) : resolvedModule; // Remove trailing slash to ensure proper project matching if (normalizedResolvedModule.endsWith('/')) { normalizedResolvedModule = normalizedResolvedModule.slice(0, -1); } 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;