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
};
}