@nx/devkit
Version:
291 lines (288 loc) • 12.8 kB
JavaScript
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]));
}
}
;