Skip to content

Commit 91fb04f

Browse files
authored
fix(dart_frog_cli): honor workspace pubspec.lock during builds (#1855)
1 parent 698ac77 commit 91fb04f

File tree

6 files changed

+281
-13
lines changed

6 files changed

+281
-13
lines changed

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
@@ -1,3 +1,4 @@
1+
export 'src/copy_workspace_pubspec_lock.dart';
12
export 'src/create_bundle.dart';
23
export 'src/create_external_packages_folder.dart';
34
export 'src/dart_pub_get.dart';
@@ -6,3 +7,6 @@ export 'src/exit_overrides.dart';
67
export 'src/get_internal_path_dependencies.dart';
78
export 'src/get_pubspec_lock.dart';
89
export 'src/uses_workspace_resolution.dart';
10+
11+
/// A void callback function (e.g. `void Function()`).
12+
typedef VoidCallback = void Function();
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
import 'dart:io';
2+
import 'package:dart_frog_prod_server_hooks/dart_frog_prod_server_hooks.dart';
3+
import 'package:mason/mason.dart';
4+
import 'package:path/path.dart' as path;
5+
import 'package:yaml/yaml.dart';
6+
7+
/// Copies the pubspec.lock from the workspace root into the project directory
8+
/// in order to ensure the production build uses the exact same versions of all
9+
/// dependencies.
10+
VoidCallback copyWorkspacePubspecLock(
11+
HookContext context, {
12+
required String projectDirectory,
13+
required void Function(int exitCode) exit,
14+
}) {
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'));
25+
if (!pubspecLockFile.existsSync()) return () {};
26+
27+
try {
28+
pubspecLockFile.copySync(path.join(projectDirectory, 'pubspec.lock'));
29+
return () {
30+
File(path.join(projectDirectory, 'pubspec.lock')).delete().ignore();
31+
};
32+
} on Exception catch (error) {
33+
context.logger.err('$error');
34+
exit(1);
35+
return () {};
36+
}
37+
}
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/disable_workspace_resolution.dart

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,10 @@
11
import 'dart:io';
2+
import 'package:dart_frog_prod_server_hooks/dart_frog_prod_server_hooks.dart';
23
import 'package:mason/mason.dart';
34
import 'package:path/path.dart' as path;
45
import 'package:yaml/yaml.dart';
56
import 'package:yaml_edit/yaml_edit.dart';
67

7-
/// A void callback function (e.g. `void Function()`).
8-
typedef VoidCallback = void Function();
9-
108
/// Opts out of dart workspaces until we can generate per package lockfiles.
119
/// https://github.com/dart-lang/pub/issues/4594
1210
VoidCallback disableWorkspaceResolution(

bricks/dart_frog_prod_server/hooks/pre_gen.dart

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ Future<void> preGen(
3030
);
3131

3232
VoidCallback? restoreWorkspaceResolution;
33+
VoidCallback? revertPubspecLock;
3334

3435
if (usesWorkspaces) {
3536
// Disable workspace resolution until we can generate per-package lockfiles.
@@ -39,6 +40,13 @@ Future<void> preGen(
3940
projectDirectory: projectDirectory.path,
4041
exit: exit,
4142
);
43+
// Copy the pubspec.lock from the workspace root to ensure the same versions
44+
// of dependencies are used in the production build.
45+
revertPubspecLock = copyWorkspacePubspecLock(
46+
context,
47+
projectDirectory: projectDirectory.path,
48+
exit: exit,
49+
);
4250
}
4351

4452
// We need to make sure that the pubspec.lock file is up to date
@@ -94,10 +102,6 @@ Future<void> preGen(
94102
onViolationEnd: () => exit(1),
95103
);
96104

97-
final customDockerFile = io.File(
98-
path.join(projectDirectory.path, 'Dockerfile'),
99-
);
100-
101105
final internalPathDependencies = await getInternalPathDependencies(
102106
projectDirectory,
103107
);
@@ -108,6 +112,11 @@ Future<void> preGen(
108112
copyPath: copyPath,
109113
);
110114

115+
revertPubspecLock?.call();
116+
117+
final customDockerFile = io.File(
118+
path.join(projectDirectory.path, 'Dockerfile'),
119+
);
111120
final addDockerfile = !customDockerFile.existsSync();
112121

113122
context.vars = {
Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,154 @@
1+
import 'dart:io';
2+
3+
import 'package:dart_frog_prod_server_hooks/dart_frog_prod_server_hooks.dart';
4+
import 'package:mason/mason.dart';
5+
import 'package:mocktail/mocktail.dart';
6+
import 'package:path/path.dart' as path;
7+
import 'package:test/test.dart';
8+
9+
class _MockHookContext extends Mock implements HookContext {}
10+
11+
class _MockLogger extends Mock implements Logger {}
12+
13+
void main() {
14+
group('copyWorkspacePubspecLock', () {
15+
late List<int> exitCalls;
16+
late HookContext context;
17+
late Logger logger;
18+
late Directory projectDirectory;
19+
late Directory rootDirectory;
20+
21+
setUp(() {
22+
exitCalls = [];
23+
context = _MockHookContext();
24+
logger = _MockLogger();
25+
rootDirectory = Directory.systemTemp.createTempSync('root');
26+
projectDirectory = Directory(
27+
path.join(rootDirectory.path, 'packages', 'project'),
28+
)..createSync(recursive: true);
29+
30+
when(() => context.logger).thenReturn(logger);
31+
32+
addTearDown(() {
33+
projectDirectory.delete().ignore();
34+
rootDirectory.delete().ignore();
35+
});
36+
});
37+
38+
test('exits with error when unable to determine the workspace root', () {
39+
copyWorkspacePubspecLock(
40+
context,
41+
projectDirectory: projectDirectory.path,
42+
exit: exitCalls.add,
43+
);
44+
expect(exitCalls, equals([1]));
45+
verify(
46+
() => logger.err(
47+
'Unable to determine workspace root for ${projectDirectory.path}',
48+
),
49+
);
50+
});
51+
52+
test('exits with error when unable to parse pubspec.yaml', () {
53+
File(path.join(rootDirectory.path, 'pubspec.yaml'))
54+
.writeAsStringSync('invalid pubspec.yaml');
55+
copyWorkspacePubspecLock(
56+
context,
57+
projectDirectory: projectDirectory.path,
58+
exit: exitCalls.add,
59+
);
60+
expect(exitCalls, equals([1]));
61+
verify(
62+
() => logger.err(
63+
'Unable to determine workspace root for ${projectDirectory.path}',
64+
),
65+
);
66+
});
67+
68+
test('does nothing when pubspec.lock does not exist in workspace root', () {
69+
File(path.join(rootDirectory.path, 'pubspec.yaml')).writeAsStringSync('''
70+
name: _
71+
version: 0.0.0
72+
environment:
73+
sdk: ^3.8.0
74+
workspace:
75+
- packages/hello_world
76+
''');
77+
copyWorkspacePubspecLock(
78+
context,
79+
projectDirectory: projectDirectory.path,
80+
exit: exitCalls.add,
81+
);
82+
expect(exitCalls, isEmpty);
83+
verifyNever(() => logger.err(any()));
84+
expect(projectDirectory.listSync(), isEmpty);
85+
});
86+
87+
test('exits with error when unable to copy lockfile', () {
88+
const pubspecLockContents = '''
89+
# Generated by pub
90+
# See https://dart.dev/tools/pub/glossary#lockfile
91+
packages:
92+
''';
93+
File(path.join(rootDirectory.path, 'pubspec.yaml')).writeAsStringSync('''
94+
name: _
95+
version: 0.0.0
96+
environment:
97+
sdk: ^3.8.0
98+
workspace:
99+
- packages/hello_world
100+
''');
101+
final file = File(path.join(rootDirectory.path, 'pubspec.lock'))
102+
..writeAsStringSync(pubspecLockContents);
103+
Process.runSync('chmod', ['000', file.path]);
104+
copyWorkspacePubspecLock(
105+
context,
106+
projectDirectory: projectDirectory.path,
107+
exit: exitCalls.add,
108+
);
109+
expect(exitCalls, equals([1]));
110+
verify(
111+
() => logger.err(any(that: contains('Permission denied'))),
112+
);
113+
});
114+
115+
test('copies pubspec.lock to project directory when found', () {
116+
const pubspecLockContents = '''
117+
# Generated by pub
118+
# See https://dart.dev/tools/pub/glossary#lockfile
119+
packages:
120+
''';
121+
File(path.join(rootDirectory.path, 'pubspec.yaml')).writeAsStringSync('''
122+
name: _
123+
version: 0.0.0
124+
environment:
125+
sdk: ^3.8.0
126+
workspace:
127+
- packages/hello_world
128+
''');
129+
File(path.join(rootDirectory.path, 'pubspec.lock'))
130+
.writeAsStringSync(pubspecLockContents);
131+
copyWorkspacePubspecLock(
132+
context,
133+
projectDirectory: projectDirectory.path,
134+
exit: exitCalls.add,
135+
);
136+
expect(exitCalls, isEmpty);
137+
verifyNever(() => logger.err(any()));
138+
final projectDirectoryContents = projectDirectory.listSync();
139+
expect(projectDirectoryContents, hasLength(1));
140+
expect(
141+
projectDirectoryContents.first,
142+
isA<File>().having(
143+
(f) => path.basename(f.path),
144+
'name',
145+
'pubspec.lock',
146+
),
147+
);
148+
expect(
149+
(projectDirectoryContents.first as File).readAsStringSync(),
150+
equals(pubspecLockContents),
151+
);
152+
});
153+
});
154+
}

0 commit comments

Comments
 (0)