devfs_web.dart 36.5 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
import 'package:dwds/data/build_result.dart';
9
// ignore: import_of_legacy_library_into_null_safe
10
import 'package:dwds/dwds.dart';
11 12
import 'package:html/dom.dart';
import 'package:html/parser.dart';
13
import 'package:logging/logging.dart' as logging;
14 15
import 'package:meta/meta.dart';
import 'package:mime/mime.dart' as mime;
16
import 'package:package_config/package_config.dart';
17 18
import 'package:shelf/shelf.dart' as shelf;
import 'package:shelf/shelf_io.dart' as shelf;
19
import 'package:vm_service/vm_service.dart' as vm_service;
20

21 22
import '../artifacts.dart';
import '../asset.dart';
23 24 25
import '../base/common.dart';
import '../base/file_system.dart';
import '../base/io.dart';
26
import '../base/logger.dart';
27
import '../base/net.dart';
28
import '../base/platform.dart';
29
import '../build_info.dart';
30
import '../build_system/targets/web.dart';
31
import '../bundle_builder.dart';
32
import '../cache.dart';
33
import '../compile.dart';
34
import '../convert.dart';
35
import '../dart/package_map.dart';
36
import '../devfs.dart';
37
import '../globals.dart' as globals;
38
import '../project.dart';
39
import '../vmservice.dart';
40 41
import '../web/bootstrap.dart';
import '../web/chrome.dart';
42
import '../web/compile.dart';
43
import '../web/file_generators/flutter_js.dart' as flutter_js;
44
import '../web/memory_fs.dart';
45
import 'sdk_web_configuration.dart';
46

47
typedef DwdsLauncher = Future<Dwds> Function({
48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65
  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,
  bool? launchDevToolsInNewWindow,
  SdkConfigurationProvider? sdkConfigurationProvider,
  bool? emitDebugEvents,
66
});
67

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

80
/// An expression compiler connecting to FrontendServer.
81
///
82
/// This is only used in development mode.
83
class WebExpressionCompiler implements ExpressionCompiler {
84
  WebExpressionCompiler(this._generator, {
85
    required FileSystem? fileSystem,
86
  }) : _fileSystem = fileSystem;
87 88

  final ResidentCompiler _generator;
89
  final FileSystem? _fileSystem;
90 91 92 93 94 95 96 97 98 99 100 101

  @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 {
102
    final CompilerOutput? compilerOutput =
103 104
        await _generator.compileExpressionToJs(libraryUri, line, column,
            jsModules, jsFrameValues, moduleName, expression);
105 106 107

    if (compilerOutput != null && compilerOutput.outputFilename != null) {
      final String content = utf8.decode(
108
          _fileSystem!.file(compilerOutput.outputFilename).readAsBytesSync());
109 110 111 112
      return ExpressionCompilationResult(
          content, compilerOutput.errorCount > 0);
    }

113
    return ExpressionCompilationResult(
114
        "InternalError: frontend server failed to compile '$expression'",
115
        true);
116
  }
117 118

  @override
119
  Future<void> initialize({String? moduleFormat, bool? soundNullSafety}) async {}
120 121 122

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

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

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

145 146 147
  final Map<String, String> _modules;
  final Map<String, String> _digests;

148 149
  int get selectedPort => _httpServer.port;

150 151 152 153 154
  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.
155 156
      final String moduleName =
          module.startsWith('/') ? module.substring(1) : module;
157 158 159
      final String name = moduleName.replaceAll('.lib.js', '');
      final String path = moduleName.replaceAll('.js', '');
      _modules[name] = path;
160
      _digests[name] = _webMemoryFS.files[moduleName].hashCode.toString();
161 162 163
    }
  }

164 165 166 167 168 169 170 171 172 173
  @visibleForTesting
  List<String> write(
    File codeFile,
    File manifestFile,
    File sourcemapFile,
    File metadataFile,
  ) {
    return _webMemoryFS.write(codeFile, manifestFile, sourcemapFile, metadataFile);
  }

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

219
    // Allow rendering in a iframe.
220
    httpServer!.defaultResponseHeaders.remove('x-frame-options', 'SAMEORIGIN');
221

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

237 238 239 240 241 242 243 244 245 246
    // 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,
      );
247
      runZonedGuarded(() {
248
        shelf.serveRequests(httpServer!, releaseAssetServer.handle);
249 250 251
      }, (Object e, StackTrace s) {
        globals.printTrace('Release asset server: error serving requests: $e:$s');
      });
252 253 254 255 256
      return server;
    }

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

    // 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);
263
    shelf.Handler middleware(FutureOr<shelf.Response> Function(shelf.Request) innerHandler) {
264 265 266 267 268 269
      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>{
270
            HttpHeaders.contentTypeHeader: 'application/javascript',
271 272 273
          });
        }
        return innerHandler(request);
274
      };
275
    }
276

277
    logging.Logger.root.level = logging.Level.ALL;
278
    logging.Logger.root.onRecord.listen(log);
279 280 281 282 283 284 285

    // 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 {
286
        final Chromium chromium = await chromiumLauncher!.connectedInstance;
287 288 289 290 291 292 293
        return chromium.chromeConnection;
      },
      hostname: hostname,
      urlEncoder: urlTunneller,
      enableDebugging: true,
      useSseForDebugProxy: useSseForDebugProxy,
      useSseForDebugBackend: useSseForDebugBackend,
294
      useSseForInjectedClient: useSseForInjectedClient,
295 296 297
      loadStrategy: FrontendServerRequireStrategyProvider(
        ReloadConfiguration.none,
        server,
298
        digestProvider,
299
        server.basePath!,
300 301
      ).strategy,
      expressionCompiler: expressionCompiler,
302
      spawnDds: enableDds,
303
      sdkConfigurationProvider: SdkWebConfigurationProvider(globals.artifacts!),
304 305 306 307 308
    );
    shelf.Pipeline pipeline = const shelf.Pipeline();
    if (enableDwds) {
      pipeline = pipeline.addMiddleware(middleware);
      pipeline = pipeline.addMiddleware(dwds.middleware);
309
    }
310 311 312 313
    final shelf.Handler dwdsHandler =
        pipeline.addHandler(server.handleRequest);
    final shelf.Cascade cascade =
        shelf.Cascade().add(dwds.handler).add(dwdsHandler);
314
    runZonedGuarded(() {
315
      shelf.serveRequests(httpServer!, cascade.handler);
316 317 318
    }, (Object e, StackTrace s) {
      globals.printTrace('Dwds server: error serving requests: $e:$s');
    });
319
    server.dwds = dwds;
320
    server._dwdsInit = true;
321
    return server;
322 323
  }

324
  final NullSafetyMode _nullSafetyMode;
325
  final HttpServer _httpServer;
326
  final WebMemoryFS _webMemoryFS = WebMemoryFS();
327
  final PackageConfig _packages;
328
  final InternetAddress internetAddress;
329 330 331
  late final Dwds dwds;
  late Directory entrypointCacheDirectory;
  bool _dwdsInit = false;
332

333 334 335
  @visibleForTesting
  HttpHeaders get defaultResponseHeaders => _httpServer.defaultResponseHeaders;

336
  @visibleForTesting
337
  Uint8List? getFile(String path) => _webMemoryFS.files[path];
338 339

  @visibleForTesting
340
  Uint8List? getSourceMap(String path) => _webMemoryFS.sourcemaps[path];
341

342
  @visibleForTesting
343
  Uint8List? getMetadata(String path) => _webMemoryFS.metadataFiles[path];
344

345
  @visibleForTesting
346

347 348 349
  /// The base path to serve from.
  ///
  /// It should have no leading or trailing slashes.
350
  String? basePath = '';
351

352
  // handle requests for JavaScript source, dart sources maps, or asset files.
353 354
  @visibleForTesting
  Future<shelf.Response> handleRequest(shelf.Request request) async {
355 356 357 358 359
    if (request.method != 'GET') {
      // Assets are served via GET only.
      return shelf.Response.notFound('');
    }

360
    final String? requestPath = _stripBasePath(request.url.path, basePath);
361 362 363

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

366
    // If the response is `/`, then we are requesting the index file.
367 368
    if (requestPath == '/' || requestPath.isEmpty) {
      return _serveIndex();
369
    }
370

371 372
    final Map<String, String> headers = <String, String>{};

373
    // Track etag headers for better caching of resources.
374
    final String? ifNoneMatch = request.headers[HttpHeaders.ifNoneMatchHeader];
375 376
    headers[HttpHeaders.cacheControlHeader] = 'max-age=0, must-revalidate';

377
    // If this is a JavaScript file, it must be in the in-memory cache.
378
    // Attempt to look up the file by URI.
379 380
    final String webServerPath =
        requestPath.replaceFirst('.dart.js', '.dart.lib.js');
381
    if (_webMemoryFS.files.containsKey(requestPath) || _webMemoryFS.files.containsKey(webServerPath)) {
382
      final List<int>? bytes = getFile(requestPath) ?? getFile(webServerPath);
383 384 385 386 387 388 389
      // 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();
      }
390
      headers[HttpHeaders.contentLengthHeader] = bytes!.length.toString();
391
      headers[HttpHeaders.contentTypeHeader] = 'application/javascript';
392
      headers[HttpHeaders.etagHeader] = etag;
393
      return shelf.Response.ok(bytes, headers: headers);
394
    }
395 396
    // If this is a sourcemap file, then it might be in the in-memory cache.
    // Attempt to lookup the file by URI.
397
    if (_webMemoryFS.sourcemaps.containsKey(requestPath)) {
398
      final List<int>? bytes = getSourceMap(requestPath);
399 400 401 402
      final String etag = bytes.hashCode.toString();
      if (ifNoneMatch == etag) {
        return shelf.Response.notModified();
      }
403
      headers[HttpHeaders.contentLengthHeader] = bytes!.length.toString();
404
      headers[HttpHeaders.contentTypeHeader] = 'application/json';
405
      headers[HttpHeaders.etagHeader] = etag;
406
      return shelf.Response.ok(bytes, headers: headers);
407 408
    }

409 410
    // If this is a metadata file, then it might be in the in-memory cache.
    // Attempt to lookup the file by URI.
411
    if (_webMemoryFS.metadataFiles.containsKey(requestPath)) {
412
      final List<int>? bytes = getMetadata(requestPath);
413 414 415 416
      final String etag = bytes.hashCode.toString();
      if (ifNoneMatch == etag) {
        return shelf.Response.notModified();
      }
417
      headers[HttpHeaders.contentLengthHeader] = bytes!.length.toString();
418 419 420 421 422
      headers[HttpHeaders.contentTypeHeader] = 'application/json';
      headers[HttpHeaders.etagHeader] = etag;
      return shelf.Response.ok(bytes, headers: headers);
    }

423
    File file = _resolveDartFile(requestPath);
424 425

    // If all of the lookups above failed, the file might have been an asset.
426 427
    // Try and resolve the path relative to the built asset directory.
    if (!file.existsSync()) {
428 429 430 431
      final Uri potential = globals.fs
          .directory(getAssetBuildDirectory())
          .uri
          .resolve(requestPath.replaceFirst('assets/', ''));
432
      file = globals.fs.file(potential);
433 434
    }

435
    if (!file.existsSync()) {
436
      final Uri webPath = globals.fs.currentDirectory
437 438 439
          .childDirectory('web')
          .uri
          .resolve(requestPath);
440 441 442
      file = globals.fs.file(webPath);
    }

443
    if (!file.existsSync()) {
444 445 446 447 448
      // Paths starting with these prefixes should've been resolved above.
      if (requestPath.startsWith('assets/') ||
          requestPath.startsWith('packages/')) {
        return shelf.Response.notFound('');
      }
449
      return _serveIndex();
450
    }
451

452 453
    // For real files, use a serialized file stat plus path as a revision.
    // This allows us to update between canvaskit and non-canvaskit SDKs.
454 455
    final String etag = file.lastModifiedSync().toIso8601String() +
        Uri.encodeComponent(file.path);
456 457 458 459
    if (ifNoneMatch == etag) {
      return shelf.Response.notModified();
    }

460 461
    final int length = file.lengthSync();
    // Attempt to determine the file's mime type. if this is not provided some
462
    // browsers will refuse to render images/show video etc. If the tool
463
    // cannot determine a mime type, fall back to application/octet-stream.
464
    final String mimeType = mime.lookupMimeType(
465
        file.path,
466 467 468
        headerBytes: await file.openRead(0, mime.defaultMagicNumbersMaxLength).first,
    ) ?? _kDefaultMimeType;

469 470
    headers[HttpHeaders.contentLengthHeader] = length.toString();
    headers[HttpHeaders.contentTypeHeader] = mimeType;
471
    headers[HttpHeaders.etagHeader] = etag;
472
    return shelf.Response.ok(file.openRead(), headers: headers);
473 474 475
  }

  /// Tear down the http server running.
476
  Future<void> dispose() async {
477 478 479
    if (_dwdsInit) {
      await dwds.stop();
    }
480 481 482 483 484
    return _httpServer.close();
  }

  /// Write a single file into the in-memory cache.
  void writeFile(String filePath, String contents) {
485 486 487 488
    writeBytes(filePath, utf8.encode(contents) as Uint8List);
  }

  void writeBytes(String filePath, Uint8List contents) {
489
    _webMemoryFS.files[filePath] = contents;
490
  }
491

492 493
  /// Determines what rendering backed to use.
  WebRendererMode webRenderer = WebRendererMode.html;
494

495 496 497 498 499
  shelf.Response _serveIndex() {
    final Map<String, String> headers = <String, String>{
      HttpHeaders.contentTypeHeader: 'text/html',
    };
    final File indexFile = globals.fs.currentDirectory
500 501
        .childDirectory('web')
        .childFile('index.html');
502 503

    if (indexFile.existsSync()) {
504 505 506 507 508 509
      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);
        }
510 511
      headers[HttpHeaders.contentLengthHeader] =
          indexFile.lengthSync().toString();
512 513 514 515 516 517 518
      return shelf.Response.ok(indexFile.openRead(), headers: headers);
    }

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

519 520
  // Attempt to resolve `path` to a dart file.
  File _resolveDartFile(String path) {
521 522
    // Return the actual file objects so that local engine changes are automatically picked up.
    switch (path) {
523
      case 'dart_sdk.js':
524
        return _resolveDartSdkJsFile;
525
      case 'dart_sdk.js.map':
526
        return _resolveDartSdkJsMapFile;
527
    }
528 529 530 531 532
    // This is the special generated entrypoint.
    if (path == 'web_entrypoint.dart') {
      return entrypointCacheDirectory.childFile('web_entrypoint.dart');
    }

533 534 535
    // 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.
536 537
    final File dartFile =
        globals.fs.file(globals.fs.currentDirectory.uri.resolve(path));
538 539 540 541 542 543 544 545 546 547 548 549
    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') {
550
      final Uri? filePath = _packages
551
          .resolve(Uri(scheme: 'package', pathSegments: segments.skip(1)));
552 553 554 555 556
      if (filePath != null) {
        final File packageFile = globals.fs.file(filePath);
        if (packageFile.existsSync()) {
          return packageFile;
        }
557 558 559 560 561
      }
    }

    // Otherwise it must be a Dart SDK source or a Flutter Web SDK source.
    final Directory dartSdkParent = globals.fs
562
        .directory(
563
            globals.artifacts!.getHostArtifact(HostArtifact.engineDartSdkPath))
564
        .parent;
565
    final File dartSdkFile = globals.fs.file(dartSdkParent.uri.resolve(path));
566 567 568 569
    if (dartSdkFile.existsSync()) {
      return dartSdkFile;
    }

570
    final Directory flutterWebSdk = globals.fs
571
        .directory(globals.artifacts!.getHostArtifact(HostArtifact.flutterWebSdk));
572
    final File webSdkFile = globals.fs.file(flutterWebSdk.uri.resolve(path));
573 574 575 576

    return webSdkFile;
  }

577
  File get _resolveDartSdkJsFile =>
578 579
      globals.fs.file(globals.artifacts!.getHostArtifact(
          kDartSdkJsArtifactMap[webRenderer]![_nullSafetyMode]!
580 581 582
      ));

  File get _resolveDartSdkJsMapFile =>
583 584
    globals.fs.file(globals.artifacts!.getHostArtifact(
        kDartSdkJsMapArtifactMap[webRenderer]![_nullSafetyMode]!
585 586
    ));

587
  @override
588 589
  Future<String?> dartSourceContents(String serverPath) async {
    serverPath = _stripBasePath(serverPath, basePath)!;
590 591 592 593 594 595 596 597 598
    final File result = _resolveDartFile(serverPath);
    if (result.existsSync()) {
      return result.readAsString();
    }
    return null;
  }

  @override
  Future<String> sourceMapContents(String serverPath) async {
599 600
    serverPath = _stripBasePath(serverPath, basePath)!;
    return utf8.decode(_webMemoryFS.sourcemaps[serverPath]!);
601
  }
602 603

  @override
604 605 606
  Future<String?> metadataContents(String serverPath) async {
    final String? resultPath = _stripBasePath(serverPath, basePath);
    if (resultPath == 'main_module.ddc_merged_metadata') {
607
      return _webMemoryFS.mergedMetadata;
608
    }
609 610
    if (_webMemoryFS.metadataFiles.containsKey(resultPath)) {
      return utf8.decode(_webMemoryFS.metadataFiles[resultPath]!);
611
    }
612
    throw Exception('Could not find metadata contents for $serverPath');
613
  }
614 615 616

  @override
  Future<void> close() async {}
617 618 619
}

class ConnectionResult {
620
  ConnectionResult(this.appConnection, this.debugConnection, this.vmService);
621

622 623
  final AppConnection? appConnection;
  final DebugConnection? debugConnection;
624
  final vm_service.VmService vmService;
625
}
626

627
/// The web specific DevFS implementation.
628
class WebDevFS implements DevFS {
629 630 631 632
  /// Create a new [WebDevFS] instance.
  ///
  /// [testMode] is true, do not actually initialize dwds or the shelf static
  /// server.
633
  WebDevFS({
634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649
    required this.hostname,
    required int? port,
    required this.packagesFilePath,
    required this.urlTunneller,
    required this.useSseForDebugProxy,
    required this.useSseForDebugBackend,
    required this.useSseForInjectedClient,
    required this.buildInfo,
    required this.enableDwds,
    required this.enableDds,
    required this.entrypoint,
    required this.expressionCompiler,
    required this.chromiumLauncher,
    required this.nullAssertions,
    required this.nativeNullAssertions,
    required this.nullSafetyMode,
650
    this.testMode = false,
651
  }) : _port = port;
652

653
  final Uri entrypoint;
654
  final String hostname;
655
  final String packagesFilePath;
656
  final UrlTunneller? urlTunneller;
657
  final bool useSseForDebugProxy;
658
  final bool useSseForDebugBackend;
659
  final bool useSseForInjectedClient;
660
  final BuildInfo buildInfo;
661
  final bool enableDwds;
662
  final bool enableDds;
663
  final bool testMode;
664 665
  final ExpressionCompiler? expressionCompiler;
  final ChromiumLauncher? chromiumLauncher;
666
  final bool nullAssertions;
667
  final bool nativeNullAssertions;
668
  final int? _port;
669
  final NullSafetyMode nullSafetyMode;
670

671
  late WebAssetServer webAssetServer;
672

673 674
  Dwds get dwds => webAssetServer.dwds;

675 676 677 678
  // A flag to indicate whether we have called `setAssetDirectory` on the target device.
  @override
  bool hasSetAssetDirectory = false;

679 680
  Future<DebugConnection>? _cachedExtensionFuture;
  StreamSubscription<void>? _connectedApps;
681 682

  /// Connect and retrieve the [DebugConnection] for the current application.
683 684
  ///
  /// Only calls [AppConnection.runMain] on the subsequent connections.
685
  Future<ConnectionResult?> connect(bool useDebugExtension) {
686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709
    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);
        }
      }
710
    }, onError: (Object error, StackTrace stackTrace) {
711 712 713 714 715 716 717
      globals.printError(
          'Unknown error while waiting for debug connection:$error\n$stackTrace');
      if (!firstConnection.isCompleted) {
        firstConnection.completeError(error, stackTrace);
      }
    });
    return firstConnection.future;
718 719
  }

720 721 722 723
  @override
  List<Uri> sources = <Uri>[];

  @override
724
  DateTime? lastCompiled;
725

726
  @override
727
  PackageConfig? lastPackageConfig;
728

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

  @override
734 735
  Uri? get baseUri => _baseUri;
  Uri? _baseUri;
736 737 738

  @override
  Future<Uri> create() async {
739
    webAssetServer = await WebAssetServer.start(
740
      chromiumLauncher,
741
      hostname,
742
      _port,
743
      urlTunneller,
744
      useSseForDebugProxy,
745
      useSseForDebugBackend,
746
      useSseForInjectedClient,
747
      buildInfo,
748
      enableDwds,
749
      enableDds,
750
      entrypoint,
751
      expressionCompiler,
752
      nullSafetyMode,
753
      testMode: testMode,
754
    );
755
    final int selectedPort = webAssetServer.selectedPort;
756 757 758
    if (buildInfo.dartDefines.contains('FLUTTER_WEB_AUTO_DETECT=true')) {
      webAssetServer.webRenderer = WebRendererMode.autoDetect;
    } else if (buildInfo.dartDefines.contains('FLUTTER_WEB_USE_SKIA=true')) {
759
      webAssetServer.webRenderer = WebRendererMode.canvaskit;
760
    }
761
    if (hostname == 'any') {
762
      _baseUri = Uri.http('localhost:$selectedPort', webAssetServer.basePath!);
763
    } else {
764
      _baseUri = Uri.http('$hostname:$selectedPort', webAssetServer.basePath!);
765
    }
766
    return _baseUri!;
767 768 769 770
  }

  @override
  Future<void> destroy() async {
771
    await webAssetServer.dispose();
772
    await _connectedApps?.cancel();
773 774 775 776 777 778 779 780 781 782 783
  }

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

  @override
  String get fsName => 'web_asset';

  @override
784
  Directory? get rootDirectory => null;
785 786 787

  @override
  Future<UpdateFSReport> update({
788 789 790 791 792 793 794 795 796 797 798
    required Uri mainUri,
    required ResidentCompiler generator,
    required bool trackWidgetCreation,
    required String pathToReload,
    required List<Uri> invalidatedFiles,
    required PackageConfig packageConfig,
    required String dillOutputPath,
    DevFSWriter? devFSWriter,
    String? target,
    AssetBundle? bundle,
    DateTime? firstBuildTime,
799 800
    bool bundleFirstUpload = false,
    bool fullRestart = false,
801
    String? projectRootPath,
802 803 804
  }) async {
    assert(trackWidgetCreation != null);
    assert(generator != null);
805 806 807
    lastPackageConfig = packageConfig;
    final File mainFile = globals.fs.file(mainUri);
    final String outputDirectoryPath = mainFile.parent.path;
808

809
    if (bundleFirstUpload) {
810 811
      webAssetServer.entrypointCacheDirectory =
          globals.fs.directory(outputDirectoryPath);
812
      generator.addFileSystemRoot(outputDirectoryPath);
813 814 815
      final String entrypoint = globals.fs.path.basename(mainFile.path);
      webAssetServer.writeBytes(entrypoint, mainFile.readAsBytesSync());
      webAssetServer.writeBytes('require.js', requireJS.readAsBytesSync());
816 817 818 819
      webAssetServer.writeBytes(
          'stack_trace_mapper.js', stackTraceMapper.readAsBytesSync());
      webAssetServer.writeFile(
          'manifest.json', '{"info":"manifest not generated in run mode."}');
820
      webAssetServer.writeFile('flutter.js', flutter_js.generateFlutterJsFile());
821 822 823 824
      webAssetServer.writeFile('flutter_service_worker.js',
          '// Service worker not loaded in run mode.');
      webAssetServer.writeFile(
          'version.json', FlutterProject.current().getVersionInfo());
825
      webAssetServer.writeFile(
826
        'main.dart.js',
827
        generateBootstrapScript(
828 829
          requireUrl: 'require.js',
          mapperUrl: 'stack_trace_mapper.js',
830 831
        ),
      );
832
      webAssetServer.writeFile(
833
        'main_module.bootstrap.js',
834
        generateMainModule(
835
          entrypoint: entrypoint,
836
          nullAssertions: nullAssertions,
837
          nativeNullAssertions: nativeNullAssertions,
838 839
        ),
      );
840
      // TODO(zanderso): refactor the asset code in this and the regular devfs to
841
      // be shared.
842 843
      if (bundle != null) {
        await writeBundle(
844 845 846
          globals.fs.directory(getAssetBuildDirectory()),
          bundle.entries,
        );
847
      }
848 849 850 851 852
    }
    final DateTime candidateCompileTime = DateTime.now();
    if (fullRestart) {
      generator.reset();
    }
853 854

    // The tool generates an entrypoint file in a temp directory to handle
855
    // the web specific bootstrap logic. To make it easier for DWDS to handle
856
    // mapping the file name, this is done via an additional file root and
857
    // special hard-coded scheme.
858
    final CompilerOutput? compilerOutput = await generator.recompile(
859 860
      Uri(
        scheme: 'org-dartlang-app',
861
        path: '/${mainUri.pathSegments.last}',
862
      ),
863
      invalidatedFiles,
864
      outputPath: dillOutputPath,
865
      packageConfig: packageConfig,
866 867
      projectRootPath: projectRootPath,
      fs: globals.fs,
868 869
    );
    if (compilerOutput == null || compilerOutput.errorCount > 0) {
870
      return UpdateFSReport();
871
    }
872

873 874 875 876
    // 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;
877
    late File codeFile;
878 879
    File manifestFile;
    File sourcemapFile;
880
    File metadataFile;
881
    late List<String> modules;
882
    try {
883 884 885 886 887 888
      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);
889 890 891
    } on FileSystemException catch (err) {
      throwToolExit('Failed to load recompiled sources:\n$err');
    }
892
    webAssetServer.performRestart(modules);
893
    return UpdateFSReport(
894 895 896
      success: true,
      syncedBytes: codeFile.lengthSync(),
      invalidatedSourcesCount: invalidatedFiles.length,
897
    );
898
  }
899 900 901

  @visibleForTesting
  final File requireJS = globals.fs.file(globals.fs.path.join(
902
    globals.artifacts!.getHostArtifact(HostArtifact.engineDartSdkPath).path,
903 904 905 906 907 908 909 910 911
    'lib',
    'dev_compiler',
    'kernel',
    'amd',
    'require.js',
  ));

  @visibleForTesting
  final File stackTraceMapper = globals.fs.file(globals.fs.path.join(
912
    globals.artifacts!.getHostArtifact(HostArtifact.engineDartSdkPath).path,
913 914 915 916 917
    'lib',
    'dev_compiler',
    'web',
    'dart_stack_trace_mapper.js',
  ));
918 919 920 921 922

  @override
  void resetLastCompiled() {
    // Not used for web compilation.
  }
923 924
}

925
class ReleaseAssetServer {
926 927
  ReleaseAssetServer(
    this.entrypoint, {
928 929 930 931
    required FileSystem fileSystem,
    required String? webBuildDirectory,
    required String? flutterRoot,
    required Platform platform,
932
    this.basePath = '',
933 934 935 936 937 938
  })  : _fileSystem = fileSystem,
        _platform = platform,
        _flutterRoot = flutterRoot,
        _webBuildDirectory = webBuildDirectory,
        _fileSystemUtils =
            FileSystemUtils(fileSystem: fileSystem, platform: platform);
939 940

  final Uri entrypoint;
941 942
  final String? _flutterRoot;
  final String? _webBuildDirectory;
943 944 945
  final FileSystem _fileSystem;
  final FileSystemUtils _fileSystemUtils;
  final Platform _platform;
946

947
  @visibleForTesting
948

949 950 951
  /// The base path to serve from.
  ///
  /// It should have no leading or trailing slashes.
952
  final String? basePath;
953

954
  // Locations where source files, assets, or source maps may be located.
955
  List<Uri> _searchPaths() => <Uri>[
956 957 958 959 960 961
        _fileSystem.directory(_webBuildDirectory).uri,
        _fileSystem.directory(_flutterRoot).uri,
        _fileSystem.directory(_flutterRoot).parent.uri,
        _fileSystem.currentDirectory.uri,
        _fileSystem.directory(_fileSystemUtils.homeDirPath).uri,
      ];
962 963

  Future<shelf.Response> handle(shelf.Request request) async {
964 965 966 967 968
    if (request.method != 'GET') {
      // Assets are served via GET only.
      return shelf.Response.notFound('');
    }

969 970
    Uri? fileUri;
    final String? requestPath = _stripBasePath(request.url.path, basePath);
971 972 973 974 975

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

976 977 978
    if (request.url.toString() == 'main.dart') {
      fileUri = entrypoint;
    } else {
979
      for (final Uri uri in _searchPaths()) {
980
        final Uri potential = uri.resolve(requestPath);
981 982 983
        if (potential == null ||
            !_fileSystem.isFileSync(
                potential.toFilePath(windows: _platform.isWindows))) {
984 985 986 987
          continue;
        }
        fileUri = potential;
        break;
988 989 990
      }
    }
    if (fileUri != null) {
991
      final File file = _fileSystem.file(fileUri);
992 993 994
      final Uint8List bytes = file.readAsBytesSync();
      // Fallback to "application/octet-stream" on null which
      // makes no claims as to the structure of the data.
995 996 997
      final String mimeType =
          mime.lookupMimeType(file.path, headerBytes: bytes) ??
              'application/octet-stream';
998 999 1000 1001
      return shelf.Response.ok(bytes, headers: <String, String>{
        'Content-Type': mimeType,
      });
    }
1002

1003
    final File file = _fileSystem
1004
        .file(_fileSystem.path.join(_webBuildDirectory!, 'index.html'));
1005 1006 1007
    return shelf.Response.ok(file.readAsBytesSync(), headers: <String, String>{
      'Content-Type': 'text/html',
    });
1008 1009
  }
}
1010

1011 1012
@visibleForTesting
void log(logging.LogRecord event) {
1013 1014 1015 1016 1017 1018 1019 1020 1021 1022
  final String error = event.error == null? '': 'Error: ${event.error}';
  if (event.level >= logging.Level.SEVERE) {
    globals.printError('${event.loggerName}: ${event.message}$error', stackTrace: event.stackTrace);
  } else if (event.level == logging.Level.WARNING) {
    globals.printWarning('${event.loggerName}: ${event.message}$error');
  } else  {
    globals.printTrace('${event.loggerName}: ${event.message}$error');
  }
}

1023 1024 1025
Future<Directory> _loadDwdsDirectory(
    FileSystem fileSystem, Logger logger) async {
  final String toolPackagePath =
1026
      fileSystem.path.join(Cache.flutterRoot!, 'packages', 'flutter_tools');
1027
  final String packageFilePath =
1028
      fileSystem.path.join(toolPackagePath, '.dart_tool', 'package_config.json');
1029 1030 1031 1032
  final PackageConfig packageConfig = await loadPackageConfigWithLogging(
    fileSystem.file(packageFilePath),
    logger: logger,
  );
1033
  return fileSystem.directory(packageConfig['dwds']!.packageUriRoot);
1034
}
1035

1036
String? _stripBasePath(String path, String? basePath) {
1037
  path = _stripLeadingSlashes(path);
1038
  if (basePath != null && path.startsWith(basePath)) {
1039 1040 1041 1042 1043
    path = path.substring(basePath.length);
  } else {
    // The given path isn't under base path, return null to indicate that.
    return null;
  }
1044 1045 1046 1047
  return _stripLeadingSlashes(path);
}

String _stripLeadingSlashes(String path) {
1048 1049 1050 1051 1052
  while (path.startsWith('/')) {
    path = path.substring(1);
  }
  return path;
}
1053 1054 1055 1056 1057 1058 1059 1060

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

1061
String? _parseBasePathFromIndexHtml(File indexHtml) {
1062 1063
  final String htmlContent =
      indexHtml.existsSync() ? indexHtml.readAsStringSync() : _kDefaultIndex;
1064
  final Document document = parse(htmlContent);
1065 1066 1067
  final Element? baseElement = document.querySelector('base');
  String? baseHref =
      baseElement?.attributes == null ? null : baseElement!.attributes['href'];
1068

1069
  if (baseHref == null || baseHref == kBaseHrefPlaceholder) {
1070 1071 1072 1073
    baseHref = '';
  } else if (!baseHref.startsWith('/')) {
    throw ToolExit(
      'Error: The base href in "web/index.html" must be absolute (i.e. start '
1074
      'with a "/"), but found: `${baseElement!.outerHtml}`.\n'
1075 1076 1077 1078
      '$basePathExample',
    );
  } else if (!baseHref.endsWith('/')) {
    throw ToolExit(
1079
      'Error: The base href in "web/index.html" must end with a "/", but found: `${baseElement!.outerHtml}`.\n'
1080 1081 1082 1083 1084 1085 1086 1087 1088 1089 1090 1091 1092 1093 1094 1095 1096 1097 1098 1099
      '$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
''';