Skip to content

Commit 42f4fd9

Browse files
authored
fix(dart_frog_cli): workspace package version resolution (#1859)
1 parent 5ad12e7 commit 42f4fd9

19 files changed

+1889
-170
lines changed
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
targets:
2+
$default:
3+
builders:
4+
source_gen|combining_builder:
5+
options:
6+
ignore_for_file:
7+
- type=lint
8+
json_serializable:
9+
options:
10+
create_to_json: false
11+
checked: true

bricks/dart_frog_prod_server/hooks/lib/dart_frog_prod_server_hooks.dart

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,11 @@ export 'src/dart_pub_get.dart';
55
export 'src/disable_workspace_resolution.dart';
66
export 'src/exit_overrides.dart';
77
export 'src/get_internal_path_dependencies.dart';
8+
export 'src/get_package_config.dart';
9+
export 'src/get_package_graph.dart';
810
export 'src/get_pubspec_lock.dart';
11+
export 'src/get_workspace_root.dart';
12+
export 'src/package_graph/package_graph.dart';
913
export 'src/uses_workspace_resolution.dart';
1014

1115
/// A void callback function (e.g. `void Function()`).

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

Lines changed: 2 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -2,26 +2,17 @@ import 'dart:io';
22
import 'package:dart_frog_prod_server_hooks/dart_frog_prod_server_hooks.dart';
33
import 'package:mason/mason.dart';
44
import 'package:path/path.dart' as path;
5-
import 'package:yaml/yaml.dart';
65

76
/// Copies the pubspec.lock from the workspace root into the project directory
87
/// in order to ensure the production build uses the exact same versions of all
98
/// dependencies.
109
VoidCallback copyWorkspacePubspecLock(
1110
HookContext context, {
1211
required String projectDirectory,
12+
required String workspaceRoot,
1313
required void Function(int exitCode) exit,
1414
}) {
15-
final workspaceRoot = _getWorkspaceRoot(projectDirectory);
16-
if (workspaceRoot == null) {
17-
context.logger.err(
18-
'Unable to determine workspace root for $projectDirectory',
19-
);
20-
exit(1);
21-
return () {};
22-
}
23-
24-
final pubspecLockFile = File(path.join(workspaceRoot.path, 'pubspec.lock'));
15+
final pubspecLockFile = File(path.join(workspaceRoot, 'pubspec.lock'));
2516
if (!pubspecLockFile.existsSync()) return () {};
2617

2718
try {
@@ -35,45 +26,3 @@ VoidCallback copyWorkspacePubspecLock(
3526
return () {};
3627
}
3728
}
38-
39-
/// Returns the root directory of the nearest Dart workspace.
40-
Directory? _getWorkspaceRoot(String workingDirectory) {
41-
final file = _findNearestAncestor(
42-
where: (path) => _getWorkspaceRootPubspecYaml(cwd: Directory(path)),
43-
cwd: Directory(workingDirectory),
44-
);
45-
if (file == null || !file.existsSync()) return null;
46-
return Directory(path.dirname(file.path));
47-
}
48-
49-
/// The workspace root `pubspec.yaml` file for this project.
50-
File? _getWorkspaceRootPubspecYaml({required Directory cwd}) {
51-
try {
52-
final pubspecYamlFile = File(path.join(cwd.path, 'pubspec.yaml'));
53-
if (!pubspecYamlFile.existsSync()) return null;
54-
final pubspec = loadYaml(pubspecYamlFile.readAsStringSync());
55-
if (pubspec is! YamlMap) return null;
56-
final workspace = pubspec['workspace'] as List?;
57-
if (workspace?.isEmpty ?? true) return null;
58-
return pubspecYamlFile;
59-
} on Exception {
60-
return null;
61-
}
62-
}
63-
64-
/// Finds nearest ancestor file
65-
/// relative to the [cwd] that satisfies [where].
66-
File? _findNearestAncestor({
67-
required File? Function(String path) where,
68-
required Directory cwd,
69-
}) {
70-
Directory? prev;
71-
var dir = cwd;
72-
while (prev?.path != dir.path) {
73-
final file = where(dir.path);
74-
if (file?.existsSync() ?? false) return file;
75-
prev = dir;
76-
dir = dir.parent;
77-
}
78-
return null;
79-
}

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,9 @@ Future<void> dartPubGet(
1414
required String workingDirectory,
1515
required ProcessRunner runProcess,
1616
required void Function(int exitCode) exit,
17+
String message = 'Installing dependencies',
1718
}) async {
18-
final progress = context.logger.progress('Installing dependencies');
19+
final progress = context.logger.progress(message);
1920
try {
2021
final result = await runProcess(
2122
'dart',

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

Lines changed: 114 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import 'dart:io';
22
import 'package:dart_frog_prod_server_hooks/dart_frog_prod_server_hooks.dart';
33
import 'package:mason/mason.dart';
4+
import 'package:package_config/package_config.dart';
45
import 'package:path/path.dart' as path;
56
import 'package:yaml/yaml.dart';
67
import 'package:yaml_edit/yaml_edit.dart';
@@ -9,18 +10,40 @@ import 'package:yaml_edit/yaml_edit.dart';
910
/// https://github.com/dart-lang/pub/issues/4594
1011
VoidCallback disableWorkspaceResolution(
1112
HookContext context, {
13+
required PackageConfig packageConfig,
14+
required PackageGraph packageGraph,
1215
required String projectDirectory,
16+
required String workspaceRoot,
1317
required void Function(int exitCode) exit,
1418
}) {
19+
final VoidCallback restoreWorkspaceResolution;
1520
try {
16-
return overrideResolutionInPubspecOverrides(projectDirectory);
21+
restoreWorkspaceResolution = overrideResolutionInPubspecOverrides(
22+
projectDirectory,
23+
);
1724
} on Exception catch (e) {
1825
context.logger.err('$e');
1926
exit(1);
2027
return () {}; // no-op
2128
}
29+
30+
try {
31+
overridePathDependenciesInPubspecOverrides(
32+
projectDirectory: projectDirectory,
33+
packageConfig: packageConfig,
34+
packageGraph: packageGraph,
35+
);
36+
} on Exception catch (e) {
37+
restoreWorkspaceResolution();
38+
context.logger.err('$e');
39+
exit(1);
40+
return () {}; // no-op
41+
}
42+
43+
return restoreWorkspaceResolution;
2244
}
2345

46+
/// Add resolution:null to pubspec_overrides.yaml.
2447
VoidCallback overrideResolutionInPubspecOverrides(String projectDirectory) {
2548
final pubspecOverridesFile = File(
2649
path.join(projectDirectory, 'pubspec_overrides.yaml'),
@@ -46,3 +69,93 @@ VoidCallback overrideResolutionInPubspecOverrides(String projectDirectory) {
4669

4770
return () => pubspecOverridesFile.writeAsStringSync(contents);
4871
}
72+
73+
/// Add overrides for all path dependencies to `pubspec_overrides.yaml`
74+
void overridePathDependenciesInPubspecOverrides({
75+
required String projectDirectory,
76+
required PackageConfig packageConfig,
77+
required PackageGraph packageGraph,
78+
}) {
79+
final name = getPackageName(projectDirectory: projectDirectory);
80+
if (name == null) {
81+
throw Exception('Failed to parse "name" from pubspec.yaml');
82+
}
83+
84+
final productionDeps = getProductionDependencies(
85+
packageName: name,
86+
packageGraph: packageGraph,
87+
);
88+
89+
final pathDependencies = packageConfig.packages.where(
90+
(package) => package.relativeRoot && productionDeps.contains(package.name),
91+
);
92+
93+
writePathDependencyOverrides(
94+
projectDirectory: projectDirectory,
95+
pathDependencies: pathDependencies,
96+
);
97+
}
98+
99+
void writePathDependencyOverrides({
100+
required String projectDirectory,
101+
required Iterable<Package> pathDependencies,
102+
}) {
103+
final pubspecOverridesFile = File(
104+
path.join(projectDirectory, 'pubspec_overrides.yaml'),
105+
);
106+
final contents = pubspecOverridesFile.readAsStringSync();
107+
final overrides = loadYaml(contents) as YamlMap;
108+
final editor = YamlEditor(contents);
109+
if (!overrides.containsKey('dependency_overrides')) {
110+
editor.update(['dependency_overrides'], {});
111+
}
112+
for (final package in pathDependencies) {
113+
editor.update(
114+
['dependency_overrides', package.name],
115+
{'path': path.relative(package.root.path, from: projectDirectory)},
116+
);
117+
}
118+
pubspecOverridesFile.writeAsStringSync(editor.toString());
119+
}
120+
121+
/// Extract the package name from the pubspec.yaml in [projectDirectory].
122+
String? getPackageName({required String projectDirectory}) {
123+
final pubspecFile = File(path.join(projectDirectory, 'pubspec.yaml'));
124+
final pubspec = loadYaml(pubspecFile.readAsStringSync());
125+
if (pubspec is! YamlMap) return null;
126+
127+
final name = pubspec['name'];
128+
if (name is! String) return null;
129+
130+
return name;
131+
}
132+
133+
/// Build a complete list of dependencies (direct and transitive).
134+
Set<String> getProductionDependencies({
135+
required String packageName,
136+
required PackageGraph packageGraph,
137+
}) {
138+
final dependencies = <String>{};
139+
final root = packageGraph.roots.firstWhere((root) => root == packageName);
140+
final rootPackage = packageGraph.packages.firstWhere((p) => p.name == root);
141+
final dependenciesToVisit = <String>[...rootPackage.dependencies];
142+
143+
do {
144+
final discoveredDependencies = <String>[];
145+
for (final dependencyToVisit in dependenciesToVisit) {
146+
final package = packageGraph.packages.firstWhere(
147+
(p) => p.name == dependencyToVisit,
148+
);
149+
dependencies.add(package.name);
150+
for (final packageDependency in package.dependencies) {
151+
// Avoid infinite loops from dependency cycles (circular dependencies).
152+
if (dependencies.contains(packageDependency)) continue;
153+
discoveredDependencies.add(packageDependency);
154+
}
155+
}
156+
dependenciesToVisit
157+
..clear()
158+
..addAll(discoveredDependencies);
159+
} while (dependenciesToVisit.isNotEmpty);
160+
return dependencies;
161+
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import 'dart:io';
2+
3+
import 'package:package_config/package_config_types.dart';
4+
import 'package:path/path.dart' as path;
5+
6+
PackageConfig? getPackageConfig(
7+
String workspaceRoot, {
8+
path.Context? pathContext,
9+
}) {
10+
final pathResolver = pathContext ?? path.context;
11+
final packageConfigFile = File(
12+
pathResolver.join(workspaceRoot, '.dart_tool/package_config.json'),
13+
);
14+
if (!packageConfigFile.existsSync()) return null;
15+
16+
try {
17+
final content = packageConfigFile.readAsStringSync();
18+
return PackageConfig.parseString(content, packageConfigFile.uri);
19+
} on Exception {
20+
return null;
21+
}
22+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import 'package:dart_frog_prod_server_hooks/dart_frog_prod_server_hooks.dart';
2+
3+
PackageGraph? getPackageGraph(String workspaceRoot) {
4+
try {
5+
return PackageGraph.load(workspaceRoot);
6+
} on Exception {
7+
return null;
8+
}
9+
}
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
import 'dart:io';
2+
3+
import 'package:path/path.dart' as path;
4+
import 'package:yaml/yaml.dart';
5+
6+
/// Returns the root directory of the nearest Dart workspace.
7+
Directory? getWorkspaceRoot(String workingDirectory) {
8+
final file = _findNearestAncestor(
9+
where: (path) => _getWorkspaceRootPubspecYaml(cwd: Directory(path)),
10+
cwd: Directory(workingDirectory),
11+
);
12+
if (file == null || !file.existsSync()) return null;
13+
return Directory(path.dirname(file.path));
14+
}
15+
16+
/// The workspace root `pubspec.yaml` file for this project.
17+
File? _getWorkspaceRootPubspecYaml({required Directory cwd}) {
18+
try {
19+
final pubspecYamlFile = File(path.join(cwd.path, 'pubspec.yaml'));
20+
if (!pubspecYamlFile.existsSync()) return null;
21+
final pubspec = loadYaml(pubspecYamlFile.readAsStringSync());
22+
if (pubspec is! YamlMap) return null;
23+
final workspace = pubspec['workspace'] as List?;
24+
if (workspace?.isEmpty ?? true) return null;
25+
return pubspecYamlFile;
26+
} on Exception {
27+
return null;
28+
}
29+
}
30+
31+
/// Finds nearest ancestor file
32+
/// relative to the [cwd] that satisfies [where].
33+
File? _findNearestAncestor({
34+
required File? Function(String path) where,
35+
required Directory cwd,
36+
}) {
37+
Directory? prev;
38+
var dir = cwd;
39+
while (prev?.path != dir.path) {
40+
final file = where(dir.path);
41+
if (file?.existsSync() ?? false) return file;
42+
prev = dir;
43+
dir = dir.parent;
44+
}
45+
return null;
46+
}

0 commit comments

Comments
 (0)