Unverified Commit 03a59bff authored by Jacob MacDonald's avatar Jacob MacDonald Committed by GitHub

Serve packages uris in flutter_tools dev web server (#48743)

* support mapping /packages/<package>/<path> requests to package:<package>/<path> uris in the web device file server
parent 03496606
...@@ -6,6 +6,8 @@ import 'dart:typed_data'; ...@@ -6,6 +6,8 @@ import 'dart:typed_data';
import 'package:meta/meta.dart'; import 'package:meta/meta.dart';
import 'package:mime/mime.dart' as mime; import 'package:mime/mime.dart' as mime;
import 'package:package_config/discovery.dart';
import 'package:package_config/packages.dart';
import '../artifacts.dart'; import '../artifacts.dart';
import '../asset.dart'; import '../asset.dart';
...@@ -26,7 +28,8 @@ import 'bootstrap.dart'; ...@@ -26,7 +28,8 @@ import 'bootstrap.dart';
/// This is only used in development mode. /// This is only used in development mode.
class WebAssetServer { class WebAssetServer {
@visibleForTesting @visibleForTesting
WebAssetServer(this._httpServer, { @required void Function(dynamic, StackTrace) onError }) { WebAssetServer(this._httpServer, this._packages,
{@required void Function(dynamic, StackTrace) onError}) {
_httpServer.listen((HttpRequest request) { _httpServer.listen((HttpRequest request) {
_handleRequest(request).catchError(onError); _handleRequest(request).catchError(onError);
// TODO(jonahwilliams): test the onError callback when https://github.com/dart-lang/sdk/issues/39094 is fixed. // TODO(jonahwilliams): test the onError callback when https://github.com/dart-lang/sdk/issues/39094 is fixed.
...@@ -44,9 +47,13 @@ class WebAssetServer { ...@@ -44,9 +47,13 @@ class WebAssetServer {
static Future<WebAssetServer> start(String hostname, int port) async { static Future<WebAssetServer> start(String hostname, int port) async {
try { try {
final HttpServer httpServer = await HttpServer.bind(hostname, port); final HttpServer httpServer = await HttpServer.bind(hostname, port);
return WebAssetServer(httpServer, onError: (dynamic error, StackTrace stackTrace) { final Packages packages =
await loadPackagesFile(Uri.base.resolve('.packages'));
return WebAssetServer(httpServer, packages,
onError: (dynamic error, StackTrace stackTrace) {
httpServer.close(force: true); httpServer.close(force: true);
throwToolExit('Unhandled exception in web development server:\n$error\n$stackTrace'); throwToolExit(
'Unhandled exception in web development server:\n$error\n$stackTrace');
}); });
} on SocketException catch (err) { } on SocketException catch (err) {
throwToolExit('Failed to bind web development server:\n$err'); throwToolExit('Failed to bind web development server:\n$err');
...@@ -63,14 +70,16 @@ class WebAssetServer { ...@@ -63,14 +70,16 @@ class WebAssetServer {
final RegExp _drivePath = RegExp(r'\/[A-Z]:\/'); final RegExp _drivePath = RegExp(r'\/[A-Z]:\/');
final Packages _packages;
// handle requests for JavaScript source, dart sources maps, or asset files. // handle requests for JavaScript source, dart sources maps, or asset files.
Future<void> _handleRequest(HttpRequest request) async { Future<void> _handleRequest(HttpRequest request) async {
final HttpResponse response = request.response; final HttpResponse response = request.response;
// If the response is `/`, then we are requesting the index file. // If the response is `/`, then we are requesting the index file.
if (request.uri.path == '/') { if (request.uri.path == '/') {
final File indexFile = globals.fs.currentDirectory final File indexFile = globals.fs.currentDirectory
.childDirectory('web') .childDirectory('web')
.childFile('index.html'); .childFile('index.html');
if (indexFile.existsSync()) { if (indexFile.existsSync()) {
response.headers.add('Content-Type', 'text/html'); response.headers.add('Content-Type', 'text/html');
response.headers.add('Content-Length', indexFile.lengthSync()); response.headers.add('Content-Length', indexFile.lengthSync());
...@@ -117,23 +126,37 @@ class WebAssetServer { ...@@ -117,23 +126,37 @@ class WebAssetServer {
// doesn't currently consider the case of Dart files as assets. // doesn't currently consider the case of Dart files as assets.
File file = globals.fs.file(Uri.base.resolve(request.uri.path)); File file = globals.fs.file(Uri.base.resolve(request.uri.path));
// If both of the lookups above failed, the file might have been an asset. // If both of the lookups above failed, the file might have been a package
// file which is signaled by a `/packages/<package>/<path>` request.
if (!file.existsSync() && request.uri.pathSegments.first == 'packages') {
file = globals.fs.file(_packages.resolve(Uri(
scheme: 'package', pathSegments: request.uri.pathSegments.skip(1))));
}
// 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. // Try and resolve the path relative to the built asset directory.
if (!file.existsSync()) { if (!file.existsSync()) {
final String assetPath = request.uri.path.replaceFirst('/assets/', ''); final String assetPath = request.uri.path.replaceFirst('/assets/', '');
file = globals.fs.file(globals.fs.path.join(getAssetBuildDirectory(), globals.fs.path.relative(assetPath))); file = globals.fs.file(globals.fs.path
.join(getAssetBuildDirectory(), globals.fs.path.relative(assetPath)));
} }
// If it isn't a project source or an asset, it must be a dart SDK source. // If it isn't a project source or an asset, it must be a dart SDK source.
// or a flutter web SDK source. // or a flutter web SDK source.
if (!file.existsSync()) { if (!file.existsSync()) {
final Directory dartSdkParent = globals.fs.directory(globals.artifacts.getArtifactPath(Artifact.engineDartSdkPath)).parent; final Directory dartSdkParent = globals.fs
file = globals.fs.file(globals.fs.path.joinAll(<String>[dartSdkParent.path, ...request.uri.pathSegments])); .directory(
globals.artifacts.getArtifactPath(Artifact.engineDartSdkPath))
.parent;
file = globals.fs.file(globals.fs.path
.joinAll(<String>[dartSdkParent.path, ...request.uri.pathSegments]));
} }
if (!file.existsSync()) { if (!file.existsSync()) {
final String flutterWebSdk = globals.artifacts.getArtifactPath(Artifact.flutterWebSdk); final String flutterWebSdk =
file = globals.fs.file(globals.fs.path.joinAll(<String>[flutterWebSdk, ...request.uri.pathSegments])); globals.artifacts.getArtifactPath(Artifact.flutterWebSdk);
file = globals.fs.file(globals.fs.path
.joinAll(<String>[flutterWebSdk, ...request.uri.pathSegments]));
} }
if (!file.existsSync()) { if (!file.existsSync()) {
...@@ -147,7 +170,7 @@ class WebAssetServer { ...@@ -147,7 +170,7 @@ class WebAssetServer {
// cannot determine a mime type, fall back to application/octet-stream. // cannot determine a mime type, fall back to application/octet-stream.
String mimeType; String mimeType;
if (length >= 12) { if (length >= 12) {
mimeType= mime.lookupMimeType( mimeType = mime.lookupMimeType(
file.path, file.path,
headerBytes: await file.openRead(0, 12).first, headerBytes: await file.openRead(0, 12).first,
); );
...@@ -176,15 +199,19 @@ class WebAssetServer { ...@@ -176,15 +199,19 @@ class WebAssetServer {
final List<String> modules = <String>[]; final List<String> modules = <String>[];
final Uint8List codeBytes = codeFile.readAsBytesSync(); final Uint8List codeBytes = codeFile.readAsBytesSync();
final Uint8List sourcemapBytes = sourcemapFile.readAsBytesSync(); final Uint8List sourcemapBytes = sourcemapFile.readAsBytesSync();
final Map<String, dynamic> manifest = castStringKeyedMap(json.decode(manifestFile.readAsStringSync())); final Map<String, dynamic> manifest =
castStringKeyedMap(json.decode(manifestFile.readAsStringSync()));
for (final String filePath in manifest.keys) { for (final String filePath in manifest.keys) {
if (filePath == null) { if (filePath == null) {
globals.printTrace('Invalid manfiest file: $filePath'); globals.printTrace('Invalid manfiest file: $filePath');
continue; continue;
} }
final Map<String, dynamic> offsets = castStringKeyedMap(manifest[filePath]); final Map<String, dynamic> offsets =
final List<int> codeOffsets = (offsets['code'] as List<dynamic>).cast<int>(); castStringKeyedMap(manifest[filePath]);
final List<int> sourcemapOffsets = (offsets['sourcemap'] as List<dynamic>).cast<int>(); final List<int> codeOffsets =
(offsets['code'] as List<dynamic>).cast<int>();
final List<int> sourcemapOffsets =
(offsets['sourcemap'] as List<dynamic>).cast<int>();
if (codeOffsets.length != 2 || sourcemapOffsets.length != 2) { if (codeOffsets.length != 2 || sourcemapOffsets.length != 2) {
globals.printTrace('Invalid manifest byte offsets: $offsets'); globals.printTrace('Invalid manifest byte offsets: $offsets');
continue; continue;
...@@ -206,13 +233,14 @@ class WebAssetServer { ...@@ -206,13 +233,14 @@ class WebAssetServer {
final int sourcemapStart = sourcemapOffsets[0]; final int sourcemapStart = sourcemapOffsets[0];
final int sourcemapEnd = sourcemapOffsets[1]; final int sourcemapEnd = sourcemapOffsets[1];
if (sourcemapStart < 0 || sourcemapEnd > sourcemapBytes.lengthInBytes) { if (sourcemapStart < 0 || sourcemapEnd > sourcemapBytes.lengthInBytes) {
globals.printTrace('Invalid byte index: [$sourcemapStart, $sourcemapEnd]'); globals
.printTrace('Invalid byte index: [$sourcemapStart, $sourcemapEnd]');
continue; continue;
} }
final Uint8List sourcemapView = Uint8List.view( final Uint8List sourcemapView = Uint8List.view(
sourcemapBytes.buffer, sourcemapBytes.buffer,
sourcemapStart, sourcemapStart,
sourcemapEnd - sourcemapStart , sourcemapEnd - sourcemapStart,
); );
_sourcemaps['${_filePathToUriFragment(filePath)}.map'] = sourcemapView; _sourcemaps['${_filePathToUriFragment(filePath)}.map'] = sourcemapView;
...@@ -246,7 +274,7 @@ class WebDevFS implements DevFS { ...@@ -246,7 +274,7 @@ class WebDevFS implements DevFS {
@override @override
Future<Uri> create() async { Future<Uri> create() async {
_webAssetServer = await WebAssetServer.start(hostname, port); _webAssetServer = await WebAssetServer.start(hostname, port);
return Uri.base; return Uri.base;
} }
@override @override
...@@ -311,29 +339,37 @@ class WebDevFS implements DevFS { ...@@ -311,29 +339,37 @@ class WebDevFS implements DevFS {
'web', 'web',
'dart_stack_trace_mapper.js', 'dart_stack_trace_mapper.js',
)); ));
_webAssetServer.writeFile('/main.dart.js', generateBootstrapScript( _webAssetServer.writeFile(
requireUrl: _filePathToUriFragment(requireJS.path), '/main.dart.js',
mapperUrl: _filePathToUriFragment(stackTraceMapper.path), generateBootstrapScript(
entrypoint: '${_filePathToUriFragment(mainPath)}.js', requireUrl: _filePathToUriFragment(requireJS.path),
)); mapperUrl: _filePathToUriFragment(stackTraceMapper.path),
_webAssetServer.writeFile('/main_module.js', generateMainModule( entrypoint: '${_filePathToUriFragment(mainPath)}.js',
entrypoint: '${_filePathToUriFragment(mainPath)}.js', ));
)); _webAssetServer.writeFile(
'/main_module.js',
generateMainModule(
entrypoint: '${_filePathToUriFragment(mainPath)}.js',
));
_webAssetServer.writeFile('/dart_sdk.js', dartSdk.readAsStringSync()); _webAssetServer.writeFile('/dart_sdk.js', dartSdk.readAsStringSync());
_webAssetServer.writeFile('/dart_sdk.js.map', dartSdkSourcemap.readAsStringSync()); _webAssetServer.writeFile(
'/dart_sdk.js.map', dartSdkSourcemap.readAsStringSync());
// TODO(jonahwilliams): refactor the asset code in this and the regular devfs to // TODO(jonahwilliams): refactor the asset code in this and the regular devfs to
// be shared. // be shared.
await writeBundle(globals.fs.directory(getAssetBuildDirectory()), bundle.entries); await writeBundle(
globals.fs.directory(getAssetBuildDirectory()), bundle.entries);
} }
final DateTime candidateCompileTime = DateTime.now(); final DateTime candidateCompileTime = DateTime.now();
if (fullRestart) { if (fullRestart) {
generator.reset(); generator.reset();
} }
final CompilerOutput compilerOutput = await generator.recompile( final CompilerOutput compilerOutput = await generator.recompile(
mainPath, mainPath,
invalidatedFiles, invalidatedFiles,
outputPath: dillOutputPath ?? getDefaultApplicationKernelPath(trackWidgetCreation: trackWidgetCreation), outputPath: dillOutputPath ??
packagesFilePath : _packagesFilePath, getDefaultApplicationKernelPath(
trackWidgetCreation: trackWidgetCreation),
packagesFilePath: _packagesFilePath,
); );
if (compilerOutput == null || compilerOutput.errorCount > 0) { if (compilerOutput == null || compilerOutput.errorCount > 0) {
return UpdateFSReport(success: false); return UpdateFSReport(success: false);
...@@ -354,18 +390,19 @@ class WebDevFS implements DevFS { ...@@ -354,18 +390,19 @@ class WebDevFS implements DevFS {
} on FileSystemException catch (err) { } on FileSystemException catch (err) {
throwToolExit('Failed to load recompiled sources:\n$err'); throwToolExit('Failed to load recompiled sources:\n$err');
} }
return UpdateFSReport(success: true, syncedBytes: codeFile.lengthSync(), return UpdateFSReport(
invalidatedSourcesCount: invalidatedFiles.length) success: true,
..invalidatedModules = modules.map(_filePathToUriFragment).toList(); syncedBytes: codeFile.lengthSync(),
invalidatedSourcesCount: invalidatedFiles.length)
..invalidatedModules = modules.map(_filePathToUriFragment).toList();
} }
} }
String _filePathToUriFragment(String path) { String _filePathToUriFragment(String path) {
if (globals.platform.isWindows) { if (globals.platform.isWindows) {
final bool startWithSlash = path.startsWith('/'); final bool startWithSlash = path.startsWith('/');
final String partial = globals.fs.path final String partial =
.split(path) globals.fs.path.split(path).skip(startWithSlash ? 2 : 1).join('/');
.skip(startWithSlash ? 2 : 1).join('/');
if (partial.startsWith('/')) { if (partial.startsWith('/')) {
return partial; return partial;
} }
......
...@@ -11,6 +11,8 @@ import 'package:flutter_tools/src/base/io.dart'; ...@@ -11,6 +11,8 @@ import 'package:flutter_tools/src/base/io.dart';
import 'package:flutter_tools/src/convert.dart'; import 'package:flutter_tools/src/convert.dart';
import 'package:flutter_tools/src/web/devfs_web.dart'; import 'package:flutter_tools/src/web/devfs_web.dart';
import 'package:mockito/mockito.dart'; import 'package:mockito/mockito.dart';
import 'package:package_config/discovery.dart';
import 'package:package_config/packages.dart';
import 'package:platform/platform.dart'; import 'package:platform/platform.dart';
import 'package:flutter_tools/src/globals.dart' as globals; import 'package:flutter_tools/src/globals.dart' as globals;
...@@ -36,6 +38,11 @@ void main() { ...@@ -36,6 +38,11 @@ void main() {
WebAssetServer webAssetServer; WebAssetServer webAssetServer;
MockPlatform windows; MockPlatform windows;
MockPlatform linux; MockPlatform linux;
Packages packages;
setUpAll(() async {
packages = await loadPackagesFile(Uri.base.resolve('.packages'));
});
setUp(() { setUp(() {
windows = MockPlatform(); windows = MockPlatform();
...@@ -60,7 +67,8 @@ void main() { ...@@ -60,7 +67,8 @@ void main() {
when(response.close()).thenAnswer((Invocation invocation) async { when(response.close()).thenAnswer((Invocation invocation) async {
closeCompleter.complete(); closeCompleter.complete();
}); });
webAssetServer = WebAssetServer(mockHttpServer, onError: (dynamic error, StackTrace stackTrace) { webAssetServer = WebAssetServer(
mockHttpServer, packages, onError: (dynamic error, StackTrace stackTrace) {
closeCompleter.completeError(error, stackTrace); closeCompleter.completeError(error, stackTrace);
}); });
}); });
...@@ -291,6 +299,23 @@ void main() { ...@@ -291,6 +299,23 @@ void main() {
verify(response.statusCode = HttpStatus.notFound).called(1); verify(response.statusCode = HttpStatus.notFound).called(1);
})); }));
test('serves /packages/<package>/<path> files as if they were '
'package:<package>/<path> uris', () => testbed.run(() async {
final Uri expectedUri = packages.resolve(
Uri.parse('package:flutter_tools/foo.dart'));
final File source = globals.fs.file(globals.fs.path.fromUri(expectedUri))
..createSync(recursive: true)
..writeAsBytesSync(<int>[1, 2, 3]);
when(request.uri).thenReturn(
Uri.parse('http:///packages/flutter_tools/foo.dart'));
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('calling dispose closes the http server', () => testbed.run(() async { test('calling dispose closes the http server', () => testbed.run(() async {
await webAssetServer.dispose(); await webAssetServer.dispose();
......
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