devfs_web.dart 12.1 KB
Newer Older
1 2 3 4 5 6 7 8 9
// Copyright 2019 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

import 'dart:typed_data';

import 'package:meta/meta.dart';
import 'package:mime/mime.dart' as mime;

10 11
import '../artifacts.dart';
import '../asset.dart';
12 13 14
import '../base/common.dart';
import '../base/file_system.dart';
import '../base/io.dart';
15
import '../base/utils.dart';
16
import '../build_info.dart';
17 18
import '../bundle.dart';
import '../compile.dart';
19
import '../convert.dart';
20
import '../devfs.dart';
21
import '../globals.dart';
22
import 'bootstrap.dart';
23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58

/// A web server which handles serving JavaScript and assets.
///
/// This is only used in development mode.
class WebAssetServer {
  @visibleForTesting
  WebAssetServer(this._httpServer, { @required void Function(dynamic, StackTrace) onError }) {
    _httpServer.listen((HttpRequest request) {
      _handleRequest(request).catchError(onError);
      // TODO(jonahwilliams): test the onError callback when https://github.com/dart-lang/sdk/issues/39094 is fixed.
    }, onError: onError);
  }

  // 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].
  ///
  /// Unhandled exceptions will throw a [ToolExit] with the error and stack
  /// trace.
  static Future<WebAssetServer> start(String hostname, int port) async {
    try {
      final HttpServer httpServer = await HttpServer.bind(hostname, port);
      return WebAssetServer(httpServer, onError: (dynamic error, StackTrace stackTrace) {
        httpServer.close(force: true);
        throwToolExit('Unhandled exception in web development server:\n$error\n$stackTrace');
      });
    } on SocketException catch (err) {
      throwToolExit('Failed to bind web development server:\n$err');
    }
    assert(false);
    return null;
  }

  final HttpServer _httpServer;
59 60
  // If holding these in memory is too much overhead, this can be switched to a
  // RandomAccessFile and read on demand.
61
  final Map<String, Uint8List> _files = <String, Uint8List>{};
62
  final Map<String, Uint8List> _sourcemaps = <String, Uint8List>{};
63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83

  // handle requests for JavaScript source, dart sources maps, or asset files.
  Future<void> _handleRequest(HttpRequest request) async {
    final HttpResponse response = request.response;
    // If the response is `/`, then we are requesting the index file.
    if (request.uri.path == '/') {
      final File indexFile = fs.currentDirectory
        .childDirectory('web')
        .childFile('index.html');
      if (indexFile.existsSync()) {
        response.headers.add('Content-Type', 'text/html');
        response.headers.add('Content-Length', indexFile.lengthSync());
        await response.addStream(indexFile.openRead());
      } else {
        response.statusCode = HttpStatus.notFound;
      }
      await response.close();
      return;
    }

    // If this is a JavaScript file, it must be in the in-memory cache.
84
    // Attempt to look up the file by URI.
85 86 87 88 89 90 91 92 93
    if (_files.containsKey(request.uri.path)) {
      final List<int> bytes = _files[request.uri.path];
      response.headers
        ..add('Content-Length', bytes.length)
        ..add('Content-Type', 'application/javascript');
      response.add(bytes);
      await response.close();
      return;
    }
94 95 96 97 98 99 100 101 102 103 104 105
    // If this is a sourcemap file, then it might be in the in-memory cache.
    // Attempt to lookup the file by URI.
    if (_sourcemaps.containsKey(request.uri.path)) {
      final List<int> bytes = _sourcemaps[request.uri.path];
      response.headers
        ..add('Content-Length', bytes.length)
        ..add('Content-Type', 'application/json');
      response.add(bytes);
      await response.close();
      return;
    }

106 107 108 109 110 111 112 113 114 115 116 117 118
    // If this is a dart file, it must be on the local file system and is
    // likely coming from a source map request. Attempt to look in the
    // local filesystem for it, and return a 404 if it is not found. The tool
    // doesn't currently consider the case of Dart files as assets.
    File file = fs.file(Uri.base.resolve(request.uri.path));

    // If both of the lookups above failed, the file might have been an asset.
    // Try and resolve the path relative to the built asset directory.
    if (!file.existsSync()) {
      final String assetPath = request.uri.path.replaceFirst('/assets/', '');
      file = fs.file(fs.path.join(getAssetBuildDirectory(), fs.path.relative(assetPath)));
    }

119 120 121 122 123 124 125 126 127 128 129 130
    // If it isn't a project source or an asset, it must be a dart SDK source.
    // or a flutter web SDK source.
    if (!file.existsSync()) {
      final Directory dartSdkParent = fs.directory(artifacts.getArtifactPath(Artifact.engineDartSdkPath)).parent;
      file = fs.file(fs.path.joinAll(<String>[dartSdkParent.path, ...request.uri.pathSegments]));
    }

    if (!file.existsSync()) {
      final String flutterWebSdk = artifacts.getArtifactPath(Artifact.flutterWebSdk);
      file = fs.file(fs.path.joinAll(<String>[flutterWebSdk, ...request.uri.pathSegments]));
    }

131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166
    if (!file.existsSync()) {
      response.statusCode = HttpStatus.notFound;
      await response.close();
      return;
    }
    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) {
      mimeType= mime.lookupMimeType(
        file.path,
        headerBytes: await file.openRead(0, 12).first,
      );
    }
    mimeType ??= _kDefaultMimeType;
    response.headers.add('Content-Length', length);
    response.headers.add('Content-Type', mimeType);
    await response.addStream(file.openRead());
    await response.close();
  }

  /// 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.
167
  List<String> write(File codeFile, File manifestFile, File sourcemapFile) {
168
    final List<String> modules = <String>[];
169 170
    final Uint8List codeBytes = codeFile.readAsBytesSync();
    final Uint8List sourcemapBytes = sourcemapFile.readAsBytesSync();
171
    final Map<String, dynamic> manifest = castStringKeyedMap(json.decode(manifestFile.readAsStringSync()));
172 173 174 175 176
    for (String filePath in manifest.keys) {
      if (filePath == null) {
        printTrace('Invalid manfiest file: $filePath');
        continue;
      }
177 178 179
      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>();
180
      if (codeOffsets.length != 2 || sourcemapOffsets.length != 2) {
181 182 183
        printTrace('Invalid manifest byte offsets: $offsets');
        continue;
      }
184 185 186 187 188

      final int codeStart = codeOffsets[0];
      final int codeEnd = codeOffsets[1];
      if (codeStart < 0 || codeEnd > codeBytes.lengthInBytes) {
        printTrace('Invalid byte index: [$codeStart, $codeEnd]');
189 190
        continue;
      }
191 192 193 194 195
      final Uint8List byteView = Uint8List.view(
        codeBytes.buffer,
        codeStart,
        codeEnd - codeStart,
      );
196
      _files[filePath] = byteView;
197 198 199 200 201 202 203 204 205 206 207 208 209 210

      final int sourcemapStart = sourcemapOffsets[0];
      final int sourcemapEnd = sourcemapOffsets[1];
      if (sourcemapStart < 0 || sourcemapEnd > sourcemapBytes.lengthInBytes) {
        printTrace('Invalid byte index: [$sourcemapStart, $sourcemapEnd]');
        continue;
      }
      final Uint8List sourcemapView = Uint8List.view(
        sourcemapBytes.buffer,
        sourcemapStart,
        sourcemapEnd - sourcemapStart ,
      );
      _sourcemaps['$filePath.map'] = sourcemapView;

211 212 213 214 215
      modules.add(filePath);
    }
    return modules;
  }
}
216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 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

class WebDevFS implements DevFS {
  WebDevFS(this.hostname, this.port, this._packagesFilePath);

  final String hostname;
  final int port;
  final String _packagesFilePath;
  WebAssetServer _webAssetServer;

  @override
  List<Uri> sources = <Uri>[];

  @override
  DateTime lastCompiled;

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

  @override
  Uri get baseUri => null;

  @override
  Future<Uri> create() async {
    _webAssetServer = await WebAssetServer.start(hostname, port);
   return Uri.base;
  }

  @override
  Future<void> destroy() async {
    await _webAssetServer.dispose();
  }

  @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,
  }) async {
    assert(trackWidgetCreation != null);
    assert(generator != null);
    if (bundleFirstUpload) {
      final File requireJS = fs.file(fs.path.join(
        artifacts.getArtifactPath(Artifact.engineDartSdkPath),
        'lib',
        'dev_compiler',
        'kernel',
        'amd',
        'require.js',
      ));
      final File dartSdk = fs.file(fs.path.join(
        artifacts.getArtifactPath(Artifact.flutterWebSdk),
        'kernel',
        'amd',
        'dart_sdk.js',
      ));
      final File dartSdkSourcemap = fs.file(fs.path.join(
        artifacts.getArtifactPath(Artifact.flutterWebSdk),
        'kernel',
        'amd',
        'dart_sdk.js.map',
      ));
      final File stackTraceMapper = fs.file(fs.path.join(
        artifacts.getArtifactPath(Artifact.engineDartSdkPath),
        'lib',
        'dev_compiler',
        'web',
        'dart_stack_trace_mapper.js',
      ));
      _webAssetServer.writeFile('/main.dart.js', generateBootstrapScript(
        requireUrl: requireJS.path,
        mapperUrl: stackTraceMapper.path,
        entrypoint: '$mainPath.js',
      ));
      _webAssetServer.writeFile('/main_module.js', generateMainModule(
        entrypoint: '$mainPath.js',
      ));
      _webAssetServer.writeFile('/dart_sdk.js', dartSdk.readAsStringSync());
      _webAssetServer.writeFile('/dart_sdk.js.map', dartSdkSourcemap.readAsStringSync());
    }
    final DateTime candidateCompileTime = DateTime.now();
    if (fullRestart) {
      generator.reset();
    }
     final CompilerOutput compilerOutput = await generator.recompile(
      mainPath,
      invalidatedFiles,
      outputPath:  dillOutputPath ?? getDefaultApplicationKernelPath(trackWidgetCreation: trackWidgetCreation),
      packagesFilePath : _packagesFilePath,
    );
    if (compilerOutput == null || compilerOutput.errorCount > 0) {
      return UpdateFSReport(success: false);
    }
    // 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 {
      codeFile = fs.file('${compilerOutput.outputFilename}.sources');
      manifestFile = fs.file('${compilerOutput.outputFilename}.json');
      sourcemapFile = fs.file('${compilerOutput.outputFilename}.map');
      modules = _webAssetServer.write(codeFile, manifestFile, sourcemapFile);
    } on FileSystemException catch (err) {
      throwToolExit('Failed to load recompiled sources:\n$err');
    }
    return UpdateFSReport(success: true, syncedBytes: codeFile.lengthSync(),
      invalidatedSourcesCount: invalidatedFiles.length)
        ..invalidatedModules = modules;
  }
}