Skip to content

Commit 3dcc4b1

Browse files
authored
fix(nx-plugin): deep merge executor options (#927)
1 parent e68ce12 commit 3dcc4b1

File tree

7 files changed

+114
-6
lines changed

7 files changed

+114
-6
lines changed

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

Lines changed: 42 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,10 @@ import { type Tree, updateProjectConfiguration } from '@nx/devkit';
22
import path from 'node:path';
33
import { readProjectConfiguration } from 'nx/src/generators/utils/project-configuration';
44
import { afterEach, expect } from 'vitest';
5-
import { generateCodePushupConfig } from '@code-pushup/nx-plugin';
5+
import {
6+
type AutorunCommandExecutorOptions,
7+
generateCodePushupConfig,
8+
} from '@code-pushup/nx-plugin';
69
import {
710
generateWorkspaceAndProject,
811
materializeTree,
@@ -20,6 +23,7 @@ import { INLINE_PLUGIN } from './inline-plugin.js';
2023
async function addTargetToWorkspace(
2124
tree: Tree,
2225
options: { cwd: string; project: string },
26+
executorOptions?: AutorunCommandExecutorOptions,
2327
) {
2428
const { cwd, project } = options;
2529
const projectCfg = readProjectConfiguration(tree, project);
@@ -29,6 +33,7 @@ async function addTargetToWorkspace(
2933
...projectCfg.targets,
3034
'code-pushup': {
3135
executor: '@code-pushup/nx-plugin:cli',
36+
...(executorOptions && { options: executorOptions }),
3237
},
3338
},
3439
});
@@ -95,6 +100,42 @@ describe('executor command', () => {
95100
).rejects.toThrow('');
96101
});
97102

103+
it('should execute collect executor and merge target and command-line options', async () => {
104+
const cwd = path.join(testFileDir, 'execute-collect-with-merged-options');
105+
await addTargetToWorkspace(
106+
tree,
107+
{ cwd, project },
108+
{
109+
persist: {
110+
outputDir: '.reports',
111+
filename: 'report',
112+
},
113+
},
114+
);
115+
116+
const { stdout, code } = await executeProcess({
117+
command: 'npx',
118+
args: [
119+
'nx',
120+
'run',
121+
`${project}:code-pushup`,
122+
'collect',
123+
'--persist.filename=terminal-report',
124+
],
125+
cwd,
126+
});
127+
128+
expect(code).toBe(0);
129+
const cleanStdout = removeColorCodes(stdout);
130+
expect(cleanStdout).toContain(
131+
'nx run my-lib:code-pushup collect --persist.filename=terminal-report',
132+
);
133+
134+
await expect(
135+
readJsonFile(path.join(cwd, '.reports', 'terminal-report.json')),
136+
).resolves.not.toThrow();
137+
});
138+
98139
it('should execute collect executor and add report to sub folder named by project', async () => {
99140
const cwd = path.join(testFileDir, 'execute-collect-command');
100141
await addTargetToWorkspace(tree, { cwd, project });

packages/nx-plugin/README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ Examples:
4646
#### CLI
4747

4848
Install JS packages configure a target in your project json.
49-
See [CLI executor docs](./src/executor/cli/README.md) for details
49+
See [CLI executor docs](./src/executors/cli/README.md) for details
5050

5151
Examples:
5252

packages/nx-plugin/src/executors/cli/README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -74,4 +74,4 @@ Show what will be executed without actually executing it:
7474
| **dryRun** | `boolean` | To debug the executor, dry run the command without real execution. |
7575
| **bin** | `string` | Path to Code PushUp CLI |
7676

77-
For all other options see the [CLI autorun documentation](../../cli/packages/cli/README.md#autorun-command).
77+
For all other options see the [CLI autorun documentation](../../../../cli/README.md#autorun-command).

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

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { execSync } from 'node:child_process';
33
import { createCliCommand } from '../internal/cli.js';
44
import { normalizeContext } from '../internal/context.js';
55
import type { AutorunCommandExecutorOptions } from './schema.js';
6-
import { parseAutorunExecutorOptions } from './utils.js';
6+
import { mergeExecutorOptions, parseAutorunExecutorOptions } from './utils.js';
77

88
export type ExecutorOutput = {
99
success: boolean;
@@ -16,11 +16,15 @@ export default function runAutorunExecutor(
1616
context: ExecutorContext,
1717
): Promise<ExecutorOutput> {
1818
const normalizedContext = normalizeContext(context);
19-
const cliArgumentObject = parseAutorunExecutorOptions(
19+
const mergedOptions = mergeExecutorOptions(
20+
context.target?.options,
2021
terminalAndExecutorOptions,
22+
);
23+
const cliArgumentObject = parseAutorunExecutorOptions(
24+
mergedOptions,
2125
normalizedContext,
2226
);
23-
const { dryRun, verbose, command } = terminalAndExecutorOptions;
27+
const { dryRun, verbose, command } = mergedOptions;
2428

2529
const commandString = createCliCommand({ command, args: cliArgumentObject });
2630
const commandStringOptions = context.cwd ? { cwd: context.cwd } : {};

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

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,3 +39,41 @@ export function parseAutorunExecutorOptions(
3939
: undefined,
4040
};
4141
}
42+
43+
/**
44+
* Deeply merges executor options.
45+
*
46+
* @param targetOptions - The original options from the target configuration.
47+
* @param cliOptions - The options from Nx, combining target options and CLI arguments.
48+
* @returns A new object with deeply merged properties.
49+
*
50+
* Nx performs a shallow merge by default, where command-line arguments can override entire objects
51+
* (e.g., `--persist.filename` replaces the entire `persist` object).
52+
* This function ensures that nested properties are deeply merged,
53+
* preserving the original target options where CLI arguments are not provided.
54+
*/
55+
export function mergeExecutorOptions(
56+
targetOptions: Partial<AutorunCommandExecutorOptions>,
57+
cliOptions: Partial<AutorunCommandExecutorOptions>,
58+
): AutorunCommandExecutorOptions {
59+
return {
60+
...targetOptions,
61+
...cliOptions,
62+
...(targetOptions?.persist || cliOptions?.persist
63+
? {
64+
persist: {
65+
...targetOptions?.persist,
66+
...cliOptions?.persist,
67+
},
68+
}
69+
: {}),
70+
...(targetOptions?.upload || cliOptions?.upload
71+
? {
72+
upload: {
73+
...targetOptions?.upload,
74+
...cliOptions?.upload,
75+
},
76+
}
77+
: {}),
78+
};
79+
}

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

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { type MockInstance, expect, vi } from 'vitest';
22
import { osAgnosticPath } from '@code-pushup/test-utils';
33
import type { Command } from '../internal/types.js';
44
import {
5+
mergeExecutorOptions,
56
parseAutorunExecutorOnlyOptions,
67
parseAutorunExecutorOptions,
78
} from './utils.js';
@@ -154,3 +155,26 @@ describe('parseAutorunExecutorOptions', () => {
154155
},
155156
);
156157
});
158+
159+
describe('mergeExecutorOptions', () => {
160+
it('should deeply merge target and CLI options', () => {
161+
const targetOptions = {
162+
persist: {
163+
outputDir: '.reports',
164+
filename: 'report',
165+
},
166+
};
167+
const cliOptions = {
168+
persist: {
169+
filename: 'report-file',
170+
},
171+
};
172+
const expected = {
173+
persist: {
174+
outputDir: '.reports',
175+
filename: 'report-file',
176+
},
177+
};
178+
expect(mergeExecutorOptions(targetOptions, cliOptions)).toEqual(expected);
179+
});
180+
});

packages/nx-plugin/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,3 +15,4 @@ export {
1515
type ProcessConfig,
1616
} from './internal/execute-process.js';
1717
export { objectToCliArgs } from './executors/internal/cli.js';
18+
export type { AutorunCommandExecutorOptions } from './executors/cli/schema.js';

0 commit comments

Comments
 (0)