Frontend Development 12 min read

JavaScript Bundle Dependency Analysis and Graph Construction

The article describes how to enforce architectural rules and assess change impact in a modularized JavaScript application by extracting import statements via AST, building a file‑map, constructing a cycle‑aware dependency graph with indexed nodes, and traversing it with a visited‑stack to support forward and reverse analysis across business and public bundles.

Amap Tech
Amap Tech
Amap Tech
JavaScript Bundle Dependency Analysis and Graph Construction

After the Gaode App was modularized into many bundles, the complexity of bundle dependencies became a critical issue. The dependencies need to be controlled so that they stay within the designed architecture, and reverse‑dependency analysis is required to quickly determine the impact range of iterative changes.

For component libraries, modifications affect all business projects that depend on the component. Therefore, before planning a change, the forward dependency (business → component) must be used to compute the reverse dependency (which places use the component) to assess impact.

Bundles are divided into business bundles and public bundles, each with multiple layers. Business bundles may depend on public bundles, but the reverse is prohibited. Similarly, lower‑level bundles must not depend on higher‑level bundles. Dependency analysis is needed to enforce these rules.

Key Implementation Steps

The process of JS dependency analysis is illustrated in the diagram below (image omitted). The main steps include:

Extracting Dependency Paths with AST

Two methods are possible: using regular expressions (easy but imprecise) or performing lexical and syntactic analysis to obtain an Abstract Syntax Tree (AST) and then traversing it. The AST approach is preferred for accuracy.

const ast = ts.createSourceFile(
  abPath,
  content,
  ts.ScriptTarget.Latest,
  false,
  SCRIPT_KIND[path.extname(abPath)]
);

After obtaining the AST, ts.forEachChild is used to walk the tree and locate import/require statements:

function walk (node: ts.Node) {
  ts.forEachChild(node, walk); // depth‑first traversal
  // handle ImportDeclaration, CallExpression, etc.
  switch (node.kind) {
    case ts.SyntaxKind.ImportDeclaration:
      // ...
      break;
    case ts.SyntaxKind.CallExpression:
      // ...
      break;
  }
}

Dynamic dependencies that are constructed at runtime (e.g., via AJAX) cannot be fully resolved, but most cases are covered by static string literals.

Building a File Map for Path Resolution

A file map { [fileName]: 'relative/path/to/file' } is created using glob to enumerate all files in the bundle directory. This enables O(1) lookup for "filename‑only" imports. For extensions that are omitted, a limited search over possible extensions is performed.

Representing Dependencies as a Graph

Initially, a tree‑like structure was considered, but real projects exhibit shared nodes and circular dependencies, which lead to redundancy and infinite loops. Therefore, a graph representation with explicit nodes and edges is required.

// Create nodes first
const fooTs = new Node({ name: 'foo.ts', children: [] });
const barTs = new Node({ name: 'bar.ts', children: [] });
const bazTs = new Node({ name: 'baz.ts', children: [] });
// Then create relationships
fooTs.children.push(barTs);
barTs.children.push(bazTs);
bazTs.children.push(fooTs); // cycle

Because such a graph cannot be directly serialized due to circular references, the adjacency list is transformed to use index‑based references:

const graph = {
  nodes: [
    { id: 0, name: 'foo.ts', children: [1] },
    { id: 1, name: 'bar.ts', children: [2] },
    { id: 2, name: 'baz.ts', children: [0] }
  ]
};

Adding an explicit id field decouples node identity from array order, making the structure robust to filtering or reordering.

Handling Cycles with a Stack

When traversing the dependency graph (DFS or BFS), cycles can cause infinite recursion. A simple solution is to maintain a stack of visited node indices: push on entry, pop on exit, and check includes before recursing. This approach works well for DFS.

Conclusion

Dependency relationships provide a powerful way to control large‑scale projects, enabling tasks such as dead‑code detection and precise impact analysis across versions. By iteratively refining the analysis from bundle‑level to file‑level and even identifier‑level, the engineering team can continuously improve quality and feedback loops.

TypeScriptJavaScriptASTgraph
Amap Tech
Written by

Amap Tech

Official Amap technology account showcasing all of Amap's technical innovations.

0 followers
Reader feedback

How this landed with the community

login Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.