Extension Example
In this chapter, we'll go over a simple real-world example of what a Phylum CLI extension might look like.
Our goal is writing an extension which can print out all dependencies with more than one version present in our dependency file.
The full example looks like this:
import { mapValues } from "https://deno.land/std@0.150.0/collections/map_values.ts";
import { distinct } from "https://deno.land/std@0.150.0/collections/distinct.ts";
import { groupBy } from "https://deno.land/std@0.150.0/collections/group_by.ts";
// Ensure dependency file argument is present.
if (Deno.args.length != 1) {
console.error("Usage: phylum duplicates <DEPENDENCY_FILE>");
Deno.exit(1);
}
// Parse dependency file using Phylum's API.
const depfile = await Phylum.parseDependencyFile(Deno.args[0]);
// Group all versions for the same dependency together.
const groupedDeps = groupBy(depfile.packages, dep => dep.name);
// Reduce each dependency to a list of its versions.
const reducedDeps = mapValues(groupedDeps, deps => deps.map(dep => dep.version));
for (const [dep, versions] of Object.entries(reducedDeps)) {
// Deduplicate identical versions.
const distinctVersions = distinct(versions);
// Print all dependencies with more than one version.
if (distinctVersions.length > 1) {
console.log(`${dep}:`, distinctVersions);
}
}
Now there's a lot to unpack here, so we'll go through things one by one:
Before we can start writing our extension code, we need to create our new extension:
phylum extension new duplicates
We can then start writing the extension by replacing ./duplicates/main.ts
with
our example code:
import { mapValues } from "https://deno.land/std@0.150.0/collections/map_values.ts";
import { distinct } from "https://deno.land/std@0.150.0/collections/distinct.ts";
import { groupBy } from "https://deno.land/std@0.150.0/collections/group_by.ts";
These are the Deno API imports. We use version 0.150.0
of Deno's STD
here and import the required functions by loading them as remote ES modules.
We'll go into more detail on what we need these for later.
// Ensure dependency file argument is present.
if (Deno.args.length != 1) {
console.error("Usage: phylum duplicates <DEPENDENCY_FILE>");
Deno.exit(1);
}
The Deno.args
variable contains an array with all CLI arguments passed after
our extension name, so for phylum my-extension one two
that would be ["one", "two"]
.
Here we make sure that we get exactly one parameter and print a useful help message to the terminal if no parameter was provided.
The Deno.exit
function will terminate the extension and return the provided
error code.
// Parse dependency file using Phylum's API.
const depfile = await Phylum.parseDependencyFile(Deno.args[0]);
Phylum's API is exposed in the global Phylum
object. Here we are using the
parseDependencyFile
method which reads the lockfile or manifest path passed as
an argument and returns an object containing all dependencies and the package
ecosystem. Since this function is asynchronous, we need to await
it.
The returned object will look something like this:
{
packages: [
{ type: "npm", name: "accepts", version: "1.3.8" },
{ type: "npm", name: "array-flatten", version: "1.1.1" },
{ type: "npm", name: "accepts", version: "1.0.0" }
],
package_type: "npm",
path: "package-lock.json"
}
// Group all versions for the same dependency together.
const groupedDeps = groupBy(depfile.packages, dep => dep.name);
Since our package list contains multiple instances of the same dependency, we
need to group each instance together to find duplicate versions. Deno's
convenient groupBy
function does this for us automatically and we just need to
tell it which field to group by using dep => dep.name
.
This will transform our package list into the following:
{
accepts: [
{ name: "accepts", version: "1.3.8" },
{ name: "accepts", version: "1.0.0" }
],
"array-flatten": [ { name: "array-flatten", version: "1.1.1" } ]
}
// Reduce each dependency to a list of its versions.
const reducedDeps = mapValues(groupedDeps, deps => deps.map(dep => dep.version));
Since our dependency structure now contains useless information like name
and
type
, we map each of these grouped values to contain only the version numbers
for each dependency.
This results in a simple array with all dependencies and their versions:
{
accepts: ["1.3.8", "1.0.0"],
"array-flatten": ["1.1.1"]
}
for (const [dep, versions] of Object.entries(reducedDeps)) {
Since we now have an object containing all dependencies and the required versions, we can iterate over all fields in this object to check the number of versions it has.
// Deduplicate identical versions.
const distinctVersions = distinct(versions);
But before we can check the versions themselves, we need to make sure all the versions are actually unique. Some dependency files might specify the same version multiple times, so we need to ensure we filter duplicate versions.
// Print all dependencies with more than one version.
if (distinctVersions.length > 1) {
console.log(`${dep}: `, distinctVersions);
}
}
With all versions deduplicated, we can finally print out each dependency with more than one version in our dependency file.
For our example, the output looks like this:
accepts: [ "1.3.8", "1.0.0" ]
And that's all the code we need to check for duplicates. Now we can use the
phylum extension run
subcommand to test the extension without installing it:
phylum extension run ./duplicates ./package-lock.json
This should then print the following error:
Extension error: Uncaught (in promise) Error: Requires read access to "./package-lock.json"
at async Function.parseDependencyFile (deno:phylum:201:16)
at async file:///tmp/duplicates/main.ts:12:14
Phylum's extensions are executed in a sandbox with restricted access to
operating system APIs. Since we want to read the lockfile from
./package-lock.json
with the parseDependencyFile
method, we need to request
read access to this file ahead of time. All available permissions are documented
in the extension manifest documentation.
While it would be possible to request read access to just ./package-lock.json
,
this would only work for package-lock.json
files defeating the purpose of
passing the dependency file as a parameter. Instead, we request read access to
all files in the working directory:
[permissions]
read = ["./"]
Alternatively if you wanted to allow read access to any file, so dependency
files outside of the working directory are supported, you could use read = true
instead.
Now phylum extension run
should prompt for these permissions and complete
without any errors if they have been granted. Then we can install and run our
extension:
phylum extension install ./duplicates
phylum duplicates ./package-lock.json