Skip to content

Commit b00ec98

Browse files
authored
feat: allow prod server brick to use external dependencies (#910)
1 parent 709be68 commit b00ec98

File tree

6 files changed

+1617
-48
lines changed

6 files changed

+1617
-48
lines changed

bricks/dart_frog_prod_server/hooks/pre_gen.dart

Lines changed: 34 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,14 @@ import 'dart:async';
22
import 'dart:io' as io;
33

44
import 'package:dart_frog_gen/dart_frog_gen.dart';
5+
import 'package:io/io.dart' as io_expanded;
56
import 'package:mason/mason.dart'
67
show HookContext, defaultForeground, lightCyan;
78
import 'package:path/path.dart' as path;
89

910
import 'src/create_bundle.dart';
11+
import 'src/create_external_packages_folder.dart';
12+
import 'src/dart_pub_get.dart';
1013
import 'src/exit_overrides.dart';
1114
import 'src/get_path_dependencies.dart';
1215

@@ -21,11 +24,21 @@ Future<void> run(HookContext context) => preGen(context);
2124
Future<void> preGen(
2225
HookContext context, {
2326
io.Directory? directory,
27+
ProcessRunner runProcess = io.Process.run,
2428
RouteConfigurationBuilder buildConfiguration = buildRouteConfiguration,
2529
void Function(int exitCode) exit = _defaultExit,
30+
Future<void> Function(String from, String to) copyPath = io_expanded.copyPath,
2631
}) async {
2732
final projectDirectory = directory ?? io.Directory.current;
2833

34+
// We need to make sure that the pubspec.lock file is up to date
35+
await dartPubGet(
36+
context,
37+
workingDirectory: projectDirectory.path,
38+
runProcess: runProcess,
39+
exit: exit,
40+
);
41+
2942
await createBundle(context, projectDirectory, exit);
3043

3144
final RouteConfiguration configuration;
@@ -64,25 +77,30 @@ Future<void> preGen(
6477
},
6578
);
6679

67-
await reportExternalPathDependencies(
68-
projectDirectory,
69-
onViolationStart: () {
70-
context.logger
71-
..err('All path dependencies must be within the project.')
72-
..err('External path dependencies detected:');
73-
},
74-
onExternalPathDependency: (dependencyName, dependencyPath) {
75-
context.logger.err(' \u{2022} $dependencyName from $dependencyPath');
76-
},
77-
onViolationEnd: () {
78-
exit(1);
79-
},
80-
);
81-
8280
final customDockerFile = io.File(
8381
path.join(projectDirectory.path, 'Dockerfile'),
8482
);
8583

84+
// Get all the internal path packages
85+
final internalPathDependencies = (await getPathDependencies(projectDirectory))
86+
.where(
87+
(dependencyPath) =>
88+
path.isWithin(projectDirectory.path, dependencyPath),
89+
)
90+
.toList();
91+
92+
// Then create the external packages folder
93+
// and add it to the list of path packages.
94+
final externalDependencies = await createExternalPackagesFolder(
95+
projectDirectory,
96+
copyPath: copyPath,
97+
);
98+
99+
final pathDependencies = [
100+
...internalPathDependencies,
101+
...externalDependencies,
102+
];
103+
86104
final addDockerfile = !customDockerFile.existsSync();
87105

88106
context.vars = {
@@ -99,7 +117,7 @@ Future<void> preGen(
99117
'serveStaticFiles': configuration.serveStaticFiles,
100118
'invokeCustomEntrypoint': configuration.invokeCustomEntrypoint,
101119
'invokeCustomInit': configuration.invokeCustomInit,
102-
'pathDependencies': await getPathDependencies(projectDirectory),
120+
'pathDependencies': pathDependencies,
103121
'dartVersion': context.vars['dartVersion'],
104122
'addDockerfile': addDockerfile,
105123
};

bricks/dart_frog_prod_server/hooks/pubspec.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ dependencies:
99
io: ^1.0.3
1010
mason: ^0.1.0-dev.39
1111
path: ^1.8.1
12+
pubspec_lock: ^3.0.2
1213
pubspec_parse: ^1.2.0
1314

1415
dev_dependencies:
Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
import 'dart:io';
2+
3+
import 'package:io/io.dart' as io;
4+
import 'package:path/path.dart' as path;
5+
import 'package:pubspec_lock/pubspec_lock.dart';
6+
7+
Future<List<String>> createExternalPackagesFolder(
8+
Directory directory, {
9+
path.Context? pathContext,
10+
Future<void> Function(String from, String to) copyPath = io.copyPath,
11+
}) async {
12+
final pathResolver = pathContext ?? path.context;
13+
final pubspecLock = await _getPubspecLock(directory.path, pathResolver);
14+
15+
final externalPathDependencies = pubspecLock.packages
16+
.map(
17+
(p) => p.iswitch(
18+
sdk: (_) => null,
19+
hosted: (_) => null,
20+
git: (_) => null,
21+
path: (d) => d.path,
22+
),
23+
)
24+
.whereType<String>()
25+
.where((dependencyPath) {
26+
return !pathResolver.isWithin('', dependencyPath);
27+
}).toList();
28+
29+
if (externalPathDependencies.isEmpty) {
30+
return [];
31+
}
32+
final mappedDependencies = externalPathDependencies
33+
.map(
34+
(dependencyPath) => (
35+
pathResolver.basename(dependencyPath),
36+
dependencyPath,
37+
),
38+
)
39+
.fold(<String, String>{}, (map, dependency) {
40+
map[dependency.$1] = dependency.$2;
41+
return map;
42+
});
43+
44+
final buildDirectory = Directory(
45+
pathResolver.join(
46+
directory.path,
47+
'build',
48+
),
49+
)..createSync();
50+
51+
final packagesDirectory = Directory(
52+
pathResolver.join(
53+
buildDirectory.path,
54+
'.dart_frog_path_dependencies',
55+
),
56+
)..createSync();
57+
58+
for (final entry in mappedDependencies.entries) {
59+
final from = pathResolver.join(directory.path, entry.value);
60+
final to = pathResolver.join(packagesDirectory.path, entry.key);
61+
62+
await copyPath(from, to);
63+
}
64+
65+
final mappedPaths = mappedDependencies.map(
66+
(key, value) => MapEntry(
67+
key,
68+
pathResolver.relative(
69+
path.join(packagesDirectory.path, key),
70+
from: buildDirectory.path,
71+
),
72+
),
73+
);
74+
75+
await File(
76+
pathResolver.join(
77+
buildDirectory.path,
78+
'pubspec_overrides.yaml',
79+
),
80+
).writeAsString('''
81+
dependency_overrides:
82+
${mappedPaths.entries.map((entry) => ' ${entry.key}:\n path: ${entry.value}').join('\n')}
83+
''');
84+
85+
return externalPathDependencies;
86+
}
87+
88+
Future<PubspecLock> _getPubspecLock(
89+
String workingDirectory,
90+
path.Context pathResolver,
91+
) async {
92+
final pubspecLockFile = File(
93+
workingDirectory.isEmpty
94+
? 'pubspec.lock'
95+
: pathResolver.join(workingDirectory, 'pubspec.lock'),
96+
);
97+
98+
final content = await pubspecLockFile.readAsString();
99+
return content.loadPubspecLockFromYaml();
100+
}

bricks/dart_frog_prod_server/hooks/test/pre_gen_test.dart

Lines changed: 50 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import 'package:test/test.dart';
99

1010
import '../pre_gen.dart';
1111
import '../src/exit_overrides.dart';
12+
import 'pubspeck_locks.dart';
1213

1314
class _FakeHookContext extends Fake implements HookContext {
1415
_FakeHookContext({Logger? logger}) : _logger = logger ?? _MockLogger();
@@ -36,6 +37,14 @@ void main() {
3637
late HookContext context;
3738
late Logger logger;
3839

40+
Future<ProcessResult> successRunProcess(
41+
executable,
42+
args, {
43+
String? workingDirectory,
44+
bool? runInShell,
45+
}) =>
46+
Future.value(ProcessResult(0, 0, '', ''));
47+
3948
setUp(() {
4049
logger = _MockLogger();
4150
context = _FakeHookContext(logger: logger)
@@ -61,6 +70,7 @@ void main() {
6170
context,
6271
buildConfiguration: (_) => throw exception,
6372
exit: exitCalls.add,
73+
runProcess: successRunProcess,
6474
);
6575
expect(exitCalls, equals([1]));
6676
verify(() => logger.err(exception.toString())).called(1);
@@ -106,6 +116,7 @@ void main() {
106116
context,
107117
buildConfiguration: (_) => configuration,
108118
exit: exitCalls.add,
119+
runProcess: successRunProcess,
109120
);
110121

111122
verify(
@@ -139,6 +150,7 @@ void main() {
139150
context,
140151
buildConfiguration: (_) => configuration,
141152
exit: exitCalls.add,
153+
runProcess: successRunProcess,
142154
);
143155

144156
verify(
@@ -149,18 +161,20 @@ void main() {
149161
expect(exitCalls, equals([1]));
150162
});
151163

152-
test('exit(1) for external dependencies', () async {
153-
const configuration = RouteConfiguration(
154-
middleware: [],
155-
directories: [],
156-
routes: [],
157-
rogueRoutes: [],
158-
endpoints: {},
159-
);
164+
test(
165+
'works with external dependencies',
166+
() async {
167+
const configuration = RouteConfiguration(
168+
middleware: [],
169+
directories: [],
170+
routes: [],
171+
rogueRoutes: [],
172+
endpoints: {},
173+
);
160174

161-
final directory = Directory.systemTemp.createTempSync();
162-
File(path.join(directory.path, 'pubspec.yaml')).writeAsStringSync(
163-
'''
175+
final directory = Directory.systemTemp.createTempSync();
176+
File(path.join(directory.path, 'pubspec.yaml')).writeAsStringSync(
177+
'''
164178
name: example
165179
version: 0.1.0
166180
environment:
@@ -172,28 +186,24 @@ dependencies:
172186
dev_dependencies:
173187
test: any
174188
''',
175-
);
176-
177-
final exitCalls = <int>[];
178-
await preGen(
179-
context,
180-
buildConfiguration: (_) => configuration,
181-
exit: exitCalls.add,
182-
directory: directory,
183-
);
189+
);
190+
File(path.join(directory.path, 'pubspec.lock')).writeAsStringSync(
191+
fooPath,
192+
);
193+
final exitCalls = <int>[];
194+
await preGen(
195+
context,
196+
buildConfiguration: (_) => configuration,
197+
exit: exitCalls.add,
198+
directory: directory,
199+
runProcess: successRunProcess,
200+
copyPath: (_, __) async {},
201+
);
184202

185-
expect(exitCalls, equals([1]));
186-
verify(
187-
() => logger.err('All path dependencies must be within the project.'),
188-
).called(1);
189-
verify(
190-
() => logger.err('External path dependencies detected:'),
191-
).called(1);
192-
verify(
193-
() => logger.err(' \u{2022} foo from ../../foo'),
194-
).called(1);
195-
directory.delete(recursive: true).ignore();
196-
});
203+
expect(exitCalls, isEmpty);
204+
directory.delete(recursive: true).ignore();
205+
},
206+
);
197207

198208
test('retains invokeCustomEntrypoint (true)', () async {
199209
const configuration = RouteConfiguration(
@@ -209,6 +219,7 @@ dev_dependencies:
209219
context,
210220
buildConfiguration: (_) => configuration,
211221
exit: exitCalls.add,
222+
runProcess: successRunProcess,
212223
);
213224
expect(exitCalls, isEmpty);
214225
verifyNever(() => logger.err(any()));
@@ -251,6 +262,10 @@ dependencies:
251262
test: any
252263
''',
253264
);
265+
266+
File(path.join(directory.path, 'pubspec.lock')).writeAsStringSync(
267+
noPathDependencies,
268+
);
254269
File(path.join(directory.path, 'Dockerfile')).writeAsStringSync(
255270
'',
256271
);
@@ -261,6 +276,7 @@ dependencies:
261276
buildConfiguration: (_) => configuration,
262277
exit: exitCalls.add,
263278
directory: directory,
279+
runProcess: successRunProcess,
264280
);
265281

266282
expect(
@@ -295,6 +311,7 @@ dependencies:
295311
context,
296312
buildConfiguration: (_) => configuration,
297313
exit: exitCalls.add,
314+
runProcess: successRunProcess,
298315
);
299316
expect(exitCalls, isEmpty);
300317
verifyNever(() => logger.err(any()));
@@ -397,6 +414,7 @@ dependencies:
397414
context,
398415
buildConfiguration: (_) => configuration,
399416
exit: exitCalls.add,
417+
runProcess: successRunProcess,
400418
);
401419
expect(exitCalls, isEmpty);
402420
verifyNever(() => logger.err(any()));

0 commit comments

Comments
 (0)