feat(webpack): add convertConfigToWebpackPlugin (#26516)
This PR introduces functionality for users who currently use the `withNx` and `withReact` plugins in their webpack configuration to migrate to the `NxAppWebpackPlugin`. The `nxUseLegacyPlugin` wraps the legacy style function so that it continues to work with the standardized generated webpack config. By implementing this change, the aim is to provide a consistent method for users opting to transition to inferred targets. This ensures a smoother migration process, offering better integration and reducing potential configuration complexities.
This commit is contained in:
parent
1d1c699c81
commit
b1dbf47aa2
@ -9867,6 +9867,14 @@
|
||||
"children": [],
|
||||
"isExternal": false,
|
||||
"disableCollapsible": false
|
||||
},
|
||||
{
|
||||
"id": "convert-config-to-webpack-plugin",
|
||||
"path": "/nx-api/webpack/generators/convert-config-to-webpack-plugin",
|
||||
"name": "convert-config-to-webpack-plugin",
|
||||
"children": [],
|
||||
"isExternal": false,
|
||||
"disableCollapsible": false
|
||||
}
|
||||
],
|
||||
"isExternal": false,
|
||||
|
||||
@ -3215,6 +3215,15 @@
|
||||
"originalFilePath": "/packages/webpack/src/generators/configuration/schema.json",
|
||||
"path": "/nx-api/webpack/generators/configuration",
|
||||
"type": "generator"
|
||||
},
|
||||
"/nx-api/webpack/generators/convert-config-to-webpack-plugin": {
|
||||
"description": "Convert the project to use the `NxAppWebpackPlugin` and `NxReactWebpackPlugin`.",
|
||||
"file": "generated/packages/webpack/generators/convert-config-to-webpack-plugin.json",
|
||||
"hidden": false,
|
||||
"name": "convert-config-to-webpack-plugin",
|
||||
"originalFilePath": "/packages/webpack/src/generators/convert-config-to-webpack-plugin/schema.json",
|
||||
"path": "/nx-api/webpack/generators/convert-config-to-webpack-plugin",
|
||||
"type": "generator"
|
||||
}
|
||||
},
|
||||
"path": "/nx-api/webpack"
|
||||
|
||||
@ -3180,6 +3180,15 @@
|
||||
"originalFilePath": "/packages/webpack/src/generators/configuration/schema.json",
|
||||
"path": "webpack/generators/configuration",
|
||||
"type": "generator"
|
||||
},
|
||||
{
|
||||
"description": "Convert the project to use the `NxAppWebpackPlugin` and `NxReactWebpackPlugin`.",
|
||||
"file": "generated/packages/webpack/generators/convert-config-to-webpack-plugin.json",
|
||||
"hidden": false,
|
||||
"name": "convert-config-to-webpack-plugin",
|
||||
"originalFilePath": "/packages/webpack/src/generators/convert-config-to-webpack-plugin/schema.json",
|
||||
"path": "webpack/generators/convert-config-to-webpack-plugin",
|
||||
"type": "generator"
|
||||
}
|
||||
],
|
||||
"githubRoot": "https://github.com/nrwl/nx/blob/master",
|
||||
|
||||
@ -0,0 +1,30 @@
|
||||
{
|
||||
"name": "convert-config-to-webpack-plugin",
|
||||
"factory": "./src/generators/convert-config-to-webpack-plugin/convert-config-to-webpack-plugin",
|
||||
"schema": {
|
||||
"$schema": "https://json-schema.org/schema",
|
||||
"$id": "NxWebpackConvertConfigToWebpackPlugin",
|
||||
"description": "Convert existing Webpack project(s) using `@nx/webpack:webpack` executor that uses `withNx` to use `NxAppWebpackPlugin`. Defaults to migrating all projects. Pass '--project' to migrate only one target.",
|
||||
"title": "Convert Webpack project using withNx to NxAppWebpackPlugin",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"project": {
|
||||
"type": "string",
|
||||
"description": "The project to convert from using the `@nx/webpack:webpack` executor and `withNx` plugin to use `NxAppWebpackPlugin`.",
|
||||
"x-priority": "important"
|
||||
},
|
||||
"skipFormat": {
|
||||
"type": "boolean",
|
||||
"description": "Whether to format files at the end of the migration.",
|
||||
"default": false
|
||||
}
|
||||
},
|
||||
"presets": []
|
||||
},
|
||||
"description": "Convert the project to use the `NxAppWebpackPlugin` and `NxReactWebpackPlugin`.",
|
||||
"implementation": "/packages/webpack/src/generators/convert-config-to-webpack-plugin/convert-config-to-webpack-plugin.ts",
|
||||
"aliases": [],
|
||||
"hidden": false,
|
||||
"path": "/packages/webpack/src/generators/convert-config-to-webpack-plugin/schema.json",
|
||||
"type": "generator"
|
||||
}
|
||||
@ -709,6 +709,7 @@
|
||||
- [generators](/nx-api/webpack/generators)
|
||||
- [init](/nx-api/webpack/generators/init)
|
||||
- [configuration](/nx-api/webpack/generators/configuration)
|
||||
- [convert-config-to-webpack-plugin](/nx-api/webpack/generators/convert-config-to-webpack-plugin)
|
||||
- [workspace](/nx-api/workspace)
|
||||
- [documents](/nx-api/workspace/documents)
|
||||
- [Overview](/nx-api/workspace/documents/overview)
|
||||
|
||||
108
e2e/webpack/src/__snapshots__/webpack.legacy.test.ts.snap
Normal file
108
e2e/webpack/src/__snapshots__/webpack.legacy.test.ts.snap
Normal file
@ -0,0 +1,108 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`Webpack Plugin (legacy) ConvertConfigToWebpackPlugin, should convert withNx webpack config to a standard config using NxWebpackPlugin 1`] = `
|
||||
"const { NxAppWebpackPlugin } = require('@nx/webpack/app-plugin');
|
||||
const { NxReactWebpackPlugin } = require('@nx/react/webpack-plugin');
|
||||
const { useLegacyNxPlugin } = require('@nx/webpack');
|
||||
|
||||
// This file was migrated using @nx/webpack:convert-config-to-webpack-plugin from your './webpack.config.old.js'
|
||||
// Please check that the options here are correct as they were moved from the old webpack.config.js to this file.
|
||||
const options = {};
|
||||
|
||||
/**
|
||||
* @type{import('webpack').WebpackOptionsNormalized}
|
||||
*/
|
||||
module.exports = async () => ({
|
||||
plugins: [
|
||||
new NxAppWebpackPlugin(),
|
||||
new NxReactWebpackPlugin({
|
||||
// Uncomment this line if you don't want to use SVGR
|
||||
// See: https://react-svgr.com/
|
||||
// svgr: false
|
||||
}),
|
||||
// eslint-disable-next-line react-hooks/rules-of-hooks
|
||||
await useLegacyNxPlugin(require('./webpack.config.old'), options),
|
||||
],
|
||||
});
|
||||
"
|
||||
`;
|
||||
|
||||
exports[`Webpack Plugin (legacy) ConvertConfigToWebpackPlugin, should convert withNx webpack config to a standard config using NxWebpackPlugin 2`] = `
|
||||
"{
|
||||
"name": "app3224373",
|
||||
"$schema": "../node_modules/nx/schemas/project-schema.json",
|
||||
"projectType": "application",
|
||||
"sourceRoot": "app3224373/src",
|
||||
"tags": [],
|
||||
"targets": {
|
||||
"build": {
|
||||
"executor": "@nx/webpack:webpack",
|
||||
"outputs": ["{options.outputPath}"],
|
||||
"defaultConfiguration": "production",
|
||||
"options": {
|
||||
"target": "web",
|
||||
"outputPath": "dist/app3224373",
|
||||
"compiler": "swc",
|
||||
"main": "app3224373/src/main.ts",
|
||||
"tsConfig": "app3224373/tsconfig.app.json",
|
||||
"webpackConfig": "app3224373/webpack.config.js",
|
||||
"assets": ["app3224373/src/favicon.ico", "app3224373/src/assets"],
|
||||
"index": "app3224373/src/index.html",
|
||||
"baseHref": "/",
|
||||
"styles": ["app3224373/src/styles.css"],
|
||||
"scripts": [],
|
||||
"standardWebpackConfigFunction": true
|
||||
},
|
||||
"configurations": {
|
||||
"production": {
|
||||
"optimization": true,
|
||||
"outputHashing": "all",
|
||||
"sourceMap": false,
|
||||
"namedChunks": false,
|
||||
"extractLicenses": true,
|
||||
"vendorChunk": false,
|
||||
"fileReplacements": [
|
||||
{
|
||||
"replace": "app3224373/src/environments/environment.ts",
|
||||
"with": "app3224373/src/environments/environment.prod.ts"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"serve": {
|
||||
"executor": "@nx/webpack:dev-server",
|
||||
"options": {
|
||||
"buildTarget": "app3224373:build"
|
||||
},
|
||||
"configurations": {
|
||||
"production": {
|
||||
"buildTarget": "app3224373:build:production"
|
||||
}
|
||||
}
|
||||
},
|
||||
"lint": {
|
||||
"executor": "@nx/eslint:lint"
|
||||
},
|
||||
"test": {
|
||||
"executor": "@nx/jest:jest",
|
||||
"outputs": ["{workspaceRoot}/coverage/{projectRoot}"],
|
||||
"options": {
|
||||
"jestConfig": "app3224373/jest.config.ts"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
"
|
||||
`;
|
||||
|
||||
exports[`Webpack Plugin (legacy) ConvertConfigToWebpackPlugin, should convert withNx webpack config to a standard config using NxWebpackPlugin 3`] = `
|
||||
"const { composePlugins } = require('@nx/webpack');
|
||||
// Nx plugins for webpack.
|
||||
module.exports = composePlugins((config) => {
|
||||
// Update the webpack config as needed here.
|
||||
// e.g. \`config.plugins.push(new MyPlugin())\`
|
||||
return config;
|
||||
});
|
||||
"
|
||||
`;
|
||||
@ -3,6 +3,7 @@ import {
|
||||
cleanupProject,
|
||||
killProcessAndPorts,
|
||||
newProject,
|
||||
readFile,
|
||||
runCLI,
|
||||
runCommandUntil,
|
||||
runE2ETests,
|
||||
@ -118,21 +119,21 @@ describe('Webpack Plugin (legacy)', () => {
|
||||
updateFile(
|
||||
`${appName}/webpack.config.js`,
|
||||
`
|
||||
const { join } = require('path');
|
||||
const {NxWebpackPlugin} = require('@nx/webpack');
|
||||
module.exports = {
|
||||
output: {
|
||||
path: join(__dirname, '../dist/app9524918'),
|
||||
},
|
||||
plugins: [
|
||||
new NxAppWebpackPlugin({
|
||||
main: './src/main.ts',
|
||||
compiler: 'tsc',
|
||||
index: './src/index.html',
|
||||
tsConfig: './tsconfig.app.json',
|
||||
})
|
||||
]
|
||||
};
|
||||
const { join } = require('path');
|
||||
const {NxWebpackPlugin} = require('@nx/webpack');
|
||||
module.exports = {
|
||||
output: {
|
||||
path: join(__dirname, '../dist/app9524918'),
|
||||
},
|
||||
plugins: [
|
||||
new NxAppWebpackPlugin({
|
||||
main: './src/main.ts',
|
||||
compiler: 'tsc',
|
||||
index: './src/index.html',
|
||||
tsConfig: './tsconfig.app.json',
|
||||
})
|
||||
]
|
||||
};
|
||||
`
|
||||
);
|
||||
|
||||
@ -146,4 +147,45 @@ module.exports = {
|
||||
}).not.toThrow();
|
||||
}
|
||||
});
|
||||
|
||||
describe('ConvertConfigToWebpackPlugin,', () => {
|
||||
it('should convert withNx webpack config to a standard config using NxWebpackPlugin', () => {
|
||||
const appName = 'app3224373'; // Needs to be reserved so that the snapshot projectName matches
|
||||
runCLI(
|
||||
`generate @nx/web:app ${appName} --bundler webpack --e2eTestRunner=playwright --projectNameAndRootFormat=as-provided`
|
||||
);
|
||||
updateFile(
|
||||
`${appName}/src/main.ts`,
|
||||
`
|
||||
const root = document.querySelector('proj-root');
|
||||
if(root) {
|
||||
root.innerHTML = '<h1>Welcome</h1>'
|
||||
}
|
||||
`
|
||||
);
|
||||
|
||||
runCLI(
|
||||
`generate @nx/webpack:convert-config-to-webpack-plugin --project ${appName}`
|
||||
);
|
||||
|
||||
const webpackConfig = readFile(`${appName}/webpack.config.js`);
|
||||
const oldWebpackConfig = readFile(`${appName}/webpack.config.old.js`);
|
||||
const projectJSON = readFile(`${appName}/project.json`);
|
||||
|
||||
expect(webpackConfig).toMatchSnapshot();
|
||||
expect(projectJSON).toMatchSnapshot(); // This file should be updated adding standardWebpackConfigFunction: true
|
||||
|
||||
expect(oldWebpackConfig).toMatchSnapshot(); // This file should be renamed and updated to not include `withNx`, `withReact`, and `withWeb`.
|
||||
|
||||
expect(() => {
|
||||
runCLI(`build ${appName}`);
|
||||
}).not.toThrow();
|
||||
|
||||
if (runE2ETests()) {
|
||||
expect(() => {
|
||||
runCLI(`e2e ${appName}-e2e`);
|
||||
}).not.toThrow();
|
||||
}
|
||||
}, 600_000);
|
||||
});
|
||||
});
|
||||
|
||||
@ -15,6 +15,11 @@
|
||||
"schema": "./src/generators/configuration/schema.json",
|
||||
"description": "Add webpack configuration to a project.",
|
||||
"hidden": true
|
||||
},
|
||||
"convert-config-to-webpack-plugin": {
|
||||
"factory": "./src/generators/convert-config-to-webpack-plugin/convert-config-to-webpack-plugin",
|
||||
"schema": "./src/generators/convert-config-to-webpack-plugin/schema.json",
|
||||
"description": "Convert the project to use the `NxAppWebpackPlugin` and `NxReactWebpackPlugin`."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,8 +1,14 @@
|
||||
import { configurationGenerator } from './src/generators/configuration/configuration';
|
||||
import { NxAppWebpackPlugin } from './src/plugins/nx-webpack-plugin/nx-app-webpack-plugin';
|
||||
import { NxTsconfigPathsWebpackPlugin as _NxTsconfigPathsWebpackPlugin } from './src/plugins/nx-typescript-webpack-plugin/nx-tsconfig-paths-webpack-plugin';
|
||||
import { convertConfigToWebpackPluginGenerator } from './src/generators/convert-config-to-webpack-plugin/convert-config-to-webpack-plugin';
|
||||
import { useLegacyNxPlugin } from './src/plugins/use-legacy-nx-plugin/use-legacy-nx-plugin';
|
||||
|
||||
export { configurationGenerator };
|
||||
export {
|
||||
configurationGenerator,
|
||||
convertConfigToWebpackPluginGenerator,
|
||||
useLegacyNxPlugin,
|
||||
};
|
||||
|
||||
// Exported for backwards compatibility in case a plugin is using the old name.
|
||||
/** @deprecated Use `configurationGenerator` instead. */
|
||||
|
||||
@ -31,6 +31,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@babel/core": "^7.23.2",
|
||||
"@phenomnomnominal/tsquery": "~5.0.1",
|
||||
"ajv": "^8.12.0",
|
||||
"autoprefixer": "^10.4.9",
|
||||
"babel-loader": "^9.1.2",
|
||||
|
||||
@ -102,7 +102,12 @@ export async function* devServerExecutor(
|
||||
);
|
||||
} else if (userDefinedWebpackConfig) {
|
||||
// New behavior, we want the webpack config to export object
|
||||
config = userDefinedWebpackConfig;
|
||||
// If the config is a function, we assume it's a standard webpack config function and it's async
|
||||
if (typeof userDefinedWebpackConfig === 'function') {
|
||||
config = await userDefinedWebpackConfig(process.env.NODE_ENV, {});
|
||||
} else {
|
||||
config = userDefinedWebpackConfig;
|
||||
}
|
||||
config.devServer ??= devServer;
|
||||
}
|
||||
}
|
||||
|
||||
@ -75,6 +75,11 @@ async function getWebpackConfigs(
|
||||
configuration: context.configurationName, // backwards compat
|
||||
});
|
||||
} else if (userDefinedWebpackConfig) {
|
||||
if (typeof userDefinedWebpackConfig === 'function') {
|
||||
// assume it's an async standard webpack config function
|
||||
// https://webpack.js.org/configuration/configuration-types/#exporting-a-promise
|
||||
return await userDefinedWebpackConfig(process.env.NODE_ENV, {});
|
||||
}
|
||||
// New behavior, we want the webpack config to export object
|
||||
return userDefinedWebpackConfig;
|
||||
} else {
|
||||
|
||||
@ -0,0 +1,429 @@
|
||||
import {
|
||||
ProjectConfiguration,
|
||||
Tree,
|
||||
addProjectConfiguration,
|
||||
} from '@nx/devkit';
|
||||
import { createTreeWithEmptyWorkspace } from '@nx/devkit/testing';
|
||||
import convertConfigToWebpackPluginGenerator from './convert-config-to-webpack-plugin';
|
||||
|
||||
interface CreateProjectOptions {
|
||||
name: string;
|
||||
root: string;
|
||||
targetName: string;
|
||||
targetOptions: Record<string, unknown>;
|
||||
additionalTargets?: Record<string, unknown>;
|
||||
}
|
||||
|
||||
const defaultOptions: CreateProjectOptions = {
|
||||
name: 'my-app',
|
||||
root: 'my-app',
|
||||
targetName: 'build',
|
||||
targetOptions: {},
|
||||
};
|
||||
|
||||
function createProject(tree: Tree, options: Partial<CreateProjectOptions>) {
|
||||
const projectOpts = {
|
||||
...defaultOptions,
|
||||
...options,
|
||||
targetOptions: {
|
||||
...defaultOptions.targetOptions,
|
||||
...options?.targetOptions,
|
||||
},
|
||||
};
|
||||
const project: ProjectConfiguration = {
|
||||
name: projectOpts.name,
|
||||
root: projectOpts.root,
|
||||
targets: {
|
||||
build: {
|
||||
executor: '@nx/webpack:webpack',
|
||||
options: {
|
||||
webpackConfig: `${projectOpts.root}/webpack.config.js`,
|
||||
...projectOpts.targetOptions,
|
||||
},
|
||||
},
|
||||
...options.additionalTargets,
|
||||
},
|
||||
};
|
||||
|
||||
addProjectConfiguration(tree, project.name, project);
|
||||
|
||||
return project;
|
||||
}
|
||||
|
||||
describe('convertConfigToWebpackPluginGenerator', () => {
|
||||
let tree: Tree;
|
||||
|
||||
beforeEach(() => {
|
||||
tree = createTreeWithEmptyWorkspace();
|
||||
});
|
||||
|
||||
it('should migrate the webpack config of the specified project', async () => {
|
||||
const project = createProject(tree, {
|
||||
name: 'my-app',
|
||||
root: 'my-app',
|
||||
});
|
||||
|
||||
createProject(tree, {
|
||||
name: 'another-app',
|
||||
root: 'another-app',
|
||||
});
|
||||
|
||||
tree.write(
|
||||
'another-app/webpack.config.js',
|
||||
`
|
||||
const { composePlugins, withNx } = require('@nx/webpack');
|
||||
const { withReact } = require('@nx/react');
|
||||
|
||||
// Nx plugins for webpack.
|
||||
module.exports = composePlugins(
|
||||
withNx(),
|
||||
withReact({
|
||||
// Uncomment this line if you don't want to use SVGR
|
||||
// See: https://react-svgr.com/
|
||||
// svgr: false
|
||||
}),
|
||||
(config) => {
|
||||
return config;
|
||||
}
|
||||
);
|
||||
`
|
||||
);
|
||||
|
||||
tree.write(
|
||||
`${project.name}/webpack.config.js`,
|
||||
`
|
||||
const { composePlugins, withNx } = require('@nx/webpack');
|
||||
const { withReact } = require('@nx/react');
|
||||
|
||||
// Nx plugins for webpack.
|
||||
module.exports = composePlugins(
|
||||
withNx(),
|
||||
withReact({
|
||||
// Uncomment this line if you don't want to use SVGR
|
||||
// See: https://react-svgr.com/
|
||||
// svgr: false
|
||||
}),
|
||||
(config) => {
|
||||
return config;
|
||||
}
|
||||
);
|
||||
`
|
||||
);
|
||||
|
||||
await convertConfigToWebpackPluginGenerator(tree, {
|
||||
project: project.name,
|
||||
});
|
||||
expect(tree.read(`${project.name}/webpack.config.js`, 'utf-8'))
|
||||
.toMatchInlineSnapshot(`
|
||||
"const { NxAppWebpackPlugin } = require('@nx/webpack/app-plugin');
|
||||
const { NxReactWebpackPlugin } = require('@nx/react/webpack-plugin');
|
||||
const { useLegacyNxPlugin } = require('@nx/webpack');
|
||||
|
||||
// This file was migrated using @nx/webpack:convert-config-to-webpack-plugin from your './webpack.config.old.js'
|
||||
// Please check that the options here are correct as they were moved from the old webpack.config.js to this file.
|
||||
const options = {};
|
||||
|
||||
/**
|
||||
* @type{import('webpack').WebpackOptionsNormalized}
|
||||
*/
|
||||
module.exports = async () => ({
|
||||
plugins: [
|
||||
new NxAppWebpackPlugin(),
|
||||
new NxReactWebpackPlugin({
|
||||
// Uncomment this line if you don't want to use SVGR
|
||||
// See: https://react-svgr.com/
|
||||
// svgr: false
|
||||
}),
|
||||
// eslint-disable-next-line react-hooks/rules-of-hooks
|
||||
await useLegacyNxPlugin(require('./webpack.config.old'), options),
|
||||
],
|
||||
});
|
||||
"
|
||||
`);
|
||||
|
||||
expect(tree.read(`${project.name}/webpack.config.old.js`, 'utf-8'))
|
||||
.toMatchInlineSnapshot(`
|
||||
"const { composePlugins } = require('@nx/webpack');
|
||||
// Nx plugins for webpack.
|
||||
module.exports = composePlugins((config) => {
|
||||
return config;
|
||||
});
|
||||
"
|
||||
`);
|
||||
|
||||
expect(tree.read(`another-app/webpack.config.js`, 'utf-8'))
|
||||
.toMatchInlineSnapshot(`
|
||||
"const { composePlugins, withNx } = require('@nx/webpack');
|
||||
const { withReact } = require('@nx/react');
|
||||
|
||||
// Nx plugins for webpack.
|
||||
module.exports = composePlugins(
|
||||
withNx(),
|
||||
withReact({
|
||||
// Uncomment this line if you don't want to use SVGR
|
||||
// See: https://react-svgr.com/
|
||||
// svgr: false
|
||||
}),
|
||||
(config) => {
|
||||
return config;
|
||||
}
|
||||
);
|
||||
"
|
||||
`);
|
||||
|
||||
expect(tree.exists(`${project.name}/webpack.config.old.js`)).toBe(true);
|
||||
expect(tree.exists(`another-app/webpack.config.old.js`)).toBe(false);
|
||||
});
|
||||
|
||||
it('should update project.json adding the standardWebpackConfigFunction option', async () => {
|
||||
const project = createProject(tree, {
|
||||
name: 'my-app',
|
||||
root: 'my-app',
|
||||
});
|
||||
|
||||
tree.write(
|
||||
`${project.name}/webpack.config.js`,
|
||||
`
|
||||
const { composePlugins, withNx } = require('@nx/webpack');
|
||||
const { withReact } = require('@nx/react');
|
||||
|
||||
// Nx plugins for webpack.
|
||||
module.exports = composePlugins(
|
||||
withNx(),
|
||||
withReact({
|
||||
// Uncomment this line if you don't want to use SVGR
|
||||
// See: https://react-svgr.com/
|
||||
// svgr: false
|
||||
}),
|
||||
(config) => {
|
||||
return config;
|
||||
}
|
||||
);
|
||||
`
|
||||
);
|
||||
|
||||
await convertConfigToWebpackPluginGenerator(tree, {
|
||||
project: project.name,
|
||||
});
|
||||
|
||||
expect(tree.read(`${project.name}/project.json`, 'utf-8'))
|
||||
.toMatchInlineSnapshot(`
|
||||
"{
|
||||
"name": "my-app",
|
||||
"$schema": "../node_modules/nx/schemas/project-schema.json",
|
||||
"targets": {
|
||||
"build": {
|
||||
"executor": "@nx/webpack:webpack",
|
||||
"options": {
|
||||
"webpackConfig": "my-app/webpack.config.js",
|
||||
"standardWebpackConfigFunction": true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
"
|
||||
`);
|
||||
});
|
||||
|
||||
it('should throw an error if no projects are found', async () => {
|
||||
const project = createProject(tree, {
|
||||
name: 'my-app',
|
||||
root: 'my-app',
|
||||
});
|
||||
|
||||
await expect(
|
||||
convertConfigToWebpackPluginGenerator(tree, {
|
||||
project: project.name,
|
||||
})
|
||||
).rejects.toThrowError('Could not find any projects to migrate.');
|
||||
});
|
||||
|
||||
it('should not migrate a webpack config that does not use withNx', async () => {
|
||||
const project = createProject(tree, {
|
||||
name: 'my-app',
|
||||
root: 'my-app',
|
||||
});
|
||||
|
||||
tree.write(`${project.name}/webpack.config.js`, `module.exports = {};`);
|
||||
|
||||
await expect(
|
||||
convertConfigToWebpackPluginGenerator(tree, {
|
||||
project: project.name,
|
||||
})
|
||||
).rejects.toThrowError('Could not find any projects to migrate.');
|
||||
|
||||
expect(
|
||||
tree.read(`${project.name}/webpack.config.js`, 'utf-8')
|
||||
).toMatchInlineSnapshot(`"module.exports = {};"`);
|
||||
});
|
||||
|
||||
it('should throw an error if the project is using Module federation', async () => {
|
||||
const project = createProject(tree, {
|
||||
name: 'my-app',
|
||||
root: 'my-app',
|
||||
additionalTargets: {
|
||||
serve: {
|
||||
executor: '@nx/react:module-federation-dev-server',
|
||||
options: {
|
||||
buildTarget: 'my-app:build',
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
await expect(
|
||||
convertConfigToWebpackPluginGenerator(tree, { project: project.name })
|
||||
).rejects.toThrowError(
|
||||
`The project ${project.name} is using Module Federation. At the moment, we don't support migrating projects that use Module Federation.`
|
||||
);
|
||||
});
|
||||
|
||||
it('should throw an error if the project is a Nest project', async () => {
|
||||
const project = createProject(tree, {
|
||||
name: 'my-app',
|
||||
root: 'my-app',
|
||||
additionalTargets: {
|
||||
serve: {
|
||||
executor: '@nx/js:node',
|
||||
options: {
|
||||
buildTarget: 'my-app:build',
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
await expect(
|
||||
convertConfigToWebpackPluginGenerator(tree, { project: project.name })
|
||||
).rejects.toThrowError(
|
||||
`The project ${project.name} is using the '@nx/js:node' executor. At the moment, we do not support migrating such projects.`
|
||||
);
|
||||
});
|
||||
|
||||
it('should not migrate a webpack config that is already using NxAppWebpackPlugin', async () => {
|
||||
const project = createProject(tree, {
|
||||
name: 'my-app',
|
||||
root: 'my-app',
|
||||
});
|
||||
|
||||
tree.write(
|
||||
`${project.name}/webpack.config.js`,
|
||||
`
|
||||
const { NxAppWebpackPlugin } = require('@nx/webpack/app-plugin');
|
||||
|
||||
module.exports = {
|
||||
plugins: [
|
||||
new NxAppWebpackPlugin(),
|
||||
],
|
||||
};
|
||||
`
|
||||
);
|
||||
|
||||
await expect(
|
||||
convertConfigToWebpackPluginGenerator(tree, { project: project.name })
|
||||
).rejects.toThrowError(`Could not find any projects to migrate.`);
|
||||
expect(tree.read(`${project.name}/webpack.config.js`, 'utf-8'))
|
||||
.toMatchInlineSnapshot(`
|
||||
"
|
||||
const { NxAppWebpackPlugin } = require('@nx/webpack/app-plugin');
|
||||
|
||||
module.exports = {
|
||||
plugins: [
|
||||
new NxAppWebpackPlugin(),
|
||||
],
|
||||
};
|
||||
"
|
||||
`);
|
||||
expect(tree.exists(`${project.name}/webpack.config.old.js`)).toBe(false);
|
||||
});
|
||||
|
||||
it('should convert absolute options paths to relative paths during the conversion', async () => {
|
||||
const project = createProject(tree, {
|
||||
name: 'my-app',
|
||||
root: 'apps/my-app',
|
||||
});
|
||||
|
||||
tree.write(
|
||||
`${project.root}/webpack.config.js`,
|
||||
`
|
||||
const { composePlugins, withNx } = require('@nx/webpack');
|
||||
const { withReact } = require('@nx/react');
|
||||
|
||||
// Nx plugins for webpack.
|
||||
module.exports = composePlugins(
|
||||
withNx({
|
||||
assets: ["apps/${project.name}/src/favicon.ico","apps/${project.name}/src/assets"],
|
||||
styles: ["apps/${project.name}/src/styles.scss"],
|
||||
scripts: ["apps/${project.name}/src/scripts.js"],
|
||||
tsConfig: "apps/${project.name}/tsconfig.app.json",
|
||||
fileReplacements: [
|
||||
{
|
||||
replace: "apps/${project.name}/src/environments/environment.ts",
|
||||
with: "apps/${project.name}/src/environments/environment.prod.ts"
|
||||
}
|
||||
],
|
||||
additionalEntryPoints: [
|
||||
{
|
||||
entryPath: "apps/${project.name}/src/polyfills.ts",
|
||||
}
|
||||
]
|
||||
}),
|
||||
withReact({
|
||||
// Uncomment this line if you don't want to use SVGR
|
||||
// See: https://react-svgr.com/
|
||||
// svgr: false
|
||||
}),
|
||||
(config) => {
|
||||
return config;
|
||||
}
|
||||
);
|
||||
`
|
||||
);
|
||||
|
||||
await convertConfigToWebpackPluginGenerator(tree, {
|
||||
project: project.name,
|
||||
});
|
||||
expect(tree.read(`${project.root}/webpack.config.js`, 'utf-8'))
|
||||
.toMatchInlineSnapshot(`
|
||||
"const { NxAppWebpackPlugin } = require('@nx/webpack/app-plugin');
|
||||
const { NxReactWebpackPlugin } = require('@nx/react/webpack-plugin');
|
||||
const { useLegacyNxPlugin } = require('@nx/webpack');
|
||||
|
||||
// This file was migrated using @nx/webpack:convert-config-to-webpack-plugin from your './webpack.config.old.js'
|
||||
// Please check that the options here are correct as they were moved from the old webpack.config.js to this file.
|
||||
const options = {
|
||||
assets: ['./src/favicon.ico', './src/assets'],
|
||||
styles: ['./src/styles.scss'],
|
||||
scripts: ['./src/scripts.js'],
|
||||
tsConfig: './tsconfig.app.json',
|
||||
fileReplacements: [
|
||||
{
|
||||
replace: './src/environments/environment.ts',
|
||||
with: './src/environments/environment.prod.ts',
|
||||
},
|
||||
],
|
||||
additionalEntryPoints: [
|
||||
{
|
||||
entryPath: './src/polyfills.ts',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
/**
|
||||
* @type{import('webpack').WebpackOptionsNormalized}
|
||||
*/
|
||||
module.exports = async () => ({
|
||||
plugins: [
|
||||
new NxAppWebpackPlugin(options),
|
||||
new NxReactWebpackPlugin({
|
||||
// Uncomment this line if you don't want to use SVGR
|
||||
// See: https://react-svgr.com/
|
||||
// svgr: false
|
||||
}),
|
||||
// eslint-disable-next-line react-hooks/rules-of-hooks
|
||||
await useLegacyNxPlugin(require('./webpack.config.old'), options),
|
||||
],
|
||||
});
|
||||
"
|
||||
`);
|
||||
});
|
||||
});
|
||||
@ -0,0 +1,135 @@
|
||||
import {
|
||||
formatFiles,
|
||||
getProjects,
|
||||
stripIndents,
|
||||
Tree,
|
||||
joinPathFragments,
|
||||
updateProjectConfiguration,
|
||||
ProjectConfiguration,
|
||||
} from '@nx/devkit';
|
||||
import { forEachExecutorOptions } from '@nx/devkit/src/generators/executor-options-utils';
|
||||
import { WebpackExecutorOptions } from '../../executors/webpack/schema';
|
||||
import { extractWebpackOptions } from './lib/extract-webpack-options';
|
||||
import { normalizePathOptions } from './lib/normalize-path-options';
|
||||
import { parse } from 'path';
|
||||
import { validateProject } from './lib/validate-project';
|
||||
|
||||
interface Schema {
|
||||
project?: string;
|
||||
skipFormat?: boolean;
|
||||
}
|
||||
|
||||
// Make text JSON compatible
|
||||
const preprocessText = (text: string) => {
|
||||
return text
|
||||
.replace(/(\w+):/g, '"$1":') // Quote property names
|
||||
.replace(/'/g, '"') // Convert single quotes to double quotes
|
||||
.replace(/,(\s*[}\]])/g, '$1') // Remove trailing commas
|
||||
.replace(/(\r\n|\n|\r|\t)/gm, ''); // Remove newlines and tabs
|
||||
};
|
||||
|
||||
export async function convertConfigToWebpackPluginGenerator(
|
||||
tree: Tree,
|
||||
options: Schema
|
||||
) {
|
||||
let migrated = 0;
|
||||
|
||||
const projects = getProjects(tree);
|
||||
forEachExecutorOptions<WebpackExecutorOptions>(
|
||||
tree,
|
||||
'@nx/webpack:webpack',
|
||||
(currentTargetOptions, projectName, targetName, configurationName) => {
|
||||
if (options.project && projectName !== options.project) {
|
||||
return;
|
||||
}
|
||||
if (!configurationName) {
|
||||
const project = projects.get(projectName);
|
||||
const target = project.targets[targetName];
|
||||
|
||||
const hasError = validateProject(tree, project);
|
||||
if (hasError) {
|
||||
throw new Error(hasError);
|
||||
}
|
||||
|
||||
const webpackConfigPath = currentTargetOptions?.webpackConfig || '';
|
||||
|
||||
if (webpackConfigPath && tree.exists(webpackConfigPath)) {
|
||||
let { withNxConfig: webpackOptions, withReactConfig } =
|
||||
extractWebpackOptions(tree, webpackConfigPath);
|
||||
|
||||
// if webpackOptions === undefined
|
||||
// withNx was not found in the webpack.config.js file so we should skip this project
|
||||
if (webpackOptions !== undefined) {
|
||||
let parsedOptions = {};
|
||||
if (webpackOptions) {
|
||||
parsedOptions = JSON.parse(
|
||||
preprocessText(webpackOptions.getText())
|
||||
);
|
||||
parsedOptions = normalizePathOptions(project.root, parsedOptions);
|
||||
}
|
||||
|
||||
target.options.standardWebpackConfigFunction = true;
|
||||
|
||||
updateProjectConfiguration(tree, projectName, project);
|
||||
|
||||
const { dir, name, ext } = parse(webpackConfigPath);
|
||||
|
||||
tree.rename(
|
||||
webpackConfigPath,
|
||||
`${joinPathFragments(dir, `${name}.old${ext}`)}`
|
||||
);
|
||||
|
||||
tree.write(
|
||||
webpackConfigPath,
|
||||
stripIndents`
|
||||
const { NxAppWebpackPlugin } = require('@nx/webpack/app-plugin');
|
||||
const { NxReactWebpackPlugin } = require('@nx/react/webpack-plugin');
|
||||
const { useLegacyNxPlugin } = require('@nx/webpack');
|
||||
|
||||
// This file was migrated using @nx/webpack:convert-config-to-webpack-plugin from your './webpack.config.old.js'
|
||||
// Please check that the options here are correct as they were moved from the old webpack.config.js to this file.
|
||||
const options = ${
|
||||
webpackOptions ? JSON.stringify(parsedOptions, null, 2) : '{}'
|
||||
};
|
||||
|
||||
/**
|
||||
* @type{import('webpack').WebpackOptionsNormalized}
|
||||
*/
|
||||
module.exports = async () => ({
|
||||
plugins: [
|
||||
${
|
||||
webpackOptions
|
||||
? 'new NxAppWebpackPlugin(options)'
|
||||
: 'new NxAppWebpackPlugin()'
|
||||
},
|
||||
${
|
||||
withReactConfig
|
||||
? `new NxReactWebpackPlugin(${withReactConfig.getText()})`
|
||||
: `new NxReactWebpackPlugin({
|
||||
// Uncomment this line if you don't want to use SVGR
|
||||
// See: https://react-svgr.com/
|
||||
// svgr: false
|
||||
})`
|
||||
},
|
||||
// eslint-disable-next-line react-hooks/rules-of-hooks
|
||||
await useLegacyNxPlugin(require('./webpack.config.old'), options),
|
||||
],
|
||||
});
|
||||
`
|
||||
);
|
||||
migrated++;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
);
|
||||
if (migrated === 0) {
|
||||
throw new Error('Could not find any projects to migrate.');
|
||||
}
|
||||
|
||||
if (!options.skipFormat) {
|
||||
await formatFiles(tree);
|
||||
}
|
||||
}
|
||||
|
||||
export default convertConfigToWebpackPluginGenerator;
|
||||
@ -0,0 +1,176 @@
|
||||
import { Tree } from '@nx/devkit';
|
||||
import { tsquery } from '@phenomnomnominal/tsquery';
|
||||
import * as ts from 'typescript';
|
||||
|
||||
export function extractWebpackOptions(tree: Tree, webpackConfigPath: string) {
|
||||
const source = tree.read(webpackConfigPath).toString('utf-8');
|
||||
const ast = tsquery.ast(source);
|
||||
|
||||
const withNxQuery = 'CallExpression:has(Identifier[name="withNx"])';
|
||||
const withReactQuery = 'CallExpression:has(Identifier[name="withReact"])';
|
||||
const withWebQuery = 'CallExpression:has(Identifier[name="withWeb"])';
|
||||
|
||||
const withNxCall = tsquery(ast, withNxQuery) as ts.CallExpression[];
|
||||
|
||||
const withReactCall = tsquery(ast, withReactQuery) as ts.CallExpression[];
|
||||
|
||||
const withWebCall = tsquery(ast, withWebQuery) as ts.CallExpression[];
|
||||
|
||||
// If the config is empty set to empty string to avoid undefined. Undefined is used to check if the withNx exists inside of the config file.
|
||||
let withNxConfig: ts.Node | '' | undefined,
|
||||
withReactConfig: ts.Node | '' | undefined;
|
||||
|
||||
withWebCall.forEach((node) => {
|
||||
const argument = node.arguments[0] || '';
|
||||
withNxConfig = argument; // Since withWeb and withNx use the same config object and both should not exist in the same file, we can reuse the withNxConfig variable.
|
||||
});
|
||||
|
||||
withNxCall.forEach((node) => {
|
||||
const argument = node.arguments[0] || ''; // The first argument is the config object
|
||||
withNxConfig = argument;
|
||||
});
|
||||
|
||||
withReactCall.forEach((node) => {
|
||||
const argument = node.arguments[0] || '';
|
||||
withReactConfig = argument;
|
||||
});
|
||||
|
||||
if (withNxConfig !== undefined) {
|
||||
// Only remove the withNx and withReact calls if they exist
|
||||
let updatedSource = removeCallExpressions(source, [
|
||||
'withNx',
|
||||
'withReact',
|
||||
'withWeb',
|
||||
]);
|
||||
updatedSource = removeImportDeclarations(
|
||||
updatedSource,
|
||||
'withNx',
|
||||
'@nx/webpack'
|
||||
);
|
||||
updatedSource = removeImportDeclarations(
|
||||
updatedSource,
|
||||
'withWeb',
|
||||
'@nx/webpack'
|
||||
);
|
||||
updatedSource = removeImportDeclarations(
|
||||
updatedSource,
|
||||
'withReact',
|
||||
'@nx/react'
|
||||
);
|
||||
|
||||
tree.write(webpackConfigPath, updatedSource);
|
||||
}
|
||||
|
||||
return { withNxConfig, withReactConfig };
|
||||
}
|
||||
|
||||
function removeCallExpressions(
|
||||
source: string,
|
||||
functionNames: string[]
|
||||
): string {
|
||||
let modifiedSource = source;
|
||||
functionNames.forEach((functionName) => {
|
||||
const callExpressionQuery = `CallExpression:has(Identifier[name="composePlugins"]) > CallExpression:has(Identifier[name="${functionName}"])`;
|
||||
|
||||
modifiedSource = tsquery.replace(
|
||||
modifiedSource,
|
||||
callExpressionQuery,
|
||||
() => {
|
||||
return ''; // Removes the entire CallExpression
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
return modifiedSource;
|
||||
}
|
||||
|
||||
function removeImportDeclarations(
|
||||
source: string,
|
||||
importName: string,
|
||||
moduleName: string
|
||||
) {
|
||||
const sourceFile = tsquery.ast(source);
|
||||
|
||||
const modifiedStatements = sourceFile.statements
|
||||
.map((statement) => {
|
||||
if (!ts.isVariableStatement(statement)) return statement;
|
||||
|
||||
const declarationList = statement.declarationList;
|
||||
const newDeclarations = declarationList.declarations
|
||||
.map((declaration) => {
|
||||
if (
|
||||
!ts.isVariableDeclaration(declaration) ||
|
||||
!declaration.initializer
|
||||
)
|
||||
return declaration;
|
||||
|
||||
if (
|
||||
ts.isCallExpression(declaration.initializer) &&
|
||||
ts.isIdentifier(declaration.initializer.expression)
|
||||
) {
|
||||
const callExpr = declaration.initializer.expression;
|
||||
if (
|
||||
callExpr.text === 'require' &&
|
||||
declaration.initializer.arguments[0]
|
||||
?.getText()
|
||||
.replace(/['"]/g, '') === moduleName
|
||||
) {
|
||||
if (ts.isObjectBindingPattern(declaration.name)) {
|
||||
const bindingElements = declaration.name.elements.filter(
|
||||
(element) => {
|
||||
const elementName = element.name.getText();
|
||||
return elementName !== importName;
|
||||
}
|
||||
);
|
||||
|
||||
if (bindingElements.length > 0) {
|
||||
const newBindingPattern =
|
||||
ts.factory.updateObjectBindingPattern(
|
||||
declaration.name,
|
||||
bindingElements
|
||||
);
|
||||
|
||||
// Update the variable declaration with the new binding pattern without the specified import name
|
||||
return ts.factory.updateVariableDeclaration(
|
||||
declaration,
|
||||
newBindingPattern,
|
||||
declaration.exclamationToken,
|
||||
declaration.type,
|
||||
declaration.initializer
|
||||
);
|
||||
} else {
|
||||
return null; // Remove this declaration entirely if no bindings remain
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return declaration;
|
||||
})
|
||||
.filter(Boolean);
|
||||
|
||||
if (newDeclarations.length > 0) {
|
||||
const newDeclarationList = ts.factory.updateVariableDeclarationList(
|
||||
declarationList,
|
||||
newDeclarations as ts.VariableDeclaration[]
|
||||
);
|
||||
return ts.factory.updateVariableStatement(
|
||||
statement,
|
||||
statement.modifiers,
|
||||
newDeclarationList
|
||||
);
|
||||
} else {
|
||||
return null; // Remove the entire statement
|
||||
}
|
||||
})
|
||||
.filter(Boolean);
|
||||
|
||||
// Use printer to format the source code and rewrite the modified
|
||||
const newSourceFile = ts.factory.updateSourceFile(
|
||||
sourceFile,
|
||||
modifiedStatements as ts.Statement[]
|
||||
);
|
||||
const printer = ts.createPrinter();
|
||||
const formattedSource = printer.printFile(newSourceFile);
|
||||
|
||||
return formattedSource;
|
||||
}
|
||||
@ -0,0 +1,92 @@
|
||||
import { WebpackExecutorOptions } from '../../../executors/webpack/schema';
|
||||
import { toProjectRelativePath } from './utils';
|
||||
|
||||
const executorFieldsToNormalize: Array<keyof WebpackExecutorOptions> = [
|
||||
'outputPath',
|
||||
'index',
|
||||
'main',
|
||||
'assets',
|
||||
'tsConfig',
|
||||
'styles',
|
||||
'babelConfig',
|
||||
'additionalEntryPoints',
|
||||
'scripts',
|
||||
'fileReplacements',
|
||||
'postcssConfig',
|
||||
'stylePreprocessorOptions',
|
||||
'publicPath',
|
||||
];
|
||||
|
||||
export function normalizePathOptions(
|
||||
projectRoot: string,
|
||||
options: Partial<WebpackExecutorOptions>
|
||||
) {
|
||||
for (const [key, value] of Object.entries(options)) {
|
||||
if (
|
||||
!executorFieldsToNormalize.includes(key as keyof WebpackExecutorOptions)
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
options[key] = normalizePath(
|
||||
projectRoot,
|
||||
key as keyof WebpackExecutorOptions,
|
||||
value
|
||||
);
|
||||
}
|
||||
return options;
|
||||
}
|
||||
|
||||
function normalizePath<K extends keyof WebpackExecutorOptions>(
|
||||
projectRoot: string,
|
||||
key: K,
|
||||
value: WebpackExecutorOptions[K]
|
||||
) {
|
||||
if (!value) return value;
|
||||
|
||||
switch (key) {
|
||||
case 'assets':
|
||||
return value.map((asset) => {
|
||||
if (typeof asset === 'string') {
|
||||
return toProjectRelativePath(asset, projectRoot);
|
||||
}
|
||||
return {
|
||||
...asset,
|
||||
input: toProjectRelativePath(asset.input, projectRoot),
|
||||
output: toProjectRelativePath(asset.output, projectRoot),
|
||||
};
|
||||
});
|
||||
|
||||
case 'styles':
|
||||
case 'scripts':
|
||||
return value.map((item) => {
|
||||
if (typeof item === 'string') {
|
||||
return toProjectRelativePath(item, projectRoot);
|
||||
}
|
||||
return {
|
||||
...item,
|
||||
input: toProjectRelativePath(item.input, projectRoot),
|
||||
};
|
||||
});
|
||||
|
||||
case 'additionalEntryPoints':
|
||||
return value.map((entry) => {
|
||||
return {
|
||||
...entry,
|
||||
entryPath: toProjectRelativePath(entry.entryPath, projectRoot),
|
||||
};
|
||||
});
|
||||
|
||||
case 'fileReplacements':
|
||||
return value.map((replacement) => {
|
||||
return {
|
||||
replace: toProjectRelativePath(replacement.replace, projectRoot),
|
||||
with: toProjectRelativePath(replacement.with, projectRoot),
|
||||
};
|
||||
});
|
||||
|
||||
default:
|
||||
return Array.isArray(value)
|
||||
? value.map((item) => toProjectRelativePath(item, projectRoot))
|
||||
: toProjectRelativePath(value, projectRoot);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,19 @@
|
||||
import { relative, resolve } from 'path/posix';
|
||||
import { workspaceRoot } from '@nx/devkit';
|
||||
|
||||
export function toProjectRelativePath(
|
||||
path: string,
|
||||
projectRoot: string
|
||||
): string {
|
||||
if (projectRoot === '.') {
|
||||
// workspace and project root are the same, we normalize it to ensure it
|
||||
return path.startsWith('.') ? path : `./${path}`;
|
||||
}
|
||||
|
||||
const relativePath = relative(
|
||||
resolve(workspaceRoot, projectRoot),
|
||||
resolve(workspaceRoot, path)
|
||||
);
|
||||
|
||||
return relativePath.startsWith('.') ? relativePath : `./${relativePath}`;
|
||||
}
|
||||
@ -0,0 +1,47 @@
|
||||
import { ProjectConfiguration, Tree } from '@nx/devkit';
|
||||
|
||||
function hasAnotherWebpackConfig(tree: Tree, projectRoot: string) {
|
||||
const files = tree.children(projectRoot);
|
||||
const projectJsonString = tree.read(`${projectRoot}/project.json`, 'utf-8');
|
||||
for (const file of files) {
|
||||
if (
|
||||
file !== 'webpack.config.js' &&
|
||||
file.endsWith('.js') &&
|
||||
file.includes('webpack.config') &&
|
||||
projectJsonString.includes(file) &&
|
||||
tree.exists(`${projectRoot}/webpack.config.js`)
|
||||
) {
|
||||
return 'Cannot convert a project with multiple webpack config files. Please consolidate them into a single webpack.config.js file.';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function isNestProject(project: ProjectConfiguration) {
|
||||
for (const target in project.targets) {
|
||||
if (project.targets[target].executor === '@nx/js:node') {
|
||||
return `The project ${project.name} is using the '@nx/js:node' executor. At the moment, we do not support migrating such projects.`;
|
||||
}
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Validates the project to ensure it can be migrated
|
||||
*
|
||||
* @param tree The virtaul file system
|
||||
* @param project the project configuration object for the project
|
||||
* @returns A string if there is an error, otherwise undefined
|
||||
*/
|
||||
export function validateProject(tree: Tree, project: ProjectConfiguration) {
|
||||
const containsMfeExecutor = Object.keys(project.targets).some((target) => {
|
||||
return [
|
||||
'@nx/react:module-federation-dev-server',
|
||||
'@nx/angular:module-federation-dev-server',
|
||||
].includes(project.targets[target].executor);
|
||||
});
|
||||
|
||||
if (containsMfeExecutor) {
|
||||
return `The project ${project.name} is using Module Federation. At the moment, we don't support migrating projects that use Module Federation.`;
|
||||
}
|
||||
|
||||
const hasAnotherConfig = hasAnotherWebpackConfig(tree, project.root);
|
||||
return hasAnotherConfig || isNestProject(project);
|
||||
}
|
||||
@ -0,0 +1,19 @@
|
||||
{
|
||||
"$schema": "https://json-schema.org/schema",
|
||||
"$id": "NxWebpackConvertConfigToWebpackPlugin",
|
||||
"description": "Convert existing Webpack project(s) using `@nx/webpack:webpack` executor that uses `withNx` to use `NxAppWebpackPlugin`. Defaults to migrating all projects. Pass '--project' to migrate only one target.",
|
||||
"title": "Convert Webpack project using withNx to NxAppWebpackPlugin",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"project": {
|
||||
"type": "string",
|
||||
"description": "The project to convert from using the `@nx/webpack:webpack` executor and `withNx` plugin to use `NxAppWebpackPlugin`.",
|
||||
"x-priority": "important"
|
||||
},
|
||||
"skipFormat": {
|
||||
"type": "boolean",
|
||||
"description": "Whether to format files at the end of the migration.",
|
||||
"default": false
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -22,6 +22,8 @@ import {
|
||||
import { instantiateScriptPlugins } from './instantiate-script-plugins';
|
||||
import CssMinimizerPlugin = require('css-minimizer-webpack-plugin');
|
||||
import MiniCssExtractPlugin = require('mini-css-extract-plugin');
|
||||
import { getDevServerOptions } from '../../../executors/dev-server/lib/get-dev-server-config';
|
||||
import { NormalizedWebpackExecutorOptions } from '../../../executors/webpack/schema';
|
||||
|
||||
export function applyWebConfig(
|
||||
options: NormalizedNxAppWebpackPluginOptions,
|
||||
|
||||
@ -76,7 +76,7 @@ export function normalizeOptions(
|
||||
|
||||
const sourceRoot = projectNode.data.sourceRoot ?? projectNode.data.root;
|
||||
|
||||
if (!options.main) {
|
||||
if (!combinedPluginAndMaybeExecutorOptions.main) {
|
||||
throw new Error(
|
||||
`Missing "main" option for the entry file. Set this option in your Nx webpack plugin.`
|
||||
);
|
||||
|
||||
@ -0,0 +1,65 @@
|
||||
import { ExecutorContext, readCachedProjectGraph } from '@nx/devkit';
|
||||
import { NxWebpackExecutionContext } from '../../utils/config';
|
||||
import { NxAppWebpackPluginOptions } from '../nx-webpack-plugin/nx-app-webpack-plugin-options';
|
||||
import { Configuration } from 'webpack';
|
||||
import { normalizeOptions } from '../nx-webpack-plugin/lib/normalize-options';
|
||||
|
||||
/**
|
||||
* This function is used to wrap the legacy plugin function to be used with the `composePlugins` function.
|
||||
* Initially the webpack config would be passed to the legacy plugin function and the options would be passed as a second argument.
|
||||
* example:
|
||||
* module.exports = composePlugins(
|
||||
withNx(),
|
||||
(config) => {
|
||||
return config;
|
||||
}
|
||||
);
|
||||
|
||||
Since composePlugins is async, this function is used to wrap the legacy plugin function to be async.
|
||||
Using the nxUseLegacyPlugin function, the first argument is the legacy plugin function and the second argument is the options.
|
||||
The context options are created and passed to the legacy plugin function.
|
||||
|
||||
module.exports = async () => ({
|
||||
plugins: [
|
||||
...otherPlugins,
|
||||
await nxUseLegacyPlugin(require({path}), options),
|
||||
],
|
||||
});
|
||||
* @param fn The legacy plugin function usually from `combinedPlugins`
|
||||
* @param executorOptions The options passed usually inside the executor or the config file
|
||||
* @returns Webpack configuration
|
||||
*/
|
||||
export async function useLegacyNxPlugin(
|
||||
fn: (
|
||||
config: Configuration,
|
||||
ctx: NxWebpackExecutionContext
|
||||
) => Promise<Configuration>,
|
||||
executorOptions: NxAppWebpackPluginOptions
|
||||
) {
|
||||
const options = normalizeOptions(executorOptions);
|
||||
|
||||
const projectGraph = readCachedProjectGraph();
|
||||
const projectName = process.env.NX_TASK_TARGET_PROJECT;
|
||||
const project = projectGraph.nodes[projectName];
|
||||
const targetName = process.env.NX_TASK_TARGET_TARGET;
|
||||
|
||||
const context: ExecutorContext = {
|
||||
cwd: process.cwd(),
|
||||
isVerbose: process.env.NX_VERBOSE_LOGGING === 'true',
|
||||
root: project.data.root,
|
||||
projectGraph: readCachedProjectGraph(),
|
||||
target: project.data.targets[targetName],
|
||||
targetName: targetName,
|
||||
projectName: projectName,
|
||||
};
|
||||
|
||||
const configuration = process.env.NX_TASK_TARGET_CONFIGURATION;
|
||||
return async (config: Configuration) => {
|
||||
const ctx: NxWebpackExecutionContext = {
|
||||
context,
|
||||
options: options as NxWebpackExecutionContext['options'],
|
||||
configuration,
|
||||
};
|
||||
return await fn(config, ctx);
|
||||
};
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user