blob: 78921e2524cdf55c1df80e44439cdb49ab774076 [file] [log] [blame]
// 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:convert';
import 'dart:io';
import 'package:build/build.dart';
import 'package:glob/glob.dart';
import 'package:shelf/shelf.dart' as shelf;
import 'package:build_runner_core/src/asset_graph/graph.dart';
import 'package:build_runner_core/src/asset_graph/node.dart';
import 'path_to_asset_id.dart';
/// A handler for `/$graph` requests under a specific `rootDir`.
class AssetGraphHandler {
final AssetReader _reader;
final String _rootPackage;
final AssetGraph _assetGraph;
AssetGraphHandler(this._reader, this._rootPackage, this._assetGraph);
/// Returns a response with the information about a specific node in the
/// graph.
///
/// For an empty path, returns the HTML page to render the graph.
///
/// For queries with `q=QUERY` will look for the assetNode referenced.
/// QUERY can be an [AssetId] or a path.
///
/// [AssetId] as `package|path`
///
/// path as:
/// - `packages/<package>/<path_under_lib>`
/// - `<path_under_root_package>`
/// - `<path_under_rootDir>`
///
/// There may be some ambiguity between paths which are under the top-level of
/// the root package, and those which are under the rootDir. Preference is
/// given to the asset (if it exists) which is not under the implicit
/// `rootDir`. For instance if the request is `$graph/web/main.dart` this will
/// prefer to serve `<package>|web/main.dart`, but if it does not exist will
/// fall back to `<package>|web/web/main.dart`.
FutureOr<shelf.Response> handle(shelf.Request request, String rootDir) async {
switch (request.url.path) {
case '':
if (!request.url.hasQuery) {
return shelf.Response.ok(
await _reader.readAsString(
AssetId('build_runner', 'lib/src/server/graph_viz.html')),
headers: {HttpHeaders.contentTypeHeader: 'text/html'});
}
var query = request.url.queryParameters['q']?.trim();
if (query != null && query.isNotEmpty) {
var filter = request.url.queryParameters['f']?.trim();
return _handleQuery(query, rootDir, filter: filter);
}
break;
case 'assets.json':
return _jsonResponse(_assetGraph.serialize());
}
return shelf.Response.notFound('Bad request: "${request.url}".');
}
Future<shelf.Response> _handleQuery(String query, String rootDir,
{String filter}) async {
var filterGlob = filter != null ? Glob(filter) : null;
var pipeIndex = query.indexOf('|');
AssetId assetId;
if (pipeIndex < 0) {
var querySplit = query.split('/');
assetId = pathToAssetId(
_rootPackage, querySplit.first, querySplit.skip(1).toList());
if (!_assetGraph.contains(assetId)) {
var secondTry = pathToAssetId(_rootPackage, rootDir, querySplit);
if (!_assetGraph.contains(secondTry)) {
return shelf.Response.notFound(
'Could not find asset for path "$query". Tried:\n'
'- $assetId\n'
'- $secondTry');
}
assetId = secondTry;
}
} else {
assetId = AssetId.parse(query);
if (!_assetGraph.contains(assetId)) {
return shelf.Response.notFound(
'Could not find asset in build graph: $assetId');
}
}
var node = _assetGraph.get(assetId);
var currentEdge = 0;
var nodes = [
{'id': '${node.id}', 'label': '${node.id}'}
];
var edges = <Map<String, String>>[];
for (final output in node.outputs) {
if (filterGlob != null && !filterGlob.matches(output.toString())) {
continue;
}
edges.add(
{'from': '${node.id}', 'to': '$output', 'id': 'e${currentEdge++}'});
nodes.add({'id': '$output', 'label': '$output'});
}
if (node is NodeWithInputs) {
for (final input in node.inputs) {
if (filterGlob != null && !filterGlob.matches(input.toString())) {
continue;
}
edges.add(
{'from': '$input', 'to': '${node.id}', 'id': 'e${currentEdge++}'});
nodes.add({'id': '$input', 'label': '$input'});
}
}
var result = <String, dynamic>{
'primary': {
'id': '${node.id}',
'hidden': node is GeneratedAssetNode ? node.isHidden : null,
'state': node is NodeWithInputs ? '${node.state}' : null,
'wasOutput': node is GeneratedAssetNode ? node.wasOutput : null,
'isFailure': node is GeneratedAssetNode ? node.isFailure : null,
'phaseNumber': node is NodeWithInputs ? node.phaseNumber : null,
'type': node.runtimeType.toString(),
'glob': node is GlobAssetNode ? node.glob.pattern : null,
'lastKnownDigest': node.lastKnownDigest.toString(),
},
'edges': edges,
'nodes': nodes,
};
return _jsonResponse(_jsonUtf8Encoder.convert(result));
}
}
final _jsonUtf8Encoder = JsonUtf8Encoder();
shelf.Response _jsonResponse(List<int> body) => shelf.Response.ok(body,
headers: {HttpHeaders.contentTypeHeader: 'application/json'});