import { CreateNodes, glob, ProjectConfiguration, readNxJson, readProjectConfiguration, TargetConfiguration, Tree, updateNxJson, updateProjectConfiguration, } from 'nx/src/devkit-exports'; import { findProjectForPath, hashObject } from 'nx/src/devkit-internals'; export async function replaceProjectConfigurationsWithPlugin( tree: Tree, rootMappings: Map, pluginPath: string, createNodes: CreateNodes, pluginOptions: T ): Promise { const nxJson = readNxJson(tree); const hasPlugin = nxJson.plugins?.some((p) => typeof p === 'string' ? p === pluginPath : p.plugin === pluginPath ); if (hasPlugin) { return; } nxJson.plugins ??= []; nxJson.plugins.push({ plugin: pluginPath, options: pluginOptions, }); updateNxJson(tree, nxJson); const [pluginGlob, createNodesFunction] = createNodes; const configFiles = glob(tree, [pluginGlob]); for (const configFile of configFiles) { try { const projectName = findProjectForPath(configFile, rootMappings); const projectConfig = readProjectConfiguration(tree, projectName); const nodes = await createNodesFunction(configFile, pluginOptions, { workspaceRoot: tree.root, nxJsonConfiguration: readNxJson(tree), configFiles, }); const node = nodes.projects[Object.keys(nodes.projects)[0]]; for (const [targetName, targetConfig] of Object.entries(node.targets)) { const targetFromProjectConfig = projectConfig.targets[targetName]; if (targetFromProjectConfig?.executor !== targetConfig.executor) { continue; } const targetFromCreateNodes = node.targets[targetName]; removeConfigurationDefinedByPlugin( targetName, targetFromProjectConfig, targetFromCreateNodes, projectConfig ); } updateProjectConfiguration(tree, projectName, projectConfig); } catch (e) { console.error(e); } } } function removeConfigurationDefinedByPlugin( targetName: string, targetFromProjectConfig: TargetConfiguration, targetFromCreateNodes: TargetConfiguration, projectConfig: ProjectConfiguration ) { // Executor delete targetFromProjectConfig.executor; // Default Configuration if ( targetFromProjectConfig.defaultConfiguration === targetFromCreateNodes.defaultConfiguration ) { delete targetFromProjectConfig.defaultConfiguration; } // Cache if (targetFromProjectConfig.cache === targetFromCreateNodes.cache) { delete targetFromProjectConfig.cache; } // Depends On if ( targetFromProjectConfig.dependsOn && shouldRemoveArrayProperty( targetFromProjectConfig.dependsOn, targetFromCreateNodes.dependsOn ) ) { delete targetFromProjectConfig.dependsOn; } // Outputs if ( targetFromProjectConfig.outputs && shouldRemoveArrayProperty( targetFromProjectConfig.outputs, targetFromCreateNodes.outputs ) ) { delete targetFromProjectConfig.outputs; } // Inputs if ( targetFromProjectConfig.inputs && shouldRemoveArrayProperty( targetFromProjectConfig.inputs, targetFromCreateNodes.inputs ) ) { delete targetFromProjectConfig.inputs; } // Options for (const [optionName, optionValue] of Object.entries( targetFromProjectConfig.options ?? {} )) { if (equals(targetFromCreateNodes.options[optionName], optionValue)) { delete targetFromProjectConfig.options[optionName]; } } if (Object.keys(targetFromProjectConfig.options).length === 0) { delete targetFromProjectConfig.options; } // Configurations for (const [configName, configOptions] of Object.entries( targetFromProjectConfig.configurations ?? {} )) { for (const [optionName, optionValue] of Object.entries(configOptions)) { if ( targetFromCreateNodes.configurations?.[configName]?.[optionName] === optionValue ) { delete targetFromProjectConfig.configurations[configName][optionName]; } } if (Object.keys(configOptions).length === 0) { delete targetFromProjectConfig.configurations[configName]; } } if (Object.keys(targetFromProjectConfig.configurations ?? {}).length === 0) { delete targetFromProjectConfig.configurations; } if (Object.keys(targetFromProjectConfig).length === 0) { delete projectConfig.targets[targetName]; } } function equals(a: T, b: T) { if (Array.isArray(a) && Array.isArray(b)) { return a.length === b.length && a.every((v, i) => v === b[i]); } if (typeof a === 'object' && typeof b === 'object') { return hashObject(a) === hashObject(b); } return a === b; } function shouldRemoveArrayProperty( arrayValuesFromProjectConfiguration: (object | string)[], arrayValuesFromCreateNodes: (object | string)[] ) { const setOfArrayValuesFromProjectConfiguration = new Set( arrayValuesFromProjectConfiguration ); loopThroughArrayValuesFromCreateNodes: for (const arrayValueFromCreateNodes of arrayValuesFromCreateNodes) { if (typeof arrayValueFromCreateNodes === 'string') { if ( !setOfArrayValuesFromProjectConfiguration.has(arrayValueFromCreateNodes) ) { // If the inputs from the project configuration is missing an input from createNodes it was removed return false; } else { setOfArrayValuesFromProjectConfiguration.delete( arrayValueFromCreateNodes ); } } else { for (const arrayValue of setOfArrayValuesFromProjectConfiguration.values()) { if ( typeof arrayValue !== 'string' && hashObject(arrayValue) === hashObject(arrayValueFromCreateNodes) ) { setOfArrayValuesFromProjectConfiguration.delete(arrayValue); // Continue the outer loop, breaking out of this loop continue loopThroughArrayValuesFromCreateNodes; } } // If an input was not matched, that means the input was removed return false; } } // If there are still inputs in the project configuration, they have added additional inputs return setOfArrayValuesFromProjectConfiguration.size === 0; }