diff --git a/lib/src/command/outdated.dart b/lib/src/command/outdated.dart index beda8b621..424bad4f7 100644 --- a/lib/src/command/outdated.dart +++ b/lib/src/command/outdated.dart @@ -4,7 +4,6 @@ import 'dart:async'; import 'dart:convert'; -import 'dart:math'; import 'package:collection/collection.dart' show IterableExtension; import 'package:path/path.dart' as p; @@ -15,6 +14,7 @@ import '../entrypoint.dart'; import '../io.dart'; import '../lock_file.dart'; import '../log.dart' as log; +import '../log.dart'; import '../package.dart'; import '../package_name.dart'; import '../pubspec.dart'; @@ -552,8 +552,8 @@ Future _outputHuman( final markedRows = Map.fromIterables(rows, await mode.markVersionDetails(rows)); - List<_FormattedString> formatted(_PackageDetails package) => [ - _FormattedString(package.name), + List formatted(_PackageDetails package) => [ + FormattedString(package.name), ...markedRows[package]!.map((m) => m.toHuman()), ]; @@ -575,64 +575,42 @@ Future _outputHuman( final devTransitiveRows = rows.where(hasKind(_DependencyKind.devTransitive)).map(formatted); - final formattedRows = >[ + final formattedRows = >[ ['Package Name', 'Current', 'Upgradable', 'Resolvable', 'Latest'] - .map((s) => _format(s, log.bold)) + .map((s) => format(s, log.bold)) .toList(), if (hasDirectDependencies) ...[ [ if (directRows.isEmpty) - _format('\ndirect dependencies: ${mode.allGood}', log.bold) + format('\ndirect dependencies: ${mode.allGood}', log.bold) else - _format('\ndirect dependencies:', log.bold), + format('\ndirect dependencies:', log.bold), ], ...directRows, ], if (includeDevDependencies && hasDevDependencies) ...[ [ if (devRows.isEmpty) - _format('\ndev_dependencies: ${mode.allGood}', log.bold) + format('\ndev_dependencies: ${mode.allGood}', log.bold) else - _format('\ndev_dependencies:', log.bold), + format('\ndev_dependencies:', log.bold), ], ...devRows, ], if (showTransitiveDependencies) ...[ if (transitiveRows.isNotEmpty) - [_format('\ntransitive dependencies:', log.bold)], + [format('\ntransitive dependencies:', log.bold)], ...transitiveRows, if (includeDevDependencies) ...[ if (devTransitiveRows.isNotEmpty) - [_format('\ntransitive dev_dependencies:', log.bold)], + [format('\ntransitive dev_dependencies:', log.bold)], ...devTransitiveRows, ], ], ]; - final columnWidths = {}; - for (var i = 0; i < formattedRows.length; i++) { - if (formattedRows[i].length > 1) { - for (var j = 0; j < formattedRows[i].length; j++) { - final currentMaxWidth = columnWidths[j] ?? 0; - columnWidths[j] = max( - formattedRows[i][j].computeLength(useColors: useColors), - currentMaxWidth, - ); - } - } - } - - for (final row in formattedRows) { - final b = StringBuffer(); - for (var j = 0; j < row.length; j++) { - b.write(row[j].formatted(useColors: useColors)); - b.write( - ' ' * - ((columnWidths[j]! + 2) - - row[j].computeLength(useColors: useColors)), - ); - } - log.message(b.toString()); + for (final line in log.renderTable(formattedRows, useColors)) { + log.message(line); } final upgradable = rows.where( @@ -1016,16 +994,8 @@ enum _DependencyKind { devTransitive, } -_FormattedString _format( - String value, - String Function(String) format, { - String? prefix = '', -}) { - return _FormattedString(value, format: format, prefix: prefix); -} - abstract class _Details { - _FormattedString toHuman(); + FormattedString toHuman(); Object? toJson(); } @@ -1035,7 +1005,7 @@ class _SimpleDetails implements _Details { _SimpleDetails(this.details); @override - _FormattedString toHuman() => _FormattedString(details); + FormattedString toHuman() => FormattedString(details); @override Object? toJson() => null; @@ -1060,7 +1030,7 @@ class _MarkedVersionDetails implements _Details { _jsonExplanation = jsonExplanation; @override - _FormattedString toHuman() => _FormattedString( + FormattedString toHuman() => FormattedString( _versionDetails?.describe ?? '-', format: _format, prefix: _prefix, @@ -1078,39 +1048,6 @@ class _MarkedVersionDetails implements _Details { } } -class _FormattedString { - final String value; - - /// Should apply the ansi codes to present this string. - final String Function(String) _format; - - /// A prefix for marking this string if colors are not used. - final String _prefix; - - final String _suffix; - - _FormattedString( - this.value, { - String Function(String)? format, - String? prefix, - String? suffix, - }) : _format = format ?? _noFormat, - _prefix = prefix ?? '', - _suffix = suffix ?? ''; - - String formatted({required bool useColors}) { - return useColors - ? _format(_prefix + value + _suffix) - : _prefix + value + _suffix; - } - - int computeLength({required bool? useColors}) { - return _prefix.length + value.length + _suffix.length; - } - - static String _noFormat(String x) => x; -} - /// Whether the package [name] is overridden anywhere in the workspace rooted at /// [workspaceRoot]. bool hasOverride(Package workspaceRoot, String name) { diff --git a/lib/src/command/workspace.dart b/lib/src/command/workspace.dart new file mode 100644 index 000000000..67148f5ea --- /dev/null +++ b/lib/src/command/workspace.dart @@ -0,0 +1,18 @@ +// Copyright (c) 2024, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import '../command.dart'; +import 'workspace_list.dart'; + +class WorkspaceCommand extends PubCommand { + @override + String get description => 'Work with the pub workspace.'; + + @override + String get name => 'workspace'; + + WorkspaceCommand() { + addSubcommand(WorkspaceListCommand()); + } +} diff --git a/lib/src/command/workspace_list.dart b/lib/src/command/workspace_list.dart new file mode 100644 index 000000000..b48716bc0 --- /dev/null +++ b/lib/src/command/workspace_list.dart @@ -0,0 +1,63 @@ +// Copyright (c) 2024, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'dart:convert'; + +import 'package:path/path.dart' as p; + +import '../command.dart'; +import '../log.dart'; +import '../utils.dart'; + +class WorkspaceListCommand extends PubCommand { + @override + String get description => + 'List all packages in the workspace, and their directory'; + + @override + String get name => 'list'; + + WorkspaceListCommand() { + argParser.addFlag( + 'json', + negatable: false, + help: 'output information in a json format', + ); + } + + @override + void runProtected() { + if (argResults.flag('json')) { + message( + const JsonEncoder.withIndent(' ').convert({ + 'packages': [ + ...entrypoint.workspaceRoot.transitiveWorkspace.map( + (package) => { + 'name': package.name, + 'path': p.canonicalize(package.dir), + }, + ), + ], + }), + ); + } else { + for (final line in renderTable( + [ + [format('Package', bold), format('Path', bold)], + for (final package in entrypoint.workspaceRoot.transitiveWorkspace) + [ + format(package.name, (x) => x), + format( + '${p.relative(p.absolute(package.dir))}${p.separator}', + (x) => x, + ), + ], + ], + canUseAnsiCodes, + )) { + message(line); + } + } + } +} diff --git a/lib/src/command_runner.dart b/lib/src/command_runner.dart index cb42f4761..22d4ade82 100644 --- a/lib/src/command_runner.dart +++ b/lib/src/command_runner.dart @@ -28,6 +28,7 @@ import 'command/unpack.dart'; import 'command/upgrade.dart'; import 'command/uploader.dart'; import 'command/version.dart'; +import 'command/workspace.dart'; import 'exit_codes.dart' as exit_codes; import 'git.dart' as git; import 'io.dart'; @@ -156,6 +157,7 @@ class PubCommandRunner extends CommandRunner implements PubTopLevel { addCommand(LoginCommand()); addCommand(LogoutCommand()); addCommand(VersionCommand()); + addCommand(WorkspaceCommand()); addCommand(TokenCommand()); } diff --git a/lib/src/log.dart b/lib/src/log.dart index 87ccb7cc8..988fba135 100644 --- a/lib/src/log.dart +++ b/lib/src/log.dart @@ -8,6 +8,7 @@ library; import 'dart:async'; import 'dart:convert'; import 'dart:io'; +import 'dart:math'; import 'package:args/command_runner.dart'; import 'package:path/path.dart' as p; @@ -635,3 +636,80 @@ class _JsonLogger { stdout.writeln(jsonEncode(message)); } } + +/// Represents a string and its highlighting separately, such that we can +/// compute the displayed length. +class FormattedString { + final String value; + + /// Should apply the ansi codes to present this string. + final String Function(String) _format; + + /// A prefix for marking this string if colors are not used. + final String _prefix; + + final String _suffix; + + FormattedString( + this.value, { + String Function(String)? format, + String? prefix, + String? suffix, + }) : _format = format ?? _noFormat, + _prefix = prefix ?? '', + _suffix = suffix ?? ''; + + String formatted({required bool useColors}) { + return useColors + ? _format(_prefix + value + _suffix) + : _prefix + value + _suffix; + } + + int computeLength({required bool? useColors}) { + return _prefix.length + value.length + _suffix.length; + } + + static String _noFormat(String x) => x; +} + +FormattedString format( + String value, + String Function(String) format, { + String? prefix = '', +}) => + FormattedString(value, format: format, prefix: prefix); + +/// Formats a table of [rows], inserting enough spaces to make columns line up. +List renderTable( + List> rows, + bool useColors, +) { + // Compute the width of each column by taking the max across all rows. + final columnWidths = {}; + for (var i = 0; i < rows.length; i++) { + if (rows[i].length > 1) { + for (var j = 0; j < rows[i].length; j++) { + final currentMaxWidth = columnWidths[j] ?? 0; + columnWidths[j] = max( + rows[i][j].computeLength(useColors: useColors), + currentMaxWidth, + ); + } + } + } + + final result = []; + for (final row in rows) { + final b = StringBuffer(); + for (var j = 0; j < row.length; j++) { + b.write(row[j].formatted(useColors: useColors)); + b.write( + ' ' * + ((columnWidths[j]! + 2) - + row[j].computeLength(useColors: useColors)), + ); + } + result.add(b.toString()); + } + return result; +} diff --git a/lib/src/pub_embeddable_command.dart b/lib/src/pub_embeddable_command.dart index e36bb4229..a2671fd31 100644 --- a/lib/src/pub_embeddable_command.dart +++ b/lib/src/pub_embeddable_command.dart @@ -21,6 +21,7 @@ import 'command/token.dart'; import 'command/unpack.dart'; import 'command/upgrade.dart'; import 'command/uploader.dart'; +import 'command/workspace.dart'; import 'log.dart' as log; import 'log.dart'; import 'utils.dart'; @@ -86,6 +87,7 @@ class PubEmbeddableCommand extends PubCommand implements PubTopLevel { addSubcommand(LoginCommand()); addSubcommand(LogoutCommand()); addSubcommand(TokenCommand()); + addSubcommand(WorkspaceCommand()); } @override diff --git a/test/testdata/goldens/embedding/embedding_test/--help.txt b/test/testdata/goldens/embedding/embedding_test/--help.txt index 16beaacbf..8b4c1ce32 100644 --- a/test/testdata/goldens/embedding/embedding_test/--help.txt +++ b/test/testdata/goldens/embedding/embedding_test/--help.txt @@ -29,6 +29,7 @@ Available subcommands: token Manage authentication tokens for hosted pub repositories. unpack Downloads a package and unpacks it in place. upgrade Upgrade the current package's dependencies to latest versions. + workspace Work with the pub workspace. Run "pub_command_runner help" to see global options. See https://dart.dev/tools/pub/cmd/pub-global for detailed documentation. diff --git a/test/testdata/goldens/help_test/pub workspace --help.txt b/test/testdata/goldens/help_test/pub workspace --help.txt new file mode 100644 index 000000000..af1f72f97 --- /dev/null +++ b/test/testdata/goldens/help_test/pub workspace --help.txt @@ -0,0 +1,14 @@ +# GENERATED BY: test/help_test.dart + +## Section 0 +$ pub workspace --help +Work with the pub workspace. + +Usage: pub workspace [arguments...] +-h, --help Print this usage information. + +Available subcommands: + list List all packages in the workspace, and their directory + +Run "pub help" to see global options. + diff --git a/test/testdata/goldens/help_test/pub workspace list --help.txt b/test/testdata/goldens/help_test/pub workspace list --help.txt new file mode 100644 index 000000000..cc943f945 --- /dev/null +++ b/test/testdata/goldens/help_test/pub workspace list --help.txt @@ -0,0 +1,12 @@ +# GENERATED BY: test/help_test.dart + +## Section 0 +$ pub workspace list --help +List all packages in the workspace, and their directory + +Usage: pub workspace list [arguments...] +-h, --help Print this usage information. + --json output information in a json format + +Run "pub help" to see global options. + diff --git a/test/workspace_test.dart b/test/workspace_test.dart index 7bd9db903..6565ad24d 100644 --- a/test/workspace_test.dart +++ b/test/workspace_test.dart @@ -1416,6 +1416,82 @@ Consider removing one of the overrides.''', ), ); }); + + test('workspace list', () async { + await dir(appPath, [ + libPubspec( + 'myapp', + '1.2.3', + extras: { + 'workspace': ['pkgs/a'], + }, + sdk: '^3.5.0', + ), + dir('pkgs', [ + dir('a', [ + libPubspec( + 'a', + '1.1.1', + resolutionWorkspace: true, + extras: { + 'workspace': ['b'], + }, + ), + dir('b', [ + libPubspec( + 'b', + '1.2.2', + resolutionWorkspace: true, + ), + ]), + ]), + ]), + ]).create(); + final s = p.separator; + await runPub( + args: ['workspace', 'list'], + environment: {'_PUB_TEST_SDK_VERSION': '3.5.0'}, + output: ''' +Package Path +myapp .$s +a pkgs${s}a$s +b pkgs${s}a${s}b$s +''', + ); + await runPub( + args: ['workspace', 'list'], + environment: {'_PUB_TEST_SDK_VERSION': '3.5.0'}, + workingDirectory: p.join(sandbox, appPath, 'pkgs'), + output: ''' +Package Path +myapp ..$s +a a$s +b a${s}b$s +''', + ); + await runPub( + args: ['workspace', 'list', '--json'], + environment: {'_PUB_TEST_SDK_VERSION': '3.5.0'}, + output: ''' +{ + "packages": [ + { + "name": "myapp", + "path": "${p.join(sandbox, appPath)}" + }, + { + "name": "a", + "path": "${p.join(sandbox, appPath, 'pkgs', 'a')}" + }, + { + "name": "b", + "path": "${p.join(sandbox, appPath, 'pkgs', 'a', 'b')}" + } + ] +} +''', + ); + }); } final s = p.separator;