From 92d0e15d1f01e5bc83438fd6e1d4dd4d22af3200 Mon Sep 17 00:00:00 2001 From: Craigory Coppola Date: Fri, 12 Jul 2024 16:25:32 -0400 Subject: [PATCH] feat(core): pattern matching for target defaults (#26870) ## Current Behavior TargetDefaults can be specified by keys matching either executor or target name only ## Expected Behavior TargetDefaults can match based on a glob pattern that may match the target name. This is useful for things like `e2e-ci--*`. Only 1 target default will ever apply to a given target. We recognize this may be confusing, but is inline with current handling. If no default matches the target name or key, the first default that matches by pattern will be used. ## Related Issue(s) Fixes # --- .../utils/project-configuration-utils.spec.ts | 18 +++++++++++++++++ .../utils/project-configuration-utils.ts | 20 ++++++++++++++++++- packages/nx/src/tasks-runner/utils.ts | 8 +++----- .../nx/src/utils/find-matching-projects.ts | 10 +++------- packages/nx/src/utils/globs.ts | 11 ++++++++++ 5 files changed, 54 insertions(+), 13 deletions(-) diff --git a/packages/nx/src/project-graph/utils/project-configuration-utils.spec.ts b/packages/nx/src/project-graph/utils/project-configuration-utils.spec.ts index c24db50ef4..9b08c24e6a 100644 --- a/packages/nx/src/project-graph/utils/project-configuration-utils.spec.ts +++ b/packages/nx/src/project-graph/utils/project-configuration-utils.spec.ts @@ -32,6 +32,16 @@ describe('project-configuration-utils', () => { key: 'default-value-for-targetname', }, }, + 'e2e-ci--*': { + options: { + key: 'default-value-for-e2e-ci', + }, + }, + 'e2e-ci--file-*': { + options: { + key: 'default-value-for-e2e-ci-file', + }, + }, }; it('should prefer executor key', () => { @@ -61,6 +71,14 @@ describe('project-configuration-utils', () => { ).toBeNull(); }); + it('should return longest matching target', () => { + expect( + // This matches both 'e2e-ci--*' and 'e2e-ci--file-*', we expect the first match to be returned. + readTargetDefaultsForTarget('e2e-ci--file-foo', targetDefaults, null) + .options['key'] + ).toEqual('default-value-for-e2e-ci-file'); + }); + it('should not merge top level properties for incompatible targets', () => { expect( mergeTargetConfigurations( diff --git a/packages/nx/src/project-graph/utils/project-configuration-utils.ts b/packages/nx/src/project-graph/utils/project-configuration-utils.ts index e1a38c2c12..f17a749a78 100644 --- a/packages/nx/src/project-graph/utils/project-configuration-utils.ts +++ b/packages/nx/src/project-graph/utils/project-configuration-utils.ts @@ -29,6 +29,7 @@ import { AggregateCreateNodesError, } from '../error-types'; import { CreateNodesResult } from '../plugins'; +import { isGlobPattern } from '../../utils/globs'; export type SourceInformation = [file: string | null, plugin: string]; export type ConfigurationSourceMaps = Record< @@ -1036,10 +1037,27 @@ export function readTargetDefaultsForTarget( // If not, use build if it is present. const key = [executor, targetName].find((x) => targetDefaults?.[x]); return key ? targetDefaults?.[key] : null; - } else { + } else if (targetDefaults?.[targetName]) { // If the executor is not defined, the only key we have is the target name. return targetDefaults?.[targetName]; } + + let matchingTargetDefaultKey: string | null = null; + for (const key in targetDefaults ?? {}) { + if (isGlobPattern(key) && minimatch(targetName, key)) { + if ( + !matchingTargetDefaultKey || + matchingTargetDefaultKey.length < key.length + ) { + matchingTargetDefaultKey = key; + } + } + } + if (matchingTargetDefaultKey) { + return targetDefaults[matchingTargetDefaultKey]; + } + + return {}; } function createRootMap(projectRootMap: Record) { diff --git a/packages/nx/src/tasks-runner/utils.ts b/packages/nx/src/tasks-runner/utils.ts index 2d7534cdc1..517737d26a 100644 --- a/packages/nx/src/tasks-runner/utils.ts +++ b/packages/nx/src/tasks-runner/utils.ts @@ -15,11 +15,9 @@ import { splitByColons } from '../utils/split-target'; import { getExecutorInformation } from '../command-line/run/executor-utils'; import { CustomHasher, ExecutorConfig } from '../config/misc-interfaces'; import { readProjectsConfigurationFromProjectGraph } from '../project-graph/project-graph'; -import { - GLOB_CHARACTERS, - findMatchingProjects, -} from '../utils/find-matching-projects'; +import { findMatchingProjects } from '../utils/find-matching-projects'; import { minimatch } from 'minimatch'; +import { isGlobPattern } from '../utils/globs'; export type NormalizedTargetDependencyConfig = TargetDependencyConfig & { projects: string[]; @@ -122,7 +120,7 @@ export function expandWildcardTargetConfiguration( dependencyConfig: NormalizedTargetDependencyConfig, allTargetNames: string[] ): NormalizedTargetDependencyConfig[] { - if (!GLOB_CHARACTERS.some((char) => dependencyConfig.target.includes(char))) { + if (!isGlobPattern(dependencyConfig.target)) { return [dependencyConfig]; } let cache = patternResultCache.get(allTargetNames); diff --git a/packages/nx/src/utils/find-matching-projects.ts b/packages/nx/src/utils/find-matching-projects.ts index de76ec1e39..b78e7197a8 100644 --- a/packages/nx/src/utils/find-matching-projects.ts +++ b/packages/nx/src/utils/find-matching-projects.ts @@ -1,5 +1,6 @@ import { minimatch } from 'minimatch'; import type { ProjectGraphProjectNode } from '../config/project-graph'; +import { isGlobPattern } from './globs'; const validPatternTypes = [ 'name', // Pattern is based on the project's name @@ -18,11 +19,6 @@ interface ProjectPattern { value: string; } -/** - * The presence of these characters in a string indicates that it might be a glob pattern. - */ -export const GLOB_CHARACTERS = ['*', '|', '{', '}', '(', ')']; - /** * Find matching project names given a list of potential project names or globs. * @@ -169,7 +165,7 @@ function addMatchingProjectsByName( return; } - if (!GLOB_CHARACTERS.some((c) => pattern.value.includes(c))) { + if (!isGlobPattern(pattern.value)) { return; } @@ -204,7 +200,7 @@ function addMatchingProjectsByTag( continue; } - if (!GLOB_CHARACTERS.some((c) => pattern.value.includes(c))) { + if (!isGlobPattern(pattern.value)) { continue; } diff --git a/packages/nx/src/utils/globs.ts b/packages/nx/src/utils/globs.ts index 969f6a8a69..335f92f2be 100644 --- a/packages/nx/src/utils/globs.ts +++ b/packages/nx/src/utils/globs.ts @@ -2,3 +2,14 @@ export function combineGlobPatterns(...patterns: (string | string[])[]) { const p = patterns.flat(); return p.length > 1 ? '{' + p.join(',') + '}' : p.length === 1 ? p[0] : ''; } + +export const GLOB_CHARACTERS = new Set(['*', '|', '{', '}', '(', ')', '[']); + +export function isGlobPattern(pattern: string) { + for (const c of pattern) { + if (GLOB_CHARACTERS.has(c)) { + return true; + } + } + return false; +}