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

View File

@ -202,7 +202,7 @@ describe('app', () => {
],
"compiler": "tsc",
"main": "myapp/src/main.ts",
"outputPath": "dist/myapp",
"outputPath": "myapp/dist",
"target": "node",
"tsConfig": "myapp/tsconfig.app.json",
"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)
? {
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'),
tsConfig: './tsconfig.app.json',
assets: ['./src/assets'],
@ -650,10 +656,12 @@ async function normalizeOptions(
unitTestRunner: options.unitTestRunner ?? 'jest',
rootProject: options.rootProject ?? false,
port: options.port ?? 3000,
outputPath: joinPathFragments(
'dist',
options.rootProject ? options.name : appProjectRoot
),
outputPath: isUsingTsSolutionConfig
? joinPathFragments(appProjectRoot, 'dist')
: joinPathFragments(
'dist',
options.rootProject ? options.name : appProjectRoot
),
isUsingTsSolutionConfig,
swcJest,
};

View File

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

View File

@ -5,6 +5,7 @@ import {
ProjectGraph,
readJson,
readNxJson,
readProjectConfiguration,
Tree,
updateJson,
updateNxJson,
@ -1554,6 +1555,92 @@ describe('app', () => {
'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', () => {

View File

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

View File

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

View File

@ -5,12 +5,12 @@ import {
ProjectConfiguration,
TargetConfiguration,
Tree,
updateProjectConfiguration,
writeJson,
} from '@nx/devkit';
import { hasWebpackPlugin } from '../../../utils/has-webpack-plugin';
import { maybeJs } from '../../../utils/maybe-js';
import { hasRspackPlugin } from '../../../utils/has-rspack-plugin';
import { getImportPath } from '@nx/js/src/utils/get-import-path';
export function addProject(host: Tree, options: NormalizedSchema) {
const project: ProjectConfiguration = {
@ -43,11 +43,6 @@ export function addProject(host: Tree, options: NormalizedSchema) {
name: options.projectName,
version: '0.0.1',
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, {
...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}'],
defaultConfiguration: 'production',
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'),
baseHref: '/',
main: joinPathFragments(
@ -139,12 +151,14 @@ function createBuildTarget(options: NormalizedSchema): TargetConfiguration {
defaultConfiguration: 'production',
options: {
compiler: options.compiler ?? 'babel',
outputPath: joinPathFragments(
'dist',
options.appProjectRoot != '.'
? options.appProjectRoot
: options.projectName
),
outputPath: options.isUsingTsSolutionConfig
? joinPathFragments(options.appProjectRoot, 'dist')
: joinPathFragments(
'dist',
options.appProjectRoot !== '.'
? options.appProjectRoot
: options.projectName
),
index: joinPathFragments(options.appProjectRoot, 'src/index.html'),
baseHref: '/',
main: joinPathFragments(

View File

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

View File

@ -9,6 +9,7 @@ import {
updateProjectConfiguration,
} from '@nx/devkit';
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 ConfigurationSchema } from '../generators/configuration/schema';
import { type Framework } from '../generators/init/schema';
@ -171,13 +172,17 @@ export function addOrChangeBuildTarget(
assets.push(joinPathFragments(project.root, 'src/assets'));
}
const isTsSolutionSetup = isUsingTsSolutionSetup(tree);
const buildOptions: RspackExecutorSchema = {
target: options.target ?? 'web',
outputPath: joinPathFragments(
'dist',
// If standalone project then use the project's name in dist.
project.root === '.' ? project.name : project.root
),
outputPath: isTsSolutionSetup
? joinPathFragments(project.root, 'dist')
: joinPathFragments(
'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'),
main: determineMain(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', () => {
beforeEach(() => {
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 {
const rootOffset = offsetFromRoot(options.appProjectRoot);
generateFiles(
tree,
join(__dirname, './files/app-webpack'),
@ -91,18 +92,21 @@ function createApplicationFiles(tree: Tree, options: NormalizedSchema) {
...options,
...names(options.name),
tmpl: '',
offsetFromRoot: offsetFromRoot(options.appProjectRoot),
offsetFromRoot: rootOffset,
rootTsConfigPath,
webpackPluginOptions: hasWebpackPlugin(tree)
? {
compiler: options.compiler,
target: 'web',
outputPath: joinPathFragments(
'dist',
options.appProjectRoot != '.'
? options.appProjectRoot
: options.projectName
),
outputPath: options.isUsingTsSolutionConfig
? 'dist'
: joinPathFragments(
rootOffset,
'dist',
options.appProjectRoot !== '.'
? options.appProjectRoot
: options.projectName
),
tsConfig: './tsconfig.app.json',
main: './src/main.ts',
assets: ['./src/favicon.ico', './src/assets'],
@ -181,7 +185,7 @@ async function setupBundler(tree: Tree, options: NormalizedSchema) {
addPlugin: options.addPlugin,
});
const project = readProjectConfiguration(tree, options.projectName);
if (project.targets.build) {
if (project.targets?.build) {
const prodConfig = project.targets.build.configurations.production;
const buildOptions = project.targets.build.options;
buildOptions.assets = assets;

View File

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