feat(core): add ability to add metadata to projects (#22299)

This commit is contained in:
Jason Jean 2024-03-19 13:33:25 -04:00 committed by GitHub
parent 9f3af7051b
commit 5a9671b3fa
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 349 additions and 30 deletions

View File

@ -8,6 +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
- [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
@ -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 ### name
`Optional` **name**: `string` `Optional` **name**: `string`

View File

@ -70,6 +70,11 @@ describe('@nx/cypress/plugin', () => {
{ {
"projects": { "projects": {
".": { ".": {
"metadata": {
"technologies": [
"cypress",
],
},
"projectType": "application", "projectType": "application",
"targets": { "targets": {
"e2e": { "e2e": {
@ -129,6 +134,11 @@ describe('@nx/cypress/plugin', () => {
{ {
"projects": { "projects": {
".": { ".": {
"metadata": {
"technologies": [
"cypress",
],
},
"projectType": "application", "projectType": "application",
"targets": { "targets": {
"component-test": { "component-test": {
@ -187,6 +197,17 @@ describe('@nx/cypress/plugin', () => {
{ {
"projects": { "projects": {
".": { ".": {
"metadata": {
"targetGroups": {
".:e2e-ci": [
"e2e-ci--src/test.cy.ts",
"e2e-ci",
],
},
"technologies": [
"cypress",
],
},
"projectType": "application", "projectType": "application",
"targets": { "targets": {
"e2e": { "e2e": {

View File

@ -3,7 +3,10 @@ import {
CreateNodes, CreateNodes,
CreateNodesContext, CreateNodesContext,
detectPackageManager, detectPackageManager,
joinPathFragments,
normalizePath,
NxJsonConfiguration, NxJsonConfiguration,
ProjectConfiguration,
readJsonFile, readJsonFile,
TargetConfiguration, TargetConfiguration,
writeJsonFile, writeJsonFile,
@ -12,7 +15,6 @@ import { dirname, join, relative } from 'path';
import { getLockFileName } from '@nx/js'; import { getLockFileName } from '@nx/js';
import { CypressExecutorOptions } from '../executors/cypress/cypress.impl';
import { getNamedInputs } from '@nx/devkit/src/utils/get-named-inputs'; import { getNamedInputs } from '@nx/devkit/src/utils/get-named-inputs';
import { existsSync, readdirSync } from 'fs'; import { existsSync, readdirSync } from 'fs';
import { globWithWorkspaceContext } from 'nx/src/utils/workspace-context'; import { globWithWorkspaceContext } from 'nx/src/utils/workspace-context';
@ -30,24 +32,13 @@ export interface CypressPluginOptions {
const cachePath = join(projectGraphCacheDirectory, 'cypress.hash'); const cachePath = join(projectGraphCacheDirectory, 'cypress.hash');
const targetsCache = existsSync(cachePath) ? readTargetsCache() : {}; const targetsCache = existsSync(cachePath) ? readTargetsCache() : {};
const calculatedTargets: Record< const calculatedTargets: Record<string, CypressTargets> = {};
string,
Record<string, TargetConfiguration>
> = {};
function readTargetsCache(): Record< function readTargetsCache(): Record<string, CypressTargets> {
string,
Record<string, TargetConfiguration<CypressExecutorOptions>>
> {
return readJsonFile(cachePath); return readJsonFile(cachePath);
} }
function writeTargetsToCache( function writeTargetsToCache(targets: Record<string, CypressTargets>) {
targets: Record<
string,
Record<string, TargetConfiguration<CypressExecutorOptions>>
>
) {
writeJsonFile(cachePath, targets); writeJsonFile(cachePath, targets);
} }
@ -75,7 +66,7 @@ export const createNodes: CreateNodes<CypressPluginOptions> = [
getLockFileName(detectPackageManager(context.workspaceRoot)), getLockFileName(detectPackageManager(context.workspaceRoot)),
]); ]);
const targets = targetsCache[hash] const { targets, ciTestingGroup } = targetsCache[hash]
? targetsCache[hash] ? targetsCache[hash]
: await buildCypressTargets( : await buildCypressTargets(
configFilePath, configFilePath,
@ -84,14 +75,25 @@ export const createNodes: CreateNodes<CypressPluginOptions> = [
context context
); );
calculatedTargets[hash] = targets; calculatedTargets[hash] = { targets, ciTestingGroup };
const project: Omit<ProjectConfiguration, 'root'> = {
projectType: 'application',
targets,
metadata: {
technologies: ['cypress'],
},
};
if (ciTestingGroup) {
project.metadata.targetGroups = {
[`${projectRoot}:e2e-ci`]: ciTestingGroup,
};
}
return { return {
projects: { projects: {
[projectRoot]: { [projectRoot]: project,
projectType: 'application',
targets,
},
}, },
}; };
}, },
@ -104,9 +106,9 @@ function getOutputs(
): string[] { ): string[] {
function getOutput(path: string): string { function getOutput(path: string): string {
if (path.startsWith('..')) { if (path.startsWith('..')) {
return join('{workspaceRoot}', join(projectRoot, path)); return joinPathFragments('{workspaceRoot}', projectRoot, path);
} else { } else {
return join('{projectRoot}', path); return joinPathFragments('{projectRoot}', path);
} }
} }
@ -145,12 +147,17 @@ function getOutputs(
return outputs; return outputs;
} }
interface CypressTargets {
targets: Record<string, TargetConfiguration>;
ciTestingGroup: string[];
}
async function buildCypressTargets( async function buildCypressTargets(
configFilePath: string, configFilePath: string,
projectRoot: string, projectRoot: string,
options: CypressPluginOptions, options: CypressPluginOptions,
context: CreateNodesContext context: CreateNodesContext
) { ): Promise<CypressTargets> {
const cypressConfig = await loadConfigFile( const cypressConfig = await loadConfigFile(
join(context.workspaceRoot, configFilePath) join(context.workspaceRoot, configFilePath)
); );
@ -167,6 +174,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 ciTestingGroup: string[] = [];
if ('e2e' in cypressConfig) { if ('e2e' in cypressConfig) {
targets[options.targetName] = { targets[options.targetName] = {
@ -214,8 +222,10 @@ async function buildCypressTargets(
const outputs = getOutputs(projectRoot, cypressConfig, 'e2e'); const outputs = getOutputs(projectRoot, cypressConfig, 'e2e');
const inputs = getInputs(namedInputs); const inputs = getInputs(namedInputs);
for (const file of specFiles) { for (const file of specFiles) {
const relativeSpecFilePath = relative(projectRoot, file); const relativeSpecFilePath = normalizePath(relative(projectRoot, file));
const targetName = options.ciTargetName + '--' + relativeSpecFilePath; const targetName = options.ciTargetName + '--' + relativeSpecFilePath;
ciTestingGroup.push(targetName);
targets[targetName] = { targets[targetName] = {
outputs, outputs,
inputs, inputs,
@ -240,6 +250,7 @@ async function buildCypressTargets(
outputs, outputs,
dependsOn, dependsOn,
}; };
ciTestingGroup.push(options.ciTargetName);
} }
} }
@ -254,7 +265,11 @@ async function buildCypressTargets(
}; };
} }
return targets; if (ciTestingGroup.length === 0) {
ciTestingGroup = null;
}
return { targets, ciTestingGroup };
} }
function normalizeOptions(options: CypressPluginOptions): CypressPluginOptions { function normalizeOptions(options: CypressPluginOptions): CypressPluginOptions {

View File

@ -44,6 +44,7 @@ export const allowedProjectExtensions = [
'projectType', 'projectType',
'release', 'release',
'includedScripts', 'includedScripts',
'metadata',
] as const; ] as const;
// If we pass props on the workspace that angular doesn't know about, // If we pass props on the workspace that angular doesn't know about,

View File

@ -111,6 +111,10 @@ export interface ProjectConfiguration {
'generator' | 'generatorOptions' 'generator' | 'generatorOptions'
>; >;
}; };
metadata?: {
technologies?: string[];
targetGroups?: Record<string, string[]>;
};
} }
export interface TargetDependencyConfig { export interface TargetDependencyConfig {

View File

@ -731,6 +731,177 @@ describe('project-configuration-utils', () => {
`); `);
}); });
it('should merge release', () => {
const rootMap = new RootMapBuilder()
.addProject({
root: 'libs/lib-a',
name: 'lib-a',
})
.getRootMap();
mergeProjectConfigurationIntoRootMap(rootMap, {
root: 'libs/lib-a',
name: 'lib-a',
release: {
version: {
generatorOptions: {
packageRoot: 'dist/libs/lib-a',
},
},
},
});
expect(rootMap.get('libs/lib-a').release).toMatchInlineSnapshot(`
{
"version": {
"generatorOptions": {
"packageRoot": "dist/libs/lib-a",
},
},
}
`);
});
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'],
targetGroups: {
group1: ['target1', 'target2'],
},
},
},
sourceMap,
['dummy', 'dummy.ts']
);
expect(rootMap.get('libs/lib-a').metadata).toEqual({
technologies: ['technology'],
targetGroups: {
group1: ['target1', 'target2'],
},
});
expect(sourceMap['libs/lib-a']).toMatchObject({
'metadata.technologies': ['dummy', 'dummy.ts'],
'metadata.targetGroups': ['dummy', 'dummy.ts'],
'metadata.targetGroups.group1': ['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']).toMatchObject({
'metadata.technologies': ['existing', 'existing.ts'],
'metadata.technologies.0': ['existing', 'existing.ts'],
'metadata.technologies.1': ['dummy', 'dummy.ts'],
});
});
it('should concat second level arrays', () => {
const rootMap = new RootMapBuilder()
.addProject({
root: 'libs/lib-a',
name: 'lib-a',
metadata: {
targetGroups: {
group1: ['target1'],
},
},
})
.getRootMap();
const sourceMap: ConfigurationSourceMaps = {
'libs/lib-a': {
'metadata.targetGroups': ['existing', 'existing.ts'],
'metadata.targetGroups.group1': ['existing', 'existing.ts'],
'metadata.targetGroups.group1.0': ['existing', 'existing.ts'],
},
};
mergeProjectConfigurationIntoRootMap(
rootMap,
{
root: 'libs/lib-a',
name: 'lib-a',
metadata: {
targetGroups: {
group1: ['target2'],
},
},
},
sourceMap,
['dummy', 'dummy.ts']
);
expect(rootMap.get('libs/lib-a').metadata).toEqual({
targetGroups: {
group1: ['target1', 'target2'],
},
});
expect(sourceMap['libs/lib-a']).toMatchObject({
'metadata.targetGroups': ['existing', 'existing.ts'],
'metadata.targetGroups.group1': ['existing', 'existing.ts'],
'metadata.targetGroups.group1.0': ['existing', 'existing.ts'],
'metadata.targetGroups.group1.1': ['dummy', 'dummy.ts'],
});
expect(sourceMap['libs/lib-a']['metadata.targetGroups']).toEqual([
'existing',
'existing.ts',
]);
expect(sourceMap['libs/lib-a']['metadata.targetGroups.group1']).toEqual(
['existing', 'existing.ts']
);
expect(
sourceMap['libs/lib-a']['metadata.targetGroups.group1.0']
).toEqual(['existing', 'existing.ts']);
expect(
sourceMap['libs/lib-a']['metadata.targetGroups.group1.1']
).toEqual(['dummy', 'dummy.ts']);
});
});
describe('source map', () => { describe('source map', () => {
it('should add new project info', () => { it('should add new project info', () => {
const rootMap = new RootMapBuilder().getRootMap(); const rootMap = new RootMapBuilder().getRootMap();

View File

@ -60,12 +60,23 @@ export function mergeProjectConfigurationIntoRootMap(
// a project.json in which case it was already updated above. // a project.json in which case it was already updated above.
const updatedProjectConfiguration = { const updatedProjectConfiguration = {
...matchingProject, ...matchingProject,
...project,
}; };
if (sourceMap) { for (const k in project) {
for (const property in project) { if (
sourceMap[`${property}`] = sourceInformation; ![
'tags',
'implicitDependencies',
'generators',
'targets',
'metadata',
'namedInputs',
].includes(k)
) {
updatedProjectConfiguration[k] = project[k];
if (sourceMap) {
sourceMap[`${k}`] = sourceInformation;
}
} }
} }
@ -76,6 +87,7 @@ export function mergeProjectConfigurationIntoRootMap(
); );
if (sourceMap) { if (sourceMap) {
sourceMap['tags'] ??= sourceInformation;
project.tags.forEach((tag) => { project.tags.forEach((tag) => {
sourceMap[`tags.${tag}`] = sourceInformation; sourceMap[`tags.${tag}`] = sourceInformation;
}); });
@ -88,6 +100,7 @@ export function mergeProjectConfigurationIntoRootMap(
).concat(project.implicitDependencies); ).concat(project.implicitDependencies);
if (sourceMap) { if (sourceMap) {
sourceMap['implicitDependencies'] ??= sourceInformation;
project.implicitDependencies.forEach((implicitDependency) => { project.implicitDependencies.forEach((implicitDependency) => {
sourceMap[`implicitDependencies.${implicitDependency}`] = sourceMap[`implicitDependencies.${implicitDependency}`] =
sourceInformation; sourceInformation;
@ -100,6 +113,7 @@ export function mergeProjectConfigurationIntoRootMap(
updatedProjectConfiguration.generators = { ...project.generators }; updatedProjectConfiguration.generators = { ...project.generators };
if (sourceMap) { if (sourceMap) {
sourceMap['generators'] ??= sourceInformation;
for (const generator in project.generators) { for (const generator in project.generators) {
sourceMap[`generators.${generator}`] = sourceInformation; sourceMap[`generators.${generator}`] = sourceInformation;
for (const property in project.generators[generator]) { for (const property in project.generators[generator]) {
@ -127,6 +141,7 @@ export function mergeProjectConfigurationIntoRootMap(
}; };
if (sourceMap) { if (sourceMap) {
sourceMap['namedInputs'] ??= sourceInformation;
for (const namedInput in project.namedInputs) { for (const namedInput in project.namedInputs) {
sourceMap[`namedInputs.${namedInput}`] = sourceInformation; sourceMap[`namedInputs.${namedInput}`] = sourceInformation;
} }
@ -137,6 +152,9 @@ export function mergeProjectConfigurationIntoRootMap(
// 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.
updatedProjectConfiguration.targets = matchingProject?.targets ?? {}; updatedProjectConfiguration.targets = matchingProject?.targets ?? {};
if (sourceMap) {
sourceMap['targets'] ??= sourceInformation;
}
// For each target defined in the new config // For each target defined in the new config
for (const targetName in project.targets) { for (const targetName in project.targets) {
@ -176,6 +194,81 @@ 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( projectRootMap.set(
updatedProjectConfiguration.root, updatedProjectConfiguration.root,
updatedProjectConfiguration updatedProjectConfiguration