diff --git a/packages/aws-cdk/lib/cli/cli.ts b/packages/aws-cdk/lib/cli/cli.ts index 84bb10275..fb73073fa 100644 --- a/packages/aws-cdk/lib/cli/cli.ts +++ b/packages/aws-cdk/lib/cli/cli.ts @@ -304,6 +304,7 @@ export async function exec(args: string[], synthesizer?: Synthesizer): Promise = []; private loaded = false; private ioHelper: IoHelper; @@ -144,6 +145,16 @@ export class Configuration { return this._projectContext; } + /** + * The names of the context files that currently contain at least one + * context value, in lookup precedence order. + */ + public get contextSourceFiles(): string[] { + return this.contextFileBags + .filter(({ bag }) => Object.keys(bag.all).length > 0) + .map(({ fileName }) => fileName); + } + /** * Load all config */ @@ -175,6 +186,7 @@ export class Configuration { } this.context = new Context(...contextSources); + this.contextFileBags = contextSources.filter((s): s is { fileName: string; bag: Settings } => s.fileName != null); // Build settings from what's left const mergedSettings = this.defaultConfig diff --git a/packages/aws-cdk/lib/commands/context.ts b/packages/aws-cdk/lib/commands/context.ts index 9b3922466..9e3f2d734 100644 --- a/packages/aws-cdk/lib/commands/context.ts +++ b/packages/aws-cdk/lib/commands/context.ts @@ -16,6 +16,14 @@ export interface ContextOptions { */ readonly context: Context; + /** + * The names of the context files that currently contain context values, + * used to describe where the listed context comes from + * + * @default - no file names are shown in the listing header + */ + readonly sourceFiles?: string[]; + /** * The context key (or its index) to reset * @@ -68,14 +76,14 @@ export async function contextHandler(options: ContextOptions): Promise { await ioHelper.defaults.result(JSON.stringify(contextValues, undefined, 2)); /* c8 ignore stop */ } else { - await listContext(ioHelper, options.context); + await listContext(ioHelper, options.context, options.sourceFiles ?? []); } } return 0; } -async function listContext(ioHelper: IoHelper, context: Context) { +async function listContext(ioHelper: IoHelper, context: Context, sourceFiles: string[]) { const keys = contextKeys(context); if (keys.length === 0) { @@ -94,7 +102,11 @@ async function listContext(ioHelper: IoHelper, context: Context) { const jsonWithoutNewlines = JSON.stringify(context.all[key], undefined, 2).replace(/\s+/g, ' '); data_out.push([i, key, jsonWithoutNewlines]); } - await ioHelper.defaults.info('Context found in %s:', chalk.blue(PROJECT_CONFIG)); + if (sourceFiles.length > 0) { + await ioHelper.defaults.info('Context found in %s:', sourceFiles.map((f) => chalk.blue(f)).join(', ')); + } else { + await ioHelper.defaults.info('Context found:'); + } await ioHelper.defaults.info(''); await ioHelper.defaults.info(renderTable(data_out, process.stdout.columns)); diff --git a/packages/aws-cdk/test/cli/user-config.test.ts b/packages/aws-cdk/test/cli/user-config.test.ts index 9f134054d..7f1ca7c5c 100644 --- a/packages/aws-cdk/test/cli/user-config.test.ts +++ b/packages/aws-cdk/test/cli/user-config.test.ts @@ -109,6 +109,63 @@ test('load context from all 3 files if available', async () => { expect(config.context.get('test')).toBe('bar'); }); +test('contextSourceFiles lists all files that contain context values', async () => { + // GIVEN + const GIVEN_CONFIG: Map = new Map([ + [PROJECT_CONFIG, { + context: { + project: 'foobar', + }, + }], + [PROJECT_CONTEXT, { + foo: 'bar', + }], + [USER_CONFIG, { + context: { + test: 'bar', + }, + }], + ]); + + // WHEN + mockedFs.pathExists.mockImplementation(path => { + return GIVEN_CONFIG.has(path); + }); + mockedFs.readJSON.mockImplementation(((path: string) => { + return GIVEN_CONFIG.get(path); + }) as any); + + const config = await Configuration.fromArgsAndFiles(ioHelper); + + // THEN + expect(config.contextSourceFiles).toEqual([PROJECT_CONFIG, PROJECT_CONTEXT, '~/.cdk.json']); +}); + +test('contextSourceFiles does not list files without context values', async () => { + // GIVEN cdk.json has settings but no context, and only cdk.context.json has context + const GIVEN_CONFIG: Map = new Map([ + [PROJECT_CONFIG, { + project: 'foobar', + }], + [PROJECT_CONTEXT, { + foo: 'bar', + }], + ]); + + // WHEN + mockedFs.pathExists.mockImplementation(path => { + return GIVEN_CONFIG.has(path); + }); + mockedFs.readJSON.mockImplementation(((path: string) => { + return GIVEN_CONFIG.get(path); + }) as any); + + const config = await Configuration.fromArgsAndFiles(ioHelper); + + // THEN + expect(config.contextSourceFiles).toEqual([PROJECT_CONTEXT]); +}); + test('throws an error if the `build` key is specified in the user config', async () => { // GIVEN const GIVEN_CONFIG: Map = new Map([ diff --git a/packages/aws-cdk/test/commands/context-command.test.ts b/packages/aws-cdk/test/commands/context-command.test.ts index 6cab50cb7..85d46ebbf 100644 --- a/packages/aws-cdk/test/commands/context-command.test.ts +++ b/packages/aws-cdk/test/commands/context-command.test.ts @@ -24,6 +24,48 @@ describe('context --list', () => { context: configuration.context, }); }); + + test('header lists the files the context comes from', async () => { + // GIVEN + ioHost.notifySpy.mockClear(); + const configuration = await Configuration.fromArgs(ioHelper); + configuration.context.set('foo', 'bar'); + + // WHEN + await contextHandler({ + ioHelper, + context: configuration.context, + sourceFiles: ['cdk.json', 'cdk.context.json'], + }); + + // THEN + expect(ioHost.notifySpy).toHaveBeenCalledWith(expect.objectContaining({ + message: expect.stringContaining('Context found in'), + })); + const header = ioHost.notifySpy.mock.calls + .map(([msg]) => msg.message as string) + .find((m) => m.includes('Context found in'))!; + expect(header).toContain('cdk.json'); + expect(header).toContain('cdk.context.json'); + }); + + test('header does not name any file when the source files are unknown', async () => { + // GIVEN + ioHost.notifySpy.mockClear(); + const configuration = await Configuration.fromArgs(ioHelper); + configuration.context.set('foo', 'bar'); + + // WHEN + await contextHandler({ + ioHelper, + context: configuration.context, + }); + + // THEN + expect(ioHost.notifySpy).toHaveBeenCalledWith(expect.objectContaining({ + message: 'Context found:', + })); + }); }); describe('context --reset', () => {