import { joinPathFragments, normalizePath, ProjectGraph, readCachedProjectGraph, targetToTargetString, } from '@nx/devkit'; import type { DependentBuildableProjectNode } from '@nx/js/internal'; import { WebpackNxBuildCoordinationPlugin } from '@nx/webpack/internal'; import { existsSync } from 'fs'; import { isNpmProject } from 'nx/src/project-graph/operators'; import { getDependencyConfigs } from 'nx/src/tasks-runner/utils'; import { relative } from 'path'; import { from, Observable } from 'rxjs'; import { switchMap } from 'rxjs/operators'; import { assertBuilderPackageIsInstalled } from '../../executors/utilities/builder-package'; import { createTmpTsConfigForBuildableLibs } from '../utilities/buildable-libs'; import { mergeCustomWebpackConfig, resolveIndexHtmlTransformer, } from '../utilities/webpack'; import type { BrowserBuilderSchema } from './schema'; // This is required to ensure that the webpack version used by the Module Federation is the same as the one used by the builders. const Module = require('module'); const originalResolveFilename = Module._resolveFilename; const patchedWebpackPath = require.resolve('webpack', { paths: [require.resolve('@angular-devkit/build-angular')], }); // Override the resolve function Module._resolveFilename = function (request, parent, isMain, options) { // Intercept webpack specifically if (request === 'webpack') { // Force webpack to resolve from your specific path return patchedWebpackPath; } // For all other modules, use the original resolver return originalResolveFilename.call(this, request, parent, isMain, options); }; function shouldSkipInitialTargetRun( projectGraph: ProjectGraph, project: string, target: string ): boolean { const allTargetNames = new Set(); for (const projectName in projectGraph.nodes) { const project = projectGraph.nodes[projectName]; for (const targetName in project.data.targets ?? {}) { allTargetNames.add(targetName); } } const projectDependencyConfigs = getDependencyConfigs( { project, target }, {}, projectGraph, Array.from(allTargetNames) ); // if the task runner already ran the target, skip the initial run return projectDependencyConfigs.some( (d) => d.target === target && d.projects === 'dependencies' ); } export function executeWebpackBrowserBuilder( options: BrowserBuilderSchema, context: import('@angular-devkit/architect').BuilderContext ): Observable { options.buildLibsFromSource ??= true; options.watchDependencies ??= true; const { buildLibsFromSource, customWebpackConfig, indexHtmlTransformer, watchDependencies, ...delegateBuilderOptions } = options; process.env.NX_BUILD_LIBS_FROM_SOURCE = `${buildLibsFromSource}`; process.env.NX_BUILD_TARGET = targetToTargetString({ ...context.target }); const pathToWebpackConfig = customWebpackConfig?.path && joinPathFragments(context.workspaceRoot, 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 pathToIndexFileTransformer = indexHtmlTransformer && joinPathFragments(context.workspaceRoot, indexHtmlTransformer); 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[]; let projectGraph: ProjectGraph; if (!buildLibsFromSource) { projectGraph = readCachedProjectGraph(); const { tsConfigPath, dependencies: foundDependencies } = createTmpTsConfigForBuildableLibs( delegateBuilderOptions.tsConfig, context, { projectGraph } ); dependencies = foundDependencies; delegateBuilderOptions.tsConfig = normalizePath( relative(context.workspaceRoot, tsConfigPath) ); } assertBuilderPackageIsInstalled('@angular-devkit/build-angular'); return from(import('@angular-devkit/build-angular')).pipe( switchMap(({ executeBrowserBuilder }) => executeBrowserBuilder(delegateBuilderOptions, context as any, { webpackConfiguration: (baseWebpackConfig) => { if (!buildLibsFromSource && delegateBuilderOptions.watch) { 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) { const skipInitialRun = shouldSkipInitialTargetRun( projectGraph, context.target.project, context.target.target ); baseWebpackConfig.plugins.push( // Cast away the angular/webpack plugin type difference (webpack versions). new (WebpackNxBuildCoordinationPlugin as any)( `nx run-many --target=${ context.target.target } --projects=${workspaceDependencies.join(',')}`, { skipInitialRun, skipWatchingDeps: !watchDependencies } ) ); } } if (!pathToWebpackConfig) { return baseWebpackConfig; } return mergeCustomWebpackConfig( baseWebpackConfig, pathToWebpackConfig, delegateBuilderOptions, context.target ); }, ...(pathToIndexFileTransformer ? { indexHtml: resolveIndexHtmlTransformer( pathToIndexFileTransformer, delegateBuilderOptions.tsConfig, context.target ), } : {}), }) ) ); } export default require('@angular-devkit/architect').createBuilder( executeWebpackBrowserBuilder ) as any;