feat(webpack): add plugin to automatically configure build and serve targets (#20243)
This commit is contained in:
parent
54eab7f8b1
commit
507fe42e4f
@ -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": [
|
||||
|
||||
34
e2e/webpack/src/webpack.pcv3.test.ts
Normal file
34
e2e/webpack/src/webpack.pcv3.test.ts
Normal 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);
|
||||
});
|
||||
@ -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/);
|
||||
|
||||
@ -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(
|
||||
{},
|
||||
|
||||
@ -43,7 +43,7 @@
|
||||
"error",
|
||||
{
|
||||
"buildTargets": ["build-base"],
|
||||
"ignoredDependencies": ["nx", "typescript"]
|
||||
"ignoredDependencies": ["nx", "typescript", "@nx/webpack"]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@ -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'`);
|
||||
});
|
||||
});
|
||||
@ -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'],
|
||||
},
|
||||
|
||||
@ -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);
|
||||
|
||||
|
||||
@ -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;
|
||||
}
|
||||
);
|
||||
<% } %>
|
||||
|
||||
10
packages/node/src/utils/has-webpack-plugin.ts
Normal file
10
packages/node/src/utils/has-webpack-plugin.ts
Normal 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'
|
||||
);
|
||||
}
|
||||
@ -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,
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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, {
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
|
||||
@ -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']`
|
||||
);
|
||||
});
|
||||
});
|
||||
@ -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',
|
||||
|
||||
@ -1,3 +0,0 @@
|
||||
export const environment = {
|
||||
production: true
|
||||
};
|
||||
@ -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
|
||||
};
|
||||
@ -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;
|
||||
}
|
||||
);
|
||||
<% } %>
|
||||
|
||||
@ -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')
|
||||
|
||||
@ -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, {
|
||||
|
||||
@ -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}`],
|
||||
};
|
||||
}
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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: {
|
||||
|
||||
@ -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);
|
||||
|
||||
|
||||
10
packages/react/src/utils/has-webpack-plugin.ts
Normal file
10
packages/react/src/utils/has-webpack-plugin.ts
Normal 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'
|
||||
);
|
||||
}
|
||||
@ -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;
|
||||
|
||||
@ -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']`
|
||||
);
|
||||
});
|
||||
});
|
||||
@ -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',
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -1,3 +0,0 @@
|
||||
export const environment = {
|
||||
production: true
|
||||
};
|
||||
@ -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
|
||||
};
|
||||
@ -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;
|
||||
}
|
||||
);
|
||||
<% } %>
|
||||
|
||||
10
packages/web/src/utils/has-webpack-plugin.ts
Normal file
10
packages/web/src/utils/has-webpack-plugin.ts
Normal 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'
|
||||
);
|
||||
}
|
||||
1
packages/webpack/plugin.ts
Normal file
1
packages/webpack/plugin.ts
Normal file
@ -0,0 +1 @@
|
||||
export { createNodes } from './src/plugins/plugin';
|
||||
@ -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],
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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 });
|
||||
}
|
||||
@ -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) {
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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": [
|
||||
|
||||
@ -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(
|
||||
|
||||
@ -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'`
|
||||
);
|
||||
});
|
||||
});
|
||||
@ -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: {
|
||||
|
||||
38
packages/webpack/src/generators/init/init.pcv3.spec.ts
Normal file
38
packages/webpack/src/generators/init/init.pcv3.spec.ts
Normal 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),
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
@ -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';
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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);
|
||||
}
|
||||
|
||||
@ -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 =
|
||||
|
||||
@ -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('.\\'))
|
||||
);
|
||||
}
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
197
packages/webpack/src/plugins/plugin.ts
Normal file
197
packages/webpack/src/plugins/plugin.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
@ -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
|
||||
|
||||
@ -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',
|
||||
};
|
||||
}
|
||||
|
||||
10
packages/webpack/src/utils/has-plugin.ts
Normal file
10
packages/webpack/src/utils/has-plugin.ts
Normal 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'
|
||||
);
|
||||
}
|
||||
@ -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';
|
||||
|
||||
45
packages/webpack/src/utils/webpack/read-webpack-options.ts
Normal file
45
packages/webpack/src/utils/webpack/read-webpack-options.ts
Normal 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;
|
||||
}
|
||||
@ -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
|
||||
);
|
||||
}
|
||||
@ -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,
|
||||
|
||||
@ -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
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user