Make `package:http/http.dart` support all platforms (#198)

Fixes https://github.com/dart-lang/http/issues/22

Adds config specific imports to make `package:http/http.dart` support all platforms, and allow the `Client` factory constructor to return a valid `Client` for the web platform.

This should eliminate almost all need for the platform specific imports for consumers, although it does also add the `io_client.dart` public import.

Passes presubmit internally, with edits in only 3 files (they use the IoClient constructor directly, and pass in an HttpClient). Externally build_runner now supports config specific imports as well, I am currently working on validating everything works as expected there with this change.

### New Features

* The regular `Client` factory constructor is now usable anywhere that `dart:io`
  or `dart:html` are available, and will give you an `IoClient` or
  `BrowserClient` respectively.
* The `package:http/http.dart` import is now safe to use on the web (or
  anywhere that either `dart:io` or `dart:html` are available).

### Breaking Changes

* In order to use or reference the `IoClient` directly, you will need to import
  the new `package:http/io_client.dart` import. This is typically only necessary
  if you are passing a custom `HttpClient` instance to the constructor, in which
  case you are already giving up support for web.
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 8c1d1b0..d084ff7 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,20 @@
+## 0.12.0-dev
+
+### New Features
+
+* The regular `Client` factory constructor is now usable anywhere that `dart:io`
+  or `dart:html` are available, and will give you an `IoClient` or
+  `BrowserClient` respectively.
+* The `package:http/http.dart` import is now safe to use on the web (or
+  anywhere that either `dart:io` or `dart:html` are available).
+
+### Breaking Changes
+
+* In order to use or reference the `IoClient` directly, you will need to import
+  the new `package:http/io_client.dart` import. This is typically only necessary
+  if you are passing a custom `HttpClient` instance to the constructor, in which
+  case you are already giving up support for web.
+
 ## 0.11.3+17
 
 * Use new Dart 2 constant names. This branch is only for allowing existing
diff --git a/lib/browser_client.dart b/lib/browser_client.dart
index f767390..2cd0e5c 100644
--- a/lib/browser_client.dart
+++ b/lib/browser_client.dart
@@ -2,107 +2,4 @@
 // 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:async';
-import 'dart:html';
-import 'dart:typed_data';
-
-import 'src/base_client.dart';
-import 'src/base_request.dart';
-import 'src/byte_stream.dart';
-import 'src/exception.dart';
-import 'src/streamed_response.dart';
-
-// TODO(nweiz): Move this under src/, re-export from lib/http.dart, and use this
-// automatically from [new Client] once sdk#24581 is fixed.
-
-/// A `dart:html`-based HTTP client that runs in the browser and is backed by
-/// XMLHttpRequests.
-///
-/// This client inherits some of the limitations of XMLHttpRequest. It ignores
-/// the [BaseRequest.contentLength], [BaseRequest.persistentConnection],
-/// [BaseRequest.followRedirects], and [BaseRequest.maxRedirects] fields. It is
-/// also unable to stream requests or responses; a request will only be sent and
-/// a response will only be returned once all the data is available.
-class BrowserClient extends BaseClient {
-  /// The currently active XHRs.
-  ///
-  /// These are aborted if the client is closed.
-  final _xhrs = new Set<HttpRequest>();
-
-  /// Creates a new HTTP client.
-  BrowserClient();
-
-  /// Whether to send credentials such as cookies or authorization headers for
-  /// cross-site requests.
-  ///
-  /// Defaults to `false`.
-  bool withCredentials = false;
-
-  /// Sends an HTTP request and asynchronously returns the response.
-  Future<StreamedResponse> send(BaseRequest request) async {
-    var bytes = await request.finalize().toBytes();
-    var xhr = new HttpRequest();
-    _xhrs.add(xhr);
-    _openHttpRequest(xhr, request.method, request.url.toString(), asynch: true);
-    xhr.responseType = 'blob';
-    xhr.withCredentials = withCredentials;
-    request.headers.forEach(xhr.setRequestHeader);
-
-    var completer = new Completer<StreamedResponse>();
-    xhr.onLoad.first.then((_) {
-      // TODO(nweiz): Set the response type to "arraybuffer" when issue 18542
-      // is fixed.
-      var blob = xhr.response == null ? new Blob([]) : xhr.response;
-      var reader = new FileReader();
-
-      reader.onLoad.first.then((_) {
-        var body = reader.result as Uint8List;
-        completer.complete(new StreamedResponse(
-            new ByteStream.fromBytes(body), xhr.status,
-            contentLength: body.length,
-            request: request,
-            headers: xhr.responseHeaders,
-            reasonPhrase: xhr.statusText));
-      });
-
-      reader.onError.first.then((error) {
-        completer.completeError(
-            new ClientException(error.toString(), request.url),
-            StackTrace.current);
-      });
-
-      reader.readAsArrayBuffer(blob);
-    });
-
-    xhr.onError.first.then((_) {
-      // Unfortunately, the underlying XMLHttpRequest API doesn't expose any
-      // specific information about the error itself.
-      completer.completeError(
-          new ClientException("XMLHttpRequest error.", request.url),
-          StackTrace.current);
-    });
-
-    xhr.send(bytes);
-
-    try {
-      return await completer.future;
-    } finally {
-      _xhrs.remove(xhr);
-    }
-  }
-
-  // TODO(nweiz): Remove this when sdk#24637 is fixed.
-  void _openHttpRequest(HttpRequest request, String method, String url,
-      {bool asynch, String user, String password}) {
-    request.open(method, url, async: asynch, user: user, password: password);
-  }
-
-  /// Closes the client.
-  ///
-  /// This terminates all active requests.
-  void close() {
-    for (var xhr in _xhrs) {
-      xhr.abort();
-    }
-  }
-}
+export 'src/browser_client.dart' show BrowserClient;
diff --git a/lib/http.dart b/lib/http.dart
index 3e16285..e682ed1 100644
--- a/lib/http.dart
+++ b/lib/http.dart
@@ -16,7 +16,6 @@
 export 'src/byte_stream.dart';
 export 'src/client.dart';
 export 'src/exception.dart';
-export 'src/io_client.dart';
 export 'src/multipart_file.dart';
 export 'src/multipart_request.dart';
 export 'src/request.dart';
diff --git a/lib/io_client.dart b/lib/io_client.dart
new file mode 100644
index 0000000..170acfb
--- /dev/null
+++ b/lib/io_client.dart
@@ -0,0 +1,5 @@
+// Copyright (c) 2018, 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.
+
+export 'src/io_client.dart' show IOClient;
diff --git a/lib/src/browser_client.dart b/lib/src/browser_client.dart
new file mode 100644
index 0000000..4f49ec7
--- /dev/null
+++ b/lib/src/browser_client.dart
@@ -0,0 +1,108 @@
+// Copyright (c) 2018, 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:async';
+import 'dart:html';
+import 'dart:typed_data';
+
+import 'base_client.dart';
+import 'base_request.dart';
+import 'byte_stream.dart';
+import 'exception.dart';
+import 'streamed_response.dart';
+
+/// Used from conditional imports, matches the definition in `client_stub.dart`.
+BaseClient createClient() => BrowserClient();
+
+/// A `dart:html`-based HTTP client that runs in the browser and is backed by
+/// XMLHttpRequests.
+///
+/// This client inherits some of the limitations of XMLHttpRequest. It ignores
+/// the [BaseRequest.contentLength], [BaseRequest.persistentConnection],
+/// [BaseRequest.followRedirects], and [BaseRequest.maxRedirects] fields. It is
+/// also unable to stream requests or responses; a request will only be sent and
+/// a response will only be returned once all the data is available.
+class BrowserClient extends BaseClient {
+  /// The currently active XHRs.
+  ///
+  /// These are aborted if the client is closed.
+  final _xhrs = new Set<HttpRequest>();
+
+  /// Creates a new HTTP client.
+  BrowserClient();
+
+  /// Whether to send credentials such as cookies or authorization headers for
+  /// cross-site requests.
+  ///
+  /// Defaults to `false`.
+  bool withCredentials = false;
+
+  /// Sends an HTTP request and asynchronously returns the response.
+  Future<StreamedResponse> send(BaseRequest request) async {
+    var bytes = await request.finalize().toBytes();
+    var xhr = new HttpRequest();
+    _xhrs.add(xhr);
+    _openHttpRequest(xhr, request.method, request.url.toString(), asynch: true);
+    xhr.responseType = 'blob';
+    xhr.withCredentials = withCredentials;
+    request.headers.forEach(xhr.setRequestHeader);
+
+    var completer = new Completer<StreamedResponse>();
+    xhr.onLoad.first.then((_) {
+      // TODO(nweiz): Set the response type to "arraybuffer" when issue 18542
+      // is fixed.
+      var blob = xhr.response == null ? new Blob([]) : xhr.response;
+      var reader = new FileReader();
+
+      reader.onLoad.first.then((_) {
+        var body = reader.result as Uint8List;
+        completer.complete(new StreamedResponse(
+            new ByteStream.fromBytes(body), xhr.status,
+            contentLength: body.length,
+            request: request,
+            headers: xhr.responseHeaders,
+            reasonPhrase: xhr.statusText));
+      });
+
+      reader.onError.first.then((error) {
+        completer.completeError(
+            new ClientException(error.toString(), request.url),
+            StackTrace.current);
+      });
+
+      reader.readAsArrayBuffer(blob);
+    });
+
+    xhr.onError.first.then((_) {
+      // Unfortunately, the underlying XMLHttpRequest API doesn't expose any
+      // specific information about the error itself.
+      completer.completeError(
+          new ClientException("XMLHttpRequest error.", request.url),
+          StackTrace.current);
+    });
+
+    xhr.send(bytes);
+
+    try {
+      return await completer.future;
+    } finally {
+      _xhrs.remove(xhr);
+    }
+  }
+
+  // TODO(nweiz): Remove this when sdk#24637 is fixed.
+  void _openHttpRequest(HttpRequest request, String method, String url,
+      {bool asynch, String user, String password}) {
+    request.open(method, url, async: asynch, user: user, password: password);
+  }
+
+  /// Closes the client.
+  ///
+  /// This terminates all active requests.
+  void close() {
+    for (var xhr in _xhrs) {
+      xhr.abort();
+    }
+  }
+}
diff --git a/lib/src/client.dart b/lib/src/client.dart
index bb78314..78ea0a1 100644
--- a/lib/src/client.dart
+++ b/lib/src/client.dart
@@ -8,7 +8,12 @@
 
 import 'base_client.dart';
 import 'base_request.dart';
-import 'io_client.dart';
+// ignore: uri_does_not_exist
+import 'client_stub.dart'
+    // ignore: uri_does_not_exist
+    if (dart.library.html) 'browser_client.dart'
+    // ignore: uri_does_not_exist
+    if (dart.library.io) 'io_client.dart';
 import 'response.dart';
 import 'streamed_response.dart';
 
@@ -24,10 +29,10 @@
 abstract class Client {
   /// Creates a new client.
   ///
-  /// Currently this will create an [IOClient] if `dart:io` is available and
-  /// throw an [UnsupportedError] otherwise. In the future, it will create a
-  /// [BrowserClient] if `dart:html` is available.
-  factory Client() => new IOClient();
+  /// Currently this will create an `IOClient` if `dart:io` is available and
+  /// a `BrowserClient` if `dart:html` is available, otherwise it will throw
+  /// an unsupported error.
+  factory Client() => createClient();
 
   /// Sends an HTTP HEAD request with the given headers to the given URL, which
   /// can be a [Uri] or a [String].
diff --git a/lib/src/client_stub.dart b/lib/src/client_stub.dart
new file mode 100644
index 0000000..1a34d50
--- /dev/null
+++ b/lib/src/client_stub.dart
@@ -0,0 +1,9 @@
+// Copyright (c) 2018, 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 'base_client.dart';
+
+/// Implemented in `browser_client.dart` and `io_client.dart`.
+BaseClient createClient() => throw UnsupportedError(
+    'Cannot create a client without dart:html or dart:io.');
diff --git a/lib/src/io_client.dart b/lib/src/io_client.dart
index 403fe00..53054f8 100644
--- a/lib/src/io_client.dart
+++ b/lib/src/io_client.dart
@@ -12,6 +12,9 @@
 import 'exception.dart';
 import 'streamed_response.dart';
 
+/// Used from conditional imports, matches the definition in `client_stub.dart`.
+BaseClient createClient() => IOClient();
+
 /// A `dart:io`-based HTTP client.
 ///
 /// This is the default client when running on the command line.
diff --git a/lib/src/multipart_file.dart b/lib/src/multipart_file.dart
index 32f2ada..a173f67 100644
--- a/lib/src/multipart_file.dart
+++ b/lib/src/multipart_file.dart
@@ -4,15 +4,17 @@
 
 import 'dart:async';
 import 'dart:convert';
-import 'dart:io';
 
-import 'package:async/async.dart';
 import 'package:http_parser/http_parser.dart';
-import 'package:path/path.dart' as path;
 
 import 'byte_stream.dart';
 import 'utils.dart';
 
+// ignore: uri_does_not_exist
+import 'multipart_file_stub.dart'
+    // ignore: uri_does_not_exist
+    if (dart.library.io) 'multipart_file_io.dart';
+
 /// A file to be uploaded as part of a [MultipartRequest]. This doesn't need to
 /// correspond to a physical file.
 class MultipartFile {
@@ -87,14 +89,9 @@
   /// Throws an [UnsupportedError] if `dart:io` isn't supported in this
   /// environment.
   static Future<MultipartFile> fromPath(String field, String filePath,
-      {String filename, MediaType contentType}) async {
-    if (filename == null) filename = path.basename(filePath);
-    var file = new File(filePath);
-    var length = await file.length();
-    var stream = new ByteStream(DelegatingStream.typed(file.openRead()));
-    return new MultipartFile(field, stream, length,
-        filename: filename, contentType: contentType);
-  }
+          {String filename, MediaType contentType}) =>
+      multipartFileFromPath(field, filePath,
+          filename: filename, contentType: contentType);
 
   // Finalizes the file in preparation for it being sent as part of a
   // [MultipartRequest]. This returns a [ByteStream] that should emit the body
diff --git a/lib/src/multipart_file_io.dart b/lib/src/multipart_file_io.dart
new file mode 100644
index 0000000..c49998a
--- /dev/null
+++ b/lib/src/multipart_file_io.dart
@@ -0,0 +1,23 @@
+// Copyright (c) 2018, 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:async';
+import 'dart:io';
+
+import 'package:async/async.dart';
+import 'package:http_parser/http_parser.dart';
+import 'package:path/path.dart' as p;
+
+import 'byte_stream.dart';
+import 'multipart_file.dart';
+
+Future<MultipartFile> multipartFileFromPath(String field, String filePath,
+    {String filename, MediaType contentType}) async {
+  if (filename == null) filename = p.basename(filePath);
+  var file = new File(filePath);
+  var length = await file.length();
+  var stream = new ByteStream(DelegatingStream.typed(file.openRead()));
+  return new MultipartFile(field, stream, length,
+      filename: filename, contentType: contentType);
+}
diff --git a/lib/src/multipart_file_stub.dart b/lib/src/multipart_file_stub.dart
new file mode 100644
index 0000000..b230441
--- /dev/null
+++ b/lib/src/multipart_file_stub.dart
@@ -0,0 +1,14 @@
+// Copyright (c) 2018, 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:async';
+
+import 'package:http_parser/http_parser.dart';
+
+import 'multipart_file.dart';
+
+Future<MultipartFile> multipartFileFromPath(String field, String filePath,
+        {String filename, MediaType contentType}) =>
+    throw UnsupportedError(
+        'MultipartFile is only supported where dart:io is available.');
diff --git a/pubspec.yaml b/pubspec.yaml
index 36be5ad..67730fe 100644
--- a/pubspec.yaml
+++ b/pubspec.yaml
@@ -1,5 +1,5 @@
 name: http
-version: 0.11.4-dev
+version: 0.12.0-dev
 author: "Dart Team <misc@dartlang.org>"
 homepage: https://github.com/dart-lang/http
 description: A composable, Future-based API for making HTTP requests.
@@ -14,3 +14,6 @@
 
 dev_dependencies:
   test: ^1.3.0
+
+dependency_overrides:
+  package_resolver: 1.0.4
diff --git a/test/io/client_test.dart b/test/io/client_test.dart
index 410a875..04fca5c 100644
--- a/test/io/client_test.dart
+++ b/test/io/client_test.dart
@@ -7,6 +7,7 @@
 import 'dart:io';
 
 import 'package:http/http.dart' as http;
+import 'package:http/src/io_client.dart' as http_io;
 import 'package:test/test.dart';
 
 import 'utils.dart';
@@ -56,7 +57,7 @@
     expect(
         startServer().then((_) {
           var ioClient = new HttpClient();
-          var client = new http.IOClient(ioClient);
+          var client = new http_io.IOClient(ioClient);
           var request = new http.StreamedRequest("POST", serverUrl);
           request.headers[HttpHeaders.contentTypeHeader] =
               'application/json; charset=utf-8';