<?xml version="1.0" encoding="UTF-8"?><rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>FurryCoder</title><description>Focusing on job working recently 最近忙于工作捏～</description><link>https://furrycoder.com/</link><language>en</language><item><title>Feopack: Mini Rspack</title><link>https://furrycoder.com/posts/feopack/</link><guid isPermaLink="true">https://furrycoder.com/posts/feopack/</guid><description>What I learned from building Feopack, a mini Rspack-like bundler: the technical pieces, the architecture, and a few thoughts from building software in the agent era.</description><pubDate>Sun, 03 May 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;Why did I build it?&lt;/h2&gt;
&lt;p&gt;Feopack started as a learning project.&lt;/p&gt;
&lt;p&gt;Not because the JavaScript ecosystem needed another production bundler. It definitely did not wake up one morning asking for my tiny Rust experiment. I built it because I wanted to understand what modern bundlers like Rspack are doing under the surface.&lt;/p&gt;
&lt;p&gt;Rspack is much faster than webpack because it moves many heavy tasks into the native layer. This helps avoid the bottleneck of JavaScript&apos;s single-threaded execution model and reduces the overhead of communication inside the build pipeline.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;flowchart LR
  Entry[&quot;【Entry Points】&amp;lt;br/&amp;gt;main.js / another entry&quot;]

  subgraph MAKE[&quot;Make&quot;]
    direction TB
    Queue[&quot;【Module Worker Queue】&amp;lt;br/&amp;gt;parallel module jobs&quot;]
    Build[&quot;【Build &amp;amp; Analyze】&amp;lt;br/&amp;gt;resolve deps / run loaders / parse AST&quot;]
    ModuleGraph[&quot;【ModuleGraph】&amp;lt;br/&amp;gt;modules + dependency edges&quot;]
    Queue --&amp;gt; Build
    Build -- &quot;discovered deps&quot; --&amp;gt; Queue
    Build --&amp;gt; ModuleGraph
  end

  subgraph SEAL[&quot;Seal&quot;]
    direction TB
    Optimize[&quot;【Optimization Analysis】&amp;lt;br/&amp;gt;used exports / side effects&quot;]
    ChunkGraph[&quot;【ChunkGraph】&amp;lt;br/&amp;gt;code splitting + chunk groups&quot;]
    Ids[&quot;【ID Generation】&amp;lt;br/&amp;gt;module ids + chunk ids&quot;]
    Codegen[&quot;【Code Generation】&amp;lt;br/&amp;gt;tree shaking + final module code&quot;]
    Runtime[&quot;【Runtime Modules】&amp;lt;br/&amp;gt;chunk loading / helpers&quot;]
    Assets[&quot;【Chunk Assets】&amp;lt;br/&amp;gt;runtime chunks + normal chunks&quot;]
    Optimize --&amp;gt; ChunkGraph
    ChunkGraph --&amp;gt; Ids
    Ids --&amp;gt; Codegen
    Codegen --&amp;gt; Assets
    Runtime --&amp;gt; Assets
  end

  subgraph EMIT[&quot;Emit&quot;]
    direction TB
    Output[&quot;【Emit Assets】&amp;lt;br/&amp;gt;write files to output&quot;]
  end

  Entry --&amp;gt; Queue
  ModuleGraph --&amp;gt; Optimize
  ChunkGraph --&amp;gt; Runtime
  Assets --&amp;gt; Output
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;I guess someone once tried to rebuild the whole webpack ecosystem in C++, then do something Rspack-like, and got lost somewhere halfway through the maze. Maybe webpack is simply too huge, and too deeply married to &lt;code&gt;Tapable&lt;/code&gt;, to be casually rebuilt from scratch.&lt;/p&gt;
&lt;p&gt;And somehow, Rspack actually did it.&lt;/p&gt;
&lt;p&gt;Of course, it also had to carry a lot of luggage from the webpack era. Compatibility is not free, and when you inherit that much history, perfect performance is probably not included in the box.&lt;/p&gt;
&lt;p&gt;But that was exactly what made it interesting to me. It suggested that there might still be room to push things further, to rethink a few pieces, and maybe, someday, to take part in building something in that direction. That curiosity is basically why I built feopack.&lt;/p&gt;
&lt;p&gt;Anyway, back to feopack itself. Let&apos;s see what I actually did.&lt;/p&gt;
&lt;p&gt;This post follows the order in which the project grew. It is less of a complete tutorial and more of a development journal: what I implemented, what I learned, and where the complexity started to show up and politely ruin my afternoon.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;BTW, the name feopack is part of the joke. Rust can mean iron oxide, and FeO is also an iron oxide, just not the fully rusted one. So feopack is a small, not-fully-rusted, Rspack-like experiment.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;1. Starting from the JavaScript Wrapper&lt;/h2&gt;
&lt;p&gt;The first meaningful layer was not Rust. It was the JavaScript-facing API.&lt;/p&gt;
&lt;p&gt;That might sound strange for a Rust-based bundler, but it makes sense. Most users do not interact with the compiler core directly. They interact with a function, a config file, a CLI command, and a &lt;code&gt;Compiler&lt;/code&gt; object.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;flowchart LR
  Config[User Config] --&amp;gt; Wrapper[TypeScript Wrapper]
  Wrapper --&amp;gt; Adapter[Raw Options Adapter]
  Adapter --&amp;gt; NAPI[NAPI Binding]
  NAPI --&amp;gt; Core[Rust Compiler Core]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;So before worrying about module graphs and chunk graphs, I needed to make this JavaScript-to-native handoff feel boring in the best possible way.&lt;/p&gt;
&lt;p&gt;In feopack, the public API begins with something like this. Nothing fancy, which is exactly the point:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import feopack from &quot;feopack&quot;;

const compiler = feopack({
  context: process.cwd(),
  entry: &quot;./src/index.js&quot;,
  output: {
    path: &quot;./dist&quot;,
    filename: &quot;main.js&quot;,
  },
});

compiler.run();
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Internally, this creates a &lt;code&gt;Compiler&lt;/code&gt; instance. The TypeScript wrapper owns the user-facing shape of the API, while the Rust side owns the actual compilation work.&lt;/p&gt;
&lt;p&gt;But a friendly JavaScript API alone is not enough. It still needs a bridge into the native layer, which brings us to the binding.&lt;/p&gt;
&lt;h2&gt;2. Making the Boundary Explicit with NAPI&lt;/h2&gt;
&lt;p&gt;Luckily, &lt;code&gt;napi-rs&lt;/code&gt; does most of the heavy lifting needed to build that bridge.&lt;/p&gt;
&lt;p&gt;The TypeScript wrapper only needs to lazily create a native compiler instance from &lt;code&gt;@feopack/binding&lt;/code&gt;. On the Rust side, that native class receives &lt;code&gt;RawOptions&lt;/code&gt;, converts them into internal compilation options, and exposes an async &lt;code&gt;build()&lt;/code&gt; method.&lt;/p&gt;
&lt;p&gt;In the bigger Rspack-shaped picture, this is the highlighted &lt;code&gt;Configuration &amp;amp; Binding&lt;/code&gt; slice: the place where user-facing configuration turns into something the native compiler can actually work with.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;graph TD
  subgraph Entrances[Entrances]
    direction TB
    CLI[&quot;@rspack/cli&amp;lt;br/&amp;gt;Command Line Interface&quot;]
    DevServer[&quot;@rspack/dev-server&amp;lt;br/&amp;gt;Development Server&quot;]
    CoreJS[&quot;@rspack/core&amp;lt;br/&amp;gt;JavaScript API&quot;]
  end

  subgraph PluginEcosystem[Plugin Ecosystem]
    direction TB
    JSPlugins[&quot;JavaScript Plugins&amp;lt;br/&amp;gt;Tapable Hook System&quot;]
    RustPlugins[&quot;Rust Plugins&amp;lt;br/&amp;gt;Native Performance&quot;]
    BuiltInPlugins[&quot;Built-in Plugins&amp;lt;br/&amp;gt;Core Functionality&quot;]
  end

  subgraph ConfigBinding[Configuration &amp;amp; Binding]
    direction TB
    ConfigSys[&quot;Configuration System&amp;lt;br/&amp;gt;rspack.config.js processing&quot;]
    BindingBridge[&quot;@rspack/binding&amp;lt;br/&amp;gt;N-API Bridge&quot;]
  end

  subgraph RustCore[Rust Core Engine]
    direction TB
    RustCompiler[&quot;rspack_core::Compiler&amp;lt;br/&amp;gt;Build Orchestration&quot;]
    RustCompilation[&quot;rspack_core::Compilation&amp;lt;br/&amp;gt;Single Build Instance&quot;]
    ModuleGraph[&quot;ModuleGraph&amp;lt;br/&amp;gt;Dependency Relationships&quot;]
    ChunkGraph[&quot;ChunkGraph&amp;lt;br/&amp;gt;Output Structure&quot;]
  end

  CLI ==&amp;gt; CoreJS
  DevServer ==&amp;gt; CoreJS
  CoreJS --&amp;gt; ConfigSys
  CoreJS -.-&amp;gt; BindingBridge
  ConfigSys --&amp;gt; BindingBridge
  BindingBridge ==&amp;gt; RustCompiler
  JSPlugins ==&amp;gt; BindingBridge
  RustPlugins ==&amp;gt; RustCompiler
  BuiltInPlugins ==&amp;gt; RustCompiler
  RustCompiler ==&amp;gt; RustCompilation
  RustCompilation ==&amp;gt; ModuleGraph
  RustCompilation ==&amp;gt; ChunkGraph

  style ConfigBinding fill:#fff3bf,stroke:#f08c00,stroke-width:3px,color:#000
  style ConfigSys fill:#ffe066,stroke:#f08c00,color:#000
  style BindingBridge fill:#ffe066,stroke:#f08c00,color:#000
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The so-called binding is essentially a platform-specific native library loaded from Node.js. It is the small door JavaScript uses to knock on Rust&apos;s very serious office.&lt;/p&gt;
&lt;p&gt;This boundary forced me to make the data structures explicit. JavaScript objects can be flexible and loose. Rust compiler options cannot be quite that casual. The binding layer becomes the place where the two worlds agree on a contract, preferably before either side starts throwing confusing errors at the other.&lt;/p&gt;
&lt;p&gt;While reading Rspack&apos;s binding layer, I found it useful to think about three kinds of cross-language traffic.&lt;/p&gt;
&lt;p&gt;First, JavaScript can hold a reference to Rust state. This is the normal &lt;code&gt;Compiler&lt;/code&gt; story: Rust owns the real compiler instance, while JavaScript receives an object that can call methods like &lt;code&gt;build()&lt;/code&gt;. In a webpack-compatible world, this also matters because JavaScript plugins may need to observe or interact with runtime compiler data. JavaScript is not rebuilding the compiler; it is holding the remote control.&lt;/p&gt;
&lt;p&gt;Second, Rust can hold a reference back to JavaScript. This is needed when an object passed from JS contains callbacks, factories, or other JS-owned behavior. Rust should not try to “interpret” that JavaScript by itself. Instead, it keeps a safe reference to the JS value and asks the JS runtime to execute it when needed. In Rspack, types like &lt;code&gt;ThreadsafeJsValueRef&lt;/code&gt; and &lt;code&gt;ThreadsafeFunction&lt;/code&gt; exist for this kind of situation: Rust can call back into JS without pretending V8 is just another Rust crate.&lt;/p&gt;
&lt;p&gt;Third, sometimes the best answer is no reference at all: just copy the data. Configuration is a good example. A config object is usually expected to be stable during a build, so the binding layer can convert the incoming JS object into Rust-side options. This is where &lt;code&gt;RawOptions&lt;/code&gt; becomes &lt;code&gt;CompilerOptions&lt;/code&gt;, and the real work is less glamorous than it sounds: flatten JavaScript&apos;s dynamic shapes into Rust&apos;s stricter types.&lt;/p&gt;
&lt;p&gt;For example, a JavaScript option might allow multiple shapes, while Rust wants an explicit enum:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;let pathinfo = match value.pathinfo {
  Either::A(value) =&amp;gt; PathInfo::Bool(value),
  Either::B(value) =&amp;gt; PathInfo::String(value),
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Some options can even be either a plain value or a JavaScript callback. The Rust type has to describe that honestly:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;pub struct JsFilename&amp;lt;F = ThreadsafeFunction&amp;lt;(JsPathData, Option&amp;lt;JsAssetInfo&amp;gt;), String&amp;gt;&amp;gt;(
  Either&amp;lt;String, F&amp;gt;,
);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;That made the binding layer feel less like glue code and more like a customs office. Objects arrive from JavaScript with flexible passports, and Rust politely asks everyone to fill out the correct forms before entering the compiler core.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;I also had a few ideas along the way, such as passing a Node-side file system or resolver factory into Rust.&lt;/p&gt;
&lt;p&gt;I did not implement them in feopack at this stage, but they were useful reminders of where the boundary could grow later.&lt;/p&gt;
&lt;p&gt;I also started another library project named &lt;code&gt;@rush-fs/core&lt;/code&gt;, which is one small experiment in that direction. Hopefully, I will eventually find enough time outside work to finish all these tiny side quests.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;After the wrapper and binding skeleton existed, I added a CLI and a small playground. A bundler without a way to run examples is mostly just a collection of confident lies.&lt;/p&gt;
&lt;h2&gt;3. Adding a CLI and Playground to Show My Love for TDD&lt;/h2&gt;
&lt;p&gt;On the one hand, I believe everyone loves TDD, because it gives us a nice, repeatable way to check whether our logic actually works.&lt;/p&gt;
&lt;p&gt;On the other hand, there is one tiny problem: I am sometimes too lazy to come up with good test cases from scratch. A tragic flaw, I know.&lt;/p&gt;
&lt;p&gt;So I borrowed some cases from Rspack&apos;s source code, made a few small changes, and wired them up so they could be triggered with commands like &lt;code&gt;pnpm test chunk/basic&lt;/code&gt;. The goal was simple: run a case, generate assets, and stare at the output until the bundler confessed what it was doing.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;To be honest, I no longer remember exactly where I found those cases. I later checked the Rspack and Webpack repositories again and somehow could not find them. Maybe they moved. Maybe I imagined them.&lt;/p&gt;
&lt;p&gt;Either way, the playground survived, which is what really matters.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;With that in place, I could finally start building the actual compiler pipeline.&lt;/p&gt;
&lt;h2&gt;4. Building the Compilation Lifecycle&lt;/h2&gt;
&lt;p&gt;As mentioned earlier, the core introduced a &lt;code&gt;Compiler&lt;/code&gt; and a &lt;code&gt;Compilation&lt;/code&gt;.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;Compiler&lt;/code&gt; owns the high-level build process.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Compilation&lt;/code&gt; owns the state of one build: options, module graph, chunk graph, and generated assets. I think of it as the working memory for a single build.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;In feopack, the outer calls are mostly wrappers around the real bundler core. The core lifecycle follows the familiar three-step shape: make, seal, and emit.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;flowchart LR
  subgraph BundlerCore[Compilation]
    direction LR
    Make[&quot;Make&quot;] --&amp;gt; Seal[&quot;Seal&quot;] --&amp;gt; Emit[&quot;Emit&quot;]
  end

  Run[&quot;compiler.run()&quot;] --&amp;gt; Make
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This is close to how many bundlers organize their work. The names may vary, and production bundlers have far more hooks and intermediate states, but the main rhythm is usually the same.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;Make&lt;/code&gt; is where modules are discovered and the module graph starts to grow.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Seal&lt;/code&gt; is where that graph becomes chunks and generated assets.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Emit&lt;/code&gt; writes the final files to disk.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Some stages, such as &lt;code&gt;CompileDone&lt;/code&gt; and &lt;code&gt;MakeDone&lt;/code&gt;, were simplified away. That was intentional. I wanted to keep the lifecycle visible without pretending the hook system existed yet.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Yes, the hook system is still unfinished. But someday, maybe I will get to it. Probably right after finishing all the other “small” ideas.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;5. From Source Files to a Module Graph&lt;/h2&gt;
&lt;p&gt;From this point on, the project became a series of data-structure transformations.&lt;/p&gt;
&lt;p&gt;A bundler starts with files on disk, but files are not a very useful shape for a compiler. The first job of &lt;code&gt;make()&lt;/code&gt; is to turn source files into a graph: each module becomes a node, and each import becomes an edge to another module.&lt;/p&gt;
&lt;p&gt;In feopack, the simplified &lt;code&gt;Module&lt;/code&gt; looked like this:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;pub struct Module {
  pub id: String,
  pub dependencies: Vec&amp;lt;String&amp;gt;,
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Storing dependency strings directly is not ideal. A more serious bundler would usually store references, dependency objects, or richer edges, especially if it wants incremental builds. But for this stage, the goal was to make the shape visible: “this module depends on those modules.”&lt;/p&gt;
&lt;p&gt;The rough transformation was:&lt;/p&gt;
&lt;p&gt;Take the &lt;code&gt;chunk/basic&lt;/code&gt; playground case as an example. The entry file is almost embarrassingly small, which is exactly why it is useful:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// src/index.js
import title, { num, plusNum } from &quot;./app.js&quot;;

title(&quot;Hello FeOPack&quot;);
console.log(num);
plusNum();
console.log(num);
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;flowchart LR
  Source[&quot;【Source File】&quot;] --&amp;gt; Imports[&quot;【Import Request】&amp;lt;br/&amp;gt;request = &apos;./app.js&apos;&quot;]
  Imports --&amp;gt; Resolved[&quot;【Resolved ID】&amp;lt;br/&amp;gt;.../src/app.js&quot;]

  subgraph ModuleGraph[&quot;ModuleGraph&quot;]
    direction TB
    IndexModule[&quot;【Module】: index.js&amp;lt;br/&amp;gt;id: .../src/index.js&amp;lt;br/&amp;gt;deps:[&apos;./app.js&apos;]&quot;]
    AppModule[&quot;【Module】: app.js&amp;lt;br/&amp;gt;id: .../src/app.js&amp;lt;br/&amp;gt;deps: []&quot;]
    IndexModule --&amp;gt; AppModule
  end

  Resolved --&amp;gt; AppModule
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The &lt;code&gt;make()&lt;/code&gt; phase uses &lt;code&gt;SWC&lt;/code&gt; to parse each module and read its import declarations.&lt;/p&gt;
&lt;p&gt;A small &lt;code&gt;VecDeque&lt;/code&gt; queue then keeps track of which resolved files still need to be processed. In real Rspack, this would involve &lt;code&gt;EntryDependency&lt;/code&gt;, a &lt;code&gt;ModuleFactory&lt;/code&gt;, and a task loop for parallel scheduling. feopack keeps only the minimal version: parse with SWC, resolve the discovered imports, push the next module paths into the queue, and repeat.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;queue.push_back(entry_path);

while let Some(module_path) = queue.pop_front() {
  if visited.contains(&amp;amp;module_path) {
    continue;
  }

  visited.insert(module_path.clone());
  // read source, parse with SWC, resolve imports, queue dependencies
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Module identity is not a detail here. In the playground case, &lt;code&gt;./app.js&lt;/code&gt; is only the request written in &lt;code&gt;src/index.js&lt;/code&gt;; the compiler still needs a stable internal ID, usually a normalized resolved path like &lt;code&gt;.../src/app.js&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Without that normalization, two equivalent paths can accidentally become two different modules, and the graph stops being a graph of the program.&lt;/p&gt;
&lt;p&gt;It becomes a graph of spelling choices.&lt;/p&gt;
&lt;h2&gt;6. From JavaScript Syntax to Import Records&lt;/h2&gt;
&lt;p&gt;The module graph needs dependencies, but JavaScript source code does not hand them over as a neat list. It gives you syntax.&lt;/p&gt;
&lt;p&gt;That is where SWC enters the design. Writing a JavaScript parser would be a different project. For a bundler, the more relevant question is how parser output becomes compiler state. SWC provides the AST; feopack decides which parts of that AST become import records, graph edges, and eventually runtime code.&lt;/p&gt;
&lt;p&gt;Take the same line from &lt;code&gt;src/index.js&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import title, { num, plusNum } from &quot;./app.js&quot;;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;For this import line, the important conversion is not the full AST shape. The useful data flow is how this declaration becomes records the compiler can use:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;flowchart LR
  ImportDecl[&quot;【Import】&amp;lt;br/&amp;gt;import title, { num, plusNum } from &apos;./app.js&apos;&quot;]

  subgraph RawRecords[&quot;RawImportRecord[]&quot;]
    direction TB
    RawDefault[&quot;【Raw】default import&amp;lt;br/&amp;gt;local: title&amp;lt;br/&amp;gt;imported: default&amp;lt;br/&amp;gt;request: &apos;./app.js&apos;&quot;]
    RawNum[&quot;【Raw】named import&amp;lt;br/&amp;gt;local: num&amp;lt;br/&amp;gt;imported: num&amp;lt;br/&amp;gt;request: &apos;./app.js&apos;&quot;]
    RawPlus[&quot;【Raw】named import&amp;lt;br/&amp;gt;local: plusNum&amp;lt;br/&amp;gt;imported: plusNum&amp;lt;br/&amp;gt;request: &apos;./app.js&apos;&quot;]
  end

  subgraph ResolvedRecords[&quot;ResolvedImportRecord[]&quot;]
    direction TB
    ResolvedDefault[&quot;【Resolved】 title&amp;lt;br/&amp;gt;request: &apos;./app.js&apos;&amp;lt;br/&amp;gt;module_id: .../src/app.js&quot;]
    ResolvedNum[&quot;【Resolved】 num&amp;lt;br/&amp;gt;request: &apos;./app.js&apos;&amp;lt;br/&amp;gt;module_id: .../src/app.js&quot;]
    ResolvedPlus[&quot;【Resolved】 plusNum&amp;lt;br/&amp;gt;request: &apos;./app.js&apos;&amp;lt;br/&amp;gt;module_id: .../src/app.js&quot;]
  end

  ImportDecl --&amp;gt; RawRecords
  RawDefault --&amp;gt; ResolvedDefault
  RawNum --&amp;gt; ResolvedNum
  RawPlus --&amp;gt; ResolvedPlus
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The raw import record keeps what the source code says:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;pub struct RawImportRecord {
  pub local: String,
  pub imported: String,
  pub request: String,
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Then &lt;code&gt;resolve_imports()&lt;/code&gt; turns that into a resolved record by adding the real module ID:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;pub struct ResolvedImportRecord {
  pub local: String,
  pub imported: String,
  pub request: String,
  pub module_id: String,
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;That extra &lt;code&gt;module_id&lt;/code&gt; is where syntax becomes compiler state. In the diagram, &lt;code&gt;request&lt;/code&gt; keeps the original source-level value, &lt;code&gt;./app.js&lt;/code&gt;, while &lt;code&gt;module_id&lt;/code&gt; points to the resolved module, &lt;code&gt;.../src/app.js&lt;/code&gt;. Keeping both fields avoids mixing two different concepts: the user&apos;s module request and the compiler&apos;s canonical module identity.&lt;/p&gt;
&lt;p&gt;The imports are also stored in a &lt;code&gt;Vec&lt;/code&gt;, not a map. The reason is not performance; it is semantics. Import declarations have source order, and source order can matter once transformations, side effects, or generated helper statements enter the picture. A map would make lookup convenient, but it would quietly discard ordering information. That is exactly the kind of “small simplification” that later becomes a semantic bug.&lt;/p&gt;
&lt;h2&gt;7. From Module Graph to Chunk Graph&lt;/h2&gt;
&lt;p&gt;Once the module graph existed, the next question was: how should these modules be packaged?&lt;/p&gt;
&lt;p&gt;That is the job of the chunk graph. A module graph describes relationships in the source program. A chunk graph describes the runtime packaging plan. They are related, but they answer different questions.&lt;/p&gt;
&lt;p&gt;In feopack, the chunk data structure was intentionally tiny:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;pub struct Chunk {
  pub id: String,
  pub module_ids: Vec&amp;lt;String&amp;gt;,
}

pub struct ChunkGraph {
  pub chunks: Vec&amp;lt;Chunk&amp;gt;,
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The transformation is deliberately simple:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;flowchart LR
  subgraph ModuleGraph[&quot;ModuleGraph&quot;]
    direction TB
    IndexModule[&quot;【Module】: index.js&amp;lt;br/&amp;gt;id: .../src/index.js&quot;]
    AppModule[&quot;【Module】: app.js&amp;lt;br/&amp;gt;id: .../src/app.js&quot;]
    IndexModule --&amp;gt; AppModule
  end

  ModuleGraph --&amp;gt; CreateChunk[&quot;【Create Chunk】&quot;]
  Strategy[&quot;【Chunking Strategy】&amp;lt;br/&amp;gt;put all modules into main&quot;] --&amp;gt; CreateChunk

  subgraph ChunkGraph[&quot;ChunkGraph&quot;]
    direction TB
    MainChunk[&quot;【Chunk】: main&amp;lt;br/&amp;gt;module_ids:&amp;lt;br/&amp;gt;.../src/index.js&amp;lt;br/&amp;gt;.../src/app.js&quot;]
  end

  CreateChunk --&amp;gt; MainChunk
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;In a real bundler, this step would involve grouping strategies.&lt;/p&gt;
&lt;p&gt;But I didn&apos;t complete it, the current version puts every module into one chunk. That means no advanced code splitting, no async chunk loading, no CSS ordering, and no optimization pass.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;hope that one day I will finish it&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;let chunk = Chunk {
  id: &quot;main&quot;.to_string(),
  module_ids,
};

self.chunk_graph.chunks.push(chunk);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Even in this simplified version, &lt;code&gt;ChunkGraph&lt;/code&gt; has a different responsibility from &lt;code&gt;ModuleGraph&lt;/code&gt;. &lt;code&gt;ModuleGraph&lt;/code&gt; is about program structure: who imports whom. &lt;code&gt;ChunkGraph&lt;/code&gt; is about delivery: what files will the runtime load, and which modules live inside them.&lt;/p&gt;
&lt;p&gt;So the data shape moved one step forward:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;source files -&amp;gt; modules -&amp;gt; module graph -&amp;gt; chunk graph
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The bundler was still not “smart,” but the representation had moved from source units to runtime units.&lt;/p&gt;
&lt;h2&gt;8. From Codegen Modules to Generated Assets&lt;/h2&gt;
&lt;p&gt;The last transformation in this part is where a chunk becomes an output asset.&lt;/p&gt;
&lt;p&gt;The important detail is that a &lt;code&gt;Chunk&lt;/code&gt; does not contain generated JavaScript yet. In this version, it only contains &lt;code&gt;module_ids&lt;/code&gt;: the list of modules that should be rendered together. Code generation then walks those IDs, consumes the module source cached during &lt;code&gt;make()&lt;/code&gt;, rewrites its import/export syntax, and stores the result as a temporary &lt;code&gt;CodegenModule&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;struct CodegenModule {
  id: String,
  source: String,
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This structure is a bridge between the compiler world and the runtime world. It no longer represents the original file exactly. It represents a module after its imports and exports have been rewritten into something the bundle runtime can execute.&lt;/p&gt;
&lt;p&gt;During code generation, two pieces of compiler state meet: the chunk tells &lt;code&gt;render_chunk()&lt;/code&gt; which modules belong to the output, and &lt;code&gt;CodegenModule[]&lt;/code&gt; provides the transformed source for those modules.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;flowchart LR
  subgraph ChunkGraph[&quot;ChunkGraph&quot;]
    direction TB
    MainChunk[&quot;【Chunk】: main&amp;lt;br/&amp;gt;module_ids:&amp;lt;br/&amp;gt;.../src/index.js&amp;lt;br/&amp;gt;.../src/app.js&quot;]
  end

  subgraph CodegenModules[&quot;CodegenModule[]&quot;]
    direction TB
    IndexCodegen[&quot;【CodegenModule】: index.js&amp;lt;br/&amp;gt;id: .../src/index.js&amp;lt;br/&amp;gt;source: transformed JS&quot;]
    AppCodegen[&quot;【CodegenModule】: app.js&amp;lt;br/&amp;gt;id: .../src/app.js&amp;lt;br/&amp;gt;source: transformed JS&quot;]
  end

  MainChunk --&amp;gt; RenderChunk[&quot;【render_chunk】&amp;lt;br/&amp;gt;choose module order from chunk&quot;]
  CodegenModules --&amp;gt; RenderChunk
  RenderChunk --&amp;gt; Runtime[&quot;【Runtime Module Table】&amp;lt;br/&amp;gt;__feopack_modules__&quot;]
  Runtime --&amp;gt; Asset[&quot;【GeneratedAsset】&amp;lt;br/&amp;gt;filename: main.js&quot;]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The final asset structure is simple:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;pub struct GeneratedAsset {
  pub filename: String,
  pub source: String,
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The important part is that &lt;code&gt;render_chunk()&lt;/code&gt; turns a list of &lt;code&gt;CodegenModule&lt;/code&gt;s into a tiny module system:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const __feopack_modules__ = {
  &quot;entry.js&quot;: (__feopack_module__, __feopack_exports__, __feopack_import__) =&amp;gt; {
    // transformed module code
  },
};

const __feopack_cache__ = {};

function __feopack_import__(id) {
  if (__feopack_cache__[id]) {
    return __feopack_cache__[id].exports;
  }

  const __feopack_module__ = { exports: {} };
  __feopack_cache__[id] = __feopack_module__;
  __feopack_modules__[id](
    __feopack_module__,
    __feopack_module__.exports,
    __feopack_import__,
  );
  return __feopack_module__.exports;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The transition matters very much.&lt;/p&gt;
&lt;p&gt;Once imports and exports enter the picture, a bundle is not just concatenated JavaScript. It is a small module system pretending to be a file.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;This part gradually moved from a CommonJS-style idea toward something closer to ESM-style runtime behavior.&lt;/p&gt;
&lt;p&gt;At first, I built the CJS-style version based on my webpack experience, but it produced incorrect bundles in a few cases.&lt;/p&gt;
&lt;p&gt;So I started moving it toward an ESM-style runtime model, which fits modern module semantics better.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;One rough edge remains in this implementation: it still creates fresh &lt;code&gt;SwcCompiler&lt;/code&gt; instances in a few places. That is not the most elegant architecture. A more mature version would think harder about compiler ownership, shared source maps, and concurrency boundaries.&lt;/p&gt;
&lt;p&gt;But for a small bundler, this tradeoff kept the pipeline explicit enough to reason about.&lt;/p&gt;
&lt;p&gt;At this point, the chunk has stopped being only a packaging plan. It has become an in-memory &lt;code&gt;GeneratedAsset&lt;/code&gt;, with a runtime table, transformed module functions, and an entry call. The later &lt;code&gt;emit_assets()&lt;/code&gt; step is responsible for writing that asset to disk.&lt;/p&gt;
&lt;h2&gt;9. Where the Runtime Semantics Start to Bite&lt;/h2&gt;
&lt;p&gt;By the time &lt;code&gt;GeneratedAsset&lt;/code&gt; exists, the main build path has already reached emit-ready output. What remains interesting is not another pipeline stage, but the semantic cost hidden inside code generation.&lt;/p&gt;
&lt;p&gt;The pipeline sounds clean: source code becomes an AST, the AST becomes a transformed AST, and the transformed AST becomes JavaScript again. In practice, this is where earlier abstraction choices start to matter. The implementation has to decide how imports become runtime reads, how exports stay live, and how generated helpers fit into the module function.&lt;/p&gt;
&lt;p&gt;There was also friction around SWC codegen APIs and ecosystem version compatibility. Emitting JavaScript from an AST required understanding &lt;code&gt;Emitter&lt;/code&gt;, &lt;code&gt;JsWriter&lt;/code&gt;, &lt;code&gt;SourceMap&lt;/code&gt;, and how errors should be mapped back into feopack&apos;s own error type.&lt;/p&gt;
&lt;p&gt;Import and export semantics were the real rabbit hole. feopack gradually added support for:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;default imports&lt;/li&gt;
&lt;li&gt;named imports&lt;/li&gt;
&lt;li&gt;exported variables&lt;/li&gt;
&lt;li&gt;exported functions&lt;/li&gt;
&lt;li&gt;named export declarations&lt;/li&gt;
&lt;li&gt;dynamic export definitions on the generated exports object&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This required transforming import declarations into namespace objects and rewriting imported bindings. For example, a named import needs to become a property access on the imported module namespace. That is the role of the binding rewriter: find identifiers that came from imports and rewrite them to the generated namespace access.&lt;/p&gt;
&lt;p&gt;The current implementation leaves many cases unsupported:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;namespace imports are not supported yet&lt;/li&gt;
&lt;li&gt;anonymous &lt;code&gt;export default function&lt;/code&gt; is not supported&lt;/li&gt;
&lt;li&gt;&lt;code&gt;export ... from&lt;/code&gt; re-exports are not supported&lt;/li&gt;
&lt;li&gt;destructuring export declarations are not supported&lt;/li&gt;
&lt;li&gt;many non-JavaScript assets are out of scope&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;These limitations are useful because they mark the actual boundary of the implementation. Every JavaScript syntax form expands the bundler&apos;s semantic surface. Supporting &lt;code&gt;import title, { num, plusNum } from &quot;./app.js&quot;&lt;/code&gt; is not just parsing a string. It affects graph resolution, AST transformation, runtime export definition, and generated code behavior.&lt;/p&gt;
&lt;h2&gt;10. Putting the Build Path Together&lt;/h2&gt;
&lt;p&gt;The useful way to look at this milestone is not as a feature checklist, but as a sequence of data structures. Each step gives the next step a more precise shape to work with.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;flowchart LR
  Config[&quot;【User Config】&amp;lt;br/&amp;gt;entry / output / mode&quot;] --&amp;gt; Compilation[&quot;【Compilation】&amp;lt;br/&amp;gt;build state&quot;]

  subgraph Make[&quot;Make&quot;]
    direction TB
    ModuleGraph[&quot;【ModuleGraph】&amp;lt;br/&amp;gt;source files -&amp;gt; modules + dependencies&quot;]
  end

  subgraph Seal[&quot;Seal&quot;]
    direction TB
    ChunkGraph[&quot;【ChunkGraph】&amp;lt;br/&amp;gt;chunk -&amp;gt; module_ids&quot;]
    Runtime[&quot;【Runtime Table】&amp;lt;br/&amp;gt;chunk + transformed modules&quot;]
    Asset[&quot;【GeneratedAsset】&amp;lt;br/&amp;gt;in-memory main.js&quot;]
    ChunkGraph --&amp;gt; Runtime --&amp;gt; Asset
  end

  subgraph Emit[&quot;Emit&quot;]
    direction TB
    Disk[&quot;【Output File】&amp;lt;br/&amp;gt;dist/main.js&quot;]
  end

  Compilation --&amp;gt; ModuleGraph
  ModuleGraph --&amp;gt; ChunkGraph
  Asset --&amp;gt; Disk
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This complete path is the part feopack currently implements: configuration enters from JavaScript, Rust builds a &lt;code&gt;Compilation&lt;/code&gt;, &lt;code&gt;make()&lt;/code&gt; constructs the module graph, &lt;code&gt;seal()&lt;/code&gt; groups modules into a chunk and generates an in-memory &lt;code&gt;GeneratedAsset&lt;/code&gt;, and &lt;code&gt;emit_assets()&lt;/code&gt; writes it to disk.&lt;/p&gt;
&lt;p&gt;Of course, this is still far from a real bundler. The obvious missing pieces are a plugin system, a more complete loader pipeline, CSS and asset modules, HMR, source maps, tree shaking, advanced chunk splitting, and much more complete ESM compatibility. If I continue working on feopack, these are probably the directions I will pick up one by one.&lt;/p&gt;
&lt;h2&gt;11. Why This Still Matters in the Agent Era&lt;/h2&gt;
&lt;p&gt;feopack is not production-ready, and it is not trying to be. That was never the point.&lt;/p&gt;
&lt;p&gt;We are in a moment where the industry is full of new tools, new workflows, new model capabilities, and new reasons to feel behind. I pay attention to those things too. It would be strange not to. But the more noise there is, the more important it becomes to keep some independent technical judgment.&lt;/p&gt;
&lt;p&gt;For me, that judgment cannot come only from reading announcements, watching demos, or collecting opinions from other people. It has to be grounded in the ability to take a system apart, rebuild a small version of it, and understand where the real complexity lives.&lt;/p&gt;
&lt;p&gt;That is why writing feopack mattered. Not because the world needs another bundler, but because implementing even a small path through a bundler forces the abstractions to become concrete: config, binding, compilation, module graph, chunk graph, runtime table, asset, emit. After that, “bundler architecture” is no longer just a phrase. It has weight.&lt;/p&gt;
&lt;p&gt;Writing about it is part of the same process. It is a way to leave evidence that I did not only skim the surface. I actually followed the data structures, hit the awkward edges, and formed my own understanding.&lt;/p&gt;
&lt;p&gt;Or, to keep the original Chinese line that says this better than I can:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;批五岳之图以为知山，不如樵夫之一足；谈沧溟之广以为知海，不如估客之一瞥；疏八珍之谱以为知味，不如庖丁之一啜。
及之而后知，履之而后艰，乌有不行而能知者乎？&lt;/p&gt;
&lt;/blockquote&gt;
</content:encoded></item><item><title>A Sui Chain TX Analyzer Based on NebulaGraph</title><link>https://furrycoder.com/posts/sui-nebula-analyzer/</link><guid isPermaLink="true">https://furrycoder.com/posts/sui-nebula-analyzer/</guid><description>How Sui-Nebula-Analyzer uses NebulaGraph to model on-chain wallet relationships and power relationship queries, statistics, and visualization.</description><pubDate>Sun, 29 Jun 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Original post: &lt;a href=&quot;https://dev.to/coderserio/ji-yu-nebulagraph-de-sui-lian-shang-guan-xi-fen-xi-qi-5522&quot;&gt;DEV Community&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Project Code: &lt;a href=&quot;https://github.com/CoderSerio/Sui-Nebula-Analyzer&quot;&gt;GitHub - CoderSerio/Sui-Nebula-Analyzer&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;I. Project Background and Challenges&lt;/h2&gt;
&lt;p&gt;As blockchain technology advances, on-chain data is exploding. Extracting valuable insights from this deluge and uncovering complex relationships is crucial for ensuring transaction compliance.&lt;/p&gt;
&lt;p&gt;Yet, raw blockchain data is often fragmented and unstructured, making analysis difficult. Blockchains like Sui, with their object-based model, differ from traditional account-based ones, posing new data analysis challenges.&lt;/p&gt;
&lt;p&gt;In blockchain relationship analysis, traditional relational databases have many limitations:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Poor performance for multi-hop queries&lt;/strong&gt;: In complex transaction networks, multi-hop queries are needed. But SQL&apos;s JOIN operations struggle with large-scale, multi-level networks, failing to meet real-time analysis needs.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Difficulty modeling dynamic relationships&lt;/strong&gt;: Relationships between wallet addresses on the blockchain change dynamically. Traditional relational databases can&apos;t easily model and adapt to these ever-changing relationships.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Limited graph algorithm support&lt;/strong&gt;: Detecting abnormal on-chain behaviors, identifying money laundering patterns, or conducting community analyses requires graph algorithms like PageRank and community detection. Traditional databases lack native graph computing capabilities, requiring data export to specialized platforms, increasing complexity and latency.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Graph databases, with their natural graph structure storage and query abilities, efficiently handle complex relationship data and natively support various graph algorithms, making them ideal for blockchain data analysis.&lt;/p&gt;
&lt;p&gt;So, I built a transaction relationship analysis platform using the NebulaGraph database to provide a solution for these problems.&lt;/p&gt;
&lt;h2&gt;II. Why Choose NebulaGraph?&lt;/h2&gt;
&lt;p&gt;NebulaGraph is a high-performance, distributed graph database whose design philosophy aligns with blockchain data analysis requirements. It can:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Complex relationship queries&lt;/strong&gt;: It natively supports multi-hop queries, path analysis, and cycle detection via its graph query language (nGQL), enabling the discovery of deep relationships within vast on-chain data.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;High-performance graph computing&lt;/strong&gt;: It has built-in or easily integrable graph algorithms like PageRank, community detection, and centrality, providing robust support for advanced analysis.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Real-time analysis&lt;/strong&gt;: It can respond to complex relationship queries in sub-seconds, meeting the real-time analysis demands of blockchain data.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Flexible modeling&lt;/strong&gt;: The graph model naturally adapts to the dynamic nature of blockchain data, making it easy to model and extend entities like wallets, transactions, and objects, as well as their complex relationships.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;III. System Architecture and Data Model&lt;/h2&gt;
&lt;h3&gt;3.1 Overview of the Architecture&lt;/h3&gt;
&lt;p&gt;The project&apos;s backend services consist of two layers: Next.js Server and Gateway Server:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://dev-to-uploads.s3.amazonaws.com/uploads/articles/8lgjad75quhitwg0y6jd.png&quot; alt=&quot;Sui analysis platform program structure&quot; /&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Frontend Application (Next.js Application)&lt;/strong&gt;: The user interface responsible for data visualization and user interaction.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;API Layer (Next.js API Routes)&lt;/strong&gt;: Provides application-specific API interfaces, handles business logic, and coordinates with the Gateway Server for database operations. For instance, &lt;code&gt;/api/execute&lt;/code&gt; for query execution, &lt;code&gt;/api/data-collection&lt;/code&gt; for blockchain data ingestion, and &lt;code&gt;/api/debug-db&lt;/code&gt; for database debugging.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Gateway Server (Express App)&lt;/strong&gt;: Since Nebula&apos;s HTTP API is provided by Nebula Gateway, and the &lt;code&gt;Nebula Gateway SDK&lt;/code&gt; is not very compatible with Next.js Server, a pure Node.js service is used here to connect and access Nebula Gateway.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;NebulaGraph Database&lt;/strong&gt;: Stores and manages Sui on-chain data, offering high-performance graph query and computing capabilities.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;3.2 Data Collection and Processing&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;https://dev-to-uploads.s3.amazonaws.com/uploads/articles/e23hxs73i2ya1fna417e.png&quot; alt=&quot;NebulaGraph data collection&quot; /&gt;&lt;/p&gt;
&lt;p&gt;Sui-Nebula-Analyzer enables automated on-chain data collection, transforming raw Sui blockchain data into structured data suitable for storage in a graph database. The collection process involves obtaining transactions, events, and object information from the Sui chain and mapping them to vertices and edges in NebulaGraph.&lt;/p&gt;
&lt;h3&gt;3.3 NebulaGraph Data Model&lt;/h3&gt;
&lt;p&gt;To efficiently store and query Sui on-chain data, the project has designed specific graph spaces, tags, and edge types in NebulaGraph.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Graph Space Management:&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;DROP SPACE IF EXISTS sui_analysis;
CREATE SPACE IF NOT EXISTS sui_analysis (partition_num = 10, replica_factor = 1, vid_type = FIXED_STRING(64));
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Tag Definitions:&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;The &lt;code&gt;wallet&lt;/code&gt; tag represents wallet addresses on the Sui chain, with the following properties:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;CREATE TAG IF NOT EXISTS wallet (
  address string NOT NULL,
  first_seen datetime,
  last_seen datetime,
  transaction_count int DEFAULT 0,
  total_amount double DEFAULT 0.0,
  is_contract bool DEFAULT false,
  sui_balance double DEFAULT 0.0,
  owned_objects_count int DEFAULT 0,
  last_activity datetime
);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Edge Type Definitions:&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;The &lt;code&gt;transaction&lt;/code&gt; edge type represents transactions between wallets, with the following properties:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;CREATE EDGE IF NOT EXISTS transaction (
  amount double NOT NULL,
  tx_timestamp datetime NOT NULL,
  tx_hash string NOT NULL,
  gas_used int DEFAULT 0,
  success bool DEFAULT true,
  transaction_type string DEFAULT &apos;unknown&apos;
);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The &lt;code&gt;related_to&lt;/code&gt; edge type represents relationships between wallets, which are derived from analysis algorithms, with the following properties:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;CREATE EDGE IF NOT EXISTS related_to (
  relationship_score double NOT NULL,
  common_transactions int DEFAULT 0,
  total_amount double DEFAULT 0.0,
  first_interaction datetime,
  last_interaction datetime,
  relationship_type string DEFAULT &quot;unknown&quot;,
  avg_gas_used double DEFAULT 0.0
);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This data model design fully utilizes NebulaGraph&apos;s graph capabilities, enabling efficient storage and querying of complex on-chain relationships and laying the foundation for in-depth analysis.&lt;/p&gt;
&lt;h2&gt;IV. Core Features and Applications&lt;/h2&gt;
&lt;p&gt;Sui-Nebula-Analyzer extensively uses NebulaGraph&apos;s graph query capabilities across its functional modules. Below are practical application examples demonstrating efficient on-chain data analysis using nGQL.&lt;/p&gt;
&lt;h3&gt;4.1 Statistical Analysis&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;https://dev-to-uploads.s3.amazonaws.com/uploads/articles/f1ag6cnqgerefm0ax8af.png&quot; alt=&quot;Sui tx and wallet count&quot; /&gt;&lt;/p&gt;
&lt;p&gt;The project provides queries for basic on-chain statistics, helping users quickly understand the overall picture of the Sui network:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Counting wallets:&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;USE sui_analysis;
MATCH (n:wallet) RETURN count(n) as count;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Counting transactions:&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;USE sui_analysis;
MATCH ()-[e:transaction]-&amp;gt;() RETURN count(e) as count;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Counting relationships:&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;USE sui_analysis;
MATCH ()-[r:related_to]-&amp;gt;() RETURN count(r) as count;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;4.2 Relationship Queries and Analysis&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;https://dev-to-uploads.s3.amazonaws.com/uploads/articles/yrp2qnmv0a04b3vo9k5z.png&quot; alt=&quot;Sui wallets&apos; relationship analysis&quot; /&gt;&lt;/p&gt;
&lt;p&gt;This is one of the core functions of Sui-Nebula-Analyzer. Through nGQL&apos;s powerful graph traversal and pattern matching capabilities, it uncovers complex relationships between wallets.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Basic mode: Querying relationships&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;This query retrieves precomputed relationships between wallets, usually based on the &lt;code&gt;related_to&lt;/code&gt; edge type and its &lt;code&gt;relationship_score&lt;/code&gt; attribute.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;USE sui_analysis;
MATCH (a:wallet)-[r:related_to]-(b:wallet)
WHERE r.relationship_score &amp;gt;= 0.1
RETURN a.wallet.address AS addr1,
       b.wallet.address AS addr2,
       r.relationship_score AS score,
       r.common_transactions AS common_tx,
       r.total_amount AS amount
LIMIT 50;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Pro mode: Querying enhanced fields&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Building on the basic query, this returns more detailed relationship information, such as average Gas consumption, first and last interaction times, relationship types, and wallet balances and object counts.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;USE sui_analysis;
MATCH (a:wallet)-[r:related_to]-(b:wallet)
WHERE r.relationship_score &amp;gt;= 0.1
RETURN a.wallet.address AS addr1,
       b.wallet.address AS addr2,
       r.relationship_score AS score,
       r.common_transactions AS common_tx,
       r.total_amount AS amount,
       r.avg_gas_used AS avg_gas,
       r.first_interaction AS first_interaction,
       r.last_interaction AS last_interaction,
       r.relationship_type AS rel_type,
       a.wallet.sui_balance AS addr1_balance,
       a.wallet.owned_objects_count AS addr1_objects,
       a.wallet.is_contract AS addr1_contract
LIMIT 50;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Fallback query: Inferring relationships from transaction edges (when there are no precomputed relationships)&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;When there are no precomputed &lt;code&gt;related_to&lt;/code&gt; edges, relationships between wallets can be inferred by directly querying &lt;code&gt;transaction&lt;/code&gt; edges. This is very useful for real-time or more granular transaction analysis.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;USE sui_analysis;
MATCH (a:wallet)-[r:transaction]-(b:wallet)
RETURN a.wallet.address AS addr1,
       b.wallet.address AS addr2,
       r.amount AS amount,
       r.gas_used AS gas_used,
       r.success AS success,
       r.transaction_type AS tx_type,
       r.tx_timestamp AS tx_time,
       a.wallet.sui_balance AS addr1_balance,
       a.wallet.owned_objects_count AS addr1_objects,
       a.wallet.is_contract AS addr1_contract
LIMIT 200;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;4.3 Address Details and Related Account Queries&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;https://dev-to-uploads.s3.amazonaws.com/uploads/articles/0u77n2ivqrsxarclsyli.png&quot; alt=&quot;Sui related addresses of wallets&quot; /&gt;&lt;/p&gt;
&lt;p&gt;The project allows users to query detailed information about a specific Sui wallet address and its directly related accounts, which is crucial for in-depth analysis of individual entity behavior patterns.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Querying target address basic information:&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;USE sui_analysis;
MATCH (target:wallet)
WHERE id(target) == &quot;${searchAddress}&quot;
RETURN target.wallet.address AS address,
       target.wallet.transaction_count AS tx_count,
       target.wallet.total_amount AS total_amount,
       target.wallet.first_seen AS first_seen,
       target.wallet.last_seen AS last_seen;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Querying related accounts of the target address:&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;USE sui_analysis;
MATCH (target:wallet)-[r:related_to]-(related:wallet)
WHERE id(target) == &quot;${searchAddress}&quot;
RETURN related.wallet.address AS address,
       r.relationship_score AS score,
       r.common_transactions AS common_tx,
       r.total_amount AS total_amount,
       r.first_interaction AS first_interaction,
       r.last_interaction AS last_interaction,
       r.relationship_type AS type
LIMIT 20;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Fallback: Analyzing relationships from transaction edges&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;When analyzing relationships of a specific address from raw transaction data, this query can be used.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;USE sui_analysis;
MATCH (target:wallet)-[r:transaction]-(related:wallet)
WHERE id(target) == &quot;${searchAddress}&quot;
RETURN related.wallet.address AS address,
       related.wallet.transaction_count AS tx_count,
       related.wallet.total_amount AS amount,
       r.amount AS tx_amount,
       r.tx_timestamp AS tx_time
LIMIT 10;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;4.4 Visualizing Network Connections&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;https://dev-to-uploads.s3.amazonaws.com/uploads/articles/sderrlwapmfhi9wkb27g.png&quot; alt=&quot;Sui tx relationship visualization&quot; /&gt;&lt;/p&gt;
&lt;p&gt;To provide an intuitive transaction network view, the project supports querying center nodes and their relationships, offering data support for frontend visualization.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Querying center node information:&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;USE sui_analysis;
MATCH (center:wallet)
WHERE id(center) == &quot;${searchAddress}&quot;
RETURN center.wallet.address AS center_address,
       center.wallet.transaction_count AS center_tx_count,
       center.wallet.total_amount AS center_amount;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Querying network relationships:&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;USE sui_analysis;
MATCH (center:wallet)-[r:transaction]-(connected:wallet)
WHERE id(center) == &quot;${searchAddress}&quot;
RETURN connected.wallet.address AS connected_address,
       connected.wallet.transaction_count AS connected_tx_count,
       connected.wallet.total_amount AS connected_amount,
       r.amount AS edge_amount,
       r.tx_timestamp AS tx_time
LIMIT 50;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;V. Summary&lt;/h2&gt;
&lt;p&gt;The Sui-Nebula-Analyzer project successfully combines Sui blockchain data with the NebulaGraph graph database to build an efficient and flexible on-chain relationship analysis platform demo.&lt;/p&gt;
&lt;p&gt;Although it&apos;s just a demo project, the demonstrated engineering architecture is effective and showcases NebulaGraph&apos;s powerful functionality and scalability in such scenarios. In a production environment, more graph algorithms like community detection and influence analysis could be integrated on this basis to achieve more precise relationship analysis.&lt;/p&gt;
</content:encoded></item></channel></rss>