diff --git a/e2e/create-cli-e2e/mocks/create-npm-workshpace.ts b/e2e/create-cli-e2e/mocks/create-npm-workshpace.ts index 62b6942b5..741c90e4a 100644 --- a/e2e/create-cli-e2e/mocks/create-npm-workshpace.ts +++ b/e2e/create-cli-e2e/mocks/create-npm-workshpace.ts @@ -1,14 +1,21 @@ -import { mkdir, writeFile } from "node:fs/promises"; -import { join } from "node:path"; +import { mkdir, writeFile } from 'node:fs/promises'; +import { join } from 'node:path'; export async function createNpmWorkspace(cwd: string) { await mkdir(cwd, { recursive: true }); - await writeFile(join(cwd, 'package.json'), JSON.stringify({ - name: 'create-npm-workspace', - version: '0.0.1', - scripts: { - test: 'echo "Error: no test specified" && exit 1', - }, - keywords: [], - }, null, 2)); + await writeFile( + join(cwd, 'package.json'), + JSON.stringify( + { + name: 'create-npm-workspace', + version: '0.0.1', + scripts: { + test: 'echo "Error: no test specified" && exit 1', + }, + keywords: [], + }, + null, + 2, + ), + ); } diff --git a/e2e/nx-plugin-e2e/project.json b/e2e/nx-plugin-e2e/project.json index b3acce3eb..b683fc5d1 100644 --- a/e2e/nx-plugin-e2e/project.json +++ b/e2e/nx-plugin-e2e/project.json @@ -18,6 +18,6 @@ } } }, - "implicitDependencies": ["nx-plugin"], + "implicitDependencies": ["nx-plugin", "test-utils"], "tags": ["scope:tooling", "type:e2e"] } diff --git a/e2e/nx-plugin-e2e/tests/__snapshots__/nx-plugin.e2e.test.ts.snap b/e2e/nx-plugin-e2e/tests/__snapshots__/create-nodes-plugin.e2e.test.ts.snap similarity index 100% rename from e2e/nx-plugin-e2e/tests/__snapshots__/nx-plugin.e2e.test.ts.snap rename to e2e/nx-plugin-e2e/tests/__snapshots__/create-nodes-plugin.e2e.test.ts.snap diff --git a/e2e/nx-plugin-e2e/tests/nx-plugin.e2e.test.ts b/e2e/nx-plugin-e2e/tests/create-nodes-plugin.e2e.test.ts similarity index 87% rename from e2e/nx-plugin-e2e/tests/nx-plugin.e2e.test.ts rename to e2e/nx-plugin-e2e/tests/create-nodes-plugin.e2e.test.ts index abc300db2..7d6dcea5a 100644 --- a/e2e/nx-plugin-e2e/tests/nx-plugin.e2e.test.ts +++ b/e2e/nx-plugin-e2e/tests/create-nodes-plugin.e2e.test.ts @@ -189,30 +189,24 @@ describe('nx-plugin', () => { )}";`, plugins: [ { - fileImports: `import jsPackagesPlugin from "${join( + fileImports: `import {customPlugin} from "${join( relativePathToCwd(cwd), pathRelativeToPackage, - 'dist/packages/plugin-js-packages', + 'dist/testing/test-utils', )}";`, - // @TODO improve formatObjectToJsString to get rid of the "`" hack - codeStrings: 'await jsPackagesPlugin({packageManager: `npm`})', + codeStrings: 'customPlugin()', }, ], }); + await materializeTree(tree, cwd); - const { stdout, stderr } = await executeProcess({ + const { stdout } = await executeProcess({ command: 'npx', - args: ['nx', 'run', `${project}:code-pushup -- --dryRun`], + args: ['nx', 'run', `${project}:code-pushup`, '--dryRun'], cwd, }); - const cleanStderr = removeColorCodes(stderr); - // @TODO create test environment for working plugin. This here misses package-lock.json to execute correctly - expect(cleanStderr).toContain( - 'DryRun execution of: npx @code-pushup/cli autorun', - ); - const cleanStdout = removeColorCodes(stdout); expect(cleanStdout).toContain( 'NX Successfully ran target code-pushup for project my-lib', @@ -242,7 +236,33 @@ describe('nx-plugin', () => { }); }); - it('should NOT add targets dynamically if plugin is NOT registered', async () => { + it('should consider plugin option projectPrefix in executor target', async () => { + const cwd = join(baseDir, 'configuration-option-bin'); + registerPluginInWorkspace(tree, { + plugin: join(relativePathToCwd(cwd), 'dist/packages/nx-plugin'), + options: { + projectPrefix: 'cli', + }, + }); + const { root } = readProjectConfiguration(tree, project); + generateCodePushupConfig(tree, root); + await materializeTree(tree, cwd); + + const { code, projectJson } = await nxShowProjectJson(cwd, project); + + expect(code).toBe(0); + + expect(projectJson.targets).toStrictEqual({ + ['code-pushup']: expect.objectContaining({ + executor: `@code-pushup/nx-plugin:autorun`, + options: { + projectPrefix: 'cli', + }, + }), + }); + }); + + it('should NOT add targets dynamically if plugin is not registered', async () => { const cwd = join(baseDir, 'plugin-not-registered'); await materializeTree(tree, cwd); diff --git a/e2e/nx-plugin-e2e/tests/executor-autorun.e2e.test.ts b/e2e/nx-plugin-e2e/tests/executor-autorun.e2e.test.ts new file mode 100644 index 000000000..c265949bc --- /dev/null +++ b/e2e/nx-plugin-e2e/tests/executor-autorun.e2e.test.ts @@ -0,0 +1,85 @@ +import { Tree, updateProjectConfiguration } from '@nx/devkit'; +import { rm } from 'node:fs/promises'; +import { join, relative } 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, +} from '@code-pushup/test-nx-utils'; +import { removeColorCodes } from '@code-pushup/test-utils'; +import { executeProcess } from '@code-pushup/utils'; + +function relativePathToCwd(testDir: string): string { + return relative(join(process.cwd(), testDir), process.cwd()); +} + +async function addTargetToWorkspace( + tree: Tree, + options: { cwd: string; project: string }, +) { + const { cwd, project } = options; + const pathRelativeToPackage = relative(join(cwd, 'libs', project), cwd); + const projectCfg = readProjectConfiguration(tree, project); + updateProjectConfiguration(tree, project, { + ...projectCfg, + targets: { + ...projectCfg.targets, + ['code-pushup']: { + executor: `${join( + relativePathToCwd(cwd), + 'dist/packages/nx-plugin', + )}:autorun`, + }, + }, + }); + const { root } = projectCfg; + generateCodePushupConfig(tree, root, { + fileImports: `import type {CoreConfig} from "${join( + relativePathToCwd(cwd), + pathRelativeToPackage, + 'dist/packages/models', + )}";`, + plugins: [ + { + fileImports: `import {customPlugin} from "${join( + relativePathToCwd(cwd), + pathRelativeToPackage, + 'dist/testing/test-utils', + )}";`, + codeStrings: 'customPlugin()', + }, + ], + }); + await materializeTree(tree, cwd); +} + +describe('executor autorun', () => { + let tree: Tree; + const project = 'my-lib'; + const baseDir = 'tmp/nx-plugin-e2e/executor'; + + beforeEach(async () => { + tree = await generateWorkspaceAndProject(project); + }); + + afterEach(async () => { + await rm(baseDir, { recursive: true, force: true }); + }); + + it('should execute autorun executor', async () => { + const cwd = join(baseDir, 'execute-dynamic-executor'); + await addTargetToWorkspace(tree, { cwd, project }); + + const { stdout, code } = await executeProcess({ + command: 'npx', + args: ['nx', 'run', `${project}:code-pushup`, '--dryRun'], + cwd, + }); + + expect(code).toBe(0); + const cleanStdout = removeColorCodes(stdout); + expect(cleanStdout).toContain('nx run my-lib:code-pushup --dryRun'); + }); +}); diff --git a/e2e/nx-plugin-e2e/tests/configuration.e2e.test.ts b/e2e/nx-plugin-e2e/tests/generator-configuration.e2e.test.ts similarity index 100% rename from e2e/nx-plugin-e2e/tests/configuration.e2e.test.ts rename to e2e/nx-plugin-e2e/tests/generator-configuration.e2e.test.ts diff --git a/e2e/nx-plugin-e2e/tests/init.e2e.test.ts b/e2e/nx-plugin-e2e/tests/generator-init.e2e.test.ts similarity index 100% rename from e2e/nx-plugin-e2e/tests/init.e2e.test.ts rename to e2e/nx-plugin-e2e/tests/generator-init.e2e.test.ts diff --git a/packages/nx-plugin/src/internal/types.ts b/packages/nx-plugin/src/internal/types.ts index 6468db0be..bf3a2d047 100644 --- a/packages/nx-plugin/src/internal/types.ts +++ b/packages/nx-plugin/src/internal/types.ts @@ -1,5 +1,4 @@ export type DynamicTargetOptions = { - // @TODO add prefix https://github.com/code-pushup/cli/issues/619 targetName?: string; bin?: string; }; diff --git a/packages/nx-plugin/src/plugin/plugin.unit.test.ts b/packages/nx-plugin/src/plugin/plugin.unit.test.ts index df7f99a2f..56854b225 100644 --- a/packages/nx-plugin/src/plugin/plugin.unit.test.ts +++ b/packages/nx-plugin/src/plugin/plugin.unit.test.ts @@ -20,7 +20,7 @@ describe('@code-pushup/nx-plugin/plugin', () => { vol.reset(); }); - it('should normalize context and use it to create target on ROOT project', async () => { + 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({ @@ -46,7 +46,7 @@ describe('@code-pushup/nx-plugin/plugin', () => { }); }); - it('should normalize context and use it to create target on PACKAGE project', async () => { + 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({ @@ -71,4 +71,68 @@ describe('@code-pushup/nx-plugin/plugin', () => { }, }); }); + + 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`]: '{}', + }; + + await expect( + invokeCreateNodesOnVirtualFiles( + createNodes, + context, + { + projectPrefix: 'cli', + }, + { matchingFilesData }, + ), + ).resolves.toStrictEqual({ + [projectRoot]: { + targets: { + [CP_TARGET_NAME]: { + executor: `${PACKAGE_NAME}:autorun`, + options: { + projectPrefix: 'cli', + }, + }, + }, + }, + }); + }); + + 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}:autorun`, + options: { + projectPrefix: 'cli', + }, + }, + }, + }, + }); + }); }); diff --git a/packages/nx-plugin/src/plugin/target/constants.ts b/packages/nx-plugin/src/plugin/target/constants.ts new file mode 100644 index 000000000..79e804e40 --- /dev/null +++ b/packages/nx-plugin/src/plugin/target/constants.ts @@ -0,0 +1,2 @@ +export const CODE_PUSHUP_CONFIG_REGEX = + /^code-pushup\.config\.(\w*\.)*(ts|js|mjs)$/; diff --git a/packages/nx-plugin/src/plugin/target/executor-target.ts b/packages/nx-plugin/src/plugin/target/executor-target.ts index 14eaaff07..2a2517b32 100644 --- a/packages/nx-plugin/src/plugin/target/executor-target.ts +++ b/packages/nx-plugin/src/plugin/target/executor-target.ts @@ -1,12 +1,20 @@ import { TargetConfiguration } from '@nx/devkit'; -import { RunCommandsOptions } from 'nx/src/executors/run-commands/run-commands.impl'; import { PACKAGE_NAME } from '../../internal/constants'; +import { ProjectPrefixOptions } from '../types'; export function createExecutorTarget(options?: { bin?: string; -}): TargetConfiguration { - const { bin = PACKAGE_NAME } = options ?? {}; + projectPrefix?: string; +}): TargetConfiguration { + const { bin = PACKAGE_NAME, projectPrefix } = options ?? {}; return { executor: `${bin}:autorun`, + ...(projectPrefix + ? { + options: { + projectPrefix, + }, + } + : {}), }; } diff --git a/packages/nx-plugin/src/plugin/target/executor.target.unit.test.ts b/packages/nx-plugin/src/plugin/target/executor.target.unit.test.ts index 245fc4c66..198027b9f 100644 --- a/packages/nx-plugin/src/plugin/target/executor.target.unit.test.ts +++ b/packages/nx-plugin/src/plugin/target/executor.target.unit.test.ts @@ -13,4 +13,13 @@ describe('createExecutorTarget', () => { executor: 'xyz:autorun', }); }); + + it('should use projectPrefix if provided', () => { + expect(createExecutorTarget({ projectPrefix: 'cli' })).toStrictEqual({ + executor: '@code-pushup/nx-plugin:autorun', + options: { + projectPrefix: 'cli', + }, + }); + }); }); diff --git a/packages/nx-plugin/src/plugin/target/targets.ts b/packages/nx-plugin/src/plugin/target/targets.ts index 66521c88f..e6d62e03a 100644 --- a/packages/nx-plugin/src/plugin/target/targets.ts +++ b/packages/nx-plugin/src/plugin/target/targets.ts @@ -2,19 +2,21 @@ 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'; export async function createTargets( normalizedContext: NormalizedCreateNodesContext, ) { - const { targetName = CP_TARGET_NAME, bin } = normalizedContext.createOptions; + const { + targetName = CP_TARGET_NAME, + bin, + projectPrefix, + } = normalizedContext.createOptions; const rootFiles = await readdir(normalizedContext.projectRoot); - return rootFiles.some(filename => - filename.match(/^code-pushup\.config.(\w*\.)*(ts|js|mjs)$/), - ) - ? // @TODO return code-pushup cli target https://github.com/code-pushup/cli/issues/619 - { - [targetName]: createExecutorTarget({ bin }), + return rootFiles.some(filename => filename.match(CODE_PUSHUP_CONFIG_REGEX)) + ? { + [targetName]: createExecutorTarget({ bin, projectPrefix }), } : // if NO code-pushup.config.*.(ts|js|mjs) is present return configuration target { diff --git a/packages/nx-plugin/src/plugin/target/targets.unit.test.ts b/packages/nx-plugin/src/plugin/target/targets.unit.test.ts index fcb6fc0f4..a784bde7e 100644 --- a/packages/nx-plugin/src/plugin/target/targets.unit.test.ts +++ b/packages/nx-plugin/src/plugin/target/targets.unit.test.ts @@ -161,4 +161,29 @@ describe('createTargets', () => { }, }); }); + + it('should include projectPrefix options in executor targets if given', async () => { + const projectName = 'plugin-my-plugin'; + vol.fromJSON( + { + [`code-pushup.config.ts`]: `{}`, + }, + MEMFS_VOLUME, + ); + await expect( + createTargets({ + projectRoot: '.', + projectJson: { + name: projectName, + }, + createOptions: { + projectPrefix: 'cli', + }, + } as NormalizedCreateNodesContext), + ).resolves.toStrictEqual({ + [DEFAULT_TARGET_NAME]: expect.objectContaining({ + options: { projectPrefix: 'cli' }, + }), + }); + }); }); diff --git a/packages/nx-plugin/src/plugin/types.ts b/packages/nx-plugin/src/plugin/types.ts index de4717abb..5e55eb8e8 100644 --- a/packages/nx-plugin/src/plugin/types.ts +++ b/packages/nx-plugin/src/plugin/types.ts @@ -2,7 +2,11 @@ import type { CreateNodesContext, ProjectConfiguration } from '@nx/devkit'; import { WithRequired } from '@code-pushup/utils'; import { DynamicTargetOptions } from '../internal/types'; -export type CreateNodesOptions = DynamicTargetOptions; +export type ProjectPrefixOptions = { + projectPrefix?: string; +}; + +export type CreateNodesOptions = DynamicTargetOptions & ProjectPrefixOptions; export type ProjectConfigurationWithName = WithRequired< ProjectConfiguration, diff --git a/testing/test-utils/src/index.ts b/testing/test-utils/src/index.ts index 19e1fa5ac..a2de808ca 100644 --- a/testing/test-utils/src/index.ts +++ b/testing/test-utils/src/index.ts @@ -12,6 +12,7 @@ export * from './lib/utils/commit.mock'; export * from './lib/utils/core-config.mock'; export * from './lib/utils/minimal-config.mock'; export * from './lib/utils/report.mock'; +export * from './lib/fixtures/configs/custom-plugin'; // dynamic mocks export * from './lib/utils/dynamic-mocks/categories.mock'; diff --git a/testing/test-utils/src/lib/fixtures/configs/custom-plugin.ts b/testing/test-utils/src/lib/fixtures/configs/custom-plugin.ts index d385c066a..6afe6bc80 100644 --- a/testing/test-utils/src/lib/fixtures/configs/custom-plugin.ts +++ b/testing/test-utils/src/lib/fixtures/configs/custom-plugin.ts @@ -1,4 +1,4 @@ -const customPlugin = { +const customPluginConfig = { slug: 'good-feels', title: 'Good feels', icon: 'javascript', @@ -18,4 +18,7 @@ const customPlugin = { ], }; -export default customPlugin; +export function customPlugin() { + return customPluginConfig; +} +export default customPluginConfig;