hmr 2.1.0
hmr: ^2.1.0 copied to clipboard
Hot Module Replacement system specially designed for command-line Dart applications.
๐ Hot Module Replacement for Dart Applications #
HMR Dart brings Flutter-grade hot reload to command-line Dart applications.
It runs your program as a child process attached to the Dart VM service, then patches code in place on every save โ without restarting the process, without losing in-memory state, and without you wiring anything up.
Highlights #
| Feature | Description |
|---|---|
| ๐ช Zero configuration | dart pub global activate hmr then hmr โ that's it |
| โก True hot reload | Patches the running isolate via the VM service; state is preserved |
| ๐ Automatic full-restart fallback | Shape changes (new fields, etc.) silently fall back to a full restart |
| โจ๏ธ Hot keys | r reload ยท R restart ยท c clear ยท h help ยท q quit |
| ๐ง Typed runtime API | Opt into package:hmr/runtime.dart to react to events from inside your app |
| ๐งฉ Composable library | Build your own runner from RunStrategy + FileWatcher + Presenter |
| ๐ฏ Targeted file watching | Glob includes / excludes configured in pubspec.yaml |
| ๐งพ Structured output | --format=json for one JSON event per line โ pipeable into anything |
| ๐ฅ Crash visibility | Child stack traces are surfaced verbatim โ no truncation, ever |
Contents #
Install #
dart pub global activate hmr
This puts the hmr executable on your PATH. Alternatively, add the package
to your pubspec.yaml and run it as dart run hmr:
dev_dependencies:
hmr: ^2.0.0
Usage modes #
1. Built-in CLI (zero-config) #
From any Dart project root:
hmr
That covers the 80% case. hmr watches **/*.dart from the current
directory and launches bin/<package>.dart (or bin/main.dart). Save a
file, see the change live, state preserved.
App arguments go after --:
hmr -- --port 8080 --verbose
2. Runtime API (in-app hooks) #
When your app needs to react to reloads (re-register handlers, invalidate
caches, re-open connections, etc.), import package:hmr/runtime.dart from
your own main.dart:
import 'package:hmr/runtime.dart';
void main(List<String> args) {
Hmr.instance.init();
Hmr.instance.onReload((e) {
print('Code reloaded โ handlers still valid');
});
Hmr.instance.onRestart((e) {
print('Restarted โ re-warming caches');
});
Hmr.instance.onFileModified((change) {
print('Saved: ${change.path}');
});
// ... your app starts here
}
The runtime is a no-op outside hmr (detected via the HMR_PARENT_PID
environment variable), so the same main() works for both hmr and
plain dart run. No conditional imports, no production-build flags.
3. Custom runner (build your own) #
When you need behaviour the built-in CLI doesn't expose โ a custom output format, alternate file watcher, extra hot keys, integration with an existing supervisor โ compose the library directly:
import 'dart:io';
import 'package:glob/glob.dart';
import 'package:hmr/hmr.dart';
import 'package:path/path.dart' as path;
Future<void> main(List<String> args) async {
final root = Directory.current.path;
final strategy = VmServiceProcessStrategy(
entrypoint: File(p.join(root, 'bin', 'main.dart')),
args: args,
);
final orchestrator = ReloadOrchestrator(
strategy: strategy,
watcher: FileWatcher(root),
debounce: const Duration(milliseconds: 50),
filters: [
ignoreSegment(const ['.git', '.dart_tool']),
includeGlobs([Glob(path.join(root, '**.dart'))]),
],
);
// Bring your own presenter, or use AnsiPresenter / JsonPresenter.
final presenter = AnsiPresenter()
..attach(orchestrator.events);
ProcessSignal.sigint.watch().listen((_) async {
await orchestrator.stop();
await presenter.dispose();
exit(0);
});
await orchestrator.start();
}
A fully-fleshed example (with hot keys + a custom timestamped presenter)
lives at example/bin/custom_hmr.dart.
Configuration reference #
pubspec.yaml (hmr: block) #
Every field is optional. With no hmr: block at all, defaults apply.
hmr:
entrypoint: bin/server.dart
debounce: 50
includes:
- "**/*.dart"
- "config/*.yaml"
excludes:
- "test/**"
- "doc/**"
| Field | Type | Default | Description |
|---|---|---|---|
entrypoint |
String |
bin/<package>.dart then bin/main.dart |
Path (relative to project root) to the Dart file hmr runs. |
debounce |
int (ms) |
0 |
Coalesce file-system events within this window into a single reload. Must be >= 0. |
includes |
List<String> |
['**.dart'] |
Glob patterns; a file change only triggers a reload if it matches at least one include. |
excludes |
List<String> |
[] (plus built-in ignores) |
Glob patterns; matching files are dropped even if includes would accept them. |
Entrypoint resolution priority (highest wins):
- Positional CLI argument (reserved โ not currently exposed)
hmr.entrypointinpubspec.yamlbin/<package-name>.dartbin/main.dart
hmr always ignores .git, .dart_tool, .idea, .vscode, and ~
regardless of includes / excludes.
Globs use the glob package syntax. They
are anchored at the project root, so lib/**.dart matches lib/a.dart
and lib/sub/b.dart.
CLI flags #
-f, --format Output format: "ansi" (default) or "json"
-h, --help Show usage
Arguments after -- are forwarded to the child process verbatim.
Runtime API #
package:hmr/runtime.dart exports the Hmr singleton and the event
sealed-class hierarchy.
import 'package:hmr/runtime.dart';
Hmr.instance.init();
| Member | Purpose |
|---|---|
Hmr.instance |
Process-wide singleton. |
Hmr.forTesting() |
Fresh isolated instance for unit tests. |
bool isActive |
true when launched by the hmr supervisor (HMR_PARENT_PID set). |
void init() |
Registers the VM service extension. Idempotent. No-op outside hmr. |
void on<E extends RunnerEvent>(handler) |
Catch-all by event type. on<RunnerEvent>(โฆ) receives every event. |
void onReload(handler) |
Fires after a successful hot reload (ReloadKind.hotReload). |
void onRestart(handler) |
Fires after a successful hot restart (ReloadKind.hotRestart). |
void onFileCreated(handler) |
Fires for each FsCreated file event. |
void onFileModified(handler) |
Fires for each FsModified file event. |
void onFileDeleted(handler) |
Fires for each FsDeleted file event. |
void onFileMoved(handler) |
Fires for each FsMoved file event. |
Handler exceptions are caught and logged to stderr โ a buggy hook never crashes your app.
Custom runner โ building blocks #
Import package:hmr/hmr.dart to get the composable pieces.
| Symbol | Role |
|---|---|
VmServiceProcessStrategy |
Launches the entrypoint as a child process, owns the VM service connection. |
RunStrategy (interface) |
Implement to replace the strategy entirely. |
FileWatcher(root) |
Recursive file-system watcher emitting FsEvents. |
ignoreSegment(List<String>) |
Filter: drop events whose path contains any forbidden path segment. |
includeGlobs(List<Glob>) |
Filter: keep events matching at least one glob. |
excludeGlobs(List<Glob>) |
Filter: drop events matching any glob. |
ReloadOrchestrator |
Wires strategy + watcher + filters + debounce. Exposes start/stop/reload/restart. |
AnsiPresenter |
Default human-readable terminal output. |
JsonPresenter |
One JSON object per event line (same schema as --format=json). |
Presenter (interface) |
Implement attach(Stream<RunnerEvent>) + dispose() for custom output. |
HotKeyController |
Reads raw stdin, emits HotKey enum values. Injectable input + raw-mode fn. |
EntrypointResolver |
Reusable entrypoint resolution (CLI โ config โ conventions). |
Config.of(YamlMap) |
Parse + validate the hmr: pubspec block. Throws ConfigError on bad input. |
ReloadOrchestrator constructor:
ReloadOrchestrator({
required RunStrategy strategy,
required FileWatcher watcher,
List<FsFilter> filters = const [],
Duration debounce = Duration.zero,
});
VmServiceProcessStrategy constructor:
VmServiceProcessStrategy({
required File entrypoint,
List<String> args = const [],
VmServiceLauncherFn? launcher, // override for tests
});
Events (JSON format) #
With --format=json each line is a self-contained JSON object. Every
event carries event (the discriminator) and ts (milliseconds since
epoch).
event |
Extra fields |
|---|---|
started |
โ |
fileChanged |
change (FsEvent: kind, path, at, optional to) |
compileStarted |
trigger, optional fileEvent |
compileSucceeded |
elapsedMs |
compileFailed |
stderr |
reloadSucceeded |
kind (hotReload or hotRestart) |
reloadFailed |
reason |
processCrashed |
exitCode, stderr (verbatim, no truncation) |
stopped |
โ |
FsEvent.kind is one of created, modified, deleted, moved.
moved events additionally carry a to field with the destination path.
Runtime-API handlers receive deserialized RunnerEvent objects of the
matching sealed class โ you never parse JSON yourself.
License #
MIT