Skip to content

Instantly share code, notes, and snippets.

@pierre-ernst
Created October 31, 2019 15:28
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save pierre-ernst/0af6a28f46eca6f5c0e831fd3bfc00e8 to your computer and use it in GitHub Desktop.
Save pierre-ernst/0af6a28f46eca6f5c0e831fd3bfc00e8 to your computer and use it in GitHub Desktop.
Builds a DOT-notation dependency graph from a yarn.lock file. Can be used to produce SVG or PNG images. $ sfdp -Gsize=50! -Goverlap=prism -Tsvg tree.dot > tree.svg
package com.github.pierre_ernst;
import org.json.JSONObject;
import org.json.JSONTokener;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.PrintStream;
import java.util.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* Builds a DOT-notation dependency graph from a yarn.lock file
* Can be used to produce SVG or PNG images
* $ sfdp -Gsize=50! -Goverlap=prism -Tsvg tree.dot > tree.svg
*
* https://en.wikipedia.org/wiki/DOT_language
*/
public class YarnLock2Dot {
private static final int MAX_NODES_PER_CLUSTER = 50;
private static final String DEP_NAME_UNESCAPED_REGEX = "[:a-zA-Z0-9_\\./-]+";
/**
* if the name is enclosed within double-quotes, then it can also contain '@'
*/
private static final String DEP_NAME_ESCAPED_REGEX = "\"[@:a-zA-Z0-9_\\./-]+\"";
private static final String DEP_NAME_REGEX = "("+DEP_NAME_UNESCAPED_REGEX+"|"+DEP_NAME_ESCAPED_REGEX+")";
private static class Package implements Comparable<Package> {
private Package parent;
private String name;
private String version;
public Package(Package parent, String name, String version) {
this.parent=parent;
this.name = name;
this.version = version;
}
public Package(Package parent, String name) {
this(parent, name, null);
}
public Package getParent(){
return parent;
}
public String getName() {
return name;
}
public String getVersion() {
return version;
}
public void setVersion(String version) {
this.version = version;
}
@Override
public String toString() {
if (version == null) {
return name;
}
return name + ":" + version;
}
@Override
public int compareTo(Package aPackage) {
return this.toString().toLowerCase().compareTo(aPackage.toString().toLowerCase());
}
public int getDepth(){
int depth = 0;
Package current = this;
while (current.getParent()!=null){
current = current.getParent();
depth++;
}
return depth;
}
}
private Package rootPackage;
private String installFolderPath;
private Set<Package> transitivesDependencies ;
private YarnLock2Dot(Package rootPackage, String installFolderPath){
this.installFolderPath=installFolderPath;
this.rootPackage=rootPackage;
transitivesDependencies = new HashSet<>();
}
private static String unescape(String s){
if (s.startsWith("\"") && s.endsWith("\"")){
return s.substring(1,s.length()-2);
}
return s;
}
private static String escape(String s){
return "\""+s+"\"";
}
private void format(PrintStream ps, File lockedYarn, int depth) throws FileNotFoundException {
Scanner scanner = new Scanner(lockedYarn);
Pattern headerPattern = Pattern.compile(DEP_NAME_REGEX+"@.+");
Pattern versionPattern = Pattern.compile("version\\s+\"([^\"]+)\"");
Pattern dependencyPattern = Pattern.compile(DEP_NAME_REGEX+"\\s+\"[^\"]+\"");
String currentHeader = null;
Map<String, Package> bom = new HashMap<>();
transitivesDependencies.clear();
boolean withinDependenciesBlock = false;
while (scanner.hasNext()) {
String line = scanner.nextLine().trim();
Matcher headerMatcher = headerPattern.matcher(line);
if (headerMatcher.matches()) {
currentHeader = line;
String packageName = unescape(headerMatcher.group(1));
bom.put(currentHeader, new Package(rootPackage, packageName));
//System.out.println("+pck "+packageName);
withinDependenciesBlock = false;
} else if (currentHeader != null) {
Matcher versionMatcher = versionPattern.matcher(line);
if (versionMatcher.matches()) {
bom.get(currentHeader).setVersion(versionMatcher.group(1));
// System.out.println("+ver "+versionMatcher.group(1));
withinDependenciesBlock = false;
} else if (line.equals("dependencies:")){
withinDependenciesBlock = true;
} else{
Matcher dependencyMatcher = dependencyPattern.matcher(line);
if (withinDependenciesBlock){
if (dependencyMatcher.matches()){
String dependencyName = unescape(dependencyMatcher.group(1));
Package resolved = resolveDependencyFromInstalledPath(bom.get(currentHeader), dependencyName);
if (resolved!=null) {
transitivesDependencies.add(resolved);
}
//System.out.println("+dep "+currentHeader+"\t"+dependencyName+"\t"+resolved);
} else {
withinDependenciesBlock = false;
//System.out.println("--- ("+(currentHeader != null)+","+withinDependenciesBlock+") "+line);
}
} else {
//System.out.println("--- ("+(currentHeader != null)+","+withinDependenciesBlock+") "+line);
}
}
}
}
// Group by depth
Map<Integer, SortedSet<Package>> grouped = new HashMap<>();
// direct dependencies
grouped.put(1, new TreeSet<>(bom.values()));
// transitives dependencies
for (Package transitiveDependency : transitivesDependencies) {
int d = transitiveDependency.getDepth();
if (!grouped.containsKey(d)){
grouped.put(d, new TreeSet<>());
}
grouped.get(d).add(transitiveDependency);
}
ps.println("digraph "+escape(rootPackage.toString())+" {");
int counter=0;
for (int d:grouped.keySet()){
if (d<=depth){
// int counter=0;
for (Package transitiveDependency : grouped.get(d)) {
/*
if (((counter) % MAX_NODES_PER_CLUSTER) == 0 ){
if (counter>0){
ps.println("}");
}
ps.println("subgraph cluster"+d+"_"+(counter / MAX_NODES_PER_CLUSTER)+" {");
}
*/
ps.println(escape(transitiveDependency.getParent().toString()) + " -> " + escape(transitiveDependency.toString())+" // "+counter);
counter++;
}
//ps.println("}");
}
}
ps.println("}");
}
private Package resolveDependencyFromInstalledPath(Package parent, String name) throws FileNotFoundException {
File dependencyFolder = new File(installFolderPath,name);
if (!dependencyFolder.isDirectory()){
//System.err.println("Dependency install directory "+installFolderPath+File.separator+name+" does not exist");
} else {
File packageJson = new File(dependencyFolder,"package.json");
if (packageJson.canRead()){
JSONObject json = new JSONObject(new JSONTokener(new FileReader(packageJson)));
if (json.has("version")){
Package pack = new Package(parent,name,json.getString("version"));
if (json.has("dependencies")) {
JSONObject dependencies = json.getJSONObject("dependencies");
for (String key : dependencies.keySet()) {
Package child = resolveDependencyFromInstalledPath(pack, key);
if (child!=null){
transitivesDependencies.add(child);
}
}
}
return pack;
}
} else {
// System.err.println("Dependency package.json file "+packageJson.getAbsolutePath()+" does not exist");
}
}
return null;
}
public static void main(String... args) {
if (args.length != 5) {
System.err.println("Usage: java " + YarnLock2Dot.class.getName() + " <root-package-name> <root-package-version> <path-to-yarn.lock> <path-to-node-modules> <depth>");
} else {
try {
Package rootPackage = new Package(null, args[0], args[1]);
new YarnLock2Dot(new Package(null, args[0], args[1]), args[3]).format(System.out, new File(args[2]), Integer.valueOf(args[4]));
} catch (Exception ex) {
ex.printStackTrace(System.err);
}
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment