Visualizing yarn.lock files using JavaScript and Pharo

Yarn is a package manager for Node.js JavaScript.

This page describes how to parse and visualize yarn.lock files. A yarn file can be dowloaded from a url or loaded from disk.

Note: In case the JavaScript snippet is not working view, please check JavaScript snippet.

Let's get ourselves a yarn.lock:

We get the file from a GitHub project. We do this through a Pharo snippet:

yarnLockUrl := ''.
response := ZnEasy get: yarnLockUrl.
response isSuccess 
	ifFalse: [ Error signal: 'Could not download file' ].
lockFileContent := response contents

Now that we have the contents of the file, we need to parse it. So, we use a JavaScript snippet to do just that and transform it to JSON.

Before we run the JavaScript code, we first need to load the module into the remote server:

JSLinkApplication start.
JSLinkApplication uniqueInstance installModule: '@yarnpkg/lockfile'

Now, we can run the code to parse the contents:

var lockFile = require('@yarnpkg/lockfile');
var parsedLockFileJson = lockFile.parse(lockFileContent);
var lockFileJsonString = JSON.stringify(parsedLockFileJson);

Ok, the JSON is here. We can just focus on extracting and visualizing the dependencies.

Extract the projects defined inside the yarn lock file.

lockFileJson := STONJSON fromString: lockFileJsonString.
lockFileData := lockFileJson at: 'object'.

Create a list of nodes and connect them with edges.

projectIds := OrderedCollection new.
edges := OrderedCollection new.

lockFileData associations collect: [ :assoc | 
	| projectName versionName projectId |
	projectName := assoc key 
		copyFrom: 1 to: (assoc key indexOf: $@ startingAt: 2) - 1.
	versionName := assoc value at: 'version'.
	projectId := projectName,'@', versionName.
	(projectIds includes: projectId) ifFalse: [
		projectIds add: projectId ].
	(assoc value includesKey: 'dependencies') ifTrue: [ 
		(assoc value at: 'dependencies') 
			keysAndValuesDo: [ :dependencyName :dependencyValue |
				| targetNodeId edge |
				targetNodeId := lockFileData 
					at: dependencyName, '@', dependencyValue
					ifPresent: [ :aTargetNode |
						dependencyName, '@', (aTargetNode at: #version) ]
					ifAbsent: [
						dependencyName, '@', dependencyValue ].
				edge := projectId -> targetNodeId.
				(edges includes: edge) ifFalse: [
					edges add: edge ] ] ] ].
mainProjectName := 'd3'.
rootProjectIds := projectIds copy.
edges do: [ :anEdge |
	rootProjectIds remove: anEdge value ifAbsent: [] ].

rootProjectIds do: [ :aRootProjectId | 
	edges add: mainProjectName -> aRootProjectId ].

projectIds add: mainProjectName.
{projectIds. edges} 

View dependencies using a tree.

view := GtMondrian new.
view nodes 
	stencil: [ :projectId |
		BrLabel  new
			aptitude: BrGlamorousLabelAptitude;
			text: projectId ];
	with: projectIds.
view edges 
	shape: [ :each |
		BlParabollaArcElement new 
			zIndex: 0;
			curvatureFraction: 0.2;
			border: (BlBorder paint: (Color gray alpha: 0.2) width: 2);
			toHead: (BlArrowheadSimpleArrow new
				border: (BlBorder builder 
					paint: (Color gray alpha: 0.2); width: 2; build))  ];
	connect: edges from: #key to: #value.
view layout custom: (GtGradHorizontalDominanceCenteredTreeLayout new 
	levelDistance: 100; 
	nodeDistance: 10).