Building a Javascript Dependency Resolver in Maven

Dependency resolution, to most javascript developers, means adding another <script> include in the header of their html page, and in most cases this is fine. In fact in 95% of any javascript applications you don’t even have to host this file- just include a link to JQuery on Google Libraries and you won’t need to worry.

In the enterprise, however, different concerns start to take over. The project you’re working on might be dependent on an internal library built by another team, which is subsequently dependent on yet another library, and so on and so forth. As those libraries are incrementally updated, managing dependencies becomes a headache. Furthermore, the true power of javascript compilers like Google Closure are realized when all of the code is available at compile time. In fact, both the Simple and Advanced optimization modes require it, as it teaches Closure which code isn’t used and may thus be discarded.

Maven already has a very mature dependency resolution mechanism built into it, which is backed by a large public repository of common libraries. Javascript is still underrepresented in this arena, true, yet hopefully this plugin will eventually remedy that situation.

Choosing Scope

The maven dependency system requires a scope for each individual dependency to determine when it is needed in the build cycle. This is quite handy- you don’t for instance, want to have all the JUnit libraries be part of your final project compile, but you do want them available during the testing phase.

Similarly, Google’s closure compiler can process an external file (dependency) in two ways: Directly, immediately compiled into the final delivery source, and externally, files which are assumed to be self-contained and included in the final product via a different script tag. These two types of dependencies map rather nicely to Maven’s “compile” and “runtime” scope, however compiling a javascript file into a library causes complexity: If two libraries have the same compiled-in dependency, with different versions, perhaps even with different optimization levels, I run the risk of version and feature conflicts.

As a result, all of our dependencies will be treated as externals.

Resolving a dependency graph

The first step to building dependency resolution into my plugin requires constructing the dependency graph. Maven makes this (relatively) easy.

Full source available on github: DependencyResolver.java

private DependencyNode getDependencyGraph(Artifact rootArtifact)
        throws DependencyResolverException {
        try {
                Dependency rootDependency = new Dependency(rootArtifact, "compile");
                CollectRequest collectRequest = new CollectRequest(rootDependency,projectRepos);

                // Collect all the nodes
                CollectResult collectResult = repoSystem.collectDependencies(repoSession, collectRequest);
                return collectResult.getRoot();
        } catch (DependencyCollectionException e) {
                throw new DependencyResolverException("Cannot build project dependency graph", e);
        }
}

Filtering dependencies by scope and extension

Now that I have a graph of dependency relationships, I should now visit each node and only collect those nodes that match our required scopes. Since I’ll also use this extension for resolving jsunit, envjs or others, it behooves me to abstract it.

Full source available on github: DependencyResolver.java

public List<Artifact> resolve(String scope, String extension)
                throws DependencyResolverException {

        getLog().debug("Resolving dependencies: [scope:" + scope + "] [extension:"  + extension + "]");
        // Construct the dependency tree of our project.
        Artifact rootArtifact = new DefaultArtifact(project.getGroupId(),
                project.getArtifactId(), project.getPackaging(),
                project.getVersion());
        DependencyNode rootNode = this.getDependencyGraph(rootArtifact);

        // Gather dependency nodes in post order
        PostorderNodeListGenerator postOrderVisitor = new PostorderNodeListGenerator();

        // Filter out the root node.
        DependencyVisitor visitor = new FilteringDependencyVisitor(
                postOrderVisitor, new PatternExclusionsDependencyFilter(
                        rootArtifact.toString()));

        // If a scope is configured, add a scope filter.
        if (null != scope) {
                visitor = new FilteringDependencyVisitor(visitor,
                                new ScopeDependencyFilter(scope));
        }

        // If a type is configured, add a pattern exclusion filter
        if (null != extension) {
                String patternFilter = "*:*:" + extension + ":*";
                visitor = new FilteringDependencyVisitor(visitor,
                                new PatternInclusionsDependencyFilter(patternFilter));
        }

        // Run the collection.
        rootNode.accept(visitor);
        return postOrderVisitor.getArtifacts(true);
}

Resolving dependencies

As it turns out, maven dependencies may, or may not, be resolved. In practice this means that the system won’t bother to download the file until it’s actually needed, so we have to do it manually. The new Aether Dependency API in Maven3 means that artifacts are immutable, so we have to make a copy of each.

Full source available on github: DependencyResolver.java

        List<Artifact> unresolvedArtifacts = postOrderVisitor.getArtifacts(true);
        List<Artifact> resolvedArtifacts = new ArrayList<Artifact>();

        for (Iterator<Artifact> iterator = unresolvedArtifacts.iterator(); iterator.hasNext();) {
                Artifact artifact = iterator.next();
                ArtifactRequest artifactRequest = new ArtifactRequest(artifact,projectRepos, null);
                ArtifactResult artifactResult = repoSystem.resolveArtifact(repoSession, artifactRequest);
                getLog().debug(" + " + artifactResult.getArtifact().toString());
                resolvedArtifacts.add(artifactResult.getArtifact());
        }
        return resolvedArtifacts;

Including dependencies in our compiler

With a list of resolved dependency artifacts in hand, I can now include them in the compile method from the last post.

Full source available on github: JSLibraryCompilerMojo.java

        // Generate any externals that we need.
        List<JSSourceFile> externs = new ArrayList<JSSourceFile>();
        getLog().info("Resolving External Includes...");
        externs.addAll(resolveDependencies("compile","js"));

        // Generate the list of source files to include.
        List<JSSourceFile> source = new ArrayList<JSSourceFile>();
        getLog().info("Resolving Compile Includes...");
        source.addAll(generateJavascriptSourceList());

And there we are! We are now resolving dependencies from other javascript libraries that live in your, or someone else’s, maven repository.

Tagged with: , , , , , ,
Posted in Blog

Leave a Reply