Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 9 additions & 5 deletions packages/aws-cdk/lib/cli/telemetry/library-version.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,23 @@
import { createRequire } from 'module';
import * as path from 'path';
import * as fs from 'fs-extra';
import type { IoHelper } from '../../api-private';
import { run } from '../../private/tools';

export async function getLibraryVersion(ioHelper: IoHelper): Promise<string | undefined> {
try {
const { stdout } = await run([process.execPath, '-e', 'process.stdout.write(require.resolve("aws-cdk-lib"))']);
// Resolve from the app's directory rather than our own. The CLI is usually
// installed globally (or run via `npx`), so its own module paths don't contain
// the app's `aws-cdk-lib`.
const requireFromApp = createRequire(path.join(process.cwd(), 'index.js'));
const entryPoint = requireFromApp.resolve('aws-cdk-lib');

// stdout should be a file path but lets double check
if (!fs.existsSync(stdout)) {
// entryPoint should be a file path but lets double check
if (!fs.existsSync(entryPoint)) {
await ioHelper.defaults.trace('Could not get CDK Library Version: require.resolve("aws-cdk-lib") did not return a file path');
return;
}

const pathToPackageJson = path.join(path.dirname(stdout), 'package.json');
const pathToPackageJson = path.join(path.dirname(entryPoint), 'package.json');
const packageJson = fs.readJSONSync(pathToPackageJson);
if (!packageJson.version) {
await ioHelper.defaults.trace('Could not get CDK Library Version: package.json does not have version field');
Expand Down
55 changes: 46 additions & 9 deletions packages/aws-cdk/test/cli/telemetry/library-version.test.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,15 @@
import { createRequire } from 'module';
import * as fs from 'fs-extra';
import type { IoHelper } from '../../../lib/api-private';
import { getLibraryVersion } from '../../../lib/cli/telemetry/library-version';
import { run } from '../../../lib/private/tools';

// Mock the subprocess tool's run()
// Mock module resolution so we don't depend on this repo's own `aws-cdk-lib` install
jest.mock('module', () => ({
createRequire: jest.fn(),
}));

// Mocked only so we can assert that we never reach for a subprocess
jest.mock('@aws-cdk/private-tools/lib/subprocess', () => ({
run: jest.fn(),
}));
Expand All @@ -15,9 +21,17 @@ jest.mock('fs-extra', () => ({
}));

const mockRun = run as jest.MockedFunction<typeof run>;
const mockCreateRequire = createRequire as jest.MockedFunction<typeof createRequire>;
const mockExistsSync = fs.existsSync as jest.MockedFunction<typeof fs.existsSync>;
const mockReadJSONSync = fs.readJSONSync as jest.MockedFunction<typeof fs.readJSONSync>;

/**
* Make `createRequire()` return a `require` whose `resolve()` behaves as given
*/
function mockResolve(impl: () => string) {
mockCreateRequire.mockReturnValue({ resolve: jest.fn(impl) } as any);
}

describe('getLibraryVersion', () => {
let mockIoHelper: IoHelper;
let traceSpy: jest.Mock;
Expand All @@ -38,22 +52,43 @@ describe('getLibraryVersion', () => {
const mockPackageJsonPath = '/path/to/node_modules/aws-cdk-lib/package.json';
const expectedVersion = '2.100.0';

mockRun.mockResolvedValue({ stdout: mockLibPath, stderr: '' });
mockResolve(() => mockLibPath);
mockExistsSync.mockReturnValue(true);
mockReadJSONSync.mockReturnValue({ version: expectedVersion });

const result = await getLibraryVersion(mockIoHelper);

expect(result).toBe(expectedVersion);
expect(mockRun).toHaveBeenCalledWith([process.execPath, '-e', 'process.stdout.write(require.resolve("aws-cdk-lib"))']);
expect(mockExistsSync).toHaveBeenCalledWith(mockLibPath);
expect(mockReadJSONSync).toHaveBeenCalledWith(mockPackageJsonPath);
expect(traceSpy).not.toHaveBeenCalled();
});

test('resolves from the app directory, not from the CLI installation', async () => {
mockResolve(() => '/path/to/node_modules/aws-cdk-lib/index.js');
mockExistsSync.mockReturnValue(true);
mockReadJSONSync.mockReturnValue({ version: '2.100.0' });

await getLibraryVersion(mockIoHelper);

// Anchored inside the current working directory so that resolution walks up
// the app's `node_modules`, the same way the app itself would resolve it.
expect(mockCreateRequire).toHaveBeenCalledWith(expect.stringContaining(process.cwd()));
});

test('resolves in-process, without spawning a subprocess', async () => {
mockResolve(() => '/path/to/node_modules/aws-cdk-lib/index.js');
mockExistsSync.mockReturnValue(true);
mockReadJSONSync.mockReturnValue({ version: '2.100.0' });

await getLibraryVersion(mockIoHelper);

expect(mockRun).not.toHaveBeenCalled();
});

test('returns undefined and logs trace when resolved path does not exist', async () => {
const mockLibPath = '/nonexistent/path/to/aws-cdk-lib/index.js';
mockRun.mockResolvedValue({ stdout: mockLibPath, stderr: '' });
mockResolve(() => mockLibPath);
mockExistsSync.mockReturnValue(false);

const result = await getLibraryVersion(mockIoHelper);
Expand All @@ -66,21 +101,23 @@ describe('getLibraryVersion', () => {
);
});

test('returns undefined and logs trace when run() throws', async () => {
const runError = new Error('spawn ENOENT');
mockRun.mockRejectedValue(runError);
test('returns undefined and logs trace when aws-cdk-lib cannot be resolved', async () => {
const resolveError = new Error("Cannot find module 'aws-cdk-lib'");
mockResolve(() => {
throw resolveError;
});

const result = await getLibraryVersion(mockIoHelper);

expect(result).toBeUndefined();
expect(mockExistsSync).not.toHaveBeenCalled();
expect(mockReadJSONSync).not.toHaveBeenCalled();
expect(traceSpy).toHaveBeenCalledWith(`Could not get CDK Library Version: ${runError}`);
expect(traceSpy).toHaveBeenCalledWith(`Could not get CDK Library Version: ${resolveError}`);
});

test('handles package.json without version field', async () => {
const mockLibPath = '/path/to/node_modules/aws-cdk-lib/index.js';
mockRun.mockResolvedValue({ stdout: mockLibPath, stderr: '' });
mockResolve(() => mockLibPath);
mockExistsSync.mockReturnValue(true);
mockReadJSONSync.mockReturnValue({ name: 'aws-cdk-lib' });

Expand Down
Loading