mirror of
https://github.com/flutter/flutter.git
synced 2026-02-20 02:29:02 +08:00
Add devfs for incremental compiler JavaScript bundle (#43219)
This commit is contained in:
parent
ed931e7941
commit
4c47fdadd4
@ -52,6 +52,7 @@ export 'dart:io'
|
||||
HttpException,
|
||||
HttpHeaders,
|
||||
HttpRequest,
|
||||
HttpResponse,
|
||||
HttpServer,
|
||||
HttpStatus,
|
||||
InternetAddress,
|
||||
|
||||
160
packages/flutter_tools/lib/src/web/devfs_web.dart
Normal file
160
packages/flutter_tools/lib/src/web/devfs_web.dart
Normal file
@ -0,0 +1,160 @@
|
||||
// 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 'dart:typed_data';
|
||||
|
||||
import 'package:meta/meta.dart';
|
||||
import 'package:mime/mime.dart' as mime;
|
||||
|
||||
import '../base/common.dart';
|
||||
import '../base/file_system.dart';
|
||||
import '../base/io.dart';
|
||||
import '../build_info.dart';
|
||||
import '../convert.dart';
|
||||
import '../globals.dart';
|
||||
|
||||
/// A web server which handles serving JavaScript and assets.
|
||||
///
|
||||
/// This is only used in development mode.
|
||||
class WebAssetServer {
|
||||
@visibleForTesting
|
||||
WebAssetServer(this._httpServer, { @required void Function(dynamic, StackTrace) onError }) {
|
||||
_httpServer.listen((HttpRequest request) {
|
||||
_handleRequest(request).catchError(onError);
|
||||
// TODO(jonahwilliams): test the onError callback when https://github.com/dart-lang/sdk/issues/39094 is fixed.
|
||||
}, onError: onError);
|
||||
}
|
||||
|
||||
// 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].
|
||||
///
|
||||
/// Unhandled exceptions will throw a [ToolExit] with the error and stack
|
||||
/// trace.
|
||||
static Future<WebAssetServer> start(String hostname, int port) async {
|
||||
try {
|
||||
final HttpServer httpServer = await HttpServer.bind(hostname, port);
|
||||
return WebAssetServer(httpServer, onError: (dynamic error, StackTrace stackTrace) {
|
||||
httpServer.close(force: true);
|
||||
throwToolExit('Unhandled exception in web development server:\n$error\n$stackTrace');
|
||||
});
|
||||
} on SocketException catch (err) {
|
||||
throwToolExit('Failed to bind web development server:\n$err');
|
||||
}
|
||||
assert(false);
|
||||
return null;
|
||||
}
|
||||
|
||||
final HttpServer _httpServer;
|
||||
final Map<String, Uint8List> _files = <String, Uint8List>{};
|
||||
|
||||
// handle requests for JavaScript source, dart sources maps, or asset files.
|
||||
Future<void> _handleRequest(HttpRequest request) async {
|
||||
final HttpResponse response = request.response;
|
||||
// If the response is `/`, then we are requesting the index file.
|
||||
if (request.uri.path == '/') {
|
||||
final File indexFile = fs.currentDirectory
|
||||
.childDirectory('web')
|
||||
.childFile('index.html');
|
||||
if (indexFile.existsSync()) {
|
||||
response.headers.add('Content-Type', 'text/html');
|
||||
response.headers.add('Content-Length', indexFile.lengthSync());
|
||||
await response.addStream(indexFile.openRead());
|
||||
} else {
|
||||
response.statusCode = HttpStatus.notFound;
|
||||
}
|
||||
await response.close();
|
||||
return;
|
||||
}
|
||||
|
||||
// If this is a JavaScript file, it must be in the in-memory cache.
|
||||
// Attempt to look up the file by URI, returning a 404 if it is not
|
||||
// found.
|
||||
if (_files.containsKey(request.uri.path)) {
|
||||
final List<int> bytes = _files[request.uri.path];
|
||||
response.headers
|
||||
..add('Content-Length', bytes.length)
|
||||
..add('Content-Type', 'application/javascript');
|
||||
response.add(bytes);
|
||||
await response.close();
|
||||
return;
|
||||
}
|
||||
// If this is a dart file, it must be on the local file system and is
|
||||
// likely coming from a source map request. Attempt to look in the
|
||||
// local filesystem for it, and return a 404 if it is not found. The tool
|
||||
// doesn't currently consider the case of Dart files as assets.
|
||||
File file = fs.file(Uri.base.resolve(request.uri.path));
|
||||
|
||||
// If both 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 String assetPath = request.uri.path.replaceFirst('/assets/', '');
|
||||
file = fs.file(fs.path.join(getAssetBuildDirectory(), fs.path.relative(assetPath)));
|
||||
}
|
||||
|
||||
if (!file.existsSync()) {
|
||||
response.statusCode = HttpStatus.notFound;
|
||||
await response.close();
|
||||
return;
|
||||
}
|
||||
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;
|
||||
response.headers.add('Content-Length', length);
|
||||
response.headers.add('Content-Type', mimeType);
|
||||
await response.addStream(file.openRead());
|
||||
await response.close();
|
||||
}
|
||||
|
||||
/// 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 sourceFile, File manifestFile) {
|
||||
final List<String> modules = <String>[];
|
||||
final Uint8List bytes = sourceFile.readAsBytesSync();
|
||||
final Map<String, Object> manifest = json.decode(manifestFile.readAsStringSync());
|
||||
for (String filePath in manifest.keys) {
|
||||
if (filePath == null) {
|
||||
printTrace('Invalid manfiest file: $filePath');
|
||||
continue;
|
||||
}
|
||||
final List<Object> offsets = manifest[filePath];
|
||||
if (offsets.length != 2) {
|
||||
printTrace('Invalid manifest byte offsets: $offsets');
|
||||
continue;
|
||||
}
|
||||
final int start = offsets[0];
|
||||
final int end = offsets[1];
|
||||
if (start < 0 || end > bytes.lengthInBytes) {
|
||||
printTrace('Invalid byte index: [$start, $end]');
|
||||
continue;
|
||||
}
|
||||
final Uint8List byteView = Uint8List.view(bytes.buffer, start, end - start);
|
||||
_files[filePath] = byteView;
|
||||
modules.add(filePath);
|
||||
}
|
||||
return modules;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,266 @@
|
||||
// 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 'dart:async';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter_tools/src/base/common.dart';
|
||||
import 'package:flutter_tools/src/base/file_system.dart';
|
||||
import 'package:flutter_tools/src/base/io.dart';
|
||||
import 'package:flutter_tools/src/base/platform.dart';
|
||||
import 'package:flutter_tools/src/convert.dart';
|
||||
import 'package:flutter_tools/src/web/devfs_web.dart';
|
||||
import 'package:mockito/mockito.dart';
|
||||
|
||||
import '../../src/common.dart';
|
||||
import '../../src/testbed.dart';
|
||||
|
||||
const List<int> kTransparentImage = <int>[
|
||||
0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A, 0x00, 0x00, 0x00, 0x0D, 0x49,
|
||||
0x48, 0x44, 0x52, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x08, 0x06,
|
||||
0x00, 0x00, 0x00, 0x1F, 0x15, 0xC4, 0x89, 0x00, 0x00, 0x00, 0x0A, 0x49, 0x44,
|
||||
0x41, 0x54, 0x78, 0x9C, 0x63, 0x00, 0x01, 0x00, 0x00, 0x05, 0x00, 0x01, 0x0D,
|
||||
0x0A, 0x2D, 0xB4, 0x00, 0x00, 0x00, 0x00, 0x49, 0x45, 0x4E, 0x44, 0xAE,
|
||||
];
|
||||
|
||||
void main() {
|
||||
MockHttpServer mockHttpServer;
|
||||
StreamController<HttpRequest> requestController;
|
||||
Testbed testbed;
|
||||
MockHttpRequest request;
|
||||
MockHttpResponse response;
|
||||
MockHttpHeaders headers;
|
||||
Completer<void> closeCompleter;
|
||||
WebAssetServer webAssetServer;
|
||||
MockPlatform windows;
|
||||
MockPlatform linux;
|
||||
|
||||
setUp(() {
|
||||
windows = MockPlatform();
|
||||
linux = MockPlatform();
|
||||
when(windows.environment).thenReturn(const <String, String>{});
|
||||
when(windows.isWindows).thenReturn(true);
|
||||
when(linux.isWindows).thenReturn(false);
|
||||
when(linux.environment).thenReturn(const <String, String>{});
|
||||
testbed = Testbed(setup: () {
|
||||
mockHttpServer = MockHttpServer();
|
||||
requestController = StreamController<HttpRequest>.broadcast();
|
||||
request = MockHttpRequest();
|
||||
response = MockHttpResponse();
|
||||
headers = MockHttpHeaders();
|
||||
closeCompleter = Completer<void>();
|
||||
when(mockHttpServer.listen(any, onError: anyNamed('onError'))).thenAnswer((Invocation invocation) {
|
||||
final Function callback = invocation.positionalArguments.first;
|
||||
return requestController.stream.listen(callback);
|
||||
});
|
||||
when(request.response).thenReturn(response);
|
||||
when(response.headers).thenReturn(headers);
|
||||
when(response.close()).thenAnswer((Invocation invocation) async {
|
||||
closeCompleter.complete();
|
||||
});
|
||||
webAssetServer = WebAssetServer(mockHttpServer, onError: (dynamic error, StackTrace stackTrace) {
|
||||
closeCompleter.completeError(error, stackTrace);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
tearDown(() async {
|
||||
await webAssetServer.dispose();
|
||||
await requestController.close();
|
||||
});
|
||||
|
||||
test('Throws a tool exit if bind fails with a SocketException', () => testbed.run(() async {
|
||||
expect(WebAssetServer.start('hello', 1234), throwsA(isInstanceOf<ToolExit>()));
|
||||
}));
|
||||
|
||||
test('Can catch exceptions through the onError callback', () => testbed.run(() async {
|
||||
when(response.close()).thenAnswer((Invocation invocation) {
|
||||
throw StateError('Something bad');
|
||||
});
|
||||
webAssetServer.writeFile('/foo.js', 'main() {}');
|
||||
|
||||
when(request.uri).thenReturn(Uri.parse('http://foobar/foo.js'));
|
||||
requestController.add(request);
|
||||
|
||||
expect(closeCompleter.future, throwsA(isInstanceOf<StateError>()));
|
||||
}));
|
||||
|
||||
test('Handles against malformed manifest', () => testbed.run(() async {
|
||||
final File source = fs.file('source')
|
||||
..writeAsStringSync('main() {}');
|
||||
|
||||
// Missing ending offset.
|
||||
final File manifestMissingOffset = fs.file('manifestA')
|
||||
..writeAsStringSync(json.encode(<String, Object>{'/foo.js': <int>[0]}));
|
||||
// Non-file URI.
|
||||
final File manifestNonFileScheme = fs.file('manifestA')
|
||||
..writeAsStringSync(json.encode(<String, Object>{'/foo.js': <int>[0, 10]}));
|
||||
|
||||
final File manifestOutOfBounds = fs.file('manifest')
|
||||
..writeAsStringSync(json.encode(<String, Object>{'/foo.js': <int>[0, 100]}));
|
||||
|
||||
expect(webAssetServer.write(source, manifestMissingOffset), isEmpty);
|
||||
expect(webAssetServer.write(source, manifestNonFileScheme), isEmpty);
|
||||
expect(webAssetServer.write(source, manifestOutOfBounds), isEmpty);
|
||||
}));
|
||||
|
||||
test('serves JavaScript files from in memory cache', () => testbed.run(() async {
|
||||
final File source = fs.file('source')
|
||||
..writeAsStringSync('main() {}');
|
||||
final File manifest = fs.file('manifest')
|
||||
..writeAsStringSync(json.encode(<String, Object>{'/foo.js': <int>[0, source.lengthSync()]}));
|
||||
webAssetServer.write(source, manifest);
|
||||
|
||||
when(request.uri).thenReturn(Uri.parse('http://foobar/foo.js'));
|
||||
requestController.add(request);
|
||||
await closeCompleter.future;
|
||||
|
||||
verify(headers.add('Content-Length', source.lengthSync())).called(1);
|
||||
verify(headers.add('Content-Type', 'application/javascript')).called(1);
|
||||
verify(response.add(source.readAsBytesSync())).called(1);
|
||||
}));
|
||||
|
||||
test('serves JavaScript files from in memory cache not from manifest', () => testbed.run(() async {
|
||||
webAssetServer.writeFile('/foo.js', 'main() {}');
|
||||
|
||||
when(request.uri).thenReturn(Uri.parse('http://foobar/foo.js'));
|
||||
requestController.add(request);
|
||||
await closeCompleter.future;
|
||||
|
||||
verify(headers.add('Content-Length', 9)).called(1);
|
||||
verify(headers.add('Content-Type', 'application/javascript')).called(1);
|
||||
verify(response.add(any)).called(1);
|
||||
}));
|
||||
|
||||
test('handles missing JavaScript files from in memory cache', () => testbed.run(() async {
|
||||
final File source = fs.file('source')
|
||||
..writeAsStringSync('main() {}');
|
||||
final File manifest = fs.file('manifest')
|
||||
..writeAsStringSync(json.encode(<String, Object>{'/foo.js': <int>[0, source.lengthSync()]}));
|
||||
webAssetServer.write(source, manifest);
|
||||
|
||||
when(request.uri).thenReturn(Uri.parse('http://foobar/bar.js'));
|
||||
requestController.add(request);
|
||||
await closeCompleter.future;
|
||||
|
||||
verify(response.statusCode = 404).called(1);
|
||||
}));
|
||||
|
||||
test('serves Dart files from in filesystem on Windows', () => testbed.run(() async {
|
||||
final File source = fs.file('foo.dart').absolute
|
||||
..createSync(recursive: true)
|
||||
..writeAsStringSync('void main() {}');
|
||||
|
||||
when(request.uri).thenReturn(Uri.parse('http://foobar/C:/foo.dart'));
|
||||
requestController.add(request);
|
||||
await closeCompleter.future;
|
||||
|
||||
verify(headers.add('Content-Length', source.lengthSync())).called(1);
|
||||
verify(response.addStream(any)).called(1);
|
||||
}, overrides: <Type, Generator>{
|
||||
Platform: () => windows,
|
||||
}));
|
||||
|
||||
test('serves Dart files from in filesystem on Linux/macOS', () => testbed.run(() async {
|
||||
final File source = fs.file('foo.dart').absolute
|
||||
..createSync(recursive: true)
|
||||
..writeAsStringSync('void main() {}');
|
||||
|
||||
when(request.uri).thenReturn(Uri.parse('http://foobar/foo.dart'));
|
||||
requestController.add(request);
|
||||
await closeCompleter.future;
|
||||
|
||||
verify(headers.add('Content-Length', source.lengthSync())).called(1);
|
||||
verify(response.addStream(any)).called(1);
|
||||
}, overrides: <Type, Generator>{
|
||||
Platform: () => linux,
|
||||
}));
|
||||
|
||||
test('Handles missing Dart files from filesystem', () => testbed.run(() async {
|
||||
when(request.uri).thenReturn(Uri.parse('http://foobar/foo.dart'));
|
||||
requestController.add(request);
|
||||
await closeCompleter.future;
|
||||
|
||||
verify(response.statusCode = 404).called(1);
|
||||
}));
|
||||
|
||||
test('serves asset files from in filesystem with known mime type', () => testbed.run(() async {
|
||||
final File source = fs.file(fs.path.join('build', 'flutter_assets', 'foo.png'))
|
||||
..createSync(recursive: true)
|
||||
..writeAsBytesSync(kTransparentImage);
|
||||
|
||||
when(request.uri).thenReturn(Uri.parse('http://foobar/assets/foo.png'));
|
||||
requestController.add(request);
|
||||
await closeCompleter.future;
|
||||
|
||||
verify(headers.add('Content-Length', source.lengthSync())).called(1);
|
||||
verify(headers.add('Content-Type', 'image/png')).called(1);
|
||||
verify(response.addStream(any)).called(1);
|
||||
}));
|
||||
|
||||
test('serves asset files from in filesystem with known mime type on Windows', () => testbed.run(() async {
|
||||
final File source = fs.file(fs.path.join('build', 'flutter_assets', 'foo.png'))
|
||||
..createSync(recursive: true)
|
||||
..writeAsBytesSync(kTransparentImage);
|
||||
|
||||
when(request.uri).thenReturn(Uri.parse('http://foobar/assets/foo.png'));
|
||||
requestController.add(request);
|
||||
await closeCompleter.future;
|
||||
|
||||
verify(headers.add('Content-Length', source.lengthSync())).called(1);
|
||||
verify(headers.add('Content-Type', 'image/png')).called(1);
|
||||
verify(response.addStream(any)).called(1);
|
||||
}, overrides: <Type, Generator>{
|
||||
Platform: () => windows,
|
||||
}));
|
||||
|
||||
|
||||
test('serves asset files files from in filesystem with unknown mime type and length > 12', () => testbed.run(() async {
|
||||
final File source = fs.file(fs.path.join('build', 'flutter_assets', 'foo'))
|
||||
..createSync(recursive: true)
|
||||
..writeAsBytesSync(List<int>.filled(100, 0));
|
||||
|
||||
when(request.uri).thenReturn(Uri.parse('http://foobar/assets/foo'));
|
||||
requestController.add(request);
|
||||
await closeCompleter.future;
|
||||
|
||||
verify(headers.add('Content-Length', source.lengthSync())).called(1);
|
||||
verify(headers.add('Content-Type', 'application/octet-stream')).called(1);
|
||||
verify(response.addStream(any)).called(1);
|
||||
}));
|
||||
|
||||
test('serves asset files files from in filesystem with unknown mime type and length < 12', () => testbed.run(() async {
|
||||
final File source = fs.file(fs.path.join('build', 'flutter_assets', 'foo'))
|
||||
..createSync(recursive: true)
|
||||
..writeAsBytesSync(<int>[1, 2, 3]);
|
||||
|
||||
when(request.uri).thenReturn(Uri.parse('http://foobar/assets/foo'));
|
||||
requestController.add(request);
|
||||
await closeCompleter.future;
|
||||
|
||||
verify(headers.add('Content-Length', source.lengthSync())).called(1);
|
||||
verify(headers.add('Content-Type', 'application/octet-stream')).called(1);
|
||||
verify(response.addStream(any)).called(1);
|
||||
}));
|
||||
|
||||
test('handles serving missing asset file', () => testbed.run(() async {
|
||||
when(request.uri).thenReturn(Uri.parse('http://foobar/assets/foo'));
|
||||
requestController.add(request);
|
||||
await closeCompleter.future;
|
||||
|
||||
verify(response.statusCode = HttpStatus.notFound).called(1);
|
||||
}));
|
||||
|
||||
test('calling dispose closes the http server', () => testbed.run(() async {
|
||||
await webAssetServer.dispose();
|
||||
|
||||
verify(mockHttpServer.close()).called(1);
|
||||
}));
|
||||
}
|
||||
|
||||
class MockHttpServer extends Mock implements HttpServer {}
|
||||
class MockHttpRequest extends Mock implements HttpRequest {}
|
||||
class MockHttpResponse extends Mock implements HttpResponse {}
|
||||
class MockHttpHeaders extends Mock implements HttpHeaders {}
|
||||
class MockPlatform extends Mock implements Platform {}
|
||||
Loading…
x
Reference in New Issue
Block a user