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": [],
|
"children": [],
|
||||||
"isExternal": false,
|
"isExternal": false,
|
||||||
"disableCollapsible": 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,
|
"isExternal": false,
|
||||||
|
|||||||
@ -3215,6 +3215,15 @@
|
|||||||
"originalFilePath": "/packages/webpack/src/generators/configuration/schema.json",
|
"originalFilePath": "/packages/webpack/src/generators/configuration/schema.json",
|
||||||
"path": "/nx-api/webpack/generators/configuration",
|
"path": "/nx-api/webpack/generators/configuration",
|
||||||
"type": "generator"
|
"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"
|
"path": "/nx-api/webpack"
|
||||||
|
|||||||
@ -3180,6 +3180,15 @@
|
|||||||
"originalFilePath": "/packages/webpack/src/generators/configuration/schema.json",
|
"originalFilePath": "/packages/webpack/src/generators/configuration/schema.json",
|
||||||
"path": "webpack/generators/configuration",
|
"path": "webpack/generators/configuration",
|
||||||
"type": "generator"
|
"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",
|
"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)
|
- [generators](/nx-api/webpack/generators)
|
||||||
- [init](/nx-api/webpack/generators/init)
|
- [init](/nx-api/webpack/generators/init)
|
||||||
- [configuration](/nx-api/webpack/generators/configuration)
|
- [configuration](/nx-api/webpack/generators/configuration)
|
||||||
|
- [convert-config-to-webpack-plugin](/nx-api/webpack/generators/convert-config-to-webpack-plugin)
|
||||||
- [workspace](/nx-api/workspace)
|
- [workspace](/nx-api/workspace)
|
||||||
- [documents](/nx-api/workspace/documents)
|
- [documents](/nx-api/workspace/documents)
|
||||||
- [Overview](/nx-api/workspace/documents/overview)
|
- [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,
|
cleanupProject,
|
||||||
killProcessAndPorts,
|
killProcessAndPorts,
|
||||||
newProject,
|
newProject,
|
||||||
|
readFile,
|
||||||
runCLI,
|
runCLI,
|
||||||
runCommandUntil,
|
runCommandUntil,
|
||||||
runE2ETests,
|
runE2ETests,
|
||||||
@ -118,21 +119,21 @@ describe('Webpack Plugin (legacy)', () => {
|
|||||||
updateFile(
|
updateFile(
|
||||||
`${appName}/webpack.config.js`,
|
`${appName}/webpack.config.js`,
|
||||||
`
|
`
|
||||||
const { join } = require('path');
|
const { join } = require('path');
|
||||||
const {NxWebpackPlugin} = require('@nx/webpack');
|
const {NxWebpackPlugin} = require('@nx/webpack');
|
||||||
module.exports = {
|
module.exports = {
|
||||||
output: {
|
output: {
|
||||||
path: join(__dirname, '../dist/app9524918'),
|
path: join(__dirname, '../dist/app9524918'),
|
||||||
},
|
},
|
||||||
plugins: [
|
plugins: [
|
||||||
new NxAppWebpackPlugin({
|
new NxAppWebpackPlugin({
|
||||||
main: './src/main.ts',
|
main: './src/main.ts',
|
||||||
compiler: 'tsc',
|
compiler: 'tsc',
|
||||||
index: './src/index.html',
|
index: './src/index.html',
|
||||||
tsConfig: './tsconfig.app.json',
|
tsConfig: './tsconfig.app.json',
|
||||||
})
|
})
|
||||||
]
|
]
|
||||||
};
|
};
|
||||||
`
|
`
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -146,4 +147,45 @@ module.exports = {
|
|||||||
}).not.toThrow();
|
}).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",
|
"schema": "./src/generators/configuration/schema.json",
|
||||||
"description": "Add webpack configuration to a project.",
|
"description": "Add webpack configuration to a project.",
|
||||||
"hidden": true
|
"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 { configurationGenerator } from './src/generators/configuration/configuration';
|
||||||
import { NxAppWebpackPlugin } from './src/plugins/nx-webpack-plugin/nx-app-webpack-plugin';
|
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 { 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.
|
// Exported for backwards compatibility in case a plugin is using the old name.
|
||||||
/** @deprecated Use `configurationGenerator` instead. */
|
/** @deprecated Use `configurationGenerator` instead. */
|
||||||
|
|||||||
@ -31,6 +31,7 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/core": "^7.23.2",
|
"@babel/core": "^7.23.2",
|
||||||
|
"@phenomnomnominal/tsquery": "~5.0.1",
|
||||||
"ajv": "^8.12.0",
|
"ajv": "^8.12.0",
|
||||||
"autoprefixer": "^10.4.9",
|
"autoprefixer": "^10.4.9",
|
||||||
"babel-loader": "^9.1.2",
|
"babel-loader": "^9.1.2",
|
||||||
|
|||||||
@ -102,7 +102,12 @@ export async function* devServerExecutor(
|
|||||||
);
|
);
|
||||||
} else if (userDefinedWebpackConfig) {
|
} else if (userDefinedWebpackConfig) {
|
||||||
// New behavior, we want the webpack config to export object
|
// 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;
|
config.devServer ??= devServer;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -75,6 +75,11 @@ async function getWebpackConfigs(
|
|||||||
configuration: context.configurationName, // backwards compat
|
configuration: context.configurationName, // backwards compat
|
||||||
});
|
});
|
||||||
} else if (userDefinedWebpackConfig) {
|
} 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
|
// New behavior, we want the webpack config to export object
|
||||||
return userDefinedWebpackConfig;
|
return userDefinedWebpackConfig;
|
||||||
} else {
|
} 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 { instantiateScriptPlugins } from './instantiate-script-plugins';
|
||||||
import CssMinimizerPlugin = require('css-minimizer-webpack-plugin');
|
import CssMinimizerPlugin = require('css-minimizer-webpack-plugin');
|
||||||
import MiniCssExtractPlugin = require('mini-css-extract-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(
|
export function applyWebConfig(
|
||||||
options: NormalizedNxAppWebpackPluginOptions,
|
options: NormalizedNxAppWebpackPluginOptions,
|
||||||
|
|||||||
@ -76,7 +76,7 @@ export function normalizeOptions(
|
|||||||
|
|
||||||
const sourceRoot = projectNode.data.sourceRoot ?? projectNode.data.root;
|
const sourceRoot = projectNode.data.sourceRoot ?? projectNode.data.root;
|
||||||
|
|
||||||
if (!options.main) {
|
if (!combinedPluginAndMaybeExecutorOptions.main) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`Missing "main" option for the entry file. Set this option in your Nx webpack plugin.`
|
`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