Skip to content

Commit 63c2c8c

Browse files
author
serverpod_cloud
committed
feat(cli): 1db0a9eb156f01910a68942446640661d433a56f
1 parent 51f367f commit 63c2c8c

5 files changed

Lines changed: 202 additions & 6 deletions

File tree

serverpod_cloud_cli/lib/commands/deploy/deploy.dart

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,18 @@ abstract class Deploy {
6666
beneath: includedSubPaths,
6767
fileReadPoolSize: concurrency,
6868
showFiles: showFiles,
69+
fileContentModifier: (final relativePath, final contentReader) async {
70+
final isPubspec =
71+
relativePath.endsWith('pubspec.yaml') &&
72+
!relativePath.contains('.scloud/');
73+
if (isPubspec) {
74+
final content = await contentReader();
75+
return WorkspaceProject.stripDevDependenciesFromPubspecContent(
76+
content,
77+
);
78+
}
79+
return null;
80+
},
6981
);
7082
return true;
7183
} on ProjectZipperExceptions catch (e) {

serverpod_cloud_cli/lib/commands/deploy/prepare_workspace.dart

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -203,6 +203,28 @@ abstract class WorkspaceProject {
203203
);
204204
}
205205

206+
/// Strips dev_dependencies from pubspec.yaml content in memory.
207+
/// Returns the modified content, or null if no changes were needed.
208+
static String? stripDevDependenciesFromPubspecContent(
209+
final String pubspecContent,
210+
) {
211+
try {
212+
final pubspecYaml = yamlDecode(pubspecContent);
213+
if (pubspecYaml is! Map) {
214+
return null;
215+
}
216+
217+
if (pubspecYaml.containsKey('dev_dependencies')) {
218+
final modifiedPubspec = Map.from(pubspecYaml);
219+
modifiedPubspec.remove('dev_dependencies');
220+
return yamlEncode(modifiedPubspec);
221+
}
222+
return null;
223+
} on Exception {
224+
return null;
225+
}
226+
}
227+
206228
/// Throws a [WorkspaceException] with one or more error messages.
207229
static Never _throwWorkspaceException({
208230
final String? message,

serverpod_cloud_cli/lib/project_zipper/project_zipper.dart

Lines changed: 29 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import 'dart:convert';
12
import 'dart:io';
23
import 'package:archive/archive_io.dart';
34
import 'package:pool/pool.dart';
@@ -31,6 +32,11 @@ abstract final class ProjectZipper {
3132
/// The [beneath] is the list of relative paths under [rootDirectory] that will be included,
3233
/// all by default.
3334
/// The [fileReadPoolSize] is the number of files that are processed concurrently.
35+
/// The [fileContentModifier] is an optional callback that can modify file content before
36+
/// it is added to the archive. It receives the relative path and a content reader function.
37+
/// The callback should return the modified content as a string, or null if no modification
38+
/// is needed (in which case the file will be added as binary). The content reader is only
39+
/// called when the modifier decides it needs to read the file content.
3440
///
3541
/// All exceptions thrown by this method are subclasses of [ProjectZipperExceptions].
3642
/// Throws [ProjectDirectoryDoesNotExistException] if the project directory
@@ -46,6 +52,11 @@ abstract final class ProjectZipper {
4652
final Iterable<String> beneath = const ['.'],
4753
final int fileReadPoolSize = 5,
4854
final bool showFiles = false,
55+
final Future<String?> Function(
56+
String relativePath,
57+
Future<String> Function() contentReader,
58+
)?
59+
fileContentModifier,
4960
}) async {
5061
final projectPath = rootDirectory.path;
5162

@@ -86,12 +97,24 @@ abstract final class ProjectZipper {
8697
if (!file.existsSync()) return;
8798

8899
await fileReadPool.withResource(() async {
89-
final length = await file.length();
90-
final bytes = await file.readAsBytes();
91-
92-
archive.addFile(
93-
ArchiveFile(stripRoot(projectPath, path), length, bytes),
94-
);
100+
final relativePath = stripRoot(projectPath, path);
101+
102+
List<int> bytes;
103+
if (fileContentModifier != null) {
104+
final modifiedContent = await fileContentModifier(
105+
relativePath,
106+
() => file.readAsString(),
107+
);
108+
if (modifiedContent != null) {
109+
bytes = utf8.encode(modifiedContent);
110+
} else {
111+
bytes = await file.readAsBytes();
112+
}
113+
} else {
114+
bytes = await file.readAsBytes();
115+
}
116+
117+
archive.addFile(ArchiveFile(relativePath, bytes.length, bytes));
95118
});
96119
}
97120

serverpod_cloud_cli/test/prepare_workspace_unit_test.dart

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -458,4 +458,78 @@ environment: not-a-map
458458
expect(result, contains('environment: "not-a-map"'));
459459
});
460460
});
461+
462+
group('WorkspaceProject.stripDevDependenciesFromPubspecContent -', () {
463+
test('Given pubspec content with dev_dependencies, '
464+
'when called, then removes dev_dependencies', () {
465+
const pubspecContent =
466+
'''
467+
name: test_package
468+
version: 1.0.0
469+
environment:
470+
sdk: ${ProjectFactory.validSdkVersion}
471+
dependencies:
472+
serverpod: ${ProjectFactory.validServerpodVersion}
473+
dev_dependencies:
474+
test: ^1.0.0
475+
build_runner: ^2.0.0
476+
''';
477+
478+
final result = WorkspaceProject.stripDevDependenciesFromPubspecContent(
479+
pubspecContent,
480+
);
481+
expect(result, isNot(contains('dev_dependencies:')));
482+
expect(result, contains('dependencies:'));
483+
expect(result, contains('test_package'));
484+
expect(result, isNot(contains('test: ^1.0.0')));
485+
expect(result, isNot(contains('build_runner: ^2.0.0')));
486+
});
487+
488+
test('Given pubspec content without dev_dependencies, '
489+
'when called, then returns null', () {
490+
const originalContent =
491+
'''
492+
name: test_package
493+
version: 1.0.0
494+
environment:
495+
sdk: ${ProjectFactory.validSdkVersion}
496+
dependencies:
497+
serverpod: ${ProjectFactory.validServerpodVersion}
498+
''';
499+
500+
final result = WorkspaceProject.stripDevDependenciesFromPubspecContent(
501+
originalContent,
502+
);
503+
expect(result, isNull);
504+
});
505+
506+
test('Given invalid YAML content, '
507+
'when called, then returns null', () {
508+
const invalidContent = 'not valid yaml: [';
509+
510+
final result = WorkspaceProject.stripDevDependenciesFromPubspecContent(
511+
invalidContent,
512+
);
513+
expect(result, isNull);
514+
});
515+
516+
test('Given pubspec content with only dev_dependencies, '
517+
'when called, then removes dev_dependencies', () {
518+
const pubspecContent =
519+
'''
520+
name: test_package
521+
version: 1.0.0
522+
environment:
523+
sdk: ${ProjectFactory.validSdkVersion}
524+
dev_dependencies:
525+
test: ^1.0.0
526+
''';
527+
528+
final result = WorkspaceProject.stripDevDependenciesFromPubspecContent(
529+
pubspecContent,
530+
);
531+
expect(result, isNot(contains('dev_dependencies:')));
532+
expect(result, contains('test_package'));
533+
});
534+
});
461535
}

serverpod_cloud_cli/test_integration/commands/deploy_command_test.dart

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ import 'dart:async';
55
import 'dart:convert';
66
import 'dart:io';
77

8+
import 'package:archive/archive.dart';
9+
import 'package:archive/archive_io.dart';
810
import 'package:args/command_runner.dart';
911
import 'package:mocktail/mocktail.dart';
1012
import 'package:path/path.dart' as p;
@@ -891,4 +893,67 @@ dependencies:
891893
});
892894
},
893895
);
896+
897+
group('Given a project with dev_dependencies in pubspec.yaml', () {
898+
late String testProjectDir;
899+
900+
setUp(() async {
901+
await d.dir('project', [
902+
d.file('pubspec.yaml', '''
903+
name: test_server
904+
version: 1.0.0
905+
environment:
906+
sdk: ${ProjectFactory.validSdkVersion}
907+
dependencies:
908+
serverpod: ${ProjectFactory.validServerpodVersion}
909+
dev_dependencies:
910+
test: ^1.0.0
911+
build_runner: ^2.0.0
912+
mockito: ^5.0.0
913+
'''),
914+
d.dir('bin', [d.file('main.dart', 'void main() {}')]),
915+
]).create();
916+
testProjectDir = p.join(d.sandbox, 'project');
917+
918+
when(() => client.deploy.createUploadDescription(any())).thenAnswer(
919+
(final _) async => BucketUploadDescription.uploadDescription,
920+
);
921+
});
922+
923+
group('when deploying through CLI', () {
924+
late Future cliCommandFuture;
925+
setUp(() async {
926+
cliCommandFuture = cli.run([
927+
'deploy',
928+
'--project',
929+
BucketUploadDescription.projectId,
930+
'--project-dir',
931+
testProjectDir,
932+
]);
933+
});
934+
935+
test(
936+
'then dev_dependencies are stripped from pubspec.yaml in zip',
937+
() async {
938+
await cliCommandFuture;
939+
940+
expect(mockFileUploader.uploadedData, isNotEmpty);
941+
942+
final archive = ZipDecoder().decodeBytes(
943+
mockFileUploader.uploadedData,
944+
);
945+
final pubspecFile = archive.findFile('pubspec.yaml');
946+
expect(pubspecFile, isNotNull);
947+
948+
final pubspecContent = utf8.decode(pubspecFile!.content);
949+
expect(pubspecContent, isNot(contains('dev_dependencies:')));
950+
expect(pubspecContent, isNot(contains('test: ^1.0.0')));
951+
expect(pubspecContent, isNot(contains('build_runner: ^2.0.0')));
952+
expect(pubspecContent, isNot(contains('mockito: ^5.0.0')));
953+
expect(pubspecContent, contains('dependencies:'));
954+
expect(pubspecContent, contains('serverpod:'));
955+
},
956+
);
957+
});
958+
});
894959
}

0 commit comments

Comments
 (0)