import { join, relative } from 'path'; import { readNxJson } from 'nx/src/config/configuration'; import { joinPathFragments, normalizePath, type ProjectFileMap, type ProjectGraph, type ProjectGraphProjectNode, readJsonFile, } from '@nx/devkit'; import { fileExists } from 'nx/src/utils/fileutils'; import { fileDataDepTarget } from 'nx/src/config/project-graph'; import { readTsConfig } from './typescript/ts-config'; import { filterUsingGlobPatterns, getTargetInputs, } from 'nx/src/hasher/task-hasher'; /** * Finds all npm dependencies and their expected versions for a given project. */ export function findNpmDependencies( workspaceRoot: string, sourceProject: ProjectGraphProjectNode, projectGraph: ProjectGraph, projectFileMap: ProjectFileMap, buildTarget: string, options: { includeTransitiveDependencies?: boolean; ignoredFiles?: string[]; useLocalPathsForWorkspaceDependencies?: boolean; } = {} ): Record { let seen: null | Set = null; if (options.includeTransitiveDependencies) { seen = new Set(); } const results: Record = {}; function collectAll( currentProject: ProjectGraphProjectNode, collectedDeps: Record ): void { if (seen?.has(currentProject.name)) return; seen?.add(currentProject.name); collectDependenciesFromFileMap( workspaceRoot, currentProject, projectGraph, projectFileMap, buildTarget, options.ignoredFiles, options.useLocalPathsForWorkspaceDependencies, collectedDeps ); collectHelperDependencies( workspaceRoot, currentProject, projectGraph, buildTarget, collectedDeps ); if (options.includeTransitiveDependencies) { const projectDeps = projectGraph.dependencies[currentProject.name]; for (const dep of projectDeps) { const projectDep = projectGraph.nodes[dep.target]; if (projectDep) collectAll(projectDep, collectedDeps); } } } collectAll(sourceProject, results); return results; } // Keep track of workspace libs we already read package.json for so we don't read from disk again. const seenWorkspaceDeps: Record = {}; function collectDependenciesFromFileMap( workspaceRoot: string, sourceProject: ProjectGraphProjectNode, projectGraph: ProjectGraph, projectFileMap: ProjectFileMap, buildTarget: string, ignoredFiles: string[], useLocalPathsForWorkspaceDependencies: boolean, npmDeps: Record ): void { const rawFiles = projectFileMap[sourceProject.name]; if (!rawFiles) return; // If build target does not exist in project, use all files as input. // This is needed for transitive dependencies for apps -- where libs may not be buildable. const inputs = sourceProject.data.targets[buildTarget] ? getTargetInputs(readNxJson(), sourceProject, buildTarget).selfInputs : ['{projectRoot}/**/*']; if (ignoredFiles) { for (const pattern of ignoredFiles) { inputs.push(`!${pattern}`); } } const files = filterUsingGlobPatterns( sourceProject.data.root, projectFileMap[sourceProject.name] || [], inputs ); for (const fileData of files) { if ( !fileData.deps || fileData.file === joinPathFragments(sourceProject.data.root, 'package.json') ) { continue; } for (const dep of fileData.deps) { const target = fileDataDepTarget(dep); // If the node is external, then read package info from `data`. const externalDep = projectGraph.externalNodes[target]; if (externalDep?.type === 'npm') { npmDeps[externalDep.data.packageName] = externalDep.data.version; continue; } // If node is internal, then try reading package info from `package.json` (which must exist for this to work). const workspaceDep = projectGraph.nodes[target]; if (!workspaceDep) continue; const cached = seenWorkspaceDeps[workspaceDep.name]; if (cached) { npmDeps[cached.name] = cached.version; } else { const packageJson = readPackageJson(workspaceDep, workspaceRoot); if ( // Check that this is a buildable project, otherwise it cannot be a dependency in package.json. workspaceDep.data.targets[buildTarget] && // Make sure package.json exists and has a valid name. packageJson?.name ) { let version: string; if (useLocalPathsForWorkspaceDependencies) { // Find the relative `file:...` path and use that as the version value. // This is useful for monorepos like Nx where the release will handle setting the correct version in dist. const depRoot = join(workspaceRoot, workspaceDep.data.root); const ownRoot = join(workspaceRoot, sourceProject.data.root); const relativePath = relative(ownRoot, depRoot); const filePath = normalizePath(relativePath); // normalize slashes for windows version = `file:${filePath}`; } else { // Otherwise, read the version from the dependencies `package.json` file. // This is useful for monorepos that commit release versions. // Users can also set version as "*" in source `package.json` files, which will be the value set here. // This is useful if they use custom scripts to update them in dist. version = packageJson.version ?? '*'; // fallback in case version is missing } npmDeps[packageJson.name] = version; seenWorkspaceDeps[workspaceDep.name] = { name: packageJson.name, version, }; } } } } } function readPackageJson( project: ProjectGraphProjectNode, workspaceRoot: string ): null | { name: string; version?: string; dependencies?: Record; optionalDependencies?: Record; peerDependencies?: Record; } { const packageJsonPath = join( workspaceRoot, project.data.root, 'package.json' ); if (fileExists(packageJsonPath)) return readJsonFile(packageJsonPath); return null; } function collectHelperDependencies( workspaceRoot: string, sourceProject: ProjectGraphProjectNode, projectGraph: ProjectGraph, buildTarget: string, npmDeps: Record ): void { const target = sourceProject.data.targets[buildTarget]; if (!target) return; if (target.executor === '@nx/js:tsc' && target.options?.tsConfig) { const tsConfig = readTsConfig(join(workspaceRoot, target.options.tsConfig)); if (tsConfig?.options['importHelpers']) { npmDeps['tslib'] = projectGraph.externalNodes['npm:tslib']?.data.version; } } if (target.executor === '@nx/js:swc') { const swcConfigPath = target.options.swcrc ? join(workspaceRoot, target.options.swcrc) : join(workspaceRoot, sourceProject.data.root, '.swcrc'); const swcConfig = fileExists(swcConfigPath) ? readJsonFile(swcConfigPath) : {}; if (swcConfig?.jsc?.externalHelpers) { npmDeps['@swc/helpers'] = projectGraph.externalNodes['npm:@swc/helpers']?.data.version; } } }