From aa28d244783cd871efa2b9d9a68a12183c12bb4f Mon Sep 17 00:00:00 2001 From: Roshane Pascual Date: Tue, 7 Jan 2025 17:57:22 -0800 Subject: [PATCH 1/8] report cdk versions --- .changeset/silver-tables-do.md | 5 + .eslint_dictionary.json | 1 + .../lock_file_reader_factory.test.ts | 72 +++++++++++ .../lock_file_reader_factory.ts | 76 +++++++++++ .../npm_lock_file_reader.test.ts | 83 ++++++++++++ .../lock-file-reader/npm_lock_file_reader.ts | 76 +++++++++++ .../pnpm_lock_file_reader.test.ts | 119 ++++++++++++++++++ .../lock-file-reader/pnpm_lock_file_reader.ts | 67 ++++++++++ .../yarn_classic_lock_file_reader.test.ts | 87 +++++++++++++ .../yarn_classic_lock_file_reader.ts | 61 +++++++++ .../yarn_modern_lock_file_reader.test.ts | 94 ++++++++++++++ .../yarn_modern_lock_file_reader.ts | 61 +++++++++ .../get_dependency_versions.test.ts | 61 +++++++++ .../src/usage-data/get_dependency_versions.ts | 21 ++++ .../src/usage-data/usage_data.ts | 2 +- .../src/usage-data/usage_data_emitter.test.ts | 21 +++- .../src/usage-data/usage_data_emitter.ts | 9 +- 17 files changed, 913 insertions(+), 3 deletions(-) create mode 100644 .changeset/silver-tables-do.md create mode 100644 packages/platform-core/src/lock-file-reader/lock_file_reader_factory.test.ts create mode 100644 packages/platform-core/src/lock-file-reader/lock_file_reader_factory.ts create mode 100644 packages/platform-core/src/lock-file-reader/npm_lock_file_reader.test.ts create mode 100644 packages/platform-core/src/lock-file-reader/npm_lock_file_reader.ts create mode 100644 packages/platform-core/src/lock-file-reader/pnpm_lock_file_reader.test.ts create mode 100644 packages/platform-core/src/lock-file-reader/pnpm_lock_file_reader.ts create mode 100644 packages/platform-core/src/lock-file-reader/yarn_classic_lock_file_reader.test.ts create mode 100644 packages/platform-core/src/lock-file-reader/yarn_classic_lock_file_reader.ts create mode 100644 packages/platform-core/src/lock-file-reader/yarn_modern_lock_file_reader.test.ts create mode 100644 packages/platform-core/src/lock-file-reader/yarn_modern_lock_file_reader.ts create mode 100644 packages/platform-core/src/usage-data/get_dependency_versions.test.ts create mode 100644 packages/platform-core/src/usage-data/get_dependency_versions.ts diff --git a/.changeset/silver-tables-do.md b/.changeset/silver-tables-do.md new file mode 100644 index 0000000000..eaa6ac2d01 --- /dev/null +++ b/.changeset/silver-tables-do.md @@ -0,0 +1,5 @@ +--- +'@aws-amplify/platform-core': patch +--- + +Report cdk versions diff --git a/.eslint_dictionary.json b/.eslint_dictionary.json index 21c64ec6a8..fd59598318 100644 --- a/.eslint_dictionary.json +++ b/.eslint_dictionary.json @@ -24,6 +24,7 @@ "changelog", "changeset", "changesets", + "checksum", "chown", "claude", "cloudformation", diff --git a/packages/platform-core/src/lock-file-reader/lock_file_reader_factory.test.ts b/packages/platform-core/src/lock-file-reader/lock_file_reader_factory.test.ts new file mode 100644 index 0000000000..5b03d01a89 --- /dev/null +++ b/packages/platform-core/src/lock-file-reader/lock_file_reader_factory.test.ts @@ -0,0 +1,72 @@ +import assert from 'assert'; +import { afterEach, beforeEach, describe, it } from 'node:test'; +import { NpmLockFileReader } from './npm_lock_file_reader'; +import { PnpmLockFileReader } from './pnpm_lock_file_reader'; +import { YarnClassicLockFileReader } from './yarn_classic_lock_file_reader'; +import { YarnModernLockFileReader } from './yarn_modern_lock_file_reader'; +import { LockFileReaderFactory } from './lock_file_reader_factory'; + +void describe('LockFileReaderFactory', () => { + let originalEnv: NodeJS.ProcessEnv; + + beforeEach(() => { + originalEnv = process.env; + }); + + afterEach(() => { + process.env = originalEnv; + }); + + void describe('getLockFileReader', () => { + const testCases = [ + { + name: 'npm', + userAgent: 'npm/7.0.0 node/v15.0.0 darwin x64', + expectedInstanceOf: NpmLockFileReader, + }, + { + name: 'pnpm', + userAgent: 'pnpm/5.0.0 node/v15.0.0 darwin x64', + expectedInstanceOf: PnpmLockFileReader, + }, + { + name: 'yarn classic', + userAgent: 'yarn/1.22.21 node/v15.0.0 darwin x64', + expectedInstanceOf: YarnClassicLockFileReader, + }, + { + name: 'yarn modern', + userAgent: 'yarn/4.0.1 node/v15.0.0 darwin x64', + expectedInstanceOf: YarnModernLockFileReader, + }, + ]; + + for (const testCase of testCases) { + void it(`should return the correct lock file reader for ${testCase.name}`, () => { + process.env.npm_config_user_agent = testCase.userAgent; + const lockFileReader = new LockFileReaderFactory().getLockFileReader(); + assert.ok(lockFileReader instanceof testCase.expectedInstanceOf); + }); + } + + void it('should throw an error for unsupport package managers', () => { + process.env.npm_config_user_agent = + 'unsupported/1.0.0 node/v15.0.0 darwin x64'; + assert.throws(() => new LockFileReaderFactory().getLockFileReader(), { + message: 'Package Manager unsupported is not supported.', + }); + }); + + void it('should throw an error for pnpm on Windows', () => { + process.env.npm_config_user_agent = 'pnpm/1.0.0 node/v15.0.0 darwin x64'; + assert.throws( + () => new LockFileReaderFactory('win32').getLockFileReader(), + { + message: 'Amplify does not support PNPM on Windows.', + details: + 'Details: https://github.com/aws-amplify/amplify-backend/blob/main/packages/create-amplify/README.md', + } + ); + }); + }); +}); diff --git a/packages/platform-core/src/lock-file-reader/lock_file_reader_factory.ts b/packages/platform-core/src/lock-file-reader/lock_file_reader_factory.ts new file mode 100644 index 0000000000..0601ec7362 --- /dev/null +++ b/packages/platform-core/src/lock-file-reader/lock_file_reader_factory.ts @@ -0,0 +1,76 @@ +import { AmplifyUserError } from '../errors'; +import { NpmLockFileReader } from './npm_lock_file_reader'; +import { PnpmLockFileReader } from './pnpm_lock_file_reader'; +import { YarnClassicLockFileReader } from './yarn_classic_lock_file_reader'; +import { YarnModernLockFileReader } from './yarn_modern_lock_file_reader'; + +/** + * LockFileReaderFactory is a factory for an abstraction around reading lock files from different package managers + */ +export class LockFileReaderFactory { + /** + * Creates a lock file reader factory + */ + constructor(private readonly platform = process.platform) {} + + /** + * Gets the lock file reader based on the package manager being used + */ + getLockFileReader(): LockFileReader { + const packageManagerName = this.getPackageManagerName(); + switch (packageManagerName) { + case 'npm': + return new NpmLockFileReader(); + case 'pnpm': + if (this.platform === 'win32') { + const message = 'Amplify does not support PNPM on Windows.'; + const details = + 'Details: https://github.com/aws-amplify/amplify-backend/blob/main/packages/create-amplify/README.md'; + throw new AmplifyUserError('UnsupportedPackageManagerError', { + message, + details, + resolution: 'Use a supported package manager for your OS', + }); + } + return new PnpmLockFileReader(); + case 'yarn-classic': + return new YarnClassicLockFileReader(); + case 'yarn-modern': + return new YarnModernLockFileReader(); + default: + throw new AmplifyUserError('UnsupportedPackageManagerError', { + message: `Package Manager ${packageManagerName} is not supported.`, + resolution: 'Use npm, yarn or pnpm.', + }); + } + } + + /** + * Get package manager name from npm_config_user_agent + */ + private getPackageManagerName() { + const userAgent = process.env.npm_config_user_agent; + if (userAgent === undefined) { + return; + } + const packageManagerAndVersion = userAgent.split(' ')[0]; + const [packageManagerName, packageManagerVersion] = + packageManagerAndVersion.split('/'); + + if (packageManagerName === 'yarn') { + const yarnMajorVersion = packageManagerVersion.split('.')[0]; + return `yarn-${yarnMajorVersion === '1' ? 'classic' : 'modern'}`; + } + return packageManagerName; + } +} + +export type LockFileReader = { + getLockFileContentsFromCwd: () => Promise; +}; + +export type LockFileContents = { + dependencies: Dependencies; +}; + +export type Dependencies = Array<{ name: string; version: string }>; diff --git a/packages/platform-core/src/lock-file-reader/npm_lock_file_reader.test.ts b/packages/platform-core/src/lock-file-reader/npm_lock_file_reader.test.ts new file mode 100644 index 0000000000..d7fa27cb64 --- /dev/null +++ b/packages/platform-core/src/lock-file-reader/npm_lock_file_reader.test.ts @@ -0,0 +1,83 @@ +import assert from 'assert'; +import fsp from 'fs/promises'; +import { afterEach, describe, it, mock } from 'node:test'; +import path from 'path'; +import { NpmLockFileReader } from './npm_lock_file_reader'; + +void describe('NpmLockFileReader', () => { + const fspAccessMock = mock.method(fsp, 'access', () => true); + const fspReadFileMock = mock.method(fsp, 'readFile', () => + JSON.stringify({ + name: 'test_project', + version: '1.0.0', + packages: { + '': { + name: 'test_project', + version: '1.0.0', + }, + 'node_modules/test_dep': { + version: '1.2.3', + }, + 'node_modules/some_other_dep': { + version: '12.13.14', + }, + }, + }) + ); + const npmLockFileReader = new NpmLockFileReader(); + + afterEach(() => { + fspReadFileMock.mock.resetCalls(); + }); + + void it('can get lock file contents from cwd', async () => { + const lockFileContents = + await npmLockFileReader.getLockFileContentsFromCwd(); + const expectedLockFileContents = { + dependencies: [ + { + name: 'test_dep', // "node_modules/" prefix is removed + version: '1.2.3', + }, + { + name: 'some_other_dep', // "node_modules/" prefix is removed + version: '12.13.14', + }, + ], + }; + assert.deepEqual(lockFileContents, expectedLockFileContents); + assert.strictEqual( + fspAccessMock.mock.calls[0].arguments[0], + path.resolve(process.cwd(), 'package-lock.json') + ); + assert.strictEqual(fspReadFileMock.mock.callCount(), 1); + }); + + void it('throws when package-lock.json is not present', async () => { + fspAccessMock.mock.mockImplementationOnce(() => + Promise.reject(new Error()) + ); + await assert.rejects( + () => npmLockFileReader.getLockFileContentsFromCwd(), + (error: Error) => { + assert.ok( + error.message.startsWith('Could not find a package-lock.json file') + ); + return true; + } + ); + assert.strictEqual(fspReadFileMock.mock.callCount(), 0); + }); + + void it('throws when package-lock.json is not parse-able', async () => { + fspReadFileMock.mock.mockImplementationOnce(() => 'not json content'); + await assert.rejects( + () => npmLockFileReader.getLockFileContentsFromCwd(), + (error: Error) => { + assert.ok(error.message.startsWith('Could not parse the contents')); + return true; + } + ); + assert.strictEqual(fspReadFileMock.mock.callCount(), 1); + }); +}); diff --git a/packages/platform-core/src/lock-file-reader/npm_lock_file_reader.ts b/packages/platform-core/src/lock-file-reader/npm_lock_file_reader.ts new file mode 100644 index 0000000000..4ded6cc122 --- /dev/null +++ b/packages/platform-core/src/lock-file-reader/npm_lock_file_reader.ts @@ -0,0 +1,76 @@ +import fsp from 'fs/promises'; +import path from 'path'; +import z from 'zod'; +import { AmplifyUserError } from '../errors'; +import { + Dependencies, + LockFileContents, + LockFileReader, +} from './lock_file_reader_factory'; + +/** + * NpmLockFileReader is an abstraction around the logic used to read and parse lock file contents + */ +export class NpmLockFileReader implements LockFileReader { + getLockFileContentsFromCwd = async (): Promise => { + const packageLockJsonPath = path.resolve( + process.cwd(), + 'package-lock.json' + ); + + try { + await fsp.access(packageLockJsonPath); + } catch (error) { + throw new AmplifyUserError('InvalidPackageLockJsonError', { + message: `Could not find a package-lock.json file at ${packageLockJsonPath}`, + resolution: `Ensure that ${packageLockJsonPath} exists and is a valid JSON file`, + }); + } + + let jsonLockParsedValue: Record; + try { + const jsonLockContents = await fsp.readFile(packageLockJsonPath, 'utf-8'); + jsonLockParsedValue = JSON.parse(jsonLockContents); + } catch (error) { + throw new AmplifyUserError( + 'InvalidPackageLockJsonError', + { + message: `Could not parse the contents of ${packageLockJsonPath}`, + resolution: `Ensure that ${packageLockJsonPath} exists and is a valid JSON file`, + }, + error as Error + ); + } + + // This will strip fields that are not part of the package lock schema + const packageLockJson = packageLockJsonSchema.parse(jsonLockParsedValue); + + const dependencies: Dependencies = []; + + for (const key in packageLockJson.packages) { + if (key === '') { + // Skip root project in packages + continue; + } + // Remove "node_modules/" prefix + const dependencyName = key.replace(/^node_modules\//, ''); + dependencies.push({ + name: dependencyName, + version: packageLockJson.packages[key].version, + }); + } + + return { dependencies }; + }; +} + +const packageLockJsonSchema = z.object({ + packages: z + .record( + z.string(), + z.object({ + version: z.string(), + }) + ) + .optional(), +}); diff --git a/packages/platform-core/src/lock-file-reader/pnpm_lock_file_reader.test.ts b/packages/platform-core/src/lock-file-reader/pnpm_lock_file_reader.test.ts new file mode 100644 index 0000000000..e2bda902df --- /dev/null +++ b/packages/platform-core/src/lock-file-reader/pnpm_lock_file_reader.test.ts @@ -0,0 +1,119 @@ +import assert from 'assert'; +import fsp from 'fs/promises'; +import { afterEach, describe, it, mock } from 'node:test'; +import path from 'path'; +import { PnpmLockFileReader } from './pnpm_lock_file_reader'; + +void describe('PnpmLockFileReader', () => { + const fspAccessMock = mock.method(fsp, 'access', () => true); + const fspReadFileMock = mock.method( + fsp, + 'readFile', + () => `lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: + dependencies: + aws-amplify: + specifier: ^6.12.0 + version: 6.12.0 + devDependencies: + '@aws-amplify/backend': + specifier: ^1.11.0 + version: 1.12.0(@aws-sdk/client-cloudformation@3.723.0)(@aws-sdk/client-s3@3.723.0)(@aws-sdk/client-sso-oidc@3.621.0(@aws-sdk/client-sts@3.621.0))(@aws-sdk/types@3.723.0)(aws-cdk-lib@2.174.1(constructs@10.4.2))(constructs@10.4.2)(zod@3.24.1) + '@aws-amplify/backend-cli': + specifier: ^1.4.5 + version: 1.4.6(@aws-sdk/client-sso-oidc@3.621.0(@aws-sdk/client-sts@3.621.0))(@aws-sdk/client-sts@3.621.0)(@aws-sdk/types@3.723.0)(aws-cdk-lib@2.174.1(constructs@10.4.2))(aws-cdk@2.174.1)(constructs@10.4.2)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.7.2) + aws-cdk: + specifier: ^2.173.4 + version: 2.174.1 + aws-cdk-lib: + specifier: ^2.173.4 + version: 2.174.1(constructs@10.4.2) + constructs: + specifier: ^10.4.2 + version: 10.4.2 + esbuild: + specifier: ^0.24.2 + version: 0.24.2 + tsx: + specifier: ^4.19.2 + version: 4.19.2 + typescript: + specifier: ^5.7.2 + version: 5.7.2 + +packages: + + '@test_dep@1.2.3': + resolution: {integrity: some-sha} + engines: {node: '>=6.0.0'} + + 'some_other_dep@12.13.14': + resolution: {integrity: some-other-sha} + engines: {node: '>=8'}` + ); + const pnpmLockFileReader = new PnpmLockFileReader(); + + afterEach(() => { + fspReadFileMock.mock.resetCalls(); + }); + + void it('can get lock file contents from cwd', async () => { + const lockFileContents = + await pnpmLockFileReader.getLockFileContentsFromCwd(); + const expectedLockFileContents = { + dependencies: [ + { + name: '@test_dep', + version: '1.2.3', + }, + { + name: 'some_other_dep', + version: '12.13.14', + }, + ], + }; + assert.deepEqual(lockFileContents, expectedLockFileContents); + assert.strictEqual( + fspAccessMock.mock.calls[0].arguments[0], + path.resolve(process.cwd(), 'pnpm-lock.yaml') + ); + assert.strictEqual(fspReadFileMock.mock.callCount(), 1); + }); + + void it('throws when pnpm-lock.yaml is not present', async () => { + fspAccessMock.mock.mockImplementationOnce(() => + Promise.reject(new Error()) + ); + await assert.rejects( + () => pnpmLockFileReader.getLockFileContentsFromCwd(), + (error: Error) => { + assert.ok( + error.message.startsWith('Could not find a pnpm-lock.yaml file') + ); + return true; + } + ); + assert.strictEqual(fspReadFileMock.mock.callCount(), 0); + }); + + void it('throws when pnpm-lock.yaml is not parse-able', async () => { + fspReadFileMock.mock.mockImplementationOnce(() => + Promise.reject(new Error()) + ); + await assert.rejects( + () => pnpmLockFileReader.getLockFileContentsFromCwd(), + (error: Error) => { + assert.ok(error.message.startsWith('Could not parse the contents')); + return true; + } + ); + assert.strictEqual(fspReadFileMock.mock.callCount(), 1); + }); +}); diff --git a/packages/platform-core/src/lock-file-reader/pnpm_lock_file_reader.ts b/packages/platform-core/src/lock-file-reader/pnpm_lock_file_reader.ts new file mode 100644 index 0000000000..f437b503ca --- /dev/null +++ b/packages/platform-core/src/lock-file-reader/pnpm_lock_file_reader.ts @@ -0,0 +1,67 @@ +import fsp from 'fs/promises'; +import { EOL } from 'os'; +import path from 'path'; +import { + Dependencies, + LockFileContents, + LockFileReader, +} from './lock_file_reader_factory'; +import { AmplifyUserError } from '../errors'; + +/** + * PnpmLockFileReader is an abstraction around the logic used to read and parse lock file contents + */ +export class PnpmLockFileReader implements LockFileReader { + getLockFileContentsFromCwd = async (): Promise => { + const pnpmLockPath = path.resolve(process.cwd(), 'pnpm-lock.yaml'); + + try { + await fsp.access(pnpmLockPath); + } catch (error) { + throw new AmplifyUserError('InvalidPackageLockJsonError', { + message: `Could not find a pnpm-lock.yaml file at ${pnpmLockPath}`, + resolution: `Ensure that ${pnpmLockPath} exists and is a valid pnpm-lock.yaml file`, + }); + } + + const dependencies: Dependencies = []; + + try { + const pnpmLockContents = await fsp.readFile(pnpmLockPath, 'utf-8'); + const pnpmLockContentsArray = pnpmLockContents.split(EOL + EOL); + + const startOfPackagesIndex = pnpmLockContentsArray.indexOf('packages:'); + const pnpmLockPackages = pnpmLockContentsArray.slice( + startOfPackagesIndex + 1 + ); + + for (const pnpmDependencyBlock of pnpmLockPackages) { + // Get line that contains dependency name and version and remove quotes and colon + const pnpmDependencyLine = pnpmDependencyBlock + .trim() + .split(EOL)[0] + .replaceAll(/[':]/g, ''); + const dependencyName = pnpmDependencyLine.slice( + 0, + pnpmDependencyLine.lastIndexOf('@') + ); + const dependencyVersion = pnpmDependencyLine.slice( + pnpmDependencyLine.lastIndexOf('@') + 1 + ); + + dependencies.push({ name: dependencyName, version: dependencyVersion }); + } + } catch (error) { + throw new AmplifyUserError( + 'InvalidPackageLockJsonError', + { + message: `Could not parse the contents of ${pnpmLockPath}`, + resolution: `Ensure that ${pnpmLockPath} exists and is a valid pnpm-lock.yaml file`, + }, + error as Error + ); + } + + return { dependencies }; + }; +} diff --git a/packages/platform-core/src/lock-file-reader/yarn_classic_lock_file_reader.test.ts b/packages/platform-core/src/lock-file-reader/yarn_classic_lock_file_reader.test.ts new file mode 100644 index 0000000000..027fa36097 --- /dev/null +++ b/packages/platform-core/src/lock-file-reader/yarn_classic_lock_file_reader.test.ts @@ -0,0 +1,87 @@ +import assert from 'assert'; +import fsp from 'fs/promises'; +import { afterEach, describe, it, mock } from 'node:test'; +import path from 'path'; +import { YarnClassicLockFileReader } from './yarn_classic_lock_file_reader'; + +void describe('YarnClassicLockFileReader', () => { + const fspAccessMock = mock.method(fsp, 'access', () => true); + const fspReadFileMock = mock.method( + fsp, + 'readFile', + () => `# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 + + +"@test_dep@^1.0.0": + version "1.2.3" + resolved "https://registry.yarnpkg.com/some_dep" + integrity some-sha + dependencies: + "sub_dep_1" "^0.3.5" + "sub_dep_2" "^0.3.24" + +some_other_dep@12.13.14: + version "12.13.14" + resolved "https://registry.yarnpkg.com/some_other_dep" + integrity some-other-sha + dependencies: + sub_dep_3 "~2.0.1"` + ); + const yarnClassicLockFileReader = new YarnClassicLockFileReader(); + + afterEach(() => { + fspReadFileMock.mock.resetCalls(); + }); + + void it('can get lock file contents from cwd', async () => { + const lockFileContents = + await yarnClassicLockFileReader.getLockFileContentsFromCwd(); + const expectedLockFileContents = { + dependencies: [ + { + name: '@test_dep', + version: '1.2.3', + }, + { + name: 'some_other_dep', + version: '12.13.14', + }, + ], + }; + assert.deepEqual(lockFileContents, expectedLockFileContents); + assert.strictEqual( + fspAccessMock.mock.calls[0].arguments[0], + path.resolve(process.cwd(), 'yarn.lock') + ); + assert.strictEqual(fspReadFileMock.mock.callCount(), 1); + }); + + void it('throws when yarn.lock is not present', async () => { + fspAccessMock.mock.mockImplementationOnce(() => + Promise.reject(new Error()) + ); + await assert.rejects( + () => yarnClassicLockFileReader.getLockFileContentsFromCwd(), + (error: Error) => { + assert.ok(error.message.startsWith('Could not find a yarn.lock file')); + return true; + } + ); + assert.strictEqual(fspReadFileMock.mock.callCount(), 0); + }); + + void it('throws when yarn.lock is not parse-able', async () => { + fspReadFileMock.mock.mockImplementationOnce(() => + Promise.reject(new Error()) + ); + await assert.rejects( + () => yarnClassicLockFileReader.getLockFileContentsFromCwd(), + (error: Error) => { + assert.ok(error.message.startsWith('Could not parse the contents')); + return true; + } + ); + assert.strictEqual(fspReadFileMock.mock.callCount(), 1); + }); +}); diff --git a/packages/platform-core/src/lock-file-reader/yarn_classic_lock_file_reader.ts b/packages/platform-core/src/lock-file-reader/yarn_classic_lock_file_reader.ts new file mode 100644 index 0000000000..c3a6c87d1a --- /dev/null +++ b/packages/platform-core/src/lock-file-reader/yarn_classic_lock_file_reader.ts @@ -0,0 +1,61 @@ +import fsp from 'fs/promises'; +import { EOL } from 'os'; +import path from 'path'; +import { + Dependencies, + LockFileContents, + LockFileReader, +} from './lock_file_reader_factory'; +import { AmplifyUserError } from '../errors'; + +/** + * YarnClassicLockFileReader is an abstraction around the logic used to read and parse lock file contents + */ +export class YarnClassicLockFileReader implements LockFileReader { + getLockFileContentsFromCwd = async (): Promise => { + const yarnLockPath = path.resolve(process.cwd(), 'yarn.lock'); + + try { + await fsp.access(yarnLockPath); + } catch (error) { + throw new AmplifyUserError('InvalidPackageLockJsonError', { + message: `Could not find a yarn.lock file at ${yarnLockPath}`, + resolution: `Ensure that ${yarnLockPath} exists and is a valid yarn.lock file`, + }); + } + + const dependencies: Dependencies = []; + + try { + const yarnLockContents = await fsp.readFile(yarnLockPath, 'utf-8'); + const yarnLockContentsArray = yarnLockContents.split(EOL + EOL); + + // Slice to remove comment block at the start of the lock file + for (const yarnDependencyBlock of yarnLockContentsArray.slice(1)) { + const yarnDependencyLines = yarnDependencyBlock.trim().split(EOL); + const yarnDependencyName = yarnDependencyLines[0]; + const yarnDependencyVersion = yarnDependencyLines[1]; + + // Get dependency name before versioning info + const dependencyName = yarnDependencyName + .slice(0, yarnDependencyName.lastIndexOf('@')) + .replaceAll(/"/g, ''); + const versionMatch = yarnDependencyVersion.match(/"(.*)"/); + const dependencyVersion = versionMatch ? versionMatch[1] : ''; + + dependencies.push({ name: dependencyName, version: dependencyVersion }); + } + } catch (error) { + throw new AmplifyUserError( + 'InvalidPackageLockJsonError', + { + message: `Could not parse the contents of ${yarnLockPath}`, + resolution: `Ensure that ${yarnLockPath} exists and is a valid yarn.lock file`, + }, + error as Error + ); + } + + return { dependencies }; + }; +} diff --git a/packages/platform-core/src/lock-file-reader/yarn_modern_lock_file_reader.test.ts b/packages/platform-core/src/lock-file-reader/yarn_modern_lock_file_reader.test.ts new file mode 100644 index 0000000000..cf00d43fd8 --- /dev/null +++ b/packages/platform-core/src/lock-file-reader/yarn_modern_lock_file_reader.test.ts @@ -0,0 +1,94 @@ +import assert from 'assert'; +import fsp from 'fs/promises'; +import { afterEach, describe, it, mock } from 'node:test'; +import path from 'path'; +import { YarnModernLockFileReader } from './yarn_modern_lock_file_reader'; + +void describe('YarnModernLockFileReader', () => { + const fspAccessMock = mock.method(fsp, 'access', () => true); + const fspReadFileMock = mock.method( + fsp, + 'readFile', + () => `# This file is generated by running "yarn install" inside your project. +# Manual changes might be lost - proceed with caution! + +__metadata: + version: 8 + cacheKey: 10c0 + +"@test_dep@npm:^1.0.0": + version: 1.2.3 + resolution: "@test_dep@npm:1.2.3" + dependencies: + "@sub_dep_1": "npm:^0.3.5" + "sub_dep_2": "npm:^0.3.24" + checksum: some-checksum + languageName: node + linkType: hard + +"some_other_dep@npm:12.13.14": + version: 12.13.14 + resolution: "some_other_dep@npm:12.13.14" + dependencies: + sub-dep_3: "npm:~2.0.1" + checksum: some-other-checksum + languageName: node + linkType: hard` + ); + const yarnModernLockFileReader = new YarnModernLockFileReader(); + + afterEach(() => { + fspReadFileMock.mock.resetCalls(); + }); + + void it('can get lock file contents from cwd', async () => { + const lockFileContents = + await yarnModernLockFileReader.getLockFileContentsFromCwd(); + const expectedLockFileContents = { + dependencies: [ + { + name: '@test_dep', + version: '1.2.3', + }, + { + name: 'some_other_dep', + version: '12.13.14', + }, + ], + }; + assert.deepEqual(lockFileContents, expectedLockFileContents); + assert.strictEqual( + fspAccessMock.mock.calls[0].arguments[0], + path.resolve(process.cwd(), 'yarn.lock') + ); + assert.strictEqual(fspReadFileMock.mock.callCount(), 1); + }); + + void it('throws when yarn.lock is not present', async () => { + fspAccessMock.mock.mockImplementationOnce(() => + Promise.reject(new Error()) + ); + await assert.rejects( + () => yarnModernLockFileReader.getLockFileContentsFromCwd(), + (error: Error) => { + assert.ok(error.message.startsWith('Could not find a yarn.lock file')); + return true; + } + ); + assert.strictEqual(fspReadFileMock.mock.callCount(), 0); + }); + + void it('throws when yarn.lock is not parse-able', async () => { + fspReadFileMock.mock.mockImplementationOnce(() => + Promise.reject(new Error()) + ); + await assert.rejects( + () => yarnModernLockFileReader.getLockFileContentsFromCwd(), + (error: Error) => { + assert.ok(error.message.startsWith('Could not parse the contents')); + return true; + } + ); + assert.strictEqual(fspReadFileMock.mock.callCount(), 1); + }); +}); diff --git a/packages/platform-core/src/lock-file-reader/yarn_modern_lock_file_reader.ts b/packages/platform-core/src/lock-file-reader/yarn_modern_lock_file_reader.ts new file mode 100644 index 0000000000..aa0bd5f859 --- /dev/null +++ b/packages/platform-core/src/lock-file-reader/yarn_modern_lock_file_reader.ts @@ -0,0 +1,61 @@ +import fsp from 'fs/promises'; +import { EOL } from 'os'; +import path from 'path'; +import { + Dependencies, + LockFileContents, + LockFileReader, +} from './lock_file_reader_factory'; +import { AmplifyUserError } from '../errors'; + +/** + * YarnModernLockFileReader is an abstraction around the logic used to read and parse lock file contents + */ +export class YarnModernLockFileReader implements LockFileReader { + getLockFileContentsFromCwd = async (): Promise => { + const yarnLockPath = path.resolve(process.cwd(), 'yarn.lock'); + + try { + await fsp.access(yarnLockPath); + } catch (error) { + throw new AmplifyUserError('InvalidPackageLockJsonError', { + message: `Could not find a yarn.lock file at ${yarnLockPath}`, + resolution: `Ensure that ${yarnLockPath} exists and is a valid yarn.lock file`, + }); + } + + const dependencies: Dependencies = []; + + try { + const yarnLockContents = await fsp.readFile(yarnLockPath, 'utf-8'); + const yarnLockContentsArray = yarnLockContents.split(EOL + EOL); + + // Slice to remove comment block and metadata at the start of the lock file + for (const yarnDependencyBlock of yarnLockContentsArray.slice(2)) { + const yarnDependencyLines = yarnDependencyBlock.trim().split(EOL); + const yarnDependencyName = yarnDependencyLines[0]; + const yarnDependencyVersion = yarnDependencyLines[1]; + + // Get dependency name before versioning info + const dependencyName = yarnDependencyName + .slice(0, yarnDependencyName.lastIndexOf('@')) + .replaceAll(/"/g, ''); + const versionMatch = yarnDependencyVersion.match(/[\d.]+/); + const dependencyVersion = versionMatch ? versionMatch[0] : ''; + + dependencies.push({ name: dependencyName, version: dependencyVersion }); + } + } catch (error) { + throw new AmplifyUserError( + 'InvalidPackageLockJsonError', + { + message: `Could not parse the contents of ${yarnLockPath}`, + resolution: `Ensure that ${yarnLockPath} exists and is a valid yarn.lock file`, + }, + error as Error + ); + } + + return { dependencies }; + }; +} diff --git a/packages/platform-core/src/usage-data/get_dependency_versions.test.ts b/packages/platform-core/src/usage-data/get_dependency_versions.test.ts new file mode 100644 index 0000000000..fb726664b4 --- /dev/null +++ b/packages/platform-core/src/usage-data/get_dependency_versions.test.ts @@ -0,0 +1,61 @@ +import assert from 'assert'; +import { describe, it } from 'node:test'; +import { getDependencyVersions } from './get_dependency_versions'; +import { LockFileContents } from '../lock-file-reader/lock_file_reader_factory'; + +void describe('getDependencyVersions', () => { + void it('successfully returns dependency versions', () => { + const lockFileContents: LockFileContents = { + dependencies: [ + { + name: 'aws-cdk', + version: '1.2.3', + }, + { + name: 'aws-cdk-lib', + version: '12.13.14', + }, + { + name: 'test_dep', + version: '1.23.45', + }, + { + name: 'some_other_dep', + version: '12.1.3', + }, + ], + }; + + const dependencyVersions = getDependencyVersions(lockFileContents); + const expectedVersions = [ + { + name: 'aws-cdk', + version: '1.2.3', + }, + { + name: 'aws-cdk-lib', + version: '12.13.14', + }, + ]; + + assert.deepEqual(dependencyVersions, expectedVersions); + }); + + void it('returns empty array if there are no matching dependencies', () => { + const lockFileContents: LockFileContents = { + dependencies: [ + { + name: 'test_dep', + version: '1.23.45', + }, + { + name: 'some_other_dep', + version: '12.1.3', + }, + ], + }; + + const dependencyVersions = getDependencyVersions(lockFileContents); + assert.deepEqual(dependencyVersions, []); + }); +}); diff --git a/packages/platform-core/src/usage-data/get_dependency_versions.ts b/packages/platform-core/src/usage-data/get_dependency_versions.ts new file mode 100644 index 0000000000..6f3bb380b7 --- /dev/null +++ b/packages/platform-core/src/usage-data/get_dependency_versions.ts @@ -0,0 +1,21 @@ +import { + Dependencies, + LockFileContents, +} from '../lock-file-reader/lock_file_reader_factory'; + +/** + * Get dependency versions from customer project's lock file + */ +export const getDependencyVersions = (lockFileContents: LockFileContents) => { + const targetDependencies = ['aws-cdk', 'aws-cdk-lib']; + + const dependencyVersions: Dependencies = []; + + for (const { name, version } of lockFileContents.dependencies) { + if (targetDependencies.includes(name)) { + dependencyVersions.push({ name, version }); + } + } + + return dependencyVersions; +}; diff --git a/packages/platform-core/src/usage-data/usage_data.ts b/packages/platform-core/src/usage-data/usage_data.ts index 68773639ed..41b7a8b995 100644 --- a/packages/platform-core/src/usage-data/usage_data.ts +++ b/packages/platform-core/src/usage-data/usage_data.ts @@ -16,5 +16,5 @@ export type UsageData = { accountId: string; input: { command: string; plugin: string }; codePathDurations: { platformStartup?: number; totalDuration?: number }; - projectSetting: { editor?: string }; + projectSetting: { editor?: string; details?: string }; }; diff --git a/packages/platform-core/src/usage-data/usage_data_emitter.test.ts b/packages/platform-core/src/usage-data/usage_data_emitter.test.ts index 89c53c5ad4..1e08b86e6c 100644 --- a/packages/platform-core/src/usage-data/usage_data_emitter.test.ts +++ b/packages/platform-core/src/usage-data/usage_data_emitter.test.ts @@ -11,6 +11,7 @@ import { AccountIdFetcher } from './account_id_fetcher'; import { UsageData } from './usage_data'; import isCI from 'is-ci'; import { AmplifyError, AmplifyUserError } from '..'; +import { LockFileReader } from '../lock-file-reader/lock_file_reader_factory'; const originalNpmUserAgent = process.env.npm_config_user_agent; const testNpmUserAgent = 'testNpmUserAgent'; @@ -40,6 +41,23 @@ void describe('UsageDataEmitter', () => { fetch: async () => '123456789012', } as AccountIdFetcher; + // For LockFileReader + const lockFileReaderMock = { + getLockFileContentsFromCwd: async () => + Promise.resolve({ + dependencies: [ + { + name: 'aws-cdk', + version: '1.2.4', + }, + { + name: 'test_dep', + version: '12.13.14', + }, + ], + }), + } as LockFileReader; + mock.method(https, 'request', () => reqMock); before(() => { @@ -157,7 +175,8 @@ void describe('UsageDataEmitter', () => { testLibraryVersion, v4(), testURL, - accountIdFetcherMock + accountIdFetcherMock, + lockFileReaderMock ); let usageDataEmitterPromise; diff --git a/packages/platform-core/src/usage-data/usage_data_emitter.ts b/packages/platform-core/src/usage-data/usage_data_emitter.ts index 2677c9b447..40109bfd52 100644 --- a/packages/platform-core/src/usage-data/usage_data_emitter.ts +++ b/packages/platform-core/src/usage-data/usage_data_emitter.ts @@ -10,6 +10,8 @@ import isCI from 'is-ci'; import { SerializableError } from './serializable_error.js'; import { UsageDataEmitter } from './usage_data_emitter_factory.js'; import { AmplifyError } from '../index.js'; +import { getDependencyVersions } from './get_dependency_versions.js'; +import { LockFileReaderFactory } from '../lock-file-reader/lock_file_reader_factory.js'; /** * Entry point for sending usage data metrics @@ -22,7 +24,8 @@ export class DefaultUsageDataEmitter implements UsageDataEmitter { private readonly libraryVersion: string, private readonly sessionUuid = uuid(), private readonly url = getUrl(), - private readonly accountIdFetcher = new AccountIdFetcher() + private readonly accountIdFetcher = new AccountIdFetcher(), + private readonly lockFileReader = new LockFileReaderFactory().getLockFileReader() ) {} emitSuccess = async ( @@ -65,6 +68,9 @@ export class DefaultUsageDataEmitter implements UsageDataEmitter { dimensions?: Record; error?: AmplifyError; }): Promise => { + const lockFileContents = + await this.lockFileReader.getLockFileContentsFromCwd(); + const dependencyVersions = getDependencyVersions(lockFileContents); return { accountId: await this.accountIdFetcher.fetch(), sessionUuid: this.sessionUuid, @@ -88,6 +94,7 @@ export class DefaultUsageDataEmitter implements UsageDataEmitter { isCi: isCI, projectSetting: { editor: process.env.npm_config_user_agent, + details: JSON.stringify(dependencyVersions), }, }; }; From 8f615b25ef7f82211abda94fbae74730e0526f93 Mon Sep 17 00:00:00 2001 From: Roshane Pascual Date: Wed, 8 Jan 2025 11:38:00 -0800 Subject: [PATCH 2/8] update how and when we read lock file --- .../lock_file_reader_factory.test.ts | 2 +- .../npm_lock_file_reader.test.ts | 32 ++------ .../lock-file-reader/npm_lock_file_reader.ts | 23 +----- .../pnpm_lock_file_reader.test.ts | 33 ++------ .../lock-file-reader/pnpm_lock_file_reader.ts | 23 +----- .../yarn_classic_lock_file_reader.test.ts | 30 ++------ .../yarn_classic_lock_file_reader.ts | 23 +----- .../yarn_modern_lock_file_reader.test.ts | 30 ++------ .../yarn_modern_lock_file_reader.ts | 23 +----- .../dependency_version_fetcher.test.ts | 75 +++++++++++++++++++ .../usage-data/dependency_version_fetcher.ts | 33 ++++++++ .../get_dependency_versions.test.ts | 61 --------------- .../src/usage-data/get_dependency_versions.ts | 21 ------ .../src/usage-data/usage_data_emitter.test.ts | 8 +- .../src/usage-data/usage_data_emitter.ts | 12 ++- 15 files changed, 154 insertions(+), 275 deletions(-) create mode 100644 packages/platform-core/src/usage-data/dependency_version_fetcher.test.ts create mode 100644 packages/platform-core/src/usage-data/dependency_version_fetcher.ts delete mode 100644 packages/platform-core/src/usage-data/get_dependency_versions.test.ts delete mode 100644 packages/platform-core/src/usage-data/get_dependency_versions.ts diff --git a/packages/platform-core/src/lock-file-reader/lock_file_reader_factory.test.ts b/packages/platform-core/src/lock-file-reader/lock_file_reader_factory.test.ts index 5b03d01a89..197480cafb 100644 --- a/packages/platform-core/src/lock-file-reader/lock_file_reader_factory.test.ts +++ b/packages/platform-core/src/lock-file-reader/lock_file_reader_factory.test.ts @@ -49,7 +49,7 @@ void describe('LockFileReaderFactory', () => { }); } - void it('should throw an error for unsupport package managers', () => { + void it('should throw an error for unsupported package managers', () => { process.env.npm_config_user_agent = 'unsupported/1.0.0 node/v15.0.0 darwin x64'; assert.throws(() => new LockFileReaderFactory().getLockFileReader(), { diff --git a/packages/platform-core/src/lock-file-reader/npm_lock_file_reader.test.ts b/packages/platform-core/src/lock-file-reader/npm_lock_file_reader.test.ts index d7fa27cb64..91fdbbf07b 100644 --- a/packages/platform-core/src/lock-file-reader/npm_lock_file_reader.test.ts +++ b/packages/platform-core/src/lock-file-reader/npm_lock_file_reader.test.ts @@ -5,7 +5,6 @@ import path from 'path'; import { NpmLockFileReader } from './npm_lock_file_reader'; void describe('NpmLockFileReader', () => { - const fspAccessMock = mock.method(fsp, 'access', () => true); const fspReadFileMock = mock.method(fsp, 'readFile', () => JSON.stringify({ name: 'test_project', @@ -47,37 +46,18 @@ void describe('NpmLockFileReader', () => { }; assert.deepEqual(lockFileContents, expectedLockFileContents); assert.strictEqual( - fspAccessMock.mock.calls[0].arguments[0], + fspReadFileMock.mock.calls[0].arguments[0], path.resolve(process.cwd(), 'package-lock.json') ); assert.strictEqual(fspReadFileMock.mock.callCount(), 1); }); - void it('throws when package-lock.json is not present', async () => { - fspAccessMock.mock.mockImplementationOnce(() => + void it('returns empty lock file contents when package-lock.json is not present or parse-able', async () => { + fspReadFileMock.mock.mockImplementationOnce(() => Promise.reject(new Error()) ); - await assert.rejects( - () => npmLockFileReader.getLockFileContentsFromCwd(), - (error: Error) => { - assert.ok( - error.message.startsWith('Could not find a package-lock.json file') - ); - return true; - } - ); - assert.strictEqual(fspReadFileMock.mock.callCount(), 0); - }); - - void it('throws when package-lock.json is not parse-able', async () => { - fspReadFileMock.mock.mockImplementationOnce(() => 'not json content'); - await assert.rejects( - () => npmLockFileReader.getLockFileContentsFromCwd(), - (error: Error) => { - assert.ok(error.message.startsWith('Could not parse the contents')); - return true; - } - ); - assert.strictEqual(fspReadFileMock.mock.callCount(), 1); + const lockFileContents = + await npmLockFileReader.getLockFileContentsFromCwd(); + assert.deepEqual(lockFileContents, { dependencies: [] }); }); }); diff --git a/packages/platform-core/src/lock-file-reader/npm_lock_file_reader.ts b/packages/platform-core/src/lock-file-reader/npm_lock_file_reader.ts index 4ded6cc122..fa3e56aa8f 100644 --- a/packages/platform-core/src/lock-file-reader/npm_lock_file_reader.ts +++ b/packages/platform-core/src/lock-file-reader/npm_lock_file_reader.ts @@ -1,7 +1,6 @@ import fsp from 'fs/promises'; import path from 'path'; import z from 'zod'; -import { AmplifyUserError } from '../errors'; import { Dependencies, LockFileContents, @@ -13,40 +12,24 @@ import { */ export class NpmLockFileReader implements LockFileReader { getLockFileContentsFromCwd = async (): Promise => { + const dependencies: Dependencies = []; const packageLockJsonPath = path.resolve( process.cwd(), 'package-lock.json' ); - try { - await fsp.access(packageLockJsonPath); - } catch (error) { - throw new AmplifyUserError('InvalidPackageLockJsonError', { - message: `Could not find a package-lock.json file at ${packageLockJsonPath}`, - resolution: `Ensure that ${packageLockJsonPath} exists and is a valid JSON file`, - }); - } - let jsonLockParsedValue: Record; try { const jsonLockContents = await fsp.readFile(packageLockJsonPath, 'utf-8'); jsonLockParsedValue = JSON.parse(jsonLockContents); } catch (error) { - throw new AmplifyUserError( - 'InvalidPackageLockJsonError', - { - message: `Could not parse the contents of ${packageLockJsonPath}`, - resolution: `Ensure that ${packageLockJsonPath} exists and is a valid JSON file`, - }, - error as Error - ); + // We failed to get lock file contents either because file doesn't exist or it is not parse-able + return { dependencies }; } // This will strip fields that are not part of the package lock schema const packageLockJson = packageLockJsonSchema.parse(jsonLockParsedValue); - const dependencies: Dependencies = []; - for (const key in packageLockJson.packages) { if (key === '') { // Skip root project in packages diff --git a/packages/platform-core/src/lock-file-reader/pnpm_lock_file_reader.test.ts b/packages/platform-core/src/lock-file-reader/pnpm_lock_file_reader.test.ts index e2bda902df..3fef3d88c3 100644 --- a/packages/platform-core/src/lock-file-reader/pnpm_lock_file_reader.test.ts +++ b/packages/platform-core/src/lock-file-reader/pnpm_lock_file_reader.test.ts @@ -5,10 +5,10 @@ import path from 'path'; import { PnpmLockFileReader } from './pnpm_lock_file_reader'; void describe('PnpmLockFileReader', () => { - const fspAccessMock = mock.method(fsp, 'access', () => true); const fspReadFileMock = mock.method( fsp, 'readFile', + // eslint-disable-next-line spellcheck/spell-checker () => `lockfileVersion: '9.0' settings: @@ -81,39 +81,18 @@ packages: }; assert.deepEqual(lockFileContents, expectedLockFileContents); assert.strictEqual( - fspAccessMock.mock.calls[0].arguments[0], + fspReadFileMock.mock.calls[0].arguments[0], path.resolve(process.cwd(), 'pnpm-lock.yaml') ); assert.strictEqual(fspReadFileMock.mock.callCount(), 1); }); - void it('throws when pnpm-lock.yaml is not present', async () => { - fspAccessMock.mock.mockImplementationOnce(() => - Promise.reject(new Error()) - ); - await assert.rejects( - () => pnpmLockFileReader.getLockFileContentsFromCwd(), - (error: Error) => { - assert.ok( - error.message.startsWith('Could not find a pnpm-lock.yaml file') - ); - return true; - } - ); - assert.strictEqual(fspReadFileMock.mock.callCount(), 0); - }); - - void it('throws when pnpm-lock.yaml is not parse-able', async () => { + void it('returns empty lock file contents when pnpm-lock.yaml is not present or parse-able', async () => { fspReadFileMock.mock.mockImplementationOnce(() => Promise.reject(new Error()) ); - await assert.rejects( - () => pnpmLockFileReader.getLockFileContentsFromCwd(), - (error: Error) => { - assert.ok(error.message.startsWith('Could not parse the contents')); - return true; - } - ); - assert.strictEqual(fspReadFileMock.mock.callCount(), 1); + const lockFileContents = + await pnpmLockFileReader.getLockFileContentsFromCwd(); + assert.deepEqual(lockFileContents, { dependencies: [] }); }); }); diff --git a/packages/platform-core/src/lock-file-reader/pnpm_lock_file_reader.ts b/packages/platform-core/src/lock-file-reader/pnpm_lock_file_reader.ts index f437b503ca..0323bc58dc 100644 --- a/packages/platform-core/src/lock-file-reader/pnpm_lock_file_reader.ts +++ b/packages/platform-core/src/lock-file-reader/pnpm_lock_file_reader.ts @@ -6,25 +6,14 @@ import { LockFileContents, LockFileReader, } from './lock_file_reader_factory'; -import { AmplifyUserError } from '../errors'; /** * PnpmLockFileReader is an abstraction around the logic used to read and parse lock file contents */ export class PnpmLockFileReader implements LockFileReader { getLockFileContentsFromCwd = async (): Promise => { - const pnpmLockPath = path.resolve(process.cwd(), 'pnpm-lock.yaml'); - - try { - await fsp.access(pnpmLockPath); - } catch (error) { - throw new AmplifyUserError('InvalidPackageLockJsonError', { - message: `Could not find a pnpm-lock.yaml file at ${pnpmLockPath}`, - resolution: `Ensure that ${pnpmLockPath} exists and is a valid pnpm-lock.yaml file`, - }); - } - const dependencies: Dependencies = []; + const pnpmLockPath = path.resolve(process.cwd(), 'pnpm-lock.yaml'); try { const pnpmLockContents = await fsp.readFile(pnpmLockPath, 'utf-8'); @@ -52,14 +41,8 @@ export class PnpmLockFileReader implements LockFileReader { dependencies.push({ name: dependencyName, version: dependencyVersion }); } } catch (error) { - throw new AmplifyUserError( - 'InvalidPackageLockJsonError', - { - message: `Could not parse the contents of ${pnpmLockPath}`, - resolution: `Ensure that ${pnpmLockPath} exists and is a valid pnpm-lock.yaml file`, - }, - error as Error - ); + // We failed to get lock file contents either because file doesn't exist or it is not parse-able + return { dependencies }; } return { dependencies }; diff --git a/packages/platform-core/src/lock-file-reader/yarn_classic_lock_file_reader.test.ts b/packages/platform-core/src/lock-file-reader/yarn_classic_lock_file_reader.test.ts index 027fa36097..2e82d58807 100644 --- a/packages/platform-core/src/lock-file-reader/yarn_classic_lock_file_reader.test.ts +++ b/packages/platform-core/src/lock-file-reader/yarn_classic_lock_file_reader.test.ts @@ -5,7 +5,6 @@ import path from 'path'; import { YarnClassicLockFileReader } from './yarn_classic_lock_file_reader'; void describe('YarnClassicLockFileReader', () => { - const fspAccessMock = mock.method(fsp, 'access', () => true); const fspReadFileMock = mock.method( fsp, 'readFile', @@ -51,37 +50,18 @@ some_other_dep@12.13.14: }; assert.deepEqual(lockFileContents, expectedLockFileContents); assert.strictEqual( - fspAccessMock.mock.calls[0].arguments[0], + fspReadFileMock.mock.calls[0].arguments[0], path.resolve(process.cwd(), 'yarn.lock') ); assert.strictEqual(fspReadFileMock.mock.callCount(), 1); }); - void it('throws when yarn.lock is not present', async () => { - fspAccessMock.mock.mockImplementationOnce(() => - Promise.reject(new Error()) - ); - await assert.rejects( - () => yarnClassicLockFileReader.getLockFileContentsFromCwd(), - (error: Error) => { - assert.ok(error.message.startsWith('Could not find a yarn.lock file')); - return true; - } - ); - assert.strictEqual(fspReadFileMock.mock.callCount(), 0); - }); - - void it('throws when yarn.lock is not parse-able', async () => { + void it('returns empty lock file contents when yarn.lock is not present or parse-able', async () => { fspReadFileMock.mock.mockImplementationOnce(() => Promise.reject(new Error()) ); - await assert.rejects( - () => yarnClassicLockFileReader.getLockFileContentsFromCwd(), - (error: Error) => { - assert.ok(error.message.startsWith('Could not parse the contents')); - return true; - } - ); - assert.strictEqual(fspReadFileMock.mock.callCount(), 1); + const lockFileContents = + await yarnClassicLockFileReader.getLockFileContentsFromCwd(); + assert.deepEqual(lockFileContents, { dependencies: [] }); }); }); diff --git a/packages/platform-core/src/lock-file-reader/yarn_classic_lock_file_reader.ts b/packages/platform-core/src/lock-file-reader/yarn_classic_lock_file_reader.ts index c3a6c87d1a..d01f9ecc7b 100644 --- a/packages/platform-core/src/lock-file-reader/yarn_classic_lock_file_reader.ts +++ b/packages/platform-core/src/lock-file-reader/yarn_classic_lock_file_reader.ts @@ -6,25 +6,14 @@ import { LockFileContents, LockFileReader, } from './lock_file_reader_factory'; -import { AmplifyUserError } from '../errors'; /** * YarnClassicLockFileReader is an abstraction around the logic used to read and parse lock file contents */ export class YarnClassicLockFileReader implements LockFileReader { getLockFileContentsFromCwd = async (): Promise => { - const yarnLockPath = path.resolve(process.cwd(), 'yarn.lock'); - - try { - await fsp.access(yarnLockPath); - } catch (error) { - throw new AmplifyUserError('InvalidPackageLockJsonError', { - message: `Could not find a yarn.lock file at ${yarnLockPath}`, - resolution: `Ensure that ${yarnLockPath} exists and is a valid yarn.lock file`, - }); - } - const dependencies: Dependencies = []; + const yarnLockPath = path.resolve(process.cwd(), 'yarn.lock'); try { const yarnLockContents = await fsp.readFile(yarnLockPath, 'utf-8'); @@ -46,14 +35,8 @@ export class YarnClassicLockFileReader implements LockFileReader { dependencies.push({ name: dependencyName, version: dependencyVersion }); } } catch (error) { - throw new AmplifyUserError( - 'InvalidPackageLockJsonError', - { - message: `Could not parse the contents of ${yarnLockPath}`, - resolution: `Ensure that ${yarnLockPath} exists and is a valid yarn.lock file`, - }, - error as Error - ); + // We failed to get lock file contents either because file doesn't exist or it is not parse-able + return { dependencies }; } return { dependencies }; diff --git a/packages/platform-core/src/lock-file-reader/yarn_modern_lock_file_reader.test.ts b/packages/platform-core/src/lock-file-reader/yarn_modern_lock_file_reader.test.ts index cf00d43fd8..e7c575b30f 100644 --- a/packages/platform-core/src/lock-file-reader/yarn_modern_lock_file_reader.test.ts +++ b/packages/platform-core/src/lock-file-reader/yarn_modern_lock_file_reader.test.ts @@ -5,7 +5,6 @@ import path from 'path'; import { YarnModernLockFileReader } from './yarn_modern_lock_file_reader'; void describe('YarnModernLockFileReader', () => { - const fspAccessMock = mock.method(fsp, 'access', () => true); const fspReadFileMock = mock.method( fsp, 'readFile', @@ -58,37 +57,18 @@ __metadata: }; assert.deepEqual(lockFileContents, expectedLockFileContents); assert.strictEqual( - fspAccessMock.mock.calls[0].arguments[0], + fspReadFileMock.mock.calls[0].arguments[0], path.resolve(process.cwd(), 'yarn.lock') ); assert.strictEqual(fspReadFileMock.mock.callCount(), 1); }); - void it('throws when yarn.lock is not present', async () => { - fspAccessMock.mock.mockImplementationOnce(() => - Promise.reject(new Error()) - ); - await assert.rejects( - () => yarnModernLockFileReader.getLockFileContentsFromCwd(), - (error: Error) => { - assert.ok(error.message.startsWith('Could not find a yarn.lock file')); - return true; - } - ); - assert.strictEqual(fspReadFileMock.mock.callCount(), 0); - }); - - void it('throws when yarn.lock is not parse-able', async () => { + void it('returns empty lock file contents when yarn.lock is not present or parse-able', async () => { fspReadFileMock.mock.mockImplementationOnce(() => Promise.reject(new Error()) ); - await assert.rejects( - () => yarnModernLockFileReader.getLockFileContentsFromCwd(), - (error: Error) => { - assert.ok(error.message.startsWith('Could not parse the contents')); - return true; - } - ); - assert.strictEqual(fspReadFileMock.mock.callCount(), 1); + const lockFileContents = + await yarnModernLockFileReader.getLockFileContentsFromCwd(); + assert.deepEqual(lockFileContents, { dependencies: [] }); }); }); diff --git a/packages/platform-core/src/lock-file-reader/yarn_modern_lock_file_reader.ts b/packages/platform-core/src/lock-file-reader/yarn_modern_lock_file_reader.ts index aa0bd5f859..b5082b1bb3 100644 --- a/packages/platform-core/src/lock-file-reader/yarn_modern_lock_file_reader.ts +++ b/packages/platform-core/src/lock-file-reader/yarn_modern_lock_file_reader.ts @@ -6,25 +6,14 @@ import { LockFileContents, LockFileReader, } from './lock_file_reader_factory'; -import { AmplifyUserError } from '../errors'; /** * YarnModernLockFileReader is an abstraction around the logic used to read and parse lock file contents */ export class YarnModernLockFileReader implements LockFileReader { getLockFileContentsFromCwd = async (): Promise => { - const yarnLockPath = path.resolve(process.cwd(), 'yarn.lock'); - - try { - await fsp.access(yarnLockPath); - } catch (error) { - throw new AmplifyUserError('InvalidPackageLockJsonError', { - message: `Could not find a yarn.lock file at ${yarnLockPath}`, - resolution: `Ensure that ${yarnLockPath} exists and is a valid yarn.lock file`, - }); - } - const dependencies: Dependencies = []; + const yarnLockPath = path.resolve(process.cwd(), 'yarn.lock'); try { const yarnLockContents = await fsp.readFile(yarnLockPath, 'utf-8'); @@ -46,14 +35,8 @@ export class YarnModernLockFileReader implements LockFileReader { dependencies.push({ name: dependencyName, version: dependencyVersion }); } } catch (error) { - throw new AmplifyUserError( - 'InvalidPackageLockJsonError', - { - message: `Could not parse the contents of ${yarnLockPath}`, - resolution: `Ensure that ${yarnLockPath} exists and is a valid yarn.lock file`, - }, - error as Error - ); + // We failed to get lock file contents either because file doesn't exist or it is not parse-able + return { dependencies }; } return { dependencies }; diff --git a/packages/platform-core/src/usage-data/dependency_version_fetcher.test.ts b/packages/platform-core/src/usage-data/dependency_version_fetcher.test.ts new file mode 100644 index 0000000000..2764b20326 --- /dev/null +++ b/packages/platform-core/src/usage-data/dependency_version_fetcher.test.ts @@ -0,0 +1,75 @@ +import assert from 'assert'; +import { describe, it } from 'node:test'; +import { DependencyVersionFetcher } from './dependency_version_fetcher'; +import { LockFileReader } from '../lock-file-reader/lock_file_reader_factory'; + +void describe('getDependencyVersions', () => { + void it('successfully returns dependency versions', async () => { + const lockFileReaderMock = { + getLockFileContentsFromCwd: async () => + Promise.resolve({ + dependencies: [ + { + name: 'aws-cdk', + version: '1.2.3', + }, + { + name: 'aws-cdk-lib', + version: '12.13.14', + }, + { + name: 'test_dep', + version: '1.23.45', + }, + { + name: 'some_other_dep', + version: '12.1.3', + }, + ], + }), + } as LockFileReader; + const dependencyVersionFetcher = new DependencyVersionFetcher( + lockFileReaderMock + ); + const dependencyVersions = + await dependencyVersionFetcher.getDependencyVersions(); + const expectedVersions = [ + { + name: 'aws-cdk', + version: '1.2.3', + }, + { + name: 'aws-cdk-lib', + version: '12.13.14', + }, + ]; + + assert.deepEqual(dependencyVersions, expectedVersions); + }); + + void it('returns empty array if there are no matching dependencies', async () => { + const lockFileReaderMock = { + getLockFileContentsFromCwd: async () => + Promise.resolve({ + dependencies: [ + { + name: 'test_dep', + version: '1.23.45', + }, + { + name: 'some_other_dep', + version: '12.1.3', + }, + ], + }), + } as LockFileReader; + + const dependencyVersionFetcher = new DependencyVersionFetcher( + lockFileReaderMock + ); + const dependencyVersions = + await dependencyVersionFetcher.getDependencyVersions(); + + assert.deepEqual(dependencyVersions, []); + }); +}); diff --git a/packages/platform-core/src/usage-data/dependency_version_fetcher.ts b/packages/platform-core/src/usage-data/dependency_version_fetcher.ts new file mode 100644 index 0000000000..6d47e19e04 --- /dev/null +++ b/packages/platform-core/src/usage-data/dependency_version_fetcher.ts @@ -0,0 +1,33 @@ +import { + Dependencies, + LockFileReader, + LockFileReaderFactory, +} from '../lock-file-reader/lock_file_reader_factory'; + +/** + * Get dependency versions from customer project's lock file + */ +export class DependencyVersionFetcher { + /** + * Creates dependency version fetcher + */ + constructor( + private readonly lockFileReader: LockFileReader = new LockFileReaderFactory().getLockFileReader() + ) {} + + getDependencyVersions = async () => { + const lockFileContents = + await this.lockFileReader.getLockFileContentsFromCwd(); + const targetDependencies = ['aws-cdk', 'aws-cdk-lib']; + + const dependencyVersions: Dependencies = []; + + for (const { name, version } of lockFileContents.dependencies) { + if (targetDependencies.includes(name)) { + dependencyVersions.push({ name, version }); + } + } + + return dependencyVersions; + }; +} diff --git a/packages/platform-core/src/usage-data/get_dependency_versions.test.ts b/packages/platform-core/src/usage-data/get_dependency_versions.test.ts deleted file mode 100644 index fb726664b4..0000000000 --- a/packages/platform-core/src/usage-data/get_dependency_versions.test.ts +++ /dev/null @@ -1,61 +0,0 @@ -import assert from 'assert'; -import { describe, it } from 'node:test'; -import { getDependencyVersions } from './get_dependency_versions'; -import { LockFileContents } from '../lock-file-reader/lock_file_reader_factory'; - -void describe('getDependencyVersions', () => { - void it('successfully returns dependency versions', () => { - const lockFileContents: LockFileContents = { - dependencies: [ - { - name: 'aws-cdk', - version: '1.2.3', - }, - { - name: 'aws-cdk-lib', - version: '12.13.14', - }, - { - name: 'test_dep', - version: '1.23.45', - }, - { - name: 'some_other_dep', - version: '12.1.3', - }, - ], - }; - - const dependencyVersions = getDependencyVersions(lockFileContents); - const expectedVersions = [ - { - name: 'aws-cdk', - version: '1.2.3', - }, - { - name: 'aws-cdk-lib', - version: '12.13.14', - }, - ]; - - assert.deepEqual(dependencyVersions, expectedVersions); - }); - - void it('returns empty array if there are no matching dependencies', () => { - const lockFileContents: LockFileContents = { - dependencies: [ - { - name: 'test_dep', - version: '1.23.45', - }, - { - name: 'some_other_dep', - version: '12.1.3', - }, - ], - }; - - const dependencyVersions = getDependencyVersions(lockFileContents); - assert.deepEqual(dependencyVersions, []); - }); -}); diff --git a/packages/platform-core/src/usage-data/get_dependency_versions.ts b/packages/platform-core/src/usage-data/get_dependency_versions.ts deleted file mode 100644 index 6f3bb380b7..0000000000 --- a/packages/platform-core/src/usage-data/get_dependency_versions.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { - Dependencies, - LockFileContents, -} from '../lock-file-reader/lock_file_reader_factory'; - -/** - * Get dependency versions from customer project's lock file - */ -export const getDependencyVersions = (lockFileContents: LockFileContents) => { - const targetDependencies = ['aws-cdk', 'aws-cdk-lib']; - - const dependencyVersions: Dependencies = []; - - for (const { name, version } of lockFileContents.dependencies) { - if (targetDependencies.includes(name)) { - dependencyVersions.push({ name, version }); - } - } - - return dependencyVersions; -}; diff --git a/packages/platform-core/src/usage-data/usage_data_emitter.test.ts b/packages/platform-core/src/usage-data/usage_data_emitter.test.ts index 1e08b86e6c..730ef161a6 100644 --- a/packages/platform-core/src/usage-data/usage_data_emitter.test.ts +++ b/packages/platform-core/src/usage-data/usage_data_emitter.test.ts @@ -12,6 +12,7 @@ import { UsageData } from './usage_data'; import isCI from 'is-ci'; import { AmplifyError, AmplifyUserError } from '..'; import { LockFileReader } from '../lock-file-reader/lock_file_reader_factory'; +import { DependencyVersionFetcher } from './dependency_version_fetcher'; const originalNpmUserAgent = process.env.npm_config_user_agent; const testNpmUserAgent = 'testNpmUserAgent'; @@ -41,7 +42,7 @@ void describe('UsageDataEmitter', () => { fetch: async () => '123456789012', } as AccountIdFetcher; - // For LockFileReader + // For DependencyVersionFetcher const lockFileReaderMock = { getLockFileContentsFromCwd: async () => Promise.resolve({ @@ -57,6 +58,9 @@ void describe('UsageDataEmitter', () => { ], }), } as LockFileReader; + const dependencyVersionFetcherMock = new DependencyVersionFetcher( + lockFileReaderMock + ); mock.method(https, 'request', () => reqMock); @@ -176,7 +180,7 @@ void describe('UsageDataEmitter', () => { v4(), testURL, accountIdFetcherMock, - lockFileReaderMock + dependencyVersionFetcherMock ); let usageDataEmitterPromise; diff --git a/packages/platform-core/src/usage-data/usage_data_emitter.ts b/packages/platform-core/src/usage-data/usage_data_emitter.ts index 40109bfd52..92b871b8ef 100644 --- a/packages/platform-core/src/usage-data/usage_data_emitter.ts +++ b/packages/platform-core/src/usage-data/usage_data_emitter.ts @@ -10,8 +10,7 @@ import isCI from 'is-ci'; import { SerializableError } from './serializable_error.js'; import { UsageDataEmitter } from './usage_data_emitter_factory.js'; import { AmplifyError } from '../index.js'; -import { getDependencyVersions } from './get_dependency_versions.js'; -import { LockFileReaderFactory } from '../lock-file-reader/lock_file_reader_factory.js'; +import { DependencyVersionFetcher } from './dependency_version_fetcher.js'; /** * Entry point for sending usage data metrics @@ -25,7 +24,7 @@ export class DefaultUsageDataEmitter implements UsageDataEmitter { private readonly sessionUuid = uuid(), private readonly url = getUrl(), private readonly accountIdFetcher = new AccountIdFetcher(), - private readonly lockFileReader = new LockFileReaderFactory().getLockFileReader() + private readonly dependencyVersionFetcher = new DependencyVersionFetcher() ) {} emitSuccess = async ( @@ -68,9 +67,6 @@ export class DefaultUsageDataEmitter implements UsageDataEmitter { dimensions?: Record; error?: AmplifyError; }): Promise => { - const lockFileContents = - await this.lockFileReader.getLockFileContentsFromCwd(); - const dependencyVersions = getDependencyVersions(lockFileContents); return { accountId: await this.accountIdFetcher.fetch(), sessionUuid: this.sessionUuid, @@ -94,7 +90,9 @@ export class DefaultUsageDataEmitter implements UsageDataEmitter { isCi: isCI, projectSetting: { editor: process.env.npm_config_user_agent, - details: JSON.stringify(dependencyVersions), + details: JSON.stringify( + await this.dependencyVersionFetcher.getDependencyVersions() + ), }, }; }; From f8c8243ce9a50ec7ddfd9244fa604caec04293da Mon Sep 17 00:00:00 2001 From: Roshane Pascual Date: Wed, 8 Jan 2025 12:20:49 -0800 Subject: [PATCH 3/8] update lock file reader factory --- .../lock_file_reader_factory.test.ts | 19 +++---------------- .../lock_file_reader_factory.ts | 17 ++--------------- 2 files changed, 5 insertions(+), 31 deletions(-) diff --git a/packages/platform-core/src/lock-file-reader/lock_file_reader_factory.test.ts b/packages/platform-core/src/lock-file-reader/lock_file_reader_factory.test.ts index 197480cafb..302f1bf6a8 100644 --- a/packages/platform-core/src/lock-file-reader/lock_file_reader_factory.test.ts +++ b/packages/platform-core/src/lock-file-reader/lock_file_reader_factory.test.ts @@ -49,24 +49,11 @@ void describe('LockFileReaderFactory', () => { }); } - void it('should throw an error for unsupported package managers', () => { + void it('defaults to npm lock file reader', () => { process.env.npm_config_user_agent = 'unsupported/1.0.0 node/v15.0.0 darwin x64'; - assert.throws(() => new LockFileReaderFactory().getLockFileReader(), { - message: 'Package Manager unsupported is not supported.', - }); - }); - - void it('should throw an error for pnpm on Windows', () => { - process.env.npm_config_user_agent = 'pnpm/1.0.0 node/v15.0.0 darwin x64'; - assert.throws( - () => new LockFileReaderFactory('win32').getLockFileReader(), - { - message: 'Amplify does not support PNPM on Windows.', - details: - 'Details: https://github.com/aws-amplify/amplify-backend/blob/main/packages/create-amplify/README.md', - } - ); + const lockFileReader = new LockFileReaderFactory().getLockFileReader(); + assert.ok(lockFileReader instanceof NpmLockFileReader); }); }); }); diff --git a/packages/platform-core/src/lock-file-reader/lock_file_reader_factory.ts b/packages/platform-core/src/lock-file-reader/lock_file_reader_factory.ts index 0601ec7362..c3957737fb 100644 --- a/packages/platform-core/src/lock-file-reader/lock_file_reader_factory.ts +++ b/packages/platform-core/src/lock-file-reader/lock_file_reader_factory.ts @@ -1,4 +1,3 @@ -import { AmplifyUserError } from '../errors'; import { NpmLockFileReader } from './npm_lock_file_reader'; import { PnpmLockFileReader } from './pnpm_lock_file_reader'; import { YarnClassicLockFileReader } from './yarn_classic_lock_file_reader'; @@ -22,26 +21,14 @@ export class LockFileReaderFactory { case 'npm': return new NpmLockFileReader(); case 'pnpm': - if (this.platform === 'win32') { - const message = 'Amplify does not support PNPM on Windows.'; - const details = - 'Details: https://github.com/aws-amplify/amplify-backend/blob/main/packages/create-amplify/README.md'; - throw new AmplifyUserError('UnsupportedPackageManagerError', { - message, - details, - resolution: 'Use a supported package manager for your OS', - }); - } return new PnpmLockFileReader(); case 'yarn-classic': return new YarnClassicLockFileReader(); case 'yarn-modern': return new YarnModernLockFileReader(); default: - throw new AmplifyUserError('UnsupportedPackageManagerError', { - message: `Package Manager ${packageManagerName} is not supported.`, - resolution: 'Use npm, yarn or pnpm.', - }); + // defaults to npm lock file reader as it is the most used package manager by customers + return new NpmLockFileReader(); } } From 9c155ac85f0e2d5cf7b555dae70f9738d46ba6f8 Mon Sep 17 00:00:00 2001 From: Roshane Pascual Date: Wed, 8 Jan 2025 13:38:51 -0800 Subject: [PATCH 4/8] try this --- .../src/lock-file-reader/pnpm_lock_file_reader.ts | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/packages/platform-core/src/lock-file-reader/pnpm_lock_file_reader.ts b/packages/platform-core/src/lock-file-reader/pnpm_lock_file_reader.ts index 0323bc58dc..f637d48595 100644 --- a/packages/platform-core/src/lock-file-reader/pnpm_lock_file_reader.ts +++ b/packages/platform-core/src/lock-file-reader/pnpm_lock_file_reader.ts @@ -1,5 +1,4 @@ import fsp from 'fs/promises'; -import { EOL } from 'os'; import path from 'path'; import { Dependencies, @@ -12,12 +11,15 @@ import { */ export class PnpmLockFileReader implements LockFileReader { getLockFileContentsFromCwd = async (): Promise => { + const eolRegex = '[\r\n]'; const dependencies: Dependencies = []; const pnpmLockPath = path.resolve(process.cwd(), 'pnpm-lock.yaml'); try { const pnpmLockContents = await fsp.readFile(pnpmLockPath, 'utf-8'); - const pnpmLockContentsArray = pnpmLockContents.split(EOL + EOL); + const pnpmLockContentsArray = pnpmLockContents.split( + new RegExp(`${eolRegex}${eolRegex}`) + ); const startOfPackagesIndex = pnpmLockContentsArray.indexOf('packages:'); const pnpmLockPackages = pnpmLockContentsArray.slice( @@ -28,7 +30,7 @@ export class PnpmLockFileReader implements LockFileReader { // Get line that contains dependency name and version and remove quotes and colon const pnpmDependencyLine = pnpmDependencyBlock .trim() - .split(EOL)[0] + .split(new RegExp(eolRegex))[0] .replaceAll(/[':]/g, ''); const dependencyName = pnpmDependencyLine.slice( 0, From 6bef61cf477c6b2e82fccd143c0498aa7afc2922 Mon Sep 17 00:00:00 2001 From: Roshane Pascual Date: Wed, 8 Jan 2025 14:06:05 -0800 Subject: [PATCH 5/8] update eol for windows --- .../lock-file-reader/yarn_classic_lock_file_reader.ts | 10 +++++++--- .../lock-file-reader/yarn_modern_lock_file_reader.ts | 10 +++++++--- 2 files changed, 14 insertions(+), 6 deletions(-) diff --git a/packages/platform-core/src/lock-file-reader/yarn_classic_lock_file_reader.ts b/packages/platform-core/src/lock-file-reader/yarn_classic_lock_file_reader.ts index d01f9ecc7b..462640f58a 100644 --- a/packages/platform-core/src/lock-file-reader/yarn_classic_lock_file_reader.ts +++ b/packages/platform-core/src/lock-file-reader/yarn_classic_lock_file_reader.ts @@ -1,5 +1,4 @@ import fsp from 'fs/promises'; -import { EOL } from 'os'; import path from 'path'; import { Dependencies, @@ -12,16 +11,21 @@ import { */ export class YarnClassicLockFileReader implements LockFileReader { getLockFileContentsFromCwd = async (): Promise => { + const eolRegex = '[\r\n]'; const dependencies: Dependencies = []; const yarnLockPath = path.resolve(process.cwd(), 'yarn.lock'); try { const yarnLockContents = await fsp.readFile(yarnLockPath, 'utf-8'); - const yarnLockContentsArray = yarnLockContents.split(EOL + EOL); + const yarnLockContentsArray = yarnLockContents.split( + new RegExp(`${eolRegex}${eolRegex}`) + ); // Slice to remove comment block at the start of the lock file for (const yarnDependencyBlock of yarnLockContentsArray.slice(1)) { - const yarnDependencyLines = yarnDependencyBlock.trim().split(EOL); + const yarnDependencyLines = yarnDependencyBlock + .trim() + .split(new RegExp(eolRegex)); const yarnDependencyName = yarnDependencyLines[0]; const yarnDependencyVersion = yarnDependencyLines[1]; diff --git a/packages/platform-core/src/lock-file-reader/yarn_modern_lock_file_reader.ts b/packages/platform-core/src/lock-file-reader/yarn_modern_lock_file_reader.ts index b5082b1bb3..e3e78ad2e8 100644 --- a/packages/platform-core/src/lock-file-reader/yarn_modern_lock_file_reader.ts +++ b/packages/platform-core/src/lock-file-reader/yarn_modern_lock_file_reader.ts @@ -1,5 +1,4 @@ import fsp from 'fs/promises'; -import { EOL } from 'os'; import path from 'path'; import { Dependencies, @@ -12,16 +11,21 @@ import { */ export class YarnModernLockFileReader implements LockFileReader { getLockFileContentsFromCwd = async (): Promise => { + const eolRegex = '[\r\n]'; const dependencies: Dependencies = []; const yarnLockPath = path.resolve(process.cwd(), 'yarn.lock'); try { const yarnLockContents = await fsp.readFile(yarnLockPath, 'utf-8'); - const yarnLockContentsArray = yarnLockContents.split(EOL + EOL); + const yarnLockContentsArray = yarnLockContents.split( + new RegExp(`${eolRegex}${eolRegex}`) + ); // Slice to remove comment block and metadata at the start of the lock file for (const yarnDependencyBlock of yarnLockContentsArray.slice(2)) { - const yarnDependencyLines = yarnDependencyBlock.trim().split(EOL); + const yarnDependencyLines = yarnDependencyBlock + .trim() + .split(new RegExp(eolRegex)); const yarnDependencyName = yarnDependencyLines[0]; const yarnDependencyVersion = yarnDependencyLines[1]; From fffb8b3345830adf5496c57c2841a868b079f0d4 Mon Sep 17 00:00:00 2001 From: Roshane Pascual Date: Wed, 8 Jan 2025 18:23:12 -0800 Subject: [PATCH 6/8] refactor lock file readers --- .changeset/silver-tables-do.md | 7 +- .eslint_dictionary.json | 1 + .../backend-deployer/src/cdk_deployer.test.ts | 1 + packages/cli-core/package.json | 3 +- .../npm_lock_file_reader.test.ts | 2 +- .../lock-file-reader/npm_lock_file_reader.ts | 19 ++-- .../pnpm_lock_file_reader.test.ts | 3 +- .../lock-file-reader/pnpm_lock_file_reader.ts | 17 ++-- .../yarn_classic_lock_file_reader.test.ts | 2 +- .../yarn_classic_lock_file_reader.ts | 17 ++-- .../yarn_modern_lock_file_reader.test.ts | 2 +- .../yarn_modern_lock_file_reader.ts | 17 ++-- .../npm_package_manager_controller.test.ts | 85 ++++++++++++++++++ .../npm_package_manager_controller.ts | 5 +- .../package_manager_controller_base.ts | 22 +++++ .../pnpm_package_manager_controller.test.ts | 85 ++++++++++++++++++ .../pnpm_package_manager_controller.ts | 5 +- ...classic_package_manager_controller.test.ts | 87 ++++++++++++++++++ ...yarn_classic_package_manager_controller.ts | 5 +- ..._modern_package_manager_controller.test.ts | 89 +++++++++++++++++++ .../yarn_modern_package_manager_controller.ts | 5 +- packages/cli/src/ampx.ts | 9 +- .../sandbox/sandbox_command_factory.ts | 16 +++- .../src/amplify_project_creator.test.ts | 1 + .../initial_project_file_generator.test.ts | 1 + packages/platform-core/API.md | 3 +- .../lock_file_reader_factory.test.ts | 59 ------------ .../lock_file_reader_factory.ts | 63 ------------- .../dependency_version_fetcher.test.ts | 75 ---------------- .../usage-data/dependency_version_fetcher.ts | 33 ------- .../src/usage-data/usage_data_emitter.test.ts | 36 +++----- .../src/usage-data/usage_data_emitter.ts | 10 +-- .../usage_data_emitter_factory.test.ts | 9 +- .../usage-data/usage_data_emitter_factory.ts | 8 +- packages/plugin-types/API.md | 17 ++++ .../src/package_manager_controller.ts | 11 +++ 36 files changed, 524 insertions(+), 306 deletions(-) rename packages/{platform-core/src => cli-core/src/package-manager-controller}/lock-file-reader/npm_lock_file_reader.test.ts (96%) rename packages/{platform-core/src => cli-core/src/package-manager-controller}/lock-file-reader/npm_lock_file_reader.ts (80%) rename packages/{platform-core/src => cli-core/src/package-manager-controller}/lock-file-reader/pnpm_lock_file_reader.test.ts (96%) rename packages/{platform-core/src => cli-core/src/package-manager-controller}/lock-file-reader/pnpm_lock_file_reader.ts (81%) rename packages/{platform-core/src => cli-core/src/package-manager-controller}/lock-file-reader/yarn_classic_lock_file_reader.test.ts (99%) rename packages/{platform-core/src => cli-core/src/package-manager-controller}/lock-file-reader/yarn_classic_lock_file_reader.ts (81%) rename packages/{platform-core/src => cli-core/src/package-manager-controller}/lock-file-reader/yarn_modern_lock_file_reader.test.ts (99%) rename packages/{platform-core/src => cli-core/src/package-manager-controller}/lock-file-reader/yarn_modern_lock_file_reader.ts (81%) delete mode 100644 packages/platform-core/src/lock-file-reader/lock_file_reader_factory.test.ts delete mode 100644 packages/platform-core/src/lock-file-reader/lock_file_reader_factory.ts delete mode 100644 packages/platform-core/src/usage-data/dependency_version_fetcher.test.ts delete mode 100644 packages/platform-core/src/usage-data/dependency_version_fetcher.ts diff --git a/.changeset/silver-tables-do.md b/.changeset/silver-tables-do.md index eaa6ac2d01..8029abf5cf 100644 --- a/.changeset/silver-tables-do.md +++ b/.changeset/silver-tables-do.md @@ -1,5 +1,10 @@ --- -'@aws-amplify/platform-core': patch +'@aws-amplify/backend-deployer': patch +'create-amplify': patch +'@aws-amplify/backend-cli': patch +'@aws-amplify/cli-core': patch +'@aws-amplify/platform-core': minor +'@aws-amplify/plugin-types': minor --- Report cdk versions diff --git a/.eslint_dictionary.json b/.eslint_dictionary.json index fd59598318..c207869a7d 100644 --- a/.eslint_dictionary.json +++ b/.eslint_dictionary.json @@ -100,6 +100,7 @@ "lang", "linux", "localhost", + "lockfile", "lsof", "lstat", "macos", diff --git a/packages/backend-deployer/src/cdk_deployer.test.ts b/packages/backend-deployer/src/cdk_deployer.test.ts index 24981682a4..39af9db733 100644 --- a/packages/backend-deployer/src/cdk_deployer.test.ts +++ b/packages/backend-deployer/src/cdk_deployer.test.ts @@ -46,6 +46,7 @@ void describe('invokeCDKCommand', () => { runWithPackageManager: mock.fn(() => Promise.resolve() as never), getCommand: (args: string[]) => `'npx ${args.join(' ')}'`, allowsSignalPropagation: () => true, + getDependencies: mock.fn(() => Promise.resolve([])), }; const invoker = new CDKDeployer( diff --git a/packages/cli-core/package.json b/packages/cli-core/package.json index a735285a12..266bbd5932 100644 --- a/packages/cli-core/package.json +++ b/packages/cli-core/package.json @@ -22,6 +22,7 @@ "@aws-amplify/platform-core": "^1.3.0", "@inquirer/prompts": "^3.0.0", "execa": "^9.5.1", - "kleur": "^4.1.5" + "kleur": "^4.1.5", + "zod": "^3.22.2" } } diff --git a/packages/platform-core/src/lock-file-reader/npm_lock_file_reader.test.ts b/packages/cli-core/src/package-manager-controller/lock-file-reader/npm_lock_file_reader.test.ts similarity index 96% rename from packages/platform-core/src/lock-file-reader/npm_lock_file_reader.test.ts rename to packages/cli-core/src/package-manager-controller/lock-file-reader/npm_lock_file_reader.test.ts index 91fdbbf07b..fc1eadbec1 100644 --- a/packages/platform-core/src/lock-file-reader/npm_lock_file_reader.test.ts +++ b/packages/cli-core/src/package-manager-controller/lock-file-reader/npm_lock_file_reader.test.ts @@ -2,7 +2,7 @@ import assert from 'assert'; import fsp from 'fs/promises'; import { afterEach, describe, it, mock } from 'node:test'; import path from 'path'; -import { NpmLockFileReader } from './npm_lock_file_reader'; +import { NpmLockFileReader } from './npm_lock_file_reader.js'; void describe('NpmLockFileReader', () => { const fspReadFileMock = mock.method(fsp, 'readFile', () => diff --git a/packages/platform-core/src/lock-file-reader/npm_lock_file_reader.ts b/packages/cli-core/src/package-manager-controller/lock-file-reader/npm_lock_file_reader.ts similarity index 80% rename from packages/platform-core/src/lock-file-reader/npm_lock_file_reader.ts rename to packages/cli-core/src/package-manager-controller/lock-file-reader/npm_lock_file_reader.ts index fa3e56aa8f..4aa3c81143 100644 --- a/packages/platform-core/src/lock-file-reader/npm_lock_file_reader.ts +++ b/packages/cli-core/src/package-manager-controller/lock-file-reader/npm_lock_file_reader.ts @@ -1,18 +1,20 @@ -import fsp from 'fs/promises'; -import path from 'path'; -import z from 'zod'; import { - Dependencies, + Dependency, LockFileContents, LockFileReader, -} from './lock_file_reader_factory'; +} from '@aws-amplify/plugin-types'; +import fsp from 'fs/promises'; +import path from 'path'; +import z from 'zod'; +import { printer } from '../../printer.js'; +import { LogLevel } from '../../printer/printer.js'; /** * NpmLockFileReader is an abstraction around the logic used to read and parse lock file contents */ export class NpmLockFileReader implements LockFileReader { getLockFileContentsFromCwd = async (): Promise => { - const dependencies: Dependencies = []; + const dependencies: Array = []; const packageLockJsonPath = path.resolve( process.cwd(), 'package-lock.json' @@ -23,7 +25,10 @@ export class NpmLockFileReader implements LockFileReader { const jsonLockContents = await fsp.readFile(packageLockJsonPath, 'utf-8'); jsonLockParsedValue = JSON.parse(jsonLockContents); } catch (error) { - // We failed to get lock file contents either because file doesn't exist or it is not parse-able + printer.log( + `Failed to get lock file contents because ${packageLockJsonPath} does not exist or is not parse-able`, + LogLevel.DEBUG + ); return { dependencies }; } diff --git a/packages/platform-core/src/lock-file-reader/pnpm_lock_file_reader.test.ts b/packages/cli-core/src/package-manager-controller/lock-file-reader/pnpm_lock_file_reader.test.ts similarity index 96% rename from packages/platform-core/src/lock-file-reader/pnpm_lock_file_reader.test.ts rename to packages/cli-core/src/package-manager-controller/lock-file-reader/pnpm_lock_file_reader.test.ts index 3fef3d88c3..2c99265ff4 100644 --- a/packages/platform-core/src/lock-file-reader/pnpm_lock_file_reader.test.ts +++ b/packages/cli-core/src/package-manager-controller/lock-file-reader/pnpm_lock_file_reader.test.ts @@ -2,13 +2,12 @@ import assert from 'assert'; import fsp from 'fs/promises'; import { afterEach, describe, it, mock } from 'node:test'; import path from 'path'; -import { PnpmLockFileReader } from './pnpm_lock_file_reader'; +import { PnpmLockFileReader } from './pnpm_lock_file_reader.js'; void describe('PnpmLockFileReader', () => { const fspReadFileMock = mock.method( fsp, 'readFile', - // eslint-disable-next-line spellcheck/spell-checker () => `lockfileVersion: '9.0' settings: diff --git a/packages/platform-core/src/lock-file-reader/pnpm_lock_file_reader.ts b/packages/cli-core/src/package-manager-controller/lock-file-reader/pnpm_lock_file_reader.ts similarity index 81% rename from packages/platform-core/src/lock-file-reader/pnpm_lock_file_reader.ts rename to packages/cli-core/src/package-manager-controller/lock-file-reader/pnpm_lock_file_reader.ts index f637d48595..86e1478881 100644 --- a/packages/platform-core/src/lock-file-reader/pnpm_lock_file_reader.ts +++ b/packages/cli-core/src/package-manager-controller/lock-file-reader/pnpm_lock_file_reader.ts @@ -1,10 +1,12 @@ -import fsp from 'fs/promises'; -import path from 'path'; import { - Dependencies, + Dependency, LockFileContents, LockFileReader, -} from './lock_file_reader_factory'; +} from '@aws-amplify/plugin-types'; +import fsp from 'fs/promises'; +import path from 'path'; +import { printer } from '../../printer.js'; +import { LogLevel } from '../../printer/printer.js'; /** * PnpmLockFileReader is an abstraction around the logic used to read and parse lock file contents @@ -12,7 +14,7 @@ import { export class PnpmLockFileReader implements LockFileReader { getLockFileContentsFromCwd = async (): Promise => { const eolRegex = '[\r\n]'; - const dependencies: Dependencies = []; + const dependencies: Array = []; const pnpmLockPath = path.resolve(process.cwd(), 'pnpm-lock.yaml'); try { @@ -43,7 +45,10 @@ export class PnpmLockFileReader implements LockFileReader { dependencies.push({ name: dependencyName, version: dependencyVersion }); } } catch (error) { - // We failed to get lock file contents either because file doesn't exist or it is not parse-able + printer.log( + `Failed to get lock file contents because ${pnpmLockPath} does not exist or is not parse-able`, + LogLevel.DEBUG + ); return { dependencies }; } diff --git a/packages/platform-core/src/lock-file-reader/yarn_classic_lock_file_reader.test.ts b/packages/cli-core/src/package-manager-controller/lock-file-reader/yarn_classic_lock_file_reader.test.ts similarity index 99% rename from packages/platform-core/src/lock-file-reader/yarn_classic_lock_file_reader.test.ts rename to packages/cli-core/src/package-manager-controller/lock-file-reader/yarn_classic_lock_file_reader.test.ts index 2e82d58807..fb3585106e 100644 --- a/packages/platform-core/src/lock-file-reader/yarn_classic_lock_file_reader.test.ts +++ b/packages/cli-core/src/package-manager-controller/lock-file-reader/yarn_classic_lock_file_reader.test.ts @@ -2,7 +2,7 @@ import assert from 'assert'; import fsp from 'fs/promises'; import { afterEach, describe, it, mock } from 'node:test'; import path from 'path'; -import { YarnClassicLockFileReader } from './yarn_classic_lock_file_reader'; +import { YarnClassicLockFileReader } from './yarn_classic_lock_file_reader.js'; void describe('YarnClassicLockFileReader', () => { const fspReadFileMock = mock.method( diff --git a/packages/platform-core/src/lock-file-reader/yarn_classic_lock_file_reader.ts b/packages/cli-core/src/package-manager-controller/lock-file-reader/yarn_classic_lock_file_reader.ts similarity index 81% rename from packages/platform-core/src/lock-file-reader/yarn_classic_lock_file_reader.ts rename to packages/cli-core/src/package-manager-controller/lock-file-reader/yarn_classic_lock_file_reader.ts index 462640f58a..8abc4779b1 100644 --- a/packages/platform-core/src/lock-file-reader/yarn_classic_lock_file_reader.ts +++ b/packages/cli-core/src/package-manager-controller/lock-file-reader/yarn_classic_lock_file_reader.ts @@ -1,10 +1,12 @@ -import fsp from 'fs/promises'; -import path from 'path'; import { - Dependencies, + Dependency, LockFileContents, LockFileReader, -} from './lock_file_reader_factory'; +} from '@aws-amplify/plugin-types'; +import fsp from 'fs/promises'; +import path from 'path'; +import { printer } from '../../printer.js'; +import { LogLevel } from '../../printer/printer.js'; /** * YarnClassicLockFileReader is an abstraction around the logic used to read and parse lock file contents @@ -12,7 +14,7 @@ import { export class YarnClassicLockFileReader implements LockFileReader { getLockFileContentsFromCwd = async (): Promise => { const eolRegex = '[\r\n]'; - const dependencies: Dependencies = []; + const dependencies: Array = []; const yarnLockPath = path.resolve(process.cwd(), 'yarn.lock'); try { @@ -39,7 +41,10 @@ export class YarnClassicLockFileReader implements LockFileReader { dependencies.push({ name: dependencyName, version: dependencyVersion }); } } catch (error) { - // We failed to get lock file contents either because file doesn't exist or it is not parse-able + printer.log( + `Failed to get lock file contents because ${yarnLockPath} does not exist or is not parse-able`, + LogLevel.DEBUG + ); return { dependencies }; } diff --git a/packages/platform-core/src/lock-file-reader/yarn_modern_lock_file_reader.test.ts b/packages/cli-core/src/package-manager-controller/lock-file-reader/yarn_modern_lock_file_reader.test.ts similarity index 99% rename from packages/platform-core/src/lock-file-reader/yarn_modern_lock_file_reader.test.ts rename to packages/cli-core/src/package-manager-controller/lock-file-reader/yarn_modern_lock_file_reader.test.ts index e7c575b30f..1b1a5213a5 100644 --- a/packages/platform-core/src/lock-file-reader/yarn_modern_lock_file_reader.test.ts +++ b/packages/cli-core/src/package-manager-controller/lock-file-reader/yarn_modern_lock_file_reader.test.ts @@ -2,7 +2,7 @@ import assert from 'assert'; import fsp from 'fs/promises'; import { afterEach, describe, it, mock } from 'node:test'; import path from 'path'; -import { YarnModernLockFileReader } from './yarn_modern_lock_file_reader'; +import { YarnModernLockFileReader } from './yarn_modern_lock_file_reader.js'; void describe('YarnModernLockFileReader', () => { const fspReadFileMock = mock.method( diff --git a/packages/platform-core/src/lock-file-reader/yarn_modern_lock_file_reader.ts b/packages/cli-core/src/package-manager-controller/lock-file-reader/yarn_modern_lock_file_reader.ts similarity index 81% rename from packages/platform-core/src/lock-file-reader/yarn_modern_lock_file_reader.ts rename to packages/cli-core/src/package-manager-controller/lock-file-reader/yarn_modern_lock_file_reader.ts index e3e78ad2e8..3c8423f5e9 100644 --- a/packages/platform-core/src/lock-file-reader/yarn_modern_lock_file_reader.ts +++ b/packages/cli-core/src/package-manager-controller/lock-file-reader/yarn_modern_lock_file_reader.ts @@ -1,10 +1,12 @@ -import fsp from 'fs/promises'; -import path from 'path'; import { - Dependencies, + Dependency, LockFileContents, LockFileReader, -} from './lock_file_reader_factory'; +} from '@aws-amplify/plugin-types'; +import fsp from 'fs/promises'; +import path from 'path'; +import { printer } from '../../printer.js'; +import { LogLevel } from '../../printer/printer.js'; /** * YarnModernLockFileReader is an abstraction around the logic used to read and parse lock file contents @@ -12,7 +14,7 @@ import { export class YarnModernLockFileReader implements LockFileReader { getLockFileContentsFromCwd = async (): Promise => { const eolRegex = '[\r\n]'; - const dependencies: Dependencies = []; + const dependencies: Array = []; const yarnLockPath = path.resolve(process.cwd(), 'yarn.lock'); try { @@ -39,7 +41,10 @@ export class YarnModernLockFileReader implements LockFileReader { dependencies.push({ name: dependencyName, version: dependencyVersion }); } } catch (error) { - // We failed to get lock file contents either because file doesn't exist or it is not parse-able + printer.log( + `Failed to get lock file contents because ${yarnLockPath} does not exist or is not parse-able`, + LogLevel.DEBUG + ); return { dependencies }; } diff --git a/packages/cli-core/src/package-manager-controller/npm_package_manager_controller.test.ts b/packages/cli-core/src/package-manager-controller/npm_package_manager_controller.test.ts index 938b1c580d..072bd1c14c 100644 --- a/packages/cli-core/src/package-manager-controller/npm_package_manager_controller.test.ts +++ b/packages/cli-core/src/package-manager-controller/npm_package_manager_controller.test.ts @@ -5,6 +5,7 @@ import assert from 'assert'; import { execa } from 'execa'; import { NpmPackageManagerController } from './npm_package_manager_controller.js'; import { executeWithDebugLogger } from './execute_with_debugger_logger.js'; +import { LockFileReader } from '@aws-amplify/plugin-types'; void describe('NpmPackageManagerController', () => { const fspMock = { @@ -124,4 +125,88 @@ void describe('NpmPackageManagerController', () => { assert.equal(fspMock.writeFile.mock.callCount(), 1); }); }); + + void describe('getDependencies', () => { + const existsSyncMock = mock.fn(() => true); + + void it('successfully returns dependency versions', async () => { + const lockFileReaderMock = { + getLockFileContentsFromCwd: async () => + Promise.resolve({ + dependencies: [ + { + name: 'aws-cdk', + version: '1.2.3', + }, + { + name: 'aws-cdk-lib', + version: '12.13.14', + }, + { + name: 'test_dep', + version: '1.23.45', + }, + { + name: 'some_other_dep', + version: '12.1.3', + }, + ], + }), + } as LockFileReader; + const npmPackageManagerController = new NpmPackageManagerController( + '/testProjectRoot', + fspMock as unknown as typeof fsp, + pathMock as unknown as typeof path, + execaMock as unknown as typeof execa, + executeWithDebugLoggerMock as unknown as typeof executeWithDebugLogger, + existsSyncMock, + lockFileReaderMock + ); + const dependencyVersions = + await npmPackageManagerController.getDependencies(); + const expectedVersions = [ + { + name: 'aws-cdk', + version: '1.2.3', + }, + { + name: 'aws-cdk-lib', + version: '12.13.14', + }, + ]; + + assert.deepEqual(dependencyVersions, expectedVersions); + }); + + void it('returns empty array if there are no matching dependencies', async () => { + const lockFileReaderMock = { + getLockFileContentsFromCwd: async () => + Promise.resolve({ + dependencies: [ + { + name: 'test_dep', + version: '1.23.45', + }, + { + name: 'some_other_dep', + version: '12.1.3', + }, + ], + }), + } as LockFileReader; + const npmPackageManagerController = new NpmPackageManagerController( + '/testProjectRoot', + fspMock as unknown as typeof fsp, + pathMock as unknown as typeof path, + execaMock as unknown as typeof execa, + executeWithDebugLoggerMock as unknown as typeof executeWithDebugLogger, + existsSyncMock, + lockFileReaderMock + ); + const dependencyVersions = + await npmPackageManagerController.getDependencies(); + + assert.deepEqual(dependencyVersions, []); + }); + }); }); diff --git a/packages/cli-core/src/package-manager-controller/npm_package_manager_controller.ts b/packages/cli-core/src/package-manager-controller/npm_package_manager_controller.ts index f680458be2..0277fbf741 100644 --- a/packages/cli-core/src/package-manager-controller/npm_package_manager_controller.ts +++ b/packages/cli-core/src/package-manager-controller/npm_package_manager_controller.ts @@ -4,6 +4,7 @@ import { execa as _execa } from 'execa'; import * as _path from 'path'; import { executeWithDebugLogger as _executeWithDebugLogger } from './execute_with_debugger_logger.js'; import { PackageManagerControllerBase } from './package_manager_controller_base.js'; +import { NpmLockFileReader } from './lock-file-reader/npm_lock_file_reader.js'; /** * NpmPackageManagerController is an abstraction around npm commands that are needed to initialize a project and install dependencies @@ -18,13 +19,15 @@ export class NpmPackageManagerController extends PackageManagerControllerBase { protected readonly path = _path, protected readonly execa = _execa, protected readonly executeWithDebugLogger = _executeWithDebugLogger, - protected readonly existsSync = _existsSync + protected readonly existsSync = _existsSync, + protected readonly lockFileReader = new NpmLockFileReader() ) { super( cwd, 'npm', ['init', '--yes'], 'install', + lockFileReader, fsp, path, execa, diff --git a/packages/cli-core/src/package-manager-controller/package_manager_controller_base.ts b/packages/cli-core/src/package-manager-controller/package_manager_controller_base.ts index 642c8a54bf..8946fec30e 100644 --- a/packages/cli-core/src/package-manager-controller/package_manager_controller_base.ts +++ b/packages/cli-core/src/package-manager-controller/package_manager_controller_base.ts @@ -3,8 +3,10 @@ import _fsp from 'fs/promises'; import { execa as _execa } from 'execa'; import * as _path from 'path'; import { + Dependency, ExecaChildProcess, ExecaOptions, + LockFileReader, type PackageManagerController, } from '@aws-amplify/plugin-types'; import { LogLevel } from '../printer/printer.js'; @@ -27,6 +29,7 @@ export abstract class PackageManagerControllerBase protected readonly executable: string, protected readonly initDefault: string[], protected readonly installCommand: string, + protected readonly lockFileReader: LockFileReader, protected readonly fsp = _fsp, protected readonly path = _path, protected readonly execa = _execa, @@ -145,6 +148,25 @@ export abstract class PackageManagerControllerBase */ allowsSignalPropagation = () => true; + /** + * getDependencies - Retrieves dependency versions from the lock file in the project root + */ + getDependencies = async (): Promise> => { + const lockFileContents = + await this.lockFileReader.getLockFileContentsFromCwd(); + const targetDependencies = ['aws-cdk', 'aws-cdk-lib']; + + const dependencyVersions: Array = []; + + for (const { name, version } of lockFileContents.dependencies) { + if (targetDependencies.includes(name)) { + dependencyVersions.push({ name, version }); + } + } + + return dependencyVersions; + }; + /** * Check if a package.json file exists in projectRoot */ diff --git a/packages/cli-core/src/package-manager-controller/pnpm_package_manager_controller.test.ts b/packages/cli-core/src/package-manager-controller/pnpm_package_manager_controller.test.ts index 3895e62251..83b631390f 100644 --- a/packages/cli-core/src/package-manager-controller/pnpm_package_manager_controller.test.ts +++ b/packages/cli-core/src/package-manager-controller/pnpm_package_manager_controller.test.ts @@ -5,6 +5,7 @@ import assert from 'assert'; import { execa } from 'execa'; import { PnpmPackageManagerController } from './pnpm_package_manager_controller.js'; import { executeWithDebugLogger } from './execute_with_debugger_logger.js'; +import { LockFileReader } from '@aws-amplify/plugin-types'; void describe('PnpmPackageManagerController', () => { const fspMock = { @@ -124,4 +125,88 @@ void describe('PnpmPackageManagerController', () => { assert.equal(fspMock.writeFile.mock.callCount(), 1); }); }); + + void describe('getDependencies', () => { + const existsSyncMock = mock.fn(() => true); + + void it('successfully returns dependency versions', async () => { + const lockFileReaderMock = { + getLockFileContentsFromCwd: async () => + Promise.resolve({ + dependencies: [ + { + name: 'aws-cdk', + version: '1.2.3', + }, + { + name: 'aws-cdk-lib', + version: '12.13.14', + }, + { + name: 'test_dep', + version: '1.23.45', + }, + { + name: 'some_other_dep', + version: '12.1.3', + }, + ], + }), + } as LockFileReader; + const pnpmPackageManagerController = new PnpmPackageManagerController( + '/testProjectRoot', + fspMock as unknown as typeof fsp, + pathMock as unknown as typeof path, + execaMock as unknown as typeof execa, + executeWithDebugLoggerMock as unknown as typeof executeWithDebugLogger, + existsSyncMock, + lockFileReaderMock + ); + const dependencyVersions = + await pnpmPackageManagerController.getDependencies(); + const expectedVersions = [ + { + name: 'aws-cdk', + version: '1.2.3', + }, + { + name: 'aws-cdk-lib', + version: '12.13.14', + }, + ]; + + assert.deepEqual(dependencyVersions, expectedVersions); + }); + + void it('returns empty array if there are no matching dependencies', async () => { + const lockFileReaderMock = { + getLockFileContentsFromCwd: async () => + Promise.resolve({ + dependencies: [ + { + name: 'test_dep', + version: '1.23.45', + }, + { + name: 'some_other_dep', + version: '12.1.3', + }, + ], + }), + } as LockFileReader; + const pnpmPackageManagerController = new PnpmPackageManagerController( + '/testProjectRoot', + fspMock as unknown as typeof fsp, + pathMock as unknown as typeof path, + execaMock as unknown as typeof execa, + executeWithDebugLoggerMock as unknown as typeof executeWithDebugLogger, + existsSyncMock, + lockFileReaderMock + ); + const dependencyVersions = + await pnpmPackageManagerController.getDependencies(); + + assert.deepEqual(dependencyVersions, []); + }); + }); }); diff --git a/packages/cli-core/src/package-manager-controller/pnpm_package_manager_controller.ts b/packages/cli-core/src/package-manager-controller/pnpm_package_manager_controller.ts index 5eb13ea7de..2cb6fb4933 100644 --- a/packages/cli-core/src/package-manager-controller/pnpm_package_manager_controller.ts +++ b/packages/cli-core/src/package-manager-controller/pnpm_package_manager_controller.ts @@ -4,6 +4,7 @@ import { existsSync as _existsSync } from 'fs'; import { execa as _execa } from 'execa'; import { executeWithDebugLogger as _executeWithDebugLogger } from './execute_with_debugger_logger.js'; import { PackageManagerControllerBase } from './package_manager_controller_base.js'; +import { PnpmLockFileReader } from './lock-file-reader/pnpm_lock_file_reader.js'; /** * PnpmPackageManagerController is an abstraction around pnpm commands that are needed to initialize a project and install dependencies @@ -18,13 +19,15 @@ export class PnpmPackageManagerController extends PackageManagerControllerBase { protected readonly path = _path, protected readonly execa = _execa, protected readonly executeWithDebugLogger = _executeWithDebugLogger, - protected readonly existsSync = _existsSync + protected readonly existsSync = _existsSync, + protected readonly lockFileReader = new PnpmLockFileReader() ) { super( cwd, 'pnpm', ['init'], 'install', + lockFileReader, fsp, path, execa, diff --git a/packages/cli-core/src/package-manager-controller/yarn_classic_package_manager_controller.test.ts b/packages/cli-core/src/package-manager-controller/yarn_classic_package_manager_controller.test.ts index 28987b2784..00ac91fc6e 100644 --- a/packages/cli-core/src/package-manager-controller/yarn_classic_package_manager_controller.test.ts +++ b/packages/cli-core/src/package-manager-controller/yarn_classic_package_manager_controller.test.ts @@ -5,6 +5,7 @@ import assert from 'assert'; import { execa } from 'execa'; import { YarnClassicPackageManagerController } from './yarn_classic_package_manager_controller.js'; import { executeWithDebugLogger } from './execute_with_debugger_logger.js'; +import { LockFileReader } from '@aws-amplify/plugin-types'; void describe('YarnClassicPackageManagerController', () => { const fspMock = { @@ -128,4 +129,90 @@ void describe('YarnClassicPackageManagerController', () => { assert.equal(fspMock.writeFile.mock.callCount(), 1); }); }); + + void describe('getDependencies', () => { + const existsSyncMock = mock.fn(() => true); + + void it('successfully returns dependency versions', async () => { + const lockFileReaderMock = { + getLockFileContentsFromCwd: async () => + Promise.resolve({ + dependencies: [ + { + name: 'aws-cdk', + version: '1.2.3', + }, + { + name: 'aws-cdk-lib', + version: '12.13.14', + }, + { + name: 'test_dep', + version: '1.23.45', + }, + { + name: 'some_other_dep', + version: '12.1.3', + }, + ], + }), + } as LockFileReader; + const yarnClassicPackageManagerController = + new YarnClassicPackageManagerController( + '/testProjectRoot', + fspMock as unknown as typeof fsp, + pathMock as unknown as typeof path, + execaMock as unknown as typeof execa, + executeWithDebugLoggerMock as unknown as typeof executeWithDebugLogger, + existsSyncMock, + lockFileReaderMock + ); + const dependencyVersions = + await yarnClassicPackageManagerController.getDependencies(); + const expectedVersions = [ + { + name: 'aws-cdk', + version: '1.2.3', + }, + { + name: 'aws-cdk-lib', + version: '12.13.14', + }, + ]; + + assert.deepEqual(dependencyVersions, expectedVersions); + }); + + void it('returns empty array if there are no matching dependencies', async () => { + const lockFileReaderMock = { + getLockFileContentsFromCwd: async () => + Promise.resolve({ + dependencies: [ + { + name: 'test_dep', + version: '1.23.45', + }, + { + name: 'some_other_dep', + version: '12.1.3', + }, + ], + }), + } as LockFileReader; + const yarnClassicPackageManagerController = + new YarnClassicPackageManagerController( + '/testProjectRoot', + fspMock as unknown as typeof fsp, + pathMock as unknown as typeof path, + execaMock as unknown as typeof execa, + executeWithDebugLoggerMock as unknown as typeof executeWithDebugLogger, + existsSyncMock, + lockFileReaderMock + ); + const dependencyVersions = + await yarnClassicPackageManagerController.getDependencies(); + + assert.deepEqual(dependencyVersions, []); + }); + }); }); diff --git a/packages/cli-core/src/package-manager-controller/yarn_classic_package_manager_controller.ts b/packages/cli-core/src/package-manager-controller/yarn_classic_package_manager_controller.ts index 48a4330ed9..70c8e45154 100644 --- a/packages/cli-core/src/package-manager-controller/yarn_classic_package_manager_controller.ts +++ b/packages/cli-core/src/package-manager-controller/yarn_classic_package_manager_controller.ts @@ -4,6 +4,7 @@ import { execa as _execa } from 'execa'; import * as _path from 'path'; import { executeWithDebugLogger as _executeWithDebugLogger } from './execute_with_debugger_logger.js'; import { PackageManagerControllerBase } from './package_manager_controller_base.js'; +import { YarnClassicLockFileReader } from './lock-file-reader/yarn_classic_lock_file_reader.js'; /** * YarnClassicPackageManagerController is an abstraction around yarn classic commands that are needed to initialize a project and install dependencies @@ -18,13 +19,15 @@ export class YarnClassicPackageManagerController extends PackageManagerControlle protected readonly path = _path, protected readonly execa = _execa, protected readonly executeWithDebugLogger = _executeWithDebugLogger, - protected readonly existsSync = _existsSync + protected readonly existsSync = _existsSync, + protected readonly lockFileReader = new YarnClassicLockFileReader() ) { super( cwd, 'yarn', ['init', '--yes'], 'add', + lockFileReader, fsp, path, execa, diff --git a/packages/cli-core/src/package-manager-controller/yarn_modern_package_manager_controller.test.ts b/packages/cli-core/src/package-manager-controller/yarn_modern_package_manager_controller.test.ts index 4dfac585d3..215c573948 100644 --- a/packages/cli-core/src/package-manager-controller/yarn_modern_package_manager_controller.test.ts +++ b/packages/cli-core/src/package-manager-controller/yarn_modern_package_manager_controller.test.ts @@ -6,6 +6,7 @@ import { execa } from 'execa'; import { Printer } from '../printer/printer.js'; import { YarnModernPackageManagerController } from './yarn_modern_package_manager_controller.js'; import { executeWithDebugLogger } from './execute_with_debugger_logger.js'; +import { LockFileReader } from '@aws-amplify/plugin-types'; void describe('YarnModernPackageManagerController', () => { const fspMock = { @@ -123,4 +124,92 @@ void describe('YarnModernPackageManagerController', () => { assert.equal(fspMock.writeFile.mock.callCount(), 2); }); }); + + void describe('getDependencies', () => { + const existsSyncMock = mock.fn(() => true); + + void it('successfully returns dependency versions', async () => { + const lockFileReaderMock = { + getLockFileContentsFromCwd: async () => + Promise.resolve({ + dependencies: [ + { + name: 'aws-cdk', + version: '1.2.3', + }, + { + name: 'aws-cdk-lib', + version: '12.13.14', + }, + { + name: 'test_dep', + version: '1.23.45', + }, + { + name: 'some_other_dep', + version: '12.1.3', + }, + ], + }), + } as LockFileReader; + const yarnModernPackageManagerController = + new YarnModernPackageManagerController( + '/testProjectRoot', + printerMock as unknown as Printer, + fspMock as unknown as typeof fsp, + pathMock as unknown as typeof path, + execaMock as unknown as typeof execa, + executeWithDebugLoggerMock as unknown as typeof executeWithDebugLogger, + existsSyncMock, + lockFileReaderMock + ); + const dependencyVersions = + await yarnModernPackageManagerController.getDependencies(); + const expectedVersions = [ + { + name: 'aws-cdk', + version: '1.2.3', + }, + { + name: 'aws-cdk-lib', + version: '12.13.14', + }, + ]; + + assert.deepEqual(dependencyVersions, expectedVersions); + }); + + void it('returns empty array if there are no matching dependencies', async () => { + const lockFileReaderMock = { + getLockFileContentsFromCwd: async () => + Promise.resolve({ + dependencies: [ + { + name: 'test_dep', + version: '1.23.45', + }, + { + name: 'some_other_dep', + version: '12.1.3', + }, + ], + }), + } as LockFileReader; + const yarnModernPackageManagerController = + new YarnModernPackageManagerController( + '/testProjectRoot', + printerMock as unknown as Printer, + fspMock as unknown as typeof fsp, + pathMock as unknown as typeof path, + execaMock as unknown as typeof execa, + executeWithDebugLoggerMock as unknown as typeof executeWithDebugLogger, + existsSyncMock, + lockFileReaderMock + ); + const dependencyVersions = + await yarnModernPackageManagerController.getDependencies(); + + assert.deepEqual(dependencyVersions, []); + }); + }); }); diff --git a/packages/cli-core/src/package-manager-controller/yarn_modern_package_manager_controller.ts b/packages/cli-core/src/package-manager-controller/yarn_modern_package_manager_controller.ts index 0188ec889d..f4d6003f1b 100644 --- a/packages/cli-core/src/package-manager-controller/yarn_modern_package_manager_controller.ts +++ b/packages/cli-core/src/package-manager-controller/yarn_modern_package_manager_controller.ts @@ -6,6 +6,7 @@ import { LogLevel, Printer } from '../printer/printer.js'; import { format } from '../format/format.js'; import { executeWithDebugLogger as _executeWithDebugLogger } from './execute_with_debugger_logger.js'; import { PackageManagerControllerBase } from './package_manager_controller_base.js'; +import { YarnModernLockFileReader } from './lock-file-reader/yarn_modern_lock_file_reader.js'; /** * YarnModernPackageManagerController is an abstraction around yarn modern (yarn v2+) commands that are needed to initialize a project and install dependencies @@ -21,13 +22,15 @@ export class YarnModernPackageManagerController extends PackageManagerController protected readonly path = _path, protected readonly execa = _execa, protected readonly executeWithDebugLogger = _executeWithDebugLogger, - protected readonly existsSync = _existsSync + protected readonly existsSync = _existsSync, + protected readonly lockFileReader = new YarnModernLockFileReader() ) { super( cwd, 'yarn', ['init', '--yes'], 'add', + lockFileReader, fsp, path, execa, diff --git a/packages/cli/src/ampx.ts b/packages/cli/src/ampx.ts index 2ddb90ccb2..8d97e526a1 100755 --- a/packages/cli/src/ampx.ts +++ b/packages/cli/src/ampx.ts @@ -13,7 +13,7 @@ import { import { fileURLToPath } from 'node:url'; import { verifyCommandName } from './verify_command_name.js'; import { hideBin } from 'yargs/helpers'; -import { format } from '@aws-amplify/cli-core'; +import { PackageManagerControllerFactory, format } from '@aws-amplify/cli-core'; const packageJson = new PackageJsonReader().read( fileURLToPath(new URL('../package.json', import.meta.url)) @@ -27,8 +27,13 @@ if (libraryVersion == undefined) { }); } +const dependencies = await new PackageManagerControllerFactory() + .getPackageManagerController() + .getDependencies(); + const usageDataEmitter = await new UsageDataEmitterFactory().getInstance( - libraryVersion + libraryVersion, + dependencies ); attachUnhandledExceptionListeners(usageDataEmitter); diff --git a/packages/cli/src/commands/sandbox/sandbox_command_factory.ts b/packages/cli/src/commands/sandbox/sandbox_command_factory.ts index ef5454a931..c63403f947 100644 --- a/packages/cli/src/commands/sandbox/sandbox_command_factory.ts +++ b/packages/cli/src/commands/sandbox/sandbox_command_factory.ts @@ -16,7 +16,11 @@ import { } from '@aws-amplify/platform-core'; import { SandboxEventHandlerFactory } from './sandbox_event_handler_factory.js'; import { CommandMiddleware } from '../../command_middleware.js'; -import { format, printer } from '@aws-amplify/cli-core'; +import { + PackageManagerControllerFactory, + format, + printer, +} from '@aws-amplify/cli-core'; import { S3Client } from '@aws-sdk/client-s3'; import { AmplifyClient } from '@aws-sdk/client-amplify'; import { CloudFormationClient } from '@aws-sdk/client-cloudformation'; @@ -57,7 +61,15 @@ export const createSandboxCommand = (): CommandModule< const eventHandlerFactory = new SandboxEventHandlerFactory( sandboxBackendIdPartsResolver.resolve, - async () => await new UsageDataEmitterFactory().getInstance(libraryVersion) + async () => { + const dependencies = await new PackageManagerControllerFactory() + .getPackageManagerController() + .getDependencies(); + return await new UsageDataEmitterFactory().getInstance( + libraryVersion, + dependencies + ); + } ); const commandMiddleWare = new CommandMiddleware(printer); diff --git a/packages/create-amplify/src/amplify_project_creator.test.ts b/packages/create-amplify/src/amplify_project_creator.test.ts index ea9407ebfb..a88c08efcb 100644 --- a/packages/create-amplify/src/amplify_project_creator.test.ts +++ b/packages/create-amplify/src/amplify_project_creator.test.ts @@ -109,6 +109,7 @@ void describe('AmplifyProjectCreator', () => { runWithPackageManager: mock.fn(() => Promise.resolve() as never), getCommand: (args: string[]) => `'npx ${args.join(' ')}'`, allowsSignalPropagation: () => true, + getDependencies: mock.fn(() => Promise.resolve([])), }; const projectRootValidatorMock = { validate: mock.fn() }; const gitIgnoreInitializerMock = { ensureInitialized: mock.fn() }; diff --git a/packages/create-amplify/src/initial_project_file_generator.test.ts b/packages/create-amplify/src/initial_project_file_generator.test.ts index 8fcfb9190a..4d5314e680 100644 --- a/packages/create-amplify/src/initial_project_file_generator.test.ts +++ b/packages/create-amplify/src/initial_project_file_generator.test.ts @@ -17,6 +17,7 @@ void describe('InitialProjectFileGenerator', () => { runWithPackageManager: mock.fn(() => Promise.resolve() as never), getCommand: (args: string[]) => `'npx ${args.join(' ')}'`, allowsSignalPropagation: () => true, + getDependencies: mock.fn(() => Promise.resolve([])), }; beforeEach(() => { executeWithDebugLoggerMock.mock.resetCalls(); diff --git a/packages/platform-core/API.md b/packages/platform-core/API.md index 06891c5af7..092d8aee7f 100644 --- a/packages/platform-core/API.md +++ b/packages/platform-core/API.md @@ -8,6 +8,7 @@ import { AppId } from '@aws-amplify/plugin-types'; import { ApplicationLogLevel } from 'aws-cdk-lib/aws-lambda'; import { BackendIdentifier } from '@aws-amplify/plugin-types'; import { DeepPartialAmplifyGeneratedConfigs } from '@aws-amplify/plugin-types'; +import { Dependency } from '@aws-amplify/plugin-types'; import { FieldLogLevel } from 'aws-cdk-lib/aws-appsync'; import { LogLevel } from '@aws-amplify/plugin-types'; import { LogRetention } from '@aws-amplify/plugin-types'; @@ -230,7 +231,7 @@ export type UsageDataEmitter = { // @public export class UsageDataEmitterFactory { - getInstance: (libraryVersion: string) => Promise; + getInstance: (libraryVersion: string, dependencies: Array) => Promise; } // (No @packageDocumentation comment for this package) diff --git a/packages/platform-core/src/lock-file-reader/lock_file_reader_factory.test.ts b/packages/platform-core/src/lock-file-reader/lock_file_reader_factory.test.ts deleted file mode 100644 index 302f1bf6a8..0000000000 --- a/packages/platform-core/src/lock-file-reader/lock_file_reader_factory.test.ts +++ /dev/null @@ -1,59 +0,0 @@ -import assert from 'assert'; -import { afterEach, beforeEach, describe, it } from 'node:test'; -import { NpmLockFileReader } from './npm_lock_file_reader'; -import { PnpmLockFileReader } from './pnpm_lock_file_reader'; -import { YarnClassicLockFileReader } from './yarn_classic_lock_file_reader'; -import { YarnModernLockFileReader } from './yarn_modern_lock_file_reader'; -import { LockFileReaderFactory } from './lock_file_reader_factory'; - -void describe('LockFileReaderFactory', () => { - let originalEnv: NodeJS.ProcessEnv; - - beforeEach(() => { - originalEnv = process.env; - }); - - afterEach(() => { - process.env = originalEnv; - }); - - void describe('getLockFileReader', () => { - const testCases = [ - { - name: 'npm', - userAgent: 'npm/7.0.0 node/v15.0.0 darwin x64', - expectedInstanceOf: NpmLockFileReader, - }, - { - name: 'pnpm', - userAgent: 'pnpm/5.0.0 node/v15.0.0 darwin x64', - expectedInstanceOf: PnpmLockFileReader, - }, - { - name: 'yarn classic', - userAgent: 'yarn/1.22.21 node/v15.0.0 darwin x64', - expectedInstanceOf: YarnClassicLockFileReader, - }, - { - name: 'yarn modern', - userAgent: 'yarn/4.0.1 node/v15.0.0 darwin x64', - expectedInstanceOf: YarnModernLockFileReader, - }, - ]; - - for (const testCase of testCases) { - void it(`should return the correct lock file reader for ${testCase.name}`, () => { - process.env.npm_config_user_agent = testCase.userAgent; - const lockFileReader = new LockFileReaderFactory().getLockFileReader(); - assert.ok(lockFileReader instanceof testCase.expectedInstanceOf); - }); - } - - void it('defaults to npm lock file reader', () => { - process.env.npm_config_user_agent = - 'unsupported/1.0.0 node/v15.0.0 darwin x64'; - const lockFileReader = new LockFileReaderFactory().getLockFileReader(); - assert.ok(lockFileReader instanceof NpmLockFileReader); - }); - }); -}); diff --git a/packages/platform-core/src/lock-file-reader/lock_file_reader_factory.ts b/packages/platform-core/src/lock-file-reader/lock_file_reader_factory.ts deleted file mode 100644 index c3957737fb..0000000000 --- a/packages/platform-core/src/lock-file-reader/lock_file_reader_factory.ts +++ /dev/null @@ -1,63 +0,0 @@ -import { NpmLockFileReader } from './npm_lock_file_reader'; -import { PnpmLockFileReader } from './pnpm_lock_file_reader'; -import { YarnClassicLockFileReader } from './yarn_classic_lock_file_reader'; -import { YarnModernLockFileReader } from './yarn_modern_lock_file_reader'; - -/** - * LockFileReaderFactory is a factory for an abstraction around reading lock files from different package managers - */ -export class LockFileReaderFactory { - /** - * Creates a lock file reader factory - */ - constructor(private readonly platform = process.platform) {} - - /** - * Gets the lock file reader based on the package manager being used - */ - getLockFileReader(): LockFileReader { - const packageManagerName = this.getPackageManagerName(); - switch (packageManagerName) { - case 'npm': - return new NpmLockFileReader(); - case 'pnpm': - return new PnpmLockFileReader(); - case 'yarn-classic': - return new YarnClassicLockFileReader(); - case 'yarn-modern': - return new YarnModernLockFileReader(); - default: - // defaults to npm lock file reader as it is the most used package manager by customers - return new NpmLockFileReader(); - } - } - - /** - * Get package manager name from npm_config_user_agent - */ - private getPackageManagerName() { - const userAgent = process.env.npm_config_user_agent; - if (userAgent === undefined) { - return; - } - const packageManagerAndVersion = userAgent.split(' ')[0]; - const [packageManagerName, packageManagerVersion] = - packageManagerAndVersion.split('/'); - - if (packageManagerName === 'yarn') { - const yarnMajorVersion = packageManagerVersion.split('.')[0]; - return `yarn-${yarnMajorVersion === '1' ? 'classic' : 'modern'}`; - } - return packageManagerName; - } -} - -export type LockFileReader = { - getLockFileContentsFromCwd: () => Promise; -}; - -export type LockFileContents = { - dependencies: Dependencies; -}; - -export type Dependencies = Array<{ name: string; version: string }>; diff --git a/packages/platform-core/src/usage-data/dependency_version_fetcher.test.ts b/packages/platform-core/src/usage-data/dependency_version_fetcher.test.ts deleted file mode 100644 index 2764b20326..0000000000 --- a/packages/platform-core/src/usage-data/dependency_version_fetcher.test.ts +++ /dev/null @@ -1,75 +0,0 @@ -import assert from 'assert'; -import { describe, it } from 'node:test'; -import { DependencyVersionFetcher } from './dependency_version_fetcher'; -import { LockFileReader } from '../lock-file-reader/lock_file_reader_factory'; - -void describe('getDependencyVersions', () => { - void it('successfully returns dependency versions', async () => { - const lockFileReaderMock = { - getLockFileContentsFromCwd: async () => - Promise.resolve({ - dependencies: [ - { - name: 'aws-cdk', - version: '1.2.3', - }, - { - name: 'aws-cdk-lib', - version: '12.13.14', - }, - { - name: 'test_dep', - version: '1.23.45', - }, - { - name: 'some_other_dep', - version: '12.1.3', - }, - ], - }), - } as LockFileReader; - const dependencyVersionFetcher = new DependencyVersionFetcher( - lockFileReaderMock - ); - const dependencyVersions = - await dependencyVersionFetcher.getDependencyVersions(); - const expectedVersions = [ - { - name: 'aws-cdk', - version: '1.2.3', - }, - { - name: 'aws-cdk-lib', - version: '12.13.14', - }, - ]; - - assert.deepEqual(dependencyVersions, expectedVersions); - }); - - void it('returns empty array if there are no matching dependencies', async () => { - const lockFileReaderMock = { - getLockFileContentsFromCwd: async () => - Promise.resolve({ - dependencies: [ - { - name: 'test_dep', - version: '1.23.45', - }, - { - name: 'some_other_dep', - version: '12.1.3', - }, - ], - }), - } as LockFileReader; - - const dependencyVersionFetcher = new DependencyVersionFetcher( - lockFileReaderMock - ); - const dependencyVersions = - await dependencyVersionFetcher.getDependencyVersions(); - - assert.deepEqual(dependencyVersions, []); - }); -}); diff --git a/packages/platform-core/src/usage-data/dependency_version_fetcher.ts b/packages/platform-core/src/usage-data/dependency_version_fetcher.ts deleted file mode 100644 index 6d47e19e04..0000000000 --- a/packages/platform-core/src/usage-data/dependency_version_fetcher.ts +++ /dev/null @@ -1,33 +0,0 @@ -import { - Dependencies, - LockFileReader, - LockFileReaderFactory, -} from '../lock-file-reader/lock_file_reader_factory'; - -/** - * Get dependency versions from customer project's lock file - */ -export class DependencyVersionFetcher { - /** - * Creates dependency version fetcher - */ - constructor( - private readonly lockFileReader: LockFileReader = new LockFileReaderFactory().getLockFileReader() - ) {} - - getDependencyVersions = async () => { - const lockFileContents = - await this.lockFileReader.getLockFileContentsFromCwd(); - const targetDependencies = ['aws-cdk', 'aws-cdk-lib']; - - const dependencyVersions: Dependencies = []; - - for (const { name, version } of lockFileContents.dependencies) { - if (targetDependencies.includes(name)) { - dependencyVersions.push({ name, version }); - } - } - - return dependencyVersions; - }; -} diff --git a/packages/platform-core/src/usage-data/usage_data_emitter.test.ts b/packages/platform-core/src/usage-data/usage_data_emitter.test.ts index 730ef161a6..281fa69142 100644 --- a/packages/platform-core/src/usage-data/usage_data_emitter.test.ts +++ b/packages/platform-core/src/usage-data/usage_data_emitter.test.ts @@ -11,8 +11,6 @@ import { AccountIdFetcher } from './account_id_fetcher'; import { UsageData } from './usage_data'; import isCI from 'is-ci'; import { AmplifyError, AmplifyUserError } from '..'; -import { LockFileReader } from '../lock-file-reader/lock_file_reader_factory'; -import { DependencyVersionFetcher } from './dependency_version_fetcher'; const originalNpmUserAgent = process.env.npm_config_user_agent; const testNpmUserAgent = 'testNpmUserAgent'; @@ -21,6 +19,16 @@ void describe('UsageDataEmitter', () => { let usageDataEmitter: DefaultUsageDataEmitter; const testLibraryVersion = '1.2.3'; + const testDependencies = [ + { + name: 'test-dep', + version: '1.2.3', + }, + { + name: 'some_other_dep', + version: '12.13.14', + }, + ]; const testURL = url.parse('https://aws.amazon.com/amplify/'); const onReqEndMock = mock.fn(); const onReqWriteMock = mock.fn(); @@ -42,26 +50,6 @@ void describe('UsageDataEmitter', () => { fetch: async () => '123456789012', } as AccountIdFetcher; - // For DependencyVersionFetcher - const lockFileReaderMock = { - getLockFileContentsFromCwd: async () => - Promise.resolve({ - dependencies: [ - { - name: 'aws-cdk', - version: '1.2.4', - }, - { - name: 'test_dep', - version: '12.13.14', - }, - ], - }), - } as LockFileReader; - const dependencyVersionFetcherMock = new DependencyVersionFetcher( - lockFileReaderMock - ); - mock.method(https, 'request', () => reqMock); before(() => { @@ -177,10 +165,10 @@ void describe('UsageDataEmitter', () => { usageDataEmitter = new DefaultUsageDataEmitter( testLibraryVersion, + testDependencies, v4(), testURL, - accountIdFetcherMock, - dependencyVersionFetcherMock + accountIdFetcherMock ); let usageDataEmitterPromise; diff --git a/packages/platform-core/src/usage-data/usage_data_emitter.ts b/packages/platform-core/src/usage-data/usage_data_emitter.ts index 92b871b8ef..653fb4f513 100644 --- a/packages/platform-core/src/usage-data/usage_data_emitter.ts +++ b/packages/platform-core/src/usage-data/usage_data_emitter.ts @@ -10,7 +10,7 @@ import isCI from 'is-ci'; import { SerializableError } from './serializable_error.js'; import { UsageDataEmitter } from './usage_data_emitter_factory.js'; import { AmplifyError } from '../index.js'; -import { DependencyVersionFetcher } from './dependency_version_fetcher.js'; +import { Dependency } from '@aws-amplify/plugin-types'; /** * Entry point for sending usage data metrics @@ -21,10 +21,10 @@ export class DefaultUsageDataEmitter implements UsageDataEmitter { */ constructor( private readonly libraryVersion: string, + private readonly dependencies: Array, private readonly sessionUuid = uuid(), private readonly url = getUrl(), - private readonly accountIdFetcher = new AccountIdFetcher(), - private readonly dependencyVersionFetcher = new DependencyVersionFetcher() + private readonly accountIdFetcher = new AccountIdFetcher() ) {} emitSuccess = async ( @@ -90,9 +90,7 @@ export class DefaultUsageDataEmitter implements UsageDataEmitter { isCi: isCI, projectSetting: { editor: process.env.npm_config_user_agent, - details: JSON.stringify( - await this.dependencyVersionFetcher.getDependencyVersions() - ), + details: JSON.stringify(this.dependencies), }, }; }; diff --git a/packages/platform-core/src/usage-data/usage_data_emitter_factory.test.ts b/packages/platform-core/src/usage-data/usage_data_emitter_factory.test.ts index 7d7e0aaf84..443a0227e9 100644 --- a/packages/platform-core/src/usage-data/usage_data_emitter_factory.test.ts +++ b/packages/platform-core/src/usage-data/usage_data_emitter_factory.test.ts @@ -41,7 +41,8 @@ void describe('UsageDataEmitterFactory', () => { void it('returns DefaultUsageDataEmitter by default', async () => { configControllerGet.mock.mockImplementationOnce(() => undefined); const dataEmitter = await new UsageDataEmitterFactory().getInstance( - '0.0.0' + '0.0.0', + [] ); assert.strictEqual(configControllerGet.mock.callCount(), 1); assert.strictEqual(dataEmitter instanceof DefaultUsageDataEmitter, true); @@ -51,7 +52,8 @@ void describe('UsageDataEmitterFactory', () => { configControllerGet.mock.mockImplementationOnce(() => undefined); process.env['AMPLIFY_DISABLE_TELEMETRY'] = '1'; const dataEmitter = await new UsageDataEmitterFactory().getInstance( - '0.0.0' + '0.0.0', + [] ); assert.strictEqual(dataEmitter instanceof NoOpUsageDataEmitter, true); assert.strictEqual(configControllerGet.mock.callCount(), 1); @@ -61,7 +63,8 @@ void describe('UsageDataEmitterFactory', () => { void it('returns NoOpUsageDataEmitter if local config file exists and reads true', async () => { configControllerGet.mock.mockImplementationOnce(() => false); const dataEmitter = await new UsageDataEmitterFactory().getInstance( - '0.0.0' + '0.0.0', + [] ); assert.strictEqual(configControllerGet.mock.callCount(), 1); assert.strictEqual(dataEmitter instanceof NoOpUsageDataEmitter, true); diff --git a/packages/platform-core/src/usage-data/usage_data_emitter_factory.ts b/packages/platform-core/src/usage-data/usage_data_emitter_factory.ts index 6ee9a2fb11..f4466f1e83 100644 --- a/packages/platform-core/src/usage-data/usage_data_emitter_factory.ts +++ b/packages/platform-core/src/usage-data/usage_data_emitter_factory.ts @@ -3,6 +3,7 @@ import { NoOpUsageDataEmitter } from './noop_usage_data_emitter.js'; import { DefaultUsageDataEmitter } from './usage_data_emitter.js'; import { USAGE_DATA_TRACKING_ENABLED } from './constants.js'; import { AmplifyError } from '../index.js'; +import { Dependency } from '@aws-amplify/plugin-types'; export type UsageDataEmitter = { emitSuccess: ( @@ -22,7 +23,10 @@ export class UsageDataEmitterFactory { /** * Creates UsageDataEmitter for a given library version, usage data tracking preferences */ - getInstance = async (libraryVersion: string): Promise => { + getInstance = async ( + libraryVersion: string, + dependencies: Array + ): Promise => { const configController = configControllerFactory.getInstance( 'usage_data_preferences.json' ); @@ -37,6 +41,6 @@ export class UsageDataEmitterFactory { ) { return new NoOpUsageDataEmitter(); } - return new DefaultUsageDataEmitter(libraryVersion); + return new DefaultUsageDataEmitter(libraryVersion, dependencies); }; } diff --git a/packages/plugin-types/API.md b/packages/plugin-types/API.md index 181acf4242..e6ecbae1b5 100644 --- a/packages/plugin-types/API.md +++ b/packages/plugin-types/API.md @@ -152,6 +152,12 @@ export type DeepPartialAmplifyGeneratedConfigs = { [P in keyof T]?: P extends 'auth' | 'data' | 'storage' ? T[P] extends object ? DeepPartialAmplifyGeneratedConfigs : Partial : T[P]; }; +// @public (undocumented) +export type Dependency = { + name: string; + version: string; +}; + // @public export type DeploymentType = 'branch' | 'sandbox'; @@ -196,6 +202,16 @@ export type ImportPathVerifier = { verify: (importStack: string | undefined, expectedImportingFile: string, errorMessage: string) => void; }; +// @public (undocumented) +export type LockFileContents = { + dependencies: Array; +}; + +// @public (undocumented) +export type LockFileReader = { + getLockFileContentsFromCwd: () => Promise; +}; + // @public (undocumented) export type LogLevel = 'all' | 'debug' | 'error' | 'fatal' | 'info' | 'none' | 'trace' | 'warn'; @@ -220,6 +236,7 @@ export type PackageManagerController = { runWithPackageManager: (args: string[] | undefined, dir: string, options?: ExecaOptions) => ExecaChildProcess; getCommand: (args: string[]) => string; allowsSignalPropagation: () => boolean; + getDependencies: () => Promise>; }; // @public (undocumented) diff --git a/packages/plugin-types/src/package_manager_controller.ts b/packages/plugin-types/src/package_manager_controller.ts index 8e53789641..9eed9e1e80 100644 --- a/packages/plugin-types/src/package_manager_controller.ts +++ b/packages/plugin-types/src/package_manager_controller.ts @@ -23,6 +23,16 @@ export type ExecaChildProcess = { stderr: Readable | null; } & Promise; +export type Dependency = { name: string; version: string }; + +export type LockFileContents = { + dependencies: Array; +}; + +export type LockFileReader = { + getLockFileContentsFromCwd: () => Promise; +}; + export type PackageManagerController = { initializeProject: () => Promise; initializeTsConfig: (targetDir: string) => Promise; @@ -37,4 +47,5 @@ export type PackageManagerController = { ) => ExecaChildProcess; getCommand: (args: string[]) => string; allowsSignalPropagation: () => boolean; + getDependencies: () => Promise>; }; From 2d42a8459d629626fd66558838b7d131a09efc56 Mon Sep 17 00:00:00 2001 From: Roshane Pascual Date: Thu, 9 Jan 2025 13:21:51 -0800 Subject: [PATCH 7/8] PR feedback --- .../backend-deployer/src/cdk_deployer.test.ts | 2 +- .../npm_lock_file_reader.test.ts | 14 +++++- .../lock-file-reader/npm_lock_file_reader.ts | 42 ++++++++-------- .../pnpm_lock_file_reader.test.ts | 48 +++++++++++++++++++ .../lock-file-reader/pnpm_lock_file_reader.ts | 16 ++++--- .../lock-file-reader/types.ts | 9 ++++ .../yarn_classic_lock_file_reader.test.ts | 17 ++++++- .../yarn_classic_lock_file_reader.ts | 19 ++++---- .../yarn_modern_lock_file_reader.test.ts | 25 +++++++++- .../yarn_modern_lock_file_reader.ts | 18 ++++--- .../npm_package_manager_controller.test.ts | 46 +++++------------- .../package_manager_controller_base.ts | 17 ++----- .../pnpm_package_manager_controller.test.ts | 46 +++++------------- ...classic_package_manager_controller.test.ts | 47 +++++------------- ..._modern_package_manager_controller.test.ts | 48 +++++-------------- packages/cli/src/ampx.ts | 2 +- .../sandbox/sandbox_command_factory.ts | 2 +- .../src/amplify_project_creator.test.ts | 2 +- .../initial_project_file_generator.test.ts | 2 +- packages/platform-core/API.md | 2 +- .../src/usage-data/usage_data_emitter.test.ts | 25 +++++++++- .../src/usage-data/usage_data_emitter.ts | 13 +++-- .../usage-data/usage_data_emitter_factory.ts | 2 +- packages/plugin-types/API.md | 12 +---- .../src/package_manager_controller.ts | 10 +--- 25 files changed, 251 insertions(+), 235 deletions(-) create mode 100644 packages/cli-core/src/package-manager-controller/lock-file-reader/types.ts diff --git a/packages/backend-deployer/src/cdk_deployer.test.ts b/packages/backend-deployer/src/cdk_deployer.test.ts index 39af9db733..254cb0ff3d 100644 --- a/packages/backend-deployer/src/cdk_deployer.test.ts +++ b/packages/backend-deployer/src/cdk_deployer.test.ts @@ -46,7 +46,7 @@ void describe('invokeCDKCommand', () => { runWithPackageManager: mock.fn(() => Promise.resolve() as never), getCommand: (args: string[]) => `'npx ${args.join(' ')}'`, allowsSignalPropagation: () => true, - getDependencies: mock.fn(() => Promise.resolve([])), + tryGetDependencies: mock.fn(() => Promise.resolve([])), }; const invoker = new CDKDeployer( diff --git a/packages/cli-core/src/package-manager-controller/lock-file-reader/npm_lock_file_reader.test.ts b/packages/cli-core/src/package-manager-controller/lock-file-reader/npm_lock_file_reader.test.ts index fc1eadbec1..fff5b1c46b 100644 --- a/packages/cli-core/src/package-manager-controller/lock-file-reader/npm_lock_file_reader.test.ts +++ b/packages/cli-core/src/package-manager-controller/lock-file-reader/npm_lock_file_reader.test.ts @@ -52,10 +52,22 @@ void describe('NpmLockFileReader', () => { assert.strictEqual(fspReadFileMock.mock.callCount(), 1); }); - void it('returns empty lock file contents when package-lock.json is not present or parse-able', async () => { + void it('returns undefined when package-lock.json is not present or parse-able', async () => { fspReadFileMock.mock.mockImplementationOnce(() => Promise.reject(new Error()) ); + const lockFileContents = + await npmLockFileReader.getLockFileContentsFromCwd(); + assert.deepEqual(lockFileContents, undefined); + }); + + void it('returns empty dependency array when package-lock.json does not have dependencies', async () => { + fspReadFileMock.mock.mockImplementationOnce(() => + JSON.stringify({ + name: 'test_project', + version: '1.0.0', + }) + ); const lockFileContents = await npmLockFileReader.getLockFileContentsFromCwd(); assert.deepEqual(lockFileContents, { dependencies: [] }); diff --git a/packages/cli-core/src/package-manager-controller/lock-file-reader/npm_lock_file_reader.ts b/packages/cli-core/src/package-manager-controller/lock-file-reader/npm_lock_file_reader.ts index 4aa3c81143..6f412085b5 100644 --- a/packages/cli-core/src/package-manager-controller/lock-file-reader/npm_lock_file_reader.ts +++ b/packages/cli-core/src/package-manager-controller/lock-file-reader/npm_lock_file_reader.ts @@ -1,11 +1,8 @@ -import { - Dependency, - LockFileContents, - LockFileReader, -} from '@aws-amplify/plugin-types'; +import { Dependency } from '@aws-amplify/plugin-types'; import fsp from 'fs/promises'; import path from 'path'; import z from 'zod'; +import { LockFileContents, LockFileReader } from './types.js'; import { printer } from '../../printer.js'; import { LogLevel } from '../../printer/printer.js'; @@ -13,39 +10,44 @@ import { LogLevel } from '../../printer/printer.js'; * NpmLockFileReader is an abstraction around the logic used to read and parse lock file contents */ export class NpmLockFileReader implements LockFileReader { - getLockFileContentsFromCwd = async (): Promise => { + getLockFileContentsFromCwd = async (): Promise< + LockFileContents | undefined + > => { const dependencies: Array = []; const packageLockJsonPath = path.resolve( process.cwd(), 'package-lock.json' ); - - let jsonLockParsedValue: Record; + let packageLockJson; try { const jsonLockContents = await fsp.readFile(packageLockJsonPath, 'utf-8'); - jsonLockParsedValue = JSON.parse(jsonLockContents); + const jsonLockParsedValue = JSON.parse(jsonLockContents); + // This will strip fields that are not part of the package lock schema + packageLockJson = packageLockJsonSchema.parse(jsonLockParsedValue); } catch (error) { printer.log( `Failed to get lock file contents because ${packageLockJsonPath} does not exist or is not parse-able`, LogLevel.DEBUG ); - return { dependencies }; + return; } - // This will strip fields that are not part of the package lock schema - const packageLockJson = packageLockJsonSchema.parse(jsonLockParsedValue); - for (const key in packageLockJson.packages) { if (key === '') { // Skip root project in packages continue; } - // Remove "node_modules/" prefix - const dependencyName = key.replace(/^node_modules\//, ''); - dependencies.push({ - name: dependencyName, - version: packageLockJson.packages[key].version, - }); + const dependencyVersion = packageLockJson.packages[key].version; + + // Version may not exist if package is a symbolic link + if (dependencyVersion) { + // Remove "node_modules/" prefix + const dependencyName = key.replace(/^node_modules\//, ''); + dependencies.push({ + name: dependencyName, + version: dependencyVersion, + }); + } } return { dependencies }; @@ -57,7 +59,7 @@ const packageLockJsonSchema = z.object({ .record( z.string(), z.object({ - version: z.string(), + version: z.string().optional(), }) ) .optional(), diff --git a/packages/cli-core/src/package-manager-controller/lock-file-reader/pnpm_lock_file_reader.test.ts b/packages/cli-core/src/package-manager-controller/lock-file-reader/pnpm_lock_file_reader.test.ts index 2c99265ff4..443a5f6e5c 100644 --- a/packages/cli-core/src/package-manager-controller/lock-file-reader/pnpm_lock_file_reader.test.ts +++ b/packages/cli-core/src/package-manager-controller/lock-file-reader/pnpm_lock_file_reader.test.ts @@ -90,6 +90,54 @@ packages: fspReadFileMock.mock.mockImplementationOnce(() => Promise.reject(new Error()) ); + const lockFileContents = + await pnpmLockFileReader.getLockFileContentsFromCwd(); + assert.deepEqual(lockFileContents, undefined); + }); + + void it('returns empty dependency array when pnpm-lock.yaml does not have dependencies', async () => { + mock.method( + fsp, + 'readFile', + () => `lockfileVersion: '9.0' + + settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + + importers: + + .: + dependencies: + aws-amplify: + specifier: ^6.12.0 + version: 6.12.0 + devDependencies: + '@aws-amplify/backend': + specifier: ^1.11.0 + version: 1.12.0(@aws-sdk/client-cloudformation@3.723.0)(@aws-sdk/client-s3@3.723.0)(@aws-sdk/client-sso-oidc@3.621.0(@aws-sdk/client-sts@3.621.0))(@aws-sdk/types@3.723.0)(aws-cdk-lib@2.174.1(constructs@10.4.2))(constructs@10.4.2)(zod@3.24.1) + '@aws-amplify/backend-cli': + specifier: ^1.4.5 + version: 1.4.6(@aws-sdk/client-sso-oidc@3.621.0(@aws-sdk/client-sts@3.621.0))(@aws-sdk/client-sts@3.621.0)(@aws-sdk/types@3.723.0)(aws-cdk-lib@2.174.1(constructs@10.4.2))(aws-cdk@2.174.1)(constructs@10.4.2)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.7.2) + aws-cdk: + specifier: ^2.173.4 + version: 2.174.1 + aws-cdk-lib: + specifier: ^2.173.4 + version: 2.174.1(constructs@10.4.2) + constructs: + specifier: ^10.4.2 + version: 10.4.2 + esbuild: + specifier: ^0.24.2 + version: 0.24.2 + tsx: + specifier: ^4.19.2 + version: 4.19.2 + typescript: + specifier: ^5.7.2 + version: 5.7.2` + ); const lockFileContents = await pnpmLockFileReader.getLockFileContentsFromCwd(); assert.deepEqual(lockFileContents, { dependencies: [] }); diff --git a/packages/cli-core/src/package-manager-controller/lock-file-reader/pnpm_lock_file_reader.ts b/packages/cli-core/src/package-manager-controller/lock-file-reader/pnpm_lock_file_reader.ts index 86e1478881..caed7ecaf6 100644 --- a/packages/cli-core/src/package-manager-controller/lock-file-reader/pnpm_lock_file_reader.ts +++ b/packages/cli-core/src/package-manager-controller/lock-file-reader/pnpm_lock_file_reader.ts @@ -1,10 +1,7 @@ -import { - Dependency, - LockFileContents, - LockFileReader, -} from '@aws-amplify/plugin-types'; +import { Dependency } from '@aws-amplify/plugin-types'; import fsp from 'fs/promises'; import path from 'path'; +import { LockFileContents, LockFileReader } from './types.js'; import { printer } from '../../printer.js'; import { LogLevel } from '../../printer/printer.js'; @@ -12,7 +9,9 @@ import { LogLevel } from '../../printer/printer.js'; * PnpmLockFileReader is an abstraction around the logic used to read and parse lock file contents */ export class PnpmLockFileReader implements LockFileReader { - getLockFileContentsFromCwd = async (): Promise => { + getLockFileContentsFromCwd = async (): Promise< + LockFileContents | undefined + > => { const eolRegex = '[\r\n]'; const dependencies: Array = []; const pnpmLockPath = path.resolve(process.cwd(), 'pnpm-lock.yaml'); @@ -24,6 +23,9 @@ export class PnpmLockFileReader implements LockFileReader { ); const startOfPackagesIndex = pnpmLockContentsArray.indexOf('packages:'); + if (startOfPackagesIndex === -1) { + return { dependencies }; + } const pnpmLockPackages = pnpmLockContentsArray.slice( startOfPackagesIndex + 1 ); @@ -49,7 +51,7 @@ export class PnpmLockFileReader implements LockFileReader { `Failed to get lock file contents because ${pnpmLockPath} does not exist or is not parse-able`, LogLevel.DEBUG ); - return { dependencies }; + return; } return { dependencies }; diff --git a/packages/cli-core/src/package-manager-controller/lock-file-reader/types.ts b/packages/cli-core/src/package-manager-controller/lock-file-reader/types.ts new file mode 100644 index 0000000000..6762c4a30d --- /dev/null +++ b/packages/cli-core/src/package-manager-controller/lock-file-reader/types.ts @@ -0,0 +1,9 @@ +import { Dependency } from '@aws-amplify/plugin-types'; + +export type LockFileContents = { + dependencies: Array; +}; + +export type LockFileReader = { + getLockFileContentsFromCwd: () => Promise; +}; diff --git a/packages/cli-core/src/package-manager-controller/lock-file-reader/yarn_classic_lock_file_reader.test.ts b/packages/cli-core/src/package-manager-controller/lock-file-reader/yarn_classic_lock_file_reader.test.ts index fb3585106e..38afb7860b 100644 --- a/packages/cli-core/src/package-manager-controller/lock-file-reader/yarn_classic_lock_file_reader.test.ts +++ b/packages/cli-core/src/package-manager-controller/lock-file-reader/yarn_classic_lock_file_reader.test.ts @@ -56,10 +56,25 @@ some_other_dep@12.13.14: assert.strictEqual(fspReadFileMock.mock.callCount(), 1); }); - void it('returns empty lock file contents when yarn.lock is not present or parse-able', async () => { + void it('returns undefined when yarn.lock is not present or parse-able', async () => { fspReadFileMock.mock.mockImplementationOnce(() => Promise.reject(new Error()) ); + const lockFileContents = + await yarnClassicLockFileReader.getLockFileContentsFromCwd(); + assert.deepEqual(lockFileContents, undefined); + }); + + void it('returns empty dependency array when yarn.lock does not have dependencies', async () => { + mock.method( + fsp, + 'readFile', + () => `# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 + + +` + ); const lockFileContents = await yarnClassicLockFileReader.getLockFileContentsFromCwd(); assert.deepEqual(lockFileContents, { dependencies: [] }); diff --git a/packages/cli-core/src/package-manager-controller/lock-file-reader/yarn_classic_lock_file_reader.ts b/packages/cli-core/src/package-manager-controller/lock-file-reader/yarn_classic_lock_file_reader.ts index 8abc4779b1..cd12059485 100644 --- a/packages/cli-core/src/package-manager-controller/lock-file-reader/yarn_classic_lock_file_reader.ts +++ b/packages/cli-core/src/package-manager-controller/lock-file-reader/yarn_classic_lock_file_reader.ts @@ -1,10 +1,7 @@ -import { - Dependency, - LockFileContents, - LockFileReader, -} from '@aws-amplify/plugin-types'; +import { Dependency } from '@aws-amplify/plugin-types'; import fsp from 'fs/promises'; import path from 'path'; +import { LockFileContents, LockFileReader } from './types.js'; import { printer } from '../../printer.js'; import { LogLevel } from '../../printer/printer.js'; @@ -12,16 +9,18 @@ import { LogLevel } from '../../printer/printer.js'; * YarnClassicLockFileReader is an abstraction around the logic used to read and parse lock file contents */ export class YarnClassicLockFileReader implements LockFileReader { - getLockFileContentsFromCwd = async (): Promise => { + getLockFileContentsFromCwd = async (): Promise< + LockFileContents | undefined + > => { const eolRegex = '[\r\n]'; const dependencies: Array = []; const yarnLockPath = path.resolve(process.cwd(), 'yarn.lock'); try { const yarnLockContents = await fsp.readFile(yarnLockPath, 'utf-8'); - const yarnLockContentsArray = yarnLockContents.split( - new RegExp(`${eolRegex}${eolRegex}`) - ); + const yarnLockContentsArray = yarnLockContents + .trim() + .split(new RegExp(`${eolRegex}${eolRegex}`)); // Slice to remove comment block at the start of the lock file for (const yarnDependencyBlock of yarnLockContentsArray.slice(1)) { @@ -45,7 +44,7 @@ export class YarnClassicLockFileReader implements LockFileReader { `Failed to get lock file contents because ${yarnLockPath} does not exist or is not parse-able`, LogLevel.DEBUG ); - return { dependencies }; + return; } return { dependencies }; diff --git a/packages/cli-core/src/package-manager-controller/lock-file-reader/yarn_modern_lock_file_reader.test.ts b/packages/cli-core/src/package-manager-controller/lock-file-reader/yarn_modern_lock_file_reader.test.ts index 1b1a5213a5..461b88f66a 100644 --- a/packages/cli-core/src/package-manager-controller/lock-file-reader/yarn_modern_lock_file_reader.test.ts +++ b/packages/cli-core/src/package-manager-controller/lock-file-reader/yarn_modern_lock_file_reader.test.ts @@ -63,10 +63,33 @@ __metadata: assert.strictEqual(fspReadFileMock.mock.callCount(), 1); }); - void it('returns empty lock file contents when yarn.lock is not present or parse-able', async () => { + void it('returns undefined when yarn.lock is not present or parse-able', async () => { fspReadFileMock.mock.mockImplementationOnce(() => Promise.reject(new Error()) ); + const lockFileContents = + await yarnModernLockFileReader.getLockFileContentsFromCwd(); + assert.deepEqual(lockFileContents, undefined); + }); + + void it('returns empty dependency array when yarn.lock does not have dependencies', async () => { + mock.method( + fsp, + 'readFile', + () => `# This file is generated by running "yarn install" inside your project. +# Manual changes might be lost - proceed with caution! + +__metadata: + version: 8 + cacheKey: 10c0 + +"testapp@workspace:.": + version: 0.0.0-use.local + resolution: "testapp@workspace:." + languageName: unknown + linkType: soft +` + ); const lockFileContents = await yarnModernLockFileReader.getLockFileContentsFromCwd(); assert.deepEqual(lockFileContents, { dependencies: [] }); diff --git a/packages/cli-core/src/package-manager-controller/lock-file-reader/yarn_modern_lock_file_reader.ts b/packages/cli-core/src/package-manager-controller/lock-file-reader/yarn_modern_lock_file_reader.ts index 3c8423f5e9..868c6dc8d0 100644 --- a/packages/cli-core/src/package-manager-controller/lock-file-reader/yarn_modern_lock_file_reader.ts +++ b/packages/cli-core/src/package-manager-controller/lock-file-reader/yarn_modern_lock_file_reader.ts @@ -1,10 +1,7 @@ -import { - Dependency, - LockFileContents, - LockFileReader, -} from '@aws-amplify/plugin-types'; +import { Dependency } from '@aws-amplify/plugin-types'; import fsp from 'fs/promises'; import path from 'path'; +import { LockFileContents, LockFileReader } from './types.js'; import { printer } from '../../printer.js'; import { LogLevel } from '../../printer/printer.js'; @@ -12,7 +9,9 @@ import { LogLevel } from '../../printer/printer.js'; * YarnModernLockFileReader is an abstraction around the logic used to read and parse lock file contents */ export class YarnModernLockFileReader implements LockFileReader { - getLockFileContentsFromCwd = async (): Promise => { + getLockFileContentsFromCwd = async (): Promise< + LockFileContents | undefined + > => { const eolRegex = '[\r\n]'; const dependencies: Array = []; const yarnLockPath = path.resolve(process.cwd(), 'yarn.lock'); @@ -23,6 +22,11 @@ export class YarnModernLockFileReader implements LockFileReader { new RegExp(`${eolRegex}${eolRegex}`) ); + if (yarnLockContentsArray.length === 3) { + // Contents are only comment block, metadata, and workspace info + return { dependencies }; + } + // Slice to remove comment block and metadata at the start of the lock file for (const yarnDependencyBlock of yarnLockContentsArray.slice(2)) { const yarnDependencyLines = yarnDependencyBlock @@ -45,7 +49,7 @@ export class YarnModernLockFileReader implements LockFileReader { `Failed to get lock file contents because ${yarnLockPath} does not exist or is not parse-able`, LogLevel.DEBUG ); - return { dependencies }; + return; } return { dependencies }; diff --git a/packages/cli-core/src/package-manager-controller/npm_package_manager_controller.test.ts b/packages/cli-core/src/package-manager-controller/npm_package_manager_controller.test.ts index 072bd1c14c..4f7aa2d9db 100644 --- a/packages/cli-core/src/package-manager-controller/npm_package_manager_controller.test.ts +++ b/packages/cli-core/src/package-manager-controller/npm_package_manager_controller.test.ts @@ -5,7 +5,7 @@ import assert from 'assert'; import { execa } from 'execa'; import { NpmPackageManagerController } from './npm_package_manager_controller.js'; import { executeWithDebugLogger } from './execute_with_debugger_logger.js'; -import { LockFileReader } from '@aws-amplify/plugin-types'; +import { LockFileReader } from './lock-file-reader/types.js'; void describe('NpmPackageManagerController', () => { const fspMock = { @@ -127,9 +127,8 @@ void describe('NpmPackageManagerController', () => { }); void describe('getDependencies', () => { - const existsSyncMock = mock.fn(() => true); - void it('successfully returns dependency versions', async () => { + const existsSyncMock = mock.fn(() => true); const lockFileReaderMock = { getLockFileContentsFromCwd: async () => Promise.resolve({ @@ -163,7 +162,7 @@ void describe('NpmPackageManagerController', () => { lockFileReaderMock ); const dependencyVersions = - await npmPackageManagerController.getDependencies(); + await npmPackageManagerController.tryGetDependencies(); const expectedVersions = [ { name: 'aws-cdk', @@ -173,40 +172,17 @@ void describe('NpmPackageManagerController', () => { name: 'aws-cdk-lib', version: '12.13.14', }, + { + name: 'test_dep', + version: '1.23.45', + }, + { + name: 'some_other_dep', + version: '12.1.3', + }, ]; assert.deepEqual(dependencyVersions, expectedVersions); }); - - void it('returns empty array if there are no matching dependencies', async () => { - const lockFileReaderMock = { - getLockFileContentsFromCwd: async () => - Promise.resolve({ - dependencies: [ - { - name: 'test_dep', - version: '1.23.45', - }, - { - name: 'some_other_dep', - version: '12.1.3', - }, - ], - }), - } as LockFileReader; - const npmPackageManagerController = new NpmPackageManagerController( - '/testProjectRoot', - fspMock as unknown as typeof fsp, - pathMock as unknown as typeof path, - execaMock as unknown as typeof execa, - executeWithDebugLoggerMock as unknown as typeof executeWithDebugLogger, - existsSyncMock, - lockFileReaderMock - ); - const dependencyVersions = - await npmPackageManagerController.getDependencies(); - - assert.deepEqual(dependencyVersions, []); - }); }); }); diff --git a/packages/cli-core/src/package-manager-controller/package_manager_controller_base.ts b/packages/cli-core/src/package-manager-controller/package_manager_controller_base.ts index 8946fec30e..be83839c87 100644 --- a/packages/cli-core/src/package-manager-controller/package_manager_controller_base.ts +++ b/packages/cli-core/src/package-manager-controller/package_manager_controller_base.ts @@ -6,13 +6,13 @@ import { Dependency, ExecaChildProcess, ExecaOptions, - LockFileReader, type PackageManagerController, } from '@aws-amplify/plugin-types'; import { LogLevel } from '../printer/printer.js'; import { printer } from '../printer.js'; import { executeWithDebugLogger as _executeWithDebugLogger } from './execute_with_debugger_logger.js'; import { getPackageManagerRunnerName } from './get_package_manager_name.js'; +import { LockFileReader } from './lock-file-reader/types.js'; /** * PackageManagerController is an abstraction around package manager commands that are needed to initialize a project and install dependencies @@ -149,22 +149,13 @@ export abstract class PackageManagerControllerBase allowsSignalPropagation = () => true; /** - * getDependencies - Retrieves dependency versions from the lock file in the project root + * tryGetDependencies - Tries to retrieve dependency versions from the lock file in the project root */ - getDependencies = async (): Promise> => { + tryGetDependencies = async (): Promise | undefined> => { const lockFileContents = await this.lockFileReader.getLockFileContentsFromCwd(); - const targetDependencies = ['aws-cdk', 'aws-cdk-lib']; - const dependencyVersions: Array = []; - - for (const { name, version } of lockFileContents.dependencies) { - if (targetDependencies.includes(name)) { - dependencyVersions.push({ name, version }); - } - } - - return dependencyVersions; + return lockFileContents?.dependencies; }; /** diff --git a/packages/cli-core/src/package-manager-controller/pnpm_package_manager_controller.test.ts b/packages/cli-core/src/package-manager-controller/pnpm_package_manager_controller.test.ts index 83b631390f..3e3c7bc342 100644 --- a/packages/cli-core/src/package-manager-controller/pnpm_package_manager_controller.test.ts +++ b/packages/cli-core/src/package-manager-controller/pnpm_package_manager_controller.test.ts @@ -5,7 +5,7 @@ import assert from 'assert'; import { execa } from 'execa'; import { PnpmPackageManagerController } from './pnpm_package_manager_controller.js'; import { executeWithDebugLogger } from './execute_with_debugger_logger.js'; -import { LockFileReader } from '@aws-amplify/plugin-types'; +import { LockFileReader } from './lock-file-reader/types.js'; void describe('PnpmPackageManagerController', () => { const fspMock = { @@ -127,9 +127,8 @@ void describe('PnpmPackageManagerController', () => { }); void describe('getDependencies', () => { - const existsSyncMock = mock.fn(() => true); - void it('successfully returns dependency versions', async () => { + const existsSyncMock = mock.fn(() => true); const lockFileReaderMock = { getLockFileContentsFromCwd: async () => Promise.resolve({ @@ -163,7 +162,7 @@ void describe('PnpmPackageManagerController', () => { lockFileReaderMock ); const dependencyVersions = - await pnpmPackageManagerController.getDependencies(); + await pnpmPackageManagerController.tryGetDependencies(); const expectedVersions = [ { name: 'aws-cdk', @@ -173,40 +172,17 @@ void describe('PnpmPackageManagerController', () => { name: 'aws-cdk-lib', version: '12.13.14', }, + { + name: 'test_dep', + version: '1.23.45', + }, + { + name: 'some_other_dep', + version: '12.1.3', + }, ]; assert.deepEqual(dependencyVersions, expectedVersions); }); - - void it('returns empty array if there are no matching dependencies', async () => { - const lockFileReaderMock = { - getLockFileContentsFromCwd: async () => - Promise.resolve({ - dependencies: [ - { - name: 'test_dep', - version: '1.23.45', - }, - { - name: 'some_other_dep', - version: '12.1.3', - }, - ], - }), - } as LockFileReader; - const pnpmPackageManagerController = new PnpmPackageManagerController( - '/testProjectRoot', - fspMock as unknown as typeof fsp, - pathMock as unknown as typeof path, - execaMock as unknown as typeof execa, - executeWithDebugLoggerMock as unknown as typeof executeWithDebugLogger, - existsSyncMock, - lockFileReaderMock - ); - const dependencyVersions = - await pnpmPackageManagerController.getDependencies(); - - assert.deepEqual(dependencyVersions, []); - }); }); }); diff --git a/packages/cli-core/src/package-manager-controller/yarn_classic_package_manager_controller.test.ts b/packages/cli-core/src/package-manager-controller/yarn_classic_package_manager_controller.test.ts index 00ac91fc6e..4c35e60e77 100644 --- a/packages/cli-core/src/package-manager-controller/yarn_classic_package_manager_controller.test.ts +++ b/packages/cli-core/src/package-manager-controller/yarn_classic_package_manager_controller.test.ts @@ -5,7 +5,7 @@ import assert from 'assert'; import { execa } from 'execa'; import { YarnClassicPackageManagerController } from './yarn_classic_package_manager_controller.js'; import { executeWithDebugLogger } from './execute_with_debugger_logger.js'; -import { LockFileReader } from '@aws-amplify/plugin-types'; +import { LockFileReader } from './lock-file-reader/types.js'; void describe('YarnClassicPackageManagerController', () => { const fspMock = { @@ -131,9 +131,8 @@ void describe('YarnClassicPackageManagerController', () => { }); void describe('getDependencies', () => { - const existsSyncMock = mock.fn(() => true); - void it('successfully returns dependency versions', async () => { + const existsSyncMock = mock.fn(() => true); const lockFileReaderMock = { getLockFileContentsFromCwd: async () => Promise.resolve({ @@ -168,7 +167,7 @@ void describe('YarnClassicPackageManagerController', () => { lockFileReaderMock ); const dependencyVersions = - await yarnClassicPackageManagerController.getDependencies(); + await yarnClassicPackageManagerController.tryGetDependencies(); const expectedVersions = [ { name: 'aws-cdk', @@ -178,41 +177,17 @@ void describe('YarnClassicPackageManagerController', () => { name: 'aws-cdk-lib', version: '12.13.14', }, + { + name: 'test_dep', + version: '1.23.45', + }, + { + name: 'some_other_dep', + version: '12.1.3', + }, ]; assert.deepEqual(dependencyVersions, expectedVersions); }); - - void it('returns empty array if there are no matching dependencies', async () => { - const lockFileReaderMock = { - getLockFileContentsFromCwd: async () => - Promise.resolve({ - dependencies: [ - { - name: 'test_dep', - version: '1.23.45', - }, - { - name: 'some_other_dep', - version: '12.1.3', - }, - ], - }), - } as LockFileReader; - const yarnClassicPackageManagerController = - new YarnClassicPackageManagerController( - '/testProjectRoot', - fspMock as unknown as typeof fsp, - pathMock as unknown as typeof path, - execaMock as unknown as typeof execa, - executeWithDebugLoggerMock as unknown as typeof executeWithDebugLogger, - existsSyncMock, - lockFileReaderMock - ); - const dependencyVersions = - await yarnClassicPackageManagerController.getDependencies(); - - assert.deepEqual(dependencyVersions, []); - }); }); }); diff --git a/packages/cli-core/src/package-manager-controller/yarn_modern_package_manager_controller.test.ts b/packages/cli-core/src/package-manager-controller/yarn_modern_package_manager_controller.test.ts index 215c573948..83119e1aef 100644 --- a/packages/cli-core/src/package-manager-controller/yarn_modern_package_manager_controller.test.ts +++ b/packages/cli-core/src/package-manager-controller/yarn_modern_package_manager_controller.test.ts @@ -6,7 +6,7 @@ import { execa } from 'execa'; import { Printer } from '../printer/printer.js'; import { YarnModernPackageManagerController } from './yarn_modern_package_manager_controller.js'; import { executeWithDebugLogger } from './execute_with_debugger_logger.js'; -import { LockFileReader } from '@aws-amplify/plugin-types'; +import { LockFileReader } from './lock-file-reader/types.js'; void describe('YarnModernPackageManagerController', () => { const fspMock = { @@ -126,9 +126,8 @@ void describe('YarnModernPackageManagerController', () => { }); void describe('getDependencies', () => { - const existsSyncMock = mock.fn(() => true); - void it('successfully returns dependency versions', async () => { + const existsSyncMock = mock.fn(() => true); const lockFileReaderMock = { getLockFileContentsFromCwd: async () => Promise.resolve({ @@ -164,7 +163,7 @@ void describe('YarnModernPackageManagerController', () => { lockFileReaderMock ); const dependencyVersions = - await yarnModernPackageManagerController.getDependencies(); + await yarnModernPackageManagerController.tryGetDependencies(); const expectedVersions = [ { name: 'aws-cdk', @@ -174,42 +173,17 @@ void describe('YarnModernPackageManagerController', () => { name: 'aws-cdk-lib', version: '12.13.14', }, + { + name: 'test_dep', + version: '1.23.45', + }, + { + name: 'some_other_dep', + version: '12.1.3', + }, ]; assert.deepEqual(dependencyVersions, expectedVersions); }); - - void it('returns empty array if there are no matching dependencies', async () => { - const lockFileReaderMock = { - getLockFileContentsFromCwd: async () => - Promise.resolve({ - dependencies: [ - { - name: 'test_dep', - version: '1.23.45', - }, - { - name: 'some_other_dep', - version: '12.1.3', - }, - ], - }), - } as LockFileReader; - const yarnModernPackageManagerController = - new YarnModernPackageManagerController( - '/testProjectRoot', - printerMock as unknown as Printer, - fspMock as unknown as typeof fsp, - pathMock as unknown as typeof path, - execaMock as unknown as typeof execa, - executeWithDebugLoggerMock as unknown as typeof executeWithDebugLogger, - existsSyncMock, - lockFileReaderMock - ); - const dependencyVersions = - await yarnModernPackageManagerController.getDependencies(); - - assert.deepEqual(dependencyVersions, []); - }); }); }); diff --git a/packages/cli/src/ampx.ts b/packages/cli/src/ampx.ts index 8d97e526a1..c9448a58d1 100755 --- a/packages/cli/src/ampx.ts +++ b/packages/cli/src/ampx.ts @@ -29,7 +29,7 @@ if (libraryVersion == undefined) { const dependencies = await new PackageManagerControllerFactory() .getPackageManagerController() - .getDependencies(); + .tryGetDependencies(); const usageDataEmitter = await new UsageDataEmitterFactory().getInstance( libraryVersion, diff --git a/packages/cli/src/commands/sandbox/sandbox_command_factory.ts b/packages/cli/src/commands/sandbox/sandbox_command_factory.ts index c63403f947..eace366cd2 100644 --- a/packages/cli/src/commands/sandbox/sandbox_command_factory.ts +++ b/packages/cli/src/commands/sandbox/sandbox_command_factory.ts @@ -64,7 +64,7 @@ export const createSandboxCommand = (): CommandModule< async () => { const dependencies = await new PackageManagerControllerFactory() .getPackageManagerController() - .getDependencies(); + .tryGetDependencies(); return await new UsageDataEmitterFactory().getInstance( libraryVersion, dependencies diff --git a/packages/create-amplify/src/amplify_project_creator.test.ts b/packages/create-amplify/src/amplify_project_creator.test.ts index a88c08efcb..cf4675f9b1 100644 --- a/packages/create-amplify/src/amplify_project_creator.test.ts +++ b/packages/create-amplify/src/amplify_project_creator.test.ts @@ -109,7 +109,7 @@ void describe('AmplifyProjectCreator', () => { runWithPackageManager: mock.fn(() => Promise.resolve() as never), getCommand: (args: string[]) => `'npx ${args.join(' ')}'`, allowsSignalPropagation: () => true, - getDependencies: mock.fn(() => Promise.resolve([])), + tryGetDependencies: mock.fn(() => Promise.resolve([])), }; const projectRootValidatorMock = { validate: mock.fn() }; const gitIgnoreInitializerMock = { ensureInitialized: mock.fn() }; diff --git a/packages/create-amplify/src/initial_project_file_generator.test.ts b/packages/create-amplify/src/initial_project_file_generator.test.ts index 4d5314e680..39bed0b488 100644 --- a/packages/create-amplify/src/initial_project_file_generator.test.ts +++ b/packages/create-amplify/src/initial_project_file_generator.test.ts @@ -17,7 +17,7 @@ void describe('InitialProjectFileGenerator', () => { runWithPackageManager: mock.fn(() => Promise.resolve() as never), getCommand: (args: string[]) => `'npx ${args.join(' ')}'`, allowsSignalPropagation: () => true, - getDependencies: mock.fn(() => Promise.resolve([])), + tryGetDependencies: mock.fn(() => Promise.resolve([])), }; beforeEach(() => { executeWithDebugLoggerMock.mock.resetCalls(); diff --git a/packages/platform-core/API.md b/packages/platform-core/API.md index 092d8aee7f..4ef316c349 100644 --- a/packages/platform-core/API.md +++ b/packages/platform-core/API.md @@ -231,7 +231,7 @@ export type UsageDataEmitter = { // @public export class UsageDataEmitterFactory { - getInstance: (libraryVersion: string, dependencies: Array) => Promise; + getInstance: (libraryVersion: string, dependencies?: Array) => Promise; } // (No @packageDocumentation comment for this package) diff --git a/packages/platform-core/src/usage-data/usage_data_emitter.test.ts b/packages/platform-core/src/usage-data/usage_data_emitter.test.ts index 281fa69142..189c9b57d7 100644 --- a/packages/platform-core/src/usage-data/usage_data_emitter.test.ts +++ b/packages/platform-core/src/usage-data/usage_data_emitter.test.ts @@ -21,13 +21,21 @@ void describe('UsageDataEmitter', () => { const testLibraryVersion = '1.2.3'; const testDependencies = [ { - name: 'test-dep', + name: 'aws-cdk', version: '1.2.3', }, { - name: 'some_other_dep', + name: 'aws-cdk-lib', version: '12.13.14', }, + { + name: 'test-dep', + version: '1.2.4', + }, + { + name: 'some_other_dep', + version: '12.12.14', + }, ]; const testURL = url.parse('https://aws.amazon.com/amplify/'); const onReqEndMock = mock.fn(); @@ -101,6 +109,19 @@ void describe('UsageDataEmitter', () => { assert.ok(validate(usageDataSent.installationUuid)); assert.ok(usageDataSent.error == undefined); assert.ok(usageDataSent.downstreamException == undefined); + assert.deepStrictEqual( + usageDataSent.projectSetting.details, + JSON.stringify([ + { + name: 'aws-cdk', + version: '1.2.3', + }, + { + name: 'aws-cdk-lib', + version: '12.13.14', + }, + ]) + ); }); void test('happy case, emitFailure generates and send correct usage data', async () => { diff --git a/packages/platform-core/src/usage-data/usage_data_emitter.ts b/packages/platform-core/src/usage-data/usage_data_emitter.ts index 653fb4f513..82616760aa 100644 --- a/packages/platform-core/src/usage-data/usage_data_emitter.ts +++ b/packages/platform-core/src/usage-data/usage_data_emitter.ts @@ -16,16 +16,23 @@ import { Dependency } from '@aws-amplify/plugin-types'; * Entry point for sending usage data metrics */ export class DefaultUsageDataEmitter implements UsageDataEmitter { + private dependenciesToReport?: Array; /** * Constructor for UsageDataEmitter */ constructor( private readonly libraryVersion: string, - private readonly dependencies: Array, + private readonly dependencies?: Array, private readonly sessionUuid = uuid(), private readonly url = getUrl(), private readonly accountIdFetcher = new AccountIdFetcher() - ) {} + ) { + const targetDependencies = ['aws-cdk', 'aws-cdk-lib']; + + this.dependenciesToReport = this.dependencies?.filter((dependency) => + targetDependencies.includes(dependency.name) + ); + } emitSuccess = async ( metrics?: Record, @@ -90,7 +97,7 @@ export class DefaultUsageDataEmitter implements UsageDataEmitter { isCi: isCI, projectSetting: { editor: process.env.npm_config_user_agent, - details: JSON.stringify(this.dependencies), + details: JSON.stringify(this.dependenciesToReport), }, }; }; diff --git a/packages/platform-core/src/usage-data/usage_data_emitter_factory.ts b/packages/platform-core/src/usage-data/usage_data_emitter_factory.ts index f4466f1e83..c8134c0e09 100644 --- a/packages/platform-core/src/usage-data/usage_data_emitter_factory.ts +++ b/packages/platform-core/src/usage-data/usage_data_emitter_factory.ts @@ -25,7 +25,7 @@ export class UsageDataEmitterFactory { */ getInstance = async ( libraryVersion: string, - dependencies: Array + dependencies?: Array ): Promise => { const configController = configControllerFactory.getInstance( 'usage_data_preferences.json' diff --git a/packages/plugin-types/API.md b/packages/plugin-types/API.md index e6ecbae1b5..b289307f05 100644 --- a/packages/plugin-types/API.md +++ b/packages/plugin-types/API.md @@ -202,16 +202,6 @@ export type ImportPathVerifier = { verify: (importStack: string | undefined, expectedImportingFile: string, errorMessage: string) => void; }; -// @public (undocumented) -export type LockFileContents = { - dependencies: Array; -}; - -// @public (undocumented) -export type LockFileReader = { - getLockFileContentsFromCwd: () => Promise; -}; - // @public (undocumented) export type LogLevel = 'all' | 'debug' | 'error' | 'fatal' | 'info' | 'none' | 'trace' | 'warn'; @@ -236,7 +226,7 @@ export type PackageManagerController = { runWithPackageManager: (args: string[] | undefined, dir: string, options?: ExecaOptions) => ExecaChildProcess; getCommand: (args: string[]) => string; allowsSignalPropagation: () => boolean; - getDependencies: () => Promise>; + tryGetDependencies: () => Promise | undefined>; }; // @public (undocumented) diff --git a/packages/plugin-types/src/package_manager_controller.ts b/packages/plugin-types/src/package_manager_controller.ts index 9eed9e1e80..d583960c43 100644 --- a/packages/plugin-types/src/package_manager_controller.ts +++ b/packages/plugin-types/src/package_manager_controller.ts @@ -25,14 +25,6 @@ export type ExecaChildProcess = { export type Dependency = { name: string; version: string }; -export type LockFileContents = { - dependencies: Array; -}; - -export type LockFileReader = { - getLockFileContentsFromCwd: () => Promise; -}; - export type PackageManagerController = { initializeProject: () => Promise; initializeTsConfig: (targetDir: string) => Promise; @@ -47,5 +39,5 @@ export type PackageManagerController = { ) => ExecaChildProcess; getCommand: (args: string[]) => string; allowsSignalPropagation: () => boolean; - getDependencies: () => Promise>; + tryGetDependencies: () => Promise | undefined>; }; From 2d891c2f75027b6635655df23f4d3820d5c66420 Mon Sep 17 00:00:00 2001 From: Roshane Pascual Date: Thu, 9 Jan 2025 15:12:58 -0800 Subject: [PATCH 8/8] fix lint --- .eslint_dictionary.json | 1 + 1 file changed, 1 insertion(+) diff --git a/.eslint_dictionary.json b/.eslint_dictionary.json index c207869a7d..bac7d03306 100644 --- a/.eslint_dictionary.json +++ b/.eslint_dictionary.json @@ -169,6 +169,7 @@ "subpath", "syncable", "synthing", + "testapp", "testname", "testnamebucket", "testuser",