nx/packages/angular/src/builders/dev-server/dev-server.impl.ts
Jason Jean 23bebd91e7
feat(devkit): bump compatibility to Nx 19 - 21.x (#28243)
BREAKING CHANGE

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

* `@nx/devkit` supports Nx 17 - 20.
* Node 18 - 22 is supported 
* `ExecutorContext.projectGraph`, `ExecutorContext.nxJsonConfiguration`,
and `ExecutorContext.projectsConfigurations` is marked as optional
because `ExecutorContext` in some versions of Nx did not have them.
* `ExecutorContext.workspace` is marked as optional because
`ExecutorContext` in some versions of Nx did not have the above
properties which contain the same information.
* `ProjectGraphNode` is deprecated.
* `NxPluginV1.processProjectGraph` was deprecated long ago and there has
been a warning since.
* `appRootPath` has been deprecated for a long time.
* `parseTargetString` had a variant that did not take either the project
graph or the executor context.
* `readNxJson` has a variant which does not take a tree. This was not
clearly deprecated.
* There are handlers to require from `@nx/` instead of `@nrwl`
* Nx tries to get a root install from `@nrwl/cli`


## Expected Behavior
<!-- This is the behavior we should expect with the changes in this PR
-->

* `@nx/devkit` supports Nx 19 - 21.
* Node 20 - 22 is supported 
* `ExecutorContext.projectGraph`, `ExecutorContext.nxJsonConfiguration`,
and `ExecutorContext.projectsConfigurations` is marked as required
because `ExecutorContext` in Nx 19+ is guaranteed to have them.
* `ExecutorContext.workspace` is removed because the same information is
available in the above properties
* `ProjectGraphNode` is removed.
* `NxPluginV1` is no more. All plugins should be `NxPluginV2`.
* `workspaceRoot` is the replacement for `appRootPath`. `appRootPath` is
removed.
* `parseTargetString` no longer has a variant that did not take either
the project graph or the executor context.
* `readNxJson` still has a variant which does not take a tree but it's
clearly deprecated to be removed in Nx 21.
* `@nrwl` packages are no more so we don't have to redirect requires
anymore.
* `@nrwl/cli` is no more so Nx shouldn't try to get a root install there
* 

## Related Issue(s)
<!-- Please link the issue being fixed so it gets closed when this is
merged. -->

Fixes #
2024-10-03 17:35:47 -04:00

294 lines
10 KiB
TypeScript

import type { BuilderContext } from '@angular-devkit/architect';
import type { DevServerBuilderOptions } from '@angular-devkit/build-angular';
import {
joinPathFragments,
normalizePath,
parseTargetString,
readCachedProjectGraph,
readProjectsConfigurationFromProjectGraph,
workspaceRoot,
} from '@nx/devkit';
import { getRootTsConfigPath } from '@nx/js';
import type { DependentBuildableProjectNode } from '@nx/js/src/utils/buildable-libs-utils';
import { WebpackNxBuildCoordinationPlugin } from '@nx/webpack/src/plugins/webpack-nx-build-coordination-plugin';
import { existsSync } from 'fs';
import { isNpmProject } from 'nx/src/project-graph/operators';
import { readCachedProjectConfiguration } from 'nx/src/project-graph/project-graph';
import { relative } from 'path';
import { combineLatest, from } from 'rxjs';
import { switchMap } from 'rxjs/operators';
import { getInstalledAngularVersionInfo } from '../../executors/utilities/angular-version-utils';
import {
loadIndexHtmlTransformer,
loadMiddleware,
loadPlugins,
type PluginSpec,
} from '../../executors/utilities/esbuild-extensions';
import { patchBuilderContext } from '../../executors/utilities/patch-builder-context';
import { createTmpTsConfigForBuildableLibs } from '../utilities/buildable-libs';
import {
mergeCustomWebpackConfig,
resolveIndexHtmlTransformer,
} from '../utilities/webpack';
import { normalizeOptions, validateOptions } from './lib';
import type {
NormalizedSchema,
Schema,
SchemaWithBrowserTarget,
} from './schema';
import { readNxJson } from 'nx/src/config/configuration';
type BuildTargetOptions = {
tsConfig: string;
buildLibsFromSource?: boolean;
customWebpackConfig?: { path?: string };
indexHtmlTransformer?: string;
indexFileTransformer?: string;
plugins?: string[] | PluginSpec[];
esbuildMiddleware?: string[];
};
export function executeDevServerBuilder(
rawOptions: Schema,
context: import('@angular-devkit/architect').BuilderContext
) {
validateOptions(rawOptions);
process.env.NX_TSCONFIG_PATH = getRootTsConfigPath();
const options = normalizeOptions(rawOptions);
const projectGraph = readCachedProjectGraph();
const parsedBuildTarget = parseTargetString(options.buildTarget, {
cwd: context.currentDirectory,
projectGraph,
projectName: context.target.project,
projectsConfigurations:
readProjectsConfigurationFromProjectGraph(projectGraph),
root: context.workspaceRoot,
nxJsonConfiguration: readNxJson(workspaceRoot),
isVerbose: false,
});
const browserTargetProjectConfiguration = readCachedProjectConfiguration(
parsedBuildTarget.project
);
const buildTarget =
browserTargetProjectConfiguration.targets[parsedBuildTarget.target];
const buildTargetOptions: BuildTargetOptions = {
...buildTarget.options,
...(parsedBuildTarget.configuration
? buildTarget.configurations[parsedBuildTarget.configuration]
: buildTarget.defaultConfiguration
? buildTarget.configurations[buildTarget.defaultConfiguration]
: {}),
};
const buildLibsFromSource =
options.buildLibsFromSource ??
buildTargetOptions.buildLibsFromSource ??
true;
process.env.NX_BUILD_LIBS_FROM_SOURCE = `${buildLibsFromSource}`;
process.env.NX_BUILD_TARGET = options.buildTarget;
let pathToWebpackConfig: string;
if (buildTargetOptions.customWebpackConfig?.path) {
pathToWebpackConfig = joinPathFragments(
context.workspaceRoot,
buildTargetOptions.customWebpackConfig.path
);
if (pathToWebpackConfig && !existsSync(pathToWebpackConfig)) {
throw new Error(
`Custom Webpack Config File Not Found!\nTo use a custom webpack config, please ensure the path to the custom webpack file is correct: \n${pathToWebpackConfig}`
);
}
}
const normalizedIndexHtmlTransformer =
buildTargetOptions.indexHtmlTransformer ??
buildTargetOptions.indexFileTransformer;
let pathToIndexFileTransformer: string;
if (normalizedIndexHtmlTransformer) {
pathToIndexFileTransformer = joinPathFragments(
context.workspaceRoot,
normalizedIndexHtmlTransformer
);
if (pathToIndexFileTransformer && !existsSync(pathToIndexFileTransformer)) {
throw new Error(
`File containing Index File Transformer function Not Found!\n Please ensure the path to the file containing the function is correct: \n${pathToIndexFileTransformer}`
);
}
}
let dependencies: DependentBuildableProjectNode[];
if (!buildLibsFromSource) {
const { tsConfigPath, dependencies: foundDependencies } =
createTmpTsConfigForBuildableLibs(buildTargetOptions.tsConfig, context, {
target: parsedBuildTarget.target,
});
dependencies = foundDependencies;
const relativeTsConfigPath = normalizePath(
relative(context.workspaceRoot, tsConfigPath)
);
// We can't just pass the tsconfig path in memory to the angular builder
// function because we can't pass the build target options to it, the build
// targets options will be retrieved by the builder from the project
// configuration. Therefore, we patch the method in the context to retrieve
// the target options to overwrite the tsconfig path to use the generated
// one with the updated path mappings.
const originalGetTargetOptions = context.getTargetOptions;
context.getTargetOptions = async (target) => {
const options = await originalGetTargetOptions(target);
options.tsConfig = relativeTsConfigPath;
return options;
};
// The buildTargetConfiguration also needs to use the generated tsconfig path
// otherwise the build will fail if customWebpack function/file is referencing
// local libs. This synchronize the behavior with webpack-browser and
// webpack-server implementation.
buildTargetOptions.tsConfig = relativeTsConfigPath;
}
const delegateBuilderOptions = getDelegateBuilderOptions(options);
const isUsingWebpackBuilder = ![
'@angular-devkit/build-angular:application',
'@angular-devkit/build-angular:browser-esbuild',
'@nx/angular:application',
'@nx/angular:browser-esbuild',
].includes(buildTarget.executor);
/**
* The Angular CLI dev-server builder make some decisions based on the build
* target builder but it only considers `@angular-devkit/build-angular:*`
* builders. Since we are using a custom builder, we patch the context to
* handle `@nx/angular:*` executors.
*/
patchBuilderContext(context, !isUsingWebpackBuilder, parsedBuildTarget);
return combineLatest([
from(import('@angular-devkit/build-angular')),
from(loadPlugins(buildTargetOptions.plugins, buildTargetOptions.tsConfig)),
from(
loadMiddleware(options.esbuildMiddleware, buildTargetOptions.tsConfig)
),
from(
loadIndexHtmlFileTransformer(
pathToIndexFileTransformer,
buildTargetOptions.tsConfig,
context,
isUsingWebpackBuilder
)
),
]).pipe(
switchMap(
([
{ executeDevServerBuilder },
plugins,
middleware,
indexHtmlTransformer,
]) =>
executeDevServerBuilder(
delegateBuilderOptions,
context,
{
webpackConfiguration: isUsingWebpackBuilder
? async (baseWebpackConfig) => {
if (!buildLibsFromSource) {
const workspaceDependencies = dependencies
.filter((dep) => !isNpmProject(dep.node))
.map((dep) => dep.node.name);
// default for `nx run-many` is --all projects
// by passing an empty string for --projects, run-many will default to
// run the target for all projects.
// This will occur when workspaceDependencies = []
if (workspaceDependencies.length > 0) {
baseWebpackConfig.plugins.push(
// @ts-expect-error - difference between angular and webpack plugin definitions bc of webpack versions
new WebpackNxBuildCoordinationPlugin(
`nx run-many --target=${
parsedBuildTarget.target
} --projects=${workspaceDependencies.join(',')}`
)
);
}
}
if (!pathToWebpackConfig) {
return baseWebpackConfig;
}
return mergeCustomWebpackConfig(
baseWebpackConfig,
pathToWebpackConfig,
buildTargetOptions,
context.target
);
}
: undefined,
...(indexHtmlTransformer
? {
indexHtml: indexHtmlTransformer,
}
: {}),
},
{
buildPlugins: plugins,
middleware,
}
)
)
);
}
export default require('@angular-devkit/architect').createBuilder(
executeDevServerBuilder
) as any;
function getDelegateBuilderOptions(
options: NormalizedSchema
): DevServerBuilderOptions {
const delegateBuilderOptions: NormalizedSchema & DevServerBuilderOptions = {
...options,
};
const { major: angularMajorVersion } = getInstalledAngularVersionInfo();
if (angularMajorVersion <= 17) {
(
delegateBuilderOptions as unknown as SchemaWithBrowserTarget
).browserTarget = delegateBuilderOptions.buildTarget;
delete delegateBuilderOptions.buildTarget;
}
// delete extra option not supported by the delegate builder
delete delegateBuilderOptions.buildLibsFromSource;
return delegateBuilderOptions;
}
async function loadIndexHtmlFileTransformer(
pathToIndexFileTransformer: string | undefined,
tsConfig: string,
context: BuilderContext,
isUsingWebpackBuilder: boolean
) {
if (!pathToIndexFileTransformer) {
return undefined;
}
return isUsingWebpackBuilder
? resolveIndexHtmlTransformer(
pathToIndexFileTransformer,
tsConfig,
context.target
)
: await loadIndexHtmlTransformer(pathToIndexFileTransformer, tsConfig);
}