| // 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 '../artifacts.dart'; |
| import '../base/file_system.dart'; |
| import '../base/io.dart'; |
| import '../build_info.dart'; |
| import '../dart/package_map.dart'; |
| import '../globals.dart'; |
| import '../project.dart'; |
| |
| /// Handles mapping requests from a dartdevc compiled application to assets. |
| /// |
| /// The server will receive size different kinds of requests: |
| /// |
| /// 1. A request to assets in the form of `/assets/foo`. These are resolved |
| /// relative to `build/flutter_assets`. |
| /// 2. A request to a bootstrap file, such as `main.dart.js`. These are |
| /// resolved relative to the dart tool directory. |
| /// 3. A request to a JavaScript asset in the form of `/packages/foo/bar.js`. |
| /// These are looked up relative to the correct package root of the |
| /// dart_tool directory. |
| /// 4. A request to a Dart asset in the form of `/packages/foo/bar.dart` for |
| /// sourcemaps. These either need to be looked up from the application lib |
| /// directory (if the package is the same), or found in the .packages file. |
| /// 5. A request for a specific dart asset such as `stack_trace_mapper.js` or |
| /// `dart_sdk.js`. These have fixed locations determined by [artifacts]. |
| /// 6. A request to `/` which is translated into `index.html`. |
| class WebAssetServer { |
| WebAssetServer(this.flutterProject, this.target, this.ipv6); |
| |
| /// The flutter project corresponding to this application. |
| final FlutterProject flutterProject; |
| |
| /// The entrypoint we have compiled for. |
| final String target; |
| |
| /// Whether to serve from ipv6 localhost. |
| final bool ipv6; |
| |
| HttpServer _server; |
| Map<String, Uri> _packages; |
| |
| /// The port being served, or null if not initialized. |
| int get port => _server?.port; |
| |
| /// Initialize the server. |
| /// |
| /// Throws a [StateError] if called multiple times. |
| Future<void> initialize() async { |
| if (_server != null) { |
| throw StateError('Already serving.'); |
| } |
| _packages = PackageMap(PackageMap.globalPackagesPath).map; |
| _server = await HttpServer.bind( |
| ipv6 ? InternetAddress.loopbackIPv6 : InternetAddress.loopbackIPv4, 0) |
| ..autoCompress = false; |
| _server.listen(_onRequest); |
| } |
| |
| /// Clean up the server. |
| Future<void> dispose() { |
| return _server.close(); |
| } |
| |
| /// An HTTP server which provides JavaScript and web assets to the browser. |
| Future<void> _onRequest(HttpRequest request) async { |
| final String targetName = '${fs.path.basenameWithoutExtension(target)}_web_entrypoint'; |
| if (request.method != 'GET') { |
| request.response.statusCode = HttpStatus.forbidden; |
| await request.response.close(); |
| return; |
| } |
| final Uri uri = request.uri; |
| if (uri.path == '/') { |
| final File file = flutterProject.directory |
| .childDirectory('web') |
| .childFile('index.html'); |
| await _completeRequest(request, file, 'text/html'); |
| } else if (uri.path.contains('stack_trace_mapper')) { |
| final File file = fs.file(fs.path.join( |
| artifacts.getArtifactPath(Artifact.engineDartSdkPath), |
| 'lib', |
| 'dev_compiler', |
| 'web', |
| 'dart_stack_trace_mapper.js' |
| )); |
| await _completeRequest(request, file, 'text/javascript'); |
| } else if (uri.path.contains('require.js')) { |
| final File file = fs.file(fs.path.join( |
| artifacts.getArtifactPath(Artifact.engineDartSdkPath), |
| 'lib', |
| 'dev_compiler', |
| 'kernel', |
| 'amd', |
| 'require.js' |
| )); |
| await _completeRequest(request, file, 'text/javascript'); |
| } else if (uri.path.endsWith('main.dart.js')) { |
| final File file = fs.file(fs.path.join( |
| flutterProject.dartTool.path, |
| 'build', |
| 'flutter_web', |
| flutterProject.manifest.appName, |
| 'lib', |
| '$targetName.dart.js', |
| )); |
| await _completeRequest(request, file, 'text/javascript'); |
| } else if (uri.path.endsWith('$targetName.dart.bootstrap.js')) { |
| final File file = fs.file(fs.path.join( |
| flutterProject.dartTool.path, |
| 'build', |
| 'flutter_web', |
| flutterProject.manifest.appName, |
| 'lib', |
| '$targetName.dart.bootstrap.js', |
| )); |
| await _completeRequest(request, file, 'text/javascript'); |
| } else if (uri.path.contains('dart_sdk')) { |
| final File file = fs.file(fs.path.join( |
| artifacts.getArtifactPath(Artifact.flutterWebSdk), |
| 'kernel', |
| 'amd', |
| 'dart_sdk.js', |
| )); |
| await _completeRequest(request, file, 'text/javascript'); |
| } else if (uri.path.startsWith('/packages') && uri.path.endsWith('.dart')) { |
| await _resolveDart(request); |
| } else if (uri.path.startsWith('/packages')) { |
| await _resolveJavascript(request); |
| } else if (uri.path.contains('assets')) { |
| await _resolveAsset(request); |
| } else { |
| request.response.statusCode = HttpStatus.notFound; |
| await request.response.close(); |
| } |
| } |
| |
| /// Resolves requests in the form of `/packages/foo/bar.js` or |
| /// `/packages/foo/bar.js.map`. |
| Future<void> _resolveJavascript(HttpRequest request) async { |
| final List<String> segments = fs.path.split(request.uri.path); |
| final String packageName = segments[2]; |
| final String filePath = fs.path.joinAll(segments.sublist(3)); |
| final Uri packageUri = flutterProject.dartTool |
| .childDirectory('build') |
| .childDirectory('flutter_web') |
| .childDirectory(packageName) |
| .childDirectory('lib') |
| .uri; |
| await _completeRequest( |
| request, fs.file(packageUri.resolve(filePath)), 'text/javascript'); |
| } |
| |
| /// Resolves requests in the form of `/packages/foo/bar.dart`. |
| Future<void> _resolveDart(HttpRequest request) async { |
| final List<String> segments = fs.path.split(request.uri.path); |
| final String packageName = segments[2]; |
| final String filePath = fs.path.joinAll(segments.sublist(3)); |
| final Uri packageUri = _packages[packageName]; |
| await _completeRequest(request, fs.file(packageUri.resolve(filePath))); |
| } |
| |
| /// Resolves requests in the form of `/assets/foo`. |
| Future<void> _resolveAsset(HttpRequest request) async { |
| final String assetPath = request.uri.path.replaceFirst('/assets/', ''); |
| await _completeRequest( |
| request, fs.file(fs.path.join(getAssetBuildDirectory(), assetPath))); |
| } |
| |
| Future<void> _completeRequest(HttpRequest request, File file, |
| [String contentType = 'text']) async { |
| if (!file.existsSync()) { |
| request.response.statusCode = HttpStatus.notFound; |
| await request.response.close(); |
| return; |
| } |
| request.response.statusCode = HttpStatus.ok; |
| if (contentType != null) { |
| request.response.headers.add(HttpHeaders.contentTypeHeader, contentType); |
| } |
| await request.response.addStream(file.openRead()); |
| await request.response.close(); |
| } |
| } |