UNPKG

@nx/devkit

Version:

The Nx Devkit is used to customize Nx for different technologies and use cases. It contains many utility functions for reading and writing files, updating configuration, working with Abstract Syntax Trees(ASTs), and more. Learn more about [extending Nx by

291 lines (288 loc) • 12.8 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.addPlugin = addPlugin; exports.addPluginV1 = addPluginV1; exports.generateCombinations = generateCombinations; const yargs = require("yargs-parser"); const devkit_exports_1 = require("nx/src/devkit-exports"); const devkit_internals_1 = require("nx/src/devkit-internals"); /** * Iterates through various forms of plugin options to find the one which does not conflict with the current graph */ async function addPlugin(tree, graph, pluginName, createNodesTuple, options, shouldUpdatePackageJsonScripts) { return _addPluginInternal(tree, graph, pluginName, (pluginOptions) => new devkit_internals_1.LoadedNxPlugin({ name: pluginName, createNodesV2: createNodesTuple, }, { plugin: pluginName, options: pluginOptions, }), options, shouldUpdatePackageJsonScripts); } /** * @deprecated Use `addPlugin` instead * Iterates through various forms of plugin options to find the one which does not conflict with the current graph */ async function addPluginV1(tree, graph, pluginName, createNodesTuple, options, shouldUpdatePackageJsonScripts) { return _addPluginInternal(tree, graph, pluginName, (pluginOptions) => new devkit_internals_1.LoadedNxPlugin({ name: pluginName, createNodes: createNodesTuple, }, { plugin: pluginName, options: pluginOptions, }), options, shouldUpdatePackageJsonScripts); } async function _addPluginInternal(tree, graph, pluginName, pluginFactory, options, shouldUpdatePackageJsonScripts) { const graphNodes = Object.values(graph.nodes); const nxJson = (0, devkit_exports_1.readNxJson)(tree); let pluginOptions; let projConfigs; if (Object.keys(options).length > 0) { const combinations = generateCombinations(options); optionsLoop: for (const _pluginOptions of combinations) { pluginOptions = _pluginOptions; nxJson.plugins ??= []; if (nxJson.plugins.some((p) => typeof p === 'string' ? p === pluginName : p.plugin === pluginName && !p.include)) { // Plugin has already been added return; } global.NX_GRAPH_CREATION = true; try { projConfigs = await (0, devkit_internals_1.retrieveProjectConfigurations)([pluginFactory(pluginOptions)], tree.root, nxJson); } catch (e) { // Errors are okay for this because we're only running 1 plugin if ((0, devkit_internals_1.isProjectConfigurationsError)(e)) { projConfigs = e.partialProjectConfigurationsResult; // ignore errors from projects with no name if (!e.errors.every(devkit_internals_1.isProjectsWithNoNameError)) { throw e; } } else { throw e; } } global.NX_GRAPH_CREATION = false; for (const projConfig of Object.values(projConfigs.projects)) { const node = graphNodes.find((node) => node.data.root === projConfig.root); if (!node) { continue; } for (const targetName in projConfig.targets) { if (node.data.targets[targetName]) { // Conflicting Target Name, check the next one pluginOptions = null; continue optionsLoop; } } } break; } } else { // If the plugin does not take in options, we add the plugin with empty options. nxJson.plugins ??= []; pluginOptions = {}; global.NX_GRAPH_CREATION = true; try { projConfigs = await (0, devkit_internals_1.retrieveProjectConfigurations)([pluginFactory(pluginOptions)], tree.root, nxJson); } catch (e) { // Errors are okay for this because we're only running 1 plugin if ((0, devkit_internals_1.isProjectConfigurationsError)(e)) { projConfigs = e.partialProjectConfigurationsResult; // ignore errors from projects with no name if (!e.errors.every(devkit_internals_1.isProjectsWithNoNameError)) { throw e; } } else { throw e; } } global.NX_GRAPH_CREATION = false; } if (!pluginOptions) { throw new Error('Could not add the plugin in a way which does not conflict with existing targets. Please report this error at: https://github.com/nrwl/nx/issues/new/choose'); } nxJson.plugins.push({ plugin: pluginName, options: pluginOptions, }); (0, devkit_exports_1.updateNxJson)(tree, nxJson); if (shouldUpdatePackageJsonScripts) { updatePackageScripts(tree, projConfigs); } } function updatePackageScripts(tree, projectConfigurations) { for (const projectConfig of Object.values(projectConfigurations.projects)) { const projectRoot = projectConfig.root; processProject(tree, projectRoot, projectConfig); } } function processProject(tree, projectRoot, projectConfiguration) { const packageJsonPath = `${projectRoot}/package.json`; if (!tree.exists(packageJsonPath)) { return; } const packageJson = (0, devkit_exports_1.readJson)(tree, packageJsonPath); if (!packageJson.scripts || !Object.keys(packageJson.scripts).length) { return; } const targetCommands = getInferredTargetCommands(projectConfiguration); if (!targetCommands.length) { return; } let hasChanges = false; targetCommands.sort((a, b) => b.command.split(/\s/).length - a.command.split(/\s/).length); for (const targetCommand of targetCommands) { const { command, target, configuration } = targetCommand; const targetCommandRegex = new RegExp(`(?<=^|&)((?: )*(?:[^&\\r\\n\\s]+ )*)(${command})((?: [^&\\r\\n\\s]+)*(?: )*)(?=$|&)`, 'g'); for (const scriptName of Object.keys(packageJson.scripts)) { const script = packageJson.scripts[scriptName]; // quick check for exact match within the script if (targetCommandRegex.test(script)) { packageJson.scripts[scriptName] = script.replace(targetCommandRegex, configuration ? `$1nx ${target} --configuration=${configuration}$3` : `$1nx ${target}$3`); hasChanges = true; } else { /** * Parse script and command to handle the following: * - if command doesn't match script => don't replace * - if command has more args => don't replace * - if command has same args, regardless of order => replace removing args * - if command has less args or with different value => replace leaving args */ const parsedCommand = yargs(command, { configuration: { 'strip-dashed': true }, }); // this assumes there are no positional args in the command, everything is a command or subcommand const commandCommand = parsedCommand._.join(' '); const commandRegex = new RegExp(`(?<=^|&)((?: )*(?:[^&\\r\\n\\s]+ )*)(${commandCommand})((?: [^&\\r\\n\\s]+)*( )*)(?=$|&)`, 'g'); const matches = script.match(commandRegex); if (!matches) { // the command doesn't match the script, don't replace continue; } for (const match of matches) { // parse the matched command within the script const parsedScript = yargs(match, { configuration: { 'strip-dashed': true }, }); let hasArgsWithDifferentValues = false; let scriptHasExtraArgs = false; let commandHasExtraArgs = false; for (const [key, value] of Object.entries(parsedCommand)) { if (key === '_') { continue; } if (parsedScript[key] === undefined) { commandHasExtraArgs = true; break; } if (parsedScript[key] !== value) { hasArgsWithDifferentValues = true; } } if (commandHasExtraArgs) { // the command has extra args, don't replace continue; } for (const key of Object.keys(parsedScript)) { if (key === '_') { continue; } if (!parsedCommand[key]) { scriptHasExtraArgs = true; break; } } if (!hasArgsWithDifferentValues && !scriptHasExtraArgs) { // they are the same, replace with the command removing the args const script = packageJson.scripts[scriptName]; packageJson.scripts[scriptName] = script.replace(match, match.replace(commandRegex, configuration ? `$1nx ${target} --configuration=${configuration}$4` : `$1nx ${target}$4`)); hasChanges = true; } else { // there are different args or the script has extra args, replace with the command leaving the args packageJson.scripts[scriptName] = packageJson.scripts[scriptName].replace(match, match.replace(commandRegex, configuration ? `$1nx ${target} --configuration=${configuration}$3` : `$1nx ${target}$3`)); hasChanges = true; } } } } } if (hasChanges) { (0, devkit_exports_1.writeJson)(tree, packageJsonPath, packageJson); } } function getInferredTargetCommands(project) { const targetCommands = []; for (const [targetName, target] of Object.entries(project.targets ?? {})) { if (target.command) { targetCommands.push({ command: target.command, target: targetName }); } else if (target.executor === 'nx:run-commands' && target.options?.command) { targetCommands.push({ command: target.options.command, target: targetName, }); } if (!target.configurations) { continue; } for (const [configurationName, configuration] of Object.entries(target.configurations)) { if (configuration.command) { targetCommands.push({ command: configuration.command, target: targetName, configuration: configurationName, }); } else if (target.executor === 'nx:run-commands' && configuration.options?.command) { targetCommands.push({ command: configuration.options.command, target: targetName, configuration: configurationName, }); } } } return targetCommands; } function generateCombinations(input) { // This is reversed so that combinations have the first defined property updated first const keys = Object.keys(input).reverse(); return _generateCombinations(Object.values(input).reverse()).map((combination) => { const result = {}; combination.reverse().forEach((combo, i) => { result[keys[keys.length - i - 1]] = combo; }); return result; }); } /** * Generate all possible combinations of a 2-dimensional array. * * Useful for generating all possible combinations of options for a plugin */ function _generateCombinations(input) { if (input.length === 0) { return [[]]; } else { const [first, ...rest] = input; const partialCombinations = _generateCombinations(rest); return first.flatMap((value) => partialCombinations.map((combination) => [value, ...combination])); } }