From f987e8a49ec4bdcb5a7b13a188db23933677690d Mon Sep 17 00:00:00 2001 From: Alejandro Date: Sun, 9 Mar 2025 21:47:07 +0100 Subject: [PATCH 01/14] feat: mvp --- .../tests/plugin-create-nodes.e2e.test.ts | 3 +- nx.json | 3 ++ packages/nx-plugin/src/plugin/plugin.ts | 41 ++++++++++++++++--- .../src/plugin/target/configuration-target.ts | 6 +-- .../src/plugin/target/executor-target.ts | 4 +- .../nx-plugin/src/plugin/target/targets.ts | 10 ++--- packages/nx-plugin/src/plugin/types.ts | 19 +++++++-- packages/nx-plugin/src/plugin/utils.ts | 6 +-- 8 files changed, 69 insertions(+), 23 deletions(-) diff --git a/e2e/nx-plugin-e2e/tests/plugin-create-nodes.e2e.test.ts b/e2e/nx-plugin-e2e/tests/plugin-create-nodes.e2e.test.ts index 11db54c22..e8d6f895d 100644 --- a/e2e/nx-plugin-e2e/tests/plugin-create-nodes.e2e.test.ts +++ b/e2e/nx-plugin-e2e/tests/plugin-create-nodes.e2e.test.ts @@ -14,7 +14,6 @@ import { E2E_ENVIRONMENTS_DIR, TEST_OUTPUT_DIR, removeColorCodes, - teardownTestFolder, } from '@code-pushup/test-utils'; import { executeProcess, readTextFile } from '@code-pushup/utils'; import { INLINE_PLUGIN } from './inline-plugin.js'; @@ -35,7 +34,7 @@ describe('nx-plugin', () => { }); afterEach(async () => { - await teardownTestFolder(testFileDir); + // await teardownTestFolder(testFileDir); }); it('should add configuration target dynamically', async () => { diff --git a/nx.json b/nx.json index 84fbf0790..0c1cd400d 100644 --- a/nx.json +++ b/nx.json @@ -94,6 +94,9 @@ "filterByTags": ["publishable"] } } + }, + { + "plugin": "./packages/nx-plugin/src/plugin/plugin.ts" } ] } diff --git a/packages/nx-plugin/src/plugin/plugin.ts b/packages/nx-plugin/src/plugin/plugin.ts index 9129f1bd7..00f511b5a 100644 --- a/packages/nx-plugin/src/plugin/plugin.ts +++ b/packages/nx-plugin/src/plugin/plugin.ts @@ -1,14 +1,17 @@ -import type { +import { CreateNodes, CreateNodesContext, CreateNodesResult, + CreateNodesV2, + createNodesFromFiles, } from '@nx/devkit'; -import { PROJECT_JSON_FILE_NAME } from '../internal/constants.js'; -import { createTargets } from './target/targets.js'; -import type { CreateNodesOptions } from './types.js'; -import { normalizedCreateNodesContext } from './utils.js'; +import { PROJECT_JSON_FILE_NAME } from '../internal/constants'; +import { createTargets } from './target/targets'; +import type { CreateNodesOptions } from './types'; +import { normalizedCreateNodesContext } from './utils'; // name has to be "createNodes" to get picked up by Nx + export const createNodes: CreateNodes = [ `**/${PROJECT_JSON_FILE_NAME}`, async ( @@ -32,3 +35,31 @@ export const createNodes: CreateNodes = [ }; }, ]; + +export const createNodesV2: CreateNodesV2 = [ + `**/${PROJECT_JSON_FILE_NAME}`, + + async (configFiles, options, context) => { + return await createNodesFromFiles( + async (globMatchingFile, internalOptions) => { + const parsedCreateNodesOptions = internalOptions as CreateNodesOptions; + + const normalizedContext = await normalizedCreateNodesContext( + context, + globMatchingFile, + parsedCreateNodesOptions, + ); + return { + projects: { + [normalizedContext.projectRoot]: { + targets: await createTargets(normalizedContext), + }, + }, + }; + }, + configFiles, + options, + context, + ); + }, +]; diff --git a/packages/nx-plugin/src/plugin/target/configuration-target.ts b/packages/nx-plugin/src/plugin/target/configuration-target.ts index d19b9325b..f9b16c985 100644 --- a/packages/nx-plugin/src/plugin/target/configuration-target.ts +++ b/packages/nx-plugin/src/plugin/target/configuration-target.ts @@ -1,8 +1,8 @@ import type { TargetConfiguration } from '@nx/devkit'; import type { RunCommandsOptions } from 'nx/src/executors/run-commands/run-commands.impl'; -import { objectToCliArgs } from '../../executors/internal/cli.js'; -import { PACKAGE_NAME } from '../../internal/constants.js'; -import { CP_TARGET_NAME } from '../constants.js'; +import { objectToCliArgs } from '../../executors/internal/cli'; +import { PACKAGE_NAME } from '../../internal/constants'; +import { CP_TARGET_NAME } from '../constants'; export function createConfigurationTarget(options?: { targetName?: string; diff --git a/packages/nx-plugin/src/plugin/target/executor-target.ts b/packages/nx-plugin/src/plugin/target/executor-target.ts index e8b52eb8f..aeba82ad8 100644 --- a/packages/nx-plugin/src/plugin/target/executor-target.ts +++ b/packages/nx-plugin/src/plugin/target/executor-target.ts @@ -1,6 +1,6 @@ import type { TargetConfiguration } from '@nx/devkit'; -import { PACKAGE_NAME } from '../../internal/constants.js'; -import type { ProjectPrefixOptions } from '../types.js'; +import { PACKAGE_NAME } from '../../internal/constants'; +import type { ProjectPrefixOptions } from '../types'; export function createExecutorTarget(options?: { bin?: string; diff --git a/packages/nx-plugin/src/plugin/target/targets.ts b/packages/nx-plugin/src/plugin/target/targets.ts index 3e659688b..e6d62e03a 100644 --- a/packages/nx-plugin/src/plugin/target/targets.ts +++ b/packages/nx-plugin/src/plugin/target/targets.ts @@ -1,9 +1,9 @@ import { readdir } from 'node:fs/promises'; -import { CP_TARGET_NAME } from '../constants.js'; -import type { NormalizedCreateNodesContext } from '../types.js'; -import { createConfigurationTarget } from './configuration-target.js'; -import { CODE_PUSHUP_CONFIG_REGEX } from './constants.js'; -import { createExecutorTarget } from './executor-target.js'; +import { CP_TARGET_NAME } from '../constants'; +import type { NormalizedCreateNodesContext } from '../types'; +import { createConfigurationTarget } from './configuration-target'; +import { CODE_PUSHUP_CONFIG_REGEX } from './constants'; +import { createExecutorTarget } from './executor-target'; export async function createTargets( normalizedContext: NormalizedCreateNodesContext, diff --git a/packages/nx-plugin/src/plugin/types.ts b/packages/nx-plugin/src/plugin/types.ts index 5e8d59db7..0e96f0e21 100644 --- a/packages/nx-plugin/src/plugin/types.ts +++ b/packages/nx-plugin/src/plugin/types.ts @@ -1,6 +1,10 @@ -import type { CreateNodesContext, ProjectConfiguration } from '@nx/devkit'; +import type { + CreateNodesContext, + CreateNodesContextV2, + ProjectConfiguration, +} from '@nx/devkit'; import type { WithRequired } from '@code-pushup/utils'; -import type { DynamicTargetOptions } from '../internal/types.js'; +import type { DynamicTargetOptions } from '../internal/types'; export type ProjectPrefixOptions = { projectPrefix?: string; @@ -13,7 +17,16 @@ export type ProjectConfigurationWithName = WithRequired< 'name' >; -export type NormalizedCreateNodesContext = CreateNodesContext & { +export type NormalizedCreateNodesContext = ( + | CreateNodesContext + | CreateNodesContextV2 +) & { + projectJson: ProjectConfigurationWithName; + projectRoot: string; + createOptions: CreateNodesOptions; +}; + +export type NormalizedCreateNodesContextV2 = CreateNodesContextV2 & { projectJson: ProjectConfigurationWithName; projectRoot: string; createOptions: CreateNodesOptions; diff --git a/packages/nx-plugin/src/plugin/utils.ts b/packages/nx-plugin/src/plugin/utils.ts index e7a819f8d..b21eb765a 100644 --- a/packages/nx-plugin/src/plugin/utils.ts +++ b/packages/nx-plugin/src/plugin/utils.ts @@ -1,7 +1,7 @@ -import type { CreateNodesContext } from '@nx/devkit'; +import type { CreateNodesContext, CreateNodesContextV2 } from '@nx/devkit'; import { readFile } from 'node:fs/promises'; import * as path from 'node:path'; -import { CP_TARGET_NAME } from './constants.js'; +import { CP_TARGET_NAME } from './constants'; import type { CreateNodesOptions, NormalizedCreateNodesContext, @@ -9,7 +9,7 @@ import type { } from './types.js'; export async function normalizedCreateNodesContext( - context: CreateNodesContext, + context: CreateNodesContext | CreateNodesContextV2, projectConfigurationFile: string, createOptions: CreateNodesOptions = {}, ): Promise { From 9eeca80d05d9eb7dfdcf08747fe3bd5ca0267925 Mon Sep 17 00:00:00 2001 From: Alejandro Date: Sun, 9 Mar 2025 21:49:18 +0100 Subject: [PATCH 02/14] chore: wip --- e2e/nx-plugin-e2e/tests/plugin-create-nodes.e2e.test.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/e2e/nx-plugin-e2e/tests/plugin-create-nodes.e2e.test.ts b/e2e/nx-plugin-e2e/tests/plugin-create-nodes.e2e.test.ts index e8d6f895d..11db54c22 100644 --- a/e2e/nx-plugin-e2e/tests/plugin-create-nodes.e2e.test.ts +++ b/e2e/nx-plugin-e2e/tests/plugin-create-nodes.e2e.test.ts @@ -14,6 +14,7 @@ import { E2E_ENVIRONMENTS_DIR, TEST_OUTPUT_DIR, removeColorCodes, + teardownTestFolder, } from '@code-pushup/test-utils'; import { executeProcess, readTextFile } from '@code-pushup/utils'; import { INLINE_PLUGIN } from './inline-plugin.js'; @@ -34,7 +35,7 @@ describe('nx-plugin', () => { }); afterEach(async () => { - // await teardownTestFolder(testFileDir); + await teardownTestFolder(testFileDir); }); it('should add configuration target dynamically', async () => { From a2cfed66ad8725a93f50a7e909c9e497719dfe05 Mon Sep 17 00:00:00 2001 From: Alejandro Date: Sun, 9 Mar 2025 21:52:34 +0100 Subject: [PATCH 03/14] chore: wip --- nx.json | 3 --- 1 file changed, 3 deletions(-) diff --git a/nx.json b/nx.json index 0c1cd400d..84fbf0790 100644 --- a/nx.json +++ b/nx.json @@ -94,9 +94,6 @@ "filterByTags": ["publishable"] } } - }, - { - "plugin": "./packages/nx-plugin/src/plugin/plugin.ts" } ] } From 9a5c13815ff4c49d7359435c6150e86b7e76fe3f Mon Sep 17 00:00:00 2001 From: Alejandro Date: Sun, 9 Mar 2025 21:56:05 +0100 Subject: [PATCH 04/14] chore: wip --- packages/nx-plugin/eslint.config.cjs | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/nx-plugin/eslint.config.cjs b/packages/nx-plugin/eslint.config.cjs index e732748e6..71a018585 100644 --- a/packages/nx-plugin/eslint.config.cjs +++ b/packages/nx-plugin/eslint.config.cjs @@ -15,6 +15,7 @@ module.exports = tseslint.config( { files: ['**/*.ts'], rules: { + 'n/file-extension-in-import': 'off', // Nx plugins don't yet support ESM: https://github.com/nrwl/nx/issues/15682 'unicorn/prefer-module': 'off', // used instead of verbatimModuleSyntax tsconfig flag (requires ESM) From d8641bb9dbb366a32aa18ba47a4d0c77d9aded7b Mon Sep 17 00:00:00 2001 From: Alejandro Date: Sun, 9 Mar 2025 21:57:57 +0100 Subject: [PATCH 05/14] chore: wip --- packages/nx-plugin/src/plugin/plugin.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/nx-plugin/src/plugin/plugin.ts b/packages/nx-plugin/src/plugin/plugin.ts index 00f511b5a..d07d56ea7 100644 --- a/packages/nx-plugin/src/plugin/plugin.ts +++ b/packages/nx-plugin/src/plugin/plugin.ts @@ -36,7 +36,7 @@ export const createNodes: CreateNodes = [ }, ]; -export const createNodesV2: CreateNodesV2 = [ +export const createNodesV2: CreateNodesV2 = [ `**/${PROJECT_JSON_FILE_NAME}`, async (configFiles, options, context) => { From cf572798824b76aced77b61c6a736eb2b3175452 Mon Sep 17 00:00:00 2001 From: Alejandro Date: Sun, 9 Mar 2025 22:01:44 +0100 Subject: [PATCH 06/14] chore: wip --- packages/nx-plugin/src/plugin/plugin.ts | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/packages/nx-plugin/src/plugin/plugin.ts b/packages/nx-plugin/src/plugin/plugin.ts index d07d56ea7..68c00b054 100644 --- a/packages/nx-plugin/src/plugin/plugin.ts +++ b/packages/nx-plugin/src/plugin/plugin.ts @@ -1,10 +1,10 @@ -import { +import type { CreateNodes, CreateNodesContext, CreateNodesResult, CreateNodesV2, - createNodesFromFiles, } from '@nx/devkit'; +import { createNodesFromFiles } from '@nx/devkit'; import { PROJECT_JSON_FILE_NAME } from '../internal/constants'; import { createTargets } from './target/targets'; import type { CreateNodesOptions } from './types'; @@ -38,9 +38,8 @@ export const createNodes: CreateNodes = [ export const createNodesV2: CreateNodesV2 = [ `**/${PROJECT_JSON_FILE_NAME}`, - - async (configFiles, options, context) => { - return await createNodesFromFiles( + async (configFiles, options, context) => + createNodesFromFiles( async (globMatchingFile, internalOptions) => { const parsedCreateNodesOptions = internalOptions as CreateNodesOptions; @@ -60,6 +59,5 @@ export const createNodesV2: CreateNodesV2 = [ configFiles, options, context, - ); - }, + ) ]; From b39e1ebd4beec4efe2dc35e8c51e59642e203825 Mon Sep 17 00:00:00 2001 From: Alejandro Date: Sun, 9 Mar 2025 22:09:34 +0100 Subject: [PATCH 07/14] chore: wip --- packages/nx-plugin/src/plugin/plugin.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/nx-plugin/src/plugin/plugin.ts b/packages/nx-plugin/src/plugin/plugin.ts index 68c00b054..fdaf75187 100644 --- a/packages/nx-plugin/src/plugin/plugin.ts +++ b/packages/nx-plugin/src/plugin/plugin.ts @@ -1,10 +1,10 @@ -import type { - CreateNodes, - CreateNodesContext, - CreateNodesResult, - CreateNodesV2, +import { + type CreateNodes, + type CreateNodesContext, + type CreateNodesResult, + type CreateNodesV2, + createNodesFromFiles } from '@nx/devkit'; -import { createNodesFromFiles } from '@nx/devkit'; import { PROJECT_JSON_FILE_NAME } from '../internal/constants'; import { createTargets } from './target/targets'; import type { CreateNodesOptions } from './types'; From db535affdc7d81c3da9daf4bd2c337a779952f96 Mon Sep 17 00:00:00 2001 From: Alejandro Date: Sun, 9 Mar 2025 22:10:59 +0100 Subject: [PATCH 08/14] chore: wip --- packages/nx-plugin/src/plugin/plugin.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/nx-plugin/src/plugin/plugin.ts b/packages/nx-plugin/src/plugin/plugin.ts index fdaf75187..433fbb0df 100644 --- a/packages/nx-plugin/src/plugin/plugin.ts +++ b/packages/nx-plugin/src/plugin/plugin.ts @@ -3,7 +3,7 @@ import { type CreateNodesContext, type CreateNodesResult, type CreateNodesV2, - createNodesFromFiles + createNodesFromFiles, } from '@nx/devkit'; import { PROJECT_JSON_FILE_NAME } from '../internal/constants'; import { createTargets } from './target/targets'; @@ -59,5 +59,5 @@ export const createNodesV2: CreateNodesV2 = [ configFiles, options, context, - ) + ), ]; From ed2dae97a61db5d77cb87ea87217388bff662337 Mon Sep 17 00:00:00 2001 From: Alejandro Date: Mon, 10 Mar 2025 22:39:02 +0100 Subject: [PATCH 09/14] chore: create v1-v2 function helpers and tests --- packages/nx-plugin/src/plugin/plugin.ts | 13 +- .../nx-plugin/src/plugin/plugin.unit.test.ts | 232 ++++++++++-------- packages/nx-plugin/src/plugin/utils.ts | 7 + .../nx-plugin/src/plugin/utils.unit.test.ts | 16 +- .../test-nx-utils/src/lib/utils/nx-plugin.ts | 42 +++- .../src/lib/utils/nx-plugin.unit.test.ts | 168 +++++++++---- 6 files changed, 313 insertions(+), 165 deletions(-) diff --git a/packages/nx-plugin/src/plugin/plugin.ts b/packages/nx-plugin/src/plugin/plugin.ts index 433fbb0df..0afdf1f25 100644 --- a/packages/nx-plugin/src/plugin/plugin.ts +++ b/packages/nx-plugin/src/plugin/plugin.ts @@ -5,13 +5,12 @@ import { type CreateNodesV2, createNodesFromFiles, } from '@nx/devkit'; -import { PROJECT_JSON_FILE_NAME } from '../internal/constants'; -import { createTargets } from './target/targets'; -import type { CreateNodesOptions } from './types'; -import { normalizedCreateNodesContext } from './utils'; - -// name has to be "createNodes" to get picked up by Nx +import { PROJECT_JSON_FILE_NAME } from '../internal/constants.js'; +import { createTargets } from './target/targets.js'; +import type { CreateNodesOptions } from './types.js'; +import { normalizedCreateNodesContext } from './utils.js'; +/** Create the nodes for a V1 Plugin. The name `createNodes` is required by Nx in order to be picked up as a plugin. */ export const createNodes: CreateNodes = [ `**/${PROJECT_JSON_FILE_NAME}`, async ( @@ -36,6 +35,7 @@ export const createNodes: CreateNodes = [ }, ]; +/** Create the nodes for a V2 Plugin. The name `createNodesV2` is required by Nx in order to be picked up as a plugin. */ export const createNodesV2: CreateNodesV2 = [ `**/${PROJECT_JSON_FILE_NAME}`, async (configFiles, options, context) => @@ -48,6 +48,7 @@ export const createNodesV2: CreateNodesV2 = [ globMatchingFile, parsedCreateNodesOptions, ); + return { projects: { [normalizedContext.projectRoot]: { diff --git a/packages/nx-plugin/src/plugin/plugin.unit.test.ts b/packages/nx-plugin/src/plugin/plugin.unit.test.ts index c51ebf570..cc8c77d0e 100644 --- a/packages/nx-plugin/src/plugin/plugin.unit.test.ts +++ b/packages/nx-plugin/src/plugin/plugin.unit.test.ts @@ -1,138 +1,160 @@ -import type { CreateNodesContext } from '@nx/devkit'; +import type { CreateNodesContext, CreateNodesContextV2 } from '@nx/devkit'; import { vol } from 'memfs'; import { describe, expect } from 'vitest'; -import { invokeCreateNodesOnVirtualFiles } from '@code-pushup/test-nx-utils'; +import { + createNodesContextV1, + createNodesContextV2, + invokeCreateNodesOnVirtualFilesV1, + invokeCreateNodesOnVirtualFilesV2, +} from '@code-pushup/test-nx-utils'; import { PACKAGE_NAME, PROJECT_JSON_FILE_NAME } from '../internal/constants.js'; import { CP_TARGET_NAME } from './constants.js'; -import { createNodes } from './plugin.js'; +import { createNodes, createNodesV2 } from './plugin.js'; describe('@code-pushup/nx-plugin/plugin', () => { - let context: CreateNodesContext; + describe('V1', () => { + let context: CreateNodesContext; - beforeEach(() => { - context = { - nxJsonConfiguration: {}, - workspaceRoot: '', - }; - }); + beforeEach(() => { + context = createNodesContextV1({ + nxJsonConfiguration: {}, + workspaceRoot: '', + }); + }); - afterEach(() => { - vol.reset(); - }); + afterEach(() => { + vol.reset(); + }); - it('should normalize context and use it to create the configuration target on ROOT project', async () => { - const projectRoot = '.'; - const matchingFilesData = { - [`${projectRoot}/${PROJECT_JSON_FILE_NAME}`]: `${JSON.stringify({ - name: '@org/empty-root', - })}`, - }; + it('should normalize context and use it to create the configuration target on ROOT project', async () => { + const projectRoot = '.'; + const matchingFilesData = { + [`${projectRoot}/${PROJECT_JSON_FILE_NAME}`]: `${JSON.stringify({ + name: '@org/empty-root', + })}`, + }; - await expect( - invokeCreateNodesOnVirtualFiles( - createNodes, - context, - {}, - { matchingFilesData }, - ), - ).resolves.toStrictEqual({ - [projectRoot]: { - targets: { - [`${CP_TARGET_NAME}--configuration`]: { - command: `nx g ${PACKAGE_NAME}:configuration --skipTarget --targetName="code-pushup" --project="@org/empty-root"`, + await expect( + invokeCreateNodesOnVirtualFilesV1( + createNodes, + context, + {}, + { matchingFilesData }, + ), + ).resolves.toStrictEqual({ + [projectRoot]: { + targets: { + [`${CP_TARGET_NAME}--configuration`]: { + command: `nx g ${PACKAGE_NAME}:configuration --skipTarget --targetName="code-pushup" --project="@org/empty-root"`, + }, }, }, - }, + }); }); - }); - it('should normalize context and use it to create the configuration target on PACKAGE project', async () => { - const projectRoot = 'apps/my-app'; - const matchingFilesData = { - [`${projectRoot}/${PROJECT_JSON_FILE_NAME}`]: `${JSON.stringify({ - name: '@org/empty-root', - })}`, - }; + it('should create the executor target on PACKAGE project if configured', async () => { + const projectRoot = 'apps/my-app'; + const matchingFilesData = { + [`${projectRoot}/${PROJECT_JSON_FILE_NAME}`]: `${JSON.stringify({ + name: '@org/empty-root', + })}`, + [`${projectRoot}/code-pushup.config.ts`]: '{}', + }; - await expect( - invokeCreateNodesOnVirtualFiles( - createNodes, - context, - {}, - { matchingFilesData }, - ), - ).resolves.toStrictEqual({ - [projectRoot]: { - targets: { - [`${CP_TARGET_NAME}--configuration`]: { - command: `nx g ${PACKAGE_NAME}:configuration --skipTarget --targetName="code-pushup" --project="@org/empty-root"`, + await expect( + invokeCreateNodesOnVirtualFilesV1( + createNodes, + context, + { + projectPrefix: 'cli', + }, + { matchingFilesData }, + ), + ).resolves.toStrictEqual({ + [projectRoot]: { + targets: { + [CP_TARGET_NAME]: { + executor: `${PACKAGE_NAME}:cli`, + options: { + projectPrefix: 'cli', + }, + }, }, }, - }, + }); }); }); - it('should create the executor target on ROOT project if configured', async () => { - const projectRoot = '.'; - const matchingFilesData = { - [`${projectRoot}/${PROJECT_JSON_FILE_NAME}`]: `${JSON.stringify({ - name: '@org/empty-root', - })}`, - [`${projectRoot}/code-pushup.config.ts`]: '{}', - }; + describe('V2', () => { + let context: CreateNodesContextV2; - await expect( - invokeCreateNodesOnVirtualFiles( - createNodes, - context, - { - projectPrefix: 'cli', - }, - { matchingFilesData }, - ), - ).resolves.toStrictEqual({ - [projectRoot]: { - targets: { - [CP_TARGET_NAME]: { - executor: `${PACKAGE_NAME}:cli`, - options: { - projectPrefix: 'cli', + beforeEach(() => { + context = createNodesContextV2({ + nxJsonConfiguration: {}, + workspaceRoot: '', + }); + }); + + afterEach(() => { + vol.reset(); + }); + + it('should normalize context and use it to create the configuration target on ROOT project', async () => { + const projectRoot = '.'; + const matchingFilesData = { + [`${projectRoot}/${PROJECT_JSON_FILE_NAME}`]: `${JSON.stringify({ + name: '@org/empty-root', + })}`, + }; + + await expect( + invokeCreateNodesOnVirtualFilesV2( + createNodesV2, + context, + {}, + { matchingFilesData }, + ), + ).resolves.toStrictEqual({ + [projectRoot]: { + targets: { + [`${CP_TARGET_NAME}--configuration`]: { + command: `nx g ${PACKAGE_NAME}:configuration --skipTarget --targetName="code-pushup" --project="@org/empty-root"`, }, }, }, - }, + }); }); - }); - it('should create the executor target on PACKAGE project if configured', async () => { - const projectRoot = 'apps/my-app'; - const matchingFilesData = { - [`${projectRoot}/${PROJECT_JSON_FILE_NAME}`]: `${JSON.stringify({ - name: '@org/empty-root', - })}`, - [`${projectRoot}/code-pushup.config.ts`]: '{}', - }; + it('should create the executor target on PACKAGE project if configured', async () => { + const projectRoot = 'apps/my-app'; + const matchingFilesData = { + [`${projectRoot}/${PROJECT_JSON_FILE_NAME}`]: `${JSON.stringify({ + name: '@org/empty-root', + })}`, + [`${projectRoot}/code-pushup.config.ts`]: '{}', + }; - await expect( - invokeCreateNodesOnVirtualFiles( - createNodes, - context, - { - projectPrefix: 'cli', - }, - { matchingFilesData }, - ), - ).resolves.toStrictEqual({ - [projectRoot]: { - targets: { - [CP_TARGET_NAME]: { - executor: `${PACKAGE_NAME}:cli`, - options: { - projectPrefix: 'cli', + await expect( + invokeCreateNodesOnVirtualFilesV2( + createNodesV2, + context, + { + projectPrefix: 'cli', + }, + { matchingFilesData }, + ), + ).resolves.toStrictEqual({ + [projectRoot]: { + targets: { + [CP_TARGET_NAME]: { + executor: `${PACKAGE_NAME}:cli`, + options: { + projectPrefix: 'cli', + }, }, }, }, - }, + }); }); }); }); diff --git a/packages/nx-plugin/src/plugin/utils.ts b/packages/nx-plugin/src/plugin/utils.ts index b21eb765a..356f773cb 100644 --- a/packages/nx-plugin/src/plugin/utils.ts +++ b/packages/nx-plugin/src/plugin/utils.ts @@ -8,6 +8,13 @@ import type { ProjectConfigurationWithName, } from './types.js'; +/** + * Normalize the context for a V1 or V2 Plugin. + * @param context - The context for a V1 or V2 Plugin. + * @param projectConfigurationFile - The project configuration file. + * @param createOptions - The create options. + * @returns The normalized context. + */ export async function normalizedCreateNodesContext( context: CreateNodesContext | CreateNodesContextV2, projectConfigurationFile: string, diff --git a/packages/nx-plugin/src/plugin/utils.unit.test.ts b/packages/nx-plugin/src/plugin/utils.unit.test.ts index edf2bf1cb..439b72445 100644 --- a/packages/nx-plugin/src/plugin/utils.unit.test.ts +++ b/packages/nx-plugin/src/plugin/utils.unit.test.ts @@ -1,6 +1,6 @@ import { vol } from 'memfs'; import { describe, expect } from 'vitest'; -import { createNodesContext } from '@code-pushup/test-nx-utils'; +import { createNodesContextV2 } from '@code-pushup/test-nx-utils'; import { MEMFS_VOLUME } from '@code-pushup/test-utils'; import { normalizedCreateNodesContext } from './utils.js'; @@ -15,7 +15,7 @@ describe('normalizedCreateNodesContext', () => { await expect( normalizedCreateNodesContext( - createNodesContext({ workspaceRoot: MEMFS_VOLUME }), + createNodesContextV2({ workspaceRoot: MEMFS_VOLUME }), 'project.json', ), ).resolves.toStrictEqual( @@ -37,7 +37,7 @@ describe('normalizedCreateNodesContext', () => { await expect( normalizedCreateNodesContext( - createNodesContext(), + createNodesContextV2(), 'packages/utils/project.json', ), ).resolves.toStrictEqual( @@ -59,7 +59,7 @@ describe('normalizedCreateNodesContext', () => { await expect( normalizedCreateNodesContext( - createNodesContext({ + createNodesContextV2({ nxJsonConfiguration: { workspaceLayout: { libsDir: 'libs', @@ -90,7 +90,7 @@ describe('normalizedCreateNodesContext', () => { ); await expect( - normalizedCreateNodesContext(createNodesContext(), 'project.json'), + normalizedCreateNodesContext(createNodesContextV2(), 'project.json'), ).resolves.toStrictEqual( expect.objectContaining({ projectJson: { @@ -109,7 +109,7 @@ describe('normalizedCreateNodesContext', () => { ); await expect( - normalizedCreateNodesContext(createNodesContext(), 'project.json'), + normalizedCreateNodesContext(createNodesContextV2(), 'project.json'), ).rejects.toThrow('Error parsing project.json file project.json.'); }); @@ -124,7 +124,7 @@ describe('normalizedCreateNodesContext', () => { ); await expect( - normalizedCreateNodesContext(createNodesContext(), 'project.json'), + normalizedCreateNodesContext(createNodesContextV2(), 'project.json'), ).resolves.toStrictEqual( expect.objectContaining({ createOptions: { @@ -145,7 +145,7 @@ describe('normalizedCreateNodesContext', () => { ); await expect( - normalizedCreateNodesContext(createNodesContext(), 'project.json', { + normalizedCreateNodesContext(createNodesContextV2(), 'project.json', { projectPrefix: 'cli', }), ).resolves.toStrictEqual( diff --git a/testing/test-nx-utils/src/lib/utils/nx-plugin.ts b/testing/test-nx-utils/src/lib/utils/nx-plugin.ts index 30d9706ba..5a4b29506 100644 --- a/testing/test-nx-utils/src/lib/utils/nx-plugin.ts +++ b/testing/test-nx-utils/src/lib/utils/nx-plugin.ts @@ -3,6 +3,7 @@ import type { CreateNodesContext, CreateNodesContextV2, CreateNodesResult, + CreateNodesV2, } from '@nx/devkit'; import { vol } from 'memfs'; import { MEMFS_VOLUME } from '@code-pushup/test-utils'; @@ -28,10 +29,9 @@ import { MEMFS_VOLUME } from '@code-pushup/test-utils'; * @param createNodeOptions * @param mockData */ -export async function invokeCreateNodesOnVirtualFiles< +export async function invokeCreateNodesOnVirtualFilesV1< T extends Record | undefined, >( - // FIXME: refactor this to use the V2 api & remove the eslint disable on the whole file createNodes: CreateNodes, context: CreateNodesContext, createNodeOptions: T, @@ -55,7 +55,43 @@ export async function invokeCreateNodesOnVirtualFiles< ); } -export function createNodesContext( +export async function invokeCreateNodesOnVirtualFilesV2< + T extends Record | undefined, +>( + createNodes: CreateNodesV2, + context: CreateNodesContextV2, + createNodeOptions: T, + mockData: { + matchingFilesData: Record; + }, +) { + const { matchingFilesData } = mockData; + vol.fromJSON(matchingFilesData, MEMFS_VOLUME); + + const files = Object.keys(matchingFilesData); + + const results = await createNodes[1](files, createNodeOptions, context); + + const result: NonNullable = {}; + return results.reduce( + (acc, [_, { projects }]) => ({ ...acc, ...projects }), + result, + ); +} + +export function createNodesContextV1( + options?: Partial, +): CreateNodesContext { + const { workspaceRoot = process.cwd(), nxJsonConfiguration = {} } = + options ?? {}; + return { + workspaceRoot, + nxJsonConfiguration, + configFiles: [], + }; +} + +export function createNodesContextV2( options?: Partial, ): CreateNodesContextV2 { const { workspaceRoot = process.cwd(), nxJsonConfiguration = {} } = diff --git a/testing/test-nx-utils/src/lib/utils/nx-plugin.unit.test.ts b/testing/test-nx-utils/src/lib/utils/nx-plugin.unit.test.ts index 68a9d5daa..5c92eae0d 100644 --- a/testing/test-nx-utils/src/lib/utils/nx-plugin.unit.test.ts +++ b/testing/test-nx-utils/src/lib/utils/nx-plugin.unit.test.ts @@ -1,61 +1,143 @@ import * as process from 'node:process'; import { describe, expect } from 'vitest'; import { - createNodesContext, - invokeCreateNodesOnVirtualFiles, + createNodesContextV1, + createNodesContextV2, + invokeCreateNodesOnVirtualFilesV1, + invokeCreateNodesOnVirtualFilesV2, } from './nx-plugin.js'; -describe('createNodesContext', () => { - it('should return a context with the provided options', () => { - const context = createNodesContext({ - workspaceRoot: 'root', - nxJsonConfiguration: { plugins: [] }, +describe('V1', () => { + describe('createNodesContextV1', () => { + it('should return a context with the provided options', () => { + const context = createNodesContextV1({ + workspaceRoot: 'root', + nxJsonConfiguration: { plugins: [] }, + }); + expect(context).toEqual({ + workspaceRoot: 'root', + nxJsonConfiguration: { plugins: [] }, + configFiles: [], + }); }); - expect(context).toEqual({ - workspaceRoot: 'root', - nxJsonConfiguration: { plugins: [] }, + + it('should return a context with defaults', () => { + const context = createNodesContextV1(); + expect(context).toEqual({ + workspaceRoot: process.cwd(), + nxJsonConfiguration: {}, + configFiles: [], + }); }); }); - it('should return a context with defaults', () => { - const context = createNodesContext(); - expect(context).toEqual({ - workspaceRoot: process.cwd(), - nxJsonConfiguration: {}, + describe('invokeCreateNodesOnVirtualFilesV1', () => { + it('should invoke passed function if matching file is given', async () => { + const createNodesFnSpy = vi + .fn() + .mockResolvedValue({ projects: { 'my-lib': {} } }); + await expect( + invokeCreateNodesOnVirtualFilesV1( + [`**/project.json`, createNodesFnSpy], + createNodesContextV1(), + {}, + { + matchingFilesData: { + '**/project.json': JSON.stringify({ + name: 'my-lib', + }), + }, + }, + ), + ).resolves.toStrictEqual({ 'my-lib': {} }); + expect(createNodesFnSpy).toHaveBeenCalledTimes(1); + expect(createNodesFnSpy).toHaveBeenCalledWith( + '**/project.json', + {}, + expect.any(Object), + ); + }); + + it('should NOT invoke passed function if matching file is NOT given', async () => { + const createNodesFnSpy = vi.fn().mockResolvedValue({}); + await expect( + invokeCreateNodesOnVirtualFilesV1( + [`**/project.json`, createNodesFnSpy], + createNodesContextV1(), + {}, + { matchingFilesData: {} }, + ), + ).resolves.toStrictEqual({}); + expect(createNodesFnSpy).not.toHaveBeenCalled(); }); }); }); -describe('invokeCreateNodesOnVirtualFiles', () => { - it('should invoke passed function if matching file is given', async () => { - const createNodesFnSpy = vi.fn().mockResolvedValue({}); - await expect( - invokeCreateNodesOnVirtualFiles( - [`**/project.json`, createNodesFnSpy], - createNodesContext(), - {}, - { - matchingFilesData: { - '**/project.json': JSON.stringify({ - name: 'my-lib', - }), - }, - }, - ), - ).resolves.toStrictEqual({}); - expect(createNodesFnSpy).toHaveBeenCalledTimes(1); +describe('V2', () => { + describe('createNodesContext', () => { + it('should return a context with the provided options', () => { + const context = createNodesContextV2({ + workspaceRoot: 'root', + nxJsonConfiguration: { plugins: [] }, + }); + expect(context).toEqual({ + workspaceRoot: 'root', + nxJsonConfiguration: { plugins: [] }, + }); + }); + + it('should return a context with defaults', () => { + const context = createNodesContextV2(); + expect(context).toEqual({ + workspaceRoot: process.cwd(), + nxJsonConfiguration: {}, + }); + }); }); - it('should NOT invoke passed function if matching file is NOT given', async () => { - const createNodesFnSpy = vi.fn().mockResolvedValue({}); - await expect( - invokeCreateNodesOnVirtualFiles( - [`**/project.json`, createNodesFnSpy], - createNodesContext(), + describe('invokeCreateNodesOnVirtualFilesV2', () => { + it('should invoke passed function if matching file is given', async () => { + const createNodesFnSpy = vi + .fn() + .mockResolvedValue([ + ['**/project.json', { projects: { 'my-lib': {} } }], + ]); + + await expect( + invokeCreateNodesOnVirtualFilesV2( + [`**/project.json`, createNodesFnSpy], + createNodesContextV2(), + {}, + { + matchingFilesData: { + '**/project.json': JSON.stringify({ + name: 'my-lib', + }), + }, + }, + ), + ).resolves.toStrictEqual({ 'my-lib': {} }); + + expect(createNodesFnSpy).toHaveBeenCalledTimes(1); + expect(createNodesFnSpy).toHaveBeenCalledWith( + ['**/project.json'], {}, - { matchingFilesData: {} }, - ), - ).resolves.toStrictEqual({}); - expect(createNodesFnSpy).not.toHaveBeenCalled(); + expect.any(Object), + ); + }); + + it('should NOT invoke passed function if matching file is NOT given', async () => { + const createNodesFnSpy = vi.fn().mockResolvedValue([]); + await expect( + invokeCreateNodesOnVirtualFilesV2( + [`**/project.json`, createNodesFnSpy], + createNodesContextV2(), + {}, + { matchingFilesData: {} }, + ), + ).resolves.toStrictEqual({}); + expect(createNodesFnSpy).toHaveBeenCalledTimes(1); + expect(createNodesFnSpy).toHaveBeenCalledWith([], {}, expect.any(Object)); + }); }); }); From 30410824ed5a2c0f2cbd553a0fddaa973c674126 Mon Sep 17 00:00:00 2001 From: Alejandro Date: Mon, 10 Mar 2025 22:40:19 +0100 Subject: [PATCH 10/14] chore: remove unused lint rule --- packages/nx-plugin/eslint.config.cjs | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/nx-plugin/eslint.config.cjs b/packages/nx-plugin/eslint.config.cjs index 71a018585..e732748e6 100644 --- a/packages/nx-plugin/eslint.config.cjs +++ b/packages/nx-plugin/eslint.config.cjs @@ -15,7 +15,6 @@ module.exports = tseslint.config( { files: ['**/*.ts'], rules: { - 'n/file-extension-in-import': 'off', // Nx plugins don't yet support ESM: https://github.com/nrwl/nx/issues/15682 'unicorn/prefer-module': 'off', // used instead of verbatimModuleSyntax tsconfig flag (requires ESM) From b942550a339609d002afd21eac10b615fe966a8b Mon Sep 17 00:00:00 2001 From: Alejandro <49059458+aramirezj@users.noreply.github.com> Date: Mon, 10 Mar 2025 22:42:28 +0100 Subject: [PATCH 11/14] Update packages/nx-plugin/src/plugin/utils.ts Co-authored-by: Michael Hladky <10064416+BioPhoton@users.noreply.github.com> --- packages/nx-plugin/src/plugin/utils.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/nx-plugin/src/plugin/utils.ts b/packages/nx-plugin/src/plugin/utils.ts index 356f773cb..17376cec8 100644 --- a/packages/nx-plugin/src/plugin/utils.ts +++ b/packages/nx-plugin/src/plugin/utils.ts @@ -1,7 +1,7 @@ import type { CreateNodesContext, CreateNodesContextV2 } from '@nx/devkit'; import { readFile } from 'node:fs/promises'; import * as path from 'node:path'; -import { CP_TARGET_NAME } from './constants'; +import { CP_TARGET_NAME } from './constants.js'; import type { CreateNodesOptions, NormalizedCreateNodesContext, From 4c2c20d45f6d8b66e5a8574796d5b33d49f11288 Mon Sep 17 00:00:00 2001 From: Alejandro <49059458+aramirezj@users.noreply.github.com> Date: Mon, 10 Mar 2025 22:42:35 +0100 Subject: [PATCH 12/14] Update packages/nx-plugin/src/plugin/types.ts Co-authored-by: Michael Hladky <10064416+BioPhoton@users.noreply.github.com> --- packages/nx-plugin/src/plugin/types.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/nx-plugin/src/plugin/types.ts b/packages/nx-plugin/src/plugin/types.ts index 0e96f0e21..623537fbe 100644 --- a/packages/nx-plugin/src/plugin/types.ts +++ b/packages/nx-plugin/src/plugin/types.ts @@ -4,7 +4,7 @@ import type { ProjectConfiguration, } from '@nx/devkit'; import type { WithRequired } from '@code-pushup/utils'; -import type { DynamicTargetOptions } from '../internal/types'; +import type { DynamicTargetOptions } from '../internal/types.js'; export type ProjectPrefixOptions = { projectPrefix?: string; From a59ee1410ce1b99332470e0cdc0a1886118de427 Mon Sep 17 00:00:00 2001 From: Alejandro Date: Mon, 10 Mar 2025 22:47:18 +0100 Subject: [PATCH 13/14] chore: add js --- .../src/plugin/target/configuration-target.ts | 8 ++++---- .../nx-plugin/src/plugin/target/executor-target.ts | 4 ++-- packages/nx-plugin/src/plugin/target/targets.ts | 10 +++++----- 3 files changed, 11 insertions(+), 11 deletions(-) diff --git a/packages/nx-plugin/src/plugin/target/configuration-target.ts b/packages/nx-plugin/src/plugin/target/configuration-target.ts index f9b16c985..dce38ff54 100644 --- a/packages/nx-plugin/src/plugin/target/configuration-target.ts +++ b/packages/nx-plugin/src/plugin/target/configuration-target.ts @@ -1,8 +1,8 @@ import type { TargetConfiguration } from '@nx/devkit'; -import type { RunCommandsOptions } from 'nx/src/executors/run-commands/run-commands.impl'; -import { objectToCliArgs } from '../../executors/internal/cli'; -import { PACKAGE_NAME } from '../../internal/constants'; -import { CP_TARGET_NAME } from '../constants'; +import type { RunCommandsOptions } from 'nx/src/executors/run-commands/run-commands.impl.js'; +import { objectToCliArgs } from '../../executors/internal/cli.js'; +import { PACKAGE_NAME } from '../../internal/constants.js'; +import { CP_TARGET_NAME } from '../constants.js'; export function createConfigurationTarget(options?: { targetName?: string; diff --git a/packages/nx-plugin/src/plugin/target/executor-target.ts b/packages/nx-plugin/src/plugin/target/executor-target.ts index aeba82ad8..e8b52eb8f 100644 --- a/packages/nx-plugin/src/plugin/target/executor-target.ts +++ b/packages/nx-plugin/src/plugin/target/executor-target.ts @@ -1,6 +1,6 @@ import type { TargetConfiguration } from '@nx/devkit'; -import { PACKAGE_NAME } from '../../internal/constants'; -import type { ProjectPrefixOptions } from '../types'; +import { PACKAGE_NAME } from '../../internal/constants.js'; +import type { ProjectPrefixOptions } from '../types.js'; export function createExecutorTarget(options?: { bin?: string; diff --git a/packages/nx-plugin/src/plugin/target/targets.ts b/packages/nx-plugin/src/plugin/target/targets.ts index e6d62e03a..3e659688b 100644 --- a/packages/nx-plugin/src/plugin/target/targets.ts +++ b/packages/nx-plugin/src/plugin/target/targets.ts @@ -1,9 +1,9 @@ import { readdir } from 'node:fs/promises'; -import { CP_TARGET_NAME } from '../constants'; -import type { NormalizedCreateNodesContext } from '../types'; -import { createConfigurationTarget } from './configuration-target'; -import { CODE_PUSHUP_CONFIG_REGEX } from './constants'; -import { createExecutorTarget } from './executor-target'; +import { CP_TARGET_NAME } from '../constants.js'; +import type { NormalizedCreateNodesContext } from '../types.js'; +import { createConfigurationTarget } from './configuration-target.js'; +import { CODE_PUSHUP_CONFIG_REGEX } from './constants.js'; +import { createExecutorTarget } from './executor-target.js'; export async function createTargets( normalizedContext: NormalizedCreateNodesContext, From 7264e6bd14a169f6a0a9bb9c8528ab01dc4e7dc2 Mon Sep 17 00:00:00 2001 From: Michael Date: Tue, 11 Mar 2025 00:25:54 +0100 Subject: [PATCH 14/14] test: add e2e for nx-plugin config derivation --- .../tests/plugin-derive-config.e2e.test.ts | 167 ++++++++++++++++++ testing/test-nx-utils/src/lib/utils/nx.ts | 20 ++- 2 files changed, 184 insertions(+), 3 deletions(-) create mode 100644 e2e/nx-plugin-e2e/tests/plugin-derive-config.e2e.test.ts diff --git a/e2e/nx-plugin-e2e/tests/plugin-derive-config.e2e.test.ts b/e2e/nx-plugin-e2e/tests/plugin-derive-config.e2e.test.ts new file mode 100644 index 000000000..f554fa1bc --- /dev/null +++ b/e2e/nx-plugin-e2e/tests/plugin-derive-config.e2e.test.ts @@ -0,0 +1,167 @@ +import { type Tree, writeJson } from '@nx/devkit'; +import path from 'node:path'; +import { readProjectConfiguration } from 'nx/src/generators/utils/project-configuration'; +import { afterEach, expect } from 'vitest'; +import { generateCodePushupConfig } from '@code-pushup/nx-plugin'; +import { + generateProject, + generateWorkspaceAndProject, + materializeTree, + nxShowProjectJson, + nxTargetProject, + registerPluginInWorkspace, +} from '@code-pushup/test-nx-utils'; +import { E2E_ENVIRONMENTS_DIR, TEST_OUTPUT_DIR } from '@code-pushup/test-utils'; + +describe('nx-plugin-derived-config', () => { + let root: string; + let tree: Tree; + const projectName = 'pkg'; + const testFileDir = path.join( + E2E_ENVIRONMENTS_DIR, + nxTargetProject(), + TEST_OUTPUT_DIR, + 'plugin-create-nodes', + ); + + beforeEach(async () => { + tree = await generateWorkspaceAndProject(); + registerPluginInWorkspace(tree, '@code-pushup/nx-plugin'); + await generateProject(tree, projectName); + root = readProjectConfiguration(tree, projectName).root; + generateCodePushupConfig(tree, root); + }); + + afterEach(async () => { + // await teardownTestFolder(testFileDir); + }); + + it('should derive config from project.json', async () => { + const cwd = path.join(testFileDir, 'project-config'); + const projectJsonPath = path.join('libs', projectName, 'project.json'); + const packageJsonPath = path.join('libs', projectName, 'package.json'); + tree.delete(projectJsonPath); + tree.delete(packageJsonPath); + writeJson(tree, projectJsonPath, { + root, + name: projectName, + targets: { + 'code-pushup': { + executor: `@code-pushup/nx-plugin:cli`, + options: { + 'persist.filename': 'my-report', + }, + }, + }, + }); + await materializeTree(tree, cwd); + + const { code, projectJson } = await nxShowProjectJson(cwd, projectName); + expect(code).toBe(0); + + expect(projectJson.targets).toStrictEqual( + expect.objectContaining({ + 'code-pushup': { + configurations: {}, + executor: `@code-pushup/nx-plugin:cli`, + options: { + 'persist.filename': 'my-report', + }, + parallelism: true, + }, + }), + ); + }); + + it('should derive config from package.json', async () => { + const cwd = path.join(testFileDir, 'package-config'); + const projectJsonPath = path.join('libs', projectName, 'project.json'); + const packageJsonPath = path.join('libs', projectName, 'package.json'); + tree.delete(projectJsonPath); + tree.delete(packageJsonPath); + writeJson(tree, packageJsonPath, { + name: `@code-pushup/${projectName}`, + nx: { + root, + name: projectName, + targets: { + 'code-pushup': { + executor: `@code-pushup/nx-plugin:cli`, + options: { + 'persist.filename': 'my-report', + }, + }, + }, + }, + }); + await materializeTree(tree, cwd); + + const { code, projectJson } = await nxShowProjectJson(cwd, projectName); + expect(code).toBe(0); + + expect(projectJson.targets).toStrictEqual( + expect.objectContaining({ + 'code-pushup': { + configurations: {}, + executor: `@code-pushup/nx-plugin:cli`, + options: { + 'persist.filename': 'my-report', + }, + parallelism: true, + }, + }), + ); + }); + + it('should derive config from mixed', async () => { + const cwd = path.join(testFileDir, 'mixed-config'); + const projectJsonPath = path.join('libs', projectName, 'project.json'); + const packageJsonPath = path.join('libs', projectName, 'package.json'); + + writeJson(tree, projectJsonPath, { + root, + name: projectName, + targets: { + 'code-pushup': { + executor: `@code-pushup/nx-plugin:cli`, + options: { + 'persist.filename': 'my-report', + }, + }, + }, + }); + writeJson(tree, packageJsonPath, { + name: `@code-pushup/${projectName}`, + nx: { + root, + name: projectName, + targets: { + 'code-pushup': { + executor: `@code-pushup/nx-plugin:cli`, + options: { + 'persist.outputPath': 'my-dir', + }, + }, + }, + }, + }); + await materializeTree(tree, cwd); + + const { code, projectJson } = await nxShowProjectJson(cwd, projectName); + expect(code).toBe(0); + + expect(projectJson.targets).toStrictEqual( + expect.objectContaining({ + 'code-pushup': { + configurations: {}, + executor: `@code-pushup/nx-plugin:cli`, + options: { + 'persist.filename': 'my-report', + 'persist.outputPath': 'my-dir', + }, + parallelism: true, + }, + }), + ); + }); +}); diff --git a/testing/test-nx-utils/src/lib/utils/nx.ts b/testing/test-nx-utils/src/lib/utils/nx.ts index e6e700068..252df5913 100644 --- a/testing/test-nx-utils/src/lib/utils/nx.ts +++ b/testing/test-nx-utils/src/lib/utils/nx.ts @@ -34,16 +34,16 @@ export function executorContext< }; } -export async function generateWorkspaceAndProject( +export async function generateProject( + tree: Tree, options: | string | (Omit, 'name'> & { name: string; }), ) { - const tree = createTreeWithEmptyWorkspace({ layout: 'apps-libs' }); const { name, ...normalizedOptions } = - typeof options === 'string' ? { name: options } : options; + typeof options === 'string' ? { name: options } : (options ?? {}); await libraryGenerator(tree, { name, directory: path.join('libs', name), @@ -56,6 +56,20 @@ export async function generateWorkspaceAndProject( projectNameAndRootFormat: 'as-provided', ...normalizedOptions, }); +} +export async function generateWorkspaceAndProject( + options?: + | string + | (Omit, 'name'> & { + name: string; + }), +) { + const tree = createTreeWithEmptyWorkspace({ layout: 'apps-libs' }); + const { name, ...opts } = + typeof options === 'string' ? { name: options } : (options ?? {}); + if (name) { + await generateProject(tree, { ...opts, name }); + } return tree; }