diff --git a/e2e/nx-plugin-e2e/tests/plugin-plugins-config.e2e.test.ts b/e2e/nx-plugin-e2e/tests/plugin-plugins-config.e2e.test.ts new file mode 100644 index 000000000..6aeb7bb29 --- /dev/null +++ b/e2e/nx-plugin-e2e/tests/plugin-plugins-config.e2e.test.ts @@ -0,0 +1,99 @@ +import type { Tree } 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 { + generateWorkspaceAndProject, + materializeTree, + nxShowProjectJson, + nxTargetProject, + registerPluginInWorkspace, +} from '@code-pushup/test-nx-utils'; +import { + E2E_ENVIRONMENTS_DIR, + TEST_OUTPUT_DIR, + teardownTestFolder, +} from '@code-pushup/test-utils'; +import { INLINE_PLUGIN } from '../mocks/inline-plugin.js'; + +describe('nx-plugin pluginsConfig', () => { + let tree: Tree; + const project = 'my-lib'; + const testFileDir = path.join( + E2E_ENVIRONMENTS_DIR, + nxTargetProject(), + TEST_OUTPUT_DIR, + 'plugin-plugins-config', + ); + + beforeEach(async () => { + tree = await generateWorkspaceAndProject(project); + }); + + afterEach(async () => { + await teardownTestFolder(testFileDir); + }); + + it('should apply pluginsConfig options to executor target', async () => { + const cwd = path.join(testFileDir, 'plugins-config-applied'); + const binPath = 'packages/cli/src/index.ts'; + const configPath = '{projectRoot}/code-pushup.config.ts'; + const projectPrefix = 'cli'; + + // Register plugin with options in the plugins array and pluginsConfig + registerPluginInWorkspace( + tree, + { + plugin: '@code-pushup/nx-plugin', + options: { + config: configPath, + persist: { + outputDir: '.code-pushup/{projectName}', + }, + }, + }, + { + projectPrefix, + bin: binPath, + env: { + NODE_OPTIONS: '--import tsx', + TSX_TSCONFIG_PATH: 'tsconfig.base.json', + }, + }, + ); + + const { root } = readProjectConfiguration(tree, project); + generateCodePushupConfig(tree, root, { + plugins: [ + { + fileImports: '', + codeStrings: INLINE_PLUGIN, + }, + ], + }); + + await materializeTree(tree, cwd); + + const { code, projectJson } = await nxShowProjectJson(cwd, project); + expect(code).toBe(0); + + expect(projectJson).toStrictEqual( + expect.objectContaining({ + targets: expect.objectContaining({ + 'code-pushup': expect.objectContaining({ + executor: '@code-pushup/nx-plugin:cli', + options: expect.objectContaining({ + projectPrefix, + bin: binPath, + env: { + NODE_OPTIONS: '--import tsx', + TSX_TSCONFIG_PATH: 'tsconfig.base.json', + }, + }), + }), + }), + }), + ); + }); +}); diff --git a/packages/nx-plugin/src/executors/cli/executor.ts b/packages/nx-plugin/src/executors/cli/executor.ts index b7d2772d7..8db40c418 100644 --- a/packages/nx-plugin/src/executors/cli/executor.ts +++ b/packages/nx-plugin/src/executors/cli/executor.ts @@ -24,6 +24,7 @@ export default async function runCliExecutor( dryRun, env: executorEnv, bin, + projectPrefix, // Do not forward to CLI, it is handled plugin logic only ...restArgs } = parseCliExecutorOptions(terminalAndExecutorOptions, normalizedContext); // this sets `CP_VERBOSE=true` on process.env @@ -48,7 +49,7 @@ export default async function runCliExecutor( logger.warn(`DryRun execution of: ${commandString}`); } else { try { - logger.debug(`Run CLI with env vars: ${loggedEnvVars}`); + logger.debug(`Run CLI with env vars: ${JSON.stringify(loggedEnvVars)}`); await executeProcess({ command, args, diff --git a/packages/nx-plugin/src/executors/internal/config.ts b/packages/nx-plugin/src/executors/internal/config.ts index 7c285a971..7c0abbe5a 100644 --- a/packages/nx-plugin/src/executors/internal/config.ts +++ b/packages/nx-plugin/src/executors/internal/config.ts @@ -50,15 +50,11 @@ export function uploadConfig( const { projectPrefix, server, apiKey, organization, project, timeout } = options; - const applyPrefix = workspaceRoot === '.'; - const prefix = projectPrefix ? `${projectPrefix}-` : ''; + const applyPrefix = workspaceRoot !== '.'; + const prefix = projectPrefix && applyPrefix ? `${projectPrefix}-` : ''; const derivedProject = - projectName && !project - ? applyPrefix - ? `${prefix}${projectName}` - : projectName - : project; + projectName && !project ? `${prefix}${projectName}` : project; return { ...parseEnv(process.env), diff --git a/packages/nx-plugin/src/executors/internal/config.unit.test.ts b/packages/nx-plugin/src/executors/internal/config.unit.test.ts index cf4077b55..c9d948bdd 100644 --- a/packages/nx-plugin/src/executors/internal/config.unit.test.ts +++ b/packages/nx-plugin/src/executors/internal/config.unit.test.ts @@ -333,6 +333,39 @@ describe('uploadConfig', () => { }, { workspaceRoot: 'workspaceRoot', projectName: 'my-app' }, ), - ).toEqual(expect.objectContaining({ project: 'my-app2' })); + ).toStrictEqual(expect.objectContaining({ project: 'my-app2' })); + }); + + it('should apply projectPrefix when workspaceRoot is not "."', () => { + expect( + uploadConfig( + { + ...baseUploadConfig, + projectPrefix: 'cli', + }, + { workspaceRoot: 'workspace-root', projectName: 'models' }, + ), + ).toStrictEqual(expect.objectContaining({ project: 'cli-models' })); + }); + + it('should NOT apply projectPrefix when workspaceRoot is "."', () => { + expect( + uploadConfig( + { + ...baseUploadConfig, + projectPrefix: 'cli', + }, + { workspaceRoot: '.', projectName: 'models' }, + ), + ).toStrictEqual(expect.objectContaining({ project: 'models' })); + }); + + it('should NOT apply projectPrefix when projectPrefix is not provided', () => { + expect( + uploadConfig(baseUploadConfig, { + workspaceRoot: 'workspace-root', + projectName: 'models', + }), + ).toStrictEqual(expect.objectContaining({ project: 'models' })); }); }); diff --git a/packages/nx-plugin/src/index.ts b/packages/nx-plugin/src/index.ts index 31beb6920..653ebad6c 100644 --- a/packages/nx-plugin/src/index.ts +++ b/packages/nx-plugin/src/index.ts @@ -1,8 +1,9 @@ +import { PLUGIN_NAME } from './plugin/constants.js'; import { createNodes, createNodesV2 } from './plugin/index.js'; // default export for nx.json#plugins const plugin = { - name: '@code-pushup/nx-plugin', + name: PLUGIN_NAME, createNodesV2, // Keep for backwards compatibility with Nx < 21 createNodes, diff --git a/packages/nx-plugin/src/plugin/constants.ts b/packages/nx-plugin/src/plugin/constants.ts index bf2e81d9f..5be953791 100644 --- a/packages/nx-plugin/src/plugin/constants.ts +++ b/packages/nx-plugin/src/plugin/constants.ts @@ -1 +1,2 @@ export const CP_TARGET_NAME = 'code-pushup'; +export const PLUGIN_NAME = '@code-pushup/nx-plugin'; diff --git a/packages/nx-plugin/src/plugin/plugin.ts b/packages/nx-plugin/src/plugin/plugin.ts index 1b4e3e2f1..9458374f9 100644 --- a/packages/nx-plugin/src/plugin/plugin.ts +++ b/packages/nx-plugin/src/plugin/plugin.ts @@ -7,6 +7,7 @@ import type { CreateNodesV2, } from '@nx/devkit'; import { PROJECT_JSON_FILE_NAME } from '../internal/constants.js'; +import { PLUGIN_NAME } from './constants.js'; import { createTargets } from './target/targets.js'; import type { CreateNodesOptions } from './types.js'; import { @@ -15,6 +16,9 @@ import { } from './utils.js'; // name has to be "createNodes" to get picked up by Nx => { const parsedCreateNodesOptions = createNodesOptions as CreateNodesOptions; + const pluginsConfig = + context.nxJsonConfiguration.pluginsConfig?.[PLUGIN_NAME] ?? {}; + const mergedOptions = { ...pluginsConfig, ...parsedCreateNodesOptions }; + const normalizedContext = await normalizedCreateNodesContext( context, projectConfigurationFile, - parsedCreateNodesOptions, + mergedOptions, ); return { @@ -47,13 +55,16 @@ export const createNodesV2: CreateNodesV2 = [ context: CreateNodesContextV2, ): Promise => { const parsedCreateNodesOptions = createNodesOptions as CreateNodesOptions; + const { pluginsConfig = {} } = context.nxJsonConfiguration; + const pluginsConfigObj = pluginsConfig[PLUGIN_NAME] ?? {}; + const mergedOptions = { ...pluginsConfigObj, ...parsedCreateNodesOptions }; return await Promise.all( projectConfigurationFiles.map(async projectConfigurationFile => { const normalizedContext = await normalizedCreateNodesV2Context( context, projectConfigurationFile, - parsedCreateNodesOptions, + mergedOptions, ); const result: CreateNodesResult = { diff --git a/testing/test-nx-utils/src/lib/utils/nx.ts b/testing/test-nx-utils/src/lib/utils/nx.ts index 050bdc541..97d565ccf 100644 --- a/testing/test-nx-utils/src/lib/utils/nx.ts +++ b/testing/test-nx-utils/src/lib/utils/nx.ts @@ -65,6 +65,7 @@ export async function generateWorkspaceAndProject( export function registerPluginInWorkspace( tree: Tree, configuration: PluginConfiguration, + pluginConfig?: Record, ) { const normalizedPluginConfiguration = typeof configuration === 'string' @@ -72,9 +73,21 @@ export function registerPluginInWorkspace( plugin: configuration, } : configuration; + + const pluginName = + typeof configuration === 'string' ? configuration : configuration.plugin; + updateJson(tree, 'nx.json', (json: NxJsonConfiguration) => ({ ...json, plugins: [...(json.plugins ?? []), normalizedPluginConfiguration], + ...(pluginConfig + ? { + pluginsConfig: { + ...json.pluginsConfig, + [pluginName]: pluginConfig, + }, + } + : {}), })); } diff --git a/testing/test-nx-utils/src/lib/utils/nx.unit.test.ts b/testing/test-nx-utils/src/lib/utils/nx.unit.test.ts index fd6593875..5da045870 100644 --- a/testing/test-nx-utils/src/lib/utils/nx.unit.test.ts +++ b/testing/test-nx-utils/src/lib/utils/nx.unit.test.ts @@ -91,4 +91,38 @@ describe('registerPluginInWorkspace', () => { }), ); }); + + it('should register pluginsConfig when provided', () => { + const tree = createTreeWithEmptyWorkspace({ layout: 'apps-libs' }); + + registerPluginInWorkspace( + tree, + { + plugin: '@code-pushup/nx-plugin', + options: { targetName: 'code-pushup' }, + }, + { + projectPrefix: 'cli', + bin: 'packages/cli/src/index.ts', + }, + ); + + const nxJson = JSON.parse(tree.read('nx.json')?.toString() ?? '{}'); + expect(nxJson).toStrictEqual( + expect.objectContaining({ + plugins: [ + { + plugin: '@code-pushup/nx-plugin', + options: { targetName: 'code-pushup' }, + }, + ], + pluginsConfig: { + '@code-pushup/nx-plugin': { + projectPrefix: 'cli', + bin: 'packages/cli/src/index.ts', + }, + }, + }), + ); + }); });