284 lines
9.8 KiB
TypeScript
284 lines
9.8 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 { readNxJson } from 'nx/src/config/configuration';
|
|
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 { assertBuilderPackageIsInstalled } from '../../executors/utilities/builder-package';
|
|
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 } from './schema';
|
|
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/build:application',
|
|
'@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);
|
|
assertBuilderPackageIsInstalled('@angular-devkit/build-angular');
|
|
|
|
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(
|
|
new WebpackNxBuildCoordinationPlugin(
|
|
`nx run-many --target=${
|
|
parsedBuildTarget.target
|
|
} --projects=${workspaceDependencies.join(',')}`,
|
|
{ skipWatchingDeps: !options.watchDependencies }
|
|
)
|
|
);
|
|
}
|
|
}
|
|
|
|
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,
|
|
};
|
|
|
|
// delete extra option not supported by the delegate builder
|
|
delete delegateBuilderOptions.buildLibsFromSource;
|
|
delete delegateBuilderOptions.watchDependencies;
|
|
|
|
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);
|
|
}
|