blob: b3f9dcf0324c8415d1a8675441764089aea58c38 [file] [log] [blame]
// 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:html/dom.dart';
import 'package:html/parser.dart';
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 '../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 '../base/utils.dart';
import '../build_info.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 '../web/bootstrap.dart';
import '../web/chrome.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 serveDevTools,
UrlEncoder urlEncoder,
bool spawnDds});
// 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);
final ResidentCompiler _generator;
@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(
globals.fs.file(compilerOutput.outputFilename).readAsBytesSync());
return ExpressionCompilationResult(
content, compilerOutput.errorCount > 0);
}
return ExpressionCompilationResult(
'InternalError: frontend server failed to compile \'$expression\'',
true);
}
@override
Future<bool> updateDependencies(Map<String, String> 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._buildInfo,
) : 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;
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] = _files[moduleName].hashCode.toString();
}
}
/// 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,
BuildInfo buildInfo,
bool enableDwds,
Uri entrypoint,
ExpressionCompiler expressionCompiler, {
bool testMode = false,
DwdsLauncher dwdsLauncher = Dwds.start,
}) async {
try {
InternetAddress address;
if (hostname == 'any') {
address = InternetAddress.anyIPv4;
} else {
address = (await InternetAddress.lookup(hostname)).first;
}
final HttpServer httpServer = await HttpServer.bind(address, port);
// Allow rendering in a iframe.
httpServer.defaultResponseHeaders.remove('x-frame-options', 'SAMEORIGIN');
final PackageConfig packageConfig = await loadPackageConfigWithLogging(
globals.fs.file(buildInfo.packagesPath),
logger: globals.logger,
);
final Map<String, String> digests = <String, String>{};
final Map<String, String> modules = <String, String>{};
final WebAssetServer server = WebAssetServer(
httpServer,
packageConfig,
address,
modules,
digests,
buildInfo,
);
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);
final shelf.Middleware 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);
};
};
// 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,
serveDevTools: false,
loadStrategy: FrontendServerRequireStrategyProvider(
ReloadConfiguration.none, server, _digestProvider)
.strategy,
expressionCompiler: expressionCompiler,
spawnDds: true);
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;
} on SocketException catch (err) {
throwToolExit('Failed to bind web development server:\n$err');
}
assert(false);
return null;
}
final BuildInfo _buildInfo;
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>{};
final Map<String, Uint8List> _metadataFiles = <String, Uint8List>{};
String _mergedMetadata;
final PackageConfig _packages;
final InternetAddress internetAddress;
/* late final */ Dwds dwds;
Directory entrypointCacheDirectory;
@visibleForTesting
HttpHeaders get defaultResponseHeaders => _httpServer.defaultResponseHeaders;
@visibleForTesting
Uint8List getFile(String path) => _files[path];
@visibleForTesting
Uint8List getSourceMap(String path) => _sourcemaps[path];
@visibleForTesting
Uint8List getMetadata(String path) => _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 (_files.containsKey(requestPath) || _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 (_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 (_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() {
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) {
_files[filePath] = 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, File metadataFile) {
final List<String> modules = <String>[];
final Uint8List codeBytes = codeFile.readAsBytesSync();
final Uint8List sourcemapBytes = sourcemapFile.readAsBytesSync();
final Uint8List metadataBytes = metadataFile.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>();
final List<int> metadataOffsets =
(offsets['metadata'] as List<dynamic>).cast<int>();
if (codeOffsets.length != 2 ||
sourcemapOffsets.length != 2 ||
metadataOffsets.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,
);
final String fileName =
filePath.startsWith('/') ? filePath.substring(1) : filePath;
_files[fileName] = 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,
);
final String sourcemapName = '$fileName.map';
_sourcemaps[sourcemapName] = sourcemapView;
final int metadataStart = metadataOffsets[0];
final int metadataEnd = metadataOffsets[1];
if (metadataStart < 0 || metadataEnd > metadataBytes.lengthInBytes) {
globals
.printTrace('Invalid byte index: [$metadataStart, $metadataEnd]');
continue;
}
final Uint8List metadataView = Uint8List.view(
metadataBytes.buffer,
metadataStart,
metadataEnd - metadataStart,
);
final String metadataName = '$fileName.metadata';
_metadataFiles[metadataName] = metadataView;
modules.add(fileName);
}
_mergedMetadata = _metadataFiles.values
.map((Uint8List encoded) => utf8.decode(encoded))
.join('\n');
return modules;
}
/// Whether to use the canvaskit SDK for rendering.
bool canvasKitRendering = false;
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()) {
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':
if (_buildInfo.nullSafetyMode == NullSafetyMode.unsound) {
return globals.fs.file(canvasKitRendering
? globals.artifacts
.getArtifactPath(Artifact.webPrecompiledCanvaskitSdk)
: globals.artifacts.getArtifactPath(Artifact.webPrecompiledSdk));
} else {
return globals.fs.file(canvasKitRendering
? globals.artifacts
.getArtifactPath(Artifact.webPrecompiledCanvaskitSoundSdk)
: globals.artifacts
.getArtifactPath(Artifact.webPrecompiledSoundSdk));
}
break;
case 'dart_sdk.js.map':
if (_buildInfo.nullSafetyMode == NullSafetyMode.unsound) {
return globals.fs.file(canvasKitRendering
? globals.artifacts.getArtifactPath(
Artifact.webPrecompiledCanvaskitSdkSourcemaps)
: globals.artifacts
.getArtifactPath(Artifact.webPrecompiledSdkSourcemaps));
} else {
return globals.fs.file(canvasKitRendering
? globals.artifacts.getArtifactPath(
Artifact.webPrecompiledCanvaskitSoundSdkSourcemaps)
: globals.artifacts
.getArtifactPath(Artifact.webPrecompiledSoundSdkSourcemaps));
}
}
// 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.getArtifactPath(Artifact.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.getArtifactPath(Artifact.flutterWebSdk));
final File webSdkFile = globals.fs.file(flutterWebSdk.uri.resolve(path));
return webSdkFile;
}
@override
Future<String> dartSourceContents(String serverPath) async {
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]);
}
@override
Future<String> metadataContents(String serverPath) async {
if (serverPath == 'main_module.ddc_merged_metadata') {
return _mergedMetadata;
}
if (_metadataFiles.containsKey(serverPath)) {
return utf8.decode(_metadataFiles[serverPath]);
}
return null;
}
@override
Future<void> close() async {}
}
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.useSseForDebugProxy,
@required this.useSseForDebugBackend,
@required this.buildInfo,
@required this.enableDwds,
@required this.entrypoint,
@required this.expressionCompiler,
@required this.chromiumLauncher,
@required this.nullAssertions,
this.testMode = false,
});
final Uri entrypoint;
final String hostname;
final int port;
final String packagesFilePath;
final UrlTunneller urlTunneller;
final bool useSseForDebugProxy;
final bool useSseForDebugBackend;
final BuildInfo buildInfo;
final bool enableDwds;
final bool testMode;
final ExpressionCompiler expressionCompiler;
final ChromiumLauncher chromiumLauncher;
final bool nullAssertions;
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;
@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,
buildInfo,
enableDwds,
entrypoint,
expressionCompiler,
testMode: testMode,
);
if (buildInfo.dartDefines.contains('FLUTTER_WEB_USE_SKIA=true')) {
webAssetServer.canvasKitRendering = true;
}
if (hostname == 'any') {
_baseUri = Uri.http('localhost:$port', '');
} else {
_baseUri = Uri.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({
@required Uri mainUri,
@required ResidentCompiler generator,
@required bool trackWidgetCreation,
@required String pathToReload,
@required List<Uri> invalidatedFiles,
@required PackageConfig packageConfig,
DevFSWriter devFSWriter,
String target,
AssetBundle bundle,
DateTime firstBuildTime,
bool bundleFirstUpload = false,
String dillOutputPath,
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,
),
);
// 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 ??
getDefaultApplicationKernelPath(
trackWidgetCreation: trackWidgetCreation),
packageConfig: packageConfig,
);
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.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.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',
));
@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, kPackagesFileName);
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 = '';
} 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
''';