Unverified Commit 89ebd700 authored by Jonah Williams's avatar Jonah Williams Committed by GitHub

Support deferred imports on profile/release builds of Flutter Web (#41222)

parent 4f3199f8
...@@ -11,6 +11,7 @@ import 'dart:isolate'; ...@@ -11,6 +11,7 @@ import 'dart:isolate';
import 'package:analyzer/dart/analysis/results.dart'; import 'package:analyzer/dart/analysis/results.dart';
import 'package:analyzer/dart/analysis/utilities.dart'; import 'package:analyzer/dart/analysis/utilities.dart';
import 'package:analyzer/dart/ast/ast.dart'; import 'package:analyzer/dart/ast/ast.dart';
import 'package:archive/archive.dart';
import 'package:build/build.dart'; import 'package:build/build.dart';
import 'package:build_config/build_config.dart'; import 'package:build_config/build_config.dart';
import 'package:build_modules/build_modules.dart'; import 'package:build_modules/build_modules.dart';
...@@ -26,6 +27,7 @@ import 'package:build_web_compilers/build_web_compilers.dart'; ...@@ -26,6 +27,7 @@ import 'package:build_web_compilers/build_web_compilers.dart';
import 'package:build_web_compilers/builders.dart'; import 'package:build_web_compilers/builders.dart';
import 'package:build_web_compilers/src/dev_compiler_bootstrap.dart'; import 'package:build_web_compilers/src/dev_compiler_bootstrap.dart';
import 'package:crypto/crypto.dart'; import 'package:crypto/crypto.dart';
import 'package:glob/glob.dart';
import 'package:path/path.dart' as path; // ignore: package_path_import import 'package:path/path.dart' as path; // ignore: package_path_import
import 'package:scratch_space/scratch_space.dart'; import 'package:scratch_space/scratch_space.dart';
import 'package:test_core/backend.dart'; import 'package:test_core/backend.dart';
...@@ -461,8 +463,32 @@ Future<void> bootstrapDart2Js(BuildStep buildStep, String flutterWebSdk, bool pr ...@@ -461,8 +463,32 @@ Future<void> bootstrapDart2Js(BuildStep buildStep, String flutterWebSdk, bool pr
final AssetId jsOutputId = dartEntrypointId.changeExtension(jsEntrypointExtension); final AssetId jsOutputId = dartEntrypointId.changeExtension(jsEntrypointExtension);
final File jsOutputFile = scratchSpace.fileFor(jsOutputId); final File jsOutputFile = scratchSpace.fileFor(jsOutputId);
if (result.succeeded && jsOutputFile.existsSync()) { if (result.succeeded && jsOutputFile.existsSync()) {
log.info(result.output); final String rootDir = path.dirname(jsOutputFile.path);
// Explicitly write out the original js file and sourcemap. final String dartFile = path.basename(dartEntrypointId.path);
final Glob fileGlob = Glob('$dartFile.js*');
final Archive archive = Archive();
await for (FileSystemEntity jsFile in fileGlob.list(root: rootDir)) {
if (jsFile.path.endsWith(jsEntrypointExtension) ||
jsFile.path.endsWith(jsEntrypointSourceMapExtension)) {
// These are explicitly output, and are not part of the archive.
continue;
}
if (jsFile is File) {
final String fileName = path.relative(jsFile.path, from: rootDir);
final FileStat fileStats = jsFile.statSync();
archive.addFile(
ArchiveFile(fileName, fileStats.size, await jsFile.readAsBytes())
..mode = fileStats.mode
..lastModTime = fileStats.modified.millisecondsSinceEpoch);
}
}
if (archive.isNotEmpty) {
final AssetId archiveId = dartEntrypointId.changeExtension(jsEntrypointArchiveExtension);
await buildStep.writeAsBytes(archiveId, TarEncoder().encode(archive));
}
// Explicitly write out the original js file and sourcemap - we can't output
// these as part of the archive because they already have asset nodes.
await scratchSpace.copyOutput(jsOutputId, buildStep); await scratchSpace.copyOutput(jsOutputId, buildStep);
final AssetId jsSourceMapId = final AssetId jsSourceMapId =
dartEntrypointId.changeExtension(jsEntrypointSourceMapExtension); dartEntrypointId.changeExtension(jsEntrypointSourceMapExtension);
......
...@@ -4,10 +4,9 @@ ...@@ -4,10 +4,9 @@
import 'dart:async'; import 'dart:async';
import 'package:archive/archive.dart';
import 'package:build_daemon/client.dart'; import 'package:build_daemon/client.dart';
import 'package:build_daemon/constants.dart'; import 'package:build_daemon/constants.dart' as daemon;
import 'package:build_daemon/constants.dart' hide BuildMode;
import 'package:build_daemon/constants.dart' as daemon show BuildMode;
import 'package:build_daemon/data/build_status.dart'; import 'package:build_daemon/data/build_status.dart';
import 'package:build_daemon/data/build_target.dart'; import 'package:build_daemon/data/build_target.dart';
import 'package:build_daemon/data/server_log.dart'; import 'package:build_daemon/data/server_log.dart';
...@@ -89,6 +88,7 @@ class WebFs { ...@@ -89,6 +88,7 @@ class WebFs {
this._server, this._server,
this._dwds, this._dwds,
this.uri, this.uri,
this._assetServer,
); );
/// The server uri. /// The server uri.
...@@ -97,6 +97,7 @@ class WebFs { ...@@ -97,6 +97,7 @@ class WebFs {
final HttpServer _server; final HttpServer _server;
final Dwds _dwds; final Dwds _dwds;
final BuildDaemonClient _client; final BuildDaemonClient _client;
final AssetServer _assetServer;
StreamSubscription<void> _connectedApps; StreamSubscription<void> _connectedApps;
static const String _kHostName = 'localhost'; static const String _kHostName = 'localhost';
...@@ -106,6 +107,7 @@ class WebFs { ...@@ -106,6 +107,7 @@ class WebFs {
await _dwds?.stop(); await _dwds?.stop();
await _server.close(force: true); await _server.close(force: true);
await _connectedApps?.cancel(); await _connectedApps?.cancel();
_assetServer?.dispose();
} }
Future<DebugConnection> _cachedExtensionFuture; Future<DebugConnection> _cachedExtensionFuture;
...@@ -180,9 +182,6 @@ class WebFs { ...@@ -180,9 +182,6 @@ class WebFs {
await assetBundle.build(); await assetBundle.build();
await writeBundle(fs.directory(getAssetBuildDirectory()), assetBundle.entries); await writeBundle(fs.directory(getAssetBuildDirectory()), assetBundle.entries);
// Initialize the dwds server.
final int hostPort = port == null ? await os.findFreePort() : int.tryParse(port);
// Map the bootstrap files to the correct package directory.
final String targetBaseName = fs.path final String targetBaseName = fs.path
.withoutExtension(target).replaceFirst('lib${fs.path.separator}', ''); .withoutExtension(target).replaceFirst('lib${fs.path.separator}', '');
final Map<String, String> mappedUrls = <String, String>{ final Map<String, String> mappedUrls = <String, String>{
...@@ -195,6 +194,10 @@ class WebFs { ...@@ -195,6 +194,10 @@ class WebFs {
'${targetBaseName}_web_entrypoint.digests': 'packages/${flutterProject.manifest.appName}/' '${targetBaseName}_web_entrypoint.digests': 'packages/${flutterProject.manifest.appName}/'
'${targetBaseName}_web_entrypoint.digests', '${targetBaseName}_web_entrypoint.digests',
}; };
// Initialize the dwds server.
final int hostPort = port == null ? await os.findFreePort() : int.tryParse(port);
final Pipeline pipeline = const Pipeline().addMiddleware((Handler innerHandler) { final Pipeline pipeline = const Pipeline().addMiddleware((Handler innerHandler) {
return (Request request) async { return (Request request) async {
// Redirect the main.dart.js to the target file we decided to serve. // Redirect the main.dart.js to the target file we decided to serve.
...@@ -237,9 +240,10 @@ class WebFs { ...@@ -237,9 +240,10 @@ class WebFs {
} else { } else {
handler = pipeline.addHandler(proxyHandler('http://localhost:$daemonAssetPort/web/')); handler = pipeline.addHandler(proxyHandler('http://localhost:$daemonAssetPort/web/'));
} }
final AssetServer assetServer = AssetServer(flutterProject, targetBaseName);
Cascade cascade = Cascade(); Cascade cascade = Cascade();
cascade = cascade.add(handler); cascade = cascade.add(handler);
cascade = cascade.add(_assetHandler(flutterProject)); cascade = cascade.add(assetServer.handle);
final HttpServer server = await httpMultiServerFactory(hostname ?? _kHostName, hostPort); final HttpServer server = await httpMultiServerFactory(hostname ?? _kHostName, hostPort);
shelf_io.serveRequests(server, cascade.handler); shelf_io.serveRequests(server, cascade.handler);
return WebFs( return WebFs(
...@@ -247,12 +251,20 @@ class WebFs { ...@@ -247,12 +251,20 @@ class WebFs {
server, server,
dwds, dwds,
'http://$_kHostName:$hostPort/', 'http://$_kHostName:$hostPort/',
assetServer,
); );
} }
}
class AssetServer {
AssetServer(this.flutterProject, this.targetBaseName);
static Future<Response> Function(Request request) _assetHandler(FlutterProject flutterProject) { final FlutterProject flutterProject;
final String targetBaseName;
final PackageMap packageMap = PackageMap(PackageMap.globalPackagesPath); final PackageMap packageMap = PackageMap(PackageMap.globalPackagesPath);
return (Request request) async { Directory partFiles;
Future<Response> handle(Request request) async {
if (request.url.path.contains('stack_trace_mapper')) { if (request.url.path.contains('stack_trace_mapper')) {
final File file = fs.file(fs.path.join( final File file = fs.file(fs.path.join(
artifacts.getArtifactPath(Artifact.engineDartSdkPath), artifacts.getArtifactPath(Artifact.engineDartSdkPath),
...@@ -264,6 +276,33 @@ class WebFs { ...@@ -264,6 +276,33 @@ class WebFs {
return Response.ok(file.readAsBytesSync(), headers: <String, String>{ return Response.ok(file.readAsBytesSync(), headers: <String, String>{
'Content-Type': 'text/javascript', 'Content-Type': 'text/javascript',
}); });
} else if (request.url.path.endsWith('part.js')) {
// Lazily unpack any deferred imports in release/profile mode. These are
// placed into an archive by build_runner, and are named based on the main
// entrypoint + a "part" suffix (Though the actual names are arbitrary).
// To make this easier to deal with they are copied into a temp directory.
if (partFiles == null) {
final File dart2jsArchive = fs.file(fs.path.join(
flutterProject.dartTool.path,
'build',
'flutter_web',
'${flutterProject.manifest.appName}',
'lib',
'${targetBaseName}_web_entrypoint.dart.js.tar.gz',
));
if (dart2jsArchive.existsSync()) {
final Archive archive = TarDecoder().decodeBytes(dart2jsArchive.readAsBytesSync());
partFiles = fs.systemTempDirectory.createTempSync('_flutter_tool')
..createSync();
for (ArchiveFile file in archive) {
partFiles.childFile(file.name).writeAsBytesSync(file.content);
}
}
}
final String fileName = fs.path.basename(request.url.path);
return Response.ok(partFiles.childFile(fileName).readAsBytesSync(), headers: <String, String>{
'Content-Type': 'text/javascript',
});
} else if (request.url.path.contains('require.js')) { } else if (request.url.path.contains('require.js')) {
final File file = fs.file(fs.path.join( final File file = fs.file(fs.path.join(
artifacts.getArtifactPath(Artifact.engineDartSdkPath), artifacts.getArtifactPath(Artifact.engineDartSdkPath),
...@@ -338,7 +377,10 @@ class WebFs { ...@@ -338,7 +377,10 @@ class WebFs {
} }
} }
return Response.notFound(''); return Response.notFound('');
}; }
void dispose() {
partFiles?.deleteSync(recursive: true);
} }
} }
...@@ -459,7 +501,7 @@ class BuildDaemonCreator { ...@@ -459,7 +501,7 @@ class BuildDaemonCreator {
/// Retrieve the asset server port for the current daemon. /// Retrieve the asset server port for the current daemon.
int assetServerPort(Directory workingDirectory) { int assetServerPort(Directory workingDirectory) {
final String portFilePath = fs.path.join(daemonWorkspace(workingDirectory.path), '.asset_server_port'); final String portFilePath = fs.path.join(daemon.daemonWorkspace(workingDirectory.path), '.asset_server_port');
return int.tryParse(fs.file(portFilePath).readAsStringSync()); return int.tryParse(fs.file(portFilePath).readAsStringSync());
} }
} }
...@@ -2,6 +2,7 @@ ...@@ -2,6 +2,7 @@
// Use of this source code is governed by a BSD-style license that can be // Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file. // found in the LICENSE file.
import 'package:archive/archive.dart';
import 'package:meta/meta.dart'; import 'package:meta/meta.dart';
import '../asset.dart'; import '../asset.dart';
...@@ -48,9 +49,23 @@ Future<void> buildWeb(FlutterProject flutterProject, String target, BuildInfo bu ...@@ -48,9 +49,23 @@ Future<void> buildWeb(FlutterProject flutterProject, String target, BuildInfo bu
flutterProject.manifest.appName, flutterProject.manifest.appName,
'${fs.path.withoutExtension(target)}_web_entrypoint.dart.js', '${fs.path.withoutExtension(target)}_web_entrypoint.dart.js',
); );
// Check for deferred import outputs.
final File dart2jsArchive = fs.file(fs.path.join(
flutterProject.dartTool.path,
'build',
'flutter_web',
'${flutterProject.manifest.appName}',
'${fs.path.withoutExtension(target)}_web_entrypoint.dart.js.tar.gz'),
);
fs.file(outputPath).copySync(fs.path.join(outputDir.path, 'main.dart.js')); fs.file(outputPath).copySync(fs.path.join(outputDir.path, 'main.dart.js'));
fs.file('$outputPath.map').copySync(fs.path.join(outputDir.path, 'main.dart.js.map')); fs.file('$outputPath.map').copySync(fs.path.join(outputDir.path, 'main.dart.js.map'));
flutterProject.web.indexFile.copySync(fs.path.join(outputDir.path, 'index.html')); flutterProject.web.indexFile.copySync(fs.path.join(outputDir.path, 'index.html'));
if (dart2jsArchive.existsSync()) {
final Archive archive = TarDecoder().decodeBytes(dart2jsArchive.readAsBytesSync());
for (ArchiveFile file in archive.files) {
outputDir.childFile(file.name).writeAsBytesSync(file.content);
}
}
} }
} catch (err) { } catch (err) {
printError(err.toString()); printError(err.toString());
......
...@@ -2,7 +2,11 @@ ...@@ -2,7 +2,11 @@
// Use of this source code is governed by a BSD-style license that can be // Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file. // found in the LICENSE file.
import 'dart:convert';
import 'package:archive/archive.dart';
import 'package:args/command_runner.dart'; import 'package:args/command_runner.dart';
import 'package:file_testing/file_testing.dart';
import 'package:flutter_tools/src/base/common.dart'; import 'package:flutter_tools/src/base/common.dart';
import 'package:flutter_tools/src/base/file_system.dart'; import 'package:flutter_tools/src/base/file_system.dart';
import 'package:flutter_tools/src/base/platform.dart'; import 'package:flutter_tools/src/base/platform.dart';
...@@ -25,6 +29,7 @@ void main() { ...@@ -25,6 +29,7 @@ void main() {
MockWebCompilationProxy mockWebCompilationProxy; MockWebCompilationProxy mockWebCompilationProxy;
Testbed testbed; Testbed testbed;
MockPlatform mockPlatform; MockPlatform mockPlatform;
bool addArchive = false;
setUpAll(() { setUpAll(() {
Cache.flutterRoot = ''; Cache.flutterRoot = '';
...@@ -32,6 +37,7 @@ void main() { ...@@ -32,6 +37,7 @@ void main() {
}); });
setUp(() { setUp(() {
addArchive = false;
mockWebCompilationProxy = MockWebCompilationProxy(); mockWebCompilationProxy = MockWebCompilationProxy();
testbed = Testbed(setup: () { testbed = Testbed(setup: () {
fs.file('pubspec.yaml') fs.file('pubspec.yaml')
...@@ -46,9 +52,18 @@ void main() { ...@@ -46,9 +52,18 @@ void main() {
mode: anyNamed('mode'), mode: anyNamed('mode'),
initializePlatform: anyNamed('initializePlatform'), initializePlatform: anyNamed('initializePlatform'),
)).thenAnswer((Invocation invocation) { )).thenAnswer((Invocation invocation) {
final String path = fs.path.join('.dart_tool', 'build', 'flutter_web', 'foo', 'lib', 'main_web_entrypoint.dart.js'); final String prefix = fs.path.join('.dart_tool', 'build', 'flutter_web', 'foo', 'lib');
final String path = fs.path.join(prefix, 'main_web_entrypoint.dart.js');
fs.file(path).createSync(recursive: true); fs.file(path).createSync(recursive: true);
fs.file('$path.map').createSync(); fs.file('$path.map').createSync();
if (addArchive) {
final List<int> bytes = utf8.encode('void main() {}');
final TarEncoder encoder = TarEncoder();
final Archive archive = Archive()
..addFile(ArchiveFile.noCompress('main_web_entrypoint.1.dart.js', bytes.length, bytes));
fs.file(fs.path.join(prefix, 'main_web_entrypoint.dart.js.tar.gz'))
..writeAsBytes(encoder.encode(archive));
}
return Future<bool>.value(true); return Future<bool>.value(true);
}); });
}, overrides: <Type, Generator>{ }, overrides: <Type, Generator>{
...@@ -59,6 +74,19 @@ void main() { ...@@ -59,6 +74,19 @@ void main() {
}); });
}); });
test('Copies generated part files out of build directory', () => testbed.run(() async {
addArchive = true;
await buildWeb(
FlutterProject.current(),
fs.path.join('lib', 'main.dart'),
BuildInfo.release,
false,
);
expect(fs.file(fs.path.join('build', 'web', 'main_web_entrypoint.1.dart.js')), exists);
expect(fs.file(fs.path.join('build', 'web', 'main.dart.js')), exists);
}));
test('Refuses to build for web when missing index.html', () => testbed.run(() async { test('Refuses to build for web when missing index.html', () => testbed.run(() async {
fs.file(fs.path.join('web', 'index.html')).deleteSync(); fs.file(fs.path.join('web', 'index.html')).deleteSync();
...@@ -82,16 +110,6 @@ void main() { ...@@ -82,16 +110,6 @@ void main() {
expect(await runner.run(), 1); expect(await runner.run(), 1);
})); }));
test('Can build for web', () => testbed.run(() async {
await buildWeb(
FlutterProject.current(),
fs.path.join('lib', 'main.dart'),
BuildInfo.debug,
false,
);
}));
test('Refuses to build a debug build for web', () => testbed.run(() async { test('Refuses to build a debug build for web', () => testbed.run(() async {
final CommandRunner<void> runner = createTestCommandRunner(BuildCommand()); final CommandRunner<void> runner = createTestCommandRunner(BuildCommand());
......
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