Files
maven-dependency-submission…/src/depgraph.ts
T

211 lines
6.3 KiB
TypeScript
Raw Normal View History

2022-08-12 12:15:51 +00:00
import * as fs from 'fs';
import { PackageURL } from 'packageurl-js'
import { PackageCache, Package, Manifest } from '@github/dependency-submission-toolkit';
import { DependencyScope } from '@github/dependency-submission-toolkit/dist/manifest';
type Depgraph = {
graphName: string,
artifacts: DepgraphArtifact[],
dependencies: DepgraphDependency[],
}
type DepgraphArtifact = {
id: string,
numericId: number,
groupId: string,
artifactId: string,
version: string,
optional?: boolean,
scopes?: string[],
types?: string[],
}
type DepgraphDependency = {
from: string,
to: string,
numericFrom: number,
numericTo: number,
resolution: string,
}
export class MavenDependencyGraph {
private depGraph: Depgraph;
private packageUrlToArtifact: Map<string, DepgraphArtifact>;
private cache: PackageCache;
private directDependencies: Array<Package>;
// //@ ts-ignore
// private rootPackage: DepgraphArtifact;
constructor(graph: Depgraph) {
this.depGraph = graph;
this.cache = new PackageCache();
this.packageUrlToArtifact = new Map();
this.directDependencies = [];
this.parseDependencies();
}
getProjectName() {
return this.depGraph.graphName;
}
getAllPackageUrls() {
return Object.keys(this.packageUrlToArtifact);
}
getArtifactForPackageUrl(packageUrl: string) {
return this.packageUrlToArtifact[packageUrl];
}
getDirectDependencies() {
return this.directDependencies;
}
getPackageCount() {
return this.cache.countPackages();
}
createManifest(filePath?: string): Manifest {
// The project name is not shown in the UI when you utilize a file path currently, but the file path is required to link up to the repository file
// which is more beneficial at this point.
const manifest = new Manifest(this.getProjectName(), filePath);
2022-08-12 12:15:51 +00:00
const packageUrlToArtifact = this.packageUrlToArtifact;
this.directDependencies.forEach(depPackage => {
const artifact = this.packageUrlToArtifact[depPackage.packageURL.toString()];
let scope = getDependencyScopeForMavenScope(artifact.scopes);
manifest.addDirectDependency(depPackage, scope);
function addTransitiveDeps(dependencies) {
if (dependencies) {
dependencies.forEach(transitiveDep => {
const transitiveDepArtifact = packageUrlToArtifact[transitiveDep.packageURL.toString()];
const transitiveDepScope = getDependencyScopeForMavenScope(transitiveDepArtifact.scopes);
manifest.addIndirectDependency(transitiveDep, transitiveDepScope);
addTransitiveDeps(transitiveDep.dependencies);
});
}
}
addTransitiveDeps(depPackage.dependencies);
});
return manifest;
}
private parseDependencies() {
const graph = this.depGraph;
const cache = this.cache;
const rootPackageArtifactId = graph.graphName;
let rootPackageNumericId = 0;
const dependencyIdMap = dependencyMap(graph.dependencies);
const idToPackageCachePackage: Map<string, Package> = new Map<string, Package>();
2022-08-12 12:15:51 +00:00
// Create the packages for all known artifacts
graph.artifacts.forEach((artifact: DepgraphArtifact) => {
const artifactUrl: PackageURL = artifactToPackageURL(artifact);
const pkg = cache.package(artifactUrl);
idToPackageCachePackage[artifact.id] = pkg;
// Store a reference from the package URL to the original artifact as the artifact has extra metadata we need later for scopes and optionality
this.packageUrlToArtifact[artifactUrl.toString()] = artifact;
if (artifact.artifactId === rootPackageArtifactId) {
rootPackageNumericId = artifact.numericId - 1;
}
});
// Now that all packages are known, process the dependencies for each and link them
Object.keys(dependencyIdMap).forEach(fromId => {
const pkg: Package = idToPackageCachePackage[fromId];
if (!pkg) {
throw new Error(`Package '${fromId}' was not found in the cache.`);
}
const deps = dependencyIdMap[fromId];
if (deps) {
// Process each dependency id and link to the package in the cache
deps.forEach(dependencyId => {
const dependencyPkg = idToPackageCachePackage[dependencyId];
if (!dependencyPkg) {
throw new Error(`Failed to find a dependency package for '${dependencyId}'`);
}
pkg.dependsOn(dependencyPkg);
});
}
});
this.directDependencies = getDirectDependencies(rootPackageNumericId, graph.dependencies).map(id => { return idToPackageCachePackage[id] });
2022-08-12 12:15:51 +00:00
}
}
export function parseDependencyJson(file: string): Depgraph {
try {
const data: Buffer = fs.readFileSync(file);
try {
const depGraph: Depgraph = JSON.parse(data.toString('utf-8'));
return depGraph;
} catch (err: any) {
2022-08-12 12:15:51 +00:00
throw new Error(`Failed to parse JSON payload: ${err.message}`);
}
} catch (err: any) {
2022-08-12 12:15:51 +00:00
throw new Error(`Failed to load file ${file}: ${err}`);
}
}
export function artifactToPackageURL(artifact: DepgraphArtifact): PackageURL {
return new PackageURL(
'maven',
artifact.groupId,
artifact.artifactId,
artifact.version,
undefined,
undefined
);
}
function getDependencyScopeForMavenScope(mavenScopes: string[] | undefined | null): DependencyScope {
// Once the API scopes are improved and expanded we should be able to perform better mapping here from Maven to cater for
// provided, runtime, compile, test, system, etc... in the future.
if (mavenScopes) {
if (mavenScopes.includes('test')) {
return 'development';
}
}
// The default scope for now as we only have runtime and development currently
return 'runtime';
}
function dependencyMap(dependencies: DepgraphDependency[]): Map<string, string[] | undefined> {
const map = new Map<string, string[]>();
dependencies.forEach(dependency => {
const fromUrl = dependency.from;
let deps = map[fromUrl];
if (!deps) {
deps = [];
map[fromUrl] = deps;
}
deps.push(dependency.to);
});
return map;
}
function getDirectDependencies(rootPackageNumericId: number, dependencies: DepgraphDependency[]): string[] {
const topLevel = dependencies.filter(dependency => { return dependency.numericFrom === rootPackageNumericId; });
return topLevel.map(dep => { return dep.to; });
}