Unverified Commit 6f5f0376 authored by Jonah Williams's avatar Jonah Williams Committed by GitHub

Add basic web device and run support (#28302)

parent ce06ef43
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>web_integration</title>
<script defer src="main.dart.js" type="application/javascript"></script>
</head>
<body>
</body>
</html>
......@@ -21,6 +21,7 @@ import 'ios/plist_utils.dart' as plist;
import 'macos/application_package.dart';
import 'project.dart';
import 'tester/flutter_tester.dart';
import 'web/web_device.dart';
class ApplicationPackageFactory {
static ApplicationPackageFactory get instance => context[ApplicationPackageFactory];
......@@ -50,10 +51,11 @@ class ApplicationPackageFactory {
return applicationBinary != null
? MacOSApp.fromPrebuiltApp(applicationBinary)
: null;
case TargetPlatform.web:
return WebApplicationPackage(await FlutterProject.current());
case TargetPlatform.linux_x64:
case TargetPlatform.windows_x64:
case TargetPlatform.fuchsia:
case TargetPlatform.web:
return null;
}
assert(platform != null);
......
......@@ -41,6 +41,7 @@ import 'run_hot.dart';
import 'usage.dart';
import 'version.dart';
import 'web/compile.dart';
import 'web/web_device.dart';
import 'windows/windows_workflow.dart';
Future<T> runInContext<T>(
......@@ -65,6 +66,7 @@ Future<T> runInContext<T>(
CocoaPods: () => CocoaPods(),
CocoaPodsValidator: () => const CocoaPodsValidator(),
Config: () => Config(),
ChromeLauncher: () => const ChromeLauncher(),
DevFSConfig: () => DevFSConfig(),
DeviceManager: () => DeviceManager(),
Doctor: () => const Doctor(),
......
......@@ -20,6 +20,7 @@ import 'ios/simulators.dart';
import 'linux/linux_device.dart';
import 'macos/macos_device.dart';
import 'tester/flutter_tester.dart';
import 'web/web_device.dart';
import 'windows/windows_device.dart';
DeviceManager get deviceManager => context[DeviceManager];
......@@ -36,7 +37,7 @@ class DeviceManager {
IOSSimulators(),
FuchsiaDevices(),
FlutterTesterDevices(),
] + _conditionalDesktopDevices);
] + _conditionalDesktopDevices + _conditionalWebDevices);
/// Only add desktop devices if the flag is enabled.
static List<DeviceDiscovery> get _conditionalDesktopDevices {
......@@ -47,6 +48,13 @@ class DeviceManager {
] : <DeviceDiscovery>[];
}
/// Only add web devices if the flag is enabled.
static List<DeviceDiscovery> get _conditionalWebDevices {
return flutterWebEnabled ? <DeviceDiscovery>[
WebDevices(),
] : <DeviceDiscovery>[];
}
String _specifiedDeviceId;
/// A user-specified device ID.
......
......@@ -19,6 +19,7 @@ import 'ios/plist_utils.dart' as plist;
import 'ios/xcodeproj.dart' as xcode;
import 'plugins.dart';
import 'template.dart';
import 'web/web_device.dart';
/// Represents the contents of a Flutter project at the specified [directory].
///
......@@ -95,6 +96,9 @@ class FlutterProject {
/// The Android sub project of this project.
AndroidProject get android => AndroidProject._(this);
/// The web sub project of this project.
WebProject get web => WebProject._(this);
/// The `pubspec.yaml` file of this project.
File get pubspecFile => directory.childFile('pubspec.yaml');
......@@ -150,6 +154,9 @@ class FlutterProject {
refreshPluginsList(this);
await android.ensureReadyForPlatformSpecificTooling();
await ios.ensureReadyForPlatformSpecificTooling();
if (flutterWebEnabled) {
await web.ensureReadyForPlatformSpecificTooling();
}
await injectPlugins(this);
}
......@@ -454,6 +461,31 @@ class AndroidProject {
}
}
/// Represents the web sub-project of a Flutter project.
class WebProject {
WebProject._(this.parent);
final FlutterProject parent;
Future<void> ensureReadyForPlatformSpecificTooling() async {
/// Generate index.html in build/web. Eventually we could support
/// a custom html under the web sub directory.
final Directory outputDir = fs.directory(getWebBuildDirectory());
if (!outputDir.existsSync()) {
outputDir.createSync(recursive: true);
}
final Template template = Template.fromName('web/index.html.tmpl');
template.render(
outputDir,
<String, dynamic>{
'appName': parent.manifest.appName,
},
printStatusWhenWriting: false,
overwriteExisting: true,
);
}
}
/// Deletes [directory] with all content.
void _deleteIfExistsSync(Directory directory) {
if (directory.existsSync())
......
......@@ -326,7 +326,11 @@ class FlutterDevice {
await stopEchoingDeviceLog();
return 2;
}
if (result.hasObservatory) {
observatoryUris = <Uri>[result.observatoryUri];
} else {
observatoryUris = <Uri>[];
}
return 0;
}
......@@ -384,8 +388,11 @@ class FlutterDevice {
await stopEchoingDeviceLog();
return 2;
}
if (result.hasObservatory)
if (result.hasObservatory) {
observatoryUris = <Uri>[result.observatoryUri];
} else {
observatoryUris = <Uri>[];
}
return 0;
}
......
......@@ -38,6 +38,7 @@ class WebCompiler {
if (!processManager.canRun(engineDartPath)) {
throwToolExit('Unable to find Dart binary at $engineDartPath');
}
/// Compile Dart to JavaScript.
final List<String> command = <String>[
engineDartPath,
dart2jsPath,
......
// Copyright 2019 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import '../application_package.dart';
import '../base/context.dart';
import '../base/file_system.dart';
import '../base/io.dart';
import '../base/logger.dart';
import '../base/platform.dart';
import '../base/process_manager.dart';
import '../build_info.dart';
import '../device.dart';
import '../globals.dart';
import '../project.dart';
import '../web/compile.dart';
ChromeLauncher get chromeLauncher => context[ChromeLauncher];
/// Only launch or display web devices if `FLUTTER_WEB`
/// environment variable is set to true.
bool get flutterWebEnabled {
_flutterWebEnabled = platform.environment['FLUTTER_WEB']?.toLowerCase() == 'true';
return _flutterWebEnabled;
}
bool _flutterWebEnabled;
class WebApplicationPackage extends ApplicationPackage {
WebApplicationPackage(this._flutterProject) : super(id: _flutterProject.manifest.appName);
final FlutterProject _flutterProject;
@override
String get name => _flutterProject.manifest.appName;
/// The location of the web source assets.
Directory get webSourcePath => _flutterProject.directory.childDirectory('web');
}
class WebDevice extends Device {
WebDevice() : super('web');
HttpServer _server;
WebApplicationPackage _package;
@override
bool get supportsHotReload => false;
@override
bool get supportsHotRestart => false;
@override
bool get supportsStartPaused => true;
@override
bool get supportsStopApp => true;
@override
bool get supportsScreenshot => false;
@override
void clearLogs() {}
@override
DeviceLogReader getLogReader({ApplicationPackage app}) {
return NoOpDeviceLogReader(app.name);
}
@override
Future<bool> installApp(ApplicationPackage app) async => true;
@override
Future<bool> isAppInstalled(ApplicationPackage app) async => true;
@override
Future<bool> isLatestBuildInstalled(ApplicationPackage app) async => true;
@override
Future<bool> get isLocalEmulator async => false;
@override
bool isSupported() => flutterWebEnabled;
@override
String get name => 'web';
@override
DevicePortForwarder get portForwarder => const NoOpDevicePortForwarder();
@override
Future<String> get sdkNameAndVersion async => 'web';
@override
Future<LaunchResult> startApp(
covariant WebApplicationPackage package, {
String mainPath,
String route,
DebuggingOptions debuggingOptions,
Map<String, Object> platformArgs,
bool prebuiltApplication = false,
bool applicationNeedsRebuild = false,
bool usesTerminalUi = true,
bool ipv6 = false,
}) async {
final Status status = logger.startProgress('Compiling ${package.name} to JavaScript...', timeout: null);
final int result = await webCompiler.compile(target: mainPath, minify: false, enabledAssertions: true);
status.stop();
if (result != 0) {
printError('Failed to compile ${package.name} to JavaScript');
return LaunchResult.failed();
}
_package = package;
_server = await HttpServer.bind(InternetAddress.loopbackIPv4, 0);
_server.listen(_basicAssetServer);
printStatus('Serving assets from http:localhost:${_server.port}');
await chromeLauncher.launch('http:localhost:${_server.port}');
return LaunchResult.succeeded(observatoryUri: null);
}
// Note: we don't currently have a way to track which chrome processes
// belong to the flutter tool, so we'll err on the side of caution by
// keeping these open.
@override
Future<bool> stopApp(ApplicationPackage app) async {
await _server?.close();
_server = null;
return true;
}
@override
Future<TargetPlatform> get targetPlatform async => TargetPlatform.web;
@override
Future<bool> uninstallApp(ApplicationPackage app) async => true;
Future<void> _basicAssetServer(HttpRequest request) async {
if (request.method != 'GET') {
request.response.statusCode = HttpStatus.forbidden;
await request.response.close();
return;
}
// Resolve all get requests to the build/web/asset directory.
final Uri uri = request.uri;
File file;
String contentType;
if (uri.path == '/') {
file = _package.webSourcePath.childFile('index.html');
contentType = 'text/html';
} else if (uri.path == '/main.dart.js') {
file = fs.file(fs.path.join(getWebBuildDirectory(), 'main.dart.js'));
contentType = 'text/javascript';
} else {
file = fs.file(fs.path.join(getAssetBuildDirectory(), uri.path));
}
if (!file.existsSync()) {
request.response.statusCode = HttpStatus.notFound;
await request.response.close();
return;
}
request.response.statusCode = HttpStatus.ok;
if (contentType != null) {
request.response.headers.add(HttpHeaders.contentTypeHeader, contentType);
}
await request.response.addStream(file.openRead());
await request.response.close();
}
}
class WebDevices extends PollingDeviceDiscovery {
WebDevices() : super('web');
final WebDevice _webDevice = WebDevice();
@override
bool get canListAnything => flutterWebEnabled;
@override
Future<List<Device>> pollingGetDevices() async {
return <Device>[
_webDevice,
];
}
@override
bool get supportsPlatform => flutterWebEnabled;
}
// Responsible for launching chrome with devtools configured.
class ChromeLauncher {
const ChromeLauncher();
static const String _kMacosLocation = '/Applications/Google\ Chrome.app/Contents/MacOS/Google\ Chrome';
Future<void> launch(String host) async {
if (platform.isMacOS) {
await processManager.start(<String>[
_kMacosLocation,
host,
]);
}
throw UnsupportedError('$platform is not supported');
}
}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>{{appName}}</title>
<script defer src="main.dart.js" type="application/javascript"></script>
</head>
<body>
</body>
</html>
// Copyright 2019 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:flutter_tools/src/base/file_system.dart';
import 'package:flutter_tools/src/base/platform.dart';
import 'package:flutter_tools/src/project.dart';
import 'package:flutter_tools/src/web/compile.dart';
import 'package:flutter_tools/src/web/web_device.dart';
import 'package:mockito/mockito.dart';
import '../src/common.dart';
import '../src/context.dart';
void main() {
group(WebDevice, () {
final MockWebCompiler mockWebCompiler = MockWebCompiler();
final MockChromeLauncher mockChromeLauncher = MockChromeLauncher();
final MockPlatform mockPlatform = MockPlatform();
FlutterProject flutterProject;
setUp(() async {
flutterProject = await FlutterProject.fromPath(fs.path.join(getFlutterRoot(), 'dev', 'integration_tests', 'web'));
when(mockWebCompiler.compile(
target: anyNamed('target'),
minify: anyNamed('minify'),
enabledAssertions: anyNamed('enabledAssertions'),
)).thenAnswer((Invocation invocation) async => 0);
when(mockChromeLauncher.launch(any)).thenAnswer((Invocation invocation) async {});
});
testUsingContext('can build and connect to chrome', () async {
final WebDevice device = WebDevice();
await device.startApp(WebApplicationPackage(flutterProject));
}, overrides: <Type, Generator>{
ChromeLauncher: () => mockChromeLauncher,
WebCompiler: () => mockWebCompiler,
Platform: () => mockPlatform,
});
});
}
class MockChromeLauncher extends Mock implements ChromeLauncher {}
class MockWebCompiler extends Mock implements WebCompiler {}
class MockPlatform extends Mock implements Platform {}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment