Tech

Building the exemONE Screen Dependency Map: A Codebase as Nodes and Edges

2026.09.10 · Yeonjin Choi

Read the original in Korean
Building the exemONE Screen Dependency Map: A Codebase as Nodes and Edges

Building the exemONE Screen Dependency Map: A Codebase as Nodes and Edges

Author
Yeonjin Choi
Date
Sep 10, 2026
Description
How we built a DEV-only screen dependency map that statically analyzes the codebase and draws screen relationships as nodes and edges.
Featured
Featured
KO slug
exemone-screen-dependency-map
Status
Published
Tag
Tech
Translation
AI draft
Translated from the Korean original. Some screenshots show the original Korean interface.

Building the exemONE Screen Dependency Map

Hi, I'm Yeonjin Choi from the FE2 team, where I develop the exemONE frontend.
“If I change this shared code, which screens do I need to check?”
This question comes up often in large frontend projects. Code search can find where a function is used, but it is hard to see at a glance which user-facing screen that usage connects to, and where you can navigate from that screen. exemONE has not only regular screens with URLs but also many detail slides that open on top of a screen and browser popups. Add components and composables shared by multiple screens, and a single user flow ends up scattered across many folders and domains.
To solve this, we statically analyzed the codebase and built a DEV-only screen dependency map that shows screen relationships as nodes and edges. It also shows which screens the current Git changes may be related to.
 
In this post, I'll cover how we turned code into graph data, why we chose Vue Flow and Dagre, and the rules we set so we would not overstate relationships that static analysis cannot prove for certain.
💡
The goal of the screen dependency map is not to draw as many relationships as possible, but to let developers trust the relationships shown and start their next check from there.
 
 

1. Reading relationships from code instead of hand-written docs

An integrated monitoring product has many areas of different kinds, and a single user flow often crosses their boundaries. You open a detail slide from a list, move from a detail screen to another target's screen, or jump straight from an alert to a related screen.
One option is to document all the connections. But copying many relationships by hand from scratch is costly, and if the docs are not updated when the implementation changes, they quickly drift from the actual behavior. We are not trying to get rid of documentation. We just decided that, at least for screen relationships that change often, regenerating them from code is better than having people maintain them separately.
 
The clues for how screens are registered and opened are already in the code.
  • Screens with URLs are registered in the router.
  • Slides and popups have their components registered in their own registries.
  • Calls such as routing, opening a detail view, and opening a new window leave clues about screen transitions.
 
We designed the screen dependency map to gather these clues and answer two questions.
  • Where is this screen opened from, and where can you go from here?
  • Which user-facing screens could the code I just changed be related to?
 
Using the code as the source of the map data cuts the time spent asking the owner or combining multiple search results, and you can recheck the structure of the current working tree whenever you need.
 
 

2. How the scanner turns code into nodes and edges

The overall structure breaks down as follows.
The static scanner builds map data from the code, and the DEV server analyzes Git change information separately. The viewer takes both results and shows screen relationships together with the evidence for changes.
The static scanner builds map data from the code, and the DEV server analyzes Git change information separately. The viewer takes both results and shows screen relationships together with the evidence for changes.
 

2-1. Turning screen registrations into nodes

The first thing to run is a static scanner written in Node.js. The scanner starts by reading the registries where the application registers its screens.
Read from code
Result
Meaning
Router configuration
Screen node
Regular screen entered via URL
Detail UI registry
Slide node
Detail screen that opens on top of the current screen
Popup component map
Popup node
Screen that opens in a separate browser window
 
Each node also stores information needed for search and navigation, such as the screen name, type, area, and declaration location.
If you add a screen in a way supported by the already-mapped router files and the central registry, it is picked up on the next scan. That said, the scanner does not automatically understand every structural change.
If you add a new router file or domain, or rename a file or registration name referenced by a manual contract, you also need to check the scanner's mappings and contracts. We kept registration info that is read automatically separate from interpretation rules that people maintain.
 

2-2. Turning call sites into evidence of connections

After collecting nodes, the scanner looks for calls that create screen transitions, such as router navigation, opening a detail view, and opening a popup.
The scanner follows import and re-export relationships in reverse to find the nearest screen that shared code connects to. Even a shared component can behave differently on each screen that uses it, so when needed it also checks events and props in the Vue template and the conditions allowed per screen.
The key here is to separate finding a call from confirming what it means. Within the supported rules, the scanner confirms the source screen and the destination, and if there is an execution condition, it records the evidence for it. It is not, however, an analyzer that fully proves whether an arbitrary function actually runs in the UI or whether every runtime branch is reachable.
💡
The scanner does not stop at finding calls. Its core job is to confirm the source screen, the destination, and the evidence for execution conditions within the supported analysis rules.
 

2-3. Interpreting only the context that is needed

We split the interpretation method by the nature of the code. Screen registrations, which have a consistent shape, are read with a mini parser. To check whether a call candidate is real code or to distinguish variable bindings, we use the TypeScript AST and symbol analysis. For parts that need template context, such as Vue events and props, we use the Vue parser.
For example, navigation code written inside a string must be distinguished from an actual call. Even if a variable has the same name, if it points to a different local variable at the call site, it cannot be treated as the same router or destination. Differences like these are hard to judge with simple string search.
Some project semantics cannot be learned from syntax alone. Even when several screens share the same detail component, the allowed behavior can differ by screen type and execution condition. In these cases, we correct and verify the automatic analysis results with explicit semantic contracts.
The final output is split into nodes, edges, dynamic actions, diagnostics, and so on, and emitted as DEV-only artifacts. To make results easy to compare for the same input and analysis environment, we also fix the order of nodes, relationships, and call sites.
 
 

3. Drawing with Vue Flow, laying out with Dagre

Part of an actual map drawn by the screen dependency map
Part of an actual map drawn by the screen dependency map
 
For the graph UI, we used the Vue Flow library. It saves us from implementing interactions like zooming in and out, panning the canvas, and selecting nodes and edges, as well as custom node rendering, and it connects naturally with our existing Vue components and state. We also built on top of Vue Flow the features that move to a node when you select a search result, and that show incoming connections, outgoing connections, and call sites in a detail panel when you click a node or edge.
Vue Flow does not decide where nodes go, though. Nobody can manage the coordinates of so many nodes by hand, so for layout we added the Dagre library. Dagre lays out directed graphs hierarchically. We pass the nodes and edges built by the scanner, along with each node's size, to Dagre, which calculates coordinates, and Vue Flow uses those coordinates to render the graph.
If you expand every screen at once, even accurate data becomes an unreadable graph. To avoid this, we chose a structure where the first view shows aggregated relationships between areas, and selecting an area expands its internal screens and their neighbors one step away.
💡
Vue Flow handles the experience of exploring the graph, and Dagre handles laying it out so it is easy to read.
 
 

4. Classifying accurately matters more than drawing a lot

The hardest part of the implementation was not finding edges, but deciding whether a call we found really deserved to be called an edge.
 
notion image
 
Suppose several screens use the same list component, and a different detail screen opens depending on the type of target selected in the list. Static analysis cannot know which target the user will select at click time.
If we connect every possible detail screen to every screen that uses the list, we create more relationships than really exist. If we exclude them all, we miss actions that can be performed from that screen.
 
To solve this, we classified analysis results as follows.
  1. If both the source screen and the destination screen are proven, it is recorded as a confirmed connection.
  1. If the destination screen is fixed but the call only runs under a specific tab, permission, or data state, it is recorded as a conditional connection.
  1. If the destination screen depends on user selection or response data, it is recorded as a dynamic connection with no target.
 
A real example showing confirmed, conditional, and dynamic connections
A real example showing confirmed, conditional, and dynamic connections
The detail panel in the example shows relationships with fixed destinations alongside actions that vary by selected value. The HomeView capture lists 7 outgoing items, but that does not mean 7 lines in the graph. It is the sum of 6 relationships with fixed destinations and 1 dynamic action. Candidate screens for actions without a fixed target are not included in graph edges or the BFS reachability range.
We also provide a separate connection review list. It shows points that need further review, such as when we could not find evidence of how a registered detail screen is opened, or could not find a registered screen matching a called popup name.
 
 

5. Mapping the impact scope to reduce side effects

One problem we kept running into while developing exemONE was side effects. Because the project is large, changing a shared component or util to fix one screen would sometimes change the behavior of unexpected screens that use the same code. Even when only a few lines changed, the range we actually had to check was often much wider.
So we overlaid the Git changes in the current working tree on the screen dependency map. It collects changed files that are not yet committed, whether staged or not, and analyzes the same range as the IDE's change list. The default is the uncommitted range, and you can also choose the whole branch (MR view).
 
A real example of changing one function in a common util file. It shows the list of screens that use that function.
A real example of changing one function in a common util file. It shows the list of screens that use that function.
 
For each changed file, it follows import relationships in reverse to find the screens that use that code. Tracing stops at the nearest screen component. This is so that changing a shared module does not by itself make it look like the whole application is affected, and the change is attributed to the screens that actually use that code.
But tracing at the file level alone can still make the scope too wide. A single shared util file holds many unrelated functions. So for TypeScript files, where possible, we identify the changed export symbols from the Git diff and trace only the import paths that actually reference those symbols.
This feature does not automatically decide whether a side effect has occurred. Static analysis alone cannot know all runtime data and dynamic calls, and when the evidence is insufficient, it may show a wider scope than the real one.
What it does change is where you start checking. Before, we started from “which files did I change?” Now we can set the test scope based on “which of the screens connected to this change do I need to check?”
💡
The change impact view is not a replacement for testing. It is a checklist that surfaces easy-to-miss things to check before the work is done.
 
 

6. Staying accurate without slowing development down

A tool that shows impact scope will not take hold in real development if it is only fast or only accurate. Showing wrong relationships can actually distort the range you check, and if you have to wait a long time every run, developers will naturally work around the tool.
 
A dev tool needs both accuracy and usability to take hold.
A dev tool needs both accuracy and usability to take hold.
 
First, we fixed the sort order of nodes, edges, and call sites so the same code always produces the same result. We check for duplicate relationships, edges pointing to screens that do not exist, and newly appearing unresolved patterns, as well as whether dynamically determined connections are actually tied to Vue events.
Usability mattered as much as accuracy. Within a single scan, we cache the raw file contents and the comment-stripped results and reuse them across analysis stages. For stages that only need template information, such as whether something in Vue is clickable, we narrowed the scope to analyze only the <template> section instead of reparsing the whole file.
For repeated runs, we build a fingerprint from the input files' paths and contents and the scanner and parser environment. We reuse the artifacts only when this value and the hash of the existing artifacts both match, and only if that earlier result also passed verification. This takes a bit more computation than comparing file modification times, but it lowers the chance of wrongly reusing stale results.
As a result, a full scan that took about 10 seconds now takes about 3 seconds. When restarting the dev server, the scan is skipped entirely if the fingerprint matches, so wait time on repeated runs is practically gone.
 
 

7. Wrapping up

The screen dependency map gathers screen registrations and call evidence spread across many files and helps you find what to check next. It cannot automatically understand every behavior the code expresses, but we tried to show, as separate things, the relationships and conditions confirmed by the supported rules and the parts that cannot yet be determined.
Beyond the use cases above, the feature can be used in other ways too, such as onboarding material, or as a reference from the scanner output during AI-assisted code review.
 
Finally, here are 3 things I learned while building it.
  1. The value of a dev tool is decided not by how many features it has, but by whether you can trust what it shows. A user who hits one false positive starts doubting the whole tool.
  1. Tools need to be managed like products. Product code changes every day, so without a structure that verifies its own results and can regenerate them at any time, a tool quietly goes stale.
  1. I came to think that structural documents that change often should be generated, not maintained. If a trustworthy source already exists in the code, the human job is not copying it down but writing the interpretation rules.
 
 

 
 

 
 
Related articles