diff --git a/packages/aws-cdk/lib/cli/telemetry/library-version.ts b/packages/aws-cdk/lib/cli/telemetry/library-version.ts index 8372bde63..9e096c3f2 100644 --- a/packages/aws-cdk/lib/cli/telemetry/library-version.ts +++ b/packages/aws-cdk/lib/cli/telemetry/library-version.ts @@ -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 { 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'); diff --git a/packages/aws-cdk/test/cli/telemetry/library-version.test.ts b/packages/aws-cdk/test/cli/telemetry/library-version.test.ts index 08555a9bb..f5b994c63 100644 --- a/packages/aws-cdk/test/cli/telemetry/library-version.test.ts +++ b/packages/aws-cdk/test/cli/telemetry/library-version.test.ts @@ -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(), })); @@ -15,9 +21,17 @@ jest.mock('fs-extra', () => ({ })); const mockRun = run as jest.MockedFunction; +const mockCreateRequire = createRequire as jest.MockedFunction; const mockExistsSync = fs.existsSync as jest.MockedFunction; const mockReadJSONSync = fs.readJSONSync as jest.MockedFunction; +/** + * 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; @@ -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); @@ -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' });