feat(core): add metadata to targets (#22655)

This commit is contained in:
Jason Jean 2024-04-04 23:19:10 -04:00 committed by GitHub
parent fc8d5ba828
commit 21f90cae85
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 310 additions and 120 deletions

View File

@ -8,7 +8,7 @@ Project configuration
- [generators](../../devkit/documents/ProjectConfiguration#generators): Object - [generators](../../devkit/documents/ProjectConfiguration#generators): Object
- [implicitDependencies](../../devkit/documents/ProjectConfiguration#implicitdependencies): string[] - [implicitDependencies](../../devkit/documents/ProjectConfiguration#implicitdependencies): string[]
- [metadata](../../devkit/documents/ProjectConfiguration#metadata): Object - [metadata](../../devkit/documents/ProjectConfiguration#metadata): ProjectMetadata
- [name](../../devkit/documents/ProjectConfiguration#name): string - [name](../../devkit/documents/ProjectConfiguration#name): string
- [namedInputs](../../devkit/documents/ProjectConfiguration#namedinputs): Object - [namedInputs](../../devkit/documents/ProjectConfiguration#namedinputs): Object
- [projectType](../../devkit/documents/ProjectConfiguration#projecttype): ProjectType - [projectType](../../devkit/documents/ProjectConfiguration#projecttype): ProjectType
@ -56,14 +56,9 @@ List of projects which are added as a dependency
### metadata ### metadata
`Optional` **metadata**: `Object` `Optional` **metadata**: `ProjectMetadata`
#### Type declaration Metadata about the project
| Name | Type |
| :-------------- | :------------------------------- |
| `targetGroups?` | `Record`\<`string`, `string`[]\> |
| `technologies?` | `string`[] |
--- ---

View File

@ -19,6 +19,7 @@ Target's configuration
- [dependsOn](../../devkit/documents/TargetConfiguration#dependson): (string | TargetDependencyConfig)[] - [dependsOn](../../devkit/documents/TargetConfiguration#dependson): (string | TargetDependencyConfig)[]
- [executor](../../devkit/documents/TargetConfiguration#executor): string - [executor](../../devkit/documents/TargetConfiguration#executor): string
- [inputs](../../devkit/documents/TargetConfiguration#inputs): (string | InputDefinition)[] - [inputs](../../devkit/documents/TargetConfiguration#inputs): (string | InputDefinition)[]
- [metadata](../../devkit/documents/TargetConfiguration#metadata): TargetMetadata
- [options](../../devkit/documents/TargetConfiguration#options): T - [options](../../devkit/documents/TargetConfiguration#options): T
- [outputs](../../devkit/documents/TargetConfiguration#outputs): string[] - [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 ### options
`Optional` **options**: `T` `Optional` **options**: `T`

View File

@ -3,6 +3,7 @@ import { defineConfig } from 'cypress';
import { createNodes } from './plugin'; import { createNodes } from './plugin';
import { TempFs } from 'nx/src/internal-testing-utils/temp-fs'; import { TempFs } from 'nx/src/internal-testing-utils/temp-fs';
import { resetWorkspaceContext } from 'nx/src/utils/workspace-context';
import { join } from 'path'; import { join } from 'path';
import { nxE2EPreset } from '../../plugins/cypress-preset'; import { nxE2EPreset } from '../../plugins/cypress-preset';
@ -16,9 +17,9 @@ describe('@nx/cypress/plugin', () => {
await tempFs.createFiles({ await tempFs.createFiles({
'package.json': '{}', 'package.json': '{}',
'cypress.config.js': '',
'src/test.cy.ts': '', 'src/test.cy.ts': '',
}); });
process.chdir(tempFs.tempDir);
context = { context = {
nxJsonConfiguration: { nxJsonConfiguration: {
// These defaults should be overridden by plugin // These defaults should be overridden by plugin
@ -41,6 +42,11 @@ describe('@nx/cypress/plugin', () => {
afterEach(() => { afterEach(() => {
jest.resetModules(); jest.resetModules();
tempFs.cleanup(); tempFs.cleanup();
tempFs = null;
});
afterAll(() => {
resetWorkspaceContext();
}); });
it('should add a target for e2e', async () => { it('should add a target for e2e', async () => {
@ -70,11 +76,7 @@ describe('@nx/cypress/plugin', () => {
{ {
"projects": { "projects": {
".": { ".": {
"metadata": { "metadata": undefined,
"technologies": [
"cypress",
],
},
"projectType": "application", "projectType": "application",
"targets": { "targets": {
"e2e": { "e2e": {
@ -94,6 +96,12 @@ describe('@nx/cypress/plugin', () => {
], ],
}, },
], ],
"metadata": {
"description": "Runs Cypress Tests",
"technologies": [
"cypress",
],
},
"options": { "options": {
"cwd": ".", "cwd": ".",
}, },
@ -104,6 +112,12 @@ describe('@nx/cypress/plugin', () => {
}, },
"open-cypress": { "open-cypress": {
"command": "cypress open", "command": "cypress open",
"metadata": {
"description": "Opens Cypress",
"technologies": [
"cypress",
],
},
"options": { "options": {
"cwd": ".", "cwd": ".",
}, },
@ -140,11 +154,7 @@ describe('@nx/cypress/plugin', () => {
{ {
"projects": { "projects": {
".": { ".": {
"metadata": { "metadata": undefined,
"technologies": [
"cypress",
],
},
"projectType": "application", "projectType": "application",
"targets": { "targets": {
"component-test": { "component-test": {
@ -159,6 +169,12 @@ describe('@nx/cypress/plugin', () => {
], ],
}, },
], ],
"metadata": {
"description": "Runs Cypress Component Tests",
"technologies": [
"cypress",
],
},
"options": { "options": {
"cwd": ".", "cwd": ".",
}, },
@ -169,6 +185,12 @@ describe('@nx/cypress/plugin', () => {
}, },
"open-cypress": { "open-cypress": {
"command": "cypress open", "command": "cypress open",
"metadata": {
"description": "Opens Cypress",
"technologies": [
"cypress",
],
},
"options": { "options": {
"cwd": ".", "cwd": ".",
}, },
@ -184,16 +206,16 @@ describe('@nx/cypress/plugin', () => {
mockCypressConfig( mockCypressConfig(
defineConfig({ defineConfig({
e2e: { e2e: {
specPattern: '**/*.cy.ts', ...nxE2EPreset(join(tempFs.tempDir, 'cypress.config.js'), {
videosFolder: './dist/videos',
screenshotsFolder: './dist/screenshots',
...nxE2EPreset('.', {
webServerCommands: { webServerCommands: {
default: 'my-app:serve', default: 'my-app:serve',
production: 'my-app:serve:production', production: 'my-app:serve:production',
}, },
ciWebServerCommand: 'my-app:serve-static', ciWebServerCommand: 'my-app:serve-static',
}), }),
specPattern: '**/*.cy.ts',
videosFolder: './dist/videos',
screenshotsFolder: './dist/screenshots',
}, },
}) })
); );
@ -216,9 +238,6 @@ describe('@nx/cypress/plugin', () => {
"e2e-ci", "e2e-ci",
], ],
}, },
"technologies": [
"cypress",
],
}, },
"projectType": "application", "projectType": "application",
"targets": { "targets": {
@ -239,12 +258,18 @@ describe('@nx/cypress/plugin', () => {
], ],
}, },
], ],
"metadata": {
"description": "Runs Cypress Tests",
"technologies": [
"cypress",
],
},
"options": { "options": {
"cwd": ".", "cwd": ".",
}, },
"outputs": [ "outputs": [
"{projectRoot}/dist/cypress/videos", "{projectRoot}/dist/videos",
"{projectRoot}/dist/cypress/screenshots", "{projectRoot}/dist/screenshots",
], ],
}, },
"e2e-ci": { "e2e-ci": {
@ -266,9 +291,15 @@ describe('@nx/cypress/plugin', () => {
], ],
}, },
], ],
"metadata": {
"description": "Runs Cypress Tests in CI",
"technologies": [
"cypress",
],
},
"outputs": [ "outputs": [
"{projectRoot}/dist/cypress/videos", "{projectRoot}/dist/videos",
"{projectRoot}/dist/cypress/screenshots", "{projectRoot}/dist/screenshots",
], ],
}, },
"e2e-ci--src/test.cy.ts": { "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": { "options": {
"cwd": ".", "cwd": ".",
}, },
"outputs": [ "outputs": [
"{projectRoot}/dist/cypress/videos", "{projectRoot}/dist/videos",
"{projectRoot}/dist/cypress/screenshots", "{projectRoot}/dist/screenshots",
], ],
}, },
"open-cypress": { "open-cypress": {
"command": "cypress open", "command": "cypress open",
"metadata": {
"description": "Opens Cypress",
"technologies": [
"cypress",
],
},
"options": { "options": {
"cwd": ".", "cwd": ".",
}, },

View File

@ -67,7 +67,7 @@ export const createNodes: CreateNodes<CypressPluginOptions> = [
getLockFileName(detectPackageManager(context.workspaceRoot)), getLockFileName(detectPackageManager(context.workspaceRoot)),
]); ]);
const { targets, targetGroups } = targetsCache[hash] const { targets, metadata } = targetsCache[hash]
? targetsCache[hash] ? targetsCache[hash]
: await buildCypressTargets( : await buildCypressTargets(
configFilePath, configFilePath,
@ -76,20 +76,14 @@ export const createNodes: CreateNodes<CypressPluginOptions> = [
context context
); );
calculatedTargets[hash] = { targets, targetGroups }; calculatedTargets[hash] = { targets, metadata };
const project: Omit<ProjectConfiguration, 'root'> = { const project: Omit<ProjectConfiguration, 'root'> = {
projectType: 'application', projectType: 'application',
targets, targets,
metadata: { metadata,
technologies: ['cypress'],
},
}; };
if (targetGroups) {
project.metadata.targetGroups = targetGroups;
}
return { return {
projects: { projects: {
[projectRoot]: project, [projectRoot]: project,
@ -146,10 +140,7 @@ function getOutputs(
return outputs; return outputs;
} }
interface CypressTargets { type CypressTargets = Pick<ProjectConfiguration, 'targets' | 'metadata'>;
targets: Record<string, TargetConfiguration>;
targetGroups: Record<string, string[]> | null;
}
async function buildCypressTargets( async function buildCypressTargets(
configFilePath: string, configFilePath: string,
@ -173,7 +164,7 @@ async function buildCypressTargets(
const namedInputs = getNamedInputs(projectRoot, context); const namedInputs = getNamedInputs(projectRoot, context);
const targets: Record<string, TargetConfiguration> = {}; const targets: Record<string, TargetConfiguration> = {};
let targetGroups: Record<string, string[]>; let metadata: ProjectConfiguration['metadata'];
if ('e2e' in cypressConfig) { if ('e2e' in cypressConfig) {
targets[options.targetName] = { targets[options.targetName] = {
@ -182,6 +173,10 @@ async function buildCypressTargets(
cache: true, cache: true,
inputs: getInputs(namedInputs), inputs: getInputs(namedInputs),
outputs: getOutputs(projectRoot, cypressConfig, 'e2e'), outputs: getOutputs(projectRoot, cypressConfig, 'e2e'),
metadata: {
technologies: ['cypress'],
description: 'Runs Cypress Tests',
},
}; };
if (webServerCommands?.default) { if (webServerCommands?.default) {
@ -222,8 +217,8 @@ async function buildCypressTargets(
const inputs = getInputs(namedInputs); const inputs = getInputs(namedInputs);
const groupName = 'E2E (CI)'; const groupName = 'E2E (CI)';
targetGroups = { [groupName]: [] }; metadata = { targetGroups: { [groupName]: [] } };
const ciTargetGroup = targetGroups[groupName]; const ciTargetGroup = metadata.targetGroups[groupName];
for (const file of specFiles) { for (const file of specFiles) {
const relativeSpecFilePath = normalizePath(relative(projectRoot, file)); const relativeSpecFilePath = normalizePath(relative(projectRoot, file));
const targetName = options.ciTargetName + '--' + relativeSpecFilePath; const targetName = options.ciTargetName + '--' + relativeSpecFilePath;
@ -237,6 +232,10 @@ async function buildCypressTargets(
options: { options: {
cwd: projectRoot, cwd: projectRoot,
}, },
metadata: {
technologies: ['cypress'],
description: `Runs Cypress Tests in ${relativeSpecFilePath} in CI`,
},
}; };
dependsOn.push({ dependsOn.push({
target: targetName, target: targetName,
@ -251,10 +250,12 @@ async function buildCypressTargets(
inputs, inputs,
outputs, outputs,
dependsOn, dependsOn,
metadata: {
technologies: ['cypress'],
description: 'Runs Cypress Tests in CI',
},
}; };
ciTargetGroup.push(options.ciTargetName); ciTargetGroup.push(options.ciTargetName);
} else {
targetGroups = null;
} }
} }
@ -266,15 +267,23 @@ async function buildCypressTargets(
cache: true, cache: true,
inputs: getInputs(namedInputs), inputs: getInputs(namedInputs),
outputs: getOutputs(projectRoot, cypressConfig, 'component'), outputs: getOutputs(projectRoot, cypressConfig, 'component'),
metadata: {
technologies: ['cypress'],
description: 'Runs Cypress Component Tests',
},
}; };
} }
targets[options.openTargetName] = { targets[options.openTargetName] = {
command: `cypress open`, command: `cypress open`,
options: { cwd: projectRoot }, options: { cwd: projectRoot },
metadata: {
technologies: ['cypress'],
description: 'Opens Cypress',
},
}; };
return { targets, targetGroups }; return { targets, metadata };
} }
function normalizeOptions(options: CypressPluginOptions): CypressPluginOptions { function normalizeOptions(options: CypressPluginOptions): CypressPluginOptions {

View File

@ -111,10 +111,21 @@ export interface ProjectConfiguration {
'generator' | 'generatorOptions' 'generator' | 'generatorOptions'
>; >;
}; };
metadata?: {
technologies?: string[]; /**
targetGroups?: Record<string, string[]>; * Metadata about the project
}; */
metadata?: ProjectMetadata;
}
export interface ProjectMetadata {
technologies?: string[];
targetGroups?: Record<string, string[]>;
}
export interface TargetMetadata {
description?: string;
technologies?: string[];
} }
export interface TargetDependencyConfig { export interface TargetDependencyConfig {
@ -203,4 +214,9 @@ export interface TargetConfiguration<T = any> {
* Determines if Nx is able to cache a given target. * Determines if Nx is able to cache a given target.
*/ */
cache?: boolean; cache?: boolean;
/**
* Metadata about the target
*/
metadata?: TargetMetadata;
} }

View File

@ -444,6 +444,101 @@ describe('project-configuration-utils', () => {
expect(result.cache).not.toBeDefined(); 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', () => { describe('mergeProjectConfigurationIntoRootMap', () => {

View File

@ -2,7 +2,9 @@ import { NxJsonConfiguration, TargetDefaults } from '../../config/nx-json';
import { ProjectGraphExternalNode } from '../../config/project-graph'; import { ProjectGraphExternalNode } from '../../config/project-graph';
import { import {
ProjectConfiguration, ProjectConfiguration,
ProjectMetadata,
TargetConfiguration, TargetConfiguration,
TargetMetadata,
} from '../../config/workspace-json-project-json'; } from '../../config/workspace-json-project-json';
import { NX_PREFIX } from '../../utils/logger'; import { NX_PREFIX } from '../../utils/logger';
import { CreateNodesResult, LoadedNxPlugin } from '../../utils/nx-plugin'; 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) { if (project.targets) {
// We merge the targets with special handling, so clear this back to the // We merge the targets with special handling, so clear this back to the
// targets as defined originally before merging. // targets as defined originally before merging.
@ -195,85 +207,85 @@ export function mergeProjectConfigurationIntoRootMap(
} }
} }
if (project.metadata) { projectRootMap.set(
if (sourceMap) { updatedProjectConfiguration.root,
sourceMap['targets'] ??= sourceInformation; updatedProjectConfiguration
} );
for (const [metadataKey, value] of Object.entries({ }
...project.metadata,
})) {
const existingValue = matchingProject.metadata?.[metadataKey];
if (Array.isArray(value) && Array.isArray(existingValue)) { function mergeMetadata<T = ProjectMetadata | TargetMetadata>(
for (const item of [...value]) { sourceMap: Record<string, [file: string, plugin: string]>,
const newLength = sourceInformation: [file: string, plugin: string],
updatedProjectConfiguration.metadata[metadataKey].push(item); 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) { if (sourceMap) {
sourceMap[`metadata.${metadataKey}.${newLength - 1}`] = sourceMap[`${baseSourceMapPath}.${metadataKey}`] =
sourceInformation; sourceInformation;
} }
} }
} else if (Array.isArray(value) && existingValue === undefined) { }
updatedProjectConfiguration.metadata ??= {}; } else {
updatedProjectConfiguration.metadata[metadataKey] ??= value; result[metadataKey] = value;
if (sourceMap) { if (sourceMap) {
sourceMap[`metadata.${metadataKey}`] = sourceInformation; sourceMap[`${baseSourceMapPath}.${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)) { if (typeof value === 'object') {
for (const item of value[key]) { for (const k in value) {
const i = sourceMap[`${baseSourceMapPath}.${metadataKey}.${k}`] =
updatedProjectConfiguration.metadata[metadataKey][key].push( sourceInformation;
item if (Array.isArray(value[k])) {
); for (let i = 0; i < value[k].length; i++) {
if (sourceMap) { sourceMap[`${baseSourceMapPath}.${metadataKey}.${k}.${i}`] =
sourceMap[`metadata.${metadataKey}.${key}.${i - 1}`] =
sourceInformation; 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;
}
}
}
} }
} }
} }
} }
} }
return result;
projectRootMap.set(
updatedProjectConfiguration.root,
updatedProjectConfiguration
);
} }
export type ConfigurationResult = { export type ConfigurationResult = {
@ -689,6 +701,17 @@ export function mergeTargetConfigurations(
targetIdentifier targetIdentifier
); );
} }
if (target.metadata) {
result.metadata = mergeMetadata(
projectConfigSourceMap,
sourceInformation,
`${targetIdentifier}.metadata`,
target.metadata,
baseTarget?.metadata
);
}
return result as TargetConfiguration; return result as TargetConfiguration;
} }