fix(webpack): add support for retrieving all transitive non-buildable dependencies (#31343)
This PR improves dependency resolution for Node.js apps using Webpack or Rspack. While we already handle direct dependencies for non-buildable libraries, this update ensures that **transitive dependencies** are also properly included. This guarantees that all necessary dependencies are bundled when the main app/library is built. closes: https://github.com/nrwl/nx/issues/31334
This commit is contained in:
parent
4a94841916
commit
f1171191dd
@ -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`);
|
||||
|
||||
|
||||
@ -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(
|
||||
|
||||
45
packages/rspack/src/plugins/utils/get-non-buildable-libs.ts
Normal file
45
packages/rspack/src/plugins/utils/get-non-buildable-libs.ts
Normal file
@ -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<string>();
|
||||
|
||||
// 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);
|
||||
}
|
||||
56
packages/rspack/src/plugins/utils/get-transitive-deps.ts
Normal file
56
packages/rspack/src/plugins/utils/get-transitive-deps.ts
Normal file
@ -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>()
|
||||
): 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;
|
||||
}
|
||||
@ -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(
|
||||
|
||||
@ -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>()
|
||||
): 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<string>();
|
||||
|
||||
// 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);
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user