Add basic web device and run support (#28302)
This commit is contained in:
parent
ce06ef43fb
commit
6f5f037689
10
dev/integration_tests/web/web/index.html
Normal file
10
dev/integration_tests/web/web/index.html
Normal file
@ -0,0 +1,10 @@
|
||||
<!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;
|
||||
}
|
||||
observatoryUris = <Uri>[result.observatoryUri];
|
||||
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,
|
||||
|
206
packages/flutter_tools/lib/src/web/web_device.dart
Normal file
206
packages/flutter_tools/lib/src/web/web_device.dart
Normal file
@ -0,0 +1,206 @@
|
||||
// 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');
|
||||
}
|
||||
}
|
10
packages/flutter_tools/templates/web/index.html.tmpl
Normal file
10
packages/flutter_tools/templates/web/index.html.tmpl
Normal file
@ -0,0 +1,10 @@
|
||||
<!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>
|
46
packages/flutter_tools/test/web/devices_test.dart
Normal file
46
packages/flutter_tools/test/web/devices_test.dart
Normal file
@ -0,0 +1,46 @@
|
||||
// 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 {}
|
||||
|
Loading…
x
Reference in New Issue
Block a user