enableHotReload function

Future<HotReloader> enableHotReload(
  1. NitroServer server, {
  2. Iterable<String>? watch,
  3. void log(
    1. String message
    )?,
})

Wires server's route NitroServer.reload to every VM hot reload. Returns the HotReloader; call .stop() to detach. A failing setup is printed to stderr rather than killing the reload loop.

hotreloader only auto-watches bin/, lib/ and test/, so a server whose entry lives elsewhere (an example/ or benchmark/ file) would never reload. watch fixes that: the directories to watch for .dart changes, defaulting to the running script's own directory. Pass const [] to rely on hotreloader's watchers alone.

Pass log to trace reload activity — the watched directories, each change that triggers a reload, the VM reload result and the route rebuild. Left null it is silent (only a failing setup still goes to stderr). Pass print, stderr.writeln, or route it into your own logger.

Implementation

Future<HotReloader> enableHotReload(
  NitroServer server, {
  Iterable<String>? watch,
  void Function(String message)? log,
}) async {
  final reloader = await HotReloader.create(
    onAfterReload: (ctx) {
      log?.call('hot reload: VM ${ctx.result.name}; rebuilding routes');
      server.reload().then(
        (_) => log?.call(
          'hot reload: routes rebuilt (${server.isolates} '
          'isolate(s))',
        ),
        onError: (Object error, StackTrace stack) {
          stderr.writeln('nitro_server: hot reload failed: $error\n$stack');
        },
      );
    },
  );

  // Tear everything down when the server stops — cancel the watchers and the
  // pending debounce BEFORE stopping the reloader, so a save racing shutdown
  // never fires a reload against a disposed VM service.
  var stopped = false;
  final watchers = <StreamSubscription<FileSystemEvent>>[];
  Timer? debounce;
  Future<void> teardown() async {
    if (stopped) return;
    stopped = true;
    debounce?.cancel();
    for (final w in watchers) {
      await w.cancel();
    }
    await reloader.stop();
  }

  unawaited(server.onClose.then((_) => teardown()));

  final dirs = {...?watch, if (watch == null) ?_scriptDir()};
  log?.call('hot reload: watching ${dirs.join(', ')}');
  for (final dir in dirs) {
    if (!Directory(dir).existsSync()) continue;
    watchers.add(
      Directory(dir).watch(recursive: true).listen((event) {
        if (stopped || !event.path.endsWith('.dart')) return;
        final path = event.path;
        // Trailing debounce: fire after the writes settle, so an editor's
        // temp-file-then-rename save is fully on disk before the VM re-reads it.
        debounce?.cancel();
        debounce = Timer(const Duration(milliseconds: 300), () async {
          if (stopped) return;
          log?.call('hot reload: $path changed, reloading');
          try {
            await reloader.reloadCode();
          } catch (error, stack) {
            // A reload can fail (a compile error in the edit) or race shutdown
            // (the VM service connection disposes mid-reload). Log it with the
            // stack trace; never let it crash the running server.
            stderr.writeln('nitro_server: hot reload skipped: $error\n$stack');
          }
        });
      }),
    );
  }
  return reloader;
}