File

libs/sdk/src/node/journey/generate/lib/install-journey-dependencies/collect-peer-dependencies.ts

Index

Properties

Properties

dependencies (Optional)
Type Record<string | string>
devDependencies (Optional)
Type Record<string | string>
import { LibraryConfiguration } from '@allianz/taly-core/schemas';
import { readFileSync } from 'node:fs';
import { join } from 'node:path';
import { parse, rsort } from 'semver';
import { execAsync } from '../../utils/exec-async';

interface PartialPackageJson {
  dependencies?: Record<string, string>;
  devDependencies?: Record<string, string>;
}

interface DependencyMetaData {
  version: string;
  dependent: string;
  conflictsWith?: string;
  existingVersion?: string;
}

interface InstallLibPeerDependenciesOptions {
  directory: string;
  libraries: LibraryConfiguration[];
}

export async function collectPeerDependencies(
  options: InstallLibPeerDependenciesOptions
): Promise<LibraryConfiguration[]> {
  const collectedPeerDependencies = new Map<string, DependencyMetaData>();
  const conflictingDependencies = new Map<string, DependencyMetaData>();

  const rootPackageJson = JSON.parse(
    readFileSync(join(options.directory, 'package.json')).toString()
  ) as PartialPackageJson;

  for (const library of options.libraries) {
    let versionedLibraryString = `${library.package}@${library.version}`;

    // if the version is a range, we need to make sure to use the peer deps for the latest
    // version that matches that range, since that is the library version that will be installed.
    // The following block makes sure to get the latest version that satisfies that range.
    //
    // semver.parse() will return null if the version is anything other than a specific version.
    const semverVersion = parse(library.version);
    if (!semverVersion) {
      const { stdout: versions } = await execAsync(
        `npm info "${versionedLibraryString}" version --json`
      );

      const maybeVersionArray = JSON.parse(versions) as string | string[];
      let latestVersion: string;
      if (Array.isArray(maybeVersionArray)) {
        // at this point we know that `versions` is an array of versions that match the
        // requested range. Now we need to find the latest version in that array.
        // We use semver.rsort() to sort the versions in descending order and then take
        // the first element of the array.
        latestVersion = rsort(maybeVersionArray).at(0) as string;
      } else {
        latestVersion = maybeVersionArray;
      }

      versionedLibraryString = `${library.package}@${latestVersion}`;
    }

    const { stdout: peerDependenciesString } = await execAsync(
      `npm info "${versionedLibraryString}" peerDependencies --json`
    );

    if (peerDependenciesString.trim().length === 0) {
      continue;
    }

    const libraryPeerDependencies = JSON.parse(peerDependenciesString) as Record<string, string>;
    for (const peerDependencyName in libraryPeerDependencies) {
      const peerDependencyVersion = libraryPeerDependencies[peerDependencyName];
      // bail if we've see this peerDependencyName already
      const existingPeerDependency = collectedPeerDependencies.get(peerDependencyName);
      if (existingPeerDependency && existingPeerDependency.version !== peerDependencyVersion) {
        conflictingDependencies.set(peerDependencyName, {
          dependent: library.package,
          version: peerDependencyVersion,
          conflictsWith: existingPeerDependency.dependent,
          existingVersion: existingPeerDependency.version
        });
        continue;
      }

      // bail if the workspace already specifies this dependency
      const existingRootDependency = getDependencyFromPackageJson(
        rootPackageJson,
        peerDependencyName
      );
      if (existingRootDependency && existingRootDependency.version !== peerDependencyVersion) {
        conflictingDependencies.set(peerDependencyName, {
          dependent: library.package,
          version: peerDependencyVersion,
          conflictsWith: 'the workspace',
          existingVersion: existingRootDependency.version
        });
        continue;
      }

      collectedPeerDependencies.set(peerDependencyName, {
        version: peerDependencyVersion,
        dependent: library.package
      });
    }
  }

  if (conflictingDependencies.size > 0) {
    for (const [conflictingDependency, metadata] of conflictingDependencies.entries()) {
      console.warn(
        `\nThe library "${metadata.dependent}" relies on version "${metadata.version}" of "${conflictingDependency}" as a peer dependency.
However, "${metadata.conflictsWith}" already states version "${metadata.existingVersion}" of "${conflictingDependency}" as a dependency.
This request to add "${conflictingDependency}" in version "${metadata.version}" will be ignored.`
      );
    }

    console.warn(
      `\nIn case of unexpected app behavior or errors please consider asking the library authors to align the listed conflicting peer dependencies.\n`
    );
  }

  return Array.from(collectedPeerDependencies).map(([libraryName, { version }]) => ({
    package: libraryName,
    version
  }));
}

function getDependencyFromPackageJson(
  packageJson: PartialPackageJson,
  libraryName: string
): LibraryConfiguration | null {
  const version =
    packageJson.dependencies?.[libraryName] ?? packageJson.devDependencies?.[libraryName];
  if (!version) {
    return null;
  }

  return {
    package: libraryName,
    version
  };
}

results matching ""

    No results matching ""