nx
Version:
265 lines (264 loc) • 12 kB
JavaScript
;
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;