diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..b03490d3 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +.pub +packages +pubspec.lock \ No newline at end of file diff --git a/README.md b/README.md index 8c631e87..fa0f021d 100644 --- a/README.md +++ b/README.md @@ -1 +1,242 @@ # Dart Dev Tools + +> Centralized tooling for Dart projects. Consistent interface across projects. Easily configurable. + +- [**Motivation**](#motivation) +- [**Supported Tasks**](#supported-tasks) +- [**Getting Started**](#getting-started) +- [**Project Configuration**](#project-configuration) +- [**CLI Usage**](#cli-usage) +- [**Programmatic Usage**](#programmatic-usage) + +## Motivation + +All Dart (https://dartlang.org) projects eventually share a common set of development requirements: + +- Tests (unit, integration, and functional) +- Consistent code formatting +- Static analysis to detect issues +- Examples for manual testing/exploration + +Together, the Dart SDK and a couple of packages from the Dart team supply the necessary tooling to support the above +requirements. But, the usage is inconsistent, configuration is limited to command-line arguments, and you inevitably end +up with a slew of shell scripts in the `tool/` directory. While this works, it lacks a consistent usage pattern across +multiple projects and requires an unnecessary amount of error-prone work to set up. + +This package improves on the above process by providing a number of benefits: + +#### Centralized Tooling +By housing the APIs and CLIs for these various dev workflows in a single location, you no longer have to worry about +keeping scripts in parity across multiple projects. Simply add the `dart_dev` package as a dependency, and you're ready +to go. + +#### Versioned Tooling +Any breaking changes to the APIs or CLIs within this package will be reflected by an appropriate version bump according +to semver. You can safely upgrade your tooling to take advantage of continuous improvements and new features with +minimal maintenance. + +#### Separation of Concerns +Every task supported in `dart_dev` is separated into three pieces: + +1. API - programmatic execution via Dart code. +2. CLI - script-based execution via the `dart_dev` executable. +3. Configuration - singleton configuration instances for simple per-project configuration. + +#### Consistent Interface +By providing a single executable (`dart_dev`) that supports multiple tasks with standardized options, project developers +have a consistent interface for development across all projects that utilize this package. Configuration is handled on a +per-project basis via a single Dart file, meaning that you don't have to know anything about a project to run tests or +static analysis - you just need to know how to use the `dart_dev` tool. + + +> **Note:** This is __not__ a replacement for the tooling provided by the Dart SDK and packages like `test` or +`dart_style`. Rather, `dart_dev` is a unified interface for interacting with said tooling in a simplified manner. + + +## Supported Tasks + +- **Tests:** runs test suites (unit, integration, and functional) via the [`test` package test runner](https://github.com/dart-lang/test). +- **Code Formatting:** runs the [`dartfmt` tool from the `dart_style` package](https://github.com/dart-lang/dart_style) over source code. +- **Static Analysis:** runs the [`dartanalyzer`](https://www.dartlang.org/tools/analyzer/) over source code. +- **Serving Examples:** uses [`pub serve`](https://www.dartlang.org/tools/pub/cmd/pub-serve.html) to serve the project examples. + + +## Getting Started + +###### Install `dart_dev` +Add the following to your `pubspec.yaml`: +```yaml +dev_dependencies: + dart_dev: any +``` + +###### Create an Alias (optional) +Add the following to your bash or zsh profile for convenience: +``` +alias ddev='pub run dart_dev' +``` + +###### Configuration +In order to configure `dart_dev` for a specific project, run `ddev init` or `pub run dart_dev init` to generate the +configuration file. This should create a `tool/dev.dart` file where each task can be configured as needed. + +```dart +import 'package:dart_dev/dart_dev.dart'; + +main(args) async { + // Define the entry points for static analysis. + config.analyze.entryPoints = ['lib/', 'test/', 'tool/']; + + // Configure the port on which examples should be served. + config.examples.port = 9000; + + // Define the directories to include when running the + // Dart formatter. + config.format.directories = ['lib/', 'test/', 'tool/']; + + // Define the location of your test suites. + config.test + ..unitTests = ['test/unit/'] + ..integrationTests = ['test/integration/']; + + // Execute the dart_dev tooling. + await dev(args); +} +``` + +[Full list of configuration options](#project-configuration). + + +###### Try It Out +The tooling in `dart_dev` works out of the box with happy defaults for each task. Run `ddev` or `pub run dart_dev` to +see the help usage. Try it out by running any of the following tasks: + +``` +# with the alias +ddev analyze +ddev examples +ddev format +ddev test + +# without the alias +pub run dart_dev analyze +pub run dart_dev examples +pub run dart_dev format +pub run dart_dev test +``` + +Add the `-h` flag to any of the above commands to receive additional help information specific to that task. + + +## Project Configuration +Project configuration occurs in the `tool/dev.dart` file where the `config` instance is imported from the `dart_dev` +package. The bare minimum for this file is: + +```dart +import 'package:dart_dev/dart_dev.dart'; + +main(args) async { + // Available config objects: + // config.analyze + // config.examples + // config.format + // config.init + // config.test + + await dev(args); +} +``` + +### `analyze` Config +All configuration options for the `analyze` task are found on the `config.analyze` object. + +Name | Type | Default | Description +--------------- | -------------- | ---------- | ----------- +`entryPoints` | `List` | `['lib/']` | Entry points to analyze. Items in this list can be directories and/or files. Directories will be expanded (depth=1) to find Dart files. +`fatalWarnings` | `bool` | `true` | Treat non-type warnings as fatal. +`hints` | `bool` | `true` | Show hint results. + +### `examples` Config +All configuration options for the `examples` task are found on the `config.examples` object. + +Name | Type | Default | Description +---------- | -------- | ------------- | ----------- +`hostname` | `String` | `'localhost'` | The host name to listen on. +`port` | `int` | `8080` | The base port to listen on. + +### `format` Config +All configuration options for the `format` task are found on the `config.format` object. + +Name | Type | Default | Description +------------- | -------------- | ----------- | ----------- +`check` | `bool` | `false` | Dry-run; checks if formatter needs to be run and sets exit code accordingly. +`directories` | `List` | `['lib/']` | Directories to run the formatter on. All files (any depth) in the given directories will be formatted. + +### `test` Config +All configuration options for the `test` task are found on the `config.test` object. + +Name | Type | Default | Description +------------------ | -------------- | ----------- | ----------- +`integrationTests` | `List` | `[]` | Integration test locations. Items in this list can be directories and/or files. +`platforms` | `List` | `[]` | Platforms on which to run the tests (handled by the Dart test runner). See https://github.com/dart-lang/test#platform-selector-syntax for a full list of supported platforms. +`unitTests` | `List` | `['test/']` | Unit test locations. Items in this list can be directories and/or files. + + +## CLI Usage +This package comes with a single executable: `dart_dev`. To run this executable: `ddev` or `pub run dart_dev`. This +usage will simply display the usage help text along with a list of supported tasks: + +``` +$ ddev +Standardized tooling for Dart projects. + +Usage: pub run dart_dev [task] [options] + + --[no-]color Colorize the output. + (defaults to on) + +-h, --help Shows this usage. +-q, --quiet Minimizes the logging output. + --version Shows the dart_dev package version. + +Supported tasks: + + analyze + examples + format + init + test +``` + +- Static analysis: `ddev analyze` +- Serving examples: `ddev examples` +- Dart formatter: `ddev format` +- Initialization: `ddev init` +- Tests: `ddev test` + +Add the `-h` flag to any of the above commands to see task-specific flags and options. + +> Any project configuration defined in the `tool/dev.dart` file should be reflected in the execution of the above +commands. CLI flags and options will override said configuration. + + +## Programmatic Usage +The tooling facilitated by this package can also be executed via a programmatic Dart API: + +```dart +import 'package:dart_dev/api.dart' as api; + +main() async { + await api.analyze(); + await api.serveExamples(); + await api.format(); + await api.init(); + await api.test(); +} +``` + +Check out the source of these API methods for additional documentation. + +> In order to provide a clean API, these methods do not leverage the configuration instances that the command-line +interfaces do. Because of this, the default usage may be different. You can access said configurations from the main +`package:dart_dev/dart_dev.dart` import. \ No newline at end of file diff --git a/bin/dart_dev.dart b/bin/dart_dev.dart new file mode 100644 index 00000000..7901f680 --- /dev/null +++ b/bin/dart_dev.dart @@ -0,0 +1,22 @@ +library dart_dev.bin.dart_dev; + +import 'dart:io'; + +import 'package:dart_dev/dart_dev.dart' show dev; +import 'package:dart_dev/process.dart' show TaskProcess; + +main(List args) async { + File devFile = new File('./tool/dev.dart'); + + if (devFile.existsSync()) { + // If dev.dart exists, run that to allow configuration. + var newArgs = [devFile.path]..addAll(args); + TaskProcess process = new TaskProcess('dart', newArgs); + process.stdout.listen(stdout.writeln); + process.stderr.listen(stderr.writeln); + await process.done; + } else { + // Otherwise, run with defaults. + await dev(args); + } +} diff --git a/lib/api.dart b/lib/api.dart new file mode 100644 index 00000000..b0effcc4 --- /dev/null +++ b/lib/api.dart @@ -0,0 +1,8 @@ +library dart_dev.api; + +export 'package:dart_dev/src/tasks/analyze/api.dart' show AnalyzeTask, analyze; +export 'package:dart_dev/src/tasks/examples/api.dart' + show ExamplesTask, serveExamples; +export 'package:dart_dev/src/tasks/format/api.dart' show FormatTask, format; +export 'package:dart_dev/src/tasks/init/api.dart' show InitTask, init; +export 'package:dart_dev/src/tasks/test/api.dart' show TestTask, test; diff --git a/lib/dart_dev.dart b/lib/dart_dev.dart new file mode 100644 index 00000000..0135562d --- /dev/null +++ b/lib/dart_dev.dart @@ -0,0 +1,6 @@ +library dart_dev; + +export 'package:dart_dev/src/dart_dev_cli.dart' show registerTask, dev; +export 'package:dart_dev/src/tasks/config.dart' show config, TaskConfig; +export 'package:dart_dev/src/tasks/cli.dart' show CliResult, TaskCli; +export 'package:dart_dev/src/tasks/task.dart' show Task; diff --git a/lib/io.dart b/lib/io.dart new file mode 100644 index 00000000..1b50bf4a --- /dev/null +++ b/lib/io.dart @@ -0,0 +1,4 @@ +library dart_dev.io; + +export 'package:dart_dev/src/io.dart' + show parseArgsFromCommand, parseExecutableFromCommand, Reporter, reporter; diff --git a/lib/process.dart b/lib/process.dart new file mode 100644 index 00000000..ff5ae83a --- /dev/null +++ b/lib/process.dart @@ -0,0 +1,3 @@ +library dart_dev.process; + +export 'package:dart_dev/src/task_process.dart' show TaskProcess; diff --git a/lib/src/dart_dev_cli.dart b/lib/src/dart_dev_cli.dart new file mode 100644 index 00000000..22175f0c --- /dev/null +++ b/lib/src/dart_dev_cli.dart @@ -0,0 +1,133 @@ +library dart_dev.src.cli; + +import 'dart:async'; +import 'dart:io'; + +import 'package:args/args.dart'; + +import 'package:dart_dev/io.dart' + show parseArgsFromCommand, parseExecutableFromCommand, reporter; +import 'package:dart_dev/process.dart'; + +import 'package:dart_dev/src/tasks/cli.dart'; +import 'package:dart_dev/src/tasks/config.dart'; + +import 'package:dart_dev/src/tasks/analyze/cli.dart'; +import 'package:dart_dev/src/tasks/examples/cli.dart'; +import 'package:dart_dev/src/tasks/format/cli.dart'; +import 'package:dart_dev/src/tasks/init/cli.dart'; +import 'package:dart_dev/src/tasks/test/cli.dart'; + +import 'package:dart_dev/src/version.dart' show printVersion; + +ArgParser _parser = new ArgParser(allowTrailingOptions: true) + ..addFlag('color', defaultsTo: true, help: 'Colorize the output.') + ..addFlag('help', abbr: 'h', negatable: false, help: 'Shows this usage.') + ..addFlag('quiet', + abbr: 'q', negatable: false, help: 'Minimizes the logging output.') + ..addFlag('version', + negatable: false, help: 'Shows the dart_dev package version.'); + +Map _cliTasks = {}; +Map _cliConfigs = {}; + +String _topLevelUsage = _parser.usage; + +dev(List args) async { + registerTask(new AnalyzeCli(), config.analyze); + registerTask(new ExamplesCli(), config.examples); + registerTask(new FormatCli(), config.format); + registerTask(new InitCli(), config.init); + registerTask(new TestCli(), config.test); + + await _run(args); +} + +void registerTask(TaskCli cli, TaskConfig config) { + _cliTasks[cli.command] = cli; + _cliConfigs[cli.command] = config; +} + +String _generateUsage([String task]) { + StringBuffer u = new StringBuffer(); + u.writeln('Standardized tooling for Dart projects.'); + u.writeln(); + + if (task != null && _cliTasks.containsKey(task)) { + u.writeln('Usage: pub run dart_dev $task [options]'); + u.writeln(); + u.writeln(_cliTasks[task].argParser.usage); + } else { + u.writeln('Usage: pub run dart_dev [task] [options]'); + u.writeln(); + u.writeln(_topLevelUsage); + u.writeln(); + u.writeln('Supported tasks:'); + u.writeln(); + u.writeln(' ${_cliTasks.keys.join('\n ')}'); + } + + return u.toString(); +} + +Future _run(List args) async { + _cliTasks.forEach((command, cli) { + _parser.addCommand(command, cli.argParser); + }); + + ArgResults env = _parser.parse(args); + String task; + if (env.command != null) { + task = env.command.name; + } + + reporter + ..color = env['color'] + ..quiet = env['quiet']; + + if (env['version']) { + if (!printVersion()) { + reporter.error('Couldn\'t find version number.', shout: true); + exitCode = 1; + } + return; + } + + if (task != null && !_cliTasks.containsKey(task)) { + reporter.error('Invalid task: $task', shout: true); + reporter.log(_generateUsage(), shout: true); + exitCode = 1; + return; + } + + if (env['help'] || task == null) { + reporter.log(_generateUsage(task), shout: true); + return; + } + + TaskConfig config = _cliConfigs[task]; + await _runAll(config.before); + CliResult result = await _cliTasks[task].run(env.command); + await _runAll(config.after); + + reporter.log(''); + if (result.successful) { + reporter.success(result.message, shout: true); + } else { + reporter.error(result.message, shout: true); + } +} + +Future _runAll(List tasks) async { + for (int i = 0; i < tasks.length; i++) { + if (tasks[i] is Function) { + await tasks[i](); + } else if (tasks[i] is String) { + TaskProcess process = new TaskProcess( + parseExecutableFromCommand(tasks[i]), parseArgsFromCommand(tasks[i])); + reporter.logGroup(tasks[i], + outputStream: process.stdout, errorStream: process.stderr); + await process.done; + } + } +} diff --git a/lib/src/io.dart b/lib/src/io.dart new file mode 100644 index 00000000..c393a813 --- /dev/null +++ b/lib/src/io.dart @@ -0,0 +1,83 @@ +library dart_dev.src.io; + +import 'dart:async'; +import 'dart:io'; + +import 'package:ansicolor/ansicolor.dart'; + +Reporter reporter = new Reporter(); + +final AnsiPen _blue = new AnsiPen()..cyan(); +final AnsiPen _green = new AnsiPen()..green(); +final AnsiPen _red = new AnsiPen()..red(); +final AnsiPen _yellow = new AnsiPen()..yellow(); + +String parseExecutableFromCommand(String command) { + return command.split(' ').first; +} + +List parseArgsFromCommand(String command) { + var parts = command.split(' '); + if (parts.length <= 1) return []; + return parts.getRange(1, parts.length).toList(); +} + +class Reporter { + bool color = true; + bool quiet = false; + + Reporter({bool this.color, bool this.quiet}); + + String colorBlue(String message) => _color(_blue, message); + + String colorGreen(String message) => _color(_green, message); + + String colorRed(String message) => _color(_red, message); + + String colorYellow(String message) => _color(_yellow, message); + + void log(String message, {bool shout: false}) { + _log(stdout, message, shout: shout); + } + + void logGroup(String title, + {String output, + Stream outputStream, + Stream errorStream}) { + log(colorBlue('\n::: $title')); + if (output != null) { + log('${output.split('\n').join('\n ')}'); + return; + } + + if (outputStream != null) { + outputStream.listen((line) { + log(' $line'); + }); + } + if (errorStream != null) { + errorStream.listen((line) { + warning(' $line'); + }); + } + } + + void error(String message, {bool shout: false}) { + _log(stderr, colorRed(message), shout: shout); + } + + void success(String message, {bool shout: false}) { + log(colorGreen(message), shout: shout); + } + + void warning(String message, {bool shout: false}) { + _log(stderr, colorYellow(message), shout: shout); + } + + String _color(AnsiPen pen, String message) => color ? pen(message) : message; + + void _log(IOSink sink, String message, {bool shout: false}) { + if (quiet && !shout) return; + sink.writeln(message); + } +} diff --git a/lib/src/task_process.dart b/lib/src/task_process.dart new file mode 100644 index 00000000..1018a408 --- /dev/null +++ b/lib/src/task_process.dart @@ -0,0 +1,40 @@ +library dart_dev.src.task_process; + +import 'dart:async'; +import 'dart:convert'; +import 'dart:io'; + +class TaskProcess { + Completer _donec = new Completer(); + Completer _errc = new Completer(); + Completer _outc = new Completer(); + Completer _procExitCode = new Completer(); + + StreamController _stdout = new StreamController(); + StreamController _stderr = new StreamController(); + + TaskProcess(String executable, List arguments) { + Process.start(executable, arguments).then((process) { + process.stdout + .transform(UTF8.decoder) + .transform(new LineSplitter()) + .listen(_stdout.add, onDone: _outc.complete); + process.stderr + .transform(UTF8.decoder) + .transform(new LineSplitter()) + .listen(_stderr.add, onDone: _errc.complete); + _outc.future.then((_) => _stdout.close()); + _errc.future.then((_) => _stderr.close()); + process.exitCode.then(_procExitCode.complete); + Future.wait([_outc.future, _errc.future, process.exitCode]) + .then((_) => _donec.complete()); + }); + } + + Future get done => _donec.future; + + Future get exitCode => _procExitCode.future; + + Stream get stderr => _stderr.stream; + Stream get stdout => _stdout.stream; +} diff --git a/lib/src/tasks/analyze/api.dart b/lib/src/tasks/analyze/api.dart new file mode 100644 index 00000000..5d712d4c --- /dev/null +++ b/lib/src/tasks/analyze/api.dart @@ -0,0 +1,64 @@ +library dart_dev.src.tasks.analyze.api; + +import 'dart:async'; +import 'dart:io'; + +import 'package:dart_dev/process.dart'; + +import 'package:dart_dev/src/tasks/analyze/config.dart'; +import 'package:dart_dev/src/tasks/task.dart'; + +AnalyzeTask analyze( + {List entryPoints: defaultEntryPoints, + bool fatalWarnings: defaultFatalWarnings, + bool hints: defaultHints}) { + var executable = 'dartanalyzer'; + var args = []; + if (fatalWarnings) { + args.add('--fatal-warnings'); + } + if (!hints) { + args.add('--no-hints'); + } + args.addAll(_findFilesFromEntryPoints(entryPoints)); + + TaskProcess process = new TaskProcess(executable, args); + AnalyzeTask task = + new AnalyzeTask('$executable ${args.join(' ')}', process.done); + + process.stdout.listen(task._analyzerOutput.add); + process.stderr.listen(task._analyzerOutput.addError); + process.exitCode.then((code) { + task.successful = code <= 0; + }); + + return task; +} + +List _findFilesFromEntryPoints(List entryPoints) { + List files = []; + entryPoints.forEach((p) { + if (FileSystemEntity.isDirectorySync(p)) { + Directory dir = new Directory(p); + List entities = dir.listSync(); + files.addAll(entities + .where((e) => + FileSystemEntity.isFileSync(e.path) && e.path.endsWith('.dart')) + .map((e) => e.path)); + } else if (FileSystemEntity.isFileSync(p) && p.endsWith('.dart')) { + files.add(p); + } else { + throw new ArgumentError('Entry point does not exist: $p'); + } + }); + return files; +} + +class AnalyzeTask extends Task { + final String analyzerCommand; + final Future done; + + StreamController _analyzerOutput = new StreamController(); + Stream get analyzerOutput => _analyzerOutput.stream; + AnalyzeTask(String this.analyzerCommand, Future this.done); +} diff --git a/lib/src/tasks/analyze/cli.dart b/lib/src/tasks/analyze/cli.dart new file mode 100644 index 00000000..48afebae --- /dev/null +++ b/lib/src/tasks/analyze/cli.dart @@ -0,0 +1,39 @@ +library dart_dev.src.tasks.analyze.cli; + +import 'dart:async'; + +import 'package:args/args.dart'; + +import 'package:dart_dev/io.dart' show reporter; + +import 'package:dart_dev/src/tasks/analyze/api.dart'; +import 'package:dart_dev/src/tasks/analyze/config.dart'; +import 'package:dart_dev/src/tasks/cli.dart'; +import 'package:dart_dev/src/tasks/config.dart'; + +class AnalyzeCli extends TaskCli { + final ArgParser argParser = new ArgParser() + ..addFlag('fatal-warnings', + defaultsTo: defaultFatalWarnings, + negatable: true, + help: 'Treat non-type warnings as fatal.') + ..addFlag('hints', + defaultsTo: defaultHints, negatable: true, help: 'Show hint results.'); + + final String command = 'analyze'; + + Future run(ArgResults parsedArgs) async { + List entryPoints = config.analyze.entryPoints; + bool fatalWarnings = TaskCli.valueOf( + 'fatal-warnings', parsedArgs, config.analyze.fatalWarnings); + bool hints = TaskCli.valueOf('hints', parsedArgs, config.analyze.hints); + + AnalyzeTask task = analyze( + entryPoints: entryPoints, fatalWarnings: fatalWarnings, hints: hints); + reporter.logGroup(task.analyzerCommand, outputStream: task.analyzerOutput); + await task.done; + return task.successful + ? new CliResult.success('Analysis completed.') + : new CliResult.fail('Analysis failed.'); + } +} diff --git a/lib/src/tasks/analyze/config.dart b/lib/src/tasks/analyze/config.dart new file mode 100644 index 00000000..0d022100 --- /dev/null +++ b/lib/src/tasks/analyze/config.dart @@ -0,0 +1,13 @@ +library dart_dev.src.tasks.analyze.config; + +import 'package:dart_dev/src/tasks/config.dart'; + +const List defaultEntryPoints = const ['lib/']; +const bool defaultFatalWarnings = true; +const bool defaultHints = true; + +class AnalyzeConfig extends TaskConfig { + List entryPoints = defaultEntryPoints.toList(); + bool fatalWarnings = defaultFatalWarnings; + bool hints = defaultHints; +} diff --git a/lib/src/tasks/cli.dart b/lib/src/tasks/cli.dart new file mode 100644 index 00000000..d4173fa8 --- /dev/null +++ b/lib/src/tasks/cli.dart @@ -0,0 +1,22 @@ +library dart_dev.src.tasks.cli; + +import 'dart:async'; + +import 'package:args/args.dart'; + +class CliResult { + final String message; + final bool successful; + CliResult.success([String this.message = '']) : successful = true; + CliResult.fail([String this.message = '']) : successful = false; +} + +abstract class TaskCli { + static valueOf(String arg, ArgResults parsedArgs, dynamic fallback) => + parsedArgs.wasParsed(arg) ? parsedArgs[arg] : fallback; + + ArgParser get argParser; + String get command; + + Future run(ArgResults parsedArgs); +} diff --git a/lib/src/tasks/config.dart b/lib/src/tasks/config.dart new file mode 100644 index 00000000..31984fb9 --- /dev/null +++ b/lib/src/tasks/config.dart @@ -0,0 +1,22 @@ +library dart_dev.src.tasks.config; + +import 'package:dart_dev/src/tasks/analyze/config.dart'; +import 'package:dart_dev/src/tasks/examples/config.dart'; +import 'package:dart_dev/src/tasks/format/config.dart'; +import 'package:dart_dev/src/tasks/init/config.dart'; +import 'package:dart_dev/src/tasks/test/config.dart'; + +Config config = new Config(); + +class Config { + AnalyzeConfig analyze = new AnalyzeConfig(); + ExamplesConfig examples = new ExamplesConfig(); + FormatConfig format = new FormatConfig(); + InitConfig init = new InitConfig(); + TestConfig test = new TestConfig(); +} + +class TaskConfig { + List after = []; + List before = []; +} diff --git a/lib/src/tasks/examples/api.dart b/lib/src/tasks/examples/api.dart new file mode 100644 index 00000000..40531581 --- /dev/null +++ b/lib/src/tasks/examples/api.dart @@ -0,0 +1,57 @@ +library dart_dev.src.tasks.examples.api; + +import 'dart:async'; + +import 'package:dart_dev/process.dart'; + +import 'package:dart_dev/src/tasks/examples/config.dart'; +import 'package:dart_dev/src/tasks/task.dart'; + +ExamplesTask serveExamples( + {String hostname: defaultHostname, int port: defaultPort}) { + var dartiumExecutable = 'dartium'; + var dartiumArgs = ['http://$hostname:$port']; + + var pubServeExecutable = 'pub'; + var pubServeArgs = [ + 'serve', + '--hostname=$hostname', + '--port=$port', + 'example' + ]; + + TaskProcess pubServeProcess = + new TaskProcess(pubServeExecutable, pubServeArgs); + TaskProcess dartiumProcess = new TaskProcess(dartiumExecutable, dartiumArgs); + + ExamplesTask task = new ExamplesTask( + '$dartiumExecutable ${dartiumArgs.join(' ')}', + '$pubServeExecutable ${pubServeArgs.join(' ')}', + Future.wait([dartiumProcess.done, pubServeProcess.done])); + + pubServeProcess.stdout.listen(task._pubServeOutput.add); + pubServeProcess.stderr.listen(task._pubServeOutput.addError); + pubServeProcess.exitCode.then((code) { + task.successful = code <= 0; + }); + + dartiumProcess.stdout.listen(task._dartiumOutput.add); + dartiumProcess.stderr.listen(task._dartiumOutput.addError); + + return task; +} + +class ExamplesTask extends Task { + final Future done; + final String dartiumCommand; + final String pubServeCommand; + + StreamController _dartiumOutput = new StreamController(); + StreamController _pubServeOutput = new StreamController(); + + ExamplesTask(String this.dartiumCommand, String this.pubServeCommand, + Future this.done); + + Stream get dartiumOutput => _dartiumOutput.stream; + Stream get pubServeOutput => _pubServeOutput.stream; +} diff --git a/lib/src/tasks/examples/cli.dart b/lib/src/tasks/examples/cli.dart new file mode 100644 index 00000000..2909b491 --- /dev/null +++ b/lib/src/tasks/examples/cli.dart @@ -0,0 +1,38 @@ +library dart_dev.src.tasks.examples.cli; + +import 'dart:async'; + +import 'package:args/args.dart'; + +import 'package:dart_dev/io.dart' show reporter; + +import 'package:dart_dev/src/tasks/examples/api.dart'; +import 'package:dart_dev/src/tasks/examples/config.dart'; +import 'package:dart_dev/src/tasks/cli.dart'; +import 'package:dart_dev/src/tasks/config.dart'; + +class ExamplesCli extends TaskCli { + final ArgParser argParser = new ArgParser() + ..addOption('hostname', + defaultsTo: defaultHostname, help: 'The host name to listen on.') + ..addOption('port', + defaultsTo: defaultPort.toString(), + help: 'The base port to listen on.'); + + final String command = 'examples'; + + Future run(ArgResults parsedArgs) async { + String hostname = + TaskCli.valueOf('hostname', parsedArgs, config.examples.hostname); + var port = TaskCli.valueOf('port', parsedArgs, config.examples.port); + if (port is String) { + port = int.parse(port); + } + + ExamplesTask task = serveExamples(hostname: hostname, port: port); + reporter.logGroup(task.pubServeCommand, outputStream: task.pubServeOutput); + await task.done; + reporter.logGroup(task.dartiumCommand, outputStream: task.dartiumOutput); + return task.successful ? new CliResult.success() : new CliResult.fail(); + } +} diff --git a/lib/src/tasks/examples/config.dart b/lib/src/tasks/examples/config.dart new file mode 100644 index 00000000..9a3a6546 --- /dev/null +++ b/lib/src/tasks/examples/config.dart @@ -0,0 +1,11 @@ +library dart_dev.src.tasks.examples.config; + +import 'package:dart_dev/src/tasks/config.dart'; + +const String defaultHostname = 'localhost'; +const int defaultPort = 8080; + +class ExamplesConfig extends TaskConfig { + String hostname = defaultHostname; + int port = defaultPort; +} diff --git a/lib/src/tasks/format/api.dart b/lib/src/tasks/format/api.dart new file mode 100644 index 00000000..1ace7c44 --- /dev/null +++ b/lib/src/tasks/format/api.dart @@ -0,0 +1,68 @@ +library dart_dev.src.tasks.format.api; + +import 'dart:async'; + +import 'package:dart_dev/process.dart'; + +import 'package:dart_dev/src/tasks/format/config.dart'; +import 'package:dart_dev/src/tasks/task.dart'; + +FormatTask format( + {bool check: defaultCheck, List directories: defaultDirectories}) { + var executable = 'pub'; + var args = ['run', 'dart_style:format']; + + if (check) { + args.add('-n'); + } else { + args.add('-w'); + } + + args.addAll(directories); + + TaskProcess process = new TaskProcess(executable, args); + FormatTask task = new FormatTask( + '$executable ${args.join(' ')}', process.done)..isDryRun = check; + + RegExp cwdPattern = new RegExp('Formatting directory (.+):'); + RegExp formattedPattern = new RegExp('Formatted (.+\.dart)'); + RegExp unchangedPattern = new RegExp('Unchanged (.+\.dart)'); + + String cwd = ''; + process.stdout.listen((line) { + if (check) { + task.affectedFiles.add(line.trim()); + } else { + if (cwdPattern.hasMatch(line)) { + cwd = cwdPattern.firstMatch(line).group(1); + } else if (formattedPattern.hasMatch(line)) { + task.affectedFiles + .add('$cwd${formattedPattern.firstMatch(line).group(1)}'); + } else if (unchangedPattern.hasMatch(line)) { + task.unaffectedFiles + .add('$cwd${unchangedPattern.firstMatch(line).group(1)}'); + } + } + task._formatterOutput.add(line); + }); + process.stderr.listen(task._formatterOutput.addError); + process.exitCode.then((code) { + task.successful = check ? task.affectedFiles.isEmpty : code <= 0; + }); + + return task; +} + +class FormatTask extends Task { + List affectedFiles = []; + final Future done; + final String formatterCommand; + bool isDryRun; + List unaffectedFiles = []; + + StreamController _formatterOutput = new StreamController(); + + FormatTask(String this.formatterCommand, Future this.done); + + Stream get formatterOutput => _formatterOutput.stream; +} diff --git a/lib/src/tasks/format/cli.dart b/lib/src/tasks/format/cli.dart new file mode 100644 index 00000000..d78aba1a --- /dev/null +++ b/lib/src/tasks/format/cli.dart @@ -0,0 +1,50 @@ +library dart_dev.src.tasks.format.cli; + +import 'dart:async'; + +import 'package:args/args.dart'; + +import 'package:dart_dev/io.dart' show reporter; + +import 'package:dart_dev/src/tasks/format/api.dart'; +import 'package:dart_dev/src/tasks/format/config.dart'; +import 'package:dart_dev/src/tasks/cli.dart'; +import 'package:dart_dev/src/tasks/config.dart'; + +class FormatCli extends TaskCli { + final ArgParser argParser = new ArgParser() + ..addFlag('check', + defaultsTo: defaultCheck, + negatable: false, + help: + 'Dry-run; checks if formatter needs to be run and sets exit code accordingly.'); + + final String command = 'format'; + + Future run(ArgResults parsedArgs) async { + bool check = TaskCli.valueOf('check', parsedArgs, config.format.check); + List directories = config.format.directories; + + FormatTask task = format(check: check, directories: directories); + reporter.logGroup(task.formatterCommand, + outputStream: task.formatterOutput); + await task.done; + + if (task.isDryRun) { + if (task.successful) return new CliResult.success( + 'You\'re Dart code is good to go!'); + if (task.affectedFiles.isEmpty) return new CliResult.fail( + 'The Dart formatter needs to be run.'); + return new CliResult.fail( + 'The Dart formatter needs to be run. The following files require changes:\n ' + + task.affectedFiles.join('\n ')); + } else { + if (!task.successful) return new CliResult.fail('Dart formatter failed.'); + if (task.affectedFiles.isEmpty) return new CliResult.success( + 'Success! All files are already formatted correctly.'); + return new CliResult.success( + 'Success! The following files were formatted:\n ' + + task.affectedFiles.join('\n ')); + } + } +} diff --git a/lib/src/tasks/format/config.dart b/lib/src/tasks/format/config.dart new file mode 100644 index 00000000..62a25651 --- /dev/null +++ b/lib/src/tasks/format/config.dart @@ -0,0 +1,11 @@ +library dart_dev.src.tasks.format.config; + +import 'package:dart_dev/src/tasks/config.dart'; + +const bool defaultCheck = false; +const List defaultDirectories = const ['lib/']; + +class FormatConfig extends TaskConfig { + bool check = defaultCheck; + List directories = defaultDirectories; +} diff --git a/lib/src/tasks/init/api.dart b/lib/src/tasks/init/api.dart new file mode 100644 index 00000000..3caf74bf --- /dev/null +++ b/lib/src/tasks/init/api.dart @@ -0,0 +1,44 @@ +library dart_dev.src.tasks.init.api; + +import 'dart:async'; +import 'dart:io'; + +import 'package:dart_dev/src/tasks/task.dart'; + +const String _initialConfig = '''library tool.dev; + +import 'package:dart_dev/dart_dev.dart' show dev, config; + +main(List args) async { + // Perform task configuration here as necessary. + + // Available task configurations: + // config.analyze + // config.examples + // config.format + // config.test + + await dev(args); +} +'''; + +InitTask init() { + InitTask task = new InitTask(); + + File configFile = new File('tool/dev.dart'); + if (configFile.existsSync()) { + task.successful = false; + return task; + } + + configFile.createSync(recursive: true); + configFile.writeAsStringSync(_initialConfig); + task.successful = true; + + return task; +} + +class InitTask extends Task { + final Future done = new Future.value(); + InitTask(); +} diff --git a/lib/src/tasks/init/cli.dart b/lib/src/tasks/init/cli.dart new file mode 100644 index 00000000..1b1bb2a2 --- /dev/null +++ b/lib/src/tasks/init/cli.dart @@ -0,0 +1,22 @@ +library dart_dev.src.tasks.init.cli; + +import 'dart:async'; + +import 'package:args/args.dart'; + +import 'package:dart_dev/src/tasks/init/api.dart'; +import 'package:dart_dev/src/tasks/cli.dart'; + +class InitCli extends TaskCli { + final ArgParser argParser = new ArgParser(); + + final String command = 'init'; + + Future run(ArgResults parsedArgs) async { + InitTask task = init(); + await task.done; + return task.successful + ? new CliResult.success('dart_dev config initialized: tool/dev.dart') + : new CliResult.fail('dart_dev config already exists!'); + } +} diff --git a/lib/src/tasks/init/config.dart b/lib/src/tasks/init/config.dart new file mode 100644 index 00000000..ad265ba7 --- /dev/null +++ b/lib/src/tasks/init/config.dart @@ -0,0 +1,5 @@ +library dart_dev.src.tasks.init.config; + +import 'package:dart_dev/src/tasks/config.dart'; + +class InitConfig extends TaskConfig {} diff --git a/lib/src/tasks/task.dart b/lib/src/tasks/task.dart new file mode 100644 index 00000000..b6e80a92 --- /dev/null +++ b/lib/src/tasks/task.dart @@ -0,0 +1,8 @@ +library dart_dev.src.tasks.task; + +import 'dart:async'; + +abstract class Task { + Future get done; + bool successful; +} diff --git a/lib/src/tasks/test/api.dart b/lib/src/tasks/test/api.dart new file mode 100644 index 00000000..39275e46 --- /dev/null +++ b/lib/src/tasks/test/api.dart @@ -0,0 +1,58 @@ +library dart_dev.src.tasks.test.api; + +import 'dart:async'; + +import 'package:dart_dev/process.dart'; + +import 'package:dart_dev/src/tasks/task.dart'; + +TestTask test( + {List platforms: const [], List tests: const []}) { + var executable = 'pub'; + var args = ['run', 'test']; + platforms.forEach((p) { + args.addAll(['-p', p]); + }); + args.addAll(tests); + args.addAll(['--reporter=expanded']); + + TaskProcess process = new TaskProcess(executable, args); + Completer outputProcessed = new Completer(); + TestTask task = new TestTask('$executable ${args.join(' ')}', + Future.wait([process.done, outputProcessed.future])); + + // TODO: Use this pattern to better parse the test summary even when the output is colorized + // RegExp resultPattern = new RegExp(r'(\d+:\d+) \+(\d+) ?~?(\d+)? ?-?(\d+)?: (All|Some) tests (failed|passed)'); + + StreamController stdoutc = new StreamController(); + process.stdout.listen((line) { + stdoutc.add(line); + if (line.contains('All tests passed!') || + line.contains('Some tests failed.')) { + task.testSummary = line; + outputProcessed.complete(); + } + }); + + stdoutc.stream.listen(task._testOutput.add); + process.stderr.listen(task._testOutput.addError); + process.exitCode.then((code) { + if (task.successful == null) { + task.successful = code <= 0; + } + }); + + return task; +} + +class TestTask extends Task { + final Future done; + final String testCommand; + String testSummary; + + StreamController _testOutput = new StreamController(); + + TestTask(String this.testCommand, Future this.done); + + Stream get testOutput => _testOutput.stream; +} diff --git a/lib/src/tasks/test/cli.dart b/lib/src/tasks/test/cli.dart new file mode 100644 index 00000000..0d6210d2 --- /dev/null +++ b/lib/src/tasks/test/cli.dart @@ -0,0 +1,63 @@ +library dart_dev.src.tasks.test.cli; + +import 'dart:async'; + +import 'package:args/args.dart'; + +import 'package:dart_dev/io.dart'; + +import 'package:dart_dev/src/tasks/cli.dart'; +import 'package:dart_dev/src/tasks/config.dart'; +import 'package:dart_dev/src/tasks/test/api.dart'; +import 'package:dart_dev/src/tasks/test/config.dart'; + +class TestCli extends TaskCli { + final ArgParser argParser = new ArgParser() + ..addFlag('unit', + defaultsTo: defaultUnit, help: 'Includes the unit test suite.') + ..addFlag('integration', + defaultsTo: defaultIntegration, + help: 'Includes the integration test suite.') + ..addOption('platform', + abbr: 'p', + allowMultiple: true, + help: + 'The platform(s) on which to run the tests.\n[vm (default), dartium, content-shell, chrome, phantomjs, firefox, safari]'); + + final String command = 'test'; + + Future run(ArgResults parsedArgs) async { + bool unit = parsedArgs['unit']; + bool integration = parsedArgs['integration']; + List platforms = + TaskCli.valueOf('platform', parsedArgs, config.test.platforms); + + if (!unit && !integration) { + return new CliResult.fail( + 'No tests were selected. Include at least one of --unit or --integration.'); + } + + List tests = []; + if (unit) { + if (config.test.unitTests.isEmpty) { + return new CliResult.fail( + 'This project does not specify any unit tests.'); + } + tests.addAll(config.test.unitTests); + } + if (integration) { + if (config.test.integrationTests.isEmpty) { + return new CliResult.fail( + 'This project does not specify any integration tests.'); + } + tests.addAll(config.test.integrationTests); + } + + TestTask task = test(platforms: platforms, tests: tests); + reporter.logGroup(task.testCommand, outputStream: task.testOutput); + await task.done; + return task.successful + ? new CliResult.success(task.testSummary) + : new CliResult.fail(task.testSummary); + } +} diff --git a/lib/src/tasks/test/config.dart b/lib/src/tasks/test/config.dart new file mode 100644 index 00000000..5daac265 --- /dev/null +++ b/lib/src/tasks/test/config.dart @@ -0,0 +1,15 @@ +library dart_dev.src.tasks.test.config; + +import 'package:dart_dev/src/tasks/config.dart'; + +const bool defaultIntegration = false; +const List defaultIntegrationTests = const []; +const bool defaultUnit = true; +const List defaultUnitTests = const ['test/']; +const List defaultPlatforms = const []; + +class TestConfig extends TaskConfig { + List integrationTests = defaultIntegrationTests; + List platforms = defaultPlatforms; + List unitTests = defaultUnitTests; +} diff --git a/lib/src/version.dart b/lib/src/version.dart new file mode 100644 index 00000000..11519b9c --- /dev/null +++ b/lib/src/version.dart @@ -0,0 +1,65 @@ +library dart_dev.src.version; + +import 'dart:io'; + +import 'package:yaml/yaml.dart'; + +/// Copied from "test" package. +/// +/// Attempts to parse the version number of the `dart_dev` package +/// from the pubspec.lock file. This will not work if run within +/// the `dart_dev` package or if `dart_dev` is activated as a global +/// package. +bool printVersion() { + var lockfile; + try { + lockfile = loadYaml(new File('pubspec.lock').readAsStringSync()); + } on FormatException catch (_) { + return false; + } on IOException catch (_) { + return false; + } + + if (lockfile is! Map) return false; + var packages = lockfile['packages']; + if (packages is! Map) return false; + var package = packages['dart_dev']; + if (package is! Map) return false; + + var source = package['source']; + if (source is! String) return false; + + switch (source) { + case 'hosted': + var version = package['version']; + if (version is! String) return false; + + stdout.writeln(version); + return true; + + case 'git': + var version = package['version']; + if (version is! String) return false; + var description = package['description']; + if (description is! Map) return false; + var ref = description['resolved-ref']; + if (ref is! String) return false; + + stdout.writeln('$version (${ref.substring(0, 7)})'); + return true; + + case 'path': + var version = package['version']; + if (version is! String) return false; + var description = package['description']; + if (description is! Map) return false; + var path = description['path']; + if (path is! String) return false; + + stdout.writeln('$version (from $path)'); + return true; + + default: + return false; + } +} diff --git a/pubspec.yaml b/pubspec.yaml new file mode 100644 index 00000000..9b1424ab --- /dev/null +++ b/pubspec.yaml @@ -0,0 +1,10 @@ +name: dart_dev +version: 0.0.0 +dependencies: + ansicolor: "^0.0.9" + args: "^0.13.0" + dart_style: "^0.2.0" + test: "^0.12.0" + yaml: "^2.1.0" +executables: + dart_dev: \ No newline at end of file diff --git a/tool/dev.dart b/tool/dev.dart new file mode 100644 index 00000000..c86e1695 --- /dev/null +++ b/tool/dev.dart @@ -0,0 +1,10 @@ +library dart_dev.dev; + +import 'package:dart_dev/dart_dev.dart'; + +main(args) async { + config.analyze.entryPoints = ['lib/', 'test/', 'tool/']; + config.format.directories = ['lib/', 'test/', 'tool/']; + + await dev(args); +}