The Asynchronous Module Definition (AMD) API provides powerful modularization options to JavaScript developers. But this introduces its own problems when it comes to dependency management. As this post demonstrates, Mozilla's Rhino engine offers developers a means to analyze these dependencies.
Correctness and efficiency
Interdependency graphs can be used to enforce or report on adherence to packaging principles. For example, it is possible to prohibit cyclic dependencies or provide unstable dependency metrics.
When designing for efficient code reuse across web applications there are competing goals: minimize the number of file transfers to reduce network latency; prevent parsing of code that won't be executed until it's needed and reduce payload size. Dependency data can be used to determine which packages can be layered together into single files without duplication.
The Rhino AST package
Rhino 1.7R3 introduces a package for producing an abstract syntax tree.
Here's a sample JavaScript module:
// hideElements.js define([ "dojo/_base/array", "dojo/dom-class"], function(array, domClass) { "use strict"; // usage: hideElements(element1, element2, ... elementN); return function() { array.forEach(arguments, function(element) { domClass.add(element, "hide"); }); }; });
This (completely untested) module provides a function that adds a CSS class to the specified nodes, but that isn't important. The important part is that it depends on two Dojo modules.
Using Rhino's API the following Java code parses and emits the elements of the code:
import java.io.*; import org.mozilla.javascript.Parser; import org.mozilla.javascript.ast.*; public class Dump { public static void main(String[] args) throws IOException { class Printer implements NodeVisitor { @Override public boolean visit(AstNode node) { String indent = "%1$Xs".replace("X", String.valueOf(node.depth() + 1)); System.out.format(indent, "").println(node.getClass()); return true; } } String file = "hideElements.js"; Reader reader = new FileReader(file); try { AstNode node = new Parser().parse(reader, file, 1); node.visit(new Printer()); } finally { reader.close(); } } }
The tree and its elements:
class org.mozilla.javascript.ast.AstRoot class org.mozilla.javascript.ast.ExpressionStatement class org.mozilla.javascript.ast.FunctionCall class org.mozilla.javascript.ast.Name class org.mozilla.javascript.ast.ArrayLiteral class org.mozilla.javascript.ast.StringLiteral class org.mozilla.javascript.ast.StringLiteral class org.mozilla.javascript.ast.FunctionNode class org.mozilla.javascript.ast.Name class org.mozilla.javascript.ast.Name class org.mozilla.javascript.ast.Block class org.mozilla.javascript.ast.ExpressionStatement class org.mozilla.javascript.ast.StringLiteral class org.mozilla.javascript.ast.ReturnStatement class org.mozilla.javascript.ast.FunctionNode class org.mozilla.javascript.ast.Block class org.mozilla.javascript.ast.ExpressionStatement class org.mozilla.javascript.ast.FunctionCall class org.mozilla.javascript.ast.PropertyGet class org.mozilla.javascript.ast.Name class org.mozilla.javascript.ast.Name class org.mozilla.javascript.ast.Name class org.mozilla.javascript.ast.FunctionNode class org.mozilla.javascript.ast.Name class org.mozilla.javascript.ast.Block class org.mozilla.javascript.ast.ExpressionStatement class org.mozilla.javascript.ast.FunctionCall class org.mozilla.javascript.ast.PropertyGet class org.mozilla.javascript.ast.Name class org.mozilla.javascript.ast.Name class org.mozilla.javascript.ast.Name class org.mozilla.javascript.ast.StringLiteral
Finding the dependencies
This code looks for string literals in the first array in a define
function call:
import java.io.*; import java.util.*; import org.mozilla.javascript.Parser; import org.mozilla.javascript.ast.*; public class Dependencies { public static void main(String[] args) throws IOException { final List<String> dependencies = new ArrayList<String>(); class Printer implements NodeVisitor { @Override public boolean visit(AstNode node) { if (isDefineCall(node)) { addDependencies(dependencies, node.getParent()); } return true; } } String file = "hideElements.js"; Reader reader = new FileReader(file); try { AstNode node = new Parser().parse(reader, file, 1); node.visit(new Printer()); } finally { reader.close(); } System.out.println(dependencies); } private static boolean isDefineCall(AstNode node) { if (node instanceof Name) { Name name = (Name) node; if ("define".equals(name.getIdentifier()) && name.getParent() instanceof FunctionCall) { return true; } } return false; } private static void addDependencies(final List<String> dependencies, final AstNode define) { class ArrayFinder implements NodeVisitor { @Override public boolean visit(AstNode node) { if (node.depth() == define.depth() + 1 && node instanceof ArrayLiteral) { collectStrings(dependencies, node); return false; } return true; } } define.visit(new ArrayFinder()); } private static void collectStrings(final List<String> dependencies, final AstNode array) { class StringCollector implements NodeVisitor { @Override public boolean visit(AstNode node) { if (node.depth() == array.depth() + 1 && node instanceof StringLiteral) { StringLiteral str = (StringLiteral) node; dependencies.add(str.getValue()); } return true; } } array.visit(new StringCollector()); } }
The application output:
[dojo/_base/array, dojo/dom-class]
Note the limitations in the posted code.
For example, it doesn't try to evaluate literal expressions like "dojo/" + "dom-class"
or
handle the redefinition or scoping of the define
reference.
End notes
Here are the Java compiler options expressed as an Apache
Maven pom.xml
:
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>demo</groupId> <artifactId>jsdep</artifactId> <version>0.0.1-SNAPSHOT</version> <build> <plugins> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-compiler-plugin</artifactId> <version>2.5</version> <configuration> <encoding>US-ASCII</encoding> <source>1.6</source> <target>1.6</target> </configuration> </plugin> </plugins> </build> <dependencies> <dependency> <groupId>org.mozilla</groupId> <artifactId>rhino</artifactId> <version>1.7R4</version> </dependency> </dependencies> </project>
Note that this is quick'n'dirty demo code and shouldn't be reused verbatim
(e.g. it uses FileReader
instead of specifying a file
encoding.)
No comments:
Post a Comment
All comments are moderated