@@ -5,7 +5,7 @@ import 'dart:math';
55
66import 'package:async/async.dart' ;
77import 'package:celest_ast/celest_ast.dart' as ast;
8- import 'package:celest_ast/celest_ast.dart' ;
8+ import 'package:celest_ast/celest_ast.dart' hide Sdk ;
99import 'package:celest_cli/src/analyzer/analysis_error.dart' ;
1010import 'package:celest_cli/src/analyzer/analysis_result.dart' ;
1111import 'package:celest_cli/src/analyzer/celest_analyzer.dart' ;
@@ -25,8 +25,11 @@ import 'package:celest_cli/src/project/project_linker.dart';
2525import 'package:celest_cli/src/repositories/organization_repository.dart' ;
2626import 'package:celest_cli/src/repositories/project_environment_repository.dart' ;
2727import 'package:celest_cli/src/repositories/project_repository.dart' ;
28+ import 'package:celest_cli/src/sdk/dart_sdk.dart' ;
2829import 'package:celest_cli/src/utils/json.dart' ;
30+ import 'package:celest_cli/src/utils/process.dart' ;
2931import 'package:celest_cli/src/utils/recase.dart' ;
32+ import 'package:celest_cli/src/utils/run.dart' ;
3033import 'package:celest_cloud/src/proto.dart' as pb;
3134import 'package:dcli/dcli.dart' as dcli;
3235import 'package:logging/logging.dart' ;
@@ -90,6 +93,15 @@ final class CelestFrontend {
9093 /// Queues changes detected by [_watcher] .
9194 StreamQueue <List <WatchEvent >>? _watcherSub;
9295
96+ /// Queues changes made by build_runner.
97+ ///
98+ /// When a Celest project also uses build_runner, we want to defer any codegen
99+ /// or analysis work until after build_runner completes its changes.
100+ ///
101+ /// This is important for correctly analyzing code which would depend on
102+ /// build_runner outputs like drift or json_serializable.
103+ Stream <void >? _buildRunner;
104+
93105 /// The list of paths changed since the last frontend pass.
94106 Set <String >? _changedPaths;
95107
@@ -106,6 +118,65 @@ final class CelestFrontend {
106118 // TODO(dnys1): If pubspec.yaml changes, we should run pub get and create
107119 // a new analysis context.
108120
121+ /// Starts a `build_runner watch` daemon and monitors reloads via stdout.
122+ Future <Stream <void >> _buildRunnerWatch () async {
123+ final process = await processManager.start (
124+ [Sdk .current.dart, 'run' , 'build_runner' , 'watch' , '-d' ],
125+ workingDirectory: projectPaths.projectRoot,
126+ );
127+
128+ final output = StreamController <String >(sync : true );
129+ process
130+ ..captureStdout (sink: logger.finest, prefix: 'build_runner: ' )
131+ ..captureStdout (sink: output.add)
132+ ..captureStderr (sink: logger.finest, prefix: 'build_runner: ' )
133+ ..captureStderr (sink: output.add);
134+ unawaited (process.exitCode.then ((_) => output.close ()));
135+
136+ final queue = StreamQueue (
137+ output.stream.where ((line) {
138+ // Two possibile end states (success/failure):
139+ // https://github.com/dart-lang/build/blob/7bb331e97238b3ec0167768d26a9d8398a32988a/build_runner_core/lib/src/generate/build.dart#L161-L169
140+ if (line.contains ('Succeeded after' )) {
141+ return true ;
142+ }
143+ if (line.contains ('Failed after' )) {
144+ throw CliException (
145+ 'Error running `build_runner`. '
146+ 'Run `dart run build_runner build` from ${projectPaths .projectRoot } and try again.' ,
147+ );
148+ }
149+ return false ;
150+ }),
151+ );
152+ await Future .any ([
153+ process.exitCode.then ((code) {
154+ throw CliException ('Error running `build_runner`: exited with $code ' );
155+ }),
156+ queue.next,
157+ ]);
158+
159+ return queue.rest;
160+ }
161+
162+ /// Runs `build_runner` build from the project root.
163+ Future <void > _buildRunnerBuild () async {
164+ final process = await processManager.start (
165+ [Sdk .current.dart, 'run' , 'build_runner' , 'build' , '-d' ],
166+ workingDirectory: projectPaths.projectRoot,
167+ );
168+ process
169+ ..captureStdout (sink: logger.finest, prefix: 'build_runner: ' )
170+ ..captureStderr (sink: logger.finest, prefix: 'build_runner: ' );
171+
172+ final exitCode = await process.exitCode;
173+ if (exitCode != 0 ) {
174+ logger.warning (
175+ 'Failed to run build_runner. Project may fail to compile.' ,
176+ );
177+ }
178+ }
179+
109180 /// Notifies the watcher that we're listening for filesystem changes.
110181 Future <void > _nextChangeSet () async {
111182 logger.finer ('Waiting for changes...' );
@@ -115,19 +186,29 @@ final class CelestFrontend {
115186 _watcher ?? = DirectoryWatcher (projectPaths.projectRoot);
116187 _watcherSub ?? = StreamQueue (
117188 _watcher! .events
118- // Ignore creation of new directories and files (they'll be empty)
119189 .tap (
120- (event) =>
121- logger.finest ('Watcher event (${event .type }): ${event .path }' ),
190+ (event) => logger.finest (
191+ 'Watcher event (${event .type }): ${event .path }' ,
192+ ),
122193 )
194+ .let ((s) {
195+ // Buffer build runner passes, e.g. wait for build_runner to
196+ // react to watcher events before opening the flood gates to Celest.
197+ if (_buildRunner case final buildRunner? ) {
198+ return s.buffer (buildRunner).expand ((it) => it);
199+ }
200+ return s;
201+ })
202+ // Ignore creation of new directories and files (they'll be empty)
123203 .where ((event) => event.type != ChangeType .ADD )
124204 .where ((event) {
125- final isReloadable = _isReloadablePath (event.path);
126- if (! isReloadable) {
127- logger.finest ('Ignoring non-reloadable path: ${event .path }' );
128- }
129- return isReloadable;
130- }).buffer (_readyForChanges.stream),
205+ final isReloadable = _isReloadablePath (event.path);
206+ if (! isReloadable) {
207+ logger.finest ('Ignoring non-reloadable path: ${event .path }' );
208+ }
209+ return isReloadable;
210+ })
211+ .buffer (_readyForChanges.stream),
131212 );
132213 _readyForChanges.add (null );
133214
@@ -260,6 +341,9 @@ final class CelestFrontend {
260341 }) async {
261342 try {
262343 while (! stopped) {
344+ if (celestProject.usesBuildRunner) {
345+ _buildRunner ?? = await _buildRunnerWatch ();
346+ }
263347 currentProgress ?? = cliLogger.progress ('Reloading Celest' );
264348
265349 void fail (List <CelestAnalysisError > errors) {
@@ -522,6 +606,10 @@ final class CelestFrontend {
522606 required String environmentId,
523607 }) async {
524608 try {
609+ if (celestProject.usesBuildRunner) {
610+ await _buildRunnerBuild ();
611+ }
612+
525613 int fail (List <CelestAnalysisError > errors) {
526614 currentProgress.fail (
527615 'Project has errors. Please fix them and save the '
@@ -592,6 +680,10 @@ final class CelestFrontend {
592680 currentProgress ?? = cliLogger.progress ('🔥 Warming up the engines' );
593681 }
594682
683+ if (celestProject.usesBuildRunner) {
684+ await _buildRunnerBuild ();
685+ }
686+
595687 void fail (List <CelestAnalysisError > errors) {
596688 currentProgress! .fail (
597689 'Project has errors. Please fix them and save the '
0 commit comments