diff --git a/e2e/node/src/node-ts-solution.test.ts b/e2e/node/src/node-ts-solution.test.ts index 2809cd7205..577f6ddb14 100644 --- a/e2e/node/src/node-ts-solution.test.ts +++ b/e2e/node/src/node-ts-solution.test.ts @@ -198,7 +198,68 @@ describe('Node Applications', () => { } runCLI(`sync`); - console.log(readJson(`apps/${nestApp}/package.json`)); + runCLI(`build ${nestApp} --verbose`); + checkFilesExist(`apps/${nestApp}/dist/main.js`); + + const p = await runCommandUntil( + `serve ${nestApp}`, + (output) => + output.includes( + `Application is running on: http://localhost:${port}/api` + ), + + { + env: { + NX_DAEMON: 'true', + }, + } + ); + + const result = await getData(port, '/api'); + expect(result.message).toMatch('Hello'); + + try { + await promisifiedTreeKill(p.pid, 'SIGKILL'); + expect(await killPorts(port)).toBeTruthy(); + } catch (err) { + expect(err).toBeFalsy(); + } + }, 300_000); + + // Dependencies that are not directly imported but are still required for the build to succeed + // App -> LibA -> LibB + // LibB is transitive, it's not imported in the app, but is required by LibA + + it('should be able to work with transitive non-dependencies', async () => { + const nestApp = uniq('nestApp'); + const nestLibA = uniq('nestliba'); + const nestLibB = uniq('nestlibb'); + const port = getRandomPort(); + + process.env.PORT = `${port}`; + runCLI(`generate @nx/nest:app apps/${nestApp} --no-interactive`); + + runCLI(`generate @nx/nest:lib packages/${nestLibA} --no-interactive`); + runCLI(`generate @nx/nest:lib packages/${nestLibB} --no-interactive`); + + if (pm === 'pnpm') { + updateJson(`apps/${nestApp}/package.json`, (json) => { + json.dependencies = { + [`@${workspaceName}/${nestLibA}`]: 'workspace:*', + }; + return json; + }); + + updateJson(`packages/${nestLibA}/package.json`, (json) => { + json.dependencies = { + [`@${workspaceName}/${nestLibB}`]: 'workspace:*', + }; + return json; + }); + runCommand(`${getPackageManagerCommand().install}`); + } + runCLI(`sync`); + runCLI(`build ${nestApp} --verbose`); checkFilesExist(`apps/${nestApp}/dist/main.js`); diff --git a/packages/rspack/src/plugins/utils/apply-base-config.ts b/packages/rspack/src/plugins/utils/apply-base-config.ts index cb4876ef87..d8cc3036ac 100644 --- a/packages/rspack/src/plugins/utils/apply-base-config.ts +++ b/packages/rspack/src/plugins/utils/apply-base-config.ts @@ -19,7 +19,7 @@ import { getTerserEcmaVersion } from './get-terser-ecma-version'; import nodeExternals = require('webpack-node-externals'); import { NormalizedNxAppRspackPluginOptions } from './models'; import { isUsingTsSolutionSetup } from '@nx/js/src/utils/typescript/ts-solution-setup'; -import { isBuildableLibrary } from './is-lib-buildable'; +import { getNonBuildableLibs } from './get-non-buildable-libs'; const IGNORED_RSPACK_WARNINGS = [ /The comment file/i, @@ -357,32 +357,13 @@ function applyNxDependentConfig( const graph = options.projectGraph; const projectName = options.projectName; - const deps = graph?.dependencies?.[projectName] ?? []; - // Collect non-buildable TS project references so that they are bundled // in the final output. This is needed for projects that are not buildable // but are referenced by buildable projects. This is needed for the new TS // solution setup. + const nonBuildableWorkspaceLibs = isUsingTsSolution - ? deps - .filter((dep) => { - const node = graph.nodes?.[dep.target]; - if (!node || node.type !== 'lib') return false; - - const hasBuildTarget = 'build' in (node.data?.targets ?? {}); - - if (hasBuildTarget) { - return false; - } - - // If there is no build target we check the package exports to see if they reference - // source files - return !isBuildableLibrary(node); - }) - .map( - (dep) => graph.nodes?.[dep.target]?.data?.metadata?.js?.packageName - ) - .filter((name): name is string => !!name) + ? getNonBuildableLibs(graph, projectName) : []; externals.push( diff --git a/packages/rspack/src/plugins/utils/get-non-buildable-libs.ts b/packages/rspack/src/plugins/utils/get-non-buildable-libs.ts new file mode 100644 index 0000000000..f280c8f9f9 --- /dev/null +++ b/packages/rspack/src/plugins/utils/get-non-buildable-libs.ts @@ -0,0 +1,45 @@ +import { type ProjectGraph } from '@nx/devkit'; +import { getAllTransitiveDeps } from './get-transitive-deps'; +import { isBuildableLibrary } from './is-lib-buildable'; + +/** + * Get all non-buildable libraries in the project graph for a given project. + * This function retrieves all direct and transitive dependencies of a project, + * filtering out only those that are libraries and not buildable. + * @param graph Project graph + * @param projectName The project name to get dependencies for + * @returns A list of all non-buildable libraries that the project depends on, including transitive dependencies. + */ + +export function getNonBuildableLibs( + graph: ProjectGraph, + projectName: string +): string[] { + const deps = graph?.dependencies?.[projectName] ?? []; + + const allNonBuildable = new Set(); + + // First, find all direct non-buildable deps and add them App -> library + const directNonBuildable = deps.filter((dep) => { + const node = graph.nodes?.[dep.target]; + if (!node || node.type !== 'lib') return false; + const hasBuildTarget = 'build' in (node.data?.targets ?? {}); + if (hasBuildTarget) return false; + return !isBuildableLibrary(node); + }); + + // Add direct non-buildable dependencies + for (const dep of directNonBuildable) { + const packageName = + graph.nodes?.[dep.target]?.data?.metadata?.js?.packageName; + if (packageName) { + allNonBuildable.add(packageName); + } + + // Get all transitive non-buildable dependencies App -> library1 -> library2 + const transitiveDeps = getAllTransitiveDeps(graph, dep.target); + transitiveDeps.forEach((pkg) => allNonBuildable.add(pkg)); + } + + return Array.from(allNonBuildable); +} diff --git a/packages/rspack/src/plugins/utils/get-transitive-deps.ts b/packages/rspack/src/plugins/utils/get-transitive-deps.ts new file mode 100644 index 0000000000..90438cfb17 --- /dev/null +++ b/packages/rspack/src/plugins/utils/get-transitive-deps.ts @@ -0,0 +1,56 @@ +import { type ProjectGraph } from '@nx/devkit'; +import { isBuildableLibrary } from './is-lib-buildable'; + +/** + * Get all transitive dependencies of a target that are non-buildable libraries. + * This function traverses the project graph to find all dependencies of a given target, + * @param graph Graph of the project + * @param targetName The project name to get dependencies for + * @param visited Set to keep track of visited nodes to prevent infinite loops in circular dependencies + * @returns string[] - List of all transitive dependencies that are non-buildable libraries + */ +export function getAllTransitiveDeps( + graph: ProjectGraph, + targetName: string, + visited = new Set() +): string[] { + if (visited.has(targetName)) { + return []; + } + + visited.add(targetName); + + const node = graph.nodes?.[targetName]; + if (!node) { + return []; + } + + // Get direct dependencies of this target + const directDeps = graph.dependencies?.[targetName] || []; + const transitiveDeps: string[] = []; + + for (const dep of directDeps) { + const depNode = graph.nodes?.[dep.target]; + + // Only consider library dependencies + if (!depNode || depNode.type !== 'lib') { + continue; + } + + // Check if this dependency is non-buildable + const hasBuildTarget = 'build' in (depNode.data?.targets ?? {}); + const isBuildable = hasBuildTarget || isBuildableLibrary(depNode); + + if (!isBuildable) { + const packageName = depNode.data?.metadata?.js?.packageName; + if (packageName) { + transitiveDeps.push(packageName); + } + + const nestedDeps = getAllTransitiveDeps(graph, dep.target, visited); + transitiveDeps.push(...nestedDeps); + } + } + + return transitiveDeps; +} diff --git a/packages/webpack/src/plugins/nx-webpack-plugin/lib/apply-base-config.ts b/packages/webpack/src/plugins/nx-webpack-plugin/lib/apply-base-config.ts index fe0562986f..2402d1f831 100644 --- a/packages/webpack/src/plugins/nx-webpack-plugin/lib/apply-base-config.ts +++ b/packages/webpack/src/plugins/nx-webpack-plugin/lib/apply-base-config.ts @@ -20,7 +20,7 @@ import { NormalizedNxAppWebpackPluginOptions } from '../nx-app-webpack-plugin-op import TerserPlugin = require('terser-webpack-plugin'); import nodeExternals = require('webpack-node-externals'); import { isUsingTsSolutionSetup } from '@nx/js/src/utils/typescript/ts-solution-setup'; -import { isBuildableLibrary } from './utils'; +import { getNonBuildableLibs } from './utils'; const IGNORED_WEBPACK_WARNINGS = [ /The comment file/i, @@ -355,32 +355,12 @@ function applyNxDependentConfig( const graph = options.projectGraph; const projectName = options.projectName; - const deps = graph?.dependencies?.[projectName] ?? []; - // Collect non-buildable TS project references so that they are bundled // in the final output. This is needed for projects that are not buildable - // but are referenced by buildable projects. This is needed for the new TS - // solution setup. + // but are referenced by buildable projects. + const nonBuildableWorkspaceLibs = isUsingTsSolution - ? deps - .filter((dep) => { - const node = graph.nodes?.[dep.target]; - if (!node || node.type !== 'lib') return false; - - const hasBuildTarget = 'build' in (node.data?.targets ?? {}); - - if (hasBuildTarget) { - return false; - } - - // If there is no build target we check the package exports to see if they reference - // source files - return !isBuildableLibrary(node); - }) - .map( - (dep) => graph.nodes?.[dep.target]?.data?.metadata?.js?.packageName - ) - .filter((name): name is string => !!name) + ? getNonBuildableLibs(graph, projectName) : []; externals.push( diff --git a/packages/webpack/src/plugins/nx-webpack-plugin/lib/utils.ts b/packages/webpack/src/plugins/nx-webpack-plugin/lib/utils.ts index e276296efa..89d799a6be 100644 --- a/packages/webpack/src/plugins/nx-webpack-plugin/lib/utils.ts +++ b/packages/webpack/src/plugins/nx-webpack-plugin/lib/utils.ts @@ -1,4 +1,4 @@ -import { type ProjectGraphProjectNode } from '@nx/devkit'; +import type { ProjectGraph, ProjectGraphProjectNode } from '@nx/devkit'; function isSourceFile(path: string): boolean { return ['.ts', '.tsx', '.mts', '.cts'].some((ext) => path.endsWith(ext)); @@ -56,3 +56,98 @@ export function isBuildableLibrary(node: ProjectGraphProjectNode): boolean { !isSourceFile(packageMain) ); } +/** + * Get all transitive dependencies of a target that are non-buildable libraries. + * This function traverses the project graph to find all dependencies of a given target, + * @param graph Graph of the project + * @param targetName The project name to get dependencies for + * @param visited Set to keep track of visited nodes to prevent infinite loops in circular dependencies + * @returns string[] - List of all transitive dependencies that are non-buildable libraries + */ +export function getAllTransitiveDeps( + graph: ProjectGraph, + targetName: string, + visited = new Set() +): string[] { + if (visited.has(targetName)) { + return []; + } + + visited.add(targetName); + + const node = graph.nodes?.[targetName]; + if (!node) { + return []; + } + + // Get direct dependencies of this target + const directDeps = graph.dependencies?.[targetName] || []; + const transitiveDeps: string[] = []; + + for (const dep of directDeps) { + const depNode = graph.nodes?.[dep.target]; + + // Only consider library dependencies + if (!depNode || depNode.type !== 'lib') { + continue; + } + + // Check if this dependency is non-buildable + const hasBuildTarget = 'build' in (depNode.data?.targets ?? {}); + const isBuildable = hasBuildTarget || isBuildableLibrary(depNode); + + if (!isBuildable) { + const packageName = depNode.data?.metadata?.js?.packageName; + if (packageName) { + transitiveDeps.push(packageName); + } + + const nestedDeps = getAllTransitiveDeps(graph, dep.target, visited); + transitiveDeps.push(...nestedDeps); + } + } + + return transitiveDeps; +} + +/** + * Get all non-buildable libraries in the project graph for a given project. + * This function retrieves all direct and transitive dependencies of a project, + * filtering out only those that are libraries and not buildable. + * @param graph Project graph + * @param projectName The project name to get dependencies for + * @returns A list of all non-buildable libraries that the project depends on, including transitive dependencies. + */ + +export function getNonBuildableLibs( + graph: ProjectGraph, + projectName: string +): string[] { + const deps = graph?.dependencies?.[projectName] ?? []; + + const allNonBuildable = new Set(); + + // First, find all direct non-buildable deps and add them App -> library + const directNonBuildable = deps.filter((dep) => { + const node = graph.nodes?.[dep.target]; + if (!node || node.type !== 'lib') return false; + const hasBuildTarget = 'build' in (node.data?.targets ?? {}); + if (hasBuildTarget) return false; + return !isBuildableLibrary(node); + }); + + // Add direct non-buildable dependencies + for (const dep of directNonBuildable) { + const packageName = + graph.nodes?.[dep.target]?.data?.metadata?.js?.packageName; + if (packageName) { + allNonBuildable.add(packageName); + } + + // Get all transitive non-buildable dependencies App -> library1 -> library2 + const transitiveDeps = getAllTransitiveDeps(graph, dep.target); + transitiveDeps.forEach((pkg) => allNonBuildable.add(pkg)); + } + + return Array.from(allNonBuildable); +}