devfs_web.dart 35 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 6
// @dart = 2.8

7
import 'dart:async';
8 9
import 'dart:typed_data';

10 11
import 'package:dwds/data/build_result.dart';
import 'package:dwds/dwds.dart';
12 13
import 'package:html/dom.dart';
import 'package:html/parser.dart';
14
import 'package:logging/logging.dart' as logging;
15 16
import 'package:meta/meta.dart';
import 'package:mime/mime.dart' as mime;
17
import 'package:package_config/package_config.dart';
18 19
import 'package:shelf/shelf.dart' as shelf;
import 'package:shelf/shelf_io.dart' as shelf;
20
import 'package:vm_service/vm_service.dart' as vm_service;
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/logger.dart';
28
import '../base/net.dart';
29
import '../base/platform.dart';
30
import '../build_info.dart';
31
import '../build_system/targets/web.dart';
32
import '../bundle_builder.dart';
33
import '../cache.dart';
34
import '../compile.dart';
35
import '../convert.dart';
36
import '../dart/package_map.dart';
37
import '../devfs.dart';
38
import '../globals_null_migrated.dart' as globals;
39
import '../project.dart';
40
import '../vmservice.dart';
41 42
import '../web/bootstrap.dart';
import '../web/chrome.dart';
43
import '../web/compile.dart';
44
import '../web/memory_fs.dart';
45

46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62
typedef DwdsLauncher = Future<Dwds> Function({
  @required AssetReader assetReader,
  @required Stream<BuildResult> buildResults,
  @required ConnectionProvider chromeConnection,
  @required LoadStrategy loadStrategy,
  @required bool enableDebugging,
  ExpressionCompiler expressionCompiler,
  bool enableDebugExtension,
  String hostname,
  bool useSseForDebugProxy,
  bool useSseForDebugBackend,
  bool useSseForInjectedClient,
  UrlEncoder urlEncoder,
  bool spawnDds,
  bool enableDevtoolsLaunch,
  DevtoolsLauncher devtoolsLauncher,
});
63

64 65 66
// A minimal index for projects that do not yet support web.
const String _kDefaultIndex = '''
<html>
67 68 69
    <head>
        <base href="/">
    </head>
70 71 72 73 74 75
    <body>
        <script src="main.dart.js"></script>
    </body>
</html>
''';

76
/// An expression compiler connecting to FrontendServer.
77
///
78
/// This is only used in development mode.
79
class WebExpressionCompiler implements ExpressionCompiler {
80 81 82
  WebExpressionCompiler(this._generator, {
    @required FileSystem fileSystem,
  }) : _fileSystem = fileSystem;
83 84

  final ResidentCompiler _generator;
85
  final FileSystem _fileSystem;
86 87 88 89 90 91 92 93 94 95 96 97

  @override
  Future<ExpressionCompilationResult> compileExpressionToJs(
    String isolateId,
    String libraryUri,
    int line,
    int column,
    Map<String, String> jsModules,
    Map<String, String> jsFrameValues,
    String moduleName,
    String expression,
  ) async {
98 99 100
    final CompilerOutput compilerOutput =
        await _generator.compileExpressionToJs(libraryUri, line, column,
            jsModules, jsFrameValues, moduleName, expression);
101 102 103

    if (compilerOutput != null && compilerOutput.outputFilename != null) {
      final String content = utf8.decode(
104
          _fileSystem.file(compilerOutput.outputFilename).readAsBytesSync());
105 106 107 108
      return ExpressionCompilationResult(
          content, compilerOutput.errorCount > 0);
    }

109
    return ExpressionCompilationResult(
110
        "InternalError: frontend server failed to compile '$expression'",
111
        true);
112
  }
113 114

  @override
115
  Future<void> initialize({String moduleFormat, bool soundNullSafety}) async {}
116 117 118

  @override
  Future<bool> updateDependencies(Map<String, ModuleInfo> modules) async => true;
119 120
}

121 122 123
/// A web server which handles serving JavaScript and assets.
///
/// This is only used in development mode.
124
class WebAssetServer implements AssetReader {
125
  @visibleForTesting
126 127 128 129 130 131
  WebAssetServer(
    this._httpServer,
    this._packages,
    this.internetAddress,
    this._modules,
    this._digests,
132
    this._nullSafetyMode,
133 134 135
  ) : basePath = _parseBasePathFromIndexHtml(globals.fs.currentDirectory
            .childDirectory('web')
            .childFile('index.html'));
136 137 138 139 140

  // 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';

141 142 143
  final Map<String, String> _modules;
  final Map<String, String> _digests;

144 145
  int get selectedPort => _httpServer.port;

146 147 148 149 150
  void performRestart(List<String> modules) {
    for (final String module in modules) {
      // We skip computing the digest by using the hashCode of the underlying buffer.
      // Whenever a file is updated, the corresponding Uint8List.view it corresponds
      // to will change.
151 152
      final String moduleName =
          module.startsWith('/') ? module.substring(1) : module;
153 154 155
      final String name = moduleName.replaceAll('.lib.js', '');
      final String path = moduleName.replaceAll('.js', '');
      _modules[name] = path;
156
      _digests[name] = _webMemoryFS.files[moduleName].hashCode.toString();
157 158 159
    }
  }

160 161 162 163 164 165 166 167 168 169
  @visibleForTesting
  List<String> write(
    File codeFile,
    File manifestFile,
    File sourcemapFile,
    File metadataFile,
  ) {
    return _webMemoryFS.write(codeFile, manifestFile, sourcemapFile, metadataFile);
  }

170 171
  /// Start the web asset server on a [hostname] and [port].
  ///
172 173 174
  /// If [testMode] is true, do not actually initialize dwds or the shelf static
  /// server.
  ///
175 176
  /// Unhandled exceptions will throw a [ToolExit] with the error and stack
  /// trace.
177
  static Future<WebAssetServer> start(
178
    ChromiumLauncher chromiumLauncher,
179 180 181
    String hostname,
    int port,
    UrlTunneller urlTunneller,
182
    bool useSseForDebugProxy,
183
    bool useSseForDebugBackend,
184
    bool useSseForInjectedClient,
185
    BuildInfo buildInfo,
186
    bool enableDwds,
187
    bool enableDds,
188
    Uri entrypoint,
189 190
    ExpressionCompiler expressionCompiler,
    NullSafetyMode nullSafetyMode, {
191
    bool testMode = false,
192
    DwdsLauncher dwdsLauncher = Dwds.start,
193
  }) async {
194 195 196 197 198 199 200 201 202 203 204 205 206 207 208
    InternetAddress address;
    if (hostname == 'any') {
      address = InternetAddress.anyIPv4;
    } else {
      address = (await InternetAddress.lookup(hostname)).first;
    }
    HttpServer httpServer;
    dynamic lastError;
    for (int i = 0; i < 5; i += 1) {
      try {
        httpServer = await HttpServer.bind(address, port ?? await globals.os.findFreePort());
        break;
      } on SocketException catch (error) {
        lastError = error;
        await Future<void>.delayed(const Duration(milliseconds: 100));
209
      }
210 211 212 213
    }
    if (httpServer == null) {
      throwToolExit('Failed to bind web development server:\n$lastError');
    }
214

215 216
    // Allow rendering in a iframe.
    httpServer.defaultResponseHeaders.remove('x-frame-options', 'SAMEORIGIN');
217

218
    final PackageConfig packageConfig = buildInfo.packageConfig;
219 220 221 222 223 224 225 226
    final Map<String, String> digests = <String, String>{};
    final Map<String, String> modules = <String, String>{};
    final WebAssetServer server = WebAssetServer(
      httpServer,
      packageConfig,
      address,
      modules,
      digests,
227
      nullSafetyMode,
228 229 230 231
    );
    if (testMode) {
      return server;
    }
232

233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254
    // In release builds deploy a simpler proxy server.
    if (buildInfo.mode != BuildMode.debug) {
      final ReleaseAssetServer releaseAssetServer = ReleaseAssetServer(
        entrypoint,
        fileSystem: globals.fs,
        platform: globals.platform,
        flutterRoot: Cache.flutterRoot,
        webBuildDirectory: getWebBuildDirectory(),
        basePath: server.basePath,
      );
      shelf.serveRequests(httpServer, releaseAssetServer.handle);
      return server;
    }

    // Return a version string for all active modules. This is populated
    // along with the `moduleProvider` update logic.
    Future<Map<String, String>> _digestProvider() async => digests;

    // Ensure dwds is present and provide middleware to avoid trying to
    // load the through the isolate APIs.
    final Directory directory =
        await _loadDwdsDirectory(globals.fs, globals.logger);
255
    shelf.Handler middleware(FutureOr<shelf.Response> Function(shelf.Request) innerHandler) {
256 257 258 259 260 261 262 263 264 265
      return (shelf.Request request) async {
        if (request.url.path.endsWith('dwds/src/injected/client.js')) {
          final Uri uri = directory.uri.resolve('src/injected/client.js');
          final String result =
              await globals.fs.file(uri.toFilePath()).readAsString();
          return shelf.Response.ok(result, headers: <String, String>{
            HttpHeaders.contentTypeHeader: 'application/javascript'
          });
        }
        return innerHandler(request);
266
      };
267
    }
268

269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286
    logging.Logger.root.onRecord.listen((logging.LogRecord event) {
      globals.printTrace('${event.loggerName}: ${event.message}');
    });

    // In debug builds, spin up DWDS and the full asset server.
    final Dwds dwds = await dwdsLauncher(
      assetReader: server,
      enableDebugExtension: true,
      buildResults: const Stream<BuildResult>.empty(),
      chromeConnection: () async {
        final Chromium chromium = await chromiumLauncher.connectedInstance;
        return chromium.chromeConnection;
      },
      hostname: hostname,
      urlEncoder: urlTunneller,
      enableDebugging: true,
      useSseForDebugProxy: useSseForDebugProxy,
      useSseForDebugBackend: useSseForDebugBackend,
287
      useSseForInjectedClient: useSseForInjectedClient,
288 289 290 291 292 293
      loadStrategy: FrontendServerRequireStrategyProvider(
        ReloadConfiguration.none,
        server,
        _digestProvider,
      ).strategy,
      expressionCompiler: expressionCompiler,
294
      spawnDds: enableDds,
295 296 297 298 299
    );
    shelf.Pipeline pipeline = const shelf.Pipeline();
    if (enableDwds) {
      pipeline = pipeline.addMiddleware(middleware);
      pipeline = pipeline.addMiddleware(dwds.middleware);
300
    }
301 302 303 304 305 306 307
    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;
308 309
  }

310
  final NullSafetyMode _nullSafetyMode;
311
  final HttpServer _httpServer;
312 313 314
  final WebMemoryFS _webMemoryFS = WebMemoryFS();


315
  final PackageConfig _packages;
316
  final InternetAddress internetAddress;
317
  /* late final */ Dwds dwds;
318
  Directory entrypointCacheDirectory;
319

320 321 322
  @visibleForTesting
  HttpHeaders get defaultResponseHeaders => _httpServer.defaultResponseHeaders;

323
  @visibleForTesting
324
  Uint8List getFile(String path) => _webMemoryFS.files[path];
325 326

  @visibleForTesting
327
  Uint8List getSourceMap(String path) => _webMemoryFS.sourcemaps[path];
328

329
  @visibleForTesting
330
  Uint8List getMetadata(String path) => _webMemoryFS.metadataFiles[path];
331

332
  @visibleForTesting
333

334 335 336 337 338
  /// The base path to serve from.
  ///
  /// It should have no leading or trailing slashes.
  String basePath = '';

339
  // handle requests for JavaScript source, dart sources maps, or asset files.
340 341
  @visibleForTesting
  Future<shelf.Response> handleRequest(shelf.Request request) async {
342 343 344 345 346
    if (request.method != 'GET') {
      // Assets are served via GET only.
      return shelf.Response.notFound('');
    }

347 348 349 350
    final String requestPath = _stripBasePath(request.url.path, basePath);

    if (requestPath == null) {
      return shelf.Response.notFound('');
351
    }
352

353
    // If the response is `/`, then we are requesting the index file.
354 355
    if (requestPath == '/' || requestPath.isEmpty) {
      return _serveIndex();
356
    }
357

358 359
    final Map<String, String> headers = <String, String>{};

360 361 362 363
    // Track etag headers for better caching of resources.
    final String ifNoneMatch = request.headers[HttpHeaders.ifNoneMatchHeader];
    headers[HttpHeaders.cacheControlHeader] = 'max-age=0, must-revalidate';

364
    // If this is a JavaScript file, it must be in the in-memory cache.
365
    // Attempt to look up the file by URI.
366 367
    final String webServerPath =
        requestPath.replaceFirst('.dart.js', '.dart.lib.js');
368
    if (_webMemoryFS.files.containsKey(requestPath) || _webMemoryFS.files.containsKey(webServerPath)) {
369
      final List<int> bytes = getFile(requestPath) ?? getFile(webServerPath);
370 371 372 373 374 375 376
      // 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();
      }
377 378
      headers[HttpHeaders.contentLengthHeader] = bytes.length.toString();
      headers[HttpHeaders.contentTypeHeader] = 'application/javascript';
379
      headers[HttpHeaders.etagHeader] = etag;
380
      return shelf.Response.ok(bytes, headers: headers);
381
    }
382 383
    // If this is a sourcemap file, then it might be in the in-memory cache.
    // Attempt to lookup the file by URI.
384
    if (_webMemoryFS.sourcemaps.containsKey(requestPath)) {
385
      final List<int> bytes = getSourceMap(requestPath);
386 387 388 389
      final String etag = bytes.hashCode.toString();
      if (ifNoneMatch == etag) {
        return shelf.Response.notModified();
      }
390 391
      headers[HttpHeaders.contentLengthHeader] = bytes.length.toString();
      headers[HttpHeaders.contentTypeHeader] = 'application/json';
392
      headers[HttpHeaders.etagHeader] = etag;
393
      return shelf.Response.ok(bytes, headers: headers);
394 395
    }

396 397
    // If this is a metadata file, then it might be in the in-memory cache.
    // Attempt to lookup the file by URI.
398
    if (_webMemoryFS.metadataFiles.containsKey(requestPath)) {
399 400 401 402 403 404 405 406 407 408 409
      final List<int> bytes = getMetadata(requestPath);
      final String etag = bytes.hashCode.toString();
      if (ifNoneMatch == etag) {
        return shelf.Response.notModified();
      }
      headers[HttpHeaders.contentLengthHeader] = bytes.length.toString();
      headers[HttpHeaders.contentTypeHeader] = 'application/json';
      headers[HttpHeaders.etagHeader] = etag;
      return shelf.Response.ok(bytes, headers: headers);
    }

410
    File file = _resolveDartFile(requestPath);
411 412

    // If all of the lookups above failed, the file might have been an asset.
413 414
    // Try and resolve the path relative to the built asset directory.
    if (!file.existsSync()) {
415 416 417 418
      final Uri potential = globals.fs
          .directory(getAssetBuildDirectory())
          .uri
          .resolve(requestPath.replaceFirst('assets/', ''));
419
      file = globals.fs.file(potential);
420 421
    }

422
    if (!file.existsSync()) {
423
      final Uri webPath = globals.fs.currentDirectory
424 425 426
          .childDirectory('web')
          .uri
          .resolve(requestPath);
427 428 429
      file = globals.fs.file(webPath);
    }

430
    if (!file.existsSync()) {
431 432 433 434 435
      // Paths starting with these prefixes should've been resolved above.
      if (requestPath.startsWith('assets/') ||
          requestPath.startsWith('packages/')) {
        return shelf.Response.notFound('');
      }
436
      return _serveIndex();
437
    }
438

439 440
    // For real files, use a serialized file stat plus path as a revision.
    // This allows us to update between canvaskit and non-canvaskit SDKs.
441 442
    final String etag = file.lastModifiedSync().toIso8601String() +
        Uri.encodeComponent(file.path);
443 444 445 446
    if (ifNoneMatch == etag) {
      return shelf.Response.notModified();
    }

447 448
    final int length = file.lengthSync();
    // Attempt to determine the file's mime type. if this is not provided some
449
    // browsers will refuse to render images/show video etc. If the tool
450 451 452
    // cannot determine a mime type, fall back to application/octet-stream.
    String mimeType;
    if (length >= 12) {
453
      mimeType = mime.lookupMimeType(
454 455 456 457 458
        file.path,
        headerBytes: await file.openRead(0, 12).first,
      );
    }
    mimeType ??= _kDefaultMimeType;
459 460
    headers[HttpHeaders.contentLengthHeader] = length.toString();
    headers[HttpHeaders.contentTypeHeader] = mimeType;
461
    headers[HttpHeaders.etagHeader] = etag;
462
    return shelf.Response.ok(file.openRead(), headers: headers);
463 464 465
  }

  /// Tear down the http server running.
466 467
  Future<void> dispose() async {
    await dwds?.stop();
468 469 470 471 472
    return _httpServer.close();
  }

  /// Write a single file into the in-memory cache.
  void writeFile(String filePath, String contents) {
473 474 475 476
    writeBytes(filePath, utf8.encode(contents) as Uint8List);
  }

  void writeBytes(String filePath, Uint8List contents) {
477
    _webMemoryFS.files[filePath] = contents;
478
  }
479

480 481
  /// Determines what rendering backed to use.
  WebRendererMode webRenderer = WebRendererMode.html;
482

483 484 485 486 487
  shelf.Response _serveIndex() {
    final Map<String, String> headers = <String, String>{
      HttpHeaders.contentTypeHeader: 'text/html',
    };
    final File indexFile = globals.fs.currentDirectory
488 489
        .childDirectory('web')
        .childFile('index.html');
490 491

    if (indexFile.existsSync()) {
492 493 494 495 496 497
      String indexFileContent =  indexFile.readAsStringSync();
      if (indexFileContent.contains(kBaseHrefPlaceholder)) {
          indexFileContent =  indexFileContent.replaceAll(kBaseHrefPlaceholder, '/');
          headers[HttpHeaders.contentLengthHeader] = indexFileContent.length.toString();
          return shelf.Response.ok(indexFileContent,headers: headers);
        }
498 499
      headers[HttpHeaders.contentLengthHeader] =
          indexFile.lengthSync().toString();
500 501 502 503 504 505 506
      return shelf.Response.ok(indexFile.openRead(), headers: headers);
    }

    headers[HttpHeaders.contentLengthHeader] = _kDefaultIndex.length.toString();
    return shelf.Response.ok(_kDefaultIndex, headers: headers);
  }

507 508
  // Attempt to resolve `path` to a dart file.
  File _resolveDartFile(String path) {
509 510
    // Return the actual file objects so that local engine changes are automatically picked up.
    switch (path) {
511
      case 'dart_sdk.js':
512
        return _resolveDartSdkJsFile;
513
      case 'dart_sdk.js.map':
514
        return _resolveDartSdkJsMapFile;
515
    }
516 517 518 519 520
    // This is the special generated entrypoint.
    if (path == 'web_entrypoint.dart') {
      return entrypointCacheDirectory.childFile('web_entrypoint.dart');
    }

521 522 523
    // 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.
524 525
    final File dartFile =
        globals.fs.file(globals.fs.currentDirectory.uri.resolve(path));
526 527 528 529 530 531 532 533 534 535 536 537
    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') {
538 539
      final Uri filePath = _packages
          .resolve(Uri(scheme: 'package', pathSegments: segments.skip(1)));
540 541 542 543 544
      if (filePath != null) {
        final File packageFile = globals.fs.file(filePath);
        if (packageFile.existsSync()) {
          return packageFile;
        }
545 546 547 548 549
      }
    }

    // Otherwise it must be a Dart SDK source or a Flutter Web SDK source.
    final Directory dartSdkParent = globals.fs
550
        .directory(
551
            globals.artifacts.getHostArtifact(HostArtifact.engineDartSdkPath))
552
        .parent;
553
    final File dartSdkFile = globals.fs.file(dartSdkParent.uri.resolve(path));
554 555 556 557
    if (dartSdkFile.existsSync()) {
      return dartSdkFile;
    }

558
    final Directory flutterWebSdk = globals.fs
559
        .directory(globals.artifacts.getHostArtifact(HostArtifact.flutterWebSdk));
560
    final File webSdkFile = globals.fs.file(flutterWebSdk.uri.resolve(path));
561 562 563 564

    return webSdkFile;
  }

565
  File get _resolveDartSdkJsFile =>
566
      globals.fs.file(globals.artifacts.getHostArtifact(
567
          kDartSdkJsArtifactMap[webRenderer][_nullSafetyMode]
568 569 570
      ));

  File get _resolveDartSdkJsMapFile =>
571
    globals.fs.file(globals.artifacts.getHostArtifact(
572
        kDartSdkJsMapArtifactMap[webRenderer][_nullSafetyMode]
573 574
    ));

575
  @override
576
  Future<String> dartSourceContents(String serverPath) async {
577
    serverPath = _stripBasePath(serverPath, basePath);
578 579 580 581 582 583 584 585 586
    final File result = _resolveDartFile(serverPath);
    if (result.existsSync()) {
      return result.readAsString();
    }
    return null;
  }

  @override
  Future<String> sourceMapContents(String serverPath) async {
587
    serverPath = _stripBasePath(serverPath, basePath);
588
    return utf8.decode(_webMemoryFS.sourcemaps[serverPath]);
589
  }
590 591

  @override
592
  Future<String> metadataContents(String serverPath) async {
593
    serverPath = _stripBasePath(serverPath, basePath);
594
    if (serverPath == 'main_module.ddc_merged_metadata') {
595
      return _webMemoryFS.mergedMetadata;
596
    }
597 598
    if (_webMemoryFS.metadataFiles.containsKey(serverPath)) {
      return utf8.decode(_webMemoryFS.metadataFiles[serverPath]);
599
    }
600
    throw Exception('Could not find metadata contents for $serverPath');
601
  }
602 603 604

  @override
  Future<void> close() async {}
605 606 607
}

class ConnectionResult {
608
  ConnectionResult(this.appConnection, this.debugConnection, this.vmService);
609 610 611

  final AppConnection appConnection;
  final DebugConnection debugConnection;
612
  final vm_service.VmService vmService;
613
}
614

615
/// The web specific DevFS implementation.
616
class WebDevFS implements DevFS {
617 618 619 620
  /// Create a new [WebDevFS] instance.
  ///
  /// [testMode] is true, do not actually initialize dwds or the shelf static
  /// server.
621 622
  WebDevFS({
    @required this.hostname,
623
    @required int port,
624 625
    @required this.packagesFilePath,
    @required this.urlTunneller,
626
    @required this.useSseForDebugProxy,
627
    @required this.useSseForDebugBackend,
628
    @required this.useSseForInjectedClient,
629
    @required this.buildInfo,
630
    @required this.enableDwds,
631
    @required this.enableDds,
632
    @required this.entrypoint,
633
    @required this.expressionCompiler,
634
    @required this.chromiumLauncher,
635
    @required this.nullAssertions,
636
    @required this.nativeNullAssertions,
637
    @required this.nullSafetyMode,
638
    this.testMode = false,
639
  }) : _port = port;
640

641
  final Uri entrypoint;
642
  final String hostname;
643 644
  final String packagesFilePath;
  final UrlTunneller urlTunneller;
645
  final bool useSseForDebugProxy;
646
  final bool useSseForDebugBackend;
647
  final bool useSseForInjectedClient;
648
  final BuildInfo buildInfo;
649
  final bool enableDwds;
650
  final bool enableDds;
651
  final bool testMode;
652
  final ExpressionCompiler expressionCompiler;
653
  final ChromiumLauncher chromiumLauncher;
654
  final bool nullAssertions;
655
  final bool nativeNullAssertions;
656
  final int _port;
657
  final NullSafetyMode nullSafetyMode;
658 659

  WebAssetServer webAssetServer;
660

661 662
  Dwds get dwds => webAssetServer.dwds;

663 664
  Future<DebugConnection> _cachedExtensionFuture;
  StreamSubscription<void> _connectedApps;
665 666

  /// Connect and retrieve the [DebugConnection] for the current application.
667 668
  ///
  /// Only calls [AppConnection.runMain] on the subsequent connections.
669
  Future<ConnectionResult> connect(bool useDebugExtension) {
670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701
    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 {
          final vm_service.VmService vmService = await createVmServiceDelegate(
            Uri.parse(debugConnection.uri),
            logger: globals.logger,
          );
          firstConnection
              .complete(ConnectionResult(appConnection, debugConnection, vmService));
        }
      } 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;
702 703
  }

704 705 706 707 708 709
  @override
  List<Uri> sources = <Uri>[];

  @override
  DateTime lastCompiled;

710 711 712
  @override
  PackageConfig lastPackageConfig;

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

  @override
718 719
  Uri get baseUri => _baseUri;
  Uri _baseUri;
720 721 722

  @override
  Future<Uri> create() async {
723
    webAssetServer = await WebAssetServer.start(
724
      chromiumLauncher,
725
      hostname,
726
      _port,
727
      urlTunneller,
728
      useSseForDebugProxy,
729
      useSseForDebugBackend,
730
      useSseForInjectedClient,
731
      buildInfo,
732
      enableDwds,
733
      enableDds,
734
      entrypoint,
735
      expressionCompiler,
736
      nullSafetyMode,
737
      testMode: testMode,
738
    );
739
    final int selectedPort = webAssetServer.selectedPort;
740 741 742
    if (buildInfo.dartDefines.contains('FLUTTER_WEB_AUTO_DETECT=true')) {
      webAssetServer.webRenderer = WebRendererMode.autoDetect;
    } else if (buildInfo.dartDefines.contains('FLUTTER_WEB_USE_SKIA=true')) {
743
      webAssetServer.webRenderer = WebRendererMode.canvaskit;
744
    }
745
    if (hostname == 'any') {
746
      _baseUri = Uri.http('localhost:$selectedPort', webAssetServer.basePath);
747
    } else {
748
      _baseUri = Uri.http('$hostname:$selectedPort', webAssetServer.basePath);
749
    }
750
    return _baseUri;
751 752 753 754
  }

  @override
  Future<void> destroy() async {
755
    await webAssetServer.dispose();
756
    await _connectedApps?.cancel();
757 758 759 760 761 762 763 764 765 766 767 768 769 770 771
  }

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

  @override
  String get fsName => 'web_asset';

  @override
  Directory get rootDirectory => null;

  @override
  Future<UpdateFSReport> update({
772 773 774 775 776 777
    @required Uri mainUri,
    @required ResidentCompiler generator,
    @required bool trackWidgetCreation,
    @required String pathToReload,
    @required List<Uri> invalidatedFiles,
    @required PackageConfig packageConfig,
778
    @required String dillOutputPath,
779
    DevFSWriter devFSWriter,
780 781 782 783 784 785 786 787 788
    String target,
    AssetBundle bundle,
    DateTime firstBuildTime,
    bool bundleFirstUpload = false,
    bool fullRestart = false,
    String projectRootPath,
  }) async {
    assert(trackWidgetCreation != null);
    assert(generator != null);
789 790 791
    lastPackageConfig = packageConfig;
    final File mainFile = globals.fs.file(mainUri);
    final String outputDirectoryPath = mainFile.parent.path;
792

793
    if (bundleFirstUpload) {
794 795
      webAssetServer.entrypointCacheDirectory =
          globals.fs.directory(outputDirectoryPath);
796
      generator.addFileSystemRoot(outputDirectoryPath);
797 798 799
      final String entrypoint = globals.fs.path.basename(mainFile.path);
      webAssetServer.writeBytes(entrypoint, mainFile.readAsBytesSync());
      webAssetServer.writeBytes('require.js', requireJS.readAsBytesSync());
800 801 802 803 804 805 806 807
      webAssetServer.writeBytes(
          'stack_trace_mapper.js', stackTraceMapper.readAsBytesSync());
      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(
          'version.json', FlutterProject.current().getVersionInfo());
808
      webAssetServer.writeFile(
809
        'main.dart.js',
810
        generateBootstrapScript(
811 812
          requireUrl: 'require.js',
          mapperUrl: 'stack_trace_mapper.js',
813 814
        ),
      );
815
      webAssetServer.writeFile(
816
        'main_module.bootstrap.js',
817
        generateMainModule(
818
          entrypoint: entrypoint,
819
          nullAssertions: nullAssertions,
820
          nativeNullAssertions: nativeNullAssertions,
821 822
        ),
      );
823 824
      // TODO(jonahwilliams): refactor the asset code in this and the regular devfs to
      // be shared.
825 826
      if (bundle != null) {
        await writeBundle(
827 828 829
          globals.fs.directory(getAssetBuildDirectory()),
          bundle.entries,
        );
830
      }
831 832 833 834 835
    }
    final DateTime candidateCompileTime = DateTime.now();
    if (fullRestart) {
      generator.reset();
    }
836 837

    // The tool generates an entrypoint file in a temp directory to handle
838
    // the web specific bootstrap logic. To make it easier for DWDS to handle
839
    // mapping the file name, this is done via an additional file root and
840
    // special hard-coded scheme.
841
    final CompilerOutput compilerOutput = await generator.recompile(
842 843
      Uri(
        scheme: 'org-dartlang-app',
844
        path: '/${mainUri.pathSegments.last}',
845
      ),
846
      invalidatedFiles,
847
      outputPath: dillOutputPath,
848
      packageConfig: packageConfig,
849 850
      projectRootPath: projectRootPath,
      fs: globals.fs,
851 852 853 854
    );
    if (compilerOutput == null || compilerOutput.errorCount > 0) {
      return UpdateFSReport(success: false);
    }
855

856 857 858 859 860 861 862
    // 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;
863
    File metadataFile;
864 865
    List<String> modules;
    try {
866 867 868 869 870 871
      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');
      metadataFile = parentDirectory.childFile('${compilerOutput.outputFilename}.metadata');
      modules = webAssetServer._webMemoryFS.write(codeFile, manifestFile, sourcemapFile, metadataFile);
872 873 874
    } on FileSystemException catch (err) {
      throwToolExit('Failed to load recompiled sources:\n$err');
    }
875
    webAssetServer.performRestart(modules);
876
    return UpdateFSReport(
877 878 879
      success: true,
      syncedBytes: codeFile.lengthSync(),
      invalidatedSourcesCount: invalidatedFiles.length,
880
    );
881
  }
882 883 884

  @visibleForTesting
  final File requireJS = globals.fs.file(globals.fs.path.join(
885
    globals.artifacts.getHostArtifact(HostArtifact.engineDartSdkPath).path,
886 887 888 889 890 891 892 893 894
    'lib',
    'dev_compiler',
    'kernel',
    'amd',
    'require.js',
  ));

  @visibleForTesting
  final File stackTraceMapper = globals.fs.file(globals.fs.path.join(
895
    globals.artifacts.getHostArtifact(HostArtifact.engineDartSdkPath).path,
896 897 898 899 900
    'lib',
    'dev_compiler',
    'web',
    'dart_stack_trace_mapper.js',
  ));
901 902 903 904 905

  @override
  void resetLastCompiled() {
    // Not used for web compilation.
  }
906 907
}

908
class ReleaseAssetServer {
909 910
  ReleaseAssetServer(
    this.entrypoint, {
911 912 913 914
    @required FileSystem fileSystem,
    @required String webBuildDirectory,
    @required String flutterRoot,
    @required Platform platform,
915
    this.basePath = '',
916 917 918 919 920 921
  })  : _fileSystem = fileSystem,
        _platform = platform,
        _flutterRoot = flutterRoot,
        _webBuildDirectory = webBuildDirectory,
        _fileSystemUtils =
            FileSystemUtils(fileSystem: fileSystem, platform: platform);
922 923

  final Uri entrypoint;
924 925 926 927 928
  final String _flutterRoot;
  final String _webBuildDirectory;
  final FileSystem _fileSystem;
  final FileSystemUtils _fileSystemUtils;
  final Platform _platform;
929

930
  @visibleForTesting
931

932 933 934 935 936
  /// The base path to serve from.
  ///
  /// It should have no leading or trailing slashes.
  final String basePath;

937
  // Locations where source files, assets, or source maps may be located.
938
  List<Uri> _searchPaths() => <Uri>[
939 940 941 942 943 944
        _fileSystem.directory(_webBuildDirectory).uri,
        _fileSystem.directory(_flutterRoot).uri,
        _fileSystem.directory(_flutterRoot).parent.uri,
        _fileSystem.currentDirectory.uri,
        _fileSystem.directory(_fileSystemUtils.homeDirPath).uri,
      ];
945 946

  Future<shelf.Response> handle(shelf.Request request) async {
947 948 949 950 951
    if (request.method != 'GET') {
      // Assets are served via GET only.
      return shelf.Response.notFound('');
    }

952
    Uri fileUri;
953 954 955 956 957 958
    final String requestPath = _stripBasePath(request.url.path, basePath);

    if (requestPath == null) {
      return shelf.Response.notFound('');
    }

959 960 961
    if (request.url.toString() == 'main.dart') {
      fileUri = entrypoint;
    } else {
962
      for (final Uri uri in _searchPaths()) {
963
        final Uri potential = uri.resolve(requestPath);
964 965 966
        if (potential == null ||
            !_fileSystem.isFileSync(
                potential.toFilePath(windows: _platform.isWindows))) {
967 968 969 970
          continue;
        }
        fileUri = potential;
        break;
971 972 973
      }
    }
    if (fileUri != null) {
974
      final File file = _fileSystem.file(fileUri);
975 976 977
      final Uint8List bytes = file.readAsBytesSync();
      // Fallback to "application/octet-stream" on null which
      // makes no claims as to the structure of the data.
978 979 980
      final String mimeType =
          mime.lookupMimeType(file.path, headerBytes: bytes) ??
              'application/octet-stream';
981 982 983 984
      return shelf.Response.ok(bytes, headers: <String, String>{
        'Content-Type': mimeType,
      });
    }
985

986 987
    final File file = _fileSystem
        .file(_fileSystem.path.join(_webBuildDirectory, 'index.html'));
988 989 990
    return shelf.Response.ok(file.readAsBytesSync(), headers: <String, String>{
      'Content-Type': 'text/html',
    });
991 992
  }
}
993

994 995 996 997 998
Future<Directory> _loadDwdsDirectory(
    FileSystem fileSystem, Logger logger) async {
  final String toolPackagePath =
      fileSystem.path.join(Cache.flutterRoot, 'packages', 'flutter_tools');
  final String packageFilePath =
999
      fileSystem.path.join(toolPackagePath, '.dart_tool', 'package_config.json');
1000 1001 1002 1003 1004 1005
  final PackageConfig packageConfig = await loadPackageConfigWithLogging(
    fileSystem.file(packageFilePath),
    logger: logger,
  );
  return fileSystem.directory(packageConfig['dwds'].packageUriRoot);
}
1006 1007

String _stripBasePath(String path, String basePath) {
1008
  path = _stripLeadingSlashes(path);
1009 1010 1011 1012 1013 1014
  if (path.startsWith(basePath)) {
    path = path.substring(basePath.length);
  } else {
    // The given path isn't under base path, return null to indicate that.
    return null;
  }
1015 1016 1017 1018
  return _stripLeadingSlashes(path);
}

String _stripLeadingSlashes(String path) {
1019 1020 1021 1022 1023
  while (path.startsWith('/')) {
    path = path.substring(1);
  }
  return path;
}
1024 1025 1026 1027 1028 1029 1030 1031 1032

String _stripTrailingSlashes(String path) {
  while (path.endsWith('/')) {
    path = path.substring(0, path.length - 1);
  }
  return path;
}

String _parseBasePathFromIndexHtml(File indexHtml) {
1033 1034
  final String htmlContent =
      indexHtml.existsSync() ? indexHtml.readAsStringSync() : _kDefaultIndex;
1035 1036
  final Document document = parse(htmlContent);
  final Element baseElement = document.querySelector('base');
1037 1038
  String baseHref =
      baseElement?.attributes == null ? null : baseElement.attributes['href'];
1039

1040
  if (baseHref == null || baseHref == kBaseHrefPlaceholder) {
1041 1042 1043 1044 1045 1046 1047 1048 1049 1050 1051 1052 1053 1054 1055 1056 1057 1058 1059 1060 1061 1062 1063 1064 1065 1066 1067 1068 1069 1070
    baseHref = '';
  } else if (!baseHref.startsWith('/')) {
    throw ToolExit(
      'Error: The base href in "web/index.html" must be absolute (i.e. start '
      'with a "/"), but found: `${baseElement.outerHtml}`.\n'
      '$basePathExample',
    );
  } else if (!baseHref.endsWith('/')) {
    throw ToolExit(
      'Error: The base href in "web/index.html" must end with a "/", but found: `${baseElement.outerHtml}`.\n'
      '$basePathExample',
    );
  } else {
    baseHref = _stripLeadingSlashes(_stripTrailingSlashes(baseHref));
  }

  return baseHref;
}

const String basePathExample = '''
For example, to serve from the root use:

    <base href="/">

To serve from a subpath "foo" (i.e. http://localhost:8080/foo/ instead of http://localhost:8080/) use:

    <base href="/foo/">

For more information, see: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/base
''';