feat(webpack): generate React and Web apps with webpack.config.js file (#14285)

This commit is contained in:
Jack Hsu 2023-01-12 10:06:25 -05:00 committed by GitHub
parent 0925c294d1
commit 454fba49b2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
78 changed files with 404 additions and 291 deletions

View File

@ -0,0 +1,35 @@
import {
checkFilesDoNotExist,
checkFilesExist,
cleanupProject,
newProject,
runCLI,
runCLIAsync,
tmpProjPath,
uniq,
updateFile,
} from '@nrwl/e2e/utils';
import { execSync } from 'child_process';
describe('Node Applications + esbuild', () => {
beforeEach(() => newProject());
afterEach(() => cleanupProject());
it('should generate an app using esbuild', async () => {
const app = uniq('nodeapp');
runCLI(`generate @nrwl/node:app ${app} --bundler=esbuild --no-interactive`);
checkFilesDoNotExist(`apps/${app}/webpack.config.js`);
updateFile(`apps/${app}/src/main.ts`, `console.log('Hello World!');`);
await runCLIAsync(`build ${app}`);
checkFilesExist(`dist/apps/${app}/main.cjs`);
const result = execSync(`node dist/apps/${app}/main.cjs`, {
cwd: tmpProjPath(),
}).toString();
expect(result).toMatch(/Hello World!/);
}, 300_000);
});

View File

@ -0,0 +1,35 @@
import {
checkFilesDoNotExist,
checkFilesExist,
cleanupProject,
newProject,
runCLI,
runCLIAsync,
tmpProjPath,
uniq,
updateFile,
} from '@nrwl/e2e/utils';
import { execSync } from 'child_process';
describe('Node Applications + webpack', () => {
beforeEach(() => newProject());
afterEach(() => cleanupProject());
it('should generate an app using webpack', async () => {
const app = uniq('nodeapp');
runCLI(`generate @nrwl/node:app ${app} --bundler=webpack --no-interactive`);
checkFilesExist(`apps/${app}/webpack.config.js`);
updateFile(`apps/${app}/src/main.ts`, `console.log('Hello World!');`);
await runCLIAsync(`build ${app}`);
checkFilesExist(`dist/apps/${app}/main.js`);
const result = execSync(`node dist/apps/${app}/main.js`, {
cwd: tmpProjPath(),
}).toString();
expect(result).toMatch(/Hello World!/);
}, 300_000);
});

View File

@ -59,8 +59,8 @@ describe('Node Applications', () => {
updateFile(`apps/${nodeapp}/src/main.ts`, `console.log('Hello World!');`);
await runCLIAsync(`build ${nodeapp}`);
checkFilesExist(`dist/apps/${nodeapp}/main.js`);
const result = execSync(`node dist/apps/${nodeapp}/main.js`, {
checkFilesExist(`dist/apps/${nodeapp}/main.cjs`);
const result = execSync(`node dist/apps/${nodeapp}/main.cjs`, {
cwd: tmpProjPath(),
}).toString();
expect(result).toContain('Hello World!');
@ -76,13 +76,15 @@ describe('Node Applications', () => {
});
await runCLIAsync(`build ${nodeapp}`);
checkFilesExist(`dist/apps/${nodeapp}/index.js`);
checkFilesExist(`dist/apps/${nodeapp}/index.cjs`);
}, 300000);
it('should be able to generate an empty application with additional entries', async () => {
const nodeapp = uniq('nodeapp');
runCLI(`generate @nrwl/node:app ${nodeapp} --linter=eslint`);
runCLI(
`generate @nrwl/node:app ${nodeapp} --linter=eslint --bundler=webpack`
);
const lintResults = runCLI(`lint ${nodeapp}`);
expect(lintResults).toContain('All files pass linting.');
@ -267,7 +269,7 @@ describe('Build Node apps', () => {
expect(satisfies(packageJson.dependencies['tslib'], '^2.3.0')).toBeTruthy();
const nodeapp = uniq('nodeapp');
runCLI(`generate @nrwl/node:app ${nodeapp}`);
runCLI(`generate @nrwl/node:app ${nodeapp} --bundler=webpack`);
const jslib = uniq('jslib');
runCLI(`generate @nrwl/js:lib ${jslib} --buildable`);

View File

@ -48,19 +48,26 @@ describe('React Module Federation', () => {
updateFile(
`apps/${shell}/webpack.config.js`,
stripIndents`
const { withModuleFederation } = require('@nrwl/react/module-federation');
const moduleFederationConfig = require('./module-federation.config');
module.exports = withModuleFederation({
...moduleFederationConfig,
remotes: [
'${remote1}',
['${remote2}', 'http://localhost:${readPort(
import { ModuleFederationConfig } from '@nrwl/devkit';
import { composePlugins, withNx } from '@nrwl/webpack';
import { withReact } from '@nrwl/react';
import { withModuleFederation } from '@nrwl/react/module-federation');
const baseConfig = require('./module-federation.config');
const config: ModuleFederationConfig = {
...baseConfig,
remotes: [
'${remote1}',
['${remote2}', 'http://localhost:${readPort(
remote2
)}/remoteEntry.js'],
['${remote3}', 'http://localhost:${readPort(remote3)}'],
],
});
['${remote3}', 'http://localhost:${readPort(remote3)}'],
],
};
// Nx plugins for webpack to build config object from Nx options and context.
module.exports = composePlugins(withNx(), withReact(), withModuleFederation(config));
`
);

View File

@ -66,6 +66,7 @@ export async function applicationGenerator(tree: Tree, schema: Schema) {
const initTask = await initGenerator(tree, { ...options, skipFormat: true });
const applicationTask = await nodeApplicationGenerator(tree, {
...schema,
bundler: 'webpack',
skipFormat: true,
});
addMainFile(tree, options);

View File

@ -43,5 +43,6 @@ export function toNodeApplicationGeneratorOptions(
tags: options.tags,
unitTestRunner: options.unitTestRunner,
setParserOptionsProject: options.setParserOptionsProject,
bundler: 'webpack', // Some features require webpack plugins such as TS transformers
};
}

View File

@ -1,6 +1,11 @@
import * as devkit from '@nrwl/devkit';
import { getProjects, NxJsonConfiguration, readJson, Tree } from '@nrwl/devkit';
import { createTreeWithEmptyV1Workspace } from '@nrwl/devkit/testing';
import {
getProjects,
readJson,
readProjectConfiguration,
Tree,
} from '@nrwl/devkit';
import { createTreeWithEmptyWorkspace } from '@nrwl/devkit/testing';
// nx-ignore-next-line
import { applicationGenerator as angularApplicationGenerator } from '@nrwl/angular/generators';
@ -13,7 +18,7 @@ describe('app', () => {
let tree: Tree;
beforeEach(() => {
tree = createTreeWithEmptyV1Workspace();
tree = createTreeWithEmptyWorkspace();
overrideCollectionResolutionForTesting({
'@nrwl/cypress': join(__dirname, '../../../../cypress/generators.json'),
@ -32,27 +37,26 @@ describe('app', () => {
});
describe('not nested', () => {
it('should update workspace.json', async () => {
it('should update project config', async () => {
await applicationGenerator(tree, {
name: 'myNodeApp',
standaloneConfig: false,
});
const workspaceJson = readJson(tree, '/workspace.json');
const nxJson = readJson<NxJsonConfiguration>(tree, 'nx.json');
const project = workspaceJson.projects['my-node-app'];
expect(project.root).toEqual('apps/my-node-app');
expect(project.architect).toEqual(
const project = readProjectConfiguration(tree, 'my-node-app');
expect(project.root).toEqual('my-node-app');
expect(project.targets).toEqual(
expect.objectContaining({
build: {
builder: '@nrwl/webpack:webpack',
executor: '@nrwl/webpack:webpack',
outputs: ['{options.outputPath}'],
options: {
target: 'node',
compiler: 'tsc',
outputPath: 'dist/apps/my-node-app',
main: 'apps/my-node-app/src/main.ts',
tsConfig: 'apps/my-node-app/tsconfig.app.json',
assets: ['apps/my-node-app/src/assets'],
outputPath: 'dist/my-node-app',
main: 'my-node-app/src/main.ts',
tsConfig: 'my-node-app/tsconfig.app.json',
webpackConfig: 'my-node-app/webpack.config.js',
assets: ['my-node-app/src/assets'],
},
configurations: {
production: {
@ -63,7 +67,7 @@ describe('app', () => {
},
},
serve: {
builder: '@nrwl/js:node',
executor: '@nrwl/js:node',
options: {
buildTarget: 'my-node-app:build',
},
@ -75,14 +79,16 @@ describe('app', () => {
},
})
);
expect(workspaceJson.projects['my-node-app'].architect.lint).toEqual({
builder: '@nrwl/linter:eslint',
expect(project.targets.lint).toEqual({
executor: '@nrwl/linter:eslint',
outputs: ['{options.outputFile}'],
options: {
lintFilePatterns: ['apps/my-node-app/**/*.ts'],
lintFilePatterns: ['my-node-app/**/*.ts'],
},
});
expect(workspaceJson.projects['my-node-app-e2e']).toBeUndefined();
expect(() => readProjectConfiguration(tree, 'my-node-app-e2e')).toThrow(
/Cannot find/
);
});
it('should update tags', async () => {
@ -104,13 +110,13 @@ describe('app', () => {
name: 'myNodeApp',
standaloneConfig: false,
});
expect(tree.exists(`apps/my-node-app/jest.config.ts`)).toBeTruthy();
expect(tree.exists('apps/my-node-app/src/main.ts')).toBeTruthy();
expect(tree.exists(`my-node-app/jest.config.ts`)).toBeTruthy();
expect(tree.exists('my-node-app/src/main.ts')).toBeTruthy();
const tsconfig = readJson(tree, 'apps/my-node-app/tsconfig.json');
const tsconfig = readJson(tree, 'my-node-app/tsconfig.json');
expect(tsconfig).toMatchInlineSnapshot(`
Object {
"extends": "../../tsconfig.base.json",
"extends": "../tsconfig.base.json",
"files": Array [],
"include": Array [],
"references": Array [
@ -124,19 +130,19 @@ describe('app', () => {
}
`);
const tsconfigApp = readJson(tree, 'apps/my-node-app/tsconfig.app.json');
expect(tsconfigApp.compilerOptions.outDir).toEqual('../../dist/out-tsc');
const tsconfigApp = readJson(tree, 'my-node-app/tsconfig.app.json');
expect(tsconfigApp.compilerOptions.outDir).toEqual('../dist/out-tsc');
expect(tsconfigApp.extends).toEqual('./tsconfig.json');
expect(tsconfigApp.exclude).toEqual([
'jest.config.ts',
'src/**/*.spec.ts',
'src/**/*.test.ts',
]);
const eslintrc = readJson(tree, 'apps/my-node-app/.eslintrc.json');
const eslintrc = readJson(tree, 'my-node-app/.eslintrc.json');
expect(eslintrc).toMatchInlineSnapshot(`
Object {
"extends": Array [
"../../.eslintrc.json",
"../.eslintrc.json",
],
"ignorePatterns": Array [
"!**/*",
@ -178,36 +184,33 @@ describe('app', () => {
standaloneConfig: false,
});
const tsconfig = readJson(tree, 'apps/my-node-app/tsconfig.json');
expect(tsconfig.extends).toBe('../../tsconfig.json');
const tsconfig = readJson(tree, 'my-node-app/tsconfig.json');
expect(tsconfig.extends).toBe('../tsconfig.json');
});
});
describe('nested', () => {
it('should update workspace.json', async () => {
it('should update project config', async () => {
await applicationGenerator(tree, {
name: 'myNodeApp',
directory: 'myDir',
standaloneConfig: false,
});
const workspaceJson = readJson(tree, '/workspace.json');
const nxJson = readJson<NxJsonConfiguration>(tree, 'nx.json');
const project = readProjectConfiguration(tree, 'my-dir-my-node-app');
expect(workspaceJson.projects['my-dir-my-node-app'].root).toEqual(
'apps/my-dir/my-node-app'
);
expect(project.root).toEqual('my-dir/my-node-app');
expect(
workspaceJson.projects['my-dir-my-node-app'].architect.lint
).toEqual({
builder: '@nrwl/linter:eslint',
expect(project.targets.lint).toEqual({
executor: '@nrwl/linter:eslint',
outputs: ['{options.outputFile}'],
options: {
lintFilePatterns: ['apps/my-dir/my-node-app/**/*.ts'],
lintFilePatterns: ['my-dir/my-node-app/**/*.ts'],
},
});
expect(workspaceJson.projects['my-dir-my-node-app-e2e']).toBeUndefined();
expect(() =>
readProjectConfiguration(tree, 'my-dir-my-node-app-e2e')
).toThrow(/Cannot find/);
});
it('should update tags', async () => {
@ -239,8 +242,8 @@ describe('app', () => {
// Make sure these exist
[
`apps/my-dir/my-node-app/jest.config.ts`,
'apps/my-dir/my-node-app/src/main.ts',
`my-dir/my-node-app/jest.config.ts`,
'my-dir/my-node-app/src/main.ts',
].forEach((path) => {
expect(tree.exists(path)).toBeTruthy();
});
@ -248,17 +251,17 @@ describe('app', () => {
// Make sure these have properties
[
{
path: 'apps/my-dir/my-node-app/tsconfig.app.json',
path: 'my-dir/my-node-app/tsconfig.app.json',
lookupFn: (json) => json.compilerOptions.outDir,
expectedValue: '../../../dist/out-tsc',
expectedValue: '../../dist/out-tsc',
},
{
path: 'apps/my-dir/my-node-app/tsconfig.app.json',
path: 'my-dir/my-node-app/tsconfig.app.json',
lookupFn: (json) => json.compilerOptions.types,
expectedValue: ['node'],
},
{
path: 'apps/my-dir/my-node-app/tsconfig.app.json',
path: 'my-dir/my-node-app/tsconfig.app.json',
lookupFn: (json) => json.exclude,
expectedValue: [
'jest.config.ts',
@ -267,9 +270,9 @@ describe('app', () => {
],
},
{
path: 'apps/my-dir/my-node-app/.eslintrc.json',
path: 'my-dir/my-node-app/.eslintrc.json',
lookupFn: (json) => json.extends,
expectedValue: ['../../../.eslintrc.json'],
expectedValue: ['../../.eslintrc.json'],
},
].forEach(hasJsonValue);
});
@ -283,21 +286,18 @@ describe('app', () => {
standaloneConfig: false,
});
expect(tree.exists('jest.config.ts')).toBeFalsy();
expect(tree.exists('apps/my-node-app/src/test-setup.ts')).toBeFalsy();
expect(tree.exists('apps/my-node-app/src/test.ts')).toBeFalsy();
expect(tree.exists('apps/my-node-app/tsconfig.spec.json')).toBeFalsy();
expect(tree.exists('apps/my-node-app/jest.config.ts')).toBeFalsy();
const workspaceJson = readJson(tree, 'workspace.json');
expect(
workspaceJson.projects['my-node-app'].architect.test
).toBeUndefined();
expect(workspaceJson.projects['my-node-app'].architect.lint)
.toMatchInlineSnapshot(`
expect(tree.exists('my-node-app/src/test-setup.ts')).toBeFalsy();
expect(tree.exists('my-node-app/src/test.ts')).toBeFalsy();
expect(tree.exists('my-node-app/tsconfig.spec.json')).toBeFalsy();
expect(tree.exists('my-node-app/jest.config.ts')).toBeFalsy();
const project = readProjectConfiguration(tree, 'my-node-app');
expect(project.targets.test).toBeUndefined();
expect(project.targets.lint).toMatchInlineSnapshot(`
Object {
"builder": "@nrwl/linter:eslint",
"executor": "@nrwl/linter:eslint",
"options": Object {
"lintFilePatterns": Array [
"apps/my-node-app/**/*.ts",
"my-node-app/**/*.ts",
],
},
"outputs": Array [
@ -318,12 +318,10 @@ describe('app', () => {
standaloneConfig: false,
});
expect(tree.exists('apps/my-frontend/proxy.conf.json')).toBeTruthy();
const serve = readJson(tree, 'workspace.json').projects['my-frontend']
.architect.serve;
expect(serve.options.proxyConfig).toEqual(
'apps/my-frontend/proxy.conf.json'
);
expect(tree.exists('my-frontend/proxy.conf.json')).toBeTruthy();
const project = readProjectConfiguration(tree, 'my-frontend');
const serve = project.targets.serve;
expect(serve.options.proxyConfig).toEqual('my-frontend/proxy.conf.json');
});
it('should configure proxies for multiple node projects with the same frontend app', async () => {
@ -341,9 +339,9 @@ describe('app', () => {
standaloneConfig: false,
});
expect(tree.exists('apps/my-frontend/proxy.conf.json')).toBeTruthy();
expect(tree.exists('my-frontend/proxy.conf.json')).toBeTruthy();
expect(readJson(tree, 'apps/my-frontend/proxy.conf.json')).toEqual({
expect(readJson(tree, 'my-frontend/proxy.conf.json')).toEqual({
'/api': { target: 'http://localhost:3333', secure: false },
'/billing-api': { target: 'http://localhost:3333', secure: false },
});
@ -358,12 +356,10 @@ describe('app', () => {
standaloneConfig: false,
});
expect(tree.exists('apps/my-frontend/proxy.conf.json')).toBeTruthy();
const serve = readJson(tree, 'workspace.json').projects['my-frontend']
.architect.serve;
expect(serve.options.proxyConfig).toEqual(
'apps/my-frontend/proxy.conf.json'
);
expect(tree.exists('my-frontend/proxy.conf.json')).toBeTruthy();
const project = readProjectConfiguration(tree, 'my-frontend');
const serve = project.targets.serve;
expect(serve.options.proxyConfig).toEqual('my-frontend/proxy.conf.json');
});
});
@ -375,18 +371,18 @@ describe('app', () => {
babelJest: true,
} as Schema);
expect(tree.read(`apps/my-node-app/jest.config.ts`, 'utf-8'))
expect(tree.read(`my-node-app/jest.config.ts`, 'utf-8'))
.toMatchInlineSnapshot(`
"/* eslint-disable */
export default {
displayName: 'my-node-app',
preset: '../../jest.preset.js',
preset: '../jest.preset.js',
testEnvironment: 'node',
transform: {
'^.+\\\\\\\\.[tj]s$': 'babel-jest'
},
moduleFileExtensions: ['ts', 'js', 'html'],
coverageDirectory: '../../coverage/apps/my-node-app'
coverageDirectory: '../coverage/my-node-app'
};
"
`);
@ -399,15 +395,15 @@ describe('app', () => {
js: true,
} as Schema);
expect(tree.exists(`apps/my-node-app/jest.config.js`)).toBeTruthy();
expect(tree.exists('apps/my-node-app/src/main.js')).toBeTruthy();
expect(tree.exists(`my-node-app/jest.config.js`)).toBeTruthy();
expect(tree.exists('my-node-app/src/main.js')).toBeTruthy();
const tsConfig = readJson(tree, 'apps/my-node-app/tsconfig.json');
const tsConfig = readJson(tree, 'my-node-app/tsconfig.json');
expect(tsConfig.compilerOptions).toEqual({
allowJs: true,
});
const tsConfigApp = readJson(tree, 'apps/my-node-app/tsconfig.app.json');
const tsConfigApp = readJson(tree, 'my-node-app/tsconfig.app.json');
expect(tsConfigApp.include).toEqual(['src/**/*.ts', 'src/**/*.js']);
expect(tsConfigApp.exclude).toEqual([
'jest.config.ts',
@ -418,16 +414,15 @@ describe('app', () => {
]);
});
it('should update workspace.json', async () => {
it('should add project config', async () => {
await applicationGenerator(tree, {
name: 'myNodeApp',
js: true,
} as Schema);
const workspaceJson = readJson(tree, '/workspace.json');
const project = workspaceJson.projects['my-node-app'];
const buildTarget = project.architect.build;
const project = readProjectConfiguration(tree, 'my-node-app');
const buildTarget = project.targets.build;
expect(buildTarget.options.main).toEqual('apps/my-node-app/src/main.js');
expect(buildTarget.options.main).toEqual('my-node-app/src/main.js');
});
it('should generate js files for nested libs as well', async () => {
@ -436,10 +431,8 @@ describe('app', () => {
directory: 'myDir',
js: true,
} as Schema);
expect(
tree.exists(`apps/my-dir/my-node-app/jest.config.js`)
).toBeTruthy();
expect(tree.exists('apps/my-dir/my-node-app/src/main.js')).toBeTruthy();
expect(tree.exists(`my-dir/my-node-app/jest.config.js`)).toBeTruthy();
expect(tree.exists('my-dir/my-node-app/src/main.js')).toBeTruthy();
});
});

View File

@ -65,6 +65,10 @@ function getWebpackBuildConfig(
),
tsConfig: joinPathFragments(options.appProjectRoot, 'tsconfig.app.json'),
assets: [joinPathFragments(project.sourceRoot, 'assets')],
webpackConfig: joinPathFragments(
options.appProjectRoot,
'webpack.config.js'
),
},
configurations: {
production: {
@ -151,6 +155,10 @@ function addAppFiles(tree: Tree, options: NormalizedSchema) {
}
);
if (options.bundler !== 'webpack') {
tree.delete(joinPathFragments(options.appProjectRoot, 'webpack.config.js'));
}
if (options.framework) {
generateFiles(
tree,
@ -358,11 +366,6 @@ function normalizeOptions(host: Tree, options: Schema): NormalizedSchema {
const appProjectName = appDirectory.replace(new RegExp('/', 'g'), '-');
const appProjectRoot = joinPathFragments(appsDir, appDirectory);
if (options.framework) {
options.bundler = options.bundler ?? 'esbuild';
} else {
options.bundler = 'webpack';
}
const parsedTags = options.tags
? options.tags.split(',').map((s) => s.trim())

View File

@ -0,0 +1,8 @@
const { composePlugins, withNx} = require('@nrwl/webpack');
// Nx plugins for webpack.
module.exports = composePlugins(withNx(), (config) => {
// Update the webpack config as needed here.
// e.g. `config.plugins.push(new MyPlugin())`
return config;
});

View File

@ -22,3 +22,4 @@ export { cypressComponentConfigGenerator } from './src/generators/cypress-compon
export { componentTestGenerator } from './src/generators/component-test/component-test';
export { setupTailwindGenerator } from './src/generators/setup-tailwind/setup-tailwind';
export type { SupportedStyles } from './typings/style';
export * from './plugins/with-react';

View File

@ -1,4 +1,5 @@
import {
ExecutorContext,
joinPathFragments,
logger,
readJsonFile,
@ -15,7 +16,7 @@ import { gte } from 'semver';
import { Configuration, DefinePlugin, WebpackPluginInstance } from 'webpack';
import * as mergeWebpack from 'webpack-merge';
import { mergePlugins } from './merge-plugins';
import { withReact } from '../webpack';
import { withReact } from '../with-react';
import { withNx, withWeb } from '@nrwl/webpack';
// This is shamelessly taken from CRA and modified for NX use
@ -128,7 +129,10 @@ export const webpack = async (
withWeb(),
withReact()
);
const finalConfig = configure(baseWebpackConfig, { options: builderOptions });
const finalConfig = configure(baseWebpackConfig, {
options: builderOptions,
context: {} as ExecutorContext, // The context is not used here.
});
// Check whether the project .babelrc uses @emotion/babel-plugin. There's currently
// a Storybook issue (https://github.com/storybookjs/storybook/issues/13277) which apparently

View File

@ -1,69 +1,4 @@
import type { Configuration } from 'webpack';
import ReactRefreshPlugin = require('@pmmmwh/react-refresh-webpack-plugin');
import { NormalizedWebpackExecutorOptions } from '@nrwl/webpack';
import { ExecutorContext } from 'nx/src/config/misc-interfaces';
// Add React-specific configuration
export function withReact() {
return function configure(
config: Configuration,
_ctx?: {
options: NormalizedWebpackExecutorOptions;
context: ExecutorContext;
}
): Configuration {
config.module.rules.push({
test: /\.svg$/,
issuer: /\.(js|ts|md)x?$/,
use: [
{
loader: require.resolve('@svgr/webpack'),
options: {
svgo: false,
titleProp: true,
ref: true,
},
},
{
loader: require.resolve('file-loader'),
options: {
name: '[name].[hash].[ext]',
},
},
],
});
if (config.mode === 'development' && config['devServer']?.hot) {
// add `react-refresh/babel` to babel loader plugin
const babelLoader = config.module.rules.find(
(rule) =>
typeof rule !== 'string' &&
rule.loader?.toString().includes('babel-loader')
);
if (babelLoader && typeof babelLoader !== 'string') {
babelLoader.options['plugins'] = [
...(babelLoader.options['plugins'] || []),
[
require.resolve('react-refresh/babel'),
{
skipEnvCheck: true,
},
],
];
}
// add https://github.com/pmmmwh/react-refresh-webpack-plugin to webpack plugin
config.plugins.push(new ReactRefreshPlugin());
}
// enable webpack node api
config.node = {
__dirname: true,
__filename: true,
};
return config;
};
}
import { withReact } from './with-react';
// Support existing default exports as well as new named export.
const legacyExport: any = withReact();

View File

@ -0,0 +1,69 @@
import type { Configuration } from 'webpack';
import { NormalizedWebpackExecutorOptions } from '@nrwl/webpack';
import { ExecutorContext } from 'nx/src/config/misc-interfaces';
const processed = new Set();
export function withReact() {
return function configure(
config: Configuration,
_ctx?: {
options: NormalizedWebpackExecutorOptions;
context: ExecutorContext;
}
): Configuration {
if (processed.has(config)) return config;
config.module.rules.push({
test: /\.svg$/,
issuer: /\.(js|ts|md)x?$/,
use: [
{
loader: require.resolve('@svgr/webpack'),
options: {
svgo: false,
titleProp: true,
ref: true,
},
},
{
loader: require.resolve('file-loader'),
options: {
name: '[name].[hash].[ext]',
},
},
],
});
if (config.mode === 'development' && config['devServer']?.hot) {
// add `react-refresh/babel` to babel loader plugin
const babelLoader = config.module.rules.find(
(rule) =>
typeof rule !== 'string' &&
rule.loader?.toString().includes('babel-loader')
);
if (babelLoader && typeof babelLoader !== 'string') {
babelLoader.options['plugins'] = [
...(babelLoader.options['plugins'] || []),
[
require.resolve('react-refresh/babel'),
{
skipEnvCheck: true,
},
],
];
}
const ReactRefreshPlugin = require('@pmmmwh/react-refresh-webpack-plugin');
config.plugins.push(new ReactRefreshPlugin());
}
// enable webpack node api
config.node = {
__dirname: true,
__filename: true,
};
processed.add(config);
return config;
};
}

View File

@ -314,7 +314,7 @@ describe('app', () => {
scripts: [],
styles: ['apps/my-app/src/styles.css'],
tsConfig: 'apps/my-app/tsconfig.app.json',
webpackConfig: '@nrwl/react/plugins/webpack',
webpackConfig: 'apps/my-app/webpack.config.js',
});
expect(targetConfig.build.configurations.production).toEqual({
optimization: true,
@ -792,7 +792,7 @@ describe('app', () => {
expect(
workspaceJson.get('my-app').targets.build.options.webpackConfig
).toEqual('@nrwl/react/plugins/webpack');
).toEqual('apps/my-app/webpack.config.js');
});
it('should NOT add custom webpack config if bundler is vite', async () => {

View File

@ -0,0 +1,9 @@
const { composePlugins, withNx } = require('@nrwl/webpack');
const { withReact } = require('@nrwl/react');
// Nx plugins for webpack.
module.exports = composePlugins(withNx(), withReact(), (config) => {
// Update the webpack config as needed here.
// e.g. `config.plugins.push(new MyPlugin())`
return config;
});

View File

@ -72,7 +72,10 @@ function createBuildTarget(options: NormalizedSchema): TargetConfiguration {
),
],
scripts: [],
webpackConfig: '@nrwl/react/plugins/webpack',
webpackConfig: joinPathFragments(
options.appProjectRoot,
'webpack.config.js'
),
},
configurations: {
development: {

View File

@ -9,15 +9,15 @@ import { getAppTests } from './get-app-tests';
export function createApplicationFiles(host: Tree, options: NormalizedSchema) {
let styleSolutionSpecificAppFiles: string;
if (options.styledModule && options.style !== 'styled-jsx') {
styleSolutionSpecificAppFiles = '../files/styled-module';
styleSolutionSpecificAppFiles = '../files/style-styled-module';
} else if (options.style === 'styled-jsx') {
styleSolutionSpecificAppFiles = '../files/styled-jsx';
styleSolutionSpecificAppFiles = '../files/style-styled-jsx';
} else if (options.style === 'none') {
styleSolutionSpecificAppFiles = '../files/none';
styleSolutionSpecificAppFiles = '../files/style-none';
} else if (options.globalCss) {
styleSolutionSpecificAppFiles = '../files/global-css';
styleSolutionSpecificAppFiles = '../files/style-global-css';
} else {
styleSolutionSpecificAppFiles = '../files/css-module';
styleSolutionSpecificAppFiles = '../files/style-css-module';
}
const relativePathToRootTsConfig = getRelativePathToRootTsConfig(
@ -38,7 +38,9 @@ export function createApplicationFiles(host: Tree, options: NormalizedSchema) {
host,
join(
__dirname,
options.bundler === 'vite' ? '../files/common-vite' : '../files/common'
options.bundler === 'vite'
? '../files/base-vite'
: '../files/base-webpack'
),
options.appProjectRoot,
templateVariables

View File

@ -1,8 +1,12 @@
const { withModuleFederationForSSR } = require('@nrwl/react/module-federation');
const baseConfig = require("./module-federation.server.config");
import { composePlugins, withNx } from '@nrwl/webpack';
import { withReact } from '@nrwl/react';
import { withModuleFederationForSSR } from '@nrwl/react/module-federation';
const baseConfig = require('./module-federation.config');
const defaultConfig = {
...baseConfig
};
module.exports = withModuleFederationForSSR(defaultConfig);
// Nx plugins for webpack to build config object from Nx options and context.
module.exports = composePlugins(withNx(), withReact(), withModuleFederationForSSR(defaultConfig));

View File

@ -1,13 +1,6 @@
// @ts-check
/**
* @type {import('@nrwl/devkit').ModuleFederationConfig}
**/
const moduleFederationConfig = {
name: '<%= projectName %>',
remotes: [
<% remotes.forEach(function(r) {%> "<%= r.fileName %>", <% }); %>
],
module.exports = {
name: '<%= projectName %>',
remotes: [
<% remotes.forEach(function(r) {%> "<%= r.fileName %>", <% }); %>
],
};
module.exports = moduleFederationConfig;

View File

@ -1,13 +1,12 @@
// @ts-check
const { composePlugins, withNx } = require('@nrwl/webpack');
const { withReact } = require('@nrwl/react');
const { withModuleFederation } = require('@nrwl/react/module-federation');
const baseConfig = require('./module-federation.config');
/**
* @type {import('@nrwl/devkit').ModuleFederationConfig}
**/
const defaultConfig = {
...baseConfig,
const config = {
...baseConfig,
};
module.exports = withModuleFederation(defaultConfig);
// Nx plugins for webpack to build config object from Nx options and context.
module.exports = composePlugins(withNx(), withReact(), withModuleFederation(config));

View File

@ -1,11 +1,9 @@
// @ts-check
const { composePlugins, withNx } = require('@nrwl/webpack');
const { withReact } = require('@nrwl/react');
const { withModuleFederation } = require('@nrwl/react/module-federation'));
const { withModuleFederation } = require('@nrwl/react/module-federation');
const baseConfig = require('./module-federation.config');
/**
* @type {import('@nrwl/devkit').ModuleFederationConfig}
**/
const prodConfig = {
...baseConfig,
/*
@ -30,4 +28,5 @@ const prodConfig = {
],
};
module.exports = withModuleFederation(prodConfig);
// Nx plugins for webpack to build config object from Nx options and context.
module.exports = composePlugins(withNx(), withReact(), withModuleFederation(prodConfig));

View File

@ -1,13 +1,6 @@
// @ts-check
/**
* @type {import('@nrwl/devkit').ModuleFederationConfig}
**/
const moduleFederationConfig = {
module.exports = {
name: '<%= projectName %>',
exposes: {
'./Module': '<%= appProjectRoot %>/src/remote-entry.ts',
},
};
module.exports = moduleFederationConfig;

View File

@ -1,8 +1,12 @@
const { composePlugins, withNx } = require('@nrwl/webpack');
const { withReact } = require('@nrwl/react');
const { withModuleFederationForSSR } = require('@nrwl/react/module-federation');
const baseConfig = require("./module-federation.server.config");
const defaultConfig = {
...baseConfig
...baseConfig,
};
module.exports = withModuleFederationForSSR(defaultConfig);
// Nx plugins for webpack to build config object from Nx options and context.
module.exports = composePlugins(withNx(), withReact(), withModuleFederationForSSR(defaultConfig));

View File

@ -1,13 +1,6 @@
// @ts-check
/**
* @type {import('@nrwl/devkit').ModuleFederationConfig}
**/
const moduleFederationConfig = {
module.exports = {
name: '<%= projectName %>',
exposes: {
'./Module': './src/remote-entry.ts',
},
};
module.exports = moduleFederationConfig;

View File

@ -1,13 +1,12 @@
// @ts-check
const { composePlugins, withNx } = require('@nrwl/webpack');
const { withReact } = require('@nrwl/react');
const { withModuleFederation } = require('@nrwl/react/module-federation'));
const { withModuleFederation } = require('@nrwl/react/module-federation');
const baseConfig = require('./module-federation.config');
/**
* @type {import('@nrwl/devkit').ModuleFederationConfig}
**/
const defaultConfig = {
...baseConfig,
const config = {
...baseConfig,
};
module.exports = withModuleFederation(defaultConfig);
// Nx plugins for webpack to build config object from Nx options and context.
module.exports = composePlugins(withNx(), withReact(), withModuleFederation(config));

View File

@ -107,7 +107,7 @@ export async function setupSsrGenerator(tree: Tree, options: Schema) {
compiler: 'babel',
externalDependencies: 'all',
outputHashing: 'none',
webpackConfig: '@nrwl/react/plugins/webpack',
webpackConfig: joinPathFragments(projectRoot, 'webpack.config.js'),
},
configurations: {
development: {

View File

@ -351,6 +351,7 @@ describe('app', () => {
scripts: [],
styles: ['apps/my-app/src/styles.css'],
tsConfig: 'apps/my-app/tsconfig.app.json',
webpackConfig: 'apps/my-app/webpack.config.js',
});
expect(architectConfig.build.configurations.production).toEqual({
optimization: true,

View File

@ -44,7 +44,7 @@ function createApplicationFiles(tree: Tree, options: NormalizedSchema) {
tree,
join(
__dirname,
options.bundler === 'vite' ? './files/app-vite' : './files/app'
options.bundler === 'vite' ? './files/app-vite' : './files/app-webpack'
),
options.appProjectRoot,
{
@ -81,6 +81,10 @@ async function setupBundler(tree: Tree, options: NormalizedSchema) {
tsConfig,
compiler: options.compiler ?? 'babel',
devServer: true,
webpackConfig: joinPathFragments(
options.appProjectRoot,
'webpack.config.js'
),
});
const project = readProjectConfiguration(tree, options.projectName);
const prodConfig = project.targets.build.configurations.production;

View File

@ -0,0 +1,8 @@
const { composePlugins, withNx, withWeb } = require('@nrwl/webpack');
// Nx plugins for webpack.
module.exports = composePlugins(withNx(), withWeb(), (config) => {
// Update the webpack config as needed here.
// e.g. `config.plugins.push(new MyPlugin())`
return config;
});

View File

@ -1,13 +0,0 @@
# This file is currently used by autoprefixer to adjust CSS to support the below specified browsers
# For additional information regarding the format and rule options, please see:
# https://github.com/browserslist/browserslist#queries
#
# If you need to support different browsers in production, you may tweak the list below.
last 1 Chrome version
last 1 Firefox version
last 2 Edge major versions
last 2 Safari major version
last 2 iOS major versions
Firefox ESR
not IE 9-11 # For IE 9-11 support, remove 'not'.

View File

@ -56,7 +56,7 @@ export async function* devServerExecutor(
);
}
let webpackConfig = getDevServerConfig(context, buildOptions, serveOptions);
let config = getDevServerConfig(context, buildOptions, serveOptions);
if (buildOptions.webpackConfig) {
let customWebpack = resolveCustomWebpackConfig(
@ -68,16 +68,17 @@ export async function* devServerExecutor(
customWebpack = await customWebpack;
}
webpackConfig = await customWebpack(webpackConfig, {
buildOptions,
config = await customWebpack(config, {
options: buildOptions,
context,
configuration: serveOptions.buildTarget.split(':')[2],
});
}
return yield* eachValueFrom(
runWebpackDevServer(webpackConfig, webpack, WebpackDevServer).pipe(
runWebpackDevServer(config, webpack, WebpackDevServer).pipe(
tap(({ stats }) => {
console.info(stats.toString((webpackConfig as any).stats));
console.info(stats.toString((config as any).stats));
}),
map(({ baseUrl, stats }) => {
return {

View File

@ -83,6 +83,7 @@ export interface WebpackExecutorOptions {
export interface NormalizedWebpackExecutorOptions
extends WebpackExecutorOptions {
outputFileName: string;
assets?: AssetGlobPattern[];
root?: string;
projectRoot?: string;

View File

@ -1,37 +1,35 @@
import { type Compiler, sources, type WebpackPluginInstance } from 'webpack';
import {
createLockFile,
createPackageJson,
ExecutorContext,
type ProjectGraph,
serializeJson,
createPackageJson,
createLockFile,
} from '@nrwl/devkit';
import {
getHelperDependenciesFromProjectGraph,
HelperDependency,
} from '@nrwl/js/src/utils/compiler-helper-dependency';
import { readTsConfig } from '@nrwl/workspace/src/utilities/typescript';
import { NormalizedWebpackExecutorOptions } from '../executors/webpack/schema';
import { getLockFileName } from 'nx/src/lock-file/lock-file';
export class GeneratePackageJsonWebpackPlugin implements WebpackPluginInstance {
const pluginName = 'GeneratePackageJsonPlugin';
export class GeneratePackageJsonPlugin implements WebpackPluginInstance {
private readonly projectGraph: ProjectGraph;
constructor(
private readonly context: ExecutorContext,
private readonly options: NormalizedWebpackExecutorOptions
private readonly options: { tsConfig: string; outputFileName: string },
private readonly context: ExecutorContext
) {
this.projectGraph = context.projectGraph;
}
apply(compiler: Compiler): void {
const pluginName = this.constructor.name;
compiler.hooks.thisCompilation.tap(pluginName, (compilation) => {
compilation.hooks.processAssets.tap(
{
name: 'nx-generate-package-json-plugin',
name: pluginName,
stage: compiler.webpack.Compilation.PROCESS_ASSETS_STAGE_ADDITIONAL,
},
() => {

View File

@ -17,18 +17,18 @@ export function getBaseWebpackPartial(
export type NxWebpackPlugin = (
config: Configuration,
ctx?: {
ctx: {
options: NormalizedWebpackExecutorOptions;
context?: ExecutorContext;
context: ExecutorContext;
}
) => Configuration;
export function composePlugins(...plugins: NxWebpackPlugin[]) {
return function combined(
config: Configuration,
ctx?: {
ctx: {
options: NormalizedWebpackExecutorOptions;
context?: ExecutorContext;
context: ExecutorContext;
}
): Configuration {
for (const plugin of plugins) {

View File

@ -1,5 +1,9 @@
export function tsNodeRegister(file: string = '', tsConfig?: string) {
if (!file?.endsWith('.ts')) return;
// Avoid double-registering which can lead to issues type-checking already transformed files.
if (isRegistered()) return;
// Register TS compiler lazily
require('ts-node').register({
project: tsConfig,
@ -29,3 +33,9 @@ export function resolveCustomWebpackConfig(path: string, tsConfig: string) {
// `{ default: { ... } }`
return customWebpackConfig.default || customWebpackConfig;
}
export function isRegistered() {
return (
require.extensions['.ts'] != undefined ||
require.extensions['.tsx'] != undefined
);
}

View File

@ -11,7 +11,7 @@ import ForkTsCheckerWebpackPlugin = require('fork-ts-checker-webpack-plugin');
import { NormalizedWebpackExecutorOptions } from '../executors/webpack/schema';
import { StatsJsonPlugin } from '../plugins/stats-json-plugin';
import { createCopyPlugin } from './create-copy-plugin';
import { GeneratePackageJsonWebpackPlugin } from '../plugins/generate-package-json-webpack-plugin';
import { GeneratePackageJsonPlugin } from '../plugins/generate-package-json-plugin';
import { getOutputHashFormat } from './hash-format';
const IGNORED_WEBPACK_WARNINGS = [
@ -22,6 +22,8 @@ const IGNORED_WEBPACK_WARNINGS = [
const extensions = ['.ts', '.tsx', '.mjs', '.js', '.jsx'];
const mainFields = ['main', 'module'];
const processed = new Set();
export function withNx(opts?: { skipTypeChecking?: boolean }) {
return function configure(
config: Configuration,
@ -33,7 +35,10 @@ export function withNx(opts?: { skipTypeChecking?: boolean }) {
context: ExecutorContext;
}
): Configuration {
if (processed.has(config)) return config;
const plugins: WebpackPluginInstance[] = [];
if (!opts?.skipTypeChecking) {
plugins.push(
new ForkTsCheckerWebpackPlugin({
@ -88,7 +93,7 @@ export function withNx(opts?: { skipTypeChecking?: boolean }) {
}
if (options.generatePackageJson && context) {
plugins.push(new GeneratePackageJsonWebpackPlugin(context, options));
plugins.push(new GeneratePackageJsonPlugin(options, context));
}
if (options.statsJson) {
@ -118,7 +123,7 @@ export function withNx(opts?: { skipTypeChecking?: boolean }) {
? `[name]${hashFormat.chunk}.js`
: '[name].js';
return {
const updated = {
...config,
target: options.target,
node: false as const,
@ -132,14 +137,14 @@ export function withNx(opts?: { skipTypeChecking?: boolean }) {
mode:
process.env.NODE_ENV === 'development' ||
process.env.NODE_ENV === 'production'
? process.env.NODE_ENV
: 'none',
? (process.env.NODE_ENV as 'development' | 'production')
: ('none' as const),
devtool:
options.sourceMap === 'hidden'
? 'hidden-source-map'
: options.sourceMap
? 'source-map'
: false,
: (false as const),
entry,
output: {
...config.output,
@ -150,7 +155,7 @@ export function withNx(opts?: { skipTypeChecking?: boolean }) {
hashFunction: 'xxhash64',
// Disabled for performance
pathinfo: false,
scriptType: 'module',
scriptType: 'module' as const,
},
watch: options.watch,
watchOptions: {
@ -186,6 +191,7 @@ export function withNx(opts?: { skipTypeChecking?: boolean }) {
? new TerserPlugin({
parallel: true,
terserOptions: {
keep_classnames: true,
ecma: 2020,
safari10: true,
output: {
@ -208,7 +214,7 @@ export function withNx(opts?: { skipTypeChecking?: boolean }) {
},
performance: {
...config.performance,
hints: false,
hints: false as const,
},
experiments: { ...config.experiments, cacheUnaffected: true },
ignoreWarnings: [
@ -269,6 +275,9 @@ export function withNx(opts?: { skipTypeChecking?: boolean }) {
usedExports: !!options.verbose,
},
};
processed.add(updated);
return updated;
};
}

View File

@ -7,6 +7,7 @@ import {
} from 'webpack';
import { SubresourceIntegrityPlugin } from 'webpack-subresource-integrity';
import * as path from 'path';
import { basename } from 'path';
import { getOutputHashFormat } from '@nrwl/webpack/src/utils/hash-format';
import { PostcssCliResources } from '@nrwl/webpack/src/utils/webpack/plugins/postcss-cli-resources';
import { normalizeExtraEntryPoints } from '@nrwl/webpack/src/utils/webpack/normalize-entry';
@ -19,7 +20,6 @@ import CssMinimizerPlugin = require('css-minimizer-webpack-plugin');
import MiniCssExtractPlugin = require('mini-css-extract-plugin');
import autoprefixer = require('autoprefixer');
import postcssImports = require('postcss-import');
import { basename } from 'path';
interface PostcssOptions {
(loader: any): any;
@ -27,11 +27,15 @@ interface PostcssOptions {
config?: string;
}
const processed = new Set();
export function withWeb() {
return function configure(
config: Configuration,
{ options }: { options: NormalizedWebpackExecutorOptions }
): Configuration {
if (processed.has(config)) return config;
const plugins = [];
const stylesOptimization =
@ -246,9 +250,6 @@ export function withWeb() {
})
);
// context needs to be set for babel to pick up correct babelrc
config.context = path.join(options.root, options.projectRoot);
config.output = {
...config.output,
crossOriginLoading: options.subresourceIntegrity
@ -303,7 +304,7 @@ export function withWeb() {
config.module = {
...config.module,
rules: [
...config.module.rules,
...(config.module.rules ?? []),
...rules,
{
test: /\.(bmp|png|jpe?g|gif|webp|avif)$/,
@ -323,6 +324,7 @@ export function withWeb() {
},
],
};
processed.add(config);
return config;
};
}