Skip to content

Commit 45e2757

Browse files
authored
fix(nx-plugin): adjust upload config handling (#937)
1 parent f1db1d6 commit 45e2757

File tree

18 files changed

+184
-43
lines changed

18 files changed

+184
-43
lines changed

e2e/nx-plugin-e2e/tests/executor-cli.e2e.test.ts

+43-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { type Tree, updateProjectConfiguration } from '@nx/devkit';
22
import path from 'node:path';
33
import { readProjectConfiguration } from 'nx/src/generators/utils/project-configuration';
4-
import { afterEach, expect } from 'vitest';
4+
import { afterAll, afterEach, beforeEach, expect, vi } from 'vitest';
55
import {
66
type AutorunCommandExecutorOptions,
77
generateCodePushupConfig,
@@ -58,15 +58,31 @@ describe('executor command', () => {
5858
TEST_OUTPUT_DIR,
5959
'executor-cli',
6060
);
61+
const processEnvCP = Object.fromEntries(
62+
Object.entries(process.env).filter(([k]) => k.startsWith('CP_')),
63+
);
64+
65+
/* eslint-disable functional/immutable-data, @typescript-eslint/no-dynamic-delete */
66+
beforeAll(() => {
67+
Object.entries(process.env)
68+
.filter(([k]) => k.startsWith('CP_'))
69+
.forEach(([k]) => delete process.env[k]);
70+
});
6171

6272
beforeEach(async () => {
73+
vi.unstubAllEnvs();
6374
tree = await generateWorkspaceAndProject(project);
6475
});
6576

6677
afterEach(async () => {
6778
await teardownTestFolder(testFileDir);
6879
});
6980

81+
afterAll(() => {
82+
Object.entries(processEnvCP).forEach(([k, v]) => (process.env[k] = v));
83+
});
84+
/* eslint-enable functional/immutable-data, @typescript-eslint/no-dynamic-delete */
85+
7086
it('should execute no specific command by default', async () => {
7187
const cwd = path.join(testFileDir, 'execute-default-command');
7288
await addTargetToWorkspace(tree, { cwd, project });
@@ -100,6 +116,32 @@ describe('executor command', () => {
100116
).rejects.toThrow('');
101117
});
102118

119+
it('should execute print-config executor with api key', async () => {
120+
const cwd = path.join(testFileDir, 'execute-print-config-command');
121+
await addTargetToWorkspace(tree, { cwd, project });
122+
123+
const { stdout, code } = await executeProcess({
124+
command: 'npx',
125+
args: [
126+
'nx',
127+
'run',
128+
`${project}:code-pushup`,
129+
'print-config',
130+
'--upload.apiKey=a123a',
131+
],
132+
cwd,
133+
});
134+
135+
expect(code).toBe(0);
136+
const cleanStdout = removeColorCodes(stdout);
137+
expect(cleanStdout).toContain('nx run my-lib:code-pushup print-config');
138+
expect(cleanStdout).toContain('a123a');
139+
140+
await expect(() =>
141+
readJsonFile(path.join(cwd, '.code-pushup', project, 'report.json')),
142+
).rejects.toThrow('');
143+
});
144+
103145
it('should execute collect executor and merge target and command-line options', async () => {
104146
const cwd = path.join(testFileDir, 'execute-collect-with-merged-options');
105147
await addTargetToWorkspace(

packages/models/tsconfig.lib.json

+1
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
"exclude": [
1616
"vite.config.unit.ts",
1717
"vite.config.integration.ts",
18+
"code-pushup.config.ts",
1819
"zod2md.config.ts",
1920
"src/**/*.test.ts",
2021
"src/**/*.mock.ts",

packages/models/tsconfig.test.json

+1
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
"outDir": "../../dist/out-tsc",
55
"types": ["vitest/globals", "vitest/importMeta", "vite/client", "node"]
66
},
7+
"exclude": ["**/code-pushup.config.ts"],
78
"include": [
89
"vite.config.unit.ts",
910
"vite.config.integration.ts",

packages/nx-plugin/src/executors/cli/executor.unit.test.ts

+35-2
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { logger } from '@nx/devkit';
22
import { execSync } from 'node:child_process';
3-
import { afterEach, expect, vi } from 'vitest';
3+
import { afterAll, afterEach, beforeEach, expect, vi } from 'vitest';
44
import { executorContext } from '@code-pushup/test-nx-utils';
55
import { MEMFS_VOLUME } from '@code-pushup/test-utils';
66
import runAutorunExecutor from './executor.js';
@@ -19,14 +19,33 @@ vi.mock('node:child_process', async () => {
1919
});
2020

2121
describe('runAutorunExecutor', () => {
22+
const processEnvCP = Object.fromEntries(
23+
Object.entries(process.env).filter(([k]) => k.startsWith('CP_')),
24+
);
2225
const loggerInfoSpy = vi.spyOn(logger, 'info');
2326
const loggerWarnSpy = vi.spyOn(logger, 'warn');
2427

28+
/* eslint-disable functional/immutable-data, @typescript-eslint/no-dynamic-delete */
29+
beforeAll(() => {
30+
Object.entries(process.env)
31+
.filter(([k]) => k.startsWith('CP_'))
32+
.forEach(([k]) => delete process.env[k]);
33+
});
34+
35+
beforeEach(() => {
36+
vi.unstubAllEnvs();
37+
});
38+
2539
afterEach(() => {
2640
loggerWarnSpy.mockReset();
2741
loggerInfoSpy.mockReset();
2842
});
2943

44+
afterAll(() => {
45+
Object.entries(processEnvCP).forEach(([k, v]) => (process.env[k] = v));
46+
});
47+
/* eslint-enable functional/immutable-data, @typescript-eslint/no-dynamic-delete */
48+
3049
it('should call execSync with return result', async () => {
3150
const output = await runAutorunExecutor({}, executorContext('utils'));
3251
expect(output.success).toBe(true);
@@ -63,7 +82,20 @@ describe('runAutorunExecutor', () => {
6382
expect(output.command).toMatch('--persist.filename="REPORT"');
6483
});
6584

66-
it('should create command from context, options and arguments', async () => {
85+
it('should create command from context and options if no api key is set', async () => {
86+
vi.stubEnv('CP_PROJECT', 'CLI');
87+
const output = await runAutorunExecutor(
88+
{ persist: { filename: 'REPORT', format: ['md', 'json'] } },
89+
executorContext('core'),
90+
);
91+
expect(output.command).toMatch('--persist.filename="REPORT"');
92+
expect(output.command).toMatch(
93+
'--persist.format="md" --persist.format="json"',
94+
);
95+
});
96+
97+
it('should create command from context, options and arguments if api key is set', async () => {
98+
vi.stubEnv('CP_API_KEY', 'cp_1234567');
6799
vi.stubEnv('CP_PROJECT', 'CLI');
68100
const output = await runAutorunExecutor(
69101
{ persist: { filename: 'REPORT', format: ['md', 'json'] } },
@@ -73,6 +105,7 @@ describe('runAutorunExecutor', () => {
73105
expect(output.command).toMatch(
74106
'--persist.format="md" --persist.format="json"',
75107
);
108+
expect(output.command).toMatch('--upload.apiKey="cp_1234567"');
76109
expect(output.command).toMatch('--upload.project="CLI"');
77110
});
78111

packages/nx-plugin/src/executors/cli/utils.ts

+6-3
Original file line numberDiff line numberDiff line change
@@ -27,16 +27,19 @@ export function parseAutorunExecutorOptions(
2727
const { projectPrefix, persist, upload, command } = options;
2828
const needsUploadParams =
2929
command === 'upload' || command === 'autorun' || command === undefined;
30+
const uploadCfg = uploadConfig(
31+
{ projectPrefix, ...upload },
32+
normalizedContext,
33+
);
34+
const hasApiToken = uploadCfg?.apiKey != null;
3035
return {
3136
...parseAutorunExecutorOnlyOptions(options),
3237
...globalConfig(options, normalizedContext),
3338
persist: persistConfig({ projectPrefix, ...persist }, normalizedContext),
3439
// @TODO This is a hack to avoid validation errors of upload config for commands that dont need it.
3540
// Fix: use utils and execute the core logic directly
3641
// Blocked by Nx plugins can't compile to es6
37-
upload: needsUploadParams
38-
? uploadConfig({ projectPrefix, ...upload }, normalizedContext)
39-
: undefined,
42+
...(needsUploadParams && hasApiToken ? { upload: uploadCfg } : {}),
4043
};
4144
}
4245

packages/nx-plugin/src/executors/cli/utils.unit.test.ts

+4-5
Original file line numberDiff line numberDiff line change
@@ -73,14 +73,13 @@ describe('parseAutorunExecutorOptions', () => {
7373
},
7474
},
7575
);
76-
expect(osAgnosticPath(executorOptions.config)).toBe(
76+
expect(osAgnosticPath(executorOptions.config ?? '')).toBe(
7777
osAgnosticPath('root/code-pushup.config.ts'),
7878
);
7979
expect(executorOptions).toEqual(
8080
expect.objectContaining({
8181
progress: false,
8282
verbose: false,
83-
upload: { project: projectName },
8483
}),
8584
);
8685

@@ -92,20 +91,20 @@ describe('parseAutorunExecutorOptions', () => {
9291
}),
9392
);
9493

95-
expect(osAgnosticPath(executorOptions.persist?.outputDir)).toBe(
94+
expect(osAgnosticPath(executorOptions.persist?.outputDir ?? '')).toBe(
9695
osAgnosticPath('workspaceRoot/.code-pushup/my-app'),
9796
);
9897
});
9998

10099
it.each<Command | undefined>(['upload', 'autorun', undefined])(
101-
'should include upload config for command %s',
100+
'should include upload config for command %s if API key is provided',
102101
command => {
103102
const projectName = 'my-app';
104103
const executorOptions = parseAutorunExecutorOptions(
105104
{
106105
command,
107106
upload: {
108-
organization: 'code-pushup',
107+
apiKey: '123456789',
109108
},
110109
},
111110
{

packages/nx-plugin/src/index.ts

+8-8
Original file line numberDiff line numberDiff line change
@@ -3,16 +3,16 @@ import { createNodes } from './plugin/index.js';
33
// default export for nx.json#plugins
44
export default createNodes;
55

6-
export * from './internal/versions.js';
7-
export { type InitGeneratorSchema } from './generators/init/schema.js';
8-
export { initGenerator, initSchematic } from './generators/init/generator.js';
9-
export type { ConfigurationGeneratorOptions } from './generators/configuration/schema.js';
10-
export { configurationGenerator } from './generators/configuration/generator.js';
6+
export type { AutorunCommandExecutorOptions } from './executors/cli/schema.js';
7+
export { objectToCliArgs } from './executors/internal/cli.js';
118
export { generateCodePushupConfig } from './generators/configuration/code-pushup-config.js';
12-
export { createNodes } from './plugin/index.js';
9+
export { configurationGenerator } from './generators/configuration/generator.js';
10+
export type { ConfigurationGeneratorOptions } from './generators/configuration/schema.js';
11+
export { initGenerator, initSchematic } from './generators/init/generator.js';
12+
export { type InitGeneratorSchema } from './generators/init/schema.js';
1313
export {
1414
executeProcess,
1515
type ProcessConfig,
1616
} from './internal/execute-process.js';
17-
export { objectToCliArgs } from './executors/internal/cli.js';
18-
export type { AutorunCommandExecutorOptions } from './executors/cli/schema.js';
17+
export * from './internal/versions.js';
18+
export { createNodes } from './plugin/index.js';
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
1-
import { name } from '../../package.json';
2-
31
export const PROJECT_JSON_FILE_NAME = 'project.json';
4-
export const PACKAGE_NAME = name;
2+
export const CODE_PUSHUP_CONFIG_REGEX = /^code-pushup(?:\.[\w-]+)?\.ts$/;
3+
export const PACKAGE_NAME = '@code-pushup/nx-plugin';
54
export const DEFAULT_TARGET_NAME = 'code-pushup';

packages/nx-plugin/src/internal/execute-process.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -125,7 +125,7 @@ export type ProcessObserver = {
125125
* // async process execution
126126
* const result = await executeProcess({
127127
* command: 'node',
128-
* args: ['download-data.js'],
128+
* args: ['download-data'],
129129
* observer: {
130130
* onStdout: updateProgress,
131131
* error: handleError,

packages/nx-plugin/src/internal/versions.ts

+4
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,10 @@ export const cpCliVersion = loadPackageJson(
1616
path.join(projectsFolder, 'models'),
1717
).version;
1818

19+
/**
20+
* Load the package.json file from the given folder path.
21+
* @param folderPath
22+
*/
1923
function loadPackageJson(folderPath: string): PackageJson {
2024
return readJsonFile<PackageJson>(path.join(folderPath, 'package.json'));
2125
}

packages/nx-plugin/src/plugin/plugin.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import { createTargets } from './target/targets.js';
88
import type { CreateNodesOptions } from './types.js';
99
import { normalizedCreateNodesContext } from './utils.js';
1010

11-
// name has to be "createNodes" to get picked up by Nx
11+
// name has to be "createNodes" to get picked up by Nx <v20
1212
export const createNodes: CreateNodes = [
1313
`**/${PROJECT_JSON_FILE_NAME}`,
1414
async (

packages/nx-plugin/src/plugin/plugin.unit.test.ts

+1
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ describe('@code-pushup/nx-plugin/plugin', () => {
1313
context = {
1414
nxJsonConfiguration: {},
1515
workspaceRoot: '',
16+
configFiles: [],
1617
};
1718
});
1819

packages/nx-plugin/src/plugin/target/targets.ts

+11-4
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,20 @@
11
import { readdir } from 'node:fs/promises';
22
import { CP_TARGET_NAME } from '../constants.js';
3-
import type { NormalizedCreateNodesContext } from '../types.js';
3+
import type {
4+
CreateNodesOptions,
5+
ProjectConfigurationWithName,
6+
} from '../types.js';
47
import { createConfigurationTarget } from './configuration-target.js';
58
import { CODE_PUSHUP_CONFIG_REGEX } from './constants.js';
69
import { createExecutorTarget } from './executor-target.js';
710

8-
export async function createTargets(
9-
normalizedContext: NormalizedCreateNodesContext,
10-
) {
11+
export type CreateTargetsOptions = {
12+
projectJson: ProjectConfigurationWithName;
13+
projectRoot: string;
14+
createOptions: CreateNodesOptions;
15+
};
16+
17+
export async function createTargets(normalizedContext: CreateTargetsOptions) {
1118
const {
1219
targetName = CP_TARGET_NAME,
1320
bin,
+11-6
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,11 @@
1-
import type { CreateNodesContext, ProjectConfiguration } from '@nx/devkit';
1+
import type {
2+
CreateNodesContext,
3+
CreateNodesContextV2,
4+
ProjectConfiguration,
5+
} from '@nx/devkit';
26
import type { WithRequired } from '@code-pushup/utils';
37
import type { DynamicTargetOptions } from '../internal/types.js';
8+
import type { CreateTargetsOptions } from './target/targets.js';
49

510
export type ProjectPrefixOptions = {
611
projectPrefix?: string;
@@ -13,8 +18,8 @@ export type ProjectConfigurationWithName = WithRequired<
1318
'name'
1419
>;
1520

16-
export type NormalizedCreateNodesContext = CreateNodesContext & {
17-
projectJson: ProjectConfigurationWithName;
18-
projectRoot: string;
19-
createOptions: CreateNodesOptions;
20-
};
21+
export type NormalizedCreateNodesContext = CreateNodesContext &
22+
CreateTargetsOptions;
23+
24+
export type NormalizedCreateNodesV2Context = CreateNodesContextV2 &
25+
CreateTargetsOptions;

packages/nx-plugin/src/plugin/utils.ts

+28-1
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
1-
import type { CreateNodesContext } from '@nx/devkit';
1+
import type { CreateNodesContext, CreateNodesContextV2 } from '@nx/devkit';
22
import { readFile } from 'node:fs/promises';
33
import * as path from 'node:path';
44
import { CP_TARGET_NAME } from './constants.js';
55
import type {
66
CreateNodesOptions,
77
NormalizedCreateNodesContext,
8+
NormalizedCreateNodesV2Context,
89
ProjectConfigurationWithName,
910
} from './types.js';
1011

@@ -36,3 +37,29 @@ export async function normalizedCreateNodesContext(
3637
);
3738
}
3839
}
40+
41+
export async function normalizedCreateNodesV2Context(
42+
context: CreateNodesContextV2,
43+
projectConfigurationFile: string,
44+
createOptions: CreateNodesOptions = {},
45+
): Promise<NormalizedCreateNodesV2Context> {
46+
const projectRoot = path.dirname(projectConfigurationFile);
47+
48+
try {
49+
const projectJson = JSON.parse(
50+
(await readFile(projectConfigurationFile)).toString(),
51+
) as ProjectConfigurationWithName;
52+
53+
const { targetName = CP_TARGET_NAME } = createOptions;
54+
return {
55+
...context,
56+
projectJson,
57+
projectRoot,
58+
createOptions: { ...createOptions, targetName },
59+
};
60+
} catch {
61+
throw new Error(
62+
`Error parsing project.json file ${projectConfigurationFile}.`,
63+
);
64+
}
65+
}

0 commit comments

Comments
 (0)