feat(core): add ability to add metadata to projects (#22299)
This commit is contained in:
parent
9f3af7051b
commit
5a9671b3fa
@ -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`
|
||||
|
||||
@ -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": {
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -111,6 +111,10 @@ export interface ProjectConfiguration {
|
||||
'generator' | 'generatorOptions'
|
||||
>;
|
||||
};
|
||||
metadata?: {
|
||||
technologies?: string[];
|
||||
targetGroups?: Record<string, string[]>;
|
||||
};
|
||||
}
|
||||
|
||||
export interface TargetDependencyConfig {
|
||||
|
||||
@ -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();
|
||||
|
||||
@ -60,12 +60,23 @@ export function mergeProjectConfigurationIntoRootMap(
|
||||
// a project.json in which case it was already updated above.
|
||||
const updatedProjectConfiguration = {
|
||||
...matchingProject,
|
||||
...project,
|
||||
};
|
||||
|
||||
if (sourceMap) {
|
||||
for (const property in project) {
|
||||
sourceMap[`${property}`] = sourceInformation;
|
||||
for (const k in project) {
|
||||
if (
|
||||
![
|
||||
'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) {
|
||||
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
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user