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..689c555b2 100644 --- a/e2e/nx-plugin-e2e/tests/executor-cli.e2e.test.ts +++ b/e2e/nx-plugin-e2e/tests/executor-cli.e2e.test.ts @@ -1,7 +1,7 @@ import { type Tree, updateProjectConfiguration } from '@nx/devkit'; import path from 'node:path'; import { readProjectConfiguration } from 'nx/src/generators/utils/project-configuration'; -import { afterEach, expect } from 'vitest'; +import { afterAll, afterEach, beforeEach, expect, vi } from 'vitest'; import { type AutorunCommandExecutorOptions, generateCodePushupConfig, @@ -58,8 +58,19 @@ describe('executor command', () => { TEST_OUTPUT_DIR, 'executor-cli', ); + const processEnvCP = Object.fromEntries( + Object.entries(process.env).filter(([k]) => k.startsWith('CP_')), + ); + + /* eslint-disable functional/immutable-data, @typescript-eslint/no-dynamic-delete */ + beforeAll(() => { + Object.entries(process.env) + .filter(([k]) => k.startsWith('CP_')) + .forEach(([k]) => delete process.env[k]); + }); beforeEach(async () => { + vi.unstubAllEnvs(); tree = await generateWorkspaceAndProject(project); }); @@ -67,6 +78,11 @@ describe('executor command', () => { await teardownTestFolder(testFileDir); }); + afterAll(() => { + Object.entries(processEnvCP).forEach(([k, v]) => (process.env[k] = v)); + }); + /* eslint-enable functional/immutable-data, @typescript-eslint/no-dynamic-delete */ + it('should execute no specific command by default', async () => { const cwd = path.join(testFileDir, 'execute-default-command'); await addTargetToWorkspace(tree, { cwd, project }); @@ -100,6 +116,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/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 100% rename from packages/nx-plugin/eslint.config.cjs rename to packages/nx-plugin/eslint.config.js diff --git a/packages/nx-plugin/src/executors/cli/executor.unit.test.ts b/packages/nx-plugin/src/executors/cli/executor.unit.test.ts index 77a81a6f0..9af4f1b6b 100644 --- a/packages/nx-plugin/src/executors/cli/executor.unit.test.ts +++ b/packages/nx-plugin/src/executors/cli/executor.unit.test.ts @@ -1,6 +1,6 @@ import { logger } from '@nx/devkit'; import { execSync } from 'node:child_process'; -import { afterEach, expect, vi } from 'vitest'; +import { afterAll, afterEach, beforeEach, 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'; @@ -19,14 +19,33 @@ vi.mock('node:child_process', async () => { }); describe('runAutorunExecutor', () => { + const processEnvCP = Object.fromEntries( + Object.entries(process.env).filter(([k]) => k.startsWith('CP_')), + ); const loggerInfoSpy = vi.spyOn(logger, 'info'); const loggerWarnSpy = vi.spyOn(logger, 'warn'); + /* eslint-disable functional/immutable-data, @typescript-eslint/no-dynamic-delete */ + beforeAll(() => { + Object.entries(process.env) + .filter(([k]) => k.startsWith('CP_')) + .forEach(([k]) => delete process.env[k]); + }); + + beforeEach(() => { + vi.unstubAllEnvs(); + }); + afterEach(() => { loggerWarnSpy.mockReset(); loggerInfoSpy.mockReset(); }); + afterAll(() => { + Object.entries(processEnvCP).forEach(([k, v]) => (process.env[k] = v)); + }); + /* eslint-enable functional/immutable-data, @typescript-eslint/no-dynamic-delete */ + it('should call execSync with return result', async () => { const output = await runAutorunExecutor({}, executorContext('utils')); expect(output.success).toBe(true); @@ -63,7 +82,20 @@ describe('runAutorunExecutor', () => { expect(output.command).toMatch('--persist.filename="REPORT"'); }); - it('should create command from context, options and arguments', async () => { + it('should create command from context and options if no api key is set', 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"', + ); + }); + + it('should create command from context, options and arguments if api key is set', async () => { + vi.stubEnv('CP_API_KEY', 'cp_1234567'); vi.stubEnv('CP_PROJECT', 'CLI'); const output = await runAutorunExecutor( { persist: { filename: 'REPORT', format: ['md', 'json'] } }, @@ -73,6 +105,7 @@ describe('runAutorunExecutor', () => { expect(output.command).toMatch( '--persist.format="md" --persist.format="json"', ); + expect(output.command).toMatch('--upload.apiKey="cp_1234567"'); expect(output.command).toMatch('--upload.project="CLI"'); }); 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(['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(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 { 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..eb68740ef 100644 --- a/packages/nx-plugin/src/plugin/target/targets.ts +++ b/packages/nx-plugin/src/plugin/target/targets.ts @@ -1,13 +1,20 @@ 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 { + 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 { + const { + workspaceRoot = process.cwd(), + nxJsonConfiguration = {}, + configFiles = [], + } = options ?? {}; + return { + workspaceRoot, + nxJsonConfiguration, + configFiles, + }; +} + +export function createNodesV2Context( 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..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: {}, + }), + ); }); });