Skip to content

Commit

Permalink
feat(mason_cli): add --set-exit-if-changed for mason bundle (#1229)
Browse files Browse the repository at this point in the history
Co-authored-by: Felix Angelov <felangelov@gmail.com>
  • Loading branch information
alestiago and felangel authored Feb 8, 2024
1 parent 076d5d8 commit 5cbf7a9
Show file tree
Hide file tree
Showing 2 changed files with 226 additions and 26 deletions.
145 changes: 119 additions & 26 deletions packages/mason_cli/lib/src/commands/bundle.dart
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,11 @@ class BundleCommand extends MasonCommand {
allowed: ['git', 'path', 'hosted'],
defaultsTo: 'path',
)
..addFlag(
'set-exit-if-changed',
help: 'Return exit code 70 if there are files modified.',
negatable: false,
)
..addOption(
'git-ref',
help: 'Git branch or commit to be used.'
Expand All @@ -63,6 +68,7 @@ class BundleCommand extends MasonCommand {
@override
Future<int> run() async {
final source = results['source'] as String;
final setExitIfChanged = results['set-exit-if-changed'] as bool;

final Brick brick;
if (source == 'git') {
Expand Down Expand Up @@ -113,17 +119,43 @@ class BundleCommand extends MasonCommand {
final bundleType = (results['type'] as String).toBundleType();
final bundleProgress = logger.progress('Bundling ${bundle.name}');

try {
late final String bundlePath;
late _BrickBundleGenerator bundleGenerator;
switch (bundleType) {
case BundleType.dart:
bundleGenerator = _BrickDartBundleGenerator(
outputDirectoryPath: outputDir,
bundle: bundle,
);
break;
case BundleType.universal:
bundleGenerator = _BrickUniversalBundleGenerator(
outputDirectoryPath: outputDir,
bundle: bundle,
);
break;
}

MasonBundle? previousBundle;
if (setExitIfChanged && bundleGenerator.bundleFile.existsSync()) {
switch (bundleType) {
case BundleType.dart:
bundlePath = await _generateDartBundle(bundle, outputDir);
previousBundle = await MasonBundle.fromDartBundle(
await bundleGenerator.bundleFile.readAsString(),
);
break;
case BundleType.universal:
bundlePath = await _generateUniversalBundle(bundle, outputDir);
previousBundle = await MasonBundle.fromUniversalBundle(
await bundleGenerator.bundleFile.readAsBytes(),
);
break;
}
}

try {
await bundleGenerator.generate();
bundleProgress.complete('Generated 1 file.');
final bundlePath = canonicalize(bundleGenerator.bundleFile.path);

logger.info(darkGray.wrap(' $bundlePath'));
} catch (_) {
bundleProgress.fail();
Expand All @@ -132,30 +164,19 @@ class BundleCommand extends MasonCommand {
tempBricksJson?.clear();
}

return ExitCode.success.code;
}
}
if (setExitIfChanged) {
final previousContent = jsonEncode(previousBundle?.toJson() ?? {});
final newContent = jsonEncode(bundle.toJson());

Future<String> _generateDartBundle(
MasonBundle bundle,
String outputDir,
) async {
final file = File(path.join(outputDir, '${bundle.name}_bundle.dart'));
await file.create(recursive: true);
await file.writeAsString(
"// GENERATED CODE - DO NOT MODIFY BY HAND\n// ignore_for_file: type=lint, implicit_dynamic_list_literal, implicit_dynamic_map_literal, inference_failure_on_collection_literal\n\nimport 'package:mason/mason.dart';\n\nfinal ${bundle.name.camelCase}Bundle = MasonBundle.fromJson(<String, dynamic>${json.encode(bundle.toJson())});",
);
return canonicalize(file.path);
}
if (previousContent != newContent) {
logger.err('${lightRed.wrap('✗')} 1 file changed');
return ExitCode.software.code;
}
logger.info('${lightGreen.wrap('✓')} 0 files changed');
}

Future<String> _generateUniversalBundle(
MasonBundle bundle,
String outputDir,
) async {
final file = File(path.join(outputDir, '${bundle.name}.bundle'));
await file.create(recursive: true);
await file.writeAsBytes(await bundle.toUniversalBundle());
return canonicalize(file.path);
return ExitCode.success.code;
}
}

extension on String {
Expand All @@ -169,3 +190,75 @@ extension on String {
}
}
}

/// {@template brick_bundle_generator}
/// A bundle generator is responsible for generating a
/// brick bundle for a given [MasonBundle].
///
/// See also:
///
/// * [_BrickDartBundleGenerator], a bundle generator which generates a Dart
/// bundle.
/// * [_BrickUniversalBundleGenerator], a bundle generator which generates a
/// universal bundle.
/// {@endtemplate}
abstract class _BrickBundleGenerator {
/// {@macro brick_bundle_generator}
const _BrickBundleGenerator({
required this.bundleFile,
required MasonBundle bundle,
}) : _bundle = bundle;

/// The respective [MasonBundle] for which a brick bundle will be generated.
final MasonBundle _bundle;

/// The file that we will be writing the brick bundle to.
final File bundleFile;

/// Generates the brick bundle file for the given [MasonBundle].
Future<void> generate();
}

/// {@template brick_dart_bundle_generator}
/// A bundle generator which generates a Dart bundle.
/// {@endtemplate}
class _BrickDartBundleGenerator extends _BrickBundleGenerator {
/// {@macro brick_dart_bundle_generator}
_BrickDartBundleGenerator({
required String outputDirectoryPath,
required super.bundle,
}) : super(
bundleFile: File(
path.join(outputDirectoryPath, '${bundle.name}_bundle.dart'),
),
);

@override
Future<void> generate() async {
await bundleFile.create(recursive: true);
await bundleFile.writeAsString(
"// GENERATED CODE - DO NOT MODIFY BY HAND\n// ignore_for_file: type=lint, implicit_dynamic_list_literal, implicit_dynamic_map_literal, inference_failure_on_collection_literal\n\nimport 'package:mason/mason.dart';\n\nfinal ${_bundle.name.camelCase}Bundle = MasonBundle.fromJson(<String, dynamic>${json.encode(_bundle.toJson())});",
);
}
}

/// {@template brick_universal_bundle_generator}
/// A bundle generator which generates a universal bundle.
/// {@endtemplate}
class _BrickUniversalBundleGenerator extends _BrickBundleGenerator {
/// {@macro brick_universal_bundle_generator}
_BrickUniversalBundleGenerator({
required String outputDirectoryPath,
required super.bundle,
}) : super(
bundleFile: File(
path.join(outputDirectoryPath, '${bundle.name}.bundle'),
),
);

@override
Future<void> generate() async {
await bundleFile.create(recursive: true);
await bundleFile.writeAsBytes(await _bundle.toUniversalBundle());
}
}
107 changes: 107 additions & 0 deletions packages/mason_cli/test/commands/bundle_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -487,6 +487,113 @@ void main() {
verifyNever(() => logger.progress(any()));
});
});

group('set-exit-if-changed', () {
final brickPath = path.join(
'..',
'..',
'..',
'..',
'..',
'..',
'bricks',
'greeting',
);

setUp(() {
final tempTestDir = Directory.current.createTempSync();
Directory.current = tempTestDir.path;

addTearDown(() {
try {
tempTestDir.deleteSync(recursive: true);
} catch (_) {}
});
});

group('with dart bundle', () {
final bundleFile = File('greeting_bundle.dart');

test('exits with code 70 when bundle was newly created', () async {
final result = await commandRunner.run(
['bundle', brickPath, '-t', 'dart', '--set-exit-if-changed'],
);

expect(result, equals(ExitCode.software.code));
});

test('exits with code 70 when bundle was modified', () async {
await commandRunner.run(['bundle', '-t', 'dart', brickPath]);

final originalContents = bundleFile.readAsStringSync();

bundleFile.writeAsStringSync(
originalContents.replaceAll('greeting', 'modified_greeting'),
);

final result = await commandRunner.run(
['bundle', brickPath, '-t', 'dart', '--set-exit-if-changed'],
);

expect(result, equals(ExitCode.software.code));
expect(bundleFile.readAsStringSync(), equals(originalContents));
});

test('exits with code 0 when bundle was not changed', () async {
await commandRunner.run(['bundle', '-t', 'dart', brickPath]);

final result = await commandRunner.run(
['bundle', brickPath, '-t', 'dart', '--set-exit-if-changed'],
);

expect(result, equals(ExitCode.success.code));
});
});

group('with universal bundle', () {
final bundleFile = File('greeting.bundle');

test('exits with code 70 when bundle was newly created', () async {
final result = await commandRunner.run(
['bundle', brickPath, '--set-exit-if-changed'],
);

expect(result, equals(ExitCode.software.code));
});

test('exits with code 70 when bundle was modified', () async {
await commandRunner.run(['bundle', brickPath]);

final originalBytes = bundleFile.readAsBytesSync();

final bundle = await MasonBundle.fromUniversalBundle(originalBytes);
final bundleJson = jsonEncode(bundle.toJson())
.replaceAll('greeting', 'modified_greeting');
final modifiedBundle = MasonBundle.fromJson(
jsonDecode(bundleJson) as Map<String, dynamic>,
);
final modifiedBytes = await modifiedBundle.toUniversalBundle();
bundleFile.writeAsBytesSync(modifiedBytes);

final result = await commandRunner.run(
['bundle', brickPath, '--set-exit-if-changed'],
);

expect(result, equals(ExitCode.software.code));
expect(bundleFile.readAsBytesSync(), equals(originalBytes));
});

test('exits with code 0 when bundle was not changed', () async {
await commandRunner.run(['bundle', brickPath]);

final result = await commandRunner.run(
['bundle', brickPath, '--set-exit-if-changed'],
);

expect(result, equals(ExitCode.success.code));
});
});
});
},
timeout: const Timeout.factor(2),
);
Expand Down

0 comments on commit 5cbf7a9

Please sign in to comment.