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:
parent
538fd8cbf6
commit
a9a486aa21
@ -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');
|
||||||
|
|||||||
@ -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)) {
|
||||||
|
|||||||
58
packages/webpack/src/plugins/nx-webpack-plugin/lib/utils.ts
Normal file
58
packages/webpack/src/plugins/nx-webpack-plugin/lib/utils.ts
Normal 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)
|
||||||
|
);
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user