@@ -30,6 +30,14 @@ typedef DirectoryWatcherBuilder = DirectoryWatcher Function(
30
30
/// Typedef for [io.exit] .
31
31
typedef Exit = dynamic Function (int exitCode);
32
32
33
+ RestorableDirectoryGeneratorTarget get _defaultGeneratorTarget {
34
+ return RestorableDirectoryGeneratorTarget (
35
+ io.Directory (
36
+ path.join (Directory .current.path, '.dart_frog' ),
37
+ ),
38
+ );
39
+ }
40
+
33
41
/// {@template dev_command}
34
42
/// `dart_frog dev` command which starts the dev server`.
35
43
/// {@endtemplate}
@@ -39,6 +47,7 @@ class DevCommand extends DartFrogCommand {
39
47
super .logger,
40
48
DirectoryWatcherBuilder ? directoryWatcher,
41
49
GeneratorBuilder ? generator,
50
+ RestorableDirectoryGeneratorTarget ? generatorTarget,
42
51
Exit ? exit,
43
52
bool ? isWindows,
44
53
ProcessRun ? runProcess,
@@ -50,7 +59,8 @@ class DevCommand extends DartFrogCommand {
50
59
_isWindows = isWindows ?? io.Platform .isWindows,
51
60
_runProcess = runProcess ?? io.Process .run,
52
61
_sigint = sigint ?? io.ProcessSignal .sigint,
53
- _startProcess = startProcess ?? io.Process .start {
62
+ _startProcess = startProcess ?? io.Process .start,
63
+ _generatorTarget = generatorTarget ?? _defaultGeneratorTarget {
54
64
argParser.addOption (
55
65
'port' ,
56
66
abbr: 'p' ,
@@ -66,6 +76,7 @@ class DevCommand extends DartFrogCommand {
66
76
final ProcessRun _runProcess;
67
77
final io.ProcessSignal _sigint;
68
78
final ProcessStart _startProcess;
79
+ final RestorableDirectoryGeneratorTarget _generatorTarget;
69
80
70
81
@override
71
82
final String description = 'Run a local development server.' ;
@@ -75,6 +86,7 @@ class DevCommand extends DartFrogCommand {
75
86
76
87
@override
77
88
Future <int > run () async {
89
+ var hotReloadEnabled = false ;
78
90
final port = Platform .environment['PORT' ] ?? results['port' ] as String ;
79
91
final generator = await _generator (dartFrogDevServerBundle);
80
92
@@ -87,9 +99,7 @@ class DevCommand extends DartFrogCommand {
87
99
);
88
100
89
101
final _ = await generator.generate (
90
- DirectoryGeneratorTarget (
91
- io.Directory (path.join (cwd.path, '.dart_frog' )),
92
- ),
102
+ _generatorTarget,
93
103
vars: vars,
94
104
fileConflictResolution: FileConflictResolution .overwrite,
95
105
);
@@ -105,18 +115,28 @@ class DevCommand extends DartFrogCommand {
105
115
// On Windows listen for CTRL-C and use taskkill to kill
106
116
// the spawned process along with any child processes.
107
117
// 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
+ });
120
140
}
121
141
122
142
final progress = logger.progress ('Serving' );
@@ -125,18 +145,87 @@ class DevCommand extends DartFrogCommand {
125
145
progress.complete ('Running on http://localhost:$port ' );
126
146
127
147
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 ());
137
149
138
150
await subscription.asFuture <void >();
139
151
await subscription.cancel ();
140
152
return ExitCode .success.code;
141
153
}
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
+ }
142
231
}
0 commit comments