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

View File

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

View File

@ -3,7 +3,10 @@ import {
CreateNodes,
CreateNodesContext,
detectPackageManager,
joinPathFragments,
normalizePath,
NxJsonConfiguration,
ProjectConfiguration,
readJsonFile,
TargetConfiguration,
writeJsonFile,
@ -12,7 +15,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 +32,13 @@ export interface CypressPluginOptions {
const cachePath = join(projectGraphCacheDirectory, 'cypress.hash');
const targetsCache = existsSync(cachePath) ? readTargetsCache() : {};
const calculatedTargets: Record<
string,
Record<string, TargetConfiguration>
> = {};
const calculatedTargets: Record<string, CypressTargets> = {};
function readTargetsCache(): Record<
string,
Record<string, TargetConfiguration<CypressExecutorOptions>>
> {
function readTargetsCache(): Record<string, CypressTargets> {
return readJsonFile(cachePath);
}
function writeTargetsToCache(
targets: Record<
string,
Record<string, TargetConfiguration<CypressExecutorOptions>>
>
) {
function writeTargetsToCache(targets: Record<string, CypressTargets>) {
writeJsonFile(cachePath, targets);
}
@ -75,7 +66,7 @@ export const createNodes: CreateNodes<CypressPluginOptions> = [
getLockFileName(detectPackageManager(context.workspaceRoot)),
]);
const targets = targetsCache[hash]
const { targets, ciTestingGroup } = targetsCache[hash]
? targetsCache[hash]
: await buildCypressTargets(
configFilePath,
@ -84,14 +75,25 @@ export const createNodes: CreateNodes<CypressPluginOptions> = [
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 {
projects: {
[projectRoot]: {
projectType: 'application',
targets,
},
[projectRoot]: project,
},
};
},
@ -104,9 +106,9 @@ function getOutputs(
): string[] {
function getOutput(path: string): string {
if (path.startsWith('..')) {
return join('{workspaceRoot}', join(projectRoot, path));
return joinPathFragments('{workspaceRoot}', projectRoot, path);
} else {
return join('{projectRoot}', path);
return joinPathFragments('{projectRoot}', path);
}
}
@ -145,12 +147,17 @@ function getOutputs(
return outputs;
}
interface CypressTargets {
targets: Record<string, TargetConfiguration>;
ciTestingGroup: string[];
}
async function buildCypressTargets(
configFilePath: string,
projectRoot: string,
options: CypressPluginOptions,
context: CreateNodesContext
) {
): Promise<CypressTargets> {
const cypressConfig = await loadConfigFile(
join(context.workspaceRoot, configFilePath)
);
@ -167,6 +174,7 @@ async function buildCypressTargets(
const namedInputs = getNamedInputs(projectRoot, context);
const targets: Record<string, TargetConfiguration> = {};
let ciTestingGroup: string[] = [];
if ('e2e' in cypressConfig) {
targets[options.targetName] = {
@ -214,8 +222,10 @@ async function buildCypressTargets(
const outputs = getOutputs(projectRoot, cypressConfig, 'e2e');
const inputs = getInputs(namedInputs);
for (const file of specFiles) {
const relativeSpecFilePath = relative(projectRoot, file);
const relativeSpecFilePath = normalizePath(relative(projectRoot, file));
const targetName = options.ciTargetName + '--' + relativeSpecFilePath;
ciTestingGroup.push(targetName);
targets[targetName] = {
outputs,
inputs,
@ -240,6 +250,7 @@ async function buildCypressTargets(
outputs,
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 {

View File

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

View File

@ -111,6 +111,10 @@ export interface ProjectConfiguration {
'generator' | 'generatorOptions'
>;
};
metadata?: {
technologies?: string[];
targetGroups?: Record<string, string[]>;
};
}
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', () => {
it('should add new project info', () => {
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.
const updatedProjectConfiguration = {
...matchingProject,
...project,
};
for (const k in project) {
if (
![
'tags',
'implicitDependencies',
'generators',
'targets',
'metadata',
'namedInputs',
].includes(k)
) {
updatedProjectConfiguration[k] = project[k];
if (sourceMap) {
for (const property in project) {
sourceMap[`${property}`] = sourceInformation;
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,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(
updatedProjectConfiguration.root,
updatedProjectConfiguration