UNPKG

nx

Version:

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

240 lines (239 loc) 9.41 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.splitTargetFromNodes = splitTargetFromNodes; exports.splitTargetFromConfigurations = splitTargetFromConfigurations; exports.splitTarget = splitTarget; exports.splitByColons = splitByColons; const output_1 = require("../utils/output"); function nodeLookup(nodes) { return { has: (name) => !!nodes[name], getTargets: (name) => nodes[name]?.data?.targets, }; } function configLookup(configs) { return { has: (name) => !!configs[name], getTargets: (name) => configs[name]?.targets, }; } /** * Collects all valid [project, target?, config?] interpretations of a * colon-delimited string by iterating over the *segments* of the string * (O(k²) where k = number of segments) rather than over every project in the * graph. * * When `currentProject` is provided, bare-target interpretations (the string * is `target` or `target:config` on that project) are also collected. */ function findAllMatchingSegments(segments, lookup, currentProject) { const matches = []; // --- Bare-target matches (currentProject context) --- if (currentProject && lookup.has(currentProject)) { const targets = lookup.getTargets(currentProject) || {}; for (let j = 1; j <= segments.length; j++) { const candidateTarget = segments.slice(0, j).join(':'); if (!(candidateTarget in targets)) { continue; } const configSegments = segments.slice(j); if (configSegments.length === 0) { matches.push([currentProject, candidateTarget]); } else { const candidateConfig = configSegments.join(':'); const configurations = targets[candidateTarget]?.configurations; if (configurations && candidateConfig in configurations) { matches.push([currentProject, candidateTarget, candidateConfig]); } } } } // --- Project-based matches --- for (let i = 1; i <= segments.length; i++) { const candidateProject = segments.slice(0, i).join(':'); if (!lookup.has(candidateProject)) { continue; } const remaining = segments.slice(i); if (remaining.length === 0) { matches.push([candidateProject]); continue; } const targets = lookup.getTargets(candidateProject) || {}; for (let j = 1; j <= remaining.length; j++) { const candidateTarget = remaining.slice(0, j).join(':'); if (!(candidateTarget in targets)) { continue; } const configSegments = remaining.slice(j); if (configSegments.length === 0) { matches.push([candidateProject, candidateTarget]); } else { const candidateConfig = configSegments.join(':'); const configurations = targets[candidateTarget]?.configurations; if (configurations && candidateConfig in configurations) { matches.push([candidateProject, candidateTarget, candidateConfig]); } } } } return matches; } /** * Returns whether `a` should be preferred over `b` using deterministic * precedence rules: * * 1. Bare-target matches (currentProject) rank highest. * 2. Longest (most-specific) project name. * 3. Longest target name. * 4. Longest configuration name. */ function isHigherPrecedence(a, b, currentProject) { const aIsBare = currentProject && a[0] === currentProject ? 1 : 0; const bIsBare = currentProject && b[0] === currentProject ? 1 : 0; if (aIsBare !== bIsBare) return aIsBare > bIsBare; if (a[0].length !== b[0].length) return a[0].length > b[0].length; const aTarget = (a[1] ?? '').length; const bTarget = (b[1] ?? '').length; if (aTarget !== bTarget) return aTarget > bTarget; return (a[2] ?? '').length > (b[2] ?? '').length; } /** * Single-pass selection of the highest-precedence match. */ function bestMatch(matches, currentProject) { let best = matches[0]; for (let i = 1; i < matches.length; i++) { if (isHigherPrecedence(matches[i], best, currentProject)) { best = matches[i]; } } return best; } function formatMatch(match) { return match.filter(Boolean).join(':'); } /** * Internal implementation shared by splitTargetFromNodes and * splitTargetFromConfigurations. */ function splitTargetImpl(s, lookup, options) { const silent = options?.silent ?? false; const currentProject = options?.currentProject; const segments = splitByColons(s); const matches = findAllMatchingSegments(segments, lookup, currentProject); if (matches.length > 0) { const best = bestMatch(matches, currentProject); if (matches.length > 1 && !silent) { output_1.output.warn({ title: `Ambiguous target specifier "${s}"`, bodyLines: [ `This string can be interpreted in multiple ways:`, ...matches.map((m) => ` ${m === best ? '→' : ' '} ${formatMatch(m)}${m === best ? ' (selected)' : ''}`), ``, `The most specific match was selected. To avoid ambiguity, use a unique target specifier.`, ], }); } return best; } // --- Fallback: no exact match found in the graph --- let colonIndex = s.indexOf(':'); if (colonIndex === 0) { // first colon can't be at the beginning of the string, try to find the next one colonIndex = s.indexOf(':', 1); } if (colonIndex > 0) { let [project, ...remainingSegments] = segments; // splitByColons splits on every ':', so a leading colon (e.g. ":pkg:build") // produces an empty first element. Greedily absorb segments to reconstruct // the longest known colon-prefixed project name (e.g. ":utils:common"). if (project === '' && remainingSegments.length > 0) { let absorbed = 1; // absorb at least one segment for (let k = remainingSegments.length - 1; k >= 1; k--) { const candidate = ':' + remainingSegments.slice(0, k).join(':'); if (lookup.has(candidate)) { absorbed = k; break; } } project = ':' + remainingSegments.slice(0, absorbed).join(':'); remainingSegments = remainingSegments.slice(absorbed); } // if only configuration cannot be matched, try to match project and target const configuration = remainingSegments[remainingSegments.length - 1]; const rest = s.slice(0, -(configuration.length + 1)); const restSegments = splitByColons(rest); const restMatches = findAllMatchingSegments(restSegments, lookup, currentProject); if (restMatches.length > 0) { const best = bestMatch(restMatches, currentProject); if (best.length === 2) { return [...best, configuration]; } } // no project-target pair found, do the naive matching const validTargets = lookup.getTargets(project); const validTargetNames = new Set(Object.keys(validTargets ?? {})); return [ project, ...groupJointSegments(remainingSegments, validTargetNames), ]; } // we don't know what to do with the string, return as is return [s]; } function splitTargetFromNodes(s, nodes, options) { return splitTargetImpl(s, nodeLookup(nodes), options); } /** * Splits a colon-delimited target specifier using a name-keyed * `Record<string, ProjectConfiguration>` — the format used during * the merge phase before the full project graph is available. */ function splitTargetFromConfigurations(s, configs, options) { return splitTargetImpl(s, configLookup(configs), options); } function splitTarget(s, projectGraph, options) { return splitTargetFromNodes(s, projectGraph.nodes, options); } function groupJointSegments(segments, validTargetNames) { for (let endingSegmentIdx = segments.length; endingSegmentIdx > 0; endingSegmentIdx--) { const potentialTargetName = segments.slice(0, endingSegmentIdx).join(':'); if (validTargetNames.has(potentialTargetName)) { const configurationName = endingSegmentIdx < segments.length ? segments.slice(endingSegmentIdx).join(':') : null; return configurationName ? [potentialTargetName, configurationName] : [potentialTargetName]; } } // If we can't find a segment match, keep older behaviour return segments; } function splitByColons(s) { const parts = []; let currentPart = ''; for (let i = 0; i < s.length; ++i) { if (s[i] === ':') { parts.push(currentPart); currentPart = ''; } else if (s[i] === '"') { i++; for (; i < s.length && s[i] != '"'; ++i) { currentPart += s[i]; } } else { currentPart += s[i]; } } parts.push(currentPart); return parts; }