Skip to content

Commit 48106c8

Browse files
authored
fix(dart_frog_cli): hot reload stability and error reporting (#91)
1 parent 1ed3a8e commit 48106c8

File tree

7 files changed

+373
-37
lines changed

7 files changed

+373
-37
lines changed

bricks/dart_frog_dev_server/__brick__/server.dart

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import 'package:dart_frog/dart_frog.dart';
1010
{{/middleware}}
1111
void main() => hotReload(createServer);
1212

13-
Future<HttpServer> createServer() async {
13+
Future<HttpServer> createServer() {
1414
final ip = InternetAddress.anyIPv4;
1515
final port = int.parse(Platform.environment['PORT'] ?? '{{port}}');
1616
final handler = buildRootHandler();
Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
import 'dart:async';
22
import 'dart:io';
33

4-
import 'package:shelf_hotreload/shelf_hotreload.dart' show withHotreload;
4+
import 'package:shelf_hotreload/shelf_hotreload.dart' show withHotreload, Level;
55

66
/// Hot reload support for the server returned by the [initializer].
77
void hotReload(FutureOr<HttpServer> Function() initializer) {
8-
withHotreload(initializer);
8+
withHotreload(initializer, logLevel: Level.WARNING);
99
}

packages/dart_frog/pubspec.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ environment:
99
dependencies:
1010
http_methods: ^1.1.0
1111
shelf: ^1.1.0
12-
shelf_hotreload: ^1.1.0
12+
shelf_hotreload: ^1.3.0
1313

1414
dev_dependencies:
1515
http: ^0.13.4

packages/dart_frog_cli/e2e/test/dart_frog_dev_test.dart

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ void main() {
2727

2828
tearDownAll(() async {
2929
await killDartFrogServer(process.pid);
30-
await tempDirectory.delete(recursive: true);
30+
tempDirectory.delete(recursive: true).ignore();
3131
});
3232

3333
testServer('GET / returns 200 with greeting', (host) async {

packages/dart_frog_cli/lib/src/commands/dev/dev.dart

Lines changed: 114 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,14 @@ typedef DirectoryWatcherBuilder = DirectoryWatcher Function(
3030
/// Typedef for [io.exit].
3131
typedef Exit = dynamic Function(int exitCode);
3232

33+
RestorableDirectoryGeneratorTarget get _defaultGeneratorTarget {
34+
return RestorableDirectoryGeneratorTarget(
35+
io.Directory(
36+
path.join(Directory.current.path, '.dart_frog'),
37+
),
38+
);
39+
}
40+
3341
/// {@template dev_command}
3442
/// `dart_frog dev` command which starts the dev server`.
3543
/// {@endtemplate}
@@ -39,6 +47,7 @@ class DevCommand extends DartFrogCommand {
3947
super.logger,
4048
DirectoryWatcherBuilder? directoryWatcher,
4149
GeneratorBuilder? generator,
50+
RestorableDirectoryGeneratorTarget? generatorTarget,
4251
Exit? exit,
4352
bool? isWindows,
4453
ProcessRun? runProcess,
@@ -50,7 +59,8 @@ class DevCommand extends DartFrogCommand {
5059
_isWindows = isWindows ?? io.Platform.isWindows,
5160
_runProcess = runProcess ?? io.Process.run,
5261
_sigint = sigint ?? io.ProcessSignal.sigint,
53-
_startProcess = startProcess ?? io.Process.start {
62+
_startProcess = startProcess ?? io.Process.start,
63+
_generatorTarget = generatorTarget ?? _defaultGeneratorTarget {
5464
argParser.addOption(
5565
'port',
5666
abbr: 'p',
@@ -66,6 +76,7 @@ class DevCommand extends DartFrogCommand {
6676
final ProcessRun _runProcess;
6777
final io.ProcessSignal _sigint;
6878
final ProcessStart _startProcess;
79+
final RestorableDirectoryGeneratorTarget _generatorTarget;
6980

7081
@override
7182
final String description = 'Run a local development server.';
@@ -75,6 +86,7 @@ class DevCommand extends DartFrogCommand {
7586

7687
@override
7788
Future<int> run() async {
89+
var hotReloadEnabled = false;
7890
final port = Platform.environment['PORT'] ?? results['port'] as String;
7991
final generator = await _generator(dartFrogDevServerBundle);
8092

@@ -87,9 +99,7 @@ class DevCommand extends DartFrogCommand {
8799
);
88100

89101
final _ = await generator.generate(
90-
DirectoryGeneratorTarget(
91-
io.Directory(path.join(cwd.path, '.dart_frog')),
92-
),
102+
_generatorTarget,
93103
vars: vars,
94104
fileConflictResolution: FileConflictResolution.overwrite,
95105
);
@@ -105,18 +115,28 @@ class DevCommand extends DartFrogCommand {
105115
// On Windows listen for CTRL-C and use taskkill to kill
106116
// the spawned process along with any child processes.
107117
// https://github.com/dart-lang/sdk/issues/22470
108-
if (_isWindows) {
109-
_sigint.watch().listen((_) async {
110-
final result = await _runProcess(
111-
'taskkill',
112-
['/F', '/T', '/PID', '${process.pid}'],
113-
);
114-
_exit(result.exitCode);
115-
});
116-
}
117-
118-
process.stdout.listen((_) => logger.info(utf8.decode(_)));
119-
process.stderr.listen((_) => logger.err(utf8.decode(_)));
118+
if (_isWindows) _sigint.watch().listen((_) => _killProcess(process));
119+
120+
var hasError = false;
121+
process.stderr.listen((_) async {
122+
hasError = true;
123+
logger.err(utf8.decode(_));
124+
125+
if (!hotReloadEnabled) {
126+
await _killProcess(process);
127+
_exit(1);
128+
}
129+
130+
await _generatorTarget.restore();
131+
});
132+
133+
process.stdout.listen((_) {
134+
final message = utf8.decode(_);
135+
if (message.contains('[hotreload]')) hotReloadEnabled = true;
136+
if (!hasError) _generatorTarget.cacheLatestSnapshot();
137+
hasError = false;
138+
logger.info(message);
139+
});
120140
}
121141

122142
final progress = logger.progress('Serving');
@@ -125,18 +145,87 @@ class DevCommand extends DartFrogCommand {
125145
progress.complete('Running on http://localhost:$port');
126146

127147
final watcher = _directoryWatcher(path.join(cwd.path, 'routes'));
128-
final subscription = watcher.events.listen((event) async {
129-
final file = io.File(event.path);
130-
if (file.existsSync()) {
131-
final contents = await file.readAsString();
132-
if (contents.isNotEmpty) {
133-
await codegen();
134-
}
135-
}
136-
});
148+
final subscription = watcher.events.listen((_) => codegen());
137149

138150
await subscription.asFuture<void>();
139151
await subscription.cancel();
140152
return ExitCode.success.code;
141153
}
154+
155+
Future<void> _killProcess(Process process) async {
156+
process.kill();
157+
if (_isWindows) {
158+
final result = await _runProcess(
159+
'taskkill',
160+
['/F', '/T', '/PID', '${process.pid}'],
161+
);
162+
_exit(result.exitCode);
163+
}
164+
}
165+
}
166+
167+
/// {@template cached_file}
168+
/// A cached file which consists of the file path and contents.
169+
/// {@endtemplate}
170+
class CachedFile {
171+
/// {@macro cached_file}
172+
const CachedFile({required this.path, required this.contents});
173+
174+
/// The generated file path.
175+
final String path;
176+
177+
/// The contents of the generated file.s
178+
final List<int> contents;
179+
}
180+
181+
/// Signature for the `createFile` method on [DirectoryGeneratorTarget].
182+
typedef CreateFile = Future<GeneratedFile> Function(
183+
String path,
184+
List<int> contents, {
185+
Logger? logger,
186+
OverwriteRule? overwriteRule,
187+
});
188+
189+
/// {@template restorable_directory_generator_target}
190+
/// A [DirectoryGeneratorTarget] that is capable of
191+
/// caching and restoring file snapshots.
192+
/// {@endtemplate}
193+
class RestorableDirectoryGeneratorTarget extends DirectoryGeneratorTarget {
194+
/// {@macro restorable_directory_generator_target}
195+
RestorableDirectoryGeneratorTarget(super.dir, {CreateFile? createFile})
196+
: _createFile = createFile;
197+
198+
final CreateFile? _createFile;
199+
CachedFile? _cachedSnapshot;
200+
CachedFile? _latestSnapshot;
201+
202+
/// Restore the latest cached snapshot.
203+
Future<void> restore() async {
204+
final snapshot = _cachedSnapshot;
205+
if (snapshot == null) return;
206+
await createFile(snapshot.path, snapshot.contents);
207+
}
208+
209+
/// Cache the latest recorded snapshot.
210+
void cacheLatestSnapshot() {
211+
final snapshot = _latestSnapshot;
212+
if (snapshot == null) return;
213+
_cachedSnapshot = snapshot;
214+
}
215+
216+
@override
217+
Future<GeneratedFile> createFile(
218+
String path,
219+
List<int> contents, {
220+
Logger? logger,
221+
OverwriteRule? overwriteRule,
222+
}) {
223+
_latestSnapshot = CachedFile(path: path, contents: contents);
224+
return (_createFile ?? super.createFile)(
225+
path,
226+
contents,
227+
logger: logger,
228+
overwriteRule: overwriteRule,
229+
);
230+
}
142231
}

packages/dart_frog_cli/lib/src/commands/dev/templates/dart_frog_dev_server_bundle.dart

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)