UNPKG

nx

Version:

The core Nx plugin contains the core functionality of Nx like the project graph, nx commands and task orchestration.

316 lines (315 loc) 16.2 kB
"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)); }