fix(webpack): nestjs workspace libs referencing when using ts solution (#30538)

<!-- Please make sure you have read the submission guidelines before
posting an PR -->
<!--
https://github.com/nrwl/nx/blob/master/CONTRIBUTING.md#-submitting-a-pr
-->

<!-- Please make sure that your commit message follows our format -->
<!-- Example: `fix(nx): must begin with lowercase` -->

<!-- If this is a particularly complex change or feature addition, you
can request a dedicated Nx release for this pull request branch. Mention
someone from the Nx team or the `@nrwl/nx-pipelines-reviewers` and they
will confirm if the PR warrants its own release for testing purposes,
and generate it for you if appropriate. -->

## Current Behavior
<!-- This is the behavior we have today -->
Currently, when you have a Nest app that imports a Nest Lib that is not
buildable inside a TS solution workspace the serving that application
fails because the library is marked as external.

Before we would include these projects as a part of
`tsconfig.compilerOptions.paths`. However, this is no longer possible as
it would not be a valid TS solution setup.

## Expected Behavior
<!-- This is the behavior we should expect with the changes in this PR
-->
If a library is not buildable it should be able to be resolved
regardless if we are using TS solutions or the legacy paths.

## Related Issue(s)
<!-- Please link the issue being fixed so it gets closed when this is
merged. -->

closes: #30492, #30410, #30544
This commit is contained in:
Nicholas Cunningham 2025-04-02 08:55:37 -06:00 committed by GitHub
parent 538fd8cbf6
commit a9a486aa21
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 154 additions and 6 deletions

View File

@ -1,8 +1,8 @@
import { import {
checkFilesExist, checkFilesExist,
cleanupProject, cleanupProject,
getPackageManagerCommand,
getSelectedPackageManager, getSelectedPackageManager,
getPackageManagerCommand,
killPorts, killPorts,
newProject, newProject,
promisifiedTreeKill, promisifiedTreeKill,
@ -22,10 +22,11 @@ let originalEnvPort;
describe('Node Applications', () => { describe('Node Applications', () => {
const pm = getSelectedPackageManager(); const pm = getSelectedPackageManager();
let workspaceName: string;
beforeAll(() => { beforeAll(() => {
originalEnvPort = process.env.PORT; originalEnvPort = process.env.PORT;
newProject({ workspaceName = newProject({
packages: ['@nx/node', '@nx/express', '@nx/nest', '@nx/webpack'], packages: ['@nx/node', '@nx/express', '@nx/nest', '@nx/webpack'],
preset: 'ts', preset: 'ts',
}); });
@ -84,18 +85,18 @@ describe('Node Applications', () => {
updateFile(`apps/${nodeapp}/src/assets/file.txt`, `Test`); updateFile(`apps/${nodeapp}/src/assets/file.txt`, `Test`);
updateFile(`apps/${nodeapp}/src/main.ts`, (content) => { updateFile(`apps/${nodeapp}/src/main.ts`, (content) => {
return `import { ${nodelib} } from '@proj/${nodelib}';\n${content}\nconsole.log(${nodelib}());`; return `import { ${nodelib} } from '@${workspaceName}/${nodelib}';\n${content}\nconsole.log(${nodelib}());`;
}); });
// pnpm does not link packages unless they are deps // pnpm does not link packages unless they are deps
// npm, yarn, and bun will link them in the root node_modules regardless // npm, yarn, and bun will link them in the root node_modules regardless
if (pm === 'pnpm') { if (pm === 'pnpm') {
updateJson(`apps/${nodeapp}/package.json`, (json) => { updateJson(`apps/${nodeapp}/package.json`, (json) => {
json.dependencies = { json.dependencies = {
[`@proj/${nodelib}`]: 'workspace:', [`@${workspaceName}/${nodelib}`]: 'workspace:*',
}; };
return json; return json;
}); });
runCommand(getPackageManagerCommand().install); runCommand(`cd apps/${nodeapp} && ${getPackageManagerCommand().install}`);
} }
runCLI(`sync`); runCLI(`sync`);
@ -172,6 +173,60 @@ describe('Node Applications', () => {
} }
}, 300_000); }, 300_000);
it('should be able to import a lib into a nest application', async () => {
const nestApp = uniq('nestapp');
const nestLib = uniq('nestlib');
const port = getRandomPort();
process.env.PORT = `${port}`;
runCLI(`generate @nx/nest:app apps/${nestApp} --no-interactive`);
runCLI(`generate @nx/nest:lib packages/${nestLib} --no-interactive`);
updateFile(`apps/${nestApp}/src/app/app.module.ts`, (content) => {
return `import '@${workspaceName}/${nestLib}';\n${content}\n`;
});
if (pm === 'pnpm') {
updateJson(`apps/${nestApp}/package.json`, (json) => {
json.dependencies = {
[`@${workspaceName}/${nestLib}`]: 'workspace:*',
};
return json;
});
runCommand(`${getPackageManagerCommand().install}`);
}
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);
it('should respect and support generating libraries with a name different than the import path', () => { it('should respect and support generating libraries with a name different than the import path', () => {
const nodeLib = uniq('node-lib'); const nodeLib = uniq('node-lib');
const nestLib = uniq('nest-lib'); const nestLib = uniq('nest-lib');

View File

@ -20,6 +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';
const IGNORED_WEBPACK_WARNINGS = [ const IGNORED_WEBPACK_WARNINGS = [
/The comment file/i, /The comment file/i,
@ -350,7 +351,41 @@ function applyNxDependentConfig(
const externals = []; const externals = [];
if (options.target === 'node' && options.externalDependencies === 'all') { if (options.target === 'node' && options.externalDependencies === 'all') {
const modulesDir = `${options.root}/node_modules`; const modulesDir = `${options.root}/node_modules`;
externals.push(nodeExternals({ modulesDir }));
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)
: [];
externals.push(
nodeExternals({ modulesDir, allowlist: nonBuildableWorkspaceLibs })
);
} else if (Array.isArray(options.externalDependencies)) { } else if (Array.isArray(options.externalDependencies)) {
externals.push(function (ctx, callback: Function) { externals.push(function (ctx, callback: Function) {
if (options.externalDependencies.includes(ctx.request)) { if (options.externalDependencies.includes(ctx.request)) {

View File

@ -0,0 +1,58 @@
import { type ProjectGraphProjectNode } from '@nx/devkit';
function isSourceFile(path: string): boolean {
return ['.ts', '.tsx', '.mts', '.cts'].some((ext) => path.endsWith(ext));
}
function isBuildableExportMap(packageExports: any): boolean {
if (!packageExports || Object.keys(packageExports).length === 0) {
return false; // exports = {} → not buildable
}
const isCompiledExport = (value: unknown): boolean => {
if (typeof value === 'string') {
return !isSourceFile(value);
}
if (typeof value === 'object' && value !== null) {
return Object.entries(value).some(([key, subValue]) => {
if (
key === 'types' ||
key === 'development' ||
key === './package.json'
)
return false;
return typeof subValue === 'string' && !isSourceFile(subValue);
});
}
return false;
};
if (packageExports['.']) {
return isCompiledExport(packageExports['.']);
}
return Object.entries(packageExports).some(
([key, value]) => key !== '.' && isCompiledExport(value)
);
}
/**
* Check if the library is buildable.
* @param node from the project graph
* @returns boolean
*/
export function isBuildableLibrary(node: ProjectGraphProjectNode): boolean {
if (!node.data.metadata?.js) {
return false;
}
const { packageExports, packageMain } = node.data.metadata.js;
// if we have exports only check this else fallback to packageMain
if (packageExports) {
return isBuildableExportMap(packageExports);
}
return (
typeof packageMain === 'string' &&
packageMain !== '' &&
!isSourceFile(packageMain)
);
}