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 c24db50ef4189..9b08c24e6a582 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 e1a38c2c12eaf..f17a749a78266 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 2d7534cdc1191..517737d26a575 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 de76ec1e39294..b78e7197a8abc 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 969f6a8a69c1d..335f92f2bed34 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; +}