// Copyright 2014 The Flutter Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. // @dart = 2.8 import 'dart:async'; import 'dart:typed_data'; import 'package:dwds/data/build_result.dart'; import 'package:dwds/dwds.dart'; import 'package:html/dom.dart'; import 'package:html/parser.dart'; import 'package:logging/logging.dart' as logging; import 'package:meta/meta.dart'; import 'package:mime/mime.dart' as mime; import 'package:package_config/package_config.dart'; import 'package:shelf/shelf.dart' as shelf; import 'package:shelf/shelf_io.dart' as shelf; import 'package:vm_service/vm_service.dart' as vm_service; import '../artifacts.dart'; import '../asset.dart'; import '../base/common.dart'; import '../base/file_system.dart'; import '../base/io.dart'; import '../base/logger.dart'; import '../base/net.dart'; import '../base/platform.dart'; import '../build_info.dart'; import '../build_system/targets/web.dart'; import '../bundle.dart'; import '../cache.dart'; import '../compile.dart'; import '../convert.dart'; import '../dart/package_map.dart'; import '../devfs.dart'; import '../globals.dart' as globals; import '../project.dart'; import '../vmservice.dart'; import '../web/bootstrap.dart'; import '../web/chrome.dart'; import '../web/compile.dart'; import '../web/memory_fs.dart'; 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, }); // A minimal index for projects that do not yet support web. const String _kDefaultIndex = ''' <html> <head> <base href="/"> </head> <body> <script src="main.dart.js"></script> </body> </html> '''; /// An expression compiler connecting to FrontendServer. /// /// This is only used in development mode. class WebExpressionCompiler implements ExpressionCompiler { WebExpressionCompiler(this._generator, { @required FileSystem fileSystem, }) : _fileSystem = fileSystem; final ResidentCompiler _generator; final FileSystem _fileSystem; @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 { final CompilerOutput compilerOutput = await _generator.compileExpressionToJs(libraryUri, line, column, jsModules, jsFrameValues, moduleName, expression); if (compilerOutput != null && compilerOutput.outputFilename != null) { final String content = utf8.decode( _fileSystem.file(compilerOutput.outputFilename).readAsBytesSync()); return ExpressionCompilationResult( content, compilerOutput.errorCount > 0); } return ExpressionCompilationResult( "InternalError: frontend server failed to compile '$expression'", true); } @override Future<void> initialize({String moduleFormat, bool soundNullSafety}) async {} @override Future<bool> updateDependencies(Map<String, ModuleInfo> modules) async => true; } /// A web server which handles serving JavaScript and assets. /// /// This is only used in development mode. class WebAssetServer implements AssetReader { @visibleForTesting WebAssetServer( this._httpServer, this._packages, this.internetAddress, this._modules, this._digests, this._nullSafetyMode, ) : basePath = _parseBasePathFromIndexHtml(globals.fs.currentDirectory .childDirectory('web') .childFile('index.html')); // 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'; final Map<String, String> _modules; final Map<String, String> _digests; int get selectedPort => _httpServer.port; 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. final String moduleName = module.startsWith('/') ? module.substring(1) : module; final String name = moduleName.replaceAll('.lib.js', ''); final String path = moduleName.replaceAll('.js', ''); _modules[name] = path; _digests[name] = _webMemoryFS.files[moduleName].hashCode.toString(); } } @visibleForTesting List<String> write( File codeFile, File manifestFile, File sourcemapFile, File metadataFile, ) { return _webMemoryFS.write(codeFile, manifestFile, sourcemapFile, metadataFile); } /// Start the web asset server on a [hostname] and [port]. /// /// If [testMode] is true, do not actually initialize dwds or the shelf static /// server. /// /// Unhandled exceptions will throw a [ToolExit] with the error and stack /// trace. static Future<WebAssetServer> start( ChromiumLauncher chromiumLauncher, String hostname, int port, UrlTunneller urlTunneller, bool useSseForDebugProxy, bool useSseForDebugBackend, bool useSseForInjectedClient, BuildInfo buildInfo, bool enableDwds, bool enableDds, Uri entrypoint, ExpressionCompiler expressionCompiler, NullSafetyMode nullSafetyMode, { bool testMode = false, DwdsLauncher dwdsLauncher = Dwds.start, }) async { 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)); } } if (httpServer == null) { throwToolExit('Failed to bind web development server:\n$lastError'); } // Allow rendering in a iframe. httpServer.defaultResponseHeaders.remove('x-frame-options', 'SAMEORIGIN'); final PackageConfig packageConfig = buildInfo.packageConfig; final Map<String, String> digests = <String, String>{}; final Map<String, String> modules = <String, String>{}; final WebAssetServer server = WebAssetServer( httpServer, packageConfig, address, modules, digests, nullSafetyMode, ); if (testMode) { return server; } // 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); shelf.Handler middleware(FutureOr<shelf.Response> Function(shelf.Request) innerHandler) { 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); }; } 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, useSseForInjectedClient: useSseForInjectedClient, loadStrategy: FrontendServerRequireStrategyProvider( ReloadConfiguration.none, server, _digestProvider, ).strategy, expressionCompiler: expressionCompiler, spawnDds: enableDds, ); shelf.Pipeline pipeline = const shelf.Pipeline(); if (enableDwds) { pipeline = pipeline.addMiddleware(middleware); pipeline = pipeline.addMiddleware(dwds.middleware); } final shelf.Handler dwdsHandler = pipeline.addHandler(server.handleRequest); final shelf.Cascade cascade = shelf.Cascade().add(dwds.handler).add(dwdsHandler); shelf.serveRequests(httpServer, cascade.handler); server.dwds = dwds; return server; } final NullSafetyMode _nullSafetyMode; final HttpServer _httpServer; final WebMemoryFS _webMemoryFS = WebMemoryFS(); final PackageConfig _packages; final InternetAddress internetAddress; /* late final */ Dwds dwds; Directory entrypointCacheDirectory; @visibleForTesting HttpHeaders get defaultResponseHeaders => _httpServer.defaultResponseHeaders; @visibleForTesting Uint8List getFile(String path) => _webMemoryFS.files[path]; @visibleForTesting Uint8List getSourceMap(String path) => _webMemoryFS.sourcemaps[path]; @visibleForTesting Uint8List getMetadata(String path) => _webMemoryFS.metadataFiles[path]; @visibleForTesting /// The base path to serve from. /// /// It should have no leading or trailing slashes. String basePath = ''; // handle requests for JavaScript source, dart sources maps, or asset files. @visibleForTesting Future<shelf.Response> handleRequest(shelf.Request request) async { if (request.method != 'GET') { // Assets are served via GET only. return shelf.Response.notFound(''); } final String requestPath = _stripBasePath(request.url.path, basePath); if (requestPath == null) { return shelf.Response.notFound(''); } // If the response is `/`, then we are requesting the index file. if (requestPath == '/' || requestPath.isEmpty) { return _serveIndex(); } final Map<String, String> headers = <String, String>{}; // Track etag headers for better caching of resources. final String ifNoneMatch = request.headers[HttpHeaders.ifNoneMatchHeader]; headers[HttpHeaders.cacheControlHeader] = 'max-age=0, must-revalidate'; // If this is a JavaScript file, it must be in the in-memory cache. // Attempt to look up the file by URI. final String webServerPath = requestPath.replaceFirst('.dart.js', '.dart.lib.js'); if (_webMemoryFS.files.containsKey(requestPath) || _webMemoryFS.files.containsKey(webServerPath)) { final List<int> bytes = getFile(requestPath) ?? getFile(webServerPath); // 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(); } headers[HttpHeaders.contentLengthHeader] = bytes.length.toString(); headers[HttpHeaders.contentTypeHeader] = 'application/javascript'; headers[HttpHeaders.etagHeader] = etag; return shelf.Response.ok(bytes, headers: headers); } // If this is a sourcemap file, then it might be in the in-memory cache. // Attempt to lookup the file by URI. if (_webMemoryFS.sourcemaps.containsKey(requestPath)) { final List<int> bytes = getSourceMap(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); } // If this is a metadata file, then it might be in the in-memory cache. // Attempt to lookup the file by URI. if (_webMemoryFS.metadataFiles.containsKey(requestPath)) { 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); } File file = _resolveDartFile(requestPath); // If all 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 Uri potential = globals.fs .directory(getAssetBuildDirectory()) .uri .resolve(requestPath.replaceFirst('assets/', '')); file = globals.fs.file(potential); } if (!file.existsSync()) { final Uri webPath = globals.fs.currentDirectory .childDirectory('web') .uri .resolve(requestPath); file = globals.fs.file(webPath); } if (!file.existsSync()) { // Paths starting with these prefixes should've been resolved above. if (requestPath.startsWith('assets/') || requestPath.startsWith('packages/')) { return shelf.Response.notFound(''); } return _serveIndex(); } // For real files, use a serialized file stat plus path as a revision. // This allows us to update between canvaskit and non-canvaskit SDKs. final String etag = file.lastModifiedSync().toIso8601String() + Uri.encodeComponent(file.path); if (ifNoneMatch == etag) { return shelf.Response.notModified(); } 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 etc. 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; headers[HttpHeaders.contentLengthHeader] = length.toString(); headers[HttpHeaders.contentTypeHeader] = mimeType; headers[HttpHeaders.etagHeader] = etag; return shelf.Response.ok(file.openRead(), headers: headers); } /// Tear down the http server running. Future<void> dispose() async { await dwds?.stop(); return _httpServer.close(); } /// Write a single file into the in-memory cache. void writeFile(String filePath, String contents) { writeBytes(filePath, utf8.encode(contents) as Uint8List); } void writeBytes(String filePath, Uint8List contents) { _webMemoryFS.files[filePath] = contents; } /// Determines what rendering backed to use. WebRendererMode webRenderer = WebRendererMode.html; shelf.Response _serveIndex() { final Map<String, String> headers = <String, String>{ HttpHeaders.contentTypeHeader: 'text/html', }; final File indexFile = globals.fs.currentDirectory .childDirectory('web') .childFile('index.html'); if (indexFile.existsSync()) { 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); } headers[HttpHeaders.contentLengthHeader] = indexFile.lengthSync().toString(); return shelf.Response.ok(indexFile.openRead(), headers: headers); } headers[HttpHeaders.contentLengthHeader] = _kDefaultIndex.length.toString(); return shelf.Response.ok(_kDefaultIndex, headers: headers); } // Attempt to resolve `path` to a dart file. File _resolveDartFile(String path) { // Return the actual file objects so that local engine changes are automatically picked up. switch (path) { case 'dart_sdk.js': return _resolveDartSdkJsFile; case 'dart_sdk.js.map': return _resolveDartSdkJsMapFile; } // This is the special generated entrypoint. if (path == 'web_entrypoint.dart') { return entrypointCacheDirectory.childFile('web_entrypoint.dart'); } // If this is a dart file, it must be on the local file system and is // likely coming from a source map request. The tool doesn't currently // consider the case of Dart files as assets. final File dartFile = globals.fs.file(globals.fs.currentDirectory.uri.resolve(path)); if (dartFile.existsSync()) { return dartFile; } final List<String> segments = path.split('/'); if (segments.first.isEmpty) { segments.removeAt(0); } // The file might have been a package file which is signaled by a // `/packages/<package>/<path>` request. if (segments.first == 'packages') { final Uri filePath = _packages .resolve(Uri(scheme: 'package', pathSegments: segments.skip(1))); if (filePath != null) { final File packageFile = globals.fs.file(filePath); if (packageFile.existsSync()) { return packageFile; } } } // Otherwise it must be a Dart SDK source or a Flutter Web SDK source. final Directory dartSdkParent = globals.fs .directory( globals.artifacts.getHostArtifact(HostArtifact.engineDartSdkPath)) .parent; final File dartSdkFile = globals.fs.file(dartSdkParent.uri.resolve(path)); if (dartSdkFile.existsSync()) { return dartSdkFile; } final Directory flutterWebSdk = globals.fs .directory(globals.artifacts.getHostArtifact(HostArtifact.flutterWebSdk)); final File webSdkFile = globals.fs.file(flutterWebSdk.uri.resolve(path)); return webSdkFile; } File get _resolveDartSdkJsFile => globals.fs.file(globals.artifacts.getHostArtifact( kDartSdkJsArtifactMap[webRenderer][_nullSafetyMode] )); File get _resolveDartSdkJsMapFile => globals.fs.file(globals.artifacts.getHostArtifact( kDartSdkJsMapArtifactMap[webRenderer][_nullSafetyMode] )); @override Future<String> dartSourceContents(String serverPath) async { serverPath = _stripBasePath(serverPath, basePath); final File result = _resolveDartFile(serverPath); if (result.existsSync()) { return result.readAsString(); } return null; } @override Future<String> sourceMapContents(String serverPath) async { serverPath = _stripBasePath(serverPath, basePath); return utf8.decode(_webMemoryFS.sourcemaps[serverPath]); } @override Future<String> metadataContents(String serverPath) async { serverPath = _stripBasePath(serverPath, basePath); if (serverPath == 'main_module.ddc_merged_metadata') { return _webMemoryFS.mergedMetadata; } if (_webMemoryFS.metadataFiles.containsKey(serverPath)) { return utf8.decode(_webMemoryFS.metadataFiles[serverPath]); } throw Exception('Could not find metadata contents for $serverPath'); } @override Future<void> close() async {} } class ConnectionResult { ConnectionResult(this.appConnection, this.debugConnection, this.vmService); final AppConnection appConnection; final DebugConnection debugConnection; final vm_service.VmService vmService; } /// The web specific DevFS implementation. class WebDevFS implements DevFS { /// Create a new [WebDevFS] instance. /// /// [testMode] is true, do not actually initialize dwds or the shelf static /// server. WebDevFS({ @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, this.testMode = false, }) : _port = port; final Uri entrypoint; final String hostname; final String packagesFilePath; final UrlTunneller urlTunneller; final bool useSseForDebugProxy; final bool useSseForDebugBackend; final bool useSseForInjectedClient; final BuildInfo buildInfo; final bool enableDwds; final bool enableDds; final bool testMode; final ExpressionCompiler expressionCompiler; final ChromiumLauncher chromiumLauncher; final bool nullAssertions; final bool nativeNullAssertions; final int _port; final NullSafetyMode nullSafetyMode; WebAssetServer webAssetServer; Dwds get dwds => webAssetServer.dwds; Future<DebugConnection> _cachedExtensionFuture; StreamSubscription<void> _connectedApps; /// Connect and retrieve the [DebugConnection] for the current application. /// /// Only calls [AppConnection.runMain] on the subsequent connections. Future<ConnectionResult> connect(bool useDebugExtension) { final Completer<ConnectionResult> firstConnection = Completer<ConnectionResult>(); _connectedApps = dwds.connectedApps.listen((AppConnection appConnection) async { try { final DebugConnection debugConnection = useDebugExtension ? await (_cachedExtensionFuture ??= dwds.extensionDebugConnections.stream.first) : await dwds.debugConnection(appConnection); if (firstConnection.isCompleted) { appConnection.runMain(); } else { 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; } @override List<Uri> sources = <Uri>[]; @override DateTime lastCompiled; @override PackageConfig lastPackageConfig; // We do not evict assets on the web. @override Set<String> get assetPathsToEvict => const <String>{}; @override Uri get baseUri => _baseUri; Uri _baseUri; @override Future<Uri> create() async { webAssetServer = await WebAssetServer.start( chromiumLauncher, hostname, _port, urlTunneller, useSseForDebugProxy, useSseForDebugBackend, useSseForInjectedClient, buildInfo, enableDwds, enableDds, entrypoint, expressionCompiler, nullSafetyMode, testMode: testMode, ); final int selectedPort = webAssetServer.selectedPort; if (buildInfo.dartDefines.contains('FLUTTER_WEB_AUTO_DETECT=true')) { webAssetServer.webRenderer = WebRendererMode.autoDetect; } else if (buildInfo.dartDefines.contains('FLUTTER_WEB_USE_SKIA=true')) { webAssetServer.webRenderer = WebRendererMode.canvaskit; } if (hostname == 'any') { _baseUri = Uri.http('localhost:$selectedPort', webAssetServer.basePath); } else { _baseUri = Uri.http('$hostname:$selectedPort', webAssetServer.basePath); } return _baseUri; } @override Future<void> destroy() async { await webAssetServer.dispose(); await _connectedApps?.cancel(); } @override Uri deviceUriToHostUri(Uri deviceUri) { return deviceUri; } @override String get fsName => 'web_asset'; @override Directory get rootDirectory => null; @override Future<UpdateFSReport> update({ @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, bool bundleFirstUpload = false, bool fullRestart = false, String projectRootPath, }) async { assert(trackWidgetCreation != null); assert(generator != null); lastPackageConfig = packageConfig; final File mainFile = globals.fs.file(mainUri); final String outputDirectoryPath = mainFile.parent.path; if (bundleFirstUpload) { webAssetServer.entrypointCacheDirectory = globals.fs.directory(outputDirectoryPath); generator.addFileSystemRoot(outputDirectoryPath); final String entrypoint = globals.fs.path.basename(mainFile.path); webAssetServer.writeBytes(entrypoint, mainFile.readAsBytesSync()); webAssetServer.writeBytes('require.js', requireJS.readAsBytesSync()); 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()); webAssetServer.writeFile( 'main.dart.js', generateBootstrapScript( requireUrl: 'require.js', mapperUrl: 'stack_trace_mapper.js', ), ); webAssetServer.writeFile( 'main_module.bootstrap.js', generateMainModule( entrypoint: entrypoint, nullAssertions: nullAssertions, nativeNullAssertions: nativeNullAssertions, ), ); // TODO(jonahwilliams): refactor the asset code in this and the regular devfs to // be shared. if (bundle != null) { await writeBundle( globals.fs.directory(getAssetBuildDirectory()), bundle.entries, ); } } final DateTime candidateCompileTime = DateTime.now(); if (fullRestart) { generator.reset(); } // The tool generates an entrypoint file in a temp directory to handle // the web specific bootstrap logic. To make it easier for DWDS to handle // mapping the file name, this is done via an additional file root and // special hard-coded scheme. final CompilerOutput compilerOutput = await generator.recompile( Uri( scheme: 'org-dartlang-app', path: '/' + mainUri.pathSegments.last, ), invalidatedFiles, outputPath: dillOutputPath, packageConfig: packageConfig, projectRootPath: projectRootPath, fs: globals.fs, ); 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; File metadataFile; List<String> modules; try { 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); } on FileSystemException catch (err) { throwToolExit('Failed to load recompiled sources:\n$err'); } webAssetServer.performRestart(modules); return UpdateFSReport( success: true, syncedBytes: codeFile.lengthSync(), invalidatedSourcesCount: invalidatedFiles.length, ); } @visibleForTesting final File requireJS = globals.fs.file(globals.fs.path.join( globals.artifacts.getHostArtifact(HostArtifact.engineDartSdkPath).path, 'lib', 'dev_compiler', 'kernel', 'amd', 'require.js', )); @visibleForTesting final File stackTraceMapper = globals.fs.file(globals.fs.path.join( globals.artifacts.getHostArtifact(HostArtifact.engineDartSdkPath).path, 'lib', 'dev_compiler', 'web', 'dart_stack_trace_mapper.js', )); @override void resetLastCompiled() { // Not used for web compilation. } } class ReleaseAssetServer { ReleaseAssetServer( this.entrypoint, { @required FileSystem fileSystem, @required String webBuildDirectory, @required String flutterRoot, @required Platform platform, this.basePath = '', }) : _fileSystem = fileSystem, _platform = platform, _flutterRoot = flutterRoot, _webBuildDirectory = webBuildDirectory, _fileSystemUtils = FileSystemUtils(fileSystem: fileSystem, platform: platform); final Uri entrypoint; final String _flutterRoot; final String _webBuildDirectory; final FileSystem _fileSystem; final FileSystemUtils _fileSystemUtils; final Platform _platform; @visibleForTesting /// The base path to serve from. /// /// It should have no leading or trailing slashes. final String basePath; // Locations where source files, assets, or source maps may be located. List<Uri> _searchPaths() => <Uri>[ _fileSystem.directory(_webBuildDirectory).uri, _fileSystem.directory(_flutterRoot).uri, _fileSystem.directory(_flutterRoot).parent.uri, _fileSystem.currentDirectory.uri, _fileSystem.directory(_fileSystemUtils.homeDirPath).uri, ]; Future<shelf.Response> handle(shelf.Request request) async { if (request.method != 'GET') { // Assets are served via GET only. return shelf.Response.notFound(''); } Uri fileUri; final String requestPath = _stripBasePath(request.url.path, basePath); if (requestPath == null) { return shelf.Response.notFound(''); } if (request.url.toString() == 'main.dart') { fileUri = entrypoint; } else { for (final Uri uri in _searchPaths()) { final Uri potential = uri.resolve(requestPath); if (potential == null || !_fileSystem.isFileSync( potential.toFilePath(windows: _platform.isWindows))) { continue; } fileUri = potential; break; } } if (fileUri != null) { final File file = _fileSystem.file(fileUri); final Uint8List bytes = file.readAsBytesSync(); // Fallback to "application/octet-stream" on null which // makes no claims as to the structure of the data. final String mimeType = mime.lookupMimeType(file.path, headerBytes: bytes) ?? 'application/octet-stream'; return shelf.Response.ok(bytes, headers: <String, String>{ 'Content-Type': mimeType, }); } final File file = _fileSystem .file(_fileSystem.path.join(_webBuildDirectory, 'index.html')); return shelf.Response.ok(file.readAsBytesSync(), headers: <String, String>{ 'Content-Type': 'text/html', }); } } Future<Directory> _loadDwdsDirectory( FileSystem fileSystem, Logger logger) async { final String toolPackagePath = fileSystem.path.join(Cache.flutterRoot, 'packages', 'flutter_tools'); final String packageFilePath = fileSystem.path.join(toolPackagePath, '.dart_tool', 'package_config.json'); final PackageConfig packageConfig = await loadPackageConfigWithLogging( fileSystem.file(packageFilePath), logger: logger, ); return fileSystem.directory(packageConfig['dwds'].packageUriRoot); } String _stripBasePath(String path, String basePath) { path = _stripLeadingSlashes(path); 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; } return _stripLeadingSlashes(path); } String _stripLeadingSlashes(String path) { while (path.startsWith('/')) { path = path.substring(1); } return path; } String _stripTrailingSlashes(String path) { while (path.endsWith('/')) { path = path.substring(0, path.length - 1); } return path; } String _parseBasePathFromIndexHtml(File indexHtml) { final String htmlContent = indexHtml.existsSync() ? indexHtml.readAsStringSync() : _kDefaultIndex; final Document document = parse(htmlContent); final Element baseElement = document.querySelector('base'); String baseHref = baseElement?.attributes == null ? null : baseElement.attributes['href']; if (baseHref == null || baseHref == kBaseHrefPlaceholder) { 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 ''';