跪拜 Guibai
← Back to the summary

dmx Injects Generated Dart Code Directly Into Your Source Files on Save


theme: smartblue

It's quite interesting. The community recently released a third-party package called dmx, which is primarily a Code Generator for Dart source code inlining. It takes a different route from Freezed / json_serializable / build_runner. Simply put: it generates code directly when you save a Dart file, and writes the generated results back into the class body of the current .dart file.

It positions itself as "Dart macros", but the "macro" here is not a language-level Macro provided by the Dart compiler. The @dmx('model') annotation provided by the project is essentially just a regular annotation, mainly relying on the external dmx generator.

For example, writing a model in Flutter usually looks something like this:

class User {
  const User({
    required this.id,
    required this.name,
    this.email,
  });


  final String id;
  final String name;
  final String? email;
}

And the common Freezed approach is to define an annotation, then run dart run build_runner build:

@freezed
class User with _$User {
  ...
}

Then dmx's idea is much more straightforward. For example, with the annotation below, you just need to Ctrl+S, and it will directly generate or modify user.dart:

import 'package:dmx/dmx.dart';


@dmx('model')
class User {
  const User({
    required this.id,
    required this.name,
    this.email,
  });


  final String id;
  final String name;
  final String? email;
}

What's the most interesting part? The generated code is right inside your class, without things like part 'user.g.dart';, and without needing build_runner watch every time. It's directly "Generated on save":

So its most interesting feature is "Inline Codegen". What dmx does is roughly like this:

So the main dmx generator is written in Rust. The VS Code extension directly bundles the dmx binary for the corresponding platform. After you install the extension and open a Dart workspace, it automatically starts a watcher. When the watcher sees profile.dart changed, it only re-processes that specific file.

Then, internally, dmx uses tree-sitter to parse Dart. It doesn't use regex to search for things like class xxx {. dmx uses tree-sitter-dart to construct a lossless Concrete Syntax Tree, meaning it can know:

where the class body is, what fields it has, what annotations are present, and which //#region belongs to which class.

Moreover, dmx's spec specifically requires that comment tokens and class-body spans must be reliable.

Then Rust converts the Dart class into data that the generator can understand, for example:

class User {
  final String id;
  final String? email;
}

dmx will parse it into something like:

className = User


fields:
  id:
    type = String
    nullable = false


  email:
    type = String?
    nullable = true

Subsequently, Rust will further calculate:

decodeExpr
encodeExpr
equalsExpr
hashExpr
copyParam
copyArg

Then complex Dart type judgments are done in Rust, and the Mustache template is only responsible for "what it looks like". So the template might just be:

@override
bool operator ==(Object other) =>
    identical(this, other) ||
    (other is {{className}}
{{#fields}}
      && {{equalsExpr}}
{{/fields}}
    );

And equalsExpr is pre-calculated by Rust, then used by Mustache to generate Dart.

Currently, dmx comes with 11 generators:

model, union, enum, diff, lerp, validate, table, route, cli, fake, restClient.

For example:

So its goal is to be a general-purpose Dart codegen framework.

And you can also write your own 'Macro', which is also the most interesting part. For example, you can write:

final class AuditMacro extends DmxMacro {


  @override
  String get name => 'audit';


  @override
  DmxOutput expand(DmxInvocation invocation) {
    final name = invocation.declaration.name;


    return DmxFragment(
      "String get auditLabel => '$name';",
    );
  }
}

Then in your project, write:

@dmx('audit')
class Order {
  final int id;
}

After saving, you get:

@dmx('audit')
class Order {
  final int id;


  //#region


  String get auditLabel => 'Order';


  //#endregion
}

So the so-called "Dart Custom Macro" here is actually just an external source generator plugin written in Dart.

And with this, you can do a lot of wild things. For example, with SQLite, a Macro can directly read the SQLite database schema, and then we can:

@dmx('sqliteSchema')
class ProductRow {}

This way, based on the real database:

products
 ├ id INTEGER
 ├ name TEXT
 └ price REAL

It can directly generate:

class ProductRow {
  final int id;
  final String name;
  final double price;


  ...
}

Or read the project's openapi.json, analyze a series of parameters:

paths
schemas
$ref
nullable
array
response

And then directly generate:

It can even handle unnamed structures in OpenAPI and assign them Dart class names. At this point, dmx is already somewhat approaching:

source_gen
+
build_runner
+
template engine
+
IDE watcher
+
source rewriter

Overall, I feel this is much more comfortable than Flutter's runner approach, and the angle is quite good. Recently, community ideas have indeed been more interesting than the official ones. However, the problem is that it seems no one opens an IDE anymore, and the demand for output scenarios like JSON doesn't seem to be high. It feels like this project arrived a bit too late.

But even if you don't open VSCode, actual projects can still use it. dmx itself has a standalone CLI, for example:

dmx build [PATHS...] [--insert-regions] [--check]
dmx watch [PATHS...]

So it can also be used in AI scenarios, but whether you need it depends on your own thoughts.

Link

https://github.com/Nimblesite/dmx