feat(webpack): add plugin to automatically configure build and serve targets (#20243)

This commit is contained in:
Jack Hsu 2023-11-28 13:01:03 -05:00 committed by GitHub
parent 54eab7f8b1
commit 507fe42e4f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
62 changed files with 1695 additions and 656 deletions

View File

@ -30,8 +30,7 @@
"compiler": {
"type": "string",
"description": "The compiler to use.",
"enum": ["babel", "swc", "tsc"],
"default": "babel"
"enum": ["babel", "swc", "tsc"]
},
"outputPath": {
"type": "string",
@ -43,8 +42,7 @@
"type": "string",
"alias": "platform",
"description": "Target platform for the build, same as the Webpack target option.",
"enum": ["node", "web", "webworker"],
"default": "web"
"enum": ["node", "web", "webworker"]
},
"deleteOutputPath": {
"type": "boolean",
@ -53,8 +51,7 @@
},
"watch": {
"type": "boolean",
"description": "Enable re-building when files change.",
"default": false
"description": "Enable re-building when files change."
},
"baseHref": {
"type": "string",
@ -66,33 +63,27 @@
},
"vendorChunk": {
"type": "boolean",
"description": "Use a separate bundle containing only vendor libraries.",
"default": true
"description": "Use a separate bundle containing only vendor libraries."
},
"commonChunk": {
"type": "boolean",
"description": "Use a separate bundle containing code used across multiple bundles.",
"default": true
"description": "Use a separate bundle containing code used across multiple bundles."
},
"runtimeChunk": {
"type": "boolean",
"description": "Use a separate bundle containing the runtime.",
"default": true
"description": "Use a separate bundle containing the runtime."
},
"sourceMap": {
"description": "Output sourcemaps. Use 'hidden' for use with error reporting tools without generating sourcemap comment.",
"default": true,
"oneOf": [{ "type": "boolean" }, { "type": "string" }]
},
"progress": {
"type": "boolean",
"description": "Log progress to the console while building.",
"default": false
"description": "Log progress to the console while building."
},
"assets": {
"type": "array",
"description": "List of static application assets.",
"default": [],
"items": {
"oneOf": [
{
@ -163,8 +154,7 @@
"x-completion-glob": "**/*@(.css|.scss|.less|.sass|.styl|.stylus)"
}
]
},
"default": []
}
},
"styles": {
"type": "array",
@ -200,18 +190,15 @@
"x-completion-glob": "**/*@(.css|.scss|.less|.sass|.styl|.stylus)"
}
]
},
"default": []
}
},
"namedChunks": {
"type": "boolean",
"description": "Names the produced bundles according to their entry file.",
"default": true
"description": "Names the produced bundles according to their entry file."
},
"outputHashing": {
"type": "string",
"description": "Define the output filename cache-busting hashing mode.",
"default": "none",
"enum": ["none", "all", "media", "bundles"]
},
"stylePreprocessorOptions": {
@ -221,8 +208,7 @@
"includePaths": {
"description": "Paths to include. Paths will be resolved to project root.",
"type": "array",
"items": { "type": "string" },
"default": []
"items": { "type": "string" }
}
},
"additionalProperties": false
@ -251,13 +237,11 @@
},
"generatePackageJson": {
"type": "boolean",
"description": "Generates a `package.json` and pruned lock file with the project's `node_module` dependencies populated for installing in a container. If a `package.json` exists in the project's directory, it will be reused with dependencies populated.",
"default": false
"description": "Generates a `package.json` and pruned lock file with the project's `node_module` dependencies populated for installing in a container. If a `package.json` exists in the project's directory, it will be reused with dependencies populated."
},
"transformers": {
"type": "array",
"description": "List of TypeScript Compiler Transfomers Plugins.",
"default": [],
"aliases": ["tsPlugins"],
"items": {
"oneOf": [
@ -302,18 +286,15 @@
{ "type": "string", "enum": ["none", "all"] },
{ "type": "array", "items": { "type": "string" } }
],
"description": "Dependencies to keep external to the bundle. (`all` (default), `none`, or an array of module names)",
"default": "all"
"description": "Dependencies to keep external to the bundle. (`all` (default), `none`, or an array of module names)"
},
"extractCss": {
"type": "boolean",
"description": "Extract CSS into a `.css` file.",
"default": true
"description": "Extract CSS into a `.css` file."
},
"subresourceIntegrity": {
"type": "boolean",
"description": "Enables the use of subresource integrity validation.",
"default": false
"description": "Enables the use of subresource integrity validation."
},
"polyfills": {
"type": "string",
@ -321,30 +302,24 @@
"x-completion-type": "file",
"x-completion-glob": "**/*@(.js|.ts|.tsx)"
},
"verbose": {
"type": "boolean",
"description": "Emits verbose output",
"default": false
},
"verbose": { "type": "boolean", "description": "Emits verbose output" },
"statsJson": {
"type": "boolean",
"description": "Generates a 'stats.json' file which can be analyzed using tools such as: 'webpack-bundle-analyzer' or `<https://webpack.github.io/analyse>`.",
"default": false
"description": "Generates a 'stats.json' file which can be analyzed using tools such as: 'webpack-bundle-analyzer' or `<https://webpack.github.io/analyse>`."
},
"isolatedConfig": {
"type": "boolean",
"description": "Do not apply Nx webpack plugins automatically. Plugins need to be applied in the project's webpack.config.js file (e.g. withNx, withReact, etc.).",
"default": true
"default": true,
"x-deprecated": "Automatic configuration of Webpack is deprecated in favor of an explicit 'webpack.config.js' file. This option will be removed in Nx 18. See https://nx.dev/recipes/webpack/webpack-config-setup."
},
"extractLicenses": {
"type": "boolean",
"description": "Extract all licenses in a separate file, in the case of production builds only.",
"default": false
"description": "Extract all licenses in a separate file, in the case of production builds only."
},
"memoryLimit": {
"type": "number",
"description": "Memory limit for type checking service process in `MB`.",
"default": 2048
"description": "Memory limit for type checking service process in `MB`."
},
"fileReplacements": {
"description": "Replace files with other files in the build.",
@ -365,18 +340,16 @@
},
"additionalProperties": false,
"required": ["replace", "with"]
},
"default": []
}
},
"buildLibsFromSource": {
"type": "boolean",
"description": "Read buildable libraries from source instead of building them separately.",
"description": "Read buildable libraries from source instead of building them separately. If set to `false`, the `tsConfig` option must also be set to remap paths.",
"default": true
},
"generateIndexHtml": {
"type": "boolean",
"description": "Generates `index.html` file to the output path. This can be turned off if using a webpack plugin to generate HTML such as `html-webpack-plugin`.",
"default": true
"description": "Generates `index.html` file to the output path. This can be turned off if using a webpack plugin to generate HTML such as `html-webpack-plugin`."
},
"postcssConfig": {
"type": "string",
@ -391,8 +364,7 @@
},
"babelUpwardRootMode": {
"type": "boolean",
"description": "Whether to set rootmode to upward. See https://babeljs.io/docs/en/options#rootmode",
"default": false
"description": "Whether to set rootmode to upward. See https://babeljs.io/docs/en/options#rootmode"
},
"babelConfig": {
"type": "string",
@ -400,7 +372,7 @@
"x-completion-type": "file"
}
},
"required": ["tsConfig", "main"],
"required": [],
"definitions": {
"assetPattern": {
"oneOf": [

View File

@ -0,0 +1,34 @@
import {
cleanupProject,
newProject,
runCLI,
runE2ETests,
uniq,
} from '@nx/e2e/utils';
describe('Webpack Plugin (PCv3)', () => {
let originalPcv3: string | undefined;
beforeAll(() => {
originalPcv3 = process.env.NX_PCV3;
process.env.NX_PCV3 = 'true';
newProject();
});
afterAll(() => {
process.env.NX_PCV3 = originalPcv3;
cleanupProject();
});
it('should generate, build, and serve React applications', () => {
const appName = uniq('app');
runCLI(
`generate @nx/react:app ${appName} --bundler webpack --e2eTestRunner=cypress --no-interactive`
);
expect(() => runCLI(`build ${appName}`)).not.toThrow();
if (runE2ETests()) {
runCLI(`e2e ${appName}-e2e --watch=false --verbose`);
}
}, 500_000);
});

View File

@ -125,12 +125,18 @@ module.exports = composePlugins(withNx(), (config) => {
path: path.join(__dirname, '../../dist/${appName}')
},
plugins: [
new NxWebpackPlugin()
new NxWebpackPlugin({
compiler: 'tsc',
main: 'apps/${appName}/src/main.ts',
tsConfig: 'apps/${appName}/tsconfig.app.json',
outputHashing: 'none',
optimization: false,
})
]
};`
);
runCLI(`build ${appName} --outputHashing none`);
runCLI(`build ${appName}`);
let output = runCommand(`node dist/${appName}/main.js`);
expect(output).toMatch(/Hello/);

View File

@ -112,12 +112,13 @@ Able to find CT project, ${!!ctProjectConfig}.`);
),
};
const configure = composePluginsSync(
withNx(),
withReact({
withNx({
target: 'web',
styles: [],
scripts: [],
postcssConfig: ctProjectConfig.root,
})
}),
withReact({})
);
const webpackConfig = configure(
{},

View File

@ -43,7 +43,7 @@
"error",
{
"buildTargets": ["build-base"],
"ignoredDependencies": ["nx", "typescript"]
"ignoredDependencies": ["nx", "typescript", "@nx/webpack"]
}
]
}

View File

@ -0,0 +1,40 @@
import {
readNxJson,
readProjectConfiguration,
Tree,
updateNxJson,
} from '@nx/devkit';
import { createTreeWithEmptyWorkspace } from '@nx/devkit/testing';
// nx-ignore-next-line
import { applicationGenerator } from './application';
describe('node app generator (PCv3)', () => {
let tree: Tree;
beforeEach(() => {
tree = createTreeWithEmptyWorkspace();
const nxJson = readNxJson(tree);
nxJson.plugins ??= [];
nxJson.plugins.push('@nx/webpack/plugin');
updateNxJson(tree, nxJson);
});
it('should skip the build target and setup webpack config', async () => {
await applicationGenerator(tree, {
name: 'my-node-app',
bundler: 'webpack',
projectNameAndRootFormat: 'as-provided',
});
const project = readProjectConfiguration(tree, 'my-node-app');
expect(project.root).toEqual('my-node-app');
expect(project.targets.build).toBeUndefined();
const webpackConfig = tree.read('my-node-app/webpack.config.js', 'utf-8');
expect(webpackConfig).toContain(`new NxWebpackPlugin`);
expect(webpackConfig).toContain(`target: 'node'`);
expect(webpackConfig).toContain(`'../dist/my-node-app'`);
expect(webpackConfig).toContain(`main: './src/main.ts'`);
expect(webpackConfig).toContain(`tsConfig: './tsconfig.app.json'`);
});
});

View File

@ -42,7 +42,6 @@ describe('app', () => {
outputPath: 'dist/my-node-app',
main: 'my-node-app/src/main.ts',
tsConfig: 'my-node-app/tsconfig.app.json',
isolatedConfig: true,
webpackConfig: 'my-node-app/webpack.config.js',
assets: ['my-node-app/src/assets'],
},

View File

@ -40,6 +40,7 @@ import { e2eProjectGenerator } from '../e2e-project/e2e-project';
import { initGenerator } from '../init/init';
import { setupDockerGenerator } from '../setup-docker/setup-docker';
import { Schema } from './schema';
import { hasWebpackPlugin } from '../../utils/has-webpack-plugin';
export interface NormalizedSchema extends Schema {
appProjectRoot: string;
@ -67,7 +68,6 @@ function getWebpackBuildConfig(
),
tsConfig: joinPathFragments(options.appProjectRoot, 'tsconfig.app.json'),
assets: [joinPathFragments(project.sourceRoot, 'assets')],
isolatedConfig: true,
webpackConfig: joinPathFragments(
options.appProjectRoot,
'webpack.config.js'
@ -153,10 +153,13 @@ function addProject(tree: Tree, options: NormalizedSchema) {
tags: options.parsedTags,
};
project.targets.build =
options.bundler === 'esbuild'
? getEsBuildConfig(project, options)
: getWebpackBuildConfig(project, options);
if (options.bundler === 'esbuild') {
project.targets.build = getEsBuildConfig(project, options);
} else if (options.bundler === 'webpack') {
if (!hasWebpackPlugin(tree)) {
project.targets.build = getWebpackBuildConfig(project, options);
}
}
project.targets.serve = getServeConfig(options);
addProjectConfiguration(
@ -168,6 +171,7 @@ function addProject(tree: Tree, options: NormalizedSchema) {
}
function addAppFiles(tree: Tree, options: NormalizedSchema) {
const sourceRoot = joinPathFragments(options.appProjectRoot, 'src');
generateFiles(
tree,
join(__dirname, './files/common'),
@ -182,6 +186,17 @@ function addAppFiles(tree: Tree, options: NormalizedSchema) {
tree,
options.appProjectRoot
),
webpackPluginOptions: hasWebpackPlugin(tree)
? {
outputPath: joinPathFragments(
'dist',
options.rootProject ? options.name : options.appProjectRoot
),
main: './src/main' + (options.js ? '.js' : '.ts'),
tsConfig: './tsconfig.app.json',
assets: ['./assets'],
}
: null,
}
);
@ -374,6 +389,18 @@ export async function applicationGeneratorInternal(tree: Tree, schema: Schema) {
const installTask = addProjectDependencies(tree, options);
tasks.push(installTask);
if (options.bundler === 'webpack') {
const { webpackInitGenerator } = ensurePackage<
typeof import('@nx/webpack')
>('@nx/webpack', nxVersion);
const webpackInitTask = await webpackInitGenerator(tree, {
uiFramework: 'react',
skipFormat: true,
});
tasks.push(webpackInitTask);
}
addAppFiles(tree, options);
addProject(tree, options);

View File

@ -1,8 +1,35 @@
<% if (webpackPluginOptions) { %>
const { NxWebpackPlugin } = require('@nx/webpack');
const { join } = require('path');
module.exports = {
output: {
path: join(__dirname, '<%= offset %><%= webpackPluginOptions.outputPath %>'),
},
plugins: [
new NxWebpackPlugin({
target: 'node',
compiler: 'tsc',
main: '<%= webpackPluginOptions.main %>',
tsConfig: '<%= webpackPluginOptions.tsConfig %>',
assets: <%- JSON.stringify(webpackPluginOptions.assets) %>,
optimization: false,
outputHashing: 'none',
})
],
};
<% } else { %>
const { composePlugins, withNx} = require('@nx/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;
});
module.exports = composePlugins(
withNx({
target: 'node',
}),
(config) => {
// Update the webpack config as needed here.
// e.g. `config.plugins.push(new MyPlugin())`
return config;
}
);
<% } %>

View File

@ -0,0 +1,10 @@
import { readNxJson, Tree } from '@nx/devkit';
export function hasWebpackPlugin(tree: Tree) {
const nxJson = readNxJson(tree);
return !!nxJson.plugins?.some((p) =>
typeof p === 'string'
? p === '@nx/webpack/plugin'
: p.plugin === '@nx/webpack/plugin'
);
}

View File

@ -8,7 +8,6 @@ import {
joinPathFragments,
logger,
parseTargetString,
ProjectGraph,
readCachedProjectGraph,
readTargetOptions,
stripIndents,
@ -235,11 +234,9 @@ function buildTargetWebpack(
normalizeOptions,
} = require('@nx/webpack/src/executors/webpack/lib/normalize-options');
const {
resolveCustomWebpackConfig,
} = require('@nx/webpack/src/utils/webpack/custom-webpack');
const {
getWebpackConfig,
} = require('@nx/webpack/src/executors/webpack/lib/get-webpack-config');
resolveUserDefinedWebpackConfig,
} = require('@nx/webpack/src/utils/webpack/resolve-user-defined-webpack-config');
const { withNx } = require('@nx/webpack/src/utils/with-nx');
const options = normalizeOptions(
withSchemaDefaults(parsed, context),
@ -251,7 +248,7 @@ function buildTargetWebpack(
let customWebpack: any;
if (options.webpackConfig) {
customWebpack = resolveCustomWebpackConfig(
customWebpack = resolveUserDefinedWebpackConfig(
options.webpackConfig,
options.tsConfig.startsWith(context.root)
? options.tsConfig
@ -262,12 +259,8 @@ function buildTargetWebpack(
return async () => {
customWebpack = await customWebpack;
// TODO(jack): Once webpackConfig is always set in @nx/webpack:webpack, we no longer need this default.
const defaultWebpack = getWebpackConfig(context, {
const defaultWebpack = withNx({
...options,
// cypress will generate its own index.html from component-index.html
generateIndexHtml: false,
// causes issues with buildable libraries with ENOENT: no such file or directory, scandir error
extractLicenses: false,
root: workspaceRoot,
projectRoot: ctProjectConfig.root,
sourceRoot: ctProjectConfig.sourceRoot,

View File

@ -1,9 +1,11 @@
import { Compiler, Configuration, WebpackOptionsNormalized } from 'webpack';
import { Configuration, WebpackOptionsNormalized } from 'webpack';
export function applyReactConfig(
options: { svgr?: boolean },
config: Partial<WebpackOptionsNormalized | Configuration> = {}
): void {
if (!process.env['NX_TASK_TARGET_PROJECT']) return;
addHotReload(config);
if (options.svgr !== false) {

View File

@ -196,8 +196,7 @@ export const webpack = async (
// ESM build for modern browsers.
let baseWebpackConfig: Configuration = {};
const configure = composePluginsSync(
withNx({ skipTypeChecking: true }),
withWeb(),
withNx({ target: 'web', skipTypeChecking: true }),
withReact()
);
const finalConfig = configure(baseWebpackConfig, {

View File

@ -4,7 +4,7 @@ import { applyReactConfig } from './nx-react-webpack-plugin/lib/apply-react-conf
const processed = new Set();
interface WithReactOptions extends WithWebOptions {
export interface WithReactOptions extends WithWebOptions {
svgr?: false;
}

View File

@ -0,0 +1,60 @@
import { installedCypressVersion } from '@nx/cypress/src/utils/cypress-version';
import {
readNxJson,
readProjectConfiguration,
Tree,
updateNxJson,
} from '@nx/devkit';
import { createTreeWithEmptyWorkspace } from '@nx/devkit/testing';
import { Linter } from '@nx/eslint';
import { applicationGenerator } from './application';
import { Schema } from './schema';
// need to mock cypress otherwise it'll use the nx installed version from package.json
// which is v9 while we are testing for the new v10 version
jest.mock('@nx/cypress/src/utils/cypress-version');
describe('react app generator (PCv3)', () => {
let appTree: Tree;
let schema: Schema = {
compiler: 'babel',
e2eTestRunner: 'cypress',
skipFormat: false,
name: 'my-app',
linter: Linter.EsLint,
style: 'css',
strict: true,
projectNameAndRootFormat: 'as-provided',
};
let mockedInstalledCypressVersion: jest.Mock<
ReturnType<typeof installedCypressVersion>
> = installedCypressVersion as never;
beforeEach(() => {
mockedInstalledCypressVersion.mockReturnValue(10);
appTree = createTreeWithEmptyWorkspace();
const nxJson = readNxJson(appTree);
nxJson.plugins ??= [];
nxJson.plugins.push('@nx/webpack/plugin');
updateNxJson(appTree, nxJson);
});
it('should setup webpack config that is compatible without project targets', async () => {
await applicationGenerator(appTree, {
...schema,
name: 'my-app',
bundler: 'webpack',
});
const targets = readProjectConfiguration(appTree, 'my-app').targets;
expect(targets.build).toBeUndefined();
expect(targets.serve).toBeUndefined();
const webpackConfig = appTree.read('my-app/webpack.config.js', 'utf-8');
expect(webpackConfig).toContain(`new NxWebpackPlugin`);
expect(webpackConfig).toContain(`'../dist/my-app'`);
expect(webpackConfig).toContain(`main: './src/main.tsx'`);
expect(webpackConfig).toContain(`tsConfig: './tsconfig.app.json'`);
expect(webpackConfig).toContain(`styles: ['./src/styles.css']`);
expect(webpackConfig).toContain(
`assets: ['./src/favicon.ico', './src/assets']`
);
});
});

View File

@ -95,9 +95,19 @@ export async function applicationGeneratorInternal(
skipFormat: true,
skipHelperLibs: options.bundler === 'vite',
});
tasks.push(initTask);
if (options.bundler === 'webpack') {
const { webpackInitGenerator } = ensurePackage<
typeof import('@nx/webpack')
>('@nx/webpack', nxVersion);
const webpackInitTask = await webpackInitGenerator(host, {
uiFramework: 'react',
skipFormat: true,
});
tasks.push(webpackInitTask);
}
if (!options.rootProject) {
extractTsConfigBase(host);
}
@ -149,15 +159,6 @@ export async function applicationGeneratorInternal(
},
false
);
} else if (options.bundler === 'webpack') {
const { webpackInitGenerator } = ensurePackage<
typeof import('@nx/webpack')
>('@nx/webpack', nxVersion);
const webpackInitTask = await webpackInitGenerator(host, {
uiFramework: 'react',
skipFormat: true,
});
tasks.push(webpackInitTask);
} else if (options.bundler === 'rspack') {
const { configurationGenerator } = ensurePackage(
'@nx/rspack',

View File

@ -1,3 +0,0 @@
export const environment = {
production: true
};

View File

@ -1,6 +0,0 @@
// This file can be replaced during build by using the `fileReplacements` array.
// When building for production, this file is replaced with `environment.prod.ts`.
export const environment = {
production: false
};

View File

@ -1,9 +1,50 @@
<% if (webpackPluginOptions) { %>
const { NxWebpackPlugin } = require('@nx/webpack');
const { NxReactWebpackPlugin } = require('@nx/react');
const { join } = require('path');
module.exports = {
output: {
path: join(__dirname, '<%= offsetFromRoot %><%= webpackPluginOptions.outputPath %>'),
},
devServer: {
port: 4200
},
plugins: [
new NxWebpackPlugin({
tsConfig: '<%= webpackPluginOptions.tsConfig %>',
compiler: '<%= webpackPluginOptions.compiler %>',
main: '<%= webpackPluginOptions.main %>',
index: '<%= webpackPluginOptions.index %>',
baseHref: '<%= webpackPluginOptions.baseHref %>',
assets: <%- JSON.stringify(webpackPluginOptions.assets) %>,
styles: <%- JSON.stringify(webpackPluginOptions.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
}),
],
};
<% } else { %>
const { composePlugins, withNx } = require('@nx/webpack');
const { withReact } = require('@nx/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;
});
module.exports = composePlugins(
withNx(),
withReact({
// Uncomment this line if you don't want to use SVGR
// See: https://react-svgr.com/
// svgr: false
}),
(config) => {
// Update the webpack config as needed here.
// e.g. `config.plugins.push(new MyPlugin())`
return config;
}
);
<% } %>

View File

@ -8,6 +8,7 @@ import {
import { webStaticServeGenerator } from '@nx/web';
import { nxVersion } from '../../../utils/versions';
import { hasWebpackPlugin } from '../../../utils/has-webpack-plugin';
import { NormalizedSchema } from '../schema';
export async function addE2e(
@ -16,10 +17,12 @@ export async function addE2e(
): Promise<GeneratorCallback> {
switch (options.e2eTestRunner) {
case 'cypress': {
webStaticServeGenerator(tree, {
buildTarget: `${options.projectName}:build`,
targetName: 'serve-static',
});
if (!hasWebpackPlugin(tree)) {
webStaticServeGenerator(tree, {
buildTarget: `${options.projectName}:build`,
targetName: 'serve-static',
});
}
const { configurationGenerator } = ensurePackage<
typeof import('@nx/cypress')

View File

@ -4,9 +4,11 @@ import {
joinPathFragments,
ProjectConfiguration,
TargetConfiguration,
Tree,
} from '@nx/devkit';
import { hasWebpackPlugin } from '../../../utils/has-webpack-plugin';
export function addProject(host, options: NormalizedSchema) {
export function addProject(host: Tree, options: NormalizedSchema) {
const project: ProjectConfiguration = {
root: options.appProjectRoot,
sourceRoot: `${options.appProjectRoot}/src`,
@ -16,10 +18,12 @@ export function addProject(host, options: NormalizedSchema) {
};
if (options.bundler === 'webpack') {
project.targets = {
build: createBuildTarget(options),
serve: createServeTarget(options),
};
if (!hasWebpackPlugin(host)) {
project.targets = {
build: createBuildTarget(options),
serve: createServeTarget(options),
};
}
}
addProjectConfiguration(host, options.projectName, {

View File

@ -1,5 +1,6 @@
import {
generateFiles,
joinPathFragments,
names,
offsetFromRoot,
toJS,
@ -12,6 +13,10 @@ import { createTsConfig } from '../../../utils/create-ts-config';
import { getInSourceVitestTestsTemplate } from '../../../utils/get-in-source-vitest-tests-template';
import { NormalizedSchema } from '../schema';
import { getAppTests } from './get-app-tests';
import { maybeJs } from './add-project';
import { WithReactOptions } from '../../../../plugins/with-react';
import { WithNxOptions } from '@nx/webpack';
import { hasWebpackPlugin } from '../../../utils/has-webpack-plugin';
export function createApplicationFiles(host: Tree, options: NormalizedSchema) {
let styleSolutionSpecificAppFiles: string;
@ -53,7 +58,12 @@ export function createApplicationFiles(host: Tree, options: NormalizedSchema) {
host,
join(__dirname, '../files/base-webpack'),
options.appProjectRoot,
templateVariables
{
...templateVariables,
webpackPluginOptions: hasWebpackPlugin(host)
? createNxWebpackPluginOptions(options)
: null,
}
);
if (options.compiler === 'babel') {
writeJson(host, `${options.appProjectRoot}/.babelrc`, {
@ -154,3 +164,27 @@ export function createApplicationFiles(host: Tree, options: NormalizedSchema) {
relativePathToRootTsConfig
);
}
function createNxWebpackPluginOptions(
options: NormalizedSchema
): WithNxOptions & WithReactOptions {
return {
target: 'web',
compiler: options.compiler ?? 'babel',
outputPath: joinPathFragments(
'dist',
options.appProjectRoot != '.'
? options.appProjectRoot
: options.projectName
),
index: './src/index.html',
baseHref: '/',
main: maybeJs(options, `./src/main.tsx`),
tsConfig: './tsconfig.app.json',
assets: ['./src/favicon.ico', './src/assets'],
styles:
options.styledModule || !options.hasStyles
? []
: [`./src/styles.${options.style}`],
};
}

View File

@ -1,4 +1,10 @@
import { getProjects, logger, normalizePath, Tree } from '@nx/devkit';
import {
getProjects,
joinPathFragments,
logger,
normalizePath,
Tree,
} from '@nx/devkit';
import { determineProjectNameAndRootOptions } from '@nx/devkit/src/generators/project-name-and-root-utils';
import { assertValidStyle } from '../../../utils/assertion';
import { NormalizedSchema, Schema } from '../schema';
@ -74,10 +80,13 @@ export async function normalizeOptions(
);
}
try {
normalized.appMain = appProjectConfig.targets.build.options.main;
normalized.appSourceRoot = normalizePath(appProjectConfig.sourceRoot);
} catch (e) {
normalized.appMain =
appProjectConfig.targets.build.options.main ??
findMainEntry(host, appProjectConfig.root);
normalized.appSourceRoot = normalizePath(appProjectConfig.sourceRoot);
// TODO(jack): We should use appEntryFile instead of appProject so users can directly set it rather than us inferring it.
if (!normalized.appMain) {
throw new Error(
`Could not locate project main for ${options.appProject}`
);
@ -88,3 +97,30 @@ export async function normalizeOptions(
return normalized;
}
function findMainEntry(tree: Tree, projectRoot: string): string | undefined {
const mainFiles = [
// These are the main files we generate with.
'src/main.ts',
'src/main.tsx',
'src/main.js',
'src/main.jsx',
// Other options just in case
'src/index.ts',
'src/index.tsx',
'src/index.js',
'src/index.jsx',
'main.ts',
'main.tsx',
'main.js',
'main.jsx',
'index.ts',
'index.tsx',
'index.js',
'index.jsx',
];
const mainEntry = mainFiles.find((file) =>
tree.exists(joinPathFragments(projectRoot, file))
);
return mainEntry ? joinPathFragments(projectRoot, mainEntry) : undefined;
}

View File

@ -120,7 +120,6 @@ export async function setupSsrGenerator(tree: Tree, options: Schema) {
compiler: 'babel',
externalDependencies: 'all',
outputHashing: 'none',
isolatedConfig: true,
webpackConfig: joinPathFragments(projectRoot, 'webpack.config.js'),
},
configurations: {

View File

@ -1,15 +1,15 @@
import { ModuleFederationConfig } from '@nx/webpack/src/utils/module-federation';
import { getModuleFederationConfig } from './utils';
import type { AsyncNxWebpackPlugin } from '@nx/webpack';
import type { AsyncNxComposableWebpackPlugin } from '@nx/webpack';
import ModuleFederationPlugin = require('webpack/lib/container/ModuleFederationPlugin');
/**
* @param {ModuleFederationConfig} options
* @return {Promise<AsyncNxWebpackPlugin>}
* @return {Promise<AsyncNxComposableWebpackPlugin>}
*/
export async function withModuleFederation(
options: ModuleFederationConfig
): Promise<AsyncNxWebpackPlugin> {
): Promise<AsyncNxComposableWebpackPlugin> {
const { sharedDependencies, sharedLibraries, mappedRemotes } =
await getModuleFederationConfig(options);

View File

@ -0,0 +1,10 @@
import { readNxJson, Tree } from '@nx/devkit';
export function hasWebpackPlugin(tree: Tree) {
const nxJson = readNxJson(tree);
return !!nxJson.plugins?.some((p) =>
typeof p === 'string'
? p === '@nx/webpack/plugin'
: p.plugin === '@nx/webpack/plugin'
);
}

View File

@ -1,7 +1,7 @@
export interface Schema {
host: string;
port: number;
ssl: boolean;
host?: string;
port?: number;
ssl?: boolean;
sslKey?: string;
sslCert?: string;
proxyUrl?: string;

View File

@ -0,0 +1,53 @@
import { installedCypressVersion } from '@nx/cypress/src/utils/cypress-version';
import {
readNxJson,
readProjectConfiguration,
Tree,
updateNxJson,
} from '@nx/devkit';
import { createTreeWithEmptyWorkspace } from '@nx/devkit/testing';
import { applicationGenerator } from './application';
// need to mock cypress otherwise it'll use the nx installed version from package.json
// which is v9 while we are testing for the new v10 version
jest.mock('@nx/cypress/src/utils/cypress-version');
jest.mock('@nx/devkit', () => {
return {
...jest.requireActual('@nx/devkit'),
ensurePackage: jest.fn((pkg) => jest.requireActual(pkg)),
};
});
describe('web app generator (PCv3)', () => {
let tree: Tree;
let mockedInstalledCypressVersion: jest.Mock<
ReturnType<typeof installedCypressVersion>
> = installedCypressVersion as never;
beforeEach(() => {
mockedInstalledCypressVersion.mockReturnValue(10);
tree = createTreeWithEmptyWorkspace();
const nxJson = readNxJson(tree);
nxJson.plugins ??= [];
nxJson.plugins.push('@nx/webpack/plugin');
updateNxJson(tree, nxJson);
});
it('should setup webpack configuration', async () => {
await applicationGenerator(tree, {
name: 'my-app',
projectNameAndRootFormat: 'as-provided',
});
const targets = readProjectConfiguration(tree, 'my-app').targets;
expect(targets.build).toBeUndefined();
expect(targets.serve).toBeUndefined();
const webpackConfig = tree.read('my-app/webpack.config.js', 'utf-8');
expect(webpackConfig).toContain(`new NxWebpackPlugin`);
expect(webpackConfig).toContain(`'../dist/my-app'`);
expect(webpackConfig).toContain(`main: './src/main.ts'`);
expect(webpackConfig).toContain(`tsConfig: './tsconfig.app.json'`);
expect(webpackConfig).toContain(`styles: ['./src/styles.css']`);
expect(webpackConfig).toContain(
`assets: ['./src/favicon.ico', './src/assets']`
);
});
});

View File

@ -349,7 +349,7 @@ describe('app', () => {
);
});
it('should setup the nrwl web build builder', async () => {
it('should setup the web build builder', async () => {
await applicationGenerator(tree, {
name: 'my-app',
projectNameAndRootFormat: 'as-provided',
@ -386,7 +386,7 @@ describe('app', () => {
});
});
it('should setup the nrwl web dev server builder', async () => {
it('should setup the web dev server builder', async () => {
await applicationGenerator(tree, {
name: 'my-app',
projectNameAndRootFormat: 'as-provided',

View File

@ -27,6 +27,7 @@ import { nxVersion, swcLoaderVersion } from '../../utils/versions';
import { webInitGenerator } from '../init/init';
import { Schema } from './schema';
import { getNpmScope } from '@nx/js/src/utils/package-json/get-npm-scope';
import { hasWebpackPlugin } from '../../utils/has-webpack-plugin';
interface NormalizedSchema extends Schema {
projectName: string;
@ -37,26 +38,60 @@ interface NormalizedSchema extends Schema {
}
function createApplicationFiles(tree: Tree, options: NormalizedSchema) {
generateFiles(
tree,
join(
__dirname,
options.bundler === 'vite' ? './files/app-vite' : './files/app-webpack'
),
options.appProjectRoot,
{
...options,
...names(options.name),
tmpl: '',
offsetFromRoot: offsetFromRoot(options.appProjectRoot),
rootTsConfigPath: getRelativePathToRootTsConfig(
tree,
options.appProjectRoot
),
if (options.bundler === 'vite') {
generateFiles(
tree,
join(__dirname, './files/app-vite'),
options.appProjectRoot,
{
...options,
...names(options.name),
tmpl: '',
offsetFromRoot: offsetFromRoot(options.appProjectRoot),
rootTsConfigPath: getRelativePathToRootTsConfig(
tree,
options.appProjectRoot
),
}
);
} else {
generateFiles(
tree,
join(__dirname, './files/app-webpack'),
options.appProjectRoot,
{
...options,
...names(options.name),
tmpl: '',
offsetFromRoot: offsetFromRoot(options.appProjectRoot),
rootTsConfigPath: getRelativePathToRootTsConfig(
tree,
options.appProjectRoot
),
webpackPluginOptions: hasWebpackPlugin(tree)
? {
target: 'web',
outputPath: joinPathFragments(
'dist',
options.appProjectRoot != '.'
? options.appProjectRoot
: options.projectName
),
tsConfig: './tsconfig.app.json',
main: './src/main.ts',
assets: ['./src/favicon.ico', './src/assets'],
index: './src/index.html',
baseHref: '/',
styles: [`./src/styles.${options.style}`],
}
: null,
}
);
if (options.unitTestRunner === 'none') {
tree.delete(
join(options.appProjectRoot, './src/app/app.element.spec.ts')
);
}
);
if (options.unitTestRunner === 'none') {
tree.delete(join(options.appProjectRoot, './src/app/app.element.spec.ts'));
}
}
@ -89,43 +124,48 @@ async function setupBundler(tree: Tree, options: NormalizedSchema) {
skipFormat: true,
});
const project = readProjectConfiguration(tree, options.projectName);
const prodConfig = project.targets.build.configurations.production;
const buildOptions = project.targets.build.options;
buildOptions.assets = assets;
buildOptions.index = joinPathFragments(
options.appProjectRoot,
'src/index.html'
);
buildOptions.baseHref = '/';
buildOptions.styles = [
joinPathFragments(options.appProjectRoot, `src/styles.${options.style}`),
];
// We can delete that, because this projest is an application
// and applications have a .babelrc file in their root dir.
// So Nx will find it and use it
delete buildOptions.babelUpwardRootMode;
buildOptions.scripts = [];
prodConfig.fileReplacements = [
{
replace: joinPathFragments(
if (project.targets.build) {
const prodConfig = project.targets.build.configurations.production;
const buildOptions = project.targets.build.options;
buildOptions.assets = assets;
buildOptions.index = joinPathFragments(
options.appProjectRoot,
'src/index.html'
);
buildOptions.baseHref = '/';
buildOptions.styles = [
joinPathFragments(
options.appProjectRoot,
`src/environments/environment.ts`
`src/styles.${options.style}`
),
with: joinPathFragments(
options.appProjectRoot,
`src/environments/environment.prod.ts`
),
},
];
prodConfig.optimization = true;
prodConfig.outputHashing = 'all';
prodConfig.sourceMap = false;
prodConfig.namedChunks = false;
prodConfig.extractLicenses = true;
prodConfig.vendorChunk = false;
updateProjectConfiguration(tree, options.projectName, project);
} else if (options.bundler === 'none') {
];
// We can delete that, because this projest is an application
// and applications have a .babelrc file in their root dir.
// So Nx will find it and use it
delete buildOptions.babelUpwardRootMode;
buildOptions.scripts = [];
prodConfig.fileReplacements = [
{
replace: joinPathFragments(
options.appProjectRoot,
`src/environments/environment.ts`
),
with: joinPathFragments(
options.appProjectRoot,
`src/environments/environment.prod.ts`
),
},
];
prodConfig.optimization = true;
prodConfig.outputHashing = 'all';
prodConfig.sourceMap = false;
prodConfig.namedChunks = false;
prodConfig.extractLicenses = true;
prodConfig.vendorChunk = false;
updateProjectConfiguration(tree, options.projectName, project);
}
// TODO(jack): Flush this out... no bundler should be possible for web but the experience isn't holistic due to missing features (e.g. writing index.html).
} else if (options.bundler === 'none') {
const project = readProjectConfiguration(tree, options.projectName);
project.targets.build = {
executor: `@nx/js:${options.compiler}`,
@ -134,7 +174,6 @@ async function setupBundler(tree: Tree, options: NormalizedSchema) {
main,
outputPath: joinPathFragments('dist', options.appProjectRoot),
tsConfig,
assets,
},
};
updateProjectConfiguration(tree, options.projectName, project);
@ -158,10 +197,6 @@ async function addProject(tree: Tree, options: NormalizedSchema) {
},
options.standaloneConfig
);
if (options.bundler !== 'vite') {
await setupBundler(tree, options);
}
}
function setDefaults(tree: Tree, options: NormalizedSchema) {
@ -195,9 +230,14 @@ export async function applicationGeneratorInternal(host: Tree, schema: Schema) {
});
tasks.push(webTask);
createApplicationFiles(host, options);
await addProject(host, options);
if (options.bundler !== 'vite') {
await setupBundler(host, options);
}
createApplicationFiles(host, options);
if (options.bundler === 'vite') {
const { viteConfigurationGenerator, createOrEditViteConfig } =
ensurePackage<typeof import('@nx/vite')>('@nx/vite', nxVersion);

View File

@ -1,3 +0,0 @@
export const environment = {
production: true
};

View File

@ -1,6 +0,0 @@
// This file can be replaced during build by using the `fileReplacements` array.
// When building for production, this file is replaced with `environment.prod.ts`.
export const environment = {
production: false
};

View File

@ -1,8 +1,38 @@
<% if (webpackPluginOptions) { %>
const { NxWebpackPlugin } = require('@nx/webpack');
const { join } = require('path');
module.exports = {
output: {
path: join(__dirname, '<%= offsetFromRoot %><%= webpackPluginOptions.outputPath %>'),
},
devServer: {
port: 4200
},
plugins: [
new NxWebpackPlugin({
tsConfig: '<%= webpackPluginOptions.tsConfig %>',
main: '<%= webpackPluginOptions.main %>',
index: '<%= webpackPluginOptions.index %>',
baseHref: '<%= webpackPluginOptions.baseHref %>',
assets: <%- JSON.stringify(webpackPluginOptions.assets) %>,
styles: <%- JSON.stringify(webpackPluginOptions.styles) %>,
outputHashing: process.env['NODE_ENV'] === 'production' ? 'all' : 'none',
optimization: process.env['NODE_ENV'] === 'production',
})
],
};
<% } else { %>
const { composePlugins, withNx, withWeb } = require('@nx/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;
});
module.exports = composePlugins(
withNx(),
withWeb(),
(config) => {
// Update the webpack config as needed here.
// e.g. `config.plugins.push(new MyPlugin())`
return config;
}
);
<% } %>

View File

@ -0,0 +1,10 @@
import { readNxJson, Tree } from '@nx/devkit';
export function hasWebpackPlugin(tree: Tree) {
const nxJson = readNxJson(tree);
return !!nxJson.plugins?.some((p) =>
typeof p === 'string'
? p === '@nx/webpack/plugin'
: p.plugin === '@nx/webpack/plugin'
);
}

View File

@ -0,0 +1 @@
export { createNodes } from './src/plugins/plugin';

View File

@ -9,17 +9,18 @@ import { eachValueFrom } from '@nx/devkit/src/utils/rxjs-for-await';
import { map, tap } from 'rxjs/operators';
import * as WebpackDevServer from 'webpack-dev-server';
import { getDevServerConfig } from './lib/get-dev-server-config';
import { getDevServerOptions } from './lib/get-dev-server-config';
import {
calculateProjectBuildableDependencies,
createTmpTsConfig,
} from '@nx/js/src/utils/buildable-libs-utils';
import { runWebpackDevServer } from '../../utils/run-webpack';
import { resolveCustomWebpackConfig } from '../../utils/webpack/custom-webpack';
import { resolveUserDefinedWebpackConfig } from '../../utils/webpack/resolve-user-defined-webpack-config';
import { normalizeOptions } from '../webpack/lib/normalize-options';
import { WebpackExecutorOptions } from '../webpack/schema';
import { WebDevServerOptions } from './schema';
import { join } from 'path';
import { isNxWebpackComposablePlugin } from '../../utils/config';
import { getRootTsConfigPath } from '@nx/js';
export async function* devServerExecutor(
serveOptions: WebDevServerOptions,
@ -37,13 +38,13 @@ export async function* devServerExecutor(
sourceRoot
);
if (!buildOptions.index) {
throw new Error(
`Cannot run dev-server without "index" option. Check the build options for ${context.projectName}.`
);
}
// TODO(jack): Figure out a way to port this into NxWebpackPlugin
if (!buildOptions.buildLibsFromSource) {
if (!buildOptions.tsConfig) {
throw new Error(
`Cannot find "tsConfig" to remap paths for. Set this option in project.json.`
);
}
const { target, dependencies } = calculateProjectBuildableDependencies(
context.taskGraph,
context.projectGraph,
@ -60,37 +61,35 @@ export async function* devServerExecutor(
);
}
let config = getDevServerConfig(context, buildOptions, serveOptions);
let config;
const devServer = getDevServerOptions(
context.root,
serveOptions,
buildOptions
);
if (buildOptions.webpackConfig) {
let tsconfigPath = buildOptions.tsConfig.startsWith(context.root)
? buildOptions.tsConfig
: join(context.root, buildOptions.tsConfig);
let customWebpack = resolveCustomWebpackConfig(
let userDefinedWebpackConfig = resolveUserDefinedWebpackConfig(
buildOptions.webpackConfig,
tsconfigPath
getRootTsConfigPath()
);
if (typeof customWebpack.then === 'function') {
customWebpack = await customWebpack;
if (typeof userDefinedWebpackConfig.then === 'function') {
userDefinedWebpackConfig = await userDefinedWebpackConfig;
}
if (typeof customWebpack === 'function') {
// Old behavior, call the webpack function that is specific to Nx
config = await customWebpack(config, {
options: buildOptions,
context,
configuration: serveOptions.buildTarget.split(':')[2],
});
} else if (customWebpack) {
// New behavior, use the config object as is with devServer defaults
config = {
devServer: {
...customWebpack.devServer,
...config.devServer,
},
...customWebpack,
};
// Only add the dev server option if user is composable plugin.
// Otherwise, user should define `devServer` option directly in their webpack config.
if (isNxWebpackComposablePlugin(userDefinedWebpackConfig)) {
config = await userDefinedWebpackConfig(
{ devServer },
{
options: buildOptions,
context,
configuration: serveOptions.buildTarget.split(':')[2],
}
);
}
}

View File

@ -1,36 +1,14 @@
import { ExecutorContext, logger } from '@nx/devkit';
import type { Configuration as WebpackConfiguration } from 'webpack';
import { logger } from '@nx/devkit';
import type { Configuration as WebpackDevServerConfiguration } from 'webpack-dev-server';
import * as path from 'path';
import { readFileSync } from 'fs';
import { getWebpackConfig } from '../../webpack/lib/get-webpack-config';
import { WebDevServerOptions } from '../schema';
import { buildServePath } from './serve-path';
import { NormalizedWebpackExecutorOptions } from '../../webpack/schema';
export function getDevServerConfig(
context: ExecutorContext,
buildOptions: NormalizedWebpackExecutorOptions,
serveOptions: WebDevServerOptions
): Partial<WebpackConfiguration> {
const workspaceRoot = context.root;
const webpackConfig = buildOptions.isolatedConfig
? {}
: getWebpackConfig(context, buildOptions);
(webpackConfig as any).devServer = getDevServerPartial(
workspaceRoot,
serveOptions,
buildOptions
);
return webpackConfig as WebpackConfiguration;
}
function getDevServerPartial(
export function getDevServerOptions(
root: string,
options: WebDevServerOptions,
serveOptions: WebDevServerOptions,
buildOptions: NormalizedWebpackExecutorOptions
): WebpackDevServerConfiguration {
const servePath = buildServePath(buildOptions);
@ -47,11 +25,13 @@ function getDevServerPartial(
}
const config: WebpackDevServerConfiguration = {
host: options.host,
port: options.port,
host: serveOptions.host,
port: serveOptions.port,
headers: { 'Access-Control-Allow-Origin': '*' },
historyApiFallback: {
index: `${servePath}${path.basename(buildOptions.index)}`,
index:
buildOptions.index &&
`${servePath}${path.basename(buildOptions.index)}`,
disableDotRule: true,
htmlAcceptHeaders: ['text/html', 'application/xhtml+xml'],
},
@ -66,7 +46,7 @@ function getDevServerPartial(
)}`
);
},
open: options.open,
open: serveOptions.open,
static: false,
compress: scriptsOptimization || stylesOptimization,
devMiddleware: {
@ -74,31 +54,31 @@ function getDevServerPartial(
stats: false,
},
client: {
webSocketURL: options.publicHost,
webSocketURL: serveOptions.publicHost,
overlay: {
errors: !(scriptsOptimization || stylesOptimization),
warnings: false,
},
},
liveReload: options.hmr ? false : options.liveReload, // disable liveReload if hmr is enabled
hot: options.hmr,
liveReload: serveOptions.hmr ? false : serveOptions.liveReload, // disable liveReload if hmr is enabled
hot: serveOptions.hmr,
};
if (options.ssl) {
if (serveOptions.ssl) {
config.server = {
type: 'https',
};
if (options.sslKey && options.sslCert) {
config.server.options = getSslConfig(root, options);
if (serveOptions.sslKey && serveOptions.sslCert) {
config.server.options = getSslConfig(root, serveOptions);
}
}
if (options.proxyConfig) {
config.proxy = getProxyConfig(root, options);
if (serveOptions.proxyConfig) {
config.proxy = getProxyConfig(root, serveOptions);
}
if (options.allowedHosts) {
config.allowedHosts = options.allowedHosts.split(',');
if (serveOptions.allowedHosts) {
config.allowedHosts = serveOptions.allowedHosts.split(',');
}
return config;

View File

@ -1,17 +1,17 @@
export interface WebDevServerOptions {
host: string;
port: number;
host?: string;
port?: number;
publicHost?: string;
ssl: boolean;
ssl?: boolean;
sslKey?: string;
sslCert?: string;
proxyConfig?: string;
buildTarget: string;
open: boolean;
liveReload: boolean;
hmr: boolean;
watch: boolean;
allowedHosts: string;
open?: boolean;
liveReload?: boolean;
hmr?: boolean;
watch?: boolean;
allowedHosts?: string;
memoryLimit?: number;
baseHref?: string;
}

View File

@ -1,27 +0,0 @@
import type { Configuration } from 'webpack';
import { ExecutorContext } from '@nx/devkit';
import { NormalizedWebpackExecutorOptions } from '../schema';
import { withNx } from '../../../utils/with-nx';
import { withWeb } from '../../../utils/with-web';
import { composePluginsSync } from '../../../utils/config';
interface GetWebpackConfigOverrides {
root: string;
sourceRoot: string;
configuration?: string;
}
/** @deprecated Use withNx, withWeb, or withReact */
// TODO(jack): Remove in Nx 16
export function getWebpackConfig(
context: ExecutorContext,
options: NormalizedWebpackExecutorOptions
): Configuration {
const config: Configuration = {};
const configure =
options.target === 'web'
? composePluginsSync(withNx(), withWeb())
: withNx();
return configure(config, { options, context });
}

View File

@ -15,14 +15,13 @@ export function normalizeOptions(
projectRoot: string,
sourceRoot: string
): NormalizedWebpackExecutorOptions {
return {
const normalizedOptions = {
...options,
root,
projectRoot,
sourceRoot,
target: options.target ?? 'web',
outputFileName: options.outputFileName ?? 'main.js',
assets: normalizeAssets(options.assets, root, sourceRoot),
webpackConfig: normalizePluginPath(options.webpackConfig, root),
fileReplacements: normalizeFileReplacements(root, options.fileReplacements),
optimization:
@ -33,6 +32,14 @@ export function normalizeOptions(
}
: options.optimization,
};
if (options.assets) {
normalizedOptions.assets = normalizeAssets(
options.assets,
root,
sourceRoot
);
}
return normalizedOptions as NormalizedWebpackExecutorOptions;
}
export function normalizePluginPath(pluginPath: void | string, root: string) {

View File

@ -47,8 +47,10 @@ export interface WebpackExecutorOptions {
extractLicenses?: boolean;
fileReplacements?: FileReplacement[];
generatePackageJson?: boolean;
// TODO(v18): Remove this option
/** @deprecated set webpackConfig and provide an explicit webpack.config.js file (See: https://nx.dev/recipes/webpack/webpack-config-setup) */
isolatedConfig?: boolean;
main: string;
main?: string;
memoryLimit?: number;
namedChunks?: boolean;
optimization?: boolean | OptimizationOptions;
@ -61,9 +63,9 @@ export interface WebpackExecutorOptions {
runtimeChunk?: boolean;
sourceMap?: boolean | 'hidden';
statsJson?: boolean;
target?: 'node' | 'web' | 'webworker';
target?: string;
transformers?: TransformerEntry[];
tsConfig: string;
tsConfig?: string;
vendorChunk?: boolean;
verbose?: boolean;
watch?: boolean;

View File

@ -27,8 +27,7 @@
"compiler": {
"type": "string",
"description": "The compiler to use.",
"enum": ["babel", "swc", "tsc"],
"default": "babel"
"enum": ["babel", "swc", "tsc"]
},
"outputPath": {
"type": "string",
@ -40,8 +39,7 @@
"type": "string",
"alias": "platform",
"description": "Target platform for the build, same as the Webpack target option.",
"enum": ["node", "web", "webworker"],
"default": "web"
"enum": ["node", "web", "webworker"]
},
"deleteOutputPath": {
"type": "boolean",
@ -50,8 +48,7 @@
},
"watch": {
"type": "boolean",
"description": "Enable re-building when files change.",
"default": false
"description": "Enable re-building when files change."
},
"baseHref": {
"type": "string",
@ -63,22 +60,18 @@
},
"vendorChunk": {
"type": "boolean",
"description": "Use a separate bundle containing only vendor libraries.",
"default": true
"description": "Use a separate bundle containing only vendor libraries."
},
"commonChunk": {
"type": "boolean",
"description": "Use a separate bundle containing code used across multiple bundles.",
"default": true
"description": "Use a separate bundle containing code used across multiple bundles."
},
"runtimeChunk": {
"type": "boolean",
"description": "Use a separate bundle containing the runtime.",
"default": true
"description": "Use a separate bundle containing the runtime."
},
"sourceMap": {
"description": "Output sourcemaps. Use 'hidden' for use with error reporting tools without generating sourcemap comment.",
"default": true,
"oneOf": [
{
"type": "boolean"
@ -90,13 +83,11 @@
},
"progress": {
"type": "boolean",
"description": "Log progress to the console while building.",
"default": false
"description": "Log progress to the console while building."
},
"assets": {
"type": "array",
"description": "List of static application assets.",
"default": [],
"items": {
"$ref": "#/definitions/assetPattern"
}
@ -112,26 +103,22 @@
"description": "External Scripts which will be included before the main application entry.",
"items": {
"$ref": "#/definitions/extraEntryPoint"
},
"default": []
}
},
"styles": {
"type": "array",
"description": "External Styles which will be included with the application",
"items": {
"$ref": "#/definitions/extraEntryPoint"
},
"default": []
}
},
"namedChunks": {
"type": "boolean",
"description": "Names the produced bundles according to their entry file.",
"default": true
"description": "Names the produced bundles according to their entry file."
},
"outputHashing": {
"type": "string",
"description": "Define the output filename cache-busting hashing mode.",
"default": "none",
"enum": ["none", "all", "media", "bundles"]
},
"stylePreprocessorOptions": {
@ -143,8 +130,7 @@
"type": "array",
"items": {
"type": "string"
},
"default": []
}
}
},
"additionalProperties": false
@ -175,13 +161,11 @@
},
"generatePackageJson": {
"type": "boolean",
"description": "Generates a `package.json` and pruned lock file with the project's `node_module` dependencies populated for installing in a container. If a `package.json` exists in the project's directory, it will be reused with dependencies populated.",
"default": false
"description": "Generates a `package.json` and pruned lock file with the project's `node_module` dependencies populated for installing in a container. If a `package.json` exists in the project's directory, it will be reused with dependencies populated."
},
"transformers": {
"type": "array",
"description": "List of TypeScript Compiler Transfomers Plugins.",
"default": [],
"aliases": ["tsPlugins"],
"items": {
"$ref": "#/definitions/transformerPattern"
@ -223,18 +207,15 @@
}
}
],
"description": "Dependencies to keep external to the bundle. (`all` (default), `none`, or an array of module names)",
"default": "all"
"description": "Dependencies to keep external to the bundle. (`all` (default), `none`, or an array of module names)"
},
"extractCss": {
"type": "boolean",
"description": "Extract CSS into a `.css` file.",
"default": true
"description": "Extract CSS into a `.css` file."
},
"subresourceIntegrity": {
"type": "boolean",
"description": "Enables the use of subresource integrity validation.",
"default": false
"description": "Enables the use of subresource integrity validation."
},
"polyfills": {
"type": "string",
@ -244,28 +225,25 @@
},
"verbose": {
"type": "boolean",
"description": "Emits verbose output",
"default": false
"description": "Emits verbose output"
},
"statsJson": {
"type": "boolean",
"description": "Generates a 'stats.json' file which can be analyzed using tools such as: 'webpack-bundle-analyzer' or `<https://webpack.github.io/analyse>`.",
"default": false
"description": "Generates a 'stats.json' file which can be analyzed using tools such as: 'webpack-bundle-analyzer' or `<https://webpack.github.io/analyse>`."
},
"isolatedConfig": {
"type": "boolean",
"description": "Do not apply Nx webpack plugins automatically. Plugins need to be applied in the project's webpack.config.js file (e.g. withNx, withReact, etc.).",
"default": true
"default": true,
"x-deprecated": "Automatic configuration of Webpack is deprecated in favor of an explicit 'webpack.config.js' file. This option will be removed in Nx 18. See https://nx.dev/recipes/webpack/webpack-config-setup."
},
"extractLicenses": {
"type": "boolean",
"description": "Extract all licenses in a separate file, in the case of production builds only.",
"default": false
"description": "Extract all licenses in a separate file, in the case of production builds only."
},
"memoryLimit": {
"type": "number",
"description": "Memory limit for type checking service process in `MB`.",
"default": 2048
"description": "Memory limit for type checking service process in `MB`."
},
"fileReplacements": {
"description": "Replace files with other files in the build.",
@ -286,18 +264,16 @@
},
"additionalProperties": false,
"required": ["replace", "with"]
},
"default": []
}
},
"buildLibsFromSource": {
"type": "boolean",
"description": "Read buildable libraries from source instead of building them separately.",
"description": "Read buildable libraries from source instead of building them separately. If set to `false`, the `tsConfig` option must also be set to remap paths.",
"default": true
},
"generateIndexHtml": {
"type": "boolean",
"description": "Generates `index.html` file to the output path. This can be turned off if using a webpack plugin to generate HTML such as `html-webpack-plugin`.",
"default": true
"description": "Generates `index.html` file to the output path. This can be turned off if using a webpack plugin to generate HTML such as `html-webpack-plugin`."
},
"postcssConfig": {
"type": "string",
@ -312,8 +288,7 @@
},
"babelUpwardRootMode": {
"type": "boolean",
"description": "Whether to set rootmode to upward. See https://babeljs.io/docs/en/options#rootmode",
"default": false
"description": "Whether to set rootmode to upward. See https://babeljs.io/docs/en/options#rootmode"
},
"babelConfig": {
"type": "string",
@ -321,7 +296,7 @@
"x-completion-type": "file"
}
},
"required": ["tsConfig", "main"],
"required": [],
"definitions": {
"assetPattern": {
"oneOf": [

View File

@ -9,25 +9,29 @@ import {
switchMap,
tap,
} from 'rxjs/operators';
import { join, resolve } from 'path';
import { resolve } from 'path';
import {
calculateProjectBuildableDependencies,
createTmpTsConfig,
} from '@nx/js/src/utils/buildable-libs-utils';
import { getWebpackConfig } from './lib/get-webpack-config';
import { runWebpack } from './lib/run-webpack';
import { deleteOutputDir } from '../../utils/fs';
import { resolveCustomWebpackConfig } from '../../utils/webpack/custom-webpack';
import { resolveUserDefinedWebpackConfig } from '../../utils/webpack/resolve-user-defined-webpack-config';
import type {
NormalizedWebpackExecutorOptions,
WebpackExecutorOptions,
} from './schema';
import { normalizeOptions } from './lib/normalize-options';
import {
composePlugins,
isNxWebpackComposablePlugin,
} from '../../utils/config';
import { withNx } from '../../utils/with-nx';
import { getRootTsConfigPath } from '@nx/js';
import { withWeb } from '../../utils/with-web';
async function getWebpackConfigs(
options: NormalizedWebpackExecutorOptions,
projectRoot: string,
context: ExecutorContext
): Promise<Configuration | Configuration[]> {
if (options.isolatedConfig && !options.webpackConfig) {
@ -36,34 +40,32 @@ async function getWebpackConfigs(
);
}
let customWebpack = null;
if (options.webpackConfig && options.tsConfig) {
customWebpack = resolveCustomWebpackConfig(
let userDefinedWebpackConfig = null;
if (options.webpackConfig) {
userDefinedWebpackConfig = resolveUserDefinedWebpackConfig(
options.webpackConfig,
options.tsConfig.startsWith(context.root)
? options.tsConfig
: join(context.root, options.tsConfig)
getRootTsConfigPath()
);
if (typeof customWebpack.then === 'function') {
customWebpack = await customWebpack;
if (typeof userDefinedWebpackConfig.then === 'function') {
userDefinedWebpackConfig = await userDefinedWebpackConfig;
}
}
const config = options.isolatedConfig
? {}
: getWebpackConfig(context, options);
: composePlugins(withNx(options), withWeb(options));
if (typeof customWebpack === 'function') {
if (isNxWebpackComposablePlugin(userDefinedWebpackConfig)) {
// Old behavior, call the Nx-specific webpack config function that user exports
return await customWebpack(config, {
return await userDefinedWebpackConfig(config, {
options,
context,
configuration: context.configurationName, // backwards compat
});
} else if (customWebpack) {
} else if (userDefinedWebpackConfig) {
// New behavior, we want the webpack config to export object
return customWebpack;
return userDefinedWebpackConfig;
} else {
// Fallback case, if we cannot find a webpack config path
return config;
@ -86,8 +88,8 @@ export async function* webpackExecutor(
_options: WebpackExecutorOptions,
context: ExecutorContext
): AsyncGenerator<WebpackExecutorEvent, WebpackExecutorEvent, undefined> {
// Pass to NxWebpackPlugin so we can get the CLI overrides.
process.env['NX_WEBPACK_EXECUTOR_RAW_OPTIONS'] = JSON.stringify(_options);
// Default to production build.
process.env['NODE_ENV'] ||= 'production';
const metadata = context.projectsConfigurations.projects[context.projectName];
const sourceRoot = metadata.sourceRoot;
@ -118,11 +120,6 @@ export async function* webpackExecutor(
);
return {
success: false,
outfile: resolve(
context.root,
options.outputPath,
options.outputFileName
),
options,
};
}
@ -157,7 +154,7 @@ export async function* webpackExecutor(
);
}
const configs = await getWebpackConfigs(options, metadata.root, context);
const configs = await getWebpackConfigs(options, context);
return yield* eachValueFrom(
of(configs).pipe(
@ -184,6 +181,8 @@ export async function* webpackExecutor(
const success = results.every(
(result) => Boolean(result) && !result.hasErrors()
);
// TODO(jack): This should read output from webpack config if provided.
// The outfile is only used by NestJS, where `@nx/js:node` executor requires it to run the file.
return {
success,
outfile: resolve(

View File

@ -0,0 +1,58 @@
import {
addProjectConfiguration,
readNxJson,
readProjectConfiguration,
Tree,
updateNxJson,
} from '@nx/devkit';
import { createTreeWithEmptyWorkspace } from '@nx/devkit/testing';
import configurationGenerator from './configuration';
describe('webpackProject', () => {
let tree: Tree;
beforeEach(async () => {
tree = createTreeWithEmptyWorkspace({ layout: 'apps-libs' });
const nxJson = readNxJson(tree);
nxJson.plugins ??= [];
nxJson.plugins.push('@nx/webpack/plugin');
updateNxJson(tree, nxJson);
addProjectConfiguration(tree, 'mypkg', {
root: 'libs/mypkg',
sourceRoot: 'libs/mypkg/src',
targets: {},
});
});
it('should generate files', async () => {
await configurationGenerator(tree, {
project: 'mypkg',
});
const project = readProjectConfiguration(tree, 'mypkg');
expect(project.targets.build).toBeUndefined();
expect(project.targets.serve).toBeUndefined();
});
it('should support --main option', async () => {
await configurationGenerator(tree, {
project: 'mypkg',
main: 'libs/mypkg/index.ts',
});
expect(tree.read('libs/mypkg/webpack.config.js', 'utf-8')).toContain(
`main: 'libs/mypkg/index.ts'`
);
});
it('should support --tsConfig option', async () => {
await configurationGenerator(tree, {
project: 'mypkg',
tsConfig: 'libs/mypkg/tsconfig.custom.json',
});
expect(tree.read('libs/mypkg/webpack.config.js', 'utf-8')).toContain(
`tsConfig: 'libs/mypkg/tsconfig.custom.json'`
);
});
});

View File

@ -10,6 +10,7 @@ import {
import { webpackInitGenerator } from '../init/init';
import { ConfigurationGeneratorSchema } from './schema';
import { WebpackExecutorOptions } from '../../executors/webpack/schema';
import { hasPlugin } from '../../utils/has-plugin';
export async function configurationGenerator(
tree: Tree,
@ -20,11 +21,16 @@ export async function configurationGenerator(
skipFormat: true,
});
checkForTargetConflicts(tree, options);
addBuildTarget(tree, options);
if (options.devServer) {
addServeTarget(tree, options);
if (!hasPlugin(tree)) {
addBuildTarget(tree, options);
if (options.devServer) {
addServeTarget(tree, options);
}
}
createWebpackConfig(tree, options);
if (!options.skipFormat) {
await formatFiles(tree);
}
@ -53,6 +59,88 @@ function checkForTargetConflicts(
}
}
function createWebpackConfig(
tree: Tree,
options: ConfigurationGeneratorSchema
) {
const project = readProjectConfiguration(tree, options.project);
const buildOptions: WebpackExecutorOptions = {
target: options.target,
outputPath: joinPathFragments('dist', project.root),
compiler: options.compiler ?? 'swc',
main: options.main ?? joinPathFragments(project.root, 'src/main.ts'),
tsConfig:
options.tsConfig ?? joinPathFragments(project.root, 'tsconfig.app.json'),
webpackConfig: joinPathFragments(project.root, 'webpack.config.js'),
};
if (options.target === 'web') {
tree.write(
joinPathFragments(project.root, 'webpack.config.js'),
hasPlugin(tree)
? `
const { NxWebpackPlugin } = require('@nx/webpack');
module.exports = {
output: {
path: '${buildOptions.outputPath}',
},
plugins: [
new NxWebpackPlugin({
target: '${buildOptions.target}',
tsConfig: '${buildOptions.tsConfig}',
compiler: '${buildOptions.compiler}',
main: '${buildOptions.main}',
})
],
}
`
: `
const { composePlugins, withNx, withWeb } = require('@nx/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;
});
`
);
} else {
tree.write(
joinPathFragments(project.root, 'webpack.config.js'),
hasPlugin(tree)
? `
const { NxWebpackPlugin } = require('@nx/webpack');
module.exports = {
output: {
path: '${buildOptions.outputPath}',
},
plugins: [
new NxWebpackPlugin({
target: '${buildOptions.target}',
tsConfig: '${buildOptions.tsConfig}',
compiler: '${buildOptions.compiler}',
main: '${buildOptions.main}',
})
],
}
`
: `
const { composePlugins, withNx } = require('@nx/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;
});
`
);
}
}
function addBuildTarget(tree: Tree, options: ConfigurationGeneratorSchema) {
const project = readProjectConfiguration(tree, options.project);
const buildOptions: WebpackExecutorOptions = {
@ -78,35 +166,6 @@ function addBuildTarget(tree: Tree, options: ConfigurationGeneratorSchema) {
});
}
if (options.target === 'web') {
tree.write(
joinPathFragments(project.root, 'webpack.config.js'),
`
const { composePlugins, withNx, withWeb } = require('@nx/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;
});
`
);
} else {
tree.write(
joinPathFragments(project.root, 'webpack.config.js'),
`
const { composePlugins, withNx } = require('@nx/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;
});
`
);
}
updateProjectConfiguration(tree, options.project, {
...project,
targets: {

View File

@ -0,0 +1,38 @@
import { readJson, Tree } from '@nx/devkit';
import { createTreeWithEmptyWorkspace } from '@nx/devkit/testing';
import { webpackInitGenerator } from './init';
describe('webpackInitGenerator (PCv3)', () => {
let tree: Tree;
let previousEnv: string | undefined;
beforeEach(async () => {
previousEnv = process.env.NX_PCV3;
process.env.NX_PCV3 = 'true';
tree = createTreeWithEmptyWorkspace({ layout: 'apps-libs' });
});
afterEach(() => {
process.env.NX_PCV3 = previousEnv;
});
it('should install webpack-cli', async () => {
await webpackInitGenerator(tree, { compiler: 'swc' });
const packageJson = readJson(tree, 'package.json');
expect(packageJson).toEqual({
name: expect.any(String),
dependencies: {
'@swc/helpers': expect.any(String),
},
devDependencies: {
'@nx/webpack': expect.any(String),
'@swc/cli': expect.any(String),
'@swc/core': expect.any(String),
'swc-loader': expect.any(String),
'webpack-cli': expect.any(String),
},
});
});
});

View File

@ -1,4 +1,4 @@
import { Tree, readJson, NxJsonConfiguration, updateJson } from '@nx/devkit';
import { readJson, Tree } from '@nx/devkit';
import { createTreeWithEmptyWorkspace } from '@nx/devkit/testing';
import { webpackInitGenerator } from './init';

View File

@ -2,8 +2,10 @@ import {
addDependenciesToPackageJson,
formatFiles,
GeneratorCallback,
readNxJson,
runTasksInSerial,
Tree,
updateNxJson,
} from '@nx/devkit';
import { addSwcDependencies } from '@nx/js/src/utils/swc/add-swc-dependencies';
@ -16,15 +18,21 @@ import {
swcLoaderVersion,
tsLibVersion,
urlLoaderVersion,
webpackCliVersion,
} from '../../utils/versions';
import { addBabelInputs } from '@nx/js/src/utils/add-babel-inputs';
import { WebpackPluginOptions } from '../../plugins/plugin';
export async function webpackInitGenerator(tree: Tree, schema: Schema) {
const shouldAddPlugin = process.env.NX_PCV3 === 'true';
const tasks: GeneratorCallback[] = [];
const devDependencies = {
'@nx/webpack': nxVersion,
};
if (shouldAddPlugin) {
devDependencies['webpack-cli'] = webpackCliVersion;
}
if (schema.compiler === 'swc') {
devDependencies['swc-loader'] = swcLoaderVersion;
const addSwcTask = addSwcDependencies(tree);
@ -47,14 +55,41 @@ export async function webpackInitGenerator(tree: Tree, schema: Schema) {
await formatFiles(tree);
}
const baseInstalTask = addDependenciesToPackageJson(
const baseInstallTask = addDependenciesToPackageJson(
tree,
{},
devDependencies
);
tasks.push(baseInstalTask);
tasks.push(baseInstallTask);
if (shouldAddPlugin) addPlugin(tree);
return runTasksInSerial(...tasks);
}
function addPlugin(tree: Tree) {
const nxJson = readNxJson(tree);
nxJson.plugins ??= [];
for (const plugin of nxJson.plugins) {
if (
typeof plugin === 'string'
? plugin === '@nx/webpack/plugin'
: plugin.plugin === '@nx/webpack/plugin'
) {
return;
}
}
nxJson.plugins.push({
plugin: '@nx/webpack/plugin',
options: {
buildTargetName: 'build',
serveTargetName: 'serve',
previewTargetName: 'preview',
} as WebpackPluginOptions,
});
updateNxJson(tree, nxJson);
}
export default webpackInitGenerator;

View File

@ -8,6 +8,7 @@ import {
WebpackOptionsNormalized,
WebpackPluginInstance,
} from 'webpack';
import { getRootTsConfigPath } from '@nx/js';
import { StatsJsonPlugin } from '../../stats-json-plugin';
import { GeneratePackageJsonPlugin } from '../../generate-package-json-plugin';
@ -40,8 +41,173 @@ export function applyBaseConfig(
useNormalizedEntry?: boolean;
} = {}
): void {
// Defaults that was applied from executor schema previously.
options.compiler ??= 'babel';
options.deleteOutputPath ??= true;
options.externalDependencies ??= 'all';
options.fileReplacements ??= [];
options.memoryLimit ??= 2048;
options.transformers ??= [];
applyNxIndependentConfig(options, config);
// Some of the options only work during actual tasks, not when reading the webpack config during CreateNodes.
if (!process.env['NX_TASK_TARGET_PROJECT']) return;
applyNxDependentConfig(options, config, { useNormalizedEntry });
}
function applyNxIndependentConfig(
options: NormalizedNxWebpackPluginOptions,
config: Partial<WebpackOptionsNormalized | Configuration>
): void {
const hashFormat = getOutputHashFormat(options.outputHashing as string);
config.context = path.join(options.root, options.projectRoot);
config.target ??= options.target;
config.node = false;
config.mode =
// When the target is Node avoid any optimizations, such as replacing `process.env.NODE_ENV` with build time value.
config.target === 'node'
? 'none'
: // Otherwise, make sure it matches `process.env.NODE_ENV`.
// When mode is development or production, webpack will automatically
// configure DefinePlugin to replace `process.env.NODE_ENV` with the
// build-time value. Thus, we need to make sure it's the same value to
// avoid conflicts.
//
// When the NODE_ENV is something else (e.g. test), then set it to none
// to prevent extra behavior from webpack.
process.env.NODE_ENV === 'development' ||
process.env.NODE_ENV === 'production'
? (process.env.NODE_ENV as 'development' | 'production')
: 'none';
// When target is Node, the Webpack mode will be set to 'none' which disables in memory caching and causes a full rebuild on every change.
// So to mitigate this we enable in memory caching when target is Node and in watch mode.
config.cache =
options.target === 'node' && options.watch ? { type: 'memory' } : undefined;
config.devtool =
options.sourceMap === 'hidden'
? 'hidden-source-map'
: options.sourceMap
? 'source-map'
: false;
config.output = {
...config.output,
path:
config.output?.path ??
(options.outputPath
? path.join(options.root, options.outputPath)
: undefined),
filename:
config.output?.filename ?? options.outputHashing
? `[name]${hashFormat.script}.js`
: '[name].js',
chunkFilename:
config.output?.chunkFilename ?? options.outputHashing
? `[name]${hashFormat.chunk}.js`
: '[name].js',
hashFunction: config.output?.hashFunction ?? 'xxhash64',
// Disabled for performance
pathinfo: config.output?.pathinfo ?? false,
// Use CJS for Node since it has the widest support.
scriptType:
config.output?.scriptType ?? options.target === 'node'
? undefined
: 'module',
};
config.watch = options.watch;
config.watchOptions = {
poll: options.poll,
};
config.profile = options.statsJson;
config.performance = {
...config.performance,
hints: false,
};
config.experiments = { ...config.experiments, cacheUnaffected: true };
config.ignoreWarnings = [
(x) =>
IGNORED_WEBPACK_WARNINGS.some((r) =>
typeof x === 'string' ? r.test(x) : r.test(x.message)
),
];
config.optimization = {
...config.optimization,
sideEffects: true,
minimize:
typeof options.optimization === 'object'
? !!options.optimization.scripts
: !!options.optimization,
minimizer: [
options.compiler !== 'swc'
? new TerserPlugin({
parallel: true,
terserOptions: {
keep_classnames: true,
ecma: getTerserEcmaVersion(
path.join(options.root, options.projectRoot)
),
safari10: true,
format: {
ascii_only: true,
comments: false,
webkit: true,
},
},
extractComments: false,
})
: new TerserPlugin({
minify: TerserPlugin.swcMinify,
// `terserOptions` options will be passed to `swc`
terserOptions: {
module: true,
mangle: false,
},
}),
],
runtimeChunk: false,
concatenateModules: true,
};
config.stats = {
hash: true,
timings: false,
cached: false,
cachedAssets: false,
modules: false,
warnings: true,
errors: true,
colors: !options.verbose && !options.statsJson,
chunks: !options.verbose,
assets: !!options.verbose,
chunkOrigins: !!options.verbose,
chunkModules: !!options.verbose,
children: !!options.verbose,
reasons: !!options.verbose,
version: !!options.verbose,
errorDetails: !!options.verbose,
moduleTrace: !!options.verbose,
usedExports: !!options.verbose,
};
}
function applyNxDependentConfig(
options: NormalizedNxWebpackPluginOptions,
config: Partial<WebpackOptionsNormalized | Configuration>,
{ useNormalizedEntry }: { useNormalizedEntry?: boolean } = {}
): void {
const tsConfig = options.tsConfig ?? getRootTsConfigPath();
const plugins: WebpackPluginInstance[] = [
new NxTsconfigPathsWebpackPlugin(options),
new NxTsconfigPathsWebpackPlugin({ tsConfig }),
];
const executorContext: Partial<ExecutorContext> = {
projectName: options.projectName,
@ -55,9 +221,9 @@ export function applyBaseConfig(
plugins.push(
new ForkTsCheckerWebpackPlugin({
typescript: {
configFile: path.isAbsolute(options.tsConfig)
? options.tsConfig
: path.join(options.root, options.tsConfig),
configFile: path.isAbsolute(tsConfig)
? tsConfig
: path.join(options.root, tsConfig),
memoryLimit: options.memoryLimit || 2018,
},
})
@ -141,7 +307,7 @@ export function applyBaseConfig(
);
}
if (options.generatePackageJson && executorContext) {
plugins.push(new GeneratePackageJsonPlugin(options));
plugins.push(new GeneratePackageJsonPlugin({ ...options, tsConfig }));
}
if (options.statsJson) {
@ -163,138 +329,23 @@ export function applyBaseConfig(
});
}
const hashFormat = getOutputHashFormat(options.outputHashing as string);
config.context = path.join(options.root, options.projectRoot);
config.target ??= options.target;
config.node = false;
config.mode =
// When the target is Node avoid any optimizations, such as replacing `process.env.NODE_ENV` with build time value.
config.target === 'node'
? 'none'
: // Otherwise, make sure it matches `process.env.NODE_ENV`.
// When mode is development or production, webpack will automatically
// configure DefinePlugin to replace `process.env.NODE_ENV` with the
// build-time value. Thus, we need to make sure it's the same value to
// avoid conflicts.
//
// When the NODE_ENV is something else (e.g. test), then set it to none
// to prevent extra behavior from webpack.
process.env.NODE_ENV === 'development' ||
process.env.NODE_ENV === 'production'
? (process.env.NODE_ENV as 'development' | 'production')
: 'none';
// When target is Node, the Webpack mode will be set to 'none' which disables in memory caching and causes a full rebuild on every change.
// So to mitigate this we enable in memory caching when target is Node and in watch mode.
config.cache =
options.target === 'node' && options.watch ? { type: 'memory' } : undefined;
config.devtool =
options.sourceMap === 'hidden'
? 'hidden-source-map'
: options.sourceMap
? 'source-map'
: false;
config.output = {
...config.output,
path:
config.output?.path ??
(options.outputPath
? path.join(options.root, options.outputPath)
: undefined),
filename:
config.output?.filename ?? options.outputHashing
? `[name]${hashFormat.script}.js`
: '[name].js',
chunkFilename:
config.output?.chunkFilename ?? options.outputHashing
? `[name]${hashFormat.chunk}.js`
: '[name].js',
hashFunction: config.output?.hashFunction ?? 'xxhash64',
// Disabled for performance
pathinfo: config.output?.pathinfo ?? false,
// Use CJS for Node since it has the widest support.
scriptType:
config.output?.scriptType ?? options.target === 'node'
? undefined
: 'module',
};
config.watch = options.watch;
config.watchOptions = {
poll: options.poll,
};
config.profile = options.statsJson;
config.resolve = {
...config.resolve,
extensions: [...extensions, ...(config?.resolve?.extensions ?? [])],
alias: options.fileReplacements.reduce(
(aliases, replacement) => ({
...aliases,
[replacement.replace]: replacement.with,
}),
{}
),
alias:
options.fileReplacements &&
options.fileReplacements.reduce(
(aliases, replacement) => ({
...aliases,
[replacement.replace]: replacement.with,
}),
{}
),
mainFields,
};
config.externals = externals;
config.optimization = {
...config.optimization,
sideEffects: true,
minimize:
typeof options.optimization === 'object'
? !!options.optimization.scripts
: !!options.optimization,
minimizer: [
options.compiler !== 'swc'
? new TerserPlugin({
parallel: true,
terserOptions: {
keep_classnames: true,
ecma: getTerserEcmaVersion(
path.join(options.root, options.projectRoot)
),
safari10: true,
format: {
ascii_only: true,
comments: false,
webkit: true,
},
},
extractComments: false,
})
: new TerserPlugin({
minify: TerserPlugin.swcMinify,
// `terserOptions` options will be passed to `swc`
terserOptions: {
module: true,
mangle: false,
},
}),
],
runtimeChunk: false,
concatenateModules: true,
};
config.performance = {
...config.performance,
hints: false,
};
config.experiments = { ...config.experiments, cacheUnaffected: true };
config.ignoreWarnings = [
(x) =>
IGNORED_WEBPACK_WARNINGS.some((r) =>
typeof x === 'string' ? r.test(x) : r.test(x.message)
),
];
config.module = {
...config.module,
// Enabled for performance
@ -327,27 +378,6 @@ export function applyBaseConfig(
].filter((r) => !!r),
};
config.stats = {
hash: true,
timings: false,
cached: false,
cachedAssets: false,
modules: false,
warnings: true,
errors: true,
colors: !options.verbose && !options.statsJson,
chunks: !options.verbose,
assets: !!options.verbose,
chunkOrigins: !!options.verbose,
chunkModules: !!options.verbose,
children: !!options.verbose,
reasons: !!options.verbose,
version: !!options.verbose,
errorDetails: !!options.verbose,
moduleTrace: !!options.verbose,
usedExports: !!options.verbose,
};
config.plugins ??= [];
config.plugins.push(...plugins);
}

View File

@ -35,6 +35,15 @@ export function applyWebConfig(
useNormalizedEntry?: boolean;
} = {}
): void {
if (!process.env['NX_TASK_TARGET_PROJECT']) return;
// Defaults that was applied from executor schema previously.
options.runtimeChunk ??= true; // need this for HMR and other things to work
options.extractCss ??= true;
options.generateIndexHtml ??= true;
options.styles ??= [];
options.scripts ??= [];
const plugins: WebpackPluginInstance[] = [];
const stylesOptimization =

View File

@ -1,4 +1,4 @@
import { basename, dirname, relative, resolve } from 'path';
import { basename, dirname, join, relative, resolve } from 'path';
import { statSync } from 'fs';
import {
normalizePath,
@ -51,12 +51,15 @@ export function normalizeOptions(
Object.assign(combinedOptions, originalTargetOptions, options);
}
normalizeRelativePaths(projectNode.data.root, options);
const sourceRoot = projectNode.data.sourceRoot ?? projectNode.data.root;
if (!options.main)
if (!options.main) {
throw new Error(
`Missing "main" option for the entry file. Set this option in your Nx webpack plugin.`
);
}
return {
...options,
@ -153,3 +156,29 @@ export function normalizeFileReplacements(
}))
: [];
}
function normalizeRelativePaths(
projectRoot: string,
options: NxWebpackPluginOptions
): void {
for (const [fieldName, fieldValue] of Object.entries(options)) {
if (isRelativePath(fieldValue)) {
options[fieldName] = join(projectRoot, fieldValue);
} else if (Array.isArray(fieldValue)) {
for (let i = 0; i < fieldValue.length; i++) {
if (isRelativePath(fieldValue[i])) {
fieldValue[i] = join(projectRoot, fieldValue[i]);
}
}
}
}
}
function isRelativePath(val: unknown): boolean {
return (
typeof val === 'string' &&
(val.startsWith('./') ||
// Windows
val.startsWith('.\\'))
);
}

View File

@ -38,51 +38,177 @@ export interface OptimizationOptions {
}
export interface NxWebpackPluginOptions {
// Required options
main: string;
outputPath: string;
tsConfig: string;
// Optional options
/**
* The tsconfig file for the project. e.g. `tsconfig.json`
*/
tsConfig?: string;
/**
* The entry point for the bundle. e.g. `src/main.ts`
*/
main?: string;
/**
* Secondary entry points for the bundle.
*/
additionalEntryPoints?: AdditionalEntryPoint[];
/**
* Assets to be copied over to the output path.
*/
assets?: Array<AssetGlob | string>;
/**
* Babel configuration file if compiler is babel.
*/
babelConfig?: string;
/**
* If true, Babel will look for a babel.config.json up the directory tree.
*/
babelUpwardRootMode?: boolean;
/**
* Set <base href> for the resulting index.html.
*/
baseHref?: string;
commonChunk?: boolean;
/**
* The compiler to use. Default is `babel` and requires a `.babelrc` file.
*/
compiler?: 'babel' | 'swc' | 'tsc';
/**
* Set `crossorigin` attribute on the `script` and `link` tags.
*/
crossOrigin?: 'none' | 'anonymous' | 'use-credentials';
/**
* Delete the output path before building.
*/
deleteOutputPath?: boolean;
/**
* The deploy path for the application. e.g. `/my-app/`
*/
deployUrl?: string;
/**
* Define external packages that will not be bundled.
* Use `all` to exclude all 3rd party packages, and `none` to bundle all packages.
* Use an array to exclude specific packages from the bundle.
* Default is `none`.
*/
externalDependencies?: 'all' | 'none' | string[];
/**
* Extract CSS as an external file. Default is `true`.
*/
extractCss?: boolean;
/**
* Extract licenses from 3rd party modules and add them to the output.
*/
extractLicenses?: boolean;
/**
* Replace files at build time. e.g. `[{ "replace": "src/a.dev.ts", "with": "src/a.prod.ts" }]`
*/
fileReplacements?: FileReplacement[];
/**
* Generate an `index.html` file if `index.html` is passed. Default is `true`
*/
generateIndexHtml?: boolean;
/**
* Generate a `package.json` file for the bundle. Useful for Node applications.
*/
generatePackageJson?: boolean;
/**
* Path to the `index.html`.
*/
index?: string;
/**
* Set the memory limit for the type-checking process. Default is `2048`.
*/
memoryLimit?: number;
/**
* Use the source file name in output chunks. Useful for development or for Node.
*/
namedChunks?: boolean;
/**
* Optimize the bundle using Terser.
*/
optimization?: boolean | OptimizationOptions;
/**
* Specify the output filename for the bundle. Useful for Node applications that use `@nx/js:node` to serve.
*/
outputFileName?: string;
/**
* Use file hashes in the output filenames. Recommended for production web applications.
*/
outputHashing?: any;
/**
* Override `output.path` in webpack configuration. This setting is not recommended and exists for backwards compatibility.
*/
outputPath?: string;
/**
* Override `watchOptions.poll` in webpack configuration. This setting is not recommended and exists for backwards compatibility.
*/
poll?: number;
/**
* The polyfill file to use. Useful for supporting legacy browsers. e.g. `src/polyfills.ts`
*/
polyfills?: string;
/**
* Manually set the PostCSS configuration file. By default, PostCSS will look for `postcss.config.js` in the directory.
*/
postcssConfig?: string;
/**
* Display build progress in the terminal.
*/
progress?: boolean;
/**
* Add an additional chunk for the Webpack runtime. Defaults to `true` when `target === 'web'`.
*/
runtimeChunk?: boolean;
/**
* External scripts that will be included before the main application entry.
*/
scripts?: Array<ExtraEntryPointClass | string>;
/**
* Skip type checking. Default is `false`.
*/
skipTypeChecking?: boolean;
/**
* Generate source maps.
*/
sourceMap?: boolean | 'hidden';
/**
* When `true`, `process.env.NODE_ENV` will be excluded from the bundle. Useful for building a web application to run in a Node environment.
*/
ssr?: boolean;
/**
* Generate a `stats.json` file which can be analyzed using tools such as `webpack-bundle-analyzer`.
*/
statsJson?: boolean;
/**
* Options for the style preprocessor. e.g. `{ "includePaths": [] }` for SASS.
*/
stylePreprocessorOptions?: any;
/**
* External stylesheets that will be included with the application.
*/
styles?: Array<ExtraEntryPointClass | string>;
/**
* Enables the use of subresource integrity validation.
*/
subresourceIntegrity?: boolean;
/**
* Override the `target` option in webpack configuration. This setting is not recommended and exists for backwards compatibility.
*/
target?: string | string[];
/**
* List of TypeScript Compiler Transformers Plugins.
*/
transformers?: TransformerEntry[];
/**
* Generate a separate vendor chunk for 3rd party packages.
*/
vendorChunk?: boolean;
/**
* Log additional information for debugging purposes.
*/
verbose?: boolean;
/**
* Watch for file changes.
*/
watch?: boolean;
}

View File

@ -21,24 +21,21 @@ import { applyWebConfig } from './lib/apply-web-config';
export class NxWebpackPlugin {
private readonly options: NormalizedNxWebpackPluginOptions;
constructor(options: NxWebpackPluginOptions) {
this.options = normalizeOptions({
...options,
...this.readExecutorOptions(),
});
constructor(options: NxWebpackPluginOptions = {}) {
// If we're not in an Nx task, we're building inferred targets, so skip normalizing build options.
if (process.env['NX_TASK_TARGET_PROJECT']) {
this.options = normalizeOptions(options);
}
}
apply(compiler: Compiler): void {
const target = this.options.target ?? compiler.options.target;
// Defaults to 'web' if not specified to match Webpack's default.
const target = this.options.target ?? compiler.options.target ?? 'web';
this.options.outputPath ??= compiler.options.output?.path;
if (typeof target === 'string') {
this.options.target = target;
}
if (this.options.deleteOutputPath) {
deleteOutputDir(this.options.root, this.options.outputPath);
}
applyBaseConfig(this.options, compiler.options, {
useNormalizedEntry: true,
});
@ -52,14 +49,9 @@ export class NxWebpackPlugin {
useNormalizedEntry: true,
});
}
}
private readExecutorOptions() {
const fromExecutor = process.env['NX_WEBPACK_EXECUTOR_RAW_OPTIONS'] ?? '{}';
try {
return JSON.parse(fromExecutor);
} catch {
return {};
if (this.options.deleteOutputPath) {
deleteOutputDir(this.options.root, this.options.outputPath);
}
}
}

View File

@ -0,0 +1,197 @@
import {
CreateDependencies,
CreateNodes,
CreateNodesContext,
detectPackageManager,
readJsonFile,
TargetConfiguration,
workspaceRoot,
writeJsonFile,
} from '@nx/devkit';
import { basename, dirname, isAbsolute, join, relative } from 'path';
import { getNamedInputs } from '@nx/devkit/src/utils/get-named-inputs';
import { readTargetDefaultsForTarget } from 'nx/src/project-graph/utils/project-configuration-utils';
import { WebpackExecutorOptions } from '../executors/webpack/schema';
import { WebDevServerOptions } from '../executors/dev-server/schema';
import { existsSync, readdirSync } from 'fs';
import { readWebpackOptions } from '../utils/webpack/read-webpack-options';
import { resolveUserDefinedWebpackConfig } from '../utils/webpack/resolve-user-defined-webpack-config';
import { getLockFileName, getRootTsConfigPath } from '@nx/js';
import { projectGraphCacheDirectory } from 'nx/src/utils/cache-directory';
import { calculateHashForCreateNodes } from '@nx/devkit/src/utils/calculate-hash-for-create-nodes';
export interface WebpackPluginOptions {
buildTargetName?: string;
serveTargetName?: string;
staticServeTargetName?: string;
previewTargetName?: string;
}
const cachePath = join(projectGraphCacheDirectory, 'webpack.hash');
const targetsCache = existsSync(cachePath) ? readTargetsCache() : {};
const calculatedTargets: Record<
string,
Record<string, TargetConfiguration>
> = {};
function readTargetsCache(): Record<
string,
Record<string, TargetConfiguration>
> {
return readJsonFile(cachePath);
}
function writeTargetsToCache(
targets: Record<string, Record<string, TargetConfiguration>>
) {
writeJsonFile(cachePath, targets);
}
export const createDependencies: CreateDependencies = () => {
writeTargetsToCache(calculatedTargets);
return [];
};
export const createNodes: CreateNodes<WebpackPluginOptions> = [
'**/webpack.config.{js,ts,mjs,mts,cjs,cts}',
async (configFilePath, options, context) => {
options ??= {};
options.buildTargetName ??= 'build';
options.serveTargetName ??= 'serve';
options.staticServeTargetName ??= 'static-serve';
options.previewTargetName ??= 'preview';
const projectRoot = dirname(configFilePath);
// Do not create a project if package.json and project.json isn't there.
const siblingFiles = readdirSync(join(context.workspaceRoot, projectRoot));
if (
!siblingFiles.includes('package.json') &&
!siblingFiles.includes('project.json')
) {
return {};
}
const hash = calculateHashForCreateNodes(projectRoot, options, context, [
getLockFileName(detectPackageManager(context.workspaceRoot)),
]);
const targets = targetsCache[hash]
? targetsCache[hash]
: await createWebpackTargets(
configFilePath,
projectRoot,
options,
context
);
return {
projects: {
[projectRoot]: {
projectType: 'application',
targets,
},
},
};
},
];
async function createWebpackTargets(
configFilePath: string,
projectRoot: string,
options: WebpackPluginOptions,
context: CreateNodesContext
): Promise<
Record<
string,
TargetConfiguration<WebpackExecutorOptions | WebDevServerOptions>
>
> {
const namedInputs = getNamedInputs(projectRoot, context);
const webpackConfig = resolveUserDefinedWebpackConfig(
join(context.workspaceRoot, configFilePath),
getRootTsConfigPath()
);
const webpackOptions = await readWebpackOptions(webpackConfig);
const outputPath =
normalizeOutputPath(webpackOptions.output?.path) ??
'{workspaceRoot}/dist/{projectRoot}';
const targets = {};
const configBasename = basename(configFilePath);
targets[options.buildTargetName] = {
command: `webpack -c ${configBasename} --node-env=production`,
options: {
cwd: projectRoot,
},
};
const buildTargetDefaults = readTargetDefaultsForTarget(
options.buildTargetName,
context.nxJsonConfiguration.targetDefaults
);
if (buildTargetDefaults?.cache === undefined) {
targets[options.buildTargetName].cache = true;
}
if (buildTargetDefaults?.inputs === undefined) {
targets[options.buildTargetName].inputs =
'production' in namedInputs
? [
'default',
'^production',
{
externalDependencies: ['webpack-cli'],
},
]
: [
'default',
'^default',
{
externalDependencies: ['webpack-cli'],
},
];
}
if (buildTargetDefaults?.outputs === undefined) {
targets[options.buildTargetName].outputs = [outputPath];
}
targets[options.serveTargetName] = {
command: `webpack serve -c ${configBasename} --node-env=development`,
options: {
cwd: projectRoot,
},
};
targets[options.previewTargetName] = {
command: `webpack serve -c ${configBasename} --node-env=production`,
options: {
cwd: projectRoot,
},
};
targets[options.staticServeTargetName] = {
executor: '@nx/web:file-server',
options: {
buildTarget: `${projectRoot}:${options.buildTargetName}`,
},
};
return targets;
}
function normalizeOutputPath(
outputPath: string | undefined
): string | undefined {
if (!outputPath) return undefined;
if (isAbsolute(outputPath)) {
return `{workspaceRoot}/${relative(workspaceRoot, outputPath)}`;
} else {
return outputPath;
}
}

View File

@ -2,6 +2,7 @@ import {
composePluginsSync,
composePlugins,
NxWebpackExecutionContext,
isNxWebpackComposablePlugin,
} from './config';
describe('composePlugins', () => {
@ -29,6 +30,7 @@ describe('composePlugins', () => {
};
const combined = composePlugins(a(), b(), c(), d());
expect(isNxWebpackComposablePlugin(combined)).toBeTruthy();
const config = await combined(
{ plugins: [] },
{} as NxWebpackExecutionContext
@ -59,6 +61,7 @@ describe('composePluginsSync', () => {
};
const combined = composePluginsSync(a(), b());
expect(isNxWebpackComposablePlugin(combined)).toBeTruthy();
const config = await combined(
{ plugins: [] },
{} as NxWebpackExecutionContext

View File

@ -1,29 +1,31 @@
import { ExecutorContext } from '@nx/devkit';
import {
ExecutorContext,
readCachedProjectGraph,
workspaceRoot,
} from '@nx/devkit';
import { Configuration } from 'webpack';
import { NormalizedWebpackExecutorOptions } from '../executors/webpack/schema';
import { withNx } from './with-nx';
import { withWeb } from './with-web';
/** @deprecated use withNx and withWeb plugins directly */
export function getBaseWebpackPartial(
options: NormalizedWebpackExecutorOptions,
context?: ExecutorContext
): Configuration {
const config: Configuration = {};
const configure = composePluginsSync(withNx(), withWeb());
return configure(config, { options, context });
export const nxWebpackComposablePlugin = 'nxWebpackComposablePlugin';
export function isNxWebpackComposablePlugin(
a: unknown
): a is AsyncNxComposableWebpackPlugin {
return a?.[nxWebpackComposablePlugin] === true;
}
export interface NxWebpackExecutionContext {
options: NormalizedWebpackExecutorOptions;
context: ExecutorContext;
configuration?: string;
}
export interface NxWebpackPlugin {
export interface NxComposableWebpackPlugin {
(config: Configuration, ctx: NxWebpackExecutionContext): Configuration;
}
export interface AsyncNxWebpackPlugin {
export interface AsyncNxComposableWebpackPlugin {
(config: Configuration, ctx: NxWebpackExecutionContext):
| Configuration
| Promise<Configuration>;
@ -31,31 +33,77 @@ export interface AsyncNxWebpackPlugin {
export function composePlugins(
...plugins: (
| NxWebpackPlugin
| AsyncNxWebpackPlugin
| Promise<NxWebpackPlugin | AsyncNxWebpackPlugin>
| NxComposableWebpackPlugin
| AsyncNxComposableWebpackPlugin
| Promise<NxComposableWebpackPlugin | AsyncNxComposableWebpackPlugin>
)[]
) {
return async function combined(
config: Configuration,
ctx: NxWebpackExecutionContext
): Promise<Configuration> {
for (const plugin of plugins) {
const fn = await plugin;
config = await fn(config, ctx);
return Object.assign(
async function combined(
config: Configuration,
ctx: NxWebpackExecutionContext
): Promise<Configuration> {
// Webpack may be calling us as a standard config function.
// Build up Nx context from environment variables.
// This is to enable `@nx/webpack/plugin` to work with existing projects.
if (ctx['env']) {
ensureNxWebpackExecutionContext(ctx);
// Build this from scratch since what webpack passes us is the env, not config,
// and `withNX()` creates a new config object anyway.
config = {};
}
for (const plugin of plugins) {
const fn = await plugin;
config = await fn(config, ctx);
}
return config;
},
{
[nxWebpackComposablePlugin]: true,
}
return config;
};
);
}
export function composePluginsSync(...plugins: NxWebpackPlugin[]) {
return function combined(
config: Configuration,
ctx: NxWebpackExecutionContext
): Configuration {
for (const plugin of plugins) {
config = plugin(config, ctx);
export function composePluginsSync(...plugins: NxComposableWebpackPlugin[]) {
return Object.assign(
function combined(
config: Configuration,
ctx: NxWebpackExecutionContext
): Configuration {
for (const plugin of plugins) {
config = plugin(config, ctx);
}
return config;
},
{
[nxWebpackComposablePlugin]: true,
}
return config;
);
}
function ensureNxWebpackExecutionContext(ctx: NxWebpackExecutionContext): void {
const projectName = process.env.NX_TASK_TARGET_PROJECT;
const targetName = process.env.NX_TASK_TARGET_TARGET;
const configurationName = process.env.NX_TASK_TARGET_CONFIGURATION;
const projectGraph = readCachedProjectGraph();
const projectNode = projectGraph.nodes[projectName];
ctx.options ??= {
root: workspaceRoot,
projectRoot: projectNode.data.root,
sourceRoot: projectNode.data.sourceRoot ?? projectNode.data.root,
// These aren't actually needed since NxWebpackPlugin and withNx both support them being undefined.
assets: undefined,
outputPath: undefined,
tsConfig: undefined,
outputFileName: undefined,
};
ctx.context ??= {
projectName,
targetName,
configurationName,
cwd: process.cwd(),
root: workspaceRoot,
isVerbose: process.env['NX_VERBOSE_LOGGING'] === 'true',
};
}

View File

@ -0,0 +1,10 @@
import { readNxJson, Tree } from '@nx/devkit';
export function hasPlugin(tree: Tree) {
const nxJson = readNxJson(tree);
return !!nxJson.plugins?.some((p) =>
typeof p === 'string'
? p === '@nx/webpack/plugin'
: p.plugin === '@nx/webpack/plugin'
);
}

View File

@ -3,6 +3,8 @@ export const nxVersion = require('../../package.json').version;
export const swcLoaderVersion = '0.1.15';
export const tsLibVersion = '^2.3.0';
export const webpackCliVersion = '^5.1.4';
// React apps
export const reactRefreshWebpackPluginVersion = '^0.5.7';
export const svgrWebpackVersion = '^8.0.1';

View File

@ -0,0 +1,45 @@
import { workspaceRoot } from '@nx/devkit';
import { isNxWebpackComposablePlugin } from '../config';
import { Configuration } from 'webpack';
/**
* Reads the webpack options from a give webpack configuration. The configuration can be:
* 1. A standard config object
* 2. A standard function that returns a config object (webpack.js.org/configuration/configuration-types/#exporting-a-function)
* 3. A Nx-specific composable function that takes Nx context, webpack config, and returns the config object.
*
* @param webpackConfig
*/
export async function readWebpackOptions(
webpackConfig: unknown
): Promise<Configuration> {
let config: Configuration;
if (isNxWebpackComposablePlugin(webpackConfig)) {
config = await webpackConfig(
{},
{
// These values are only used during build-time, so passing stubs here just to read out
// the returned config object.
options: {
root: workspaceRoot,
projectRoot: '',
sourceRoot: '',
outputFileName: undefined,
outputPath: undefined,
assets: undefined,
},
context: { root: workspaceRoot, cwd: undefined, isVerbose: false },
}
);
} else if (typeof webpackConfig === 'function') {
config = await webpackConfig(
{
production: true, // we want the production build options
},
{}
);
} else {
config = webpackConfig;
}
return config;
}

View File

@ -1,6 +1,9 @@
import { registerTsProject } from '@nx/js/src/internal';
export function resolveCustomWebpackConfig(path: string, tsConfig: string) {
export function resolveUserDefinedWebpackConfig(
path: string,
tsConfig: string
) {
// Don't transpile non-TS files. This prevents workspaces libs from being registered via tsconfig-paths.
// There's an issue here with Nx workspace where loading plugins from source (via tsconfig-paths) can lead to errors.
if (!/\.(ts|mts|cts)$/.test(path)) {
@ -27,10 +30,3 @@ export function resolveCustomWebpackConfig(path: string, tsConfig: string) {
return customWebpackConfig;
}
export function isRegistered() {
return (
require.extensions['.ts'] != undefined ||
require.extensions['.tsx'] != undefined
);
}

View File

@ -1,18 +1,20 @@
import { Configuration } from 'webpack';
import { NxWebpackExecutionContext, NxWebpackPlugin } from './config';
import { NxComposableWebpackPlugin, NxWebpackExecutionContext } from './config';
import { applyBaseConfig } from '../plugins/nx-webpack-plugin/lib/apply-base-config';
import { NxWebpackPluginOptions } from '../plugins/nx-webpack-plugin/nx-webpack-plugin-options';
import { normalizeAssets } from '../plugins/nx-webpack-plugin/lib/normalize-options';
const processed = new Set();
export interface WithNxOptions {
skipTypeChecking?: boolean;
}
export type WithNxOptions = Partial<NxWebpackPluginOptions>;
/**
* @param {WithNxOptions} pluginOptions
* @returns {NxWebpackPlugin}
*/
export function withNx(pluginOptions?: WithNxOptions): NxWebpackPlugin {
export function withNx(
pluginOptions: WithNxOptions = {}
): NxComposableWebpackPlugin {
return function configure(
config: Configuration,
{ options, context }: NxWebpackExecutionContext
@ -23,6 +25,15 @@ export function withNx(pluginOptions?: WithNxOptions): NxWebpackPlugin {
{
...options,
...pluginOptions,
assets: options.assets
? options.assets
: pluginOptions.assets
? normalizeAssets(
pluginOptions.assets,
options.root,
options.sourceRoot
)
: [],
root: context.root,
projectName: context.projectName,
targetName: context.targetName,

View File

@ -1,6 +1,6 @@
import { Configuration } from 'webpack';
import { NxWebpackExecutionContext, NxWebpackPlugin } from './config';
import { NxComposableWebpackPlugin, NxWebpackExecutionContext } from './config';
import {
ExtraEntryPointClass,
NormalizedWebpackExecutorOptions,
@ -35,7 +35,9 @@ export type MergedOptions = Omit<
* @param {WithWebOptions} pluginOptions
* @returns {NxWebpackPlugin}
*/
export function withWeb(pluginOptions: WithWebOptions = {}): NxWebpackPlugin {
export function withWeb(
pluginOptions: WithWebOptions = {}
): NxComposableWebpackPlugin {
return function configure(
config: Configuration,
{ options, context }: NxWebpackExecutionContext