diff --git a/docs/generated/devkit/ProjectConfiguration.md b/docs/generated/devkit/ProjectConfiguration.md index a5946690f89044..4a55f30a87462e 100644 --- a/docs/generated/devkit/ProjectConfiguration.md +++ b/docs/generated/devkit/ProjectConfiguration.md @@ -8,6 +8,7 @@ Project configuration - [generators](../../devkit/documents/ProjectConfiguration#generators): Object - [implicitDependencies](../../devkit/documents/ProjectConfiguration#implicitdependencies): string[] +- [metadata](../../devkit/documents/ProjectConfiguration#metadata): Object - [name](../../devkit/documents/ProjectConfiguration#name): string - [namedInputs](../../devkit/documents/ProjectConfiguration#namedinputs): Object - [projectType](../../devkit/documents/ProjectConfiguration#projecttype): ProjectType @@ -53,6 +54,19 @@ List of projects which are added as a dependency --- +### metadata + +• `Optional` **metadata**: `Object` + +#### Type declaration + +| Name | Type | +| :-------------- | :------------------------------- | +| `targetGroups?` | `Record`\<`string`, `string`[]\> | +| `technologies?` | `string`[] | + +--- + ### name • `Optional` **name**: `string` diff --git a/packages/cypress/src/plugins/plugin.spec.ts b/packages/cypress/src/plugins/plugin.spec.ts index fb06b10d283130..7a9073e1ebbea4 100644 --- a/packages/cypress/src/plugins/plugin.spec.ts +++ b/packages/cypress/src/plugins/plugin.spec.ts @@ -69,6 +69,14 @@ describe('@nx/cypress/plugin', () => { { "projects": { ".": { + "metadata": { + "targetGroups": { + ".:e2e-ci": null, + }, + "technologies": [ + "cypress", + ], + }, "projectType": "application", "targets": { "e2e": { diff --git a/packages/cypress/src/plugins/plugin.ts b/packages/cypress/src/plugins/plugin.ts index 8a815da95936a4..675318425414df 100644 --- a/packages/cypress/src/plugins/plugin.ts +++ b/packages/cypress/src/plugins/plugin.ts @@ -12,7 +12,6 @@ import { dirname, join, relative } from 'path'; import { getLockFileName } from '@nx/js'; -import { CypressExecutorOptions } from '../executors/cypress/cypress.impl'; import { getNamedInputs } from '@nx/devkit/src/utils/get-named-inputs'; import { existsSync, readdirSync } from 'fs'; import { globWithWorkspaceContext } from 'nx/src/utils/workspace-context'; @@ -30,24 +29,16 @@ export interface CypressPluginOptions { const cachePath = join(projectGraphCacheDirectory, 'cypress.hash'); const targetsCache = existsSync(cachePath) ? readTargetsCache() : {}; -const calculatedTargets: Record< - string, - Record -> = {}; +const calculatedTargets: CypressTargets = { + targets: {}, + ciTestingGroup: null, +}; -function readTargetsCache(): Record< - string, - Record> -> { +function readTargetsCache(): Record { return readJsonFile(cachePath); } -function writeTargetsToCache( - targets: Record< - string, - Record> - > -) { +function writeTargetsToCache(targets: CypressTargets) { writeJsonFile(cachePath, targets); } @@ -75,7 +66,7 @@ export const createNodes: CreateNodes = [ getLockFileName(detectPackageManager(context.workspaceRoot)), ]); - const targets = targetsCache[hash] + const { targets, ciTestingGroup } = targetsCache[hash] ? targetsCache[hash] : await buildCypressTargets( configFilePath, @@ -91,6 +82,12 @@ export const createNodes: CreateNodes = [ [projectRoot]: { projectType: 'application', targets, + metadata: { + technologies: ['cypress'], + targetGroups: { + [`${projectRoot}:e2e-ci`]: ciTestingGroup, + }, + }, }, }, }; @@ -145,12 +142,17 @@ function getOutputs( return outputs; } +interface CypressTargets { + targets: Record; + ciTestingGroup: string[]; +} + async function buildCypressTargets( configFilePath: string, projectRoot: string, options: CypressPluginOptions, context: CreateNodesContext -) { +): Promise { const cypressConfig = await loadConfigFile( join(context.workspaceRoot, configFilePath) ); @@ -167,6 +169,7 @@ async function buildCypressTargets( const namedInputs = getNamedInputs(projectRoot, context); const targets: Record = {}; + let ciTestingGroup: string[] = []; if ('e2e' in cypressConfig) { targets[options.targetName] = { @@ -216,6 +219,8 @@ async function buildCypressTargets( for (const file of specFiles) { const relativeSpecFilePath = relative(projectRoot, file); const targetName = options.ciTargetName + '--' + relativeSpecFilePath; + + ciTestingGroup.push(targetName); targets[targetName] = { outputs, inputs, @@ -254,7 +259,11 @@ async function buildCypressTargets( }; } - return targets; + if (ciTestingGroup.length === 0) { + ciTestingGroup = null; + } + + return { targets, ciTestingGroup }; } function normalizeOptions(options: CypressPluginOptions): CypressPluginOptions { diff --git a/packages/nx/src/adapter/compat.ts b/packages/nx/src/adapter/compat.ts index 05f8b8ab1b4df1..d13d3c02fddce3 100644 --- a/packages/nx/src/adapter/compat.ts +++ b/packages/nx/src/adapter/compat.ts @@ -44,6 +44,7 @@ export const allowedProjectExtensions = [ 'projectType', 'release', 'includedScripts', + 'metadata', ] as const; // If we pass props on the workspace that angular doesn't know about, diff --git a/packages/nx/src/config/workspace-json-project-json.ts b/packages/nx/src/config/workspace-json-project-json.ts index a2256eb3440054..d71d2676140c19 100644 --- a/packages/nx/src/config/workspace-json-project-json.ts +++ b/packages/nx/src/config/workspace-json-project-json.ts @@ -111,6 +111,10 @@ export interface ProjectConfiguration { 'generator' | 'generatorOptions' >; }; + metadata?: { + technologies?: string[]; + targetGroups?: Record; + }; } export interface TargetDependencyConfig { 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 be5e41c51e65ec..f6a5bf91c195d3 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 @@ -731,6 +731,90 @@ describe('project-configuration-utils', () => { `); }); + describe('metadata', () => { + it('should be set if not previously defined', () => { + const rootMap = new RootMapBuilder() + .addProject({ + root: 'libs/lib-a', + name: 'lib-a', + }) + .getRootMap(); + const sourceMap: ConfigurationSourceMaps = { + 'libs/lib-a': {}, + }; + mergeProjectConfigurationIntoRootMap( + rootMap, + { + root: 'libs/lib-a', + name: 'lib-a', + metadata: { + technologies: ['technology'], + }, + }, + sourceMap, + ['dummy', 'dummy.ts'] + ); + + expect(rootMap.get('libs/lib-a').metadata).toEqual({ + technologies: ['technology'], + }); + expect(sourceMap['libs/lib-a']['metadata.technologies']).toEqual([ + 'dummy', + 'dummy.ts', + ]); + expect(sourceMap['libs/lib-a']['metadata.technologies.0']).toEqual([ + 'dummy', + 'dummy.ts', + ]); + }); + + it('should concat arrays', () => { + const rootMap = new RootMapBuilder() + .addProject({ + root: 'libs/lib-a', + name: 'lib-a', + metadata: { + technologies: ['technology1'], + }, + }) + .getRootMap(); + const sourceMap: ConfigurationSourceMaps = { + 'libs/lib-a': { + 'metadata.technologies': ['existing', 'existing.ts'], + 'metadata.technologies.0': ['existing', 'existing.ts'], + }, + }; + mergeProjectConfigurationIntoRootMap( + rootMap, + { + root: 'libs/lib-a', + name: 'lib-a', + metadata: { + technologies: ['technology2'], + }, + }, + sourceMap, + ['dummy', 'dummy.ts'] + ); + + expect(rootMap.get('libs/lib-a').metadata).toEqual({ + technologies: ['technology1', 'technology2'], + }); + expect(sourceMap['libs/lib-a']['metadata.technologies']).toEqual([ + 'existing', + 'existing.ts', + ]); + expect(sourceMap['libs/lib-a']['metadata.technologies.0']).toEqual([ + 'existing', + 'existing.ts', + ]); + expect(sourceMap['libs/lib-a']['metadata.technologies.1']).toEqual([ + 'dummy', + 'dummy.ts', + ]); + }); + }); + describe('source map', () => { it('should add new project info', () => { const rootMap = new RootMapBuilder().getRootMap(); 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 275cea0f6c853d..30b9dae422d6b4 100644 --- a/packages/nx/src/project-graph/utils/project-configuration-utils.ts +++ b/packages/nx/src/project-graph/utils/project-configuration-utils.ts @@ -60,12 +60,23 @@ export function mergeProjectConfigurationIntoRootMap( // a project.json in which case it was already updated above. const updatedProjectConfiguration = { ...matchingProject, - ...project, }; - if (sourceMap) { - for (const property in project) { - sourceMap[`${property}`] = sourceInformation; + for (const k in project) { + if ( + [ + 'projectType', + 'root', + 'name', + '$schema', + 'sourceRoot', + 'prefix', + ].includes(k) + ) { + updatedProjectConfiguration[k] = project[k]; + if (sourceMap) { + sourceMap[`${k}`] = sourceInformation; + } } } @@ -76,6 +87,7 @@ export function mergeProjectConfigurationIntoRootMap( ); if (sourceMap) { + sourceMap['tags'] ??= sourceInformation; project.tags.forEach((tag) => { sourceMap[`tags.${tag}`] = sourceInformation; }); @@ -88,6 +100,7 @@ export function mergeProjectConfigurationIntoRootMap( ).concat(project.implicitDependencies); if (sourceMap) { + sourceMap['implicitDependencies'] ??= sourceInformation; project.implicitDependencies.forEach((implicitDependency) => { sourceMap[`implicitDependencies.${implicitDependency}`] = sourceInformation; @@ -100,6 +113,7 @@ export function mergeProjectConfigurationIntoRootMap( 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]) { @@ -127,6 +141,7 @@ export function mergeProjectConfigurationIntoRootMap( }; if (sourceMap) { + sourceMap['namedInputs'] ??= sourceInformation; for (const namedInput in project.namedInputs) { sourceMap[`namedInputs.${namedInput}`] = sourceInformation; } @@ -137,6 +152,9 @@ export function mergeProjectConfigurationIntoRootMap( // 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) { @@ -176,6 +194,66 @@ export function mergeProjectConfigurationIntoRootMap( } } + if (project.metadata) { + if (sourceMap) { + sourceMap['targets'] ??= sourceInformation; + } + for (const [metadataKey, value] of Object.entries({ + ...project.metadata, + })) { + const existingValue = matchingProject.metadata?.[metadataKey]; + + if (Array.isArray(value) && Array.isArray(existingValue)) { + for (const item of [...value]) { + const newLength = + updatedProjectConfiguration.metadata[metadataKey].push(item); + if (sourceMap) { + sourceMap[`metadata.${metadataKey}.${newLength - 1}`] = + sourceInformation; + } + } + } else if (Array.isArray(value) && existingValue === undefined) { + updatedProjectConfiguration.metadata ??= {}; + updatedProjectConfiguration.metadata[metadataKey] ??= value; + if (sourceMap) { + sourceMap[`metadata.${metadataKey}`] = sourceInformation; + } + for (let i = 0; i < value.length; i++) { + if (sourceMap) { + sourceMap[`metadata.${metadataKey}.${i}`] = sourceInformation; + } + } + } else if ( + typeof value === 'object' && + typeof existingValue === 'object' + ) { + for (const key in value) { + const existingValue = matchingProject.metadata?.[metadataKey]?.[key]; + + if (Array.isArray(value[key]) && Array.isArray(existingValue)) { + for (const item of value[key]) { + const i = + updatedProjectConfiguration.metadata[metadataKey].push(item); + if (sourceMap) { + sourceMap[`metadata.${metadataKey}.${i}`] = sourceInformation; + } + } + } else { + updatedProjectConfiguration.metadata[metadataKey] = value; + if (sourceMap) { + sourceMap[`metadata.${metadataKey}`] = sourceInformation; + } + } + } + } else { + updatedProjectConfiguration.metadata[metadataKey] = value; + if (sourceMap) { + sourceMap[`metadata.${metadataKey}`] = sourceInformation; + } + } + } + } + projectRootMap.set( updatedProjectConfiguration.root, updatedProjectConfiguration