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
- [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
---

View File

@ -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`

View File

@ -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": ".",
},

View File

@ -67,7 +67,7 @@ export const createNodes: CreateNodes<CypressPluginOptions> = [
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<CypressPluginOptions> = [
context
);
calculatedTargets[hash] = { targets, targetGroups };
calculatedTargets[hash] = { targets, metadata };
const project: Omit<ProjectConfiguration, 'root'> = {
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<string, TargetConfiguration>;
targetGroups: Record<string, string[]> | null;
}
type CypressTargets = Pick<ProjectConfiguration, 'targets' | 'metadata'>;
async function buildCypressTargets(
configFilePath: string,
@ -173,7 +164,7 @@ async function buildCypressTargets(
const namedInputs = getNamedInputs(projectRoot, context);
const targets: Record<string, TargetConfiguration> = {};
let targetGroups: Record<string, string[]>;
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 {

View File

@ -111,10 +111,21 @@ export interface ProjectConfiguration {
'generator' | 'generatorOptions'
>;
};
metadata?: {
/**
* 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 {
@ -203,4 +214,9 @@ export interface TargetConfiguration<T = any> {
* Determines if Nx is able to cache a given target.
*/
cache?: boolean;
/**
* Metadata about the target
*/
metadata?: TargetMetadata;
}

View File

@ -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', () => {

View File

@ -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,87 +207,87 @@ 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][key].push(
item
);
if (sourceMap) {
sourceMap[`metadata.${metadataKey}.${key}.${i - 1}`] =
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
);
}
function mergeMetadata<T = ProjectMetadata | TargetMetadata>(
sourceMap: Record<string, [file: string, plugin: string]>,
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[`${baseSourceMapPath}.${metadataKey}`] =
sourceInformation;
}
}
}
} else {
result[metadataKey] = value;
if (sourceMap) {
sourceMap[`${baseSourceMapPath}.${metadataKey}`] = sourceInformation;
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;
}
}
}
}
}
}
}
return result;
}
export type ConfigurationResult = {
projects: Record<string, ProjectConfiguration>;
externalNodes: Record<string, ProjectGraphExternalNode>;
@ -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;
}