feat(webpack): add convert-to-inferred generator (#26621)
<!-- Please make sure you have read the submission guidelines before posting an PR --> <!-- https://github.com/nrwl/nx/blob/master/CONTRIBUTING.md#-submitting-a-pr --> <!-- Please make sure that your commit message follows our format --> <!-- Example: `fix(nx): must begin with lowercase` --> <!-- If this is a particularly complex change or feature addition, you can request a dedicated Nx release for this pull request branch. Mention someone from the Nx team or the `@nrwl/nx-pipelines-reviewers` and they will confirm if the PR warrants its own release for testing purposes, and generate it for you if appropriate. --> ## Current Behavior <!-- This is the behavior we have today --> ## Expected Behavior <!-- This is the behavior we should expect with the changes in this PR --> ## Related Issue(s) <!-- Please link the issue being fixed so it gets closed when this is merged. --> Fixes # --------- Co-authored-by: Jack Hsu <jack.hsu@gmail.com>
This commit is contained in:
parent
a3322f76ef
commit
47dfdcfc6b
@ -9891,6 +9891,14 @@
|
||||
"children": [],
|
||||
"isExternal": false,
|
||||
"disableCollapsible": false
|
||||
},
|
||||
{
|
||||
"id": "convert-to-inferred",
|
||||
"path": "/nx-api/webpack/generators/convert-to-inferred",
|
||||
"name": "convert-to-inferred",
|
||||
"children": [],
|
||||
"isExternal": false,
|
||||
"disableCollapsible": false
|
||||
}
|
||||
],
|
||||
"isExternal": false,
|
||||
|
||||
@ -3242,6 +3242,15 @@
|
||||
"originalFilePath": "/packages/webpack/src/generators/convert-config-to-webpack-plugin/schema.json",
|
||||
"path": "/nx-api/webpack/generators/convert-config-to-webpack-plugin",
|
||||
"type": "generator"
|
||||
},
|
||||
"/nx-api/webpack/generators/convert-to-inferred": {
|
||||
"description": "Convert existing Webpack project(s) using `@nx/webpack:wepack` executor to use `@nx/webpack/plugin`.",
|
||||
"file": "generated/packages/webpack/generators/convert-to-inferred.json",
|
||||
"hidden": false,
|
||||
"name": "convert-to-inferred",
|
||||
"originalFilePath": "/packages/webpack/src/generators/convert-to-inferred/schema.json",
|
||||
"path": "/nx-api/webpack/generators/convert-to-inferred",
|
||||
"type": "generator"
|
||||
}
|
||||
},
|
||||
"path": "/nx-api/webpack"
|
||||
|
||||
@ -3207,6 +3207,15 @@
|
||||
"originalFilePath": "/packages/webpack/src/generators/convert-config-to-webpack-plugin/schema.json",
|
||||
"path": "webpack/generators/convert-config-to-webpack-plugin",
|
||||
"type": "generator"
|
||||
},
|
||||
{
|
||||
"description": "Convert existing Webpack project(s) using `@nx/webpack:wepack` executor to use `@nx/webpack/plugin`.",
|
||||
"file": "generated/packages/webpack/generators/convert-to-inferred.json",
|
||||
"hidden": false,
|
||||
"name": "convert-to-inferred",
|
||||
"originalFilePath": "/packages/webpack/src/generators/convert-to-inferred/schema.json",
|
||||
"path": "webpack/generators/convert-to-inferred",
|
||||
"type": "generator"
|
||||
}
|
||||
],
|
||||
"githubRoot": "https://github.com/nrwl/nx/blob/master",
|
||||
|
||||
@ -0,0 +1,30 @@
|
||||
{
|
||||
"name": "convert-to-inferred",
|
||||
"factory": "./src/generators/convert-to-inferred/convert-to-inferred#convertToInferred",
|
||||
"schema": {
|
||||
"$schema": "https://json-schema.org/schema",
|
||||
"$id": "NxWebpackConvertToInferred",
|
||||
"description": "Convert existing Webpack project(s) using `@nx/webpack:wepack` executor to use `@nx/webpack/plugin`.",
|
||||
"title": "Convert a Webpack project from executor to plugin",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"project": {
|
||||
"type": "string",
|
||||
"description": "The project to convert from using the `@nx/webpack:webpack` executor to use `@nx/webpack/plugin`. If not provided, all projects using the `@nx/webpack:webpack` executor will be converted.",
|
||||
"x-priority": "important"
|
||||
},
|
||||
"skipFormat": {
|
||||
"type": "boolean",
|
||||
"description": "Whether to format files.",
|
||||
"default": false
|
||||
}
|
||||
},
|
||||
"presets": []
|
||||
},
|
||||
"description": "Convert existing Webpack project(s) using `@nx/webpack:wepack` executor to use `@nx/webpack/plugin`.",
|
||||
"implementation": "/packages/webpack/src/generators/convert-to-inferred/convert-to-inferred#convertToInferred.ts",
|
||||
"aliases": [],
|
||||
"hidden": false,
|
||||
"path": "/packages/webpack/src/generators/convert-to-inferred/schema.json",
|
||||
"type": "generator"
|
||||
}
|
||||
@ -712,6 +712,7 @@
|
||||
- [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)
|
||||
- [convert-to-inferred](/nx-api/webpack/generators/convert-to-inferred)
|
||||
- [workspace](/nx-api/workspace)
|
||||
- [documents](/nx-api/workspace/documents)
|
||||
- [Overview](/nx-api/workspace/documents/overview)
|
||||
|
||||
@ -44,6 +44,10 @@ export class AggregatedLog {
|
||||
}
|
||||
|
||||
flushLogs(): void {
|
||||
if (this.logs.size === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
let fullLog = '';
|
||||
for (const executorName of this.logs.keys()) {
|
||||
fullLog = `${fullLog}${output.bold(
|
||||
|
||||
@ -27,7 +27,10 @@ import {
|
||||
ProjectConfigurationsError,
|
||||
} from 'nx/src/devkit-internals';
|
||||
import type { ConfigurationResult } from 'nx/src/project-graph/utils/project-configuration-utils';
|
||||
import type { InputDefinition } from 'nx/src/config/workspace-json-project-json';
|
||||
import type {
|
||||
InputDefinition,
|
||||
ProjectConfiguration,
|
||||
} from 'nx/src/config/workspace-json-project-json';
|
||||
|
||||
type PluginOptionsBuilder<T> = (targetName: string) => T;
|
||||
type PostTargetTransformer = (
|
||||
@ -38,7 +41,10 @@ type PostTargetTransformer = (
|
||||
) => TargetConfiguration | Promise<TargetConfiguration>;
|
||||
type SkipTargetFilter = (
|
||||
targetConfiguration: TargetConfiguration
|
||||
) => [boolean, string];
|
||||
) => false | string;
|
||||
type SkipProjectFilter = (
|
||||
projectConfiguration: ProjectConfiguration
|
||||
) => false | string;
|
||||
|
||||
class ExecutorToPluginMigrator<T> {
|
||||
readonly tree: Tree;
|
||||
@ -48,6 +54,7 @@ class ExecutorToPluginMigrator<T> {
|
||||
readonly #pluginOptionsBuilder: PluginOptionsBuilder<T>;
|
||||
readonly #postTargetTransformer: PostTargetTransformer;
|
||||
readonly #skipTargetFilter: SkipTargetFilter;
|
||||
readonly #skipProjectFilter: SkipProjectFilter;
|
||||
readonly #specificProjectToMigrate: string;
|
||||
#nxJson: NxJsonConfiguration;
|
||||
#targetDefaultsForExecutor: Partial<TargetConfiguration>;
|
||||
@ -56,6 +63,7 @@ class ExecutorToPluginMigrator<T> {
|
||||
#createNodes?: CreateNodes<T>;
|
||||
#createNodesV2?: CreateNodesV2<T>;
|
||||
#createNodesResultsForTargets: Map<string, ConfigurationResult>;
|
||||
#skippedProjects: Set<string>;
|
||||
|
||||
constructor(
|
||||
tree: Tree,
|
||||
@ -67,7 +75,10 @@ class ExecutorToPluginMigrator<T> {
|
||||
createNodes?: CreateNodes<T>,
|
||||
createNodesV2?: CreateNodesV2<T>,
|
||||
specificProjectToMigrate?: string,
|
||||
skipTargetFilter?: SkipTargetFilter
|
||||
filters?: {
|
||||
skipProjectFilter?: SkipProjectFilter;
|
||||
skipTargetFilter?: SkipTargetFilter;
|
||||
}
|
||||
) {
|
||||
this.tree = tree;
|
||||
this.#projectGraph = projectGraph;
|
||||
@ -78,7 +89,9 @@ class ExecutorToPluginMigrator<T> {
|
||||
this.#createNodes = createNodes;
|
||||
this.#createNodesV2 = createNodesV2;
|
||||
this.#specificProjectToMigrate = specificProjectToMigrate;
|
||||
this.#skipTargetFilter = skipTargetFilter ?? ((...args) => [false, '']);
|
||||
this.#skipProjectFilter =
|
||||
filters?.skipProjectFilter ?? ((...args) => false);
|
||||
this.#skipTargetFilter = filters?.skipTargetFilter ?? ((...args) => false);
|
||||
}
|
||||
|
||||
async run(): Promise<Map<string, Set<string>>> {
|
||||
@ -99,6 +112,7 @@ class ExecutorToPluginMigrator<T> {
|
||||
this.#targetAndProjectsToMigrate = new Map();
|
||||
this.#pluginToAddForTarget = new Map();
|
||||
this.#createNodesResultsForTargets = new Map();
|
||||
this.#skippedProjects = new Set();
|
||||
|
||||
this.#getTargetDefaultsForExecutor();
|
||||
this.#getTargetAndProjectsToMigrate();
|
||||
@ -311,7 +325,7 @@ class ExecutorToPluginMigrator<T> {
|
||||
this.tree,
|
||||
this.#executor,
|
||||
(targetConfiguration, projectName, targetName, configurationName) => {
|
||||
if (configurationName) {
|
||||
if (this.#skippedProjects.has(projectName) || configurationName) {
|
||||
return;
|
||||
}
|
||||
|
||||
@ -322,10 +336,23 @@ class ExecutorToPluginMigrator<T> {
|
||||
return;
|
||||
}
|
||||
|
||||
const [skipTarget, reasonTargetWasSkipped] =
|
||||
this.#skipTargetFilter(targetConfiguration);
|
||||
if (skipTarget) {
|
||||
const errorMsg = `${targetName} target on project "${projectName}" cannot be migrated. ${reasonTargetWasSkipped}`;
|
||||
const skipProjectReason = this.#skipProjectFilter(
|
||||
this.#projectGraph.nodes[projectName].data
|
||||
);
|
||||
if (skipProjectReason) {
|
||||
this.#skippedProjects.add(projectName);
|
||||
const errorMsg = `The "${projectName}" project cannot be migrated. ${skipProjectReason}`;
|
||||
if (this.#specificProjectToMigrate) {
|
||||
throw new Error(errorMsg);
|
||||
}
|
||||
|
||||
console.warn(errorMsg);
|
||||
return;
|
||||
}
|
||||
|
||||
const skipTargetReason = this.#skipTargetFilter(targetConfiguration);
|
||||
if (skipTargetReason) {
|
||||
const errorMsg = `${targetName} target on project "${projectName}" cannot be migrated. ${skipTargetReason}`;
|
||||
if (this.#specificProjectToMigrate) {
|
||||
throw new Error(errorMsg);
|
||||
} else {
|
||||
@ -375,6 +402,7 @@ class ExecutorToPluginMigrator<T> {
|
||||
return;
|
||||
}
|
||||
|
||||
global.NX_GRAPH_CREATION = true;
|
||||
for (const targetName of this.#targetAndProjectsToMigrate.keys()) {
|
||||
const loadedPlugin = new LoadedNxPlugin(
|
||||
{
|
||||
@ -398,12 +426,14 @@ class ExecutorToPluginMigrator<T> {
|
||||
if (e instanceof ProjectConfigurationsError) {
|
||||
projectConfigs = e.partialProjectConfigurationsResult;
|
||||
} else {
|
||||
global.NX_GRAPH_CREATION = false;
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
this.#createNodesResultsForTargets.set(targetName, projectConfigs);
|
||||
}
|
||||
global.NX_GRAPH_CREATION = false;
|
||||
}
|
||||
}
|
||||
|
||||
@ -416,7 +446,10 @@ export async function migrateExecutorToPlugin<T>(
|
||||
postTargetTransformer: PostTargetTransformer,
|
||||
createNodes: CreateNodesV2<T>,
|
||||
specificProjectToMigrate?: string,
|
||||
skipTargetFilter?: SkipTargetFilter
|
||||
filters?: {
|
||||
skipProjectFilter?: SkipProjectFilter;
|
||||
skipTargetFilter?: SkipTargetFilter;
|
||||
}
|
||||
): Promise<Map<string, Set<string>>> {
|
||||
const migrator = new ExecutorToPluginMigrator<T>(
|
||||
tree,
|
||||
@ -428,7 +461,7 @@ export async function migrateExecutorToPlugin<T>(
|
||||
undefined,
|
||||
createNodes,
|
||||
specificProjectToMigrate,
|
||||
skipTargetFilter
|
||||
filters
|
||||
);
|
||||
return await migrator.run();
|
||||
}
|
||||
@ -442,7 +475,10 @@ export async function migrateExecutorToPluginV1<T>(
|
||||
postTargetTransformer: PostTargetTransformer,
|
||||
createNodes: CreateNodes<T>,
|
||||
specificProjectToMigrate?: string,
|
||||
skipTargetFilter?: SkipTargetFilter
|
||||
filters?: {
|
||||
skipProjectFilter?: SkipProjectFilter;
|
||||
skipTargetFilter?: SkipTargetFilter;
|
||||
}
|
||||
): Promise<Map<string, Set<string>>> {
|
||||
const migrator = new ExecutorToPluginMigrator<T>(
|
||||
tree,
|
||||
@ -454,7 +490,7 @@ export async function migrateExecutorToPluginV1<T>(
|
||||
createNodes,
|
||||
undefined,
|
||||
specificProjectToMigrate,
|
||||
skipTargetFilter
|
||||
filters
|
||||
);
|
||||
return await migrator.run();
|
||||
}
|
||||
|
||||
@ -20,6 +20,11 @@
|
||||
"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`."
|
||||
},
|
||||
"convert-to-inferred": {
|
||||
"factory": "./src/generators/convert-to-inferred/convert-to-inferred#convertToInferred",
|
||||
"schema": "./src/generators/convert-to-inferred/schema.json",
|
||||
"description": "Convert existing Webpack project(s) using `@nx/webpack:wepack` executor to use `@nx/webpack/plugin`."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -0,0 +1,280 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`convert-to-inferred all projects should migrate all projects using the webpack executors 1`] = `
|
||||
"const { NxAppWebpackPlugin } = require('@nx/webpack/app-plugin');
|
||||
const { NxReactWebpackPlugin } = require('@nx/react/webpack-plugin');
|
||||
const { useLegacyNxPlugin } = require('@nx/webpack');
|
||||
|
||||
// These options were migrated by @nx/webpack:convert-to-inferred from
|
||||
// the project.json file and merged with the options in this file
|
||||
const configValues = {
|
||||
build: {
|
||||
default: {
|
||||
compiler: 'babel',
|
||||
outputPath: '../../dist/apps/app1',
|
||||
index: './src/index.html',
|
||||
baseHref: '/',
|
||||
main: './src/main.tsx',
|
||||
tsConfig: './tsconfig.app.json',
|
||||
assets: ['./src/favicon.ico', './src/assets'],
|
||||
styles: ['./src/styles.scss'],
|
||||
},
|
||||
development: {
|
||||
extractLicenses: false,
|
||||
optimization: false,
|
||||
sourceMap: true,
|
||||
vendorChunk: true,
|
||||
},
|
||||
production: {
|
||||
fileReplacements: [
|
||||
{
|
||||
replace: './src/environments/environment.ts',
|
||||
with: './src/environments/environment.prod.ts',
|
||||
},
|
||||
],
|
||||
optimization: true,
|
||||
outputHashing: 'all',
|
||||
sourceMap: false,
|
||||
namedChunks: false,
|
||||
extractLicenses: true,
|
||||
vendorChunk: false,
|
||||
},
|
||||
},
|
||||
serve: {
|
||||
default: {
|
||||
hot: true,
|
||||
liveReload: false,
|
||||
server: {
|
||||
type: 'https',
|
||||
options: { cert: './server.crt', key: './server.key' },
|
||||
},
|
||||
proxy: { '/api': { target: 'http://localhost:3333', secure: false } },
|
||||
port: 4200,
|
||||
headers: { 'Access-Control-Allow-Origin': '*' },
|
||||
historyApiFallback: {
|
||||
index: '/index.html',
|
||||
disableDotRule: true,
|
||||
htmlAcceptHeaders: ['text/html', 'application/xhtml+xml'],
|
||||
},
|
||||
},
|
||||
development: { open: true },
|
||||
production: { hot: false },
|
||||
},
|
||||
};
|
||||
|
||||
// Determine the correct configValue to use based on the configuration
|
||||
const configuration = process.env.NX_TASK_TARGET_CONFIGURATION || 'default';
|
||||
|
||||
const buildOptions = {
|
||||
...configValues.build.default,
|
||||
...configValues.build[configuration],
|
||||
};
|
||||
const devServerOptions = {
|
||||
...configValues.serve.default,
|
||||
...configValues.serve[configuration],
|
||||
};
|
||||
|
||||
/**
|
||||
* @type{import('webpack').WebpackOptionsNormalized}
|
||||
*/
|
||||
module.exports = async () => ({
|
||||
devServer: devServerOptions,
|
||||
plugins: [
|
||||
new NxAppWebpackPlugin(buildOptions),
|
||||
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'), buildOptions),
|
||||
],
|
||||
});
|
||||
"
|
||||
`;
|
||||
|
||||
exports[`convert-to-inferred all projects should migrate all projects using the webpack executors 2`] = `
|
||||
"const { NxAppWebpackPlugin } = require('@nx/webpack/app-plugin');
|
||||
const { NxReactWebpackPlugin } = require('@nx/react/webpack-plugin');
|
||||
const { useLegacyNxPlugin } = require('@nx/webpack');
|
||||
|
||||
// These options were migrated by @nx/webpack:convert-to-inferred from
|
||||
// the project.json file and merged with the options in this file
|
||||
const configValues = {
|
||||
build: {
|
||||
default: {
|
||||
compiler: 'babel',
|
||||
outputPath: '../../dist/apps/app2',
|
||||
index: './src/index.html',
|
||||
baseHref: '/',
|
||||
main: './src/main.tsx',
|
||||
tsConfig: './tsconfig.app.json',
|
||||
assets: ['./src/favicon.ico', './src/assets'],
|
||||
styles: ['./src/styles.scss'],
|
||||
},
|
||||
development: {
|
||||
extractLicenses: false,
|
||||
optimization: false,
|
||||
sourceMap: true,
|
||||
vendorChunk: true,
|
||||
},
|
||||
production: {
|
||||
fileReplacements: [
|
||||
{
|
||||
replace: './src/environments/environment.ts',
|
||||
with: './src/environments/environment.prod.ts',
|
||||
},
|
||||
],
|
||||
optimization: true,
|
||||
outputHashing: 'all',
|
||||
sourceMap: false,
|
||||
namedChunks: false,
|
||||
extractLicenses: true,
|
||||
vendorChunk: false,
|
||||
},
|
||||
},
|
||||
serve: {
|
||||
default: {
|
||||
hot: true,
|
||||
liveReload: false,
|
||||
server: {
|
||||
type: 'https',
|
||||
options: { cert: './server.crt', key: './server.key' },
|
||||
},
|
||||
proxy: { '/api': { target: 'http://localhost:3333', secure: false } },
|
||||
port: 4200,
|
||||
headers: { 'Access-Control-Allow-Origin': '*' },
|
||||
historyApiFallback: {
|
||||
index: '/index.html',
|
||||
disableDotRule: true,
|
||||
htmlAcceptHeaders: ['text/html', 'application/xhtml+xml'],
|
||||
},
|
||||
},
|
||||
development: { open: true },
|
||||
production: { hot: false },
|
||||
},
|
||||
};
|
||||
|
||||
// Determine the correct configValue to use based on the configuration
|
||||
const configuration = process.env.NX_TASK_TARGET_CONFIGURATION || 'default';
|
||||
|
||||
const buildOptions = {
|
||||
...configValues.build.default,
|
||||
...configValues.build[configuration],
|
||||
};
|
||||
const devServerOptions = {
|
||||
...configValues.serve.default,
|
||||
...configValues.serve[configuration],
|
||||
};
|
||||
|
||||
/**
|
||||
* @type{import('webpack').WebpackOptionsNormalized}
|
||||
*/
|
||||
module.exports = async () => ({
|
||||
devServer: devServerOptions,
|
||||
plugins: [
|
||||
new NxAppWebpackPlugin(buildOptions),
|
||||
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'), buildOptions),
|
||||
],
|
||||
});
|
||||
"
|
||||
`;
|
||||
|
||||
exports[`convert-to-inferred all projects should migrate all projects using the webpack executors 3`] = `
|
||||
"const { NxAppWebpackPlugin } = require('@nx/webpack/app-plugin');
|
||||
const { NxReactWebpackPlugin } = require('@nx/react/webpack-plugin');
|
||||
const { useLegacyNxPlugin } = require('@nx/webpack');
|
||||
|
||||
// These options were migrated by @nx/webpack:convert-to-inferred from
|
||||
// the project.json file and merged with the options in this file
|
||||
const configValues = {
|
||||
build: {
|
||||
default: {
|
||||
compiler: 'babel',
|
||||
outputPath: '../../dist/apps/app3',
|
||||
index: './src/index.html',
|
||||
baseHref: '/',
|
||||
main: './src/main.tsx',
|
||||
tsConfig: './tsconfig.app.json',
|
||||
assets: ['./src/favicon.ico', './src/assets'],
|
||||
styles: ['./src/styles.scss'],
|
||||
},
|
||||
development: {
|
||||
extractLicenses: false,
|
||||
optimization: false,
|
||||
sourceMap: true,
|
||||
vendorChunk: true,
|
||||
},
|
||||
production: {
|
||||
fileReplacements: [
|
||||
{
|
||||
replace: './src/environments/environment.ts',
|
||||
with: './src/environments/environment.prod.ts',
|
||||
},
|
||||
],
|
||||
optimization: true,
|
||||
outputHashing: 'all',
|
||||
sourceMap: false,
|
||||
namedChunks: false,
|
||||
extractLicenses: true,
|
||||
vendorChunk: false,
|
||||
},
|
||||
},
|
||||
serve: {
|
||||
default: {
|
||||
hot: true,
|
||||
liveReload: false,
|
||||
server: {
|
||||
type: 'https',
|
||||
options: { cert: './server.crt', key: './server.key' },
|
||||
},
|
||||
proxy: { '/api': { target: 'http://localhost:3333', secure: false } },
|
||||
port: 4200,
|
||||
headers: { 'Access-Control-Allow-Origin': '*' },
|
||||
historyApiFallback: {
|
||||
index: '/index.html',
|
||||
disableDotRule: true,
|
||||
htmlAcceptHeaders: ['text/html', 'application/xhtml+xml'],
|
||||
},
|
||||
},
|
||||
development: { open: true },
|
||||
production: { hot: false },
|
||||
},
|
||||
};
|
||||
|
||||
// Determine the correct configValue to use based on the configuration
|
||||
const configuration = process.env.NX_TASK_TARGET_CONFIGURATION || 'default';
|
||||
|
||||
const buildOptions = {
|
||||
...configValues.build.default,
|
||||
...configValues.build[configuration],
|
||||
};
|
||||
const devServerOptions = {
|
||||
...configValues.serve.default,
|
||||
...configValues.serve[configuration],
|
||||
};
|
||||
|
||||
/**
|
||||
* @type{import('webpack').WebpackOptionsNormalized}
|
||||
*/
|
||||
module.exports = async () => ({
|
||||
devServer: devServerOptions,
|
||||
plugins: [
|
||||
new NxAppWebpackPlugin(buildOptions),
|
||||
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'), buildOptions),
|
||||
],
|
||||
});
|
||||
"
|
||||
`;
|
||||
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,174 @@
|
||||
import {
|
||||
addDependenciesToPackageJson,
|
||||
createProjectGraphAsync,
|
||||
formatFiles,
|
||||
type ProjectConfiguration,
|
||||
runTasksInSerial,
|
||||
type Tree,
|
||||
} from '@nx/devkit';
|
||||
import { AggregatedLog } from '@nx/devkit/src/generators/plugin-migrations/aggregate-log-util';
|
||||
import { migrateExecutorToPlugin } from '@nx/devkit/src/generators/plugin-migrations/executor-to-plugin-migrator';
|
||||
import { tsquery } from '@phenomnomnominal/tsquery';
|
||||
import * as ts from 'typescript';
|
||||
import { createNodesV2, type WebpackPluginOptions } from '../../plugins/plugin';
|
||||
import { webpackCliVersion } from '../../utils/versions';
|
||||
import {
|
||||
buildPostTargetTransformerFactory,
|
||||
type MigrationContext,
|
||||
servePostTargetTransformerFactory,
|
||||
} from './utils';
|
||||
|
||||
interface Schema {
|
||||
project?: string;
|
||||
skipFormat?: boolean;
|
||||
}
|
||||
|
||||
export async function convertToInferred(tree: Tree, options: Schema) {
|
||||
const projectGraph = await createProjectGraphAsync();
|
||||
const migrationContext: MigrationContext = {
|
||||
logger: new AggregatedLog(),
|
||||
projectGraph,
|
||||
workspaceRoot: tree.root,
|
||||
};
|
||||
|
||||
// build
|
||||
const migratedBuildProjects =
|
||||
await migrateExecutorToPlugin<WebpackPluginOptions>(
|
||||
tree,
|
||||
projectGraph,
|
||||
'@nx/webpack:webpack',
|
||||
'@nx/webpack/plugin',
|
||||
(targetName) => ({
|
||||
buildTargetName: targetName,
|
||||
previewTargetName: 'preview',
|
||||
serveStaticTargetName: 'serve-static',
|
||||
serveTargetName: 'serve',
|
||||
}),
|
||||
buildPostTargetTransformerFactory(migrationContext),
|
||||
createNodesV2,
|
||||
options.project,
|
||||
{ skipProjectFilter: skipProjectFilterFactory(tree) }
|
||||
);
|
||||
const migratedBuildProjectsLegacy =
|
||||
await migrateExecutorToPlugin<WebpackPluginOptions>(
|
||||
tree,
|
||||
projectGraph,
|
||||
'@nrwl/webpack:webpack',
|
||||
'@nx/webpack/plugin',
|
||||
(targetName) => ({
|
||||
buildTargetName: targetName,
|
||||
previewTargetName: 'preview',
|
||||
serveStaticTargetName: 'serve-static',
|
||||
serveTargetName: 'serve',
|
||||
}),
|
||||
buildPostTargetTransformerFactory(migrationContext),
|
||||
createNodesV2,
|
||||
options.project,
|
||||
{ skipProjectFilter: skipProjectFilterFactory(tree) }
|
||||
);
|
||||
|
||||
// serve
|
||||
const migratedServeProjects =
|
||||
await migrateExecutorToPlugin<WebpackPluginOptions>(
|
||||
tree,
|
||||
projectGraph,
|
||||
'@nx/webpack:dev-server',
|
||||
'@nx/webpack/plugin',
|
||||
(targetName) => ({
|
||||
buildTargetName: 'build',
|
||||
previewTargetName: 'preview',
|
||||
serveStaticTargetName: 'serve-static',
|
||||
serveTargetName: targetName,
|
||||
}),
|
||||
servePostTargetTransformerFactory(migrationContext),
|
||||
createNodesV2,
|
||||
options.project,
|
||||
{ skipProjectFilter: skipProjectFilterFactory(tree) }
|
||||
);
|
||||
const migratedServeProjectsLegacy =
|
||||
await migrateExecutorToPlugin<WebpackPluginOptions>(
|
||||
tree,
|
||||
projectGraph,
|
||||
'@nrwl/webpack:dev-server',
|
||||
'@nx/webpack/plugin',
|
||||
(targetName) => ({
|
||||
buildTargetName: 'build',
|
||||
previewTargetName: 'preview',
|
||||
serveStaticTargetName: 'serve-static',
|
||||
serveTargetName: targetName,
|
||||
}),
|
||||
servePostTargetTransformerFactory(migrationContext),
|
||||
createNodesV2,
|
||||
options.project,
|
||||
{ skipProjectFilter: skipProjectFilterFactory(tree) }
|
||||
);
|
||||
|
||||
const migratedProjects =
|
||||
migratedBuildProjects.size +
|
||||
migratedBuildProjectsLegacy.size +
|
||||
migratedServeProjects.size +
|
||||
migratedServeProjectsLegacy.size;
|
||||
|
||||
if (migratedProjects === 0) {
|
||||
throw new Error('Could not find any targets to migrate.');
|
||||
}
|
||||
|
||||
const installCallback = addDependenciesToPackageJson(
|
||||
tree,
|
||||
{},
|
||||
{ 'webpack-cli': webpackCliVersion },
|
||||
undefined,
|
||||
true
|
||||
);
|
||||
|
||||
if (!options.skipFormat) {
|
||||
await formatFiles(tree);
|
||||
}
|
||||
|
||||
return runTasksInSerial(installCallback, () => {
|
||||
migrationContext.logger.flushLogs();
|
||||
});
|
||||
}
|
||||
|
||||
function skipProjectFilterFactory(tree: Tree) {
|
||||
return function skipProjectFilter(
|
||||
projectConfiguration: ProjectConfiguration
|
||||
): false | string {
|
||||
const buildTarget = Object.values(projectConfiguration.targets).find(
|
||||
(target) =>
|
||||
target.executor === '@nx/webpack:webpack' ||
|
||||
target.executor === '@nrwl/webpack:webpack'
|
||||
);
|
||||
// the projects for which this is called are guaranteed to have a build target
|
||||
const webpackConfigPath = buildTarget.options.webpackConfig;
|
||||
if (!webpackConfigPath) {
|
||||
return `The webpack config path is missing in the project configuration (${projectConfiguration.root}).`;
|
||||
}
|
||||
|
||||
const sourceFile = tsquery.ast(tree.read(webpackConfigPath, 'utf-8'));
|
||||
|
||||
const composePluginsSelector =
|
||||
'CallExpression:has(Identifier[name=composePlugins])';
|
||||
const composePlugins = tsquery<ts.CallExpression>(
|
||||
sourceFile,
|
||||
composePluginsSelector
|
||||
)[0];
|
||||
|
||||
if (composePlugins) {
|
||||
return `The webpack config (${webpackConfigPath}) can only work with the "@nx/webpack:webpack" executor. Run the "@nx/webpack:convert-config-to-webpack-plugin" generator first.`;
|
||||
}
|
||||
|
||||
const nxAppWebpackPluginSelector =
|
||||
'PropertyAssignment:has(Identifier[name=plugins]) NewExpression:has(Identifier[name=NxAppWebpackPlugin])';
|
||||
const nxAppWebpackPlugin = tsquery<ts.NewExpression>(
|
||||
sourceFile,
|
||||
nxAppWebpackPluginSelector
|
||||
)[0];
|
||||
|
||||
if (!nxAppWebpackPlugin) {
|
||||
return `No "NxAppWebpackPlugin" found in the webpack config (${webpackConfigPath}). Its usage is required for the migration to work.`;
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
}
|
||||
@ -0,0 +1,19 @@
|
||||
{
|
||||
"$schema": "https://json-schema.org/schema",
|
||||
"$id": "NxWebpackConvertToInferred",
|
||||
"description": "Convert existing Webpack project(s) using `@nx/webpack:wepack` executor to use `@nx/webpack/plugin`.",
|
||||
"title": "Convert a Webpack project from executor to plugin",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"project": {
|
||||
"type": "string",
|
||||
"description": "The project to convert from using the `@nx/webpack:webpack` executor to use `@nx/webpack/plugin`. If not provided, all projects using the `@nx/webpack:webpack` executor will be converted.",
|
||||
"x-priority": "important"
|
||||
},
|
||||
"skipFormat": {
|
||||
"type": "boolean",
|
||||
"description": "Whether to format files.",
|
||||
"default": false
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,59 @@
|
||||
import * as ts from 'typescript';
|
||||
|
||||
export function toPropertyAssignment(
|
||||
key: string,
|
||||
value: unknown
|
||||
): ts.PropertyAssignment {
|
||||
if (typeof value === 'string') {
|
||||
return ts.factory.createPropertyAssignment(
|
||||
ts.factory.createStringLiteral(key),
|
||||
ts.factory.createStringLiteral(value)
|
||||
);
|
||||
} else if (typeof value === 'number') {
|
||||
return ts.factory.createPropertyAssignment(
|
||||
ts.factory.createStringLiteral(key),
|
||||
ts.factory.createNumericLiteral(value)
|
||||
);
|
||||
} else if (typeof value === 'boolean') {
|
||||
return ts.factory.createPropertyAssignment(
|
||||
ts.factory.createStringLiteral(key),
|
||||
value ? ts.factory.createTrue() : ts.factory.createFalse()
|
||||
);
|
||||
} else if (Array.isArray(value)) {
|
||||
return ts.factory.createPropertyAssignment(
|
||||
ts.factory.createStringLiteral(key),
|
||||
ts.factory.createArrayLiteralExpression(
|
||||
value.map((item) => toExpression(item))
|
||||
)
|
||||
);
|
||||
} else {
|
||||
return ts.factory.createPropertyAssignment(
|
||||
ts.factory.createStringLiteral(key),
|
||||
ts.factory.createObjectLiteralExpression(
|
||||
Object.entries(value).map(([key, value]) =>
|
||||
toPropertyAssignment(key, value)
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export function toExpression(value: unknown): ts.Expression {
|
||||
if (typeof value === 'string') {
|
||||
return ts.factory.createStringLiteral(value);
|
||||
} else if (typeof value === 'number') {
|
||||
return ts.factory.createNumericLiteral(value);
|
||||
} else if (typeof value === 'boolean') {
|
||||
return value ? ts.factory.createTrue() : ts.factory.createFalse();
|
||||
} else if (Array.isArray(value)) {
|
||||
return ts.factory.createArrayLiteralExpression(
|
||||
value.map((item) => toExpression(item))
|
||||
);
|
||||
} else {
|
||||
return ts.factory.createObjectLiteralExpression(
|
||||
Object.entries(value).map(([key, value]) =>
|
||||
toPropertyAssignment(key, value)
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,416 @@
|
||||
import type { TargetConfiguration, Tree } from '@nx/devkit';
|
||||
import {
|
||||
processTargetOutputs,
|
||||
toProjectRelativePath,
|
||||
} from '@nx/devkit/src/generators/plugin-migrations/plugin-migration-utils';
|
||||
import { tsquery } from '@phenomnomnominal/tsquery';
|
||||
import * as ts from 'typescript';
|
||||
import type { WebpackExecutorOptions } from '../../../executors/webpack/schema';
|
||||
import type { NxAppWebpackPluginOptions } from '../../../plugins/nx-webpack-plugin/nx-app-webpack-plugin-options';
|
||||
import { toPropertyAssignment } from './ast';
|
||||
import type { MigrationContext, TransformerContext } from './types';
|
||||
|
||||
export function buildPostTargetTransformerFactory(
|
||||
migrationContext: MigrationContext
|
||||
) {
|
||||
return function buildPostTargetTransformer(
|
||||
target: TargetConfiguration,
|
||||
tree: Tree,
|
||||
projectDetails: { projectName: string; root: string },
|
||||
inferredTarget: TargetConfiguration
|
||||
): TargetConfiguration {
|
||||
const context: TransformerContext = {
|
||||
...migrationContext,
|
||||
projectName: projectDetails.projectName,
|
||||
projectRoot: projectDetails.root,
|
||||
};
|
||||
|
||||
const { pluginOptions, webpackConfigPath } = processOptions(
|
||||
target,
|
||||
context
|
||||
);
|
||||
|
||||
updateWebpackConfig(tree, webpackConfigPath, pluginOptions);
|
||||
|
||||
if (target.outputs) {
|
||||
processTargetOutputs(target, [], inferredTarget, {
|
||||
projectName: projectDetails.projectName,
|
||||
projectRoot: projectDetails.root,
|
||||
});
|
||||
}
|
||||
|
||||
return target;
|
||||
};
|
||||
}
|
||||
|
||||
type ExtractedOptions = {
|
||||
default: NxAppWebpackPluginOptions;
|
||||
[configName: string]: NxAppWebpackPluginOptions;
|
||||
};
|
||||
function processOptions(
|
||||
target: TargetConfiguration<WebpackExecutorOptions>,
|
||||
context: TransformerContext
|
||||
): {
|
||||
pluginOptions: ExtractedOptions;
|
||||
webpackConfigPath: string;
|
||||
} {
|
||||
const webpackConfigPath = target.options.webpackConfig;
|
||||
delete target.options.webpackConfig;
|
||||
|
||||
const pluginOptions: ExtractedOptions = {
|
||||
default: extractPluginOptions(target.options, context),
|
||||
};
|
||||
|
||||
if (target.configurations && Object.keys(target.configurations).length) {
|
||||
for (const [configName, config] of Object.entries(target.configurations)) {
|
||||
pluginOptions[configName] = extractPluginOptions(
|
||||
config,
|
||||
context,
|
||||
configName
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return { pluginOptions, webpackConfigPath };
|
||||
}
|
||||
|
||||
const pathOptions = new Set([
|
||||
'babelConfig',
|
||||
'index',
|
||||
'main',
|
||||
'outputPath',
|
||||
'polyfills',
|
||||
'postcssConfig',
|
||||
'tsConfig',
|
||||
]);
|
||||
const assetsOptions = new Set(['assets', 'scripts', 'styles']);
|
||||
function extractPluginOptions(
|
||||
options: WebpackExecutorOptions,
|
||||
context: TransformerContext,
|
||||
configName?: string
|
||||
): NxAppWebpackPluginOptions {
|
||||
const pluginOptions: NxAppWebpackPluginOptions = {};
|
||||
|
||||
for (const [key, value] of Object.entries(options)) {
|
||||
if (pathOptions.has(key)) {
|
||||
pluginOptions[key] = toProjectRelativePath(value, context.projectRoot);
|
||||
delete options[key];
|
||||
} else if (assetsOptions.has(key)) {
|
||||
pluginOptions[key] = value.map((asset: string | { input: string }) => {
|
||||
if (typeof asset === 'string') {
|
||||
return toProjectRelativePath(asset, context.projectRoot);
|
||||
}
|
||||
|
||||
asset.input = toProjectRelativePath(asset.input, context.projectRoot);
|
||||
return asset;
|
||||
});
|
||||
delete options[key];
|
||||
} else if (key === 'fileReplacements') {
|
||||
pluginOptions.fileReplacements = value.map(
|
||||
(replacement: { replace: string; with: string }) => ({
|
||||
replace: toProjectRelativePath(
|
||||
replacement.replace,
|
||||
context.projectRoot
|
||||
),
|
||||
with: toProjectRelativePath(replacement.with, context.projectRoot),
|
||||
})
|
||||
);
|
||||
delete options.fileReplacements;
|
||||
} else if (key === 'additionalEntryPoints') {
|
||||
pluginOptions.additionalEntryPoints = value.map((entryPoint) => {
|
||||
entryPoint.entryPath = toProjectRelativePath(
|
||||
entryPoint.entryPath,
|
||||
context.projectRoot
|
||||
);
|
||||
return entryPoint;
|
||||
});
|
||||
delete options.additionalEntryPoints;
|
||||
} else if (key === 'memoryLimit') {
|
||||
pluginOptions.memoryLimit = value;
|
||||
const serveMemoryLimit = getMemoryLimitFromServeTarget(
|
||||
context,
|
||||
configName
|
||||
);
|
||||
if (serveMemoryLimit) {
|
||||
pluginOptions.memoryLimit = Math.max(serveMemoryLimit, value);
|
||||
context.logger.addLog({
|
||||
executorName: '@nx/webpack:webpack',
|
||||
log: `The "memoryLimit" option was set in both the serve and build configurations. The migration set the higher value to the build configuration and removed the option from the serve configuration.`,
|
||||
project: context.projectName,
|
||||
});
|
||||
}
|
||||
delete options.memoryLimit;
|
||||
} else if (key === 'isolatedConfig') {
|
||||
context.logger.addLog({
|
||||
executorName: '@nx/webpack:webpack',
|
||||
log: `The 'isolatedConfig' option is deprecated and not supported by the NxAppWebpackPlugin. It was removed from your project configuration.`,
|
||||
project: context.projectName,
|
||||
});
|
||||
delete options.isolatedConfig;
|
||||
} else if (key === 'standardWebpackConfigFunction') {
|
||||
delete options.standardWebpackConfigFunction;
|
||||
} else {
|
||||
pluginOptions[key] = value;
|
||||
delete options[key];
|
||||
}
|
||||
}
|
||||
|
||||
return pluginOptions;
|
||||
}
|
||||
|
||||
function updateWebpackConfig(
|
||||
tree: Tree,
|
||||
webpackConfig: string,
|
||||
pluginOptions: ExtractedOptions
|
||||
): void {
|
||||
let sourceFile: ts.SourceFile;
|
||||
let webpackConfigText: string;
|
||||
|
||||
const updateSources = () => {
|
||||
webpackConfigText = tree.read(webpackConfig, 'utf-8');
|
||||
sourceFile = tsquery.ast(webpackConfigText);
|
||||
};
|
||||
updateSources();
|
||||
|
||||
setOptionsInWebpackConfig(
|
||||
tree,
|
||||
webpackConfigText,
|
||||
sourceFile,
|
||||
webpackConfig,
|
||||
pluginOptions
|
||||
);
|
||||
updateSources();
|
||||
|
||||
setOptionsInNxWebpackPlugin(
|
||||
tree,
|
||||
webpackConfigText,
|
||||
sourceFile,
|
||||
webpackConfig
|
||||
);
|
||||
updateSources();
|
||||
|
||||
setOptionsInLegacyNxPlugin(
|
||||
tree,
|
||||
webpackConfigText,
|
||||
sourceFile,
|
||||
webpackConfig
|
||||
);
|
||||
}
|
||||
|
||||
function setOptionsInWebpackConfig(
|
||||
tree: Tree,
|
||||
text: string,
|
||||
sourceFile: ts.SourceFile,
|
||||
webpackConfig: string,
|
||||
pluginOptions: ExtractedOptions
|
||||
): void {
|
||||
const { default: defaultOptions, ...configurationOptions } = pluginOptions;
|
||||
|
||||
const optionsSelector =
|
||||
'VariableStatement:has(VariableDeclaration:has(Identifier[name=options]))';
|
||||
const optionsVariable = tsquery<ts.VariableStatement>(
|
||||
sourceFile,
|
||||
optionsSelector
|
||||
)[0];
|
||||
|
||||
// This is assuming the `options` variable will be available since it's what the
|
||||
// `convert-config-to-webpack-plugin` generates
|
||||
|
||||
let defaultOptionsObject: ts.ObjectLiteralExpression;
|
||||
const optionsObject = tsquery<ts.ObjectLiteralExpression>(
|
||||
optionsVariable,
|
||||
'ObjectLiteralExpression'
|
||||
)[0];
|
||||
if (optionsObject.properties.length === 0) {
|
||||
defaultOptionsObject = ts.factory.createObjectLiteralExpression(
|
||||
Object.entries(defaultOptions).map(([key, value]) =>
|
||||
toPropertyAssignment(key, value)
|
||||
)
|
||||
);
|
||||
} else {
|
||||
// filter out the default options that are already in the options object
|
||||
// the existing options override the options from the project.json file
|
||||
const filteredDefaultOptions = Object.entries(defaultOptions).filter(
|
||||
([key]) =>
|
||||
!optionsObject.properties.some(
|
||||
(property) =>
|
||||
ts.isPropertyAssignment(property) &&
|
||||
ts.isIdentifier(property.name) &&
|
||||
property.name.text === key
|
||||
)
|
||||
);
|
||||
defaultOptionsObject = ts.factory.createObjectLiteralExpression([
|
||||
...optionsObject.properties,
|
||||
...filteredDefaultOptions.map(([key, value]) =>
|
||||
toPropertyAssignment(key, value)
|
||||
),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* const configValues = {
|
||||
* build: {
|
||||
* default: { ... },
|
||||
* configuration1: { ... },
|
||||
* configuration2: { ... },
|
||||
* }
|
||||
*/
|
||||
const configValuesVariable = ts.factory.createVariableStatement(
|
||||
undefined,
|
||||
ts.factory.createVariableDeclarationList(
|
||||
[
|
||||
ts.factory.createVariableDeclaration(
|
||||
'configValues',
|
||||
undefined,
|
||||
undefined,
|
||||
ts.factory.createObjectLiteralExpression(
|
||||
[
|
||||
ts.factory.createPropertyAssignment(
|
||||
'build',
|
||||
ts.factory.createObjectLiteralExpression([
|
||||
ts.factory.createPropertyAssignment(
|
||||
'default',
|
||||
defaultOptionsObject
|
||||
),
|
||||
...(configurationOptions
|
||||
? Object.entries(configurationOptions).map(([key, value]) =>
|
||||
ts.factory.createPropertyAssignment(
|
||||
key,
|
||||
ts.factory.createObjectLiteralExpression(
|
||||
Object.entries(value).map(([key, value]) =>
|
||||
toPropertyAssignment(key, value)
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
: []),
|
||||
])
|
||||
),
|
||||
],
|
||||
true
|
||||
)
|
||||
),
|
||||
],
|
||||
ts.NodeFlags.Const
|
||||
)
|
||||
);
|
||||
|
||||
text = `${text.slice(0, optionsVariable.getStart())}
|
||||
// These options were migrated by @nx/webpack:convert-to-inferred from
|
||||
// the project.json file and merged with the options in this file
|
||||
${ts
|
||||
.createPrinter()
|
||||
.printNode(ts.EmitHint.Unspecified, configValuesVariable, sourceFile)}
|
||||
|
||||
// Determine the correct configValue to use based on the configuration
|
||||
const configuration = process.env.NX_TASK_TARGET_CONFIGURATION || 'default';
|
||||
|
||||
const buildOptions = {
|
||||
...configValues.build.default,
|
||||
...configValues.build[configuration],
|
||||
};${text.slice(optionsVariable.getEnd())}`;
|
||||
|
||||
// These are comments written by the `convert-config-to-webpack-plugin` that are no longer needed
|
||||
text = text
|
||||
.replace(
|
||||
`// This file was migrated using @nx/webpack:convert-config-to-webpack-plugin from your './webpack.config.old.js'`,
|
||||
''
|
||||
)
|
||||
.replace(
|
||||
'// Please check that the options here are correct as they were moved from the old webpack.config.js to this file.',
|
||||
''
|
||||
);
|
||||
|
||||
tree.write(webpackConfig, text);
|
||||
}
|
||||
|
||||
function setOptionsInNxWebpackPlugin(
|
||||
tree: Tree,
|
||||
text: string,
|
||||
sourceFile: ts.SourceFile,
|
||||
webpackConfig: string
|
||||
): void {
|
||||
const nxAppWebpackPluginSelector =
|
||||
'PropertyAssignment:has(Identifier[name=plugins]) NewExpression:has(Identifier[name=NxAppWebpackPlugin])';
|
||||
const nxAppWebpackPlugin = tsquery<ts.NewExpression>(
|
||||
sourceFile,
|
||||
nxAppWebpackPluginSelector
|
||||
)[0];
|
||||
|
||||
// the NxAppWebpackPlugin must exists, otherwise, the migration doesn't run and we wouldn't reach this point
|
||||
const updatedNxAppWebpackPlugin = ts.factory.updateNewExpression(
|
||||
nxAppWebpackPlugin,
|
||||
nxAppWebpackPlugin.expression,
|
||||
undefined,
|
||||
[ts.factory.createIdentifier('buildOptions')]
|
||||
);
|
||||
|
||||
text = `${text.slice(0, nxAppWebpackPlugin.getStart())}${ts
|
||||
.createPrinter()
|
||||
.printNode(
|
||||
ts.EmitHint.Unspecified,
|
||||
updatedNxAppWebpackPlugin,
|
||||
sourceFile
|
||||
)}${text.slice(nxAppWebpackPlugin.getEnd())}`;
|
||||
|
||||
tree.write(webpackConfig, text);
|
||||
}
|
||||
|
||||
function setOptionsInLegacyNxPlugin(
|
||||
tree: Tree,
|
||||
text: string,
|
||||
sourceFile: ts.SourceFile,
|
||||
webpackConfig: string
|
||||
): void {
|
||||
const legacyNxPluginSelector =
|
||||
'AwaitExpression CallExpression:has(Identifier[name=useLegacyNxPlugin])';
|
||||
const legacyNxPlugin = tsquery<ts.CallExpression>(
|
||||
sourceFile,
|
||||
legacyNxPluginSelector
|
||||
)[0];
|
||||
|
||||
// we're assuming the `useLegacyNxPlugin` function is being called since it's what the `convert-config-to-webpack-plugin` generates
|
||||
// we've already "ensured" that the `convert-config-to-webpack-plugin` was run by checking for the `NxAppWebpackPlugin` in the project validation
|
||||
const updatedLegacyNxPlugin = ts.factory.updateCallExpression(
|
||||
legacyNxPlugin,
|
||||
legacyNxPlugin.expression,
|
||||
undefined,
|
||||
[legacyNxPlugin.arguments[0], ts.factory.createIdentifier('buildOptions')]
|
||||
);
|
||||
|
||||
text = `${text.slice(0, legacyNxPlugin.getStart())}${ts
|
||||
.createPrinter()
|
||||
.printNode(
|
||||
ts.EmitHint.Unspecified,
|
||||
updatedLegacyNxPlugin,
|
||||
sourceFile
|
||||
)}${text.slice(legacyNxPlugin.getEnd())}`;
|
||||
|
||||
tree.write(webpackConfig, text);
|
||||
}
|
||||
|
||||
function getMemoryLimitFromServeTarget(
|
||||
context: TransformerContext,
|
||||
configName: string | undefined
|
||||
): number | undefined {
|
||||
const { targets } = context.projectGraph.nodes[context.projectName].data;
|
||||
|
||||
const serveTarget = Object.values(targets).find(
|
||||
(target) =>
|
||||
target.executor === '@nx/webpack:dev-server' ||
|
||||
target.executor === '@nrwl/web:dev-server'
|
||||
);
|
||||
|
||||
if (!serveTarget) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (configName && serveTarget.configurations?.[configName]) {
|
||||
return (
|
||||
serveTarget.configurations[configName].options?.memoryLimit ??
|
||||
serveTarget.options?.memoryLimit
|
||||
);
|
||||
}
|
||||
|
||||
return serveTarget.options?.memoryLimit;
|
||||
}
|
||||
@ -0,0 +1,3 @@
|
||||
export * from './build-post-target-transformer';
|
||||
export * from './serve-post-target-transformer';
|
||||
export * from './types';
|
||||
@ -0,0 +1,414 @@
|
||||
import {
|
||||
parseTargetString,
|
||||
readJson,
|
||||
readTargetOptions,
|
||||
type ExecutorContext,
|
||||
type ProjectsConfigurations,
|
||||
type TargetConfiguration,
|
||||
type Tree,
|
||||
} from '@nx/devkit';
|
||||
import {
|
||||
processTargetOutputs,
|
||||
toProjectRelativePath,
|
||||
} from '@nx/devkit/src/generators/plugin-migrations/plugin-migration-utils';
|
||||
import { tsquery } from '@phenomnomnominal/tsquery';
|
||||
import { basename, resolve } from 'path';
|
||||
import * as ts from 'typescript';
|
||||
import type { WebpackOptionsNormalized } from 'webpack';
|
||||
import { buildServePath } from '../../../executors/dev-server/lib/serve-path';
|
||||
import type { WebDevServerOptions as DevServerExecutorOptions } from '../../../executors/dev-server/schema';
|
||||
import { toPropertyAssignment } from './ast';
|
||||
import type { MigrationContext, TransformerContext } from './types';
|
||||
|
||||
export function servePostTargetTransformerFactory(
|
||||
migrationContext: MigrationContext
|
||||
) {
|
||||
return async function servePostTargetTransformer(
|
||||
target: TargetConfiguration,
|
||||
tree: Tree,
|
||||
projectDetails: { projectName: string; root: string },
|
||||
inferredTarget: TargetConfiguration
|
||||
): Promise<TargetConfiguration> {
|
||||
const context: TransformerContext = {
|
||||
...migrationContext,
|
||||
projectName: projectDetails.projectName,
|
||||
projectRoot: projectDetails.root,
|
||||
};
|
||||
|
||||
const { devServerOptions, webpackConfigPath } = await processOptions(
|
||||
tree,
|
||||
target,
|
||||
context
|
||||
);
|
||||
|
||||
updateWebpackConfig(tree, webpackConfigPath, devServerOptions, context);
|
||||
|
||||
if (target.outputs) {
|
||||
processTargetOutputs(target, [], inferredTarget, {
|
||||
projectName: projectDetails.projectName,
|
||||
projectRoot: projectDetails.root,
|
||||
});
|
||||
}
|
||||
|
||||
return target;
|
||||
};
|
||||
}
|
||||
|
||||
type WebpackConfigDevServerOptions = WebpackOptionsNormalized['devServer'];
|
||||
type ExtractedOptions = {
|
||||
default: WebpackConfigDevServerOptions;
|
||||
[configName: string]: WebpackConfigDevServerOptions;
|
||||
};
|
||||
|
||||
async function processOptions(
|
||||
tree: Tree,
|
||||
target: TargetConfiguration<DevServerExecutorOptions>,
|
||||
context: TransformerContext
|
||||
): Promise<{
|
||||
devServerOptions: ExtractedOptions;
|
||||
webpackConfigPath: string;
|
||||
}> {
|
||||
const executorContext = {
|
||||
cwd: process.cwd(),
|
||||
nxJsonConfiguration: readJson(tree, 'nx.json'),
|
||||
projectGraph: context.projectGraph,
|
||||
projectName: context.projectName,
|
||||
projectsConfigurations: Object.entries(context.projectGraph.nodes).reduce(
|
||||
(acc, [projectName, project]) => {
|
||||
acc.projects[projectName] = project.data;
|
||||
return acc;
|
||||
},
|
||||
{ version: 1, projects: {} } as ProjectsConfigurations
|
||||
),
|
||||
root: context.workspaceRoot,
|
||||
} as ExecutorContext;
|
||||
const buildTarget = parseTargetString(
|
||||
target.options.buildTarget,
|
||||
executorContext
|
||||
);
|
||||
const buildOptions = readTargetOptions(buildTarget, executorContext);
|
||||
|
||||
// it must exist, we validated it in the project filter
|
||||
const webpackConfigPath = buildOptions.webpackConfig;
|
||||
|
||||
const defaultOptions = extractDevServerOptions(target.options, context);
|
||||
applyDefaults(defaultOptions, buildOptions);
|
||||
const devServerOptions: ExtractedOptions = {
|
||||
default: defaultOptions,
|
||||
};
|
||||
|
||||
if (target.configurations && Object.keys(target.configurations).length) {
|
||||
for (const [configName, config] of Object.entries(target.configurations)) {
|
||||
devServerOptions[configName] = extractDevServerOptions(config, context);
|
||||
}
|
||||
}
|
||||
|
||||
return { devServerOptions, webpackConfigPath };
|
||||
}
|
||||
|
||||
function extractDevServerOptions(
|
||||
options: DevServerExecutorOptions,
|
||||
context: TransformerContext
|
||||
): WebpackConfigDevServerOptions {
|
||||
const devServerOptions: WebpackConfigDevServerOptions = {};
|
||||
|
||||
for (const [key, value] of Object.entries(options)) {
|
||||
if (key === 'hmr') {
|
||||
devServerOptions.hot = value;
|
||||
|
||||
if (value) {
|
||||
// the executor disables liveReload when hmr is enabled
|
||||
devServerOptions.liveReload = false;
|
||||
delete options.liveReload;
|
||||
}
|
||||
|
||||
delete options.hmr;
|
||||
} else if (key === 'allowedHosts') {
|
||||
devServerOptions.allowedHosts = value.split(',');
|
||||
delete options.allowedHosts;
|
||||
} else if (key === 'publicHost') {
|
||||
devServerOptions.client = {
|
||||
webSocketURL: value,
|
||||
};
|
||||
delete options.publicHost;
|
||||
} else if (key === 'proxyConfig') {
|
||||
devServerOptions.proxy = getProxyConfig(context.workspaceRoot, value);
|
||||
delete options.proxyConfig;
|
||||
} else if (key === 'ssl' || key === 'sslCert' || key === 'sslKey') {
|
||||
if (key === 'ssl' || 'ssl' in options) {
|
||||
if (options.ssl) {
|
||||
devServerOptions.server = { type: 'https' };
|
||||
|
||||
if (options.sslCert && options.sslKey) {
|
||||
devServerOptions.server.options = {};
|
||||
devServerOptions.server.options.cert = toProjectRelativePath(
|
||||
options.sslCert,
|
||||
context.projectRoot
|
||||
);
|
||||
devServerOptions.server.options.key = toProjectRelativePath(
|
||||
options.sslKey,
|
||||
context.projectRoot
|
||||
);
|
||||
} else if (options.sslCert) {
|
||||
context.logger.addLog({
|
||||
executorName: '@nx/webpack:dev-server',
|
||||
log: 'The "sslCert" option was set but "sslKey" was missing and "ssl" was set to "true". This means that "sslCert" was ignored by the executor. It has been removed from the options.',
|
||||
project: context.projectName,
|
||||
});
|
||||
} else if (options.sslKey) {
|
||||
context.logger.addLog({
|
||||
executorName: '@nx/webpack:dev-server',
|
||||
log: 'The "sslKey" option was set but "sslCert" was missing and "ssl" was set to "true". This means that "sslKey" was ignored by the executor. It has been removed from the options.',
|
||||
project: context.projectName,
|
||||
});
|
||||
}
|
||||
} else if (options.sslCert || options.sslKey) {
|
||||
context.logger.addLog({
|
||||
executorName: '@nx/webpack:dev-server',
|
||||
log: 'The "sslCert" and/or "sslKey" were set with "ssl" set to "false". This means they were ignored by the executor. They have been removed from the options.',
|
||||
project: context.projectName,
|
||||
});
|
||||
}
|
||||
delete options.ssl;
|
||||
delete options.sslCert;
|
||||
delete options.sslKey;
|
||||
} else if (options.sslCert || options.sslKey) {
|
||||
context.logger.addLog({
|
||||
executorName: '@nx/webpack:dev-server',
|
||||
log: 'The "sslCert" and/or "sslKey" were set but the "ssl" was not set. This means they were ignored by the executor. They have been removed from the options.',
|
||||
project: context.projectName,
|
||||
});
|
||||
delete options.sslCert;
|
||||
delete options.sslKey;
|
||||
}
|
||||
} else if (key === 'buildTarget') {
|
||||
delete options.buildTarget;
|
||||
} else if (key === 'watch') {
|
||||
context.logger.addLog({
|
||||
executorName: '@nx/webpack:dev-server',
|
||||
log: 'The "watch" option was removed from the serve configuration since it is not needed. The dev server always watches the files.',
|
||||
project: context.projectName,
|
||||
});
|
||||
delete options.watch;
|
||||
} else if (key === 'baseHref') {
|
||||
context.logger.addLog({
|
||||
executorName: '@nx/webpack:dev-server',
|
||||
log: 'The "baseHref" option was removed from the serve configuration. If you need different base hrefs for the build and the dev server, please update the final webpack config manually to achieve that.',
|
||||
project: context.projectName,
|
||||
});
|
||||
delete options.baseHref;
|
||||
} else if (key === 'memoryLimit') {
|
||||
// we already log a message for this one when processing the build options
|
||||
delete options.memoryLimit;
|
||||
} else {
|
||||
devServerOptions[key] = value;
|
||||
delete options[key];
|
||||
}
|
||||
}
|
||||
|
||||
return devServerOptions;
|
||||
}
|
||||
|
||||
function applyDefaults(
|
||||
options: WebpackConfigDevServerOptions,
|
||||
buildOptions: any
|
||||
) {
|
||||
if (options.port === undefined) {
|
||||
options.port = 4200;
|
||||
}
|
||||
|
||||
options.headers = { 'Access-Control-Allow-Origin': '*' };
|
||||
|
||||
const servePath = buildServePath(buildOptions);
|
||||
options.historyApiFallback = {
|
||||
index: buildOptions.index && `${servePath}${basename(buildOptions.index)}`,
|
||||
disableDotRule: true,
|
||||
htmlAcceptHeaders: ['text/html', 'application/xhtml+xml'],
|
||||
};
|
||||
}
|
||||
|
||||
function getProxyConfig(root: string, proxyConfig: string) {
|
||||
const proxyPath = resolve(root, proxyConfig);
|
||||
return require(proxyPath);
|
||||
}
|
||||
|
||||
function updateWebpackConfig(
|
||||
tree: Tree,
|
||||
webpackConfigPath: string,
|
||||
devServerOptions: ExtractedOptions,
|
||||
context: TransformerContext
|
||||
): void {
|
||||
let sourceFile: ts.SourceFile;
|
||||
let webpackConfigText: string;
|
||||
|
||||
const updateSources = () => {
|
||||
webpackConfigText = tree.read(webpackConfigPath, 'utf-8');
|
||||
sourceFile = tsquery.ast(webpackConfigText);
|
||||
};
|
||||
updateSources();
|
||||
|
||||
setOptionsInWebpackConfig(
|
||||
tree,
|
||||
webpackConfigText,
|
||||
sourceFile,
|
||||
webpackConfigPath,
|
||||
devServerOptions
|
||||
);
|
||||
updateSources();
|
||||
|
||||
setDevServerOptionsInWebpackConfig(
|
||||
tree,
|
||||
webpackConfigText,
|
||||
sourceFile,
|
||||
webpackConfigPath,
|
||||
context
|
||||
);
|
||||
}
|
||||
|
||||
function setOptionsInWebpackConfig(
|
||||
tree: Tree,
|
||||
text: string,
|
||||
sourceFile: ts.SourceFile,
|
||||
webpackConfigPath: string,
|
||||
devServerOptions: ExtractedOptions
|
||||
) {
|
||||
const { default: defaultOptions, ...configurationOptions } = devServerOptions;
|
||||
|
||||
const configValuesSelector =
|
||||
'VariableDeclaration:has(Identifier[name=configValues]) ObjectLiteralExpression';
|
||||
const configValuesObject = tsquery<ts.ObjectLiteralExpression>(
|
||||
sourceFile,
|
||||
configValuesSelector
|
||||
)[0];
|
||||
|
||||
// configValues must exist at this point, we added it when processing the build target
|
||||
|
||||
/**
|
||||
* const configValues = {
|
||||
* ...
|
||||
* serve: {
|
||||
* default: { ... },
|
||||
* configuration1: { ... },
|
||||
* ...
|
||||
* },
|
||||
*/
|
||||
const updatedConfigValuesObject = ts.factory.updateObjectLiteralExpression(
|
||||
configValuesObject,
|
||||
[
|
||||
...configValuesObject.properties,
|
||||
ts.factory.createPropertyAssignment(
|
||||
'serve',
|
||||
ts.factory.createObjectLiteralExpression([
|
||||
ts.factory.createPropertyAssignment(
|
||||
'default',
|
||||
ts.factory.createObjectLiteralExpression(
|
||||
Object.entries(defaultOptions).map(([key, value]) =>
|
||||
toPropertyAssignment(key, value)
|
||||
)
|
||||
)
|
||||
),
|
||||
...(configurationOptions
|
||||
? Object.entries(configurationOptions).map(([key, value]) =>
|
||||
ts.factory.createPropertyAssignment(
|
||||
key,
|
||||
ts.factory.createObjectLiteralExpression(
|
||||
Object.entries(value).map(([key, value]) =>
|
||||
toPropertyAssignment(key, value)
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
: []),
|
||||
])
|
||||
),
|
||||
]
|
||||
);
|
||||
|
||||
text = `${text.slice(0, configValuesObject.getStart())}${ts
|
||||
.createPrinter()
|
||||
.printNode(
|
||||
ts.EmitHint.Unspecified,
|
||||
updatedConfigValuesObject,
|
||||
sourceFile
|
||||
)}${text.slice(configValuesObject.getEnd())}`;
|
||||
|
||||
tree.write(webpackConfigPath, text);
|
||||
|
||||
sourceFile = tsquery.ast(text);
|
||||
const buildOptionsSelector =
|
||||
'VariableStatement:has(VariableDeclaration:has(Identifier[name=buildOptions]))';
|
||||
const buildOptionsStatement = tsquery<ts.VariableStatement>(
|
||||
sourceFile,
|
||||
buildOptionsSelector
|
||||
)[0];
|
||||
|
||||
text = `${text.slice(0, buildOptionsStatement.getEnd())}
|
||||
const devServerOptions = {
|
||||
...configValues.serve.default,
|
||||
...configValues.serve[configuration],
|
||||
};${text.slice(buildOptionsStatement.getEnd())}`;
|
||||
|
||||
tree.write(webpackConfigPath, text);
|
||||
}
|
||||
|
||||
function setDevServerOptionsInWebpackConfig(
|
||||
tree: Tree,
|
||||
text: string,
|
||||
sourceFile: ts.SourceFile,
|
||||
webpackConfigPath: string,
|
||||
context: TransformerContext
|
||||
) {
|
||||
const webpackConfigDevServerSelector =
|
||||
'ObjectLiteralExpression > PropertyAssignment:has(Identifier[name=devServer])';
|
||||
const webpackConfigDevServer = tsquery<ts.PropertyAssignment>(
|
||||
sourceFile,
|
||||
webpackConfigDevServerSelector
|
||||
)[0];
|
||||
if (webpackConfigDevServer) {
|
||||
context.logger.addLog({
|
||||
executorName: '@nx/webpack:dev-server',
|
||||
log: `The "devServer" option is already set in the webpack config. The migration doesn't support updating it. Please review it and make any necessary changes manually.`,
|
||||
project: context.projectName,
|
||||
});
|
||||
|
||||
text = `${text.slice(
|
||||
0,
|
||||
webpackConfigDevServer.getStart()
|
||||
)}// This is the untouched "devServer" option from the original webpack config. Please review it and make any necessary changes manually.
|
||||
${text.slice(webpackConfigDevServer.getStart())}`;
|
||||
|
||||
tree.write(webpackConfigPath, text);
|
||||
|
||||
// If the devServer property already exists, we don't know how to merge the
|
||||
// options, so we leave it as is.
|
||||
return;
|
||||
}
|
||||
|
||||
const webpackConfigSelector =
|
||||
'ObjectLiteralExpression:has(PropertyAssignment:has(Identifier[name=plugins]))';
|
||||
const webpackConfig = tsquery<ts.ObjectLiteralExpression>(
|
||||
sourceFile,
|
||||
webpackConfigSelector
|
||||
)[0];
|
||||
|
||||
const updatedWebpackConfig = ts.factory.updateObjectLiteralExpression(
|
||||
webpackConfig,
|
||||
[
|
||||
ts.factory.createPropertyAssignment(
|
||||
'devServer',
|
||||
ts.factory.createIdentifier('devServerOptions')
|
||||
),
|
||||
...webpackConfig.properties,
|
||||
]
|
||||
);
|
||||
|
||||
text = `${text.slice(0, webpackConfig.getStart())}${ts
|
||||
.createPrinter()
|
||||
.printNode(
|
||||
ts.EmitHint.Unspecified,
|
||||
updatedWebpackConfig,
|
||||
sourceFile
|
||||
)}${text.slice(webpackConfig.getEnd())}`;
|
||||
|
||||
tree.write(webpackConfigPath, text);
|
||||
}
|
||||
@ -0,0 +1,13 @@
|
||||
import type { ProjectGraph } from '@nx/devkit';
|
||||
import type { AggregatedLog } from '@nx/devkit/src/generators/plugin-migrations/aggregate-log-util';
|
||||
|
||||
export type MigrationContext = {
|
||||
logger: AggregatedLog;
|
||||
projectGraph: ProjectGraph;
|
||||
workspaceRoot: string;
|
||||
};
|
||||
|
||||
export type TransformerContext = MigrationContext & {
|
||||
projectName: string;
|
||||
projectRoot: string;
|
||||
};
|
||||
@ -100,7 +100,11 @@ function applyNxIndependentConfig(
|
||||
path:
|
||||
config.output?.path ??
|
||||
(options.outputPath
|
||||
? path.join(options.root, options.outputPath)
|
||||
? // If path is relative, it is relative from project root (aka cwd).
|
||||
// Otherwise, it is relative to workspace root (legacy behavior).
|
||||
options.outputPath.startsWith('.')
|
||||
? path.join(options.root, options.projectRoot, options.outputPath)
|
||||
: path.join(options.root, options.outputPath)
|
||||
: undefined),
|
||||
filename:
|
||||
config.output?.filename ??
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user