Skip to content

Commit 189bae3

Browse files
authored
refactor(cli): docs, doctor, context commands do not use configuration directly anymore (aws#32578)
This is a refactor that follows aws#32558. The effort is to ensure that the `configuration` object is not directly used by commands and instead, the relevant information is passed directly to the function. - doctor: no options needed - docs: 1 option needed - context: in addition to a few options, the context command also saves *some* values to `cdk.context.json`. this requires some modifications to the `Context` object to support this. We move away from using the archaic API in `command-api.ts`. This seems to be an old effort to standardize CLI commands that was only used for doctor/docs/context. We have since evolved to use command-specific property bags. Since this is a refactor PR. I aim to change no functionality of the CLI. ### Checklist - [x] My code adheres to the [CONTRIBUTING GUIDE](https://github.com/aws/aws-cdk/blob/main/CONTRIBUTING.md) and [DESIGN GUIDELINES](https://github.com/aws/aws-cdk/blob/main/docs/DESIGN_GUIDELINES.md) ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license*
1 parent 3377c3b commit 189bae3

17 files changed

+264
-158
lines changed

codecov.yml

+1
Original file line numberDiff line numberDiff line change
@@ -34,3 +34,4 @@ component_management:
3434
# https://docs.codecov.com/docs/ignoring-paths
3535
ignore:
3636
- packages/aws-cdk/lib/parse-command-line-arguments.ts # this file is generated and some lines cannot be tested
37+
- packages/aws-cdk/lib/cli.ts # we integ test this file

packages/aws-cdk/lib/cli-arguments.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -1224,7 +1224,7 @@ export interface ContextOptions {
12241224
/**
12251225
* Clear all context
12261226
*
1227-
* @default - undefined
1227+
* @default - false
12281228
*/
12291229
readonly clear?: boolean;
12301230
}

packages/aws-cdk/lib/cli.ts

+12-11
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,9 @@ import { Deployments } from '../lib/api/deployments';
1717
import { PluginHost } from '../lib/api/plugin';
1818
import { ToolkitInfo } from '../lib/api/toolkit-info';
1919
import { CdkToolkit, AssetBuildTime } from '../lib/cdk-toolkit';
20-
import { realHandler as context } from '../lib/commands/context';
21-
import { realHandler as docs } from '../lib/commands/docs';
22-
import { realHandler as doctor } from '../lib/commands/doctor';
20+
import { contextHandler as context } from '../lib/commands/context';
21+
import { docs } from '../lib/commands/docs';
22+
import { doctor } from '../lib/commands/doctor';
2323
import { getMigrateScanType } from '../lib/commands/migrate';
2424
import { cliInit, printAvailableTemplates } from '../lib/init';
2525
import { data, debug, error, print, setCI, setLogLevel, LogLevel } from '../lib/logging';
@@ -37,7 +37,6 @@ if (!process.stdout.isTTY) {
3737
}
3838

3939
export async function exec(args: string[], synthesizer?: Synthesizer): Promise<number | void> {
40-
4140
const argv = await parseCommandLineArguments(args);
4241

4342
// if one -v, log at a DEBUG level
@@ -154,9 +153,6 @@ export async function exec(args: string[], synthesizer?: Synthesizer): Promise<n
154153
throw new Error(`First argument should be a string. Got: ${cmd} (${typeof(cmd)})`);
155154
}
156155

157-
// Bundle up global objects so the commands have access to them
158-
const commandOptions = { args: argv, configuration, aws: sdkProvider };
159-
160156
try {
161157
return await main(cmd, argv);
162158
} finally {
@@ -174,7 +170,6 @@ export async function exec(args: string[], synthesizer?: Synthesizer): Promise<n
174170
await notices.refresh();
175171
notices.display();
176172
}
177-
178173
}
179174

180175
async function main(command: string, args: any): Promise<number | void> {
@@ -207,13 +202,19 @@ export async function exec(args: string[], synthesizer?: Synthesizer): Promise<n
207202

208203
switch (command) {
209204
case 'context':
210-
return context(commandOptions);
205+
return context({
206+
context: configuration.context,
207+
clear: argv.clear,
208+
json: argv.json,
209+
force: argv.force,
210+
reset: argv.reset,
211+
});
211212

212213
case 'docs':
213-
return docs(commandOptions);
214+
return docs({ browser: configuration.settings.get(['browser']) });
214215

215216
case 'doctor':
216-
return doctor(commandOptions);
217+
return doctor();
217218

218219
case 'ls':
219220
case 'list':

packages/aws-cdk/lib/command-api.ts

-26
This file was deleted.

packages/aws-cdk/lib/commands/context.ts

+52-12
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,64 @@
11
import * as chalk from 'chalk';
22
import { minimatch } from 'minimatch';
33
import * as version from '../../lib/version';
4-
import { CommandOptions } from '../command-api';
54
import { print, error, warning } from '../logging';
65
import { Context, PROJECT_CONFIG, PROJECT_CONTEXT, USER_DEFAULTS } from '../settings';
76
import { renderTable } from '../util';
87

9-
export async function realHandler(options: CommandOptions): Promise<number> {
10-
const { configuration, args } = options;
11-
if (args.clear) {
12-
configuration.context.clear();
13-
await configuration.saveContext();
8+
/**
9+
* Options for the context command
10+
*/
11+
export interface ContextOptions {
12+
/**
13+
* The context object sourced from all context locations
14+
*/
15+
context: Context;
16+
17+
/**
18+
* The context key (or its index) to reset
19+
*
20+
* @default undefined
21+
*/
22+
reset?: string;
23+
24+
/**
25+
* Ignore missing key error
26+
*
27+
* @default false
28+
*/
29+
force?: boolean;
30+
31+
/**
32+
* Clear all context
33+
*
34+
* @default false
35+
*/
36+
clear?: boolean;
37+
38+
/**
39+
* Use JSON output instead of YAML when templates are printed to STDOUT
40+
*
41+
* @default false
42+
*/
43+
json?: boolean;
44+
}
45+
46+
export async function contextHandler(options: ContextOptions): Promise<number> {
47+
if (options.clear) {
48+
options.context.clear();
49+
await options.context.save(PROJECT_CONTEXT);
1450
print('All context values cleared.');
15-
} else if (args.reset) {
16-
invalidateContext(configuration.context, args.reset as string, args.force as boolean);
17-
await configuration.saveContext();
51+
} else if (options.reset) {
52+
invalidateContext(options.context, options.reset, options.force ?? false);
53+
await options.context.save(PROJECT_CONTEXT);
1854
} else {
1955
// List -- support '--json' flag
20-
if (args.json) {
21-
const contextValues = configuration.context.all;
56+
if (options.json) {
57+
/* istanbul ignore next */
58+
const contextValues = options.context.all;
2259
process.stdout.write(JSON.stringify(contextValues, undefined, 2));
2360
} else {
24-
listContext(configuration.context);
61+
listContext(options.context);
2562
}
2663
}
2764
await version.displayVersionMessage();
@@ -105,13 +142,15 @@ function invalidateContext(context: Context, key: string, force: boolean) {
105142
throw new Error(`No context value matching key: ${key}`);
106143
}
107144
}
145+
108146
function printUnset(unset: string[]) {
109147
if (unset.length === 0) return;
110148
print('The following matched context values reset. They will be refreshed on next synthesis');
111149
unset.forEach((match) => {
112150
print(' %s', match);
113151
});
114152
}
153+
115154
function printReadonly(readonly: string[]) {
116155
if (readonly.length === 0) return;
117156
warning('The following matched context values could not be reset through the CLI');
@@ -121,6 +160,7 @@ function printReadonly(readonly: string[]) {
121160
print('');
122161
print('This usually means they are configured in %s or %s', chalk.blue(PROJECT_CONFIG), chalk.blue(USER_DEFAULTS));
123162
}
163+
124164
function keysByExpression(context: Context, expression: string) {
125165
return context.keys.filter(minimatch.filter(expression));
126166
}

packages/aws-cdk/lib/commands/docs.ts

+12-3
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,25 @@
11
import * as childProcess from 'child_process';
22
import * as chalk from 'chalk';
33
import { debug, print, warning } from '../../lib/logging';
4-
import { CommandOptions } from '../command-api';
54

65
export const command = 'docs';
76
export const describe = 'Opens the reference documentation in a browser';
87
export const aliases = ['doc'];
98

10-
export async function realHandler(options: CommandOptions): Promise<number> {
9+
/**
10+
* Options for the docs command
11+
*/
12+
export interface DocsOptions {
13+
/**
14+
* The command to use to open the browser
15+
*/
16+
browser: string;
17+
}
18+
19+
export async function docs(options: DocsOptions): Promise<number> {
1120
const url = 'https://docs.aws.amazon.com/cdk/api/v2/';
1221
print(chalk.green(url));
13-
const browserCommand = (options.args.browser as string).replace(/%u/g, url);
22+
const browserCommand = (options.browser).replace(/%u/g, url);
1423
debug(`Opening documentation ${chalk.green(browserCommand)}`);
1524
return new Promise<number>((resolve, _reject) => {
1625
childProcess.exec(browserCommand, (err, stdout, stderr) => {

packages/aws-cdk/lib/commands/doctor.ts

+1-2
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,8 @@ import * as cxapi from '@aws-cdk/cx-api';
33
import * as chalk from 'chalk';
44
import { print } from '../../lib/logging';
55
import * as version from '../../lib/version';
6-
import { CommandOptions } from '../command-api';
76

8-
export async function realHandler(_options: CommandOptions): Promise<number> {
7+
export async function doctor(): Promise<number> {
98
let exitStatus: number = 0;
109
for (const verification of verifications) {
1110
if (!await verification()) {

packages/aws-cdk/lib/config.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -373,9 +373,9 @@ export async function makeConfig(): Promise<CliConfig> {
373373
context: {
374374
description: 'Manage cached context values',
375375
options: {
376-
reset: { alias: 'e', desc: 'The context key (or its index) to reset', type: 'string', requiresArg: true },
376+
reset: { alias: 'e', desc: 'The context key (or its index) to reset', type: 'string', requiresArg: true, default: undefined },
377377
force: { alias: 'f', desc: 'Ignore missing key error', type: 'boolean', default: false },
378-
clear: { desc: 'Clear all context', type: 'boolean' },
378+
clear: { desc: 'Clear all context', type: 'boolean', default: false },
379379
},
380380
},
381381
docs: {

packages/aws-cdk/lib/parse-command-line-arguments.ts

+2
Original file line numberDiff line numberDiff line change
@@ -742,6 +742,7 @@ export function parseCommandLineArguments(args: Array<string>): any {
742742
desc: 'The context key (or its index) to reset',
743743
type: 'string',
744744
requiresArg: true,
745+
default: undefined,
745746
})
746747
.option('force', {
747748
alias: 'f',
@@ -752,6 +753,7 @@ export function parseCommandLineArguments(args: Array<string>): any {
752753
.option('clear', {
753754
desc: 'Clear all context',
754755
type: 'boolean',
756+
default: false,
755757
})
756758
)
757759
.command(['docs', 'doc'], 'Opens the reference documentation in a browser', (yargs: Argv) =>

packages/aws-cdk/lib/settings.ts

+42-7
Original file line numberDiff line numberDiff line change
@@ -99,7 +99,7 @@ export class Configuration {
9999
return this._projectConfig;
100100
}
101101

102-
private get projectContext() {
102+
public get projectContext() {
103103
if (!this._projectContext) {
104104
throw new Error('#load has not been called yet!');
105105
}
@@ -121,12 +121,12 @@ export class Configuration {
121121
}
122122

123123
const contextSources = [
124-
this.commandLineContext,
125-
this.projectConfig.subSettings([CONTEXT_KEY]).makeReadOnly(),
126-
this.projectContext,
124+
{ bag: this.commandLineContext },
125+
{ fileName: PROJECT_CONFIG, bag: this.projectConfig.subSettings([CONTEXT_KEY]).makeReadOnly() },
126+
{ fileName: PROJECT_CONTEXT, bag: this.projectContext },
127127
];
128128
if (readUserContext) {
129-
contextSources.push(userConfig.subSettings([CONTEXT_KEY]).makeReadOnly());
129+
contextSources.push({ fileName: USER_DEFAULTS, bag: userConfig.subSettings([CONTEXT_KEY]).makeReadOnly() });
130130
}
131131

132132
this.context = new Context(...contextSources);
@@ -166,6 +166,19 @@ async function loadAndLog(fileName: string): Promise<Settings> {
166166
return ret;
167167
}
168168

169+
interface ContextBag {
170+
/**
171+
* The file name of the context. Will be used to potentially
172+
* save new context back to the original file.
173+
*/
174+
fileName?: string;
175+
176+
/**
177+
* The context values.
178+
*/
179+
bag: Settings;
180+
}
181+
169182
/**
170183
* Class that supports overlaying property bags
171184
*
@@ -176,9 +189,11 @@ async function loadAndLog(fileName: string): Promise<Settings> {
176189
*/
177190
export class Context {
178191
private readonly bags: Settings[];
192+
private readonly fileNames: (string|undefined)[];
179193

180-
constructor(...bags: Settings[]) {
181-
this.bags = bags.length > 0 ? bags : [new Settings()];
194+
constructor(...bags: ContextBag[]) {
195+
this.bags = bags.length > 0 ? bags.map(b => b.bag) : [new Settings()];
196+
this.fileNames = bags.length > 0 ? bags.map(b => b.fileName) : ['default'];
182197
}
183198

184199
public get keys(): string[] {
@@ -227,6 +242,26 @@ export class Context {
227242
this.unset(key);
228243
}
229244
}
245+
246+
/**
247+
* Save a specific context file
248+
*/
249+
public async save(fileName: string): Promise<this> {
250+
const index = this.fileNames.indexOf(fileName);
251+
252+
// File not found, don't do anything in this scenario
253+
if (index === -1) {
254+
return this;
255+
}
256+
257+
const bag = this.bags[index];
258+
if (bag.readOnly) {
259+
throw new Error(`Context file ${fileName} is read only!`);
260+
}
261+
262+
await bag.save(fileName);
263+
return this;
264+
}
230265
}
231266

232267
/**
+3-9
Original file line numberDiff line numberDiff line change
@@ -1,31 +1,25 @@
11
import * as child_process from 'child_process';
22
import { mocked } from 'jest-mock';
3-
import { CommandHandler } from '../lib/command-api';
4-
import { realHandler } from '../lib/commands/docs';
5-
const argv = {
6-
browser: 'echo %u',
7-
commandHandler: undefined as (CommandHandler | undefined),
8-
};
3+
import { docs } from '../lib/commands/docs';
94

105
// eslint-disable-next-line no-console
116
console.log = jest.fn();
127
jest.mock('child_process');
138

149
describe('`cdk docs`', () => {
15-
1610
test('exits with 0 when everything is OK', async () => {
1711
const mockChildProcessExec: any = (_: string, cb: (err?: Error, stdout?: string, stderr?: string) => void) => cb();
1812
mocked(child_process.exec).mockImplementation(mockChildProcessExec);
1913

20-
const result = await realHandler({ args: argv } as any);
14+
const result = await docs({ browser: 'echo %u' });
2115
expect(result).toBe(0);
2216
});
2317

2418
test('exits with 0 when opening the browser fails', async () => {
2519
const mockChildProcessExec: any = (_: string, cb: (err: Error, stdout?: string, stderr?: string) => void) => cb(new Error('TEST'));
2620
mocked(child_process.exec).mockImplementation(mockChildProcessExec);
2721

28-
const result = await realHandler({ args: argv } as any);
22+
const result = await docs({ browser: 'echo %u' });
2923
expect(result).toBe(0);
3024
});
3125
});

0 commit comments

Comments
 (0)