diff --git a/e2e/nx-plugin-e2e/tests/executor-cli.e2e.test.ts b/e2e/nx-plugin-e2e/tests/executor-cli.e2e.test.ts index 2a3797492..e5561a462 100644 --- a/e2e/nx-plugin-e2e/tests/executor-cli.e2e.test.ts +++ b/e2e/nx-plugin-e2e/tests/executor-cli.e2e.test.ts @@ -100,6 +100,32 @@ describe('executor command', () => { ).rejects.toThrow(''); }); + it('should execute print-config executor with api key', async () => { + const cwd = path.join(testFileDir, 'execute-print-config-command'); + await addTargetToWorkspace(tree, { cwd, project }); + + const { stdout, code } = await executeProcess({ + command: 'npx', + args: [ + 'nx', + 'run', + `${project}:code-pushup`, + 'print-config', + '--upload.apiKey=a123a', + ], + cwd, + }); + + expect(code).toBe(0); + const cleanStdout = removeColorCodes(stdout); + expect(cleanStdout).toContain('nx run my-lib:code-pushup print-config'); + expect(cleanStdout).toContain('a123a'); + + await expect(() => + readJsonFile(path.join(cwd, '.code-pushup', project, 'report.json')), + ).rejects.toThrow(''); + }); + it('should execute collect executor and merge target and command-line options', async () => { const cwd = path.join(testFileDir, 'execute-collect-with-merged-options'); await addTargetToWorkspace( diff --git a/packages/models/code-pushup.config.ts b/packages/models/code-pushup.config.ts new file mode 100644 index 000000000..dad38404f --- /dev/null +++ b/packages/models/code-pushup.config.ts @@ -0,0 +1,10 @@ +import { eslintCoreConfigNx } from '../../code-pushup.preset.js'; +import { mergeConfigs } from '../../dist/packages/utils/src/index.js'; + +// see: https://github.com/code-pushup/cli/blob/main/packages/models/docs/models-reference.md#coreconfig +export default mergeConfigs( + { + plugins: [], + }, + await eslintCoreConfigNx(), +); diff --git a/packages/models/tsconfig.lib.json b/packages/models/tsconfig.lib.json index 6b178086a..ae5510ec7 100644 --- a/packages/models/tsconfig.lib.json +++ b/packages/models/tsconfig.lib.json @@ -15,6 +15,7 @@ "exclude": [ "vite.config.unit.ts", "vite.config.integration.ts", + "code-pushup.config.ts", "zod2md.config.ts", "src/**/*.test.ts", "src/**/*.mock.ts", diff --git a/packages/models/tsconfig.test.json b/packages/models/tsconfig.test.json index bb1ab5e0c..c7a178bbe 100644 --- a/packages/models/tsconfig.test.json +++ b/packages/models/tsconfig.test.json @@ -4,6 +4,7 @@ "outDir": "../../dist/out-tsc", "types": ["vitest/globals", "vitest/importMeta", "vite/client", "node"] }, + "exclude": ["**/code-pushup.config.ts"], "include": [ "vite.config.unit.ts", "vite.config.integration.ts", diff --git a/packages/nx-plugin/eslint.config.cjs b/packages/nx-plugin/eslint.config.js similarity index 96% rename from packages/nx-plugin/eslint.config.cjs rename to packages/nx-plugin/eslint.config.js index e732748e6..937c98765 100644 --- a/packages/nx-plugin/eslint.config.cjs +++ b/packages/nx-plugin/eslint.config.js @@ -17,6 +17,7 @@ module.exports = tseslint.config( rules: { // Nx plugins don't yet support ESM: https://github.com/nrwl/nx/issues/15682 'unicorn/prefer-module': 'off', + 'n/file-extension-in-import': 'off', // used instead of verbatimModuleSyntax tsconfig flag (requires ESM) '@typescript-eslint/consistent-type-imports': [ 'warn', diff --git a/packages/nx-plugin/src/executors/cli/executor.unit.test.ts b/packages/nx-plugin/src/executors/cli/executor.unit.test.ts deleted file mode 100644 index 77a81a6f0..000000000 --- a/packages/nx-plugin/src/executors/cli/executor.unit.test.ts +++ /dev/null @@ -1,109 +0,0 @@ -import { logger } from '@nx/devkit'; -import { execSync } from 'node:child_process'; -import { afterEach, expect, vi } from 'vitest'; -import { executorContext } from '@code-pushup/test-nx-utils'; -import { MEMFS_VOLUME } from '@code-pushup/test-utils'; -import runAutorunExecutor from './executor.js'; - -vi.mock('node:child_process', async () => { - const actual = await vi.importActual('node:child_process'); - - return { - ...actual, - execSync: vi.fn((command: string) => { - if (command.includes('THROW_ERROR')) { - throw new Error(command); - } - }), - }; -}); - -describe('runAutorunExecutor', () => { - const loggerInfoSpy = vi.spyOn(logger, 'info'); - const loggerWarnSpy = vi.spyOn(logger, 'warn'); - - afterEach(() => { - loggerWarnSpy.mockReset(); - loggerInfoSpy.mockReset(); - }); - - it('should call execSync with return result', async () => { - const output = await runAutorunExecutor({}, executorContext('utils')); - expect(output.success).toBe(true); - expect(output.command).toMatch('npx @code-pushup/cli'); - // eslint-disable-next-line n/no-sync - expect(execSync).toHaveBeenCalledWith( - expect.stringContaining('npx @code-pushup/cli'), - { cwd: MEMFS_VOLUME }, - ); - }); - - it('should normalize context', async () => { - const output = await runAutorunExecutor( - {}, - { - ...executorContext('utils'), - cwd: 'cwd-form-context', - }, - ); - expect(output.success).toBe(true); - expect(output.command).toMatch('utils'); - // eslint-disable-next-line n/no-sync - expect(execSync).toHaveBeenCalledWith(expect.stringContaining('utils'), { - cwd: 'cwd-form-context', - }); - }); - - it('should process executorOptions', async () => { - const output = await runAutorunExecutor( - { persist: { filename: 'REPORT' } }, - executorContext('testing-utils'), - ); - expect(output.success).toBe(true); - expect(output.command).toMatch('--persist.filename="REPORT"'); - }); - - it('should create command from context, options and arguments', async () => { - vi.stubEnv('CP_PROJECT', 'CLI'); - const output = await runAutorunExecutor( - { persist: { filename: 'REPORT', format: ['md', 'json'] } }, - executorContext('core'), - ); - expect(output.command).toMatch('--persist.filename="REPORT"'); - expect(output.command).toMatch( - '--persist.format="md" --persist.format="json"', - ); - expect(output.command).toMatch('--upload.project="CLI"'); - }); - - it('should log information if verbose is set', async () => { - const output = await runAutorunExecutor( - { verbose: true }, - { ...executorContext('github-action'), cwd: '<CWD>' }, - ); - // eslint-disable-next-line n/no-sync - expect(execSync).toHaveBeenCalledTimes(1); - - expect(output.command).toMatch('--verbose'); - expect(loggerWarnSpy).toHaveBeenCalledTimes(0); - expect(loggerInfoSpy).toHaveBeenCalledTimes(2); - expect(loggerInfoSpy).toHaveBeenCalledWith( - expect.stringContaining(`Run CLI executor`), - ); - expect(loggerInfoSpy).toHaveBeenCalledWith( - expect.stringContaining('Command: npx @code-pushup/cli'), - ); - }); - - it('should log command if dryRun is set', async () => { - await runAutorunExecutor({ dryRun: true }, executorContext('utils')); - - expect(loggerInfoSpy).toHaveBeenCalledTimes(0); - expect(loggerWarnSpy).toHaveBeenCalledTimes(1); - expect(loggerWarnSpy).toHaveBeenCalledWith( - expect.stringContaining( - 'DryRun execution of: npx @code-pushup/cli --dryRun', - ), - ); - }); -}); diff --git a/packages/nx-plugin/src/executors/cli/utils.ts b/packages/nx-plugin/src/executors/cli/utils.ts index 61fccf0e9..afcca4542 100644 --- a/packages/nx-plugin/src/executors/cli/utils.ts +++ b/packages/nx-plugin/src/executors/cli/utils.ts @@ -27,6 +27,11 @@ export function parseAutorunExecutorOptions( const { projectPrefix, persist, upload, command } = options; const needsUploadParams = command === 'upload' || command === 'autorun' || command === undefined; + const uploadCfg = uploadConfig( + { projectPrefix, ...upload }, + normalizedContext, + ); + const hasApiToken = uploadCfg?.apiKey != null; return { ...parseAutorunExecutorOnlyOptions(options), ...globalConfig(options, normalizedContext), @@ -34,9 +39,7 @@ export function parseAutorunExecutorOptions( // @TODO This is a hack to avoid validation errors of upload config for commands that dont need it. // Fix: use utils and execute the core logic directly // Blocked by Nx plugins can't compile to es6 - upload: needsUploadParams - ? uploadConfig({ projectPrefix, ...upload }, normalizedContext) - : undefined, + ...(needsUploadParams && hasApiToken ? { upload: uploadCfg } : {}), }; } diff --git a/packages/nx-plugin/src/executors/cli/utils.unit.test.ts b/packages/nx-plugin/src/executors/cli/utils.unit.test.ts index 19da42b17..7a4141eff 100644 --- a/packages/nx-plugin/src/executors/cli/utils.unit.test.ts +++ b/packages/nx-plugin/src/executors/cli/utils.unit.test.ts @@ -73,14 +73,13 @@ describe('parseAutorunExecutorOptions', () => { }, }, ); - expect(osAgnosticPath(executorOptions.config)).toBe( + expect(osAgnosticPath(executorOptions.config ?? '')).toBe( osAgnosticPath('root/code-pushup.config.ts'), ); expect(executorOptions).toEqual( expect.objectContaining({ progress: false, verbose: false, - upload: { project: projectName }, }), ); @@ -92,20 +91,20 @@ describe('parseAutorunExecutorOptions', () => { }), ); - expect(osAgnosticPath(executorOptions.persist?.outputDir)).toBe( + expect(osAgnosticPath(executorOptions.persist?.outputDir ?? '')).toBe( osAgnosticPath('workspaceRoot/.code-pushup/my-app'), ); }); it.each<Command | undefined>(['upload', 'autorun', undefined])( - 'should include upload config for command %s', + 'should include upload config for command %s if API key is provided', command => { const projectName = 'my-app'; const executorOptions = parseAutorunExecutorOptions( { command, upload: { - organization: 'code-pushup', + apiKey: '123456789', }, }, { diff --git a/packages/nx-plugin/src/index.ts b/packages/nx-plugin/src/index.ts index 20ebba48a..e516b18ce 100644 --- a/packages/nx-plugin/src/index.ts +++ b/packages/nx-plugin/src/index.ts @@ -3,16 +3,16 @@ import { createNodes } from './plugin/index.js'; // default export for nx.json#plugins export default createNodes; -export * from './internal/versions.js'; -export { type InitGeneratorSchema } from './generators/init/schema.js'; -export { initGenerator, initSchematic } from './generators/init/generator.js'; -export type { ConfigurationGeneratorOptions } from './generators/configuration/schema.js'; -export { configurationGenerator } from './generators/configuration/generator.js'; +export type { AutorunCommandExecutorOptions } from './executors/cli/schema.js'; +export { objectToCliArgs } from './executors/internal/cli.js'; export { generateCodePushupConfig } from './generators/configuration/code-pushup-config.js'; -export { createNodes } from './plugin/index.js'; +export { configurationGenerator } from './generators/configuration/generator.js'; +export type { ConfigurationGeneratorOptions } from './generators/configuration/schema.js'; +export { initGenerator, initSchematic } from './generators/init/generator.js'; +export { type InitGeneratorSchema } from './generators/init/schema.js'; export { executeProcess, type ProcessConfig, } from './internal/execute-process.js'; -export { objectToCliArgs } from './executors/internal/cli.js'; -export type { AutorunCommandExecutorOptions } from './executors/cli/schema.js'; +export * from './internal/versions.js'; +export { createNodes } from './plugin/index.js'; diff --git a/packages/nx-plugin/src/internal/constants.ts b/packages/nx-plugin/src/internal/constants.ts index cac41a656..f69356ea3 100644 --- a/packages/nx-plugin/src/internal/constants.ts +++ b/packages/nx-plugin/src/internal/constants.ts @@ -1,5 +1,4 @@ -import { name } from '../../package.json'; - export const PROJECT_JSON_FILE_NAME = 'project.json'; -export const PACKAGE_NAME = name; +export const CODE_PUSHUP_CONFIG_REGEX = /^code-pushup(?:\.[\w-]+)?\.ts$/; +export const PACKAGE_NAME = '@code-pushup/nx-plugin'; export const DEFAULT_TARGET_NAME = 'code-pushup'; diff --git a/packages/nx-plugin/src/internal/execute-process.ts b/packages/nx-plugin/src/internal/execute-process.ts index ee5813397..cf61f3e84 100644 --- a/packages/nx-plugin/src/internal/execute-process.ts +++ b/packages/nx-plugin/src/internal/execute-process.ts @@ -125,7 +125,7 @@ export type ProcessObserver = { * // async process execution * const result = await executeProcess({ * command: 'node', - * args: ['download-data.js'], + * args: ['download-data'], * observer: { * onStdout: updateProgress, * error: handleError, diff --git a/packages/nx-plugin/src/internal/versions.ts b/packages/nx-plugin/src/internal/versions.ts index 884dad9a7..b7e24f64a 100644 --- a/packages/nx-plugin/src/internal/versions.ts +++ b/packages/nx-plugin/src/internal/versions.ts @@ -16,6 +16,10 @@ export const cpCliVersion = loadPackageJson( path.join(projectsFolder, 'models'), ).version; +/** + * Load the package.json file from the given folder path. + * @param folderPath + */ function loadPackageJson(folderPath: string): PackageJson { return readJsonFile<PackageJson>(path.join(folderPath, 'package.json')); } diff --git a/packages/nx-plugin/src/plugin/plugin.ts b/packages/nx-plugin/src/plugin/plugin.ts index 9129f1bd7..1f125f5a8 100644 --- a/packages/nx-plugin/src/plugin/plugin.ts +++ b/packages/nx-plugin/src/plugin/plugin.ts @@ -8,7 +8,7 @@ import { createTargets } from './target/targets.js'; import type { CreateNodesOptions } from './types.js'; import { normalizedCreateNodesContext } from './utils.js'; -// name has to be "createNodes" to get picked up by Nx +// name has to be "createNodes" to get picked up by Nx <v20 export const createNodes: CreateNodes = [ `**/${PROJECT_JSON_FILE_NAME}`, async ( diff --git a/packages/nx-plugin/src/plugin/plugin.unit.test.ts b/packages/nx-plugin/src/plugin/plugin.unit.test.ts index c51ebf570..62b3c0b2f 100644 --- a/packages/nx-plugin/src/plugin/plugin.unit.test.ts +++ b/packages/nx-plugin/src/plugin/plugin.unit.test.ts @@ -13,6 +13,7 @@ describe('@code-pushup/nx-plugin/plugin', () => { context = { nxJsonConfiguration: {}, workspaceRoot: '', + configFiles: [], }; }); diff --git a/packages/nx-plugin/src/plugin/target/targets.ts b/packages/nx-plugin/src/plugin/target/targets.ts index 3e659688b..55e608896 100644 --- a/packages/nx-plugin/src/plugin/target/targets.ts +++ b/packages/nx-plugin/src/plugin/target/targets.ts @@ -1,13 +1,19 @@ import { readdir } from 'node:fs/promises'; import { CP_TARGET_NAME } from '../constants.js'; -import type { NormalizedCreateNodesContext } from '../types.js'; +import type { + CreateNodesOptions, + ProjectConfigurationWithName, +} 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, -) { +export type CreateTargetsOptions = { + projectJson: ProjectConfigurationWithName; + projectRoot: string; + createOptions: CreateNodesOptions; +}; +export async function createTargets(normalizedContext: CreateTargetsOptions) { const { targetName = CP_TARGET_NAME, bin, diff --git a/packages/nx-plugin/src/plugin/types.ts b/packages/nx-plugin/src/plugin/types.ts index 5e8d59db7..4fd57ed95 100644 --- a/packages/nx-plugin/src/plugin/types.ts +++ b/packages/nx-plugin/src/plugin/types.ts @@ -1,6 +1,11 @@ -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 { CreateTargetsOptions } from './target/targets.js'; export type ProjectPrefixOptions = { projectPrefix?: string; @@ -13,8 +18,8 @@ export type ProjectConfigurationWithName = WithRequired< 'name' >; -export type NormalizedCreateNodesContext = CreateNodesContext & { - projectJson: ProjectConfigurationWithName; - projectRoot: string; - createOptions: CreateNodesOptions; -}; +export type NormalizedCreateNodesContext = CreateNodesContext & + CreateTargetsOptions; + +export type NormalizedCreateNodesV2Context = CreateNodesContextV2 & + CreateTargetsOptions; diff --git a/packages/nx-plugin/src/plugin/utils.ts b/packages/nx-plugin/src/plugin/utils.ts index e7a819f8d..8d551f682 100644 --- a/packages/nx-plugin/src/plugin/utils.ts +++ b/packages/nx-plugin/src/plugin/utils.ts @@ -1,10 +1,11 @@ -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 type { CreateNodesOptions, NormalizedCreateNodesContext, + NormalizedCreateNodesV2Context, ProjectConfigurationWithName, } from './types.js'; @@ -36,3 +37,29 @@ export async function normalizedCreateNodesContext( ); } } + +export async function normalizedCreateNodesV2Context( + context: CreateNodesContextV2, + projectConfigurationFile: string, + createOptions: CreateNodesOptions = {}, +): Promise<NormalizedCreateNodesV2Context> { + const projectRoot = path.dirname(projectConfigurationFile); + + try { + const projectJson = JSON.parse( + (await readFile(projectConfigurationFile)).toString(), + ) as ProjectConfigurationWithName; + + const { targetName = CP_TARGET_NAME } = createOptions; + return { + ...context, + projectJson, + projectRoot, + createOptions: { ...createOptions, targetName }, + }; + } catch { + throw new Error( + `Error parsing project.json file ${projectConfigurationFile}.`, + ); + } +} 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..65676bd9a 100644 --- a/testing/test-nx-utils/src/lib/utils/nx-plugin.ts +++ b/testing/test-nx-utils/src/lib/utils/nx-plugin.ts @@ -56,6 +56,21 @@ export async function invokeCreateNodesOnVirtualFiles< } export function createNodesContext( + options?: Partial<CreateNodesContext>, +): CreateNodesContext { + const { + workspaceRoot = process.cwd(), + nxJsonConfiguration = {}, + configFiles = [], + } = options ?? {}; + return { + workspaceRoot, + nxJsonConfiguration, + configFiles, + }; +} + +export function createNodesV2Context( options?: Partial<CreateNodesContextV2>, ): 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..7710cfccf 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 @@ -11,18 +11,22 @@ describe('createNodesContext', () => { workspaceRoot: 'root', nxJsonConfiguration: { plugins: [] }, }); - expect(context).toEqual({ - workspaceRoot: 'root', - nxJsonConfiguration: { plugins: [] }, - }); + expect(context).toStrictEqual( + expect.objectContaining({ + workspaceRoot: 'root', + nxJsonConfiguration: { plugins: [] }, + }), + ); }); it('should return a context with defaults', () => { const context = createNodesContext(); - expect(context).toEqual({ - workspaceRoot: process.cwd(), - nxJsonConfiguration: {}, - }); + expect(context).toStrictEqual( + expect.objectContaining({ + workspaceRoot: process.cwd(), + nxJsonConfiguration: {}, + }), + ); }); });