devfs_web.dart 23.1 KB
Newer Older
Ian Hickson's avatar
Ian Hickson committed
1
// Copyright 2014 The Flutter Authors. All rights reserved.
2 3 4
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

5
import 'dart:async';
6 7
import 'dart:typed_data';

8 9 10
import 'package:dwds/data/build_result.dart';
import 'package:dwds/dwds.dart';
import 'package:logging/logging.dart';
11 12
import 'package:meta/meta.dart';
import 'package:mime/mime.dart' as mime;
13 14
// TODO(bkonyi): remove deprecated member usage, https://github.com/flutter/flutter/issues/51951
// ignore: deprecated_member_use
15
import 'package:package_config/discovery.dart';
16 17
// TODO(bkonyi): remove deprecated member usage, https://github.com/flutter/flutter/issues/51951
// ignore: deprecated_member_use
18
import 'package:package_config/packages.dart';
19 20
import 'package:shelf/shelf.dart' as shelf;
import 'package:shelf/shelf_io.dart' as shelf;
21

22 23
import '../artifacts.dart';
import '../asset.dart';
24 25 26
import '../base/common.dart';
import '../base/file_system.dart';
import '../base/io.dart';
27
import '../base/net.dart';
28
import '../base/utils.dart';
29
import '../build_info.dart';
30
import '../bundle.dart';
31
import '../cache.dart';
32
import '../compile.dart';
33
import '../convert.dart';
34
import '../devfs.dart';
35
import '../globals.dart' as globals;
36 37
import '../web/bootstrap.dart';
import '../web/chrome.dart';
38 39 40 41

/// A web server which handles serving JavaScript and assets.
///
/// This is only used in development mode.
42
class WebAssetServer implements AssetReader {
43
  @visibleForTesting
44
  WebAssetServer(this._httpServer, this._packages, this.internetAddress);
45 46 47 48 49 50 51

  // Fallback to "application/octet-stream" on null which
  // makes no claims as to the structure of the data.
  static const String _kDefaultMimeType = 'application/octet-stream';

  /// Start the web asset server on a [hostname] and [port].
  ///
52 53 54
  /// If [testMode] is true, do not actually initialize dwds or the shelf static
  /// server.
  ///
55 56
  /// Unhandled exceptions will throw a [ToolExit] with the error and stack
  /// trace.
57 58 59 60 61 62
  static Future<WebAssetServer> start(
    String hostname,
    int port,
    UrlTunneller urlTunneller,
    BuildMode buildMode,
    bool enableDwds,
63 64 65
    Uri entrypoint, {
    bool testMode = false,
  }) async {
66
    try {
67 68
      final InternetAddress address = (await InternetAddress.lookup(hostname)).first;
      final HttpServer httpServer = await HttpServer.bind(address, port);
69 70
      // TODO(bkonyi): remove deprecated member usage, https://github.com/flutter/flutter/issues/51951
      // ignore: deprecated_member_use
71 72 73
      final Packages packages = await loadPackagesFile(
        Uri.base.resolve('.packages'), loader: (Uri uri) => globals.fs.file(uri).readAsBytes());
      final WebAssetServer server = WebAssetServer(httpServer, packages, address);
74 75 76
      if (testMode) {
        return server;
      }
77 78 79

      // In release builds deploy a simpler proxy server.
      if (buildMode != BuildMode.debug) {
80
        final ReleaseAssetServer releaseAssetServer = ReleaseAssetServer(entrypoint);
81 82 83 84 85 86 87 88 89 90 91 92 93
        shelf.serveRequests(httpServer, releaseAssetServer.handle);
        return server;
      }
      // In debug builds, spin up DWDS and the full asset server.
      final Dwds dwds = await Dwds.start(
        assetReader: server,
        buildResults: const Stream<BuildResult>.empty(),
        chromeConnection: () async {
          final Chrome chrome = await ChromeLauncher.connectedInstance;
          return chrome.chromeConnection;
        },
        urlEncoder: urlTunneller,
        enableDebugging: true,
94
        serveDevTools: false,
95 96 97 98 99 100 101 102 103 104 105 106 107
        logWriter: (Level logLevel, String message) => globals.printTrace(message)
      );
      shelf.Pipeline pipeline = const shelf.Pipeline();
      if (enableDwds) {
        pipeline = pipeline.addMiddleware(dwds.middleware);
      }
      final shelf.Handler dwdsHandler = pipeline.addHandler(server.handleRequest);
      final shelf.Cascade cascade = shelf.Cascade()
        .add(dwds.handler)
        .add(dwdsHandler);
      shelf.serveRequests(httpServer, cascade.handler);
      server.dwds = dwds;
      return server;
108 109 110 111 112 113 114 115
    } on SocketException catch (err) {
      throwToolExit('Failed to bind web development server:\n$err');
    }
    assert(false);
    return null;
  }

  final HttpServer _httpServer;
116 117
  // If holding these in memory is too much overhead, this can be switched to a
  // RandomAccessFile and read on demand.
118
  final Map<String, Uint8List> _files = <String, Uint8List>{};
119
  final Map<String, Uint8List> _sourcemaps = <String, Uint8List>{};
120 121
  // TODO(bkonyi): remove deprecated member usage, https://github.com/flutter/flutter/issues/51951
  // ignore: deprecated_member_use
122
  final Packages _packages;
123
  final InternetAddress internetAddress;
124
  /* late final */ Dwds dwds;
125

126 127 128 129 130 131
  @visibleForTesting
  Uint8List getFile(String path) => _files[path];

  @visibleForTesting
  Uint8List getSourceMap(String path) => _sourcemaps[path];

132
  // handle requests for JavaScript source, dart sources maps, or asset files.
133 134 135
  @visibleForTesting
  Future<shelf.Response> handleRequest(shelf.Request request) async {
    final Map<String, String> headers = <String, String>{};
136
    // If the response is `/`, then we are requesting the index file.
137
    if (request.url.path == '/' || request.url.path.isEmpty) {
138
      final File indexFile = globals.fs.currentDirectory
139 140
        .childDirectory('web')
        .childFile('index.html');
141
      if (indexFile.existsSync()) {
142 143 144
        headers[HttpHeaders.contentTypeHeader] = 'text/html';
        headers[HttpHeaders.contentLengthHeader] = indexFile.lengthSync().toString();
        return shelf.Response.ok(indexFile.openRead(), headers: headers);
145
      }
146
      return shelf.Response.notFound('');
147
    }
148

149 150 151 152
    // Track etag headers for better caching of resources.
    final String ifNoneMatch = request.headers[HttpHeaders.ifNoneMatchHeader];
    headers[HttpHeaders.cacheControlHeader] = 'max-age=0, must-revalidate';

153 154 155 156
    // NOTE: shelf removes leading `/` for some reason.
    final String requestPath = request.url.path.startsWith('/')
      ?  request.url.path
      : '/${request.url.path}';
157 158

    // If this is a JavaScript file, it must be in the in-memory cache.
159
    // Attempt to look up the file by URI.
160
    if (_files.containsKey(requestPath)) {
161
      final List<int> bytes = getFile(requestPath);
162 163 164 165 166 167 168
      // Use the underlying buffer hashCode as a revision string. This buffer is
      // replaced whenever the frontend_server produces new output files, which
      // will also change the hashCode.
      final String etag = bytes.hashCode.toString();
      if (ifNoneMatch == etag) {
        return shelf.Response.notModified();
      }
169 170
      headers[HttpHeaders.contentLengthHeader] = bytes.length.toString();
      headers[HttpHeaders.contentTypeHeader] = 'application/javascript';
171
      headers[HttpHeaders.etagHeader] = etag;
172
      return shelf.Response.ok(bytes, headers: headers);
173
    }
174 175
    // If this is a sourcemap file, then it might be in the in-memory cache.
    // Attempt to lookup the file by URI.
176
    if (_sourcemaps.containsKey(requestPath)) {
177
      final List<int> bytes = getSourceMap(requestPath);
178 179 180 181
      final String etag = bytes.hashCode.toString();
      if (ifNoneMatch == etag) {
        return shelf.Response.notModified();
      }
182 183
      headers[HttpHeaders.contentLengthHeader] = bytes.length.toString();
      headers[HttpHeaders.contentTypeHeader] = 'application/json';
184
      headers[HttpHeaders.etagHeader] = etag;
185
      return shelf.Response.ok(bytes, headers: headers);
186 187
    }

188
    File file = _resolveDartFile(requestPath);
189 190

    // If all of the lookups above failed, the file might have been an asset.
191 192
    // Try and resolve the path relative to the built asset directory.
    if (!file.existsSync()) {
193
      final Uri potential = globals.fs.directory(getAssetBuildDirectory())
194
        .uri.resolve(requestPath.replaceFirst('/assets/', ''));
195
      file = globals.fs.file(potential);
196 197
    }

198 199 200 201 202 203
    if (!file.existsSync()) {
      final String webPath = globals.fs.path.join(
        globals.fs.currentDirectory.childDirectory('web').path, requestPath.substring(1));
      file = globals.fs.file(webPath);
    }

204
    if (!file.existsSync()) {
205
      return shelf.Response.notFound('');
206
    }
207 208 209 210 211 212 213

    // For real files, use a serialized file stat as a revision
    final String etag = file.lastModifiedSync().toIso8601String();
    if (ifNoneMatch == etag) {
      return shelf.Response.notModified();
    }

214 215 216 217 218 219
    final int length = file.lengthSync();
    // Attempt to determine the file's mime type. if this is not provided some
    // browsers will refuse to render images/show video et cetera. If the tool
    // cannot determine a mime type, fall back to application/octet-stream.
    String mimeType;
    if (length >= 12) {
220
      mimeType = mime.lookupMimeType(
221 222 223 224 225
        file.path,
        headerBytes: await file.openRead(0, 12).first,
      );
    }
    mimeType ??= _kDefaultMimeType;
226 227
    headers[HttpHeaders.contentLengthHeader] = length.toString();
    headers[HttpHeaders.contentTypeHeader] = mimeType;
228
    headers[HttpHeaders.etagHeader] = etag;
229
    return shelf.Response.ok(file.openRead(), headers: headers);
230 231 232 233 234 235 236 237 238 239 240 241 242 243 244
  }

  /// Tear down the http server running.
  Future<void> dispose() {
    return _httpServer.close();
  }

  /// Write a single file into the in-memory cache.
  void writeFile(String filePath, String contents) {
    _files[filePath] = Uint8List.fromList(utf8.encode(contents));
  }

  /// Update the in-memory asset server with the provided source and manifest files.
  ///
  /// Returns a list of updated modules.
245
  List<String> write(File codeFile, File manifestFile, File sourcemapFile) {
246
    final List<String> modules = <String>[];
247 248
    final Uint8List codeBytes = codeFile.readAsBytesSync();
    final Uint8List sourcemapBytes = sourcemapFile.readAsBytesSync();
249
    final Map<String, dynamic> manifest = castStringKeyedMap(json.decode(manifestFile.readAsStringSync()));
250
    for (final String filePath in manifest.keys) {
251
      if (filePath == null) {
252
        globals.printTrace('Invalid manfiest file: $filePath');
253 254
        continue;
      }
255 256 257
      final Map<String, dynamic> offsets = castStringKeyedMap(manifest[filePath]);
      final List<int> codeOffsets = (offsets['code'] as List<dynamic>).cast<int>();
      final List<int> sourcemapOffsets = (offsets['sourcemap'] as List<dynamic>).cast<int>();
258
      if (codeOffsets.length != 2 || sourcemapOffsets.length != 2) {
259
        globals.printTrace('Invalid manifest byte offsets: $offsets');
260 261
        continue;
      }
262 263 264 265

      final int codeStart = codeOffsets[0];
      final int codeEnd = codeOffsets[1];
      if (codeStart < 0 || codeEnd > codeBytes.lengthInBytes) {
266
        globals.printTrace('Invalid byte index: [$codeStart, $codeEnd]');
267 268
        continue;
      }
269 270 271 272 273
      final Uint8List byteView = Uint8List.view(
        codeBytes.buffer,
        codeStart,
        codeEnd - codeStart,
      );
274
      _files[filePath] = byteView;
275 276 277 278

      final int sourcemapStart = sourcemapOffsets[0];
      final int sourcemapEnd = sourcemapOffsets[1];
      if (sourcemapStart < 0 || sourcemapEnd > sourcemapBytes.lengthInBytes) {
279
        globals.printTrace('Invalid byte index: [$sourcemapStart, $sourcemapEnd]');
280 281 282 283 284
        continue;
      }
      final Uint8List sourcemapView = Uint8List.view(
        sourcemapBytes.buffer,
        sourcemapStart,
285
        sourcemapEnd - sourcemapStart,
286
      );
287
      _sourcemaps['$filePath.map'] = sourcemapView;
288

289 290 291 292
      modules.add(filePath);
    }
    return modules;
  }
293

294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309
  @visibleForTesting
  final File dartSdk = globals.fs.file(globals.fs.path.join(
    globals.artifacts.getArtifactPath(Artifact.flutterWebSdk),
    'kernel',
    'amd',
    'dart_sdk.js',
  ));

  @visibleForTesting
  final File dartSdkSourcemap = globals.fs.file(globals.fs.path.join(
    globals.artifacts.getArtifactPath(Artifact.flutterWebSdk),
    'kernel',
    'amd',
    'dart_sdk.js.map',
  ));

310 311
  // Attempt to resolve `path` to a dart file.
  File _resolveDartFile(String path) {
312 313 314 315 316 317 318
    // Return the actual file objects so that local engine changes are automatically picked up.
    switch (path) {
      case '/dart_sdk.js':
        return dartSdk;
      case '.dart_sdk.js.map':
        return dartSdkSourcemap;
    }
319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379
    // If this is a dart file, it must be on the local file system and is
    // likely coming from a source map request. The tool doesn't currently
    // consider the case of Dart files as assets.
    final File dartFile = globals.fs.file(globals.fs.currentDirectory.uri.resolve(path));
    if (dartFile.existsSync()) {
      return dartFile;
    }

    final List<String> segments = path.split('/');
    if (segments.first.isEmpty) {
      segments.removeAt(0);
    }

    // The file might have been a package file which is signaled by a
    // `/packages/<package>/<path>` request.
    if (segments.first == 'packages') {
      final File packageFile = globals.fs.file(_packages.resolve(Uri(
        scheme: 'package', pathSegments: segments.skip(1))));
      if (packageFile.existsSync()) {
        return packageFile;
      }
    }

    // Otherwise it must be a Dart SDK source or a Flutter Web SDK source.
    final Directory dartSdkParent = globals.fs
      .directory(globals.artifacts.getArtifactPath(Artifact.engineDartSdkPath))
      .parent;
    final File dartSdkFile = globals.fs.file(globals.fs.path
      .joinAll(<String>[dartSdkParent.path, ...segments]));
    if (dartSdkFile.existsSync()) {
      return dartSdkFile;
    }

    final String flutterWebSdk = globals.artifacts
      .getArtifactPath(Artifact.flutterWebSdk);
    final File webSdkFile = globals.fs
      .file(globals.fs.path.joinAll(<String>[flutterWebSdk, ...segments]));

    return webSdkFile;
  }

  @override
  Future<String> dartSourceContents(String serverPath) {
    final File result = _resolveDartFile(serverPath);
    if (result.existsSync()) {
      return result.readAsString();
    }
    return null;
  }

  @override
  Future<String> sourceMapContents(String serverPath) async {
    return utf8.decode(_sourcemaps[serverPath]);
  }
}

class ConnectionResult {
  ConnectionResult(this.appConnection, this.debugConnection);

  final AppConnection appConnection;
  final DebugConnection debugConnection;
380
}
381

382
/// The web specific DevFS implementation.
383
class WebDevFS implements DevFS {
384 385 386 387
  /// Create a new [WebDevFS] instance.
  ///
  /// [testMode] is true, do not actually initialize dwds or the shelf static
  /// server.
388 389 390 391 392 393 394
  WebDevFS({
    @required this.hostname,
    @required this.port,
    @required this.packagesFilePath,
    @required this.urlTunneller,
    @required this.buildMode,
    @required this.enableDwds,
395
    @required this.entrypoint,
396
    this.testMode = false,
397
  });
398

399
  final Uri entrypoint;
400 401
  final String hostname;
  final int port;
402 403 404 405
  final String packagesFilePath;
  final UrlTunneller urlTunneller;
  final BuildMode buildMode;
  final bool enableDwds;
406
  final bool testMode;
407 408 409

  @visibleForTesting
  WebAssetServer webAssetServer;
410

411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444
  Dwds get dwds => webAssetServer.dwds;

  Future<DebugConnection> _cachedExtensionFuture;
  StreamSubscription<void> _connectedApps;

  /// Connect and retrieve the [DebugConnection] for the current application.
  ///
  /// Only calls [AppConnection.runMain] on the subsequent connections.
  Future<ConnectionResult> connect(bool useDebugExtension) {
    final Completer<ConnectionResult> firstConnection = Completer<ConnectionResult>();
    _connectedApps = dwds.connectedApps.listen((AppConnection appConnection) async {
      try {
        final DebugConnection debugConnection = useDebugExtension
          ? await (_cachedExtensionFuture ??= dwds.extensionDebugConnections.stream.first)
          : await dwds.debugConnection(appConnection);
        if (firstConnection.isCompleted) {
          appConnection.runMain();
        } else {
          firstConnection.complete(ConnectionResult(appConnection, debugConnection));
        }
      } on Exception catch (error, stackTrace) {
        if (!firstConnection.isCompleted) {
          firstConnection.completeError(error, stackTrace);
        }
      }
    }, onError: (dynamic error, StackTrace stackTrace) {
      globals.printError('Unknown error while waiting for debug connection:$error\n$stackTrace');
      if (!firstConnection.isCompleted) {
        firstConnection.completeError(error, stackTrace);
      }
    });
    return firstConnection.future;
  }

445 446 447 448 449 450 451 452 453 454 455
  @override
  List<Uri> sources = <Uri>[];

  @override
  DateTime lastCompiled;

  // We do not evict assets on the web.
  @override
  Set<String> get assetPathsToEvict => const <String>{};

  @override
456 457
  Uri get baseUri => _baseUri;
  Uri _baseUri;
458 459 460

  @override
  Future<Uri> create() async {
461 462 463 464 465 466
    webAssetServer = await WebAssetServer.start(
      hostname,
      port,
      urlTunneller,
      buildMode,
      enableDwds,
467
      entrypoint,
468
      testMode: testMode,
469
    );
470 471
    _baseUri = Uri.parse('http://$hostname:$port');
    return _baseUri;
472 473 474 475
  }

  @override
  Future<void> destroy() async {
476
    await webAssetServer.dispose();
477
    await _connectedApps?.cancel();
478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504
  }

  @override
  Uri deviceUriToHostUri(Uri deviceUri) {
    return deviceUri;
  }

  @override
  String get fsName => 'web_asset';

  @override
  Directory get rootDirectory => null;

  @override
  Future<UpdateFSReport> update({
    String mainPath,
    String target,
    AssetBundle bundle,
    DateTime firstBuildTime,
    bool bundleFirstUpload = false,
    @required ResidentCompiler generator,
    String dillOutputPath,
    @required bool trackWidgetCreation,
    bool fullRestart = false,
    String projectRootPath,
    String pathToReload,
    List<Uri> invalidatedFiles,
505
    bool skipAssets = false,
506 507 508
  }) async {
    assert(trackWidgetCreation != null);
    assert(generator != null);
509 510
    final String outputDirectoryPath = globals.fs.file(mainPath).parent.path;

511
    if (bundleFirstUpload) {
512 513
      generator.addFileSystemRoot(outputDirectoryPath);
      final String entrypoint = globals.fs.path.basename(mainPath);
514
      webAssetServer.writeFile('/$entrypoint', globals.fs.file(mainPath).readAsStringSync());
515 516 517
      webAssetServer.writeFile('/manifest.json', '{"info":"manifest not generated in run mode."}');
      webAssetServer.writeFile('/flutter_service_worker.js', '// Service worker not loaded in run mode.');
      webAssetServer.writeFile(
518 519 520 521 522 523 524
        '/main.dart.js',
        generateBootstrapScript(
          requireUrl: _filePathToUriFragment(requireJS.path),
          mapperUrl: _filePathToUriFragment(stackTraceMapper.path),
          entrypoint: '/$entrypoint.lib.js',
        ),
      );
525
      webAssetServer.writeFile(
526 527 528 529 530 531 532
        '/main_module.bootstrap.js',
        generateMainModule(
          entrypoint: '/$entrypoint.lib.js',
        ),
      );
      // TODO(jonahwilliams): switch to DWDS provided APIs when they are ready.
      webAssetServer.writeFile('/basic.digests', '{}');
533

534 535
      // TODO(jonahwilliams): refactor the asset code in this and the regular devfs to
      // be shared.
536 537
      if (bundle != null) {
        await writeBundle(
538 539 540
          globals.fs.directory(getAssetBuildDirectory()),
          bundle.entries,
        );
541
      }
542 543 544 545 546
    }
    final DateTime candidateCompileTime = DateTime.now();
    if (fullRestart) {
      generator.reset();
    }
547 548 549 550 551

    // The tool generates an entrypoint file in a temp directory to handle
    // the web specific bootrstrap logic. To make it easier for DWDS to handle
    // mapping the file name, this is done via an additional file root and
    // specicial hard-coded scheme.
552
    final CompilerOutput compilerOutput = await generator.recompile(
553
     'org-dartlang-app:///' + globals.fs.path.basename(mainPath),
554
      invalidatedFiles,
555
      outputPath: dillOutputPath ??
556 557
        getDefaultApplicationKernelPath(trackWidgetCreation: trackWidgetCreation),
      packagesFilePath: packagesFilePath,
558 559 560 561
    );
    if (compilerOutput == null || compilerOutput.errorCount > 0) {
      return UpdateFSReport(success: false);
    }
562

563 564 565 566 567 568 569 570 571
    // Only update the last compiled time if we successfully compiled.
    lastCompiled = candidateCompileTime;
    // list of sources that needs to be monitored are in [compilerOutput.sources]
    sources = compilerOutput.sources;
    File codeFile;
    File manifestFile;
    File sourcemapFile;
    List<String> modules;
    try {
572 573 574 575
      final Directory parentDirectory = globals.fs.directory(outputDirectoryPath);
      codeFile = parentDirectory.childFile('${compilerOutput.outputFilename}.sources');
      manifestFile = parentDirectory.childFile('${compilerOutput.outputFilename}.json');
      sourcemapFile = parentDirectory.childFile('${compilerOutput.outputFilename}.map');
576
      modules = webAssetServer.write(codeFile, manifestFile, sourcemapFile);
577 578 579
    } on FileSystemException catch (err) {
      throwToolExit('Failed to load recompiled sources:\n$err');
    }
580

581
    return UpdateFSReport(
582 583 584 585
      success: true,
      syncedBytes: codeFile.lengthSync(),
      invalidatedSourcesCount: invalidatedFiles.length,
    )..invalidatedModules = modules;
586
  }
587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605

  @visibleForTesting
  final File requireJS = globals.fs.file(globals.fs.path.join(
    globals.artifacts.getArtifactPath(Artifact.engineDartSdkPath),
    'lib',
    'dev_compiler',
    'kernel',
    'amd',
    'require.js',
  ));

  @visibleForTesting
  final File stackTraceMapper = globals.fs.file(globals.fs.path.join(
    globals.artifacts.getArtifactPath(Artifact.engineDartSdkPath),
    'lib',
    'dev_compiler',
    'web',
    'dart_stack_trace_mapper.js',
  ));
606 607 608
}

String _filePathToUriFragment(String path) {
609
  if (globals.platform.isWindows) {
610
    final bool startWithSlash = path.startsWith('/');
611 612
    final String partial =
        globals.fs.path.split(path).skip(startWithSlash ? 2 : 1).join('/');
613 614 615 616
    if (partial.startsWith('/')) {
      return partial;
    }
    return '/$partial';
617
  }
618
  return path;
619
}
620 621

class ReleaseAssetServer {
622 623 624 625
  ReleaseAssetServer(this.entrypoint);

  final Uri entrypoint;

626 627 628
  // Locations where source files, assets, or source maps may be located.
  final List<Uri> _searchPaths = <Uri>[
    globals.fs.directory(getWebBuildDirectory()).uri,
629
    globals.fs.directory(Cache.flutterRoot).uri,
630
    globals.fs.directory(Cache.flutterRoot).parent.uri,
631
    globals.fs.currentDirectory.uri,
632
    globals.fs.directory(globals.fsUtils.homeDirPath).uri,
633 634 635 636
  ];

  Future<shelf.Response> handle(shelf.Request request) async {
    Uri fileUri;
637 638 639 640 641 642 643 644 645 646
    if (request.url.toString() == 'main.dart') {
      fileUri = entrypoint;
    } else {
      for (final Uri uri in _searchPaths) {
        final Uri potential = uri.resolve(request.url.path);
        if (potential == null || !globals.fs.isFileSync(potential.toFilePath())) {
          continue;
        }
        fileUri = potential;
        break;
647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668
      }
    }
    if (fileUri != null) {
      final File file = globals.fs.file(fileUri);
      final Uint8List bytes = file.readAsBytesSync();
      // Fallback to "application/octet-stream" on null which
      // makes no claims as to the structure of the data.
      final String mimeType = mime.lookupMimeType(file.path, headerBytes: bytes)
        ?? 'application/octet-stream';
      return shelf.Response.ok(bytes, headers: <String, String>{
        'Content-Type': mimeType,
      });
    }
    if (request.url.path == '') {
      final File file = globals.fs.file(globals.fs.path.join(getWebBuildDirectory(), 'index.html'));
      return shelf.Response.ok(file.readAsBytesSync(), headers: <String, String>{
        'Content-Type': 'text/html',
      });
    }
    return shelf.Response.notFound('');
  }
}