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:
Leosvel Pérez Espinosa 2024-06-25 03:13:24 +02:00 committed by GitHub
parent a3322f76ef
commit 47dfdcfc6b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
18 changed files with 2594 additions and 14 deletions

View File

@ -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,

View File

@ -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"

View File

@ -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",

View File

@ -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"
}

View File

@ -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)

View File

@ -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(

View File

@ -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();
}

View File

@ -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`."
}
}
}

View File

@ -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

View File

@ -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;
};
}

View File

@ -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
}
}
}

View File

@ -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)
)
);
}
}

View File

@ -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;
}

View File

@ -0,0 +1,3 @@
export * from './build-post-target-transformer';
export * from './serve-post-target-transformer';
export * from './types';

View File

@ -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);
}

View File

@ -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;
};

View File

@ -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 ??