nx
Version:
316 lines (315 loc) • 16.2 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.readTargetDefaultsForTarget = exports.mergeTargetConfigurations = void 0;
exports.createProjectConfigurationsWithPlugins = createProjectConfigurationsWithPlugins;
exports.mergeCreateNodesResults = mergeCreateNodesResults;
exports.findMatchingConfigFiles = findMatchingConfigFiles;
const workspace_root_1 = require("../../utils/workspace-root");
const project_nodes_manager_1 = require("./project-configuration/project-nodes-manager");
const target_normalization_1 = require("./project-configuration/target-normalization");
const minimatch_1 = require("minimatch");
const perf_hooks_1 = require("perf_hooks");
const delayed_spinner_1 = require("../../utils/delayed-spinner");
const plugin_progress_text_1 = require("../../utils/plugin-progress-text");
const progress_topics_1 = require("../../utils/progress-topics");
const error_types_1 = require("../error-types");
const target_defaults_1 = require("./project-configuration/target-defaults");
var target_merging_1 = require("./project-configuration/target-merging");
Object.defineProperty(exports, "mergeTargetConfigurations", { enumerable: true, get: function () { return target_merging_1.mergeTargetConfigurations; } });
var target_defaults_2 = require("./project-configuration/target-defaults");
Object.defineProperty(exports, "readTargetDefaultsForTarget", { enumerable: true, get: function () { return target_defaults_2.readTargetDefaultsForTarget; } });
/**
* Transforms a list of project paths into a map of project configurations.
*
* Plugins are run in parallel, then results are merged in a single ordered pass:
* specified plugins → synthetic target defaults → default plugins
*
* This ordering ensures '...' spread tokens in default plugin configs
* (project.json, package.json) expand against accumulated values from
* specified plugins and target defaults.
*
* @param root The workspace root
* @param nxJson The NxJson configuration
* @param projectFiles Plugin config files, separated by plugin set
* @param plugins The plugins separated into specified and default sets
*/
async function createProjectConfigurationsWithPlugins(root = workspace_root_1.workspaceRoot, nxJson, projectFiles, plugins) {
perf_hooks_1.performance.mark('build-project-configs:start');
let spinner;
const inProgressPlugins = new Set();
const getSpinnerText = () => spinner
? (0, plugin_progress_text_1.formatPluginProgressText)('Creating project graph nodes', inProgressPlugins)
: '';
const specifiedCreateNodesPlugins = plugins.specifiedPlugins.filter((plugin) => plugin.createNodes?.[0]);
const defaultCreateNodesPlugins = plugins.defaultPlugins.filter((plugin) => plugin.createNodes?.[0]);
const allCreateNodesPlugins = [
...specifiedCreateNodesPlugins,
...defaultCreateNodesPlugins,
];
const allProjectFiles = [
...projectFiles.specifiedPluginFiles,
...projectFiles.defaultPluginFiles,
];
const specifiedCount = specifiedCreateNodesPlugins.length;
spinner = new delayed_spinner_1.DelayedSpinner(getSpinnerText(), {
progressTopic: progress_topics_1.ProgressTopics.GraphConstruction,
});
const results = [];
const errors = [];
// We iterate over plugins first - this ensures that plugins specified first take precedence.
for (const [index, { index: pluginIndex, createNodes: createNodesTuple, include, exclude, name: pluginName, },] of allCreateNodesPlugins.entries()) {
const [, createNodes] = createNodesTuple;
const matchingConfigFiles = findMatchingConfigFiles(allProjectFiles[index], include, exclude);
inProgressPlugins.add(pluginName);
let r = createNodes(matchingConfigFiles, {
nxJsonConfiguration: nxJson,
workspaceRoot: root,
})
.catch((e) => {
const error = (0, error_types_1.isAggregateCreateNodesError)(e)
? // This is an expected error if something goes wrong while processing files.
e
: // This represents a single plugin erroring out with a hard error.
new error_types_1.AggregateCreateNodesError([[null, e]], []);
if (pluginIndex !== undefined) {
error.pluginIndex = pluginIndex;
}
(0, error_types_1.formatAggregateCreateNodesError)(error, pluginName);
// This represents a single plugin erroring out with a hard error.
errors.push(error);
// The plugin didn't return partial results, so we return an empty array.
return error.partialResults.map((r) => [pluginName, r[0], r[1], index]);
})
.finally(() => {
inProgressPlugins.delete(pluginName);
spinner.setMessage(getSpinnerText());
});
results.push(r);
}
return Promise.all(results).then((results) => {
spinner?.cleanup();
// Split results into specified and default plugin sets
const specifiedResults = results.slice(0, specifiedCount);
const defaultResults = results.slice(specifiedCount);
const { projectRootMap, externalNodes, rootMap, configurationSourceMaps } = mergeCreateNodesResults(specifiedResults, defaultResults, nxJson, root, errors);
perf_hooks_1.performance.mark('build-project-configs:end');
perf_hooks_1.performance.measure('build-project-configs', 'build-project-configs:start', 'build-project-configs:end');
const allProjectFilesFlat = [
...projectFiles.specifiedPluginFiles.flat(),
...projectFiles.defaultPluginFiles.flat(),
];
if (errors.length === 0) {
return {
projects: projectRootMap,
externalNodes,
projectRootMap: rootMap,
sourceMaps: configurationSourceMaps,
matchingProjectFiles: allProjectFilesFlat,
};
}
else {
throw new error_types_1.ProjectConfigurationsError(errors, {
projects: projectRootMap,
externalNodes,
projectRootMap: rootMap,
sourceMaps: configurationSourceMaps,
matchingProjectFiles: allProjectFilesFlat,
});
}
});
}
/**
* Runs a single plugin batch through two passes:
*
* 1. Every project node in every plugin result is handed to `mergeFn`,
* which decides where it lands (the manager's rootMap, an
* intermediate rootMap, etc.). Any failure is collected into
* `errors`; processing keeps going. External nodes are accumulated
* onto the shared `externalNodes` record.
* 2. After every project in the batch has been merged, name-reference
* sentinels for the batch are registered against `nameRefRootMap` —
* the rootMap the batch was merged into — so sentinels point at the
* target objects that actually received the merges.
*
* The two passes can't be collapsed: a sentinel registered too early
* would point at the pre-merge object, and a later project in the same
* batch may still rename a project the sentinel refers to. Splitting
* the registration into a second pass also lets forward references
* inside the same batch resolve eagerly.
*/
function mergeCreateNodesResultsFromSinglePlugin(pluginResults, mergeFn, nodesManager, nameRefRootMap, externalNodes, errors) {
for (const result of pluginResults) {
const [pluginName, file, nodes, pluginIndex] = result;
const { projects: projectNodes, externalNodes: pluginExternalNodes } = nodes;
const sourceInfo = [file, pluginName];
for (const root in projectNodes) {
if (!projectNodes[root])
continue;
const project = { root, ...projectNodes[root] };
try {
mergeFn(project, sourceInfo);
}
catch (error) {
errors.push(new error_types_1.MergeNodesError({ file, pluginName, error, pluginIndex }));
}
}
Object.assign(externalNodes, pluginExternalNodes);
}
for (const result of pluginResults) {
const [pluginName, file, nodes, pluginIndex] = result;
const { projects: projectNodes } = nodes;
try {
nodesManager.registerNameRefs(projectNodes, nameRefRootMap);
}
catch (error) {
errors.push(new error_types_1.MergeNodesError({ file, pluginName, error, pluginIndex }));
}
}
}
/**
* Merges create nodes results into a single rootMap.
*
* Default plugin results are merged twice: first into an intermediate
* rootMap with unresolved spread sentinels preserved, so target
* defaults selection sees the real merged shape of defaults; then
* applied as a single layer onto the main rootMap where the preserved
* spreads resolve against the specified + target-defaults base.
*/
function mergeCreateNodesResults(specifiedResults, defaultResults, nxJsonConfiguration, workspaceRoot, errors) {
perf_hooks_1.performance.mark('createNodes:merge - start');
const nodesManager = new project_nodes_manager_1.ProjectNodesManager();
const externalNodes = {};
const configurationSourceMaps = {};
const intermediateDefaultRootMap = {};
// Kept separate so the intermediate merge doesn't clobber
// specified/TD attribution on fields the defaults don't touch.
const defaultConfigurationSourceMaps = {};
const mergeToManager = (project, sourceInfo) => nodesManager.mergeProjectNode(project, configurationSourceMaps, sourceInfo);
const mergeToIntermediate = (project, sourceInfo) => {
(0, project_nodes_manager_1.mergeProjectConfigurationIntoRootMap)(intermediateDefaultRootMap, project, defaultConfigurationSourceMaps, sourceInfo, false, true);
};
for (const pluginResults of specifiedResults) {
mergeCreateNodesResultsFromSinglePlugin(pluginResults, mergeToManager, nodesManager, nodesManager.getRootMap(), externalNodes, errors);
}
for (const pluginResults of defaultResults) {
mergeCreateNodesResultsFromSinglePlugin(pluginResults, mergeToIntermediate, nodesManager, intermediateDefaultRootMap, externalNodes, errors);
}
const targetDefaultsResults = (0, target_defaults_1.createTargetDefaultsResults)(nodesManager.getRootMap(), intermediateDefaultRootMap, nxJsonConfiguration);
if (targetDefaultsResults.length > 0) {
mergeCreateNodesResultsFromSinglePlugin(targetDefaultsResults, mergeToManager, nodesManager, nodesManager.getRootMap(), externalNodes, errors);
}
// Apply the intermediate default rootMap as a single layer. Preserved
// spread sentinels resolve here against the real specified + TD base.
// Source maps are intentionally not written — TD attribution for
// fields that yield to the base (e.g. keys before `...`) stays intact.
for (const root in intermediateDefaultRootMap) {
const project = intermediateDefaultRootMap[root];
try {
nodesManager.mergeProjectNode(project, undefined, undefined);
}
catch (error) {
errors.push(new error_types_1.MergeNodesError({
file: 'nx.json',
pluginName: 'nx/default-plugins',
error,
pluginIndex: undefined,
}));
}
}
// The intermediate apply may have rebuilt dependsOn / inputs arrays
// via spread merges, leaving sentinels inserted against the
// intermediate rootMap pointing at now-orphaned arrays. Re-walking
// the final merged targets rebinds each encountered sentinel's
// `parent` to the current array (see
// ProjectNameInNodePropsManager#processInputs / processDependsOn).
nodesManager.registerNameRefs(intermediateDefaultRootMap);
// Overlay default-plugin attribution onto the main source maps using
// "only fill missing" semantics. Any key already present in
// configurationSourceMaps was written by a specified plugin or by
// target defaults, and that attribution is strictly more correct:
// - For fields the default plugin never shadowed, the existing entry
// already matches what the default plugin would overlay.
// - For fields where a default plugin placed `...` after other keys,
// those keys yielded to the base during the single-layer apply
// above. The stale default-plugin entry in
// `defaultConfigurationSourceMaps` must NOT clobber the base
// attribution that the specified plugin / TD already recorded.
for (const root in defaultConfigurationSourceMaps) {
const existing = (configurationSourceMaps[root] ??= {});
const incoming = defaultConfigurationSourceMaps[root];
for (const key in incoming) {
if (existing[key] === undefined) {
existing[key] = incoming[key];
}
}
}
const projectRootMap = nodesManager.getRootMap();
try {
nodesManager.applySubstitutions();
(0, target_normalization_1.validateAndNormalizeProjectRootMap)(workspaceRoot, projectRootMap, nxJsonConfiguration, configurationSourceMaps);
}
catch (error) {
let _errors = error instanceof AggregateError ? error.errors : [error];
for (const e of _errors) {
if ((0, error_types_1.isProjectsWithNoNameError)(e) ||
(0, error_types_1.isMultipleProjectsWithSameNameError)(e) ||
(0, error_types_1.isWorkspaceValidityError)(e)) {
errors.push(e);
}
else {
throw e;
}
}
}
const rootMap = (0, project_nodes_manager_1.createRootMap)(projectRootMap);
perf_hooks_1.performance.mark('createNodes:merge - end');
perf_hooks_1.performance.measure('createNodes:merge', 'createNodes:merge - start', 'createNodes:merge - end');
return { projectRootMap, externalNodes, rootMap, configurationSourceMaps };
}
/**
* Fast matcher for patterns without negations - uses short-circuit evaluation.
*/
function matchesSimplePatterns(file, patterns) {
return patterns.some((pattern) => (0, minimatch_1.minimatch)(file, pattern, { dot: true }));
}
/**
* Full matcher for patterns with negations - processes all patterns sequentially.
* Patterns starting with '!' are negation patterns that remove files from the match set.
* Patterns are processed in order, with later patterns overriding earlier ones.
*/
function matchesNegationPatterns(file, patterns) {
// If first pattern is negation, start by matching everything
let isMatch = patterns[0].startsWith('!');
for (const pattern of patterns) {
const isNegation = pattern.startsWith('!');
const actualPattern = isNegation ? pattern.substring(1) : pattern;
if ((0, minimatch_1.minimatch)(file, actualPattern, { dot: true })) {
// Last matching pattern wins
isMatch = !isNegation;
}
}
return isMatch;
}
/**
* Creates a matcher function for the given patterns.
* @param patterns Array of glob patterns (can include negation patterns starting with '!')
* @param emptyValue Value to return when patterns array is empty
* @returns A function that checks if a file matches the patterns
*/
function createMatcher(patterns, emptyValue) {
if (!patterns || patterns.length === 0) {
return () => emptyValue;
}
const hasNegationPattern = patterns.some((p) => p.startsWith('!'));
return hasNegationPattern
? (file) => matchesNegationPatterns(file, patterns)
: (file) => matchesSimplePatterns(file, patterns);
}
function findMatchingConfigFiles(projectFiles, include, exclude) {
// projectFiles already comes from multiGlobWithWorkspaceContext for the
// plugin's createNodes pattern, so only include/exclude filters remain here.
// Empty include means include everything, empty exclude means exclude nothing
const includes = createMatcher(include, true);
const excludes = createMatcher(exclude, false);
return projectFiles.filter((file) => includes(file) && !excludes(file));
}