| // 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. |
| |
| import 'dart:async'; |
| import 'dart:typed_data'; |
| |
| import 'package:dwds/data/build_result.dart'; |
| import 'package:dwds/dwds.dart'; |
| import 'package:logging/logging.dart'; |
| import 'package:meta/meta.dart'; |
| import 'package:mime/mime.dart' as mime; |
| // TODO(bkonyi): remove deprecated member usage, https://github.com/flutter/flutter/issues/51951 |
| // ignore: deprecated_member_use |
| import 'package:package_config/discovery.dart'; |
| // TODO(bkonyi): remove deprecated member usage, https://github.com/flutter/flutter/issues/51951 |
| // ignore: deprecated_member_use |
| import 'package:package_config/packages.dart'; |
| import 'package:shelf/shelf.dart' as shelf; |
| import 'package:shelf/shelf_io.dart' as shelf; |
| |
| import '../artifacts.dart'; |
| import '../asset.dart'; |
| import '../base/common.dart'; |
| import '../base/file_system.dart'; |
| import '../base/io.dart'; |
| import '../base/net.dart'; |
| import '../base/utils.dart'; |
| import '../build_info.dart'; |
| import '../bundle.dart'; |
| import '../cache.dart'; |
| import '../compile.dart'; |
| import '../convert.dart'; |
| import '../devfs.dart'; |
| import '../globals.dart' as globals; |
| import '../web/bootstrap.dart'; |
| import '../web/chrome.dart'; |
| |
| /// 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); |
| |
| // 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]. |
| /// |
| /// 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( |
| String hostname, |
| int port, |
| UrlTunneller urlTunneller, |
| BuildMode buildMode, |
| bool enableDwds, |
| Uri entrypoint, { |
| bool testMode = false, |
| }) async { |
| try { |
| final InternetAddress address = (await InternetAddress.lookup(hostname)).first; |
| final HttpServer httpServer = await HttpServer.bind(address, port); |
| // TODO(bkonyi): remove deprecated member usage, https://github.com/flutter/flutter/issues/51951 |
| // ignore: deprecated_member_use |
| final Packages packages = await loadPackagesFile( |
| Uri.base.resolve('.packages'), loader: (Uri uri) => globals.fs.file(uri).readAsBytes()); |
| final WebAssetServer server = WebAssetServer(httpServer, packages, address); |
| if (testMode) { |
| return server; |
| } |
| |
| // In release builds deploy a simpler proxy server. |
| if (buildMode != BuildMode.debug) { |
| final ReleaseAssetServer releaseAssetServer = ReleaseAssetServer(entrypoint); |
| shelf.serveRequests(httpServer, releaseAssetServer.handle); |
| return server; |
| } |
| // In debug builds, spin up DWDS and the full asset server. |
| final Dwds dwds = await Dwds.start( |
| assetReader: server, |
| buildResults: const Stream<BuildResult>.empty(), |
| chromeConnection: () async { |
| final Chrome chrome = await ChromeLauncher.connectedInstance; |
| return chrome.chromeConnection; |
| }, |
| urlEncoder: urlTunneller, |
| enableDebugging: true, |
| serveDevTools: false, |
| logWriter: (Level logLevel, String message) => globals.printTrace(message) |
| ); |
| shelf.Pipeline pipeline = const shelf.Pipeline(); |
| if (enableDwds) { |
| 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; |
| } on SocketException catch (err) { |
| throwToolExit('Failed to bind web development server:\n$err'); |
| } |
| assert(false); |
| return null; |
| } |
| |
| final HttpServer _httpServer; |
| // If holding these in memory is too much overhead, this can be switched to a |
| // RandomAccessFile and read on demand. |
| final Map<String, Uint8List> _files = <String, Uint8List>{}; |
| final Map<String, Uint8List> _sourcemaps = <String, Uint8List>{}; |
| // TODO(bkonyi): remove deprecated member usage, https://github.com/flutter/flutter/issues/51951 |
| // ignore: deprecated_member_use |
| final Packages _packages; |
| final InternetAddress internetAddress; |
| /* late final */ Dwds dwds; |
| |
| @visibleForTesting |
| Uint8List getFile(String path) => _files[path]; |
| |
| @visibleForTesting |
| Uint8List getSourceMap(String path) => _sourcemaps[path]; |
| |
| // handle requests for JavaScript source, dart sources maps, or asset files. |
| @visibleForTesting |
| Future<shelf.Response> handleRequest(shelf.Request request) async { |
| final Map<String, String> headers = <String, String>{}; |
| // If the response is `/`, then we are requesting the index file. |
| if (request.url.path == '/' || request.url.path.isEmpty) { |
| final File indexFile = globals.fs.currentDirectory |
| .childDirectory('web') |
| .childFile('index.html'); |
| if (indexFile.existsSync()) { |
| headers[HttpHeaders.contentTypeHeader] = 'text/html'; |
| headers[HttpHeaders.contentLengthHeader] = indexFile.lengthSync().toString(); |
| return shelf.Response.ok(indexFile.openRead(), headers: headers); |
| } |
| return shelf.Response.notFound(''); |
| } |
| |
| // Track etag headers for better caching of resources. |
| final String ifNoneMatch = request.headers[HttpHeaders.ifNoneMatchHeader]; |
| headers[HttpHeaders.cacheControlHeader] = 'max-age=0, must-revalidate'; |
| |
| // NOTE: shelf removes leading `/` for some reason. |
| final String requestPath = request.url.path.startsWith('/') |
| ? request.url.path |
| : '/${request.url.path}'; |
| |
| // If this is a JavaScript file, it must be in the in-memory cache. |
| // Attempt to look up the file by URI. |
| if (_files.containsKey(requestPath)) { |
| final List<int> bytes = getFile(requestPath); |
| // 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 (_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); |
| } |
| |
| 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 String webPath = globals.fs.path.join( |
| globals.fs.currentDirectory.childDirectory('web').path, requestPath.substring(1)); |
| file = globals.fs.file(webPath); |
| } |
| |
| if (!file.existsSync()) { |
| return shelf.Response.notFound(''); |
| } |
| |
| // For real files, use a serialized file stat as a revision |
| final String etag = file.lastModifiedSync().toIso8601String(); |
| 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 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; |
| 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() { |
| 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. |
| List<String> write(File codeFile, File manifestFile, File sourcemapFile) { |
| final List<String> modules = <String>[]; |
| final Uint8List codeBytes = codeFile.readAsBytesSync(); |
| final Uint8List sourcemapBytes = sourcemapFile.readAsBytesSync(); |
| final Map<String, dynamic> manifest = castStringKeyedMap(json.decode(manifestFile.readAsStringSync())); |
| for (final String filePath in manifest.keys) { |
| if (filePath == null) { |
| globals.printTrace('Invalid manfiest file: $filePath'); |
| continue; |
| } |
| 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>(); |
| if (codeOffsets.length != 2 || sourcemapOffsets.length != 2) { |
| globals.printTrace('Invalid manifest byte offsets: $offsets'); |
| continue; |
| } |
| |
| final int codeStart = codeOffsets[0]; |
| final int codeEnd = codeOffsets[1]; |
| if (codeStart < 0 || codeEnd > codeBytes.lengthInBytes) { |
| globals.printTrace('Invalid byte index: [$codeStart, $codeEnd]'); |
| continue; |
| } |
| final Uint8List byteView = Uint8List.view( |
| codeBytes.buffer, |
| codeStart, |
| codeEnd - codeStart, |
| ); |
| _files[filePath] = byteView; |
| |
| final int sourcemapStart = sourcemapOffsets[0]; |
| final int sourcemapEnd = sourcemapOffsets[1]; |
| if (sourcemapStart < 0 || sourcemapEnd > sourcemapBytes.lengthInBytes) { |
| globals.printTrace('Invalid byte index: [$sourcemapStart, $sourcemapEnd]'); |
| continue; |
| } |
| final Uint8List sourcemapView = Uint8List.view( |
| sourcemapBytes.buffer, |
| sourcemapStart, |
| sourcemapEnd - sourcemapStart, |
| ); |
| _sourcemaps['$filePath.map'] = sourcemapView; |
| |
| modules.add(filePath); |
| } |
| return modules; |
| } |
| |
| @visibleForTesting |
| final File dartSdk = globals.fs.file(globals.fs.path.join( |
| globals.artifacts.getArtifactPath(Artifact.flutterWebSdk), |
| 'kernel', |
| 'amd', |
| 'dart_sdk.js', |
| )); |
| |
| @visibleForTesting |
| final File dartSdkSourcemap = globals.fs.file(globals.fs.path.join( |
| globals.artifacts.getArtifactPath(Artifact.flutterWebSdk), |
| 'kernel', |
| 'amd', |
| 'dart_sdk.js.map', |
| )); |
| |
| // 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 dartSdk; |
| case '.dart_sdk.js.map': |
| return dartSdkSourcemap; |
| } |
| // 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 File packageFile = globals.fs.file(_packages.resolve(Uri( |
| scheme: 'package', pathSegments: segments.skip(1)))); |
| 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.getArtifactPath(Artifact.engineDartSdkPath)) |
| .parent; |
| final File dartSdkFile = globals.fs.file(globals.fs.path |
| .joinAll(<String>[dartSdkParent.path, ...segments])); |
| if (dartSdkFile.existsSync()) { |
| return dartSdkFile; |
| } |
| |
| final String flutterWebSdk = globals.artifacts |
| .getArtifactPath(Artifact.flutterWebSdk); |
| final File webSdkFile = globals.fs |
| .file(globals.fs.path.joinAll(<String>[flutterWebSdk, ...segments])); |
| |
| return webSdkFile; |
| } |
| |
| @override |
| Future<String> dartSourceContents(String serverPath) { |
| final File result = _resolveDartFile(serverPath); |
| if (result.existsSync()) { |
| return result.readAsString(); |
| } |
| return null; |
| } |
| |
| @override |
| Future<String> sourceMapContents(String serverPath) async { |
| return utf8.decode(_sourcemaps[serverPath]); |
| } |
| } |
| |
| class ConnectionResult { |
| ConnectionResult(this.appConnection, this.debugConnection); |
| |
| final AppConnection appConnection; |
| final DebugConnection debugConnection; |
| } |
| |
| /// 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 this.port, |
| @required this.packagesFilePath, |
| @required this.urlTunneller, |
| @required this.buildMode, |
| @required this.enableDwds, |
| @required this.entrypoint, |
| this.testMode = false, |
| }); |
| |
| final Uri entrypoint; |
| final String hostname; |
| final int port; |
| final String packagesFilePath; |
| final UrlTunneller urlTunneller; |
| final BuildMode buildMode; |
| final bool enableDwds; |
| final bool testMode; |
| |
| @visibleForTesting |
| 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 { |
| firstConnection.complete(ConnectionResult(appConnection, debugConnection)); |
| } |
| } 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; |
| |
| // 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( |
| hostname, |
| port, |
| urlTunneller, |
| buildMode, |
| enableDwds, |
| entrypoint, |
| testMode: testMode, |
| ); |
| _baseUri = Uri.parse('http://$hostname:$port'); |
| 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({ |
| 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, |
| bool skipAssets = false, |
| }) async { |
| assert(trackWidgetCreation != null); |
| assert(generator != null); |
| final String outputDirectoryPath = globals.fs.file(mainPath).parent.path; |
| |
| if (bundleFirstUpload) { |
| generator.addFileSystemRoot(outputDirectoryPath); |
| final String entrypoint = globals.fs.path.basename(mainPath); |
| webAssetServer.writeFile('/$entrypoint', globals.fs.file(mainPath).readAsStringSync()); |
| 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( |
| '/main.dart.js', |
| generateBootstrapScript( |
| requireUrl: _filePathToUriFragment(requireJS.path), |
| mapperUrl: _filePathToUriFragment(stackTraceMapper.path), |
| entrypoint: '/$entrypoint.lib.js', |
| ), |
| ); |
| webAssetServer.writeFile( |
| '/main_module.bootstrap.js', |
| generateMainModule( |
| entrypoint: '/$entrypoint.lib.js', |
| ), |
| ); |
| // TODO(jonahwilliams): switch to DWDS provided APIs when they are ready. |
| webAssetServer.writeFile('/basic.digests', '{}'); |
| |
| // 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 bootrstrap logic. To make it easier for DWDS to handle |
| // mapping the file name, this is done via an additional file root and |
| // specicial hard-coded scheme. |
| final CompilerOutput compilerOutput = await generator.recompile( |
| 'org-dartlang-app:///' + globals.fs.path.basename(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 { |
| 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'); |
| 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; |
| } |
| |
| @visibleForTesting |
| final File requireJS = globals.fs.file(globals.fs.path.join( |
| globals.artifacts.getArtifactPath(Artifact.engineDartSdkPath), |
| 'lib', |
| 'dev_compiler', |
| 'kernel', |
| 'amd', |
| 'require.js', |
| )); |
| |
| @visibleForTesting |
| final File stackTraceMapper = globals.fs.file(globals.fs.path.join( |
| globals.artifacts.getArtifactPath(Artifact.engineDartSdkPath), |
| 'lib', |
| 'dev_compiler', |
| 'web', |
| 'dart_stack_trace_mapper.js', |
| )); |
| } |
| |
| String _filePathToUriFragment(String path) { |
| if (globals.platform.isWindows) { |
| final bool startWithSlash = path.startsWith('/'); |
| final String partial = |
| globals.fs.path.split(path).skip(startWithSlash ? 2 : 1).join('/'); |
| if (partial.startsWith('/')) { |
| return partial; |
| } |
| return '/$partial'; |
| } |
| return path; |
| } |
| |
| class ReleaseAssetServer { |
| ReleaseAssetServer(this.entrypoint); |
| |
| final Uri entrypoint; |
| |
| // Locations where source files, assets, or source maps may be located. |
| final List<Uri> _searchPaths = <Uri>[ |
| globals.fs.directory(getWebBuildDirectory()).uri, |
| globals.fs.directory(Cache.flutterRoot).uri, |
| globals.fs.directory(Cache.flutterRoot).parent.uri, |
| globals.fs.currentDirectory.uri, |
| globals.fs.directory(globals.fsUtils.homeDirPath).uri, |
| ]; |
| |
| Future<shelf.Response> handle(shelf.Request request) async { |
| Uri fileUri; |
| if (request.url.toString() == 'main.dart') { |
| fileUri = entrypoint; |
| } else { |
| for (final Uri uri in _searchPaths) { |
| final Uri potential = uri.resolve(request.url.path); |
| if (potential == null || !globals.fs.isFileSync(potential.toFilePath())) { |
| continue; |
| } |
| fileUri = potential; |
| break; |
| } |
| } |
| if (fileUri != null) { |
| final File file = globals.fs.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, |
| }); |
| } |
| if (request.url.path == '') { |
| final File file = globals.fs.file(globals.fs.path.join(getWebBuildDirectory(), 'index.html')); |
| return shelf.Response.ok(file.readAsBytesSync(), headers: <String, String>{ |
| 'Content-Type': 'text/html', |
| }); |
| } |
| return shelf.Response.notFound(''); |
| } |
| } |