Dart code generation that runs when you save. Annotate a class, and copyWith,
equality, hashCode, toString and typed JSON appear inside the same file,
below a divider — no part files, no .g.dart, no mixins, no delegating
factories, and no build_runner run.
Run the real generator in your browser →
The playground compiles this repository's Rust generator to WebAssembly, so it builds the same context, renders the same Mustache template, validates the same way, and splices the same region as the CLI. Edit the Dart and the template. Neither input leaves the tab.
VS Code — install dmx — Dart code generation from the Marketplace, or from a terminal:
code --install-extension nimblesite.dmxThe extension bundles the dmx binary and starts watching when you open a
trusted Dart workspace. There is no Rust, no Cargo, and no command to run.
Any other editor — install the CLI and leave the watcher running:
brew install nimblesite/tap/dmx # or: scoop install dmx
dmx watch libAdd the runtime the generated code composes with:
dart pub add dmxAnnotate a class and save the file:
import 'package:dmx/dmx.dart';
@dmx('model')
class User {
const User({required this.id, this.email, this.tags = const []});
final String id;
final String? email;
final List<String> tags;
}The members appear below the divider, in the file you were already looking at:
//#region
static Result<User, DecodeError> fromJson(Object? json, [String path = 'User']) =>
switch (json) {
{
'id': final String id,
'tags': final List<dynamic> tags,
} =>
switch ((
dmxNullable<String>(dmxKey(json, 'email'), '$path.email', dmxString),
dmxList<String>(tags, '$path.tags', dmxString),
)) {
(Ok(value: final email), Ok(value: final tags)) =>
Ok(User(id: id, email: email, tags: tags)),
(Err(error: final e), _) => Err(e),
(_, Err(error: final e)) => Err(e),
},
_ => Err(DecodeError(path, 'User', json)),
};
//#endregionPlus toJson, ==, hashCode, toString and copyWith. Decoding returns a
sealed Result, so a bad payload is a branch that names the field that failed
— Order.lines[2].product.price — rather than a thrown type error.
The getting started guide covers the rest.
Eleven built-in macros. Each one is an annotation name, a Rust context builder, and a Mustache template.
@dmx('model') immutable data class |
@dmx('union') sealed sum type |
@dmx('enum') wire-safe enums |
@dmx('diff') what changed, as data |
@dmx('lerp') interpolation |
@dmx('validate') accumulating constraints |
@dmx('table') SQL schema and rows |
@dmx('route') typed deep links |
@dmx('cli') argv parser and usage |
@dmx('fake') deterministic fixtures |
@dmx('restClient') HTTP implementations |
Templates you own. src/dmx/templates/model.mustache
decides what the output looks like, top to bottom. Every expression reaches the
template already worked out (resultExpr, equalsExpr, copyArg, …), so
changing the layout never means working out Dart types yourself.
Macros written in Dart. A macro receives a typed view of the declaration — name, fields, types, annotations — and returns the Dart to emit, or hands its model to a Mustache template and lets dmx render it with the same engine the built-ins use. Two worked examples do exactly that: one reads a live SQLite database, one reads an OpenAPI document.
Models defined by a diagram. Some types have no Dart file to annotate yet. Write the model once as a typeDiagram definition and save:
models/parcel.td the definition
lib/parcel.dart what dmx writes
type Parcel {
id: Uuid
weightG: Int
insured: Option<Decimal>
}
final class Parcel {
const Parcel({required this.id, required this.weightG, this.insured});
final String id;
final int weightG;
final String? insured;
@override
bool operator ==(Object other) => /* every field, collections by content */;
@override
int get hashCode => Object.hash(runtimeType, id, weightG, insured);
Parcel copyWith({String? id, int? weightG, dmx.DmxPatch<String?> insured});
}
/// JSON for [Parcel].
extension ParcelJson on Parcel {
static dmx.Result<Parcel, dmx.DecodeError> fromJson(Object? json, [String path = 'Parcel']);
Map<String, Object?> toJson();
}Nothing is embedded in anything: the .td is what any typeDiagram tool reads,
and lib/parcel.dart is one complete Dart file, relative to the package the
definition belongs to. The class is an immutable value — it compares by value,
hashes consistently with that comparison, and copies — and its JSON lives on an
extension beside it rather than inside it, so the class reads as what the
diagram said and nothing else. That is the canonical model template dmx
ships: one template, and every model class comes out of it.
To decide the shape yourself, put parcel.mustache beside parcel.td and it
takes the canonical template's place. Any other template beside the definition
is an extra output, bound by its name — the
shipping example defines four types once
and generates two different Dart files from them. dmx reads the definition
itself — no Node, no npm package, no typediagram executable.
The definition and its templates can also live inside one *.dmx.md document,
when the model, the diagram, and the prose explaining them belong on one page:
```typeDiagram
type Parcel {
id: Uuid
}
```
```mustache {"dmx":{"output":"lib/parcel.dart"}}
{{#declarations}}final class {{name}} {}{{/declarations}}
```It never writes broken Dart.
| Guarantee | Mechanism |
|---|---|
| Never emits unparseable Dart | The whole candidate file is re-parsed before writing [validation] |
| Never touches your code | Bytes outside the region are diffed pre-write [emission.inline-backend.byte-exactness] |
| Leaves labelled folds alone | Only the bare, unlabelled //#region block is machine-owned |
| Repairs a region you gutted | dmx empties the region, re-parses, and regenerates [emission.inline-backend.region-recovery] |
| Zero writes when nothing changed | Byte-compare before write [emission.inline-backend.no-op-writes] |
| Generated code obeys the house rules | No throw, as, ! or _$ names — asserted over the whole golden corpus, and enforced on the CST for templates dmx did not write [hygiene] |
| Never overwrites a file it does not own | A generated file carries an ownership marker on its first line; anything without one is yours [dartmacros.files] |
dmx build [PATHS...] [--insert-regions] [--check]
dmx watch [PATHS...]
dmx explain FILE
build and watch default to lib. Both take Dart sources, *.td definitions
and *.dmx.md documents found under the paths given, and any Markdown file named
explicitly. watch regenerates what changed — including when you edit a
.mustache file beside a definition — and debounces save bursts. --check
writes nothing and exits 2 on drift, for CI. dmx explain models/parcel.td
prints each generation group, its outputs, its dependency digests, and the exact
context its templates will see — without generating anything.
make help # every target
make ci # every gate CI runs: fmt, clippy, duplication, tests, Dart, website, buildThe specifications and plans give
every requirement a dotted identifier — [emission.inline-backend] — that code,
tests, and diagnostics cite, so grep -r returns the requirement, its
implementation, and its tests together.
BSD-3-Clause, copyright Nimblesite Pty Ltd. The crate, the extension, the published Dart package, and the generated code all ship under it.