diff --git a/docs/generated/devkit/ProjectConfiguration.md b/docs/generated/devkit/ProjectConfiguration.md index 4a55f30a87..a086b3e3d5 100644 --- a/docs/generated/devkit/ProjectConfiguration.md +++ b/docs/generated/devkit/ProjectConfiguration.md @@ -8,7 +8,7 @@ Project configuration - [generators](../../devkit/documents/ProjectConfiguration#generators): Object - [implicitDependencies](../../devkit/documents/ProjectConfiguration#implicitdependencies): string[] -- [metadata](../../devkit/documents/ProjectConfiguration#metadata): Object +- [metadata](../../devkit/documents/ProjectConfiguration#metadata): ProjectMetadata - [name](../../devkit/documents/ProjectConfiguration#name): string - [namedInputs](../../devkit/documents/ProjectConfiguration#namedinputs): Object - [projectType](../../devkit/documents/ProjectConfiguration#projecttype): ProjectType @@ -56,14 +56,9 @@ List of projects which are added as a dependency ### metadata -• `Optional` **metadata**: `Object` +• `Optional` **metadata**: `ProjectMetadata` -#### Type declaration - -| Name | Type | -| :-------------- | :------------------------------- | -| `targetGroups?` | `Record`\<`string`, `string`[]\> | -| `technologies?` | `string`[] | +Metadata about the project --- diff --git a/docs/generated/devkit/TargetConfiguration.md b/docs/generated/devkit/TargetConfiguration.md index a20cc4a053..ef01902f85 100644 --- a/docs/generated/devkit/TargetConfiguration.md +++ b/docs/generated/devkit/TargetConfiguration.md @@ -19,6 +19,7 @@ Target's configuration - [dependsOn](../../devkit/documents/TargetConfiguration#dependson): (string | TargetDependencyConfig)[] - [executor](../../devkit/documents/TargetConfiguration#executor): string - [inputs](../../devkit/documents/TargetConfiguration#inputs): (string | InputDefinition)[] +- [metadata](../../devkit/documents/TargetConfiguration#metadata): TargetMetadata - [options](../../devkit/documents/TargetConfiguration#options): T - [outputs](../../devkit/documents/TargetConfiguration#outputs): string[] @@ -86,6 +87,14 @@ This describes filesets, runtime dependencies and other inputs that a target dep --- +### metadata + +• `Optional` **metadata**: `TargetMetadata` + +Metadata about the target + +--- + ### options • `Optional` **options**: `T` diff --git a/packages/cypress/src/plugins/plugin.spec.ts b/packages/cypress/src/plugins/plugin.spec.ts index d6b511b94b..7754a4ffdd 100644 --- a/packages/cypress/src/plugins/plugin.spec.ts +++ b/packages/cypress/src/plugins/plugin.spec.ts @@ -3,6 +3,7 @@ import { defineConfig } from 'cypress'; import { createNodes } from './plugin'; import { TempFs } from 'nx/src/internal-testing-utils/temp-fs'; +import { resetWorkspaceContext } from 'nx/src/utils/workspace-context'; import { join } from 'path'; import { nxE2EPreset } from '../../plugins/cypress-preset'; @@ -16,9 +17,9 @@ describe('@nx/cypress/plugin', () => { await tempFs.createFiles({ 'package.json': '{}', + 'cypress.config.js': '', 'src/test.cy.ts': '', }); - process.chdir(tempFs.tempDir); context = { nxJsonConfiguration: { // These defaults should be overridden by plugin @@ -41,6 +42,11 @@ describe('@nx/cypress/plugin', () => { afterEach(() => { jest.resetModules(); tempFs.cleanup(); + tempFs = null; + }); + + afterAll(() => { + resetWorkspaceContext(); }); it('should add a target for e2e', async () => { @@ -70,11 +76,7 @@ describe('@nx/cypress/plugin', () => { { "projects": { ".": { - "metadata": { - "technologies": [ - "cypress", - ], - }, + "metadata": undefined, "projectType": "application", "targets": { "e2e": { @@ -94,6 +96,12 @@ describe('@nx/cypress/plugin', () => { ], }, ], + "metadata": { + "description": "Runs Cypress Tests", + "technologies": [ + "cypress", + ], + }, "options": { "cwd": ".", }, @@ -104,6 +112,12 @@ describe('@nx/cypress/plugin', () => { }, "open-cypress": { "command": "cypress open", + "metadata": { + "description": "Opens Cypress", + "technologies": [ + "cypress", + ], + }, "options": { "cwd": ".", }, @@ -140,11 +154,7 @@ describe('@nx/cypress/plugin', () => { { "projects": { ".": { - "metadata": { - "technologies": [ - "cypress", - ], - }, + "metadata": undefined, "projectType": "application", "targets": { "component-test": { @@ -159,6 +169,12 @@ describe('@nx/cypress/plugin', () => { ], }, ], + "metadata": { + "description": "Runs Cypress Component Tests", + "technologies": [ + "cypress", + ], + }, "options": { "cwd": ".", }, @@ -169,6 +185,12 @@ describe('@nx/cypress/plugin', () => { }, "open-cypress": { "command": "cypress open", + "metadata": { + "description": "Opens Cypress", + "technologies": [ + "cypress", + ], + }, "options": { "cwd": ".", }, @@ -184,16 +206,16 @@ describe('@nx/cypress/plugin', () => { mockCypressConfig( defineConfig({ e2e: { - specPattern: '**/*.cy.ts', - videosFolder: './dist/videos', - screenshotsFolder: './dist/screenshots', - ...nxE2EPreset('.', { + ...nxE2EPreset(join(tempFs.tempDir, 'cypress.config.js'), { webServerCommands: { default: 'my-app:serve', production: 'my-app:serve:production', }, ciWebServerCommand: 'my-app:serve-static', }), + specPattern: '**/*.cy.ts', + videosFolder: './dist/videos', + screenshotsFolder: './dist/screenshots', }, }) ); @@ -216,9 +238,6 @@ describe('@nx/cypress/plugin', () => { "e2e-ci", ], }, - "technologies": [ - "cypress", - ], }, "projectType": "application", "targets": { @@ -239,12 +258,18 @@ describe('@nx/cypress/plugin', () => { ], }, ], + "metadata": { + "description": "Runs Cypress Tests", + "technologies": [ + "cypress", + ], + }, "options": { "cwd": ".", }, "outputs": [ - "{projectRoot}/dist/cypress/videos", - "{projectRoot}/dist/cypress/screenshots", + "{projectRoot}/dist/videos", + "{projectRoot}/dist/screenshots", ], }, "e2e-ci": { @@ -266,9 +291,15 @@ describe('@nx/cypress/plugin', () => { ], }, ], + "metadata": { + "description": "Runs Cypress Tests in CI", + "technologies": [ + "cypress", + ], + }, "outputs": [ - "{projectRoot}/dist/cypress/videos", - "{projectRoot}/dist/cypress/screenshots", + "{projectRoot}/dist/videos", + "{projectRoot}/dist/screenshots", ], }, "e2e-ci--src/test.cy.ts": { @@ -283,16 +314,28 @@ describe('@nx/cypress/plugin', () => { ], }, ], + "metadata": { + "description": "Runs Cypress Tests in src/test.cy.ts in CI", + "technologies": [ + "cypress", + ], + }, "options": { "cwd": ".", }, "outputs": [ - "{projectRoot}/dist/cypress/videos", - "{projectRoot}/dist/cypress/screenshots", + "{projectRoot}/dist/videos", + "{projectRoot}/dist/screenshots", ], }, "open-cypress": { "command": "cypress open", + "metadata": { + "description": "Opens Cypress", + "technologies": [ + "cypress", + ], + }, "options": { "cwd": ".", }, diff --git a/packages/cypress/src/plugins/plugin.ts b/packages/cypress/src/plugins/plugin.ts index 5229fd9a6b..dba51d94a2 100644 --- a/packages/cypress/src/plugins/plugin.ts +++ b/packages/cypress/src/plugins/plugin.ts @@ -67,7 +67,7 @@ export const createNodes: CreateNodes = [ getLockFileName(detectPackageManager(context.workspaceRoot)), ]); - const { targets, targetGroups } = targetsCache[hash] + const { targets, metadata } = targetsCache[hash] ? targetsCache[hash] : await buildCypressTargets( configFilePath, @@ -76,20 +76,14 @@ export const createNodes: CreateNodes = [ context ); - calculatedTargets[hash] = { targets, targetGroups }; + calculatedTargets[hash] = { targets, metadata }; const project: Omit = { projectType: 'application', targets, - metadata: { - technologies: ['cypress'], - }, + metadata, }; - if (targetGroups) { - project.metadata.targetGroups = targetGroups; - } - return { projects: { [projectRoot]: project, @@ -146,10 +140,7 @@ function getOutputs( return outputs; } -interface CypressTargets { - targets: Record; - targetGroups: Record | null; -} +type CypressTargets = Pick; async function buildCypressTargets( configFilePath: string, @@ -173,7 +164,7 @@ async function buildCypressTargets( const namedInputs = getNamedInputs(projectRoot, context); const targets: Record = {}; - let targetGroups: Record; + let metadata: ProjectConfiguration['metadata']; if ('e2e' in cypressConfig) { targets[options.targetName] = { @@ -182,6 +173,10 @@ async function buildCypressTargets( cache: true, inputs: getInputs(namedInputs), outputs: getOutputs(projectRoot, cypressConfig, 'e2e'), + metadata: { + technologies: ['cypress'], + description: 'Runs Cypress Tests', + }, }; if (webServerCommands?.default) { @@ -222,8 +217,8 @@ async function buildCypressTargets( const inputs = getInputs(namedInputs); const groupName = 'E2E (CI)'; - targetGroups = { [groupName]: [] }; - const ciTargetGroup = targetGroups[groupName]; + metadata = { targetGroups: { [groupName]: [] } }; + const ciTargetGroup = metadata.targetGroups[groupName]; for (const file of specFiles) { const relativeSpecFilePath = normalizePath(relative(projectRoot, file)); const targetName = options.ciTargetName + '--' + relativeSpecFilePath; @@ -237,6 +232,10 @@ async function buildCypressTargets( options: { cwd: projectRoot, }, + metadata: { + technologies: ['cypress'], + description: `Runs Cypress Tests in ${relativeSpecFilePath} in CI`, + }, }; dependsOn.push({ target: targetName, @@ -251,10 +250,12 @@ async function buildCypressTargets( inputs, outputs, dependsOn, + metadata: { + technologies: ['cypress'], + description: 'Runs Cypress Tests in CI', + }, }; ciTargetGroup.push(options.ciTargetName); - } else { - targetGroups = null; } } @@ -266,15 +267,23 @@ async function buildCypressTargets( cache: true, inputs: getInputs(namedInputs), outputs: getOutputs(projectRoot, cypressConfig, 'component'), + metadata: { + technologies: ['cypress'], + description: 'Runs Cypress Component Tests', + }, }; } targets[options.openTargetName] = { command: `cypress open`, options: { cwd: projectRoot }, + metadata: { + technologies: ['cypress'], + description: 'Opens Cypress', + }, }; - return { targets, targetGroups }; + return { targets, metadata }; } function normalizeOptions(options: CypressPluginOptions): CypressPluginOptions { diff --git a/packages/nx/src/config/workspace-json-project-json.ts b/packages/nx/src/config/workspace-json-project-json.ts index d71d267614..83b3ea352e 100644 --- a/packages/nx/src/config/workspace-json-project-json.ts +++ b/packages/nx/src/config/workspace-json-project-json.ts @@ -111,10 +111,21 @@ export interface ProjectConfiguration { 'generator' | 'generatorOptions' >; }; - metadata?: { - technologies?: string[]; - targetGroups?: Record; - }; + + /** + * Metadata about the project + */ + metadata?: ProjectMetadata; +} + +export interface ProjectMetadata { + technologies?: string[]; + targetGroups?: Record; +} + +export interface TargetMetadata { + description?: string; + technologies?: string[]; } export interface TargetDependencyConfig { @@ -203,4 +214,9 @@ export interface TargetConfiguration { * Determines if Nx is able to cache a given target. */ cache?: boolean; + + /** + * Metadata about the target + */ + metadata?: TargetMetadata; } 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 c47040d1e7..fa1853f820 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 @@ -444,6 +444,101 @@ describe('project-configuration-utils', () => { expect(result.cache).not.toBeDefined(); }); }); + + describe('metadata', () => { + it('should be added', () => { + 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', + targets: { + build: { + metadata: { + description: 'do stuff', + technologies: ['tech'], + }, + }, + }, + }, + sourceMap, + ['dummy', 'dummy.ts'] + ); + + expect(rootMap.get('libs/lib-a').targets.build.metadata).toEqual({ + description: 'do stuff', + technologies: ['tech'], + }); + expect(sourceMap['libs/lib-a']).toMatchObject({ + 'targets.build.metadata.description': ['dummy', 'dummy.ts'], + 'targets.build.metadata.technologies': ['dummy', 'dummy.ts'], + 'targets.build.metadata.technologies.0': ['dummy', 'dummy.ts'], + }); + }); + + it('should be merged', () => { + const rootMap = new RootMapBuilder() + .addProject({ + root: 'libs/lib-a', + name: 'lib-a', + targets: { + build: { + metadata: { + description: 'do stuff', + technologies: ['tech'], + }, + }, + }, + }) + .getRootMap(); + const sourceMap: ConfigurationSourceMaps = { + 'libs/lib-a': { + 'targets.build.metadata.technologies': ['existing', 'existing.ts'], + 'targets.build.metadata.technologies.0': [ + 'existing', + 'existing.ts', + ], + }, + }; + mergeProjectConfigurationIntoRootMap( + rootMap, + { + root: 'libs/lib-a', + name: 'lib-a', + targets: { + build: { + metadata: { + description: 'do cool stuff', + technologies: ['tech2'], + }, + }, + }, + }, + sourceMap, + ['dummy', 'dummy.ts'] + ); + + expect(rootMap.get('libs/lib-a').targets.build.metadata).toEqual({ + description: 'do cool stuff', + technologies: ['tech', 'tech2'], + }); + expect(sourceMap['libs/lib-a']).toMatchObject({ + 'targets.build.metadata.description': ['dummy', 'dummy.ts'], + 'targets.build.metadata.technologies': ['existing', 'existing.ts'], + 'targets.build.metadata.technologies.0': ['existing', 'existing.ts'], + 'targets.build.metadata.technologies.1': ['dummy', 'dummy.ts'], + }); + }); + }); }); describe('mergeProjectConfigurationIntoRootMap', () => { 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 22b2d38a15..a38fe90e87 100644 --- a/packages/nx/src/project-graph/utils/project-configuration-utils.ts +++ b/packages/nx/src/project-graph/utils/project-configuration-utils.ts @@ -2,7 +2,9 @@ import { NxJsonConfiguration, TargetDefaults } from '../../config/nx-json'; import { ProjectGraphExternalNode } from '../../config/project-graph'; import { ProjectConfiguration, + ProjectMetadata, TargetConfiguration, + TargetMetadata, } from '../../config/workspace-json-project-json'; import { NX_PREFIX } from '../../utils/logger'; import { CreateNodesResult, LoadedNxPlugin } from '../../utils/nx-plugin'; @@ -149,6 +151,16 @@ export function mergeProjectConfigurationIntoRootMap( } } + if (project.metadata) { + updatedProjectConfiguration.metadata = 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. @@ -195,85 +207,85 @@ 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]; + projectRootMap.set( + updatedProjectConfiguration.root, + updatedProjectConfiguration + ); +} - if (Array.isArray(value) && Array.isArray(existingValue)) { - for (const item of [...value]) { - const newLength = - updatedProjectConfiguration.metadata[metadataKey].push(item); +function mergeMetadata( + sourceMap: Record, + sourceInformation: [file: string, plugin: string], + baseSourceMapPath: string, + metadata: T, + matchingMetadata?: T +): T { + const result: T = { + ...(matchingMetadata ?? ({} as T)), + }; + for (const [metadataKey, value] of Object.entries(metadata)) { + const existingValue = matchingMetadata?.[metadataKey]; + + if (Array.isArray(value) && Array.isArray(existingValue)) { + for (const item of [...value]) { + const newLength = result[metadataKey].push(item); + if (sourceMap) { + sourceMap[`${baseSourceMapPath}.${metadataKey}.${newLength - 1}`] = + sourceInformation; + } + } + } else if (Array.isArray(value) && existingValue === undefined) { + result[metadataKey] ??= value; + if (sourceMap) { + sourceMap[`${baseSourceMapPath}.${metadataKey}`] = sourceInformation; + } + for (let i = 0; i < value.length; i++) { + if (sourceMap) { + sourceMap[`${baseSourceMapPath}.${metadataKey}.${i}`] = + sourceInformation; + } + } + } else if (typeof value === 'object' && typeof existingValue === 'object') { + for (const key in value) { + const existingValue = matchingMetadata?.[metadataKey]?.[key]; + + if (Array.isArray(value[key]) && Array.isArray(existingValue)) { + for (const item of value[key]) { + const i = result[metadataKey][key].push(item); + if (sourceMap) { + sourceMap[`${baseSourceMapPath}.${metadataKey}.${key}.${i - 1}`] = + sourceInformation; + } + } + } else { + result[metadataKey] = value; if (sourceMap) { - sourceMap[`metadata.${metadataKey}.${newLength - 1}`] = + sourceMap[`${baseSourceMapPath}.${metadataKey}`] = 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]; + } + } else { + result[metadataKey] = value; + if (sourceMap) { + sourceMap[`${baseSourceMapPath}.${metadataKey}`] = sourceInformation; - if (Array.isArray(value[key]) && Array.isArray(existingValue)) { - for (const item of value[key]) { - const i = - updatedProjectConfiguration.metadata[metadataKey][key].push( - item - ); - if (sourceMap) { - sourceMap[`metadata.${metadataKey}.${key}.${i - 1}`] = + if (typeof value === 'object') { + for (const k in value) { + sourceMap[`${baseSourceMapPath}.${metadataKey}.${k}`] = + sourceInformation; + if (Array.isArray(value[k])) { + for (let i = 0; i < value[k].length; i++) { + sourceMap[`${baseSourceMapPath}.${metadataKey}.${k}.${i}`] = sourceInformation; } } - } else { - updatedProjectConfiguration.metadata[metadataKey] = value; - if (sourceMap) { - sourceMap[`metadata.${metadataKey}`] = sourceInformation; - } - } - } - } else { - updatedProjectConfiguration.metadata[metadataKey] = value; - if (sourceMap) { - sourceMap[`metadata.${metadataKey}`] = sourceInformation; - - if (typeof value === 'object') { - for (const k in value) { - sourceMap[`metadata.${metadataKey}.${k}`] = sourceInformation; - if (Array.isArray(value[k])) { - for (let i = 0; i < value[k].length; i++) { - sourceMap[`metadata.${metadataKey}.${k}.${i}`] = - sourceInformation; - } - } - } } } } } } - - projectRootMap.set( - updatedProjectConfiguration.root, - updatedProjectConfiguration - ); + return result; } export type ConfigurationResult = { @@ -689,6 +701,17 @@ export function mergeTargetConfigurations( targetIdentifier ); } + + if (target.metadata) { + result.metadata = mergeMetadata( + projectConfigSourceMap, + sourceInformation, + `${targetIdentifier}.metadata`, + target.metadata, + baseTarget?.metadata + ); + } + return result as TargetConfiguration; }