diff --git a/packages/mason_cli/lib/src/commands/bundle.dart b/packages/mason_cli/lib/src/commands/bundle.dart index b5d989971..2bb132938 100644 --- a/packages/mason_cli/lib/src/commands/bundle.dart +++ b/packages/mason_cli/lib/src/commands/bundle.dart @@ -63,81 +63,108 @@ class BundleCommand extends MasonCommand { @override Future run() async { final source = results['source'] as String; + final outputDir = results['output-dir'] as String; + final bundleType = (results['type'] as String).toBundleType(); + + final bricks = _parseBricks(source); + final tempBricksJson = []; + final bundleProgress = logger.progress( + 'Bundling ${bricks.length} ${_pluralize('brick', bricks.length > 1)}', + ); + + final bundlePaths = []; + try { + for (final brick in bricks) { + final Directory brickDirectory; + if (brick.location.path != null) { + brickDirectory = Directory(brick.location.path!); + } else { + final tempBrickJson = BricksJson.temp(); + final cachedBrick = await tempBrickJson.add(brick); + tempBricksJson.add(tempBrickJson); + brickDirectory = Directory(cachedBrick.path); + } + + if (!brickDirectory.existsSync()) { + throw BrickNotFoundException(brickDirectory.path); + } + + final bundle = createBundle(brickDirectory); + bundleProgress.update('Bundling ${bundle.name}'); + + final String bundlePath; + switch (bundleType) { + case BundleType.dart: + bundlePath = await _generateDartBundle(bundle, outputDir); + break; + case BundleType.universal: + bundlePath = await _generateUniversalBundle(bundle, outputDir); + break; + } + bundleProgress.update('Bundled ${bundle.name}'); + bundlePaths.add(bundlePath); + } + + final message = + 'Generated ${bricks.length} ${_pluralize('file', bricks.length > 1)}'; + bundleProgress.update('${lightGreen.wrap('✓')} $message:'); + for (final bundlePath in bundlePaths) { + final logLine = darkGray.wrap(' $bundlePath'); + if (logLine != null) { + bundleProgress.update(logLine); + } + } + bundleProgress.complete(); + } catch (_) { + bundleProgress.fail(); + rethrow; + } finally { + for (final tmp in tempBricksJson) { + tmp.clear(); + } + } + + return ExitCode.success.code; + } - final Brick brick; + String _pluralize(String word, bool isPlural) { + return '$word${isPlural ? 's' : ''}'; + } + + List _parseBricks(String source) { if (source == 'git') { if (results.rest.isEmpty) { usageException('A repository url must be provided'); } - brick = Brick( - location: BrickLocation( - git: GitPath( - results.rest.first, - path: results['git-path'] as String?, - ref: results['git-ref'] as String?, + return results.rest.map((url) { + return Brick( + location: BrickLocation( + git: GitPath( + url, + path: results['git-path'] as String?, + ref: results['git-ref'] as String?, + ), ), - ), - ); + ); + }).toList(); } else if (source == 'hosted') { if (results.rest.isEmpty) { usageException('A brick name must be provided'); } - brick = Brick( - name: results.rest.first, - location: const BrickLocation(version: 'any'), - ); + return results.rest.map((name) { + return Brick( + name: name, + location: const BrickLocation(version: 'any'), + ); + }).toList(); } else { if (results.rest.isEmpty) { usageException('A path to the brick template must be provided'); } - brick = Brick(location: BrickLocation(path: results.rest.first)); - } - - BricksJson? tempBricksJson; - - final Directory brickDirectory; - if (brick.location.path != null) { - brickDirectory = Directory(brick.location.path!); - } else { - tempBricksJson = BricksJson.temp(); - final cachedBrick = await tempBricksJson.add(brick); - brickDirectory = Directory(cachedBrick.path); - } - - if (!brickDirectory.existsSync()) { - throw BrickNotFoundException(brickDirectory.path); - } - - final bundle = createBundle(brickDirectory); - final outputDir = results['output-dir'] as String; - final bundleType = (results['type'] as String).toBundleType(); - final bundleProgress = logger.progress('Bundling ${bundle.name}'); - - try { - late final String bundlePath; - switch (bundleType) { - case BundleType.dart: - bundlePath = await _generateDartBundle(bundle, outputDir); - break; - case BundleType.universal: - bundlePath = await _generateUniversalBundle(bundle, outputDir); - break; - } - bundleProgress.complete('Bundled ${bundle.name}'); - logger - ..info( - '${lightGreen.wrap('✓')} ' - 'Generated 1 file:', - ) - ..info(darkGray.wrap(' $bundlePath')); - } catch (_) { - bundleProgress.fail(); - rethrow; - } finally { - tempBricksJson?.clear(); + return results.rest.map((location) { + return Brick(location: BrickLocation(path: location)); + }).toList(); } - - return ExitCode.success.code; } } diff --git a/packages/mason_cli/test/commands/bundle_test.dart b/packages/mason_cli/test/commands/bundle_test.dart index 1d1975e77..312ee0f6e 100644 --- a/packages/mason_cli/test/commands/bundle_test.dart +++ b/packages/mason_cli/test/commands/bundle_test.dart @@ -22,14 +22,16 @@ void main() { group('mason bundle', () { late Logger logger; + late Progress progress; late PubUpdater pubUpdater; late MasonCommandRunner commandRunner; setUp(() { logger = MockLogger(); + progress = MockProgress(); pubUpdater = MockPubUpdater(); - when(() => logger.progress(any())).thenReturn(MockProgress()); + when(() => logger.progress(any())).thenReturn(progress); when( () => pubUpdater.getLatestVersion(any()), ).thenAnswer((_) async => packageVersion); @@ -82,15 +84,16 @@ void main() { '''"vars":{"name":{"type":"string","description":"Your name","default":"Dash","prompt":"What is your name?"}}}''', ), ); - verify(() => logger.progress('Bundling greeting')).called(1); + verify(() => logger.progress('Bundling 1 brick')).called(1); + verify(() => progress.update('Bundling greeting')).called(1); verify( - () => logger.info( + () => progress.update( '${lightGreen.wrap('✓')} ' 'Generated 1 file:', ), ).called(1); verify( - () => logger.info(darkGray.wrap(' ${canonicalize(file.path)}')), + () => progress.update(darkGray.wrap(' ${canonicalize(file.path)}')!), ).called(1); }); @@ -144,15 +147,16 @@ void main() { '''"name":"hooks","description":"A Hooks Example Template","version":"0.1.0+1","environment":{"mason":"any"},"vars":{"name":{"type":"string","description":"Your name","default":"Dash","prompt":"What is your name?"}}''', ), ); - verify(() => logger.progress('Bundling hooks')).called(1); + verify(() => logger.progress('Bundling 1 brick')).called(1); + verify(() => progress.update('Bundling hooks')).called(1); verify( - () => logger.info( + () => progress.update( '${lightGreen.wrap('✓')} ' 'Generated 1 file:', ), ).called(1); verify( - () => logger.info(darkGray.wrap(' ${canonicalize(file.path)}')), + () => progress.update(darkGray.wrap(' ${canonicalize(file.path)}')!), ).called(1); }); @@ -197,15 +201,16 @@ void main() { '''"vars":{"name":{"type":"string","description":"Your name","default":"Dash","prompt":"What is your name?"}}});''', ), ); - verify(() => logger.progress('Bundling greeting')).called(1); + verify(() => logger.progress('Bundling 1 brick')).called(1); + verify(() => progress.update('Bundling greeting')).called(1); verify( - () => logger.info( + () => progress.update( '${lightGreen.wrap('✓')} ' 'Generated 1 file:', ), ).called(1); verify( - () => logger.info(darkGray.wrap(' ${canonicalize(file.path)}')), + () => progress.update(darkGray.wrap(' ${canonicalize(file.path)}')!), ).called(1); }); @@ -271,15 +276,64 @@ void main() { '''"name":"hooks","description":"A Hooks Example Template","version":"0.1.0+1","environment":{"mason":"any"},"vars":{"name":{"type":"string","description":"Your name","default":"Dash","prompt":"What is your name?"}}''', ), ); - verify(() => logger.progress('Bundling hooks')).called(1); + verify(() => logger.progress('Bundling 1 brick')).called(1); + verify(() => progress.update('Bundling hooks')).called(1); verify( - () => logger.info( + () => progress.update( '${lightGreen.wrap('✓')} ' 'Generated 1 file:', ), ).called(1); verify( - () => logger.info(darkGray.wrap(' ${canonicalize(file.path)}')), + () => progress.update(darkGray.wrap(' ${canonicalize(file.path)}')!), + ).called(1); + }); + + test('creates a new dart bundle with multiple files', () async { + final testDir = Directory( + path.join(Directory.current.path, 'dart'), + )..createSync(recursive: true); + final brick1Path = + path.join('..', '..', '..', '..', '..', '..', 'bricks', 'greeting'); + final brick2Path = + path.join('..', '..', '..', '..', '..', '..', 'bricks', 'hello'); + Directory.current = testDir.path; + final result = await commandRunner.run( + ['bundle', brick1Path, brick2Path, '-t', 'dart'], + ); + expect(result, equals(ExitCode.success.code)); + final file1 = File( + path.join( + testFixturesPath(cwd, suffix: '.bundle'), + 'dart', + 'greeting_bundle.dart', + ), + ); + final file2 = File( + path.join( + testFixturesPath(cwd, suffix: '.bundle'), + 'dart', + 'hello_bundle.dart', + ), + ); + verify(() => logger.progress('Bundling 2 bricks')).called(1); + verify(() => progress.update('Bundling greeting')).called(1); + verify(() => progress.update('Bundled greeting')).called(1); + verify(() => progress.update('Bundling hello')).called(1); + verify(() => progress.update('Bundled hello')).called(1); + verify( + () => progress.update( + '${lightGreen.wrap('✓')} ' + 'Generated 2 files:', + ), + ).called(1); + verify( + () => + progress.update(darkGray.wrap(' ${canonicalize(file1.path)}')!), + ).called(1); + verify( + () => + progress.update(darkGray.wrap(' ${canonicalize(file2.path)}')!), ).called(1); }); @@ -299,12 +353,12 @@ void main() { verify( () => logger.err('Could not find brick at $brickPath'), ).called(1); - verifyNever(() => logger.progress(any())); + verifyNever(() => progress.update(any())); }); test('exists with code 64 when exception occurs on bundling', () async { final progress = MockProgress(); - when(() => progress.complete(any())).thenAnswer((invocation) { + when(() => progress.update(any())).thenAnswer((invocation) { final update = invocation.positionalArguments[0] as String?; if (update == 'Bundled greeting') { @@ -367,15 +421,16 @@ void main() { '''"vars":{"name":{"type":"string","description":"Your name","default":"Dash","prompt":"What is your name?"}}}''', ), ); - verify(() => logger.progress('Bundling greeting')).called(1); + verify(() => logger.progress('Bundling 1 brick')).called(1); + verify(() => progress.update('Bundling greeting')).called(1); verify( - () => logger.info( + () => progress.update( '${lightGreen.wrap('✓')} ' 'Generated 1 file:', ), ).called(1); verify( - () => logger.info(darkGray.wrap(' ${canonicalize(file.path)}')), + () => progress.update(darkGray.wrap(' ${canonicalize(file.path)}')!), ).called(1); }); @@ -405,10 +460,10 @@ void main() { expect(result, equals(ExitCode.usage.code)); + verify(() => logger.progress('Bundling 1 brick')).called(1); verify( () => logger.err('Could not find brick at $url'), ).called(1); - verifyNever(() => logger.progress(any())); }); }); @@ -444,15 +499,16 @@ void main() { expect(actual, contains('"changelog":{"path":"CHANGELOG.md","data":"')); expect(actual, contains('"license":{"path":"LICENSE","data":"')); - verify(() => logger.progress('Bundling greeting')).called(1); + verify(() => logger.progress('Bundling 1 brick')).called(1); + verify(() => progress.update('Bundling greeting')).called(1); verify( - () => logger.info( + () => progress.update( '${lightGreen.wrap('✓')} ' 'Generated 1 file:', ), ).called(1); verify( - () => logger.info(darkGray.wrap(' ${canonicalize(file.path)}')), + () => progress.update(darkGray.wrap(' ${canonicalize(file.path)}')!), ).called(1); }); @@ -482,7 +538,7 @@ void main() { verify( () => logger.err('Brick "nonexistent-brick" does not exist.'), ).called(1); - verifyNever(() => logger.progress(any())); + verifyNever(() => progress.update(any())); }); }); });