A configurable JavaScript dependency graph parser written in Go.
I have CI scripts that make use of tools that analyze the project's dependency graph to perform certain actions, but I don't have enough freedom to configure those tools to fit all of my use cases.
Having a JavaScript depency parser means I can start writing my own dependency tools.
Dependor is written in Go, which means it can be compiled and (eventually) make use of concurrency. Dependor also only parses dependencies instead of a full JavaScript AST. This can help prevent dependency parsing from becoming too much of a bottleneck for tooling that uses it as a first step.
You can install dependor using:
go get github.com/stilt0n/dependor
Or by importing it manually and using:
go mod tidy
Dependor uses a dependor.json
file for configuration. There are two ways you can currently customize dependor:
- Add ignore glob patterns for ignoring files and directories (it is usually a good idea to ignore node_modules and build/dist directories)
- Path aliases in case you project uses any (e.g. Remix uses
~
for theapp
directory)
The dependor.json
looks like this:
{
"ignorePatterns": ["**/node_modules", "**/dist", "**/build"],
"pathAliases": { "~": "app" }
}
It's easy to get started parsing dependencies with dependor:
parser := dependor.NewSync(".")
graph, err := parser.ParseGraph()
if err != nil {
return err
}
for file, imports := range graph {
for _, imp := range imports {
fmt.Printf("%q imports %q\n", file, imp)
}
}
💡 Tip: dependor has an ESLint plugin for the issues below
Dependor does not handle all possible export syntax yet. Dependor tries to handle as many cases from the mdn docs as possible (see mdn docs for imports and exports) but for exports there are some cases that are not yet handled:
// 1.
export let x, y;
// 2.
export const a = "a",
b = "b",
c = "c";
// 3.
export { foo as "invalid identifier alias" } from "./foo";
I plan to add support for case 3 in the future. Case 1 and 2 will likely never be supported unless someone can propose a way to parse these that does not require full expression parsing. 1. and 2. can be refactored quite easily, and dependor has an ESLint plugin that will help you track them down.
There is also a known bug where import statements inside JSX tags are not ignored. Unless you have a completely valid import statement inside of a JSX tag this will cause the tokenizer to panic, so if you're not getting errors this bug probably doesn't effect you.
If you need to include the words "import" or "export" inside of JSX tags and want to use dependor, you can work around this issue by using these words as strings. e.g.
<p>using the words import or export in JSX can cause issues for dependor</p>
Can be refactored to
<p>{"using the words import or export in JSX can cause issues for dependor"}</p>
Dependor ignores keywords inside of strings, so this should prevent panics or incorrect output when parsing.
Constructor for SingleThreadedGraphParser
Arguments:
rootPath string (optional)
:
- An optional argument to tell dependor which directory to parse. If omitted dependor will parse the directory it is called from.
Returns:
*SingleThreadedGraphParser
:
- A pointer to a
SingleThreadedGraphParser
struct
Example:
parser := dependor.NewSync("./path/to/root")
Parses the file tree into an adjacency list representation of the file tree's JavaScript dependency structure. For example this file:
// rootPath/files/foo.js
import React from "react";
import Foo, { bar, b as baz } from "./bar";
import { x, y, z } from "../letters";
import { JSXComponent } from "../components/JSXComponent";
Is parsed into the graph node:
map[string][]string {
// ... other nodes
"rootPath/files/foo.js": { "react", "rootPath/files/bar.js", "rooPath/letters.js", "rootPath/components/JSXComponent.jsx" },
// ... other nodes
}
Arguments:
None
Returns:
DependencyGraph
- An adjacency list representation of the parsed directory's dependencies
- Is an alias for
map[string][]string
and can be used the same way - keys refer to files
- values are lists of the files the key files import
error
non-nil when something goes wrong with parsing
Example:
parser := dependor.NewSync("./path/to/root")
graph, err := parser.ParseGraph()
if err != nil {
handleError(err)
}
for file, imports := range graph {
for _, imprt := range imports {
fmt.Printf("%q imports %q\n", file, imprt)
}
}
Retrieves custom config values from dependor.json
. Dependor is intended to be used in other tooling and in some cases it may be useful for that tooling to piggyback on the dependor.json
config file rather than requiring an additional config file. Dependor will parse arbitrary config values and can return values it does not make use of for other tooling to use.
Arguments:
None
Returns:
[]bytes
An array of marshalled JSON bytes which can be unmarshalled into an arbitrary structerror
An error if there is an issue converting into JSON
Example:
parser := dependor.NewSync()
graph, err := parser.ParseGraph()
// ...
type CustomConfigOptions struct {
Foo {
Bar: []string `json:"bar"`,
IsCool: bool `json:"isCool"`
} `json:"foo"`
}
var customConfig CustomConfigOptions
jsonBytes, err := parser.GetCustomConfig()
if err != nil {
panic(err)
}
if err := json.Unmarshal(jsonBytes, &customConfig); err == nil {
myGraphFunc(graph, customConfig)
}
The dependency graph is an alias for map[string][]string
with some helpful receiver methods attached. Since it is just a map
alias, it can be used the same way a map is used:
// Get value using key
dependencies := dependencyGraph["foo.js"]
// Set value using key (note: you shouldn't usually modify the graph)
dependencyGraph["foo.js"] = []string{"bar.js", "baz.js"}
Writes the dependency graph to a JSON file.
Arguments:
fileName string (optional)
:
- The name of the file to write to.
- If no name is provided the name
"dependor-output.json"
is used. - Panics if it fails to marshalling json or write the file
Returns:
void
Writes the dependency graph to a JSON string.
Arguments:
None
Returns:
(string, error)
- stringified JSON representation of the graph
- error if json marshalling fails
Example:
json, err := graph.WriteToJSONString()
if err != nil {
return err
}
fmt.Println(json)
Returns a new dependency graph with the edge directions reversed. i.e.
example := DependencyGraph{ "foo": { "bar", "baz" }}
reversed := example.ReverseEdges()
// { "bar": {"foo"}, "baz": {"foo"}}
This can be useful if you need to figure out where a certain file is imported. Does not modify original graph.
Arguments:
None
Returns:
DependencyGraph
- A dependency graph with edges in reverse direction of the calling graph
Performs a breadth-first traversal of the dependency graph starting from a given node and calls a function on each visited node.
Arguments:
startingNode string
the node to start the traversal fromfn func(node string)
a function to call on each visited node
Returns:
void
Example:
var indirectDependencies []string
graph.Traverse("foo.js", func(node string) {
// these are direct dependencies
if node != "foo.js" && !slices.Contains(graph["foo.js"], node) {
indirectDependencies = append(indirectDependencies, node)
}
})
fmt.Println("Indirect dependencies of foo.js:")
for _, dep := range indirectDependencies {
fmt.Println(dep)
}
dependor.json
files can be extended to fit the use case of tooling that makes use of Dependor. See GetCustomConfig