fix(misc): generate config with output contained within project root (#29850)

Updates some generators to ensure the build tool produces the output
contained within the project root for the TS solution setup.

## Current Behavior

## Expected Behavior

## Related Issue(s)

Fixes #
This commit is contained in:
Leosvel Pérez Espinosa 2025-02-04 15:16:02 +01:00 committed by GitHub
parent 1fbcd73cde
commit 8d056c9cdb
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
14 changed files with 331 additions and 55 deletions

View File

@ -49,8 +49,8 @@ describe('Node Applications', () => {
updateFile(`apps/${nodeapp}/src/main.ts`, `console.log('Hello World!');`); updateFile(`apps/${nodeapp}/src/main.ts`, `console.log('Hello World!');`);
runCLI(`build ${nodeapp}`); runCLI(`build ${nodeapp}`);
checkFilesExist(`dist/apps/${nodeapp}/main.js`); checkFilesExist(`apps/${nodeapp}/dist/main.js`);
const result = execSync(`node dist/apps/${nodeapp}/main.js`, { const result = execSync(`node apps/${nodeapp}/dist/main.js`, {
cwd: tmpProjPath(), cwd: tmpProjPath(),
}).toString(); }).toString();
expect(result).toContain('Hello World!'); expect(result).toContain('Hello World!');
@ -144,7 +144,7 @@ describe('Node Applications', () => {
expect(() => runCLI(`test ${nestapp}`)).not.toThrow(); expect(() => runCLI(`test ${nestapp}`)).not.toThrow();
runCLI(`build ${nestapp}`); runCLI(`build ${nestapp}`);
checkFilesExist(`dist/apps/${nestapp}/main.js`); checkFilesExist(`apps/${nestapp}/dist/main.js`);
const p = await runCommandUntil( const p = await runCommandUntil(
`serve ${nestapp}`, `serve ${nestapp}`,

View File

@ -202,7 +202,7 @@ describe('app', () => {
], ],
"compiler": "tsc", "compiler": "tsc",
"main": "myapp/src/main.ts", "main": "myapp/src/main.ts",
"outputPath": "dist/myapp", "outputPath": "myapp/dist",
"target": "node", "target": "node",
"tsConfig": "myapp/tsconfig.app.json", "tsConfig": "myapp/tsconfig.app.json",
"webpackConfig": "myapp/webpack.config.js", "webpackConfig": "myapp/webpack.config.js",

View File

@ -775,5 +775,67 @@ describe('app', () => {
" "
`); `);
}); });
it('should configure webpack correctly with the output contained within the project root', async () => {
await applicationGenerator(tree, {
directory: 'apps/my-app',
bundler: 'webpack',
addPlugin: true,
skipFormat: true,
});
expect(tree.read('apps/my-app/webpack.config.js', 'utf-8'))
.toMatchInlineSnapshot(`
"const { NxAppWebpackPlugin } = require('@nx/webpack/app-plugin');
const { join } = require('path');
module.exports = {
output: {
path: join(__dirname, 'dist'),
},
plugins: [
new NxAppWebpackPlugin({
target: 'node',
compiler: 'tsc',
main: './src/main.ts',
tsConfig: './tsconfig.app.json',
assets: ["./src/assets"],
optimization: false,
outputHashing: 'none',
generatePackageJson: true,
})
],
};
"
`);
});
it('should configure webpack build task correctly with the output contained within the project root', async () => {
await applicationGenerator(tree, {
directory: 'apps/my-app',
bundler: 'webpack',
addPlugin: false,
skipFormat: true,
});
expect(
readProjectConfiguration(tree, 'my-app').targets.build.options
.outputPath
).toBe('apps/my-app/dist');
});
it('should configure esbuild build task correctly with the output contained within the project root', async () => {
await applicationGenerator(tree, {
directory: 'apps/my-app',
bundler: 'esbuild',
addPlugin: false,
skipFormat: true,
});
expect(
readProjectConfiguration(tree, 'my-app').targets.build.options
.outputPath
).toBe('apps/my-app/dist');
});
}); });
}); });

View File

@ -246,7 +246,13 @@ function addAppFiles(tree: Tree, options: NormalizedSchema) {
), ),
webpackPluginOptions: hasWebpackPlugin(tree) webpackPluginOptions: hasWebpackPlugin(tree)
? { ? {
outputPath: options.outputPath, outputPath: options.isUsingTsSolutionConfig
? 'dist'
: joinPathFragments(
offsetFromRoot(options.appProjectRoot),
'dist',
options.rootProject ? options.name : options.appProjectRoot
),
main: './src/main' + (options.js ? '.js' : '.ts'), main: './src/main' + (options.js ? '.js' : '.ts'),
tsConfig: './tsconfig.app.json', tsConfig: './tsconfig.app.json',
assets: ['./src/assets'], assets: ['./src/assets'],
@ -650,10 +656,12 @@ async function normalizeOptions(
unitTestRunner: options.unitTestRunner ?? 'jest', unitTestRunner: options.unitTestRunner ?? 'jest',
rootProject: options.rootProject ?? false, rootProject: options.rootProject ?? false,
port: options.port ?? 3000, port: options.port ?? 3000,
outputPath: joinPathFragments( outputPath: isUsingTsSolutionConfig
'dist', ? joinPathFragments(appProjectRoot, 'dist')
options.rootProject ? options.name : appProjectRoot : joinPathFragments(
), 'dist',
options.rootProject ? options.name : appProjectRoot
),
isUsingTsSolutionConfig, isUsingTsSolutionConfig,
swcJest, swcJest,
}; };

View File

@ -4,7 +4,7 @@ const { join } = require('path');
module.exports = { module.exports = {
output: { output: {
path: join(__dirname, '<%= offset %><%= webpackPluginOptions.outputPath %>'), path: join(__dirname, '<%= webpackPluginOptions.outputPath %>'),
}, },
plugins: [ plugins: [
new NxAppWebpackPlugin({ new NxAppWebpackPlugin({

View File

@ -5,6 +5,7 @@ import {
ProjectGraph, ProjectGraph,
readJson, readJson,
readNxJson, readNxJson,
readProjectConfiguration,
Tree, Tree,
updateJson, updateJson,
updateNxJson, updateNxJson,
@ -1554,6 +1555,92 @@ describe('app', () => {
'packages/shared/*', 'packages/shared/*',
]); ]);
}); });
it('should configure webpack correctly with the output contained within the project root', async () => {
await applicationGenerator(appTree, {
directory: 'apps/my-app',
bundler: 'webpack',
linter: Linter.EsLint,
style: 'none',
e2eTestRunner: 'none',
addPlugin: true,
skipFormat: true,
});
expect(appTree.read('apps/my-app/webpack.config.js', 'utf-8'))
.toMatchInlineSnapshot(`
"const { NxAppWebpackPlugin } = require('@nx/webpack/app-plugin');
const { NxReactWebpackPlugin } = require('@nx/react/webpack-plugin');
const { join } = require('path');
module.exports = {
output: {
path: join(__dirname, 'dist'),
},
devServer: {
port: 4200,
historyApiFallback: {
index: '/index.html',
disableDotRule: true,
htmlAcceptHeaders: ['text/html', 'application/xhtml+xml'],
},
},
plugins: [
new NxAppWebpackPlugin({
tsConfig: './tsconfig.app.json',
compiler: 'babel',
main: './src/main.tsx',
index: './src/index.html',
baseHref: '/',
assets: ["./src/favicon.ico","./src/assets"],
styles: [],
outputHashing: process.env['NODE_ENV'] === 'production' ? 'all' : 'none',
optimization: process.env['NODE_ENV'] === 'production',
}),
new NxReactWebpackPlugin({
// Uncomment this line if you don't want to use SVGR
// See: https://react-svgr.com/
// svgr: false
}),
],
};
"
`);
});
it('should configure webpack build task correctly with the output contained within the project root', async () => {
await applicationGenerator(appTree, {
directory: 'apps/my-app',
bundler: 'webpack',
linter: Linter.EsLint,
style: 'none',
e2eTestRunner: 'none',
addPlugin: false,
skipFormat: true,
});
expect(
readProjectConfiguration(appTree, '@proj/my-app').targets.build.options
.outputPath
).toBe('apps/my-app/dist');
});
it('should configure rspack build task correctly with the output contained within the project root', async () => {
await applicationGenerator(appTree, {
directory: 'apps/my-app',
bundler: 'rspack',
linter: Linter.EsLint,
style: 'none',
e2eTestRunner: 'none',
addPlugin: true,
skipFormat: true,
});
expect(
readProjectConfiguration(appTree, '@proj/my-app').targets.build.options
.outputPath
).toBe('apps/my-app/dist');
});
}); });
describe('--bundler=rsbuild', () => { describe('--bundler=rsbuild', () => {

View File

@ -5,7 +5,7 @@ const { join } = require('path');
module.exports = { module.exports = {
output: { output: {
path: join(__dirname, '<%= offsetFromRoot %><%= rspackPluginOptions.outputPath %>'), path: join(__dirname, '<%= rspackPluginOptions.outputPath %>'),
}, },
devServer: { devServer: {
port: 4200, port: 4200,

View File

@ -5,7 +5,7 @@ const { join } = require('path');
module.exports = { module.exports = {
output: { output: {
path: join(__dirname, '<%= offsetFromRoot %><%= webpackPluginOptions.outputPath %>'), path: join(__dirname, '<%= webpackPluginOptions.outputPath %>'),
}, },
devServer: { devServer: {
port: 4200, port: 4200,

View File

@ -5,12 +5,12 @@ import {
ProjectConfiguration, ProjectConfiguration,
TargetConfiguration, TargetConfiguration,
Tree, Tree,
updateProjectConfiguration,
writeJson, writeJson,
} from '@nx/devkit'; } from '@nx/devkit';
import { hasWebpackPlugin } from '../../../utils/has-webpack-plugin'; import { hasWebpackPlugin } from '../../../utils/has-webpack-plugin';
import { maybeJs } from '../../../utils/maybe-js'; import { maybeJs } from '../../../utils/maybe-js';
import { hasRspackPlugin } from '../../../utils/has-rspack-plugin'; import { hasRspackPlugin } from '../../../utils/has-rspack-plugin';
import { getImportPath } from '@nx/js/src/utils/get-import-path';
export function addProject(host: Tree, options: NormalizedSchema) { export function addProject(host: Tree, options: NormalizedSchema) {
const project: ProjectConfiguration = { const project: ProjectConfiguration = {
@ -43,11 +43,6 @@ export function addProject(host: Tree, options: NormalizedSchema) {
name: options.projectName, name: options.projectName,
version: '0.0.1', version: '0.0.1',
private: true, private: true,
nx: options.parsedTags?.length
? {
tags: options.parsedTags,
}
: undefined,
}); });
} }
@ -55,6 +50,16 @@ export function addProject(host: Tree, options: NormalizedSchema) {
addProjectConfiguration(host, options.projectName, { addProjectConfiguration(host, options.projectName, {
...project, ...project,
}); });
} else if (
options.parsedTags?.length ||
Object.keys(project.targets).length
) {
const updatedProject: ProjectConfiguration = {
root: options.appProjectRoot,
targets: project.targets,
tags: options.parsedTags?.length ? options.parsedTags : undefined,
};
updateProjectConfiguration(host, options.projectName, updatedProject);
} }
} }
@ -66,7 +71,14 @@ function createRspackBuildTarget(
outputs: ['{options.outputPath}'], outputs: ['{options.outputPath}'],
defaultConfiguration: 'production', defaultConfiguration: 'production',
options: { options: {
outputPath: joinPathFragments('dist', options.appProjectRoot), outputPath: options.isUsingTsSolutionConfig
? joinPathFragments(options.appProjectRoot, 'dist')
: joinPathFragments(
'dist',
options.appProjectRoot !== '.'
? options.appProjectRoot
: options.projectName
),
index: joinPathFragments(options.appProjectRoot, 'src/index.html'), index: joinPathFragments(options.appProjectRoot, 'src/index.html'),
baseHref: '/', baseHref: '/',
main: joinPathFragments( main: joinPathFragments(
@ -139,12 +151,14 @@ function createBuildTarget(options: NormalizedSchema): TargetConfiguration {
defaultConfiguration: 'production', defaultConfiguration: 'production',
options: { options: {
compiler: options.compiler ?? 'babel', compiler: options.compiler ?? 'babel',
outputPath: joinPathFragments( outputPath: options.isUsingTsSolutionConfig
'dist', ? joinPathFragments(options.appProjectRoot, 'dist')
options.appProjectRoot != '.' : joinPathFragments(
? options.appProjectRoot 'dist',
: options.projectName options.appProjectRoot !== '.'
), ? options.appProjectRoot
: options.projectName
),
index: joinPathFragments(options.appProjectRoot, 'src/index.html'), index: joinPathFragments(options.appProjectRoot, 'src/index.html'),
baseHref: '/', baseHref: '/',
main: joinPathFragments( main: joinPathFragments(

View File

@ -86,7 +86,10 @@ export async function createApplicationFiles(
{ {
...templateVariables, ...templateVariables,
webpackPluginOptions: hasWebpackPlugin(host) webpackPluginOptions: hasWebpackPlugin(host)
? createNxWebpackPluginOptions(options) ? createNxWebpackPluginOptions(
options,
templateVariables.offsetFromRoot
)
: null, : null,
} }
); );
@ -151,7 +154,10 @@ export async function createApplicationFiles(
{ {
...templateVariables, ...templateVariables,
rspackPluginOptions: hasRspackPlugin(host) rspackPluginOptions: hasRspackPlugin(host)
? createNxRspackPluginOptions(options) ? createNxRspackPluginOptions(
options,
templateVariables.offsetFromRoot
)
: null, : null,
} }
); );
@ -211,17 +217,21 @@ export async function createApplicationFiles(
} }
function createNxWebpackPluginOptions( function createNxWebpackPluginOptions(
options: NormalizedSchema options: NormalizedSchema,
rootOffset: string
): WithNxOptions & WithReactOptions { ): WithNxOptions & WithReactOptions {
return { return {
target: 'web', target: 'web',
compiler: options.compiler ?? 'babel', compiler: options.compiler ?? 'babel',
outputPath: joinPathFragments( outputPath: options.isUsingTsSolutionConfig
'dist', ? 'dist'
options.appProjectRoot != '.' : joinPathFragments(
? options.appProjectRoot rootOffset,
: options.projectName 'dist',
), options.appProjectRoot != '.'
? options.appProjectRoot
: options.projectName
),
index: './src/index.html', index: './src/index.html',
baseHref: '/', baseHref: '/',
main: maybeJs( main: maybeJs(
@ -245,16 +255,20 @@ function createNxWebpackPluginOptions(
} }
function createNxRspackPluginOptions( function createNxRspackPluginOptions(
options: NormalizedSchema options: NormalizedSchema,
rootOffset: string
): WithNxOptions & WithReactOptions { ): WithNxOptions & WithReactOptions {
return { return {
target: 'web', target: 'web',
outputPath: joinPathFragments( outputPath: options.isUsingTsSolutionConfig
'dist', ? 'dist'
options.appProjectRoot != '.' : joinPathFragments(
? options.appProjectRoot rootOffset,
: options.projectName 'dist',
), options.appProjectRoot != '.'
? options.appProjectRoot
: options.projectName
),
index: './src/index.html', index: './src/index.html',
baseHref: '/', baseHref: '/',
main: maybeJs( main: maybeJs(

View File

@ -9,6 +9,7 @@ import {
updateProjectConfiguration, updateProjectConfiguration,
} from '@nx/devkit'; } from '@nx/devkit';
import { ensureTypescript } from '@nx/js/src/utils/typescript/ensure-typescript'; import { ensureTypescript } from '@nx/js/src/utils/typescript/ensure-typescript';
import { isUsingTsSolutionSetup } from '@nx/js/src/utils/typescript/ts-solution-setup';
import { type RspackExecutorSchema } from '../executors/rspack/schema'; import { type RspackExecutorSchema } from '../executors/rspack/schema';
import { type ConfigurationSchema } from '../generators/configuration/schema'; import { type ConfigurationSchema } from '../generators/configuration/schema';
import { type Framework } from '../generators/init/schema'; import { type Framework } from '../generators/init/schema';
@ -171,13 +172,17 @@ export function addOrChangeBuildTarget(
assets.push(joinPathFragments(project.root, 'src/assets')); assets.push(joinPathFragments(project.root, 'src/assets'));
} }
const isTsSolutionSetup = isUsingTsSolutionSetup(tree);
const buildOptions: RspackExecutorSchema = { const buildOptions: RspackExecutorSchema = {
target: options.target ?? 'web', target: options.target ?? 'web',
outputPath: joinPathFragments( outputPath: isTsSolutionSetup
'dist', ? joinPathFragments(project.root, 'dist')
// If standalone project then use the project's name in dist. : joinPathFragments(
project.root === '.' ? project.name : project.root 'dist',
), // If standalone project then use the project's name in dist.
project.root === '.' ? project.name : project.root
),
index: joinPathFragments(project.root, 'src/index.html'), index: joinPathFragments(project.root, 'src/index.html'),
main: determineMain(tree, options), main: determineMain(tree, options),
tsConfig: determineTsConfig(tree, options), tsConfig: determineTsConfig(tree, options),

View File

@ -697,6 +697,48 @@ describe('app', () => {
}); });
}); });
describe('--bundler=webpack', () => {
it('should configure webpack correctly', async () => {
await applicationGenerator(tree, {
directory: 'apps/my-app',
bundler: 'webpack',
addPlugin: true,
skipFormat: true,
});
expect(tree.read('apps/my-app/webpack.config.js', 'utf-8'))
.toMatchInlineSnapshot(`
"
const { NxAppWebpackPlugin } = require('@nx/webpack/app-plugin');
const { join } = require('path');
module.exports = {
output: {
path: join(__dirname, '../../dist/apps/my-app'),
},
devServer: {
port: 4200
},
plugins: [
new NxAppWebpackPlugin({
tsConfig: './tsconfig.app.json',
compiler: 'babel',
main: './src/main.ts',
index: './src/index.html',
baseHref: '/',
assets: ["./src/favicon.ico","./src/assets"],
styles: ["./src/styles.css"],
outputHashing: process.env['NODE_ENV'] === 'production' ? 'all' : 'none',
optimization: process.env['NODE_ENV'] === 'production',
})
],
};
"
`);
});
});
describe('TS solution setup', () => { describe('TS solution setup', () => {
beforeEach(() => { beforeEach(() => {
tree = createTreeWithEmptyWorkspace(); tree = createTreeWithEmptyWorkspace();
@ -852,5 +894,45 @@ describe('app', () => {
} }
`); `);
}); });
it('should configure webpack correctly with the output contained within the project root', async () => {
await applicationGenerator(tree, {
directory: 'apps/my-app',
bundler: 'webpack',
addPlugin: true,
skipFormat: true,
});
expect(tree.read('apps/my-app/webpack.config.js', 'utf-8'))
.toMatchInlineSnapshot(`
"
const { NxAppWebpackPlugin } = require('@nx/webpack/app-plugin');
const { join } = require('path');
module.exports = {
output: {
path: join(__dirname, 'dist'),
},
devServer: {
port: 4200
},
plugins: [
new NxAppWebpackPlugin({
tsConfig: './tsconfig.app.json',
compiler: 'babel',
main: './src/main.ts',
index: './src/index.html',
baseHref: '/',
assets: ["./src/favicon.ico","./src/assets"],
styles: ["./src/styles.css"],
outputHashing: process.env['NODE_ENV'] === 'production' ? 'all' : 'none',
optimization: process.env['NODE_ENV'] === 'production',
})
],
};
"
`);
});
}); });
}); });

View File

@ -83,6 +83,7 @@ function createApplicationFiles(tree: Tree, options: NormalizedSchema) {
} }
); );
} else { } else {
const rootOffset = offsetFromRoot(options.appProjectRoot);
generateFiles( generateFiles(
tree, tree,
join(__dirname, './files/app-webpack'), join(__dirname, './files/app-webpack'),
@ -91,18 +92,21 @@ function createApplicationFiles(tree: Tree, options: NormalizedSchema) {
...options, ...options,
...names(options.name), ...names(options.name),
tmpl: '', tmpl: '',
offsetFromRoot: offsetFromRoot(options.appProjectRoot), offsetFromRoot: rootOffset,
rootTsConfigPath, rootTsConfigPath,
webpackPluginOptions: hasWebpackPlugin(tree) webpackPluginOptions: hasWebpackPlugin(tree)
? { ? {
compiler: options.compiler, compiler: options.compiler,
target: 'web', target: 'web',
outputPath: joinPathFragments( outputPath: options.isUsingTsSolutionConfig
'dist', ? 'dist'
options.appProjectRoot != '.' : joinPathFragments(
? options.appProjectRoot rootOffset,
: options.projectName 'dist',
), options.appProjectRoot !== '.'
? options.appProjectRoot
: options.projectName
),
tsConfig: './tsconfig.app.json', tsConfig: './tsconfig.app.json',
main: './src/main.ts', main: './src/main.ts',
assets: ['./src/favicon.ico', './src/assets'], assets: ['./src/favicon.ico', './src/assets'],
@ -181,7 +185,7 @@ async function setupBundler(tree: Tree, options: NormalizedSchema) {
addPlugin: options.addPlugin, addPlugin: options.addPlugin,
}); });
const project = readProjectConfiguration(tree, options.projectName); const project = readProjectConfiguration(tree, options.projectName);
if (project.targets.build) { if (project.targets?.build) {
const prodConfig = project.targets.build.configurations.production; const prodConfig = project.targets.build.configurations.production;
const buildOptions = project.targets.build.options; const buildOptions = project.targets.build.options;
buildOptions.assets = assets; buildOptions.assets = assets;

View File

@ -4,7 +4,7 @@ const { join } = require('path');
module.exports = { module.exports = {
output: { output: {
path: join(__dirname, '<%= offsetFromRoot %><%= webpackPluginOptions.outputPath %>'), path: join(__dirname, '<%= webpackPluginOptions.outputPath %>'),
}, },
devServer: { devServer: {
port: 4200 port: 4200