From ca5e673a80132c5affe1a96b2f717cd83dffde86 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leosvel=20P=C3=A9rez=20Espinosa?= Date: Wed, 19 Jul 2023 20:11:13 +0100 Subject: [PATCH] feat(misc): add alternative implementation to calculate buildable dependencies using the task graph (#18125) --- e2e/js/src/js.test.ts | 70 +++ .../delegate-build/delegate-build.impl.ts | 5 +- .../ng-packagr-lite.impl.spec.ts | 2 +- .../executors/package/package.impl.spec.ts | 2 +- .../src/executors/package/package.impl.ts | 5 +- packages/js/src/executors/node/node.impl.ts | 5 +- .../batch/build-task-info-per-tsconfig-map.ts | 5 +- .../js/src/utils/buildable-libs-utils.spec.ts | 450 +++++++++++++++++- packages/js/src/utils/buildable-libs-utils.ts | 241 +++++++++- packages/js/src/utils/check-dependencies.ts | 5 +- .../next/src/executors/export/export.impl.ts | 5 +- .../src/executors/rollup/rollup.impl.ts | 5 +- packages/vite/src/utils/executor-utils.ts | 5 +- .../executors/dev-server/dev-server.impl.ts | 5 +- .../src/executors/webpack/webpack.impl.ts | 5 +- 15 files changed, 783 insertions(+), 32 deletions(-) diff --git a/e2e/js/src/js.test.ts b/e2e/js/src/js.test.ts index aabb81df37d13..b0227237541d7 100644 --- a/e2e/js/src/js.test.ts +++ b/e2e/js/src/js.test.ts @@ -4,7 +4,9 @@ import { cleanupProject, createFile, newProject, + readFile, readJson, + rmDist, runCLI, runCLIAsync, uniq, @@ -160,4 +162,72 @@ export function ${lib}Wildcard() { runCLI(`build ${nonBuildable}`); checkFilesExist(`dist/libs/${nonBuildable}/src/index.js`); }); + + it('should build buildable libraries using the task graph and handle more scenarios than current implementation', () => { + const lib1 = uniq('lib1'); + const lib2 = uniq('lib2'); + runCLI(`generate @nx/js:lib ${lib1} --bundler=tsc --no-interactive`); + runCLI(`generate @nx/js:lib ${lib2} --bundler=tsc --no-interactive`); + + // add dep between lib1 and lib2 + updateFile( + `libs/${lib1}/src/index.ts`, + `export { ${lib2} } from '@${scope}/${lib2}';` + ); + + // check current implementation + expect(runCLI(`build ${lib1} --skip-nx-cache`)).toContain( + 'Done compiling TypeScript files' + ); + checkFilesExist(`dist/libs/${lib1}/src/index.js`); + checkFilesExist(`dist/libs/${lib2}/src/index.js`); + + // cleanup dist + rmDist(); + + // check task graph implementation + expect( + runCLI(`build ${lib1} --skip-nx-cache`, { + env: { NX_BUILDABLE_LIBRARIES_TASK_GRAPH: 'true' }, + }) + ).toContain('Done compiling TypeScript files'); + checkFilesExist(`dist/libs/${lib1}/src/index.js`); + checkFilesExist(`dist/libs/${lib2}/src/index.js`); + + // change build target name of lib2 and update target dependencies + updateJson(`libs/${lib2}/project.json`, (json) => { + json.targets['my-custom-build'] = json.targets.build; + delete json.targets.build; + return json; + }); + const originalNxJson = readFile('nx.json'); + updateJson('nx.json', (json) => { + json.targetDefaults.build = { + ...json.targetDefaults.build, + dependsOn: [...json.targetDefaults.build.dependsOn, '^my-custom-build'], + }; + return json; + }); + + // cleanup dist + rmDist(); + + // check current implementation, it doesn't support a different build target name + expect(() => runCLI(`build ${lib1} --skip-nx-cache`)).toThrow(); + + // cleanup dist + rmDist(); + + // check task graph implementation + expect( + runCLI(`build ${lib1} --skip-nx-cache`, { + env: { NX_BUILDABLE_LIBRARIES_TASK_GRAPH: 'true' }, + }) + ).toContain('Done compiling TypeScript files'); + checkFilesExist(`dist/libs/${lib1}/src/index.js`); + checkFilesExist(`dist/libs/${lib2}/src/index.js`); + + // restore nx.json + updateFile('nx.json', () => originalNxJson); + }); }); diff --git a/packages/angular/src/executors/delegate-build/delegate-build.impl.ts b/packages/angular/src/executors/delegate-build/delegate-build.impl.ts index d877e3a40a8e3..248c8241d4afb 100644 --- a/packages/angular/src/executors/delegate-build/delegate-build.impl.ts +++ b/packages/angular/src/executors/delegate-build/delegate-build.impl.ts @@ -1,7 +1,7 @@ import type { ExecutorContext } from '@nx/devkit'; import { joinPathFragments, parseTargetString, runExecutor } from '@nx/devkit'; import { - calculateProjectDependencies, + calculateProjectBuildableDependencies, checkDependentProjectsHaveBeenBuilt, createTmpTsConfig, } from '@nx/js/src/utils/buildable-libs-utils'; @@ -11,7 +11,8 @@ export async function* delegateBuildExecutor( options: DelegateBuildExecutorSchema, context: ExecutorContext ) { - const { target, dependencies } = calculateProjectDependencies( + const { target, dependencies } = calculateProjectBuildableDependencies( + context.taskGraph, context.projectGraph, context.root, context.projectName, diff --git a/packages/angular/src/executors/ng-packagr-lite/ng-packagr-lite.impl.spec.ts b/packages/angular/src/executors/ng-packagr-lite/ng-packagr-lite.impl.spec.ts index e2aa55244b1ec..216cc52c90f93 100644 --- a/packages/angular/src/executors/ng-packagr-lite/ng-packagr-lite.impl.spec.ts +++ b/packages/angular/src/executors/ng-packagr-lite/ng-packagr-lite.impl.spec.ts @@ -30,7 +30,7 @@ describe('NgPackagrLite executor', () => { beforeEach(async () => { ( - buildableLibsUtils.calculateProjectDependencies as jest.Mock + buildableLibsUtils.calculateProjectBuildableDependencies as jest.Mock ).mockImplementation(() => ({ target: {}, dependencies: [], diff --git a/packages/angular/src/executors/package/package.impl.spec.ts b/packages/angular/src/executors/package/package.impl.spec.ts index b55dc29cf9981..384c58e4ebe2c 100644 --- a/packages/angular/src/executors/package/package.impl.spec.ts +++ b/packages/angular/src/executors/package/package.impl.spec.ts @@ -30,7 +30,7 @@ describe('Package executor', () => { beforeEach(async () => { ( - buildableLibsUtils.calculateProjectDependencies as jest.Mock + buildableLibsUtils.calculateProjectBuildableDependencies as jest.Mock ).mockImplementation(() => ({ target: {}, dependencies: [], diff --git a/packages/angular/src/executors/package/package.impl.ts b/packages/angular/src/executors/package/package.impl.ts index 04f2f18d70c50..b4e29c084f4c0 100644 --- a/packages/angular/src/executors/package/package.impl.ts +++ b/packages/angular/src/executors/package/package.impl.ts @@ -1,7 +1,7 @@ import type { ExecutorContext } from '@nx/devkit'; import { eachValueFrom } from '@nx/devkit/src/utils/rxjs-for-await'; import { - calculateProjectDependencies, + calculateProjectBuildableDependencies, checkDependentProjectsHaveBeenBuilt, createTmpTsConfig, DependentBuildableProjectNode, @@ -72,7 +72,8 @@ export function createLibraryExecutor( context: ExecutorContext ) { const { target, dependencies, topLevelDependencies } = - calculateProjectDependencies( + calculateProjectBuildableDependencies( + context.taskGraph, context.projectGraph, context.root, context.projectName, diff --git a/packages/js/src/executors/node/node.impl.ts b/packages/js/src/executors/node/node.impl.ts index b54b87a405828..ab1101d06f722 100644 --- a/packages/js/src/executors/node/node.impl.ts +++ b/packages/js/src/executors/node/node.impl.ts @@ -17,7 +17,7 @@ import * as path from 'path'; import { join } from 'path'; import { InspectType, NodeExecutorOptions } from './schema'; -import { calculateProjectDependencies } from '../../utils/buildable-libs-utils'; +import { calculateProjectBuildableDependencies } from '../../utils/buildable-libs-utils'; import { killTree } from './lib/kill-tree'; import { fileExists } from 'nx/src/utils/fileutils'; import { getMainFileDirRelativeToProjectRoot } from '../../utils/get-main-file-dir'; @@ -314,7 +314,8 @@ function calculateResolveMappings( options: NodeExecutorOptions ) { const parsed = parseTargetString(options.buildTarget, context.projectGraph); - const { dependencies } = calculateProjectDependencies( + const { dependencies } = calculateProjectBuildableDependencies( + context.taskGraph, context.projectGraph, context.root, parsed.project, diff --git a/packages/js/src/executors/tsc/lib/batch/build-task-info-per-tsconfig-map.ts b/packages/js/src/executors/tsc/lib/batch/build-task-info-per-tsconfig-map.ts index 5a5f853f48a0f..0a27acf5244db 100644 --- a/packages/js/src/executors/tsc/lib/batch/build-task-info-per-tsconfig-map.ts +++ b/packages/js/src/executors/tsc/lib/batch/build-task-info-per-tsconfig-map.ts @@ -2,7 +2,7 @@ import type { ExecutorContext } from '@nx/devkit'; import { parseTargetString } from '@nx/devkit'; import { join, relative } from 'path'; import { CopyAssetsHandler } from '../../../../utils/assets/copy-assets-handler'; -import { calculateProjectDependencies } from '../../../../utils/buildable-libs-utils'; +import { calculateProjectBuildableDependencies } from '../../../../utils/buildable-libs-utils'; import type { NormalizedExecutorOptions } from '../../../../utils/schema'; import { getTaskWithTscExecutorOptions } from '../get-task-options'; import type { TypescriptInMemoryTsConfig } from '../typescript-compilation'; @@ -97,7 +97,8 @@ function createTaskInfo( const { target: projectGraphNode, dependencies: buildableProjectNodeDependencies, - } = calculateProjectDependencies( + } = calculateProjectBuildableDependencies( + context.taskGraph, context.projectGraph, context.root, context.taskGraph.tasks[taskName].target.project, diff --git a/packages/js/src/utils/buildable-libs-utils.spec.ts b/packages/js/src/utils/buildable-libs-utils.spec.ts index 07709f6743740..44590ae8a22c9 100644 --- a/packages/js/src/utils/buildable-libs-utils.spec.ts +++ b/packages/js/src/utils/buildable-libs-utils.spec.ts @@ -1,6 +1,7 @@ -import { DependencyType, ProjectGraph } from '@nx/devkit'; +import { DependencyType, ProjectGraph, TaskGraph } from '@nx/devkit'; import { calculateProjectDependencies, + calculateDependenciesFromTaskGraph, DependentBuildableProjectNode, updatePaths, } from './buildable-libs-utils'; @@ -162,6 +163,453 @@ describe('calculateProjectDependencies', () => { }); }); +describe('calculateDependenciesFromTaskGraph', () => { + it('should calculate workspace and npm dependencies correctly', () => { + /** + * Project Graph: + * lib1 -> lib2 -> lib3 + * -> lib4 -> npm:formik,npm:lodash + * -> lib3 // should not be duplicated + * -> npm:formik // should not be duplicated + * lib5 -> npm:fs-extra // lib5 is not a dependency, not part of the task graph or the result + * + * Target deps config: + * build: [^build, build-base] + * + * Task Graph: + * lib1:build -> lib2:build -> lib2:build-base + * -> lib3:build + * -> lib4:build + * -> lib3:build + */ + const projectGraph: ProjectGraph = { + nodes: { + lib1: { + type: 'lib', + name: 'lib1', + data: { root: 'libs/lib1', targets: { build: {} } }, + }, + lib2: { + type: 'lib', + name: 'lib2', + data: { root: 'libs/lib2', targets: { build: {}, 'build-base': {} } }, + }, + lib3: { + type: 'lib', + name: 'lib3', + data: { root: 'libs/lib3', targets: { build: {} } }, + }, + lib4: { + type: 'lib', + name: 'lib4', + data: { root: 'libs/lib4', targets: { build: {} } }, + }, + lib5: { + type: 'lib', + name: 'lib5', + data: { root: 'libs/lib5', targets: { build: {} } }, + }, + }, + externalNodes: { + 'npm:formik': { + type: 'npm', + name: 'npm:formik', + data: { packageName: 'formik', version: '0.0.0' }, + }, + 'npm:lodash': { + type: 'npm', + name: 'npm:lodash', + data: { packageName: 'lodash', version: '0.0.0' }, + }, + 'npm:fs-extra': { + type: 'npm', + name: 'npm:fs-extra', + data: { packageName: 'fs-extra', version: '0.0.0' }, + }, + }, + dependencies: { + lib1: [ + { + source: 'lib1', + target: 'npm:formik', + type: DependencyType.static, + }, + { + source: 'lib1', + target: 'lib2', + type: DependencyType.static, + }, + { + source: 'lib1', + target: 'lib3', + type: DependencyType.static, + }, + ], + lib2: [ + { + source: 'lib2', + target: 'lib3', + type: DependencyType.static, + }, + { + source: 'lib2', + target: 'lib4', + type: DependencyType.static, + }, + ], + lib3: [], + lib4: [ + { + source: 'lib4', + target: 'npm:formik', + type: DependencyType.static, + }, + { + source: 'lib4', + target: 'npm:lodash', + type: DependencyType.static, + }, + ], + lib5: [ + { + source: 'lib5', + target: 'npm:fs-extra', + type: DependencyType.static, + }, + ], + }, + }; + const taskGraph: TaskGraph = { + dependencies: { + 'lib1:build': ['lib2:build', 'lib3:build'], + 'lib2:build': ['lib2:build-base', 'lib3:build', 'lib4:build'], + 'lib2:build-base': [], + 'lib3:build': [], + 'lib4:build': [], + }, + roots: [], + tasks: { + 'lib1:build': { + id: 'lib1:build', + overrides: {}, + target: { project: 'lib1', target: 'build' }, + }, + 'lib2:build': { + id: 'lib2:build', + overrides: {}, + target: { project: 'lib2', target: 'build' }, + }, + 'lib2:build-base': { + id: 'lib2:build-base', + overrides: {}, + target: { project: 'lib2', target: 'build-base' }, + }, + 'lib3:build': { + id: 'lib3:build', + overrides: {}, + target: { project: 'lib3', target: 'build' }, + }, + 'lib4:build': { + id: 'lib4:build', + overrides: {}, + target: { project: 'lib4', target: 'build' }, + }, + }, + }; + + const results = calculateDependenciesFromTaskGraph( + taskGraph, + projectGraph, + 'root', + 'lib1', + 'build', + undefined + ); + + expect(results).toMatchObject({ + target: { type: 'lib', name: 'lib1' }, + dependencies: [ + { name: 'formik' }, + { name: 'lib2' }, + { name: 'lib3' }, + { name: 'lib4' }, + { name: 'lodash' }, + ], + nonBuildableDependencies: [], + topLevelDependencies: [ + { name: 'lib2' }, + { name: 'lib3' }, + { name: 'formik' }, + ], + }); + }); + + it('should calculate workspace and npm dependencies correctly with a different target dependencies setup', () => { + /** + * Project Graph: + * lib1 -> lib2 -> lib3 + * -> lib4 -> npm:formik,npm:lodash + * -> lib3 // should not be duplicated + * -> npm:formik // should not be duplicated + * + * Target deps config: + * build: [build-base] + * build-base: [^build] + * + * Task Graph: + * lib1:build -> lib1:build-base -> lib2:build -> lib2:build-base -> lib3:build -> lib3:build-base + * -> lib4:build -> lib4:build-base + * -> lib3:build -> lib3:build-base + */ + const projectGraph: ProjectGraph = { + nodes: { + lib1: { + type: 'lib', + name: 'lib1', + data: { root: 'libs/lib1', targets: { build: {}, 'build-base': {} } }, + }, + lib2: { + type: 'lib', + name: 'lib2', + data: { root: 'libs/lib2', targets: { build: {}, 'build-base': {} } }, + }, + lib3: { + type: 'lib', + name: 'lib3', + data: { root: 'libs/lib3', targets: { build: {}, 'build-base': {} } }, + }, + lib4: { + type: 'lib', + name: 'lib4', + data: { root: 'libs/lib4', targets: { build: {}, 'build-base': {} } }, + }, + }, + externalNodes: { + 'npm:formik': { + type: 'npm', + name: 'npm:formik', + data: { packageName: 'formik', version: '0.0.0' }, + }, + 'npm:lodash': { + type: 'npm', + name: 'npm:lodash', + data: { packageName: 'lodash', version: '0.0.0' }, + }, + }, + dependencies: { + lib1: [ + { + source: 'lib1', + target: 'npm:formik', + type: DependencyType.static, + }, + { + source: 'lib1', + target: 'lib2', + type: DependencyType.static, + }, + { + source: 'lib1', + target: 'lib3', + type: DependencyType.static, + }, + ], + lib2: [ + { + source: 'lib2', + target: 'lib3', + type: DependencyType.static, + }, + { + source: 'lib2', + target: 'lib4', + type: DependencyType.static, + }, + ], + lib3: [], + lib4: [ + { + source: 'lib4', + target: 'npm:formik', + type: DependencyType.static, + }, + { + source: 'lib4', + target: 'npm:lodash', + type: DependencyType.static, + }, + ], + }, + }; + const taskGraph: TaskGraph = { + dependencies: { + 'lib1:build': ['lib1:build-base'], + 'lib1:build-base': ['lib2:build', 'lib3:build'], + 'lib2:build': ['lib2:build-base'], + 'lib2:build-base': ['lib3:build', 'lib4:build'], + 'lib3:build': ['lib3:build-base'], + 'lib3:build-base': [], + 'lib4:build': ['lib4:build-base'], + 'lib4:build-base': [], + }, + roots: [], + tasks: { + 'lib1:build': { + id: 'lib1:build', + overrides: {}, + target: { project: 'lib1', target: 'build' }, + }, + 'lib1:build-base': { + id: 'lib1:build-base', + overrides: {}, + target: { project: 'lib1', target: 'build-base' }, + }, + 'lib2:build': { + id: 'lib2:build', + overrides: {}, + target: { project: 'lib2', target: 'build' }, + }, + 'lib2:build-base': { + id: 'lib2:build-base', + overrides: {}, + target: { project: 'lib2', target: 'build-base' }, + }, + 'lib3:build': { + id: 'lib3:build', + overrides: {}, + target: { project: 'lib3', target: 'build' }, + }, + 'lib3:build-base': { + id: 'lib3:build-base', + overrides: {}, + target: { project: 'lib3', target: 'build-base' }, + }, + 'lib4:build': { + id: 'lib4:build', + overrides: {}, + target: { project: 'lib4', target: 'build' }, + }, + 'lib4:build-base': { + id: 'lib4:build-base', + overrides: {}, + target: { project: 'lib4', target: 'build-base' }, + }, + }, + }; + + const results = calculateDependenciesFromTaskGraph( + taskGraph, + projectGraph, + 'root', + 'lib1', + 'build', + undefined + ); + + expect(results).toMatchObject({ + target: { type: 'lib', name: 'lib1' }, + dependencies: [ + { name: 'formik' }, + { name: 'lib2' }, + { name: 'lib3' }, + { name: 'lib4' }, + { name: 'lodash' }, + ], + nonBuildableDependencies: [], + topLevelDependencies: [ + { name: 'lib2' }, + { name: 'lib3' }, + { name: 'formik' }, + ], + }); + }); + + it('should include npm packages in dependency list and sort them correctly', () => { + const projectGraph: ProjectGraph = { + nodes: { + example: { + type: 'lib', + name: 'example', + data: { + root: '/root/example', + }, + }, + }, + externalNodes: { + 'npm:some-lib': { + type: 'npm', + name: 'npm:some-lib', + data: { + packageName: 'some-lib', + version: '0.0.0', + }, + }, + 'npm:formik': { + type: 'npm', + name: 'npm:formik', + data: { + packageName: 'formik', + version: '0.0.0', + }, + }, + 'npm:@prefixed-lib': { + type: 'npm', + name: 'npm:@prefixed-lib', + data: { + packageName: '@prefixed-lib', + version: '0.0.0', + }, + }, + }, + dependencies: { + example: [ + { + source: 'example', + target: 'npm:some-lib', + type: DependencyType.static, + }, + { + source: 'example', + target: 'npm:formik', + type: DependencyType.static, + }, + { + source: 'example', + target: 'npm:@prefixed-lib', + type: DependencyType.static, + }, + ], + }, + }; + // not relevant for this test case + const taskGraph: TaskGraph = { + dependencies: {}, + roots: [], + tasks: {}, + }; + + const results = calculateDependenciesFromTaskGraph( + taskGraph, + projectGraph, + 'root', + 'example', + 'build', + undefined + ); + expect(results).toMatchObject({ + target: { + type: 'lib', + name: 'example', + }, + dependencies: [ + { name: '@prefixed-lib' }, + { name: 'formik' }, + { name: 'some-lib' }, + ], + }); + }); +}); + describe('missingDependencies', () => { it('should throw an error if dependency is missing', async () => { const graph: ProjectGraph = { diff --git a/packages/js/src/utils/buildable-libs-utils.ts b/packages/js/src/utils/buildable-libs-utils.ts index 0801a959fd2fb..862a529cbf95d 100644 --- a/packages/js/src/utils/buildable-libs-utils.ts +++ b/packages/js/src/utils/buildable-libs-utils.ts @@ -1,22 +1,24 @@ -import { dirname, join, relative } from 'path'; -import { directoryExists, fileExists } from 'nx/src/utils/fileutils'; -import type { ProjectGraph, ProjectGraphProjectNode } from '@nx/devkit'; +import type { + ProjectGraph, + ProjectGraphExternalNode, + ProjectGraphProjectNode, + TaskGraph, +} from '@nx/devkit'; import { getOutputsForTargetAndConfiguration, - ProjectGraphExternalNode, + parseTargetString, readJsonFile, stripIndents, writeJsonFile, } from '@nx/devkit'; -import type * as ts from 'typescript'; import { unlinkSync } from 'fs'; -import { output } from 'nx/src/utils/output'; import { isNpmProject } from 'nx/src/project-graph/operators'; -import { ensureTypescript } from './typescript/ensure-typescript'; +import { directoryExists, fileExists } from 'nx/src/utils/fileutils'; +import { output } from 'nx/src/utils/output'; +import { dirname, join, relative } from 'path'; +import type * as ts from 'typescript'; import { readTsConfigPaths } from './typescript/ts-config'; -let tsModule: typeof import('typescript'); - function isBuildable(target: string, node: ProjectGraphProjectNode): boolean { return ( node.data.targets && @@ -31,6 +33,42 @@ export type DependentBuildableProjectNode = { node: ProjectGraphProjectNode | ProjectGraphExternalNode; }; +export function calculateProjectBuildableDependencies( + taskGraph: TaskGraph | undefined, + projGraph: ProjectGraph, + root: string, + projectName: string, + targetName: string, + configurationName: string, + shallow?: boolean +): { + target: ProjectGraphProjectNode; + dependencies: DependentBuildableProjectNode[]; + nonBuildableDependencies: string[]; + topLevelDependencies: DependentBuildableProjectNode[]; +} { + if (process.env.NX_BUILDABLE_LIBRARIES_TASK_GRAPH === 'true' && taskGraph) { + return calculateDependenciesFromTaskGraph( + taskGraph, + projGraph, + root, + projectName, + targetName, + configurationName, + shallow + ); + } + + return calculateProjectDependencies( + projGraph, + root, + projectName, + targetName, + configurationName, + shallow + ); +} + export function calculateProjectDependencies( projGraph: ProjectGraph, root: string, @@ -179,6 +217,191 @@ function readTsConfigWithRemappedPaths( return generatedTsConfig; } +export function calculateDependenciesFromTaskGraph( + taskGraph: TaskGraph, + projectGraph: ProjectGraph, + root: string, + projectName: string, + targetName: string, + configurationName: string, + shallow?: boolean +): { + target: ProjectGraphProjectNode; + dependencies: DependentBuildableProjectNode[]; + nonBuildableDependencies: string[]; + topLevelDependencies: DependentBuildableProjectNode[]; +} { + const target = projectGraph.nodes[projectName]; + const nonBuildableDependencies = []; + const topLevelDependencies: DependentBuildableProjectNode[] = []; + + const dependentTasks = collectDependentTasks( + projectName, + `${projectName}:${targetName}${ + configurationName ? `:${configurationName}` : '' + }`, + taskGraph, + projectGraph, + shallow + ); + + const npmDependencies = collectNpmDependencies( + projectName, + projectGraph, + !shallow ? dependentTasks : undefined + ); + + const dependencies: DependentBuildableProjectNode[] = []; + for (const [taskName, { isTopLevel }] of dependentTasks) { + let project: DependentBuildableProjectNode = null; + const depTask = taskGraph.tasks[taskName]; + const depProjectNode = projectGraph.nodes?.[depTask.target.project]; + if (depProjectNode?.type !== 'lib') { + return null; + } + + let outputs = getOutputsForTargetAndConfiguration(depTask, depProjectNode); + + if (outputs.length === 0) { + nonBuildableDependencies.push(depTask.target.project); + continue; + } + + const libPackageJsonPath = join( + root, + depProjectNode.data.root, + 'package.json' + ); + + project = { + name: fileExists(libPackageJsonPath) + ? readJsonFile(libPackageJsonPath).name // i.e. @workspace/mylib + : depTask.target.project, + outputs, + node: depProjectNode, + }; + + if (isTopLevel) { + topLevelDependencies.push(project); + } + + dependencies.push(project); + } + + for (const { project, isTopLevel } of npmDependencies) { + if (isTopLevel) { + topLevelDependencies.push(project); + } + + dependencies.push(project); + } + + dependencies.sort((a, b) => (a.name > b.name ? 1 : b.name > a.name ? -1 : 0)); + + return { + target, + dependencies, + nonBuildableDependencies, + topLevelDependencies, + }; +} + +function collectNpmDependencies( + projectName: string, + projectGraph: ProjectGraph, + dependentTasks: + | Map + | undefined, + collectedPackages = new Set(), + isTopLevel = true +): Array<{ project: DependentBuildableProjectNode; isTopLevel: boolean }> { + const dependencies: Array<{ + project: DependentBuildableProjectNode; + isTopLevel: boolean; + }> = projectGraph.dependencies[projectName] + .map((dep) => { + const projectNode = + projectGraph.nodes?.[dep.target] ?? + projectGraph.externalNodes?.[dep.target]; + if ( + projectNode?.type !== 'npm' || + collectedPackages.has(projectNode.data.packageName) + ) { + return null; + } + + const project = { + name: projectNode.data.packageName, + outputs: [], + node: projectNode, + }; + collectedPackages.add(project.name); + + return { project, isTopLevel }; + }) + .filter((x) => !!x); + + if (dependentTasks?.size) { + for (const [, { project: projectName }] of dependentTasks) { + dependencies.push( + ...collectNpmDependencies( + projectName, + projectGraph, + undefined, + collectedPackages, + false + ) + ); + } + } + + return dependencies; +} + +function collectDependentTasks( + project: string, + task: string, + taskGraph: TaskGraph, + projectGraph: ProjectGraph, + shallow?: boolean, + areTopLevelDeps = true, + dependentTasks = new Map() +): Map { + for (const depTask of taskGraph.dependencies[task] ?? []) { + if (dependentTasks.has(depTask)) { + if (!dependentTasks.get(depTask).isTopLevel && areTopLevelDeps) { + dependentTasks.get(depTask).isTopLevel = true; + } + continue; + } + + const { project: depTaskProject } = parseTargetString( + depTask, + projectGraph + ); + if (depTaskProject !== project) { + dependentTasks.set(depTask, { + project: depTaskProject, + isTopLevel: areTopLevelDeps, + }); + } + + if (!shallow) { + collectDependentTasks( + depTaskProject, + depTask, + taskGraph, + projectGraph, + shallow, + depTaskProject === project && areTopLevelDeps, + dependentTasks + ); + } + } + + return dependentTasks; +} + /** * Util function to create tsconfig compilerOptions object with support for workspace libs paths. * diff --git a/packages/js/src/utils/check-dependencies.ts b/packages/js/src/utils/check-dependencies.ts index 6044ef41bf90a..a744088e1ce4b 100644 --- a/packages/js/src/utils/check-dependencies.ts +++ b/packages/js/src/utils/check-dependencies.ts @@ -1,6 +1,6 @@ import { ExecutorContext, ProjectGraphProjectNode } from '@nx/devkit'; import { - calculateProjectDependencies, + calculateProjectBuildableDependencies, createTmpTsConfig, DependentBuildableProjectNode, } from './buildable-libs-utils'; @@ -14,7 +14,8 @@ export function checkDependencies( target: ProjectGraphProjectNode; dependencies: DependentBuildableProjectNode[]; } { - const { target, dependencies } = calculateProjectDependencies( + const { target, dependencies } = calculateProjectBuildableDependencies( + context.taskGraph, context.projectGraph, context.root, context.projectName, diff --git a/packages/next/src/executors/export/export.impl.ts b/packages/next/src/executors/export/export.impl.ts index 27c0ad8b7e9c9..75d9c7ff5c7be 100644 --- a/packages/next/src/executors/export/export.impl.ts +++ b/packages/next/src/executors/export/export.impl.ts @@ -8,7 +8,7 @@ import { import exportApp from 'next/dist/export'; import { join, resolve } from 'path'; import { - calculateProjectDependencies, + calculateProjectBuildableDependencies, DependentBuildableProjectNode, } from '@nx/js/src/utils/buildable-libs-utils'; @@ -43,7 +43,8 @@ export default async function exportExecutor( ) { let dependencies: DependentBuildableProjectNode[] = []; if (!options.buildLibsFromSource) { - const result = calculateProjectDependencies( + const result = calculateProjectBuildableDependencies( + context.taskGraph, context.projectGraph, context.root, context.projectName, diff --git a/packages/rollup/src/executors/rollup/rollup.impl.ts b/packages/rollup/src/executors/rollup/rollup.impl.ts index 881dbcd593e3a..a554d383405ca 100644 --- a/packages/rollup/src/executors/rollup/rollup.impl.ts +++ b/packages/rollup/src/executors/rollup/rollup.impl.ts @@ -11,7 +11,7 @@ import * as autoprefixer from 'autoprefixer'; import type { ExecutorContext } from '@nx/devkit'; import { joinPathFragments, logger, names, readJsonFile } from '@nx/devkit'; import { - calculateProjectDependencies, + calculateProjectBuildableDependencies, computeCompilerOptionsPaths, DependentBuildableProjectNode, } from '@nx/js/src/utils/buildable-libs-utils'; @@ -52,7 +52,8 @@ export async function* rollupExecutor( const project = context.projectsConfigurations.projects[context.projectName]; const sourceRoot = project.sourceRoot; - const { target, dependencies } = calculateProjectDependencies( + const { target, dependencies } = calculateProjectBuildableDependencies( + context.taskGraph, context.projectGraph, context.root, context.projectName, diff --git a/packages/vite/src/utils/executor-utils.ts b/packages/vite/src/utils/executor-utils.ts index d80069746bde3..d3bb3c6a0965b 100644 --- a/packages/vite/src/utils/executor-utils.ts +++ b/packages/vite/src/utils/executor-utils.ts @@ -4,7 +4,7 @@ import { ViteBuildExecutorOptions } from '../executors/build/schema'; import { ExecutorContext } from '@nx/devkit'; import { ViteDevServerExecutorOptions } from '../executors/dev-server/schema'; import { - calculateProjectDependencies, + calculateProjectBuildableDependencies, createTmpTsConfig, } from '@nx/js/src/utils/buildable-libs-utils'; @@ -35,7 +35,8 @@ export function createBuildableTsConfig( options.buildLibsFromSource ??= true; if (!options.buildLibsFromSource) { - const { dependencies } = calculateProjectDependencies( + const { dependencies } = calculateProjectBuildableDependencies( + context.taskGraph, context.projectGraph, context.root, context.projectName, diff --git a/packages/webpack/src/executors/dev-server/dev-server.impl.ts b/packages/webpack/src/executors/dev-server/dev-server.impl.ts index a53c0807dd85f..2e58ed2240cad 100644 --- a/packages/webpack/src/executors/dev-server/dev-server.impl.ts +++ b/packages/webpack/src/executors/dev-server/dev-server.impl.ts @@ -11,7 +11,7 @@ import * as WebpackDevServer from 'webpack-dev-server'; import { getDevServerConfig } from './lib/get-dev-server-config'; import { - calculateProjectDependencies, + calculateProjectBuildableDependencies, createTmpTsConfig, } from '@nx/js/src/utils/buildable-libs-utils'; import { runWebpackDevServer } from '../../utils/run-webpack'; @@ -43,7 +43,8 @@ export async function* devServerExecutor( } if (!buildOptions.buildLibsFromSource) { - const { target, dependencies } = calculateProjectDependencies( + const { target, dependencies } = calculateProjectBuildableDependencies( + context.taskGraph, context.projectGraph, context.root, context.projectName, diff --git a/packages/webpack/src/executors/webpack/webpack.impl.ts b/packages/webpack/src/executors/webpack/webpack.impl.ts index 68516586e0258..6376cfd7629be 100644 --- a/packages/webpack/src/executors/webpack/webpack.impl.ts +++ b/packages/webpack/src/executors/webpack/webpack.impl.ts @@ -12,7 +12,7 @@ import { } from 'rxjs/operators'; import { resolve } from 'path'; import { - calculateProjectDependencies, + calculateProjectBuildableDependencies, createTmpTsConfig, } from '@nx/js/src/utils/buildable-libs-utils'; @@ -121,7 +121,8 @@ export async function* webpackExecutor( } if (!options.buildLibsFromSource && context.targetName) { - const { dependencies } = calculateProjectDependencies( + const { dependencies } = calculateProjectBuildableDependencies( + context.taskGraph, context.projectGraph, context.root, context.projectName,