blob: e3b4f55e373368d1eedc0291e4b58df135fb3c8c [file]
// Copyright (c) 2019, the Dart project authors. Please see the AUTHORS file
// for details. 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:convert';
import 'dart:io';
import 'package:build_daemon/data/build_status.dart' as daemon;
import 'package:dwds/data/build_result.dart';
import 'package:dwds/dwds.dart';
import 'package:dwds/sdk_configuration.dart';
import 'package:file/local.dart';
import 'package:http/http.dart' as http;
import 'package:http/io_client.dart';
import 'package:http_multi_server/http_multi_server.dart';
import 'package:logging/logging.dart';
import 'package:shelf/shelf.dart';
import 'package:shelf_proxy/shelf_proxy.dart';
import '../command/configuration.dart';
import '../util.dart';
import 'chrome.dart';
import 'handlers/favicon_handler.dart';
import 'utils.dart' show findPackageConfigFilePath, findPackageConfigUri;
Logger _logger = Logger('WebDevServer');
const reloadedSourcesFileName = 'reloaded_sources.json';
const jsLibraryBundleExtension = '.ddc.js';
const multiRootScheme = 'org-dartlang-app';
class ServerOptions {
final Configuration configuration;
final int port;
final String target;
final int daemonPort;
ServerOptions(this.configuration, this.port, this.target, this.daemonPort);
}
class WebDevServer {
final HttpServer _server;
final http.Client _client;
final String _protocol;
final Stream<BuildResult> buildResults;
/// Can be null if client.js injection is disabled.
final Dwds? dwds;
final ExpressionCompilerService? ddcService;
final String target;
WebDevServer._(
this.target,
this._server,
this._client,
this._protocol,
this.buildResults,
bool autoRun, {
this.dwds,
this.ddcService,
}) {
if (autoRun) {
dwds?.connectedApps.listen((connection) {
connection.runMain();
});
}
}
String get host => _server.address.host;
int get port => _server.port;
String get protocol => _protocol;
Future<void> stop() async {
await dwds?.stop();
await ddcService?.stop();
await _server.close(force: true);
_client.close();
}
static Future<WebDevServer> start(
ServerOptions options,
Stream<daemon.BuildResults> buildResults,
) async {
final basePath = 'http://localhost:${options.port}';
var pipeline = const Pipeline();
if (options.configuration.logRequests) {
pipeline = pipeline.addMiddleware(logRequests());
}
pipeline = pipeline.addMiddleware(interceptFavicon);
/// JSON-ifiable list of sources that were reloaded in this restart and
/// follows the following format:
///
/// `src`: A string that corresponds to the file path containing a DDC library
/// bundle. To support embedded libraries, the path should include the
/// `baseUri` of the web server.
/// `module`: The name of the library bundle in `src`.
/// `libraries`: An array of strings containing the libraries that were
/// compiled in `src`.
///
/// For example:
/// ```json
/// [
/// {
/// "src": "<baseUri>/<file_name>",
/// "module": "<module_name>",
/// "libraries": ["<lib1>", "<lib2>"],
/// },
/// ]
/// ```
///
/// The path of the output file should stay consistent across the lifetime of
/// the app.
final reloadedSources = <Map<String, dynamic>>[];
// Only provide relevant build results
final filteredBuildResults = buildResults.asyncMap<BuildResult>((results) {
if (options.configuration.canaryFeatures) {
// Clear reloaded sources for the new build results.
reloadedSources.clear();
results.changedAssets?.forEach((uri) {
if (uri.path.endsWith(jsLibraryBundleExtension)) {
final reloadedSource = {
'src': ddcUriToSourceUrl(basePath, options.target, uri),
'module': ddcUriToLibraryId(uri),
'libraries': [ddcUriToLibraryId(uri)],
};
reloadedSources.add(reloadedSource);
}
});
}
final result = results.results.firstWhere(
(result) => result.target == options.target,
);
switch (result.status) {
case daemon.BuildStatus.started:
return BuildResult(status: BuildStatus.started);
case daemon.BuildStatus.failed:
return BuildResult(status: BuildStatus.failed);
case daemon.BuildStatus.succeeded:
return BuildResult(status: BuildStatus.succeeded);
default:
break;
}
throw StateError('Unexpected Daemon build result: $result');
});
var cascade = Cascade();
final client = IOClient(
HttpClient()
..maxConnectionsPerHost = 200
..idleTimeout = const Duration(seconds: 30)
..connectionTimeout = const Duration(seconds: 30),
);
final assetHandler = proxyHandler(
'http://localhost:${options.daemonPort}/${options.target}/',
client: client,
);
Dwds? dwds;
ExpressionCompilerService? ddcService;
if (options.configuration.enableInjectedClient) {
final assetReader = ProxyServerAssetReader(
options.daemonPort,
root: options.target,
);
// TODO(https://github.com/flutter/devtools/issues/5350): Figure out how
// to determine the build settings from the build.
// Can we save build metadata in build_web_compilers and and read it in
// the load strategy?
final buildSettings = BuildSettings(
appEntrypoint: Uri.parse(
'$multiRootScheme:///${options.target}/main.dart',
),
canaryFeatures: options.configuration.canaryFeatures,
isFlutterApp: false,
experiments: options.configuration.experiments,
);
final LoadStrategy loadStrategy;
if (options.configuration.canaryFeatures) {
final frontendServerFileSystem = LocalFileSystem();
final packageUriMapper = await PackageUriMapper.create(
frontendServerFileSystem,
findPackageConfigUri()!,
useDebuggerModuleNames: false,
);
loadStrategy = FrontendServerDdcLibraryBundleStrategyProvider(
options.configuration.reload,
assetReader,
packageUriMapper,
() async => {},
buildSettings,
packageConfigPath: findPackageConfigFilePath(),
reloadedSourcesUri: Uri.parse('$basePath/$reloadedSourcesFileName'),
injectScriptLoad: false,
).strategy;
} else {
loadStrategy = BuildRunnerRequireStrategyProvider(
options.configuration.reload,
assetReader,
buildSettings,
packageConfigPath: findPackageConfigFilePath(),
).strategy;
}
if (options.configuration.enableExpressionEvaluation) {
ddcService = ExpressionCompilerService(
options.configuration.hostname,
options.port,
verbose: options.configuration.verbose,
sdkConfigurationProvider: const DefaultSdkConfigurationProvider(),
);
}
final shouldServeDevTools =
options.configuration.debug || options.configuration.debugExtension;
final debugSettings = DebugSettings(
enableDebugExtension: options.configuration.debugExtension,
enableDebugging: options.configuration.debug,
ddsConfiguration: DartDevelopmentServiceConfiguration(
enable: !options.configuration.disableDds,
serveDevTools: shouldServeDevTools,
),
expressionCompiler: ddcService,
);
final appMetadata = AppMetadata(hostname: options.configuration.hostname);
final toolConfiguration = ToolConfiguration(
loadStrategy: loadStrategy,
debugSettings: debugSettings,
appMetadata: appMetadata,
);
dwds = await Dwds.start(
toolConfiguration: toolConfiguration,
assetReader: assetReader,
buildResults: filteredBuildResults,
chromeConnection: () async =>
(await Chrome.connectedInstance).chromeConnection,
);
pipeline = pipeline.addMiddleware(dwds.middleware);
cascade = cascade.add(dwds.handler);
if (options.configuration.canaryFeatures) {
// Add a handler to serve reloaded sources.
cascade = cascade.add((Request request) {
if (request.url.path == reloadedSourcesFileName) {
return Response.ok(
jsonEncode(reloadedSources),
headers: {'Content-Type': 'application/json'},
);
}
return Response.notFound('');
});
}
cascade = cascade.add(assetHandler);
} else {
cascade = cascade.add(assetHandler);
}
final hostname = options.configuration.hostname;
final tlsCertChain = options.configuration.tlsCertChain ?? '';
final tlsCertKey = options.configuration.tlsCertKey ?? '';
HttpServer server;
final protocol = (tlsCertChain.isNotEmpty && tlsCertKey.isNotEmpty)
? 'https'
: 'http';
if (protocol == 'https') {
final serverContext = SecurityContext()
..useCertificateChain(tlsCertChain)
..usePrivateKey(tlsCertKey);
server = await HttpMultiServer.bindSecure(
hostname,
options.port,
serverContext,
);
} else {
server = await HttpMultiServer.bind(hostname, options.port);
}
serveHttpRequests(server, pipeline.addHandler(cascade.handler), (e, s) {
_logger.warning('Error serving requests', e, s);
});
return WebDevServer._(
options.target,
server,
client,
protocol,
filteredBuildResults,
options.configuration.autoRun,
dwds: dwds,
ddcService: ddcService,
);
}
}
/// Transforms a package:build JS asset id [uri] into a source url compatible
/// with DDC's bootstrapper.
///
/// [basePath] is the path from which JS files as served up to but not
/// including the path.
/// [target] is the path whose files will be served from [basePath].
/// [uri] is the asset id's uri being transformed.
///
/// Example:
/// basePath: http://localhost:8080
/// target: web
///
/// uri: asset:some_package/web/main.ddc.js
/// returns http://localhost:8080/main.ddc.js
///
/// uri: package:some_package/src/sub_dir/file.ddc.js
/// returns http://localhost:8080/some_package/src/sub_dir/file.ddc.js
String ddcUriToSourceUrl(String basePath, String target, Uri uri) {
String jsPath;
if (uri.isScheme('asset')) {
// This indicates that this asset is the 'main' web asset. We directly
// serve all files under the package's [target] directory.
var pathParts = uri.pathSegments.skip(1);
if (pathParts.first == target) {
pathParts = pathParts.skip(1);
}
jsPath = pathParts.join('/');
} else if (uri.isScheme('package')) {
jsPath = 'packages/${uri.path}';
} else {
jsPath = uri.path;
}
return '$basePath/$jsPath';
}
/// Transforms a package:build JS asset id [uri] into a library id compatible
/// with DDC's bootstrapper.
///
/// Example:
/// uri: asset:some_package/web/main.ddc.js
/// returns org-dartlang-app:///web/main.dart
///
/// uri: package:some_package/src/sub_dir/file.ddc.js
/// returns package:some_package/src/sub_dir/file.dart
String ddcUriToLibraryId(Uri uri) {
final jsPath = uri.isScheme('package')
? 'package:${uri.path}'
: '$multiRootScheme:///${uri.path}';
final prefix = jsPath.substring(
0,
jsPath.length - jsLibraryBundleExtension.length,
);
return '$prefix.dart';
}