Skip to content

Commit 0b167a3

Browse files
authored
feat(dart_frog_cli): support for Dart workspaces (#1825)
1 parent 85302f2 commit 0b167a3

File tree

15 files changed

+706
-149
lines changed

15 files changed

+706
-149
lines changed

bricks/dart_frog_prod_server/__brick__/build/{{#addDockerfile}}Dockerfile{{/addDockerfile}}

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ COPY ./pubspec_overrides.yaml ./pubspec_overrides.yaml
1919
{{/hasExternalDependencies}}
2020
# Resolve app dependencies.
2121
COPY pubspec.* ./
22+
COPY pubspec_overrides.yaml* ./
2223
RUN dart pub get
2324

2425
# Copy app source code and AOT compile it.
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
export 'src/create_bundle.dart';
22
export 'src/create_external_packages_folder.dart';
33
export 'src/dart_pub_get.dart';
4+
export 'src/disable_workspace_resolution.dart';
45
export 'src/exit_overrides.dart';
56
export 'src/get_internal_path_dependencies.dart';
67
export 'src/get_pubspec_lock.dart';
8+
export 'src/uses_workspace_resolution.dart';

bricks/dart_frog_prod_server/hooks/lib/src/create_external_packages_folder.dart

Lines changed: 28 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -22,9 +22,7 @@ Future<List<String>> createExternalPackagesFolder({
2222
.map(
2323
(dependency) {
2424
final pathDescription = dependency.pathDescription;
25-
if (pathDescription == null) {
26-
return null;
27-
}
25+
if (pathDescription == null) return null;
2826

2927
final isExternal = !pathResolver.isWithin('', pathDescription.path);
3028
if (!isExternal) return null;
@@ -38,9 +36,7 @@ Future<List<String>> createExternalPackagesFolder({
3836
.whereType<_ExternalPathDependency>()
3937
.toList();
4038

41-
if (externalPathDependencies.isEmpty) {
42-
return [];
43-
}
39+
if (externalPathDependencies.isEmpty) return [];
4440

4541
final packagesDirectory = Directory(
4642
pathResolver.join(
@@ -51,34 +47,38 @@ Future<List<String>> createExternalPackagesFolder({
5147

5248
final copiedExternalPathDependencies = await Future.wait(
5349
externalPathDependencies.map(
54-
(externalPathDependency) => externalPathDependency.copyTo(
55-
copyPath: copyPath,
56-
targetDirectory: Directory(
57-
pathResolver.join(
58-
packagesDirectory.path,
59-
externalPathDependency.name,
50+
(externalPathDependency) async {
51+
final copy = await externalPathDependency.copyTo(
52+
copyPath: copyPath,
53+
targetDirectory: Directory(
54+
pathResolver.join(
55+
packagesDirectory.path,
56+
externalPathDependency.name,
57+
),
6058
),
61-
),
62-
),
59+
);
60+
overrideResolutionInPubspecOverrides(copy.path);
61+
return copy;
62+
},
6363
),
6464
);
6565

66-
await File(
67-
pathResolver.join(
68-
buildDirectory.path,
69-
'pubspec_overrides.yaml',
70-
),
71-
).writeAsString('''
66+
File(
67+
pathResolver.join(buildDirectory.path, 'pubspec_overrides.yaml'),
68+
).writeAsStringSync(
69+
'''
70+
resolution: null
7271
dependency_overrides:
7372
${copiedExternalPathDependencies.map(
74-
(dependency) {
75-
final name = dependency.name;
76-
final path =
77-
pathResolver.relative(dependency.path, from: buildDirectory.path);
78-
return ' $name:\n path: $path';
79-
},
80-
).join('\n')}
81-
''');
73+
(dependency) {
74+
final name = dependency.name;
75+
final path =
76+
pathResolver.relative(dependency.path, from: buildDirectory.path);
77+
return ' $name:\n path: $path';
78+
},
79+
).join('\n')}
80+
''',
81+
);
8282

8383
return copiedExternalPathDependencies
8484
.map((dependency) => dependency.path)
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
import 'dart:io';
2+
import 'package:mason/mason.dart';
3+
import 'package:path/path.dart' as path;
4+
import 'package:yaml/yaml.dart';
5+
import 'package:yaml_edit/yaml_edit.dart';
6+
7+
/// A void callback function (e.g. `void Function()`).
8+
typedef VoidCallback = void Function();
9+
10+
/// Opts out of dart workspaces until we can generate per package lockfiles.
11+
/// https://github.com/dart-lang/pub/issues/4594
12+
VoidCallback disableWorkspaceResolution(
13+
HookContext context, {
14+
required String projectDirectory,
15+
required void Function(int exitCode) exit,
16+
}) {
17+
try {
18+
return overrideResolutionInPubspecOverrides(projectDirectory);
19+
} on Exception catch (e) {
20+
context.logger.err('$e');
21+
exit(1);
22+
return () {}; // no-op
23+
}
24+
}
25+
26+
VoidCallback overrideResolutionInPubspecOverrides(String projectDirectory) {
27+
final pubspecOverridesFile = File(
28+
path.join(projectDirectory, 'pubspec_overrides.yaml'),
29+
);
30+
31+
if (!pubspecOverridesFile.existsSync()) {
32+
pubspecOverridesFile.writeAsStringSync('resolution: null');
33+
return pubspecOverridesFile.deleteSync;
34+
}
35+
36+
final contents = pubspecOverridesFile.readAsStringSync();
37+
final pubspecOverrides = loadYaml(contents) as YamlMap?;
38+
39+
if (pubspecOverrides == null) {
40+
pubspecOverridesFile.writeAsStringSync('resolution: null');
41+
return () => pubspecOverridesFile.writeAsStringSync(contents);
42+
}
43+
44+
if (pubspecOverrides['resolution'] == 'null') return () {}; // no-op
45+
46+
final editor = YamlEditor(contents)..update(['resolution'], null);
47+
pubspecOverridesFile.writeAsStringSync(editor.toString());
48+
49+
return () => pubspecOverridesFile.writeAsStringSync(contents);
50+
}
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import 'dart:io';
2+
import 'package:mason/mason.dart';
3+
import 'package:path/path.dart' as path;
4+
import 'package:yaml/yaml.dart';
5+
6+
/// Determines whether the project in the provided [workingDirectory]
7+
/// is configured to use `resolution: workspace`.
8+
bool usesWorkspaceResolution(
9+
HookContext context, {
10+
required String workingDirectory,
11+
required void Function(int exitCode) exit,
12+
}) {
13+
final pubspecFile = File(path.join(workingDirectory, 'pubspec.yaml'));
14+
if (!pubspecFile.existsSync()) return false;
15+
16+
final YamlMap pubspec;
17+
try {
18+
final yaml = loadYaml(pubspecFile.readAsStringSync());
19+
if (yaml is! YamlMap) {
20+
throw Exception('Unable to parse ${pubspecFile.path}');
21+
}
22+
pubspec = yaml;
23+
} on Exception catch (e) {
24+
context.logger.err('$e');
25+
exit(1);
26+
return false;
27+
}
28+
29+
return pubspec['resolution'] == 'workspace';
30+
}

bricks/dart_frog_prod_server/hooks/pre_gen.dart

Lines changed: 21 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,23 @@ Future<void> preGen(
2323
Future<void> Function(String from, String to) copyPath = io_expanded.copyPath,
2424
}) async {
2525
final projectDirectory = directory ?? io.Directory.current;
26+
final usesWorkspaces = usesWorkspaceResolution(
27+
context,
28+
workingDirectory: projectDirectory.path,
29+
exit: exit,
30+
);
31+
32+
VoidCallback? restoreWorkspaceResolution;
33+
34+
if (usesWorkspaces) {
35+
// Disable workspace resolution until we can generate per-package lockfiles.
36+
// https://github.com/dart-lang/pub/issues/4594
37+
restoreWorkspaceResolution = disableWorkspaceResolution(
38+
context,
39+
projectDirectory: projectDirectory.path,
40+
exit: exit,
41+
);
42+
}
2643

2744
// We need to make sure that the pubspec.lock file is up to date
2845
await dartPubGet(
@@ -43,6 +60,8 @@ Future<void> preGen(
4360
exit: exit,
4461
);
4562

63+
restoreWorkspaceResolution?.call();
64+
4665
final RouteConfiguration configuration;
4766
try {
4867
configuration = buildConfiguration(projectDirectory);
@@ -62,9 +81,7 @@ Future<void> preGen(
6281
'''Route conflict detected. ${lightCyan.wrap(originalFilePath)} and ${lightCyan.wrap(conflictingFilePath)} both resolve to ${lightCyan.wrap(conflictingEndpoint)}.''',
6382
);
6483
},
65-
onViolationEnd: () {
66-
exit(1);
67-
},
84+
onViolationEnd: () => exit(1),
6885
);
6986

7087
reportRogueRoutes(
@@ -74,9 +91,7 @@ Future<void> preGen(
7491
'''Rogue route detected.${defaultForeground.wrap(' ')}Rename ${lightCyan.wrap(filePath)} to ${lightCyan.wrap(idealPath)}.''',
7592
);
7693
},
77-
onViolationEnd: () {
78-
exit(1);
79-
},
94+
onViolationEnd: () => exit(1),
8095
);
8196

8297
final customDockerFile = io.File(

bricks/dart_frog_prod_server/hooks/pubspec.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ dependencies:
1111
mason: ^0.1.0
1212
path: ^1.8.1
1313
yaml: ^3.1.2
14+
yaml_edit: ^2.2.2
1415

1516
dev_dependencies:
1617
mocktail: ^1.0.0

bricks/dart_frog_prod_server/hooks/test/post_gen_test.dart

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,6 @@ void main() {
5454
expect(
5555
ExitOverrides.runZoned(
5656
() => post_gen.run(_FakeHookContext(logger: logger)),
57-
exit: (_) {},
5857
),
5958
completes,
6059
);

bricks/dart_frog_prod_server/hooks/test/pre_gen_test.dart

Lines changed: 77 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import 'package:test/test.dart';
1010

1111
import '../pre_gen.dart' as pre_gen;
1212
import 'pubspec_locks.dart';
13+
import 'pubspecs.dart';
1314

1415
class _FakeHookContext extends Fake implements HookContext {
1516
_FakeHookContext({Logger? logger}) : _logger = logger ?? _MockLogger();
@@ -161,49 +162,84 @@ void main() {
161162
expect(exitCalls, equals([1]));
162163
});
163164

164-
test(
165-
'works with external dependencies',
166-
() async {
167-
const configuration = RouteConfiguration(
168-
middleware: [],
169-
directories: [],
170-
routes: [],
171-
rogueRoutes: [],
172-
endpoints: {},
173-
);
165+
test('works with workspaces', () async {
166+
const configuration = RouteConfiguration(
167+
middleware: [],
168+
directories: [],
169+
routes: [],
170+
rogueRoutes: [],
171+
endpoints: {},
172+
);
174173

175-
final directory = Directory.systemTemp.createTempSync();
176-
File(path.join(directory.path, 'pubspec.yaml')).writeAsStringSync(
177-
'''
178-
name: example
179-
version: 0.1.0
180-
environment:
181-
sdk: ^2.17.0
182-
dependencies:
183-
mason: any
184-
foo:
185-
path: ../../foo
186-
dev_dependencies:
187-
test: any
188-
''',
189-
);
190-
File(path.join(directory.path, 'pubspec.lock')).writeAsStringSync(
191-
fooPath,
192-
);
193-
final exitCalls = <int>[];
194-
await pre_gen.preGen(
195-
context,
196-
buildConfiguration: (_) => configuration,
197-
exit: exitCalls.add,
198-
directory: directory,
199-
runProcess: successRunProcess,
200-
copyPath: (_, __) async {},
201-
);
174+
final directory = Directory.systemTemp.createTempSync();
175+
File(
176+
path.join(directory.path, 'pubspec.yaml'),
177+
).writeAsStringSync(workspaceRoot);
178+
final server = Directory(
179+
path.join(directory.path, 'server'),
180+
)..createSync();
181+
File(
182+
path.join(server.path, 'pubspec.yaml'),
183+
).writeAsStringSync(workspaceChild);
184+
File(
185+
path.join(server.path, 'pubspec.lock'),
186+
).writeAsStringSync('''
187+
# Generated by pub
188+
# See https://dart.dev/tools/pub/glossary#lockfile
189+
packages:
190+
_fe_analyzer_shared:
191+
dependency: transitive
192+
description:
193+
name: _fe_analyzer_shared
194+
sha256: da0d9209ca76bde579f2da330aeb9df62b6319c834fa7baae052021b0462401f
195+
url: "https://pub.dev"
196+
source: hosted
197+
version: "85.0.0"
198+
''');
199+
final exitCalls = <int>[];
200+
await pre_gen.preGen(
201+
context,
202+
buildConfiguration: (_) => configuration,
203+
exit: exitCalls.add,
204+
directory: server,
205+
runProcess: successRunProcess,
206+
copyPath: (_, __) async {},
207+
);
202208

203-
expect(exitCalls, isEmpty);
204-
directory.delete(recursive: true).ignore();
205-
},
206-
);
209+
expect(exitCalls, isEmpty);
210+
directory.delete(recursive: true).ignore();
211+
});
212+
213+
test('works with external dependencies', () async {
214+
const configuration = RouteConfiguration(
215+
middleware: [],
216+
directories: [],
217+
routes: [],
218+
rogueRoutes: [],
219+
endpoints: {},
220+
);
221+
222+
final directory = Directory.systemTemp.createTempSync();
223+
File(
224+
path.join(directory.path, 'pubspec.lock'),
225+
).writeAsStringSync(fooPath);
226+
final exitCalls = <int>[];
227+
await pre_gen.preGen(
228+
context,
229+
buildConfiguration: (_) => configuration,
230+
exit: exitCalls.add,
231+
directory: directory,
232+
runProcess: successRunProcess,
233+
copyPath: (from, to) async {
234+
File(
235+
path.join(to, 'pubspec_overrides.yaml'),
236+
).createSync(recursive: true);
237+
},
238+
);
239+
240+
expect(exitCalls, isEmpty);
241+
directory.delete(recursive: true).ignore();
242+
});
207243

208244
test('retains invokeCustomEntrypoint (true)', () async {
209245
const configuration = RouteConfiguration(

0 commit comments

Comments
 (0)