Skip to content

Commit f2ebc2a

Browse files
authored
chore(cli): Run build_runner daemon with celest start (#313)
Instead of requiring a separate build_runner dev cycle when using Drift or others, manage a build_runner daemon for the user with `celest start`.
1 parent a712e28 commit f2ebc2a

File tree

5 files changed

+110
-16
lines changed

5 files changed

+110
-16
lines changed

apps/cli/lib/src/frontend/celest_frontend.dart

Lines changed: 102 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import 'dart:math';
55

66
import 'package:async/async.dart';
77
import '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;
99
import 'package:celest_cli/src/analyzer/analysis_error.dart';
1010
import 'package:celest_cli/src/analyzer/analysis_result.dart';
1111
import 'package:celest_cli/src/analyzer/celest_analyzer.dart';
@@ -25,8 +25,11 @@ import 'package:celest_cli/src/project/project_linker.dart';
2525
import 'package:celest_cli/src/repositories/organization_repository.dart';
2626
import 'package:celest_cli/src/repositories/project_environment_repository.dart';
2727
import 'package:celest_cli/src/repositories/project_repository.dart';
28+
import 'package:celest_cli/src/sdk/dart_sdk.dart';
2829
import 'package:celest_cli/src/utils/json.dart';
30+
import 'package:celest_cli/src/utils/process.dart';
2931
import 'package:celest_cli/src/utils/recase.dart';
32+
import 'package:celest_cli/src/utils/run.dart';
3033
import 'package:celest_cloud/src/proto.dart' as pb;
3134
import 'package:dcli/dcli.dart' as dcli;
3235
import '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 '

apps/cli/lib/src/project/celest_project.dart

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -193,6 +193,11 @@ final class CelestProject {
193193
Pubspec get pubspec => _cachedPubspec.pubspec;
194194
String get pubspecYaml => _cachedPubspec.pubspecYaml;
195195

196+
/// Whether the project uses `package:build_runner`.
197+
bool get usesBuildRunner {
198+
return pubspec.devDependencies.containsKey('build_runner');
199+
}
200+
196201
late final _cachedClientPubspec = CachedPubspec(
197202
fileSystem.directory(projectPaths.clientRoot).childFile('pubspec.yaml'),
198203
);

examples/tasks/celest/lib/src/database/task_database.g.dart

Lines changed: 0 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

examples/tasks/celest/lib/src/generated/data.celest.dart

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,8 @@ class CelestData {
2424
context,
2525
name: 'TaskDatabase',
2626
factory: TaskDatabase.new,
27-
hostnameVariable: const _$celest.env('CELEST_DATABASE_HOST'),
28-
tokenSecret: const _$celest.secret('CELEST_DATABASE_TOKEN'),
27+
hostnameVariable: const _$celest.env('TASK_DATABASE_HOST'),
28+
tokenSecret: const _$celest.secret('TASK_DATABASE_TOKEN'),
2929
),
3030
);
3131
}

packages/celest_cloud/pubspec.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ resolution: workspace
99

1010
dependencies:
1111
async: ^2.11.0
12-
celest_ast: ^0.1.6-0
12+
celest_ast: ^0.1.5
1313
celest_core: ^1.0.0
1414
code_builder: ^4.10.0
1515
collection: ^1.18.0

0 commit comments

Comments
 (0)