Chromium Code Reviews| Index: sdk/lib/_internal/pub/lib/src/command/serve.dart |
| diff --git a/sdk/lib/_internal/pub/lib/src/command/serve.dart b/sdk/lib/_internal/pub/lib/src/command/serve.dart |
| new file mode 100644 |
| index 0000000000000000000000000000000000000000..d5b24d9a589582a7425ff7932fb11fd2a2cffed8 |
| --- /dev/null |
| +++ b/sdk/lib/_internal/pub/lib/src/command/serve.dart |
| @@ -0,0 +1,198 @@ |
| +// Copyright (c) 2012, 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. |
| + |
| +library pub.command.serve; |
| + |
| +import 'dart:async'; |
| +import 'dart:io'; |
| + |
| +import 'package:barback/barback.dart'; |
| +import 'package:path/path.dart' as path; |
| + |
| +import '../command.dart'; |
| +import '../entrypoint.dart'; |
| +import '../exit_codes.dart' as exit_codes; |
| +import '../log.dart' as log; |
| +import '../utils.dart'; |
| + |
| +final _green = getPlatformString('\u001b[32m'); |
| +final _red = getPlatformString('\u001b[31m'); |
| +final _none = getPlatformString('\u001b[0m'); |
| + |
| +/// Handles the `serve` pub command. |
| +class ServeCommand extends PubCommand { |
| + String get description => "Run a local web development server."; |
| + String get usage => 'pub serve'; |
| + |
| + ServeCommand() { |
| + commandParser.addOption('port', defaultsTo: '8080', |
| + help: 'The port to listen on.'); |
| + } |
| + |
| + Future onRun() { |
| + return PubPackageProvider.create(entrypoint).then((provider) { |
| + var port; |
| + try { |
| + port = int.parse(commandOptions['port']); |
| + } on FormatException catch(_) { |
| + log.error('Could not parse port "${commandOptions['port']}"'); |
| + this.printUsage(); |
| + exit(exit_codes.USAGE); |
| + } |
|
nweiz
2013/07/25 22:58:58
Style nit: put this in a getter to avoid the awkwa
Bob Nystrom
2013/07/26 20:20:11
It is a bit awkward, but I'm not crazy about a get
|
| + |
| + var barback = new Barback(provider); |
| + |
| + barback.results.listen((result) { |
|
nweiz
2013/07/25 22:58:58
Add a TODO to report errors and successful build c
Bob Nystrom
2013/07/26 20:20:11
Do we actually intend to do that? That seems painf
nweiz
2013/07/29 20:23:14
In my experience, people like it. We can be smart
Bob Nystrom
2013/07/29 21:45:43
Done.
|
| + if (result.succeeded) { |
| + log.message("Build completed ${_green}successfully$_none"); |
| + } else { |
| + log.message("Build completed with " |
| + "${_red}${result.errors.length}$_none errors."); |
| + } |
| + }); |
| + |
| + barback.errors.listen((error) { |
| + log.error("${_red}Build error:\n$error$_none"); |
| + }); |
| + |
| + // Add all of the visible files. |
| + for (var package in provider.packages) { |
| + barback.updateSources(provider.listAssets(package)); |
|
nweiz
2013/07/25 22:58:58
Add a TODO to re-update these sources when they ch
Bob Nystrom
2013/07/26 20:20:11
Done.
|
| + } |
| + |
| + HttpServer.bind(InternetAddress.LOOPBACK_IP_V4, port).then((server) { |
|
nweiz
2013/07/25 22:58:58
Doesn't "localhost" work these days?
Bob Nystrom
2013/07/26 20:20:11
Not sure. I figured an enum is better than a magic
nweiz
2013/07/29 20:23:14
We're using "localhost" below, and "127.0.0.1" els
Bob Nystrom
2013/07/29 21:45:43
Done.
|
| + log.message("Serving ${entrypoint.root.name} " |
| + "on http://localhost:${server.port}..."); |
|
nweiz
2013/07/25 22:58:58
The "..." here may make copy/pasting this URL diff
Bob Nystrom
2013/07/26 20:20:11
Done.
|
| + |
| + server.listen((HttpRequest request) { |
|
nweiz
2013/07/25 22:58:58
Don't type annotate [request].
We should check th
Bob Nystrom
2013/07/26 20:20:11
Done.
|
| + var id = getIdFromUri(request.uri); |
| + barback.getAssetById(id).then((asset) { |
| + log.message( |
| + "$_green${request.method}$_none ${request.uri} -> $asset"); |
| + request.response.addStream(asset.read()).then((_) { |
|
nweiz
2013/07/25 22:58:58
Add a TODO to set the Content-Type based on the ex
Bob Nystrom
2013/07/26 20:20:11
Done.
|
| + request.response.close(); |
|
nweiz
2013/07/25 22:58:58
I thought [addStream] automatically closed the con
Bob Nystrom
2013/07/26 20:20:11
Nope:
"When the IOSink is bound to a stream (thro
|
| + }); |
|
nweiz
2013/07/25 22:58:58
We should handle errors that come off this future.
Bob Nystrom
2013/07/26 20:20:11
Done.
nweiz
2013/07/29 20:23:14
Unlike errors coming off [getAssetById], errors co
Bob Nystrom
2013/07/29 21:45:43
Took out the 500 stuff. There isn't any tests for
|
| + }).catchError((error) { |
| + if (error is! AssetNotFoundException) throw error; |
|
nweiz
2013/07/25 22:58:58
This will top-level the error. That's not what we
Bob Nystrom
2013/07/26 20:20:11
Done. I log it and respond with a 500. Seems about
nweiz
2013/07/29 20:23:14
The only error getAssetById is expected to raise i
Bob Nystrom
2013/07/29 21:45:43
Done.
|
| + log.error("$_red${request.method}$_none ${request.uri} -> $error"); |
| + request.response.statusCode = 404; |
| + request.response.reasonPhrase = error.toString(); |
|
nweiz
2013/07/25 22:58:58
The reason phrase is expected to be very brief, an
Bob Nystrom
2013/07/26 20:20:11
Done.
|
| + request.response.close(); |
| + return; |
|
nweiz
2013/07/25 22:58:58
Remove.
Bob Nystrom
2013/07/26 20:20:11
Done.
|
| + }); |
| + }); |
| + }); |
| + |
| + // TODO(rnystrom): Hack! Return a future that never completes to leave |
| + // pub running. |
| + return new Completer().future; |
|
nweiz
2013/07/25 22:58:58
This seems, as you say, hacky. It feels like we sh
Bob Nystrom
2013/07/26 20:20:11
In the above code, I'm catching all errors that co
nweiz
2013/07/29 20:23:14
As with [getAssetById], programmatic errors in bar
Bob Nystrom
2013/07/29 21:45:43
Done.
|
| + }); |
| + } |
| + |
| + AssetId getIdFromUri(Uri uri) { |
| + // Strip the leading "/" from the URL. |
| + var relative = uri.path.substring(1); |
|
nweiz
2013/07/25 22:58:58
Rather than explicitly using substring, it seems c
Bob Nystrom
2013/07/26 20:20:11
Done.
|
| + if (relative == "") relative = "index.html"; |
|
nweiz
2013/07/25 22:58:58
Supporting "index.html" only for the root seems wr
Bob Nystrom
2013/07/26 20:20:11
That the nice thing about "/". For that one case,
nweiz
2013/07/29 20:23:14
I think supporting only "/" is actively harmful. U
Bob Nystrom
2013/07/29 21:45:43
Removed it.
|
| + |
| + var parts = path.split(relative); |
|
nweiz
2013/07/25 22:58:58
This will break on windows. You need to make a URL
Bob Nystrom
2013/07/26 20:20:11
Done.
|
| + |
| + // Checks to see if [uri]'s path contains a special "symlink" directory |
| + // named [name]. If so, maps the path after that to be within [dir]. |
|
nweiz
2013/07/25 22:58:58
This isn't strictly accurate; it treats the direct
Bob Nystrom
2013/07/26 20:20:11
Done.
Bob Nystrom
2013/07/26 20:20:11
Done.
|
| + AssetId _trySpecialUrl(String name, String dir) { |
| + // Find the package name and the relative path in the package. |
| + var index = parts.indexOf(name); |
| + if (index == -1) return null; |
| + |
| + var package = parts[index + 1]; |
| + var assetPath = path.join(dir, path.joinAll(parts.skip(index + 2))); |
| + return new AssetId(package, assetPath); |
|
nweiz
2013/07/25 22:58:58
Right now this will crash on paths like "packages"
Bob Nystrom
2013/07/26 20:20:11
Done.
|
| + } |
| + |
| + // See if it's "packages" URL. |
| + var id = _trySpecialUrl("packages", "lib"); |
| + if (id != null) return id; |
| + |
| + // See if it's an "assets" URL. |
| + id = _trySpecialUrl("assets", "asset"); |
| + if (id != null) return id; |
| + |
| + // Otherwise, it's a path in current package's web directory. |
| + relative = path.join("web", relative); |
| + return new AssetId(entrypoint.root.name, relative); |
| + } |
| +} |
| + |
| +class PubPackageProvider implements PackageProvider { |
|
nweiz
2013/07/25 22:58:58
This is going to be used elsewhere pretty soon. Mi
Bob Nystrom
2013/07/26 20:20:11
Done.
|
| + /// The [Entrypoint] package being served. |
| + final Entrypoint _entrypoint; |
| + |
| + /// Maps the names of all of the packages in [_entrypoint]'s transitive |
| + /// dependency graph to the local path of the directory for that package. |
| + final Map<String, String> _packageDirs; |
| + |
| + /// Creates a new provider for [entrypoint]. |
| + static Future<PubPackageProvider> create(Entrypoint entrypoint) { |
| + var packageDirs = <String, String>{}; |
| + var futures = []; |
| + |
| + packageDirs[entrypoint.root.name] = entrypoint.root.dir; |
| + |
| + // Cache package directories up front so we can have synchronous access |
| + // to them. |
| + // TODO(rnystrom): Handle missing or out of date lockfile. |
| + entrypoint.loadLockFile().packages.forEach((name, package) { |
| + var source = entrypoint.cache.sources[package.source]; |
| + futures.add(source.getDirectory(package).then((packageDir) { |
| + packageDirs[name] = packageDir; |
| + })); |
| + }); |
|
nweiz
2013/07/25 22:58:58
Rather than manually expanding it, just use `entry
Bob Nystrom
2013/07/26 20:20:11
packages is a Map, not an Iterable, so I tried thi
|
| + |
| + return Future.wait(futures).then((_) { |
| + return new PubPackageProvider._(entrypoint, packageDirs); |
| + }); |
| + } |
| + |
| + PubPackageProvider._(this._entrypoint, this._packageDirs); |
| + |
| + Iterable<String> get packages => _packageDirs.keys; |
| + |
| + List<AssetId> listAssets(String package, {String within}) { |
|
nweiz
2013/07/25 22:58:58
Right now [listAssets] is used nowhere in barback.
Bob Nystrom
2013/07/26 20:20:11
It was being used forever ago when I was prototypi
|
| + var files = <AssetId>[]; |
| + |
| + addFiles(String dirPath) { |
| + var packageDir = _packageDirs[package]; |
| + var dir = new Directory(path.join(packageDir, dirPath)); |
| + if (!dir.existsSync()) return; |
|
nweiz
2013/07/25 22:58:58
Use [dirExists].
Bob Nystrom
2013/07/29 21:45:43
Done.
|
| + var entries = dir.listSync(recursive: true); |
|
nweiz
2013/07/25 22:58:58
Use [listDir] rather than [Directory.listSync].
Bob Nystrom
2013/07/26 20:20:11
Done.
|
| + files.addAll(entries |
| + .where((entry) => entry is File) |
| + .map((entry) => new AssetId(package, |
| + path.relative(entry.path, from: packageDir)))); |
| + } |
| + |
| + // If we aren't given a subdirectory, use all externally visible files. |
| + if (within == null) { |
| + // Expose the "asset" and "lib" directories. |
| + addFiles("asset"); |
| + addFiles("lib"); |
| + |
| + // The entrypoint's "web" directory is also visible. |
| + if (package == _entrypoint.root.name) { |
| + addFiles("web"); |
| + } |
| + } else { |
| + addFiles(within); |
| + } |
| + |
| + return files; |
| + } |
|
nweiz
2013/07/25 22:58:58
This shouldn't list the contents of packages direc
Bob Nystrom
2013/07/26 20:20:11
Done.
|
| + |
| + // TODO(rnystrom): Actually support transformers. |
| + Iterable<Iterable<Transformer>> getTransformers(String package) => []; |
| + |
| + Future<Asset> getAsset(AssetId id) { |
| + var file = path.normalize(path.join(_packageDirs[id.package], id.path)); |
|
nweiz
2013/07/25 22:58:58
What's the value of normalizing the path here?
Bob Nystrom
2013/07/26 20:20:11
Not much. Removed.
|
| + return new Future.value(new Asset.fromPath(id, file)); |
| + } |
| +} |