UNPKG

nx

Version:

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

265 lines (264 loc) 12 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.ProjectNodesManager = exports.validateProject = void 0; exports.mergeProjectConfigurationIntoRootMap = mergeProjectConfigurationIntoRootMap; exports.readProjectConfigurationsFromRootMap = readProjectConfigurationsFromRootMap; exports.createRootMap = createRootMap; const error_types_1 = require("../../error-types"); const target_merging_1 = require("./target-merging"); const target_normalization_1 = require("./target-normalization"); const name_substitution_manager_1 = require("./name-substitution-manager"); const source_maps_1 = require("./source-maps"); const minimatch_1 = require("minimatch"); const globs_1 = require("../../../utils/globs"); var target_normalization_2 = require("./target-normalization"); Object.defineProperty(exports, "validateProject", { enumerable: true, get: function () { return target_normalization_2.validateProject; } }); function mergeProjectConfigurationIntoRootMap(projectRootMap, project, configurationSourceMaps, sourceInformation, // This function is used when reading project configuration // in generators, where we don't want to do this. skipTargetNormalization) { project.root = project.root === '' ? '.' : project.root; if (configurationSourceMaps && !configurationSourceMaps[project.root]) { configurationSourceMaps[project.root] = {}; } const sourceMap = configurationSourceMaps?.[project.root]; let matchingProject = projectRootMap[project.root]; if (!matchingProject) { projectRootMap[project.root] = { root: project.root, }; matchingProject = projectRootMap[project.root]; if (sourceMap) { sourceMap[`root`] = sourceInformation; } } // This handles top level properties that are overwritten. // e.g. `srcRoot`, `projectType`, or other fields that shouldn't be extended // Note: `name` is set specifically here to keep it from changing. The name is // always determined by the first inference plugin to ID a project, unless it has // a project.json in which case it was already updated above. const updatedProjectConfiguration = { ...matchingProject, }; for (const k in project) { if (![ 'tags', 'implicitDependencies', 'generators', 'targets', 'metadata', 'namedInputs', ].includes(k)) { updatedProjectConfiguration[k] = project[k]; if (sourceMap) { sourceMap[`${k}`] = sourceInformation; } } } // The next blocks handle properties that should be themselves merged (e.g. targets, tags, and implicit dependencies) if (project.tags) { updatedProjectConfiguration.tags = Array.from(new Set((matchingProject.tags ?? []).concat(project.tags))); if (sourceMap) { sourceMap['tags'] ??= sourceInformation; project.tags.forEach((tag) => { sourceMap[`tags.${tag}`] = sourceInformation; }); } } if (project.implicitDependencies) { updatedProjectConfiguration.implicitDependencies = (matchingProject.implicitDependencies ?? []).concat(project.implicitDependencies); if (sourceMap) { sourceMap['implicitDependencies'] ??= sourceInformation; project.implicitDependencies.forEach((implicitDependency) => { sourceMap[`implicitDependencies.${implicitDependency}`] = sourceInformation; }); } } if (project.generators) { // Start with generators config in new project. updatedProjectConfiguration.generators = { ...project.generators }; if (sourceMap) { sourceMap['generators'] ??= sourceInformation; for (const generator in project.generators) { sourceMap[`generators.${generator}`] = sourceInformation; for (const property in project.generators[generator]) { sourceMap[`generators.${generator}.${property}`] = sourceInformation; } } } if (matchingProject.generators) { // For each generator that was already defined, shallow merge the options. // Project contains the new info, so it has higher priority. for (const generator in matchingProject.generators) { updatedProjectConfiguration.generators[generator] = { ...matchingProject.generators[generator], ...project.generators[generator], }; } } } if (project.namedInputs) { updatedProjectConfiguration.namedInputs = { ...matchingProject.namedInputs, ...project.namedInputs, }; if (sourceMap) { sourceMap['namedInputs'] ??= sourceInformation; for (const namedInput in project.namedInputs) { sourceMap[`namedInputs.${namedInput}`] = sourceInformation; } } } if (project.metadata) { updatedProjectConfiguration.metadata = (0, target_merging_1.mergeMetadata)(sourceMap, sourceInformation, 'metadata', project.metadata, matchingProject.metadata); } if (project.targets) { // We merge the targets with special handling, so clear this back to the // targets as defined originally before merging. updatedProjectConfiguration.targets = matchingProject?.targets ?? {}; if (sourceMap) { sourceMap['targets'] ??= sourceInformation; } // For each target defined in the new config for (const targetName in project.targets) { // Always set source map info for the target, but don't overwrite info already there // if augmenting an existing target. const target = project.targets?.[targetName]; if (sourceMap) { sourceMap[(0, source_maps_1.targetSourceMapKey)(targetName)] = sourceInformation; } const normalizedTarget = skipTargetNormalization ? target : (0, target_merging_1.resolveCommandSyntacticSugar)(target, project.root); let matchingTargets = []; if ((0, globs_1.isGlobPattern)(targetName)) { // find all targets matching the glob pattern // this will map atomized targets to the glob pattern same as it does for targetDefaults matchingTargets = Object.keys(updatedProjectConfiguration.targets).filter((key) => (0, minimatch_1.minimatch)(key, targetName)); } // If no matching targets were found, we can assume that the target name is not (meant to be) a glob pattern if (!matchingTargets.length) { matchingTargets = [targetName]; } for (const matchingTargetName of matchingTargets) { updatedProjectConfiguration.targets[matchingTargetName] = (0, target_merging_1.mergeTargetConfigurations)(normalizedTarget, matchingProject.targets?.[matchingTargetName], sourceMap, sourceInformation, `targets.${matchingTargetName}`); } } } projectRootMap[updatedProjectConfiguration.root] = updatedProjectConfiguration; const nameChanged = !!updatedProjectConfiguration.name && updatedProjectConfiguration.name !== matchingProject?.name; return { nameChanged }; } function readProjectConfigurationsFromRootMap(projectRootMap) { const projects = {}; // If there are projects that have the same name, that is an error. // This object tracks name -> (all roots of projects with that name) // to provide better error messaging. const conflicts = new Map(); const projectRootsWithNoName = []; for (const root in projectRootMap) { const project = projectRootMap[root]; // We're setting `// targets` as a comment `targets` is empty due to Project Crystal. // Strip it before returning configuration for usage. if (project['// targets']) delete project['// targets']; try { (0, target_normalization_1.validateProject)(project, projects); projects[project.name] = project; } catch (e) { if ((0, error_types_1.isProjectWithNoNameError)(e)) { projectRootsWithNoName.push(e.projectRoot); } else if ((0, error_types_1.isProjectWithExistingNameError)(e)) { const rootErrors = conflicts.get(e.projectName) ?? [ projects[e.projectName].root, ]; rootErrors.push(e.projectRoot); conflicts.set(e.projectName, rootErrors); } else { throw e; } } } if (conflicts.size > 0) { throw new error_types_1.MultipleProjectsWithSameNameError(conflicts, projects); } if (projectRootsWithNoName.length > 0) { throw new error_types_1.ProjectsWithNoNameError(projectRootsWithNoName, projects); } return projects; } function createRootMap(projectRootMap) { const map = {}; for (const projectRoot in projectRootMap) { const projectName = projectRootMap[projectRoot].name; map[projectRoot] = projectName; } return map; } /** * Owns the rootMap (root → ProjectConfiguration) and nameMap * (name → ProjectConfiguration), coordinating merges with the * {@link ProjectNameInNodePropsManager} for deferred name substitutions. * * The nameMap entries are the *same object references* as the rootMap * entries, so when a merge adds targets to a rootMap entry the nameMap * entry automatically has them too — no copying, no staleness. */ class ProjectNodesManager { constructor() { // root → ProjectConfiguration (the merge target) this.rootMap = {}; // name → ProjectConfiguration (same object references as rootMap) this.nameMap = {}; // Pass a lazy accessor so the substitution manager always sees // the current nameMap without manual synchronization. this.nameSubstitutionManager = new name_substitution_manager_1.ProjectNameInNodePropsManager(() => this.nameMap); } getRootMap() { return this.rootMap; } /** * Merges a project into the rootMap, updates the nameMap, and notifies * the substitution manager if the name changed at this root. */ mergeProjectNode(project, configurationSourceMaps, sourceInformation) { const previousName = this.rootMap[project.root]?.name; mergeProjectConfigurationIntoRootMap(this.rootMap, project, configurationSourceMaps, sourceInformation); const merged = this.rootMap[project.root]; const currentName = merged?.name; if (currentName) { // Remove old nameMap entry on rename if (previousName && previousName !== currentName) { delete this.nameMap[previousName]; } // Point nameMap at the same object as rootMap this.nameMap[currentName] = merged; // Notify substitution manager of name change if (currentName !== previousName) { this.nameSubstitutionManager.identifyProjectWithRoot(project.root, currentName); } } } /** * Registers substitutors for a plugin result's project references * in `inputs` and `dependsOn`. */ registerSubstitutors(pluginResultProjects) { this.nameSubstitutionManager.registerSubstitutorsForNodeResults(pluginResultProjects); } /** * Applies all pending name substitutions. Call once after all plugin * results have been merged. */ applySubstitutions() { this.nameSubstitutionManager.applySubstitutions(this.rootMap); } } exports.ProjectNodesManager = ProjectNodesManager;