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`);
|
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`);
|
runCLI(`build ${nestApp} --verbose`);
|
||||||
checkFilesExist(`apps/${nestApp}/dist/main.js`);
|
checkFilesExist(`apps/${nestApp}/dist/main.js`);
|
||||||
|
|
||||||
|
|||||||
@ -19,7 +19,7 @@ import { getTerserEcmaVersion } from './get-terser-ecma-version';
|
|||||||
import nodeExternals = require('webpack-node-externals');
|
import nodeExternals = require('webpack-node-externals');
|
||||||
import { NormalizedNxAppRspackPluginOptions } from './models';
|
import { NormalizedNxAppRspackPluginOptions } from './models';
|
||||||
import { isUsingTsSolutionSetup } from '@nx/js/src/utils/typescript/ts-solution-setup';
|
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 = [
|
const IGNORED_RSPACK_WARNINGS = [
|
||||||
/The comment file/i,
|
/The comment file/i,
|
||||||
@ -357,32 +357,13 @@ function applyNxDependentConfig(
|
|||||||
const graph = options.projectGraph;
|
const graph = options.projectGraph;
|
||||||
const projectName = options.projectName;
|
const projectName = options.projectName;
|
||||||
|
|
||||||
const deps = graph?.dependencies?.[projectName] ?? [];
|
|
||||||
|
|
||||||
// Collect non-buildable TS project references so that they are bundled
|
// Collect non-buildable TS project references so that they are bundled
|
||||||
// in the final output. This is needed for projects that are not buildable
|
// 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
|
// but are referenced by buildable projects. This is needed for the new TS
|
||||||
// solution setup.
|
// solution setup.
|
||||||
|
|
||||||
const nonBuildableWorkspaceLibs = isUsingTsSolution
|
const nonBuildableWorkspaceLibs = isUsingTsSolution
|
||||||
? deps
|
? getNonBuildableLibs(graph, projectName)
|
||||||
.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)
|
|
||||||
: [];
|
: [];
|
||||||
|
|
||||||
externals.push(
|
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 TerserPlugin = require('terser-webpack-plugin');
|
||||||
import nodeExternals = require('webpack-node-externals');
|
import nodeExternals = require('webpack-node-externals');
|
||||||
import { isUsingTsSolutionSetup } from '@nx/js/src/utils/typescript/ts-solution-setup';
|
import { isUsingTsSolutionSetup } from '@nx/js/src/utils/typescript/ts-solution-setup';
|
||||||
import { isBuildableLibrary } from './utils';
|
import { getNonBuildableLibs } from './utils';
|
||||||
|
|
||||||
const IGNORED_WEBPACK_WARNINGS = [
|
const IGNORED_WEBPACK_WARNINGS = [
|
||||||
/The comment file/i,
|
/The comment file/i,
|
||||||
@ -355,32 +355,12 @@ function applyNxDependentConfig(
|
|||||||
const graph = options.projectGraph;
|
const graph = options.projectGraph;
|
||||||
const projectName = options.projectName;
|
const projectName = options.projectName;
|
||||||
|
|
||||||
const deps = graph?.dependencies?.[projectName] ?? [];
|
|
||||||
|
|
||||||
// Collect non-buildable TS project references so that they are bundled
|
// Collect non-buildable TS project references so that they are bundled
|
||||||
// in the final output. This is needed for projects that are not buildable
|
// 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
|
// but are referenced by buildable projects.
|
||||||
// solution setup.
|
|
||||||
const nonBuildableWorkspaceLibs = isUsingTsSolution
|
const nonBuildableWorkspaceLibs = isUsingTsSolution
|
||||||
? deps
|
? getNonBuildableLibs(graph, projectName)
|
||||||
.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)
|
|
||||||
: [];
|
: [];
|
||||||
|
|
||||||
externals.push(
|
externals.push(
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import { type ProjectGraphProjectNode } from '@nx/devkit';
|
import type { ProjectGraph, ProjectGraphProjectNode } from '@nx/devkit';
|
||||||
|
|
||||||
function isSourceFile(path: string): boolean {
|
function isSourceFile(path: string): boolean {
|
||||||
return ['.ts', '.tsx', '.mts', '.cts'].some((ext) => path.endsWith(ext));
|
return ['.ts', '.tsx', '.mts', '.cts'].some((ext) => path.endsWith(ext));
|
||||||
@ -56,3 +56,98 @@ export function isBuildableLibrary(node: ProjectGraphProjectNode): boolean {
|
|||||||
!isSourceFile(packageMain)
|
!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